:- module renderer.
:- interface.
:- import_module eval, space_partition, trans, vector, tree.
:- import_module list, io, maybe, pair.

:- type render_params
	---> render_params(
		amb :: color,		% the ambient light
		lights :: array,	% array(light)
		scene :: scene,		% the scene to render
		depth :: int,
		fov :: real,		% the field of view
		wid :: int,		% the width, in pixels
		ht :: int,		% the height, in pixels
		file :: string
	).


:- pred render(render_params::in, io__state::di, io__state::uo)
	is det.

:- type unimplemented_object
	---> unimplemented_object(object).

% mea culpa - this is needed by precompute_lights.m
:- func light_unit_vector(light, position) = vector.
%-----------------------------------------------------------------------------%

:- implementation.

:- import_module transform_object, gml, op.
:- import_module precompute_lights.
:- import_module map, array, exception, require, math.
:- import_module int, float, std_util, string, prolog.

render(Params1) -->
	{ Wid = Params1 ^ wid },
	{ Ht = Params1 ^ ht },
	{ FileName = Params1 ^ file },
	%
	% XXX Note that we first create an array,
	% and then output it. It would be slightly more
	% efficient to just output the values as we go along.
	%
	tell(FileName, Res),
	( { Res = ok } ->
		output_stream(Stream),
		{ private_builtin__unsafe_type_cast(Stream, BinStream) },
		set_binary_output_stream(BinStream, _Old),
		% Write the ppm header.
		format("P6\n", []),
		format("# Merry Mercuryians )O+\n", []),
		format("%d %d\n", [i(Wid), i(Ht)]),
		format("255\n", []),


		% work in progress : pde
		% Pre-compute lighting for "cleanly illuminated" objects
		% first unfold the set of objects that might cast shadows
		
		%scene_list(Obj, ObjList),
		
		% now use this to find object/light pairs which are clear of 'em

		%pre_compute_lighting(Obj, ObjList, Params1 ^ lights, NewScene),
		%Params2 = Params1 ^ scene := NewScene,
		{ Params2 = Params1 },
	

		% Render the image
		render_pixel_loop(Params2, 0, 0, Wid, Ht),

		told
	;
		{ throw(Res) }
	).


:- pred render_pixel_loop(render_params, int, int, int, int,
		io__state, io__state).
:- mode render_pixel_loop(in, in, in, in, in, di, uo)
		is det.

render_pixel_loop(Params, I, J, Width, Height) -->
	( { J = Height } ->
		[]
	; { I = Width } ->
		render_pixel_loop(Params, 0, J + 1, Width, Height)
	;
		{ Pixel = pixel_coords(I, J) },
		render_pixel(Params, Pixel, Intensity),

		{ Intensity = point(R0, G0, B0) },
		{ R = round_to_int(R0 * 255.0) },
		{ G = round_to_int(G0 * 255.0) },
		{ B = round_to_int(B0 * 255.0) },
		write_byte(R), write_byte(G), write_byte(B),

		render_pixel_loop(Params, I + 1, J, Width, Height)
	).

%
% ALGORITHM OVERVIEW
%
% for each pixel in image
%   1. compute direction of ray through pixel
%   2. figure out where the ray first intersects with an object
%   3. compute the object surface coordinates (face, u, v) for
%      the intersection point
%   4. call the surface function to compute the object's texture at that point
%   5. compute the pixel color

:- type pixel_coords
	---> pixel_coords(
		pixel_i :: int,
		pixel_j :: int
	).

:- pred render_pixel(render_params::in, pixel_coords::in, color::out,
		io__state::di, io__state::uo) is det.

render_pixel(RenderParams, PixelCoords, Intensity) -->
	{ RayOrigin = eye_position },
	{ RayDirection = pixel_direction(RenderParams, PixelCoords) },
	fire_ray(RenderParams, no_object, RayOrigin, RayDirection, Intensity).

% Returns an object to ignore for intersections.
% Used to avoid surface acne.
:- func no_object = int.
no_object = 0.

	% Finds the intensity from a ray.
