光线追踪与神经渲染技术:从零基础到前沿实践
本教材面向零基础读者,系统讲解光线追踪、全局光照及神经渲染技术的原理与实践。
第一章:光线追踪与全局光照基础概念
1.1 什么是光线追踪?
光线追踪(Ray
Tracing)是一种基于物理原理的渲染技术,通过模拟光线在场景中的传播行为来生成逼真的图像。
1.1.1 光线追踪的基本思想
在现实世界中,光线从光源发出,经过物体的反射、折射、散射等过程,最终进入人眼或相机。光线追踪正是模拟这一过程,但为了计算效率,我们通常采用逆向追踪 (Backward
Tracing):从相机发出光线,逆向追踪其与场景中物体的交互,直到到达光源。
1 2 3 4 5 光源 ──────────────────────────────────────> 人眼 (物理世界中光线传播方向) 相机 ──────────────────────────────────────> 光源 (光线追踪中的逆向追踪)
1.1.2 核心光线类型
光线类型
英文名称
作用描述
主光线
Primary Ray / Camera Ray
从相机发出,穿过像素进入场景的第一条光线
阴影光线
Shadow Ray
从着色点射向光源,判断该点是否被遮挡
反射光线
Reflection Ray
光线击中光滑表面后按反射定律继续传播
折射光线
Refraction Ray
光线穿过透明物体时改变方向
1.1.3 光线与物体的相交测试
光线追踪的核心操作是判断光线与几何体的相交。对于一条光线,可以用参数方程表示:
\(\mathbf{r}(t) = \mathbf{o} +
t\mathbf{d}\) ,其中 \(t \geq
0\)
其中:
\(\mathbf{o}\)
是光线起点(origin)
\(\mathbf{d}\)
是光线方向(direction),通常为单位向量
\(t\)
是参数,表示光线上的点与起点的距离
光线与球体相交测试示例:
设球心为 \(\mathbf{c}\) ,半径为
\(r\) ,光线上的点 \(\mathbf{r}(t)\)
到球心的距离等于半径时相交:
\[\mathbf{o} + t\mathbf{d} - \mathbf{c} =
r\]
两边平方并展开:
\[(\mathbf{o} - \mathbf{c} + t\mathbf{d})
\cdot (\mathbf{o} - \mathbf{c} + t\mathbf{d}) = r^2\]
整理得到关于 \(t\)
的一元二次方程:
\[t^2(\mathbf{d} \cdot \mathbf{d}) +
2t\mathbf{d} \cdot (\mathbf{o} - \mathbf{c}) + (\mathbf{o} - \mathbf{c})
\cdot (\mathbf{o} - \mathbf{c}) - r^2 = 0\]
由于 \(\mathbf{d}\)
是单位向量,\(\mathbf{d} \cdot \mathbf{d} =
1\) ,简化为:
\[t^2 + 2bt + c = 0\]
其中:
\(b = \mathbf{d} \cdot (\mathbf{o} -
\mathbf{c})\)
\(c = \mathbf{o} - \mathbf{c}^2 -
r^2\)
判别式 \(\Delta = b^2 - c\) :
\(\Delta < 0\) :不相交
\(\Delta = 0\) :相切
\(\Delta > 0\) :相交于两点
1.2 渲染的基本物理量
理解光线追踪需要先掌握光度学的基本概念。
1.2.1 辐射通量(Radiant Flux, \(\Phi\) )
定义: 单位时间内发射、传播或接收的辐射能量。
\[\Phi = \frac{dQ}{dt} \quad [W =
J/s]\]
单位是瓦特(Watt, W),表示光源的总功率。
1.2.2 辐射强度(Radiant Intensity,
\(I\) )
定义: 点光源在单位立体角内发出的辐射通量。
\[I = \frac{d\Phi}{d\omega} \quad
[W/sr]\]
单位是瓦特每球面度(W/sr)。
立体角(Solid Angle)
是三维空间中角度的推广。一个球面上的面积 \(A\) 对球心的立体角为:
\[\omega = \frac{A}{r^2} \quad
[sr]\]
完整球面的立体角为 \(4\pi\) sr。
1.2.3 辐照度(Irradiance, \(E\) )
定义: 单位面积上接收的辐射通量。
\[E = \frac{d\Phi}{dA} \quad
[W/m^2]\]
辐照度描述了入射到表面的光能密度。
重要性质:Lambert余弦定律
当光线以角度 \(\theta\)
倾斜入射时,有效接收面积增大,辐照度减小:
\[E = E_0 \cos\theta = \frac{\Phi
\cos\theta}{A}\]
这就是为什么在北半球,南坡比北坡更温暖——阳光入射角更小,接收的能量更多。
1.2.4 辐射率(Radiance, \(L\) )
定义: 单位投影面积、单位立体角内的辐射通量。
\[L = \frac{d^2\Phi}{dA_{\perp} d\omega} =
\frac{d^2\Phi}{dA \cos\theta d\omega} \quad [W/(m^2 \cdot
sr)]\]
辐射率是渲染中最核心的物理量,因为:
感知相关性: 人眼和相机传感器感知的是辐射率
传播不变性:
在真空中沿光线传播时,辐射率保持不变(忽略衰减)
可加性: 多个光源的辐射率可以线性叠加
1.2.5 各物理量之间的关系
1 2 3 4 5 6 7 Radiant Flux (Φ) [W] │ ├──> Intensity (I = dΦ/dω) [W/sr] ← 点光源特性 │ └──> Irradiance (E = dΦ/dA) [W/m²] ← 表面接收量 │ └──> Radiance (L = dE/dω⊥) [W/(m²·sr)] ← 最精细的描述
1.3 双向反射分布函数(BRDF)
1.3.1 BRDF的定义
当光线照射到物体表面时,一部分被吸收,一部分被反射。双向反射分布函数(Bidirectional
Reflectance Distribution Function,
BRDF)描述了入射光如何被反射到不同方向。
定义: 出射辐射率与入射辐照度的比值。
\[f_r(\omega_i \rightarrow \omega_o) =
\frac{dL_o(\omega_o)}{dE_i(\omega_i)} =
\frac{dL_o(\omega_o)}{L_i(\omega_i) \cos\theta_i d\omega_i}\]
其中:
\(\omega_i\) :入射方向
\(\omega_o\) :出射方向
\(L_o\) :出射辐射率
\(L_i\) :入射辐射率
\(\theta_i\) :入射角
单位:\([sr^{-1}]\)
1.3.2 BRDF的性质
1. 非负性(Positivity)
\[f_r(\omega_i \rightarrow \omega_o) \geq
0\]
物理上,反射光不可能为负。
2. 互易性(Reciprocity, Helmholtz原理)
\[f_r(\omega_i \rightarrow \omega_o) =
f_r(\omega_o \rightarrow \omega_i)\]
入射和出射方向交换,BRDF值不变。这一性质在双向路径追踪中非常重要。
3. 能量守恒(Energy Conservation)
\[\int_{\Omega} f_r(\omega_i \rightarrow
\omega_o) \cos\theta_o d\omega_o \leq 1\]
反射的总能量不超过入射能量。
1.3.3 常见BRDF模型
1. Lambertian BRDF(理想漫反射)
\[f_r = \frac{\rho}{\pi}\]
其中 \(\rho\)
是反照率(albedo),取值范围 \([0,
1]\) 。
Lambert模型假设光线均匀地向所有方向反射,因此BRDF是常数。粗糙表面如纸张、粉笔可用此模型近似。
2. Phong反射模型
Phong模型包含三个分量:
\[L_o = k_a L_a + k_d (L \cdot N) + k_s (R
\cdot V)^n\]
环境光(Ambient):\(k_a
L_a\) ,简化间接光照
漫反射(Diffuse):\(k_d (L \cdot
N)\)
镜面反射(Specular):\(k_s (R \cdot
V)^n\)
其中:
\(L\) :指向光源的方向
\(N\) :表面法线
\(R\) :反射方向
\(V\) :指向观察者的方向
\(n\) :高光指数,值越大高光越锐利
3. Blinn-Phong模型
用半程向量(Half Vector)替代反射方向:
\[H = \frac{L + V}{L + V}\]
镜面反射项变为:
\[k_s (N \cdot H)^n\]
计算更高效,且在某些情况下效果更好。
4. 微表面模型(Microfacet Model)
更物理准确的模型,假设表面由无数微小镜面组成:
\[f_r = \frac{F(\omega_i, \omega_o) \cdot
G(\omega_i, \omega_o) \cdot D(h)}{4(\omega_i \cdot n)(\omega_o \cdot
n)}\]
\(F\) :Fresnel项,描述不同角度的反射率
\(G\) :几何遮蔽项,考虑微表面间的遮挡
\(D\) :法线分布函数(NDF),描述微表面法线的分布
\(h\) :半程向量
常用的NDF包括GGX(Trowbridge-Reitz):
\[D_{GGX}(h) = \frac{\alpha^2}{\pi((n
\cdot h)^2(\alpha^2 - 1) + 1)^2}\]
其中 \(\alpha\) 是粗糙度参数。
1.4 渲染方程(The Rendering
Equation)
1.4.1 渲染方程的推导
渲染方程是全局光照的核心,描述了光能在场景中的平衡状态。由James
Kajiya于1986年提出。
推导思路:
考虑表面一点 \(x\) ,其向方向 \(\omega_o\) 发出的出射辐射率 \(L_o\) 由两部分组成:
自发光(Emission): 物体自身发出的光 \(L_e(x, \omega_o)\)
反射光(Reflection):
来自各方向的入射光被BRDF反射的部分
对于反射部分,根据BRDF定义:
\[L_r(x, \omega_o) = \int_{\Omega} f_r(x,
\omega_i \rightarrow \omega_o) L_i(x, \omega_i) \cos\theta_i
d\omega_i\]
其中积分域 \(\Omega\)
是以表面法线为中心的上半球面。
入射辐射率 \(L_i(x, \omega_i)\)
来自场景中另一点 \(x'\) :
\[L_i(x, \omega_i) = L_o(x',
-\omega_i)\]
这里 \(x'\) 是沿方向 \(\omega_i\) 从 \(x\) 出发与场景的第一个交点。
1.4.2 渲染方程的标准形式
\[L_o(x, \omega_o) = L_e(x, \omega_o) +
\int_{\Omega} f_r(x, \omega_i \rightarrow \omega_o) L_i(x, \omega_i)
(\omega_i \cdot n_x) d\omega_i\]
或者使用更紧凑的符号:
\[L(x \rightarrow \omega_o) = L_e(x
\rightarrow \omega_o) + \int_{S^2} f_r(x, \omega_i \leftrightarrow
\omega_o) L(r(x, \omega_i) \rightarrow -\omega_i) |\cos\theta_i|
d\omega_i\]
其中 \(r(x, \omega_i)\)
是射线投射函数,返回从 \(x\) 沿方向
\(\omega_i\) 的第一个交点。
1.4.3 渲染方程的递归性质
渲染方程是一个积分方程,具有递归结构:
要计算 \(L_o(x)\) ,需要知道 \(L_i(x)\)
\(L_i(x)\) 来自其他点的 \(L_o\)
形成无限递归
这种递归结构反映了光能在场景中的多次反弹,是全局光照复杂性的根源。
1.4.4 渲染方程的算子形式
将渲染方程写成算子形式有助于理解和求解:
\[L = L_e + K L\]
其中 \(K\) 是积分算子:
\[(KL)(x, \omega_o) = \int_{\Omega} f_r(x,
\omega_i \rightarrow \omega_o) L(x, \omega_i) \cos\theta_i
d\omega_i\]
移项得到:
\[L - KL = L_e\]
\[(I - K)L = L_e\]
形式解为 Neumann 级数:
\[L = (I - K)^{-1} L_e =
\sum_{n=0}^{\infty} K^n L_e\]
展开后:
\[L = L_e + KL_e + K^2L_e + K^3L_e +
\cdots\]
每一项对应不同的光照反弹次数:
\(L_e\) :直接看到的自发光
\(KL_e\) :一次反弹光照
\(K^2L_e\) :两次反弹光照
...
1.5 全局光照(Global
Illumination)
1.5.1 什么是全局光照?
全局光照(Global Illumination,
GI)是指在渲染中模拟光线在场景中的多次反弹,包括直接光照和间接光照。
直接光照(Direct Illumination):
光线直接从光源到达物体表面。
间接光照(Indirect Illumination):
光线经过其他物体的反射或散射后到达物体表面,包括:
颜色溢出(Color Bleeding)
焦散(Caustics)
次表面散射(Subsurface Scattering)
体积散射(Volume Scattering)
1.5.2 为什么全局光照重要?
GI示意图
全局光照能显著提升渲染的真实感:
间接照明: 阴影区域不是纯黑,而是被环境光照亮
颜色溢出: 红色地毯附近的白色墙面会带有红色调
真实感: 符合人眼对真实世界的认知
上图展示了全局光照的效果,可以明显看到间接光照对场景整体亮度和色彩的影响。
1.5.3 渲染方程中的全局光照
从渲染方程的角度:
\[L_o = L_e + \underbrace{\int_{\Omega}
f_r L_{direct} \cos\theta d\omega}*{\text{直接光照}} +
\underbrace{\int*{\Omega} f_r L_{indirect} \cos\theta
d\omega}_{\text{间接光照}}\]
直接光照可以从光源直接计算,而间接光照需要递归求解渲染方程。
1.5.4 GI公式与半球积分
渲染方程中的积分是在半球面上进行的:
半球积分示意
半球积分的几何意义:
对于表面一点,所有可能入射光来自以法线为中心的上半球
每个入射方向的贡献 = 入射辐射率 × BRDF × 余弦因子
余弦因子 \(\cos\theta = \omega_i \cdot
n\) 表示光线倾斜时的能量衰减
球坐标系下的积分:
\[\int_{\Omega} f(\omega) d\omega =
\int_0^{2\pi} \int_0^{\pi/2} f(\theta, \phi) \sin\theta d\theta
d\phi\]
1.6 路径追踪(Path Tracing)
1.6.1 路径追踪的基本原理
路径追踪(Path Tracing)是求解渲染方程的蒙特卡洛方法,由James
Kajiya在同一年提出。
核心思想:
从相机发射主光线
在每个交点处,随机选择一个方向继续追踪
重复直到命中光源或超过最大反弹次数
对多条路径取平均
1.6.2 路径追踪算法伪代码
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 function TracePath(ray): throughput = 1.0 radiance = 0.0 for bounce = 0 to maxBounces: hit = IntersectScene(ray) if not hit: break // 直接光照 radiance += throughput * DirectLighting(hit) // 采样BRDF得到新方向 (newDir, pdf, brdf) = SampleBRDF(hit) // 更新throughput throughput *= brdf * dot(newDir, hit.normal) / pdf // 俄罗斯轮盘赌终止 if bounce > 3: p = min(throughput.maxComponent(), 0.95) if random() > p: break throughput /= p ray = Ray(hit.position, newDir) return radiance
1.6.3 路径追踪的问题
路径追踪虽然准确,但计算开销很大:
Path Trace问题
问题1:收敛慢
对于间接光照,需要大量样本才能收敛。特别是:
问题2:噪点
低采样数时会产生明显噪点:
噪��示意
问题3:计算开销
每条路径可能需要多次反弹,每次反弹需要:
计算开销示意
1.6.4 路径追踪的改进技术
1. 直接光照采样
将渲染方程拆分:
\[L_o = L_e + \underbrace{\int_{\Omega}
f_r L_{direct} \cos\theta d\omega}*{\text{直接光照,采样光源}} +
\underbrace{\int*{\Omega} f_r L_{indirect} \cos\theta
d\omega}_{\text{间接光照,采样BRDF}}\]
对直接光照,直接采样光源可以大幅降低方差。
2. 双向路径追踪(BDPT)
同时从相机和光源生成路径,在中间连接。适合处理小光源和焦散。
3. Metropolis光传输(MLT)
使用马尔可夫链蒙特卡洛方法,在路径空间中进行局部探索。一旦找到一条贡献大的路径,就在附近继续搜索。
4. 光子映射(Photon Mapping)
两遍算法:
从光源发射光子,存储在场景中
从相机渲染,使用存储的光子估计间接光照
1.7 小结
本章介绍了光线追踪和全局光照的基础概念:
光线追踪 通过模拟光线传播来渲染图像,核心是光线-几何体相交测试
辐射度学量 (辐照度、辐射率等)是量化描述光照的基础
BRDF 描述了材质如何反射光线
渲染方程 是全局光照的核心,其递归结构导致了计算的复杂性
路径追踪 是求解渲染方程的蒙特卡洛方法,但收敛慢、计算开销大
下一章将深入讲解渲染方程的数学基础和求解方法。
第二章:渲染方程数学基础
2.1 数学预备知识
2.1.1 球坐标与立体角
球坐标系
在三维空间中,球坐标 \((r, \theta,
\phi)\) 与直角坐标 \((x, y, z)\)
的关系:
\[\begin{cases}
x = r \sin\theta \cos\phi
y = r \sin\theta \sin\phi
z = r \cos\theta
\end{cases}\]
其中:
\(r\) :到原点的距离
\(\theta\) :极角(与z轴夹角),范围
\([0, \pi]\)
\(\phi\) :方位角(在xy平面的投影与x轴夹角),范围
\([0, 2\pi)\)
立体角的定义
立体角是三维空间中"角度"概念的推广。正如平面角是弧长除以半径:
\[\theta = \frac{s}{r} \quad
[rad]\]
立体角是球面上的面积除以半径平方:
\[\omega = \frac{A}{r^2} \quad
[sr]\]
单位是球面度(steradian, sr)。
微分立体角
在球坐标系中,微分立体角为:
\[d\omega = \frac{dA}{r^2} = \frac{r
\sin\theta d\phi \cdot r d\theta}{r^2} = \sin\theta d\theta
d\phi\]
半球面上积分时:
\[\int_{\Omega} f(\omega) d\omega =
\int_0^{2\pi} \int_0^{\pi/2} f(\theta, \phi) \sin\theta d\theta
d\phi\]
立体角与方向向量
设方向向量为 \(\omega = (x, y,
z)\) ,则:
\[\cos\theta = z = \omega \cdot
\mathbf{n}\]
其中 \(\mathbf{n} = (0, 0, 1)\)
是半球顶点的法线。
2.1.2 积分变换
面积积分到立体角积分的转换
设光源面积为 \(A\) ,从着色点到光源的方向为 \(\omega\) ,距离为 \(r\) :
\[L_i d\omega = L_o \frac{dA
\cos\theta'}{r^2}\]
其中 \(\theta'\)
是光源表面法线与出射方向的夹角。
这给出了一个重要转换:
\[d\omega = \frac{\cos\theta'
dA}{r^2}\]
这个关系在从面积光源采样时非常重要。
2.2 渲染方程的详细推导
2.2.1 光能平衡方程
考虑表面一点 \(x\) ,分析其出射辐射率
\(L_o\) 的来源:
能量守恒原理: 出射光能 = 自发光 + 反射光 +
透射光
对于不透明材质,忽略透射:
\[L_o(x, \omega_o) = L_e(x, \omega_o) +
L_r(x, \omega_o)\]
反射光来自四面八方的入射光:
\[L_r(x, \omega_o) = \int_{\Omega} f_r(x,
\omega_i \rightarrow \omega_o) L_i(x, \omega_i) \cos\theta_i
d\omega_i\]
2.2.2 各项符号的物理意义
符号
含义
单位
\(L_o(x, \omega_o)\)
点 \(x\) 向方向 \(\omega_o\) 的出射辐射率
\(W/(m^2 \cdot sr)\)
\(L_e(x, \omega_o)\)
点 \(x\) 向方向 \(\omega_o\) 的自发光辐射率
\(W/(m^2 \cdot sr)\)
\(L_i(x, \omega_i)\)
从方向 \(\omega_i\) 入射到点 \(x\) 的辐射率
\(W/(m^2 \cdot sr)\)
\(f_r(x, \omega_i \rightarrow
\omega_o)\)
BRDF,描述反射特性
\(sr^{-1}\)
\(\cos\theta_i\)
入射角余弦,\(\omega_i \cdot
n_x\)
无量纲
\(d\omega_i\)
微分立体角
\(sr\)
2.2.3 完整渲染方程
\[L_o(x, \omega_o) = L_e(x, \omega_o) +
\int_{\Omega^+} f_r(x, \omega_i \rightarrow \omega_o) L_i(x, \omega_i)
(\omega_i \cdot n_x) d\omega_i\]
其中 \(\Omega^+\) 表示朝向法线 \(n_x\) 的正半球。
紧凑形式:
\[L = L_e + K_e L_e\]
或者使用光传输算子:
\[L = L_e + T L\]
其中 \(T\)
是光传输算子,包含BRDF和几何项。
2.3 蒙特卡洛积分
2.3.1 为什么需要蒙特卡洛积分?
渲染方程中的半球积分没有解析解(除了极少数简单场景)。数值积分方法:
均匀采样:
使用固定网格,高维时效率低(维度灾难)
蒙特卡洛: 随机采样,收敛速度 \(O(1/\sqrt{N})\) ,与维度无关
2.3.2 蒙特卡洛积分原理
基本形式:
设要估计积分 \(I = \int_a^b f(x)
dx\)
随机采样 \(x_1, x_2, \ldots, x_N \sim
p(x)\) ,则:
\[\hat{I} = \frac{1}{N} \sum_{i=1}^{N}
\frac{f(x_i)}{p(x_i)}\]
是积分的无偏估计。
证明(简要):
\[E[\hat{I}] = E\left[\frac{1}{N}
\sum_{i=1}^{N} \frac{f(x_i)}{p(x_i)}\right] = \frac{1}{N} \sum_{i=1}^{N}
E\left[\frac{f(X)}{p(X)}\right]\]
\[= E\left[\frac{f(X)}{p(X)}\right] =
\int_a^b \frac{f(x)}{p(x)} p(x) dx = \int_a^b f(x) dx = I\]
2.3.3 方差分析
估计量的方差:
\[Var[\hat{I}] = \frac{1}{N}
Var\left[\frac{f(X)}{p(X)}\right] = \frac{\sigma^2}{N}\]
标准差(误差):
\[\sigma_{\hat{I}} =
\frac{\sigma}{\sqrt{N}}\]
这就是著名的 \(O(1/\sqrt{N})\)
收敛率。
2.3.4 重要性采样
核心思想: 在函数值大的地方多采样。
选择采样分布 \(p(x)\) 与 \(f(x)\) 相似时,方差最小。
理论最优:\(p(x) \propto
|f(x)|\)
应用到渲染方程:
\[L_r = \int_{\Omega} f_r(\omega_i)
L_i(\omega_i) \cos\theta_i d\omega_i\]
采样策略:
采样BRDF:\(p(\omega_i) \propto
f_r(\omega_i) \cos\theta_i\)
采样光源:\(p(\omega_i) \propto
L_i(\omega_i)\)
多重重要性采样(MIS):结合多种采样策略
2.4 多重重要性采样(MIS)
2.4.1 问题背景
渲染积分中通常有多个因子:
\[f(\omega) = f_r(\omega) \cdot
L_i(\omega) \cdot \cos\theta\]
BRDF采样:适合BRDF主导(如高光表面)
光源采样:适合光源主导(如小面积光源)
单一采样策略可能在某些情况下失效。
2.4.2 MIS公式
设从分布 \(p_1, p_2, \ldots, p_n\)
中采样,MIS估计为:
\[\hat{I} = \sum_{i=1}^{n} \frac{1}{n_i}
\sum_{j=1}^{n_i} w_i(x_{i,j})
\frac{f(x_{i,j})}{p_i(x_{i,j})}\]
权重 \(w_i(x)\) 满足 \(\sum_i w_i(x) = 1\) 。
平衡启发式(Balance Heuristic):
\[w_i(x) = \frac{n_i p_i(x)}{\sum_k n_k
p_k(x)}\]
幂次启发式(Power Heuristic):
\[w_i(x) = \frac{(n_i
p_i(x))^\beta}{\sum_k (n_k p_k(x))^\beta}\]
通常取 \(\beta = 2\) 。
2.5 渲染方程的求解方法
2.5.1 直接光照计算
直接光照可以高效计算:
\[L_{direct} = \sum_{k=1}^{N_l} L_{e,k}
\cdot f_r \cdot \cos\theta \cdot V\]
其中 \(V\)
是可见性项(阴影测试)。
解析解: 对于点光源,直接光照有解析解。
采样光源: 对面积光源采样,使用MIS。
2.5.2 间接光照的挑战
间接光照涉及递归积分:
\[L_{indirect} = \int_{\Omega}
f_r(\omega_i) L_o(x', -\omega_i) \cos\theta_i d\omega_i\]
其中 \(L_o(x', -\omega_i)\)
本身也是一个积分,形成无限递归。
解决方案:
路径追踪: 蒙特卡洛估计每条路径
辐射度方法: 假设漫反射,将问题离散化
光子映射: 预计算光子分布
辐射缓存: 缓存并插值间接光照
2.5.3 无偏估计与有偏估计
无偏(Unbiased): 期望值等于真实值
\[E[\hat{I}] = I\]
路径追踪、双向路径追踪是无偏的。
一致(Consistent):
样本量趋于无穷时收敛到真实值
\[\lim_{N \to \infty} \hat{I} =
I\]
光子映射是一致但有偏的。
有偏但一致:
实际中常用,噪点少但可能有系统性误差。
2.6 辐射度方法(Radiosity)
2.6.1 假设与限制
辐射度方法基于以下假设:
所有表面都是Lambertian(完全漫反射)
场景离散化为面片
对于Lambert表面,BRDF是常数 \(f_r =
\rho/\pi\) ,渲染方程简化为:
\[B_i = E_i + \rho_i \sum_j F_{ij}
B_j\]
其中:
\(B_i\) :面片 \(i\) 的辐射度(出射功率密度)
\(E_i\) :面片 \(i\) 的自发光
\(\rho_i\) :面片 \(i\) 的反射率
\(F_{ij}\) :从面片 \(i\) 到面片 \(j\) 的形状因子
2.6.2 形状因子
形状因子 \(F_{ij}\) 表示从面片 \(i\) 发出的辐射中到达面片 \(j\) 的比例:
\[F_{ij} = \frac{1}{A_i} \int_{A_i}
\int_{A_j} \frac{\cos\theta_i \cos\theta_j}{\pi r^2} V(x_i, x_j) dA_j
dA_i\]
性质:
互易性:\(A_i F_{ij} = A_j
F_{ji}\)
守恒性:\(\sum_j F_{ij} = 1\)
2.6.3 矩阵形式
设 \(N\) 个面片,写成矩阵形式:
\[\mathbf{B} = \mathbf{E} + \mathbf{R}
\mathbf{F} \mathbf{B}\]
\[\mathbf{B} = (\mathbf{I} -
\mathbf{R}\mathbf{F})^{-1} \mathbf{E}\]
可以通过迭代方法求解:
Jacobi迭代
Gauss-Seidel迭代
Southwell迭代
2.6.4 辐射度方法的局限
只适用于漫反射材质: 无法处理镜面反射、折射
计算复杂度: \(O(N^2)\) 的形状因子计算
内存需求: 存储 \(N^2\) 的形状因子矩阵
预计算需求: 场景变化需要重新计算
2.7 球面调和函数(Spherical
Harmonics)
2.7.1 基函数概念
球面调和函数是定义在球面上的正交基函数,用于表示球面上的函数。
类比:傅里叶级数将周期函数分解为正弦余弦函数的和。
球面调和函数将球面函数分解为特定基函数的组合:
\[f(\theta, \phi) = \sum_{l=0}^{\infty}
\sum_{m=-l}^{l} c_l^m Y_l^m(\theta, \phi)\]
2.7.2 球面调和函数的定义
\[Y_l^m(\theta, \phi) = K_l^m
P_l^{|m|}(\cos\theta) e^{im\phi}\]
其中:
\(P_l^m\) :关联勒让德多项式
\(K_l^m\) :归一化常数
\(l\) :阶数(degree),决定频率
\(m\) :序数(order),\(-l \leq m \leq l\)
前几阶球面调和函数:
\(l\)
\(m\)
实数形式
描述
0
0
\(Y_0^0 =
\frac{1}{2}\sqrt{\frac{1}{\pi}}\)
常数项
1
-1
\(Y_1^{-1} = \sqrt{\frac{3}{4\pi}}
y\)
线性项
1
0
\(Y_1^0 = \sqrt{\frac{3}{4\pi}}
z\)
线性项
1
1
\(Y_1^1 = \sqrt{\frac{3}{4\pi}}
x\)
线性项
2
...
...
二次项
其中 \((x, y, z)\)
是单位方向向量的分量。
2.7.3 在渲染中的应用
环境光表示:
环境光照 \(L_i(\omega)\)
可以投影到SH基:
\[L_i(\omega) \approx \sum_{l=0}^{n}
\sum_{m=-l}^{l} c_l^m Y_l^m(\omega)\]
辐照度计算:
对于Lambert表面,辐照度积分:
\[E(n) = \int_{\Omega} L_i(\omega) \max(0,
\omega \cdot n) d\omega\]
由于卷积性质,只需存储低阶SH系数(通常3阶):
\[E(n) \approx \sum_{l=0}^{2}
\sum_{m=-l}^{l} c_l^m Y_l^m(n)\]
旋转不变性:
SH的一个重要性质:低阶SH系数的旋转可以表示为线性变换。
这允许快速旋转预计算的环境光。
2.7.4 SH光照贴图
预计算辐射率传输(PRT):
\[L_{out}(n) = \sum_{l,m}
\sum_{l',m'} c_l^m T_{l,m}^{l',m'}
Y_{l'}^{m'}(n)\]
其中传输矩阵 \(T\)
预先计算,运行时只需矩阵乘法。
2.8 小结
本章深入讲解了渲染方程的数学基础:
立体角与球坐标 :量化描述方向和角度
蒙特卡洛积分 :数值求解积分的核心方法
重要性采样与MIS :降低方差的关键技术
辐射度方法 :离散化求解,适用于漫反射场景
球面调和函数 :高效表示和计算环境光照
下一章将介绍传统GI解决方案及其实现原理。
第三章:传统GI解决方案详解
3.1 概述
在实时渲染中,完全的路径追踪计算代价太高。游戏和实时应用需要各种近似技术来高效计算全局光照。本章介绍游戏中常用的传统GI解决方案。
GI方案概览
3.2 直接光照与间接光照分离
3.2.1 渲染方程的拆分
渲染方程可以拆分为直接光照和间接光照两部分:
\[L_o = L_e + L_{direct} +
L_{indirect}\]
直接光照:
\[L_{direct} = \sum_{k} \int_{A_k}
f_r(\omega_i \rightarrow \omega_o) L_e(x_k, \omega_k) \frac{\cos\theta_i
\cos\theta_k}{r^2} V(x, x_k) dA_k\]
直接光照可以从光源直接采样计算,通常使用阴影贴图(Shadow
Map)或阴影光线。
间接光照:
\[L_{indirect} = \int_{\Omega}
f_r(\omega_i \rightarrow \omega_o) L_{indirect,in}(\omega_i)
\cos\theta_i d\omega_i\]
间接光照没有明确的解析解,需要近似方法。
3.2.2 为什么间接光照难以计算?
递归性:
间接光照来自其他表面,而这些表面本身也在接收间接光照
全局依赖:
场景中任何物体的变化都可能影响所有其他物体的间接光照
高频信息: 焦散、阴影等高频效果需要大量采样
动态场景: 物体移动或光源变化时需要重新计算
3.3 环境光遮蔽(Ambient
Occlusion, AO)
3.3.1 基本原理
环境光遮蔽是最简单的间接光照近似,假设环境光均匀分布:
\[AO(x) = \frac{1}{\pi} \int_{\Omega} V(x,
\omega) \cos\theta d\omega\]
其中 \(V(x, \omega)\)
是可见性函数:
\(V = 1\) :方向 \(\omega\) 可见(未被遮挡)
\(V = 0\) :方向 \(\omega\) 被遮挡
最终光照近似:
\[L_o \approx L_{ambient} \cdot AO \cdot
\rho\]
3.3.2 屏幕空间环境光遮蔽(SSAO)
SSAO在屏幕空间计算AO,无需预先烘焙:
算法步骤:
对每个像素,在以像素位置为中心的半球内随机采样若干点
检查每个采样点是否被几何体遮挡
根据遮挡比例计算AO值
伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 float SSAO(vec3 position, vec3 normal) { float occlusion = 0.0 ; for (int i = 0 ; i < KERNEL_SIZE; i++) { vec3 sampleDir = normalize (kernel[i]); vec3 samplePos = position + sampleDir * radius * random(); vec4 offset = projection * vec4 (samplePos, 1.0 ); offset .xy /= offset .w; float sampleDepth = texture (depthBuffer, offset .xy).r; float rangeCheck = smoothstep (0.0 , 1.0 , radius / abs (position.z - sampleDepth)); occlusion += (sampleDepth >= samplePos.z ? 1.0 : 0.0 ) * rangeCheck; } return 1.0 - (occlusion / KERNEL_SIZE); }
优点:
实时计算,支持动态场景
实现简单,性能开销小
广泛应用于游戏中
缺点:
只考虑局部遮挡
缺少颜色溢出(Color Bleeding)
屏幕空间信息丢失导致伪影
3.3.3
水平环境光遮蔽(Horizon-Based Ambient Occlusion, HBAO)
HBAO是SSAO的改进版本,考虑地平线角度:
\[AO = \frac{1}{2\pi} \int_{0}^{2\pi}
\left(1 - \cos(h(\phi))\right) d\phi\]
其中 \(h(\phi)\) 是方位角 \(\phi\) 方向的地平线角度。
HBAO提供更物理准确的结果,但计算量更大。
3.4 光照贴图(Light Map)
3.4.1 基本原理
光照贴图是一种预计算技术:
预计算阶段(烘焙):
离线计算场景中每个点的间接光照
存储: 将结果存储在纹理贴图中
运行时: 通过纹理采样获取预计算的光照
Light Map示意
3.4.2 光照贴图的UV映射
为了存储光照信息,需要将3D场景展开到2D纹理空间:
流程:
为场景中的静态物体生成第二套UV坐标(光照UV)
确保UV展开时没有重叠
将场景离散化为光照贴图纹素
每个纹素存储该位置的光照信息
打包策略:
自动UV展开:使用chart生成算法
纹素密度控制:根据几何复杂度和重要性调整
填充边界:避免纹素间泄漏
3.4.3 光照贴图烘焙
烘焙过程通常使用离线路径追踪:
1 2 3 4 5 6 7 8 9 for each texel in lightmap: position = getWorldPosition(texel) normal = getWorldNormal(texel) // 路径追踪计算间接光照 radiance = PathTrace(position, normal) // 存储结果 lightmap[texel] = radiance
采样策略:
辐照度(Irradiance): 存储半球积分结果 \[E(n) = \int_{\Omega} L_i(\omega) \cos\theta
d\omega\]
辐射率(Radiance): 存储方向分布
3.4.4 光照贴图的优缺点
优点:
运行时开销极低(一次纹理采样)
高质量的间接光照
支持复杂的光传输效果
缺点:
只适用于静态场景
烘焙时间长
存储空间需求大
分辨率受限,可能模糊
适用场景:
静态场景的游戏
室内环境
对实时性要求不高的预渲染
3.5 光照探针(Light Probe)
3.5.1 基本原理
光照探针是一种点采样技术,在场景中离散位置采样光照信息:
Light Probe示意
每个探针存储其位置的光照信息,运行时通过插值获取任意点的光照。
3.5.2 探针的组织方式
1. 点云探针
在场景中手动或自动放置探针点:
1 2 3 4 Probe Positions: - P1: (x1, y1, z1) - P2: (x2, y2, z2) - ...
运行时使用最近邻或三角插值。
2. 体素网格探针(Volumetric Probe)
将空间划分为规则的3D网格,每个网格单元存储光照信息:
1 2 3 4 5 6 struct ProbeVolume { vec3 origin; vec3 cellSize; ivec3 dimensions; Probe[] probes; };
3. 八叉树探针
根据场景复杂度自适应放置探针:
3.5.3 探针存储的内容
探针可以存储不同粒度的光照信息:
辐照度探针:
存储辐照度的球面调和函数系数:
1 2 3 struct IrradianceProbe { vec3 shCoeffs[9 ]; };
重建辐照度:
\[E(n) \approx \sum_{i=0}^{8} c_i \cdot
Y_i(n)\]
辐射率探针:
存储完整的方向辐射率分布,通常使用:
立方体贴图(Cubemap)
八面体映射(Octahedral Map)
3.5.4 探针插值
给定任意位置 \(p\) ,从周围探针插值获取光照:
三线性插值:
对于体素网格探针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vec3 sampleIrradiance(vec3 position, vec3 normal) { ivec3 cell = floor ((position - volume.origin) / volume.cellSize); vec3 frac = fract ((position - volume.origin) / volume.cellSize); vec3 result = vec3 (0.0 ); for (int i = 0 ; i < 8 ; i++) { ivec3 offset = ivec3 (i & 1 , (i >> 1 ) & 1 , (i >> 2 ) & 1 ); vec3 weight = mix (1.0 - frac, frac, offset ); result += probes[cell + offset ].eval(normal) * weight.x * weight.y * weight.z; } return result; }
重心坐标插值:
对于四面体网格:
1 2 3 4 5 6 7 8 9 10 vec3 interpolateTetrahedra(vec3 position, vec3 normal) { Tetrahedron tet = findContainingTetrahedron(position); vec4 barycentric = computeBarycentric(position, tet); vec3 result = vec3 (0.0 ); for (int i = 0 ; i < 4 ; i++) { result += probes[tet.vertices [i]].eval(normal) * barycentric[i]; } return result; }
3.5.5 探针的生成方式
预烘焙探针:
离线阶段:使用路径追踪计算每个探针位置的光照
存储:保存SH系数或辐射率贴图
运行时:插值获取
实时探针更新:
对于动态场景,探针需要实时更新:
时间分片更新: 每帧更新部分探针
重要性驱动更新: 优先更新变化大的区域
渐进式更新: 使用累积采样提高质量
3.6 体素全局光照(Voxel GI)
3.6.1 体素圆锥追踪(Voxel
Cone Tracing, VCT)
VCT是一种实时全局光照技术:
核心思想:
将场景体素化,存储辐射率
使用圆锥追踪近似半球积分
体素化:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Voxel { vec3 radiance; vec3 normal; float occupancy; }; void voxelizeScene () { for each triangle in scene: determine voxel coverage accumulate radiance and normal clip to cascade boundaries }
圆锥追踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vec3 coneTrace(vec3 origin, vec3 direction, float coneAngle) { vec3 radiance = vec3 (0.0 ); float distance = 0.0 ; while (distance < maxDistance) { float coneRadius = distance * tan (coneAngle); float lod = log2 (coneRadius / voxelSize); vec3 samplePos = origin + direction * distance ; vec3 voxelRadiance = sampleVoxelVolume(samplePos, lod); radiance += voxelRadiance * occlusion; distance += step ; } return radiance; }
间接光照计算:
1 2 3 4 5 6 7 8 9 10 11 vec3 computeIndirect(vec3 position, vec3 normal) { vec3 result = vec3 (0.0 ); for (int i = 0 ; i < NUM_CONES; i++) { vec3 coneDir = getHemisphereCone(i, normal); result += coneTrace(position, coneDir, coneAngle); } return result / NUM_CONES; }
3.6.2 VCT的优缺点
优点:
支持动态场景
实时更新
可产生软阴影和光泽反射效果
缺点:
体素化开销大
内存占用高
需要高分辨率体素网格才能获得高质量结果
3.7 光照传播体(Light
Propagation Volumes, LPV)
3.7.1 基本原理
LPV将光照传播建模为网格中的辐射率扩散过程:
注入阶段: 将直接光照注入到体素网格
传播阶段: 迭代传播辐射率
渲染阶段: 从网格采样间接光照
3.7.2 辐射率表示
每个体素单元存储辐射率的SH系数:
1 2 3 4 5 struct LPVCell { vec3 shCoeffsR[4 ]; vec3 shCoeffsG[4 ]; vec3 shCoeffsB[4 ]; };
3.7.3 传播过程
辐射率从一个单元传播到相邻单元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void propagateStep () { LPVCell newGrid[GRID_SIZE]; for each cell in grid: for each direction in 6 face directions: LPVCell neighbor = getNeighbor (cell, direction); vec3 incident = neighbor.getFlux (direction); cell.shCoeffs += projectToSH (incident, direction); grid = newGrid; }
通常需要3-4次传播迭代。
3.7.4 LPV的问题
光泄漏: 辐射率穿过薄墙传播
自发光: 错误的自阴影
分辨率限制: 低分辨率导致光照模糊
3.8 屏幕空间全局光照(SSGI)
3.8.1 屏幕空间方法的优势
屏幕空间方法只需要当前帧的G-buffer信息:
无需预计算,完全实时。
3.8.2
屏幕空间环境光遮蔽改进(SSAO → SSDO)
屏幕空间方向遮挡(SSDO)扩展AO以计算间接光照:
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 vec3 SSDO(vec2 screenPos, vec3 position, vec3 normal) { vec3 indirect = vec3 (0.0 ); for (int i = 0 ; i < NUM_SAMPLES; i++) { vec2 sampleOffset = samplingPattern[i]; vec2 sampleScreenPos = screenPos + sampleOffset * radius; vec3 samplePos = reconstructPosition(sampleScreenPos); vec3 toSample = samplePos - position; if (dot (normalize (toSample), normal) < 0 ) { vec3 sampleColor = texture (colorBuffer, sampleScreenPos).rgb; float weight = max (0 , dot (normal, normalize (toSample))); indirect += sampleColor * weight; } } return indirect / NUM_SAMPLES; }
3.8.3 屏幕空间反射(SSR)
SSR在屏幕空间追踪反射光线:
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 vec3 screenSpaceReflection(vec3 position, vec3 viewDir, vec3 normal) { vec3 reflectDir = reflect (-viewDir, normal); vec3 rayPos = position; vec3 rayDir = reflectDir; for (int step = 0 ; step < MAX_STEPS; step ++) { rayPos += rayDir * stepSize; vec2 screenPos = projectToScreen(rayPos); if (!inScreen(screenPos)) break ; float depth = texture (depthBuffer, screenPos).r; float rayDepth = rayPos.z; if (rayDepth > depth) { return texture (colorBuffer, screenPos).rgb; } } return texture (envMap, reflectDir).rgb; }
3.8.4 屏幕空间方法的局限
致命缺陷:
屏幕外信息丢失: 屏幕外的物体无法影响当前像素
背面信息丢失: 被遮挡的几何体不可见
解决方案:
结合其他方法(如探针)补充屏幕外信息
使用时间累积减少噪点
混合多分辨率采样
3.9 传统方法总结
方法
预计算
动态场景
质量
性能
Light Map
是
否
高
运行时极快
Light Probe
可选
部分支持
中
快
SSAO
否
是
低
快
SSGI
否
是
中-低
中等
VCT
否
是
中
中等
LPV
否
是
低-中
快
传统方法对比
3.10 小结
本章介绍了游戏和实时渲染中常用的传统GI解决方案:
AO方法 :简单有效,但只有遮蔽效果
光照贴图 :高质量,但只适用于静态场景
光照探针 :灵活,可部分支持动态
体素方法(VCT/LPV) :实时支持动态,但质量和性能有折衷
屏幕空间方法 :完全实时,但信息不完整
下一章将介绍DDGI(Dynamic Diffuse Global
Illumination)技术,这是传统探针方法的现代化改进,也是神经渲染技术的基础。
第四章:DDGI(Dynamic
Diffuse Global Illumination)原理与实现
4.1 DDGI概述
4.1.1 什么是DDGI?
DDGI(Dynamic Diffuse Global
Illumination)是由NVIDIA提出的实时全局光照技术,用于计算场景中的漫反射间接光照。它是传统光照探针方法的现代化改进,结合了光线追踪和探针更新机制。
DDGI示意图
4.1.2 DDGI的核心思想
DDGI的核心思想是:
空间划分: 在场景中放置规则网格的探针(Probe)
光线追踪更新:
使用光线追踪从每个探针位置投射光线,收集光照信息
辐照度存储:
每个探针存储其位置周围的辐照度信息
实时插值:
运行时从探针插值获取任意位置的间接光照
4.1.3 DDGI与传统方法的对比
特性
传统Light Probe
DDGI
更新方式
离线烘焙
实时光追更新
动态支持
有限
完全支持
存储方式
SH系数
辐照度+深度Atlas
漏光问题
严重
有解决方案
硬件要求
低
需要RTX或软件光追
4.2 Probe的基本原理
4.2.1 Probe的物理意义
每个探针本质上存储的是其所在位置的球面光照分布 :
Probe示意图
对于点 \(p\)
处的探针,我们关心的是从各个方向 \(\omega\) 入射的辐射率 \(L_i(p, \omega)\) 。
4.2.2 辐照度与辐射率
辐照度(Irradiance) 是辐射率在半球面上的积分:
\[E(p, n) = \int_{\Omega} L_i(p, \omega)
(\omega \cdot n) d\omega\]
其中 \(n\) 是表面法线。
关键洞察:
对于漫反射材质,我们只需要辐照度,不需要完整的方向辐射率分布。
4.2.3 Probe存储的内容
DDGI中每个探针存储两类信息:
1. 辐照度(Irradiance)Atlas
将每个探针的辐照度信息存储为一个小型纹理,通常使用球面映射:
1 2 3 4 5 6 Probe Atlas结构: ┌────────────────────────────────────┐ │ Probe 0 │ Probe 1 │ Probe 2 │ ... │ ← 辐照度信息 ├────────────────────────────────────┤ │ Probe N │ ... │ ... │ ... │ └────────────────────────────────────┘
每个探针存储其球面上的辐照度分布,通常使用:
八面体映射(Octahedral Mapping):将球面映射到正方形
等距柱状投影(Equirectangular):类似世界地图
2. 深度(Depth)Atlas
存储每个探针方向上到最近表面的距离:
深度Atlas
深度信息的作用:
防止漏光(Light Leaking)
提高探针插值的准确性
实现更精确的遮挡计算
4.3 八面体映射(Octahedral
Mapping)
4.3.1 为什么需要映射?
将球面上的方向映射到2D纹理坐标,方便存储和采样。理想的映射应该:
4.3.2 八面体映射原理
将单位球面上的点投影到正八面体,再展开为正方形:
方向到纹理坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vec2 directionToOctahedral (vec3 dir) { dir = normalize (dir); vec2 octCoord = dir.xy / (abs (dir.x) + abs (dir.y) + abs (dir.z)); if (dir.z < 0.0 ) { octCoord = vec2 ( (1.0 - abs (octCoord.y)) * sign (octCoord.x), (1.0 - abs (octCoord.x)) * sign (octCoord.y) ); } return octCoord * 0.5 + 0.5 ; }
纹理坐标到方向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vec3 octahedralToDirection (vec2 octCoord) { vec2 p = octCoord * 2.0 - 1.0 ; float z = 1.0 - abs (p.x) - abs (p.y); if (z < 0.0 ) { p = vec2 ( (1.0 - abs (p.y)) * sign (p.x), (1.0 - abs (p.x)) * sign (p.y) ); } return normalize (vec3 (p.x, p.y, z)); }
4.3.3 八面体映射的优点
方向均匀分布: 比等距柱状投影更均匀
高效计算: 只需少量算术运算
无缝采样: 边界处理简单
低失真: 在球面上均匀采样
4.4 DDGI Probe更新算法
4.4.1 更新流程概览
DDGI的Probe更新包含以下步骤:
1 2 3 4 5 6 7 8 9 10 每帧更新: ┌─────────────────────────────────────────────────────┐ │ 1. 从每个探针发射光线(Ray Tracing) │ │ ↓ │ │ 2. 计算光线交点处的直接光照 │ │ ↓ │ │ 3. 累积更新辐照度和深度Atlas │ │ ↓ │ │ 4. 时间滤波减少噪点 │ └─────────────────────────────────────────────────────┘
4.4.2 光线追踪更新
从每个探针位置发射光线:
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 void updateProbeRadiance (int probeIndex, vec3 probePosition) { ivec2 atlasCoord = getProbeAtlasCoord (probeIndex); for (int rayIndex = 0 ; rayIndex < RAYS_PER_PROBE; rayIndex++) { vec3 rayDir = getRayDirection (rayIndex); Ray ray = Ray (probePosition, rayDir); RayHit hit = traceRay (ray); if (hit.valid) { vec3 directLight = computeDirectLight (hit.position, hit.normal); vec3 indirectLight = interpolateFromProbes (hit.position, hit.normal); vec3 totalLight = directLight + indirectLight; ivec2 texelCoord = atlasCoord + directionToTexel (rayDir); radianceAtlas[texelCoord] = totalLight; depthAtlas[texelCoord] = hit.distance; } else { vec3 skyLight = sampleSkyLight (rayDir); radianceAtlas[texelCoord] = skyLight; depthAtlas[texelCoord] = MAX_DISTANCE; } } }
4.4.3 辐照度累积
为了减少噪点,DDGI使用时间累积:
1 2 3 4 5 6 7 8 9 10 11 12 vec3 currentIrradiance = computeIrradianceFromRays (); vec3 previousIrradiance = loadPreviousIrradiance (probeIndex); float alpha = 0.05 ; vec3 blendedIrradiance = lerp (currentIrradiance, previousIrradiance, alpha); storeIrradiance (probeIndex, blendedIrradiance);
4.4.4 深度的作用
深度信息用于解决漏光问题:
深度防漏光
问题:
当探针位于墙壁外侧时,可能错误地照亮墙内侧。
解决方案: 使用深度信息检测墙壁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vec3 sampleProbeWithDepth (vec3 position, vec3 normal, Probe probe) { vec3 toProbe = probe.position - position; float distToProbe = length (toProbe); vec3 dirToProbe = toProbe / distToProbe; float probeDepth = sampleDepthAtlas (probe, -dirToProbe); if (distToProbe > probeDepth + threshold) { return vec3 (0.0 ); } return sampleIrradianceAtlas (probe, normal); }
4.5 探针插值与渲染
4.5.1 三线性插值
对于规则网格探针,使用三线性插值获取任意位置的光照:
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 vec3 interpolateProbes (vec3 position, vec3 normal) { vec3 localPos = (position - probeVolume.origin) / probeVolume.spacing; ivec3 lower = ivec3 (floor (localPos)); ivec3 upper = lower + ivec3 (1 ); vec3 t = fract (localPos); vec3 result = vec3 (0.0 ); float totalWeight = 0.0 ; for (int i = 0 ; i < 8 ; i++) { ivec3 offset = ivec3 (i & 1 , (i >> 1 ) & 1 , (i >> 2 ) & 1 ); ivec3 probeCoord = lower + offset; vec3 probePos = probeVolume.origin + probeCoord * probeVolume.spacing; vec3 toProbe = probePos - position; float dist = length (toProbe); vec3 dir = toProbe / dist; float probeDepth = sampleDepthAtlas (probeCoord, -dir); float depthWeight = (dist < probeDepth + threshold) ? 1.0 : 0.0 ; float normalWeight = max (0.0 , dot (normal, dir)); vec3 blendWeight = mix (1.0 - t, t, offset); float trilinearWeight = blendWeight.x * blendWeight.y * blendWeight.z; float weight = trilinearWeight * depthWeight * normalWeight; vec3 irradiance = sampleIrradianceAtlas (probeCoord, normal); result += irradiance * weight; totalWeight += weight; } return result / max (totalWeight, 0.0001 ); }
4.5.2 双边滤波
为了进一步减少噪点,可以使用空间滤波:
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 vec3 bilateralFilter (ivec2 centerCoord, vec3 centerNormal, float centerDepth) { vec3 result = vec3 (0.0 ); float totalWeight = 0.0 ; for (int y = -FILTER_RADIUS; y <= FILTER_RADIUS; y++) { for (int x = -FILTER_RADIUS; x <= FILTER_RADIUS; x++) { ivec2 coord = centerCoord + ivec2 (x, y); vec3 sampleIrradiance = texelFetch (irradianceTex, coord, 0 ).rgb; vec3 sampleNormal = texelFetch (normalTex, coord, 0 ).rgb; float sampleDepth = texelFetch (depthTex, coord, 0 ).r; float normalWeight = pow (max (0.0 , dot (centerNormal, sampleNormal)), normalSigma); float depthWeight = exp (-abs (centerDepth - sampleDepth) / depthSigma); float spatialWeight = exp (-(x*x + y*y) / spatialSigma); float weight = normalWeight * depthWeight * spatialWeight; result += sampleIrradiance * weight; totalWeight += weight; } } return result / totalWeight; }
4.6 DDGI实现细节
4.6.1 Probe Grid配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct DDGIVolume { vec3 origin; vec3 extents; ivec3 probeCounts; float probeRadius; ivec2 atlasResolution; int probesPerRow; int raysPerProbe; float historyBlendWeight; };
4.6.2 光线方向生成
使用低差异序列生成均匀分布的光线方向:
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 vec3 generateRayDirection (int rayIndex, int totalRays) { float phi = (1.0 + sqrt (5.0 )) * 0.5 ; float theta = 2.0 * PI * rayIndex / phi; float z = 1.0 - 2.0 * (rayIndex + 0.5 ) / totalRays; float radius = sqrt (1.0 - z * z); return vec3 ( radius * cos (theta), radius * sin (theta), z ); } vec3 generateJitteredRayDirection (int rayIndex, int totalRays, vec2 randomSeed) { vec3 baseDir = generateRayDirection (rayIndex, totalRays); vec3 jitter = vec3 ( rand (randomSeed) - 0.5 , rand (randomSeed + vec2 (1.0 )) - 0.5 , rand (randomSeed + vec2 (2.0 )) - 0.5 ); return normalize (baseDir + jitter * 0.1 ); }
4.6.3 探针分类
为了优化性能,可以对探针进行分类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum ProbeState { INACTIVE, ACTIVE, DIRTY }; void classifyProbes () { for each probe in probeVolume: vec3 probePos = getProbePosition (probe); if (isInsideGeometry (probePos)) { probe.state = INACTIVE; } else { probe.state = ACTIVE; } }
4.7 DDGI的优化技术
4.7.1 时间分片更新
不需要每帧更新所有探针:
1 2 3 4 5 6 7 8 9 10 void updateProbesTemporal (int frameIndex) { int probesPerFrame = totalProbes / UPDATE_SPAN; int startProbe = (frameIndex % UPDATE_SPAN) * probesPerFrame; for (int i = 0 ; i < probesPerFrame; i++) { int probeIndex = startProbe + i; updateProbeRadiance (probeIndex); } }
4.7.2 自适应采样
根据光照变化动态调整采样密度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void adaptiveUpdate () { for each probe: vec3 currentIrradiance = computeCurrentIrradiance (); vec3 previousIrradiance = probe.history; float change = length (currentIrradiance - previousIrradiance); probe.priority = change; sortProbesByPriority (); for (int i = 0 ; i < UPDATE_BUDGET; i++) { updateProbeRadiance (sortedProbes[i]); } }
4.7.3 多分辨率探针
在大型场景中使用多级探针网格:
1 2 3 Level 0 (高分辨率): 小物体、室内场景 Level 1 (中分辨率): 中等区域 Level 2 (低分辨率): 远距离、室外大场景
1 2 3 4 5 6 7 8 9 10 11 vec3 sampleMultiResolutionProbes (vec3 position, vec3 normal) { int level = selectProbeLevel (position); vec3 result0 = sampleProbeLevel (position, normal, level); vec3 result1 = sampleProbeLevel (position, normal, level + 1 ); float blendFactor = computeBlendFactor (position, level); return mix (result0, result1, blendFactor); }
4.8 DDGI与UE5 Lumen
4.8.1 Lumen概述
Lumen是虚幻引擎5的全动态全局光照系统,其核心组件包括:
软件光线追踪: 使用距离场和网格距离场
硬件光线追踪: 在支持的GPU上使用
辐射率缓存: 类似DDGI的探针系统
Lumen架构
4.8.2 Lumen中的辐射率缓存
Lumen使用Screen Space Radiance Cache和World Space Radiance
Cache的组合:
Screen Space Radiance Cache:
在屏幕空间缓存辐射率
高分辨率,更新频繁
受屏幕空间限制
World Space Radiance Cache:
世界空间探针网格
补充屏幕外信息
使用类似DDGI的更新机制
4.8.3 Lumen与DDGI的对比
特性
DDGI
Lumen
更新方式
固定光线数
自适应
存储方式
Atlas
纹理 + 体素
漫反射GI
支持
支持
镜面反射
有限支持
支持
硬件要求
RTX或软件光追
多种后端
4.9 DDGI的局限与改进方向
4.9.1 当前局限
漏光问题: 虽然深度可以缓解,但无法完全消除
分辨率限制: 探针密度有限,难以捕捉高频细节
动态物体: 快速移动物体的GI更新有延迟
镜面反射: 主要针对漫反射,镜面反射效果有限
4.9.2 改进方向
自适应探针放置:
根据场景复杂度动态调整探针密度
神经网络加速: 使用神经网络预测光照分布
混合方法: 结合屏幕空间方法和探针方法
4.10 小结
本章详细介绍了DDGI技术:
Probe原理: 存储空间点的球面光照分布
八面体映射: 高效的球面-平面映射方法
光线追踪更新: 实时更新探针的光照信息
深度防漏光: 使用深度信息解决墙壁漏光问题
插值与滤波: 从探针获取任意位置的光照
DDGI是连接传统GI方法和神经渲染技术的桥梁,下一章将介绍如何使用神经网络进一步改进光照缓存。
第五章:Neural Radiance
Caching(NRC)详解
5.1 NRC概述
5.1.1 什么是Neural Radiance
Caching?
Neural Radiance
Caching(NRC,神经辐射缓存)是一种使用神经网络来近似场景中辐射率分布的技术。它最初由Müller等人在2021年提出,旨在加速路径追踪渲染。
NRC概念图
5.1.2 核心思想
传统路径追踪中,每条路径都需要递归计算多次反弹的光照,计算量巨大。NRC的核心思想是:
用神经网络学习场景的辐射率分布
缓存中间结果,避免重复计算
通过查询网络获取间接光照估计
5.1.3 NRC与传统Radiance
Cache的区别
特性
传统Radiance Cache
Neural Radiance Cache
存储方式
离散点/探针
神经网络权重
插值方式
线性/三次插值
神经网络推断
内存占用
与分辨率成正比
固定(网络大小)
泛化能力
局限于采样点
可泛化到未采样区域
更新成本
需要更新大量缓存
增量学习
5.2 神经网络基础
5.2.1 多层感知机(MLP)
NRC使用简单的多层感知机作为核心网络结构:
1 2 3 输入层 → 隐藏层1 → 隐藏层2 → 隐藏层3 → 输出层 位置编码 + 方向编码 → MLP → 辐射率输出
推荐配置:
隐藏层数:3层
每层宽度:64个神经元
激活函数:Leaky ReLU
5.2.2 激活函数
Leaky ReLU:
\[f(x) = \begin{cases} x & \text{if }
x > 0 \alpha x & \text{if } x \leq 0 \end{cases}\]
其中 \(\alpha\) 通常取0.01。
优点:
解决ReLU的"死亡神经元"问题
计算简单高效
保持稀疏激活特性
1 2 def leaky_relu (x, alpha=0.01 ): return np.where(x > 0 , x, alpha * x)
5.2.3 位置编码(Positional
Encoding)
直接使用坐标作为输入会导致网络难以学习高频信息。位置编码将输入映射到高维空间:
\[\gamma(p) = [\sin(2^0 \pi p), \cos(2^0
\pi p), \sin(2^1 \pi p), \cos(2^1 \pi p), \ldots]\]
原理:
5.3 Hash Grid Encoding
5.3.1 为什么需要Hash Grid?
传统位置编码有两个问题:
固定分辨率: 无法自适应场景复杂度
内存开销大: 高分辨率需要大量存储
Hash Grid Encoding(哈希网格编码)解决了这些问题:
可变分辨率,适应场景复杂度
内存占用固定
支持无界场景
5.3.2 Hash Grid原理
核心思想: 使用空间哈希将3D位置映射到特征向量。
1 位置(x, y, z) → 多分辨率体素网格 → 哈希查找 → 特征插值 → 输出特征向量
算法步骤:
多分辨率网格: 创建 \(L\) 个不同分辨率的网格
\[N_l = \lfloor N_{min} \cdot b^l \rfloor,
\quad l = 0, 1, \ldots, L-1\]
其中 \(b = \exp\left(\frac{\ln N_{max} -
\ln N_{min}}{L-1}\right)\)
哈希函数: 将体素角点映射到特征表
\[h(x, y, z) = ((x \oplus y \oplus z) \mod
T)\]
其中 \(T\) 是特征表大小,\(\oplus\) 是按位异或。
特征插值: 在体素内三线性插值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 vec3 scaledPos = pos * resolutions[l]; ivec3 voxelCoord = ivec3 (floor (scaledPos)); vec3 frac = fract (scaledPos); vec4 features[8 ]; for (int i = 0 ; i < 8 ; i++) { ivec3 offset = ivec3 (i & 1 , (i >> 1 ) & 1 , (i >> 2 ) & 1 ); ivec3 corner = voxelCoord + offset; uint hash = hashFunction (corner); features[i] = featureTable[hash % TABLE_SIZE]; } vec4 result = trilinearInterpolate (features, frac);
5.3.3 Unbounded Hash-Grid
Encoding
对于开放世界场景,使用无界哈希网格编码:
关键改进:
对数空间映射: 将无限空间压缩到有限范围
\[\text{scaled}(x) = \text{sign}(x) \cdot
\frac{\log(1 + |x|)}{\log(1 + R)}\]
其中 \(R\) 是参考距离。
分辨率层级: 推荐配置
参数
推荐值
说明
Level (L)
6
分辨率层数
Feature Dimension (F)
2
每个特征向量的维度
Table Size (T)
\(2^{19}\)
哈希表大小
5.4 NRC的网络架构
5.4.1 输入编码
NRC对不同的输入使用不同的编码策略:
位置编码(Position Encoding):
推荐使用Hash Grid Encoding:
Levels: 6
Feature Dimension: 2
Base Resolution: 16
Finest Resolution: 2048
视角方向编码(View Direction Encoding):
推荐使用恒等编码(Identity Encoding):
\[\gamma_{dir}(\omega) =
\omega\]
直接使用方向向量 \((\theta, \phi)\)
或 \((x, y, z)\) 作为输入。
法线编码(Normal Encoding):
同样推荐恒等编码:
\[\gamma_n(n) = n\]
5.4.2 完整网络结构
1 2 3 4 5 6 7 8 9 10 11 12 输入层: ├── 位置 (x, y, z) → Hash Grid Encoding → 12维特征 (6 levels × 2 dim) ├── 方向 (θ, φ) → 恒等编码 → 2维 └── 法线 (nx, ny, nz) → 恒等编码 → 3维 合并: [17维特征向量] MLP: ├── 隐藏层1: 17 → 64, Leaky ReLU ├── 隐藏层2: 64 → 64, Leaky ReLU ├── 隐藏层3: 64 → 64, Leaky ReLU └── 输出层: 64 → 3 (RGB辐射率)
NRC架构图
5.4.3 网络参数量估算
1 2 3 4 5 6 7 8 9 10 11 输入编码参数: - Hash Table: 2^19 × 2 × 4 bytes = 4 MB MLP参数: - Layer 1: 17 × 64 + 64 = 1152 - Layer 2: 64 × 64 + 64 = 4160 - Layer 3: 64 × 64 + 64 = 4160 - Output: 64 × 3 + 3 = 195 - Total: ~10K parameters 总参数量: ~4 MB (主要是Hash Table)
5.5 NRC的训练
5.5.1 在线训练
NRC在渲染过程中实时训练,而非预训练:
1 2 3 4 5 6 每帧: 1. 从屏幕空间采样6%的像素作为训练射线 2. 对每条训练射线执行路径追踪 3. 收集路径上的采样点作为训练数据 4. 更新神经网络参数 5. 对其余像素使用网络查询替代部分路径追踪
5.5.2 训练数据收集
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 struct TrainingSample { vec3 position; vec3 direction; vec3 normal; vec3 radiance; float weight; }; void collectTrainingSamples (Ray ray, std::vector<TrainingSample>& samples) { vec3 throughput = vec3 (1.0 ); for (int bounce = 0 ; bounce < MAX_BOUNCES; bounce++) { RayHit hit = traceRay (ray); if (!hit.valid) break ; TrainingSample sample; sample.position = hit.position; sample.direction = -ray.direction; sample.normal = hit.normal; vec3 directLight = computeDirectLight (hit); vec3 indirectEstimate = queryNetwork (hit.position, randomDirection, hit.normal); sample.radiance = directLight + indirectEstimate; sample.weight = throughput.luminance (); samples.push_back (sample); vec3 newDir = sampleBRDF (hit); throughput *= evaluateBRDF (hit, newDir) * dot (newDir, hit.normal); if (bounce > 3 ) { float p = min (throughput.luminance (), 0.95 ); if (random () > p) break ; throughput /= p; } ray = Ray (hit.position, newDir); } }
5.5.3 损失函数
NRC使用相对L2损失函数:
\[\mathcal{L} = \sum_i w_i \left(
\frac{L_{pred,i} - L_{target,i}}{L_{target,i} + \epsilon}
\right)^2\]
损失函数
相对损失的原因:
HDR范围: 辐射率值跨越多个数量级
重要性平衡: 避免高亮度区域主导损失
数值稳定: 添加 \(\epsilon\) 防止除零
1 2 3 4 5 6 7 8 def relative_l2_loss (pred, target, epsilon=0.01 ): """ 相对L2损失函数 pred: 预测的辐射率 (N, 3) target: 目标辐射率 (N, 3) """ relative_error = (pred - target) / (target + epsilon) return torch.mean(relative_error ** 2 )
5.5.4 优化器
推荐使用Adam优化器,参数配置:
参数
值
说明
Learning Rate
\(10^{-3}\)
初始学习率
\(\beta_1\)
0.9
一阶矩估计衰减率
\(\beta_2\)
0.99
二阶矩估计衰减率
\(\epsilon\)
\(10^{-15}\)
数值稳定性常数
1 2 3 4 optimizer = torch.optim.Adam([ {'params' : hash_grid.parameters(), 'lr' : 1e-3 }, {'params' : mlp.parameters(), 'lr' : 1e-3 } ], betas=(0.9 , 0.99 ), eps=1e-15 )
5.6 NRC渲染流程
5.6.1 完整渲染管线
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ┌─────────────────────────────────────────────────────────┐ │ NRC渲染管线 │ ├─────────────────────────────────────────────────────────┤ │ 1. G-Buffer生成 │ │ - 位置、法线、材质信息 │ ├─────────────────────────────────────────────────────────┤ │ 2. 训练射线采样 (屏幕空间的6%) │ │ - 执行完整路径追踪 │ │ - 收集训练样本 │ │ - 更新神经网络 │ ├─────────────────────────────────────────────────────────┤ │ 3. 直接光照计算 │ │ - 使用传统方法计算直接光照 │ ├─────────────────────────────────────────────────────────┤ │ 4. 间接光照查询 │ │ - 使用NRC网络查询间接光照 │ │ - 对剩余94%像素 │ ├─────────────────────────────────────────────────────────┤ │ 5. 合成最终图像 │ │ - 直接光照 + 间接光照 │ └─────────────────────────────────────────────────────────┘
5.6.2 推断代码示例
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 vec3 queryNRC (vec3 position, vec3 direction, vec3 normal) { vec3 posEncoded = hashGridEncode (position); vec3 dirEncoded = direction; vec3 normalEncoded = normal; float features[17 ]; float hidden1[64 ], hidden2[64 ], hidden3[64 ]; float output[3 ]; for (int i = 0 ; i < 64 ; i++) { hidden1[i] = 0.0 ; for (int j = 0 ; j < 17 ; j++) { hidden1[i] += features[j] * weights1[j][i]; } hidden1[i] = leakyReLU (hidden1[i] + bias1[i]); } for (int i = 0 ; i < 3 ; i++) { output[i] = hidden3[i]; } return vec3 (output[0 ], output[1 ], output[2 ]); }
5.7 NRC的高级技术
5.7.1 处理HDR辐射率
问题: MLP输出范围有限,难以直接表示HDR值。
解决方案:
对数空间训练:
\[L_{train} = \log(1 + L)\]
1 2 3 4 5 def encode_hdr (radiance ): return torch.log(1.0 + radiance) def decode_hdr (encoded ): return torch.exp(encoded) - 1.0
色调映射:
\[L_{mapped} = \frac{L}{1 +
L}\]
1 2 3 4 5 def tonemap (radiance ): return radiance / (1.0 + radiance) def inverse_tonemap (mapped ): return mapped / (1.0 - mapped + 1e-6 )
5.7.2 时间一致性
使用时间累积提高稳定性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void accumulateParameters (float * currentParams, float * historyParams, float alpha) { for (int i = 0 ; i < PARAM_COUNT; i++) { historyParams[i] = alpha * historyParams[i] + (1 - alpha) * currentParams[i]; } } vec3 queryStable (vec3 pos, vec3 dir, vec3 normal) { vec3 current = queryNRC (pos, dir, normal, currentParams); vec3 history = queryNRC (pos, dir, normal, historyParams); float diff = length (current - history); if (diff > threshold) { return current; } return mix (current, history, 0.5 ); }
5.7.3 多分辨率采样
使用重要性采样优化训练:
1 2 3 4 5 6 7 8 9 10 11 int getSampleCount (vec2 pixel) { float error = estimateError (pixel); if (error > HIGH_ERROR_THRESHOLD) { return 4 ; } else if (error > MEDIUM_ERROR_THRESHOLD) { return 2 ; } return 1 ; }
5.8 NRC与DDGI的结合
5.8.1 动机
NRC最初为路径追踪设计,但其思想可以应用到DDGI:
用NRC替代DDGI的部分光线追踪:
减少每个探针需要的光线数
用NRC提供更好的初始猜测: 加速收敛
用NRC泛化到未采样区域: 提高空间覆盖率
5.8.2 结合方案
1 2 3 4 5 传统DDGI: 探针 → 光线追踪 → 辐照度 NRC增强DDGI: 探针 → 少量光线追踪 + NRC查询 → 辐照度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vec3 updateProbeWithNRC (Probe probe, int reducedRayCount) { vec3 irradiance = vec3 (0.0 ); for (int i = 0 ; i < reducedRayCount; i++) { vec3 dir = getRayDirection (i); vec3 traced = traceRayForIrradiance (probe.position, dir); vec3 nrcEstimate = queryNRC (probe.position, dir, dir); irradiance += mix (traced, nrcEstimate, NRC_WEIGHT); } for (int i = reducedRayCount; i < FULL_RAY_COUNT; i++) { vec3 dir = getRayDirection (i); irradiance += queryNRC (probe.position, dir, dir); } return irradiance / FULL_RAY_COUNT; }
5.9 NRC的局限与展望
5.9.1 当前局限
训练开销: 在线训练需要计算资源
泛化边界: 对训练数据中未出现的场景泛化有限
高频细节: 神经网络对高频光照细节的表达有限
动态场景: 场景剧烈变化时需要重新适应
5.9.2 未来方向
更好的编码: 探索更高效的位置编码方法
混合架构: 结合显式存储和神经网络
预训练模型: 利用大规模数据集预训练
多任务学习: 同时学习多个相关任务
5.10 小结
本章详细介绍了Neural Radiance Caching技术:
基本原理: 使用神经网络近似场景的辐射率分布
网络架构: Hash Grid Encoding + 简单MLP
训练方法: 在线训练,相对L2损失
HDR处理: 对数空间或色调映射
与DDGI结合: 可以减少光线追踪开销
下一章将介绍Neural Irradiance
Volume,这是一种直接学习探针辐照度的技术。
第六章:Neural Irradiance
Volume(NIV)
6.1 NIV概述
6.1.1 什么是Neural Irradiance
Volume?
Neural Irradiance
Volume(NIV,神经辐照度体)是一种直接学习场景中辐照度分布的神经渲染技术。与NRC不同,NIV直接学习每个探针位置的辐照度,而非辐射率。
NIV概念图
6.1.2 NIV与NRC的区别
特性
NRC
NIV
学习目标
辐射率(Radiance)
辐照度(Irradiance)
输入
位置 + 方向 + 法线
位置 + 法线
输出
方向辐射率
半球积分辐照度
计算量
每方向需查询
一次查询即得结果
适用场景
路径追踪加速
探针辐照度学习
6.1.3 为什么选择学习辐照度?
辐照度的优势:
维度更低:
辐照度只依赖于位置和法线,无需方向参数
查询更简单: 一次查询替代半球积分
存储更紧凑: 只需存储标量场而非方向场
数学基础:
\[E(p, n) = \int_{\Omega} L_i(p, \omega)
(\omega \cdot n) d\omega\]
对于漫反射材质,辐照度完全决定了出射辐射率:
\[L_o = \frac{\rho}{\pi} E\]
6.2 NIV的数学模型
6.2.1 辐照度作为函数
将辐照度视为场景的函数:
\[E: \mathbb{R}^3 \times S^2 \rightarrow
\mathbb{R}^3\]
\[E(p, n) = \text{位置 } p \text{ 处法线为
} n \text{ 的辐照度}\]
关键性质:
空间连续性: 辐照度在空间中通常是连续的
法线依赖: 相同位置不同法线会有不同辐照度
局部相似性: 相邻位置的辐照度相似
6.2.2 球面调和函数表示
使用球面调和函数(SH)压缩辐照度的方向依赖:
\[E(p, n) \approx \sum_{l=0}^{L}
\sum_{m=-l}^{l} c_l^m(p) Y_l^m(n)\]
其中 \(c_l^m(p)\)
是位置相关的SH系数。
对于Lambertian辐照度:
只用前3阶SH就能很好地近似(共9个系数):
1 2 3 4 5 struct SHCoefficients { vec3 L0; vec3 L1[3 ]; vec2 L2[5 ]; };
6.3 NIV的网络架构
6.3.1 输入编码
位置编码:
推荐使用Hash Grid Encoding,与NRC相同:
Levels: 6
Feature Dimension: 2
Base Resolution: 16
Finest Resolution: 2048
法线编码:
两种选择:
恒等编码: 直接使用法线向量 \[\gamma(n) = n = (n_x, n_y, n_z)\]
SH基函数: 预计算法线的SH基值 \[\gamma(n) = [Y_0^0(n), Y_1^{-1}(n), Y_1^0(n),
Y_1^1(n), \ldots]\]
6.3.2 完整网络结构
方案A:直接预测辐照度
1 2 3 4 5 输入: position (Hash Grid) + normal (恒等编码) ↓ MLP (3层,每层64神经元) ↓ 输出: irradiance (RGB)
方案B:预测SH系数
1 2 3 4 5 6 7 8 9 输入: position (Hash Grid) ↓ MLP (3层,每层64神经元) ↓ 输出: SH coefficients (9个RGB系数 = 27维) ↓ 与法线SH基点乘 ↓ 输出: irradiance (RGB)
NIV架构
6.3.3 网络代码示例
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 import torchimport torch.nn as nnclass NeuralIrradianceVolume (nn.Module): def __init__ (self, num_levels=6 , feature_dim=2 , hidden_dim=64 ): super ().__init__() self .hash_grid = HashGridEncoding( num_levels=num_levels, feature_dim=feature_dim, log2_hashmap_size=19 , base_resolution=16 , finest_resolution=2048 ) input_dim = num_levels * feature_dim + 3 self .mlp = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, 3 ) ) def forward (self, position, normal ): pos_encoded = self .hash_grid(position) features = torch.cat([pos_encoded, normal], dim=-1 ) irradiance = self .mlp(features) return irradiance
6.4 NIV的训练
6.4.1 训练数据生成
NIV需要预先烘焙或实时收集辐照度数据:
离线烘焙方法:
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 struct IrradianceSample { vec3 position; vec3 normal; vec3 irradiance; }; std::vector<IrradianceSample> bakeIrradianceSamples (Scene& scene) { std::vector<IrradianceSample> samples; for (int i = 0 ; i < NUM_POSITIONS; i++) { vec3 pos = sampleRandomPosition (scene.boundingBox); if (!isValidPosition (pos)) continue ; for (int j = 0 ; j < NUM_NORMALS; j++) { vec3 normal = sampleHemisphereDirection (j); vec3 irradiance = computeIrradiance (pos, normal); samples.push_back ({pos, normal, irradiance}); } } return samples; }
实时收集方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void collectFromDDGIProbes (DDGIVolume& ddgi, std::vector<IrradianceSample>& samples) { for (int i = 0 ; i < ddgi.probeCount; i++) { Probe& probe = ddgi.probes[i]; vec3 probePos = probe.position; for (int n = 0 ; n < NUM_NORMALS; n++) { vec3 normal = getSampleNormal (n); vec3 irradiance = computeIrradianceFromAtlas (probe, normal); samples.push_back ({probePos, normal, irradiance}); } } }
6.4.2 损失函数
NIV使用相对L2损失,与NRC类似:
\[\mathcal{L} = \frac{1}{N} \sum_{i=1}^{N}
\left \frac{E_{pred,i} - E_{target,i}}{E_{target,i} + \epsilon}
\right^2\]
损失函数示意
1 2 3 4 5 6 def irradiance_loss (pred, target, epsilon=0.01 ): """ NIV的损失函数 """ relative_error = (pred - target) / (target + epsilon) return torch.mean(relative_error ** 2 )
6.4.3 训练流程
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 def train_niv (model, train_loader, num_epochs=1000 ): optimizer = torch.optim.Adam(model.parameters(), lr=1e-3 ) for epoch in range (num_epochs): total_loss = 0 for batch in train_loader: position = batch['position' ] normal = batch['normal' ] target_irradiance = batch['irradiance' ] pred_irradiance = model(position, normal) loss = irradiance_loss(pred_irradiance, target_irradiance) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() if epoch % 100 == 0 : print (f"Epoch {epoch} , Loss: {total_loss / len (train_loader)} " )
6.5 NIV的应用场景
6.5.1 初始化DDGI探针
使用NIV为DDGI探针提供初始辐照度猜测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void initializeDDGIWithNIV (DDGIVolume& ddgi, NeuralIrradianceVolume& niv) { for (int i = 0 ; i < ddgi.probeCount; i++) { vec3 probePos = ddgi.probes[i].position; for (int dir = 0 ; dir < NUM_DIRECTIONS; dir++) { vec3 normal = getDirectionNormal (dir); vec3 predictedIrradiance = niv.query (probePos, normal); ddgi.probes[i].setIrradiance (dir, predictedIrradiance); } } }
6.5.2 加速辐照度收敛
在动态场景中,NIV可以加速DDGI的收敛:
1 2 3 4 5 6 7 8 9 10 11 12 vec3 updateProbeWithNIVPrior (Probe& probe, NeuralIrradianceVolume& niv) { vec3 newIrradiance = traceRaysForIrradiance (probe); vec3 nivPrior = niv.query (probe.position, probe.normal); float confidence = computeConfidence (probe.rayCount); vec3 result = mix (nivPrior, newIrradiance, confidence); return result; }
6.5.3 补充未采样区域
对于探针稀疏的区域,NIV可以提供补充:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vec3 sampleIrradianceHybrid (vec3 position, vec3 normal, DDGIVolume& ddgi, NeuralIrradianceVolume& niv) { vec3 ddgiIrradiance = ddgi.interpolate (position, normal); float ddgiConfidence = ddgi.computeInterpolationConfidence (position); vec3 nivIrradiance = niv.query (position, normal); if (ddgiConfidence < LOW_CONFIDENCE_THRESHOLD) { return mix (ddgiIrradiance, nivIrradiance, 0.7 ); } else { return mix (ddgiIrradiance, nivIrradiance, 0.1 ); } }
6.6 NIV的高级技术
6.6.1 泛化能力提升
问题: NIV在训练数据未覆盖的区域泛化有限。
解决方案:
数据增强:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def augment_training_data (samples ): augmented = [] for sample in samples: augmented.append(sample) for _ in range (3 ): noisy_pos = sample.position + random_noise() * 0.1 noisy_normal = normalize(sample.normal + random_noise() * 0.05 ) augmented.append({ 'position' : noisy_pos, 'normal' : noisy_normal, 'irradiance' : sample.irradiance }) return augmented
正则化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def smoothness_regularization (model, positions ): """ 鼓励网络在空间中平滑 """ loss = 0 for pos in positions: pos.requires_grad = True normal = torch.tensor([0 , 1 , 0 ], dtype=torch.float32) output = model(pos, normal) grad = torch.autograd.grad(output.sum (), pos, create_graph=True )[0 ] loss += torch.mean(grad ** 2 ) return loss
6.6.2 实时学习
支持动态场景的实时学习:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void realTimeLearning (NeuralIrradianceVolume& niv, FrameData& frame) { std::vector<IrradianceSample> newSamples; for (auto & pixel : frame.visiblePixels) { vec3 pos = pixel.worldPosition; vec3 normal = pixel.normal; vec3 irradiance = computeGroundTruthIrradiance (pixel); newSamples.push_back ({pos, normal, irradiance}); } niv.onlineUpdate (newSamples, learningRate=0.001 , numSteps=10 ); }
6.6.3 多尺度表示
使用多分辨率Hash Grid捕捉不同尺度的细节:
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 class MultiScaleNIV (nn.Module): def __init__ (self ): super ().__init__() self .coarse_grid = HashGridEncoding( num_levels=4 , base_resolution=8 , finest_resolution=128 ) self .fine_grid = HashGridEncoding( num_levels=4 , base_resolution=128 , finest_resolution=2048 ) self .coarse_mlp = MLP(input_dim=8 , hidden_dim=32 , output_dim=3 ) self .fine_mlp = MLP(input_dim=8 , hidden_dim=32 , output_dim=3 ) def forward (self, position, normal ): coarse_features = self .coarse_grid(position) coarse_irradiance = self .coarse_mlp( torch.cat([coarse_features, normal], dim=-1 ) ) fine_features = self .fine_grid(position) fine_detail = self .fine_mlp( torch.cat([fine_features, normal], dim=-1 ) ) return coarse_irradiance + fine_detail
6.7 NIV与预烘焙探针的结合
6.7.1 从烘焙探针初始化NIV
如果游戏已有烘焙的Light Probe,可以用于初始化NIV:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void initializeNIVFromBakedProbes (NeuralIrradianceVolume& niv, LightProbeGrid& bakedProbes) { std::vector<IrradianceSample> trainingData; for (auto & probe : bakedProbes.probes) { for (int n = 0 ; n < NUM_NORMALS; n++) { vec3 normal = getSampleNormal (n); vec3 irradiance = evaluateSH (probe.shCoefficients, normal); trainingData.push_back ({ probe.position, normal, irradiance }); } } niv.train (trainingData, numEpochs=10000 ); }
6.7.2 NIV泛化到未烘焙区域
1 2 3 4 5 6 7 8 9 10 11 12 vec3 queryLighting (vec3 position, vec3 normal, LightProbeGrid& baked, NeuralIrradianceVolume& niv) { bool hasBakedData = baked.isValidPosition (position); if (hasBakedData) { return baked.interpolate (position, normal); } else { return niv.query (position, normal); } }
6.8 NIV的局限与解决方案
6.8.1 光环境变化
问题:
NIV学习的辐照度对应特定光环境,光环境变化时需要更新。
解决方案:
条件输入: 将光环境参数作为网络输入
1 2 3 4 5 6 7 8 9 class ConditionalNIV (nn.Module): def forward (self, position, normal, light_params ): features = torch.cat([ self .encode_position(position), normal, light_params ], dim=-1 ) return self .mlp(features)
增量学习: 光环境变化时增量更新
1 2 3 4 5 6 7 void adaptToLightChange (NeuralIrradianceVolume& niv, LightChange& change) { std::vector<IrradianceSample> newSamples = recomputeAffectedSamples (change); niv.incrementalUpdate (newSamples); }
6.8.2 高频细节缺失
问题:
神经网络倾向于平滑,可能丢失高频光照细节。
解决方案:
残差学习: 学习相对于低频基的残差
1 2 3 4 5 6 7 8 9 class ResidualNIV (nn.Module): def forward (self, position, normal ): base_irradiance = self .computeLowFrequencyBase(position, normal) residual = self .mlp(self .encode(position), normal) return base_irradiance + residual
细节增强: 使用单独的细节网络
6.9 NIV实践建议
6.9.1 参数配置推荐
参数
推荐值
说明
Hash Grid Levels
6
平衡精度和内存
Feature Dimension
2
每级特征维度
MLP隐藏层数
3
足够的表达能力
MLP隐藏层宽度
64
平衡性能和质量
训练样本数
100K-1M
根据场景复杂度
学习率
\(10^{-3}\)
初始学习率
ε(损失函数)
0.01
相对损失的稳定项
6.9.2 性能优化
量化: 使用FP16或INT8推理
剪枝: 移除不重要的网络连接
蒸馏: 训练更小的学生网络
6.10 小结
本章详细介绍了Neural Irradiance Volume技术:
基本原理: 直接学习场景的辐照度分布
网络架构: Hash Grid Encoding + MLP
与NRC的区别: 辐照度vs辐射率,更低维度
应用场景:
初始化DDGI探针、加速收敛、补充未采样区域
局限性: 光环境变化适应、高频细节表达
下一章将探讨神经渲染的前沿方向,包括用神经网络替代传统光线追踪组件的探索。
第七章:神经渲染前沿方向
7.1 概述
前面章节介绍的NRC和NIV主要用于加速光照计算,但仍依赖传统的光线追踪架构(BVH、射线-三角形相交测试等)。本章探讨更激进的方向:使用神经网络替代光线追踪的核心组件。
神经渲染前沿
7.2 传统光线追踪的性能瓶颈
7.2.1 加速结构回顾
现代光线追踪使用层次化加速结构:
Bottom-Level Acceleration Structure (BLAS):
存储单个物体的几何信息
通常使用BVH(Bounding Volume Hierarchy)
每个叶子节点包含三角形
Top-Level Acceleration Structure (TLAS):
组织场景中的所有BLAS实例
支持动态物体的快速更新
7.2.2 性能分析
光线追踪的计算开销分解:
阶段
开销占比
说明
BVH遍历
30-40%
树结构遍历,访存密集
射线-三角形相交
20-30%
数值计算密集
着色计算
30-50%
材质、光照计算
BLAS的问题:
构建开销: 复杂模型需要大量时间构建BVH
内存占用: 高精度模型需要存储大量三角形
更新代价: 动态变形物体需要重新构建或更新
BVH结构
7.3 神经网络替代方案
7.3.1 核心思想
能否用神经网络学习场景的几何和外观,替代传统的:
BVH加速结构
射线-几何体相交测试
材质和光照计算
理想目标:
1 2 3 4 5 传统管线: 光线 → BVH遍历 → 相交测试 → 着色 → 颜色 神经渲染管线: 光线 → 神经网络查询 → 颜色
7.3.2 可行性分析
神经网络的优势:
紧凑表示:用网络权重替代大量几何数据
连续表示:可以表示任意精度的几何
隐式编码:自然处理复杂拓扑
挑战:
7.4 NeRF(Neural Radiance
Fields)
7.4.1 NeRF基本原理
NeRF用多层感知机表示场景的辐射率场:
\[F_\theta: (x, y, z, \theta, \phi)
\rightarrow (r, g, b, \sigma)\]
其中:
\((x, y, z)\) 是3D位置
\((\theta, \phi)\) 是视角方向
\((r, g, b)\) 是颜色
\(\sigma\)
是体密度(不透明度)
7.4.2 体渲染积分
NeRF使用体渲染方程从辐射率场合成图像:
\[C(\mathbf{r}) = \int_{t_n}^{t_f} T(t)
\sigma(\mathbf{r}(t)) \mathbf{c}(\mathbf{r}(t), \mathbf{d})
dt\]
其中: \[T(t) = \exp\left(-\int_{t_n}^{t}
\sigma(\mathbf{r}(s)) ds\right)\]
是累积透射率。
离散化近似:
\[\hat{C}(\mathbf{r}) = \sum_{i=1}^{N} T_i
\alpha_i \mathbf{c}_i\]
其中: \[\alpha_i = 1 - \exp(-\sigma_i
\delta_i)\]
7.4.3 NeRF渲染管线
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 def render_ray (nerf_model, ray_origin, ray_direction, near, far, num_samples ): """ NeRF单条光线渲染 """ t_values = torch.linspace(near, far, num_samples) points = ray_origin + ray_direction.unsqueeze(-1 ) * t_values rgbs = [] densities = [] for point in points: rgb, density = nerf_model(point, ray_direction) rgbs.append(rgb) densities.append(density) rgbs = torch.stack(rgbs) densities = torch.stack(densities) deltas = t_values[1 :] - t_values[:-1 ] alphas = 1 - torch.exp(-densities[:-1 ] * deltas) transmittance = torch.cumprod(1 - alphas + 1e-10 , dim=0 ) transmittance = torch.cat([torch.ones(1 ), transmittance[:-1 ]]) weights = transmittance * alphas color = (weights.unsqueeze(-1 ) * rgbs[:-1 ]).sum (dim=0 ) return color
7.4.4 NeRF与光线追踪的对比
特性
传统光线追踪
NeRF
几何表示
显式三角形
隐式网络
相交测试
BVH遍历
体采样
存储需求
与复杂度成正比
固定(网络大小)
渲染速度
快(硬件加速)
慢(多次网络查询)
质量
取决于模型精度
高(视图合成)
适用场景
通用渲染
特定场景重建
7.4.5 NeRF的改进方向
加速技术:
Instant-NGP: 使用Hash Grid
Encoding大幅加速训练和推断
Plenoxels: 用稀疏体素网格替代MLP
KiloNeRF: 使用数千个小MLP替代大网络
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 class InstantNGPNeRF (nn.Module): """ 使用Hash Grid Encoding的快速NeRF """ def __init__ (self ): super ().__init__() self .hash_grid = HashGridEncoding( num_levels=16 , feature_dim=2 , log2_hashmap_size=19 ) self .density_net = nn.Sequential( nn.Linear(32 , 16 ), nn.ReLU(), nn.Linear(16 , 1 ) ) self .color_net = nn.Sequential( nn.Linear(32 + 3 , 16 ), nn.ReLU(), nn.Linear(16 , 3 ), nn.Sigmoid() ) def forward (self, position, view_dir ): features = self .hash_grid(position) density = self .density_net(features) color = self .color_net(torch.cat([features, view_dir], dim=-1 )) return color, density
7.5 3D Gaussian Splatting
7.5.1 基本原理
3D Gaussian
Splatting是另一种神经表示方法,使用显式的3D高斯球集合表示场景:
每个高斯球由以下参数定义:
位置 \(\mu \in \mathbb{R}^3\)
协方差矩阵 \(\Sigma \in
\mathbb{R}^{3\times3}\)
不透明度 \(\alpha \in [0, 1]\)
球谐系数 \(SH\) (用于视图相关的颜色)
7.5.2 高斯球的光栅化
与传统光线追踪不同,3DGS使用可微分光栅化:
1 2 3 4 对于每个像素: 1. 找到投影到该像素的高斯球 2. 按深度排序 3. Alpha混合计算最终颜色
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 def rasterize_gaussians (gaussians, camera, image_size ): """ 高斯球光栅化 """ image = torch.zeros(image_size[0 ], image_size[1 ], 3 ) projected = project_gaussians(gaussians, camera) sorted_indices = torch.argsort(projected.depths, descending=True ) for y in range (image_size[0 ]): for x in range (image_size[1 ]): overlapping = find_overlapping_gaussians( projected, x, y, sorted_indices ) alpha_acc = 0.0 color = torch.zeros(3 ) for idx in overlapping: gaussian = projected[idx] alpha = gaussian.opacity * evaluate_2d_gaussian( gaussian, x, y ) color += (1 - alpha_acc) * alpha * gaussian.color alpha_acc += (1 - alpha_acc) * alpha if alpha_acc > 0.99 : break image[y, x] = color return image
7.5.3 与光线追踪的关系
3DGS可以与光线追踪结合:
混合渲染:
使用3DGS表示场景几何,用光线追踪计算阴影/反射
神经网络查询替代:
高斯球可作为加速结构的一部分
7.6 神经隐式表面表示
7.6.1 有符号距离场(SDF)
SDF是另一种隐式几何表示:
\[SDF(p) = \begin{cases} d(p,
\partial\Omega) & p \notin \Omega -d(p, \partial\Omega) & p \in
\Omega \end{cases}\]
其中 \(\Omega\) 是物体内部,\(\partial\Omega\) 是表面。
使用神经网络学习SDF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class NeuralSDF (nn.Module): def __init__ (self, hidden_dim=256 ): super ().__init__() self .net = nn.Sequential( nn.Linear(3 , hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1 ) ) def forward (self, points ): return self .net(points)
7.6.2 从SDF到光线追踪
球体追踪(Sphere Tracing):
使用SDF进行光线追踪的算法:
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 vec3 sphereTrace (vec3 origin, vec3 direction, NeuralSDF& sdf) { float t = 0.0 ; const float MAX_DIST = 100.0 ; const float EPSILON = 0.001 ; for (int i = 0 ; i < MAX_STEPS; i++) { vec3 p = origin + t * direction; float d = sdf.query (p); if (d < EPSILON) { return p; } if (t > MAX_DIST) { break ; } t += d; } return vec3 (INFINITY); }
7.6.3 神经SDF的优势
紧凑表示: 复杂几何用小网络表示
无限细节: 网络输出连续值,理论上无限分辨率
拓扑灵活: 无需处理网格拓扑
7.7 神经相交测试
7.7.1 学习射线-几何相交
更激进的方案是直接用神经网络学习相交测试:
\[f_{intersect}: (\mathbf{o}, \mathbf{d})
\rightarrow (t, \mathbf{n}, \text{material})\]
其中:
\((\mathbf{o},
\mathbf{d})\) :光线起点和方向
\(t\) :相交距离
\(\mathbf{n}\) :交点法线
\(\text{material}\) :材质ID或属性
7.7.2 实现方案
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 class NeuralIntersection (nn.Module): def __init__ (self ): super ().__init__() self .ray_encoder = nn.Sequential( nn.Linear(6 , 128 ), nn.ReLU(), nn.Linear(128 , 256 ), nn.ReLU() ) self .intersection_head = nn.Linear(256 , 1 ) self .normal_head = nn.Linear(256 , 3 ) self .material_head = nn.Linear(256 , 32 ) def forward (self, origin, direction ): ray_features = self .ray_encoder( torch.cat([origin, direction], dim=-1 ) ) t = self .intersection_head(ray_features) normal = torch.tanh(self .normal_head(ray_features)) normal = F.normalize(normal, dim=-1 ) material = self .material_head(ray_features) return t, normal, material
7.7.3 挑战
精度问题: 神经网络难以保证精确的相交判断
边界情况: 擦边、切线相交等边界情况难处理
泛化性: 训练数据外的几何难以处理
7.8 混合方案
7.8.1 神经网络辅助BVH
保守但实用的方案是用神经网络辅助而非替代:
加速结构改进:
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 struct HybridBVH { BVHNode* nodes; Triangle* triangles; NeuralSDF coarseGeometry; NeuralCache intersectionCache; }; bool hybridIntersect (HybridBVH& bvh, Ray ray, Hit& hit) { float sdfDist = bvh.coarseGeometry.query (ray.origin); if (sdfDist > ray.tmax) { return false ; } CacheKey key = computeCacheKey (ray); if (bvh.intersectionCache.lookup (key, hit)) { return true ; } bool found = bvh.traverse (ray, hit); if (found) { bvh.intersectionCache.store (key, hit); } return found; }
7.8.2 自适应表示
根据场景复杂度和性能需求选择表示方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 enum class GeometryRepresentation { MESH, SDF, NEURAL_SDF, GAUSSIAN, NERF }; GeometryRepresentation selectRepresentation ( const Object& obj, float performanceBudget ) { if (obj.triangleCount > HIGH_POLY_THRESHOLD && performanceBudget < LOW_PERFORMANCE_THRESHOLD) { return GeometryRepresentation::NEURAL_SDF; } if (obj.isComplexTopology) { return GeometryRepresentation::GAUSSIAN; } return GeometryRepresentation::MESH; }
7.9 实时神经渲染的挑战
7.9.1 推断速度
神经网络的推断速度是主要瓶颈:
方法
每像素查询次数
相对速度
传统光线追踪
1-10
快
NeRF
64-256
慢
3D Gaussian Splatting
10-100
中等
Neural SDF
50-200
中等
优化策略:
网络压缩: 蒸馏、量化、剪枝
硬件加速: Tensor Core、专用硬件
缓存和近似: 空间时间缓存
7.9.2 训练数据需求
神经渲染方法通常需要大量训练数据:
方法
训练数据
训练时间
NeRF
100+视角照片
数小时
3DGS
100+视角照片
10-30分钟
Neural SDF
点云/网格
数小时
解决方案:
7.10 未来展望
7.10.1 研究方向
更快的推断: 研究更高效的网络架构
更好的泛化: 减少对训练数据的依赖
物理一致性: 确保满足物理约束
混合渲染: 结合传统方法和神经方法
7.10.2 工业应用前景
应用场景
适用技术
时间线
电影特效
NeRF/3DGS
已应用
虚拟现实
混合方法
1-3年
实时游戏
辅助神经组件
3-5年
全神经渲染
研究阶段
5-10年+
7.11 小结
本章探讨了用神经网络替代传统光线追踪组件的前沿方向:
NeRF: 隐式辐射率场,适合场景重建
3D Gaussian Splatting:
显式高斯球,渲染效率较高
Neural SDF: 隐式几何表示,可用于光线追踪
神经相交测试: 更激进但尚不成熟
混合方案: 实用性强,是近期的主要方向
这些技术仍在快速发展中,是未来图形学的重要研究方向。
下一章将提供实践指南和代码示例,帮助读者开始实际开发。
第八章:实践指南与代码示例
8.1 开发环境搭建
8.1.1 硬件要求
配置项
最低要求
推荐配置
GPU
RTX 2060 或同等
RTX 3080 或更高
显存
6 GB
12 GB+
内存
16 GB
32 GB+
存储
50 GB SSD
500 GB NVMe SSD
为什么需要RTX显卡?
硬件光线追踪加速(RT Cores)
Tensor Core用于神经网络推断
CUDA支持GPU计算
8.1.2 软件环境
基础开发环境:
1 2 3 4 5 6 7 8 9 10 11 12 conda create -n raytracing python=3.10 conda activate raytracing pip install torch torchvision --index-url https://download.pytorch.org/whl/cu117 pip install numpy scipy matplotlib pip install glfw PyOpenGL pip install tensorboard
图形API开发:
8.1.3 推荐的开发框架
框架
用途
链接
PyTorch3D
神经渲染
facebookresearch/pytorch3d
tiny-cuda-nn
快速神经网络
NVlabs/tiny-cuda-nn
Vulkan-Hpp
光线追踪API
KhronosGroup/Vulkan-Hpp
OptiX
NVIDIA光线追踪
NVIDIA OptiX SDK
Falcor
渲染研究框架
NVIDIA/Falcor
8.2 基础光线追踪器实现
8.2.1 最小化光线追踪器
以下是一个简化的Python光线追踪器:
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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 import numpy as npfrom dataclasses import dataclassfrom typing import Optional , Tuple @dataclass class Ray : origin: np.ndarray direction: np.ndarray def at (self, t: float ) -> np.ndarray: return self .origin + t * self .direction @dataclass class HitRecord : t: float position: np.ndarray normal: np.ndarray material_id: int class Sphere : """球体几何""" def __init__ (self, center: np.ndarray, radius: float , material_id: int = 0 ): self .center = center self .radius = radius self .material_id = material_id def intersect (self, ray: Ray, t_min: float = 0.001 , t_max: float = float ('inf' ) ) -> Optional [HitRecord]: """光线-球体相交测试""" oc = ray.origin - self .center a = np.dot(ray.direction, ray.direction) half_b = np.dot(oc, ray.direction) c = np.dot(oc, oc) - self .radius * self .radius discriminant = half_b * half_b - a * c if discriminant < 0 : return None sqrtd = np.sqrt(discriminant) root = (-half_b - sqrtd) / a if root < t_min or root > t_max: root = (-half_b + sqrtd) / a if root < t_min or root > t_max: return None position = ray.at(root) normal = (position - self .center) / self .radius return HitRecord( t=root, position=position, normal=normal, material_id=self .material_id ) class Scene : """简单场景""" def __init__ (self ): self .objects = [] self .materials = {} def add_object (self, obj ): self .objects.append(obj) def intersect (self, ray: Ray, t_min: float = 0.001 , t_max: float = float ('inf' ) ) -> Optional [HitRecord]: """遍历所有物体找最近交点""" closest_hit = None closest_t = t_max for obj in self .objects: hit = obj.intersect(ray, t_min, closest_t) if hit is not None : closest_hit = hit closest_t = hit.t return closest_hit def random_in_unit_sphere () -> np.ndarray: """在单位球内随机采样""" while True : p = np.random.uniform(-1 , 1 , 3 ) if np.dot(p, p) < 1 : return p def lambertian_scatter (normal: np.ndarray ) -> np.ndarray: """Lambertian漫反射采样""" return normal + random_in_unit_sphere() def ray_color (ray: Ray, scene: Scene, depth: int , max_depth: int = 50 ) -> np.ndarray: """递归路径追踪""" if depth <= 0 : return np.zeros(3 ) hit = scene.intersect(ray) if hit is None : t = 0.5 * (ray.direction[1 ] + 1.0 ) return (1.0 - t) * np.ones(3 ) + t * np.array([0.5 , 0.7 , 1.0 ]) scatter_dir = lambertian_scatter(hit.normal) scattered = Ray(hit.position, scatter_dir) return 0.5 * ray_color(scattered, scene, depth - 1 , max_depth) def render (scene: Scene, width: int , height: int , samples_per_pixel: int = 100 ) -> np.ndarray: """渲染图像""" image = np.zeros((height, width, 3 )) aspect_ratio = width / height viewport_height = 2.0 viewport_width = aspect_ratio * viewport_height focal_length = 1.0 origin = np.zeros(3 ) horizontal = np.array([viewport_width, 0 , 0 ]) vertical = np.array([0 , viewport_height, 0 ]) lower_left = origin - horizontal / 2 - vertical / 2 - np.array([0 , 0 , focal_length]) for j in range (height): print (f"Rendering row {j+1 } /{height} " ) for i in range (width): color = np.zeros(3 ) for s in range (samples_per_pixel): u = (i + np.random.random()) / (width - 1 ) v = (j + np.random.random()) / (height - 1 ) ray = Ray( origin=origin, direction=lower_left + u * horizontal + v * vertical - origin ) color += ray_color(ray, scene, max_depth=50 ) image[height - 1 - j, i] = np.sqrt(color / samples_per_pixel) return image if __name__ == "__main__" : import matplotlib.pyplot as plt scene = Scene() scene.add_object(Sphere(np.array([0 , 0 , -1 ]), 0.5 )) scene.add_object(Sphere(np.array([0 , -100.5 , -1 ]), 100 )) image = render(scene, width=400 , height=300 , samples_per_pixel=50 ) plt.imshow(image) plt.axis('off' ) plt.savefig('output.png' ) plt.show()
8.3 DDGI实现框架
8.3.1 探针网格管理
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 #pragma once #include <vector> #include <glm/glm.hpp> struct DDGIProbe { glm::vec3 position; std::vector<glm::vec3> irradiance; std::vector<float > depth; bool active; }; class DDGIVolume {public : DDGIVolume (const glm::vec3& origin, const glm::vec3& extents, const glm::ivec3& probeCounts); int getProbeIndex (const glm::ivec3& coord) const ; glm::ivec3 getProbeCoord (int index) const ; glm::vec3 sampleIrradiance (const glm::vec3& position, const glm::vec3& normal) const ; void updateProbe (int probeIndex, const std::vector<glm::vec3>& rayDirections, const std::vector<glm::vec3>& rayRadiance, const std::vector<float >& rayDepths) ; const glm::vec3& getOrigin () const { return m_origin; } const glm::vec3& getSpacing () const { return m_spacing; } int getTotalProbeCount () const { return m_totalProbeCount; } private : glm::vec3 m_origin; glm::vec3 m_extents; glm::vec3 m_spacing; glm::ivec3 m_probeCounts; int m_totalProbeCount; std::vector<DDGIProbe> m_probes; glm::vec2 directionToOctahedral (const glm::vec3& dir) const ; glm::vec3 octahedralToDirection (const glm::vec2& oct) const ; };
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 #include "ddgi_volume.h" #include <cmath> #include <algorithm> DDGIVolume::DDGIVolume (const glm::vec3& origin, const glm::vec3& extents, const glm::ivec3& probeCounts) : m_origin (origin) , m_extents (extents) , m_probeCounts (probeCounts) { m_spacing = extents / glm::vec3 (probeCounts - 1 ); m_totalProbeCount = probeCounts.x * probeCounts.y * probeCounts.z; m_probes.resize (m_totalProbeCount); for (int z = 0 ; z < probeCounts.z; z++) { for (int y = 0 ; y < probeCounts.y; y++) { for (int x = 0 ; x < probeCounts.x; x++) { int index = getProbeIndex (glm::ivec3 (x, y, z)); m_probes[index].position = origin + glm::vec3 (x, y, z) * m_spacing; m_probes[index].irradiance.resize (64 , glm::vec3 (0.0f )); m_probes[index].depth.resize (64 , 1000.0f ); m_probes[index].active = true ; } } } } int DDGIVolume::getProbeIndex (const glm::ivec3& coord) const { return coord.z * m_probeCounts.x * m_probeCounts.y + coord.y * m_probeCounts.x + coord.x; } glm::ivec3 DDGIVolume::getProbeCoord (int index) const { int z = index / (m_probeCounts.x * m_probeCounts.y); int remainder = index % (m_probeCounts.x * m_probeCounts.y); int y = remainder / m_probeCounts.x; int x = remainder % m_probeCounts.x; return glm::ivec3 (x, y, z); } glm::vec2 DDGIVolume::directionToOctahedral (const glm::vec3& dir) const { glm::vec3 d = glm::normalize (dir); glm::vec2 oct (d.x / (fabs(d.x) + fabs(d.y) + fabs(d.z)), d.y / (fabs(d.x) + fabs(d.y) + fabs(d.z))) ; if (d.z < 0.0f ) { oct = glm::vec2 ( (1.0f - fabs (oct.y)) * (oct.x >= 0.0f ? 1.0f : -1.0f ), (1.0f - fabs (oct.x)) * (oct.y >= 0.0f ? 1.0f : -1.0f ) ); } return oct * 0.5f + 0.5f ; } glm::vec3 DDGIVolume::sampleIrradiance (const glm::vec3& position, const glm::vec3& normal) const { glm::vec3 localPos = (position - m_origin) / m_spacing; glm::ivec3 lowerCoord = glm::ivec3 (glm::floor (localPos)); glm::vec3 t = localPos - glm::vec3 (lowerCoord); glm::vec3 result (0.0f ) ; float totalWeight = 0.0f ; for (int i = 0 ; i < 8 ; i++) { glm::ivec3 offset (i & 1 , (i >> 1 ) & 1 , (i >> 2 ) & 1 ) ; glm::ivec3 coord = lowerCoord + offset; if (coord.x < 0 || coord.x >= m_probeCounts.x || coord.y < 0 || coord.y >= m_probeCounts.y || coord.z < 0 || coord.z >= m_probeCounts.z) { continue ; } int probeIndex = getProbeIndex (coord); const DDGIProbe& probe = m_probes[probeIndex]; if (!probe.active) continue ; glm::vec2 octCoord = directionToOctahedral (normal); int texelIndex = int (octCoord.x * 7.99f ) + int (octCoord.y * 7.99f ) * 8 ; glm::vec3 irradiance = probe.irradiance[texelIndex]; glm::vec3 blendWeight = glm::mix (1.0f - t, t, glm::vec3 (offset)); float trilinearWeight = blendWeight.x * blendWeight.y * blendWeight.z; glm::vec3 toProbe = probe.position - position; float distToProbe = glm::length (toProbe); glm::vec3 dirToProbe = toProbe / distToProbe; glm::vec2 probeOct = directionToOctahedral (-dirToProbe); int depthIndex = int (probeOct.x * 7.99f ) + int (probeOct.y * 7.99f ) * 8 ; float probeDepth = probe.depth[depthIndex]; float depthWeight = (distToProbe < probeDepth + 0.1f ) ? 1.0f : 0.0f ; float weight = trilinearWeight * depthWeight; result += irradiance * weight; totalWeight += weight; } return totalWeight > 0.0f ? result / totalWeight : glm::vec3 (0.0f ); }
8.4 神经辐射缓存PyTorch实现
8.4.1 Hash Grid Encoding实现
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 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 import torchimport torch.nn as nnimport numpy as npclass HashGridEncoding (nn.Module): """ 多分辨率哈希网格编码 基于Instant-NGP论文实现 """ def __init__ ( self, num_levels: int = 16 , feature_dim: int = 2 , log2_hashmap_size: int = 19 , base_resolution: int = 16 , finest_resolution: int = 2048 , ): super ().__init__() self .num_levels = num_levels self .feature_dim = feature_dim self .log2_hashmap_size = log2_hashmap_size self .hashmap_size = 2 ** log2_hashmap_size b = np.exp((np.log(finest_resolution) - np.log(base_resolution)) / (num_levels - 1 )) self .resolutions = [int (base_resolution * (b ** i)) for i in range (num_levels)] self .embeddings = nn.ModuleList([ nn.Embedding(self .hashmap_size, feature_dim) for _ in range (num_levels) ]) for emb in self .embeddings: nn.init.uniform_(emb.weight, -1e-4 , 1e-4 ) self .primes = torch.tensor([1 , 2654435761 , 805459861 ], dtype=torch.int64) def hash_function (self, coords: torch.Tensor ) -> torch.Tensor: """ 空间哈希函数 coords: (N, 3) int64 """ result = coords[:, 0 ] * self .primes[0 ] result ^= coords[:, 1 ] * self .primes[1 ] result ^= coords[:, 2 ] * self .primes[2 ] return result % self .hashmap_size def forward (self, positions: torch.Tensor ) -> torch.Tensor: """ positions: (N, 3) float32, 在[0, 1]范围内 返回: (N, num_levels * feature_dim) """ batch_size = positions.shape[0 ] device = positions.device primes = self .primes.to(device) features = [] for level in range (self .num_levels): resolution = self .resolutions[level] scaled_pos = positions * resolution voxel_coords = torch.floor(scaled_pos).long() frac = scaled_pos - voxel_coords.float () corner_coords = [] for i in range (8 ): offset = torch.tensor([ i & 1 , (i >> 1 ) & 1 , (i >> 2 ) & 1 ], device=device) corner_coords.append(voxel_coords + offset) hash_indices = [] for coords in corner_coords: hash_val = coords[:, 0 ] * primes[0 ] hash_val = hash_val ^ (coords[:, 1 ] * primes[1 ]) hash_val = hash_val ^ (coords[:, 2 ] * primes[2 ]) hash_val = hash_val % self .hashmap_size hash_indices.append(hash_val) corner_features = [] for hash_idx in hash_indices: feat = self .embeddings[level](hash_idx) corner_features.append(feat) c000, c001, c010, c011, c100, c101, c110, c111 = corner_features c00 = c000 * (1 - frac[:, 0 :1 ]) + c100 * frac[:, 0 :1 ] c01 = c001 * (1 - frac[:, 0 :1 ]) + c101 * frac[:, 0 :1 ] c10 = c010 * (1 - frac[:, 0 :1 ]) + c110 * frac[:, 0 :1 ] c11 = c011 * (1 - frac[:, 0 :1 ]) + c111 * frac[:, 0 :1 ] c0 = c00 * (1 - frac[:, 1 :2 ]) + c10 * frac[:, 1 :2 ] c1 = c01 * (1 - frac[:, 1 :2 ]) + c11 * frac[:, 1 :2 ] c = c0 * (1 - frac[:, 2 :3 ]) + c1 * frac[:, 2 :3 ] features.append(c) return torch.cat(features, dim=-1 ) class NeuralRadianceCache (nn.Module): """ 神经辐射缓存网络 """ def __init__ ( self, num_levels: int = 6 , feature_dim: int = 2 , hidden_dim: int = 64 , ): super ().__init__() self .position_encoder = HashGridEncoding( num_levels=num_levels, feature_dim=feature_dim, ) input_dim = num_levels * feature_dim + 6 self .mlp = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, hidden_dim), nn.LeakyReLU(0.01 ), nn.Linear(hidden_dim, 3 ), ) def forward ( self, position: torch.Tensor, direction: torch.Tensor, normal: torch.Tensor, ) -> torch.Tensor: """ position: (N, 3) 归一化到[0, 1] direction: (N, 3) 单位向量 normal: (N, 3) 单位向量 返回: (N, 3) RGB辐射率 """ pos_encoded = self .position_encoder(position) features = torch.cat([pos_encoded, direction, normal], dim=-1 ) radiance = self .mlp(features) radiance = torch.relu(radiance) return radiance class RelativeL2Loss (nn.Module): """相对L2损失函数""" def __init__ (self, epsilon: float = 0.01 ): super ().__init__() self .epsilon = epsilon def forward (self, pred: torch.Tensor, target: torch.Tensor ) -> torch.Tensor: relative_error = (pred - target) / (target + self .epsilon) return torch.mean(relative_error ** 2 ) def train_nrc (): model = NeuralRadianceCache() loss_fn = RelativeL2Loss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3 ) num_samples = 10000 positions = torch.rand(num_samples, 3 ) directions = torch.randn(num_samples, 3 ) directions = directions / directions.norm(dim=-1 , keepdim=True ) normals = torch.randn(num_samples, 3 ) normals = normals / normals.norm(dim=-1 , keepdim=True ) target_radiance = torch.rand(num_samples, 3 ) * 2.0 for epoch in range (100 ): optimizer.zero_grad() pred_radiance = model(positions, directions, normals) loss = loss_fn(pred_radiance, target_radiance) loss.backward() optimizer.step() if epoch % 10 == 0 : print (f"Epoch {epoch} , Loss: {loss.item():.6 f} " ) return model if __name__ == "__main__" : model = train_nrc() print ("训练完成!" )
8.5 学习资源推荐
8.5.1 经典论文
光线追踪基础:
Kajiya, J. T. (1986). "The Rendering Equation"
Whitted, T. (1980). "An Improved Illumination Model for Shaded
Display"
全局光照:
Jensen, H. W. (1996). "Global Illumination using Photon Maps"
Veach, E. (1997). "Robust Monte Carlo Methods for Light Transport
Simulation"
神经渲染:
Mildenhall et al. (2020). "NeRF: Representing Scenes as Neural
Radiance Fields"
Müller et al. (2021). "Instant-NGP: Instant Neural Graphics
Primitives"
Müller et al. (2021). "Neural Radiance Caching"
DDGI:
Majercik et al. (2019). "Dynamic Diffuse Global Illumination with
Ray-Traced Irradiance Fields"
8.5.2 开源项目
项目
说明
链接
tinyrenderer
软件渲染器教学
ssloy/tinyrenderer
Ray Tracing in One Weekend
经典教程
RayTracing/raytracing.github.io
instant-ngp
NVIDIA NeRF实现
NVlabs/instant-ngp
PyTorch3D
神经渲染框架
facebookresearch/pytorch3d
Falcor
渲染研究框架
NVIDIA/Falcor
8.5.3 进阶书籍
Physically Based Rendering (PBRT) - Matt Pharr,
Wenzel Jakob, Greg Humphreys
Real-Time Rendering (4th Edition) - Tomas
Akenine-Möller et al.
Ray Tracing Gems I & II - NVIDIA
8.6 研究方向建议
8.6.1 初学者建议
阶段一:基础(1-3个月)
实现简单的光线追踪器
理解渲染方程和蒙特卡洛积分
学习基本的BRDF模型
阶段二:进阶(3-6个月)
实现路径追踪
学习重要性采样和MIS
了解传统GI方法
阶段三:前沿(6-12个月)
8.6.2 可能的研究课题
DDGI优化:
自适应探针放置
更好的深度防漏光算法
与神经网络的结合
神经渲染:
实时NRC推断优化
NIV的光环境适应
混合渲染管线设计
前沿探索:
神经网络替代BVH
3D Gaussian Splatting与光线追踪结合
神经SDF渲染
8.7 小结
本章提供了实践开发的起点:
开发环境: 硬件要求、软件配置、推荐框架
基础实现: 简化的光线追踪器Python代码
DDGI框架: C++核心结构示例
神经渲染: PyTorch实现的Hash Grid和NRC
学习资源: 论文、项目、书籍推荐
研究方向: 循序渐进的学习路径
希望这份教材能帮助你进入光线追踪和神经渲染的精彩世界!
附录:术语表
术语
英文
解释
光线追踪
Ray Tracing
通过模拟光线传播进行渲染的技术
全局光照
Global Illumination (GI)
考虑光线多次反弹的完整光照计算
辐射率
Radiance
单位投影面积单位立体角的辐射通量
辐照度
Irradiance
单位面积接收的辐射通量
BRDF
Bidirectional Reflectance Distribution Function
双向反射分布函数,描述材质反射特性
渲染方程
Rendering Equation
描述光能平衡的积分方程
路径追踪
Path Tracing
使用蒙特卡洛方法求解渲染方程
探针
Probe
存储空间点光照信息的数据结构
DDGI
Dynamic Diffuse Global Illumination
动态漫反射全局光照技术
NRC
Neural Radiance Caching
神经辐射缓存
NIV
Neural Irradiance Volume
神经辐照度体
NeRF
Neural Radiance Fields
神经辐射场
SDF
Signed Distance Field
有符号距离场
BVH
Bounding Volume Hierarchy
层次包围盒加速结构
SH
Spherical Harmonics
球面调和函数