commented to site sources of formulas and algorithms
[eraytracer.git] / raytracer.erl
blob6757ac59919d95666f7cb8e45d4be558702b2c90
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(ERROR_COLOUR, #colour{r=1, g=0, b=0}).
84 -define(UNKNOWN_COLOUR, #colour{r=0, g=1, b=0}).
85 -define(FOG_DISTANCE, 40).
87 raytraced_pixel_list_simple(0, 0, _, _) ->
88 done;
89 raytraced_pixel_list_simple(Width, Height, Scene, Recursion_depth)
90 when Width > 0, Height > 0 ->
91 lists:flatmap(
92 fun(Y) ->
93 lists:map(
94 fun(X) ->
95 % coordinates passed as a percentage
96 {1, colour_to_pixel(
97 trace_ray_through_pixel(
98 {X/Width, Y/Height}, Scene, Recursion_depth))} end,
99 lists:seq(0, Width - 1)) end,
100 lists:seq(0, Height - 1)).
102 raytraced_pixel_list_concurrent(0, 0, _, _) ->
103 done;
104 raytraced_pixel_list_concurrent(Width, Height, Scene, Recursion_depth)
105 when Width > 0, Height > 0 ->
106 Master_PID = spawn(raytracer, master, [self(), Width*Height]),
107 lists:flatmap(
108 fun(Y) ->
109 lists:map(
110 fun(X) ->
111 % coordinates passed as a percentage
112 spawn(raytracer, worker,
113 [Master_PID, X+Y*Width, {X/Width, Y/Height}, Scene, Recursion_depth]) end,
114 lists:seq(0, Width - 1)) end,
115 lists:seq(0, Height - 1)),
116 io:format("all workers have been spawned~n", []),
117 receive
118 Final_pixel_list ->
119 Final_pixel_list
120 end.
122 raytraced_pixel_list_distributed(0, 0, _, _) ->
123 done;
124 raytraced_pixel_list_distributed(Width, Height, Scene, Recursion_depth)
125 when Width > 0, Height > 0 ->
126 io:format("distributed tracing~n", []),
127 Pool_master = pool:start(renderslave),
128 io:format("Pool master is ~p~n", [Pool_master]),
129 io:format("Nodes are ~p~n", [pool:get_nodes()]),
130 Master_PID = pool:pspawn(raytracer, master, [self(), Width*Height]),
131 Pixels = [{X, Y} || X <- lists:seq(0, Width-1), Y <- lists:seq(0, Height-1)],
132 distribute_work(Pixels, trunc(Width*Height/64), Master_PID, Width, Height, Scene,
133 Recursion_depth),
134 io:format("all workers have been spawned~n", []),
135 receive
136 Final_pixel_list ->
137 Final_pixel_list
138 end.
140 distribute_work(Pixels, Pixels_per_worker, Master_PID, Width, Height, Scene,
141 Recursion_depth) when length(Pixels) > Pixels_per_worker ->
142 {To_work_on, The_rest} = lists:split(Pixels_per_worker, Pixels),
143 pool:pspawn(raytracer, distributed_worker,
144 [Master_PID, To_work_on, Width, Height, Scene, Recursion_depth]),
145 distribute_work(The_rest, Pixels_per_worker, Master_PID,
146 Width, Height, Scene, Recursion_depth);
147 distribute_work(Pixels, _Pixels_per_worker, Master_PID, Width, Height, Scene,
148 Recursion_depth) ->
149 pool:pspawn(raytracer, distributed_worker,
150 [Master_PID, Pixels, Width, Height, Scene, Recursion_depth]).
152 master(Program_PID, Pixel_count) ->
153 master(Program_PID, Pixel_count, []).
154 master(Program_PID, 0, Pixel_list) ->
155 io:format("master is done~n", []),
156 Program_PID ! lists:keysort(1, Pixel_list);
157 % assumes all workers eventually return a good value
158 master(Program_PID, Pixel_count, Pixel_list) ->
159 receive
160 Pixel_tuple ->
161 master(Program_PID, Pixel_count-1, [Pixel_tuple|Pixel_list])
162 end.
165 % assumes X and Y are percentages of the screen dimensions
166 worker(Master_PID, Pixel_num, {X, Y}, Scene, Recursion_depth) ->
167 Master_PID ! {Pixel_num,
168 colour_to_pixel(trace_ray_through_pixel({X, Y}, Scene, Recursion_depth))}.
170 distributed_worker(Master_PID, Pixels, Width, Height, Scene, Recursion_depth) ->
171 %io:format("~pworker doing ~p pixels=~p~n", [node(), length(Pixels), Pixels]),
172 lists:foreach(
173 fun({X, Y}) ->
174 Master_PID ! {X+Y*Width,
175 colour_to_pixel(
176 trace_ray_through_pixel(
177 {X/Width, Y/Height}, Scene, Recursion_depth))}
178 end,
179 Pixels).
181 trace_ray_through_pixel({X, Y}, [Camera|Rest_of_scene], Recursion_depth) ->
182 pixel_colour_from_ray(
183 ray_through_pixel(X, Y, Camera),
184 Rest_of_scene,
185 Recursion_depth).
187 pixel_colour_from_ray(_Ray, _Scene, 0) ->
188 #colour{r=0, g=0, b=0};
189 pixel_colour_from_ray(Ray, Scene, Recursion_depth) ->
190 case nearest_object_intersecting_ray(Ray, Scene) of
191 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
192 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
194 vector_to_colour(lighting_function(Ray,
195 Nearest_object,
196 Hit_location,
197 Hit_normal,
198 Scene,
199 Recursion_depth));
200 none ->
201 ?BACKGROUND_COLOUR;
202 _Else ->
203 ?ERROR_COLOUR
204 end.
206 % my own illumination formula
207 % ideas were borrowed from:
208 % http://www.devmaster.net/wiki/Lighting
209 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
210 lighting_function(Ray, Object, Hit_location, Hit_normal, Scene,
211 Recursion_depth) ->
212 lists:foldl(
213 fun (#point_light{diffuse_colour=Light_colour,
214 location=Light_location,
215 specular_colour=Specular_colour},
216 Final_colour) ->
217 Reflection = vector_scalar_mult(
218 colour_to_vector(
219 pixel_colour_from_ray(
220 #ray{origin=Hit_location,
221 direction=vector_bounce_off_plane(
222 Ray#ray.direction, Hit_normal)},
223 Scene,
224 Recursion_depth-1)),
225 object_reflectivity(Object)),
226 Light_contribution = vector_add(
227 diffuse_term(
228 Object,
229 Light_location,
230 Hit_location,
231 Hit_normal),
232 specular_term(
233 Ray#ray.direction,
234 Light_location,
235 Hit_location,
236 Hit_normal,
237 object_specular_power(Object),
238 object_shininess(Object),
239 Specular_colour)),
240 vector_add(
241 Final_colour,
242 vector_add(
243 Reflection,
244 vector_scalar_mult(
245 vector_component_mult(
246 colour_to_vector(Light_colour),
247 Light_contribution),
248 shadow_factor(Light_location, Hit_location, Scene))));
249 (_Not_a_point_light, Final_colour) ->
250 Final_colour
251 end,
252 #vector{x=0, y=0, z=0},
253 Scene).
255 % returns 0 when the Hit_location is completely occluded from light
256 % at Light_location
257 % otherwise returns 1
258 shadow_factor(Light_location, Hit_location, Scene) ->
259 Light_vector = vector_sub(Light_location, Hit_location),
260 Light_vector_length = vector_mag(Light_vector),
261 Light_direction = vector_normalize(Light_vector),
262 % start the ray a little bit farther to prevent artefacts due to unit precision limitations
263 Shadow_ray = #ray{origin=vector_add(
264 Hit_location,
265 vector_scalar_mult(
266 Light_direction,
267 0.001)),
268 direction=Light_direction},
269 case nearest_object_intersecting_ray(Shadow_ray, Scene) of
270 {_Obj, Distance, _Loc, _Normal} ->
271 if Distance == infinity ->
273 Light_vector_length > Distance ->
275 true ->
277 end;
278 none ->
280 end.
282 % based on
283 % http://www.devmaster.net/wiki/Lambert_diffuse_lighting
284 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
285 diffuse_term(Object, Light_location, Hit_location, Hit_normal) ->
286 vector_scalar_mult(
287 colour_to_vector(object_diffuse_colour(Object)),
288 lists:max([0,
289 vector_dot_product(Hit_normal,
290 vector_normalize(
291 vector_sub(Light_location,
292 Hit_location)))])).
294 % based on
295 % http://svn.icculus.org/darkwar/trunk/base/shaders/light.frag?rev=1067&view=auto
296 % http://www.flipcode.com/archives/Raytracing_Topics_Techniques-Part_2_Phong_Mirrors_and_Shadows.shtml
297 % http://www.devmaster.net/wiki/Phong_shading
298 specular_term(EyeVector, Light_location, Hit_location, Hit_normal,
299 Specular_power, Shininess, Specular_colour) ->
300 vector_scalar_mult(
301 colour_to_vector(Specular_colour),
302 Shininess*math:pow(
303 lists:max([0,
304 vector_dot_product(
305 vector_normalize(
306 vector_add(
307 vector_normalize(
308 vector_sub(Light_location, Hit_location)),
309 vector_neg(EyeVector))),
310 Hit_normal)]), Specular_power)).
312 % object agnostic intersection function
313 nearest_object_intersecting_ray(Ray, Scene) ->
314 nearest_object_intersecting_ray(
315 Ray, none, hitlocation, hitnormal, infinity, Scene).
316 nearest_object_intersecting_ray(
317 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
318 none;
319 nearest_object_intersecting_ray(
320 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
321 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
322 {NearestObj, Distance, Hit_location, Normal};
323 nearest_object_intersecting_ray(Ray,
324 NearestObj,
325 Hit_location,
326 Normal,
327 Distance,
328 [CurrentObject|Rest_of_scene]) ->
329 NewDistance = ray_object_intersect(Ray, CurrentObject),
330 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
331 if (NewDistance /= infinity)
332 and ((Distance == infinity) or (Distance > NewDistance)) ->
333 %io:format("another closer object found~n", []),
334 New_hit_location =
335 vector_add(Ray#ray.origin,
336 vector_scalar_mult(Ray#ray.direction, NewDistance)),
337 New_normal = object_normal_at_point(
338 CurrentObject, New_hit_location),
339 nearest_object_intersecting_ray(
340 Ray,
341 CurrentObject,
342 New_hit_location,
343 New_normal,
344 NewDistance,
345 Rest_of_scene);
346 true ->
347 %io:format("no closer obj found~n", []),
348 nearest_object_intersecting_ray(Ray,
349 NearestObj,
350 Hit_location,
351 Normal,
352 Distance,
353 Rest_of_scene)
354 end.
356 % object specific intersection function
357 ray_object_intersect(Ray, Object) ->
358 case Object of
359 #sphere{} ->
360 ray_sphere_intersect(Ray, Object);
361 #triangle{} ->
362 ray_triangle_intersect(Ray, Object);
363 #plane{} ->
364 ray_plane_intersect(Ray, Object);
365 _Else ->
366 infinity
367 end.
369 object_normal_at_point(#sphere{center=Center}, Point) ->
370 vector_normalize(
371 vector_sub(Point, Center));
372 object_normal_at_point(#plane{normal=Normal}, _Point) ->
373 Normal.
375 % based on
376 % http://www.devmaster.net/articles/raytracing/
377 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter1.htm
378 ray_sphere_intersect(
379 #ray{origin=#vector{
380 x=X0, y=Y0, z=Z0},
381 direction=#vector{
382 x=Xd, y=Yd, z=Zd}},
383 #sphere{radius=Radius, center=#vector{
384 x=Xc, y=Yc, z=Zc}}) ->
385 Epsilon = 0.001,
386 A = Xd*Xd + Yd*Yd + Zd*Zd,
387 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
388 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
389 Discriminant = B*B - 4*A*C,
390 %io:format("A=~w B=~w C=~w discriminant=~w~n",
391 % [A, B, C, Discriminant]),
392 if Discriminant >= Epsilon ->
393 T0 = (-B + math:sqrt(Discriminant))/2,
394 T1 = (-B - math:sqrt(Discriminant))/2,
395 if (T0 >= 0) and (T1 >= 0) ->
396 %io:format("T0=~w T1=~w~n", [T0, T1]),
397 lists:min([T0, T1]);
398 true ->
399 infinity
400 end;
401 true ->
402 infinity
403 end.
405 ray_triangle_intersect(_Ray, _Triangle) ->
406 infinity.
408 % based on
409 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
410 % http://www.cs.princeton.edu/courses/archive/fall00/cs426/lectures/raycast/sld017.htm
411 % http://www.devmaster.net/articles/raytracing/
412 ray_plane_intersect(Ray, Plane) ->
413 Epsilon = 0.001,
414 Vd = vector_dot_product(Plane#plane.normal, Ray#ray.direction),
415 if Vd < 0 ->
416 V0 = -(vector_dot_product(Plane#plane.normal, Ray#ray.origin)
417 + Plane#plane.distance),
418 Distance = V0 / Vd,
419 if Distance < Epsilon ->
420 infinity;
421 true ->
422 Distance
423 end;
424 true ->
425 infinity
426 end.
429 focal_length(Angle, Dimension) ->
430 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
432 point_on_screen(X, Y, Camera) ->
433 %TODO: implement rotation (using quaternions)
434 Screen_width = (Camera#camera.screen)#screen.width,
435 Screen_height = (Camera#camera.screen)#screen.height,
436 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
437 Camera#camera.location,
438 [vector_scalar_mult(
439 #vector{x=0, y=0, z=1},
440 focal_length(
441 Camera#camera.fov,
442 Screen_width)),
443 #vector{x = (X-0.5) * Screen_width,
444 y=0,
445 z=0},
446 #vector{x=0,
447 y= (Y-0.5) * Screen_height,
448 z=0}
452 shoot_ray(From, Through) ->
453 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
455 % assume that X and Y are percentages of the 3D world screen dimensions
456 ray_through_pixel(X, Y, Camera) ->
457 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
459 vectors_equal(V1, V2) ->
460 vectors_equal(V1, V2, 0.0001).
461 vectors_equal(V1, V2, Epsilon) ->
462 (V1#vector.x + Epsilon >= V2#vector.x)
463 and (V1#vector.x - Epsilon =<V2#vector.x)
464 and (V1#vector.y + Epsilon >= V2#vector.y)
465 and (V1#vector.y - Epsilon =<V2#vector.y)
466 and (V1#vector.z + Epsilon >= V2#vector.z)
467 and (V1#vector.z - Epsilon =<V2#vector.z).
470 vector_add(V1, V2) ->
471 #vector{x = V1#vector.x + V2#vector.x,
472 y = V1#vector.y + V2#vector.y,
473 z = V1#vector.z + V2#vector.z}.
475 vector_sub(V1, V2) ->
476 #vector{x = V1#vector.x - V2#vector.x,
477 y = V1#vector.y - V2#vector.y,
478 z = V1#vector.z - V2#vector.z}.
480 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
481 X*X + Y*Y + Z*Z.
483 vector_mag(V) ->
484 math:sqrt(vector_square_mag(V)).
486 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
487 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
489 vector_component_mult(#vector{x=X1, y=Y1, z=Z1}, #vector{x=X2, y=Y2, z=Z2}) ->
490 #vector{x=X1*X2, y=Y1*Y2, z=Z1*Z2}.
492 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
493 A1*B1 + A2*B2 + A3*B3.
495 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
496 #vector{x = A2*B3 - A3*B2,
497 y = A3*B1 - A1*B3,
498 z = A1*B2 - A2*B1}.
500 vector_normalize(V) ->
501 Mag = vector_mag(V),
502 if Mag == 0 ->
503 #vector{x=0, y=0, z=0};
504 true ->
505 vector_scalar_mult(V, 1/vector_mag(V))
506 end.
508 vector_neg(#vector{x=X, y=Y, z=Z}) ->
509 #vector{x=-X, y=-Y, z=-Z}.
511 vector_bounce_off_plane(Vector, Normal) ->
512 vector_add(
513 vector_scalar_mult(
514 Normal,
515 2*vector_dot_product(Normal, vector_neg(Vector))),
516 Vector).
518 object_diffuse_colour(#sphere{material=#material{colour=C}}) ->
520 object_diffuse_colour(#plane{material=#material{colour=C}}) ->
522 object_specular_power(#sphere{material=#material{specular_power=SP}}) ->
524 object_specular_power(#plane{material=#material{specular_power=SP}}) ->
527 object_shininess(#sphere{material=#material{shininess=S}}) ->
529 object_shininess(#plane{material=#material{shininess=S}}) ->
532 object_reflectivity(#sphere{material=#material{reflectivity=R}}) ->
534 object_reflectivity(#plane{material=#material{reflectivity=R}}) ->
537 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
538 #vector{x=X, y=Y, z=Z}) ->
539 Epsilon = 0.001,
540 Epsilon > abs(
541 ((X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC)) - Radius*Radius).
543 colour_to_vector(#colour{r=R, g=G, b=B}) ->
544 #vector{x=R, y=G, z=B}.
545 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
546 #colour{r=X, g=Y, b=Z}.
547 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
548 {R, G, B}.
550 % returns a list of objects in the scene
551 % camera is assumed to be the first element in the scene
552 scene() ->
553 [#camera{location=#vector{x=0, y=0, z=-2},
554 rotation=#vector{x=0, y=0, z=0},
555 fov=90,
556 screen=#screen{width=4, height=3}},
557 #point_light{diffuse_colour=#colour{r=1, g=1, b=0.5},
558 location=#vector{x=5, y=-2, z=0},
559 specular_colour=#colour{r=1, g=1, b=1}},
560 #point_light{diffuse_colour=#colour{r=1, g=0, b=0.5},
561 location=#vector{x=-10, y=0, z=7},
562 specular_colour=#colour{r=1, g=0, b=0.5}},
563 #sphere{radius=4,
564 center=#vector{x=4, y=0, z=10},
565 material=#material{
566 colour=#colour{r=0, g=0.5, b=1},
567 specular_power=20,
568 shininess=1,
569 reflectivity=0.1}},
570 #sphere{radius=4,
571 center=#vector{x=-5, y=3, z=9},
572 material=#material{
573 colour=#colour{r=1, g=0.5, b=0},
574 specular_power=4,
575 shininess=0.25,
576 reflectivity=0.5}},
577 #sphere{radius=4,
578 center=#vector{x=-4.5, y=-2.5, z=14},
579 material=#material{
580 colour=#colour{r=0.5, g=1, b=0},
581 specular_power=20,
582 shininess=0.25,
583 reflectivity=0.7}},
584 #triangle{v1=#vector{x=2, y=1.5, z=0},
585 v2=#vector{x=2, y=1.5, z=10},
586 v3=#vector{x=-2, y=1.5, z=0},
587 material=#material{
588 colour=#colour{r=0.5, g=0, b=1},
589 specular_power=40,
590 shininess=1,
591 reflectivity=1}},
592 #plane{normal=#vector{x=0, y=-1, z=0},
593 distance=5,
594 material=#material{
595 colour=#colour{r=1, g=1, b=1},
596 specular_power=1,
597 shininess=0,
598 reflectivity=0.01}}
602 % assumes Pixels are ordered in a row by row fasion
603 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
604 case file:open(Filename, write) of
605 {ok, IoDevice} ->
606 io:format("file opened~n", []),
607 io:format(IoDevice, "P3~n", []),
608 io:format(IoDevice, "~p ~p~n", [Width, Height]),
609 io:format(IoDevice, "~p~n", [MaxValue]),
610 lists:foreach(
611 fun({_Num, {R, G, B}}) ->
612 io:format(IoDevice, "~p ~p ~p ",
613 [lists:min([trunc(R*MaxValue), MaxValue]),
614 lists:min([trunc(G*MaxValue), MaxValue]),
615 lists:min([trunc(B*MaxValue), MaxValue])]) end,
616 Pixels),
617 file:close(IoDevice);
618 error ->
619 io:format("error opening file~n", [])
620 end.
622 % various invocation style functions
623 standalone([Width, Height, Filename, Recursion_depth, Strategy]) ->
624 standalone(list_to_integer(Width),
625 list_to_integer(Height),
626 Filename,
627 list_to_integer(Recursion_depth),
628 tracing_function(list_to_atom(Strategy))).
630 standalone(Width, Height, Filename, Recursion_depth, Function) ->
631 {Time, _Value} = timer:tc(
632 raytracer,
633 raytrace,
634 [Width,
635 Height,
636 Filename,
637 Recursion_depth,
638 Function]),
639 io:format("Done in ~w seconds~n", [Time/1000000]),
640 halt().
642 go(Strategy) ->
643 raytrace(tracing_function(Strategy)).
645 go(Width, Height, Filename, Recursion_depth, Strategy) ->
646 raytrace(Width, Height, Filename, Recursion_depth,
647 tracing_function(Strategy)).
649 tracing_function(simple) ->
650 fun raytraced_pixel_list_simple/4;
651 tracing_function(concurrent) ->
652 fun raytraced_pixel_list_concurrent/4;
653 tracing_function(distributed) ->
654 fun raytraced_pixel_list_distributed/4.
656 raytrace(Function) ->
657 raytrace(4, 3, "/tmp/traced.ppm", 5, Function).
658 raytrace(Width, Height, Filename, Recursion_depth, Function) ->
659 write_pixels_to_ppm(
660 Width,
661 Height,
662 255,
663 Function(
664 Width,
665 Height,
666 scene(),
667 Recursion_depth),
668 Filename).
670 % testing
671 run_tests() ->
672 Tests = [fun scene_test/0,
673 fun passing_test/0,
674 fun vector_equality_test/0,
675 fun vector_addition_test/0,
676 fun vector_subtraction_test/0,
677 fun vector_square_mag_test/0,
678 fun vector_mag_test/0,
679 fun vector_scalar_multiplication_test/0,
680 fun vector_dot_product_test/0,
681 fun vector_cross_product_test/0,
682 fun vector_normalization_test/0,
683 fun vector_negation_test/0,
684 % fun ray_through_pixel_test/0,
685 fun ray_shooting_test/0,
686 fun point_on_screen_test/0,
687 fun nearest_object_intersecting_ray_test/0,
688 fun focal_length_test/0,
689 % fun vector_rotation_test/0,
690 fun object_normal_at_point_test/0,
691 fun vector_bounce_off_plane_test/0,
692 fun ray_sphere_intersection_test/0
694 run_tests(Tests, 1, true).
696 scene_test() ->
697 io:format("testing the scene function", []),
698 case scene() of
699 [{camera,
700 {vector, 0, 0, -2},
701 {vector, 0, 0, 0},
703 {screen, 4, 3}},
704 {point_light,
705 {colour, 1, 1, 0.5},
706 {vector, 5, -2, 0},
707 {colour, 1, 1, 1}},
708 {point_light,
709 {colour, 1, 0, 0.5},
710 {vector, -10, 0, 7},
711 {colour, 1, 0, 0.5}},
712 {sphere,
714 {vector, 4, 0, 10},
715 {material, {colour, 0, 0.5, 1}, 20, 1, 0.1}},
716 {sphere,
718 {vector, -5, 3, 9},
719 {material, {colour, 1, 0.5, 0}, 4, 0.25, 0.5}},
720 {sphere,
722 {vector, -4.5, -2.5, 14},
723 {material, {colour, 0.5, 1, 0}, 20, 0.25, 0.7}},
724 {triangle,
725 {vector, 2, 1.5, 0},
726 {vector, 2, 1.5, 10},
727 {vector, -2, 1.5, 0},
728 {material, {colour, 0.5, 0, 1}, 40, 1, 1}},
729 {plane,
730 {vector, 0, -1, 0},
732 {material, {colour, 1, 1, 1}, 1, 0, 0.01}}
733 ] ->
734 true;
735 _Else ->
736 false
737 end.
739 passing_test() ->
740 io:format("this test always passes", []),
741 true.
743 run_tests([], _Num, Success) ->
744 case Success of
745 true ->
746 io:format("Success!~n", []),
748 _Else ->
749 io:format("some tests failed~n", []),
750 failed
751 end;
753 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
754 io:format("test #~p: ", [Num]),
755 Current_success = First_test(),
756 case Current_success of
757 true ->
758 io:format(" - OK~n", []);
759 _Else ->
760 io:format(" - FAILED~n", [])
761 end,
762 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
764 vector_equality_test() ->
765 io:format("vector equality"),
766 Vector1 = #vector{x=0, y=0, z=0},
767 Vector2 = #vector{x=1234, y=-234, z=0},
768 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
769 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
770 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
771 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
773 Subtest1 = vectors_equal(Vector1, Vector1)
774 and vectors_equal(Vector2, Vector2)
775 and not (vectors_equal(Vector1, Vector2))
776 and not (vectors_equal(Vector2, Vector1)),
777 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
778 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
780 Subtest1 and Subtest2 and Subtest3.
783 vector_addition_test() ->
784 io:format("vector addition", []),
785 Vector0 = vector_add(
786 #vector{x=3, y=7, z=-3},
787 #vector{x=0, y=-24, z=123}),
788 Subtest1 = (Vector0#vector.x == 3)
789 and (Vector0#vector.y == -17)
790 and (Vector0#vector.z == 120),
792 Vector1 = #vector{x=5, y=0, z=984},
793 Vector2 = vector_add(Vector1, Vector1),
794 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
795 and (Vector2#vector.y == Vector1#vector.y*2)
796 and (Vector2#vector.z == Vector1#vector.z*2),
798 Vector3 = #vector{x=908, y=-098, z=234},
799 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
800 Subtest3 = vectors_equal(Vector3, Vector4),
802 Subtest1 and Subtest2 and Subtest3.
804 vector_subtraction_test() ->
805 io:format("vector subtraction", []),
806 Vector1 = #vector{x=0, y=0, z=0},
807 Vector2 = #vector{x=8390, y=-2098, z=939},
808 Vector3 = #vector{x=1, y=1, z=1},
809 Vector4 = #vector{x=-1, y=-1, z=-1},
811 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
812 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
813 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
814 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
815 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
816 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
817 vector_sub(Vector2, Vector3)),
819 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
821 vector_square_mag_test() ->
822 io:format("vector square magnitude test", []),
823 Vector1 = #vector{x=0, y=0, z=0},
824 Vector2 = #vector{x=1, y=1, z=1},
825 Vector3 = #vector{x=3, y=-4, z=0},
827 Subtest1 = (0 == vector_square_mag(Vector1)),
828 Subtest2 = (3 == vector_square_mag(Vector2)),
829 Subtest3 = (25 == vector_square_mag(Vector3)),
831 Subtest1 and Subtest2 and Subtest3.
833 vector_mag_test() ->
834 io:format("vector magnitude test", []),
835 Vector1 = #vector{x=0, y=0, z=0},
836 Vector2 = #vector{x=1, y=1, z=1},
837 Vector3 = #vector{x=3, y=-4, z=0},
839 Subtest1 = (0 == vector_mag(Vector1)),
840 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
841 Subtest3 = (5 == vector_mag(Vector3)),
843 Subtest1 and Subtest2 and Subtest3.
845 vector_scalar_multiplication_test() ->
846 io:format("scalar multiplication test", []),
847 Vector1 = #vector{x=0, y=0, z=0},
848 Vector2 = #vector{x=1, y=1, z=1},
849 Vector3 = #vector{x=3, y=-4, z=0},
851 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
852 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
853 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
854 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
855 vector_scalar_mult(Vector2, 4)),
856 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
857 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
859 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
861 vector_dot_product_test() ->
862 io:format("dot product test", []),
863 Vector1 = #vector{x=1, y=3, z=-5},
864 Vector2 = #vector{x=4, y=-2, z=-1},
865 Vector3 = #vector{x=0, y=0, z=0},
866 Vector4 = #vector{x=1, y=0, z=0},
867 Vector5 = #vector{x=0, y=1, z=0},
869 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
870 Subtest2 = vector_dot_product(Vector2, Vector2)
871 == vector_square_mag(Vector2),
872 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
873 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
875 Subtest1 and Subtest2 and Subtest3 and Subtest4.
877 vector_cross_product_test() ->
878 io:format("cross product test", []),
879 Vector1 = #vector{x=0, y=0, z=0},
880 Vector2 = #vector{x=1, y=0, z=0},
881 Vector3 = #vector{x=0, y=1, z=0},
882 Vector4 = #vector{x=0, y=0, z=1},
883 Vector5 = #vector{x=1, y=2, z=3},
884 Vector6 = #vector{x=4, y=5, z=6},
885 Vector7 = #vector{x=-3, y=6, z=-3},
886 Vector8 = #vector{x=-1, y=0, z=0},
887 Vector9 = #vector{x=-9, y=8, z=433},
889 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
890 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
891 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
892 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
893 Subtest5 = vectors_equal(
894 vector_cross_product(Vector7,
895 vector_add(Vector8, Vector9)),
896 vector_add(
897 vector_cross_product(Vector7, Vector8),
898 vector_cross_product(Vector7, Vector9))),
899 Subtest6 = vectors_equal(Vector1,
900 vector_add(
901 vector_add(
902 vector_cross_product(
903 Vector7,
904 vector_cross_product(Vector8, Vector9)),
905 vector_cross_product(
906 Vector8,
907 vector_cross_product(Vector9, Vector7))),
908 vector_cross_product(
909 Vector9,
910 vector_cross_product(Vector7, Vector8)))),
912 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
914 vector_normalization_test() ->
915 io:format("normalization test", []),
916 Vector1 = #vector{x=0, y=0, z=0},
917 Vector2 = #vector{x=1, y=0, z=0},
918 Vector3 = #vector{x=5, y=0, z=0},
920 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
921 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
922 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
923 Subtest4 = vectors_equal(Vector2, vector_normalize(
924 vector_scalar_mult(Vector2, 324))),
926 Subtest1 and Subtest2 and Subtest3 and Subtest4.
928 vector_negation_test() ->
929 io:format("vector negation test", []),
930 Vector1 = #vector{x=0, y=0, z=0},
931 Vector2 = #vector{x=4, y=-5, z=6},
933 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
934 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
936 Subtest1 and Subtest2.
938 ray_shooting_test() ->
939 io:format("ray shooting test"),
940 Vector1 = #vector{x=0, y=0, z=0},
941 Vector2 = #vector{x=1, y=0, z=0},
943 Subtest1 = vectors_equal(
944 (shoot_ray(Vector1, Vector2))#ray.direction,
945 Vector2),
947 Subtest1.
949 ray_sphere_intersection_test() ->
950 io:format("ray sphere intersection test", []),
952 Sphere = #sphere{
953 radius=3,
954 center=#vector{x = 0, y=0, z=10},
955 material=#material{
956 colour=#colour{r=0.4, g=0.4, b=0.4}}},
957 Ray1 = #ray{
958 origin=#vector{x=0, y=0, z=0},
959 direction=#vector{x=0, y=0, z=1}},
960 Ray2 = #ray{
961 origin=#vector{x=3, y=0, z=0},
962 direction=#vector{x=0, y=0, z=1}},
963 Ray3 = #ray{
964 origin=#vector{x=4, y=0, z=0},
965 direction=#vector{x=0, y=0, z=1}},
966 Subtest1 = ray_sphere_intersect(Ray1, Sphere) == 7.0,
967 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == infinity,
968 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == infinity,
969 Subtest1 and Subtest2 and Subtest3.
971 point_on_screen_test() ->
972 io:format("point on screen test", []),
973 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
974 rotation=#vector{x=0, y=0, z=0},
975 fov=90,
976 screen=#screen{width=1, height=1}},
977 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
978 rotation=#vector{x=0, y=0, z=0},
979 fov=90,
980 screen=#screen{width=640, height=480}},
982 Subtest1 = vectors_equal(
983 #vector{x=0, y=0, z=0.5},
984 point_on_screen(0.5, 0.5, Camera1)),
985 Subtest2 = vectors_equal(
986 #vector{x=-0.5, y=-0.5, z=0.5},
987 point_on_screen(0, 0, Camera1)),
988 Subtest3 = vectors_equal(
989 #vector{x=0.5, y=0.5, z=0.5},
990 point_on_screen(1, 1, Camera1)),
991 Subtest4 = vectors_equal(
992 point_on_screen(0, 0, Camera2),
993 #vector{x=-320, y=-240, z=320}),
994 Subtest5 = vectors_equal(
995 point_on_screen(1, 1, Camera2),
996 #vector{x=320, y=240, z=320}),
997 Subtest6 = vectors_equal(
998 point_on_screen(0.5, 0.5, Camera2),
999 #vector{x=0, y=0, z=320}),
1001 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
1003 nearest_object_intersecting_ray_test() ->
1004 io:format("nearest object intersecting ray test", []),
1005 % test to make sure that we really get the closest object
1006 Sphere1=#sphere{radius=5,
1007 center=#vector{x=0, y=0, z=10},
1008 material=#material{
1009 colour=#colour{r=0, g=0, b=0.03}}},
1010 Sphere2=#sphere{radius=5,
1011 center=#vector{x=0, y=0, z=20},
1012 material=#material{
1013 colour=#colour{r=0, g=0, b=0.06}}},
1014 Sphere3=#sphere{radius=5,
1015 center=#vector{x=0, y=0, z=30},
1016 material=#material{
1017 colour=#colour{r=0, g=0, b=0.09}}},
1018 Sphere4=#sphere{radius=5,
1019 center=#vector{x=0, y=0, z=-10},
1020 material=#material{
1021 colour=#colour{r=0, g=0, b=-0.4}}},
1022 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
1023 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
1024 direction=#vector{x=0, y=0, z=1}},
1026 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
1027 Ray1, Scene1),
1028 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
1029 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
1030 and point_on_sphere(Sphere1, Hit_location),
1032 Subtest1.
1034 focal_length_test() ->
1035 Epsilon = 0.1,
1036 Size = 36,
1037 io:format("focal length test", []),
1038 lists:foldl(
1039 fun({Focal_length, Dimension}, Matches) ->
1040 %Result = focal_length(Dimension, Size),
1041 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
1042 Matches
1043 and ((Focal_length + Epsilon >= focal_length(
1044 Dimension, Size))
1045 and (Focal_length - Epsilon =< focal_length(
1046 Dimension, Size)))
1047 end, true,
1048 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
1050 object_normal_at_point_test() ->
1051 io:format("object normal at point test"),
1052 Sphere1 = #sphere{radius=13.5,
1053 center=#vector{x=0, y=0, z=0},
1054 material=#material{
1055 colour=#colour{r=0, g=0, b=0}}},
1056 Point1 = #vector{x=13.5, y=0, z=0},
1057 Point2 = #vector{x=0, y=13.5, z=0},
1058 Point3 = #vector{x=0, y=0, z=13.5},
1059 Point4 = vector_neg(Point1),
1060 Point5 = vector_neg(Point2),
1061 Point6 = vector_neg(Point3),
1063 % sphere object tests
1064 Subtest1 = vectors_equal(
1065 vector_normalize(Point1),
1066 object_normal_at_point(Sphere1, Point1)),
1067 Subtest2 = vectors_equal(
1068 vector_normalize(Point2),
1069 object_normal_at_point(Sphere1, Point2)),
1070 Subtest3 = vectors_equal(
1071 vector_normalize(Point3),
1072 object_normal_at_point(Sphere1, Point3)),
1073 Subtest4 = vectors_equal(
1074 vector_normalize(Point4),
1075 object_normal_at_point(Sphere1, Point4)),
1076 Subtest5 = vectors_equal(
1077 vector_normalize(Point5),
1078 object_normal_at_point(Sphere1, Point5)),
1079 Subtest6 = vectors_equal(
1080 vector_normalize(Point6),
1081 object_normal_at_point(Sphere1, Point6)),
1082 Subtest7 = not vectors_equal(
1083 vector_normalize(Point1),
1084 object_normal_at_point(Sphere1, Point4)),
1086 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
1087 and Subtest7.
1089 vector_bounce_off_plane_test() ->
1090 io:format("vector reflect about normal", []),
1091 Vector1 = #vector{x=1, y=1, z=0},
1092 Vector2 = #vector{x=0, y=-1, z=0},
1093 Vector3 = #vector{x=1, y=-1, z=0},
1094 Vector4 = #vector{x=1, y=0, z=0},
1096 Subtest1 = vectors_equal(vector_bounce_off_plane(
1097 Vector1,
1098 vector_normalize(Vector2)),
1099 Vector3),
1101 Subtest2 = vectors_equal(
1102 vector_bounce_off_plane(
1103 Vector2,
1104 vector_normalize(Vector1)),
1105 Vector4),
1107 Subtest1 and Subtest2.