added a function for standalone execution
[eraytracer.git] / raytracer.erl
blobd97e185c04f6d0ca6cf4f55de23fe5c28c102315
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/0, go/4, run_tests/0, master/2, trace_ray_from_pixel/5, standalone_go/0]).
44 -record(vector, {x, y, z}).
45 -record(colour, {r, g, b}).
46 -record(ray, {origin, direction}).
47 -record(screen, {width, height}). % screen dimensions in the 3D world
48 -record(camera, {location, rotation, fov, screen}).
49 -record(material, {colour, specular_power, shininess, reflectivity}).
50 -record(sphere, {radius, center, material}).
51 -record(triangle, {v1, v2, v3, material}).
52 -record(point_light, {diffuse_colour, location, specular_colour}).
53 -define(BACKGROUND_COLOUR, #colour{r=0, g=0, b=0}).
54 -define(ERROR_COLOUR, #colour{r=1, g=0, b=0}).
55 -define(UNKNOWN_COLOUR, #colour{r=0, g=1, b=0}).
56 -define(FOG_DISTANCE, 40).
59 raytraced_pixel_list(0, 0, _, _) ->
60 done;
61 raytraced_pixel_list(Width, Height, Scene, Recursion_depth)
62 when Width > 0, Height > 0 ->
63 Master_PID = spawn(raytracer, master, [self(), Width*Height]),
64 lists:flatmap(
65 fun(Y) ->
66 lists:map(
67 fun(X) ->
68 % coordinates passed as a percentage
69 spawn(raytracer, trace_ray_from_pixel,
70 [Master_PID, X+Y*Width, {X/Width, Y/Height}, Scene, Recursion_depth]) end,
71 lists:seq(0, Width - 1)) end,
72 lists:seq(0, Height - 1)),
73 io:format("all workers have been spawned~n", []),
74 receive
75 Final_pixel_list ->
76 Final_pixel_list
77 end.
79 master(Program_PID, Pixel_count) ->
80 master(Program_PID, Pixel_count, []).
81 master(Program_PID, 0, Pixel_list) ->
82 io:format("master is done~n", []),
83 Program_PID ! lists:keysort(1, Pixel_list);
84 master(Program_PID, Pixel_count, Pixel_list) ->
85 receive
86 Pixel_tuple ->
87 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
88 end.
91 % assumes X and Y are percentages of the screen dimensions
92 trace_ray_from_pixel(Master_PID, Pixel_num, {X, Y}, [Camera|Rest_of_scene], Recursion_depth) ->
93 Master_PID ! {Pixel_num, colour_to_pixel(pixel_colour_from_ray(ray_through_pixel(X, Y, Camera), Rest_of_scene, Recursion_depth))}.
95 pixel_colour_from_ray(_Ray, _Scene, 0) ->
96 #colour{r=0, g=0, b=0};
97 pixel_colour_from_ray(Ray, Scene, Recursion_depth) ->
98 case nearest_object_intersecting_ray(Ray, Scene) of
99 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
100 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
102 vector_to_colour(lighting_function(Ray,
103 Nearest_object,
104 Hit_location,
105 Hit_normal,
106 Scene,
107 Recursion_depth));
108 none ->
109 ?BACKGROUND_COLOUR;
110 _Else ->
111 ?ERROR_COLOUR
112 end.
114 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
115 Recursion_depth) ->
116 lists:foldl(
117 fun (#point_light{diffuse_colour=Light_colour,
118 location=Light_location,
119 specular_colour=Specular_colour},
120 Final_colour) ->
121 vector_add(
122 vector_scalar_mult(
123 colour_to_vector(
124 pixel_colour_from_ray(
125 #ray{origin=Hit_location,
126 direction=vector_bounce_off_plane(
127 Ray#ray.direction, Hit_normal)},
128 Scene,
129 Recursion_depth-1)),
130 object_reflectivity(Object)),
131 vector_add(
132 vector_component_mult(
133 colour_to_vector(Light_colour),
134 vector_add(
135 diffuse_term(Object,
136 Light_location,
137 Hit_location,
138 Hit_normal),
139 specular_term(Ray#ray.direction,
140 Light_location,
141 Hit_location,
142 Hit_normal,
143 object_specular_power(Object),
144 object_shininess(Object),
145 Specular_colour))),
146 Final_colour));
147 (_Not_a_point_light, Final_colour) ->
148 Final_colour
149 end,
150 #vector{x=0, y=0, z=0},
151 Scene).
153 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
154 vector_scalar_mult(
155 colour_to_vector(object_diffuse_colour(Object)),
156 lists:max([0,
157 vector_dot_product(Hit_normal,
158 vector_normalize(
159 vector_sub(Light_location,
160 Hit_location)))])).
162 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
163 Specular_power, Shininess, Specular_colour) ->
164 vector_scalar_mult(
165 colour_to_vector(Specular_colour),
166 Shininess*math:pow(
167 lists:max([0,
168 vector_dot_product(
169 vector_normalize(
170 vector_add(
171 vector_normalize(
172 vector_sub(Light_location, Hit_location)),
173 vector_neg(EyeVector))),
174 Hit_normal)]), Specular_power)).
176 nearest_object_intersecting_ray(Ray, Scene) ->
177 nearest_object_intersecting_ray(
178 Ray, none, hitlocation, hitnormal, infinity, Scene).
179 nearest_object_intersecting_ray(
180 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
181 none;
182 nearest_object_intersecting_ray(
183 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
184 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
185 {NearestObj, Distance, Hit_location, Normal};
186 nearest_object_intersecting_ray(Ray,
187 NearestObj,
188 Hit_location,
189 Normal,
190 Distance,
191 [CurrentObject|Rest_of_scene]) ->
192 NewDistance = ray_object_intersect(Ray, CurrentObject),
193 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
194 if (NewDistance /= infinity)
195 and ((Distance == infinity) or (Distance > NewDistance)) ->
196 %io:format("another closer object found~n", []),
197 New_hit_location =
198 vector_add(Ray#ray.origin,
199 vector_scalar_mult(Ray#ray.direction, NewDistance)),
200 New_normal = object_normal_at_point(
201 CurrentObject, New_hit_location),
202 nearest_object_intersecting_ray(
203 Ray,
204 CurrentObject,
205 New_hit_location,
206 New_normal,
207 NewDistance,
208 Rest_of_scene);
209 true ->
210 %io:format("no closer obj found~n", []),
211 nearest_object_intersecting_ray(Ray,
212 NearestObj,
213 Hit_location,
214 Normal,
215 Distance,
216 Rest_of_scene)
217 end.
219 ray_object_intersect(Ray, Object) ->
220 case Object of
221 #sphere{} ->
222 ray_sphere_intersect(Ray, Object);
223 #triangle{} ->
224 ray_triangle_intersect(Ray, Object);
225 _Else ->
226 infinity
227 end.
229 object_normal_at_point(#sphere{center=Center}, Point) ->
230 vector_normalize(
231 vector_sub(Point, Center)).
233 ray_sphere_intersect(
234 #ray{origin=#vector{
235 x=X0, y=Y0, z=Z0},
236 direction=#vector{
237 x=Xd, y=Yd, z=Zd}},
238 #sphere{radius=Radius, center=#vector{
239 x=Xc, y=Yc, z=Zc}}) ->
240 Epsilon = 0.001,
241 A = Xd*Xd + Yd*Yd + Zd*Zd,
242 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
243 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
244 Discriminant = B*B - 4*A*C,
245 %io:format("A=~w B=~w C=~w discriminant=~w~n",
246 % [A, B, C, Discriminant]),
247 if Discriminant >= 0 ->
248 T0 = (-B + math:sqrt(Discriminant))/2,
249 T1 = (-B - math:sqrt(Discriminant))/2,
250 if (T0 >= 0) and (T1 >= 0) ->
251 %io:format("T0=~w T1=~w~n", [T0, T1]),
252 lists:min([T0, T1]);
253 true ->
254 infinity
255 end;
256 true ->
257 infinity
258 end.
260 ray_triangle_intersect(Ray, Triangle) ->
261 infinity.
263 focal_length(Angle, Dimension) ->
264 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
266 point_on_screen(X, Y, Camera) ->
267 %TODO: implement rotation (using quaternions)
268 Screen_width = (Camera#camera.screen)#screen.width,
269 Screen_height = (Camera#camera.screen)#screen.height,
270 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
271 Camera#camera.location,
272 [vector_scalar_mult(
273 #vector{x=0, y=0, z=1},
274 focal_length(
275 Camera#camera.fov,
276 Screen_width)),
277 #vector{x = (X-0.5) * Screen_width,
278 y=0,
279 z=0},
280 #vector{x=0,
281 y= (Y-0.5) * Screen_height,
282 z=0}
286 shoot_ray(From, Through) ->
287 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
289 % assume that X and Y are percentages of the 3D world screen dimensions
290 ray_through_pixel(X, Y, Camera) ->
291 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
293 vectors_equal(V1, V2) ->
294 vectors_equal(V1, V2, 0.0001).
295 vectors_equal(V1, V2, Epsilon) ->
296 (V1#vector.x + Epsilon >= V2#vector.x)
297 and (V1#vector.x - Epsilon =<V2#vector.x)
298 and (V1#vector.y + Epsilon >= V2#vector.y)
299 and (V1#vector.y - Epsilon =<V2#vector.y)
300 and (V1#vector.z + Epsilon >= V2#vector.z)
301 and (V1#vector.z - Epsilon =<V2#vector.z).
304 vector_add(V1, V2) ->
305 #vector{x = V1#vector.x + V2#vector.x,
306 y = V1#vector.y + V2#vector.y,
307 z = V1#vector.z + V2#vector.z}.
309 vector_sub(V1, V2) ->
310 #vector{x = V1#vector.x - V2#vector.x,
311 y = V1#vector.y - V2#vector.y,
312 z = V1#vector.z - V2#vector.z}.
314 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
315 X*X + Y*Y + Z*Z.
317 vector_mag(V) ->
318 math:sqrt(vector_square_mag(V)).
320 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
321 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
323 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
324 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
326 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
327 A1*B1 + A2*B2 + A3*B3.
329 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
330 #vector{x = A2*B3 - A3*B2,
331 y = A3*B1 - A1*B3,
332 z = A1*B2 - A2*B1}.
334 vector_normalize(V) ->
335 Mag = vector_mag(V),
336 if Mag == 0 ->
337 #vector{x=0, y=0, z=0};
338 true ->
339 vector_scalar_mult(V, 1/vector_mag(V))
340 end.
342 vector_neg(#vector{x=X, y=Y, z=Z}) ->
343 #vector{x=-X, y=-Y, z=-Z}.
345 vector_bounce_off_plane(Vector, Normal) ->
346 vector_add(
347 vector_scalar_mult(
348 Normal,
349 2*vector_dot_product(Normal, vector_neg(Vector))),
350 Vector).
352 vector_rotate(V1, _V2) ->
353 %TODO: implement using quaternions
356 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
358 object_diffuse_colour(_Unknown) ->
359 ?UNKNOWN_COLOUR.
360 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
362 object_shininess(#sphere{material=#material{shininess=S}}) ->
364 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
367 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
368 #vector{x=X, y=Y, z=Z}) ->
369 Epsilon = 0.001,
370 Epsilon > abs(
371 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
373 colour_to_vector(#colour{r=R, g=G, b=B}) ->
374 #vector{x=R, y=G, z=B}.
375 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
376 #colour{r=X, g=Y, b=Z}.
377 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
378 {R, G, B}.
380 % returns a list of objects in the scene
381 % camera is assumed to be the first element in the scene
382 scene() ->
383 [#camera{location=#vector{x=0, y=0, z=0},
384 rotation=#vector{x=0, y=0, z=0},
385 fov=90,
386 screen=#screen{width=4, height=3}},
387 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
388 location=#vector{x=5, y=-2, z=0},
389 specular_colour=#colour{r=1, g=1, b=1}},
390 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
391 location=#vector{x=-10, y=0, z=7},
392 specular_colour=#colour{r=1, g=0, b=0.5}},
393 #sphere{radius=4,
394 center=#vector{x=4, y=0, z=10},
395 material=#material{
396 colour=#colour{r=0, g=0.5, b=1},
397 specular_power=20,
398 shininess=1,
399 reflectivity=0.1}},
400 #sphere{radius=4,
401 center=#vector{x=-5, y=3, z=9},
402 material=#material{
403 colour=#colour{r=1, g=0.5, b=0},
404 specular_power=4,
405 shininess=0.25,
406 reflectivity=0.5}},
407 #sphere{radius=4,
408 center=#vector{x=-5, y=-2, z=10},
409 material=#material{
410 colour=#colour{r=0.5, g=1, b=0},
411 specular_power=20,
412 shininess=0.25,
413 reflectivity=0.7}},
414 #triangle{v1=#vector{x=2, y=1.5, z=0},
415 v2=#vector{x=2, y=1.5, z=10},
416 v3=#vector{x=-2, y=1.5, z=0},
417 material=#material{
418 colour=#colour{r=0.5, g=0, b=1},
419 specular_power=40,
420 shininess=1,
421 reflectivity=1}}
425 % assumes Pixels are ordered in a row by row fasion
426 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
427 case file:open(Filename, write) of
428 {ok, IoDevice} ->
429 io:format("file opened~n", []),
430 io:format(IoDevice, "P3~n", []),
431 io:format(IoDevice, "~p ~p~n", [Width, Height]),
432 io:format(IoDevice, "~p~n", [MaxValue]),
433 lists:foreach(
434 fun({_Num, {R, G, B}}) ->
435 io:format(IoDevice, "~p ~p ~p ",
436 [lists:min([trunc(R*MaxValue), MaxValue]),
437 lists:min([trunc(G*MaxValue), MaxValue]),
438 lists:min([trunc(B*MaxValue), MaxValue])]) end,
439 Pixels),
440 file:close(IoDevice),
441 io:format("done~n", []);
442 error ->
443 io:format("error opening file~n", [])
444 end.
446 go() ->
447 go(16, 12, "/tmp/traced.ppm", 3).
448 standalone_go() ->
449 go(640, 480, "/tmp/traced.ppm", 10),
450 halt().
451 go(Width, Height, Filename, Recursion_depth) ->
452 write_pixels_to_ppm(Width,
453 Height,
454 255,
455 raytraced_pixel_list(Width,
456 Height,
457 scene(),
458 Recursion_depth),
459 Filename).
461 % testing
462 run_tests() ->
463 Tests = [fun scene_test/0,
464 fun passing_test/0,
465 fun vector_equality_test/0,
466 fun vector_addition_test/0,
467 fun vector_subtraction_test/0,
468 fun vector_square_mag_test/0,
469 fun vector_mag_test/0,
470 fun vector_scalar_multiplication_test/0,
471 fun vector_dot_product_test/0,
472 fun vector_cross_product_test/0,
473 fun vector_normalization_test/0,
474 fun vector_negation_test/0,
475 % fun ray_through_pixel_test/0,
476 fun ray_shooting_test/0,
477 fun point_on_screen_test/0,
478 fun nearest_object_intersecting_ray_test/0,
479 fun focal_length_test/0,
480 % fun vector_rotation_test/0,
481 fun object_normal_at_point_test/0,
482 fun vector_bounce_off_plane_test/0,
483 fun ray_sphere_intersection_test/0
485 run_tests(Tests, 1, true).
487 scene_test() ->
488 io:format("testing the scene function", []),
489 case scene() of
490 [{camera,
491 {vector, 0, 0, 0},
492 {vector, 0, 0, 0},
494 {screen, 4, 3}},
495 {point_light,
496 {colour, 1, 1, 0.5},
497 {vector, 5, -2, 0},
498 {colour, 1, 1, 1}},
499 {point_light,
500 {colour, 1, 0, 0.5},
501 {vector, -10, 0, 7},
502 {colour, 1, 0, 0.5}},
503 {sphere,
505 {vector, 4, 0, 10},
506 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
507 {sphere,
509 {vector, -5, 3, 9},
510 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
511 {sphere,
513 {vector, -5, -2, 10},
514 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
515 {triangle,
516 {vector, 2, 1.5, 0},
517 {vector, 2, 1.5, 10},
518 {vector, -2, 1.5, 0},
519 {material, {colour, 0.5, 0, 1}, 40, 1, 1}}
520 ] ->
521 true;
522 _Else ->
523 false
524 end.
526 passing_test() ->
527 io:format("this test always passes", []),
528 true.
530 run_tests([], _Num, Success) ->
531 case Success of
532 true ->
533 io:format("Success!~n", []),
535 _Else ->
536 io:format("some tests failed~n", []),
537 failed
538 end;
540 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
541 io:format("test #~p: ", [Num]),
542 Current_success = First_test(),
543 case Current_success of
544 true ->
545 io:format(" - OK~n", []);
546 _Else ->
547 io:format(" - FAILED~n", [])
548 end,
549 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
551 vector_equality_test() ->
552 io:format("vector equality"),
553 Vector1 = #vector{x=0, y=0, z=0},
554 Vector2 = #vector{x=1234, y=-234, z=0},
555 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
556 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
557 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
558 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
560 Subtest1 = vectors_equal(Vector1, Vector1)
561 and vectors_equal(Vector2, Vector2)
562 and not (vectors_equal(Vector1, Vector2))
563 and not (vectors_equal(Vector2, Vector1)),
564 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
565 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
567 Subtest1 and Subtest2 and Subtest3.
570 vector_addition_test() ->
571 io:format("vector addition", []),
572 Vector0 = vector_add(
573 #vector{x=3, y=7, z=-3},
574 #vector{x=0, y=-24, z=123}),
575 Subtest1 = (Vector0#vector.x == 3)
576 and (Vector0#vector.y == -17)
577 and (Vector0#vector.z == 120),
579 Vector1 = #vector{x=5, y=0, z=984},
580 Vector2 = vector_add(Vector1, Vector1),
581 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
582 and (Vector2#vector.y == Vector1#vector.y*2)
583 and (Vector2#vector.z == Vector1#vector.z*2),
585 Vector3 = #vector{x=908, y=-098, z=234},
586 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
587 Subtest3 = vectors_equal(Vector3, Vector4),
589 Subtest1 and Subtest2 and Subtest3.
591 vector_subtraction_test() ->
592 io:format("vector subtraction", []),
593 Vector1 = #vector{x=0, y=0, z=0},
594 Vector2 = #vector{x=8390, y=-2098, z=939},
595 Vector3 = #vector{x=1, y=1, z=1},
596 Vector4 = #vector{x=-1, y=-1, z=-1},
598 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
599 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
600 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
601 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
602 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
603 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
604 vector_sub(Vector2, Vector3)),
606 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
608 vector_square_mag_test() ->
609 io:format("vector square magnitude test", []),
610 Vector1 = #vector{x=0, y=0, z=0},
611 Vector2 = #vector{x=1, y=1, z=1},
612 Vector3 = #vector{x=3, y=-4, z=0},
614 Subtest1 = (0 == vector_square_mag(Vector1)),
615 Subtest2 = (3 == vector_square_mag(Vector2)),
616 Subtest3 = (25 == vector_square_mag(Vector3)),
618 Subtest1 and Subtest2 and Subtest3.
620 vector_mag_test() ->
621 io:format("vector magnitude test", []),
622 Vector1 = #vector{x=0, y=0, z=0},
623 Vector2 = #vector{x=1, y=1, z=1},
624 Vector3 = #vector{x=3, y=-4, z=0},
626 Subtest1 = (0 == vector_mag(Vector1)),
627 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
628 Subtest3 = (5 == vector_mag(Vector3)),
630 Subtest1 and Subtest2 and Subtest3.
632 vector_scalar_multiplication_test() ->
633 io:format("scalar multiplication test", []),
634 Vector1 = #vector{x=0, y=0, z=0},
635 Vector2 = #vector{x=1, y=1, z=1},
636 Vector3 = #vector{x=3, y=-4, z=0},
638 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
639 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
640 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
641 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
642 vector_scalar_mult(Vector2, 4)),
643 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
644 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
646 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
648 vector_dot_product_test() ->
649 io:format("dot product test", []),
650 Vector1 = #vector{x=1, y=3, z=-5},
651 Vector2 = #vector{x=4, y=-2, z=-1},
652 Vector3 = #vector{x=0, y=0, z=0},
653 Vector4 = #vector{x=1, y=0, z=0},
654 Vector5 = #vector{x=0, y=1, z=0},
656 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
657 Subtest2 = vector_dot_product(Vector2, Vector2)
658 == vector_square_mag(Vector2),
659 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
660 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
662 Subtest1 and Subtest2 and Subtest3 and Subtest4.
664 vector_cross_product_test() ->
665 io:format("cross product test", []),
666 Vector1 = #vector{x=0, y=0, z=0},
667 Vector2 = #vector{x=1, y=0, z=0},
668 Vector3 = #vector{x=0, y=1, z=0},
669 Vector4 = #vector{x=0, y=0, z=1},
670 Vector5 = #vector{x=1, y=2, z=3},
671 Vector6 = #vector{x=4, y=5, z=6},
672 Vector7 = #vector{x=-3, y=6, z=-3},
673 Vector8 = #vector{x=-1, y=0, z=0},
674 Vector9 = #vector{x=-9, y=8, z=433},
676 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
677 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
678 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
679 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
680 Subtest5 = vectors_equal(
681 vector_cross_product(Vector7,
682 vector_add(Vector8, Vector9)),
683 vector_add(
684 vector_cross_product(Vector7, Vector8),
685 vector_cross_product(Vector7, Vector9))),
686 Subtest6 = vectors_equal(Vector1,
687 vector_add(
688 vector_add(
689 vector_cross_product(
690 Vector7,
691 vector_cross_product(Vector8, Vector9)),
692 vector_cross_product(
693 Vector8,
694 vector_cross_product(Vector9, Vector7))),
695 vector_cross_product(
696 Vector9,
697 vector_cross_product(Vector7, Vector8)))),
699 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
701 vector_normalization_test() ->
702 io:format("normalization test", []),
703 Vector1 = #vector{x=0, y=0, z=0},
704 Vector2 = #vector{x=1, y=0, z=0},
705 Vector3 = #vector{x=5, y=0, z=0},
707 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
708 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
709 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
710 Subtest4 = vectors_equal(Vector2, vector_normalize(
711 vector_scalar_mult(Vector2, 324))),
713 Subtest1 and Subtest2 and Subtest3 and Subtest4.
715 vector_negation_test() ->
716 io:format("vector negation test", []),
717 Vector1 = #vector{x=0, y=0, z=0},
718 Vector2 = #vector{x=4, y=-5, z=6},
720 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
721 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
723 Subtest1 and Subtest2.
725 ray_through_pixel_test() ->
726 io:format("ray through pixel test", []),
727 false.
729 ray_shooting_test() ->
730 io:format("ray shooting test"),
731 Vector1 = #vector{x=0, y=0, z=0},
732 Vector2 = #vector{x=1, y=0, z=0},
734 Subtest1 = vectors_equal(
735 (shoot_ray(Vector1, Vector2))#ray.direction,
736 Vector2),
738 Subtest1.
740 ray_sphere_intersection_test() ->
741 io:format("ray sphere intersection test", []),
743 Sphere = #sphere{
744 radius=3,
745 center=#vector{x = 0, y=0, z=10},
746 material=#material{
747 colour=#colour{r=0.4, g=0.4, b=0.4}}},
748 Ray1 = #ray{
749 origin=#vector{x=0, y=0, z=0},
750 direction=#vector{x=0, y=0, z=1}},
751 Ray2 = #ray{
752 origin=#vector{x=3, y=0, z=0},
753 direction=#vector{x=0, y=0, z=1}},
754 Ray3 = #ray{
755 origin=#vector{x=4, y=0, z=0},
756 direction=#vector{x=0, y=0, z=1}},
757 Subtest1 = ray_sphere_intersect(Ray1, Sphere) == 7.0,
758 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == 10.0,
759 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == infinity,
760 Subtest1 and Subtest2 and Subtest3.
762 point_on_screen_test() ->
763 io:format("point on screen test", []),
764 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
765 rotation=#vector{x=0, y=0, z=0},
766 fov=90,
767 screen=#screen{width=1, height=1}},
768 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
769 rotation=#vector{x=0, y=0, z=0},
770 fov=90,
771 screen=#screen{width=640, height=480}},
773 Subtest1 = vectors_equal(
774 #vector{x=0, y=0, z=0.5},
775 point_on_screen(0.5, 0.5, Camera1)),
776 Subtest2 = vectors_equal(
777 #vector{x=-0.5, y=-0.5, z=0.5},
778 point_on_screen(0, 0, Camera1)),
779 Subtest3 = vectors_equal(
780 #vector{x=0.5, y=0.5, z=0.5},
781 point_on_screen(1, 1, Camera1)),
782 Subtest4 = vectors_equal(
783 point_on_screen(0, 0, Camera2),
784 #vector{x=-320, y=-240, z=320}),
785 Subtest5 = vectors_equal(
786 point_on_screen(1, 1, Camera2),
787 #vector{x=320, y=240, z=320}),
788 Subtest6 = vectors_equal(
789 point_on_screen(0.5, 0.5, Camera2),
790 #vector{x=0, y=0, z=320}),
792 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
794 nearest_object_intersecting_ray_test() ->
795 io:format("nearest object intersecting ray test", []),
796 % test to make sure that we really get the closest object
797 Sphere1=#sphere{radius=5,
798 center=#vector{x=0, y=0, z=10},
799 material=#material{
800 colour=#colour{r=0, g=0, b=0.03}}},
801 Sphere2=#sphere{radius=5,
802 center=#vector{x=0, y=0, z=20},
803 material=#material{
804 colour=#colour{r=0, g=0, b=0.06}}},
805 Sphere3=#sphere{radius=5,
806 center=#vector{x=0, y=0, z=30},
807 material=#material{
808 colour=#colour{r=0, g=0, b=0.09}}},
809 Sphere4=#sphere{radius=5,
810 center=#vector{x=0, y=0, z=-10},
811 material=#material{
812 colour=#colour{r=0, g=0, b=-0.4}}},
813 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
814 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
815 direction=#vector{x=0, y=0, z=1}},
817 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
818 Ray1, Scene1),
819 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
820 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
821 and point_on_sphere(Sphere1, Hit_location),
823 Subtest1.
825 focal_length_test() ->
826 Epsilon = 0.1,
827 Size = 36,
828 io:format("focal length test", []),
829 lists:foldl(
830 fun({Focal_length, Dimension}, Matches) ->
831 %Result = focal_length(Dimension, Size),
832 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
833 Matches
834 and ((Focal_length + Epsilon >= focal_length(
835 Dimension, Size))
836 and (Focal_length - Epsilon =< focal_length(
837 Dimension, Size)))
838 end, true,
839 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
841 vector_rotation_test() ->
842 io:format("vector rotation test", []),
843 Vector1 = #vector{x=0, y=0, z=0},
844 Vector2 = #vector{x=0, y=1, z=0},
845 Vector3 = #vector{x=90, y=0, z=0},
846 Vector4 = #vector{x=45, y=0, z=0},
847 Vector5 = #vector{x=30.11, y=-988.2, z=92.231},
848 Vector6 = #vector{x=0, y=0, z=1},
850 Subtest1 = vectors_equal(
851 vector_rotate(Vector1, Vector1),
852 Vector1),
853 Subtest2 = vectors_equal(
854 vector_rotate(Vector5, Vector1),
855 Vector5),
856 Subtest3 = vectors_equal(
857 vector_rotate(
858 vector_rotate(Vector5, Vector4),
859 Vector4),
860 vector_rotate(Vector5, Vector3)),
861 Subtest4 = vectors_equal(
862 Vector6,
863 vector_rotate(Vector2, Vector3)),
865 Subtest1 and Subtest2 and Subtest3 and Subtest4.
867 object_normal_at_point_test() ->
868 io:format("object normal at point test"),
869 Sphere1 = #sphere{radius=13.5,
870 center=#vector{x=0, y=0, z=0},
871 material=#material{
872 colour=#colour{r=0, g=0, b=0}}},
873 Point1 = #vector{x=13.5, y=0, z=0},
874 Point2 = #vector{x=0, y=13.5, z=0},
875 Point3 = #vector{x=0, y=0, z=13.5},
876 Point4 = vector_neg(Point1),
877 Point5 = vector_neg(Point2),
878 Point6 = vector_neg(Point3),
880 % sphere object tests
881 Subtest1 = vectors_equal(
882 vector_normalize(Point1),
883 object_normal_at_point(Sphere1, Point1)),
884 Subtest2 = vectors_equal(
885 vector_normalize(Point2),
886 object_normal_at_point(Sphere1, Point2)),
887 Subtest3 = vectors_equal(
888 vector_normalize(Point3),
889 object_normal_at_point(Sphere1, Point3)),
890 Subtest4 = vectors_equal(
891 vector_normalize(Point4),
892 object_normal_at_point(Sphere1, Point4)),
893 Subtest5 = vectors_equal(
894 vector_normalize(Point5),
895 object_normal_at_point(Sphere1, Point5)),
896 Subtest6 = vectors_equal(
897 vector_normalize(Point6),
898 object_normal_at_point(Sphere1, Point6)),
899 Subtest7 = not vectors_equal(
900 vector_normalize(Point1),
901 object_normal_at_point(Sphere1, Point4)),
903 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
904 and Subtest7.
906 vector_bounce_off_plane_test() ->
907 io:format("vector reflect about normal", []),
908 Vector1 = #vector{x=1, y=1, z=0},
909 Vector2 = #vector{x=0, y=-1, z=0},
910 Vector3 = #vector{x=1, y=-1, z=0},
911 Vector4 = #vector{x=1, y=0, z=0},
913 Subtest1 = vectors_equal(vector_bounce_off_plane(
914 Vector1,
915 vector_normalize(Vector2)),
916 Vector3),
918 Subtest2 = vectors_equal(
919 vector_bounce_off_plane(
920 Vector2,
921 vector_normalize(Vector1)),
922 Vector4),
924 Subtest1 and Subtest2.