made nearest_object_intersecting_ray return the hit location and the normal at the...
[eraytracer.git] / raytracer.erl
blob2a2e17f0283fe0c2d496e77ffb2720c8a326c6e9
1 -module(raytracer).
2 -compile(export_all).
4 -record(vector, {x, y, z}).
5 -record(colour, {r, g, b}).
6 -record(ray, {origin, direction}).
7 -record(screen, {width, height}). % screen dimensions in the 3D world
8 -record(camera, {location, rotation, fov, screen}).
9 -record(sphere, {radius, center, colour}).
10 -record(point_light, {colour, intensity, location}).
11 %-record(axis_aligned_cube, {size, location}).
12 -define(BACKGROUND_COLOUR, #colour{r=0, g=0, b=0}).
13 -define(ERROR_COLOUR, #colour{r=255, g=0, b=0}).
14 -define(UNKNOWN_COLOUR, #colour{r=0, g=255, b=0}).
15 -define(FOG_DISTANCE, 40).
18 raytraced_pixel_list(0, 0, _) ->
19 done;
20 raytraced_pixel_list(Width, Height, Scene) when Width > 0, Height > 0 ->
21 lists:flatmap(
22 fun(Y) ->
23 lists:map(
24 fun(X) ->
25 % coordinates passed as a percentage
26 colour_to_pixel(
27 colour_trunc(
28 trace_ray_from_pixel(
29 {X/Width, Y/Height}, Scene))) end,
30 lists:seq(0, Width - 1)) end,
31 lists:seq(0, Height - 1)).
33 % assumes X and Y are percentages of the screen dimensions
34 trace_ray_from_pixel({X, Y}, [Camera|Rest_of_scene]) ->
35 Ray = ray_through_pixel(X, Y, Camera),
36 case nearest_object_intersecting_ray(Ray, Rest_of_scene) of
37 {Nearest_object, _Distance, Hit_location, Hit_normal} ->
38 %io:format("hit: ~w~n", [{Nearest_object, _Distance}]),
40 vector_to_colour(lighting_function(Nearest_object,
41 Hit_location,
42 Hit_normal,
43 Rest_of_scene));
44 none ->
45 ?BACKGROUND_COLOUR;
46 _Else ->
47 ?ERROR_COLOUR
48 end.
50 lighting_function(Object, Hit_location, Hit_normal, Scene) ->
51 lists:foldl(
52 fun (#point_light{colour=Light_colour,
53 intensity=Light_intensity,
54 location=Light_location},
55 Total_intensity) ->
56 vector_add(
57 vector_add(
58 vector_scalar_mult(
59 colour_to_vector(
60 point_light_intensity(
61 #point_light{colour=Light_colour,
62 intensity=Light_intensity,
63 location=Light_location},
64 Hit_normal,
65 Hit_location)),
66 Light_intensity),
67 colour_to_vector(object_colour(Object))),
68 Total_intensity);
70 (_Not_a_point_light, Total_intensity) ->
71 Total_intensity
72 end,
73 #vector{x=0, y=0, z=0},
74 Scene).
76 point_light_intensity(
77 #point_light{colour=Light_colour,
78 location=Light_location},
79 Hit_normal,
80 Hit_location) ->
81 vector_to_colour(
82 vector_scalar_mult(
83 colour_to_vector(Light_colour),
84 lists:max([0,
85 vector_dot_product(
86 Hit_normal,
87 vector_normalize(
88 vector_sub(Light_location,
89 Hit_location)))]))).
91 nearest_object_intersecting_ray(Ray, Scene) ->
92 nearest_object_intersecting_ray(
93 Ray, none, hitlocation, hitnormal, infinity, Scene).
94 nearest_object_intersecting_ray(
95 _Ray, _NearestObj, _Hit_location, _Normal, infinity, []) ->
96 none;
97 nearest_object_intersecting_ray(
98 _Ray, NearestObj, Hit_location, Normal, Distance, []) ->
99 % io:format("intersecting ~w at ~w~n", [NearestObj, Distance]),
100 {NearestObj, Distance, Hit_location, Normal};
101 nearest_object_intersecting_ray(Ray,
102 NearestObj,
103 Hit_location,
104 Normal,
105 Distance,
106 [CurrentObject|Rest_of_scene]) ->
107 NewDistance = ray_object_intersect(Ray, CurrentObject),
108 %io:format("Distace=~w NewDistace=~w~n", [Distance, NewDistance]),
109 if (NewDistance /= infinity)
110 and ((Distance == infinity) or (Distance > NewDistance)) ->
111 %io:format("another closer object found~n", []),
112 New_hit_location =
113 vector_add(Ray#ray.origin,
114 vector_scalar_mult(Ray#ray.direction, NewDistance)),
115 New_normal = object_normal_at_point(
116 CurrentObject, New_hit_location),
117 nearest_object_intersecting_ray(
118 Ray,
119 CurrentObject,
120 New_hit_location,
121 New_normal,
122 NewDistance,
123 Rest_of_scene);
124 true ->
125 %io:format("no closer obj found~n", []),
126 nearest_object_intersecting_ray(Ray,
127 NearestObj,
128 Hit_location,
129 Normal,
130 Distance,
131 Rest_of_scene)
132 end.
134 ray_object_intersect(Ray, Object) ->
135 case Object of
136 #sphere{} ->
137 ray_sphere_intersect(Ray, Object);
138 _Else ->
139 infinity
140 end.
142 object_normal_at_point(#sphere{center=Center}, Point) ->
143 vector_normalize(
144 vector_sub(Point, Center)).
146 ray_sphere_intersect(
147 #ray{origin=#vector{
148 x=X0, y=Y0, z=Z0},
149 direction=#vector{
150 x=Xd, y=Yd, z=Zd}},
151 #sphere{radius=Radius, center=#vector{
152 x=Xc, y=Yc, z=Zc}}) ->
153 A = Xd*Xd + Yd*Yd + Zd*Zd,
154 B = 2 * (Xd*(X0-Xc) + Yd*(Y0-Yc) + Zd*(Z0-Zc)),
155 C = (X0-Xc)*(X0-Xc) + (Y0-Yc)*(Y0-Yc) + (Z0-Zc)*(Z0-Zc) - Radius*Radius,
156 Discriminant = B*B - 4*A*C,
157 %io:format("A=~w B=~w C=~w discriminant=~w~n",
158 % [A, B, C, Discriminant]),
159 if Discriminant >= 0 ->
160 T0 = (-B + math:sqrt(Discriminant))/2,
161 T1 = (-B - math:sqrt(Discriminant))/2,
162 if (T0 >= 0) and (T1 >= 0) ->
163 %io:format("T0=~w T1=~w~n", [T0, T1]),
164 lists:min([T0, T1]);
165 true ->
166 infinity
167 end;
168 true ->
169 infinity
170 end.
174 focal_length(Angle, Dimension) ->
175 Dimension/(2*math:tan(Angle*(math:pi()/180)/2)).
177 point_on_screen(X, Y, Camera) ->
178 %TODO: implement rotation (using quaternions)
179 Screen_width = (Camera#camera.screen)#screen.width,
180 Screen_height = (Camera#camera.screen)#screen.height,
181 lists:foldl(fun(Vect, Sum) -> vector_add(Vect, Sum) end,
182 Camera#camera.location,
183 [vector_scalar_mult(
184 #vector{x=0, y=0, z=1},
185 focal_length(
186 Camera#camera.fov,
187 Screen_width)),
188 #vector{x = (X-0.5) * Screen_width,
189 y=0,
190 z=0},
191 #vector{x=0,
192 y= (Y-0.5) * Screen_height,
193 z=0}
197 shoot_ray(From, Through) ->
198 #ray{origin=From, direction=vector_normalize(vector_sub(Through, From))}.
200 % assume that X and Y are percentages of the 3D world screen dimensions
201 ray_through_pixel(X, Y, Camera) ->
202 shoot_ray(Camera#camera.location, point_on_screen(X, Y, Camera)).
204 vectors_equal(V1, V2) ->
205 vectors_equal(V1, V2, 0.0001).
206 vectors_equal(V1, V2, Epsilon) ->
207 (V1#vector.x + Epsilon >= V2#vector.x)
208 and (V1#vector.x - Epsilon =<V2#vector.x)
209 and (V1#vector.y + Epsilon >= V2#vector.y)
210 and (V1#vector.y - Epsilon =<V2#vector.y)
211 and (V1#vector.z + Epsilon >= V2#vector.z)
212 and (V1#vector.z - Epsilon =<V2#vector.z).
215 vector_add(V1, V2) ->
216 #vector{x = V1#vector.x + V2#vector.x,
217 y = V1#vector.y + V2#vector.y,
218 z = V1#vector.z + V2#vector.z}.
220 vector_sub(V1, V2) ->
221 #vector{x = V1#vector.x - V2#vector.x,
222 y = V1#vector.y - V2#vector.y,
223 z = V1#vector.z - V2#vector.z}.
225 vector_square_mag(#vector{x=X, y=Y, z=Z}) ->
226 X*X + Y*Y + Z*Z.
228 vector_mag(V) ->
229 math:sqrt(vector_square_mag(V)).
231 vector_scalar_mult(#vector{x=X, y=Y, z=Z}, Scalar) ->
232 #vector{x=X*Scalar, y=Y*Scalar, z=Z*Scalar}.
234 vector_dot_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
235 A1*B1 + A2*B2 + A3*B3.
237 vector_cross_product(#vector{x=A1, y=A2, z=A3}, #vector{x=B1, y=B2, z=B3}) ->
238 #vector{x = A2*B3 - A3*B2,
239 y = A3*B1 - A1*B3,
240 z = A1*B2 - A2*B1}.
242 vector_normalize(V) ->
243 Mag = vector_mag(V),
244 if Mag == 0 ->
245 #vector{x=0, y=0, z=0};
246 true ->
247 vector_scalar_mult(V, 1/vector_mag(V))
248 end.
250 vector_neg(#vector{x=X, y=Y, z=Z}) ->
251 #vector{x=-X, y=-Y, z=-Z}.
253 vector_rotate(V1, _V2) ->
254 %TODO: implement using quaternions
257 object_colour(#sphere{ colour=C}) ->
259 object_colour(_Unknown) ->
260 ?UNKNOWN_COLOUR.
262 point_on_sphere(#sphere{radius=Radius, center=#vector{x=XC, y=YC, z=ZC}},
263 #vector{x=X, y=Y, z=Z}) ->
264 Epsilon = 0.001,
265 Left_hand_side = (X-XC)*(X-XC) + (Y-YC)*(Y-YC) + (Z-ZC)*(Z-ZC),
266 Right_hand_side = Radius*Radius,
267 (Left_hand_side + Epsilon >= Right_hand_side) and (Left_hand_side - Epsilon =< Right_hand_side).
269 colour_to_vector(#colour{r=R, g=G, b=B}) ->
270 #vector{x=R, y=G, z=B}.
271 vector_to_colour(#vector{x=X, y=Y, z=Z}) ->
272 #colour{r=X, g=Y, b=Z}.
273 colour_to_pixel(#colour{r=R, g=G, b=B}) ->
274 {R, G, B}.
275 colour_trunc(#colour{r=R, g=G, b=B}) ->
276 #colour{r=trunc(R), g=trunc(G), b=trunc(B)}.
278 % returns a list of objects in the scene
279 % camera is assumed to be the first element in the scene
280 scene() ->
281 [#camera{location=#vector{x=0, y=0, z=0},
282 rotation=#vector{x=0, y=0, z=0},
283 fov=90,
284 screen=#screen{width=4, height=3}},
285 #point_light{colour=#colour{r=255, g=255, b=128},
286 intensity=1,
287 location=#vector{x=5, y=-2, z=0}},
288 #sphere{radius=4,
289 center=#vector{x=0, y=0, z=7},
290 colour=#colour{r=0, g=128, b=255}},
291 #sphere{radius=4,
292 center=#vector{x=-5, y=3, z=9},
293 colour=#colour{r=255, g=128, b=0}},
294 #sphere{radius=4,
295 center=#vector{x=-5, y=-2, z=10},
296 colour=#colour{r=128, g=255, b=0}}
300 % assumes Pixels are ordered in a row by row fasion
301 write_pixels_to_ppm(Width, Height, MaxValue, Pixels, Filename) ->
302 case file:open(Filename, write) of
303 {ok, IoDevice} ->
304 io:format("file opened~n", []),
305 io:format(IoDevice, "P3~n", []),
306 io:format(IoDevice, "~p ~p~n", [Width, Height]),
307 io:format(IoDevice, "~p~n", [MaxValue]),
308 lists:foreach(
309 fun({R, G, B}) ->
310 io:format(IoDevice, "~p ~p ~p ",
311 [R, G, B]) end,
312 Pixels),
313 file:close(IoDevice),
314 io:format("done~n", []);
315 error ->
316 io:format("error opening file~n", [])
317 end.
319 go() ->
320 go(16, 12, "/tmp/traced.ppm").
321 go(Width, Height, Filename) ->
322 write_pixels_to_ppm(Width,
323 Height,
324 255,
325 raytraced_pixel_list(Width,
326 Height,
327 scene()),
328 Filename).
330 % testing
331 scene_test() ->
332 io:format("testing the scene function", []),
333 case scene() of
334 [{camera,
335 {vector, 0, 0, 0},
336 {vector, 0, 0, 0},
338 {screen, 4, 3}},
339 {point_light,
340 {colour, 255, 255, 128},
342 {vector, 5, -2, 0}},
343 {sphere,
345 {vector, 0, 0, 7},
346 {colour, 0, 128, 255}},
347 {sphere,
349 {vector, -5, 3, 9},
350 {colour, 255, 128, 0}},
351 {sphere,
353 {vector, -5, -2, 10},
354 {colour, 128, 255, 0}}] ->
355 true;
356 _Else ->
357 false
358 end.
360 failing_test() ->
361 io:format("this test always fails", []),
362 false.
364 passing_test() ->
365 io:format("this test always passes", []),
366 true.
368 run_tests() ->
369 Tests = [fun scene_test/0,
370 fun passing_test/0,
371 fun vector_equality_test/0,
372 fun vector_addition_test/0,
373 fun vector_subtraction_test/0,
374 fun vector_square_mag_test/0,
375 fun vector_mag_test/0,
376 fun vector_scalar_multiplication_test/0,
377 fun vector_dot_product_test/0,
378 fun vector_cross_product_test/0,
379 fun vector_normalization_test/0,
380 fun vector_negation_test/0,
381 fun ray_through_pixel_test/0,
382 fun ray_shooting_test/0,
383 fun point_on_screen_test/0,
384 fun nearest_object_intersecting_ray_test/0,
385 fun focal_length_test/0,
386 fun vector_rotation_test/0,
387 fun point_light_intensity_test/0,
388 fun object_normal_at_point_test/0
390 run_tests(Tests, 1, true).
392 run_tests([], _Num, Success) ->
393 case Success of
394 true ->
395 io:format("Success!~n", []),
397 _Else ->
398 io:format("some tests failed~n", []),
399 failed
400 end;
402 run_tests([First_test|Rest_of_tests], Num, Success_so_far) ->
403 io:format("test #~p: ", [Num]),
404 Current_success = First_test(),
405 case Current_success of
406 true ->
407 io:format(" - OK~n", []);
408 _Else ->
409 io:format(" - FAILED~n", [])
410 end,
411 run_tests(Rest_of_tests, Num + 1, Current_success and Success_so_far).
413 vector_equality_test() ->
414 io:format("vector equality"),
415 Vector1 = #vector{x=0, y=0, z=0},
416 Vector2 = #vector{x=1234, y=-234, z=0},
417 Vector3 = #vector{x=0.0983, y=0.0214, z=0.12342},
418 Vector4 = #vector{x=0.0984, y=0.0213, z=0.12341},
419 Vector5 = #vector{x=10/3, y=-10/6, z=8/7},
420 Vector6 = #vector{x=3.3, y=-1.6, z=1.1},
422 Subtest1 = vectors_equal(Vector1, Vector1)
423 and vectors_equal(Vector2, Vector2)
424 and not (vectors_equal(Vector1, Vector2))
425 and not (vectors_equal(Vector2, Vector1)),
426 Subtest2 = vectors_equal(Vector3, Vector4, 0.0001),
427 Subtest3 = vectors_equal(Vector5, Vector6, 0.1),
429 Subtest1 and Subtest2 and Subtest3.
432 vector_addition_test() ->
433 io:format("vector addition", []),
434 Vector0 = vector_add(
435 #vector{x=3, y=7, z=-3},
436 #vector{x=0, y=-24, z=123}),
437 Subtest1 = (Vector0#vector.x == 3)
438 and (Vector0#vector.y == -17)
439 and (Vector0#vector.z == 120),
441 Vector1 = #vector{x=5, y=0, z=984},
442 Vector2 = vector_add(Vector1, Vector1),
443 Subtest2 = (Vector2#vector.x == Vector1#vector.x*2)
444 and (Vector2#vector.y == Vector1#vector.y*2)
445 and (Vector2#vector.z == Vector1#vector.z*2),
447 Vector3 = #vector{x=908, y=-098, z=234},
448 Vector4 = vector_add(Vector3, #vector{x=0, y=0, z=0}),
449 Subtest3 = vectors_equal(Vector3, Vector4),
451 Subtest1 and Subtest2 and Subtest3.
453 vector_subtraction_test() ->
454 io:format("vector subtraction", []),
455 Vector1 = #vector{x=0, y=0, z=0},
456 Vector2 = #vector{x=8390, y=-2098, z=939},
457 Vector3 = #vector{x=1, y=1, z=1},
458 Vector4 = #vector{x=-1, y=-1, z=-1},
460 Subtest1 = vectors_equal(Vector1, vector_sub(Vector1, Vector1)),
461 Subtest2 = vectors_equal(Vector3, vector_sub(Vector3, Vector1)),
462 Subtest3 = not vectors_equal(Vector3, vector_sub(Vector1, Vector3)),
463 Subtest4 = vectors_equal(Vector4, vector_sub(Vector4, Vector1)),
464 Subtest5 = not vectors_equal(Vector4, vector_sub(Vector1, Vector4)),
465 Subtest5 = vectors_equal(vector_add(Vector2, Vector4),
466 vector_sub(Vector2, Vector3)),
468 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5.
470 vector_square_mag_test() ->
471 io:format("vector square magnitude test", []),
472 Vector1 = #vector{x=0, y=0, z=0},
473 Vector2 = #vector{x=1, y=1, z=1},
474 Vector3 = #vector{x=3, y=-4, z=0},
476 Subtest1 = (0 == vector_square_mag(Vector1)),
477 Subtest2 = (3 == vector_square_mag(Vector2)),
478 Subtest3 = (25 == vector_square_mag(Vector3)),
480 Subtest1 and Subtest2 and Subtest3.
482 vector_mag_test() ->
483 io:format("vector magnitude test", []),
484 Vector1 = #vector{x=0, y=0, z=0},
485 Vector2 = #vector{x=1, y=1, z=1},
486 Vector3 = #vector{x=3, y=-4, z=0},
488 Subtest1 = (0 == vector_mag(Vector1)),
489 Subtest2 = (math:sqrt(3) == vector_mag(Vector2)),
490 Subtest3 = (5 == vector_mag(Vector3)),
492 Subtest1 and Subtest2 and Subtest3.
494 vector_scalar_multiplication_test() ->
495 io:format("scalar multiplication test", []),
496 Vector1 = #vector{x=0, y=0, z=0},
497 Vector2 = #vector{x=1, y=1, z=1},
498 Vector3 = #vector{x=3, y=-4, z=0},
500 Subtest1 = vectors_equal(Vector1, vector_scalar_mult(Vector1, 45)),
501 Subtest2 = vectors_equal(Vector1, vector_scalar_mult(Vector1, -13)),
502 Subtest3 = vectors_equal(Vector1, vector_scalar_mult(Vector3, 0)),
503 Subtest4 = vectors_equal(#vector{x=4, y=4, z=4},
504 vector_scalar_mult(Vector2, 4)),
505 Subtest5 = vectors_equal(Vector3, vector_scalar_mult(Vector3, 1)),
506 Subtest6 = not vectors_equal(Vector3, vector_scalar_mult(Vector3, -3)),
508 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
510 vector_dot_product_test() ->
511 io:format("dot product test", []),
512 Vector1 = #vector{x=1, y=3, z=-5},
513 Vector2 = #vector{x=4, y=-2, z=-1},
514 Vector3 = #vector{x=0, y=0, z=0},
515 Vector4 = #vector{x=1, y=0, z=0},
516 Vector5 = #vector{x=0, y=1, z=0},
518 Subtest1 = 3 == vector_dot_product(Vector1, Vector2),
519 Subtest2 = vector_dot_product(Vector2, Vector2)
520 == vector_square_mag(Vector2),
521 Subtest3 = 0 == vector_dot_product(Vector3, Vector1),
522 Subtest4 = 0 == vector_dot_product(Vector4, Vector5),
524 Subtest1 and Subtest2 and Subtest3 and Subtest4.
526 vector_cross_product_test() ->
527 io:format("cross product test", []),
528 Vector1 = #vector{x=0, y=0, z=0},
529 Vector2 = #vector{x=1, y=0, z=0},
530 Vector3 = #vector{x=0, y=1, z=0},
531 Vector4 = #vector{x=0, y=0, z=1},
532 Vector5 = #vector{x=1, y=2, z=3},
533 Vector6 = #vector{x=4, y=5, z=6},
534 Vector7 = #vector{x=-3, y=6, z=-3},
535 Vector8 = #vector{x=-1, y=0, z=0},
536 Vector9 = #vector{x=-9, y=8, z=433},
538 Subtest1 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector2)),
539 Subtest2 = vectors_equal(Vector1, vector_cross_product(Vector2, Vector8)),
540 Subtest3 = vectors_equal(Vector2, vector_cross_product(Vector3, Vector4)),
541 Subtest4 = vectors_equal(Vector7, vector_cross_product(Vector5, Vector6)),
542 Subtest5 = vectors_equal(
543 vector_cross_product(Vector7,
544 vector_add(Vector8, Vector9)),
545 vector_add(
546 vector_cross_product(Vector7, Vector8),
547 vector_cross_product(Vector7, Vector9))),
548 Subtest6 = vectors_equal(Vector1,
549 vector_add(
550 vector_add(
551 vector_cross_product(
552 Vector7,
553 vector_cross_product(Vector8, Vector9)),
554 vector_cross_product(
555 Vector8,
556 vector_cross_product(Vector9, Vector7))),
557 vector_cross_product(
558 Vector9,
559 vector_cross_product(Vector7, Vector8)))),
561 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
563 vector_normalization_test() ->
564 io:format("normalization test", []),
565 Vector1 = #vector{x=0, y=0, z=0},
566 Vector2 = #vector{x=1, y=0, z=0},
567 Vector3 = #vector{x=5, y=0, z=0},
569 Subtest1 = vectors_equal(Vector1, vector_normalize(Vector1)),
570 Subtest2 = vectors_equal(Vector2, vector_normalize(Vector2)),
571 Subtest3 = vectors_equal(Vector2, vector_normalize(Vector3)),
572 Subtest4 = vectors_equal(Vector2, vector_normalize(
573 vector_scalar_mult(Vector2, 324))),
575 Subtest1 and Subtest2 and Subtest3 and Subtest4.
577 vector_negation_test() ->
578 io:format("vector negation test", []),
579 Vector1 = #vector{x=0, y=0, z=0},
580 Vector2 = #vector{x=4, y=-5, z=6},
582 Subtest1 = vectors_equal(Vector1, vector_neg(Vector1)),
583 Subtest2 = vectors_equal(Vector2, vector_neg(vector_neg(Vector2))),
585 Subtest1 and Subtest2.
587 ray_through_pixel_test() ->
588 io:format("ray through pixel test", []),
589 false.
591 ray_shooting_test() ->
592 io:format("ray shooting test"),
593 Vector1 = #vector{x=0, y=0, z=0},
594 Vector2 = #vector{x=1, y=0, z=0},
596 Subtest1 = vectors_equal(
597 (shoot_ray(Vector1, Vector2))#ray.direction,
598 Vector2),
600 Subtest1.
602 ray_sphere_intersection_test() ->
603 Sphere = #sphere{
604 radius=3,
605 center=#vector{x = 0, y=0, z=10},
606 colour=#colour{r=111, g=111, b=111}},
607 Ray1 = #ray{
608 origin=#vector{x=0, y=0, z=0},
609 direction=#vector{x=0, y=0, z=1}},
610 Ray2 = #ray{
611 origin=#vector{x=3, y=0, z=0},
612 direction=#vector{x=0, y=0, z=1}},
613 Ray3 = #ray{
614 origin=#vector{x=4, y=0, z=0},
615 direction=#vector{x=0, y=0, z=1}},
616 io:format("ray/sphere intersection=~w~n", [ray_sphere_intersect(Ray1, Sphere)]),
617 Subtest1 = ray_sphere_intersect(Ray1, Sphere) == 7.0,
618 Subtest2 = ray_sphere_intersect(Ray2, Sphere) == 10.0,
619 Subtest3 = ray_sphere_intersect(Ray3, Sphere) == none,
620 io:format("ray/sphere intersection=~w~n", [ray_sphere_intersect(Ray2, Sphere)]),
621 io:format("ray/sphere intersection=~w~n", [ray_sphere_intersect(Ray3, Sphere)]),
622 Subtest1 and Subtest2 and Subtest3.
624 point_on_screen_test() ->
625 io:format("point on screen test", []),
626 Camera1 = #camera{location=#vector{x=0, y=0, z=0},
627 rotation=#vector{x=0, y=0, z=0},
628 fov=90,
629 screen=#screen{width=1, height=1}},
630 Camera2 = #camera{location=#vector{x=0, y=0, z=0},
631 rotation=#vector{x=0, y=0, z=0},
632 fov=90,
633 screen=#screen{width=640, height=480}},
635 Subtest1 = vectors_equal(
636 #vector{x=0, y=0, z=0.5},
637 point_on_screen(0.5, 0.5, Camera1)),
638 Subtest2 = vectors_equal(
639 #vector{x=-0.5, y=-0.5, z=0.5},
640 point_on_screen(0, 0, Camera1)),
641 Subtest3 = vectors_equal(
642 #vector{x=0.5, y=0.5, z=0.5},
643 point_on_screen(1, 1, Camera1)),
644 Subtest4 = vectors_equal(
645 point_on_screen(0, 0, Camera2),
646 #vector{x=-320, y=-240, z=320}),
647 Subtest5 = vectors_equal(
648 point_on_screen(1, 1, Camera2),
649 #vector{x=320, y=240, z=320}),
650 Subtest6 = vectors_equal(
651 point_on_screen(0.5, 0.5, Camera2),
652 #vector{x=0, y=0, z=320}),
654 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6.
656 nearest_object_intersecting_ray_test() ->
657 io:format("nearest object intersecting ray test", []),
658 % test to make sure that we really get the closest object
659 Sphere1=#sphere{radius=5,
660 center=#vector{x=0, y=0, z=10},
661 colour=#colour{r=0, g=0, b=10}},
662 Sphere2=#sphere{radius=5,
663 center=#vector{x=0, y=0, z=20},
664 colour=#colour{r=0, g=0, b=20}},
665 Sphere3=#sphere{radius=5,
666 center=#vector{x=0, y=0, z=30},
667 colour=#colour{r=0, g=0, b=30}},
668 Sphere4=#sphere{radius=5,
669 center=#vector{x=0, y=0, z=-10},
670 colour=#colour{r=0, g=0, b=-10}},
671 Scene1=[Sphere1, Sphere2, Sphere3, Sphere4],
672 Ray1=#ray{origin=#vector{x=0, y=0, z=0},
673 direction=#vector{x=0, y=0, z=1}},
675 {Object1, Distance1, Hit_location, Normal} = nearest_object_intersecting_ray(
676 Ray1, Scene1),
677 Subtest1 = (Object1 == Sphere1) and (Distance1 == 5)
678 and vectors_equal(Normal, vector_neg(Ray1#ray.direction))
679 and point_on_sphere(Sphere1, Hit_location),
681 Subtest1.
683 focal_length_test() ->
684 Epsilon = 0.1,
685 Size = 36,
686 io:format("focal length test", []),
687 lists:foldl(
688 fun({Focal_length, Dimension}, Matches) ->
689 %Result = focal_length(Dimension, Size),
690 %io:format("comparing ~w ~w ~w ~w~n", [Focal_length, Dimension, Result, Matches]),
691 Matches
692 and ((Focal_length + Epsilon >= focal_length(
693 Dimension, Size))
694 and (Focal_length - Epsilon =< focal_length(
695 Dimension, Size)))
696 end, true,
697 [{13, 108}, {15, 100.4}, {18, 90}, {21, 81.2}]).
699 vector_rotation_test() ->
700 io:format("vector rotation test", []),
701 Vector1 = #vector{x=0, y=0, z=0},
702 Vector2 = #vector{x=0, y=1, z=0},
703 Vector3 = #vector{x=90, y=0, z=0},
704 Vector4 = #vector{x=45, y=0, z=0},
705 Vector5 = #vector{x=30.11, y=-988.2, z=92.231},
706 Vector6 = #vector{x=0, y=0, z=1},
708 Subtest1 = vectors_equal(
709 vector_rotate(Vector1, Vector1),
710 Vector1),
711 Subtest2 = vectors_equal(
712 vector_rotate(Vector5, Vector1),
713 Vector5),
714 Subtest3 = vectors_equal(
715 vector_rotate(
716 vector_rotate(Vector5, Vector4),
717 Vector4),
718 vector_rotate(Vector5, Vector3)),
719 Subtest4 = vectors_equal(
720 Vector6,
721 vector_rotate(Vector2, Vector3)),
723 Subtest1 and Subtest2 and Subtest3 and Subtest4.
725 point_light_intensity_test() ->
726 io:format("point light intensity test", []),
727 Light1 = #point_light{colour=#colour{r=255, g=255, b=200},
728 intensity=7,
729 location=#vector{x=0, y=0, z=0}},
730 Hit_normal1 = #vector{x=1, y=0, z=0},
731 Hit_location1 = #vector{x=-1, y=0, z=0},
733 Subtest1 = point_light_intensity(Light1, Hit_normal1, Hit_location1) == #colour{r=255, g=255, b=200},
735 Subtest1.
737 object_normal_at_point_test() ->
738 io:format("object normal at point test"),
739 Sphere1 = #sphere{radius=13.5,
740 center=#vector{x=0, y=0, z=0},
741 colour=#colour{r=0, g=0, b=0}},
742 Point1 = #vector{x=13.5, y=0, z=0},
743 Point2 = #vector{x=0, y=13.5, z=0},
744 Point3 = #vector{x=0, y=0, z=13.5},
745 Point4 = vector_neg(Point1),
746 Point5 = vector_neg(Point2),
747 Point6 = vector_neg(Point3),
749 % sphere object tests
750 Subtest1 = vectors_equal(
751 vector_normalize(Point1),
752 object_normal_at_point(Sphere1, Point1)),
753 Subtest2 = vectors_equal(
754 vector_normalize(Point2),
755 object_normal_at_point(Sphere1, Point2)),
756 Subtest3 = vectors_equal(
757 vector_normalize(Point3),
758 object_normal_at_point(Sphere1, Point3)),
759 Subtest4 = vectors_equal(
760 vector_normalize(Point4),
761 object_normal_at_point(Sphere1, Point4)),
762 Subtest5 = vectors_equal(
763 vector_normalize(Point5),
764 object_normal_at_point(Sphere1, Point5)),
765 Subtest6 = vectors_equal(
766 vector_normalize(Point6),
767 object_normal_at_point(Sphere1, Point6)),
768 Subtest7 = not vectors_equal(
769 vector_normalize(Point1),
770 object_normal_at_point(Sphere1, Point4)),
772 Subtest1 and Subtest2 and Subtest3 and Subtest4 and Subtest5 and Subtest6
773 and Subtest7.