【Heskey帶你玩渲染】 PBR基礎


未經允許禁止轉載(防止某些人亂轉,轉着轉着就到蠻牛之類的地方去了)

B站:Heskey0

本文很大一部分是我讀斯坦福Veach博士論文過程中的筆記,一步步把筆記翻譯成中文(手動捂臉)最近期末考考了信號與系統,正好也幫助理解Ravir的那篇博士論文。

大概看了下教程,老師只是做了一些簡單介紹,我稍微擴充一點知識吧。

PBR

基於物理的渲染,就是依照現實世界構建一個虛擬的空間,在這個模擬空間中模擬真實世界進行渲染。

輻射度量學

N個光子的位置,方向和波長組成一個6N維的相空間,再增加一個時間維,就成為了軌道空間光子事件就是軌道空間中的一個點。一些光子事件組成光子事件空間。(光子事件空間是軌道空間的子空間)

幾何測度可以用來測量光子事件空間。能量函數用來測量光子空間中的所有光子的能量。

光線空間由場景的表面和一個方向的笛卡爾積構成。光線空間中可以定義范數和內積。

通量測度是對光線拾取能力的一個度量。給定一個光線空間D,通量測度可以定義為\(\mu(D)=\int_DdA(x)d\sigma_x^\bot(\omega)\)

輻射度就可以定義為\(L(r)=\frac{d\Phi(r)}{d\mu(r)}\)

光線傳輸

光線傳輸算子可以定義為\(T=KG\)。K是散射算子,G是傳輸算子。光線傳輸方程可以定義為\(L=L_e+TL\)。解得\(L=(I-T)^{-1}L_e\)。當傳輸算子T的范數<1時,\(I-T\)可逆。(\(I-T\)的逆也可以用紐曼序列求解)

打住打住。emmm,好像扯的有點復雜了。前面這里主要是想告訴大家,從泛函和測度論的角度看,輻射度也好,輻射能量也好,其實都是在軌道空間上的測度

BRDF

BRDF只是表示材質的一種方式。材質是一個很復雜的概念,一根光線打到物體表面,這根光線跟物體表面和物體內部發生一些交互,之后光線出來的時候是多種多樣的。材質就是要去描述一個這樣的過程!

描述一個材質,有可能是一個非常復雜的工作,會從物理光學,波動光學,能量守恆方面去分析。一個真實的材質表面反射函數有16個參數,非常復雜(也就是GRF)

對GRF進行降維,可以得到9維的BSSRDF(其實BSSRDF的意義在於快速近似,它是有偏的。無偏的做法比如體積光追,光線在參與介質中步進,可以參考斯坦福Mark Pauly的博士論文)再壓縮一把,我們可以得到7維的BSDF,BTF,SVBRDF(讀過GPU Gems2的同學可以回憶一下BTF的概念,這里就不扯遠了)其中BSDF再壓縮就得到了5維的BRDF和BTDFBTF簡化到極致就是我們常用的Texture

這里偷偷插一句:
渲染方程的右邊其實是一個卷積過程,對應到z域就是乘積,BRDF和入射光線卷積就得到了出射光的函數,想深入的可以看看Ravir的那篇博士論文。

引擎中的PBR

僅從輻射學的角度看,法線分布函數D代表表面上單位面積朝向某單位立體角的微表面的面積,經過推導,微表面模型的BRDF可以由法線分布函數求得。即\(f_r=D\)。再考慮上電介質的菲涅爾效應和微表面之間的遮擋,即可求得引擎中常用的PBR公式:

\[f_r= k_d\frac{\rho}{\pi}+\frac{DFG}{cos\theta_{i,n}cos\theta_{o,n}} \]

這個補光的操作一定程度上破壞了能量守恆。正確的做法是SIGGRAPH Physically Based Shading2017上的Kulla-Conty(Physically Based Shading建議大家把每一年的都翻出來學習一下)

關於法線分布函數NDF,常用的有ggx(長尾,所以看起來高光過渡比較軟)關於菲涅爾項,我也稍微提一點:

菲涅爾

電介質的菲涅爾這里就不多說了,可以用Schlick公式近似。

讀過PBRT的同學應該還記得

不同於電介質,導體的ior是復數,導體表面只有0.1微米會吸收光,所以PBRT中忽略了這個效應。PBR中金屬是沒有所謂的basecolor的,金屬只有specular

