效果展示

最终结果

制作思路

  1. 从相机向每个像素方向射出射线

  2. 每条射线碰撞后有一定几率继续传播,否则直接返回。

  3. 光线只有打到光源处才会返回带有亮度的颜色,否则返回黑色

  4. 材质使用简化的光线反射方程:

    image

    这里漫反射采用了更简化的重要性采样方式,直接在单位球面均匀采样然后投影

    简化的重要性采样

代码实现

射线

直接构造一个简单的结构体即可

1
2
3
4
5
6
7
8
9
10
11
import taichi as ti

vec3f = ti.types.vector(3, ti.f32)
Ray = ti.types.struct(
ori=vec3f,
dir=vec3f
)

@ti.func
def ray_at(ray, t):
return ray.ori + t * ray.dir

场景物品

首先是场景内的球体(球体好定义且好求交)

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
# 球形物体
@ti.data_oriented
class Sphere:
def __init__(self, center, radius, material, color):
self.center = center
self.radius = radius
self.material = material
self.color = color

# 求交函数
@ti.func
def hit(self, ray, t_min=0.001, t_max=10e8):
oc = ray.ori - self.center
a = ray.dir.dot(ray.dir)
b = 2.0 * oc.dot(ray.dir)
c = oc.dot(oc) - self.radius * self.radius
discriminant = b * b - 4 * a * c
is_hit = False
front_face = False
root = 0.0
hit_point = ti.Vector([0.0, 0.0, 0.0])
hit_point_normal = ti.Vector([0.0, 0.0, 0.0])
if discriminant > 0:
sqrtd = ti.sqrt(discriminant)
root = (-b - sqrtd) / (2 * a)
if root < t_min or root > t_max:
root = (-b + sqrtd) / (2 * a)
if root >= t_min and root <= t_max:
is_hit = True
else:
is_hit = True
if is_hit:
hit_point = ray_at(ray, root)
hit_point_normal = (hit_point - self.center) / self.radius
# 检测是外面还是里面
if ray.dir.dot(hit_point_normal) < 0:
front_face = True
else:
hit_point_normal = -hit_point_normal
return is_hit, root, hit_point, hit_point_normal, front_face, self.material, self.color

然后是整个场景的列表以及求交函数:

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
@ti.data_oriented
class SceneList:
def __init__(self):
self.list = []

def add(self, obj):
self.list.append(obj)

def clear(self):
self.list = []

# 求交函数
@ti.func
def hit(self, ray, t_min=0.001, t_max=10e8):
closest_t = t_max
is_hit = False
front_face = False
hit_point = ti.Vector([0.0, 0.0, 0.0])
hit_point_normal = ti.Vector([0.0, 0.0, 0.0])
color = ti.Vector([0.0, 0.0, 0.0])
material = 1
for index in ti.static(range(len(self.list))):
is_hit_tmp, root_tmp, hit_point_tmp, hit_point_normal_tmp, front_face_tmp, material_tmp, color_tmp = \
self.list[index].hit(ray, t_min, closest_t)
if is_hit_tmp:
closest_t = root_tmp
is_hit = is_hit_tmp
hit_point = hit_point_tmp
hit_point_normal = hit_point_normal_tmp
front_face = front_face_tmp
material = material_tmp
color = color_tmp
return is_hit, hit_point, hit_point_normal, front_face, material, color

摄像机

摄像机构造后初始化自己的一些属性

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
import taichi as ti
from Ray import Ray
PI = 3.1415926

@ti.data_oriented
class Camera:
def __init__(self, pos, lookat, up, fov, ratio):
self.pos = pos
self.lookat = lookat
self.up = up
self.fov = fov
self.ratio = ratio

# 需要计算的属性
self.lower_left_corner = ti.Vector([0.0, 0.0, 0.0])
self.vertical = ti.Vector([0.0, 0.0, 0.0])
self.horizontal = ti.Vector([0.0, 0.0, 0.0])
self.initialize()

# 计算一些自己的属性
def initialize(self):
theta = (self.fov / 180.0) * PI
half_height = ti.tan(theta / 2.0)
half_width = half_height * self.ratio
w = (self.pos - self.lookat).normalized()
u = (self.up.cross(w)).normalized()
v = w.cross(u)
self.lower_left_corner = self.pos - u * half_width - v * half_height - w
self.vertical = u * half_width * 2
self.horizontal = v * half_height * 2
print(self.pos, self.lower_left_corner, self.vertical, self.horizontal)

# 向给定UV方向射出射线
@ti.func
def shoot_ray(self, u, v):
origin = self.pos
direction = self.lower_left_corner + u * self.vertical + v * self.horizontal - origin
ray = Ray(ori=origin, dir=direction)
return ray

主函数

构造场景 然后从相机发出射线开始渲染

由于Taichi不支持递归,所以把递归拆解成循环执行

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
import taichi as ti
import numpy as np
from Scene import SceneList, Sphere
from Camera import Camera
from FunctionLib import random_unit_vector, reflect, refract, reflectance

