Featured image of post 用Taichi写一个Ray Tracer

用Taichi写一个Ray Tracer

这估计是我在校内写的第一篇博客

前言

之前在知乎上刷到了由刘天添老师开设的太极图形课,听信了其中“小白友好”的言论,遂报名参加。 之前刷完了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")

Day1最终结果

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")

Day2 最终效果

渲染一个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)

Cornell Box

封面图

不知道为什么不能按照原文添加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域共用内存的地方解决得不是很好。 Bunny in a Cornell Box Earth in a Cornell Box 刻晴! 刻晴!! Code

从此这个项目已经算是彻底结束了,代码估计不会再动了,虽然仍然有很多事(PBR,体积体,双向光追)可以做,但也都是the rest of my life的事了。

TODO

  • 渲染一张封面图
  • 增加显式光源,渲染一个Cornell Box
  • 添加其他一些模型
  • 添加材质,贴图
  • 加入BVH
  • 加入三维模型.obj的读取与渲染,渲染一只Standford Bunny
  • 增加键鼠交互
  • 跟进下一本书的内容
  • 在学习了之后的课程后,使用更多Taichi的特性

芜湖!完结于2021.12.17

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计