fixed a bug in ray_triangle_intersect()
[eraytracer.git] / raytracer.erl
blobac621bec46a4dfbf5caef0a7dcc9543fbc240db5
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(
194 lighting_function(
195 Ray,
196 Nearest_object,
197 Hit_location,
198 Hit_normal,
199 Scene,
200 Recursion_depth));
201 _Else ->
202 ?BACKGROUND_COLOUR
203 end.
205 % my own illumination formula
206 % ideas were borrowed from:
207 % http://www.devmaster.net/wiki/Lighting
208 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
209 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
210 Recursion_depth) ->
211 lists:foldl(
212 fun (#point_light{diffuse_colour=Light_colour,
213 location=Light_location,
214 specular_colour=Specular_colour},
215 Final_colour) ->
216 Reflection = vector_scalar_mult(
217 colour_to_vector(
218 pixel_colour_from_ray(
219 #ray{origin=Hit_location,
220 direction=vector_bounce_off_plane(
221 Ray#ray.direction, Hit_normal)},
222 Scene,
223 Recursion_depth-1)),
224 object_reflectivity(Object)),
225 Light_contribution = vector_add(
226 diffuse_term(
227 Object,
228 Light_location,
229 Hit_location,
230 Hit_normal),
231 specular_term(
232 Ray#ray.direction,
233 Light_location,
234 Hit_location,
235 Hit_normal,
236 object_specular_power(Object),
237 object_shininess(Object),
238 Specular_colour)),
239 vector_add(
240 Final_colour,
241 vector_add(
242 Reflection,
243 vector_scalar_mult(
244 vector_component_mult(
245 colour_to_vector(Light_colour),
246 Light_contribution),
247 shadow_factor(Light_location, Hit_location, Object, Scene))));
248 (_Not_a_point_light, Final_colour) ->
249 Final_colour
250 end,
251 #vector{x=0, y=0, z=0},
252 Scene).
254 % returns 0 if Object is occluded from the light at Light_location, otherwise
255 % returns 1 if light can see Object
256 shadow_factor(Light_location, Hit_location, Object, Scene) ->
257 Light_vector = vector_sub(Hit_location, Light_location),
258 Light_direction = vector_normalize(Light_vector),
259 Shadow_ray = #ray{origin=Light_location,
260 direction=Light_direction},
261 case nearest_object_intersecting_ray(Shadow_ray, Scene) of
262 % this could match another copy of the same object
263 {Object, _Distance, _Loc, _Normal} ->
265 _Else ->
267 end.
269 % based on
270 % http://www.devmaster.net/wiki/Lambert_diffuse_lighting
271 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
272 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
273 vector_scalar_mult(
274 colour_to_vector(object_diffuse_colour(Object)),
275 lists:max([0,
276 vector_dot_product(Hit_normal,
277 vector_normalize(
278 vector_sub(Light_location,
279 Hit_location)))])).
281 % based on
282 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
283 % http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_2_Phong_Mirrors_and_Shadows.shtml
284 % http://www.devmaster.net/wiki/Phong_shading
285 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
286 Specular_power, Shininess, Specular_colour) ->
287 vector_scalar_mult(
288 colour_to_vector(Specular_colour),
289 Shininess*math:pow(
290 lists:max([0,
291 vector_dot_product(
292 vector_normalize(
293 vector_add(
294 vector_normalize(
295 vector_sub(Light_location, Hit_location)),
296 vector_neg(EyeVector))),
297 Hit_normal)]), Specular_power)).
299 % object agnostic intersection function
300 nearest_object_intersecting_ray(Ray, Scene) ->
301 nearest_object_intersecting_ray(
302 Ray, none, hitlocation, hitnormal, infinity, Scene).
303 nearest_object_intersecting_ray(
304 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
305 none;
306 nearest_object_intersecting_ray(
307 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
308 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
309 {NearestObj, Distance, Hit_location, Normal};
310 nearest_object_intersecting_ray(Ray,
311 NearestObj,
312 Hit_location,
313 Normal,
314 Distance,
315 [CurrentObject|Rest_of_scene]) ->
316 case ray_object_intersect(Ray, CurrentObject) of
317 {NewDistance, New_hit_location, New_normal} ->
318 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
319 if (Distance == infinity) or (Distance > NewDistance) ->
320 %io:format("another closer object found~n", []),
321 nearest_object_intersecting_ray(
322 Ray,
323 CurrentObject,
324 New_hit_location,
325 New_normal,
326 NewDistance,
327 Rest_of_scene);
328 true ->
329 %io:format("no closer obj found~n", []),
330 nearest_object_intersecting_ray(
331 Ray,
332 NearestObj,
333 Hit_location,
334 Normal,
335 Distance,
336 Rest_of_scene)
337 end;
338 none ->
339 nearest_object_intersecting_ray(
340 Ray,
341 NearestObj,
342 Hit_location,
343 Normal,
344 Distance,
345 Rest_of_scene)
346 end.
348 % object specific intersection function
349 ray_object_intersect(Ray, Object) ->
350 case Object of
351 #sphere{} ->
352 ray_sphere_intersect(Ray, Object);
353 #triangle{} ->
354 ray_triangle_intersect(Ray, Object);
355 #plane{} ->
356 ray_plane_intersect(Ray, Object);
357 _Else ->
358 none
359 end.
361 % based on
362 % http://www.devmaster.net/articles/raytracing/
363 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter1.htm
364 ray_sphere_intersect(
365 #ray{origin=#vector{
366 x=X0, y=Y0, z=Z0},
367 direction=#vector{
368 x=Xd, y=Yd, z=Zd}},
369 #sphere{radius=Radius, center=#vector{
370 x=Xc, y=Yc, z=Zc}}) ->
371 Epsilon = 0.001,
372 A = Xd*Xd + Yd*Yd + Zd*Zd,
373 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
374 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
375 Discriminant = B*B - 4*A*C,
376 %io:format("A=~w B=~w C=~w discriminant=~w~n",
377 % [A, B, C, Discriminant]),
378 if Discriminant >= Epsilon ->
379 T0 = (-B + math:sqrt(Discriminant))/2,
380 T1 = (-B - math:sqrt(Discriminant))/2,
381 if (T0 >= 0) and (T1 >= 0) ->
382 %io:format("T0=~w T1=~w~n", [T0, T1]),
383 Distance = lists:min([T0, T1]),
384 Intersection = vector_add(
385 #vector{x=X0, y=Y0, z=Z0},
386 vector_scalar_mult(
387 #vector{x=Xd, y=Yd, z=Zd}, Distance)),
388 Normal = vector_normalize(
389 vector_sub(Intersection,
390 #vector{x=Xc, y=Yc, z=Zc})),
391 {Distance, Intersection, Normal};
392 true ->
393 none
394 end;
395 true ->
396 none
397 end.
399 % based on
400 % http://www.graphics.cornell.edu/pubs/1997/MT97.html
401 % http://jgt.akpeters.com/papers/GuigueDevillers03/addendum.html
402 ray_triangle_intersect(Ray, Triangle) ->
403 Epsilon = 0.000001,
405 % find vectors for two edges sharing v1
406 Edge1 = vector_sub(Triangle#triangle.v2, Triangle#triangle.v1),
407 Edge2 = vector_sub(Triangle#triangle.v3, Triangle#triangle.v1),
409 % begin calculating determinant
410 P = vector_cross_product(Ray#ray.direction, Edge2),
411 Determinant = vector_dot_product(Edge1, P),
413 % negative determinant means the triangle is facing away
414 % from the ray
416 if Determinant < Epsilon ->
417 % for our purposes we ignore such triangles
418 %% io:format("ray is either behind or on the triangle: ~p~n", [Determinant]),
419 none;
420 true ->
421 % calculate the distance from v1 to ray origin
422 T = vector_sub(Ray#ray.origin, Triangle#triangle.v1),
424 % calculate the U parameter and test bounds
425 U = vector_dot_product(T, P),
426 if (U < 0) or (U > Determinant) ->
427 %% io:format("U is negative or greater than det: ~p~n", [U]),
428 none;
429 true ->
430 % prepare to test the V parameter
431 Q = vector_cross_product(T, Edge1),
432 % calculate the V parameter and test bounds
433 V = vector_dot_product(Ray#ray.direction, Q),
434 if (V < 0) or (U+V > Determinant) ->
435 %% io:format("V less than 0.0 or U+V greater than det: ~p ~p~n",
436 %% [U, V]),
437 none;
438 true ->
439 % calculate the distance to the
440 % intersection point and return
441 %% io:format("found ray/triangle intersection ~n", []),
442 Distance = vector_dot_product(Edge2, Q) / Determinant,
443 Intersection = vector_add(
444 Ray#ray.origin,
445 vector_scalar_mult(
446 Ray#ray.direction,
447 Distance)),
448 Normal = vector_normalize(
449 vector_cross_product(
450 Triangle#triangle.v1,
451 Triangle#triangle.v2)),
452 {Distance, Intersection, Normal}
455 end.
457 % based on
458 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
459 % http://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm
460 % http://www.devmaster.net/articles/raytracing/
461 ray_plane_intersect(Ray, Plane) ->
462 Epsilon = 0.001,
463 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
464 if Vd < 0 ->
465 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
466 + Plane#plane.distance),
467 Distance = V0 / Vd,
468 if Distance < Epsilon ->
469 none;
470 true ->
471 Intersection = vector_add(
472 Ray#ray.origin,
473 vector_scalar_mult(
474 Ray#ray.direction,
475 Distance)),
476 {Distance, Intersection, Plane#plane.normal}
477 end;
478 true ->
479 none
480 end.
483 focal_length(Angle, Dimension) ->
484 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
486 point_on_screen(X, Y, Camera) ->
487 %TODO: implement rotation (using quaternions)
488 Screen_width = (Camera#camera.screen)#screen.width,
489 Screen_height = (Camera#camera.screen)#screen.height,
490 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
491 Camera#camera.location,
492 [vector_scalar_mult(
493 #vector{x=0, y=0, z=1},
494 focal_length(
495 Camera#camera.fov,
496 Screen_width)),
497 #vector{x = (X-0.5) * Screen_width,
498 y=0,
499 z=0},
500 #vector{x=0,
501 y= (Y-0.5) * Screen_height,
502 z=0}
506 shoot_ray(From, Through) ->
507 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
509 % assume that X and Y are percentages of the 3D world screen dimensions
510 ray_through_pixel(X, Y, Camera) ->
511 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
513 vectors_equal(V1, V2) ->
514 vectors_equal(V1, V2, 0.0001).
515 vectors_equal(V1, V2, Epsilon) ->
516 (V1#vector.x + Epsilon >= V2#vector.x)
517 and (V1#vector.x - Epsilon =<V2#vector.x)
518 and (V1#vector.y + Epsilon >= V2#vector.y)
519 and (V1#vector.y - Epsilon =<V2#vector.y)
520 and (V1#vector.z + Epsilon >= V2#vector.z)
521 and (V1#vector.z - Epsilon =<V2#vector.z).
524 vector_add(V1, V2) ->
525 #vector{x = V1#vector.x + V2#vector.x,
526 y = V1#vector.y + V2#vector.y,
527 z = V1#vector.z + V2#vector.z}.
529 vector_sub(V1, V2) ->
530 #vector{x = V1#vector.x - V2#vector.x,
531 y = V1#vector.y - V2#vector.y,
532 z = V1#vector.z - V2#vector.z}.
534 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
535 X*X + Y*Y + Z*Z.
537 vector_mag(V) ->
538 math:sqrt(vector_square_mag(V)).
540 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
541 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
543 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
544 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
546 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
547 A1*B1 + A2*B2 + A3*B3.
549 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
550 #vector{x = A2*B3 - A3*B2,
551 y = A3*B1 - A1*B3,
552 z = A1*B2 - A2*B1}.
554 vector_normalize(V) ->
555 Mag = vector_mag(V),
556 if Mag == 0 ->
557 #vector{x=0, y=0, z=0};
558 true ->
559 vector_scalar_mult(V, 1/vector_mag(V))
560 end.
562 vector_neg(#vector{x=X, y=Y, z=Z}) ->
563 #vector{x=-X, y=-Y, z=-Z}.
565 % based on
566 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtreflec.htm
567 % http://www.devmaster.net/articles/raytracing/
568 vector_bounce_off_plane(Vector, Normal) ->
569 vector_add(
570 vector_scalar_mult(
571 Normal,
572 2*vector_dot_product(Normal, vector_neg(Vector))),
573 Vector).
575 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
577 object_diffuse_colour(#plane{material=#material{colour=C}}) ->
579 object_diffuse_colour(#triangle{material=#material{colour=C}}) ->
582 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
584 object_specular_power(#plane{material=#material{specular_power=SP}}) ->
586 object_specular_power(#triangle{material=#material{specular_power=SP}}) ->
589 object_shininess(#sphere{material=#material{shininess=S}}) ->
591 object_shininess(#plane{material=#material{shininess=S}}) ->
593 object_shininess(#triangle{material=#material{shininess=S}}) ->
596 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
598 object_reflectivity(#plane{material=#material{reflectivity=R}}) ->
600 object_reflectivity(#triangle{material=#material{reflectivity=R}}) ->
603 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
604 #vector{x=X, y=Y, z=Z}) ->
605 Epsilon = 0.001,
606 Epsilon > abs(
607 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
609 colour_to_vector(#colour{r=R, g=G, b=B}) ->
610 #vector{x=R, y=G, z=B}.
611 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
612 #colour{r=X, g=Y, b=Z}.
613 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
614 {R, G, B}.
616 % returns a list of objects in the scene
617 % camera is assumed to be the first element in the scene
618 scene() ->
619 [#camera{location=#vector{x=0, y=0, z=-2},
620 rotation=#vector{x=0, y=0, z=0},
621 fov=90,
622 screen=#screen{width=4, height=3}},
623 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
624 location=#vector{x=5, y=-2, z=0},
625 specular_colour=#colour{r=1, g=1, b=1}},
626 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
627 location=#vector{x=-10, y=0, z=7},
628 specular_colour=#colour{r=1, g=0, b=0.5}},
629 #sphere{radius=4,
630 center=#vector{x=4, y=0, z=10},
631 material=#material{
632 colour=#colour{r=0, g=0.5, b=1},
633 specular_power=20,
634 shininess=1,
635 reflectivity=0.1}},
636 #sphere{radius=4,
637 center=#vector{x=-5, y=3, z=9},
638 material=#material{
639 colour=#colour{r=1, g=0.5, b=0},
640 specular_power=4,
641 shininess=0.25,
642 reflectivity=0.5}},
643 #sphere{radius=4,
644 center=#vector{x=-4.5, y=-2.5, z=14},
645 material=#material{
646 colour=#colour{r=0.5, g=1, b=0},
647 specular_power=20,
648 shininess=0.25,
649 reflectivity=0.7}},
650 #triangle{v1=#vector{x=-2, y=5, z=5},
651 v2=#vector{x=4, y=5, z=10},
652 v3=#vector{x=4, y=-5, z=10},
653 material=#material{
654 colour=#colour{r=1, g=0.5, b=0},
655 specular_power=4,
656 shininess=0.25,
657 reflectivity=0.5}},
658 #plane{normal=#vector{x=0, y=-1, z=0},
659 distance=5,
660 material=#material{
661 colour=#colour{r=1, g=1, b=1},
662 specular_power=1,
663 shininess=0,
664 reflectivity=0.01}}
667 % assumes Pixels are ordered in a row by row fasion
668 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
669 case file:open(Filename, write) of
670 {ok, IoDevice} ->
671 io:format("file opened~n", []),
672 io:format(IoDevice, "P3~n", []),
673 io:format(IoDevice, "~p ~p~n", [Width, Height]),
674 io:format(IoDevice, "~p~n", [MaxValue]),
675 lists:foreach(
676 fun({_Num, {R, G, B}}) ->
677 io:format(IoDevice, "~p ~p ~p ",
678 [lists:min([trunc(R*MaxValue), MaxValue]),
679 lists:min([trunc(G*MaxValue), MaxValue]),
680 lists:min([trunc(B*MaxValue), MaxValue])]) end,
681 Pixels),
682 file:close(IoDevice);
683 error ->
684 io:format("error opening file~n", [])
685 end.
687 % various invocation style functions
688 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
689 standalone(list_to_integer(Width),
690 list_to_integer(Height),
691 Filename,
692 list_to_integer(Recursion_depth),
693 tracing_function(list_to_atom(Strategy))).
695 standalone(Width, Height, Filename, Recursion_depth, Function) ->
696 {Time, _Value} = timer:tc(
697 raytracer,
698 raytrace,
699 [Width,
700 Height,
701 Filename,
702 Recursion_depth,
703 Function]),
704 io:format("Done in ~w seconds~n", [Time/1000000]),
705 halt().
707 go(Strategy) ->
708 raytrace(tracing_function(Strategy)).
710 go(Width, Height, Filename, Recursion_depth, Strategy) ->
711 raytrace(Width, Height, Filename, Recursion_depth,
712 tracing_function(Strategy)).
714 tracing_function(simple) ->
715 fun raytraced_pixel_list_simple/4;
716 tracing_function(concurrent) ->
717 fun raytraced_pixel_list_concurrent/4;
718 tracing_function(distributed) ->
719 fun raytraced_pixel_list_distributed/4.
721 raytrace(Function) ->
722 raytrace(4, 3, "/tmp/traced.ppm", 5, Function).
723 raytrace(Width, Height, Filename, Recursion_depth, Function) ->
724 write_pixels_to_ppm(
725 Width,
726 Height,
727 255,
728 Function(
729 Width,
730 Height,
731 scene(),
732 Recursion_depth),
733 Filename).
735 % testing
736 run_tests() ->
737 Tests = [fun scene_test/0,
738 fun passing_test/0,
739 fun vector_equality_test/0,
740 fun vector_addition_test/0,
741 fun vector_subtraction_test/0,
742 fun vector_square_mag_test/0,
743 fun vector_mag_test/0,
744 fun vector_scalar_multiplication_test/0,
745 fun vector_dot_product_test/0,
746 fun vector_cross_product_test/0,
747 fun vector_normalization_test/0,
748 fun vector_negation_test/0,
749 % fun ray_through_pixel_test/0,
750 fun ray_shooting_test/0,
751 fun point_on_screen_test/0,
752 fun nearest_object_intersecting_ray_test/0,
753 fun focal_length_test/0,
754 % fun vector_rotation_test/0,
755 fun vector_bounce_off_plane_test/0,
756 fun ray_sphere_intersection_test/0
758 run_tests(Tests, 1, true).
760 scene_test() ->
761 io:format("testing the scene function", []),
762 case scene() of
763 [{camera,
764 {vector, 0, 0, -2},
765 {vector, 0, 0, 0},
767 {screen, 4, 3}},
768 {point_light,
769 {colour, 1, 1, 0.5},
770 {vector, 5, -2, 0},
771 {colour, 1, 1, 1}},
772 {point_light,
773 {colour, 1, 0, 0.5},
774 {vector, -10, 0, 7},
775 {colour, 1, 0, 0.5}},
776 {sphere,
778 {vector, 4, 0, 10},
779 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
780 {sphere,
782 {vector, -5, 3, 9},
783 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
784 {sphere,
786 {vector, -4.5, -2.5, 14},
787 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
788 {triangle,
789 {vector, -2, 5, 5},
790 {vector, 4, 5, 10},
791 {vector, 4, -5, 10},
792 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
793 {plane,
794 {vector, 0, -1, 0},
796 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
797 ] ->
798 true;
799 _Else ->
800 false
801 end.
803 passing_test() ->
804 io:format("this test always passes", []),
805 true.
807 run_tests([], _Num, Success) ->
808 case Success of
809 true ->
810 io:format("Success!~n", []),
812 _Else ->
813 io:format("some tests failed~n", []),
814 failed
815 end;
817 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
818 io:format("test #~p: ", [Num]),
819 Current_success = First_test(),
820 case Current_success of
821 true ->
822 io:format(" - OK~n", []);
823 _Else ->
824 io:format(" - FAILED~n", [])
825 end,
826 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
828 vector_equality_test() ->
829 io:format("vector equality"),
830 Vector1 = #vector{x=0, y=0, z=0},
831 Vector2 = #vector{x=1234, y=-234, z=0},
832 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
833 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
834 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
835 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
837 Subtest1 = vectors_equal(Vector1, Vector1)
838 and vectors_equal(Vector2, Vector2)
839 and not (vectors_equal(Vector1, Vector2))
840 and not (vectors_equal(Vector2, Vector1)),
841 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
842 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
844 Subtest1 and Subtest2 and Subtest3.
847 vector_addition_test() ->
848 io:format("vector addition", []),
849 Vector0 = vector_add(
850 #vector{x=3, y=7, z=-3},
851 #vector{x=0, y=-24, z=123}),
852 Subtest1 = (Vector0#vector.x == 3)
853 and (Vector0#vector.y == -17)
854 and (Vector0#vector.z == 120),
856 Vector1 = #vector{x=5, y=0, z=984},
857 Vector2 = vector_add(Vector1, Vector1),
858 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
859 and (Vector2#vector.y == Vector1#vector.y*2)
860 and (Vector2#vector.z == Vector1#vector.z*2),
862 Vector3 = #vector{x=908, y=-098, z=234},
863 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
864 Subtest3 = vectors_equal(Vector3, Vector4),
866 Subtest1 and Subtest2 and Subtest3.
868 vector_subtraction_test() ->
869 io:format("vector subtraction", []),
870 Vector1 = #vector{x=0, y=0, z=0},
871 Vector2 = #vector{x=8390, y=-2098, z=939},
872 Vector3 = #vector{x=1, y=1, z=1},
873 Vector4 = #vector{x=-1, y=-1, z=-1},
875 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
876 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
877 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
878 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
879 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
880 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
881 vector_sub(Vector2, Vector3)),
883 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
885 vector_square_mag_test() ->
886 io:format("vector square magnitude test", []),
887 Vector1 = #vector{x=0, y=0, z=0},
888 Vector2 = #vector{x=1, y=1, z=1},
889 Vector3 = #vector{x=3, y=-4, z=0},
891 Subtest1 = (0 == vector_square_mag(Vector1)),
892 Subtest2 = (3 == vector_square_mag(Vector2)),
893 Subtest3 = (25 == vector_square_mag(Vector3)),
895 Subtest1 and Subtest2 and Subtest3.
897 vector_mag_test() ->
898 io:format("vector magnitude test", []),
899 Vector1 = #vector{x=0, y=0, z=0},
900 Vector2 = #vector{x=1, y=1, z=1},
901 Vector3 = #vector{x=3, y=-4, z=0},
903 Subtest1 = (0 == vector_mag(Vector1)),
904 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
905 Subtest3 = (5 == vector_mag(Vector3)),
907 Subtest1 and Subtest2 and Subtest3.
909 vector_scalar_multiplication_test() ->
910 io:format("scalar multiplication test", []),
911 Vector1 = #vector{x=0, y=0, z=0},
912 Vector2 = #vector{x=1, y=1, z=1},
913 Vector3 = #vector{x=3, y=-4, z=0},
915 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
916 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
917 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
918 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
919 vector_scalar_mult(Vector2, 4)),
920 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
921 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
923 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
925 vector_dot_product_test() ->
926 io:format("dot product test", []),
927 Vector1 = #vector{x=1, y=3, z=-5},
928 Vector2 = #vector{x=4, y=-2, z=-1},
929 Vector3 = #vector{x=0, y=0, z=0},
930 Vector4 = #vector{x=1, y=0, z=0},
931 Vector5 = #vector{x=0, y=1, z=0},
933 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
934 Subtest2 = vector_dot_product(Vector2, Vector2)
935 == vector_square_mag(Vector2),
936 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
937 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
939 Subtest1 and Subtest2 and Subtest3 and Subtest4.
941 vector_cross_product_test() ->
942 io:format("cross product test", []),
943 Vector1 = #vector{x=0, y=0, z=0},
944 Vector2 = #vector{x=1, y=0, z=0},
945 Vector3 = #vector{x=0, y=1, z=0},
946 Vector4 = #vector{x=0, y=0, z=1},
947 Vector5 = #vector{x=1, y=2, z=3},
948 Vector6 = #vector{x=4, y=5, z=6},
949 Vector7 = #vector{x=-3, y=6, z=-3},
950 Vector8 = #vector{x=-1, y=0, z=0},
951 Vector9 = #vector{x=-9, y=8, z=433},
953 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
954 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
955 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
956 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
957 Subtest5 = vectors_equal(
958 vector_cross_product(Vector7,
959 vector_add(Vector8, Vector9)),
960 vector_add(
961 vector_cross_product(Vector7, Vector8),
962 vector_cross_product(Vector7, Vector9))),
963 Subtest6 = vectors_equal(Vector1,
964 vector_add(
965 vector_add(
966 vector_cross_product(
967 Vector7,
968 vector_cross_product(Vector8, Vector9)),
969 vector_cross_product(
970 Vector8,
971 vector_cross_product(Vector9, Vector7))),
972 vector_cross_product(
973 Vector9,
974 vector_cross_product(Vector7, Vector8)))),
976 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
978 vector_normalization_test() ->
979 io:format("normalization test", []),
980 Vector1 = #vector{x=0, y=0, z=0},
981 Vector2 = #vector{x=1, y=0, z=0},
982 Vector3 = #vector{x=5, y=0, z=0},
984 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
985 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
986 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
987 Subtest4 = vectors_equal(Vector2, vector_normalize(
988 vector_scalar_mult(Vector2, 324))),
990 Subtest1 and Subtest2 and Subtest3 and Subtest4.
992 vector_negation_test() ->
993 io:format("vector negation test", []),
994 Vector1 = #vector{x=0, y=0, z=0},
995 Vector2 = #vector{x=4, y=-5, z=6},
997 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
998 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
1000 Subtest1 and Subtest2.
1002 ray_shooting_test() ->
1003 io:format("ray shooting test"),
1004 Vector1 = #vector{x=0, y=0, z=0},
1005 Vector2 = #vector{x=1, y=0, z=0},
1007 Subtest1 = vectors_equal(
1008 (shoot_ray(Vector1, Vector2))#ray.direction,
1009 Vector2),
1011 Subtest1.
1013 ray_sphere_intersection_test() ->
1014 io:format("ray sphere intersection test", []),
1016 Sphere = #sphere{
1017 radius=3,
1018 center=#vector{x = 0, y=0, z=10},
1019 material=#material{
1020 colour=#colour{r=0.4, g=0.4, b=0.4}}},
1021 Ray1 = #ray{
1022 origin=#vector{x=0, y=0, z=0},
1023 direction=#vector{x=0, y=0, z=1}},
1024 Ray2 = #ray{
1025 origin=#vector{x=3, y=0, z=0},
1026 direction=#vector{x=0, y=0, z=1}},
1027 Ray3 = #ray{
1028 origin=#vector{x=4, y=0, z=0},
1029 direction=#vector{x=0, y=0, z=1}},
1030 {Distance1, _Hit_location1, _Hit_normal1} = ray_sphere_intersect(Ray1, Sphere),
1031 Subtest1 = Distance1 == 7.0,
1032 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == none,
1033 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == none,
1034 Subtest1 and Subtest2 and Subtest3.
1036 point_on_screen_test() ->
1037 io:format("point on screen test", []),
1038 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
1039 rotation=#vector{x=0, y=0, z=0},
1040 fov=90,
1041 screen=#screen{width=1, height=1}},
1042 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
1043 rotation=#vector{x=0, y=0, z=0},
1044 fov=90,
1045 screen=#screen{width=640, height=480}},
1047 Subtest1 = vectors_equal(
1048 #vector{x=0, y=0, z=0.5},
1049 point_on_screen(0.5, 0.5, Camera1)),
1050 Subtest2 = vectors_equal(
1051 #vector{x=-0.5, y=-0.5, z=0.5},
1052 point_on_screen(0, 0, Camera1)),
1053 Subtest3 = vectors_equal(
1054 #vector{x=0.5, y=0.5, z=0.5},
1055 point_on_screen(1, 1, Camera1)),
1056 Subtest4 = vectors_equal(
1057 point_on_screen(0, 0, Camera2),
1058 #vector{x=-320, y=-240, z=320}),
1059 Subtest5 = vectors_equal(
1060 point_on_screen(1, 1, Camera2),
1061 #vector{x=320, y=240, z=320}),
1062 Subtest6 = vectors_equal(
1063 point_on_screen(0.5, 0.5, Camera2),
1064 #vector{x=0, y=0, z=320}),
1066 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
1068 nearest_object_intersecting_ray_test() ->
1069 io:format("nearest object intersecting ray test", []),
1070 % test to make sure that we really get the closest object
1071 Sphere1=#sphere{radius=5,
1072 center=#vector{x=0, y=0, z=10},
1073 material=#material{
1074 colour=#colour{r=0, g=0, b=0.03}}},
1075 Sphere2=#sphere{radius=5,
1076 center=#vector{x=0, y=0, z=20},
1077 material=#material{
1078 colour=#colour{r=0, g=0, b=0.06}}},
1079 Sphere3=#sphere{radius=5,
1080 center=#vector{x=0, y=0, z=30},
1081 material=#material{
1082 colour=#colour{r=0, g=0, b=0.09}}},
1083 Sphere4=#sphere{radius=5,
1084 center=#vector{x=0, y=0, z=-10},
1085 material=#material{
1086 colour=#colour{r=0, g=0, b=-0.4}}},
1087 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
1088 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
1089 direction=#vector{x=0, y=0, z=1}},
1091 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
1092 Ray1, Scene1),
1093 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
1094 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
1095 and point_on_sphere(Sphere1, Hit_location),
1097 Subtest1.
1099 focal_length_test() ->
1100 Epsilon = 0.1,
1101 Size = 36,
1102 io:format("focal length test", []),
1103 lists:foldl(
1104 fun({Focal_length, Dimension}, Matches) ->
1105 %Result = focal_length(Dimension, Size),
1106 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
1107 Matches
1108 and ((Focal_length + Epsilon >= focal_length(
1109 Dimension, Size))
1110 and (Focal_length - Epsilon =< focal_length(
1111 Dimension, Size)))
1112 end, true,
1113 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
1115 vector_bounce_off_plane_test() ->
1116 io:format("vector reflect about normal", []),
1117 Vector1 = #vector{x=1, y=1, z=0},
1118 Vector2 = #vector{x=0, y=-1, z=0},
1119 Vector3 = #vector{x=1, y=-1, z=0},
1120 Vector4 = #vector{x=1, y=0, z=0},
1122 Subtest1 = vectors_equal(vector_bounce_off_plane(
1123 Vector1,
1124 vector_normalize(Vector2)),
1125 Vector3),
1127 Subtest2 = vectors_equal(
1128 vector_bounce_off_plane(
1129 Vector2,
1130 vector_normalize(Vector1)),
1131 Vector4),
1133 Subtest1 and Subtest2.