毕设/原理伪代码

from taichi.math import *  # 导入 Taichi 数学库
import taichi as ti  # 导入 Taichi 库
import time  # 导入时间库

ti.init(arch=ti.gpu)  # 初始化 Taichi ,GPU 加速

image_resolution = (1920, 1080)  # 图像分辨率
image_pixels = ti.Vector.field(3, float, image_resolution)  # 图像的像素场

aspect_ratio = image_resolution[0] / image_resolution[1]  # 图像宽高比

# 配置常量

TMIN = 0.001  # 光开始传播的起始偏移,避免光线自相交
TMAX = 2000.0  # 最大单次光线传播距离
PRECISION = 0.0001  # 必须要小于 TMIN,否则光线会自相交产生阴影痤疮
MAP_SIZE = float(0x7fffffff);  # 地图大小

MAX_RAYMARCH = 512  # 最大光线步进次数
MAX_RAYTRACE = 512  # 最大路径追踪次数

SHAPE_NONE = 0  # 无形状
SHAPE_SPHERE = 1  # 球体
SHAPE_BOX = 2  # 箱体

ENV_IOR = 1.000277  # 环境的折射率


@ti.data_oriented
class Image:
    def __init__(self, path: str):
        self.img = ti.tools.imread(path)
        self.img = self.img.astype("float32")
        self.img = self.img / 255.0
        self.img = vec3.field(shape=self.img.shape)
        self.img.from_numpy(self.img.to_numpy())

    @ti.func
    def texture(self, uv: vec2):
        x = int(uv.x * self.img.shape[0])
        y = int(uv.y * self.img.shape[1])
        return self.img[x, y]


@ti.dataclass
class Ray:  # 光线类
    origin: vec3  # 光线起点
    direction: vec3  # 光线方向
    color: vec4  # 光的颜色

    @ti.func
    def at(r, t: float) -> vec3:  # 计算光子所在位置
        return r.origin + t * r.direction


@ti.dataclass
class Material:
    albedo: vec3  # 材质颜色
    roughness: float  # 材质粗糙度
    metallic: float  # 材质金属度
    transmission: float  # 材质透明度
    ior: float  # 材质折射率
    emission: vec4  # 材质自发光
    normal: vec3  # 切线空间法线


@ti.dataclass
class Transform:
    position: vec3
    scale: vec3


@ti.dataclass
class Object:
    type: ti.u32
    trs: Transform
    mtl: Material
    sd: float


@ti.dataclass
class HitRecord:  # 光子碰撞记录类
    position: vec3  # 光子碰撞的位置
    distance: float  # 光子步进的距离
    hit: ti.i32  # 是否击中到物体
    obj: Object  # 碰撞到的物体


@ti.func
def random_in_unit_disk():  # 单位圆内随机取一点
    x = ti.random()
    a = ti.random() * 2 * pi
    return sqrt(x) * vec2(sin(a), cos(a))


@ti.dataclass
class Camera:
    lookfrom: vec3  # 视点位置
    lookat: vec3  # 目标位置
    vup: vec3  # 向上的方向
    vfov: float  # 视野
    aspect: float  # 传感器长宽比
    aperture: float  # 光圈大小
    focus: float  # 对焦距离

    @ti.func
    def get_ray(c, uv: vec2, color: vec4) -> Ray:
        # 根据 vfov 和显示画布长宽比计算传感器长宽
        theta = radians(c.vfov)
        half_height = tan(theta / 2)
        half_width = c.aspect * half_height

        # 以目标位置到摄像机位置为 Z 轴正方向
        z = normalize(c.lookfrom - c.lookat)
        # 计算出摄像机传感器的 XY 轴正方向
        x = normalize(cross(c.vup, z))
        y = cross(z, x)

        # 计算出画布左下角
        lower_left_corner = c.lookfrom - half_width * c.focus * x \
                            - half_height * c.focus * y \
                            - c.focus * z

        horizontal = 2.0 * half_width * c.focus * x
        vertical = 2.0 * half_height * c.focus * y

        # 模拟光进入镜头光圈
        lens_radius = c.aperture / 2.0
        rud = lens_radius * random_in_unit_disk()
        offset = x * rud.x + y * rud.y

        # 计算光线起点和方向
        ro = c.lookfrom + offset
        rp = lower_left_corner + uv.x * horizontal \
             + uv.y * vertical
        rd = normalize(rp - ro)

        return Ray(ro, rd, color)


@ti.func
def sd_sphere(p: vec3, r: float) -> float:  # SDF 球体
    return length(p) - r


@ti.func
def sd_box(p: vec3, b: vec3) -> float:  # SDF 盒子
    q = abs(p) - b
    return length(max(q, 0)) + min(max(q.x, max(q.y, q.z)), 0)


