forked from deermichel/raytracer.py
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtracer.py
executable file
·140 lines (116 loc) · 5.86 KB
/
tracer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
from ray import Ray
from vector3 import Vector3
class Tracer:
"""Main (ray) tracer coordinating the heavy algorithmic work"""
def __init__(self, max_recursion_depth=5, bias=1e-4):
"""Creates a new tracer"""
self.__max_recursion_depth = max_recursion_depth
self.__bias = bias
def trace(self, ray, scene):
"""Traces a ray through a scene to return the traced color"""
self.__scene = scene
return self.__trace_recursively(ray, 0)
def __trace_recursively(self, ray, depth):
"""Traces a ray through a scene recursively"""
hit_object, hit_point, hit_normal = self.__intersect(ray)
if hit_object is None:
return Vector3(0.3, 0.3, 0.3) # horizon
traced_color = Vector3()
if not hit_object.material.is_diffuse and depth < self.__max_recursion_depth:
traced_color = self.__trace_non_diffuse(ray, hit_object, hit_point, hit_normal, depth)
else:
traced_color = self.__trace_diffuse(hit_object, hit_point, hit_normal)
return traced_color + hit_object.material.emission_color
def intersect(self, ray, scene):
self.__scene = scene
return self.__intersect(ray)
def trace_diffuse(self, ray, scene):
self.__scene = scene
hit_object, hit_point, hit_normal = self.__intersect(ray)
if hit_object is None:
return Vector3(0.3, 0.3, 0.3) # horizon
traced_color = Vector3()
if hit_object.material.is_diffuse:
traced_color = self.__trace_diffuse(hit_object, hit_point, hit_normal)
return traced_color
def trace_non_diffuse(self, ray, scene):
self.__scene = scene
hit_object, hit_point, hit_normal = self.__intersect(ray)
traced_color = Vector3()
reflection_ray = Ray(Vector3(), Vector3(), 0)
refraction_ray = Ray(Vector3(), Vector3(), 0)
fresnel = 0
if hit_object is None:
return reflection_ray, refraction_ray, fresnel
if not hit_object.material.is_diffuse:
inside = ray.direction.dot(hit_normal) > 0
if inside:
hit_normal = -hit_normal
facing_ratio = -ray.direction.dot(hit_normal)
fresnel = self.__mix((1 - facing_ratio) ** 2, 1, 0.1)
reflection_ray = Ray(hit_point + self.__bias * hit_normal,
ray.direction.reflect(hit_normal).normalize())
refraction = Vector3()
# transparent?
if hit_object.material.transparency > 0:
from_ior = ray.current_ior if inside else hit_object.material.ior
to_ior = hit_object.material.ior if inside else ray.current_ior
refraction_ray = Ray(hit_point - self.__bias * hit_normal,
ray.direction.refract(from_ior, to_ior, hit_normal)
.normalize())
# mix according to fresnel
return reflection_ray, refraction_ray, fresnel
def __intersect(self, ray):
"""Returns the (nearest) intersection of the ray"""
hit_object = None
hit_t, hit_point, hit_normal = float("inf"), None, None
for obj in self.__scene:
intersection = obj.primitive.intersect(ray)
if intersection and intersection[0] < hit_t:
hit_object = obj
hit_t, hit_point, hit_normal = intersection
return hit_object, hit_point, hit_normal
def __trace_diffuse(self, hit_object, hit_point, hit_normal):
"""Traces color of an object with diffuse material"""
summed_color = Vector3()
for light in filter(lambda obj: obj.is_light, self.__scene):
transmission = Vector3(1, 1, 1)
light_direction = (light.primitive.position - hit_point).normalize()
for other in filter(lambda obj: obj != light, self.__scene):
if other.primitive.intersect(Ray(hit_point + self.__bias * hit_normal,
light_direction)):
transmission = Vector3()
break
summed_color = summed_color + (
hit_object.material.surface_color
.mul_comp(transmission)
.mul_comp(light.material.emission_color) *
max(0, hit_normal.dot(light_direction)))
return summed_color
def __trace_non_diffuse(self, ray, hit_object, hit_point, hit_normal, depth):
"""Traces color of an object with refractive/reflective material"""
inside = ray.direction.dot(hit_normal) > 0
if inside:
hit_normal = -hit_normal
# corret the normal vector in case it points inside of the sphere
facing_ratio = -ray.direction.dot(hit_normal)
fresnel = self.__mix((1 - facing_ratio) ** 2, 1, 0.1)
reflection_ray = Ray(hit_point + self.__bias * hit_normal,
ray.direction.reflect(hit_normal).normalize())
reflection = self.__trace_recursively(reflection_ray, depth + 1)
refraction = Vector3()
# transparent?
if hit_object.material.transparency > 0:
from_ior = ray.current_ior if inside else hit_object.material.ior
to_ior = hit_object.material.ior if inside else ray.current_ior
refraction_ray = Ray(hit_point - self.__bias * hit_normal,
ray.direction.refract(from_ior, to_ior, hit_normal)
.normalize())
refraction = self.__trace_recursively(refraction_ray, depth + 1)
# mix according to fresnel
return ((reflection * fresnel +
refraction * (1 - fresnel) * hit_object.material.transparency)
.mul_comp(hit_object.material.surface_color))
def __mix(self, a, b, mix):
"""Mixes to values by a factor"""
return b * mix + a * (1 - mix)