:- pred fire_ray(render_params::in, object_id::in, point::in, vector::in,
		color::out, io__state::di, io__state::uo) is det.
fire_ray(RenderParams, IgnoreId, RayOrigin, RayDirection, Intensity) -->
	( { find_intersection(RenderParams, IgnoreId, RayOrigin, RayDirection,
		HitId, IntersectionPoint, UnitSurfaceNormal,
		SurfaceCoords, SurfaceTextureFunc) }
	->
		compute_texture(SurfaceTextureFunc, SurfaceCoords,
			SurfaceProperties),
		compute_intensity(RenderParams, RayDirection, HitId,
			IntersectionPoint, UnitSurfaceNormal,
			SurfaceProperties, Intensity)
	;
		% XXX I made this up.
		{ Intensity = point(0.0, 0.0, 0.0) }
	).

%-----------------------------------------------------------------------------%
%
% step 2: find the intersection point
% step 3: compute the surface coordinates
% (these two are now combined)
%
% figure out where the ray first intersects with an object;
% then compute the object surface coordinates (face, u, v) for
% the intersection point
%

:- interface.

% XXX clean up the module hierarchy.

	% The real is the square of the distance from the ray
	% origin to the intersection point.
:- type intersection_result == tree(pair(real, intersection)).

	% Combine two results into a tree, avoiding allocating
	% memory where possible.
:- func make_tree(intersection_result, intersection_result)
		= intersection_result.

:- pred find_object_intersection(object::in, maybe(transformation)::in,
		point::in, vector::in, intersection_result::out) is det.

:- pred find_object_list_intersection(list(object)::in, point::in, vector::in,
		intersection_result::in, intersection_result::out) is det.

:- func maybe_transformation_to_trans(maybe(transformation)) = trans.

:- implementation.

	% find_intersection(Params, IgnoreId, ViewPoint, ViewVector,
	%	HitId, IntersectionPoint, IntersectionNormalVector,
	%	PixelCoords, SurfaceFunc).
	%
:- pred find_intersection(render_params::in, object_id::in, point::in,
		vector::in, object_id::out, point::out, vector::out,
		surface_coordinates::out, surface::out) is semidet.

find_intersection(RenderParams, IgnoreId, RayOrigin, RayDirection, Id,
		IntersectionPoint, UnitSurfaceNormal, SurfaceCoords, Surface) :-
	find_scene_intersection(RenderParams ^ scene, 
		RayOrigin, RayDirection, Intersections),
	choose_closest_intersection(RayOrigin, RayDirection, Intersections,
			IgnoreId, MaybeIntersection),
	MaybeIntersection = yes(_ - Intersection),
%	sanity_check_intersection(RayOrigin, RayDirection, Intersection0,
%			Intersection),
	Intersection = intersection(Id, IntersectionPoint, UnitSurfaceNormal,
				SurfaceCoords, Surface).

% :- pred sanity_check_intersection(position::in, vector::in, intersection::in,
% 		intersection::out) is det.
% 
% sanity_check_intersection(Position, Direction, Intersection0, Intersection) :-
% 	Int = Intersection0^intersection_point,
% 	(
% 		dot(Direction, Int - Position) > 0.0
% 	->
% 		Intersection = Intersection0
% 	;
% 		throw(intersection_is_behind_vantage_point(Position,
% 				Direction, Intersection0))
% 	).
% 
% :- type my_excp ---> intersection_is_behind_vantage_point(
% 				position, vector, intersection).
% 			

:- pred find_scene_intersection(scene::in, point::in, vector::in,
		intersection_result::out) is det.

find_scene_intersection(scene(Partition, OtherObjects),
		RayOrigin, RayDirection, IntersectionResult) :-
	traverse_space_tree(Partition, RayOrigin, RayDirection,
		IntersectionResult1),
	find_object_list_intersection(OtherObjects, RayOrigin, RayDirection,
		empty, IntersectionResult2),
	IntersectionResult =
		make_tree(IntersectionResult1, IntersectionResult2).