@ti.func
def signed_distance(obj, pos: vec3) -> float:  # 对物体求 SDF 距离
    position = obj.trs.position  # 位置空间变换(下一步再实现旋转变换)
    scale = obj.trs.scale  # 用缩放控制物体大小

    p = pos - position
    # 为不同形状选择不同的 SDF 函数
    if obj.type == SHAPE_SPHERE:
        obj.sd = sd_sphere(p, scale.x)
    elif obj.type == SHAPE_BOX:
        obj.sd = sd_box(p, scale)
    else:
        obj.sd = sd_sphere(p, scale.x)

    return obj.sd  # 返回符号距离


objects_num = 6  # 地图中物体的数量
objects = Object.field(shape=objects_num)  # 存放物体的场

objects[0] = Object(type=SHAPE_SPHERE,
                    trs=Transform(vec3(0, -100.5, 0), vec3(100)),
                    mtl=Material(vec3(1, 1, 1), 1, 1, 0, 1, vec4(0), vec3(0, 0, 1)))

objects[1] = Object(type=SHAPE_BOX,
                    trs=Transform(vec3(0, 0, -2), vec3(2, 1, 0.2)),
                    mtl=Material(vec3(1, 1, 1), 0, 1, 0, 1, vec4(0), vec3(0, 0, 1)))

objects[2] = Object(type=SHAPE_SPHERE,
                    trs=Transform(vec3(0, 0, 0), vec3(0.5)),
                    mtl=Material(vec3(1, 1, 1), 1, 0, 0, 1, vec4(0.1, 1, 0.1, 10), vec3(0, 0, 1)))

objects[3] = Object(type=SHAPE_SPHERE,
                    trs=Transform(vec3(-1, -0.2, 0), vec3(0.3)),
                    mtl=Material(vec3(1, 0.1, 0.1), 0.9, 0.1, 0, 1, vec4(0), vec3(0, 0, 1)))

objects[4] = Object(type=SHAPE_SPHERE,
                    trs=Transform(vec3(1, -0.2, 0), vec3(0.3)),
                    mtl=Material(vec3(0.1, 0.1, 1), 0.2, 1, 0, 1, vec4(0), vec3(0, 0, 1)))

objects[5] = Object(type=SHAPE_SPHERE,
                    trs=Transform(vec3(0.5, -0.2, -1), vec3(0.3)),
                    mtl=Material(vec3(0.9, 0.9, 1), 0, 0, 1, 1.5, vec4(0), vec3(0, 0, 1)))


@ti.func
def nearest_object(p: vec3) -> Object:  # 求最近的物体
    o = Object(sd=MAP_SIZE)  # 设置一个最大的 SDF 值,即地图边界
    for i in range(objects_num):
        oi = objects[i]
        oi.sd = signed_distance(oi, p)  # 求物体的 SDF 值
        if abs(oi.sd) < abs(o.sd): o = oi
    return o


@ti.func
def calc_normal(obj, p: vec3) -> vec3:  # 计算物体法线
    e = vec2(1, -1) * 0.5773 * 0.0005
    return normalize(e.xyy * signed_distance(obj, p + e.xyy) + \
                     e.yyx * signed_distance(obj, p + e.yyx) + \
                     e.yxy * signed_distance(obj, p + e.yxy) + \
                     e.xxx * signed_distance(obj, p + e.xxx))


@ti.func
def raycast(ray) -> HitRecord:  # 光线步进求交
    record = HitRecord(ray.origin, TMIN, False)  # 初始化光子碰撞记录
    for _ in range(MAX_RAYMARCH):  # 光线步进
        record.position = ray.at(record.distance)  # 计算光子所在位置
        record.obj = nearest_object(record.position)  # 计算光子与球体的有向距离
        dis = abs(record.obj.sd)  # 绝对值为无符号距离
        if dis < PRECISION:  # 如果光子与球体的距离小于精度即为击中
            record.hit = True  # 设置击中状态
            break
        record.distance += dis  # 光子继续传播
        if record.distance > TMAX:  # 如果光子传播距离大于最大传播距离
            break
    return record  # 返回光子碰撞记录


inv_atan = vec2(0.5 / pi, 1 / pi)


@ti.func
def sample_spherical_map(v: vec3) -> vec2:  # 球面坐标到笛卡尔坐标
    uv = vec2(atan2(v.z, v.x), asin(v.y))
    uv *= inv_atan
    uv += 0.5
    return uv


@ti.func
def IBL(ray, img) -> vec3:  # 光照环境光
    uv = sample_spherical_map(ray.direction)
    return img.texture(uv)


@ti.func
def sky_color(ray, time) -> vec3:
    t = 0.5 * ray.direction.y + 0.5  # 将 y 分量归一化
    blue = 0.5 * sin(time) + 0.5  # 计算蓝色分量
    return mix(vec3(1.0, 1.0, blue), vec3(0.5, 0.7, 1.0), t)  # 混合两种颜色