這里啰嗦一下,翻過UE源代碼的應該還記得:

specular=lerp(0.08* specular.rrr,basecolor.rgb,metallic)

ue4的高光是有hack的,specular本是一個float3,但是虛幻里面在做法是specular.rrr。(再次手動捂臉,可能是為了方便藝術家們吧)

貼幾張我渲染出來的圖

采樣光源:

image

采樣BRDF:

image

MIS混合再順便加個微表面材質:

image

最后貼一下我的路徑追蹤代碼

//B站:Heskey0
import taichi as ti
import numpy as np

ti.init(arch=ti.cuda)
resolution = (1920, 1080)

eps = 0.0001  # 浮點數精度
inf = 1e10

mat_none = 0
mat_lambertian = 1
mat_specular = 2    # 鏡面
mat_glass = 3       # 玻璃
mat_light = 4
mat_microfacet = 5
mat_pm = 6

# 光區域為一塊板
light_y_pos = 2.0 - eps
light_x_min_pos = -0.7
light_x_range = 1.4
light_z_min_pos = 0.6
light_z_range = 0.4
light_area = light_x_range * light_z_range
light_min_pos = ti.Vector([
    light_x_min_pos,
    light_y_pos,
    light_z_min_pos])
light_max_pos = ti.Vector([
    light_x_min_pos + light_x_range,
    light_y_pos,
    light_z_min_pos + light_z_range
])
light_color = ti.Vector([1, 1, 1])
light_normal = ti.Vector([0.0, -1.0, 0.0])  # 光源方向向下


# 1.7700 : 紅寶石的折射率
refract_index = 1.7700

# right sphere
sp1_center = ti.Vector([0.5, 1.18, 1.40])
sp1_radius = 0.18
# left sphere
sp2_center = ti.Vector([-0.35, 0.65, 1.70])
sp2_radius = 0.15
# middle sphere(microfacet)
sp3_center = ti.Vector([-0.10, 0.35, 0.6])
sp3_radius = 0.35
sp3_microfacet_roughness = 0.15
# sp3_idx = 1.55  # 石英晶體折射率
sp3_idx = 2.4  # 鑽石折射率
# right front sphere(microfacet)
sp4_center = ti.Vector([-0.05, 1, 1])
sp4_radius = 0.3
sp4_microfacet_roughness = 1


# 構造變換矩陣,用於box
def make_box_transform_matrices(rotate_rad, translation):
    c, s = np.cos(rotate_rad), np.sin(rotate_rad)
    rot = np.array([[c, 0, s, 0],
                    [0, 1, 0, 0],
                    [-s, 0, c, 0],
                    [0, 0, 0, 1]])  # 繞y軸旋轉67.5°
    # rot = np.array([[1, 0, 0, 0],
    #                 [0, c, s, 0],
    #                 [0,-s, c, 0],
    #                 [0, 0, 0, 1]])  # 繞y軸旋轉67.5°
    translate = np.array([  # 平移 (0.5, 0, 1.4)
        [1, 0, 0, translation.x],
        [0, 1, 0, translation.y],
        [0, 0, 1, translation.z],
        [0, 0, 0, 1],
    ])
    m = translate @ rot  # 平移 + 旋轉
    m_inv = np.linalg.inv(m)  # 逆矩陣
    m_inv_t = np.transpose(m_inv)  # 轉置矩陣
    return ti.Matrix(m_inv), ti.Matrix(m_inv_t)  # 旋轉-22.5° + 平移 (0.5, 0, 1)


# right box
box1_min = ti.Vector([0.0, 0.0, 0.0])
box1_max = ti.Vector([0.35, 1.0, 0.35])
box1_rotate_rad = np.pi / 16
box1_m_inv, box1_m_inv_t = make_box_transform_matrices(box1_rotate_rad, ti.Vector([0.30, 0, 1.20]))  # box的transform的 逆矩陣, 逆轉置矩陣
# left box
box2_min = ti.Vector([0.0, 0.0, 0.0])
box2_max = ti.Vector([0.4, 0.5, 0.4])
box2_rotate_rad = np.pi / 4
box2_m_inv, box2_m_inv_t = make_box_transform_matrices(box2_rotate_rad, ti.Vector([-0.75, 0, 1.70]))  # box的transform的 逆矩陣, 逆轉置矩陣