ti.init(arch=ti.cuda)

res = 900
max_depth = 50
p_rr = 0.8

pixels = ti.Vector.field(3, dtype=ti.f32, shape=(res, res))


@ti.kernel
def render(frame: ti.int32):
for i, j in pixels:
u = (i + ti.random()) / float(res)
v = (j + ti.random()) / float(res)

# 摄像机发出射线
color = ti.Vector([0.0, 0.0, 0.0])
ray = camera.shoot_ray(u, v)
color += ray_color(ray)

# 根据帧数加权混合颜色
if frame == 1:
pixels[i, j] += color
else:
inverse_frame = 1 / frame
pixels[i, j] *= 1 - inverse_frame
pixels[i, j] += color * inverse_frame


@ti.func
def ray_color(ray):
final_color = ti.Vector([1.0, 1.0, 1.0])
final_brightness = 0

for n in range(max_depth):
if ti.random() > p_rr:
break
is_hit, hit_point, hit_point_normal, front_face, material, color = scene.hit(ray)
if is_hit:
# 灯
if material == 0:
final_color *= color * 10
final_brightness = 1
break
# 漫反射
elif material == 1:
target = hit_point + hit_point_normal * 1
target += random_unit_vector()
ray.ori = hit_point
ray.dir = target - hit_point
final_color *= color
# 金属
elif material == 2 or material == 4:
fuzz = 0.0
if material == 4:
fuzz = 0.3
ray.dir = reflect(ray.dir.normalized(), hit_point_normal)
ray.dir += fuzz * random_unit_vector()
ray.ori = hit_point
if ray.dir.dot(hit_point_normal) <= 0:
break
else:
final_color *= color
# 玻璃
elif material == 3:
IOR = 1.5
if front_face:
IOR = 1 / IOR
cos_theta = min(-ray.dir.normalized().dot(hit_point_normal), 1.0)
sin_theta = ti.sqrt(1 - cos_theta * cos_theta)
# total internal reflection
if IOR * sin_theta > 1.0 or reflectance(cos_theta, IOR) > ti.random():
ray.dir = reflect(ray.dir.normalized(), hit_point_normal)
else:
ray.dir = refract(ray.dir.normalized(), hit_point_normal, IOR)
ray.ori = hit_point
final_color *= color

final_color /= p_rr

return final_color * final_brightness

# Gamma矫正
@ti.kernel
def gamma():
for i, j in pixels:
pixels[i, j] = ti.pow(pixels[i, j], 1/2.2)


scene = SceneList()
# Light source
scene.add(Sphere(center=ti.Vector([0, 5.4, -1]), radius=3.0, material=0, color=ti.Vector([1.0, 1.0, 1.0])))
# Ground
scene.add(Sphere(center=ti.Vector([0, -100.5, -1]), radius=100.0, material=1, color=ti.Vector([0.8, 0.8, 0.8])))
# ceiling
scene.add(Sphere(center=ti.Vector([0, 102.5, -1]), radius=100.0, material=1, color=ti.Vector([0.8, 0.8, 0.8])))
# back wall
scene.add(Sphere(center=ti.Vector([0, 1, 101]), radius=100.0, material=1, color=ti.Vector([0.8, 0.8, 0.8])))
# right wall
scene.add(Sphere(center=ti.Vector([-101.5, 0, -1]), radius=100.0, material=1, color=ti.Vector([0.6, 0.0, 0.0])))
# left wall
scene.add(Sphere(center=ti.Vector([101.5, 0, -1]), radius=100.0, material=1, color=ti.Vector([0.0, 0.6, 0.0])))

# Diffuse ball
scene.add(Sphere(center=ti.Vector([0, -0.2, -1.5]), radius=0.3, material=1, color=ti.Vector([0.8, 0.3, 0.3])))
# Metal ball
scene.add(Sphere(center=ti.Vector([-0.7, 0.2, -0.7]), radius=0.7, material=2, color=ti.Vector([0.6, 0.8, 0.8])))
# Glass ball
scene.add(Sphere(center=ti.Vector([0.7, 0, -0.5]), radius=0.5, material=3, color=ti.Vector([1.0, 1.0, 1.0])))
# Metal ball-2
scene.add(Sphere(center=ti.Vector([0.6, -0.3, -2.0]), radius=0.2, material=4, color=ti.Vector([0.8, 0.6, 0.2])))

# 构造相机
camera = Camera(ti.Vector([0.0, 1.0, -5.0]), ti.Vector([0.0, 1.0, 0.0]), ti.Vector([0.0, 1.0, 0.0]), 60, 1)

gui = ti.GUI(name="Path Tracing", res=res)

frame = 0
while gui.running:
frame += 1
render(frame)
gui.set_image(np.power(pixels.to_numpy(), 1 / 2.2))
gui.show()

资源

https://github.com/1keven1/TaichiClassS1/tree/master/MyPathTracing