@ti.func
def pow5(x: float):  # 快速计算 x 的 5 次方
    t = x * x
    t *= t
    return t * x


@ti.func
def TBN(N: vec3) -> mat3:  # 用世界坐标下的法线计算 TBN 矩阵
    # Building an Orthonormal Basis from a 3D Unit Vector Without Normalization
    # https://doi.org/10.1080/2165347X.2012.689606
    T = vec3(0)
    B = vec3(0)

    # 这个判断条件是为了避免出现除 0 的情况
    # 精度要根据浮点数精度调整
    if N.z < -0.99999:
        T = vec3(0, -1, 0)
        B = vec3(-1, 0, 0)
    else:
        a = 1 / (1 + N.z)
        b = -N.x * N.y * a

        T = vec3(1 - N.x * N.x * a, b, -N.x)
        B = vec3(b, 1 - N.y * N.y * a, -N.y)

    return mat3(T, B, N)


@ti.func
def hemispheric_sampling(n: vec3) -> vec3:  # 以 n 为法线进行半球采样
    ra = ti.random() * 2 * pi
    rb = ti.random()

    rz = sqrt(rb)
    v = vec2(cos(ra), sin(ra))
    rxy = sqrt(1 - rb) * v

    return TBN(n) @ vec3(rxy, rz)  # 用 TBN 矩阵将切线空间方向转换到世界空间


@ti.func
def hemispheric_sampling_roughness(n: vec3, roughness: float) -> vec3:  # 用粗糙度采样沿向量 n 半球采样
    ra = ti.random() * 2 * pi
    rb = ti.random()

    shiny = pow5(roughness)  # 光感越大高光越锐利

    rz = sqrt((1.0 - rb) / (1.0 + (shiny - 1.0) * rb))
    v = vec2(cos(ra), sin(ra))
    rxy = sqrt(abs(1 - rz * rz)) * v

    return TBN(n) @ vec3(rxy, rz)


@ti.func
def fresnel_schlick(cosine: float, F0: float) -> float:  # 计算菲涅尔近似值
    return F0 + (1 - F0) * pow5(abs(1 - cosine))


@ti.func
def fresnel_schlick_roughness(cosine: float, F0: float, roughness: float) -> float:  # 计算粗糙度下的菲涅尔近似值
    return F0 + (max(1 - roughness, F0) - F0) * pow5(abs(1 - cosine))


@ti.func
def PBR(ray, record, normal: vec3) -> Ray:
    roughness = record.obj.mtl.roughness  # 获取粗糙度
    metallic = record.obj.mtl.metallic  # 获取金属度
    transmission = record.obj.mtl.transmission  # 获取透明度
    ior = record.obj.mtl.ior  # 获取折射率

    I = ray.direction  # 入射方向
    V = -ray.direction  # 观察方向
    # 将材质的切线空间法线转换到世界空间
    N = TBN(record.obj.mtl.normal) @ normal  # 法线方向

    NoV = dot(N, V)

    if ti.random() < transmission:  # 折射部分
        eta = ENV_IOR / ior  # 折射率之比
        outer = sign(NoV)  # 大于零就是穿入物体,小于零是穿出物体
        eta = pow(eta, outer)  # 更改折射率之比
        N *= outer  # 如果是穿出物体表面,就更改法线方向

        NoI = -NoV
        k = 1.0 - eta * eta * (1.0 - NoI * NoI)  # 这个值如果小于 0 就说明全反射了

        F0 = (eta - 1) / (eta + 1)  # 基础反射率
        F0 *= F0
        F = fresnel_schlick(NoV, F0)
        N = hemispheric_sampling_roughness(N, roughness)  # 根据粗糙度抖动法线方向

        # k < 0 为全反射
        if ti.random() < F + metallic and outer > 0 or k < 0:
            ray.direction = reflect(I, N)  # 反射
        else:
            # ray.direction = refract(I, N, eta)    # 折射
            ray.direction = eta * I - (eta * NoI + sqrt(k)) * N
    else:
        F = fresnel_schlick_roughness(NoV, 0.04, roughness)
        if ti.random() < F + metallic:  # 反射部分
            N = hemispheric_sampling_roughness(N, roughness)
            ray.direction = reflect(I, N)  # 平面反射
        else:  # 漫反射部分
            ray.direction = hemispheric_sampling(N)  # 半球采样

    # N = hemispheric_sampling_roughness(N, 0)
    # ray.direction = reflect(I, N)

    ray.origin = record.position  # 更新光线起点
    ray.color.rgb *= record.obj.mtl.albedo  # 更新光的颜色

    return ray


