changed shadow_factor() to handle one-sided objects
[eraytracer.git] / raytracer.erl
blob1d56baeb4ba25ba80ff5f888711204c25b87b1dc
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, Object, 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 if Object is occluded from the light at Light_location, otherwise
253 % returns 1 if light can see Object
254 shadow_factor(Light_location, Hit_location, Object, Scene) ->
255 Light_vector = vector_sub(Hit_location, Light_location),
256 Light_vector_length = vector_mag(Light_vector),
257 Light_direction = vector_normalize(Light_vector),
258 Shadow_ray = #ray{origin=Light_location,
259 direction=Light_direction},
260 case nearest_object_intersecting_ray(Shadow_ray, Scene) of
261 % this could match another copy of the same object
262 {Object, Distance, _Loc, _Normal} ->
264 _Else ->
266 end.
268 % based on
269 % http://www.devmaster.net/wiki/Lambert_diffuse_lighting
270 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
271 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
272 vector_scalar_mult(
273 colour_to_vector(object_diffuse_colour(Object)),
274 lists:max([0,
275 vector_dot_product(Hit_normal,
276 vector_normalize(
277 vector_sub(Light_location,
278 Hit_location)))])).
280 % based on
281 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
282 % http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_2_Phong_Mirrors_and_Shadows.shtml
283 % http://www.devmaster.net/wiki/Phong_shading
284 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
285 Specular_power, Shininess, Specular_colour) ->
286 vector_scalar_mult(
287 colour_to_vector(Specular_colour),
288 Shininess*math:pow(
289 lists:max([0,
290 vector_dot_product(
291 vector_normalize(
292 vector_add(
293 vector_normalize(
294 vector_sub(Light_location, Hit_location)),
295 vector_neg(EyeVector))),
296 Hit_normal)]), Specular_power)).
298 % object agnostic intersection function
299 nearest_object_intersecting_ray(Ray, Scene) ->
300 nearest_object_intersecting_ray(
301 Ray, none, hitlocation, hitnormal, infinity, Scene).
302 nearest_object_intersecting_ray(
303 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
304 none;
305 nearest_object_intersecting_ray(
306 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
307 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
308 {NearestObj, Distance, Hit_location, Normal};
309 nearest_object_intersecting_ray(Ray,
310 NearestObj,
311 Hit_location,
312 Normal,
313 Distance,
314 [CurrentObject|Rest_of_scene]) ->
315 case ray_object_intersect(Ray, CurrentObject) of
316 {NewDistance, New_hit_location, New_normal} ->
317 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
318 if (Distance == infinity) or (Distance > NewDistance) ->
319 %io:format("another closer object found~n", []),
320 nearest_object_intersecting_ray(
321 Ray,
322 CurrentObject,
323 New_hit_location,
324 New_normal,
325 NewDistance,
326 Rest_of_scene);
327 true ->
328 %io:format("no closer obj found~n", []),
329 nearest_object_intersecting_ray(
330 Ray,
331 NearestObj,
332 Hit_location,
333 Normal,
334 Distance,
335 Rest_of_scene)
336 end;
337 none ->
338 nearest_object_intersecting_ray(
339 Ray,
340 NearestObj,
341 Hit_location,
342 Normal,
343 Distance,
344 Rest_of_scene)
345 end.
347 % object specific intersection function
348 ray_object_intersect(Ray, Object) ->
349 case Object of
350 #sphere{} ->
351 ray_sphere_intersect(Ray, Object);
352 #triangle{} ->
353 ray_triangle_intersect(Ray, Object);
354 #plane{} ->
355 ray_plane_intersect(Ray, Object);
356 _Else ->
357 none
358 end.
360 % based on
361 % http://www.devmaster.net/articles/raytracing/
362 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter1.htm
363 ray_sphere_intersect(
364 #ray{origin=#vector{
365 x=X0, y=Y0, z=Z0},
366 direction=#vector{
367 x=Xd, y=Yd, z=Zd}},
368 #sphere{radius=Radius, center=#vector{
369 x=Xc, y=Yc, z=Zc}}) ->
370 Epsilon = 0.001,
371 A = Xd*Xd + Yd*Yd + Zd*Zd,
372 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
373 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
374 Discriminant = B*B - 4*A*C,
375 %io:format("A=~w B=~w C=~w discriminant=~w~n",
376 % [A, B, C, Discriminant]),
377 if Discriminant >= Epsilon ->
378 T0 = (-B + math:sqrt(Discriminant))/2,
379 T1 = (-B - math:sqrt(Discriminant))/2,
380 if (T0 >= 0) and (T1 >= 0) ->
381 %io:format("T0=~w T1=~w~n", [T0, T1]),
382 Distance = lists:min([T0, T1]),
383 Intersection = vector_add(
384 #vector{x=X0, y=Y0, z=Z0},
385 vector_scalar_mult(
386 #vector{x=Xd, y=Yd, z=Zd}, Distance)),
387 Normal = vector_normalize(
388 vector_sub(Intersection,
389 #vector{x=Xc, y=Yc, z=Zc})),
390 {Distance, Intersection, Normal};
391 true ->
392 none
393 end;
394 true ->
395 none
396 end.
398 % based on
399 % http://www.graphics.cornell.edu/pubs/1997/MT97.html
400 % http://jgt.akpeters.com/papers/GuigueDevillers03/addendum.html
401 ray_triangle_intersect(Ray, Triangle) ->
402 Epsilon = 0.000001,
404 % find vectors for two edges sharing v1
405 Edge1 = vector_sub(Triangle#triangle.v2, Triangle#triangle.v1),
406 Edge2 = vector_sub(Triangle#triangle.v3, Triangle#triangle.v1),
408 % begin calculating determinant
409 P = vector_cross_product(Ray#ray.direction, Edge2),
410 Determinant = vector_dot_product(Edge1, P),
412 % negative determinant means the triangle is facing away
413 % from the ray
415 if Determinant < Epsilon ->
416 % for our purposes we ignore such triangles
417 %% io:format("ray is either behind or on the triangle: ~p~n", [Determinant]),
418 none;
419 true ->
420 % calculate the distance from v1 to ray origin
421 T = vector_sub(Ray#ray.origin, Triangle#triangle.v1),
423 % calculate the U parameter and test bounds
424 U = vector_dot_product(T, P),
425 if (U < 0) or (U > Determinant) ->
426 %% io:format("U is negative or greater than det: ~p~n", [U]),
427 none;
428 true ->
429 % prepare to test the V parameter
430 Q = vector_cross_product(T, Edge1),
431 % calculate the V parameter and test bounds
432 V = vector_dot_product(Ray#ray.direction, Q),
433 if (V < 0) or (U+V > Determinant) ->
434 %% io:format("V less than 0.0 or U+V greater than det: ~p ~p~n",
435 %% [U, V]),
436 none;
437 true ->
438 % calculate the distance to the
439 % intersection point and return
440 %% io:format("found ray/triangle intersection ~n", []),
441 Distance = vector_dot_product(Edge2, Q) / Determinant,
442 Intersection = vector_add(
443 Ray#ray.origin,
444 vector_scalar_mult(
445 Ray#ray.direction,
446 Distance)),
447 Normal = vector_normalize(
448 vector_sub(
449 Triangle#triangle.v1,
450 Triangle#triangle.v2)),
451 {Distance, Intersection, Normal}
454 end.
456 % based on
457 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
458 % http://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm
459 % http://www.devmaster.net/articles/raytracing/
460 ray_plane_intersect(Ray, Plane) ->
461 Epsilon = 0.001,
462 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
463 if Vd < 0 ->
464 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
465 + Plane#plane.distance),
466 Distance = V0 / Vd,
467 if Distance < Epsilon ->
468 none;
469 true ->
470 Intersection = vector_add(
471 Ray#ray.origin,
472 vector_scalar_mult(
473 Ray#ray.direction,
474 Distance)),
475 {Distance, Intersection, Plane#plane.normal}
476 end;
477 true ->
478 none
479 end.
482 focal_length(Angle, Dimension) ->
483 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
485 point_on_screen(X, Y, Camera) ->
486 %TODO: implement rotation (using quaternions)
487 Screen_width = (Camera#camera.screen)#screen.width,
488 Screen_height = (Camera#camera.screen)#screen.height,
489 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
490 Camera#camera.location,
491 [vector_scalar_mult(
492 #vector{x=0, y=0, z=1},
493 focal_length(
494 Camera#camera.fov,
495 Screen_width)),
496 #vector{x = (X-0.5) * Screen_width,
497 y=0,
498 z=0},
499 #vector{x=0,
500 y= (Y-0.5) * Screen_height,
501 z=0}
505 shoot_ray(From, Through) ->
506 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
508 % assume that X and Y are percentages of the 3D world screen dimensions
509 ray_through_pixel(X, Y, Camera) ->
510 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
512 vectors_equal(V1, V2) ->
513 vectors_equal(V1, V2, 0.0001).
514 vectors_equal(V1, V2, Epsilon) ->
515 (V1#vector.x + Epsilon >= V2#vector.x)
516 and (V1#vector.x - Epsilon =<V2#vector.x)
517 and (V1#vector.y + Epsilon >= V2#vector.y)
518 and (V1#vector.y - Epsilon =<V2#vector.y)
519 and (V1#vector.z + Epsilon >= V2#vector.z)
520 and (V1#vector.z - Epsilon =<V2#vector.z).
523 vector_add(V1, V2) ->
524 #vector{x = V1#vector.x + V2#vector.x,
525 y = V1#vector.y + V2#vector.y,
526 z = V1#vector.z + V2#vector.z}.
528 vector_sub(V1, V2) ->
529 #vector{x = V1#vector.x - V2#vector.x,
530 y = V1#vector.y - V2#vector.y,
531 z = V1#vector.z - V2#vector.z}.
533 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
534 X*X + Y*Y + Z*Z.
536 vector_mag(V) ->
537 math:sqrt(vector_square_mag(V)).
539 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
540 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
542 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
543 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
545 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
546 A1*B1 + A2*B2 + A3*B3.
548 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
549 #vector{x = A2*B3 - A3*B2,
550 y = A3*B1 - A1*B3,
551 z = A1*B2 - A2*B1}.
553 vector_normalize(V) ->
554 Mag = vector_mag(V),
555 if Mag == 0 ->
556 #vector{x=0, y=0, z=0};
557 true ->
558 vector_scalar_mult(V, 1/vector_mag(V))
559 end.
561 vector_neg(#vector{x=X, y=Y, z=Z}) ->
562 #vector{x=-X, y=-Y, z=-Z}.
564 % based on
565 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtreflec.htm
566 % http://www.devmaster.net/articles/raytracing/
567 vector_bounce_off_plane(Vector, Normal) ->
568 vector_add(
569 vector_scalar_mult(
570 Normal,
571 2*vector_dot_product(Normal, vector_neg(Vector))),
572 Vector).
574 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
576 object_diffuse_colour(#plane{material=#material{colour=C}}) ->
578 object_diffuse_colour(#triangle{material=#material{colour=C}}) ->
581 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
583 object_specular_power(#plane{material=#material{specular_power=SP}}) ->
585 object_specular_power(#triangle{material=#material{specular_power=SP}}) ->
588 object_shininess(#sphere{material=#material{shininess=S}}) ->
590 object_shininess(#plane{material=#material{shininess=S}}) ->
592 object_shininess(#triangle{material=#material{shininess=S}}) ->
595 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
597 object_reflectivity(#plane{material=#material{reflectivity=R}}) ->
599 object_reflectivity(#triangle{material=#material{reflectivity=R}}) ->
602 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
603 #vector{x=X, y=Y, z=Z}) ->
604 Epsilon = 0.001,
605 Epsilon > abs(
606 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
608 colour_to_vector(#colour{r=R, g=G, b=B}) ->
609 #vector{x=R, y=G, z=B}.
610 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
611 #colour{r=X, g=Y, b=Z}.
612 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
613 {R, G, B}.
615 % returns a list of objects in the scene
616 % camera is assumed to be the first element in the scene
617 scene() ->
618 [#camera{location=#vector{x=0, y=0, z=-2},
619 rotation=#vector{x=0, y=0, z=0},
620 fov=90,
621 screen=#screen{width=4, height=3}},
622 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
623 location=#vector{x=5, y=-2, z=0},
624 specular_colour=#colour{r=1, g=1, b=1}},
625 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
626 location=#vector{x=-10, y=0, z=7},
627 specular_colour=#colour{r=1, g=0, b=0.5}},
628 #sphere{radius=4,
629 center=#vector{x=4, y=0, z=10},
630 material=#material{
631 colour=#colour{r=0, g=0.5, b=1},
632 specular_power=20,
633 shininess=1,
634 reflectivity=0.1}},
635 #sphere{radius=4,
636 center=#vector{x=-5, y=3, z=9},
637 material=#material{
638 colour=#colour{r=1, g=0.5, b=0},
639 specular_power=4,
640 shininess=0.25,
641 reflectivity=0.5}},
642 #sphere{radius=4,
643 center=#vector{x=-4.5, y=-2.5, z=14},
644 material=#material{
645 colour=#colour{r=0.5, g=1, b=0},
646 specular_power=20,
647 shininess=0.25,
648 reflectivity=0.7}},
649 #triangle{v1=#vector{x=-2, y=5, z=5},
650 v2=#vector{x=4, y=5, z=10},
651 v3=#vector{x=4, y=-5, z=10},
652 material=#material{
653 colour=#colour{r=1, g=0.5, b=0},
654 specular_power=4,
655 shininess=0.25,
656 reflectivity=0.5}},
657 #plane{normal=#vector{x=0, y=-1, z=0},
658 distance=5,
659 material=#material{
660 colour=#colour{r=1, g=1, b=1},
661 specular_power=1,
662 shininess=0,
663 reflectivity=0.01}}
666 % assumes Pixels are ordered in a row by row fasion
667 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
668 case file:open(Filename, write) of
669 {ok, IoDevice} ->
670 io:format("file opened~n", []),
671 io:format(IoDevice, "P3~n", []),
672 io:format(IoDevice, "~p ~p~n", [Width, Height]),
673 io:format(IoDevice, "~p~n", [MaxValue]),
674 lists:foreach(
675 fun({_Num, {R, G, B}}) ->
676 io:format(IoDevice, "~p ~p ~p ",
677 [lists:min([trunc(R*MaxValue), MaxValue]),
678 lists:min([trunc(G*MaxValue), MaxValue]),
679 lists:min([trunc(B*MaxValue), MaxValue])]) end,
680 Pixels),
681 file:close(IoDevice);
682 error ->
683 io:format("error opening file~n", [])
684 end.
686 % various invocation style functions
687 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
688 standalone(list_to_integer(Width),
689 list_to_integer(Height),
690 Filename,
691 list_to_integer(Recursion_depth),
692 tracing_function(list_to_atom(Strategy))).
694 standalone(Width, Height, Filename, Recursion_depth, Function) ->
695 {Time, _Value} = timer:tc(
696 raytracer,
697 raytrace,
698 [Width,
699 Height,
700 Filename,
701 Recursion_depth,
702 Function]),
703 io:format("Done in ~w seconds~n", [Time/1000000]),
704 halt().
706 go(Strategy) ->
707 raytrace(tracing_function(Strategy)).
709 go(Width, Height, Filename, Recursion_depth, Strategy) ->
710 raytrace(Width, Height, Filename, Recursion_depth,
711 tracing_function(Strategy)).
713 tracing_function(simple) ->
714 fun raytraced_pixel_list_simple/4;
715 tracing_function(concurrent) ->
716 fun raytraced_pixel_list_concurrent/4;
717 tracing_function(distributed) ->
718 fun raytraced_pixel_list_distributed/4.
720 raytrace(Function) ->
721 raytrace(4, 3, "/tmp/traced.ppm", 5, Function).
722 raytrace(Width, Height, Filename, Recursion_depth, Function) ->
723 write_pixels_to_ppm(
724 Width,
725 Height,
726 255,
727 Function(
728 Width,
729 Height,
730 scene(),
731 Recursion_depth),
732 Filename).
734 % testing
735 run_tests() ->
736 Tests = [fun scene_test/0,
737 fun passing_test/0,
738 fun vector_equality_test/0,
739 fun vector_addition_test/0,
740 fun vector_subtraction_test/0,
741 fun vector_square_mag_test/0,
742 fun vector_mag_test/0,
743 fun vector_scalar_multiplication_test/0,
744 fun vector_dot_product_test/0,
745 fun vector_cross_product_test/0,
746 fun vector_normalization_test/0,
747 fun vector_negation_test/0,
748 % fun ray_through_pixel_test/0,
749 fun ray_shooting_test/0,
750 fun point_on_screen_test/0,
751 fun nearest_object_intersecting_ray_test/0,
752 fun focal_length_test/0,
753 % fun vector_rotation_test/0,
754 fun vector_bounce_off_plane_test/0,
755 fun ray_sphere_intersection_test/0
757 run_tests(Tests, 1, true).
759 scene_test() ->
760 io:format("testing the scene function", []),
761 case scene() of
762 [{camera,
763 {vector, 0, 0, -2},
764 {vector, 0, 0, 0},
766 {screen, 4, 3}},
767 {point_light,
768 {colour, 1, 1, 0.5},
769 {vector, 5, -2, 0},
770 {colour, 1, 1, 1}},
771 {point_light,
772 {colour, 1, 0, 0.5},
773 {vector, -10, 0, 7},
774 {colour, 1, 0, 0.5}},
775 {sphere,
777 {vector, 4, 0, 10},
778 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
779 {sphere,
781 {vector, -5, 3, 9},
782 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
783 {sphere,
785 {vector, -4.5, -2.5, 14},
786 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
787 {triangle,
788 {vector, -2, 5, 5},
789 {vector, 4, 5, 10},
790 {vector, 4, -5, 10},
791 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
792 {plane,
793 {vector, 0, -1, 0},
795 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
796 ] ->
797 true;
798 _Else ->
799 false
800 end.
802 passing_test() ->
803 io:format("this test always passes", []),
804 true.
806 run_tests([], _Num, Success) ->
807 case Success of
808 true ->
809 io:format("Success!~n", []),
811 _Else ->
812 io:format("some tests failed~n", []),
813 failed
814 end;
816 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
817 io:format("test #~p: ", [Num]),
818 Current_success = First_test(),
819 case Current_success of
820 true ->
821 io:format(" - OK~n", []);
822 _Else ->
823 io:format(" - FAILED~n", [])
824 end,
825 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
827 vector_equality_test() ->
828 io:format("vector equality"),
829 Vector1 = #vector{x=0, y=0, z=0},
830 Vector2 = #vector{x=1234, y=-234, z=0},
831 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
832 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
833 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
834 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
836 Subtest1 = vectors_equal(Vector1, Vector1)
837 and vectors_equal(Vector2, Vector2)
838 and not (vectors_equal(Vector1, Vector2))
839 and not (vectors_equal(Vector2, Vector1)),
840 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
841 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
843 Subtest1 and Subtest2 and Subtest3.
846 vector_addition_test() ->
847 io:format("vector addition", []),
848 Vector0 = vector_add(
849 #vector{x=3, y=7, z=-3},
850 #vector{x=0, y=-24, z=123}),
851 Subtest1 = (Vector0#vector.x == 3)
852 and (Vector0#vector.y == -17)
853 and (Vector0#vector.z == 120),
855 Vector1 = #vector{x=5, y=0, z=984},
856 Vector2 = vector_add(Vector1, Vector1),
857 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
858 and (Vector2#vector.y == Vector1#vector.y*2)
859 and (Vector2#vector.z == Vector1#vector.z*2),
861 Vector3 = #vector{x=908, y=-098, z=234},
862 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
863 Subtest3 = vectors_equal(Vector3, Vector4),
865 Subtest1 and Subtest2 and Subtest3.
867 vector_subtraction_test() ->
868 io:format("vector subtraction", []),
869 Vector1 = #vector{x=0, y=0, z=0},
870 Vector2 = #vector{x=8390, y=-2098, z=939},
871 Vector3 = #vector{x=1, y=1, z=1},
872 Vector4 = #vector{x=-1, y=-1, z=-1},
874 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
875 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
876 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
877 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
878 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
879 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
880 vector_sub(Vector2, Vector3)),
882 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
884 vector_square_mag_test() ->
885 io:format("vector square magnitude test", []),
886 Vector1 = #vector{x=0, y=0, z=0},
887 Vector2 = #vector{x=1, y=1, z=1},
888 Vector3 = #vector{x=3, y=-4, z=0},
890 Subtest1 = (0 == vector_square_mag(Vector1)),
891 Subtest2 = (3 == vector_square_mag(Vector2)),
892 Subtest3 = (25 == vector_square_mag(Vector3)),
894 Subtest1 and Subtest2 and Subtest3.
896 vector_mag_test() ->
897 io:format("vector magnitude test", []),
898 Vector1 = #vector{x=0, y=0, z=0},
899 Vector2 = #vector{x=1, y=1, z=1},
900 Vector3 = #vector{x=3, y=-4, z=0},
902 Subtest1 = (0 == vector_mag(Vector1)),
903 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
904 Subtest3 = (5 == vector_mag(Vector3)),
906 Subtest1 and Subtest2 and Subtest3.
908 vector_scalar_multiplication_test() ->
909 io:format("scalar multiplication test", []),
910 Vector1 = #vector{x=0, y=0, z=0},
911 Vector2 = #vector{x=1, y=1, z=1},
912 Vector3 = #vector{x=3, y=-4, z=0},
914 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
915 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
916 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
917 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
918 vector_scalar_mult(Vector2, 4)),
919 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
920 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
922 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
924 vector_dot_product_test() ->
925 io:format("dot product test", []),
926 Vector1 = #vector{x=1, y=3, z=-5},
927 Vector2 = #vector{x=4, y=-2, z=-1},
928 Vector3 = #vector{x=0, y=0, z=0},
929 Vector4 = #vector{x=1, y=0, z=0},
930 Vector5 = #vector{x=0, y=1, z=0},
932 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
933 Subtest2 = vector_dot_product(Vector2, Vector2)
934 == vector_square_mag(Vector2),
935 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
936 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
938 Subtest1 and Subtest2 and Subtest3 and Subtest4.
940 vector_cross_product_test() ->
941 io:format("cross product test", []),
942 Vector1 = #vector{x=0, y=0, z=0},
943 Vector2 = #vector{x=1, y=0, z=0},
944 Vector3 = #vector{x=0, y=1, z=0},
945 Vector4 = #vector{x=0, y=0, z=1},
946 Vector5 = #vector{x=1, y=2, z=3},
947 Vector6 = #vector{x=4, y=5, z=6},
948 Vector7 = #vector{x=-3, y=6, z=-3},
949 Vector8 = #vector{x=-1, y=0, z=0},
950 Vector9 = #vector{x=-9, y=8, z=433},
952 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
953 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
954 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
955 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
956 Subtest5 = vectors_equal(
957 vector_cross_product(Vector7,
958 vector_add(Vector8, Vector9)),
959 vector_add(
960 vector_cross_product(Vector7, Vector8),
961 vector_cross_product(Vector7, Vector9))),
962 Subtest6 = vectors_equal(Vector1,
963 vector_add(
964 vector_add(
965 vector_cross_product(
966 Vector7,
967 vector_cross_product(Vector8, Vector9)),
968 vector_cross_product(
969 Vector8,
970 vector_cross_product(Vector9, Vector7))),
971 vector_cross_product(
972 Vector9,
973 vector_cross_product(Vector7, Vector8)))),
975 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
977 vector_normalization_test() ->
978 io:format("normalization test", []),
979 Vector1 = #vector{x=0, y=0, z=0},
980 Vector2 = #vector{x=1, y=0, z=0},
981 Vector3 = #vector{x=5, y=0, z=0},
983 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
984 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
985 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
986 Subtest4 = vectors_equal(Vector2, vector_normalize(
987 vector_scalar_mult(Vector2, 324))),
989 Subtest1 and Subtest2 and Subtest3 and Subtest4.
991 vector_negation_test() ->
992 io:format("vector negation test", []),
993 Vector1 = #vector{x=0, y=0, z=0},
994 Vector2 = #vector{x=4, y=-5, z=6},
996 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
997 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
999 Subtest1 and Subtest2.
1001 ray_shooting_test() ->
1002 io:format("ray shooting test"),
1003 Vector1 = #vector{x=0, y=0, z=0},
1004 Vector2 = #vector{x=1, y=0, z=0},
1006 Subtest1 = vectors_equal(
1007 (shoot_ray(Vector1, Vector2))#ray.direction,
1008 Vector2),
1010 Subtest1.
1012 ray_sphere_intersection_test() ->
1013 io:format("ray sphere intersection test", []),
1015 Sphere = #sphere{
1016 radius=3,
1017 center=#vector{x = 0, y=0, z=10},
1018 material=#material{
1019 colour=#colour{r=0.4, g=0.4, b=0.4}}},
1020 Ray1 = #ray{
1021 origin=#vector{x=0, y=0, z=0},
1022 direction=#vector{x=0, y=0, z=1}},
1023 Ray2 = #ray{
1024 origin=#vector{x=3, y=0, z=0},
1025 direction=#vector{x=0, y=0, z=1}},
1026 Ray3 = #ray{
1027 origin=#vector{x=4, y=0, z=0},
1028 direction=#vector{x=0, y=0, z=1}},
1029 {Distance1, _Hit_location1, _Hit_normal1} = ray_sphere_intersect(Ray1, Sphere),
1030 Subtest1 = Distance1 == 7.0,
1031 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == none,
1032 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == none,
1033 Subtest1 and Subtest2 and Subtest3.
1035 point_on_screen_test() ->
1036 io:format("point on screen test", []),
1037 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
1038 rotation=#vector{x=0, y=0, z=0},
1039 fov=90,
1040 screen=#screen{width=1, height=1}},
1041 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
1042 rotation=#vector{x=0, y=0, z=0},
1043 fov=90,
1044 screen=#screen{width=640, height=480}},
1046 Subtest1 = vectors_equal(
1047 #vector{x=0, y=0, z=0.5},
1048 point_on_screen(0.5, 0.5, Camera1)),
1049 Subtest2 = vectors_equal(
1050 #vector{x=-0.5, y=-0.5, z=0.5},
1051 point_on_screen(0, 0, Camera1)),
1052 Subtest3 = vectors_equal(
1053 #vector{x=0.5, y=0.5, z=0.5},
1054 point_on_screen(1, 1, Camera1)),
1055 Subtest4 = vectors_equal(
1056 point_on_screen(0, 0, Camera2),
1057 #vector{x=-320, y=-240, z=320}),
1058 Subtest5 = vectors_equal(
1059 point_on_screen(1, 1, Camera2),
1060 #vector{x=320, y=240, z=320}),
1061 Subtest6 = vectors_equal(
1062 point_on_screen(0.5, 0.5, Camera2),
1063 #vector{x=0, y=0, z=320}),
1065 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
1067 nearest_object_intersecting_ray_test() ->
1068 io:format("nearest object intersecting ray test", []),
1069 % test to make sure that we really get the closest object
1070 Sphere1=#sphere{radius=5,
1071 center=#vector{x=0, y=0, z=10},
1072 material=#material{
1073 colour=#colour{r=0, g=0, b=0.03}}},
1074 Sphere2=#sphere{radius=5,
1075 center=#vector{x=0, y=0, z=20},
1076 material=#material{
1077 colour=#colour{r=0, g=0, b=0.06}}},
1078 Sphere3=#sphere{radius=5,
1079 center=#vector{x=0, y=0, z=30},
1080 material=#material{
1081 colour=#colour{r=0, g=0, b=0.09}}},
1082 Sphere4=#sphere{radius=5,
1083 center=#vector{x=0, y=0, z=-10},
1084 material=#material{
1085 colour=#colour{r=0, g=0, b=-0.4}}},
1086 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
1087 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
1088 direction=#vector{x=0, y=0, z=1}},
1090 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
1091 Ray1, Scene1),
1092 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
1093 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
1094 and point_on_sphere(Sphere1, Hit_location),
1096 Subtest1.
1098 focal_length_test() ->
1099 Epsilon = 0.1,
1100 Size = 36,
1101 io:format("focal length test", []),
1102 lists:foldl(
1103 fun({Focal_length, Dimension}, Matches) ->
1104 %Result = focal_length(Dimension, Size),
1105 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
1106 Matches
1107 and ((Focal_length + Epsilon >= focal_length(
1108 Dimension, Size))
1109 and (Focal_length - Epsilon =< focal_length(
1110 Dimension, Size)))
1111 end, true,
1112 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
1114 vector_bounce_off_plane_test() ->
1115 io:format("vector reflect about normal", []),
1116 Vector1 = #vector{x=1, y=1, z=0},
1117 Vector2 = #vector{x=0, y=-1, z=0},
1118 Vector3 = #vector{x=1, y=-1, z=0},
1119 Vector4 = #vector{x=1, y=0, z=0},
1121 Subtest1 = vectors_equal(vector_bounce_off_plane(
1122 Vector1,
1123 vector_normalize(Vector2)),
1124 Vector3),
1126 Subtest2 = vectors_equal(
1127 vector_bounce_off_plane(
1128 Vector2,
1129 vector_normalize(Vector1)),
1130 Vector4),
1132 Subtest1 and Subtest2.