fixed the ray_sphere_intersection_test() to work with the new intersection function...
[eraytracer.git] / raytracer.erl
blob9ab36e89bf5bf008b3448dcaba378ed0b77d58b2
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 %% * three object types:
23 %% * spheres
24 %% * planes
25 %% * triangles (not done)
26 %% * point lights
27 %% * shadows
28 %% * lighting based on local illumination models
29 %% * ambient (not done)
30 %% * diffuse
31 %% * specular
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)
37 %% * concurrent
38 %% * specify how many pixels to give to each process
39 %% * distributed (across multiple computers)
41 %% Instructions
42 %% The simplest way to run this raytracer is through an Erlang shell with the go() function.
43 %% At the very least, the go function expects one parameter,
44 %% which tells it how to run the raytracer:
45 %% To execute in a simple, serial manner use:
46 %% > raytracer:go(simple)
47 %% To execute in a parallel manner (taking advantage of multiple CPUs available to the system) use:
48 %% > raytracer:go(concurrent)
49 %% To execute in a distributed manner use:
50 %% > raytracer:go(distributed)
51 %% For the last command to work you need to do some preparation of the nodes (computers) that will be running the raytracer. To same myself some time I'll assume that you know how to do that or can figure out how to do that by reading http://erlang.org/doc/getting_started/part_frame.html and then http://www.erlang.org/doc/man/pool.html .
52 %% Read the rest of the code for additional parameters that the go() function takes
53 %% See the example *.sh files for examples of standalone invocation
56 -module(raytracer).
57 -export([go/1,
58 go/5,
59 raytrace/1,
60 raytrace/5,
61 run_tests/0,
62 master/2,
63 worker/5,
64 distributed_worker/6,
65 standalone/1,
66 standalone/5,
67 raytraced_pixel_list_simple/4,
68 raytraced_pixel_list_concurrent/4,
69 raytraced_pixel_list_distributed/4
70 ]).
72 -record(vector, {x, y, z}).
73 -record(colour, {r, g, b}).
74 -record(ray, {origin, direction}).
75 -record(screen, {width, height}). % screen dimensions in the 3D world
76 -record(camera, {location, rotation, fov, screen}).
77 -record(material, {colour, specular_power, shininess, reflectivity}).
78 -record(sphere, {radius, center, material}).
79 -record(triangle, {v1, v2, v3, material}).
80 -record(plane, {normal, distance, material}).
81 -record(point_light, {diffuse_colour, location, specular_colour}).
82 -define(BACKGROUND_COLOUR, #colour{r=0, g=0, b=0}).
83 -define(UNKNOWN_COLOUR, #colour{r=0, g=1, b=0}).
84 -define(FOG_DISTANCE, 40).
86 raytraced_pixel_list_simple(0, 0, _, _) ->
87 done;
88 raytraced_pixel_list_simple(Width, Height, Scene, Recursion_depth)
89 when Width > 0, Height > 0 ->
90 lists:flatmap(
91 fun(Y) ->
92 lists:map(
93 fun(X) ->
94 % coordinates passed as a percentage
95 {1, colour_to_pixel(
96 trace_ray_through_pixel(
97 {X/Width, Y/Height}, Scene, Recursion_depth))} end,
98 lists:seq(0, Width - 1)) end,
99 lists:seq(0, Height - 1)).
101 raytraced_pixel_list_concurrent(0, 0, _, _) ->
102 done;
103 raytraced_pixel_list_concurrent(Width, Height, Scene, Recursion_depth)
104 when Width > 0, Height > 0 ->
105 Master_PID = spawn(raytracer, master, [self(), Width*Height]),
106 lists:flatmap(
107 fun(Y) ->
108 lists:map(
109 fun(X) ->
110 % coordinates passed as a percentage
111 spawn(raytracer, worker,
112 [Master_PID, X+Y*Width, {X/Width, Y/Height}, Scene, Recursion_depth]) end,
113 lists:seq(0, Width - 1)) end,
114 lists:seq(0, Height - 1)),
115 io:format("all workers have been spawned~n", []),
116 receive
117 Final_pixel_list ->
118 Final_pixel_list
119 end.
121 raytraced_pixel_list_distributed(0, 0, _, _) ->
122 done;
123 raytraced_pixel_list_distributed(Width, Height, Scene, Recursion_depth)
124 when Width > 0, Height > 0 ->
125 io:format("distributed tracing~n", []),
126 Pool_master = pool:start(renderslave),
127 io:format("Pool master is ~p~n", [Pool_master]),
128 io:format("Nodes are ~p~n", [pool:get_nodes()]),
129 Master_PID = pool:pspawn(raytracer, master, [self(), Width*Height]),
130 Pixels = [{X, Y} || X <- lists:seq(0, Width-1), Y <- lists:seq(0, Height-1)],
131 distribute_work(Pixels, trunc(Width*Height/64), Master_PID, Width, Height, Scene,
132 Recursion_depth),
133 io:format("all workers have been spawned~n", []),
134 receive
135 Final_pixel_list ->
136 Final_pixel_list
137 end.
139 distribute_work(Pixels, Pixels_per_worker, Master_PID, Width, Height, Scene,
140 Recursion_depth) when length(Pixels) > Pixels_per_worker ->
141 {To_work_on, The_rest} = lists:split(Pixels_per_worker, Pixels),
142 pool:pspawn(raytracer, distributed_worker,
143 [Master_PID, To_work_on, Width, Height, Scene, Recursion_depth]),
144 distribute_work(The_rest, Pixels_per_worker, Master_PID,
145 Width, Height, Scene, Recursion_depth);
146 distribute_work(Pixels, _Pixels_per_worker, Master_PID, Width, Height, Scene,
147 Recursion_depth) ->
148 pool:pspawn(raytracer, distributed_worker,
149 [Master_PID, Pixels, Width, Height, Scene, Recursion_depth]).
151 master(Program_PID, Pixel_count) ->
152 master(Program_PID, Pixel_count, []).
153 master(Program_PID, 0, Pixel_list) ->
154 io:format("master is done~n", []),
155 Program_PID ! lists:keysort(1, Pixel_list);
156 % assumes all workers eventually return a good value
157 master(Program_PID, Pixel_count, Pixel_list) ->
158 receive
159 Pixel_tuple ->
160 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
161 end.
164 % assumes X and Y are percentages of the screen dimensions
165 worker(Master_PID, Pixel_num, {X, Y}, Scene, Recursion_depth) ->
166 Master_PID ! {Pixel_num,
167 colour_to_pixel(trace_ray_through_pixel({X, Y}, Scene, Recursion_depth))}.
169 distributed_worker(Master_PID, Pixels, Width, Height, Scene, Recursion_depth) ->
170 %io:format("~pworker doing ~p pixels=~p~n", [node(), length(Pixels), Pixels]),
171 lists:foreach(
172 fun({X, Y}) ->
173 Master_PID ! {X+Y*Width,
174 colour_to_pixel(
175 trace_ray_through_pixel(
176 {X/Width, Y/Height}, Scene, Recursion_depth))}
177 end,
178 Pixels).
180 trace_ray_through_pixel({X, Y}, [Camera|Rest_of_scene], Recursion_depth) ->
181 pixel_colour_from_ray(
182 ray_through_pixel(X, Y, Camera),
183 Rest_of_scene,
184 Recursion_depth).
186 pixel_colour_from_ray(_Ray, _Scene, 0) ->
187 #colour{r=0, g=0, b=0};
188 pixel_colour_from_ray(Ray, Scene, Recursion_depth) ->
189 case nearest_object_intersecting_ray(Ray, Scene) of
190 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
191 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
193 vector_to_colour(lighting_function(Ray,
194 Nearest_object,
195 Hit_location,
196 Hit_normal,
197 Scene,
198 Recursion_depth));
199 _Else ->
200 ?BACKGROUND_COLOUR
201 end.
203 % my own illumination formula
204 % ideas were borrowed from:
205 % http://www.devmaster.net/wiki/Lighting
206 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
207 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
208 Recursion_depth) ->
209 lists:foldl(
210 fun (#point_light{diffuse_colour=Light_colour,
211 location=Light_location,
212 specular_colour=Specular_colour},
213 Final_colour) ->
214 Reflection = vector_scalar_mult(
215 colour_to_vector(
216 pixel_colour_from_ray(
217 #ray{origin=Hit_location,
218 direction=vector_bounce_off_plane(
219 Ray#ray.direction, Hit_normal)},
220 Scene,
221 Recursion_depth-1)),
222 object_reflectivity(Object)),
223 Light_contribution = vector_add(
224 diffuse_term(
225 Object,
226 Light_location,
227 Hit_location,
228 Hit_normal),
229 specular_term(
230 Ray#ray.direction,
231 Light_location,
232 Hit_location,
233 Hit_normal,
234 object_specular_power(Object),
235 object_shininess(Object),
236 Specular_colour)),
237 vector_add(
238 Final_colour,
239 vector_add(
240 Reflection,
241 vector_scalar_mult(
242 vector_component_mult(
243 colour_to_vector(Light_colour),
244 Light_contribution),
245 shadow_factor(Light_location, Hit_location, Scene))));
246 (_Not_a_point_light, Final_colour) ->
247 Final_colour
248 end,
249 #vector{x=0, y=0, z=0},
250 Scene).
252 % returns 0 when the Hit_location is completely occluded from light
253 % at Light_location
254 % otherwise returns 1
255 shadow_factor(Light_location, Hit_location, Scene) ->
256 Light_vector = vector_sub(Light_location, Hit_location),
257 Light_vector_length = vector_mag(Light_vector),
258 Light_direction = vector_normalize(Light_vector),
259 % start the ray a little bit farther to prevent artefacts due to unit precision limitations
260 Shadow_ray = #ray{origin=vector_add(
261 Hit_location,
262 vector_scalar_mult(
263 Light_direction,
264 0.001)),
265 direction=Light_direction},
266 case nearest_object_intersecting_ray(Shadow_ray, Scene) of
267 {_Obj, Distance, _Loc, _Normal} ->
268 if Distance == infinity ->
270 Light_vector_length > Distance ->
272 true ->
274 end;
275 none ->
277 end.
279 % based on
280 % http://www.devmaster.net/wiki/Lambert_diffuse_lighting
281 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
282 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
283 vector_scalar_mult(
284 colour_to_vector(object_diffuse_colour(Object)),
285 lists:max([0,
286 vector_dot_product(Hit_normal,
287 vector_normalize(
288 vector_sub(Light_location,
289 Hit_location)))])).
291 % based on
292 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
293 % http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_2_Phong_Mirrors_and_Shadows.shtml
294 % http://www.devmaster.net/wiki/Phong_shading
295 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
296 Specular_power, Shininess, Specular_colour) ->
297 vector_scalar_mult(
298 colour_to_vector(Specular_colour),
299 Shininess*math:pow(
300 lists:max([0,
301 vector_dot_product(
302 vector_normalize(
303 vector_add(
304 vector_normalize(
305 vector_sub(Light_location, Hit_location)),
306 vector_neg(EyeVector))),
307 Hit_normal)]), Specular_power)).
309 % object agnostic intersection function
310 nearest_object_intersecting_ray(Ray, Scene) ->
311 nearest_object_intersecting_ray(
312 Ray, none, hitlocation, hitnormal, infinity, Scene).
313 nearest_object_intersecting_ray(
314 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
315 none;
316 nearest_object_intersecting_ray(
317 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
318 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
319 {NearestObj, Distance, Hit_location, Normal};
320 nearest_object_intersecting_ray(Ray,
321 NearestObj,
322 Hit_location,
323 Normal,
324 Distance,
325 [CurrentObject|Rest_of_scene]) ->
326 case ray_object_intersect(Ray, CurrentObject) of
327 {NewDistance, New_hit_location, New_normal} ->
328 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
329 if (Distance == infinity) or (Distance > NewDistance) ->
330 %io:format("another closer object found~n", []),
331 nearest_object_intersecting_ray(
332 Ray,
333 CurrentObject,
334 New_hit_location,
335 New_normal,
336 NewDistance,
337 Rest_of_scene);
338 true ->
339 %io:format("no closer obj found~n", []),
340 nearest_object_intersecting_ray(
341 Ray,
342 NearestObj,
343 Hit_location,
344 Normal,
345 Distance,
346 Rest_of_scene)
347 end;
348 none ->
349 nearest_object_intersecting_ray(
350 Ray,
351 NearestObj,
352 Hit_location,
353 Normal,
354 Distance,
355 Rest_of_scene)
356 end.
358 % object specific intersection function
359 ray_object_intersect(Ray, Object) ->
360 case Object of
361 #sphere{} ->
362 ray_sphere_intersect(Ray, Object);
363 #triangle{} ->
364 ray_triangle_intersect(Ray, Object);
365 #plane{} ->
366 ray_plane_intersect(Ray, Object);
367 _Else ->
368 none
369 end.
371 % based on
372 % http://www.devmaster.net/articles/raytracing/
373 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter1.htm
374 ray_sphere_intersect(
375 #ray{origin=#vector{
376 x=X0, y=Y0, z=Z0},
377 direction=#vector{
378 x=Xd, y=Yd, z=Zd}},
379 #sphere{radius=Radius, center=#vector{
380 x=Xc, y=Yc, z=Zc}}) ->
381 Epsilon = 0.001,
382 A = Xd*Xd + Yd*Yd + Zd*Zd,
383 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
384 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
385 Discriminant = B*B - 4*A*C,
386 %io:format("A=~w B=~w C=~w discriminant=~w~n",
387 % [A, B, C, Discriminant]),
388 if Discriminant >= Epsilon ->
389 T0 = (-B + math:sqrt(Discriminant))/2,
390 T1 = (-B - math:sqrt(Discriminant))/2,
391 if (T0 >= 0) and (T1 >= 0) ->
392 %io:format("T0=~w T1=~w~n", [T0, T1]),
393 Distance = lists:min([T0, T1]),
394 Intersection = vector_add(
395 #vector{x=X0, y=Y0, z=Z0},
396 vector_scalar_mult(
397 #vector{x=Xd, y=Yd, z=Zd}, Distance)),
398 Normal = vector_normalize(
399 vector_sub(Intersection,
400 #vector{x=Xc, y=Yc, z=Zc})),
401 {Distance, Intersection, Normal};
402 true ->
403 none
404 end;
405 true ->
406 none
407 end.
409 % based on
410 % http://www.graphics.cornell.edu/pubs/1997/MT97.html
411 % http://jgt.akpeters.com/papers/GuigueDevillers03/addendum.html
412 ray_triangle_intersect(Ray, Triangle) ->
413 Epsilon = 0.000001,
415 % find vectors for two edges sharing v1
416 Edge1 = vector_sub(Triangle#triangle.v2, Triangle#triangle.v1),
417 Edge2 = vector_sub(Triangle#triangle.v3, Triangle#triangle.v1),
419 % begin calculating determinant
420 P = vector_cross_product(Ray#ray.direction, Edge2),
421 Determinant = vector_dot_product(Edge1, P),
423 % negative determinant means the triangle is facing away
424 % from the ray
426 if Determinant < Epsilon ->
427 % for our purposes we ignore such triangles
428 %% io:format("ray is either behind or on the triangle: ~p~n", [Determinant]),
429 none;
430 true ->
431 % calculate the distance from v1 to ray origin
432 T = vector_sub(Ray#ray.origin, Triangle#triangle.v1),
434 % calculate the U parameter and test bounds
435 U = vector_dot_product(T, P),
436 if (U < 0) or (U > Determinant) ->
437 %% io:format("U is negative or greater than det: ~p~n", [U]),
438 none;
439 true ->
440 % prepare to test the V parameter
441 Q = vector_cross_product(T, Edge1),
442 % calculate the V parameter and test bounds
443 V = vector_dot_product(Ray#ray.direction, Q),
444 if (V < 0) or (U+V > Determinant) ->
445 %% io:format("V less than 0.0 or U+V greater than det: ~p ~p~n",
446 %% [U, V]),
447 none;
448 true ->
449 % calculate the distance to the
450 % intersection point and return
451 %% io:format("found ray/triangle intersection ~n", []),
452 Distance = vector_dot_product(Edge2, Q) / Determinant,
453 Intersection = vector_add(
454 Ray#ray.origin,
455 vector_scalar_mult(
456 Ray#ray.direction,
457 Distance)),
458 Normal = vector_normalize(
459 vector_sub(
460 Triangle#triangle.v1,
461 Triangle#triangle.v2)),
462 {Distance, Intersection, Normal}
465 end.
467 % based on
468 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
469 % http://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm
470 % http://www.devmaster.net/articles/raytracing/
471 ray_plane_intersect(Ray, Plane) ->
472 Epsilon = 0.001,
473 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
474 if Vd < 0 ->
475 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
476 + Plane#plane.distance),
477 Distance = V0 / Vd,
478 if Distance < Epsilon ->
479 none;
480 true ->
481 Intersection = vector_add(
482 Ray#ray.origin,
483 vector_scalar_mult(
484 Ray#ray.direction,
485 Distance)),
486 {Distance, Intersection, Plane#plane.normal}
487 end;
488 true ->
489 none
490 end.
493 focal_length(Angle, Dimension) ->
494 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
496 point_on_screen(X, Y, Camera) ->
497 %TODO: implement rotation (using quaternions)
498 Screen_width = (Camera#camera.screen)#screen.width,
499 Screen_height = (Camera#camera.screen)#screen.height,
500 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
501 Camera#camera.location,
502 [vector_scalar_mult(
503 #vector{x=0, y=0, z=1},
504 focal_length(
505 Camera#camera.fov,
506 Screen_width)),
507 #vector{x = (X-0.5) * Screen_width,
508 y=0,
509 z=0},
510 #vector{x=0,
511 y= (Y-0.5) * Screen_height,
512 z=0}
516 shoot_ray(From, Through) ->
517 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
519 % assume that X and Y are percentages of the 3D world screen dimensions
520 ray_through_pixel(X, Y, Camera) ->
521 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
523 vectors_equal(V1, V2) ->
524 vectors_equal(V1, V2, 0.0001).
525 vectors_equal(V1, V2, Epsilon) ->
526 (V1#vector.x + Epsilon >= V2#vector.x)
527 and (V1#vector.x - Epsilon =<V2#vector.x)
528 and (V1#vector.y + Epsilon >= V2#vector.y)
529 and (V1#vector.y - Epsilon =<V2#vector.y)
530 and (V1#vector.z + Epsilon >= V2#vector.z)
531 and (V1#vector.z - Epsilon =<V2#vector.z).
534 vector_add(V1, V2) ->
535 #vector{x = V1#vector.x + V2#vector.x,
536 y = V1#vector.y + V2#vector.y,
537 z = V1#vector.z + V2#vector.z}.
539 vector_sub(V1, V2) ->
540 #vector{x = V1#vector.x - V2#vector.x,
541 y = V1#vector.y - V2#vector.y,
542 z = V1#vector.z - V2#vector.z}.
544 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
545 X*X + Y*Y + Z*Z.
547 vector_mag(V) ->
548 math:sqrt(vector_square_mag(V)).
550 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
551 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
553 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
554 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
556 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
557 A1*B1 + A2*B2 + A3*B3.
559 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
560 #vector{x = A2*B3 - A3*B2,
561 y = A3*B1 - A1*B3,
562 z = A1*B2 - A2*B1}.
564 vector_normalize(V) ->
565 Mag = vector_mag(V),
566 if Mag == 0 ->
567 #vector{x=0, y=0, z=0};
568 true ->
569 vector_scalar_mult(V, 1/vector_mag(V))
570 end.
572 vector_neg(#vector{x=X, y=Y, z=Z}) ->
573 #vector{x=-X, y=-Y, z=-Z}.
575 % based on
576 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtreflec.htm
577 % http://www.devmaster.net/articles/raytracing/
578 vector_bounce_off_plane(Vector, Normal) ->
579 vector_add(
580 vector_scalar_mult(
581 Normal,
582 2*vector_dot_product(Normal, vector_neg(Vector))),
583 Vector).
585 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
587 object_diffuse_colour(#plane{material=#material{colour=C}}) ->
589 object_diffuse_colour(#triangle{material=#material{colour=C}}) ->
592 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
594 object_specular_power(#plane{material=#material{specular_power=SP}}) ->
596 object_specular_power(#triangle{material=#material{specular_power=SP}}) ->
599 object_shininess(#sphere{material=#material{shininess=S}}) ->
601 object_shininess(#plane{material=#material{shininess=S}}) ->
603 object_shininess(#triangle{material=#material{shininess=S}}) ->
606 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
608 object_reflectivity(#plane{material=#material{reflectivity=R}}) ->
610 object_reflectivity(#triangle{material=#material{reflectivity=R}}) ->
613 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
614 #vector{x=X, y=Y, z=Z}) ->
615 Epsilon = 0.001,
616 Epsilon > abs(
617 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
619 colour_to_vector(#colour{r=R, g=G, b=B}) ->
620 #vector{x=R, y=G, z=B}.
621 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
622 #colour{r=X, g=Y, b=Z}.
623 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
624 {R, G, B}.
626 % returns a list of objects in the scene
627 % camera is assumed to be the first element in the scene
628 scene() ->
629 [#camera{location=#vector{x=0, y=0, z=-2},
630 rotation=#vector{x=0, y=0, z=0},
631 fov=90,
632 screen=#screen{width=4, height=3}},
633 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
634 location=#vector{x=5, y=-2, z=0},
635 specular_colour=#colour{r=1, g=1, b=1}},
636 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
637 location=#vector{x=-10, y=0, z=7},
638 specular_colour=#colour{r=1, g=0, b=0.5}},
639 #sphere{radius=4,
640 center=#vector{x=4, y=0, z=10},
641 material=#material{
642 colour=#colour{r=0, g=0.5, b=1},
643 specular_power=20,
644 shininess=1,
645 reflectivity=0.1}},
646 #sphere{radius=4,
647 center=#vector{x=-5, y=3, z=9},
648 material=#material{
649 colour=#colour{r=1, g=0.5, b=0},
650 specular_power=4,
651 shininess=0.25,
652 reflectivity=0.5}},
653 #sphere{radius=4,
654 center=#vector{x=-4.5, y=-2.5, z=14},
655 material=#material{
656 colour=#colour{r=0.5, g=1, b=0},
657 specular_power=20,
658 shininess=0.25,
659 reflectivity=0.7}},
660 #triangle{v1=#vector{x=-2, y=5, z=5},
661 v2=#vector{x=4, y=5, z=10},
662 v3=#vector{x=4, y=-5, z=10},
663 material=#material{
664 colour=#colour{r=1, g=0.5, b=0},
665 specular_power=4,
666 shininess=0.25,
667 reflectivity=0.5}},
668 #plane{normal=#vector{x=0, y=-1, z=0},
669 distance=5,
670 material=#material{
671 colour=#colour{r=1, g=1, b=1},
672 specular_power=1,
673 shininess=0,
674 reflectivity=0.01}}
677 % assumes Pixels are ordered in a row by row fasion
678 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
679 case file:open(Filename, write) of
680 {ok, IoDevice} ->
681 io:format("file opened~n", []),
682 io:format(IoDevice, "P3~n", []),
683 io:format(IoDevice, "~p ~p~n", [Width, Height]),
684 io:format(IoDevice, "~p~n", [MaxValue]),
685 lists:foreach(
686 fun({_Num, {R, G, B}}) ->
687 io:format(IoDevice, "~p ~p ~p ",
688 [lists:min([trunc(R*MaxValue), MaxValue]),
689 lists:min([trunc(G*MaxValue), MaxValue]),
690 lists:min([trunc(B*MaxValue), MaxValue])]) end,
691 Pixels),
692 file:close(IoDevice);
693 error ->
694 io:format("error opening file~n", [])
695 end.
697 % various invocation style functions
698 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
699 standalone(list_to_integer(Width),
700 list_to_integer(Height),
701 Filename,
702 list_to_integer(Recursion_depth),
703 tracing_function(list_to_atom(Strategy))).
705 standalone(Width, Height, Filename, Recursion_depth, Function) ->
706 {Time, _Value} = timer:tc(
707 raytracer,
708 raytrace,
709 [Width,
710 Height,
711 Filename,
712 Recursion_depth,
713 Function]),
714 io:format("Done in ~w seconds~n", [Time/1000000]),
715 halt().
717 go(Strategy) ->
718 raytrace(tracing_function(Strategy)).
720 go(Width, Height, Filename, Recursion_depth, Strategy) ->
721 raytrace(Width, Height, Filename, Recursion_depth,
722 tracing_function(Strategy)).
724 tracing_function(simple) ->
725 fun raytraced_pixel_list_simple/4;
726 tracing_function(concurrent) ->
727 fun raytraced_pixel_list_concurrent/4;
728 tracing_function(distributed) ->
729 fun raytraced_pixel_list_distributed/4.
731 raytrace(Function) ->
732 raytrace(4, 3, "/tmp/traced.ppm", 5, Function).
733 raytrace(Width, Height, Filename, Recursion_depth, Function) ->
734 write_pixels_to_ppm(
735 Width,
736 Height,
737 255,
738 Function(
739 Width,
740 Height,
741 scene(),
742 Recursion_depth),
743 Filename).
745 % testing
746 run_tests() ->
747 Tests = [fun scene_test/0,
748 fun passing_test/0,
749 fun vector_equality_test/0,
750 fun vector_addition_test/0,
751 fun vector_subtraction_test/0,
752 fun vector_square_mag_test/0,
753 fun vector_mag_test/0,
754 fun vector_scalar_multiplication_test/0,
755 fun vector_dot_product_test/0,
756 fun vector_cross_product_test/0,
757 fun vector_normalization_test/0,
758 fun vector_negation_test/0,
759 % fun ray_through_pixel_test/0,
760 fun ray_shooting_test/0,
761 fun point_on_screen_test/0,
762 fun nearest_object_intersecting_ray_test/0,
763 fun focal_length_test/0,
764 % fun vector_rotation_test/0,
765 fun vector_bounce_off_plane_test/0,
766 fun ray_sphere_intersection_test/0
768 run_tests(Tests, 1, true).
770 scene_test() ->
771 io:format("testing the scene function", []),
772 case scene() of
773 [{camera,
774 {vector, 0, 0, -2},
775 {vector, 0, 0, 0},
777 {screen, 4, 3}},
778 {point_light,
779 {colour, 1, 1, 0.5},
780 {vector, 5, -2, 0},
781 {colour, 1, 1, 1}},
782 {point_light,
783 {colour, 1, 0, 0.5},
784 {vector, -10, 0, 7},
785 {colour, 1, 0, 0.5}},
786 {sphere,
788 {vector, 4, 0, 10},
789 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
790 {sphere,
792 {vector, -5, 3, 9},
793 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
794 {sphere,
796 {vector, -4.5, -2.5, 14},
797 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
798 {triangle,
799 {vector, -2, 5, 5},
800 {vector, 4, 5, 10},
801 {vector, 4, -5, 10},
802 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
803 {plane,
804 {vector, 0, -1, 0},
806 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
807 ] ->
808 true;
809 _Else ->
810 false
811 end.
813 passing_test() ->
814 io:format("this test always passes", []),
815 true.
817 run_tests([], _Num, Success) ->
818 case Success of
819 true ->
820 io:format("Success!~n", []),
822 _Else ->
823 io:format("some tests failed~n", []),
824 failed
825 end;
827 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
828 io:format("test #~p: ", [Num]),
829 Current_success = First_test(),
830 case Current_success of
831 true ->
832 io:format(" - OK~n", []);
833 _Else ->
834 io:format(" - FAILED~n", [])
835 end,
836 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
838 vector_equality_test() ->
839 io:format("vector equality"),
840 Vector1 = #vector{x=0, y=0, z=0},
841 Vector2 = #vector{x=1234, y=-234, z=0},
842 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
843 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
844 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
845 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
847 Subtest1 = vectors_equal(Vector1, Vector1)
848 and vectors_equal(Vector2, Vector2)
849 and not (vectors_equal(Vector1, Vector2))
850 and not (vectors_equal(Vector2, Vector1)),
851 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
852 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
854 Subtest1 and Subtest2 and Subtest3.
857 vector_addition_test() ->
858 io:format("vector addition", []),
859 Vector0 = vector_add(
860 #vector{x=3, y=7, z=-3},
861 #vector{x=0, y=-24, z=123}),
862 Subtest1 = (Vector0#vector.x == 3)
863 and (Vector0#vector.y == -17)
864 and (Vector0#vector.z == 120),
866 Vector1 = #vector{x=5, y=0, z=984},
867 Vector2 = vector_add(Vector1, Vector1),
868 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
869 and (Vector2#vector.y == Vector1#vector.y*2)
870 and (Vector2#vector.z == Vector1#vector.z*2),
872 Vector3 = #vector{x=908, y=-098, z=234},
873 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
874 Subtest3 = vectors_equal(Vector3, Vector4),
876 Subtest1 and Subtest2 and Subtest3.
878 vector_subtraction_test() ->
879 io:format("vector subtraction", []),
880 Vector1 = #vector{x=0, y=0, z=0},
881 Vector2 = #vector{x=8390, y=-2098, z=939},
882 Vector3 = #vector{x=1, y=1, z=1},
883 Vector4 = #vector{x=-1, y=-1, z=-1},
885 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
886 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
887 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
888 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
889 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
890 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
891 vector_sub(Vector2, Vector3)),
893 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
895 vector_square_mag_test() ->
896 io:format("vector square magnitude test", []),
897 Vector1 = #vector{x=0, y=0, z=0},
898 Vector2 = #vector{x=1, y=1, z=1},
899 Vector3 = #vector{x=3, y=-4, z=0},
901 Subtest1 = (0 == vector_square_mag(Vector1)),
902 Subtest2 = (3 == vector_square_mag(Vector2)),
903 Subtest3 = (25 == vector_square_mag(Vector3)),
905 Subtest1 and Subtest2 and Subtest3.
907 vector_mag_test() ->
908 io:format("vector magnitude test", []),
909 Vector1 = #vector{x=0, y=0, z=0},
910 Vector2 = #vector{x=1, y=1, z=1},
911 Vector3 = #vector{x=3, y=-4, z=0},
913 Subtest1 = (0 == vector_mag(Vector1)),
914 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
915 Subtest3 = (5 == vector_mag(Vector3)),
917 Subtest1 and Subtest2 and Subtest3.
919 vector_scalar_multiplication_test() ->
920 io:format("scalar multiplication test", []),
921 Vector1 = #vector{x=0, y=0, z=0},
922 Vector2 = #vector{x=1, y=1, z=1},
923 Vector3 = #vector{x=3, y=-4, z=0},
925 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
926 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
927 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
928 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
929 vector_scalar_mult(Vector2, 4)),
930 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
931 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
933 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
935 vector_dot_product_test() ->
936 io:format("dot product test", []),
937 Vector1 = #vector{x=1, y=3, z=-5},
938 Vector2 = #vector{x=4, y=-2, z=-1},
939 Vector3 = #vector{x=0, y=0, z=0},
940 Vector4 = #vector{x=1, y=0, z=0},
941 Vector5 = #vector{x=0, y=1, z=0},
943 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
944 Subtest2 = vector_dot_product(Vector2, Vector2)
945 == vector_square_mag(Vector2),
946 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
947 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
949 Subtest1 and Subtest2 and Subtest3 and Subtest4.
951 vector_cross_product_test() ->
952 io:format("cross product test", []),
953 Vector1 = #vector{x=0, y=0, z=0},
954 Vector2 = #vector{x=1, y=0, z=0},
955 Vector3 = #vector{x=0, y=1, z=0},
956 Vector4 = #vector{x=0, y=0, z=1},
957 Vector5 = #vector{x=1, y=2, z=3},
958 Vector6 = #vector{x=4, y=5, z=6},
959 Vector7 = #vector{x=-3, y=6, z=-3},
960 Vector8 = #vector{x=-1, y=0, z=0},
961 Vector9 = #vector{x=-9, y=8, z=433},
963 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
964 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
965 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
966 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
967 Subtest5 = vectors_equal(
968 vector_cross_product(Vector7,
969 vector_add(Vector8, Vector9)),
970 vector_add(
971 vector_cross_product(Vector7, Vector8),
972 vector_cross_product(Vector7, Vector9))),
973 Subtest6 = vectors_equal(Vector1,
974 vector_add(
975 vector_add(
976 vector_cross_product(
977 Vector7,
978 vector_cross_product(Vector8, Vector9)),
979 vector_cross_product(
980 Vector8,
981 vector_cross_product(Vector9, Vector7))),
982 vector_cross_product(
983 Vector9,
984 vector_cross_product(Vector7, Vector8)))),
986 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
988 vector_normalization_test() ->
989 io:format("normalization test", []),
990 Vector1 = #vector{x=0, y=0, z=0},
991 Vector2 = #vector{x=1, y=0, z=0},
992 Vector3 = #vector{x=5, y=0, z=0},
994 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
995 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
996 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
997 Subtest4 = vectors_equal(Vector2, vector_normalize(
998 vector_scalar_mult(Vector2, 324))),
1000 Subtest1 and Subtest2 and Subtest3 and Subtest4.
1002 vector_negation_test() ->
1003 io:format("vector negation test", []),
1004 Vector1 = #vector{x=0, y=0, z=0},
1005 Vector2 = #vector{x=4, y=-5, z=6},
1007 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
1008 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
1010 Subtest1 and Subtest2.
1012 ray_shooting_test() ->
1013 io:format("ray shooting test"),
1014 Vector1 = #vector{x=0, y=0, z=0},
1015 Vector2 = #vector{x=1, y=0, z=0},
1017 Subtest1 = vectors_equal(
1018 (shoot_ray(Vector1, Vector2))#ray.direction,
1019 Vector2),
1021 Subtest1.
1023 ray_sphere_intersection_test() ->
1024 io:format("ray sphere intersection test", []),
1026 Sphere = #sphere{
1027 radius=3,
1028 center=#vector{x = 0, y=0, z=10},
1029 material=#material{
1030 colour=#colour{r=0.4, g=0.4, b=0.4}}},
1031 Ray1 = #ray{
1032 origin=#vector{x=0, y=0, z=0},
1033 direction=#vector{x=0, y=0, z=1}},
1034 Ray2 = #ray{
1035 origin=#vector{x=3, y=0, z=0},
1036 direction=#vector{x=0, y=0, z=1}},
1037 Ray3 = #ray{
1038 origin=#vector{x=4, y=0, z=0},
1039 direction=#vector{x=0, y=0, z=1}},
1040 {Distance1, _Hit_location1, _Hit_normal1} = ray_sphere_intersect(Ray1, Sphere),
1041 Subtest1 = Distance1 == 7.0,
1042 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == none,
1043 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == none,
1044 Subtest1 and Subtest2 and Subtest3.
1046 point_on_screen_test() ->
1047 io:format("point on screen test", []),
1048 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
1049 rotation=#vector{x=0, y=0, z=0},
1050 fov=90,
1051 screen=#screen{width=1, height=1}},
1052 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
1053 rotation=#vector{x=0, y=0, z=0},
1054 fov=90,
1055 screen=#screen{width=640, height=480}},
1057 Subtest1 = vectors_equal(
1058 #vector{x=0, y=0, z=0.5},
1059 point_on_screen(0.5, 0.5, Camera1)),
1060 Subtest2 = vectors_equal(
1061 #vector{x=-0.5, y=-0.5, z=0.5},
1062 point_on_screen(0, 0, Camera1)),
1063 Subtest3 = vectors_equal(
1064 #vector{x=0.5, y=0.5, z=0.5},
1065 point_on_screen(1, 1, Camera1)),
1066 Subtest4 = vectors_equal(
1067 point_on_screen(0, 0, Camera2),
1068 #vector{x=-320, y=-240, z=320}),
1069 Subtest5 = vectors_equal(
1070 point_on_screen(1, 1, Camera2),
1071 #vector{x=320, y=240, z=320}),
1072 Subtest6 = vectors_equal(
1073 point_on_screen(0.5, 0.5, Camera2),
1074 #vector{x=0, y=0, z=320}),
1076 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
1078 nearest_object_intersecting_ray_test() ->
1079 io:format("nearest object intersecting ray test", []),
1080 % test to make sure that we really get the closest object
1081 Sphere1=#sphere{radius=5,
1082 center=#vector{x=0, y=0, z=10},
1083 material=#material{
1084 colour=#colour{r=0, g=0, b=0.03}}},
1085 Sphere2=#sphere{radius=5,
1086 center=#vector{x=0, y=0, z=20},
1087 material=#material{
1088 colour=#colour{r=0, g=0, b=0.06}}},
1089 Sphere3=#sphere{radius=5,
1090 center=#vector{x=0, y=0, z=30},
1091 material=#material{
1092 colour=#colour{r=0, g=0, b=0.09}}},
1093 Sphere4=#sphere{radius=5,
1094 center=#vector{x=0, y=0, z=-10},
1095 material=#material{
1096 colour=#colour{r=0, g=0, b=-0.4}}},
1097 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
1098 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
1099 direction=#vector{x=0, y=0, z=1}},
1101 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
1102 Ray1, Scene1),
1103 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
1104 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
1105 and point_on_sphere(Sphere1, Hit_location),
1107 Subtest1.
1109 focal_length_test() ->
1110 Epsilon = 0.1,
1111 Size = 36,
1112 io:format("focal length test", []),
1113 lists:foldl(
1114 fun({Focal_length, Dimension}, Matches) ->
1115 %Result = focal_length(Dimension, Size),
1116 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
1117 Matches
1118 and ((Focal_length + Epsilon >= focal_length(
1119 Dimension, Size))
1120 and (Focal_length - Epsilon =< focal_length(
1121 Dimension, Size)))
1122 end, true,
1123 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
1125 vector_bounce_off_plane_test() ->
1126 io:format("vector reflect about normal", []),
1127 Vector1 = #vector{x=1, y=1, z=0},
1128 Vector2 = #vector{x=0, y=-1, z=0},
1129 Vector3 = #vector{x=1, y=-1, z=0},
1130 Vector4 = #vector{x=1, y=0, z=0},
1132 Subtest1 = vectors_equal(vector_bounce_off_plane(
1133 Vector1,
1134 vector_normalize(Vector2)),
1135 Vector3),
1137 Subtest2 = vectors_equal(
1138 vector_bounce_off_plane(
1139 Vector2,
1140 vector_normalize(Vector1)),
1141 Vector4),
1143 Subtest1 and Subtest2.