'''
lambertian brdf
'''
# No absorbtion     沒有吸收光譜,Albedo為1,對單位半球積分
lambertian_brdf = 1.0 / np.pi  # f(lambert) = k*c / π       # k = 1,  c = hit_color*light_color


'''
microfacet brdf
'''
# compute reflectance
# 計算反射比
@ti.func
def schlick(cos, eta):  # 入射角cosine, 折射率refractive index
    r0 = (1.0 - eta) / (1.0 + eta)
    r0 = r0 * r0  # 反射比 reflectance
    return r0 + (1 - r0) * ((1.0 - cos) ** 5)


# normal distribution function
@ti.func
def ggx(alpha, i_dir, o_dir, n_dir):  # roughness, incident, exit, normal
    m_dir = (i_dir + o_dir).normalized()
    cos_theta_square = m_dir.dot(n_dir)
    tan_theta_square = (1-cos_theta_square) / cos_theta_square
    root = alpha / cos_theta_square * (alpha*alpha + tan_theta_square)
    return root*root / np.pi


@ti.func
def ggx2(alpha, i_dir, o_dir, n_dir):
    m_dir = (i_dir + o_dir).normalized()
    NoM = n_dir.dot(m_dir)
    d = NoM*NoM * (alpha*alpha-1) + 1
    return alpha*alpha / np.pi*d*d


@ti.func
def smithG1(alpha, v_dir, n_dir):
    out = 0.0
    # compute tan_theta(v / n)
    cos_theta_square = v_dir.dot(n_dir) ** 2
    tan_theta_square = (1-cos_theta_square) / cos_theta_square
    tan_theta = ti.sqrt(tan_theta_square)
    if tan_theta == 0:
        out = 1
    else:
        root = alpha * tan_theta
        out = 2 / (1 + ti.sqrt(1.0 + root * root))

    return out


@ti.func
# shadowing-masking
def smith(alpha, i_dir, o_dir, n_dir):  # roughness, incident, exit, normal
    # m_dir = (i_dir + o_dir).normalized()
    # shadowing * masking
    return smithG1(alpha, i_dir, n_dir) * smithG1(alpha, o_dir, n_dir)


@ti.func
def GGX(alpha, NoH, h):
    a = NoH * alpha
    k = alpha / (1 - NoH*NoH + a*a)
    d = k*k * (1/np.pi)
    return d

@ti.func
def SmithGGX(alpha, NoV, NoL):
    a2 = alpha*alpha
    GGXV = NoL * ti.sqrt((NoV - a2*NoV)*NoV + a2)
    GGXL = NoL * ti.sqrt((NoL - a2*NoL)*NoL + a2)
    return 0.5 / (GGXV + GGXL)

@ti.func
def compute_microfacet_brdf(alpha, idx, i_dir, o_dir, n_dir):
    micro_cos = o_dir.dot((i_dir + o_dir).normalized())
    # # numerator and denominator
    # D = ggx2(alpha, i_dir, o_dir, n_dir)
    # G = smith(alpha, i_dir, o_dir, n_dir)
    # F = schlick(micro_cos, idx)
    # # print(D, G, F)
    #
    # numerator = D * G * F
    # denominator = 4 * o_dir.dot(n_dir) * i_dir.dot(n_dir)
    # cook_torrance = numerator / ti.abs(denominator)
    # return cook_torrance

    h = (i_dir + o_dir).normalized()
    NoH = n_dir.dot(h)
    NoV = n_dir.dot(i_dir)
    NoL = n_dir.dot(o_dir)
    D = GGX(alpha, NoH, h)
    V = SmithGGX(alpha, NoV, NoL)
    F = schlick(micro_cos, idx)

    # print(D * V * F)
    out = D * V * F
    return out


'''
basic functions
'''
# 反射
@ti.func
def reflect(d, n):
    # d and n are both normalized
    ret = d - 2.0 * d.dot(n) * n  # d - 2*|d|*|n|*n*cos<d,n>(theta) = d - 2 |d|*cos(theta) * (n/|n|)
    return ret  # reflect vector