find_object_list_intersection([], _, _, Results, Results).
find_object_list_intersection([Obj | Objs], Point, Vector,
		Results0, Results) :-
	MaybeTrans = no,
	find_object_intersection(Obj, MaybeTrans, Point, Vector, Result),
	find_object_list_intersection(Objs, Point, Vector,
		make_tree(Result, Results0), Results).

	% Choose the closest intersection in the intersection_result
	% which is in front of the viewer.
:- pred choose_closest_intersection(position::in, vector::in,
		intersection_result::in, object_id::in,
		maybe(best_intersection)::out) is det.

choose_closest_intersection(Pos, Dir, tree(Left, Right), IgnoreId, Best) :-
	choose_closest_intersection(Pos, Dir, Left, IgnoreId, LeftBest),
	choose_closest_intersection(Pos, Dir, Right, IgnoreId, RightBest),
	choose_maybe_intersection(LeftBest, RightBest, Best).
choose_closest_intersection(Pos, Dir, node(Intersection), IgnoreId,
		MaybeIntersection) :-
	(
		snd(Intersection)^object_id \= IgnoreId,
		% Check that the intersection is not behind us:
		Point = snd(Intersection)^intersection_point,
		dot(Dir, Point - Pos) > 0.0
	->
		MaybeIntersection = yes(Intersection)
	;
		MaybeIntersection = no
	).
choose_closest_intersection(_Pos, _Dir, empty, _IgnoreId, no).

:- type best_intersection == pair(real, intersection).

:- pred choose_maybe_intersection(maybe(best_intersection)::in,
		maybe(best_intersection)::in,
		maybe(best_intersection)::out) is det.

choose_maybe_intersection(no, no, no).
choose_maybe_intersection(yes(Best), no, yes(Best)).
choose_maybe_intersection(no, yes(Best), yes(Best)).
choose_maybe_intersection(MaybeBest1, MaybeBest2, MaybeBest) :-
	MaybeBest1 = yes(Best1),
	MaybeBest2 = yes(Best2),
	Best1 = Distance1 - _,
	Best2 = Distance2 - _,
	( Distance1 < Distance2 ->
		MaybeBest = MaybeBest1
	;
		MaybeBest = MaybeBest2
	).

%
% find the points where a ray intersects with an object
%
find_object_intersection(basic_object(Id, Obj, _List), MaybeTransformation,
		RayOrigin, RayDirection, IntersectionResult) :-
	find_basic_object_intersection(Id, Obj, MaybeTransformation, 
		RayOrigin, RayDirection, Intersections),
	IntersectionResult = 
		intersection_list_to_tree(RayOrigin, Intersections).

find_object_intersection(transform(Object, Transformation1),
		MaybeTransformation, RayOrigin, RayDirection, Intersection) :-
	( MaybeTransformation = yes(_) ->
		% All transformations should have been collapsed into
		% a single transformation.
		throw(invalid_transformation(Transformation1))
	;
		true
	),
	find_object_intersection(Object, yes(Transformation1),
		RayOrigin, RayDirection, Intersection).
find_object_intersection(union(Object1, Object2), MaybeTransformation,
		RayOrigin, RayDirection, Intersection) :-
	%
	% find the points that intersect the surface of Object1
	% and that are NOT inside Object2
	%
	find_surface_and_not_object_intersection(Object1, Object2,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection1),
	%
	% find the points that intersect the surface of Object2
	% and that are NOT inside Object1
	%
	find_surface_and_not_object_intersection(Object2, Object1,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection2),
	%
	% Take the union of those.
	%
	Intersection = make_tree(Intersection1, Intersection2).

/***********
	%% XXX old code; this causes wings on the flag in examples/golf.gml
	find_object_intersection(Object1, MaybeTransformation,
			RayOrigin, RayDirection, Intersection1),
	find_object_intersection(Object2, MaybeTransformation,
			RayOrigin, RayDirection, Intersection2),
	Intersection = make_tree(Intersection1, Intersection2).
***********/

