4 %% a simple raytracer written in Erlang
6 %% Copyright (c) 2008 Michael Ploujnikov
8 %% This program is free software: you can redistribute it and/or modify
9 %% it under the terms of the GNU General Public License as published by
10 %% the Free Software Foundation, either version 2 of the License, or
11 %% (at your option) any later version.
13 %% This program is distributed in the hope that it will be useful,
14 %% but WITHOUT ANY WARRANTY; without even the implied warranty of
15 %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 %% GNU General Public License for more details.
18 %% You should have received a copy of the GNU General Public License
19 %% along with this program. If not, see <http://www.gnu.org/licenses/>.
22 %% * three object types:
25 %% * triangles (not done)
28 %% * lighting based on local illumination models
29 %% * ambient (not done)
32 %% * attenuation (not done)
33 %% * reflections to a fixed depth
34 %% * PPM output file format
35 %% * randomly generated scene (not done)
36 %% * useful test suite (working but not very friendly when fails)
38 %% * specify how many pixels to give to each process
39 %% * distributed (across multiple computers)
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
67 raytraced_pixel_list_simple
/4,
68 raytraced_pixel_list_concurrent
/4,
69 raytraced_pixel_list_distributed
/4
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, _
, _
) ->
88 raytraced_pixel_list_simple(Width
, Height
, Scene
, Recursion_depth
)
89 when Width
> 0, Height
> 0 ->
94 % coordinates passed as a percentage
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, _
, _
) ->
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
]),
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", []),
121 raytraced_pixel_list_distributed(0, 0, _
, _
) ->
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
,
133 io:format("all workers have been spawned~n", []),
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
,
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
) ->
160 master(Program_PID
, Pixel_count
-1, [Pixel_tuple
|Pixel_list
])
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]),
173 Master_PID
! {X
+Y
*Width
,
175 trace_ray_through_pixel(
176 {X
/Width
, Y
/Height
}, Scene
, Recursion_depth
))}
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
),
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}]),
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
,
212 fun (#point_light
{diffuse_colour
=Light_colour
,
213 location
=Light_location
,
214 specular_colour
=Specular_colour
},
216 Reflection
= vector_scalar_mult(
218 pixel_colour_from_ray(
219 #ray
{origin
=Hit_location
,
220 direction
=vector_bounce_off_plane(
221 Ray#ray
.direction
, Hit_normal
)},
224 object_reflectivity(Object
)),
225 Light_contribution
= vector_add(
236 object_specular_power(Object
),
237 object_shininess(Object
),
244 vector_component_mult(
245 colour_to_vector(Light_colour
),
247 shadow_factor(Light_location
, Hit_location
, Object
, Scene
))));
248 (_Not_a_point_light
, Final_colour
) ->
251 #vector
{x
=0, y
=0, z
=0},
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
} ->
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
) ->
274 colour_to_vector(object_diffuse_colour(Object
)),
276 vector_dot_product(Hit_normal
,
278 vector_sub(Light_location
,
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
) ->
288 colour_to_vector(Specular_colour
),
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
, []) ->
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
,
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(
329 %io:format("no closer obj found~n", []),
330 nearest_object_intersecting_ray(
339 nearest_object_intersecting_ray(
348 % object specific intersection function
349 ray_object_intersect(Ray
, Object
) ->
352 ray_sphere_intersect(Ray
, Object
);
354 ray_triangle_intersect(Ray
, Object
);
356 ray_plane_intersect(Ray
, Object
);
362 % http://www.devmaster.net/articles/raytracing/
363 % http://www.siggraph.org/education/materials/HyperGraph/raytrace/rtinter1.htm
364 ray_sphere_intersect(
369 #sphere
{radius
=Radius
, center
=#vector
{
370 x
=Xc
, y
=Yc
, z
=Zc
}}) ->
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
},
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
};
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
) ->
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
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]),
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]),
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",
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(
448 Normal
= vector_normalize(
449 vector_cross_product(
450 Triangle#triangle
.v1
,
451 Triangle#triangle
.v2
)),
452 {Distance
, Intersection
, Normal
}
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
) ->
463 Vd
= vector_dot_product(Plane#plane
.normal
, Ray#ray
.direction
),
465 V0
= -(vector_dot_product(Plane#plane
.normal
, Ray#ray
.origin
)
466 + Plane#plane
.distance
),
468 if Distance
< Epsilon
->
471 Intersection
= vector_add(
476 {Distance
, Intersection
, Plane#plane
.normal
}
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
,
493 #vector
{x
=0, y
=0, z
=1},
497 #vector
{x
= (X
-0.5) * Screen_width
,
501 y
= (Y
-0.5) * Screen_height
,
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
}) ->
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
,
554 vector_normalize(V
) ->
557 #vector
{x
=0, y
=0, z
=0};
559 vector_scalar_mult(V
, 1/vector_mag(V
))
562 vector_neg(#vector
{x
=X
, y
=Y
, z
=Z
}) ->
563 #vector
{x
=-X
, y
=-Y
, z
=-Z
}.
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
) ->
572 2*vector_dot_product(Normal
, vector_neg(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
}) ->
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
}) ->
616 % returns a list of objects in the scene
617 % camera is assumed to be the first element in the scene
619 [#camera
{location
=#vector
{x
=0, y
=0, z
=-2},
620 rotation
=#vector
{x
=0, y
=0, z
=0},
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}},
630 center
=#vector
{x
=4, y
=0, z
=10},
632 colour
=#colour
{r
=0, g
=0.5, b
=1},
637 center
=#vector
{x
=-5, y
=3, z
=9},
639 colour
=#colour
{r
=1, g
=0.5, b
=0},
644 center
=#vector
{x
=-4.5, y
=-2.5, z
=14},
646 colour
=#colour
{r
=0.5, g
=1, b
=0},
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},
654 colour
=#colour
{r
=1, g
=0.5, b
=0},
658 #plane
{normal
=#vector
{x
=0, y
=-1, z
=0},
661 colour
=#colour
{r
=1, g
=1, b
=1},
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
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
]),
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,
682 file:close(IoDevice
);
684 io:format("error opening file~n", [])
687 % various invocation style functions
688 standalone([Width
, Height
, Filename
, Recursion_depth
, Strategy
]) ->
689 standalone(list_to_integer(Width
),
690 list_to_integer(Height
),
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(
704 io:format("Done in ~w seconds~n", [Time
/1000000]),
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
) ->
737 Tests
= [fun scene_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
).
761 io:format("testing the scene function", []),
775 {colour
, 1, 0, 0.5}},
779 {material
, {colour
, 0, 0.5, 1}, 20, 1, 0.1}},
783 {material
, {colour
, 1, 0.5, 0}, 4, 0.25, 0.5}},
786 {vector
, -4.5, -2.5, 14},
787 {material
, {colour
, 0.5, 1, 0}, 20, 0.25, 0.7}},
792 {material
, {colour
, 1, 0.5, 0}, 4, 0.25, 0.5}},
796 {material
, {colour
, 1, 1, 1}, 1, 0, 0.01}}
804 io:format("this test always passes", []),
807 run_tests([], _Num
, Success
) ->
810 io:format("Success!~n", []),
813 io:format("some tests failed~n", []),
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
822 io:format(" - OK~n", []);
824 io:format(" - FAILED~n", [])
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
.
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
)),
961 vector_cross_product(Vector7
, Vector8
),
962 vector_cross_product(Vector7
, Vector9
))),
963 Subtest6
= vectors_equal(Vector1
,
966 vector_cross_product(
968 vector_cross_product(Vector8
, Vector9
)),
969 vector_cross_product(
971 vector_cross_product(Vector9
, Vector7
))),
972 vector_cross_product(
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
,
1013 ray_sphere_intersection_test() ->
1014 io:format("ray sphere intersection test", []),
1018 center
=#vector
{x
= 0, y
=0, z
=10},
1020 colour
=#colour
{r
=0.4, g
=0.4, b
=0.4}}},
1022 origin
=#vector
{x
=0, y
=0, z
=0},
1023 direction
=#vector
{x
=0, y
=0, z
=1}},
1025 origin
=#vector
{x
=3, y
=0, z
=0},
1026 direction
=#vector
{x
=0, y
=0, z
=1}},
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},
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},
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},
1074 colour
=#colour
{r
=0, g
=0, b
=0.03}}},
1075 Sphere2
=#sphere
{radius
=5,
1076 center
=#vector
{x
=0, y
=0, z
=20},
1078 colour
=#colour
{r
=0, g
=0, b
=0.06}}},
1079 Sphere3
=#sphere
{radius
=5,
1080 center
=#vector
{x
=0, y
=0, z
=30},
1082 colour
=#colour
{r
=0, g
=0, b
=0.09}}},
1083 Sphere4
=#sphere
{radius
=5,
1084 center
=#vector
{x
=0, y
=0, z
=-10},
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(
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
),
1099 focal_length_test() ->
1102 io:format("focal length test", []),
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]),
1108 and ((Focal_length
+ Epsilon
>= focal_length(
1110 and (Focal_length
- Epsilon
=< focal_length(
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(
1124 vector_normalize(Vector2
)),
1127 Subtest2
= vectors_equal(
1128 vector_bounce_off_plane(
1130 vector_normalize(Vector1
)),
1133 Subtest1 and Subtest2
.