# 折射
@ti.func
def refract(d, n, ni_over_nt):
    dt = d.dot(n)  # cos    # sin**2 = 1 - cos**2
    discr = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt)  # discr:折射角的cos
    rd = (ni_over_nt * (d - n * dt) - n * ti.sqrt(discr)).normalized()
    return rd  # 是否有反射光, 反射光方向


# 點由矩陣變換
@ti.func
def mat_mul_point(m, p):
    hp = ti.Vector([p[0], p[1], p[2], 1.0])
    hp = m @ hp
    hp /= hp[3]
    return ti.Vector([hp[0], hp[1], hp[2]])


# [3] => ti.Vector(4);   m@v  # [4, 4]@[4]
# 忽略矩陣的第4行第4列, 忽略矩陣的平移
@ti.func
def mat_mul_vec(m, v):
    hv = ti.Vector([v[0], v[1], v[2], 0.0])
    hv = m @ hv
    return ti.Vector([hv[0], hv[1], hv[2]])


# 判斷射線與球是否相交
@ti.func
def intersect_sphere(pos, d, center, radius):  # pos:light_position, d:ray_dir
    # 構建余弦定理三角形:判斷光與球是否相交
    T = pos - center
    A = 1.0
    B = 2.0 * T.dot(d)
    C = T.dot(T) - radius * radius
    delta = B * B - 4.0 * A * C
    dist = inf
    hit_pos = ti.Vector([0.0, 0.0, 0.0])

    if delta > 0:  # 有解
        delta = ti.max(delta, 0)
        sdelta = ti.sqrt(delta)
        ratio = 0.5 / A
        ret1 = ratio * (-B - sdelta)  # 方程的解, 即三角形的邊長(離入射光近的點)
        dist = ret1
        hit_pos = pos + d * dist

    return dist, hit_pos  # 光源到命中點的距離, 命中點坐標


# plane
@ti.func
def intersect_plane(pos, d, pt_on_plane, norm):  # position, ray_dir, offset, normal
    dist = inf
    hit_pos = ti.Vector([0.0, 0.0, 0.0])
    denom = d.dot(norm)
    if abs(denom) > eps:  # 光與平面不平行
        dist = norm.dot(pt_on_plane - pos) / denom
        hit_pos = pos + d * dist
    return dist, hit_pos  # 光源到命中點的距離, 命中點坐標


# 參考清華大學圖形學課程中的基於slab的求交算法:Liang_Barsky算法
# aabb包圍體 call by intersect_box and intersect_light
@ti.func
def intersect_aabb(box_min, box_max, o, d):  # box_min, box_max, pos(box空間), ray_dir(box空間)
    intersect = 1  # 光與box是否相交

    near_t = -inf
    far_t = inf
    near_face = 0
    near_is_max = 0

    for i in ti.static(range(3)):  # ti.static(range()) can iterate matrix elements
        if d[i] == 0:  # 光平行於包圍體的一個面
            if o[i] < box_min[i] or o[i] > box_max[i]:
                intersect = 0
        else:
            i1 = (box_min[i] - o[i]) / d[i]  # 除以d[i] : 判斷光是否正對box
            i2 = (box_max[i] - o[i]) / d[i]

            new_far_t = max(i1, i2)     # 光朝着正半軸時,為i2
            new_near_t = min(i1, i2)    # 光朝着正半軸時,為i1
            new_near_is_max = i2 < i1   # 光朝着負半軸時(near_t取i2),為true

            far_t = min(new_far_t, far_t)  # far_t 取最小
            if new_near_t > near_t:  # near_t 取最大
                near_t = new_near_t
                near_face = int(i)  # 記錄最小的i所在的維
                near_is_max = new_near_is_max  # 在當前維中near_t, i2<i1 ?

    near_norm = ti.Vector([0.0, 0.0, 0.0])
    if near_t > far_t:
        intersect = 0
    if intersect:
        for i in ti.static(range(2)):
            if near_face == i:
                near_norm[i] = -1 + near_is_max * 2     # near_is_max => return 1; else => return -1
    return intersect, near_t, far_t, near_norm  # 是否相交, 首先相交的平面的距離, 遠平面, 近平面法線