find_object_intersection(intersect(Object1, Object2), MaybeTransformation,
		RayOrigin, RayDirection, Intersection) :-
	%
	% find the points that intersect the surface of Object1
	% and that are inside Object2
	%
	find_surface_and_object_intersection(Object1, Object2,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection1),
	%
	% find the points that intersect the surface of Object2
	% and that are inside Object1
	%
	find_surface_and_object_intersection(Object2, Object1,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection2),
	%
	% take the union of those
	%
	Intersection = make_tree(Intersection1, Intersection2).

find_object_intersection(difference(Object1, Object2), MaybeTransformation,
		RayOrigin, RayDirection, IntersectionResult) :-
	%
	% find the points that intersect the surface of Object1
	% and that are NOT inside Object2
	%
	find_surface_and_not_object_intersection(Object1, Object2,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection1),
	%
	% find the points that intersect the surface of Object2
	% and that ARE inside Object1, and then reverse the
	% normals of the intersection points
	%
	find_surface_and_object_intersection(Object2, Object1,
		MaybeTransformation, RayOrigin, RayDirection,
		Intersection2),
	reverse_normals(Intersection2, Intersection3),
	%
	% take the union of those
	%
	IntersectionResult = tree(Intersection1, Intersection3).

%
% Find the points where a ray intersects the surface of one object
% and is inside another object.
%
:- pred find_surface_and_object_intersection(object::in, object::in,
		maybe(transformation)::in, point::in, vector::in,
		intersection_result::out) is det.

find_surface_and_object_intersection(Object1, Object2, MaybeTransformation,
		RayOrigin, RayDirection, IntersectionResult) :-
	find_object_intersection(Object1, MaybeTransformation,
	       RayOrigin, RayDirection, IntersectionResult0),
	select_intersection_points_in_object(IntersectionResult0,
		Object2, MaybeTransformation, IntersectionResult).

%
% Find the points where a ray intersects the surface of one object
% and is NOT inside the other object.
%
% XXX should avoid code duplication

:- pred find_surface_and_not_object_intersection(object::in, object::in,
		maybe(transformation)::in, point::in, vector::in,
		intersection_result::out) is det.

find_surface_and_not_object_intersection(Object1, Object2, MaybeTransformation,
		RayOrigin, RayDirection, IntersectionResult) :-
	find_object_intersection(Object1, MaybeTransformation,
	       RayOrigin, RayDirection, IntersectionResult0),
	select_intersection_points_NOT_in_object(IntersectionResult0,
		Object2, MaybeTransformation, IntersectionResult).

:- pred select_intersection_points_in_object(intersection_result::in,
		object::in, maybe(transformation)::in,
		intersection_result::out) is det.
select_intersection_points_in_object(empty, _, _, empty).
select_intersection_points_in_object(node(I), Object,
		MaybeTransformation, Tree) :-
	I = _Dist - Intersection,
	(
		point_is_in_object(Intersection^intersection_point,
			Object, MaybeTransformation)
	->
		Tree = node(I)
	;
		Tree = empty
	).
select_intersection_points_in_object(tree(I1, I2), Object, MT, Tree) :-
	select_intersection_points_in_object(I1, Object, MT, N1),
	select_intersection_points_in_object(I2, Object, MT, N2),
	Tree = make_tree(N1, N2).

% XXX should avoid code duplication

:- pred select_intersection_points_NOT_in_object(intersection_result::in,
		object::in, maybe(transformation)::in,
		intersection_result::out) is det.
select_intersection_points_NOT_in_object(empty, _, _, empty).
select_intersection_points_NOT_in_object(node(I), Object,
		MaybeTransformation, Tree) :-
	I = _Dist - Intersection,
	(
		\+ point_is_in_object(Intersection^intersection_point,
			Object, MaybeTransformation)
	->
		Tree = node(I)
	;
		Tree = empty
	).
select_intersection_points_NOT_in_object(tree(I1, I2), Object, MT, Tree) :-
	select_intersection_points_NOT_in_object(I1, Object, MT, N1),
	select_intersection_points_NOT_in_object(I2, Object, MT, N2),
	Tree = make_tree(N1, N2).

