-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrenderer.py
More file actions
139 lines (111 loc) · 5.54 KB
/
renderer.py
File metadata and controls
139 lines (111 loc) · 5.54 KB
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
from vector import Vector3
from scene import Scene
from sphere import Sphere
from light import Light
from typing import Tuple
import math
class Renderer:
def __init__(self, scene: Scene, canvas_width: int, canvas_height: int, viewport_width: float, viewport_height: float, viewport_distance: float, camera_position: Vector3, background_color: tuple):
self.scene = scene
self.canvas_width = canvas_width
self.canvas_height = canvas_height
self.viewport_width = viewport_width
self.viewport_height = viewport_height
self.viewport_distance = viewport_distance
self.camera_position = camera_position
self.background_color = background_color
def canvas_to_viewport(self, x: int, y: int) -> Vector3:
return Vector3(x * self.viewport_width / self.canvas_width, y * self.viewport_height / self.canvas_height, self.viewport_distance)
def intersect_ray_sphere(self, O: Vector3, D: Vector3, sphere: Sphere) -> Tuple[float, float]:
CO = O - sphere.center
a = D.dot(D)
b = 2 * CO.dot(D)
c = CO.dot(CO) - sphere.radius * sphere.radius
discriminant = b * b - 4 * a * c
if discriminant < 0:
return float('inf'), float('inf')
t1 = (-b + math.sqrt(discriminant)) / (2 * a)
t2 = (-b - math.sqrt(discriminant)) / (2 * a)
return t1, t2
def trace_ray(self, O: Vector3, D: Vector3, t_min: float, t_max: float, recursion_depth: int = 3) -> Tuple[int, int, int]:
closest_t = float('inf')
closest_sphere = None
for sphere in self.scene.spheres:
t1, t2 = self.intersect_ray_sphere(O, D, sphere)
if t1 >= t_min and t1 <= t_max and t1 < closest_t:
closest_t = t1
closest_sphere = sphere
if t2 >= t_min and t2 <= t_max and t2 < closest_t:
closest_t = t2
closest_sphere = sphere
if closest_sphere is None:
return self.background_color
# Compute intersection point and normal
P = O + D * closest_t
N = (P - closest_sphere.center).normalize()
# Compute lighting
local_color = closest_sphere.color
intensity = self.compute_lighting(P, N, -D, closest_sphere.specular)
# Compute reflected color
if recursion_depth <= 0 or closest_sphere.reflective <= 0:
return (int(local_color[0] * intensity), int(local_color[1] * intensity), int(local_color[2] * intensity))
R = self.reflect_ray(-D, N)
reflected_color = self.trace_ray(P, R, 0.001, float('inf'), recursion_depth - 1)
# Blend local and reflected color
return (
int(local_color[0] * (1 - closest_sphere.reflective) * intensity + reflected_color[0] * closest_sphere.reflective),
int(local_color[1] * (1 - closest_sphere.reflective) * intensity + reflected_color[1] * closest_sphere.reflective),
int(local_color[2] * (1 - closest_sphere.reflective) * intensity + reflected_color[2] * closest_sphere.reflective)
)
def reflect_ray(self, R: Vector3, N: Vector3) -> Vector3:
return N * 2 * N.dot(R) - R
def compute_lighting(self, P: Vector3, N: Vector3, V: Vector3, s: float) -> float:
intensity = 0.0
for light in self.scene.lights:
if light.type == "ambient":
intensity += light.intensity
else:
if light.type == "point":
L = (light.position - P).normalize()
t_max = 1
else:
L = light.direction.normalize()
t_max = float('inf')
# Shadow check
shadow_sphere, shadow_t = self.closest_intersection(P, L, 0.001, t_max)
if shadow_sphere is not None:
continue
# Diffuse reflection
n_dot_l = N.dot(L)
if n_dot_l > 0:
intensity += light.intensity * n_dot_l / (N.length() * L.length())
# Specular reflection
if s != -1:
R = self.reflect_ray(L, N)
r_dot_v = R.dot(V)
if r_dot_v > 0:
intensity += light.intensity * (r_dot_v / (R.length() * V.length())) ** s
return intensity
def closest_intersection(self, O: Vector3, D: Vector3, t_min: float, t_max: float) -> Tuple[Sphere, float]:
closest_t = float('inf')
closest_sphere = None
for sphere in self.scene.spheres:
t1, t2 = self.intersect_ray_sphere(O, D, sphere)
if t1 >= t_min and t1 <= t_max and t1 < closest_t:
closest_t = t1
closest_sphere = sphere
if t2 >= t_min and t2 <= t_max and t2 < closest_t:
closest_t = t2
closest_sphere = sphere
return closest_sphere, closest_t
def render_scene(self):
from PIL import Image
image = Image.new("RGB", (self.canvas_width, self.canvas_height), self.background_color)
pixels = image.load()
for x in range(-self.canvas_width // 2, self.canvas_width // 2):
for y in range(-self.canvas_height // 2, self.canvas_height // 2):
D = self.canvas_to_viewport(x, -y)#flipping my canvas so my spheres are not rendered upsdide down
color = self.trace_ray(self.camera_position, D, 1, float('inf'))
pixels[x + self.canvas_width // 2, y + self.canvas_height // 2] = color
image.show()
image.save("output.png")