implemented all the possible invocation strategies I might want
[eraytracer.git] / raytracer.erl
blob630480a26bec54d0f81d09c5937b6fb42fee93c7
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 %% * two object types:
23 %% * spheres
24 %% * triangles (not done)
25 %% * point lights
26 %% * shadows (not done)
27 %% * lighting based on local illumination models
28 %% * ambient (not done)
29 %% * diffuse
30 %% * specular
31 %% * attenuation (not done)
32 %% * reflections to a fixed depth
33 %% * PPM output file format
34 %% * randomly generated scene (not done)
35 %% * useful test suite (working but not very friendly when fails)
36 %% * concurrent (utilizes all CPUs in a single computer) (not done)
37 %% * distributed (across multiple computers) (not done)
41 -module(raytracer).
42 -export([go/1,
43 go/5,
44 go_simple/0,
45 go_concurrent/0,
46 go_simple/4,
47 go_concurrent/4,
48 run_tests/0,
49 master/2,
50 worker/5,
51 standalone_concurrent/0,
52 standalone_simple/0,
53 standalone_concurrent/1,
54 standalone_simple/1,
55 raytraced_pixel_list_simple/4,
56 raytraced_pixel_list_concurrent/4
57 ]).
59 -record(vector, {x, y, z}).
60 -record(colour, {r, g, b}).
61 -record(ray, {origin, direction}).
62 -record(screen, {width, height}). % screen dimensions in the 3D world
63 -record(camera, {location, rotation, fov, screen}).
64 -record(material, {colour, specular_power, shininess, reflectivity}).
65 -record(sphere, {radius, center, material}).
66 -record(triangle, {v1, v2, v3, 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 master(Program_PID, Pixel_count) ->
109 master(Program_PID, Pixel_count, []).
110 master(Program_PID, 0, Pixel_list) ->
111 io:format("master is done~n", []),
112 Program_PID ! lists:keysort(1, Pixel_list);
113 % assumes all workers eventually return a good value
114 master(Program_PID, Pixel_count, Pixel_list) ->
115 receive
116 Pixel_tuple ->
117 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
118 end.
121 % assumes X and Y are percentages of the screen dimensions
122 worker(Master_PID, Pixel_num, {X, Y}, Scene, Recursion_depth) ->
123 Master_PID ! {Pixel_num,
124 colour_to_pixel(trace_ray_through_pixel({X, Y}, Scene, Recursion_depth))}.
126 trace_ray_through_pixel({X, Y}, [Camera|Rest_of_scene], Recursion_depth) ->
127 pixel_colour_from_ray(
128 ray_through_pixel(X, Y, Camera),
129 Rest_of_scene,
130 Recursion_depth).
132 pixel_colour_from_ray(_Ray, _Scene, 0) ->
133 #colour{r=0, g=0, b=0};
134 pixel_colour_from_ray(Ray, Scene, Recursion_depth) ->
135 case nearest_object_intersecting_ray(Ray, Scene) of
136 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
137 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
139 vector_to_colour(lighting_function(Ray,
140 Nearest_object,
141 Hit_location,
142 Hit_normal,
143 Scene,
144 Recursion_depth));
145 none ->
146 ?BACKGROUND_COLOUR;
147 _Else ->
148 ?ERROR_COLOUR
149 end.
151 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
152 Recursion_depth) ->
153 lists:foldl(
154 fun (#point_light{diffuse_colour=Light_colour,
155 location=Light_location,
156 specular_colour=Specular_colour},
157 Final_colour) ->
158 vector_add(
159 vector_scalar_mult(
160 colour_to_vector(
161 pixel_colour_from_ray(
162 #ray{origin=Hit_location,
163 direction=vector_bounce_off_plane(
164 Ray#ray.direction, Hit_normal)},
165 Scene,
166 Recursion_depth-1)),
167 object_reflectivity(Object)),
168 vector_add(
169 vector_component_mult(
170 colour_to_vector(Light_colour),
171 vector_add(
172 diffuse_term(Object,
173 Light_location,
174 Hit_location,
175 Hit_normal),
176 specular_term(Ray#ray.direction,
177 Light_location,
178 Hit_location,
179 Hit_normal,
180 object_specular_power(Object),
181 object_shininess(Object),
182 Specular_colour))),
183 Final_colour));
184 (_Not_a_point_light, Final_colour) ->
185 Final_colour
186 end,
187 #vector{x=0, y=0, z=0},
188 Scene).
190 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
191 vector_scalar_mult(
192 colour_to_vector(object_diffuse_colour(Object)),
193 lists:max([0,
194 vector_dot_product(Hit_normal,
195 vector_normalize(
196 vector_sub(Light_location,
197 Hit_location)))])).
199 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
200 Specular_power, Shininess, Specular_colour) ->
201 vector_scalar_mult(
202 colour_to_vector(Specular_colour),
203 Shininess*math:pow(
204 lists:max([0,
205 vector_dot_product(
206 vector_normalize(
207 vector_add(
208 vector_normalize(
209 vector_sub(Light_location, Hit_location)),
210 vector_neg(EyeVector))),
211 Hit_normal)]), Specular_power)).
213 nearest_object_intersecting_ray(Ray, Scene) ->
214 nearest_object_intersecting_ray(
215 Ray, none, hitlocation, hitnormal, infinity, Scene).
216 nearest_object_intersecting_ray(
217 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
218 none;
219 nearest_object_intersecting_ray(
220 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
221 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
222 {NearestObj, Distance, Hit_location, Normal};
223 nearest_object_intersecting_ray(Ray,
224 NearestObj,
225 Hit_location,
226 Normal,
227 Distance,
228 [CurrentObject|Rest_of_scene]) ->
229 NewDistance = ray_object_intersect(Ray, CurrentObject),
230 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
231 if (NewDistance /= infinity)
232 and ((Distance == infinity) or (Distance > NewDistance)) ->
233 %io:format("another closer object found~n", []),
234 New_hit_location =
235 vector_add(Ray#ray.origin,
236 vector_scalar_mult(Ray#ray.direction, NewDistance)),
237 New_normal = object_normal_at_point(
238 CurrentObject, New_hit_location),
239 nearest_object_intersecting_ray(
240 Ray,
241 CurrentObject,
242 New_hit_location,
243 New_normal,
244 NewDistance,
245 Rest_of_scene);
246 true ->
247 %io:format("no closer obj found~n", []),
248 nearest_object_intersecting_ray(Ray,
249 NearestObj,
250 Hit_location,
251 Normal,
252 Distance,
253 Rest_of_scene)
254 end.
256 ray_object_intersect(Ray, Object) ->
257 case Object of
258 #sphere{} ->
259 ray_sphere_intersect(Ray, Object);
260 #triangle{} ->
261 ray_triangle_intersect(Ray, Object);
262 _Else ->
263 infinity
264 end.
266 object_normal_at_point(#sphere{center=Center}, Point) ->
267 vector_normalize(
268 vector_sub(Point, Center)).
270 ray_sphere_intersect(
271 #ray{origin=#vector{
272 x=X0, y=Y0, z=Z0},
273 direction=#vector{
274 x=Xd, y=Yd, z=Zd}},
275 #sphere{radius=Radius, center=#vector{
276 x=Xc, y=Yc, z=Zc}}) ->
277 A = Xd*Xd + Yd*Yd + Zd*Zd,
278 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
279 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
280 Discriminant = B*B - 4*A*C,
281 %io:format("A=~w B=~w C=~w discriminant=~w~n",
282 % [A, B, C, Discriminant]),
283 if Discriminant >= 0 ->
284 T0 = (-B + math:sqrt(Discriminant))/2,
285 T1 = (-B - math:sqrt(Discriminant))/2,
286 if (T0 >= 0) and (T1 >= 0) ->
287 %io:format("T0=~w T1=~w~n", [T0, T1]),
288 lists:min([T0, T1]);
289 true ->
290 infinity
291 end;
292 true ->
293 infinity
294 end.
296 ray_triangle_intersect(_Ray, _Triangle) ->
297 infinity.
299 focal_length(Angle, Dimension) ->
300 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
302 point_on_screen(X, Y, Camera) ->
303 %TODO: implement rotation (using quaternions)
304 Screen_width = (Camera#camera.screen)#screen.width,
305 Screen_height = (Camera#camera.screen)#screen.height,
306 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
307 Camera#camera.location,
308 [vector_scalar_mult(
309 #vector{x=0, y=0, z=1},
310 focal_length(
311 Camera#camera.fov,
312 Screen_width)),
313 #vector{x = (X-0.5) * Screen_width,
314 y=0,
315 z=0},
316 #vector{x=0,
317 y= (Y-0.5) * Screen_height,
318 z=0}
322 shoot_ray(From, Through) ->
323 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
325 % assume that X and Y are percentages of the 3D world screen dimensions
326 ray_through_pixel(X, Y, Camera) ->
327 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
329 vectors_equal(V1, V2) ->
330 vectors_equal(V1, V2, 0.0001).
331 vectors_equal(V1, V2, Epsilon) ->
332 (V1#vector.x + Epsilon >= V2#vector.x)
333 and (V1#vector.x - Epsilon =<V2#vector.x)
334 and (V1#vector.y + Epsilon >= V2#vector.y)
335 and (V1#vector.y - Epsilon =<V2#vector.y)
336 and (V1#vector.z + Epsilon >= V2#vector.z)
337 and (V1#vector.z - Epsilon =<V2#vector.z).
340 vector_add(V1, V2) ->
341 #vector{x = V1#vector.x + V2#vector.x,
342 y = V1#vector.y + V2#vector.y,
343 z = V1#vector.z + V2#vector.z}.
345 vector_sub(V1, V2) ->
346 #vector{x = V1#vector.x - V2#vector.x,
347 y = V1#vector.y - V2#vector.y,
348 z = V1#vector.z - V2#vector.z}.
350 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
351 X*X + Y*Y + Z*Z.
353 vector_mag(V) ->
354 math:sqrt(vector_square_mag(V)).
356 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
357 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
359 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
360 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
362 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
363 A1*B1 + A2*B2 + A3*B3.
365 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
366 #vector{x = A2*B3 - A3*B2,
367 y = A3*B1 - A1*B3,
368 z = A1*B2 - A2*B1}.
370 vector_normalize(V) ->
371 Mag = vector_mag(V),
372 if Mag == 0 ->
373 #vector{x=0, y=0, z=0};
374 true ->
375 vector_scalar_mult(V, 1/vector_mag(V))
376 end.
378 vector_neg(#vector{x=X, y=Y, z=Z}) ->
379 #vector{x=-X, y=-Y, z=-Z}.
381 vector_bounce_off_plane(Vector, Normal) ->
382 vector_add(
383 vector_scalar_mult(
384 Normal,
385 2*vector_dot_product(Normal, vector_neg(Vector))),
386 Vector).
388 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
390 object_diffuse_colour(_Unknown) ->
391 ?UNKNOWN_COLOUR.
392 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
394 object_shininess(#sphere{material=#material{shininess=S}}) ->
396 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
399 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
400 #vector{x=X, y=Y, z=Z}) ->
401 Epsilon = 0.001,
402 Epsilon > abs(
403 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
405 colour_to_vector(#colour{r=R, g=G, b=B}) ->
406 #vector{x=R, y=G, z=B}.
407 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
408 #colour{r=X, g=Y, b=Z}.
409 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
410 {R, G, B}.
412 % returns a list of objects in the scene
413 % camera is assumed to be the first element in the scene
414 scene() ->
415 [#camera{location=#vector{x=0, y=0, z=0},
416 rotation=#vector{x=0, y=0, z=0},
417 fov=90,
418 screen=#screen{width=4, height=3}},
419 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
420 location=#vector{x=5, y=-2, z=0},
421 specular_colour=#colour{r=1, g=1, b=1}},
422 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
423 location=#vector{x=-10, y=0, z=7},
424 specular_colour=#colour{r=1, g=0, b=0.5}},
425 #sphere{radius=4,
426 center=#vector{x=4, y=0, z=10},
427 material=#material{
428 colour=#colour{r=0, g=0.5, b=1},
429 specular_power=20,
430 shininess=1,
431 reflectivity=0.1}},
432 #sphere{radius=4,
433 center=#vector{x=-5, y=3, z=9},
434 material=#material{
435 colour=#colour{r=1, g=0.5, b=0},
436 specular_power=4,
437 shininess=0.25,
438 reflectivity=0.5}},
439 #sphere{radius=4,
440 center=#vector{x=-5, y=-2, z=10},
441 material=#material{
442 colour=#colour{r=0.5, g=1, b=0},
443 specular_power=20,
444 shininess=0.25,
445 reflectivity=0.7}},
446 #triangle{v1=#vector{x=2, y=1.5, z=0},
447 v2=#vector{x=2, y=1.5, z=10},
448 v3=#vector{x=-2, y=1.5, z=0},
449 material=#material{
450 colour=#colour{r=0.5, g=0, b=1},
451 specular_power=40,
452 shininess=1,
453 reflectivity=1}}
457 % assumes Pixels are ordered in a row by row fasion
458 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
459 case file:open(Filename, write) of
460 {ok, IoDevice} ->
461 io:format("file opened~n", []),
462 io:format(IoDevice, "P3~n", []),
463 io:format(IoDevice, "~p ~p~n", [Width, Height]),
464 io:format(IoDevice, "~p~n", [MaxValue]),
465 lists:foreach(
466 fun({_Num, {R, G, B}}) ->
467 io:format(IoDevice, "~p ~p ~p ",
468 [lists:min([trunc(R*MaxValue), MaxValue]),
469 lists:min([trunc(G*MaxValue), MaxValue]),
470 lists:min([trunc(B*MaxValue), MaxValue])]) end,
471 Pixels),
472 file:close(IoDevice);
473 error ->
474 io:format("error opening file~n", [])
475 end.
477 % various invocation style functions
478 standalone_simple() ->
479 standalone(fun raytraced_pixel_list_simple/4).
481 standalone_simple([Width, Height, Filename, Recursion_depth]) ->
482 standalone(list_to_integer(Width),
483 list_to_integer(Height),
484 Filename,
485 list_to_integer(Recursion_depth),
486 fun raytraced_pixel_list_simple/4).
488 standalone_concurrent() ->
489 standalone(fun raytraced_pixel_list_concurrent/4).
491 standalone_concurrent([Width, Height, Filename, Recursion_depth]) ->
492 standalone(list_to_integer(Width),
493 list_to_integer(Height),
494 Filename,
495 list_to_integer(Recursion_depth),
496 fun raytraced_pixel_list_concurrent/4).
498 go_simple() ->
499 go(fun raytraced_pixel_list_simple/4).
501 go_simple(Width, Height, Filename, Recursion_depth) ->
502 go(Width, Height, Filename, Recursion_depth,
503 fun raytraced_pixel_list_simple/4).
505 go_concurrent() ->
506 go(fun raytraced_pixel_list_concurrent/4).
508 go_concurrent(Width, Height, Filename, Recursion_depth) ->
509 go(Width, Height, Filename, Recursion_depth,
510 fun raytraced_pixel_list_concurrent/4).
512 standalone(Function) ->
513 {Time, _Value} = timer:tc(
514 raytracer,
516 [Function]),
517 io:format("Done in ~w seconds~n", [Time/1000000]),
518 halt().
520 standalone(Width, Height, Filename, Recursion_depth, Function) ->
521 {Time, _Value} = timer:tc(
522 raytracer,
524 [Width,
525 Height,
526 Filename,
527 Recursion_depth,
528 Function]),
529 io:format("Done in ~w seconds~n", [Time/1000000]),
530 halt().
532 go(Function) ->
533 go(4, 3, "/tmp/traced.ppm", 5, Function).
534 go(Width, Height, Filename, Recursion_depth, Function) ->
535 write_pixels_to_ppm(
536 Width,
537 Height,
538 255,
539 Function(
540 Width,
541 Height,
542 scene(),
543 Recursion_depth),
544 Filename).
546 % testing
547 run_tests() ->
548 Tests = [fun scene_test/0,
549 fun passing_test/0,
550 fun vector_equality_test/0,
551 fun vector_addition_test/0,
552 fun vector_subtraction_test/0,
553 fun vector_square_mag_test/0,
554 fun vector_mag_test/0,
555 fun vector_scalar_multiplication_test/0,
556 fun vector_dot_product_test/0,
557 fun vector_cross_product_test/0,
558 fun vector_normalization_test/0,
559 fun vector_negation_test/0,
560 % fun ray_through_pixel_test/0,
561 fun ray_shooting_test/0,
562 fun point_on_screen_test/0,
563 fun nearest_object_intersecting_ray_test/0,
564 fun focal_length_test/0,
565 % fun vector_rotation_test/0,
566 fun object_normal_at_point_test/0,
567 fun vector_bounce_off_plane_test/0,
568 fun ray_sphere_intersection_test/0
570 run_tests(Tests, 1, true).
572 scene_test() ->
573 io:format("testing the scene function", []),
574 case scene() of
575 [{camera,
576 {vector, 0, 0, 0},
577 {vector, 0, 0, 0},
579 {screen, 4, 3}},
580 {point_light,
581 {colour, 1, 1, 0.5},
582 {vector, 5, -2, 0},
583 {colour, 1, 1, 1}},
584 {point_light,
585 {colour, 1, 0, 0.5},
586 {vector, -10, 0, 7},
587 {colour, 1, 0, 0.5}},
588 {sphere,
590 {vector, 4, 0, 10},
591 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
592 {sphere,
594 {vector, -5, 3, 9},
595 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
596 {sphere,
598 {vector, -5, -2, 10},
599 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
600 {triangle,
601 {vector, 2, 1.5, 0},
602 {vector, 2, 1.5, 10},
603 {vector, -2, 1.5, 0},
604 {material, {colour, 0.5, 0, 1}, 40, 1, 1}}
605 ] ->
606 true;
607 _Else ->
608 false
609 end.
611 passing_test() ->
612 io:format("this test always passes", []),
613 true.
615 run_tests([], _Num, Success) ->
616 case Success of
617 true ->
618 io:format("Success!~n", []),
620 _Else ->
621 io:format("some tests failed~n", []),
622 failed
623 end;
625 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
626 io:format("test #~p: ", [Num]),
627 Current_success = First_test(),
628 case Current_success of
629 true ->
630 io:format(" - OK~n", []);
631 _Else ->
632 io:format(" - FAILED~n", [])
633 end,
634 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
636 vector_equality_test() ->
637 io:format("vector equality"),
638 Vector1 = #vector{x=0, y=0, z=0},
639 Vector2 = #vector{x=1234, y=-234, z=0},
640 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
641 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
642 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
643 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
645 Subtest1 = vectors_equal(Vector1, Vector1)
646 and vectors_equal(Vector2, Vector2)
647 and not (vectors_equal(Vector1, Vector2))
648 and not (vectors_equal(Vector2, Vector1)),
649 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
650 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
652 Subtest1 and Subtest2 and Subtest3.
655 vector_addition_test() ->
656 io:format("vector addition", []),
657 Vector0 = vector_add(
658 #vector{x=3, y=7, z=-3},
659 #vector{x=0, y=-24, z=123}),
660 Subtest1 = (Vector0#vector.x == 3)
661 and (Vector0#vector.y == -17)
662 and (Vector0#vector.z == 120),
664 Vector1 = #vector{x=5, y=0, z=984},
665 Vector2 = vector_add(Vector1, Vector1),
666 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
667 and (Vector2#vector.y == Vector1#vector.y*2)
668 and (Vector2#vector.z == Vector1#vector.z*2),
670 Vector3 = #vector{x=908, y=-098, z=234},
671 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
672 Subtest3 = vectors_equal(Vector3, Vector4),
674 Subtest1 and Subtest2 and Subtest3.
676 vector_subtraction_test() ->
677 io:format("vector subtraction", []),
678 Vector1 = #vector{x=0, y=0, z=0},
679 Vector2 = #vector{x=8390, y=-2098, z=939},
680 Vector3 = #vector{x=1, y=1, z=1},
681 Vector4 = #vector{x=-1, y=-1, z=-1},
683 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
684 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
685 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
686 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
687 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
688 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
689 vector_sub(Vector2, Vector3)),
691 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
693 vector_square_mag_test() ->
694 io:format("vector square magnitude test", []),
695 Vector1 = #vector{x=0, y=0, z=0},
696 Vector2 = #vector{x=1, y=1, z=1},
697 Vector3 = #vector{x=3, y=-4, z=0},
699 Subtest1 = (0 == vector_square_mag(Vector1)),
700 Subtest2 = (3 == vector_square_mag(Vector2)),
701 Subtest3 = (25 == vector_square_mag(Vector3)),
703 Subtest1 and Subtest2 and Subtest3.
705 vector_mag_test() ->
706 io:format("vector magnitude test", []),
707 Vector1 = #vector{x=0, y=0, z=0},
708 Vector2 = #vector{x=1, y=1, z=1},
709 Vector3 = #vector{x=3, y=-4, z=0},
711 Subtest1 = (0 == vector_mag(Vector1)),
712 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
713 Subtest3 = (5 == vector_mag(Vector3)),
715 Subtest1 and Subtest2 and Subtest3.
717 vector_scalar_multiplication_test() ->
718 io:format("scalar multiplication test", []),
719 Vector1 = #vector{x=0, y=0, z=0},
720 Vector2 = #vector{x=1, y=1, z=1},
721 Vector3 = #vector{x=3, y=-4, z=0},
723 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
724 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
725 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
726 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
727 vector_scalar_mult(Vector2, 4)),
728 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
729 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
731 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
733 vector_dot_product_test() ->
734 io:format("dot product test", []),
735 Vector1 = #vector{x=1, y=3, z=-5},
736 Vector2 = #vector{x=4, y=-2, z=-1},
737 Vector3 = #vector{x=0, y=0, z=0},
738 Vector4 = #vector{x=1, y=0, z=0},
739 Vector5 = #vector{x=0, y=1, z=0},
741 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
742 Subtest2 = vector_dot_product(Vector2, Vector2)
743 == vector_square_mag(Vector2),
744 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
745 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
747 Subtest1 and Subtest2 and Subtest3 and Subtest4.
749 vector_cross_product_test() ->
750 io:format("cross product test", []),
751 Vector1 = #vector{x=0, y=0, z=0},
752 Vector2 = #vector{x=1, y=0, z=0},
753 Vector3 = #vector{x=0, y=1, z=0},
754 Vector4 = #vector{x=0, y=0, z=1},
755 Vector5 = #vector{x=1, y=2, z=3},
756 Vector6 = #vector{x=4, y=5, z=6},
757 Vector7 = #vector{x=-3, y=6, z=-3},
758 Vector8 = #vector{x=-1, y=0, z=0},
759 Vector9 = #vector{x=-9, y=8, z=433},
761 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
762 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
763 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
764 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
765 Subtest5 = vectors_equal(
766 vector_cross_product(Vector7,
767 vector_add(Vector8, Vector9)),
768 vector_add(
769 vector_cross_product(Vector7, Vector8),
770 vector_cross_product(Vector7, Vector9))),
771 Subtest6 = vectors_equal(Vector1,
772 vector_add(
773 vector_add(
774 vector_cross_product(
775 Vector7,
776 vector_cross_product(Vector8, Vector9)),
777 vector_cross_product(
778 Vector8,
779 vector_cross_product(Vector9, Vector7))),
780 vector_cross_product(
781 Vector9,
782 vector_cross_product(Vector7, Vector8)))),
784 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
786 vector_normalization_test() ->
787 io:format("normalization test", []),
788 Vector1 = #vector{x=0, y=0, z=0},
789 Vector2 = #vector{x=1, y=0, z=0},
790 Vector3 = #vector{x=5, y=0, z=0},
792 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
793 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
794 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
795 Subtest4 = vectors_equal(Vector2, vector_normalize(
796 vector_scalar_mult(Vector2, 324))),
798 Subtest1 and Subtest2 and Subtest3 and Subtest4.
800 vector_negation_test() ->
801 io:format("vector negation test", []),
802 Vector1 = #vector{x=0, y=0, z=0},
803 Vector2 = #vector{x=4, y=-5, z=6},
805 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
806 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
808 Subtest1 and Subtest2.
810 ray_shooting_test() ->
811 io:format("ray shooting test"),
812 Vector1 = #vector{x=0, y=0, z=0},
813 Vector2 = #vector{x=1, y=0, z=0},
815 Subtest1 = vectors_equal(
816 (shoot_ray(Vector1, Vector2))#ray.direction,
817 Vector2),
819 Subtest1.
821 ray_sphere_intersection_test() ->
822 io:format("ray sphere intersection test", []),
824 Sphere = #sphere{
825 radius=3,
826 center=#vector{x = 0, y=0, z=10},
827 material=#material{
828 colour=#colour{r=0.4, g=0.4, b=0.4}}},
829 Ray1 = #ray{
830 origin=#vector{x=0, y=0, z=0},
831 direction=#vector{x=0, y=0, z=1}},
832 Ray2 = #ray{
833 origin=#vector{x=3, y=0, z=0},
834 direction=#vector{x=0, y=0, z=1}},
835 Ray3 = #ray{
836 origin=#vector{x=4, y=0, z=0},
837 direction=#vector{x=0, y=0, z=1}},
838 Subtest1 = ray_sphere_intersect(Ray1, Sphere) == 7.0,
839 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == 10.0,
840 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == infinity,
841 Subtest1 and Subtest2 and Subtest3.
843 point_on_screen_test() ->
844 io:format("point on screen test", []),
845 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
846 rotation=#vector{x=0, y=0, z=0},
847 fov=90,
848 screen=#screen{width=1, height=1}},
849 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
850 rotation=#vector{x=0, y=0, z=0},
851 fov=90,
852 screen=#screen{width=640, height=480}},
854 Subtest1 = vectors_equal(
855 #vector{x=0, y=0, z=0.5},
856 point_on_screen(0.5, 0.5, Camera1)),
857 Subtest2 = vectors_equal(
858 #vector{x=-0.5, y=-0.5, z=0.5},
859 point_on_screen(0, 0, Camera1)),
860 Subtest3 = vectors_equal(
861 #vector{x=0.5, y=0.5, z=0.5},
862 point_on_screen(1, 1, Camera1)),
863 Subtest4 = vectors_equal(
864 point_on_screen(0, 0, Camera2),
865 #vector{x=-320, y=-240, z=320}),
866 Subtest5 = vectors_equal(
867 point_on_screen(1, 1, Camera2),
868 #vector{x=320, y=240, z=320}),
869 Subtest6 = vectors_equal(
870 point_on_screen(0.5, 0.5, Camera2),
871 #vector{x=0, y=0, z=320}),
873 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
875 nearest_object_intersecting_ray_test() ->
876 io:format("nearest object intersecting ray test", []),
877 % test to make sure that we really get the closest object
878 Sphere1=#sphere{radius=5,
879 center=#vector{x=0, y=0, z=10},
880 material=#material{
881 colour=#colour{r=0, g=0, b=0.03}}},
882 Sphere2=#sphere{radius=5,
883 center=#vector{x=0, y=0, z=20},
884 material=#material{
885 colour=#colour{r=0, g=0, b=0.06}}},
886 Sphere3=#sphere{radius=5,
887 center=#vector{x=0, y=0, z=30},
888 material=#material{
889 colour=#colour{r=0, g=0, b=0.09}}},
890 Sphere4=#sphere{radius=5,
891 center=#vector{x=0, y=0, z=-10},
892 material=#material{
893 colour=#colour{r=0, g=0, b=-0.4}}},
894 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
895 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
896 direction=#vector{x=0, y=0, z=1}},
898 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
899 Ray1, Scene1),
900 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
901 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
902 and point_on_sphere(Sphere1, Hit_location),
904 Subtest1.
906 focal_length_test() ->
907 Epsilon = 0.1,
908 Size = 36,
909 io:format("focal length test", []),
910 lists:foldl(
911 fun({Focal_length, Dimension}, Matches) ->
912 %Result = focal_length(Dimension, Size),
913 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
914 Matches
915 and ((Focal_length + Epsilon >= focal_length(
916 Dimension, Size))
917 and (Focal_length - Epsilon =< focal_length(
918 Dimension, Size)))
919 end, true,
920 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
922 object_normal_at_point_test() ->
923 io:format("object normal at point test"),
924 Sphere1 = #sphere{radius=13.5,
925 center=#vector{x=0, y=0, z=0},
926 material=#material{
927 colour=#colour{r=0, g=0, b=0}}},
928 Point1 = #vector{x=13.5, y=0, z=0},
929 Point2 = #vector{x=0, y=13.5, z=0},
930 Point3 = #vector{x=0, y=0, z=13.5},
931 Point4 = vector_neg(Point1),
932 Point5 = vector_neg(Point2),
933 Point6 = vector_neg(Point3),
935 % sphere object tests
936 Subtest1 = vectors_equal(
937 vector_normalize(Point1),
938 object_normal_at_point(Sphere1, Point1)),
939 Subtest2 = vectors_equal(
940 vector_normalize(Point2),
941 object_normal_at_point(Sphere1, Point2)),
942 Subtest3 = vectors_equal(
943 vector_normalize(Point3),
944 object_normal_at_point(Sphere1, Point3)),
945 Subtest4 = vectors_equal(
946 vector_normalize(Point4),
947 object_normal_at_point(Sphere1, Point4)),
948 Subtest5 = vectors_equal(
949 vector_normalize(Point5),
950 object_normal_at_point(Sphere1, Point5)),
951 Subtest6 = vectors_equal(
952 vector_normalize(Point6),
953 object_normal_at_point(Sphere1, Point6)),
954 Subtest7 = not vectors_equal(
955 vector_normalize(Point1),
956 object_normal_at_point(Sphere1, Point4)),
958 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
959 and Subtest7.
961 vector_bounce_off_plane_test() ->
962 io:format("vector reflect about normal", []),
963 Vector1 = #vector{x=1, y=1, z=0},
964 Vector2 = #vector{x=0, y=-1, z=0},
965 Vector3 = #vector{x=1, y=-1, z=0},
966 Vector4 = #vector{x=1, y=0, z=0},
968 Subtest1 = vectors_equal(vector_bounce_off_plane(
969 Vector1,
970 vector_normalize(Vector2)),
971 Vector3),
973 Subtest2 = vectors_equal(
974 vector_bounce_off_plane(
975 Vector2,
976 vector_normalize(Vector1)),
977 Vector4),
979 Subtest1 and Subtest2.