:- pred reverse_normals(intersection_result::in,
		intersection_result::out) is det.
reverse_normals(empty, empty).
reverse_normals(node(Dist - Intersection0), node(Dist - Intersection)) :-
	Intersection = Intersection0^surface_normal :=
		-(Intersection0^surface_normal).
reverse_normals(tree(I1, I2), make_tree(N1, N2)) :-
	reverse_normals(I1, N1),
	reverse_normals(I2, N2).

:- pragma inline(func(make_tree/2)).

make_tree(Result1, Result2) = Result :-
	( Result1 = empty ->
		Result = Result2
	; Result2 = empty ->
		Result = Result1
	;	
		Result = tree(Result1, Result2)
	).

:- pred find_basic_object_intersection(object_id::in, basic_object::in,
		maybe(transformation)::in, point::in, vector::in,
		intersections::out) is det.

find_basic_object_intersection(Id, Obj, MaybeTrans,
		RayOrigin, RayDirection, Intersections) :-
	Trans = maybe_transformation_to_trans(MaybeTrans),
	find_basic_object_intersection_2(Id, Obj, Trans,
		RayOrigin, RayDirection, Intersections).

:- pred find_basic_object_intersection_2(object_id::in, basic_object::in,
		trans::in, point::in, vector::in,
		intersections::out) is det.

find_basic_object_intersection_2(Id, sphere(Surface), Trans, RayOrigin,
		RayDirection, Intersections) :-
	intersects_sphere(Id, Trans, RayOrigin, RayDirection,
			Surface, Intersections).

find_basic_object_intersection_2(Id, Obj, Trans, RayOrigin, RayDirection, 
		Intersections) :-
	Obj = plane(Surface),
	intersects_plane(Id, Trans, RayOrigin, RayDirection,
			Surface, Intersections).

find_basic_object_intersection_2(Id, Obj, Trans, RayOrigin, RayDirection, 
		Intersections) :-
	Obj = cube(Surface),
	intersects_cube(Id, Trans, RayOrigin, RayDirection,
			Surface, Intersections).

find_basic_object_intersection_2(Id, Obj, Trans, RayOrigin, RayDirection, 
		Intersections) :-
	Obj = cylinder(Surface),
	intersects_cylinder(Id, Trans, RayOrigin, RayDirection,
			Surface, Intersections).

find_basic_object_intersection_2(Id, Obj, Trans, RayOrigin, RayDirection, 
		Intersections) :-
	Obj = cone(Surface),
	intersects_cone(Id, Trans, RayOrigin, RayDirection,
			Surface, Intersections).

%-----------------------------------------------------------------------------%
%
% step 4:
% given the surface coordinates, use the interpreter to
% call the surface function to compute the object's texture at that point
%

:- pred compute_texture(surface, surface_coordinates, surface_properties,
	io__state, io__state).
:- mode compute_texture(in, in, out, di, uo) is det.

compute_texture(Surface, Coords, Properties) -->
	{ Surface = surface(Env0, Code) },
	{ Coords = surface_coordinates(Face, U, V) },
	{ Stack0 = [real(V), real(U), int(Face)] },
	interpret(Code, Env0, Stack0, _Env, Stack),
	{ Stack = [real(N), real(Ks), real(Kd), point(C)] ->
		Properties = surface_properties(C, Kd, Ks, N)
	;
		throw(
		"surface texture function returned wrong number/type of values"
		)
	}.
compute_texture(Surface, _Coords, Properties) -->
	{ Surface = constant(Properties) }.

%-----------------------------------------------------------------------------%
%
% step 5:
% compute the pixel color at a given intersection point,
% given the surface properties at that point
%

:- pred compute_intensity(render_params::in, vector::in, object_id::in,
		point::in, vector::in, surface_properties::in, color::out,
		io__state::di, io__state::uo).
