4 %% a simple raytracer written in Erlang
6 %% Copyright (c) 2008 Michael Ploujnikov
8 %% This program is free software: you can redistribute it and/or modify
9 %% it under the terms of the GNU General Public License as published by
10 %% the Free Software Foundation, either version 2 of the License, or
11 %% (at your option) any later version.
13 %% This program is distributed in the hope that it will be useful,
14 %% but WITHOUT ANY WARRANTY; without even the implied warranty of
15 %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 %% GNU General Public License for more details.
18 %% You should have received a copy of the GNU General Public License
19 %% along with this program. If not, see <http://www.gnu.org/licenses/>.
22 %% * three object types:
25 %% * triangles (not done)
28 %% * lighting based on local illumination models
29 %% * ambient (not done)
32 %% * attenuation (not done)
33 %% * reflections to a fixed depth
34 %% * PPM output file format
35 %% * randomly generated scene (not done)
36 %% * useful test suite (working but not very friendly when fails)
38 %% * specify how many pixels to give to each process (not done)
39 %% * distributed (across multiple computers) (not done)
53 raytraced_pixel_list_simple
/4,
54 raytraced_pixel_list_concurrent
/4,
55 raytraced_pixel_list_distributed
/4
58 -record(vector
, {x
, y
, z
}).
59 -record(colour
, {r
, g
, b
}).
60 -record(ray
, {origin
, direction
}).
61 -record(screen
, {width
, height
}). % screen dimensions in the 3D world
62 -record(camera
, {location
, rotation
, fov
, screen
}).
63 -record(material
, {colour
, specular_power
, shininess
, reflectivity
}).
64 -record(sphere
, {radius
, center
, material
}).
65 -record(triangle
, {v1
, v2
, v3
, material
}).
66 -record(plane
, {normal
, distance
, material
}).
67 -record(point_light
, {diffuse_colour
, location
, specular_colour
}).
68 -define(BACKGROUND_COLOUR
, #colour
{r
=0, g
=0, b
=0}).
69 -define(ERROR_COLOUR
, #colour
{r
=1, g
=0, b
=0}).
70 -define(UNKNOWN_COLOUR
, #colour
{r
=0, g
=1, b
=0}).
71 -define(FOG_DISTANCE
, 40).
73 raytraced_pixel_list_simple(0, 0, _
, _
) ->
75 raytraced_pixel_list_simple(Width
, Height
, Scene
, Recursion_depth
)
76 when Width
> 0, Height
> 0 ->
81 % coordinates passed as a percentage
83 trace_ray_through_pixel(
84 {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
))} end,
85 lists:seq(0, Width
- 1)) end,
86 lists:seq(0, Height
- 1)).
88 raytraced_pixel_list_concurrent(0, 0, _
, _
) ->
90 raytraced_pixel_list_concurrent(Width
, Height
, Scene
, Recursion_depth
)
91 when Width
> 0, Height
> 0 ->
92 Master_PID
= spawn(raytracer
, master
, [self(), Width
*Height
]),
97 % coordinates passed as a percentage
98 spawn(raytracer
, worker
,
99 [Master_PID
, X
+Y
*Width
, {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
]) end,
100 lists:seq(0, Width
- 1)) end,
101 lists:seq(0, Height
- 1)),
102 io:format("all workers have been spawned~n", []),
108 raytraced_pixel_list_distributed(0, 0, _
, _
) ->
110 raytraced_pixel_list_distributed(Width
, Height
, Scene
, Recursion_depth
)
111 when Width
> 0, Height
> 0 ->
112 io:format("distributed tracing~n", []),
113 Pool_master
= pool:start(renderslave
),
114 io:format("Pool master is ~p~n", [Pool_master
]),
115 io:format("Nodes are ~p~n", [pool:get_nodes()]),
116 Master_PID
= pool:pspawn(raytracer
, master
, [self(), Width
*Height
]),
121 % coordinates passed as a percentage
122 pool:pspawn(raytracer
, worker
,
123 [Master_PID
, X
+Y
*Width
, {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
]) end,
124 lists:seq(0, Width
- 1)) end,
125 lists:seq(0, Height
- 1)),
126 io:format("all workers have been spawned~n", []),
133 master(Program_PID
, Pixel_count
) ->
134 master(Program_PID
, Pixel_count
, []).
135 master(Program_PID
, 0, Pixel_list
) ->
136 io:format("master is done~n", []),
137 Program_PID
! lists:keysort(1, Pixel_list
);
138 % assumes all workers eventually return a good value
139 master(Program_PID
, Pixel_count
, Pixel_list
) ->
142 master(Program_PID
, Pixel_count
-1, [Pixel_tuple
|Pixel_list
])
146 % assumes X and Y are percentages of the screen dimensions
147 worker(Master_PID
, Pixel_num
, {X
, Y
}, Scene
, Recursion_depth
) ->
148 Master_PID
! {Pixel_num
,
149 colour_to_pixel(trace_ray_through_pixel({X
, Y
}, Scene
, Recursion_depth
))}.
151 trace_ray_through_pixel({X
, Y
}, [Camera
|Rest_of_scene
], Recursion_depth
) ->
152 pixel_colour_from_ray(
153 ray_through_pixel(X
, Y
, Camera
),
157 pixel_colour_from_ray(_Ray
, _Scene
, 0) ->
158 #colour
{r
=0, g
=0, b
=0};
159 pixel_colour_from_ray(Ray
, Scene
, Recursion_depth
) ->
160 case nearest_object_intersecting_ray(Ray
, Scene
) of
161 {Nearest_object
, _Distance
, Hit_location
, Hit_normal
} ->
162 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
164 vector_to_colour(lighting_function(Ray
,
176 lighting_function(Ray
, Object
, Hit_location
, Hit_normal
, Scene
,
179 fun (#point_light
{diffuse_colour
=Light_colour
,
180 location
=Light_location
,
181 specular_colour
=Specular_colour
},
183 Reflection
= vector_scalar_mult(
185 pixel_colour_from_ray(
186 #ray
{origin
=Hit_location
,
187 direction
=vector_bounce_off_plane(
188 Ray#ray
.direction
, Hit_normal
)},
191 object_reflectivity(Object
)),
192 Light_contribution
= vector_add(
203 object_specular_power(Object
),
204 object_shininess(Object
),
211 vector_component_mult(
212 colour_to_vector(Light_colour
),
214 shadow_factor(Light_location
, Hit_location
, Scene
))));
215 (_Not_a_point_light
, Final_colour
) ->
218 #vector
{x
=0, y
=0, z
=0},
221 shadow_factor(Light_location
, Hit_location
, Scene
) ->
222 Light_vector
= vector_sub(Light_location
, Hit_location
),
223 Light_vector_length
= vector_mag(Light_vector
),
224 Light_direction
= vector_normalize(Light_vector
),
225 % start the ray a little bit farther to prevent artefacts due to unit precision limitations
226 Shadow_ray
= #ray
{origin
=vector_add(
231 direction
=Light_direction
},
232 case nearest_object_intersecting_ray(Shadow_ray
, Scene
) of
233 {_Obj
, Distance
, _Loc
, _Normal
} ->
234 if Distance
== infinity
->
236 Light_vector_length
> Distance
->
245 diffuse_term(Object
, Light_location
, Hit_location
, Hit_normal
) ->
247 colour_to_vector(object_diffuse_colour(Object
)),
249 vector_dot_product(Hit_normal
,
251 vector_sub(Light_location
,
254 specular_term(EyeVector
, Light_location
, Hit_location
, Hit_normal
,
255 Specular_power
, Shininess
, Specular_colour
) ->
257 colour_to_vector(Specular_colour
),
264 vector_sub(Light_location
, Hit_location
)),
265 vector_neg(EyeVector
))),
266 Hit_normal
)]), Specular_power
)).
268 nearest_object_intersecting_ray(Ray
, Scene
) ->
269 nearest_object_intersecting_ray(
270 Ray
, none
, hitlocation
, hitnormal
, infinity
, Scene
).
271 nearest_object_intersecting_ray(
272 _Ray
, _NearestObj
, _Hit_location
, _Normal
, infinity
, []) ->
274 nearest_object_intersecting_ray(
275 _Ray
, NearestObj
, Hit_location
, Normal
, Distance
, []) ->
276 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
277 {NearestObj
, Distance
, Hit_location
, Normal
};
278 nearest_object_intersecting_ray(Ray
,
283 [CurrentObject
|Rest_of_scene
]) ->
284 NewDistance
= ray_object_intersect(Ray
, CurrentObject
),
285 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
286 if (NewDistance
/= infinity
)
287 and ((Distance
== infinity
) or (Distance
> NewDistance
)) ->
288 %io:format("another closer object found~n", []),
290 vector_add(Ray#ray
.origin
,
291 vector_scalar_mult(Ray#ray
.direction
, NewDistance
)),
292 New_normal
= object_normal_at_point(
293 CurrentObject
, New_hit_location
),
294 nearest_object_intersecting_ray(
302 %io:format("no closer obj found~n", []),
303 nearest_object_intersecting_ray(Ray
,
311 ray_object_intersect(Ray
, Object
) ->
314 ray_sphere_intersect(Ray
, Object
);
316 ray_triangle_intersect(Ray
, Object
);
318 ray_plane_intersect(Ray
, Object
);
323 object_normal_at_point(#sphere
{center
=Center
}, Point
) ->
325 vector_sub(Point
, Center
));
326 object_normal_at_point(#plane
{normal
=Normal
}, _Point
) ->
329 ray_sphere_intersect(
334 #sphere
{radius
=Radius
, center
=#vector
{
335 x
=Xc
, y
=Yc
, z
=Zc
}}) ->
337 A
= Xd
*Xd
+ Yd
*Yd
+ Zd
*Zd
,
338 B
= 2 * (Xd
*(X0
-Xc
) + Yd
*(Y0
-Yc
) + Zd
*(Z0
-Zc
)),
339 C
= (X0
-Xc
)*(X0
-Xc
) + (Y0
-Yc
)*(Y0
-Yc
) + (Z0
-Zc
)*(Z0
-Zc
) - Radius
*Radius
,
340 Discriminant
= B
*B
- 4*A
*C
,
341 %io:format("A=~w B=~w C=~w discriminant=~w~n",
342 % [A, B, C, Discriminant]),
343 if Discriminant
>= Epsilon
->
344 T0
= (-B
+ math:sqrt(Discriminant
))/2,
345 T1
= (-B
- math:sqrt(Discriminant
))/2,
346 if (T0
>= 0) and (T1
>= 0) ->
347 %io:format("T0=~w T1=~w~n", [T0, T1]),
356 ray_triangle_intersect(_Ray
, _Triangle
) ->
359 ray_plane_intersect(Ray
, Plane
) ->
361 Vd
= vector_dot_product(Plane#plane
.normal
, Ray#ray
.direction
),
363 V0
= -(vector_dot_product(Plane#plane
.normal
, Ray#ray
.origin
)
364 + Plane#plane
.distance
),
366 if Distance
< Epsilon
->
376 focal_length(Angle
, Dimension
) ->
377 Dimension
/(2*math:tan(Angle
*(math:pi()/180)/2)).
379 point_on_screen(X
, Y
, Camera
) ->
380 %TODO: implement rotation (using quaternions)
381 Screen_width
= (Camera#camera
.screen
)#screen
.width
,
382 Screen_height
= (Camera#camera
.screen
)#screen
.height
,
383 lists:foldl(fun(Vect
, Sum
) -> vector_add(Vect
, Sum
) end,
384 Camera#camera
.location
,
386 #vector
{x
=0, y
=0, z
=1},
390 #vector
{x
= (X
-0.5) * Screen_width
,
394 y
= (Y
-0.5) * Screen_height
,
399 shoot_ray(From
, Through
) ->
400 #ray
{origin
=From
, direction
=vector_normalize(vector_sub(Through
, From
))}.
402 % assume that X and Y are percentages of the 3D world screen dimensions
403 ray_through_pixel(X
, Y
, Camera
) ->
404 shoot_ray(Camera#camera
.location
, point_on_screen(X
, Y
, Camera
)).
406 vectors_equal(V1
, V2
) ->
407 vectors_equal(V1
, V2
, 0.0001).
408 vectors_equal(V1
, V2
, Epsilon
) ->
409 (V1#vector
.x
+ Epsilon
>= V2#vector
.x
)
410 and (V1#vector
.x
- Epsilon
=<V2#vector
.x
)
411 and (V1#vector
.y
+ Epsilon
>= V2#vector
.y
)
412 and (V1#vector
.y
- Epsilon
=<V2#vector
.y
)
413 and (V1#vector
.z
+ Epsilon
>= V2#vector
.z
)
414 and (V1#vector
.z
- Epsilon
=<V2#vector
.z
).
417 vector_add(V1
, V2
) ->
418 #vector
{x
= V1#vector
.x
+ V2#vector
.x
,
419 y
= V1#vector
.y
+ V2#vector
.y
,
420 z
= V1#vector
.z
+ V2#vector
.z
}.
422 vector_sub(V1
, V2
) ->
423 #vector
{x
= V1#vector
.x
- V2#vector
.x
,
424 y
= V1#vector
.y
- V2#vector
.y
,
425 z
= V1#vector
.z
- V2#vector
.z
}.
427 vector_square_mag(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
431 math:sqrt(vector_square_mag(V
)).
433 vector_scalar_mult(#vector
{x
=X
, y
=Y
, z
=Z
}, Scalar
) ->
434 #vector
{x
=X
*Scalar
, y
=Y
*Scalar
, z
=Z
*Scalar
}.
436 vector_component_mult(#vector
{x
=X1
, y
=Y1
, z
=Z1
}, #vector
{x
=X2
, y
=Y2
, z
=Z2
}) ->
437 #vector
{x
=X1
*X2
, y
=Y1
*Y2
, z
=Z1
*Z2
}.
439 vector_dot_product(#vector
{x
=A1
, y
=A2
, z
=A3
}, #vector
{x
=B1
, y
=B2
, z
=B3
}) ->
440 A1
*B1
+ A2
*B2
+ A3
*B3
.
442 vector_cross_product(#vector
{x
=A1
, y
=A2
, z
=A3
}, #vector
{x
=B1
, y
=B2
, z
=B3
}) ->
443 #vector
{x
= A2
*B3
- A3
*B2
,
447 vector_normalize(V
) ->
450 #vector
{x
=0, y
=0, z
=0};
452 vector_scalar_mult(V
, 1/vector_mag(V
))
455 vector_neg(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
456 #vector
{x
=-X
, y
=-Y
, z
=-Z
}.
458 vector_bounce_off_plane(Vector
, Normal
) ->
462 2*vector_dot_product(Normal
, vector_neg(Vector
))),
465 object_diffuse_colour(#sphere
{material
=#material
{colour
=C
}}) ->
467 object_diffuse_colour(#plane
{material
=#material
{colour
=C
}}) ->
469 object_specular_power(#sphere
{material
=#material
{specular_power
=SP
}}) ->
471 object_specular_power(#plane
{material
=#material
{specular_power
=SP
}}) ->
474 object_shininess(#sphere
{material
=#material
{shininess
=S
}}) ->
476 object_shininess(#plane
{material
=#material
{shininess
=S
}}) ->
479 object_reflectivity(#sphere
{material
=#material
{reflectivity
=R
}}) ->
481 object_reflectivity(#plane
{material
=#material
{reflectivity
=R
}}) ->
484 point_on_sphere(#sphere
{radius
=Radius
, center
=#vector
{x
=XC
, y
=YC
, z
=ZC
}},
485 #vector
{x
=X
, y
=Y
, z
=Z
}) ->
488 ((X
-XC
)*(X
-XC
) + (Y
-YC
)*(Y
-YC
) + (Z
-ZC
)*(Z
-ZC
)) - Radius
*Radius
).
490 colour_to_vector(#colour
{r
=R
, g
=G
, b
=B
}) ->
491 #vector
{x
=R
, y
=G
, z
=B
}.
492 vector_to_colour(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
493 #colour
{r
=X
, g
=Y
, b
=Z
}.
494 colour_to_pixel(#colour
{r
=R
, g
=G
, b
=B
}) ->
497 % returns a list of objects in the scene
498 % camera is assumed to be the first element in the scene
500 [#camera
{location
=#vector
{x
=0, y
=0, z
=-2},
501 rotation
=#vector
{x
=0, y
=0, z
=0},
503 screen
=#screen
{width
=4, height
=3}},
504 #point_light
{diffuse_colour
=#colour
{r
=1, g
=1, b
=0.5},
505 location
=#vector
{x
=5, y
=-2, z
=0},
506 specular_colour
=#colour
{r
=1, g
=1, b
=1}},
507 #point_light
{diffuse_colour
=#colour
{r
=1, g
=0, b
=0.5},
508 location
=#vector
{x
=-10, y
=0, z
=7},
509 specular_colour
=#colour
{r
=1, g
=0, b
=0.5}},
511 center
=#vector
{x
=4, y
=0, z
=10},
513 colour
=#colour
{r
=0, g
=0.5, b
=1},
518 center
=#vector
{x
=-5, y
=3, z
=9},
520 colour
=#colour
{r
=1, g
=0.5, b
=0},
525 center
=#vector
{x
=-4.5, y
=-2.5, z
=14},
527 colour
=#colour
{r
=0.5, g
=1, b
=0},
531 #triangle
{v1
=#vector
{x
=2, y
=1.5, z
=0},
532 v2
=#vector
{x
=2, y
=1.5, z
=10},
533 v3
=#vector
{x
=-2, y
=1.5, z
=0},
535 colour
=#colour
{r
=0.5, g
=0, b
=1},
539 #plane
{normal
=#vector
{x
=0, y
=-1, z
=0},
542 colour
=#colour
{r
=1, g
=1, b
=1},
549 % assumes Pixels are ordered in a row by row fasion
550 write_pixels_to_ppm(Width
, Height
, MaxValue
, Pixels
, Filename
) ->
551 case file:open(Filename
, write
) of
553 io:format("file opened~n", []),
554 io:format(IoDevice
, "P3~n", []),
555 io:format(IoDevice
, "~p ~p~n", [Width
, Height
]),
556 io:format(IoDevice
, "~p~n", [MaxValue
]),
558 fun({_Num
, {R
, G
, B
}}) ->
559 io:format(IoDevice
, "~p ~p ~p ",
560 [lists:min([trunc(R
*MaxValue
), MaxValue
]),
561 lists:min([trunc(G
*MaxValue
), MaxValue
]),
562 lists:min([trunc(B
*MaxValue
), MaxValue
])]) end,
564 file:close(IoDevice
);
566 io:format("error opening file~n", [])
569 % various invocation style functions
570 standalone([Width
, Height
, Filename
, Recursion_depth
, Strategy
]) ->
571 standalone(list_to_integer(Width
),
572 list_to_integer(Height
),
574 list_to_integer(Recursion_depth
),
575 tracing_function(list_to_atom(Strategy
))).
577 standalone(Width
, Height
, Filename
, Recursion_depth
, Function
) ->
578 {Time
, _Value
} = timer:tc(
586 io:format("Done in ~w seconds~n", [Time
/1000000]),
590 raytrace(tracing_function(Strategy
)).
592 go(Width
, Height
, Filename
, Recursion_depth
, Strategy
) ->
593 raytrace(Width
, Height
, Filename
, Recursion_depth
,
594 tracing_function(Strategy
)).
596 tracing_function(simple
) ->
597 fun raytraced_pixel_list_simple
/4;
598 tracing_function(concurrent
) ->
599 fun raytraced_pixel_list_concurrent
/4;
600 tracing_function(distributed
) ->
601 fun raytraced_pixel_list_distributed
/4.
603 raytrace(Function
) ->
604 raytrace(4, 3, "/tmp/traced.ppm", 5, Function
).
605 raytrace(Width
, Height
, Filename
, Recursion_depth
, Function
) ->
619 Tests
= [fun scene_test
/0,
621 fun vector_equality_test
/0,
622 fun vector_addition_test
/0,
623 fun vector_subtraction_test
/0,
624 fun vector_square_mag_test
/0,
625 fun vector_mag_test
/0,
626 fun vector_scalar_multiplication_test
/0,
627 fun vector_dot_product_test
/0,
628 fun vector_cross_product_test
/0,
629 fun vector_normalization_test
/0,
630 fun vector_negation_test
/0,
631 % fun ray_through_pixel_test/0,
632 fun ray_shooting_test
/0,
633 fun point_on_screen_test
/0,
634 fun nearest_object_intersecting_ray_test
/0,
635 fun focal_length_test
/0,
636 % fun vector_rotation_test/0,
637 fun object_normal_at_point_test
/0,
638 fun vector_bounce_off_plane_test
/0,
639 fun ray_sphere_intersection_test
/0
641 run_tests(Tests
, 1, true
).
644 io:format("testing the scene function", []),
658 {colour
, 1, 0, 0.5}},
662 {material
, {colour
, 0, 0.5, 1}, 20, 1, 0.1}},
666 {material
, {colour
, 1, 0.5, 0}, 4, 0.25, 0.5}},
669 {vector
, -4.5, -2.5, 14},
670 {material
, {colour
, 0.5, 1, 0}, 20, 0.25, 0.7}},
673 {vector
, 2, 1.5, 10},
674 {vector
, -2, 1.5, 0},
675 {material
, {colour
, 0.5, 0, 1}, 40, 1, 1}},
679 {material
, {colour
, 1, 1, 1}, 1, 0, 0.01}}
687 io:format("this test always passes", []),
690 run_tests([], _Num
, Success
) ->
693 io:format("Success!~n", []),
696 io:format("some tests failed~n", []),
700 run_tests([First_test
|Rest_of_tests
], Num
, Success_so_far
) ->
701 io:format("test #~p: ", [Num
]),
702 Current_success
= First_test(),
703 case Current_success
of
705 io:format(" - OK~n", []);
707 io:format(" - FAILED~n", [])
709 run_tests(Rest_of_tests
, Num
+ 1, Current_success and Success_so_far
).
711 vector_equality_test() ->
712 io:format("vector equality"),
713 Vector1
= #vector
{x
=0, y
=0, z
=0},
714 Vector2
= #vector
{x
=1234, y
=-234, z
=0},
715 Vector3
= #vector
{x
=0.0983, y
=0.0214, z
=0.12342},
716 Vector4
= #vector
{x
=0.0984, y
=0.0213, z
=0.12341},
717 Vector5
= #vector
{x
=10/3, y
=-10/6, z
=8/7},
718 Vector6
= #vector
{x
=3.3, y
=-1.6, z
=1.1},
720 Subtest1
= vectors_equal(Vector1
, Vector1
)
721 and
vectors_equal(Vector2
, Vector2
)
722 and
not (vectors_equal(Vector1
, Vector2
))
723 and
not (vectors_equal(Vector2
, Vector1
)),
724 Subtest2
= vectors_equal(Vector3
, Vector4
, 0.0001),
725 Subtest3
= vectors_equal(Vector5
, Vector6
, 0.1),
727 Subtest1 and Subtest2 and Subtest3
.
730 vector_addition_test() ->
731 io:format("vector addition", []),
732 Vector0
= vector_add(
733 #vector
{x
=3, y
=7, z
=-3},
734 #vector
{x
=0, y
=-24, z
=123}),
735 Subtest1
= (Vector0#vector
.x
== 3)
736 and (Vector0#vector
.y
== -17)
737 and (Vector0#vector
.z
== 120),
739 Vector1
= #vector
{x
=5, y
=0, z
=984},
740 Vector2
= vector_add(Vector1
, Vector1
),
741 Subtest2
= (Vector2#vector
.x
== Vector1#vector
.x
*2)
742 and (Vector2#vector
.y
== Vector1#vector
.y
*2)
743 and (Vector2#vector
.z
== Vector1#vector
.z
*2),
745 Vector3
= #vector
{x
=908, y
=-098, z
=234},
746 Vector4
= vector_add(Vector3
, #vector
{x
=0, y
=0, z
=0}),
747 Subtest3
= vectors_equal(Vector3
, Vector4
),
749 Subtest1 and Subtest2 and Subtest3
.
751 vector_subtraction_test() ->
752 io:format("vector subtraction", []),
753 Vector1
= #vector
{x
=0, y
=0, z
=0},
754 Vector2
= #vector
{x
=8390, y
=-2098, z
=939},
755 Vector3
= #vector
{x
=1, y
=1, z
=1},
756 Vector4
= #vector
{x
=-1, y
=-1, z
=-1},
758 Subtest1
= vectors_equal(Vector1
, vector_sub(Vector1
, Vector1
)),
759 Subtest2
= vectors_equal(Vector3
, vector_sub(Vector3
, Vector1
)),
760 Subtest3
= not
vectors_equal(Vector3
, vector_sub(Vector1
, Vector3
)),
761 Subtest4
= vectors_equal(Vector4
, vector_sub(Vector4
, Vector1
)),
762 Subtest5
= not
vectors_equal(Vector4
, vector_sub(Vector1
, Vector4
)),
763 Subtest5
= vectors_equal(vector_add(Vector2
, Vector4
),
764 vector_sub(Vector2
, Vector3
)),
766 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5
.
768 vector_square_mag_test() ->
769 io:format("vector square magnitude test", []),
770 Vector1
= #vector
{x
=0, y
=0, z
=0},
771 Vector2
= #vector
{x
=1, y
=1, z
=1},
772 Vector3
= #vector
{x
=3, y
=-4, z
=0},
774 Subtest1
= (0 == vector_square_mag(Vector1
)),
775 Subtest2
= (3 == vector_square_mag(Vector2
)),
776 Subtest3
= (25 == vector_square_mag(Vector3
)),
778 Subtest1 and Subtest2 and Subtest3
.
781 io:format("vector magnitude test", []),
782 Vector1
= #vector
{x
=0, y
=0, z
=0},
783 Vector2
= #vector
{x
=1, y
=1, z
=1},
784 Vector3
= #vector
{x
=3, y
=-4, z
=0},
786 Subtest1
= (0 == vector_mag(Vector1
)),
787 Subtest2
= (math:sqrt(3) == vector_mag(Vector2
)),
788 Subtest3
= (5 == vector_mag(Vector3
)),
790 Subtest1 and Subtest2 and Subtest3
.
792 vector_scalar_multiplication_test() ->
793 io:format("scalar multiplication test", []),
794 Vector1
= #vector
{x
=0, y
=0, z
=0},
795 Vector2
= #vector
{x
=1, y
=1, z
=1},
796 Vector3
= #vector
{x
=3, y
=-4, z
=0},
798 Subtest1
= vectors_equal(Vector1
, vector_scalar_mult(Vector1
, 45)),
799 Subtest2
= vectors_equal(Vector1
, vector_scalar_mult(Vector1
, -13)),
800 Subtest3
= vectors_equal(Vector1
, vector_scalar_mult(Vector3
, 0)),
801 Subtest4
= vectors_equal(#vector
{x
=4, y
=4, z
=4},
802 vector_scalar_mult(Vector2
, 4)),
803 Subtest5
= vectors_equal(Vector3
, vector_scalar_mult(Vector3
, 1)),
804 Subtest6
= not
vectors_equal(Vector3
, vector_scalar_mult(Vector3
, -3)),
806 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
808 vector_dot_product_test() ->
809 io:format("dot product test", []),
810 Vector1
= #vector
{x
=1, y
=3, z
=-5},
811 Vector2
= #vector
{x
=4, y
=-2, z
=-1},
812 Vector3
= #vector
{x
=0, y
=0, z
=0},
813 Vector4
= #vector
{x
=1, y
=0, z
=0},
814 Vector5
= #vector
{x
=0, y
=1, z
=0},
816 Subtest1
= 3 == vector_dot_product(Vector1
, Vector2
),
817 Subtest2
= vector_dot_product(Vector2
, Vector2
)
818 == vector_square_mag(Vector2
),
819 Subtest3
= 0 == vector_dot_product(Vector3
, Vector1
),
820 Subtest4
= 0 == vector_dot_product(Vector4
, Vector5
),
822 Subtest1 and Subtest2 and Subtest3 and Subtest4
.
824 vector_cross_product_test() ->
825 io:format("cross product test", []),
826 Vector1
= #vector
{x
=0, y
=0, z
=0},
827 Vector2
= #vector
{x
=1, y
=0, z
=0},
828 Vector3
= #vector
{x
=0, y
=1, z
=0},
829 Vector4
= #vector
{x
=0, y
=0, z
=1},
830 Vector5
= #vector
{x
=1, y
=2, z
=3},
831 Vector6
= #vector
{x
=4, y
=5, z
=6},
832 Vector7
= #vector
{x
=-3, y
=6, z
=-3},
833 Vector8
= #vector
{x
=-1, y
=0, z
=0},
834 Vector9
= #vector
{x
=-9, y
=8, z
=433},
836 Subtest1
= vectors_equal(Vector1
, vector_cross_product(Vector2
, Vector2
)),
837 Subtest2
= vectors_equal(Vector1
, vector_cross_product(Vector2
, Vector8
)),
838 Subtest3
= vectors_equal(Vector2
, vector_cross_product(Vector3
, Vector4
)),
839 Subtest4
= vectors_equal(Vector7
, vector_cross_product(Vector5
, Vector6
)),
840 Subtest5
= vectors_equal(
841 vector_cross_product(Vector7
,
842 vector_add(Vector8
, Vector9
)),
844 vector_cross_product(Vector7
, Vector8
),
845 vector_cross_product(Vector7
, Vector9
))),
846 Subtest6
= vectors_equal(Vector1
,
849 vector_cross_product(
851 vector_cross_product(Vector8
, Vector9
)),
852 vector_cross_product(
854 vector_cross_product(Vector9
, Vector7
))),
855 vector_cross_product(
857 vector_cross_product(Vector7
, Vector8
)))),
859 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
861 vector_normalization_test() ->
862 io:format("normalization test", []),
863 Vector1
= #vector
{x
=0, y
=0, z
=0},
864 Vector2
= #vector
{x
=1, y
=0, z
=0},
865 Vector3
= #vector
{x
=5, y
=0, z
=0},
867 Subtest1
= vectors_equal(Vector1
, vector_normalize(Vector1
)),
868 Subtest2
= vectors_equal(Vector2
, vector_normalize(Vector2
)),
869 Subtest3
= vectors_equal(Vector2
, vector_normalize(Vector3
)),
870 Subtest4
= vectors_equal(Vector2
, vector_normalize(
871 vector_scalar_mult(Vector2
, 324))),
873 Subtest1 and Subtest2 and Subtest3 and Subtest4
.
875 vector_negation_test() ->
876 io:format("vector negation test", []),
877 Vector1
= #vector
{x
=0, y
=0, z
=0},
878 Vector2
= #vector
{x
=4, y
=-5, z
=6},
880 Subtest1
= vectors_equal(Vector1
, vector_neg(Vector1
)),
881 Subtest2
= vectors_equal(Vector2
, vector_neg(vector_neg(Vector2
))),
883 Subtest1 and Subtest2
.
885 ray_shooting_test() ->
886 io:format("ray shooting test"),
887 Vector1
= #vector
{x
=0, y
=0, z
=0},
888 Vector2
= #vector
{x
=1, y
=0, z
=0},
890 Subtest1
= vectors_equal(
891 (shoot_ray(Vector1
, Vector2
))#ray
.direction
,
896 ray_sphere_intersection_test() ->
897 io:format("ray sphere intersection test", []),
901 center
=#vector
{x
= 0, y
=0, z
=10},
903 colour
=#colour
{r
=0.4, g
=0.4, b
=0.4}}},
905 origin
=#vector
{x
=0, y
=0, z
=0},
906 direction
=#vector
{x
=0, y
=0, z
=1}},
908 origin
=#vector
{x
=3, y
=0, z
=0},
909 direction
=#vector
{x
=0, y
=0, z
=1}},
911 origin
=#vector
{x
=4, y
=0, z
=0},
912 direction
=#vector
{x
=0, y
=0, z
=1}},
913 Subtest1
= ray_sphere_intersect(Ray1
, Sphere
) == 7.0,
914 Subtest2
= ray_sphere_intersect(Ray2
, Sphere
) == infinity
,
915 Subtest3
= ray_sphere_intersect(Ray3
, Sphere
) == infinity
,
916 Subtest1 and Subtest2 and Subtest3
.
918 point_on_screen_test() ->
919 io:format("point on screen test", []),
920 Camera1
= #camera
{location
=#vector
{x
=0, y
=0, z
=0},
921 rotation
=#vector
{x
=0, y
=0, z
=0},
923 screen
=#screen
{width
=1, height
=1}},
924 Camera2
= #camera
{location
=#vector
{x
=0, y
=0, z
=0},
925 rotation
=#vector
{x
=0, y
=0, z
=0},
927 screen
=#screen
{width
=640, height
=480}},
929 Subtest1
= vectors_equal(
930 #vector
{x
=0, y
=0, z
=0.5},
931 point_on_screen(0.5, 0.5, Camera1
)),
932 Subtest2
= vectors_equal(
933 #vector
{x
=-0.5, y
=-0.5, z
=0.5},
934 point_on_screen(0, 0, Camera1
)),
935 Subtest3
= vectors_equal(
936 #vector
{x
=0.5, y
=0.5, z
=0.5},
937 point_on_screen(1, 1, Camera1
)),
938 Subtest4
= vectors_equal(
939 point_on_screen(0, 0, Camera2
),
940 #vector
{x
=-320, y
=-240, z
=320}),
941 Subtest5
= vectors_equal(
942 point_on_screen(1, 1, Camera2
),
943 #vector
{x
=320, y
=240, z
=320}),
944 Subtest6
= vectors_equal(
945 point_on_screen(0.5, 0.5, Camera2
),
946 #vector
{x
=0, y
=0, z
=320}),
948 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
950 nearest_object_intersecting_ray_test() ->
951 io:format("nearest object intersecting ray test", []),
952 % test to make sure that we really get the closest object
953 Sphere1
=#sphere
{radius
=5,
954 center
=#vector
{x
=0, y
=0, z
=10},
956 colour
=#colour
{r
=0, g
=0, b
=0.03}}},
957 Sphere2
=#sphere
{radius
=5,
958 center
=#vector
{x
=0, y
=0, z
=20},
960 colour
=#colour
{r
=0, g
=0, b
=0.06}}},
961 Sphere3
=#sphere
{radius
=5,
962 center
=#vector
{x
=0, y
=0, z
=30},
964 colour
=#colour
{r
=0, g
=0, b
=0.09}}},
965 Sphere4
=#sphere
{radius
=5,
966 center
=#vector
{x
=0, y
=0, z
=-10},
968 colour
=#colour
{r
=0, g
=0, b
=-0.4}}},
969 Scene1
=[Sphere1
, Sphere2
, Sphere3
, Sphere4
],
970 Ray1
=#ray
{origin
=#vector
{x
=0, y
=0, z
=0},
971 direction
=#vector
{x
=0, y
=0, z
=1}},
973 {Object1
, Distance1
, Hit_location
, Normal
} = nearest_object_intersecting_ray(
975 Subtest1
= (Object1
== Sphere1
) and (Distance1
== 5)
976 and
vectors_equal(Normal
, vector_neg(Ray1#ray
.direction
))
977 and
point_on_sphere(Sphere1
, Hit_location
),
981 focal_length_test() ->
984 io:format("focal length test", []),
986 fun({Focal_length
, Dimension
}, Matches
) ->
987 %Result = focal_length(Dimension, Size),
988 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
990 and ((Focal_length
+ Epsilon
>= focal_length(
992 and (Focal_length
- Epsilon
=< focal_length(
995 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
997 object_normal_at_point_test() ->
998 io:format("object normal at point test"),
999 Sphere1
= #sphere
{radius
=13.5,
1000 center
=#vector
{x
=0, y
=0, z
=0},
1002 colour
=#colour
{r
=0, g
=0, b
=0}}},
1003 Point1
= #vector
{x
=13.5, y
=0, z
=0},
1004 Point2
= #vector
{x
=0, y
=13.5, z
=0},
1005 Point3
= #vector
{x
=0, y
=0, z
=13.5},
1006 Point4
= vector_neg(Point1
),
1007 Point5
= vector_neg(Point2
),
1008 Point6
= vector_neg(Point3
),
1010 % sphere object tests
1011 Subtest1
= vectors_equal(
1012 vector_normalize(Point1
),
1013 object_normal_at_point(Sphere1
, Point1
)),
1014 Subtest2
= vectors_equal(
1015 vector_normalize(Point2
),
1016 object_normal_at_point(Sphere1
, Point2
)),
1017 Subtest3
= vectors_equal(
1018 vector_normalize(Point3
),
1019 object_normal_at_point(Sphere1
, Point3
)),
1020 Subtest4
= vectors_equal(
1021 vector_normalize(Point4
),
1022 object_normal_at_point(Sphere1
, Point4
)),
1023 Subtest5
= vectors_equal(
1024 vector_normalize(Point5
),
1025 object_normal_at_point(Sphere1
, Point5
)),
1026 Subtest6
= vectors_equal(
1027 vector_normalize(Point6
),
1028 object_normal_at_point(Sphere1
, Point6
)),
1029 Subtest7
= not
vectors_equal(
1030 vector_normalize(Point1
),
1031 object_normal_at_point(Sphere1
, Point4
)),
1033 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
1036 vector_bounce_off_plane_test() ->
1037 io:format("vector reflect about normal", []),
1038 Vector1
= #vector
{x
=1, y
=1, z
=0},
1039 Vector2
= #vector
{x
=0, y
=-1, z
=0},
1040 Vector3
= #vector
{x
=1, y
=-1, z
=0},
1041 Vector4
= #vector
{x
=1, y
=0, z
=0},
1043 Subtest1
= vectors_equal(vector_bounce_off_plane(
1045 vector_normalize(Vector2
)),
1048 Subtest2
= vectors_equal(
1049 vector_bounce_off_plane(
1051 vector_normalize(Vector1
)),
1054 Subtest1 and Subtest2
.