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 %% * three object types:
25 %% * triangles (not done)
27 %% * shadows (not done)
28 %% * lighting based on local illumination models
29 %% * ambient (not done)
32 %% * attenuation (not done)
33 %% * reflections to a fixed depth
34 %% * PPM output file format
35 %% * randomly generated scene (not done)
36 %% * useful test suite (working but not very friendly when fails)
38 %% * distributed (across multiple computers) (not done)
52 raytraced_pixel_list_simple
/4,
53 raytraced_pixel_list_concurrent
/4
56 -record(vector
, {x
, y
, z
}).
57 -record(colour
, {r
, g
, b
}).
58 -record(ray
, {origin
, direction
}).
59 -record(screen
, {width
, height
}). % screen dimensions in the 3D world
60 -record(camera
, {location
, rotation
, fov
, screen
}).
61 -record(material
, {colour
, specular_power
, shininess
, reflectivity
}).
62 -record(sphere
, {radius
, center
, material
}).
63 -record(triangle
, {v1
, v2
, v3
, material
}).
64 -record(plane
, {normal
, distance
, material
}).
65 -record(point_light
, {diffuse_colour
, location
, specular_colour
}).
66 -define(BACKGROUND_COLOUR
, #colour
{r
=0, g
=0, b
=0}).
67 -define(ERROR_COLOUR
, #colour
{r
=1, g
=0, b
=0}).
68 -define(UNKNOWN_COLOUR
, #colour
{r
=0, g
=1, b
=0}).
69 -define(FOG_DISTANCE
, 40).
71 raytraced_pixel_list_simple(0, 0, _
, _
) ->
73 raytraced_pixel_list_simple(Width
, Height
, Scene
, Recursion_depth
)
74 when Width
> 0, Height
> 0 ->
79 % coordinates passed as a percentage
81 trace_ray_through_pixel(
82 {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
))} end,
83 lists:seq(0, Width
- 1)) end,
84 lists:seq(0, Height
- 1)).
86 raytraced_pixel_list_concurrent(0, 0, _
, _
) ->
88 raytraced_pixel_list_concurrent(Width
, Height
, Scene
, Recursion_depth
)
89 when Width
> 0, Height
> 0 ->
90 Master_PID
= spawn(raytracer
, master
, [self(), Width
*Height
]),
95 % coordinates passed as a percentage
96 spawn(raytracer
, worker
,
97 [Master_PID
, X
+Y
*Width
, {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
]) end,
98 lists:seq(0, Width
- 1)) end,
99 lists:seq(0, Height
- 1)),
100 io:format("all workers have been spawned~n", []),
106 master(Program_PID
, Pixel_count
) ->
107 master(Program_PID
, Pixel_count
, []).
108 master(Program_PID
, 0, Pixel_list
) ->
109 io:format("master is done~n", []),
110 Program_PID
! lists:keysort(1, Pixel_list
);
111 % assumes all workers eventually return a good value
112 master(Program_PID
, Pixel_count
, Pixel_list
) ->
115 master(Program_PID
, Pixel_count
-1, [Pixel_tuple
|Pixel_list
])
119 % assumes X and Y are percentages of the screen dimensions
120 worker(Master_PID
, Pixel_num
, {X
, Y
}, Scene
, Recursion_depth
) ->
121 Master_PID
! {Pixel_num
,
122 colour_to_pixel(trace_ray_through_pixel({X
, Y
}, Scene
, Recursion_depth
))}.
124 trace_ray_through_pixel({X
, Y
}, [Camera
|Rest_of_scene
], Recursion_depth
) ->
125 pixel_colour_from_ray(
126 ray_through_pixel(X
, Y
, Camera
),
130 pixel_colour_from_ray(_Ray
, _Scene
, 0) ->
131 #colour
{r
=0, g
=0, b
=0};
132 pixel_colour_from_ray(Ray
, Scene
, Recursion_depth
) ->
133 case nearest_object_intersecting_ray(Ray
, Scene
) of
134 {Nearest_object
, _Distance
, Hit_location
, Hit_normal
} ->
135 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
137 vector_to_colour(lighting_function(Ray
,
149 lighting_function(Ray
, Object
, Hit_location
, Hit_normal
, Scene
,
152 fun (#point_light
{diffuse_colour
=Light_colour
,
153 location
=Light_location
,
154 specular_colour
=Specular_colour
},
156 Reflection
= vector_scalar_mult(
158 pixel_colour_from_ray(
159 #ray
{origin
=Hit_location
,
160 direction
=vector_bounce_off_plane(
161 Ray#ray
.direction
, Hit_normal
)},
164 object_reflectivity(Object
)),
165 Light_contribution
= vector_add(
176 object_specular_power(Object
),
177 object_shininess(Object
),
184 vector_component_mult(
185 colour_to_vector(Light_colour
),
187 shadow_factor(Light_location
, Hit_location
, Scene
))));
188 (_Not_a_point_light
, Final_colour
) ->
191 #vector
{x
=0, y
=0, z
=0},
194 shadow_factor(Light_location
, Hit_location
, Scene
) ->
195 Light_vector
= vector_sub(Light_location
, Hit_location
),
196 Light_vector_length
= vector_mag(Light_vector
),
197 Light_direction
= vector_normalize(Light_vector
),
198 % start the ray a little bit farther to prevent artefacts due to unit precision limitations
199 Shadow_ray
= #ray
{origin
=vector_add(
204 direction
=Light_direction
},
205 case nearest_object_intersecting_ray(Shadow_ray
, Scene
) of
206 {_Obj
, Distance
, _Loc
, _Normal
} ->
207 if Distance
== infinity
->
209 Light_vector_length
> Distance
->
218 diffuse_term(Object
, Light_location
, Hit_location
, Hit_normal
) ->
220 colour_to_vector(object_diffuse_colour(Object
)),
222 vector_dot_product(Hit_normal
,
224 vector_sub(Light_location
,
227 specular_term(EyeVector
, Light_location
, Hit_location
, Hit_normal
,
228 Specular_power
, Shininess
, Specular_colour
) ->
230 colour_to_vector(Specular_colour
),
237 vector_sub(Light_location
, Hit_location
)),
238 vector_neg(EyeVector
))),
239 Hit_normal
)]), Specular_power
)).
241 nearest_object_intersecting_ray(Ray
, Scene
) ->
242 nearest_object_intersecting_ray(
243 Ray
, none
, hitlocation
, hitnormal
, infinity
, Scene
).
244 nearest_object_intersecting_ray(
245 _Ray
, _NearestObj
, _Hit_location
, _Normal
, infinity
, []) ->
247 nearest_object_intersecting_ray(
248 _Ray
, NearestObj
, Hit_location
, Normal
, Distance
, []) ->
249 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
250 {NearestObj
, Distance
, Hit_location
, Normal
};
251 nearest_object_intersecting_ray(Ray
,
256 [CurrentObject
|Rest_of_scene
]) ->
257 NewDistance
= ray_object_intersect(Ray
, CurrentObject
),
258 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
259 if (NewDistance
/= infinity
)
260 and ((Distance
== infinity
) or (Distance
> NewDistance
)) ->
261 %io:format("another closer object found~n", []),
263 vector_add(Ray#ray
.origin
,
264 vector_scalar_mult(Ray#ray
.direction
, NewDistance
)),
265 New_normal
= object_normal_at_point(
266 CurrentObject
, New_hit_location
),
267 nearest_object_intersecting_ray(
275 %io:format("no closer obj found~n", []),
276 nearest_object_intersecting_ray(Ray
,
284 ray_object_intersect(Ray
, Object
) ->
287 ray_sphere_intersect(Ray
, Object
);
289 ray_triangle_intersect(Ray
, Object
);
291 ray_plane_intersect(Ray
, Object
);
296 object_normal_at_point(#sphere
{center
=Center
}, Point
) ->
298 vector_sub(Point
, Center
));
299 object_normal_at_point(#plane
{normal
=Normal
}, _Point
) ->
302 ray_sphere_intersect(
307 #sphere
{radius
=Radius
, center
=#vector
{
308 x
=Xc
, y
=Yc
, z
=Zc
}}) ->
310 A
= Xd
*Xd
+ Yd
*Yd
+ Zd
*Zd
,
311 B
= 2 * (Xd
*(X0
-Xc
) + Yd
*(Y0
-Yc
) + Zd
*(Z0
-Zc
)),
312 C
= (X0
-Xc
)*(X0
-Xc
) + (Y0
-Yc
)*(Y0
-Yc
) + (Z0
-Zc
)*(Z0
-Zc
) - Radius
*Radius
,
313 Discriminant
= B
*B
- 4*A
*C
,
314 %io:format("A=~w B=~w C=~w discriminant=~w~n",
315 % [A, B, C, Discriminant]),
316 if Discriminant
>= Epsilon
->
317 T0
= (-B
+ math:sqrt(Discriminant
))/2,
318 T1
= (-B
- math:sqrt(Discriminant
))/2,
319 if (T0
>= 0) and (T1
>= 0) ->
320 %io:format("T0=~w T1=~w~n", [T0, T1]),
329 ray_triangle_intersect(_Ray
, _Triangle
) ->
332 ray_plane_intersect(Ray
, Plane
) ->
334 Vd
= vector_dot_product(Plane#plane
.normal
, Ray#ray
.direction
),
336 V0
= -(vector_dot_product(Plane#plane
.normal
, Ray#ray
.origin
)
337 + Plane#plane
.distance
),
339 if Distance
< Epsilon
->
349 focal_length(Angle
, Dimension
) ->
350 Dimension
/(2*math:tan(Angle
*(math:pi()/180)/2)).
352 point_on_screen(X
, Y
, Camera
) ->
353 %TODO: implement rotation (using quaternions)
354 Screen_width
= (Camera#camera
.screen
)#screen
.width
,
355 Screen_height
= (Camera#camera
.screen
)#screen
.height
,
356 lists:foldl(fun(Vect
, Sum
) -> vector_add(Vect
, Sum
) end,
357 Camera#camera
.location
,
359 #vector
{x
=0, y
=0, z
=1},
363 #vector
{x
= (X
-0.5) * Screen_width
,
367 y
= (Y
-0.5) * Screen_height
,
372 shoot_ray(From
, Through
) ->
373 #ray
{origin
=From
, direction
=vector_normalize(vector_sub(Through
, From
))}.
375 % assume that X and Y are percentages of the 3D world screen dimensions
376 ray_through_pixel(X
, Y
, Camera
) ->
377 shoot_ray(Camera#camera
.location
, point_on_screen(X
, Y
, Camera
)).
379 vectors_equal(V1
, V2
) ->
380 vectors_equal(V1
, V2
, 0.0001).
381 vectors_equal(V1
, V2
, Epsilon
) ->
382 (V1#vector
.x
+ Epsilon
>= V2#vector
.x
)
383 and (V1#vector
.x
- Epsilon
=<V2#vector
.x
)
384 and (V1#vector
.y
+ Epsilon
>= V2#vector
.y
)
385 and (V1#vector
.y
- Epsilon
=<V2#vector
.y
)
386 and (V1#vector
.z
+ Epsilon
>= V2#vector
.z
)
387 and (V1#vector
.z
- Epsilon
=<V2#vector
.z
).
390 vector_add(V1
, V2
) ->
391 #vector
{x
= V1#vector
.x
+ V2#vector
.x
,
392 y
= V1#vector
.y
+ V2#vector
.y
,
393 z
= V1#vector
.z
+ V2#vector
.z
}.
395 vector_sub(V1
, V2
) ->
396 #vector
{x
= V1#vector
.x
- V2#vector
.x
,
397 y
= V1#vector
.y
- V2#vector
.y
,
398 z
= V1#vector
.z
- V2#vector
.z
}.
400 vector_square_mag(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
404 math:sqrt(vector_square_mag(V
)).
406 vector_scalar_mult(#vector
{x
=X
, y
=Y
, z
=Z
}, Scalar
) ->
407 #vector
{x
=X
*Scalar
, y
=Y
*Scalar
, z
=Z
*Scalar
}.
409 vector_component_mult(#vector
{x
=X1
, y
=Y1
, z
=Z1
}, #vector
{x
=X2
, y
=Y2
, z
=Z2
}) ->
410 #vector
{x
=X1
*X2
, y
=Y1
*Y2
, z
=Z1
*Z2
}.
412 vector_dot_product(#vector
{x
=A1
, y
=A2
, z
=A3
}, #vector
{x
=B1
, y
=B2
, z
=B3
}) ->
413 A1
*B1
+ A2
*B2
+ A3
*B3
.
415 vector_cross_product(#vector
{x
=A1
, y
=A2
, z
=A3
}, #vector
{x
=B1
, y
=B2
, z
=B3
}) ->
416 #vector
{x
= A2
*B3
- A3
*B2
,
420 vector_normalize(V
) ->
423 #vector
{x
=0, y
=0, z
=0};
425 vector_scalar_mult(V
, 1/vector_mag(V
))
428 vector_neg(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
429 #vector
{x
=-X
, y
=-Y
, z
=-Z
}.
431 vector_bounce_off_plane(Vector
, Normal
) ->
435 2*vector_dot_product(Normal
, vector_neg(Vector
))),
438 object_diffuse_colour(#sphere
{material
=#material
{colour
=C
}}) ->
440 object_diffuse_colour(#plane
{material
=#material
{colour
=C
}}) ->
442 object_specular_power(#sphere
{material
=#material
{specular_power
=SP
}}) ->
444 object_specular_power(#plane
{material
=#material
{specular_power
=SP
}}) ->
447 object_shininess(#sphere
{material
=#material
{shininess
=S
}}) ->
449 object_shininess(#plane
{material
=#material
{shininess
=S
}}) ->
452 object_reflectivity(#sphere
{material
=#material
{reflectivity
=R
}}) ->
454 object_reflectivity(#plane
{material
=#material
{reflectivity
=R
}}) ->
457 point_on_sphere(#sphere
{radius
=Radius
, center
=#vector
{x
=XC
, y
=YC
, z
=ZC
}},
458 #vector
{x
=X
, y
=Y
, z
=Z
}) ->
461 ((X
-XC
)*(X
-XC
) + (Y
-YC
)*(Y
-YC
) + (Z
-ZC
)*(Z
-ZC
)) - Radius
*Radius
).
463 colour_to_vector(#colour
{r
=R
, g
=G
, b
=B
}) ->
464 #vector
{x
=R
, y
=G
, z
=B
}.
465 vector_to_colour(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
466 #colour
{r
=X
, g
=Y
, b
=Z
}.
467 colour_to_pixel(#colour
{r
=R
, g
=G
, b
=B
}) ->
470 % returns a list of objects in the scene
471 % camera is assumed to be the first element in the scene
473 [#camera
{location
=#vector
{x
=0, y
=0, z
=-2},
474 rotation
=#vector
{x
=0, y
=0, z
=0},
476 screen
=#screen
{width
=4, height
=3}},
477 #point_light
{diffuse_colour
=#colour
{r
=1, g
=1, b
=0.5},
478 location
=#vector
{x
=5, y
=-2, z
=0},
479 specular_colour
=#colour
{r
=1, g
=1, b
=1}},
480 #point_light
{diffuse_colour
=#colour
{r
=1, g
=0, b
=0.5},
481 location
=#vector
{x
=-10, y
=0, z
=7},
482 specular_colour
=#colour
{r
=1, g
=0, b
=0.5}},
484 center
=#vector
{x
=4, y
=0, z
=10},
486 colour
=#colour
{r
=0, g
=0.5, b
=1},
491 center
=#vector
{x
=-5, y
=3, z
=9},
493 colour
=#colour
{r
=1, g
=0.5, b
=0},
498 center
=#vector
{x
=-4.5, y
=-2.5, z
=14},
500 colour
=#colour
{r
=0.5, g
=1, b
=0},
504 #triangle
{v1
=#vector
{x
=2, y
=1.5, z
=0},
505 v2
=#vector
{x
=2, y
=1.5, z
=10},
506 v3
=#vector
{x
=-2, y
=1.5, z
=0},
508 colour
=#colour
{r
=0.5, g
=0, b
=1},
512 #plane
{normal
=#vector
{x
=0, y
=-1, z
=0},
515 colour
=#colour
{r
=1, g
=1, b
=1},
522 % assumes Pixels are ordered in a row by row fasion
523 write_pixels_to_ppm(Width
, Height
, MaxValue
, Pixels
, Filename
) ->
524 case file:open(Filename
, write
) of
526 io:format("file opened~n", []),
527 io:format(IoDevice
, "P3~n", []),
528 io:format(IoDevice
, "~p ~p~n", [Width
, Height
]),
529 io:format(IoDevice
, "~p~n", [MaxValue
]),
531 fun({_Num
, {R
, G
, B
}}) ->
532 io:format(IoDevice
, "~p ~p ~p ",
533 [lists:min([trunc(R
*MaxValue
), MaxValue
]),
534 lists:min([trunc(G
*MaxValue
), MaxValue
]),
535 lists:min([trunc(B
*MaxValue
), MaxValue
])]) end,
537 file:close(IoDevice
);
539 io:format("error opening file~n", [])
542 % various invocation style functions
543 standalone([Width
, Height
, Filename
, Recursion_depth
, Strategy
]) ->
544 standalone(list_to_integer(Width
),
545 list_to_integer(Height
),
547 list_to_integer(Recursion_depth
),
548 tracing_function(list_to_atom(Strategy
))).
550 standalone(Width
, Height
, Filename
, Recursion_depth
, Function
) ->
551 {Time
, _Value
} = timer:tc(
559 io:format("Done in ~w seconds~n", [Time
/1000000]),
563 raytrace(tracing_function(Strategy
)).
565 go(Width
, Height
, Filename
, Recursion_depth
, Strategy
) ->
566 raytrace(Width
, Height
, Filename
, Recursion_depth
,
567 tracing_function(Strategy
)).
569 tracing_function(simple
) ->
570 fun raytraced_pixel_list_simple
/4;
571 tracing_function(concurrent
) ->
572 fun raytraced_pixel_list_concurrent
/4.
574 raytrace(Function
) ->
575 raytrace(4, 3, "/tmp/traced.ppm", 5, Function
).
576 raytrace(Width
, Height
, Filename
, Recursion_depth
, Function
) ->
590 Tests
= [fun scene_test
/0,
592 fun vector_equality_test
/0,
593 fun vector_addition_test
/0,
594 fun vector_subtraction_test
/0,
595 fun vector_square_mag_test
/0,
596 fun vector_mag_test
/0,
597 fun vector_scalar_multiplication_test
/0,
598 fun vector_dot_product_test
/0,
599 fun vector_cross_product_test
/0,
600 fun vector_normalization_test
/0,
601 fun vector_negation_test
/0,
602 % fun ray_through_pixel_test/0,
603 fun ray_shooting_test
/0,
604 fun point_on_screen_test
/0,
605 fun nearest_object_intersecting_ray_test
/0,
606 fun focal_length_test
/0,
607 % fun vector_rotation_test/0,
608 fun object_normal_at_point_test
/0,
609 fun vector_bounce_off_plane_test
/0,
610 fun ray_sphere_intersection_test
/0
612 run_tests(Tests
, 1, true
).
615 io:format("testing the scene function", []),
629 {colour
, 1, 0, 0.5}},
633 {material
, {colour
, 0, 0.5, 1}, 20, 1, 0.1}},
637 {material
, {colour
, 1, 0.5, 0}, 4, 0.25, 0.5}},
640 {vector
, -4.5, -2.5, 14},
641 {material
, {colour
, 0.5, 1, 0}, 20, 0.25, 0.7}},
644 {vector
, 2, 1.5, 10},
645 {vector
, -2, 1.5, 0},
646 {material
, {colour
, 0.5, 0, 1}, 40, 1, 1}},
650 {material
, {colour
, 1, 1, 1}, 1, 0, 0.01}}
658 io:format("this test always passes", []),
661 run_tests([], _Num
, Success
) ->
664 io:format("Success!~n", []),
667 io:format("some tests failed~n", []),
671 run_tests([First_test
|Rest_of_tests
], Num
, Success_so_far
) ->
672 io:format("test #~p: ", [Num
]),
673 Current_success
= First_test(),
674 case Current_success
of
676 io:format(" - OK~n", []);
678 io:format(" - FAILED~n", [])
680 run_tests(Rest_of_tests
, Num
+ 1, Current_success and Success_so_far
).
682 vector_equality_test() ->
683 io:format("vector equality"),
684 Vector1
= #vector
{x
=0, y
=0, z
=0},
685 Vector2
= #vector
{x
=1234, y
=-234, z
=0},
686 Vector3
= #vector
{x
=0.0983, y
=0.0214, z
=0.12342},
687 Vector4
= #vector
{x
=0.0984, y
=0.0213, z
=0.12341},
688 Vector5
= #vector
{x
=10/3, y
=-10/6, z
=8/7},
689 Vector6
= #vector
{x
=3.3, y
=-1.6, z
=1.1},
691 Subtest1
= vectors_equal(Vector1
, Vector1
)
692 and
vectors_equal(Vector2
, Vector2
)
693 and
not (vectors_equal(Vector1
, Vector2
))
694 and
not (vectors_equal(Vector2
, Vector1
)),
695 Subtest2
= vectors_equal(Vector3
, Vector4
, 0.0001),
696 Subtest3
= vectors_equal(Vector5
, Vector6
, 0.1),
698 Subtest1 and Subtest2 and Subtest3
.
701 vector_addition_test() ->
702 io:format("vector addition", []),
703 Vector0
= vector_add(
704 #vector
{x
=3, y
=7, z
=-3},
705 #vector
{x
=0, y
=-24, z
=123}),
706 Subtest1
= (Vector0#vector
.x
== 3)
707 and (Vector0#vector
.y
== -17)
708 and (Vector0#vector
.z
== 120),
710 Vector1
= #vector
{x
=5, y
=0, z
=984},
711 Vector2
= vector_add(Vector1
, Vector1
),
712 Subtest2
= (Vector2#vector
.x
== Vector1#vector
.x
*2)
713 and (Vector2#vector
.y
== Vector1#vector
.y
*2)
714 and (Vector2#vector
.z
== Vector1#vector
.z
*2),
716 Vector3
= #vector
{x
=908, y
=-098, z
=234},
717 Vector4
= vector_add(Vector3
, #vector
{x
=0, y
=0, z
=0}),
718 Subtest3
= vectors_equal(Vector3
, Vector4
),
720 Subtest1 and Subtest2 and Subtest3
.
722 vector_subtraction_test() ->
723 io:format("vector subtraction", []),
724 Vector1
= #vector
{x
=0, y
=0, z
=0},
725 Vector2
= #vector
{x
=8390, y
=-2098, z
=939},
726 Vector3
= #vector
{x
=1, y
=1, z
=1},
727 Vector4
= #vector
{x
=-1, y
=-1, z
=-1},
729 Subtest1
= vectors_equal(Vector1
, vector_sub(Vector1
, Vector1
)),
730 Subtest2
= vectors_equal(Vector3
, vector_sub(Vector3
, Vector1
)),
731 Subtest3
= not
vectors_equal(Vector3
, vector_sub(Vector1
, Vector3
)),
732 Subtest4
= vectors_equal(Vector4
, vector_sub(Vector4
, Vector1
)),
733 Subtest5
= not
vectors_equal(Vector4
, vector_sub(Vector1
, Vector4
)),
734 Subtest5
= vectors_equal(vector_add(Vector2
, Vector4
),
735 vector_sub(Vector2
, Vector3
)),
737 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5
.
739 vector_square_mag_test() ->
740 io:format("vector square magnitude test", []),
741 Vector1
= #vector
{x
=0, y
=0, z
=0},
742 Vector2
= #vector
{x
=1, y
=1, z
=1},
743 Vector3
= #vector
{x
=3, y
=-4, z
=0},
745 Subtest1
= (0 == vector_square_mag(Vector1
)),
746 Subtest2
= (3 == vector_square_mag(Vector2
)),
747 Subtest3
= (25 == vector_square_mag(Vector3
)),
749 Subtest1 and Subtest2 and Subtest3
.
752 io:format("vector magnitude test", []),
753 Vector1
= #vector
{x
=0, y
=0, z
=0},
754 Vector2
= #vector
{x
=1, y
=1, z
=1},
755 Vector3
= #vector
{x
=3, y
=-4, z
=0},
757 Subtest1
= (0 == vector_mag(Vector1
)),
758 Subtest2
= (math:sqrt(3) == vector_mag(Vector2
)),
759 Subtest3
= (5 == vector_mag(Vector3
)),
761 Subtest1 and Subtest2 and Subtest3
.
763 vector_scalar_multiplication_test() ->
764 io:format("scalar multiplication test", []),
765 Vector1
= #vector
{x
=0, y
=0, z
=0},
766 Vector2
= #vector
{x
=1, y
=1, z
=1},
767 Vector3
= #vector
{x
=3, y
=-4, z
=0},
769 Subtest1
= vectors_equal(Vector1
, vector_scalar_mult(Vector1
, 45)),
770 Subtest2
= vectors_equal(Vector1
, vector_scalar_mult(Vector1
, -13)),
771 Subtest3
= vectors_equal(Vector1
, vector_scalar_mult(Vector3
, 0)),
772 Subtest4
= vectors_equal(#vector
{x
=4, y
=4, z
=4},
773 vector_scalar_mult(Vector2
, 4)),
774 Subtest5
= vectors_equal(Vector3
, vector_scalar_mult(Vector3
, 1)),
775 Subtest6
= not
vectors_equal(Vector3
, vector_scalar_mult(Vector3
, -3)),
777 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
779 vector_dot_product_test() ->
780 io:format("dot product test", []),
781 Vector1
= #vector
{x
=1, y
=3, z
=-5},
782 Vector2
= #vector
{x
=4, y
=-2, z
=-1},
783 Vector3
= #vector
{x
=0, y
=0, z
=0},
784 Vector4
= #vector
{x
=1, y
=0, z
=0},
785 Vector5
= #vector
{x
=0, y
=1, z
=0},
787 Subtest1
= 3 == vector_dot_product(Vector1
, Vector2
),
788 Subtest2
= vector_dot_product(Vector2
, Vector2
)
789 == vector_square_mag(Vector2
),
790 Subtest3
= 0 == vector_dot_product(Vector3
, Vector1
),
791 Subtest4
= 0 == vector_dot_product(Vector4
, Vector5
),
793 Subtest1 and Subtest2 and Subtest3 and Subtest4
.
795 vector_cross_product_test() ->
796 io:format("cross product test", []),
797 Vector1
= #vector
{x
=0, y
=0, z
=0},
798 Vector2
= #vector
{x
=1, y
=0, z
=0},
799 Vector3
= #vector
{x
=0, y
=1, z
=0},
800 Vector4
= #vector
{x
=0, y
=0, z
=1},
801 Vector5
= #vector
{x
=1, y
=2, z
=3},
802 Vector6
= #vector
{x
=4, y
=5, z
=6},
803 Vector7
= #vector
{x
=-3, y
=6, z
=-3},
804 Vector8
= #vector
{x
=-1, y
=0, z
=0},
805 Vector9
= #vector
{x
=-9, y
=8, z
=433},
807 Subtest1
= vectors_equal(Vector1
, vector_cross_product(Vector2
, Vector2
)),
808 Subtest2
= vectors_equal(Vector1
, vector_cross_product(Vector2
, Vector8
)),
809 Subtest3
= vectors_equal(Vector2
, vector_cross_product(Vector3
, Vector4
)),
810 Subtest4
= vectors_equal(Vector7
, vector_cross_product(Vector5
, Vector6
)),
811 Subtest5
= vectors_equal(
812 vector_cross_product(Vector7
,
813 vector_add(Vector8
, Vector9
)),
815 vector_cross_product(Vector7
, Vector8
),
816 vector_cross_product(Vector7
, Vector9
))),
817 Subtest6
= vectors_equal(Vector1
,
820 vector_cross_product(
822 vector_cross_product(Vector8
, Vector9
)),
823 vector_cross_product(
825 vector_cross_product(Vector9
, Vector7
))),
826 vector_cross_product(
828 vector_cross_product(Vector7
, Vector8
)))),
830 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
832 vector_normalization_test() ->
833 io:format("normalization test", []),
834 Vector1
= #vector
{x
=0, y
=0, z
=0},
835 Vector2
= #vector
{x
=1, y
=0, z
=0},
836 Vector3
= #vector
{x
=5, y
=0, z
=0},
838 Subtest1
= vectors_equal(Vector1
, vector_normalize(Vector1
)),
839 Subtest2
= vectors_equal(Vector2
, vector_normalize(Vector2
)),
840 Subtest3
= vectors_equal(Vector2
, vector_normalize(Vector3
)),
841 Subtest4
= vectors_equal(Vector2
, vector_normalize(
842 vector_scalar_mult(Vector2
, 324))),
844 Subtest1 and Subtest2 and Subtest3 and Subtest4
.
846 vector_negation_test() ->
847 io:format("vector negation test", []),
848 Vector1
= #vector
{x
=0, y
=0, z
=0},
849 Vector2
= #vector
{x
=4, y
=-5, z
=6},
851 Subtest1
= vectors_equal(Vector1
, vector_neg(Vector1
)),
852 Subtest2
= vectors_equal(Vector2
, vector_neg(vector_neg(Vector2
))),
854 Subtest1 and Subtest2
.
856 ray_shooting_test() ->
857 io:format("ray shooting test"),
858 Vector1
= #vector
{x
=0, y
=0, z
=0},
859 Vector2
= #vector
{x
=1, y
=0, z
=0},
861 Subtest1
= vectors_equal(
862 (shoot_ray(Vector1
, Vector2
))#ray
.direction
,
867 ray_sphere_intersection_test() ->
868 io:format("ray sphere intersection test", []),
872 center
=#vector
{x
= 0, y
=0, z
=10},
874 colour
=#colour
{r
=0.4, g
=0.4, b
=0.4}}},
876 origin
=#vector
{x
=0, y
=0, z
=0},
877 direction
=#vector
{x
=0, y
=0, z
=1}},
879 origin
=#vector
{x
=3, y
=0, z
=0},
880 direction
=#vector
{x
=0, y
=0, z
=1}},
882 origin
=#vector
{x
=4, y
=0, z
=0},
883 direction
=#vector
{x
=0, y
=0, z
=1}},
884 Subtest1
= ray_sphere_intersect(Ray1
, Sphere
) == 7.0,
885 Subtest2
= ray_sphere_intersect(Ray2
, Sphere
) == infinity
,
886 Subtest3
= ray_sphere_intersect(Ray3
, Sphere
) == infinity
,
887 Subtest1 and Subtest2 and Subtest3
.
889 point_on_screen_test() ->
890 io:format("point on screen test", []),
891 Camera1
= #camera
{location
=#vector
{x
=0, y
=0, z
=0},
892 rotation
=#vector
{x
=0, y
=0, z
=0},
894 screen
=#screen
{width
=1, height
=1}},
895 Camera2
= #camera
{location
=#vector
{x
=0, y
=0, z
=0},
896 rotation
=#vector
{x
=0, y
=0, z
=0},
898 screen
=#screen
{width
=640, height
=480}},
900 Subtest1
= vectors_equal(
901 #vector
{x
=0, y
=0, z
=0.5},
902 point_on_screen(0.5, 0.5, Camera1
)),
903 Subtest2
= vectors_equal(
904 #vector
{x
=-0.5, y
=-0.5, z
=0.5},
905 point_on_screen(0, 0, Camera1
)),
906 Subtest3
= vectors_equal(
907 #vector
{x
=0.5, y
=0.5, z
=0.5},
908 point_on_screen(1, 1, Camera1
)),
909 Subtest4
= vectors_equal(
910 point_on_screen(0, 0, Camera2
),
911 #vector
{x
=-320, y
=-240, z
=320}),
912 Subtest5
= vectors_equal(
913 point_on_screen(1, 1, Camera2
),
914 #vector
{x
=320, y
=240, z
=320}),
915 Subtest6
= vectors_equal(
916 point_on_screen(0.5, 0.5, Camera2
),
917 #vector
{x
=0, y
=0, z
=320}),
919 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
.
921 nearest_object_intersecting_ray_test() ->
922 io:format("nearest object intersecting ray test", []),
923 % test to make sure that we really get the closest object
924 Sphere1
=#sphere
{radius
=5,
925 center
=#vector
{x
=0, y
=0, z
=10},
927 colour
=#colour
{r
=0, g
=0, b
=0.03}}},
928 Sphere2
=#sphere
{radius
=5,
929 center
=#vector
{x
=0, y
=0, z
=20},
931 colour
=#colour
{r
=0, g
=0, b
=0.06}}},
932 Sphere3
=#sphere
{radius
=5,
933 center
=#vector
{x
=0, y
=0, z
=30},
935 colour
=#colour
{r
=0, g
=0, b
=0.09}}},
936 Sphere4
=#sphere
{radius
=5,
937 center
=#vector
{x
=0, y
=0, z
=-10},
939 colour
=#colour
{r
=0, g
=0, b
=-0.4}}},
940 Scene1
=[Sphere1
, Sphere2
, Sphere3
, Sphere4
],
941 Ray1
=#ray
{origin
=#vector
{x
=0, y
=0, z
=0},
942 direction
=#vector
{x
=0, y
=0, z
=1}},
944 {Object1
, Distance1
, Hit_location
, Normal
} = nearest_object_intersecting_ray(
946 Subtest1
= (Object1
== Sphere1
) and (Distance1
== 5)
947 and
vectors_equal(Normal
, vector_neg(Ray1#ray
.direction
))
948 and
point_on_sphere(Sphere1
, Hit_location
),
952 focal_length_test() ->
955 io:format("focal length test", []),
957 fun({Focal_length
, Dimension
}, Matches
) ->
958 %Result = focal_length(Dimension, Size),
959 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
961 and ((Focal_length
+ Epsilon
>= focal_length(
963 and (Focal_length
- Epsilon
=< focal_length(
966 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
968 object_normal_at_point_test() ->
969 io:format("object normal at point test"),
970 Sphere1
= #sphere
{radius
=13.5,
971 center
=#vector
{x
=0, y
=0, z
=0},
973 colour
=#colour
{r
=0, g
=0, b
=0}}},
974 Point1
= #vector
{x
=13.5, y
=0, z
=0},
975 Point2
= #vector
{x
=0, y
=13.5, z
=0},
976 Point3
= #vector
{x
=0, y
=0, z
=13.5},
977 Point4
= vector_neg(Point1
),
978 Point5
= vector_neg(Point2
),
979 Point6
= vector_neg(Point3
),
981 % sphere object tests
982 Subtest1
= vectors_equal(
983 vector_normalize(Point1
),
984 object_normal_at_point(Sphere1
, Point1
)),
985 Subtest2
= vectors_equal(
986 vector_normalize(Point2
),
987 object_normal_at_point(Sphere1
, Point2
)),
988 Subtest3
= vectors_equal(
989 vector_normalize(Point3
),
990 object_normal_at_point(Sphere1
, Point3
)),
991 Subtest4
= vectors_equal(
992 vector_normalize(Point4
),
993 object_normal_at_point(Sphere1
, Point4
)),
994 Subtest5
= vectors_equal(
995 vector_normalize(Point5
),
996 object_normal_at_point(Sphere1
, Point5
)),
997 Subtest6
= vectors_equal(
998 vector_normalize(Point6
),
999 object_normal_at_point(Sphere1
, Point6
)),
1000 Subtest7
= not
vectors_equal(
1001 vector_normalize(Point1
),
1002 object_normal_at_point(Sphere1
, Point4
)),
1004 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
1007 vector_bounce_off_plane_test() ->
1008 io:format("vector reflect about normal", []),
1009 Vector1
= #vector
{x
=1, y
=1, z
=0},
1010 Vector2
= #vector
{x
=0, y
=-1, z
=0},
1011 Vector3
= #vector
{x
=1, y
=-1, z
=0},
1012 Vector4
= #vector
{x
=1, y
=0, z
=0},
1014 Subtest1
= vectors_equal(vector_bounce_off_plane(
1016 vector_normalize(Vector2
)),
1019 Subtest2
= vectors_equal(
1020 vector_bounce_off_plane(
1022 vector_normalize(Vector1
)),
1025 Subtest1 and Subtest2
.