compute_intensity(RenderParams, RayDirection, IgnoreId, IntersectionPoint,
		UnitSurfaceNormal, SurfaceProperties, I, IO0, IO) :-

	%
	% see equation (10) in the spec
	%

	I0 = AmbientI + DirectionalOrPositionalI + ReflectedI,
	I = clamp(I0),
	AmbientI = scale(Kd, Ia * C),

	Ia = RenderParams ^ amb,
		
	C = SurfaceProperties ^ surface_c,
	Kd = SurfaceProperties ^ surface_kd,
	Ks = SurfaceProperties ^ surface_ks,
	PhongExp = SurfaceProperties ^ surface_n,

	Depth = RenderParams ^ depth,
	( Depth > 0 ->
		NewRenderParams = RenderParams ^ depth := Depth - 1,
		N = UnitSurfaceNormal,
		ReflectionDirection = RayDirection +
			scale(2.0, scale(dot(N, -RayDirection), N)),
		fire_ray(NewRenderParams, IgnoreId, IntersectionPoint, 
			ReflectionDirection, Is, IO0, IO1),
		ReflectedI = scale(Ks, Is * C),
		IO = IO1
	;
		ReflectedI = point(0.0, 0.0, 0.0),
		IO = IO0
	),


		% This is simple but could be done more
		% efficiently.
		%
		% XXX check both LightContribution1
		% and LightContribution2 for correctness.

	Lights = RenderParams ^ lights,
	LightContribution1 = (
		func(LightVal) = Result1 :-
			Light = light_value_to_light(LightVal),
			Lj = light_unit_vector(Light, IntersectionPoint),
			DotProd = dot(UnitSurfaceNormal, Lj),
			( DotProd =< 0.0 ->
				Result1 = zero
			;
				Ij = light_intensity(Light, IntersectionPoint),
				Result1 = scale(DotProd, Ij * C)
			)),

	array__to_list(Lights, LightList0),

	list__filter((pred(LightVal::in) is semidet :-
			position_not_in_shadow(RenderParams, IgnoreId,
					IntersectionPoint, LightVal)
		), LightList0, LightList),

	ContributionList1 = list__map(LightContribution1, LightList),

		% do we need unit?
	LightContribution2 = (
		func(LightVal) = Result :-
			% compute the direction to the light
			Light = light_value_to_light(LightVal),
			Lj = light_unit_vector(Light, IntersectionPoint),
			%
			% check that light is on the right side
			% of the surface
			%
			SanityCheck = dot(UnitSurfaceNormal, Lj),
			(
				SanityCheck =< 0.0
			->
				Result = zero
			;
				Hj = unit(Lj - unit(RayDirection)),
				ReflectionBase = dot(UnitSurfaceNormal, Hj),
				( ReflectionBase =< 0.0 ->
					Result = zero
				;
					Ij = light_intensity(Light,
						IntersectionPoint),
					Result = scale(math__pow(ReflectionBase,
						PhongExp), Ij * C)
				)
			)
		),

	ContributionList2 = list__map(LightContribution2, LightList),

	SumContributions1 = list__foldl(+, ContributionList1, 
		point(0.0, 0.0, 0.0)),
	SumContributions2 = list__foldl(+, ContributionList2, 
		point(0.0, 0.0, 0.0)),

	DirectionalOrPositionalI = scale(Kd, SumContributions1) +
		scale(Ks, SumContributions2).

	% Filter out all lights which are blocked by some
	% object, not including the object on whose surface Position is.
:- pred position_not_in_shadow(render_params, object_id, position, value).
:- mode position_not_in_shadow(in, in, in, in) is semidet.

position_not_in_shadow(RenderParams, IgnoreId, Position, LightVal) :-
	Light = light_value_to_light(LightVal),
	Lj = light_unit_vector(Light, Position),
	not (some [Intersection] (
		find_intersection(RenderParams, IgnoreId, Position, Lj, _Id,
				Intersection, _, _, _),
		light_is_behind_point(Position, Light, Intersection)
	)).

	% light_is_behind_point(VantagePoint, Light, Intersection) is true
	% if Intersection is in between the Light and the Vantage.
:- pred light_is_behind_point(position, light, position).
:- mode light_is_behind_point(in, in, in) is semidet.

