implemented distributed tracing using a node pool (currently slower than single machi...
[eraytracer.git] / raytracer.erl
blob8f2458d57bd3dff21e4eab78f8d0451d6490b631
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
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 %% * specify how many pixels to give to each process (not done)
39 %% * distributed (across multiple computers) (not done)
43 -module(raytracer).
44 -export([go/1,
45 go/5,
46 raytrace/1,
47 raytrace/5,
48 run_tests/0,
49 master/2,
50 worker/5,
51 standalone/1,
52 standalone/5,
53 raytraced_pixel_list_simple/4,
54 raytraced_pixel_list_concurrent/4,
55 raytraced_pixel_list_distributed/4
56 ]).
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, _, _) ->
74 done;
75 raytraced_pixel_list_simple(Width, Height, Scene, Recursion_depth)
76 when Width > 0, Height > 0 ->
77 lists:flatmap(
78 fun(Y) ->
79 lists:map(
80 fun(X) ->
81 % coordinates passed as a percentage
82 {1, colour_to_pixel(
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, _, _) ->
89 done;
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]),
93 lists:flatmap(
94 fun(Y) ->
95 lists:map(
96 fun(X) ->
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", []),
103 receive
104 Final_pixel_list ->
105 Final_pixel_list
106 end.
108 raytraced_pixel_list_distributed(0, 0, _, _) ->
109 done;
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]),
117 lists:flatmap(
118 fun(Y) ->
119 lists:map(
120 fun(X) ->
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", []),
127 receive
128 Final_pixel_list ->
129 Final_pixel_list
130 end,
131 pool:stop().
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) ->
140 receive
141 Pixel_tuple ->
142 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
143 end.
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),
154 Rest_of_scene,
155 Recursion_depth).
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,
165 Nearest_object,
166 Hit_location,
167 Hit_normal,
168 Scene,
169 Recursion_depth));
170 none ->
171 ?BACKGROUND_COLOUR;
172 _Else ->
173 ?ERROR_COLOUR
174 end.
176 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
177 Recursion_depth) ->
178 lists:foldl(
179 fun (#point_light{diffuse_colour=Light_colour,
180 location=Light_location,
181 specular_colour=Specular_colour},
182 Final_colour) ->
183 Reflection = vector_scalar_mult(
184 colour_to_vector(
185 pixel_colour_from_ray(
186 #ray{origin=Hit_location,
187 direction=vector_bounce_off_plane(
188 Ray#ray.direction, Hit_normal)},
189 Scene,
190 Recursion_depth-1)),
191 object_reflectivity(Object)),
192 Light_contribution = vector_add(
193 diffuse_term(
194 Object,
195 Light_location,
196 Hit_location,
197 Hit_normal),
198 specular_term(
199 Ray#ray.direction,
200 Light_location,
201 Hit_location,
202 Hit_normal,
203 object_specular_power(Object),
204 object_shininess(Object),
205 Specular_colour)),
206 vector_add(
207 Final_colour,
208 vector_add(
209 Reflection,
210 vector_scalar_mult(
211 vector_component_mult(
212 colour_to_vector(Light_colour),
213 Light_contribution),
214 shadow_factor(Light_location, Hit_location, Scene))));
215 (_Not_a_point_light, Final_colour) ->
216 Final_colour
217 end,
218 #vector{x=0, y=0, z=0},
219 Scene).
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(
227 Hit_location,
228 vector_scalar_mult(
229 Light_direction,
230 0.001)),
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 ->
238 true ->
240 end;
241 none ->
243 end.
245 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
246 vector_scalar_mult(
247 colour_to_vector(object_diffuse_colour(Object)),
248 lists:max([0,
249 vector_dot_product(Hit_normal,
250 vector_normalize(
251 vector_sub(Light_location,
252 Hit_location)))])).
254 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
255 Specular_power, Shininess, Specular_colour) ->
256 vector_scalar_mult(
257 colour_to_vector(Specular_colour),
258 Shininess*math:pow(
259 lists:max([0,
260 vector_dot_product(
261 vector_normalize(
262 vector_add(
263 vector_normalize(
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, []) ->
273 none;
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,
279 NearestObj,
280 Hit_location,
281 Normal,
282 Distance,
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", []),
289 New_hit_location =
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(
295 Ray,
296 CurrentObject,
297 New_hit_location,
298 New_normal,
299 NewDistance,
300 Rest_of_scene);
301 true ->
302 %io:format("no closer obj found~n", []),
303 nearest_object_intersecting_ray(Ray,
304 NearestObj,
305 Hit_location,
306 Normal,
307 Distance,
308 Rest_of_scene)
309 end.
311 ray_object_intersect(Ray, Object) ->
312 case Object of
313 #sphere{} ->
314 ray_sphere_intersect(Ray, Object);
315 #triangle{} ->
316 ray_triangle_intersect(Ray, Object);
317 #plane{} ->
318 ray_plane_intersect(Ray, Object);
319 _Else ->
320 infinity
321 end.
323 object_normal_at_point(#sphere{center=Center}, Point) ->
324 vector_normalize(
325 vector_sub(Point, Center));
326 object_normal_at_point(#plane{normal=Normal}, _Point) ->
327 Normal.
329 ray_sphere_intersect(
330 #ray{origin=#vector{
331 x=X0, y=Y0, z=Z0},
332 direction=#vector{
333 x=Xd, y=Yd, z=Zd}},
334 #sphere{radius=Radius, center=#vector{
335 x=Xc, y=Yc, z=Zc}}) ->
336 Epsilon = 0.001,
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]),
348 lists:min([T0, T1]);
349 true ->
350 infinity
351 end;
352 true ->
353 infinity
354 end.
356 ray_triangle_intersect(_Ray, _Triangle) ->
357 infinity.
359 ray_plane_intersect(Ray, Plane) ->
360 Epsilon = 0.001,
361 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
362 if Vd < 0 ->
363 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
364 + Plane#plane.distance),
365 Distance = V0 / Vd,
366 if Distance < Epsilon ->
367 infinity;
368 true ->
369 Distance
370 end;
371 true ->
372 infinity
373 end.
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,
385 [vector_scalar_mult(
386 #vector{x=0, y=0, z=1},
387 focal_length(
388 Camera#camera.fov,
389 Screen_width)),
390 #vector{x = (X-0.5) * Screen_width,
391 y=0,
392 z=0},
393 #vector{x=0,
394 y= (Y-0.5) * Screen_height,
395 z=0}
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}) ->
428 X*X + Y*Y + Z*Z.
430 vector_mag(V) ->
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,
444 y = A3*B1 - A1*B3,
445 z = A1*B2 - A2*B1}.
447 vector_normalize(V) ->
448 Mag = vector_mag(V),
449 if Mag == 0 ->
450 #vector{x=0, y=0, z=0};
451 true ->
452 vector_scalar_mult(V, 1/vector_mag(V))
453 end.
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) ->
459 vector_add(
460 vector_scalar_mult(
461 Normal,
462 2*vector_dot_product(Normal, vector_neg(Vector))),
463 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}) ->
486 Epsilon = 0.001,
487 Epsilon > abs(
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}) ->
495 {R, G, B}.
497 % returns a list of objects in the scene
498 % camera is assumed to be the first element in the scene
499 scene() ->
500 [#camera{location=#vector{x=0, y=0, z=-2},
501 rotation=#vector{x=0, y=0, z=0},
502 fov=90,
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}},
510 #sphere{radius=4,
511 center=#vector{x=4, y=0, z=10},
512 material=#material{
513 colour=#colour{r=0, g=0.5, b=1},
514 specular_power=20,
515 shininess=1,
516 reflectivity=0.1}},
517 #sphere{radius=4,
518 center=#vector{x=-5, y=3, z=9},
519 material=#material{
520 colour=#colour{r=1, g=0.5, b=0},
521 specular_power=4,
522 shininess=0.25,
523 reflectivity=0.5}},
524 #sphere{radius=4,
525 center=#vector{x=-4.5, y=-2.5, z=14},
526 material=#material{
527 colour=#colour{r=0.5, g=1, b=0},
528 specular_power=20,
529 shininess=0.25,
530 reflectivity=0.7}},
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},
534 material=#material{
535 colour=#colour{r=0.5, g=0, b=1},
536 specular_power=40,
537 shininess=1,
538 reflectivity=1}},
539 #plane{normal=#vector{x=0, y=-1, z=0},
540 distance=5,
541 material=#material{
542 colour=#colour{r=1, g=1, b=1},
543 specular_power=1,
544 shininess=0,
545 reflectivity=0.01}}
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
552 {ok, IoDevice} ->
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]),
557 lists:foreach(
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,
563 Pixels),
564 file:close(IoDevice);
565 error ->
566 io:format("error opening file~n", [])
567 end.
569 % various invocation style functions
570 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
571 standalone(list_to_integer(Width),
572 list_to_integer(Height),
573 Filename,
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(
579 raytracer,
580 raytrace,
581 [Width,
582 Height,
583 Filename,
584 Recursion_depth,
585 Function]),
586 io:format("Done in ~w seconds~n", [Time/1000000]),
587 halt().
589 go(Strategy) ->
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) ->
606 write_pixels_to_ppm(
607 Width,
608 Height,
609 255,
610 Function(
611 Width,
612 Height,
613 scene(),
614 Recursion_depth),
615 Filename).
617 % testing
618 run_tests() ->
619 Tests = [fun scene_test/0,
620 fun passing_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).
643 scene_test() ->
644 io:format("testing the scene function", []),
645 case scene() of
646 [{camera,
647 {vector, 0, 0, -2},
648 {vector, 0, 0, 0},
650 {screen, 4, 3}},
651 {point_light,
652 {colour, 1, 1, 0.5},
653 {vector, 5, -2, 0},
654 {colour, 1, 1, 1}},
655 {point_light,
656 {colour, 1, 0, 0.5},
657 {vector, -10, 0, 7},
658 {colour, 1, 0, 0.5}},
659 {sphere,
661 {vector, 4, 0, 10},
662 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
663 {sphere,
665 {vector, -5, 3, 9},
666 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
667 {sphere,
669 {vector, -4.5, -2.5, 14},
670 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
671 {triangle,
672 {vector, 2, 1.5, 0},
673 {vector, 2, 1.5, 10},
674 {vector, -2, 1.5, 0},
675 {material, {colour, 0.5, 0, 1}, 40, 1, 1}},
676 {plane,
677 {vector, 0, -1, 0},
679 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
680 ] ->
681 true;
682 _Else ->
683 false
684 end.
686 passing_test() ->
687 io:format("this test always passes", []),
688 true.
690 run_tests([], _Num, Success) ->
691 case Success of
692 true ->
693 io:format("Success!~n", []),
695 _Else ->
696 io:format("some tests failed~n", []),
697 failed
698 end;
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
704 true ->
705 io:format(" - OK~n", []);
706 _Else ->
707 io:format(" - FAILED~n", [])
708 end,
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.
780 vector_mag_test() ->
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)),
843 vector_add(
844 vector_cross_product(Vector7, Vector8),
845 vector_cross_product(Vector7, Vector9))),
846 Subtest6 = vectors_equal(Vector1,
847 vector_add(
848 vector_add(
849 vector_cross_product(
850 Vector7,
851 vector_cross_product(Vector8, Vector9)),
852 vector_cross_product(
853 Vector8,
854 vector_cross_product(Vector9, Vector7))),
855 vector_cross_product(
856 Vector9,
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,
892 Vector2),
894 Subtest1.
896 ray_sphere_intersection_test() ->
897 io:format("ray sphere intersection test", []),
899 Sphere = #sphere{
900 radius=3,
901 center=#vector{x = 0, y=0, z=10},
902 material=#material{
903 colour=#colour{r=0.4, g=0.4, b=0.4}}},
904 Ray1 = #ray{
905 origin=#vector{x=0, y=0, z=0},
906 direction=#vector{x=0, y=0, z=1}},
907 Ray2 = #ray{
908 origin=#vector{x=3, y=0, z=0},
909 direction=#vector{x=0, y=0, z=1}},
910 Ray3 = #ray{
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},
922 fov=90,
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},
926 fov=90,
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},
955 material=#material{
956 colour=#colour{r=0, g=0, b=0.03}}},
957 Sphere2=#sphere{radius=5,
958 center=#vector{x=0, y=0, z=20},
959 material=#material{
960 colour=#colour{r=0, g=0, b=0.06}}},
961 Sphere3=#sphere{radius=5,
962 center=#vector{x=0, y=0, z=30},
963 material=#material{
964 colour=#colour{r=0, g=0, b=0.09}}},
965 Sphere4=#sphere{radius=5,
966 center=#vector{x=0, y=0, z=-10},
967 material=#material{
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(
974 Ray1, Scene1),
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),
979 Subtest1.
981 focal_length_test() ->
982 Epsilon = 0.1,
983 Size = 36,
984 io:format("focal length test", []),
985 lists:foldl(
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]),
989 Matches
990 and ((Focal_length + Epsilon >= focal_length(
991 Dimension, Size))
992 and (Focal_length - Epsilon =< focal_length(
993 Dimension, Size)))
994 end, true,
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},
1001 material=#material{
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
1034 and Subtest7.
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(
1044 Vector1,
1045 vector_normalize(Vector2)),
1046 Vector3),
1048 Subtest2 = vectors_equal(
1049 vector_bounce_off_plane(
1050 Vector2,
1051 vector_normalize(Vector1)),
1052 Vector4),
1054 Subtest1 and Subtest2.