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