light_is_behind_point(Vantage, directional(Dir, _), Point) :-
	% Ensure that the Point is in the same direction as the light
	% source.
	dot(Vantage - Point, Dir) > 0.0.
light_is_behind_point(Vantage, pointlight(LightPos, _), Point) :-
	% Ensure that the Point is between the Vantage and the Light.
	dot(LightPos - Point, Vantage - Point) < 0.0.
light_is_behind_point(Vantage, spotlight(LightPos, _, _, _, _), Point) :-
	% Ensure that the Point is between the Vantage and the Light.
	dot(LightPos - Point, Vantage - Point) < 0.0.

:- func light_value_to_light(value) = light.
light_value_to_light(Val) = Light :-
	( Val = light(Light0) ->
		Light = Light0
	;
		throw(program_error("incorrect type for element of light array"))
	).

	% Return the intensity of this light at the given position.
:- func light_intensity(light, position) = color.
light_intensity(directional(_Dir, Intensity), _Point) = Intensity.
light_intensity(pointlight(Pos, Intensity), Point) =
	scale(distance_attenuation(Pos, Point), Intensity).
light_intensity(spotlight(Pos, At, Intensity, Cutoff, Exp), Point) = 
	scale(DistanceAttenuation * AngleAttenuation, Intensity) :-

	DistanceAttenuation = distance_attenuation(Pos, Point),
	AngleCos = dot(unit(At - Pos), unit(Point - Pos)),
	(
		AngleCos > math__cos(radians(Cutoff))
	->
		AngleAttenuation = math__pow(AngleCos, Exp)
	;
		AngleAttenuation = 0.0
	).

:- func distance_attenuation(position, position) = real.
distance_attenuation(Pos1, Pos2) = 100.0 / (99.0 + D2) :-
	D2 = mag2(Pos2 - Pos1).

	% Return Lj -- the direction to the light source.
	% For directional lights, the unit vector is essentially
	% constant.
light_unit_vector(directional(Dir, _Intensity), _Point) = unit(-Dir).
light_unit_vector(pointlight(Pos, _Intensity), Point) = unit(Pos - Point).
light_unit_vector(spotlight(Pos, _At, _Intensity, _Cutoff, _Exp), Point) = 
		unit(Pos - Point).

:- func *(color, color) = color. % XYZ is really RGB
point(AX, AY, AZ) * point(BX, BY, BZ) = point(AX * BX, AY * BY, AZ * BZ).

:- func clamp(color) = color. 
clamp(point(R0, G0, B0)) = point(R, G, B) :-
	R = op_clampf(R0),
	G = op_clampf(G0),
	B = op_clampf(B0).

%-----------------------------------------------------------------------------%
%
% misc stuff that may be useful
%

:- func eye_position = point.
eye_position = point(0.0, 0.0, -1.0).

	% given the fov (in degrees),
	% return the image width in world space
:- func image_width(degrees) = real.
image_width(Fov) = 2.0 * image_halfwidth(Fov).

	% given the fov (in degrees),
	% return half the image width in world space
:- func image_halfwidth(degrees) = real.
image_halfwidth(Fov) = tan(0.5 * radians(Fov)).

	% return half the image height in world space
:- func image_halfheight(real, int, int) = real.
image_halfheight(HalfWidth, WidthInPixels, HeightInPixels) =
	HalfWidth * float(HeightInPixels) / float(WidthInPixels).

:- func image_topleft(real, int, int) = point.
image_topleft(Fov, WidthInPixels, HeightInPixels) = TopLeft :-
	HalfWidth = image_halfwidth(Fov),
	HalfHeight = image_halfheight(HalfWidth, WidthInPixels, HeightInPixels),
	TopLeft = point(-HalfWidth, HalfHeight, 0.0).

	% return the ray direction for the specified pixel
:- func pixel_direction(render_params, pixel_coords) = vector.
pixel_direction(RenderParams, Pixel) = Direction :-
	Fov = RenderParams ^ fov,
	WidthInPixels = RenderParams ^ wid,
	HeightInPixels = RenderParams ^ ht,
	I = Pixel ^ pixel_i,
	J = Pixel ^ pixel_j,
	Direction = pixel_direction_2(Fov, WidthInPixels, HeightInPixels, I, J).

	% return the direction of the Jth pixel in the Ith row
:- func pixel_direction_2(real, int, int, int, int) = vector.
pixel_direction_2(Fov, WidthInPixels, HeightInPixels, I, J) = Direction :-
	TopLeft = image_topleft(Fov, WidthInPixels, HeightInPixels),
	TopLeft = point(TopLeftX, TopLeftY, _),
	Delta = image_width(Fov) / float(WidthInPixels),
	XV = TopLeftX + (float(I) + 0.5) * Delta,
	YV = TopLeftY - (float(J) + 0.5) * Delta,
	ZV = 1.0,
	Direction = point(XV, YV, ZV).

:- pred point_is_in_object(point::in, object::in, maybe(transformation)::in)
		is semidet.

point_is_in_object(Point, basic_object(_Id, Obj, _List), MaybeTransformation) :-
	point_is_in_basic_object(Point, Obj, MaybeTransformation).
point_is_in_object(Point, transform(Obj, Transformation1), MaybeTransformation)
		:-
	( MaybeTransformation = yes(_) ->
		% All transformations should have been collapsed into
		% a single transformation.
		throw(invalid_transformation(Transformation1))
	;
		true
	),
	point_is_in_object(Point, Obj, yes(Transformation1)).
point_is_in_object(Point, union(Obj1, Obj2), MaybeTransformation) :-
	(
		point_is_in_object(Point, Obj1, MaybeTransformation)
	;
		point_is_in_object(Point, Obj2, MaybeTransformation)
	).
point_is_in_object(Point, intersect(Obj1, Obj2), MaybeTransformation) :-
		point_is_in_object(Point, Obj1, MaybeTransformation),
		point_is_in_object(Point, Obj2, MaybeTransformation).
point_is_in_object(Point, difference(Obj1, Obj2), MaybeTransformation) :-
		point_is_in_object(Point, Obj1, MaybeTransformation),
		\+ point_is_in_object(Point, Obj2, MaybeTransformation).

:- pred point_is_in_basic_object(point::in, basic_object::in,
		maybe(transformation)::in) is semidet.

point_is_in_basic_object(Point, Obj, MaybeTrans) :-
	Trans = maybe_transformation_to_trans(MaybeTrans),
	point_is_in_basic_object_2(Point, Obj, Trans).

maybe_transformation_to_trans(MaybeTrans) = Trans :-
	( MaybeTrans = yes(Trans0) ->
		( Trans0 = matrix(Trans1) ->
			Trans = Trans1
		;
			% All transformations should now be matrices
			% because push_transformations was applied to
			% the object.
			throw(invalid_transformation(Trans0))
		)
	;
		Trans = identity
	).

:- pred point_is_in_basic_object_2(point::in, basic_object::in, trans::in)
		is semidet.

point_is_in_basic_object_2(Point, sphere(_Surface), Trans) :-
	inside_sphere(Point, Trans).
point_is_in_basic_object_2(Point, plane(_Surface), Trans) :-
	inside_plane(Point, Trans).
point_is_in_basic_object_2(Point, cube(_Surface), Trans) :-
	inside_cube(Point, Trans).
point_is_in_basic_object_2(Point, cylinder(_Surface), Trans) :-
	inside_cylinder(Point, Trans).
point_is_in_basic_object_2(Point, cone(_Surface), Trans) :-
	inside_cone(Point, Trans).
	
%-----------------------------------------------------------------------------%

:- func intersection_list_to_tree(point, intersections) = intersection_result.

intersection_list_to_tree(_, []) = empty.
intersection_list_to_tree(RayOrigin, [Int | Ints]) =
		tree(node(DistanceSquared - Int), Tree0) :-
	Tree0 = intersection_list_to_tree(RayOrigin, Ints),
	DistanceSquared = 
		distance_squared(RayOrigin, Int ^ intersection_point).

%-----------------------------------------------------------------------------%
