refactored lighting_function() by using some local variables
[eraytracer.git] / raytracer.erl
blob68c5f8d6ddffb5e69c3eed11386c4bcb010c5e30
2 %% raytracer.erl
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/>.
21 %% Features:
22 %% * three object types:
23 %% * spheres
24 %% * planes
25 %% * triangles (not done)
26 %% * point lights
27 %% * shadows (not done)
28 %% * lighting based on local illumination models
29 %% * ambient (not done)
30 %% * diffuse
31 %% * specular
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)
37 %% * concurrent
38 %% * distributed (across multiple computers) (not done)
42 -module(raytracer).
43 -export([go/1,
44 go/5,
45 raytrace/1,
46 raytrace/5,
47 run_tests/0,
48 master/2,
49 worker/5,
50 standalone/1,
51 standalone/5,
52 raytraced_pixel_list_simple/4,
53 raytraced_pixel_list_concurrent/4
54 ]).
56 -record(vector, {x, y, z}).
57 -record(colour, {r, g, b}).
58 -record(ray, {origin, direction}).
59 -record(screen, {width, height}). % screen dimensions in the 3D world
60 -record(camera, {location, rotation, fov, screen}).
61 -record(material, {colour, specular_power, shininess, reflectivity}).
62 -record(sphere, {radius, center, material}).
63 -record(triangle, {v1, v2, v3, material}).
64 -record(plane, {normal, distance, material}).
65 -record(point_light, {diffuse_colour, location, specular_colour}).
66 -define(BACKGROUND_COLOUR, #colour{r=0, g=0, b=0}).
67 -define(ERROR_COLOUR, #colour{r=1, g=0, b=0}).
68 -define(UNKNOWN_COLOUR, #colour{r=0, g=1, b=0}).
69 -define(FOG_DISTANCE, 40).
71 raytraced_pixel_list_simple(0, 0, _, _) ->
72 done;
73 raytraced_pixel_list_simple(Width, Height, Scene, Recursion_depth)
74 when Width > 0, Height > 0 ->
75 lists:flatmap(
76 fun(Y) ->
77 lists:map(
78 fun(X) ->
79 % coordinates passed as a percentage
80 {1, colour_to_pixel(
81 trace_ray_through_pixel(
82 {X/Width, Y/Height}, Scene, Recursion_depth))} end,
83 lists:seq(0, Width - 1)) end,
84 lists:seq(0, Height - 1)).
86 raytraced_pixel_list_concurrent(0, 0, _, _) ->
87 done;
88 raytraced_pixel_list_concurrent(Width, Height, Scene, Recursion_depth)
89 when Width > 0, Height > 0 ->
90 Master_PID = spawn(raytracer, master, [self(), Width*Height]),
91 lists:flatmap(
92 fun(Y) ->
93 lists:map(
94 fun(X) ->
95 % coordinates passed as a percentage
96 spawn(raytracer, worker,
97 [Master_PID, X+Y*Width, {X/Width, Y/Height}, Scene, Recursion_depth]) end,
98 lists:seq(0, Width - 1)) end,
99 lists:seq(0, Height - 1)),
100 io:format("all workers have been spawned~n", []),
101 receive
102 Final_pixel_list ->
103 Final_pixel_list
104 end.
106 master(Program_PID, Pixel_count) ->
107 master(Program_PID, Pixel_count, []).
108 master(Program_PID, 0, Pixel_list) ->
109 io:format("master is done~n", []),
110 Program_PID ! lists:keysort(1, Pixel_list);
111 % assumes all workers eventually return a good value
112 master(Program_PID, Pixel_count, Pixel_list) ->
113 receive
114 Pixel_tuple ->
115 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
116 end.
119 % assumes X and Y are percentages of the screen dimensions
120 worker(Master_PID, Pixel_num, {X, Y}, Scene, Recursion_depth) ->
121 Master_PID ! {Pixel_num,
122 colour_to_pixel(trace_ray_through_pixel({X, Y}, Scene, Recursion_depth))}.
124 trace_ray_through_pixel({X, Y}, [Camera|Rest_of_scene], Recursion_depth) ->
125 pixel_colour_from_ray(
126 ray_through_pixel(X, Y, Camera),
127 Rest_of_scene,
128 Recursion_depth).
130 pixel_colour_from_ray(_Ray, _Scene, 0) ->
131 #colour{r=0, g=0, b=0};
132 pixel_colour_from_ray(Ray, Scene, Recursion_depth) ->
133 case nearest_object_intersecting_ray(Ray, Scene) of
134 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
135 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
137 vector_to_colour(lighting_function(Ray,
138 Nearest_object,
139 Hit_location,
140 Hit_normal,
141 Scene,
142 Recursion_depth));
143 none ->
144 ?BACKGROUND_COLOUR;
145 _Else ->
146 ?ERROR_COLOUR
147 end.
149 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
150 Recursion_depth) ->
151 lists:foldl(
152 fun (#point_light{diffuse_colour=Light_colour,
153 location=Light_location,
154 specular_colour=Specular_colour},
155 Final_colour) ->
156 Reflection = vector_scalar_mult(
157 colour_to_vector(
158 pixel_colour_from_ray(
159 #ray{origin=Hit_location,
160 direction=vector_bounce_off_plane(
161 Ray#ray.direction, Hit_normal)},
162 Scene,
163 Recursion_depth-1)),
164 object_reflectivity(Object)),
165 Light_contribution = vector_add(
166 diffuse_term(
167 Object,
168 Light_location,
169 Hit_location,
170 Hit_normal),
171 specular_term(
172 Ray#ray.direction,
173 Light_location,
174 Hit_location,
175 Hit_normal,
176 object_specular_power(Object),
177 object_shininess(Object),
178 Specular_colour)),
179 vector_add(
180 Final_colour,
181 vector_add(
182 Reflection,
183 vector_scalar_mult(
184 vector_component_mult(
185 colour_to_vector(Light_colour),
186 Light_contribution),
187 shadow_factor(Light_location, Hit_location, Scene))));
188 (_Not_a_point_light, Final_colour) ->
189 Final_colour
190 end,
191 #vector{x=0, y=0, z=0},
192 Scene).
194 shadow_factor(Light_location, Hit_location, Scene) ->
195 Light_vector = vector_sub(Light_location, Hit_location),
196 Light_vector_length = vector_mag(Light_vector),
197 Light_direction = vector_normalize(Light_vector),
198 % start the ray a little bit farther to prevent artefacts due to unit precision limitations
199 Shadow_ray = #ray{origin=vector_add(
200 Hit_location,
201 vector_scalar_mult(
202 Light_direction,
203 0.001)),
204 direction=Light_direction},
205 case nearest_object_intersecting_ray(Shadow_ray, Scene) of
206 {_Obj, Distance, _Loc, _Normal} ->
207 if Distance == infinity ->
209 Light_vector_length > Distance ->
211 true ->
213 end;
214 none ->
216 end.
218 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
219 vector_scalar_mult(
220 colour_to_vector(object_diffuse_colour(Object)),
221 lists:max([0,
222 vector_dot_product(Hit_normal,
223 vector_normalize(
224 vector_sub(Light_location,
225 Hit_location)))])).
227 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
228 Specular_power, Shininess, Specular_colour) ->
229 vector_scalar_mult(
230 colour_to_vector(Specular_colour),
231 Shininess*math:pow(
232 lists:max([0,
233 vector_dot_product(
234 vector_normalize(
235 vector_add(
236 vector_normalize(
237 vector_sub(Light_location, Hit_location)),
238 vector_neg(EyeVector))),
239 Hit_normal)]), Specular_power)).
241 nearest_object_intersecting_ray(Ray, Scene) ->
242 nearest_object_intersecting_ray(
243 Ray, none, hitlocation, hitnormal, infinity, Scene).
244 nearest_object_intersecting_ray(
245 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
246 none;
247 nearest_object_intersecting_ray(
248 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
249 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
250 {NearestObj, Distance, Hit_location, Normal};
251 nearest_object_intersecting_ray(Ray,
252 NearestObj,
253 Hit_location,
254 Normal,
255 Distance,
256 [CurrentObject|Rest_of_scene]) ->
257 NewDistance = ray_object_intersect(Ray, CurrentObject),
258 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
259 if (NewDistance /= infinity)
260 and ((Distance == infinity) or (Distance > NewDistance)) ->
261 %io:format("another closer object found~n", []),
262 New_hit_location =
263 vector_add(Ray#ray.origin,
264 vector_scalar_mult(Ray#ray.direction, NewDistance)),
265 New_normal = object_normal_at_point(
266 CurrentObject, New_hit_location),
267 nearest_object_intersecting_ray(
268 Ray,
269 CurrentObject,
270 New_hit_location,
271 New_normal,
272 NewDistance,
273 Rest_of_scene);
274 true ->
275 %io:format("no closer obj found~n", []),
276 nearest_object_intersecting_ray(Ray,
277 NearestObj,
278 Hit_location,
279 Normal,
280 Distance,
281 Rest_of_scene)
282 end.
284 ray_object_intersect(Ray, Object) ->
285 case Object of
286 #sphere{} ->
287 ray_sphere_intersect(Ray, Object);
288 #triangle{} ->
289 ray_triangle_intersect(Ray, Object);
290 #plane{} ->
291 ray_plane_intersect(Ray, Object);
292 _Else ->
293 infinity
294 end.
296 object_normal_at_point(#sphere{center=Center}, Point) ->
297 vector_normalize(
298 vector_sub(Point, Center));
299 object_normal_at_point(#plane{normal=Normal}, _Point) ->
300 Normal.
302 ray_sphere_intersect(
303 #ray{origin=#vector{
304 x=X0, y=Y0, z=Z0},
305 direction=#vector{
306 x=Xd, y=Yd, z=Zd}},
307 #sphere{radius=Radius, center=#vector{
308 x=Xc, y=Yc, z=Zc}}) ->
309 Epsilon = 0.001,
310 A = Xd*Xd + Yd*Yd + Zd*Zd,
311 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
312 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
313 Discriminant = B*B - 4*A*C,
314 %io:format("A=~w B=~w C=~w discriminant=~w~n",
315 % [A, B, C, Discriminant]),
316 if Discriminant >= Epsilon ->
317 T0 = (-B + math:sqrt(Discriminant))/2,
318 T1 = (-B - math:sqrt(Discriminant))/2,
319 if (T0 >= 0) and (T1 >= 0) ->
320 %io:format("T0=~w T1=~w~n", [T0, T1]),
321 lists:min([T0, T1]);
322 true ->
323 infinity
324 end;
325 true ->
326 infinity
327 end.
329 ray_triangle_intersect(_Ray, _Triangle) ->
330 infinity.
332 ray_plane_intersect(Ray, Plane) ->
333 Epsilon = 0.001,
334 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
335 if Vd < 0 ->
336 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
337 + Plane#plane.distance),
338 Distance = V0 / Vd,
339 if Distance < Epsilon ->
340 infinity;
341 true ->
342 Distance
343 end;
344 true ->
345 infinity
346 end.
349 focal_length(Angle, Dimension) ->
350 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
352 point_on_screen(X, Y, Camera) ->
353 %TODO: implement rotation (using quaternions)
354 Screen_width = (Camera#camera.screen)#screen.width,
355 Screen_height = (Camera#camera.screen)#screen.height,
356 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
357 Camera#camera.location,
358 [vector_scalar_mult(
359 #vector{x=0, y=0, z=1},
360 focal_length(
361 Camera#camera.fov,
362 Screen_width)),
363 #vector{x = (X-0.5) * Screen_width,
364 y=0,
365 z=0},
366 #vector{x=0,
367 y= (Y-0.5) * Screen_height,
368 z=0}
372 shoot_ray(From, Through) ->
373 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
375 % assume that X and Y are percentages of the 3D world screen dimensions
376 ray_through_pixel(X, Y, Camera) ->
377 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
379 vectors_equal(V1, V2) ->
380 vectors_equal(V1, V2, 0.0001).
381 vectors_equal(V1, V2, Epsilon) ->
382 (V1#vector.x + Epsilon >= V2#vector.x)
383 and (V1#vector.x - Epsilon =<V2#vector.x)
384 and (V1#vector.y + Epsilon >= V2#vector.y)
385 and (V1#vector.y - Epsilon =<V2#vector.y)
386 and (V1#vector.z + Epsilon >= V2#vector.z)
387 and (V1#vector.z - Epsilon =<V2#vector.z).
390 vector_add(V1, V2) ->
391 #vector{x = V1#vector.x + V2#vector.x,
392 y = V1#vector.y + V2#vector.y,
393 z = V1#vector.z + V2#vector.z}.
395 vector_sub(V1, V2) ->
396 #vector{x = V1#vector.x - V2#vector.x,
397 y = V1#vector.y - V2#vector.y,
398 z = V1#vector.z - V2#vector.z}.
400 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
401 X*X + Y*Y + Z*Z.
403 vector_mag(V) ->
404 math:sqrt(vector_square_mag(V)).
406 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
407 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
409 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
410 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
412 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
413 A1*B1 + A2*B2 + A3*B3.
415 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
416 #vector{x = A2*B3 - A3*B2,
417 y = A3*B1 - A1*B3,
418 z = A1*B2 - A2*B1}.
420 vector_normalize(V) ->
421 Mag = vector_mag(V),
422 if Mag == 0 ->
423 #vector{x=0, y=0, z=0};
424 true ->
425 vector_scalar_mult(V, 1/vector_mag(V))
426 end.
428 vector_neg(#vector{x=X, y=Y, z=Z}) ->
429 #vector{x=-X, y=-Y, z=-Z}.
431 vector_bounce_off_plane(Vector, Normal) ->
432 vector_add(
433 vector_scalar_mult(
434 Normal,
435 2*vector_dot_product(Normal, vector_neg(Vector))),
436 Vector).
438 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
440 object_diffuse_colour(#plane{material=#material{colour=C}}) ->
442 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
444 object_specular_power(#plane{material=#material{specular_power=SP}}) ->
447 object_shininess(#sphere{material=#material{shininess=S}}) ->
449 object_shininess(#plane{material=#material{shininess=S}}) ->
452 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
454 object_reflectivity(#plane{material=#material{reflectivity=R}}) ->
457 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
458 #vector{x=X, y=Y, z=Z}) ->
459 Epsilon = 0.001,
460 Epsilon > abs(
461 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
463 colour_to_vector(#colour{r=R, g=G, b=B}) ->
464 #vector{x=R, y=G, z=B}.
465 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
466 #colour{r=X, g=Y, b=Z}.
467 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
468 {R, G, B}.
470 % returns a list of objects in the scene
471 % camera is assumed to be the first element in the scene
472 scene() ->
473 [#camera{location=#vector{x=0, y=0, z=-2},
474 rotation=#vector{x=0, y=0, z=0},
475 fov=90,
476 screen=#screen{width=4, height=3}},
477 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
478 location=#vector{x=5, y=-2, z=0},
479 specular_colour=#colour{r=1, g=1, b=1}},
480 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
481 location=#vector{x=-10, y=0, z=7},
482 specular_colour=#colour{r=1, g=0, b=0.5}},
483 #sphere{radius=4,
484 center=#vector{x=4, y=0, z=10},
485 material=#material{
486 colour=#colour{r=0, g=0.5, b=1},
487 specular_power=20,
488 shininess=1,
489 reflectivity=0.1}},
490 #sphere{radius=4,
491 center=#vector{x=-5, y=3, z=9},
492 material=#material{
493 colour=#colour{r=1, g=0.5, b=0},
494 specular_power=4,
495 shininess=0.25,
496 reflectivity=0.5}},
497 #sphere{radius=4,
498 center=#vector{x=-4.5, y=-2.5, z=14},
499 material=#material{
500 colour=#colour{r=0.5, g=1, b=0},
501 specular_power=20,
502 shininess=0.25,
503 reflectivity=0.7}},
504 #triangle{v1=#vector{x=2, y=1.5, z=0},
505 v2=#vector{x=2, y=1.5, z=10},
506 v3=#vector{x=-2, y=1.5, z=0},
507 material=#material{
508 colour=#colour{r=0.5, g=0, b=1},
509 specular_power=40,
510 shininess=1,
511 reflectivity=1}},
512 #plane{normal=#vector{x=0, y=-1, z=0},
513 distance=5,
514 material=#material{
515 colour=#colour{r=1, g=1, b=1},
516 specular_power=1,
517 shininess=0,
518 reflectivity=0.01}}
522 % assumes Pixels are ordered in a row by row fasion
523 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
524 case file:open(Filename, write) of
525 {ok, IoDevice} ->
526 io:format("file opened~n", []),
527 io:format(IoDevice, "P3~n", []),
528 io:format(IoDevice, "~p ~p~n", [Width, Height]),
529 io:format(IoDevice, "~p~n", [MaxValue]),
530 lists:foreach(
531 fun({_Num, {R, G, B}}) ->
532 io:format(IoDevice, "~p ~p ~p ",
533 [lists:min([trunc(R*MaxValue), MaxValue]),
534 lists:min([trunc(G*MaxValue), MaxValue]),
535 lists:min([trunc(B*MaxValue), MaxValue])]) end,
536 Pixels),
537 file:close(IoDevice);
538 error ->
539 io:format("error opening file~n", [])
540 end.
542 % various invocation style functions
543 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
544 standalone(list_to_integer(Width),
545 list_to_integer(Height),
546 Filename,
547 list_to_integer(Recursion_depth),
548 tracing_function(list_to_atom(Strategy))).
550 standalone(Width, Height, Filename, Recursion_depth, Function) ->
551 {Time, _Value} = timer:tc(
552 raytracer,
553 raytrace,
554 [Width,
555 Height,
556 Filename,
557 Recursion_depth,
558 Function]),
559 io:format("Done in ~w seconds~n", [Time/1000000]),
560 halt().
562 go(Strategy) ->
563 raytrace(tracing_function(Strategy)).
565 go(Width, Height, Filename, Recursion_depth, Strategy) ->
566 raytrace(Width, Height, Filename, Recursion_depth,
567 tracing_function(Strategy)).
569 tracing_function(simple) ->
570 fun raytraced_pixel_list_simple/4;
571 tracing_function(concurrent) ->
572 fun raytraced_pixel_list_concurrent/4.
574 raytrace(Function) ->
575 raytrace(4, 3, "/tmp/traced.ppm", 5, Function).
576 raytrace(Width, Height, Filename, Recursion_depth, Function) ->
577 write_pixels_to_ppm(
578 Width,
579 Height,
580 255,
581 Function(
582 Width,
583 Height,
584 scene(),
585 Recursion_depth),
586 Filename).
588 % testing
589 run_tests() ->
590 Tests = [fun scene_test/0,
591 fun passing_test/0,
592 fun vector_equality_test/0,
593 fun vector_addition_test/0,
594 fun vector_subtraction_test/0,
595 fun vector_square_mag_test/0,
596 fun vector_mag_test/0,
597 fun vector_scalar_multiplication_test/0,
598 fun vector_dot_product_test/0,
599 fun vector_cross_product_test/0,
600 fun vector_normalization_test/0,
601 fun vector_negation_test/0,
602 % fun ray_through_pixel_test/0,
603 fun ray_shooting_test/0,
604 fun point_on_screen_test/0,
605 fun nearest_object_intersecting_ray_test/0,
606 fun focal_length_test/0,
607 % fun vector_rotation_test/0,
608 fun object_normal_at_point_test/0,
609 fun vector_bounce_off_plane_test/0,
610 fun ray_sphere_intersection_test/0
612 run_tests(Tests, 1, true).
614 scene_test() ->
615 io:format("testing the scene function", []),
616 case scene() of
617 [{camera,
618 {vector, 0, 0, -2},
619 {vector, 0, 0, 0},
621 {screen, 4, 3}},
622 {point_light,
623 {colour, 1, 1, 0.5},
624 {vector, 5, -2, 0},
625 {colour, 1, 1, 1}},
626 {point_light,
627 {colour, 1, 0, 0.5},
628 {vector, -10, 0, 7},
629 {colour, 1, 0, 0.5}},
630 {sphere,
632 {vector, 4, 0, 10},
633 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
634 {sphere,
636 {vector, -5, 3, 9},
637 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
638 {sphere,
640 {vector, -4.5, -2.5, 14},
641 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
642 {triangle,
643 {vector, 2, 1.5, 0},
644 {vector, 2, 1.5, 10},
645 {vector, -2, 1.5, 0},
646 {material, {colour, 0.5, 0, 1}, 40, 1, 1}},
647 {plane,
648 {vector, 0, -1, 0},
650 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
651 ] ->
652 true;
653 _Else ->
654 false
655 end.
657 passing_test() ->
658 io:format("this test always passes", []),
659 true.
661 run_tests([], _Num, Success) ->
662 case Success of
663 true ->
664 io:format("Success!~n", []),
666 _Else ->
667 io:format("some tests failed~n", []),
668 failed
669 end;
671 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
672 io:format("test #~p: ", [Num]),
673 Current_success = First_test(),
674 case Current_success of
675 true ->
676 io:format(" - OK~n", []);
677 _Else ->
678 io:format(" - FAILED~n", [])
679 end,
680 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
682 vector_equality_test() ->
683 io:format("vector equality"),
684 Vector1 = #vector{x=0, y=0, z=0},
685 Vector2 = #vector{x=1234, y=-234, z=0},
686 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
687 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
688 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
689 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
691 Subtest1 = vectors_equal(Vector1, Vector1)
692 and vectors_equal(Vector2, Vector2)
693 and not (vectors_equal(Vector1, Vector2))
694 and not (vectors_equal(Vector2, Vector1)),
695 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
696 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
698 Subtest1 and Subtest2 and Subtest3.
701 vector_addition_test() ->
702 io:format("vector addition", []),
703 Vector0 = vector_add(
704 #vector{x=3, y=7, z=-3},
705 #vector{x=0, y=-24, z=123}),
706 Subtest1 = (Vector0#vector.x == 3)
707 and (Vector0#vector.y == -17)
708 and (Vector0#vector.z == 120),
710 Vector1 = #vector{x=5, y=0, z=984},
711 Vector2 = vector_add(Vector1, Vector1),
712 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
713 and (Vector2#vector.y == Vector1#vector.y*2)
714 and (Vector2#vector.z == Vector1#vector.z*2),
716 Vector3 = #vector{x=908, y=-098, z=234},
717 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
718 Subtest3 = vectors_equal(Vector3, Vector4),
720 Subtest1 and Subtest2 and Subtest3.
722 vector_subtraction_test() ->
723 io:format("vector subtraction", []),
724 Vector1 = #vector{x=0, y=0, z=0},
725 Vector2 = #vector{x=8390, y=-2098, z=939},
726 Vector3 = #vector{x=1, y=1, z=1},
727 Vector4 = #vector{x=-1, y=-1, z=-1},
729 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
730 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
731 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
732 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
733 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
734 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
735 vector_sub(Vector2, Vector3)),
737 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
739 vector_square_mag_test() ->
740 io:format("vector square magnitude test", []),
741 Vector1 = #vector{x=0, y=0, z=0},
742 Vector2 = #vector{x=1, y=1, z=1},
743 Vector3 = #vector{x=3, y=-4, z=0},
745 Subtest1 = (0 == vector_square_mag(Vector1)),
746 Subtest2 = (3 == vector_square_mag(Vector2)),
747 Subtest3 = (25 == vector_square_mag(Vector3)),
749 Subtest1 and Subtest2 and Subtest3.
751 vector_mag_test() ->
752 io:format("vector magnitude test", []),
753 Vector1 = #vector{x=0, y=0, z=0},
754 Vector2 = #vector{x=1, y=1, z=1},
755 Vector3 = #vector{x=3, y=-4, z=0},
757 Subtest1 = (0 == vector_mag(Vector1)),
758 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
759 Subtest3 = (5 == vector_mag(Vector3)),
761 Subtest1 and Subtest2 and Subtest3.
763 vector_scalar_multiplication_test() ->
764 io:format("scalar multiplication test", []),
765 Vector1 = #vector{x=0, y=0, z=0},
766 Vector2 = #vector{x=1, y=1, z=1},
767 Vector3 = #vector{x=3, y=-4, z=0},
769 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
770 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
771 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
772 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
773 vector_scalar_mult(Vector2, 4)),
774 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
775 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
777 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
779 vector_dot_product_test() ->
780 io:format("dot product test", []),
781 Vector1 = #vector{x=1, y=3, z=-5},
782 Vector2 = #vector{x=4, y=-2, z=-1},
783 Vector3 = #vector{x=0, y=0, z=0},
784 Vector4 = #vector{x=1, y=0, z=0},
785 Vector5 = #vector{x=0, y=1, z=0},
787 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
788 Subtest2 = vector_dot_product(Vector2, Vector2)
789 == vector_square_mag(Vector2),
790 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
791 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
793 Subtest1 and Subtest2 and Subtest3 and Subtest4.
795 vector_cross_product_test() ->
796 io:format("cross product test", []),
797 Vector1 = #vector{x=0, y=0, z=0},
798 Vector2 = #vector{x=1, y=0, z=0},
799 Vector3 = #vector{x=0, y=1, z=0},
800 Vector4 = #vector{x=0, y=0, z=1},
801 Vector5 = #vector{x=1, y=2, z=3},
802 Vector6 = #vector{x=4, y=5, z=6},
803 Vector7 = #vector{x=-3, y=6, z=-3},
804 Vector8 = #vector{x=-1, y=0, z=0},
805 Vector9 = #vector{x=-9, y=8, z=433},
807 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
808 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
809 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
810 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
811 Subtest5 = vectors_equal(
812 vector_cross_product(Vector7,
813 vector_add(Vector8, Vector9)),
814 vector_add(
815 vector_cross_product(Vector7, Vector8),
816 vector_cross_product(Vector7, Vector9))),
817 Subtest6 = vectors_equal(Vector1,
818 vector_add(
819 vector_add(
820 vector_cross_product(
821 Vector7,
822 vector_cross_product(Vector8, Vector9)),
823 vector_cross_product(
824 Vector8,
825 vector_cross_product(Vector9, Vector7))),
826 vector_cross_product(
827 Vector9,
828 vector_cross_product(Vector7, Vector8)))),
830 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
832 vector_normalization_test() ->
833 io:format("normalization test", []),
834 Vector1 = #vector{x=0, y=0, z=0},
835 Vector2 = #vector{x=1, y=0, z=0},
836 Vector3 = #vector{x=5, y=0, z=0},
838 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
839 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
840 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
841 Subtest4 = vectors_equal(Vector2, vector_normalize(
842 vector_scalar_mult(Vector2, 324))),
844 Subtest1 and Subtest2 and Subtest3 and Subtest4.
846 vector_negation_test() ->
847 io:format("vector negation test", []),
848 Vector1 = #vector{x=0, y=0, z=0},
849 Vector2 = #vector{x=4, y=-5, z=6},
851 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
852 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
854 Subtest1 and Subtest2.
856 ray_shooting_test() ->
857 io:format("ray shooting test"),
858 Vector1 = #vector{x=0, y=0, z=0},
859 Vector2 = #vector{x=1, y=0, z=0},
861 Subtest1 = vectors_equal(
862 (shoot_ray(Vector1, Vector2))#ray.direction,
863 Vector2),
865 Subtest1.
867 ray_sphere_intersection_test() ->
868 io:format("ray sphere intersection test", []),
870 Sphere = #sphere{
871 radius=3,
872 center=#vector{x = 0, y=0, z=10},
873 material=#material{
874 colour=#colour{r=0.4, g=0.4, b=0.4}}},
875 Ray1 = #ray{
876 origin=#vector{x=0, y=0, z=0},
877 direction=#vector{x=0, y=0, z=1}},
878 Ray2 = #ray{
879 origin=#vector{x=3, y=0, z=0},
880 direction=#vector{x=0, y=0, z=1}},
881 Ray3 = #ray{
882 origin=#vector{x=4, y=0, z=0},
883 direction=#vector{x=0, y=0, z=1}},
884 Subtest1 = ray_sphere_intersect(Ray1, Sphere) == 7.0,
885 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == infinity,
886 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == infinity,
887 Subtest1 and Subtest2 and Subtest3.
889 point_on_screen_test() ->
890 io:format("point on screen test", []),
891 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
892 rotation=#vector{x=0, y=0, z=0},
893 fov=90,
894 screen=#screen{width=1, height=1}},
895 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
896 rotation=#vector{x=0, y=0, z=0},
897 fov=90,
898 screen=#screen{width=640, height=480}},
900 Subtest1 = vectors_equal(
901 #vector{x=0, y=0, z=0.5},
902 point_on_screen(0.5, 0.5, Camera1)),
903 Subtest2 = vectors_equal(
904 #vector{x=-0.5, y=-0.5, z=0.5},
905 point_on_screen(0, 0, Camera1)),
906 Subtest3 = vectors_equal(
907 #vector{x=0.5, y=0.5, z=0.5},
908 point_on_screen(1, 1, Camera1)),
909 Subtest4 = vectors_equal(
910 point_on_screen(0, 0, Camera2),
911 #vector{x=-320, y=-240, z=320}),
912 Subtest5 = vectors_equal(
913 point_on_screen(1, 1, Camera2),
914 #vector{x=320, y=240, z=320}),
915 Subtest6 = vectors_equal(
916 point_on_screen(0.5, 0.5, Camera2),
917 #vector{x=0, y=0, z=320}),
919 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
921 nearest_object_intersecting_ray_test() ->
922 io:format("nearest object intersecting ray test", []),
923 % test to make sure that we really get the closest object
924 Sphere1=#sphere{radius=5,
925 center=#vector{x=0, y=0, z=10},
926 material=#material{
927 colour=#colour{r=0, g=0, b=0.03}}},
928 Sphere2=#sphere{radius=5,
929 center=#vector{x=0, y=0, z=20},
930 material=#material{
931 colour=#colour{r=0, g=0, b=0.06}}},
932 Sphere3=#sphere{radius=5,
933 center=#vector{x=0, y=0, z=30},
934 material=#material{
935 colour=#colour{r=0, g=0, b=0.09}}},
936 Sphere4=#sphere{radius=5,
937 center=#vector{x=0, y=0, z=-10},
938 material=#material{
939 colour=#colour{r=0, g=0, b=-0.4}}},
940 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
941 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
942 direction=#vector{x=0, y=0, z=1}},
944 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
945 Ray1, Scene1),
946 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
947 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
948 and point_on_sphere(Sphere1, Hit_location),
950 Subtest1.
952 focal_length_test() ->
953 Epsilon = 0.1,
954 Size = 36,
955 io:format("focal length test", []),
956 lists:foldl(
957 fun({Focal_length, Dimension}, Matches) ->
958 %Result = focal_length(Dimension, Size),
959 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
960 Matches
961 and ((Focal_length + Epsilon >= focal_length(
962 Dimension, Size))
963 and (Focal_length - Epsilon =< focal_length(
964 Dimension, Size)))
965 end, true,
966 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
968 object_normal_at_point_test() ->
969 io:format("object normal at point test"),
970 Sphere1 = #sphere{radius=13.5,
971 center=#vector{x=0, y=0, z=0},
972 material=#material{
973 colour=#colour{r=0, g=0, b=0}}},
974 Point1 = #vector{x=13.5, y=0, z=0},
975 Point2 = #vector{x=0, y=13.5, z=0},
976 Point3 = #vector{x=0, y=0, z=13.5},
977 Point4 = vector_neg(Point1),
978 Point5 = vector_neg(Point2),
979 Point6 = vector_neg(Point3),
981 % sphere object tests
982 Subtest1 = vectors_equal(
983 vector_normalize(Point1),
984 object_normal_at_point(Sphere1, Point1)),
985 Subtest2 = vectors_equal(
986 vector_normalize(Point2),
987 object_normal_at_point(Sphere1, Point2)),
988 Subtest3 = vectors_equal(
989 vector_normalize(Point3),
990 object_normal_at_point(Sphere1, Point3)),
991 Subtest4 = vectors_equal(
992 vector_normalize(Point4),
993 object_normal_at_point(Sphere1, Point4)),
994 Subtest5 = vectors_equal(
995 vector_normalize(Point5),
996 object_normal_at_point(Sphere1, Point5)),
997 Subtest6 = vectors_equal(
998 vector_normalize(Point6),
999 object_normal_at_point(Sphere1, Point6)),
1000 Subtest7 = not vectors_equal(
1001 vector_normalize(Point1),
1002 object_normal_at_point(Sphere1, Point4)),
1004 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
1005 and Subtest7.
1007 vector_bounce_off_plane_test() ->
1008 io:format("vector reflect about normal", []),
1009 Vector1 = #vector{x=1, y=1, z=0},
1010 Vector2 = #vector{x=0, y=-1, z=0},
1011 Vector3 = #vector{x=1, y=-1, z=0},
1012 Vector4 = #vector{x=1, y=0, z=0},
1014 Subtest1 = vectors_equal(vector_bounce_off_plane(
1015 Vector1,
1016 vector_normalize(Vector2)),
1017 Vector3),
1019 Subtest2 = vectors_equal(
1020 vector_bounce_off_plane(
1021 Vector2,
1022 vector_normalize(Vector1)),
1023 Vector4),
1025 Subtest1 and Subtest2.