# params: min, max, position, ray_dir
# box
@ti.func
def intersect_aabb_transformed(box_m_inv, box_m_inv_t, box_min, box_max, o, d):
    # 射線轉換到包圍體的local position
    obj_o = mat_mul_point(box_m_inv, o)
    obj_d = mat_mul_vec(box_m_inv, d)

    intersect, near_t, _, near_norm = intersect_aabb(box_min, box_max, obj_o, obj_d)
    # print(near_norm)
    if intersect and 0 < near_t:
        near_norm = mat_mul_vec(box_m_inv_t, near_norm)
    else:
        intersect = 0
    # out params: hit?, cur_dist, pnorm
    return intersect, near_t, near_norm


# light
@ti.func
def intersect_light(pos, ray_dir, tmax):
    # t:near intersect distance
    hit, t, far_t, near_norm = intersect_aabb(light_min_pos, light_max_pos, pos, ray_dir)
    if hit and 0 < t < tmax:
        hit = 1
    else:
        hit = 0
        t = inf
    return hit, t


# 光線與場景相交
@ti.func
def intersect_scene(pos, ray_dir):
    # closest:深度緩沖區
    closest, normal = inf, ti.Vector.zero(ti.f32, 3)
    # color, material
    c, mat = ti.Vector.zero(ti.f32, 3), mat_none

    # right sphere
    # cur_dist, hit_pos = intersect_sphere(pos, ray_dir, sp1_center, sp1_radius)
    # if 0 < cur_dist < closest:  # 深度測試
    #     closest = cur_dist
    #     normal = (hit_pos - sp1_center).normalized()
    #     c, mat = ti.Vector([1.0, 1.0, 1.0]), mat_glass

    # middle Sphere
    cur_dist, hit_pos = intersect_sphere(pos, ray_dir, sp3_center, sp3_radius)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = (hit_pos - sp3_center).normalized()
        c, mat = ti.Vector([102.0/255.0, 153.0/255.0, 255.0/255.0]), mat_lambertian
        # c, mat = ti.Vector([68.0/255.0, 175.0/255.0, 238.0/255.0]), mat_microfacet

    # left Sphere
    # cur_dist, hit_pos = intersect_sphere(pos, ray_dir, sp2_center, sp2_radius)
    # if 0 < cur_dist < closest:  # 深度測試
    #     closest = cur_dist
    #     normal = (hit_pos - sp2_center).normalized()
    #     c, mat = ti.Vector([1.0, 1.0, 1.0]), mat_specular

    # left box
    hit, cur_dist, pnorm = intersect_aabb_transformed(box2_m_inv, box2_m_inv_t, box2_min, box2_max, pos, ray_dir)
    if hit and 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = ti.Vector([0.8, 1, 1]), mat_lambertian

    # right box
    hit, cur_dist, pnorm = intersect_aabb_transformed(box1_m_inv, box1_m_inv_t, box1_min, box1_max, pos, ray_dir)
    if hit and 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = ti.Vector([0.8, 1, 1]), mat_lambertian

    # left plane
    pnorm = ti.Vector([1.0, 0.0, 0.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([-1.1, 0.0, 0.0]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = ti.Vector([227.0/255.0, 141.0/255.0, 152.0/255.0]), mat_lambertian
    # right plane
    pnorm = ti.Vector([-1.0, 0.0, 0.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([1.1, 0.0, 0.0]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = ti.Vector([163.0 / 255.0, 156.0 / 255.0, 252.0 / 255.0]), mat_lambertian
    # bottom plane
    gray = ti.Vector([0.93, 0.93, 0.93])
    pnorm = ti.Vector([0.0, 1.0, 0.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([0.0, 0.0, 0.0]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = gray, mat_lambertian
    # top
    pnorm = ti.Vector([0.0, -1.0, 0.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([0.0, 2.0, 0.0]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = gray, mat_lambertian
    # far
    pnorm = ti.Vector([0.0, 0.0, 1.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([0.0, 0.0, 0.0]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = gray, mat_lambertian
    # close
    pnorm = ti.Vector([0.0, 0.0, -1.0])
    cur_dist, _ = intersect_plane(pos, ray_dir, ti.Vector([0.0, 0.0, 3]), pnorm)
    if 0 < cur_dist < closest:  # 深度測試
        closest = cur_dist
        normal = pnorm
        c, mat = ti.Vector([0, 0, 0]), mat_lambertian

    # light
    hit_l, cur_dist = intersect_light(pos, ray_dir, closest)
    if hit_l and 0 < cur_dist < closest:  # 深度測試
        # no need to check the second term
        closest = cur_dist
        normal = light_normal
        c, mat = light_color, mat_light

    return closest, normal, c, mat


# 判斷ray_dir是否與光源相交
@ti.func
def visible_to_light(pos, ray_dir):
    # eps*ray_dir to prevent rounding error
    a, b, c, mat = intersect_scene(pos + eps * ray_dir, ray_dir)
    return mat == mat_light


@ti.func
def dot_or_zero(n, l):
    return max(0.0, n.dot(l))











# TODO:begin
# '''
# sampling functions

# multiple importance sampling
@ti.func
def compute_heuristic(pf, pg):
    # Assume 1 sample for each distribution
    f = pf ** 2
    g = pg ** 2
    return f / (f + g)


# 已知sample dir
# area light pdf
@ti.func
def compute_area_light_pdf(pos, ray_dir):
    hit_l, t = intersect_light(pos, ray_dir, inf)
    pdf = 0.0
    if hit_l:  # ray_dir命中了燈光
        l_cos = light_normal.dot(-ray_dir)  # 光源的方向 與 ray_dir 的夾角cosine
        if l_cos > eps:  # 光源 與 ray_dir 同向
            tmp = ray_dir * t
            dist_sqr = tmp.dot(tmp)
            pdf = dist_sqr / (light_area * l_cos)
    return pdf


# 已知sample dir
# cosine weighted sampling
@ti.func
def compute_cosineWeighted_pdf(normal, sample_dir):
    return dot_or_zero(normal, sample_dir) / np.pi  # p(theta, phi) = cos(theta) * sin(theta) / pi


# 未知sample dir
# sample light
@ti.func
def sample_area_light(hit_pos, pos_normal):
    # sampling inside the light area
    x = ti.random() * light_x_range + light_x_min_pos
    z = ti.random() * light_z_range + light_z_min_pos
    on_light_pos = ti.Vector([x, light_y_pos, z])
    return (on_light_pos - hit_pos).normalized()


# 未知sample dir
# Cosine-Weighted Sampling
@ti.func
def cosine_weighted_sampling(normal):
    r, phi = 0.0, 0.0  # 圓上的 (r, theta) 在半球里實際上是 (sin(theta), phi) ,將其變換到 (theta, phi)
    sx = ti.random() * 2.0 - 1.0  # -1 ~ 1 random
    sy = ti.random() * 2.0 - 1.0  # -1 ~ 1 random
    # 1.concentric sample
    # sample on a unit disk
    if sx != 0 or sy != 0:
        if abs(sx) > abs(sy):
            r = sx
            phi = np.pi / 4 * (sy / sx)
        else:
            r = sy
            phi = np.pi / 4 * (2 - sx / sy)

    # 2.apply Malley's method
    # project disk to hemisphere

    # 由normal為中心軸,u和v為水平軸建立笛卡爾坐標系
    # 不需要關心normal和vector.up的關系,vector.up的引入是為了輔助建立起坐標系(u,v,normal)
    u = ti.Vector([1.0, 0.0, 0.0])
    if abs(normal[1]) < 1 - eps:
        u = normal.cross(ti.Vector([0.0, 1.0, 0.0]))  # normal x vector.up = sin(eta)
    v = normal.cross(u)  # normal x u = |u| = sin(eta)

    # theta : vector.up 與 normal 的夾角
    # u,v垂直, 長度均為sin(phi), 均在微平面上

    xy = r * ti.cos(phi) * u + r * ti.sin(phi) * v  # 采樣時的x,y,normal坐標系轉換到u,v,normal坐標系(采樣點隨之旋轉並變為sin(eta)倍)
    zlen = ti.sqrt(max(0.0, 1.0 - xy.dot(xy)))  # zlen:采樣線沿normal的長度

    return xy + zlen * normal  # sample dir


# 兩種pdf相乘, 結果為對光采樣
# sample direct light
@ti.func
def sample_light_and_cosineWeighted(hit_pos, hit_normal):
    cosine_by_pdf = ti.Vector([0.0, 0.0, 0.0])

    light_pdf, cosineWeighted_pdf = 0.0, 0.0

    # sample area light => dir, light_pdf; then dir => lambertian_pdf; then mis
    light_dir = sample_area_light(hit_pos, hit_normal)
    if light_dir.dot(hit_normal) > 0:
        light_pdf = compute_area_light_pdf(hit_pos, light_dir)
        cosineWeighted_pdf = compute_cosineWeighted_pdf(hit_normal, light_dir)
        if light_pdf > 0 and cosineWeighted_pdf > 0:
            l_visible = visible_to_light(hit_pos, light_dir)
            if l_visible:
                heuristic = compute_heuristic(light_pdf, cosineWeighted_pdf)
                DoN = dot_or_zero(light_dir, hit_normal)
                cosine_by_pdf += heuristic * DoN / light_pdf

    # sample cosine weighted => dir, lambertian_pdf; then dir => light_pdf; then mis
    cosineWeighted_dir = cosine_weighted_sampling(hit_normal)
    cosineWeighted_pdf = compute_cosineWeighted_pdf(hit_normal, cosineWeighted_dir)
    light_pdf = compute_area_light_pdf(hit_pos, cosineWeighted_dir)
    if visible_to_light(hit_pos, cosineWeighted_dir):
        heuristic = compute_heuristic(cosineWeighted_pdf, light_pdf)
        DoN = dot_or_zero(cosineWeighted_dir, hit_normal)
        cosine_by_pdf += heuristic * DoN / cosineWeighted_pdf

    # direct_li = mis_weight * cosine / pdf
    return cosine_by_pdf



@ti.func
def sample_ray_dir(indir, normal, hit_pos, mat):
    u = ti.Vector([0.0, 0.0, 0.0])  # 用於下一次追蹤的ray_dir
    pdf = 1.0
    if mat == mat_lambertian:
        u = cosine_weighted_sampling(normal)  # sample brdf : return ray_dir
        pdf = max(eps, compute_cosineWeighted_pdf(normal, u))  # 計算在該方向采樣射線的pdf
    elif mat == mat_pm:
        pass
    elif mat == mat_microfacet:
        # TODO:對cosine項采樣
        u = cosine_weighted_sampling(normal)  # sample brdf : return ray_dir
        pdf = max(eps, compute_cosineWeighted_pdf(normal, u))  # 計算在該方向采樣射線的pdf

    elif mat == mat_specular:  # 反射, pdf = 1
        u = reflect(indir, normal)
    elif mat == mat_glass:  # 折射, 反射, pdf = 1
        cos = indir.dot(normal)  # indir和normal的夾角 (indir和normal為單位向量)
        ni_over_nt = refract_index  # ni / nt = 折射率
        outn = normal
        if cos > 0.0:
            outn = -normal
            cos = refract_index * cos  # 出射角度
        else:
            ni_over_nt = 1.0 / refract_index
            cos = -cos  # indir轉180°

        refl_prob = schlick(cos, refract_index)  # Fresnel reflectance
        if ti.random() < refl_prob:  # 反射的能量
            u = reflect(indir, normal)
        else:  # 折射的能量
            u = refract(indir, outn, ni_over_nt)
    return u.normalized(), pdf  # 用於下一次追蹤的ray_dir, pdf


# Base為質數
@ti.func
def RadicalInverse(Base, i):
    Digit = 0.0
    Radical = 0.0
    Inverse = 0.0
    Digit = Radical = 1.0 / Base
    while i > 0:
        # i余Base求出i在"Base"進制下的最低位的數
        # 乘以Digit將這個數鏡像到小數點右邊
        Inverse += Digit * (i % Base)
        Digit *= Radical
        # i除以Base即可求右一位的數
        i /= Base
        return Inverse


# Dimension為質數
@ti.func
def Halton(Dimension, Index):
    return RadicalInverse(Dimension, Index)


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

camera_pos = ti.Vector([0.0, 0.6, 3.0])
fov = 0.8

max_bounce = 10


@ti.kernel
def render():
    for u, v in pixels:  # 遍歷像素
        pos = camera_pos
        # ray_dir = ti.Vector([
        #     (2 * fov * (u + ti.random()) / resolution[1] - fov * resolution[0] / resolution[1] - 1e-5),
        #     2 * fov * (v + ti.random()) / resolution[1] - fov - 1e-5, -1.0
        # ]).normalized()

        ray_dir = ti.Vector([
            (2 * fov * (u + ti.random()) / resolution[1] - fov * resolution[0] / resolution[1] - 1e-5),
            2 * fov * (v + ti.random()) / resolution[1] - fov - 1e-5, -1.0
        ]).normalized()

        final_throughput = ti.Vector([0.0, 0.0, 0.0])  # 累加到pixels
        throughput = ti.Vector([1.0, 1.0, 1.0])  # Lighting : (r, g, b)

        # 追蹤開始
        bounce = 0
        while bounce < max_bounce:  # bounce的最大次數
            bounce += 1
            # closest:光源到物體的距離
            closest, hit_normal, hit_color, mat = intersect_scene(pos, ray_dir)  # 光發出后碰到場景

            # 0.命中燈光或無材質, 則中斷追蹤
            if mat == mat_none:
                final_throughput += throughput * 0
                break
            if mat == mat_light:
                final_throughput += throughput * light_color
                break

            hit_pos = pos + closest * ray_dir
            ray_dir_i = -ray_dir

            # 1.計算采樣后的ray_dir, pdf

            # 2.lambertian : sample direct light [ mis(sample area light, sample brdf)=> Li ]
            if mat == mat_lambertian:  # lambertian模型
                final_throughput += light_color * throughput * lambertian_brdf * hit_color * sample_light_and_cosineWeighted(hit_pos, hit_normal)
                # Sample Direct Light Only
                # throughput *= sample_light_and_cosineWeighted(hit_pos, hit_normal, hit_color)

            # 2.lambertian : sample cosine-Weighted
            ray_dir, pdf = sample_ray_dir(ray_dir, hit_normal, hit_pos, mat)  # 由反射更新ray_dir
            pos = hit_pos + eps * ray_dir
            if mat == mat_lambertian:  # lambertian
                # f(lambert) * max(0.0, cos(n,l)) / pdf
                # throughput : Li or Lo
                # the light transport equation
                throughput *= (lambertian_brdf * hit_color) * dot_or_zero(hit_normal, ray_dir) / pdf

            # 3.specular全反射
            if mat == mat_specular:
                throughput *= hit_color
            # 4.glass折射btdf
            if mat == mat_glass:
                throughput *= hit_color

            # 5.microfacet
            if mat == mat_microfacet:
                # compute_microfacet_brdf params:(alpha, idx, i_dir, o_dir, n_dir)
                cook_torrance_brdf = compute_microfacet_brdf(sp3_microfacet_roughness, sp3_idx, ray_dir_i, ray_dir, hit_normal)
                # print(lambertian_brdf, cook_torrance_brdf)

                # microfacet_brdf = lambertian_brdf
                microfacet_brdf = 0.7 * lambertian_brdf + cook_torrance_brdf

                throughput *= (microfacet_brdf * hit_color) * dot_or_zero(hit_normal, ray_dir) / pdf

            # 6.glossy
            if mat == mat_pm:  # TODO:
                throughput *= (lambertian_brdf * hit_color) * dot_or_zero(hit_normal, ray_dir) / pdf


        # 追蹤結束

        pixels[u, v] += final_throughput


gui = ti.GUI('Path Tracing', resolution)
i = 0

while gui.running:
    # if gui.get_event(ti.GUI.PRESS):
    #     if gui.event.key == 'w':
    #         gui.clear()
    #         i = 0
    #         interval = 10
    #         # pixels = ti.Vector.field(3, dtype=ti.f32, shape=resolution)  # 屏幕像素緩沖 [800, 800] 元素為(r, g, b)
    #         count_var = ti.field(ti.i32, shape=(1,))
    #         box1_rotate_rad += np.pi/8

    if gui.get_event(ti.GUI.PRESS):
        if gui.event.key == 'w':
            img = pixels.to_numpy()
            img = np.sqrt(img / img.mean() * 0.24)
            fname = f'cornell_box.png'
            ti.imwrite(img, fname)
            print("圖片已存儲")

    render()
    interval = 10  # render()10次, 繪1次圖
    if i % interval == 0 and i > 0:
        img = pixels.to_numpy()
        img = np.sqrt(img / img.mean() * 0.24)
        gui.set_image(img)

        gui.show()
    i += 1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM