#ifdef lint
static char *sccsid = "@(#)shading.c	1.3 (Steve Hill) 5/24/90";
#endif

/* shading.c
 *
 * Routines to implement the shading model.
 *
 * The colour of a surface is composed of the following components:
 *
 * 1) Diffuse reflection (Lambert's law)
 * 2) Specular reflection (Phong shading)
 * 3) Ambient lighting
 * 4) Reflections
 * 5) Transmitted light
 */

#include <stdio.h>
#include <math.h>

#include "basetype.h"
#include "cartesian.h"
#include "vector.h"
#include "matrix.h"
#include "point.h"
#include "indexlist.h"
#include "ray.h"
#include "quadric.h"
#include "colour.h"
#include "surface.h"
#include "bitmap.h"
#include "pattern.h"
#include "solid.h"
#include "bbox.h"
#include "list.h"
#include "hitlist.h"
#include "light.h"
#include "hitdata.h"
#include "world.h"
#include "intersect.h"
#include "shading.h"


/* Diffuse
 *
 * Adds diffuse component into colour according to Lambert's cosine law.
 */

bool_t
Diffuse(colour, hit_data, to_light, light_colour)
colour_t	*colour;
hit_data_t	*hit_data;
vector_t	*to_light;
colour_t	*light_colour;
{
	surface_t	*surface;
	real_t		diffuse;

	surface = hit_data->solid->body.atom.surface;
	diffuse = VectorDot(to_light, hit_data->normal);

	if (diffuse > REAL_ZERO)
	{
		colour->r += diffuse * surface->diffuse->r * light_colour->r;
		colour->g += diffuse * surface->diffuse->g * light_colour->g;
		colour->b += diffuse * surface->diffuse->b * light_colour->b;
		return(TRUE);
	}
	return(FALSE);
}


/* Specular
 *
 * Adds specular component into colour according to the Phong
 * (power of cosine) model.
 */

void
Specular(colour, ray, hit_data, to_light, light_colour)
colour_t	*colour;
ray_t		*ray;
hit_data_t	*hit_data;
vector_t	*to_light;
colour_t	*light_colour;
{
	surface_t	*surface;
	vector_t	bisector, to_observer;
	real_t		specular;

	surface = hit_data->solid->body.atom.surface;

	if (surface->specular == REAL_ZERO)
		return;

	VectorNeg(&to_observer, ray->vector);

	VectorAdd(&bisector, to_light, &to_observer);
	VectorNormalise(&bisector, &bisector);

	specular = VectorDot(&bisector, hit_data->normal);
	if (specular > REAL_ZERO)
	{
		specular = (real_t) pow((double) specular,
					(double) surface->power);

		colour->r += specular * surface->specular * light_colour->r;
		colour->g += specular * surface->specular * light_colour->g;
		colour->b += specular * surface->specular * light_colour->b;
	}
}


/* Ambient
 *
 * Adds an ambient light component into colour.
 */

void
Ambient(colour, world, hit_data)
colour_t	*colour;
world_t		*world;
hit_data_t	*hit_data;
{
	colour_t	*ambient;

	ambient = hit_data->solid->body.atom.surface->ambient;

	colour->r += world->ambient->r * ambient->r;
	colour->g += world->ambient->g * ambient->g;
	colour->b += world->ambient->b * ambient->b;
}


/* Reflect
 * 
 * Reflect a ray from a surface.
 */

void
Reflect(colour, ray, world, hit_data, multiplier)
colour_t	*colour;
ray_t		*ray;
world_t		*world;
hit_data_t	*hit_data;
real_t		multiplier;
{
	colour_t	reflection;
	surface_t	*surface;
	vector_t	reflected;
	real_t		cos_angle;
	ray_t		new_ray;

	surface = hit_data->solid->body.atom.surface;

	if (surface->mirror->r == REAL_ZERO &&
	    surface->mirror->g == REAL_ZERO &&
	    surface->mirror->b == REAL_ZERO)
		return;

	cos_angle = VectorDot(ray->vector, hit_data->normal);

	if (cos_angle > REAL_ZERO)
		return;

	VectorScale(&reflected, -REAL_TWO * cos_angle, hit_data->normal);
	VectorAdd(&reflected, &reflected, ray->vector);
	VectorNormalise(&reflected, &reflected);

	RayListCopy(&new_ray, hit_data->point, &reflected, ray->list);

	multiplier *= RealMax(surface->mirror->r,
			      RealMax(surface->mirror->g,
				      surface->mirror->b));

	if (multiplier > MINIMUM_MULTIPLIER)
	{
		reflection.r = reflection.g = reflection.b = REAL_ZERO;

		Trace(&reflection, &new_ray, world, multiplier);

		colour->r += reflection.r * surface->mirror->r;
		colour->g += reflection.g * surface->mirror->g;
		colour->b += reflection.b * surface->mirror->b;
	}
	RayListFree(&new_ray);
}


/* Transmit
 *
 * Deals with transmitted light.
 */

void
Transmit(colour, ray, world, hit_data, multiplier)
colour_t	*colour;
ray_t		*ray;
world_t		*world;
hit_data_t	*hit_data;
real_t		multiplier;
{
	surface_t	*surface;
	colour_t	transmission;
	vector_t	new_vector;
	bool_t		going_in;
	real_t		index;
	ray_t		new_ray;

	surface = hit_data->solid->body.atom.surface;

	if (surface->transmit->r == REAL_ZERO &&
	    surface->transmit->g == REAL_ZERO &&
	    surface->transmit->b == REAL_ZERO)
		return;

	RayListCopy(&new_ray, hit_data->point, &new_vector, ray->list);

	multiplier *= RealMax(surface->transmit->r,
			      RealMax(surface->transmit->g,
				      surface->transmit->b));

	if (multiplier < MINIMUM_MULTIPLIER)
		return;

	going_in = VectorDot(ray->vector, hit_data->normal) < REAL_ZERO;
	index = IndexListTop(ray->list);

	if (going_in)
	{
		transmission.r = transmission.g = transmission.b = REAL_ZERO;

		new_ray.list = IndexListPush(surface->index, new_ray.list);

		if (Refract(&new_vector, index, surface->index,
			    ray->vector, hit_data->normal))
			return;

		Trace(&transmission, &new_ray, world, multiplier);

		colour->r += transmission.r * surface->transmit->r;
		colour->g += transmission.g * surface->transmit->g;
		colour->b += transmission.b * surface->transmit->b;
	}
	else
	{
		new_ray.list = IndexListPop(new_ray.list);

		if (Refract(&new_vector, index, IndexListTop(new_ray.list),
			    ray->vector, hit_data->normal))
			return;

		Trace(colour, &new_ray, world, multiplier);
	}

	RayListFree(&new_ray);
}


/* Shadow
 *
 * Trace a ray back to a light source - if the ray hits
 * non-translucent objects, it stops.  Translucent objects
 * attenuate the light according to their transmitivity.
 *
 * Returns true if the point is in shadow.
 */

bool_t
Shadow(colour, light, ray, solid)
colour_t	*colour;
light_t		*light;
ray_t		*ray;
solid_t		*solid;
{
	hit_list_t	*hit_list, *hit_ptr;
	bool_t		in_shadow;
	real_t		max_t;

	in_shadow = FALSE;
	hit_list = Intersections(ray, solid);
	hit_ptr  = hit_list;
	max_t = RayDistance(ray, light->point);

	while (hit_ptr != HitListNull && hit_ptr->elem->t < max_t)
	{
		hit_data_t	hit_data;
		surface_t	*surface;

		surface = hit_ptr->elem->solid->body.atom.surface;

		if (surface->transmit->r == REAL_ZERO &&
		    surface->transmit->g == REAL_ZERO &&
		    surface->transmit->b == REAL_ZERO)
		{
			in_shadow = TRUE;
			break;
		}

		colour->r *= surface->transmit->r;
		colour->g *= surface->transmit->g;
		colour->b *= surface->transmit->b;

		hit_ptr = hit_ptr->ptr;
	}

	HitListFree(hit_list);
	return(in_shadow);
}


/* SurfaceColour
 *
 * Calculates the colour of a surface as illuminated by a number of
 * lights.
 */

void
SurfaceColour(colour, ray, world, hit_data, multiplier)
colour_t	*colour;
ray_t		*ray;
world_t		*world;
hit_data_t	*hit_data;
real_t		multiplier;
{
	light_list_t	*lights;
	vector_t	to_light;
	ray_t		light_ray;

	lights = world->lights;
	light_ray.point = hit_data->point;
	light_ray.vector = &to_light;

	if (hit_data->solid->body.atom.pattern != PatternNull)
		PatternCall(hit_data->solid, hit_data->point);

	while (lights != LightListNull)
	{
		light_t		*light;
		colour_t	light_colour;

		light = lights->elem;
		ColourLet(&light_colour, light->colour);
		MakeVector(&to_light, hit_data->point, light->point);

		if (! Shadow(&light_colour, light, &light_ray, world->solid) &&
		      Diffuse(colour, hit_data, &to_light, &light_colour))
			Specular(colour, ray, hit_data, &to_light, &light_colour);

		lights = lights->ptr;
	}
	Reflect(colour, ray, world, hit_data, multiplier);
	Transmit(colour, ray, world, hit_data, multiplier);
	Ambient(colour, world, hit_data);
	HitDataFreeParts(hit_data);
}


/* BackgroundColour
 *
 * Rays that miss everything are this colour.  Will be more sophisticated
 * is later versions.
 */

void
BackgroundColour(colour, world)
colour_t	*colour;
world_t		*world;
{
	ColourLet(colour, world->sky);
}


/* Trace
 *
 * Trace a ray into the scene described by world.
 */

void
Trace(colour, ray, world, multiplier)
colour_t	*colour;
ray_t		*ray;
world_t		*world;
real_t		multiplier;
{
	hit_data_t	hit_data;

	if (Intersect(&hit_data, ray, world->solid))
		SurfaceColour(colour, ray, world, &hit_data, multiplier);
	else
		BackgroundColour(colour, world);
}
