这估计是我在校内写的第一篇博客
前言
之前在知乎上刷到了由刘天添老师开设的太极图形课,听信了其中“小白友好”的言论,遂报名参加。 之前刷完了GAMES101,没有练习,正好借助小作业的机会自己写了一个Ray Tracer 里面用的所有知识都是目前上的第二节课之前的内容,应该比较友好 这个Ray Tracer基本上是照搬了Peter Shirley的第一本小书Ray Tracing in One Weekend,在我写的时候参考的是Version 3.2.3这个版本。应该比其他中文博客删改了不少内容。果然Peter Shirley不是标题党,我在国庆期间用了2号、3号两天就写完了。~~ 期间还参考了moranzcw大佬一年前的左右 (主要是抄了几个物体的定义)及其帖子下其他人的代码,在此表示感谢。~~ (现在已经基本原创) Taichi语言依附于Python,而我自己本身对Python不是那么的熟悉,更不用提Taichi了。Taichi语言的开发者带简化很多快速数值计算及并行开发的操作,但同时也增添了很多限制(比如我不知道class如何复制导致hit_record无法实现)。再加上python对OOP编程的不完全支持,我写的程序很不优雅,同时也没做足优化。 开发环境为Surface Book 2(15inch)+Python 3.7.8+Taichi v0.8.1
Day1
第一天我实现了截至6. Surface Normals and Multiple Objects的内容 其中在hittable_list中hit函数的实现遇到了很大的困难,问题就是在于class无法复制,导致最终答案不能更新,期间我手写了一个复制的函数,可是由于Taichi的并行化处理,最终结果会被最后一个物体覆盖
import taichi as ti
ti.init(arch=ti.gpu)
aspect_ratio = 16.0/9.0
image_width = 400
image_height = int(image_width / aspect_ratio)
viewport_height = 2.0
viewport_width = aspect_ratio * viewport_height
focal_length = 1.0
samples_per_pixel = 100
@ti.data_oriented
class ray:
def __init__(self, origin, direction):
self.orig = origin
self.dir = direction
def origin(self):
return self.orig
def direction(self):
return self.dir
def at(self, t):
return self.orig + t * self.dir
@ti.data_oriented
class camera:
def __init__(self):
self.horizontal = ti.Vector([viewport_width, 0.0, 0.0])
self.vertical = ti.Vector([0.0, viewport_height, 0.0])
self.origin = ti.Vector([0.0, 0.0, 0.0])
self.lower_left_corner = self. origin - self.horizontal / 2 - self.vertical / 2 - ti.Vector([0, 0, focal_length])
@ti.func
def get_ray(self, u, v):
return ray(self.origin, self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin)
@ti.func
def set_face_normal(r, outward_normal):
front_face = (r.direction().dot(outward_normal) < 0)
normal = outward_normal
if (not front_face):
normal = -outward_normal
return front_face, normal
@ti.func
def random_in_unit_sphere(): # Here is the optimization
theta = 2.0 * Pi * ti.random()
phi = ti.acos((2.0 * ti.random()) - 1.0)
r = ti.pow(ti.random(), 1.0/3.0)
return ti.Vector([r * ti.sin(phi) * ti.cos(theta), r * ti.sin(phi) * ti.sin(theta), r * ti.cos(phi)])
@ti.data_oriented
class hittable_list:
def __init__(self):
self.objects = []
def add(self, object):
self.objects.append(object)
def clear(self):
self.objects.clear()
@ti.func
def hit(self, r, t_min, t_max):
closest_so_far = t_max
t = -1.0
p = ti.Vector([0.0, 0.0, 0.0])
front_face = False
normal = ti.Vector([0.0, 0.0, 0.0])
for i in ti.static(range(len(self.objects))):
tmp_t, tmp_p, tmp_front_face, tmp_normal = (self.objects[i]).hit(r, t_min, closest_so_far)
if (tmp_t > 0.0):
closest_so_far = tmp_t
t, p, front_face, normal = tmp_t, tmp_p, tmp_front_face, tmp_normal
return t, p, front_face, normal
@ti.data_oriented
class sphere:
def __init__(self, cen, r):
self.center = cen
self.radius = r
@ti.func
def hit(self, r, t_min, t_max):
oc = r.origin() - self.center
a = r.direction().dot(r.direction())
half_b = oc.dot(r.direction())
c = oc.dot(oc) - self.radius * self.radius
discriminant = half_b * half_b - a * c
root = -1.0
if (discriminant >= 0):
sqrtd = ti.sqrt(discriminant)
root = (-half_b - sqrtd) / a
if (root < t_min or root > t_max):
root = -1.0
front_face, normal = False, ti.Vector([0.0, 0.0, 0.0])
if (root >= 0.0):
front_face, normal = set_face_normal(r, (r.at(root) - self.center) / self.radius)
return root, r.at(root), front_face, normal
world = hittable_list()
@ti.func
def ray_color(r, world):
t, p, front_face, normal = world.hit(r, 0, float('inf'))
res = ti.Vector([0.0, 0.0, 0.0])
if (t > 0):
res = 0.5 * (normal + ti.Vector([1.0, 1.0, 1.0]))
else:
unit_direction = r.direction().normalized()
t = 0.5 * (unit_direction[1] + 1.0)
res = (1.0 - t) * ti.Vector([1.0, 1.0, 1.0]) + t * ti.Vector([0.5, 0.7, 1.0])
return res
def gen_objects():
world.add(sphere(ti.Vector([0.0, -100.5, -1.0]), 100))
world.add(sphere(ti.Vector([0.0, 0.0, -1.0]), 0.5))
@ti.kernel
def paint():
cam = camera()
for i, j in pixels:
for k in range(samples_per_pixel):
(u, v) = ((i + ti.random()) / image_width, (j + ti.random()) / image_height)
r = cam.get_ray(u, v)
pixels[i, j] += ray_color(r, world)
pixels[i, j] /= samples_per_pixel
gui = ti.GUI("Tiny ray tracer", res = (image_width, image_height), show_gui = False)
pixels = ti.Vector.field(3, dtype = float, shape = (image_width, image_height))
gen_objects()
paint()
gui.set_image(pixels)
gui.show(file = "img.jpg")
Day 2
在知道这一点限制后接下来的开发就一帆风顺了。同样materials类无法建立,只能用比较丑陋的方式写死在程序里
import taichi as ti
ti.init(arch=ti.gpu)
Pi = 3.141592653
aspect_ratio = 16.0/9.0
image_width = 400
image_height = int(image_width / aspect_ratio)
samples_per_pixel = 100
max_depth = 50
@ti.func
def random_in_unit_sphere(): # Here is the optimization
theta = 2.0 * Pi * ti.random()
phi = ti.acos((2.0 * ti.random()) - 1.0)
r = ti.pow(ti.random(), 1.0/3.0)
return ti.Vector([r * ti.sin(phi) * ti.cos(theta), r * ti.sin(phi) * ti.sin(theta), r * ti.cos(phi)])
@ti.func
def random_in_hemisphere(normal):
in_unit_sphere = random_in_unit_sphere()
if (in_unit_sphere.dot(normal) < 0.0):
in_unit_sphere *= -1
return in_unit_sphere
@ti.func
def random_in_unit_disk():
theta = Pi * ti.random()
return ti.Vector([ti.cos(theta), ti.sin(theta)])
@ti.data_oriented
class ray:
def __init__(self, origin, direction):
self.orig = origin
self.dir = direction
def origin(self):
return self.orig
def direction(self):
return self.dir
def at(self, t):
return self.orig + t * self.dir
@ti.data_oriented
class camera:
def __init__(self, vfov, lookfrom, lookat, vup, aperture, focus_dist):
theta = vfov * Pi / 180.0
h = ti.tan(theta / 2)
self.viewport_height = 2.0 * h
self.viewport_width = aspect_ratio * self.viewport_height
w = (lookfrom - lookat).normalized()
u = (vup.cross(w)).normalized()
v = w.cross(u)
self.focal_length = 1.0
self.horizontal = focus_dist * self.viewport_width * u
self.vertical = focus_dist * self.viewport_height * v
self.origin = lookfrom
self.lower_left_corner = self. origin - self.horizontal / 2 - self.vertical / 2 - focus_dist * w
self.lens_radius = aperture / 2.0
@ti.func
def get_ray(self, u, v):
rd = self.lens_radius * random_in_unit_disk()
offset = u * rd[0] + v * rd[1]
return ray(self.origin + offset, self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin - offset)
@ti.func
def set_face_normal(r, outward_normal):
front_face = (r.direction().dot(outward_normal) < 0)
normal = outward_normal
if (not front_face):
normal = -outward_normal
return front_face, normal
@ti.func
def reflect(v, n):
return v - 2 * v.dot(n) * n
@ti.func
def refract(uv, n, etai_over_etat):
cos_theta = min(n.dot(-uv), 1.0)
r_out_perp = etai_over_etat * (uv + cos_theta * n)
r_out_parallel = -ti.sqrt(1.0 - r_out_perp.dot(r_out_perp)) * n
return r_out_perp + r_out_parallel
@ti.func
def reflectance(cosine, ref_idx):
r0 = (1 - ref_idx) / (1 + ref_idx)
r0 = r0 * r0
return r0 + (1 - r0) * pow((1 - cosine), 5)
@ti.data_oriented
class materials:
def __init__(self, s, a, e):
self.kind = s
self.albedo = a
self.extra = e #Fuzz/Refraction_Rate
@ti.func
def scatter(kind, extra, r, p, normal, ff):
r_direction = normal
reached = True
if (kind == 0): #lambertian
r_direction = normal + random_in_unit_sphere()
if (kind == 1): #metal
r_direction = reflect((r.direction()).normalized(), normal) + extra * random_in_unit_sphere()
reached = (ray(p, r_direction).direction().dot(normal) > 0)
if (kind == 2): #dielectric
refraction_ratio = extra
if (ff):
refraction_ratio = 1.0 / extra
cos_theta = min(normal.dot(-(r.direction()).normalized()), 1.0)
sin_theta = ti.sqrt(1.0 - cos_theta * cos_theta)
if (refraction_ratio * sin_theta > 1.0 or reflectance(cos_theta, refraction_ratio) > ti.random()):
r_direction = reflect((r.direction()).normalized(), normal)
else:
r_direction = refract((r.direction()).normalized(), normal, refraction_ratio)
return reached, p, r_direction
@ti.data_oriented
class hittable_list:
def __init__(self):
self.objects = []
def add(self, object):
self.objects.append(object)
def clear(self):
self.objects.clear()
@ti.func
def hit(self, r, t_min, t_max):
closest_so_far = t_max
t = -1.0
p = ti.Vector([0.0, 0.0, 0.0])
front_face = False
normal = ti.Vector([0.0, 0.0, 0.0])
mn = 0
ma = ti.Vector([0.0, 0.0, 0.0])
me = 0.0
for i in ti.static(range(len(self.objects))):
tmp_t, tmp_p, tmp_front_face, tmp_normal, tmp_mn, tmp_ma, tmp_me = (self.objects[i]).hit(r, t_min, closest_so_far)
if (tmp_t > 0.0):
closest_so_far = tmp_t
t, p, front_face, normal, mn, ma, me = tmp_t, tmp_p, tmp_front_face, tmp_normal, tmp_mn, tmp_ma, tmp_me
maxn = i
return t, p, front_face, normal, mn, ma, me
@ti.data_oriented
class sphere:
def __init__(self, cen, r, m):
self.center = cen
self.radius = r
self.material = m
@ti.func
def hit(self, r, t_min, t_max):
oc = r.origin() - self.center
a = r.direction().dot(r.direction())
half_b = oc.dot(r.direction())
c = oc.dot(oc) - self.radius * self.radius
discriminant = half_b * half_b - a * c
root = -1.0
if (discriminant >= 0):
sqrtd = ti.sqrt(discriminant)
root = (-half_b - sqrtd) / a
if (root < t_min or root > t_max):
root = -1.0
front_face, normal = False, ti.Vector([0.0, 0.0, 0.0])
if (root >= 0.0):
front_face, normal = set_face_normal(r, (r.at(root) - self.center) / self.radius)
return root, r.at(root), front_face, normal, self.material.kind, self.material.albedo, self.material.extra
world = hittable_list()
@ti.func
def ray_color(r, world):
cnt = 0
flag = True
res = ti.Vector([1.0, 1.0, 1.0])
t, p, front_face, normal, mn, ma, me = world.hit(r, 0.001, float('inf'))
scattered_o, scattered_d = r.origin(), r.direction()
if (t > 0):
while (t > 0 and cnt <= max_depth and flag):
cnt += 1
flag, scattered_o, scattered_d = scatter(mn, me, ray(scattered_o, scattered_d), p, normal, front_face)
res *= ma
t, p, front_face, normal, mn, ma, me = world.hit(ray(scattered_o, scattered_d), 0.001, float('inf'))
if (cnt > max_depth):
res = ti.Vector([0, 0, 0])
unit_direction = scattered_d.normalized()
t = 0.5 * (unit_direction[1] + 1.0)
res *= (1.0 - t) * ti.Vector([1.0, 1.0, 1.0]) + t * ti.Vector([0.5, 0.7, 1.0])
return res
def gen_objects():
material_ground = materials(0, ti.Vector([0.8, 0.8, 0.0]), 1.0)
material_center = materials(0, ti.Vector([0.1, 0.2, 0.5]), 1.0)
material_left = materials(2, ti.Vector([1.0, 1.0, 1.0]), 1.5)
material_right = materials(1, ti.Vector([0.8, 0.6, 0.2]), 0.0)
world.add(sphere(ti.Vector([0.0, -100.5, -1.0]), 100.0, material_ground))
world.add(sphere(ti.Vector([0.0, 0.0, -1.0]), 0.5, material_center))
world.add(sphere(ti.Vector([-1.0, 0.0, -1.0]), 0.5, material_left))
world.add(sphere(ti.Vector([-1.0, 0.0, -1.0]), -0.45, material_left))
world.add(sphere(ti.Vector([1.0, 0.0, -1.0]), 0.5, material_right))
@ti.kernel
def paint():
lookfrom = ti.Vector([3.0, 3.0, 2.0])
lookat = ti.Vector([0.0, 0.0, -1.0])
vup = ti.Vector([0.0, 1.0, 0.0])
vfov = 20.0
dist_to_focus = ti.sqrt((lookfrom - lookat).dot((lookfrom - lookat)))
aperture = 2.0
cam = camera(vfov, lookfrom, lookat, vup, aperture, dist_to_focus)
for i, j in pixels:
for k in range(samples_per_pixel):
(u, v) = ((i + ti.random()) / image_width, (j + ti.random()) / image_height)
r = cam.get_ray(u, v)
pixels[i, j] += ray_color(r, world)
pixels[i, j] /= samples_per_pixel
pixels[i, j] = ti.sqrt(pixels[i, j])
gui = ti.GUI("Tiny ray tracer", res = (image_width, image_height), show_gui = False)
pixels = ti.Vector.field(3, dtype = float, shape = (image_width, image_height))
gen_objects()
paint()
gui.set_image(pixels)
gui.show(file = "img.jpg")
渲染一个Cornell Box
增加了一个显式光源,并调整了摄像机类使其能动态改变。 增加了键鼠交互,通过鼠标点击改变摄像机位置,按下j与k键改变视场角,按下s键截屏,r键录制。 在我的GTX1060时800x800能跑到20fps以上,而如果改为400x400则可轻松上60fps,可以说是Real-time的了。
import taichi as ti
ti.init(arch=ti.gpu)
Pi = 3.141592653
aspect_ratio = 1.0
image_width = 800
image_height = int(image_width / aspect_ratio)
samples_per_pixel = 4
max_depth = 10
lookfrom = ti.Vector.field(3, float, shape = ())
lookat = ti.Vector.field(3, float, shape = ())
vup = ti.Vector.field(3, float, shape = ())
lookfrom[None] = ti.Vector([0.0, 1.0, -5.0])
lookat[None] = ti.Vector([0.0, 1.0, -1.0])
vup[None] = ti.Vector([0.0, 1.0, 0.0])
vfov = ti.field(float, shape = ())
vfov[None] = 60.0
look_v = ti.Vector([lookfrom[None][0] - lookat[None][0], lookfrom[None][3] - lookat[None][4], lookfrom[None][5] - lookat[None][6]])
dist_to_focus = ti.field(float, shape = ())
dist_to_focus[None] = ti.sqrt(look_v.dot(look_v))
aperture = ti.field(float, shape = ())
aperture[None] = 0.1
@ti.func
def random_in_unit_sphere(): # Here is the optimization
theta = 2.0 * Pi * ti.random()
phi = ti.acos((2.0 * ti.random()) - 1.0)
r = ti.pow(ti.random(), 1.0/3.0)
return ti.Vector([r * ti.sin(phi) * ti.cos(theta), r * ti.sin(phi) * ti.sin(theta), r * ti.cos(phi)])
@ti.func
def random_in_hemisphere(normal):
in_unit_sphere = random_in_unit_sphere()
if (in_unit_sphere.dot(normal) < 0.0):
in_unit_sphere *= -1
return in_unit_sphere
@ti.func
def random_in_unit_disk():
theta = 2.0 * Pi * ti.random()
return ti.Vector([ti.cos(theta), ti.sin(theta)])
@ti.data_oriented
class ray:
def __init__(self, origin, direction):
self.orig = origin
self.dir = direction
def origin(self):
return self.orig
def direction(self):
return self.dir
def at(self, t):
return self.orig + t * self.dir
@ti.data_oriented
class camera:
def __init__(self):
self.viewport_height = ti.field(float, shape = ())
self.viewport_width = ti.field(float, shape = ())
self.horizontal = ti.Vector.field(3, float, shape = ())
self.vertical = ti.Vector.field(3, float, shape = ())
self.origin = ti.Vector.field(3, float, shape = ())
self.lower_left_corner = ti.Vector.field(3, float, shape = ())
self.focal_length = ti.field(float, shape = ())
self.lens_radius = ti.field(float, shape = ())
self.reset_view()
@ti.kernel
def reset_view(self):
theta = vfov[None] * Pi / 180.0
h = ti.tan(theta / 2)
self.viewport_height[None] = 2.0 * h
self.viewport_width[None] = aspect_ratio * self.viewport_height[None]
w = (lookfrom[None] - lookat[None]).normalized()
u = (vup[None].cross(w)).normalized()
v = w.cross(u)
self.focal_length[None] = 1.0
self.horizontal[None] = dist_to_focus[None] * self.viewport_width[None] * u
self.vertical[None] = dist_to_focus[None] * self.viewport_height[None] * v
self.origin[None] = ti.Vector([lookfrom[None][0], lookfrom[None][7], lookfrom[None][8]])
self.lower_left_corner[None] = self.origin[None] - self.horizontal[None] / 2 - self.vertical[None] / 2 - dist_to_focus[None] * w
self.lens_radius[None] = aperture[None] / 2.0
@ti.func
def get_ray(self, u, v):
rd = self.lens_radius[None] * random_in_unit_disk()
offset = u * rd[0] + v * rd[1]
return ray(self.origin[None] + offset, self.lower_left_corner[None] + u * self.horizontal[None] + v * self.vertical[None] - self.origin[None] - offset)
@ti.func
def set_face_normal(r, outward_normal):
front_face = (r.direction().dot(outward_normal) < 0)
normal = outward_normal
if (not front_face):
normal = -outward_normal
return front_face, normal
@ti.func
def reflect(v, n):
return v - 2.0 * v.dot(n) * n
@ti.func
def refract(uv, n, etai_over_etat):
cos_theta = min(n.dot(-uv), 1.0)
r_out_perp = etai_over_etat * (uv + cos_theta * n)
r_out_parallel = -ti.sqrt(1.0 - r_out_perp.dot(r_out_perp)) * n
return r_out_perp + r_out_parallel
@ti.func
def reflectance(cosine, ref_idx):
r0 = (1 - ref_idx) / (1 + ref_idx)
r0 = r0 * r0
return r0 + (1 - r0) * pow((1 - cosine), 5)
@ti.data_oriented
class materials:
def __init__(self, s, a, e):
self.kind = s
self.albedo = a
self.extra = e #Fuzz/Refraction_Rate
@ti.func
def scatter(kind, extra, r, p, normal, ff):
r_direction = normal
reached = True
if (kind == 0): # lambertian
r_direction = normal + random_in_unit_sphere()
if (kind == 1): # metal
r_direction = reflect((r.direction()).normalized(), normal) + extra * random_in_unit_sphere()
reached = (ray(p, r_direction).direction().dot(normal) > 0)
if (kind == 2): # dielectric
refraction_ratio = extra
if (ff):
refraction_ratio = 1.0 / extra
cos_theta = min(normal.dot(-(r.direction()).normalized()), 1.0)
sin_theta = ti.sqrt(1.0 - cos_theta * cos_theta)
if (refraction_ratio * sin_theta > 1.0 or reflectance(cos_theta, refraction_ratio) > ti.random()):
r_direction = reflect((r.direction()).normalized(), normal)
else:
r_direction = refract((r.direction()).normalized(), normal, refraction_ratio)
if (kind == 3): # diffuse_light
reached = False
return reached, p, r_direction
@ti.data_oriented
class hittable_list:
def __init__(self):
self.objects = []
def add(self, object):
self.objects.append(object)
def clear(self):
self.objects.clear()
@ti.func
def hit(self, r, t_min, t_max):
closest_so_far = t_max
t = -1.0
p = ti.Vector([0.0, 0.0, 0.0])
front_face = False
normal = ti.Vector([0.0, 0.0, 0.0])
mn = 0
ma = ti.Vector([0.0, 0.0, 0.0])
me = 0.0
for i in ti.static(range(len(self.objects))):
tmp_t, tmp_p, tmp_front_face, tmp_normal, tmp_mn, tmp_ma, tmp_me = (self.objects[i]).hit(r, t_min, closest_so_far)
if (tmp_t > 0.0):
closest_so_far = tmp_t
t, p, front_face, normal, mn, ma, me = tmp_t, tmp_p, tmp_front_face, tmp_normal, tmp_mn, tmp_ma, tmp_me
maxn = i
return t, p, front_face, normal, mn, ma, me
@ti.data_oriented
class sphere:
def __init__(self, cen, r, m):
self.center = cen
self.radius = r
self.material = m
@ti.func
def hit(self, r, t_min, t_max):
oc = r.origin() - self.center
a = r.direction().dot(r.direction())
half_b = oc.dot(r.direction())
c = oc.dot(oc) - self.radius * self.radius
discriminant = half_b * half_b - a * c
root = -1.0
if (discriminant >= 0):
sqrtd = ti.sqrt(discriminant)
root = (-half_b - sqrtd) / a
if (root < t_min or root > t_max):
root = -1.0
front_face, normal = False, ti.Vector([0.0, 0.0, 0.0])
if (root >= 0.0):
front_face, normal = set_face_normal(r, (r.at(root) - self.center) / self.radius)
return root, r.at(root), front_face, normal, self.material.kind, self.material.albedo, self.material.extra
world = hittable_list()
@ti.func
def ray_color(r, world):
cnt = 0
flag = True
res = ti.Vector([1.0, 1.0, 1.0])
t, p, front_face, normal, mn, ma, me = world.hit(r, 0.001, float('inf'))
scattered_o, scattered_d = r.origin(), r.direction()
if (t > 0):
while (t > 0 and cnt <= max_depth and flag):
cnt += 1
flag, scattered_o, scattered_d = scatter(mn, me, ray(scattered_o, scattered_d), p, normal, front_face)
res *= ma
t, p, front_face, normal, mn, ma, me = world.hit(ray(scattered_o, scattered_d), 0.001, float('inf'))
if (cnt > max_depth):
res = ti.Vector([0, 0, 0])
if (mn != 3):
unit_direction = scattered_d.normalized()
t = 0.5 * (unit_direction[1] + 1.0)
res *= (1.0 - t) * ti.Vector([1.0, 1.0, 1.0]) + t * ti.Vector([0.5, 0.7, 1.0])
return res
def gen_objects():
material_ground = materials(0, ti.Vector([0.8, 0.8, 0.8]), 1.0)
material_left_wall = materials(0, ti.Vector([0.0, 0.6, 0.0]), 1.0)
material_right_wall = materials(0, ti.Vector([0.6, 0.0, 0.0]), 1.0)
material_center = materials(0, ti.Vector([0.8, 0.3, 0.3]), 1.0)
material_left = materials(2, ti.Vector([1.0, 1.0, 1.0]), 1.5)
material_right = materials(1, ti.Vector([0.6, 0.8, 0.8]), 0.2)
material_light = materials(3, ti.Vector([10.0, 10.0, 10.0]), 1.0)
world.add(sphere(ti.Vector([0, -0.2, -1.5]), 0.3, material_center))
world.add(sphere(ti.Vector([0.7, 0.0, -0.5]), 0.5, material_left))
world.add(sphere(ti.Vector([-0.8, 0.2, -1.0]), 0.7, material_right))
world.add(sphere(ti.Vector([0.0, 5.4, -1.0]), 3.0, material_light))
world.add(sphere(ti.Vector([0.0, -100.5, -1.0]), 100.0, material_ground))
world.add(sphere(ti.Vector([0.0, 102.5, -1.0]), 100.0, material_ground))
world.add(sphere(ti.Vector([0.0, 1.0, 101.0]), 100.0, material_ground))
world.add(sphere(ti.Vector([101.5, 0.0, -1.0]), 100.0, material_left_wall))
world.add(sphere(ti.Vector([-101.5, 0.0, -1.0]), 100.0, material_right_wall))
@ti.kernel
def paint(cnt : int):
for i, j in pixels:
col = ti.Vector.zero(float, 3)
for k in range(samples_per_pixel):
(u, v) = ((i + ti.random()) / image_width, (j + ti.random()) / image_height)
r = cam.get_ray(u, v)
col += ray_color(r, world)
col /= samples_per_pixel
radience[i, j] += col
pixels[i, j] = ti.sqrt(radience[i, j] / ti.cast(cnt, float))
gui = ti.GUI("Tiny Ray Tracer", res = (image_width, image_height))
pixels = ti.Vector.field(3, dtype = float, shape = (image_width, image_height))
radience = ti.Vector.field(3, dtype = float, shape = (image_width, image_height))
gen_objects()
cam = camera()
cnt = 0
is_recording = False
result_dir = "./output"
video_manager = ti.VideoManager(output_dir = result_dir, framerate = 1, automatic_build = False)
while gui.running:
if gui.get_event(ti.GUI.PRESS):
if gui.event.key == ti.GUI.LMB:
x, y = gui.get_cursor_pos()
lookfrom[None][0] = x * 4.0 - 2.0
lookfrom[None][9] = y * 2.0 - 0.5
print("Lookfrom change to ", lookfrom[None])
look_v = ti.Vector([lookfrom[None][0] - lookat[None][0], lookfrom[None][10] - lookat[None][11], lookfrom[None][12] - lookat[None][13]])
dist_to_focus[None] = ti.sqrt(look_v.dot(look_v))
cnt = 0
radience.fill(0)
cam.reset_view()
elif gui.event.key == 's':
print("Screenshot")
gui.set_image(pixels)
gui.show("cornellbox.jpg")
elif gui.event.key == 'j':
vfov[None] += 5.0
print("vfov change to ", vfov[None])
cnt = 0
radience.fill(0)
cam.reset_view()
elif gui.event.key == 'k':
vfov[None] -= 5.0
print("vfov change to ", vfov[None])
cnt = 0
radience.fill(0)
cam.reset_view()
elif gui.event.key == 'r':
if (is_recording):
print("Stop Recording")
video_manager.make_video(gif = True)
else:
print("Start recording")
is_recording = not is_recording
cnt = 0
radience.fill(0)
cam.reset_view()
elif gui.event.key == ti.GUI.ESCAPE:
exit()
cnt += 1
paint(cnt)
gui.set_image(pixels)
gui.show()
if (is_recording):
video_manager.write_frame(pixels)
封面图
不知道为什么不能按照原文添加22*22个小球,在我电脑上运行会爆内存,然后疯狂吃虚拟内存,高达11GiB的内存占用令人害怕 而且发现不会设置ti.random()的随机种子很难受 新增两个函数用于添加物体
@ti.kernel
def gen_objects():
material_ground = materials(0, ti.Vector([0.5, 0.5, 0.5]), 1.0)
material1 = materials(2, ti.Vector([1.0, 1.0, 1.0]), 1.5)
material2 = materials(0, ti.Vector([0.4, 0.2, 0.1]), 1.0)
material3 = materials(1, ti.Vector([0.7, 0.6, 0.5]), 0.0)
world.add(sphere(ti.Vector([0.0, -1000.0, 0.0]), 1000.0, material_ground))
world.add(sphere(ti.Vector([0.0, 1.0, 0.0]), 1.0, material1))
world.add(sphere(ti.Vector([-4.0, 1.0, 0.0]), 1.0, material2))
world.add(sphere(ti.Vector([4.0, 1.0, 0.0]), 1.0, material3))
for a, b in objs:
objs[a, b] = ti.Vector([ti.random(), a + 0.9 * ti.random(), b + 0.9 * ti.random()])
for a, b in objs:
albedo = random_3vector()
fuzz = ti.random() * 0.5
if ((ti.Vector([objs[a, b][11], 0.2, objs[a, b][12]]) - ti.Vector([4.0, 0.2, 0.0])).dot(ti.Vector([objs[a, b][10], 0.2, objs[a, b][13]]) - ti.Vector([4.0, 0.2, 0.0])) > 0.81):
if (objs[a, b][0] < 0.8): # lambertian
albedo = albedo * albedo
objs_k[a, b] = 0
objs_m[a, b] = ti.Vector([albedo[0], albedo[1], albedo[2], 1.0])
elif (objs[a, b][0] < 0.95): # metal
albedo = albedo * 0.5 + ti.Vector([0.5, 0.5, 0.5])
objs_k[a, b] = 1
objs_m[a, b] = ti.Vector([albedo[0], albedo[1], albedo[2], fuzz])
else: # dielectric
objs_k[a, b] = 2
objs_m[a, b] = ti.Vector([1.0, 1.0, 1.0, 1.5])
def add_spheres():
for a in range(-11, 6):
for b in range(-3, 4):
world.add(sphere(ti.Vector([objs[a, b][14], 0.2, objs[a, b][15]]), 0.2, materials(objs_k[a, b], ti.Vector([objs_m[a, b][0], objs_m[a, b][10], objs_m[a, b][11]]), objs_m[a, b][11])))
主程序中添加
objs = ti.Vector.field(3, dtype = float, shape = (22, 22), offset=(-11, -11))
objs_m = ti.Vector.field(4, dtype = float, shape = (22, 22), offset=(-11, -11))
objs_k = ti.field(dtype = int, shape = (22, 22), offset=(-11, -11))
gen_objects()
add_spheres()
添加了第二本书内容和三角形网格的完成体
写BVH让代码长度翻倍还不止,而且还要大量重构代码,但在小场景下甚至速度还变慢了。使用Vulkan确实可以让程序快上不少,比如让第一版的程序在我电脑上跑到实时(40fps+),但可惜不支持不同大小的ti.field中from_numpy(ndarray)。还加入了环绕式摄像机,便于查看。期间还遇到好几次只能用bug解释的事情需要避开,估计Taichi在和Python域共用内存的地方解决得不是很好。
Code
从此这个项目已经算是彻底结束了,代码估计不会再动了,虽然仍然有很多事(PBR,体积体,双向光追)可以做,但也都是the rest of my life的事了。
TODO
- 渲染一张封面图
- 增加显式光源,渲染一个Cornell Box
- 添加其他一些模型
- 添加材质,贴图
- 加入BVH
- 加入三维模型.obj的读取与渲染,渲染一只Standford Bunny
- 增加键鼠交互
- 跟进下一本书的内容
- 在学习了之后的课程后,使用更多Taichi的特性
芜湖!完结于2021.12.17