@ti.func
def raytrace(ray, time: float) -> Ray:
    for i in range(MAX_RAYTRACE):
        record = raycast(ray)  # 光线步进求交

        # 俄罗斯轮盘赌概率,防止光线过分的反复反射
        light_quality = 1 / 50
        inv_pdf = exp(float(i) * light_quality)
        roulette_prob = 1.0 - (1.0 / inv_pdf)

        visible = length(ray.color.rgb * ray.color.a)
        # 如果光已经衰减到不可分辨程度,或者光线毙掉就不继续了
        if visible < 0.001 or ti.random() < roulette_prob:
            ray.color *= roulette_prob
            break

        if not record.hit:
            ray.color.rgb *= sky_color(ray, time)  # 获取天空颜色
            break

        # 这里的法线会始终指向物体外面
        normal = calc_normal(record.obj, record.position)  # 计算法线
        # ray.color.rgb = 0.5 + 0.5 * normal  # 设置为法线颜色
        # break

        # 处理自发光
        emission = record.obj.mtl.emission
        if abs(record.obj.mtl.emission.a) > 0.0:
            ray.color.rgb *= emission.rgb * emission.a
            break

        ray = PBR(ray, record, normal)  # 应用 PBR 材质

        if dot(normal, ray.direction) < 0:  # 如果光线反射到了物体内部,也直接跳出循环
            break

        ray.color *= inv_pdf  # 能量守恒

    return ray


# ACES fitted
# from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
ACESInputMat = mat3(
    vec3(0.59719, 0.35458, 0.04823),
    vec3(0.07600, 0.90834, 0.01566),
    vec3(0.02840, 0.13383, 0.83777)
)

# ODT_SAT => XYZ => D60_2_D65 => sRGB
ACESOutputMat = mat3(
    vec3(1.60475, -0.53108, -0.07367),
    vec3(-0.10208, 1.10813, -0.00605),
    vec3(-0.00327, -0.07276, 1.07602)
)


@ti.func
def RRTAndODTFit(v: vec3) -> vec3:
    a = v * (v + 0.0245786) - 0.000090537
    b = v * (0.983729 * v + 0.4329510) + 0.238081
    return a / b


@ti.func
def ACESFitted(color: vec3) -> vec3:  # ACES 色调映射
    color = ACESInputMat @ color
    color = RRTAndODTFit(color)  # Apply RRT and ODT
    color = ACESOutputMat @ color
    return color


@ti.kernel
def render(
        time: float,
        camera_position: vec3,
        camera_lookat: vec3,
        camera_up: vec3,
        denoise_frame: float):  # 渲染函数

    for i, j in image_pixels:  # 并行遍历像素场
        SCREEN_PIXEL_SIZE = 1.0 / vec2(image_resolution)
        uv = vec2(i, j) * SCREEN_PIXEL_SIZE  # 计算像素坐标

        uv += vec2(ti.random(), ti.random()) * SCREEN_PIXEL_SIZE  # 超采样

        camera = Camera()
        camera.lookfrom = camera_position  # 设置摄像机位置
        camera.lookat = camera_lookat  # 设置目标位置
        camera.vup = camera_up  # 设置向上的方向
        camera.aspect = aspect_ratio  # 设置长宽比
        camera.vfov = 30  # 设置视野
        camera.aperture = 0.01  # 设置光圈大小
        camera.focus = 4  # 设置对焦距离

        ray = camera.get_ray(uv, vec4(1.0))  # 生成光线
        ray = raytrace(ray, time)  # 光线追踪

        color = ray.color.rgb * ray.color.a  # 混合颜色与光强

        color = pow(color, vec3(0.5))  # 伽马矫正
        exposure = 1.5
        color *= exposure
        color = ACESFitted(color)  # ACES 色调映射
        last_color = image_pixels[i, j]  # 获取上一帧的颜色
        out_color = mix(last_color, color, 1.0 / denoise_frame)  # 混合当前帧和上一帧的颜色

        image_pixels[i, j] = out_color  # 设置像素颜色


window = ti.ui.Window("Taichi Renderer", image_resolution)  # 创建窗口
canvas = window.get_canvas()  # 获取画布
camera = ti.ui.Camera()  # 创建摄像机
camera.position(0, 0, 4)  # 设置摄像机初始位置

denoise_frame = 0
start_time = time.time()  # 获取程序开始时时间
while window.running:
    camera.track_user_inputs(window, movement_speed=0.03, hold_key=ti.ui.LMB)
    delta_time = time.time() - start_time  # 计算时间差
    if window.is_pressed(' '):
        denoise_frame = 0
    denoise_frame += 1
    render(
        delta_time,
        camera.curr_position,
        camera.curr_lookat,
        camera.curr_up,
        denoise_frame)  # 调用渲染函数
    canvas.set_image(image_pixels)  # 为画布设置图像
    window.show()  # 显示窗口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值