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/>.
22 %% * two object types:
24 %% * triangles (not done)
26 %% * shadows (not done)
27 %% * lighting based on local illumination models
28 %% * ambient (not done)
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)
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, _
, _
) ->
61 raytraced_pixel_list(Width
, Height
, Scene
, Recursion_depth
)
62 when Width
> 0, Height
> 0 ->
63 Master_PID
= spawn(raytracer
, master
, [self(), Width
*Height
]),
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", []),
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
) ->
87 master(Program_PID
, Pixel_count
-1, [Pixel_tuple
|Pixel_list
])
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
,
114 lighting_function(Ray
, Object
, Hit_location
, Hit_normal
, Scene
,
117 fun (#point_light
{diffuse_colour
=Light_colour
,
118 location
=Light_location
,
119 specular_colour
=Specular_colour
},
124 pixel_colour_from_ray(
125 #ray
{origin
=Hit_location
,
126 direction
=vector_bounce_off_plane(
127 Ray#ray
.direction
, Hit_normal
)},
130 object_reflectivity(Object
)),
132 vector_component_mult(
133 colour_to_vector(Light_colour
),
139 specular_term(Ray#ray
.direction
,
143 object_specular_power(Object
),
144 object_shininess(Object
),
147 (_Not_a_point_light
, Final_colour
) ->
150 #vector
{x
=0, y
=0, z
=0},
153 diffuse_term(Object
, Light_location
, Hit_location
, Hit_normal
) ->
155 colour_to_vector(object_diffuse_colour(Object
)),
157 vector_dot_product(Hit_normal
,
159 vector_sub(Light_location
,
162 specular_term(EyeVector
, Light_location
, Hit_location
, Hit_normal
,
163 Specular_power
, Shininess
, Specular_colour
) ->
165 colour_to_vector(Specular_colour
),
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
, []) ->
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
,
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", []),
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(
210 %io:format("no closer obj found~n", []),
211 nearest_object_intersecting_ray(Ray
,
219 ray_object_intersect(Ray
, Object
) ->
222 ray_sphere_intersect(Ray
, Object
);
224 ray_triangle_intersect(Ray
, Object
);
229 object_normal_at_point(#sphere
{center
=Center
}, Point
) ->
231 vector_sub(Point
, Center
)).
233 ray_sphere_intersect(
238 #sphere
{radius
=Radius
, center
=#vector
{
239 x
=Xc
, y
=Yc
, z
=Zc
}}) ->
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]),
260 ray_triangle_intersect(Ray
, Triangle
) ->
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
,
273 #vector
{x
=0, y
=0, z
=1},
277 #vector
{x
= (X
-0.5) * Screen_width
,
281 y
= (Y
-0.5) * Screen_height
,
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
}) ->
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
,
334 vector_normalize(V
) ->
337 #vector
{x
=0, y
=0, z
=0};
339 vector_scalar_mult(V
, 1/vector_mag(V
))
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
) ->
349 2*vector_dot_product(Normal
, vector_neg(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
) ->
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
}) ->
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
}) ->
380 % returns a list of objects in the scene
381 % camera is assumed to be the first element in the scene
383 [#camera
{location
=#vector
{x
=0, y
=0, z
=0},
384 rotation
=#vector
{x
=0, y
=0, z
=0},
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}},
394 center
=#vector
{x
=4, y
=0, z
=10},
396 colour
=#colour
{r
=0, g
=0.5, b
=1},
401 center
=#vector
{x
=-5, y
=3, z
=9},
403 colour
=#colour
{r
=1, g
=0.5, b
=0},
408 center
=#vector
{x
=-5, y
=-2, z
=10},
410 colour
=#colour
{r
=0.5, g
=1, b
=0},
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},
418 colour
=#colour
{r
=0.5, g
=0, b
=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
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
]),
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,
440 file:close(IoDevice
),
441 io:format("done~n", []);
443 io:format("error opening file~n", [])
447 go(16, 12, "/tmp/traced.ppm", 3).
449 go(640, 480, "/tmp/traced.ppm", 10),
451 go(Width
, Height
, Filename
, Recursion_depth
) ->
452 write_pixels_to_ppm(Width
,
455 raytraced_pixel_list(Width
,
463 Tests
= [fun scene_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
).
488 io:format("testing the scene function", []),
502 {colour
, 1, 0, 0.5}},
506 {material
, {colour
, 0, 0.5, 1}, 20, 1, 0.1}},
510 {material
, {colour
, 1, 0.5, 0}, 4, 0.25, 0.5}},
513 {vector
, -5, -2, 10},
514 {material
, {colour
, 0.5, 1, 0}, 20, 0.25, 0.7}},
517 {vector
, 2, 1.5, 10},
518 {vector
, -2, 1.5, 0},
519 {material
, {colour
, 0.5, 0, 1}, 40, 1, 1}}
527 io:format("this test always passes", []),
530 run_tests([], _Num
, Success
) ->
533 io:format("Success!~n", []),
536 io:format("some tests failed~n", []),
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
545 io:format(" - OK~n", []);
547 io:format(" - FAILED~n", [])
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
.
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
)),
684 vector_cross_product(Vector7
, Vector8
),
685 vector_cross_product(Vector7
, Vector9
))),
686 Subtest6
= vectors_equal(Vector1
,
689 vector_cross_product(
691 vector_cross_product(Vector8
, Vector9
)),
692 vector_cross_product(
694 vector_cross_product(Vector9
, Vector7
))),
695 vector_cross_product(
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", []),
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
,
740 ray_sphere_intersection_test() ->
741 io:format("ray sphere intersection test", []),
745 center
=#vector
{x
= 0, y
=0, z
=10},
747 colour
=#colour
{r
=0.4, g
=0.4, b
=0.4}}},
749 origin
=#vector
{x
=0, y
=0, z
=0},
750 direction
=#vector
{x
=0, y
=0, z
=1}},
752 origin
=#vector
{x
=3, y
=0, z
=0},
753 direction
=#vector
{x
=0, y
=0, z
=1}},
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},
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},
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},
800 colour
=#colour
{r
=0, g
=0, b
=0.03}}},
801 Sphere2
=#sphere
{radius
=5,
802 center
=#vector
{x
=0, y
=0, z
=20},
804 colour
=#colour
{r
=0, g
=0, b
=0.06}}},
805 Sphere3
=#sphere
{radius
=5,
806 center
=#vector
{x
=0, y
=0, z
=30},
808 colour
=#colour
{r
=0, g
=0, b
=0.09}}},
809 Sphere4
=#sphere
{radius
=5,
810 center
=#vector
{x
=0, y
=0, z
=-10},
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(
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
),
825 focal_length_test() ->
828 io:format("focal length test", []),
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]),
834 and ((Focal_length
+ Epsilon
>= focal_length(
836 and (Focal_length
- Epsilon
=< focal_length(
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
),
853 Subtest2
= vectors_equal(
854 vector_rotate(Vector5
, Vector1
),
856 Subtest3
= vectors_equal(
858 vector_rotate(Vector5
, Vector4
),
860 vector_rotate(Vector5
, Vector3
)),
861 Subtest4
= vectors_equal(
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},
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
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(
915 vector_normalize(Vector2
)),
918 Subtest2
= vectors_equal(
919 vector_bounce_off_plane(
921 vector_normalize(Vector1
)),
924 Subtest1 and Subtest2
.