-
Notifications
You must be signed in to change notification settings - Fork 4
/
render.py
166 lines (127 loc) · 6.76 KB
/
render.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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# -*- encoding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright © 2021 Maurizio Tomasi
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the “Software”), to deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of
# the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
# SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from colors import Color, WHITE, BLACK
from geometry import normalized_dot
from pcg import PCG
from ray import Ray
from world import World
class Renderer:
"""A class implementing a solver of the rendering equation.
This is an abstract class; you should use a derived concrete class."""
def __init__(self, world: World, background_color: Color = BLACK):
self.world = world
self.background_color = background_color
def __call__(self, ray: Ray) -> Color:
"""Estimate the radiance along a ray"""
raise NotImplementedError("Unable to call Renderer.radiance, it is an abstract method")
class OnOffRenderer(Renderer):
"""A on/off renderer
This renderer is mostly useful for debugging purposes, as it is really fast, but it produces boring images."""
def __init__(self, world: World, background_color: Color = BLACK, color=WHITE):
super().__init__(world, background_color)
self.world = world
self.color = color
def __call__(self, ray: Ray) -> Color:
return self.color if self.world.ray_intersection(ray) else self.background_color
class FlatRenderer(Renderer):
"""A «flat» renderer
This renderer estimates the solution of the rendering equation by neglecting any contribution of the light.
It just uses the pigment of each surface to determine how to compute the final radiance."""
def __init__(self, world: World, background_color: Color = BLACK):
super().__init__(world, background_color)
def __call__(self, ray: Ray) -> Color:
hit = self.world.ray_intersection(ray)
if not hit:
return self.background_color
material = hit.material
return (material.brdf.pigment.get_color(hit.surface_point) +
material.emitted_radiance.get_color(hit.surface_point))
class PathTracer(Renderer):
"""A simple path-tracing renderer
The algorithm implemented here allows the caller to tune number of rays thrown at each iteration, as well as the
maximum depth. It implements Russian roulette to reduce the number of recursive calls.
"""
def __init__(self, world: World, background_color: Color = BLACK, pcg: PCG = PCG(), num_of_rays: int = 10,
max_depth: int = 10, russian_roulette_limit=3):
super().__init__(world, background_color)
self.pcg = pcg
self.num_of_rays = num_of_rays
self.max_depth = max_depth
self.russian_roulette_limit = russian_roulette_limit
def __call__(self, ray: Ray) -> Color:
if ray.depth > self.max_depth:
return Color(0.0, 0.0, 0.0)
hit_record = self.world.ray_intersection(ray)
if not hit_record:
return self.background_color
hit_material = hit_record.material
hit_color = hit_material.brdf.pigment.get_color(hit_record.surface_point)
emitted_radiance = hit_material.emitted_radiance.get_color(hit_record.surface_point)
hit_color_lum = max(hit_color.r, hit_color.g, hit_color.b)
# Russian roulette
if ray.depth >= self.russian_roulette_limit:
q = max(0.05, 1 - hit_color_lum)
if self.pcg.random_float() > q:
# Keep the recursion going, but compensate for other potentially discarded rays
hit_color *= 1.0 / (1.0 - q)
else:
# Terminate prematurely
return emitted_radiance
cum_radiance = Color(0.0, 0.0, 0.0)
if hit_color_lum > 0.0: # Only do costly recursions if it's worth it
for ray_index in range(self.num_of_rays):
new_ray = hit_material.brdf.scatter_ray(
pcg=self.pcg,
incoming_dir=hit_record.ray.dir,
interaction_point=hit_record.world_point,
normal=hit_record.normal,
depth=ray.depth + 1,
)
# Recursive call
new_radiance = self(new_ray)
cum_radiance += hit_color * new_radiance
return emitted_radiance + cum_radiance * (1.0 / self.num_of_rays)
class PointLightRenderer(Renderer):
"""A simple point-light renderer
This renderer is similar to what POV-Ray provides by default.
"""
def __init__(self, world: World, background_color: Color = BLACK, ambient_color: Color = Color(0.1, 0.1, 0.1)):
super().__init__(world, background_color)
self.ambient_color = ambient_color
def __call__(self, ray: Ray) -> Color:
hit_record = self.world.ray_intersection(ray)
if not hit_record:
return self.background_color
hit_material = hit_record.material
result_color = self.ambient_color
for cur_light in self.world.point_lights:
if self.world.is_point_visible(point = cur_light.position, observer_pos=hit_record.world_point):
distance_vec = hit_record.world_point - cur_light.position
distance = distance_vec.norm()
in_dir = distance_vec * (1.0 / distance)
cos_theta = max(0.0, normalized_dot(-in_dir, hit_record.normal))
distance_factor = (cur_light.linear_radius / distance)**2 if (cur_light.linear_radius > 0) else 1.0
emitted_color = hit_material.emitted_radiance.get_color(hit_record.surface_point)
brdf_color = hit_material.brdf.eval(
normal=hit_record.normal,
in_dir=in_dir,
out_dir=-ray.dir,
uv=hit_record.surface_point,
)
result_color += (emitted_color + brdf_color) * cur_light.color * cos_theta * distance_factor
return result_color