第7章:光线追踪基础
光线追踪是计算机图形学中实现真实感渲染的核心技术之一。不同于光栅化的前向渲染方式,光线追踪采用逆向追踪光线的方式,从相机出发追踪光线在场景中的传播路径。本章将深入探讨光线追踪的基本原理、加速结构的设计与实现,以及光线与物体相交算法的优化技术。我们将重点关注算法的数学基础和性能优化策略,为后续的全局光照和高级渲染技术打下坚实基础。
7.1 光线追踪基本原理
7.1.1 光线的数学表示
光线可以用参数方程表示: $$\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}$$ 其中:
- $\mathbf{o}$ 是光线起点(origin)
- $\mathbf{d}$ 是归一化的方向向量(direction)
- $t \geq 0$ 是参数,表示沿光线方向的距离
扩展表示:为了数值稳定性,实际应用中常使用: $$\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}, \quad t \in [t_{\min}, t_{\max}]$$ 其中 $t_{\min} > 0$ 避免自相交,$t_{\max}$ 限制光线长度。典型值:$t_{\min} = 10^{-4}$ 到 $10^{-3}$,取决于场景规模。
光线微分:用于抗锯齿和纹理过滤,追踪光线的微分信息: $$\frac{\partial \mathbf{r}}{\partial x} = \frac{\partial \mathbf{o}}{\partial x} + t\frac{\partial \mathbf{d}}{\partial x}$$ 这对于计算纹理footprint至关重要。考虑相邻像素的光线: $$\begin{aligned} \frac{\partial \mathbf{d}}{\partial x} &\approx \mathbf{d}_{i+1,j} - \mathbf{d}_{i,j} \ \frac{\partial \mathbf{d}}{\partial y} &\approx \mathbf{d}_{i,j+1} - \mathbf{d}_{i,j} \end{aligned}$$ 光线锥(Ray Cone)表示: 为了更准确的LOD选择和过滤,可以将光线扩展为圆锥: $$r(t) = r_0 + t \cdot \tan(\theta)$$ 其中 $r_0$ 是像素footprint在近平面的半径,$\theta$ 是锥角,通常: $$\theta \approx \frac{1}{\text{image_width}} \cdot \text{fov}$$ 光线的齐次坐标表示: 在某些情况下(如投影变换),使用Plücker坐标更方便: $$\mathbf{L} = (\mathbf{d}, \mathbf{o} \times \mathbf{d})$$ 这种表示在处理光线-光线距离和相交测试时特别有用。
7.1.2 基本光线追踪算法
光线追踪的核心步骤:
- 主光线生成(Primary Ray Generation) 对于图像平面上的每个像素 $(i, j)$,生成从相机位置出发的主光线: $$\mathbf{d}_{i,j} = \text{normalize}(\mathbf{p}_{i,j} - \mathbf{e})$$ 其中 $\mathbf{e}$ 是相机位置,$\mathbf{p}_{i,j}$ 是像素在世界坐标系中的位置。
子像素采样:为了抗锯齿,在像素内部进行多次采样: $$\mathbf{p}_{i,j}^{(k)} = \mathbf{p}_{i,j} + \xi_x \Delta_x \mathbf{right} + \xi_y \Delta_y \mathbf{up}$$ 其中 $\xi_x, \xi_y \in [-0.5, 0.5]$ 是采样偏移。
采样模式:
- 规则采样:$\xi_x = \frac{k_x + 0.5}{n_x} - 0.5$
- 随机采样:$\xi_x \sim U(-0.5, 0.5)$
- 分层采样(Stratified):将像素分为 $n \times n$ 子格,每格内随机采样
- 低差异序列:使用Halton或Sobol序列
重要性采样优化: 根据前帧或预览pass的信息,在高频区域增加采样: $$p(\xi) \propto |\nabla I(x, y)|^2 + \epsilon$$
- 光线-场景相交测试 对每条光线,找到最近的相交点: $$t_{\text{hit}} = \min_{k \in \text{objects}} t_k$$ 其中 $t_k$ 是光线与第 $k$ 个物体的相交参数。
相交信息记录:
struct HitRecord {
float t; // 相交参数
vec3 point; // 相交点
vec3 normal; // 表面法线
vec2 uv; // 纹理坐标
Material* mat; // 材质指针
float pdf_area; // 面积概率密度
vec3 dpdu, dpdv; // 表面参数化导数
vec3 tangent; // 切线向量
float curvature; // 曲率信息
}
浮点误差处理: 相交点的精确计算考虑误差界: $$\mathbf{p}_{\text{hit}} = \mathbf{o} + t_{\text{hit}}\mathbf{d} \pm \epsilon_{\text{hit}}$$ 其中 $\epsilon_{\text{hit}} = \gamma_3(|\mathbf{o}| + t_{\text{hit}}|\mathbf{d}|)$, $\gamma_n = \frac{n\epsilon_{\text{machine}}}{1 - n\epsilon_{\text{machine}}}$
- 着色计算 在相交点处计算颜色,考虑:
- 直接光照(使用阴影光线)
- 反射(递归追踪反射光线)
- 折射(对透明物体)
- 环境光照(环境贴图采样)
光线相干性利用:
- 光线包(Ray Packets):同时追踪多条相邻光线
- 光线排序:按方向或起点聚类以改善缓存局部性
- 光线重排序缓冲:延迟处理以批量化相似操作
7.1.3 递归光线追踪
递归光线追踪通过追踪次级光线来模拟复杂的光照效果: $$L_o(\mathbf{x}, \omega_o) = L_e(\mathbf{x}, \omega_o) + \int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o) L_i(\mathbf{x}, \omega_i) (\omega_i \cdot \mathbf{n}) d\omega_i$$ Whitted-style光线追踪简化为: $$L_o = L_e + k_d L_{\text{direct}} + k_s L_{\text{reflect}} + k_t L_{\text{refract}}$$ 其中:
- $L_{\text{direct}}$:直接光照(Lambert + Phong)
- $L_{\text{reflect}}$:镜面反射贡献
- $L_{\text{refract}}$:折射贡献
- $k_d, k_s, k_t$:材质参数,满足能量守恒 $k_d + k_s + k_t \leq 1$
直接光照计算的细节:
对于点光源: $$L_{\text{direct}} = \sum_{i} \frac{I_i}{r_i^2} \cdot V_i \cdot \max(0, \mathbf{n} \cdot \mathbf{l}_i) \cdot \left(\frac{k_d}{\pi} + k_s \frac{(\mathbf{h}_i \cdot \mathbf{n})^n}{(\mathbf{n} \cdot \mathbf{l}_i)}\right)$$ 其中:
- $I_i$:光源强度
- $r_i$:到光源距离
- $V_i$:可见性函数(0或1)
- $\mathbf{h}_i = \text{normalize}(\mathbf{l}_i + \mathbf{v})$:半向量
- $n$:Phong指数
反射方向计算: $$\mathbf{r}_{\text{reflect}} = \mathbf{d} - 2(\mathbf{d} \cdot \mathbf{n})\mathbf{n}$$ 改进的反射模型: 考虑微表面理论,可以添加粗糙度扰动: $$\mathbf{r}'_{\text{reflect}} = \text{normalize}(\mathbf{r}_{\text{reflect}} + \alpha \mathbf{\xi})$$ 其中 $\mathbf{\xi}$ 是随机扰动向量,$\alpha$ 是粗糙度参数。
扰动向量的生成: 使用切线空间:
- 构建正交基:${\mathbf{n}, \mathbf{t}, \mathbf{b}}$
- 在半球内采样:$\mathbf{\xi} = \sqrt{1-u^2}\cos(2\pi v)\mathbf{t} + \sqrt{1-u^2}\sin(2\pi v)\mathbf{b} + u\mathbf{n}$
- 其中 $u, v \in [0,1]$ 是随机数
折射方向计算(Snell定律): $$\mathbf{r}_{\text{refract}} = \frac{\eta_i}{\eta_t}\mathbf{d} + \left(\frac{\eta_i}{\eta_t}(\mathbf{n} \cdot \mathbf{d}) - \sqrt{1 - \sin^2\theta_t}\right)\mathbf{n}$$ 其中 $\sin^2\theta_t = \left(\frac{\eta_i}{\eta_t}\right)^2(1 - (\mathbf{n} \cdot \mathbf{d})^2)$
稳定的折射计算:
令 k = 1 - η²(1 - (n·d)²)
if k < 0: 全内反射
else: t = η*d + (η*(n·d) - √k)*n
全内反射判断:当 $\sin^2\theta_t > 1$ 时发生全内反射。
临界角:$\theta_c = \arcsin(\eta_t/\eta_i)$(当 $\eta_i > \eta_t$)
Fresnel方程: 计算反射和折射的能量分配: $$F_r = \frac{1}{2}\left[\left(\frac{\eta_i\cos\theta_i - \eta_t\cos\theta_t}{\eta_i\cos\theta_i + \eta_t\cos\theta_t}\right)^2 + \left(\frac{\eta_t\cos\theta_i - \eta_i\cos\theta_t}{\eta_t\cos\theta_i + \eta_i\cos\theta_t}\right)^2\right]$$ Schlick近似(计算效率更高): $$F_r \approx F_0 + (1 - F_0)(1 - \cos\theta_i)^5$$ 其中 $F_0 = \left(\frac{\eta_i - \eta_t}{\eta_i + \eta_t}\right)^2$
金属材质的Fresnel: 复数折射率 $\tilde{\eta} = \eta + i\kappa$ $$F_r = \frac{(\eta - 1)^2 + \kappa^2}{(\eta + 1)^2 + \kappa^2}$$ 色散效果: 不同波长具有不同折射率: $$\eta(\lambda) = A + \frac{B}{\lambda^2} + \frac{C}{\lambda^4}$$ (Cauchy方程)
典型值(玻璃):
- 红光(700nm):$\eta_r \approx 1.513$
- 绿光(550nm):$\eta_g \approx 1.517$
- 蓝光(450nm):$\eta_b \approx 1.522$
实现色散:
- 将白光分解为RGB分量
- 对每个分量使用不同折射率
- 分别追踪三条折射光线
- 合成最终颜色
递归深度控制:
简单深度限制:
if (depth > MAX_DEPTH) return background_color;
基于贡献的终止:
if (throughput < THRESHOLD) return black;
俄罗斯轮盘赌终止策略:
float p = max(color.r, color.g, color.b);
if (random() > p) return black;
return color / p; // 能量补偿
改进的终止策略: 考虑路径贡献和计算成本的平衡: $$p_{\text{continue}} = \min\left(1, \frac{\text{throughput} \cdot \text{max_radiance}}{\text{current_radiance}}\right)$$ 自适应深度策略:
- 镜面反射:允许更深递归
- 漫反射:较早终止
- 基于材质重要性调整深度限制
光线树优化: 记录光线贡献路径,避免重复计算:
struct RayNode {
Ray ray;
float contribution;
Material* mat;
RayNode* children[2]; // reflect, refract
}
多次反射间的相互作用:
- 焦散(Caustics):光线经过折射/反射聚焦
- 间接光照:多次漫反射
- 色彩渗透:相邻表面的颜色互相影响
这些效果在Whitted模型中是近似的,更准确的模拟需要路径追踪或光子映射。
7.1.4 相机模型与光线生成
透视投影相机的光线生成:
给定视场角 $\text{fov}$、宽高比 $\text{aspect}$,像素 $(i, j)$ 对应的光线方向: $$\begin{aligned} u &= \frac{2i - \text{width}}{\text{width}} \cdot \text{aspect} \cdot \tan(\text{fov}/2) \ v &= \frac{2j - \text{height}}{\text{height}} \cdot \tan(\text{fov}/2) \ \mathbf{d} &= \text{normalize}(u\mathbf{right} + v\mathbf{up} - \mathbf{forward}) \end{aligned}$$ 改进的相机模型考虑:
- 传感器偏移:模拟tilt-shift效果
- 非方形像素:像素宽高比校正
- 畸变模型:径向和切向畸变
薄透镜相机模型(景深效果):
-
计算焦平面上的目标点: $$\mathbf{p}_{\text{focus}} = \mathbf{o} + \frac{\text{focus_distance}}{|\mathbf{d} \cdot \mathbf{forward}|} \mathbf{d}$$
-
在透镜上采样: $$\mathbf{o}' = \mathbf{o} + r(\cos\theta \mathbf{right} + \sin\theta \mathbf{up})$$ 其中 $r \leq \text{aperture}/2$,采样分布可以是:
- 均匀圆盘:$r = \sqrt{\xi_1} \cdot \text{aperture}/2$, $\theta = 2\pi\xi_2$
- 多边形光圈:模拟真实光圈叶片
- 自定义形状:实现散景(bokeh)效果
- 新的光线方向: $$\mathbf{d}' = \text{normalize}(\mathbf{p}_{\text{focus}} - \mathbf{o}')$$ 景深的物理参数:
- 弥散圆直径:$c = \frac{A|S_2 - S_1|}{S_2}$
- 景深范围:$\text{DOF} = \frac{2Nc(S - f)^2}{f^2 - N^2c^2}$ 其中 $N$ 是光圈数,$S$ 是对焦距离,$f$ 是焦距
鱼眼相机: $$\begin{aligned} r &= \sqrt{u^2 + v^2} \ \theta &= r \cdot \text{fov} / 2 \ \phi &= \arctan2(v, u) \ \mathbf{d} &= \sin\theta\cos\phi \mathbf{right} + \sin\theta\sin\phi \mathbf{up} - \cos\theta \mathbf{forward} \end{aligned}$$ 其他投影模型:
- 等距投影:$\theta = r$
- 等立体角投影:$\theta = 2\sin^{-1}(r/2)$
- 正交投影:$\theta = \sin^{-1}(r)$
- 立体投影:$\theta = 2\tan^{-1}(r/2)$
全景相机:
- 等矩形投影:$(u, v) \rightarrow (\theta, \phi)$ 直接映射
- 立方体贴图:6个透视投影相机
- 双抛物面映射:前后两个抛物面投影
运动模糊: 在快门时间内对相机位置和方向进行积分: $$\mathbf{o}(t) = \mathbf{o}_0 + t \cdot \mathbf{v}$$ $$\mathbf{d}(t) = \mathbf{R}(t) \cdot \mathbf{d}_0$$ 其中 $t \in [0, \text{shutter_time}]$
7.2 加速结构(BVH、KD-Tree)
7.2.1 空间数据结构的必要性
朴素的光线追踪需要测试每条光线与场景中所有物体的相交,复杂度为 $O(N)$。对于包含百万级三角形的场景,这是不可接受的。空间加速结构可以将平均复杂度降低到 $O(\log N)$。
理论分析:
- 无加速结构:每条光线需要 $N$ 次相交测试
- 理想加速结构:$O(\log N)$ 次节点访问 + $O(1)$ 次图元测试
- 实际性能:取决于场景分布和构建质量
性能模型: 设光线追踪总时间 $T = T_{\text{build}} + R \cdot T_{\text{trace}}$,其中:
- $T_{\text{build}}$:构建时间
- $R$:光线数量
- $T_{\text{trace}}$:单条光线追踪时间
对于单条光线: $$T_{\text{trace}} = T_{\text{traverse}} + N_{\text{leaf}} \cdot T_{\text{intersect}}$$ 其中 $N_{\text{leaf}}$ 是访问的叶节点数。
空间连贯性原理:
- 光线连贯性:相邻光线倾向于相交相似的物体集合
- 几何连贯性:空间相近的物体应该在加速结构中相近
- 时间连贯性:连续帧之间的相交模式相似
常见加速结构分类:
-
空间分割:KD-Tree、Octree、BSP Tree - 优点:无重叠,内存效率高 - 缺点:物体可能跨越多个节点
-
物体分割:BVH、R-Tree - 优点:每个物体只属于一个叶节点 - 缺点:包围盒可能重叠
-
混合方法:SBVH、Dual-Split BVH - 结合空间和物体分割的优点 - 更复杂的构建算法
加速结构选择准则:
- 静态场景:KD-Tree(遍历效率最高)
- 动态场景:BVH(易于更新)
- GPU实现:BVH(更规则的内存访问)
- 内存受限:压缩BVH或隐式Grid
7.2.2 层次包围盒(BVH)
BVH是一种物体层次结构,每个节点包含其所有子节点的轴对齐包围盒(AABB)。
BVH节点结构:
struct BVHNode {
AABB bounds; // 包围盒
union {
struct { // 内部节点
BVHNode* left;
BVHNode* right;
};
struct { // 叶节点
int primOffset; // 图元起始索引
int primCount; // 图元数量
};
};
}
BVH构建算法:
-
自顶向下构建 ``` function BuildBVH(primitives, start, end): if (end - start <= leaf_threshold): return CreateLeaf(primitives[start:end])
axis = ChooseSplitAxis(primitives[start:end]) mid = Partition(primitives, start, end, axis)
left = BuildBVH(primitives, start, mid) right = BuildBVH(primitives, mid, end)
return CreateNode(left, right) ```
-
SAH(Surface Area Heuristic) 最优分割的代价函数: $$C = C_{\text{trav}} + \frac{A_L}{A} \cdot N_L \cdot C_{\text{isect}} + \frac{A_R}{A} \cdot N_R \cdot C_{\text{isect}}$$ 其中:
- $C_{\text{trav}}$ 是遍历节点的代价(典型值:1.0)
- $C_{\text{isect}}$ 是相交测试的代价(典型值:4.0)
- $A_L, A_R$ 是左右子节点的表面积
- $N_L, N_R$ 是左右子节点包含的图元数
完整SAH实现考虑: $$C_{\text{split}} = C_{\text{trav}} + P_L \cdot C_L + P_R \cdot C_R$$ 其中概率 $P_L = \frac{A_L}{A}$,$P_R = \frac{A_R}{A}$
表面积计算(AABB): $$A = 2(dx \cdot dy + dy \cdot dz + dz \cdot dx)$$ 其中 $dx = \text{max}_x - \text{min}_x$ 等。
- 分割策略比较:
中点分割(Middle Split):
- 简单快速:$p = (\text{min} + \text{max}) / 2$
- 可能产生不平衡的树
等数分割(Equal Count):
- 保证平衡:每边 $N/2$ 个图元
- 可能产生重叠严重的包围盒
SAH分割:
- 最小化期望遍历代价
- 计算开销大,但质量最高
混合策略:
if (图元数 < 阈值) 使用中点分割
else 使用SAH分割
- 高效SAH实现:
桶排序方法(Binned SAH): ``` 将包围盒范围分为K个桶(典型K=32) for each 图元: 计算质心 分配到对应桶
for each 可能的分割位置(K-1个): 计算左右两边的图元数和包围盒 评估SAH代价 选择最小代价的分割 ```
复杂度:$O(KN)$ 而非 $O(N^2)$
- BVH遍历
递归遍历: ``` function Intersect(node, ray, tmin, tmax): if (IsLeaf(node)): return IntersectPrimitives(node.primitives, ray)
t1 = IntersectAABB(node.left.bbox, ray)
t2 = IntersectAABB(node.right.bbox, ray)
if (t1.hit && t2.hit):
first = (t1.tmin < t2.tmin) ? node.left : node.right
second = (t1.tmin < t2.tmin) ? node.right : node.left
hit1 = Intersect(first, ray, tmin, tmax)
if (hit1.t < second.tmin) return hit1
hit2 = Intersect(second, ray, tmin, tmax)
return Closer(hit1, hit2)
```
栈式遍历(更适合GPU):
stack[0] = root
while (stack not empty):
node = stack.pop()
if (IntersectAABB(node.bbox, ray)):
if (IsLeaf(node)):
ProcessPrimitives(node)
else:
stack.push(node.farChild)
stack.push(node.nearChild)
优化的遍历顺序:
基于光线方向符号预计算子节点访问顺序:
dirIsNeg[3] = {ray.dir.x < 0, ray.dir.y < 0, ray.dir.z < 0}
// 使用dirIsNeg[node.axis]决定先访问哪个子节点
- BVH质量评估:
SAH代价: $$\text{SAH}(T) = \sum_{\text{leaves}} P(\text{leaf}) \cdot N(\text{leaf})$$ EPO(Expected Primary Operations): 平均每条光线的期望操作数
树平衡度: $$\text{Balance} = \frac{\text{avg_depth}}{\log_2 N}$$
- 高级BVH技术:
SBVH(Spatial Split BVH):
- 允许空间分割,不仅是物体分割
- 可以减少包围盒重叠
- 代价:可能增加图元引用数
实现策略:
if (SAH代价 > 阈值 && 重叠率 > 阈值):
尝试空间分割
选择最优的物体或空间分割
压缩BVH:
- 量化包围盒坐标(16位或8位)
- 节点合并(将多个节点打包)
- 典型压缩率:50-75%
量化公式: $$\text{quantized} = \text{round}\left(\frac{\text{value} - \text{parent_min}}{\text{parent_max} - \text{parent_min}} \cdot (2^{bits} - 1)\right)$$ Wide BVH:
- 每个节点有4个或8个子节点
- 更好的SIMD利用率
- 减少内存访问次数
- 动态场景的BVH更新:
重拟合(Refit):
自底向上更新包围盒
function Refit(node):
if (IsLeaf(node)):
node.bounds = ComputeBounds(node.primitives)
else:
Refit(node.left)
Refit(node.right)
node.bounds = Union(node.left.bounds, node.right.bounds)
局部重建:
- 标记"脏"节点
- 只重建变化较大的子树
- 使用启发式判断是否需要重建
双缓冲策略:
- 维护两个BVH
- 异步更新非活动BVH
- 快速切换
- 内存布局优化:
深度优先布局:
- 改善缓存局部性
- 适合递归遍历
van Emde Boas布局:
- 优化缓存性能
- 适合任意遍历模式
节点压缩布局:
struct CompactBVHNode {
float bounds_min[3];
float bounds_max[3];
uint32_t offset; // 子节点偏移或图元偏移
uint8_t nPrims; // 0表示内部节点
uint8_t axis; // 分割轴
uint16_t pad; // 对齐
} // 32字节,正好两个缓存行
7.2.3 KD-Tree
KD-Tree特点:
- 空间分割而非物体分割
- 分割平面与坐标轴对齐
- 可能需要处理跨越分割平面的三角形
构建策略:
- 分割位置选择:
中位数分割: $$p_{\text{split}} = \text{median}({p_i \cdot \mathbf{axis} : i \in \text{primitives}})$$ SAH分割: 最小化代价函数: $$C(p) = C_{\text{trav}} + \frac{V_L(p)}{V} N_L(p) C_{\text{isect}} + \frac{V_R(p)}{V} N_R(p) C_{\text{isect}}$$ 空盒优化: $$C_{\text{empty}} = 0.8 \cdot C_{\text{trav}}$$
-
精确SAH计算:
for each candidate position p: leftCount = CountPrimitivesLeft(p) rightCount = CountPrimitivesRight(p) leftVolume = ComputeVolume(min, p) rightVolume = ComputeVolume(p, max) cost = EvaluateSAH(leftCount, rightCount, leftVolume, rightVolume) -
KD-Tree遍历算法: ``` function TraverseKDTree(ray, tmin, tmax, node): if (IsLeaf(node)): return IntersectPrimitives(node.primitives, ray)
axis = node.splitAxis t_split = (node.splitPos - ray.origin[axis]) / ray.dir[axis]
nearNode = (ray.origin[axis] < node.splitPos) ? node.left : node.right farNode = (ray.origin[axis] < node.splitPos) ? node.right : node.left
if (t_split > tmax || t_split < 0): return TraverseKDTree(ray, tmin, tmax, nearNode) if (t_split < tmin): return TraverseKDTree(ray, tmin, tmax, farNode)
hit = TraverseKDTree(ray, tmin, t_split, nearNode) if (hit.valid) return hit
return TraverseKDTree(ray, t_split, tmax, farNode) ```
遍历优化:
-
Mailboxing:避免重复相交测试
struct Mailbox { int rayID; float t; } -
早期终止:当找到相交点后提前退出
- Rope技术:存储邻居指针加速遍历
7.2.4 加速结构比较
| 特性 | BVH | KD-Tree | Octree |
| 特性 | BVH | KD-Tree | Octree |
|---|---|---|---|
| 构建复杂度 | $O(N\log N)$ | $O(N\log N)$ | $O(N)$ |
| 内存占用 | $O(N)$ | $O(N) - O(N\log N)$ | $O(N)$ |
| 遍历效率 | 中等 | 高 | 低 |
| 动态更新 | 支持(重拟合) | 困难 | 中等 |
| GPU友好性 | 高 | 低(分支多) | 中等 |
| 空间利用率 | 可能重叠 | 无重叠 | 可能稀疏 |
性能模型: 给定场景包含 $N$ 个图元,光线数量为 $R$:
-
构建时间: - BVH: $T_{\text{build}} = O(N\log N)$ - KD-Tree: $T_{\text{build}} = O(N\log^2 N)$(SAH)
-
遍历时间: - 期望深度:$D = O(\log N)$ - 每条光线:$T_{\text{ray}} = D \cdot C_{\text{node}} + L \cdot C_{\text{leaf}}$ - 总时间:$T_{\text{total}} = R \cdot T_{\text{ray}}$
-
内存访问模式: - BVH:更好的空间局部性 - KD-Tree:更深的树,更多cache miss
混合加速结构:
顶层:BVH(场景级别)
├── 中层:KD-Tree(物体级别)
└── 底层:Grid(密集三角形)
7.3 光线-物体相交算法优化
7.3.1 光线-三角形相交
光线-三角形相交是光线追踪中最频繁的操作,其性能直接影响渲染速度。
Möller-Trumbore算法:
给定三角形顶点 $\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2$,光线 $\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}$: $$\begin{aligned} \mathbf{e}_1 &= \mathbf{v}_1 - \mathbf{v}_0 \ \mathbf{e}_2 &= \mathbf{v}_2 - \mathbf{v}_0 \ \mathbf{h} &= \mathbf{d} \times \mathbf{e}_2 \ a &= \mathbf{e}_1 \cdot \mathbf{h} \end{aligned}$$ 如果 $|a| < \epsilon$,光线与三角形平行。否则: $$\begin{aligned} f &= 1/a \ \mathbf{s} &= \mathbf{o} - \mathbf{v}_0 \ u &= f(\mathbf{s} \cdot \mathbf{h}) \end{aligned}$$ 如果 $u < 0$ 或 $u > 1$,无相交。继续: $$\begin{aligned} \mathbf{q} &= \mathbf{s} \times \mathbf{e}_1 \ v &= f(\mathbf{d} \cdot \mathbf{q}) \end{aligned}$$ 如果 $v < 0$ 或 $u + v > 1$,无相交。否则: $$t = f(\mathbf{e}_2 \cdot \mathbf{q})$$ 算法复杂度分析:
- 1次除法,27次乘法,17次加减法
- 2次叉积,3次点积
- 分支预测友好(早期退出)
Baldwin-Weber算法(避免除法): 使用齐次坐标避免除法: $$\begin{aligned} \mathbf{o}' &= \mathbf{o} - \mathbf{v}_0 \ \mathbf{q}_1 &= \mathbf{d} \times \mathbf{e}_2 \ \mathbf{q}_2 &= \mathbf{o}' \times \mathbf{e}_1 \ \text{det} &= \mathbf{q}_1 \cdot \mathbf{e}_1 \ u &= \mathbf{q}_1 \cdot \mathbf{o}' \ v &= \mathbf{q}_2 \cdot \mathbf{d} \ t &= \mathbf{q}_2 \cdot \mathbf{e}_2 \end{aligned}$$ 相交条件(避免除法):
if (det > 0):
return (u >= 0) && (v >= 0) && (u + v <= det) && (t >= 0)
else:
return (u <= 0) && (v <= 0) && (u + v >= det) && (t <= 0)
Woop算法(预计算优化): 预计算变换矩阵,将三角形变换到单位三角形: $$\mathbf{M} = \begin{bmatrix} \mathbf{v}_0 - \mathbf{v}_2 & \mathbf{v}_1 - \mathbf{v}_2 & \mathbf{v}_2 \end{bmatrix}^{-1}$$ 光线变换: $$\begin{aligned} \mathbf{o}' &= \mathbf{M} \cdot \mathbf{o} \ \mathbf{d}' &= \mathbf{M} \cdot \mathbf{d} \end{aligned}$$ 相交测试简化为: $$t = -\frac{o'_z}{d'_z}, \quad u = o'_x + t \cdot d'_x, \quad v = o'_y + t \cdot d'_y$$ 相交条件:$u \geq 0, v \geq 0, u + v \leq 1$
SIMD优化: 同时测试4个三角形:
__m128 e1x = _mm_sub_ps(v1x, v0x);
__m128 e1y = _mm_sub_ps(v1y, v0y);
__m128 e1z = _mm_sub_ps(v1z, v0z);
// ... 继续向量化计算
精度改进: 使用Shewchuk的精确谓词算法处理边界情况:
- 精确的方向测试
- 鲁棒的重心坐标计算
- 避免数值不一致
7.3.2 光线-包围盒相交
Slab方法:
对于轴对齐包围盒(AABB),计算光线与每对平行平面的相交: $$\begin{aligned} t_{\text{min}} &= \max(t_{x,\text{min}}, t_{y,\text{min}}, t_{z,\text{min}}) \ t_{\text{max}} &= \min(t_{x,\text{max}}, t_{y,\text{max}}, t_{z,\text{max}}) \end{aligned}$$ 相交条件:$t_{\text{min}} \leq t_{\text{max}}$ 且 $t_{\text{max}} \geq 0$
优化实现:
invD = 1.0 / ray.dir // 预计算
t0 = (box.min - ray.origin) * invD
t1 = (box.max - ray.origin) * invD
// 处理负方向
tmin = min(t0, t1)
tmax = max(t0, t1)
// 计算最终区间
tmin_final = max(tmin.x, tmin.y, tmin.z)
tmax_final = min(tmax.x, tmax.y, tmax.z)
return tmin_final <= tmax_final && tmax_final >= 0
Williams等人的优化: 消除min/max中的分支:
sign[3] = {ray.dir.x < 0, ray.dir.y < 0, ray.dir.z < 0}
tmin = (box[sign[0]].x - ray.origin.x) * invD.x
tmax = (box[1-sign[0]].x - ray.origin.x) * invD.x
SSE优化版本:
__m128 tmin = _mm_mul_ps(_mm_sub_ps(box_min, origin), inv_dir);
__m128 tmax = _mm_mul_ps(_mm_sub_ps(box_max, origin), inv_dir);
__m128 t0 = _mm_min_ps(tmin, tmax);
__m128 t1 = _mm_max_ps(tmin, tmax);
鲁棒性改进: 处理光线方向分量为0的情况:
if (abs(ray.dir.x) < epsilon):
if (ray.origin.x < box.min.x || ray.origin.x > box.max.x):
return false
tx_min = -infinity
tx_max = +infinity
7.3.3 光线-球体相交
球体方程:$||\mathbf{p} - \mathbf{c}||^2 = r^2$
代入光线方程得到二次方程: $$at^2 + bt + c = 0$$ 其中: $$\begin{aligned} a &= \mathbf{d} \cdot \mathbf{d} = 1 \text{ (归一化方向)} \ b &= 2\mathbf{d} \cdot (\mathbf{o} - \mathbf{c}) \ c &= ||\mathbf{o} - \mathbf{c}||^2 - r^2 \end{aligned}$$ 判别式 $\Delta = b^2 - 4ac$,相交参数: $$t = \frac{-b \pm \sqrt{\Delta}}{2a}$$ 几何优化方法:
L = center - ray.origin
tca = dot(L, ray.dir)
if (tca < 0) return false // 球在光线后方
d2 = dot(L, L) - tca * tca
if (d2 > radius2) return false // 光线错过球体
thc = sqrt(radius2 - d2)
t0 = tca - thc
t1 = tca + thc
数值稳定性改进: 使用以下形式避免相减相消: $$t = \frac{-b - \text{sign}(b)\sqrt{\Delta}}{2a}$$ 椭球体相交: 使用变换矩阵 $\mathbf{M}$ 将椭球变换为单位球: $$\begin{aligned} \mathbf{o}' &= \mathbf{M}(\mathbf{o} - \mathbf{c}) \ \mathbf{d}' &= \mathbf{M}\mathbf{d} \end{aligned}$$ 然后使用球体相交算法。
7.3.4 其他几何体相交
光线-圆柱相交: 无限圆柱:$(x - c_x)^2 + (z - c_z)^2 = r^2$
相交计算: $$a = d_x^2 + d_z^2$$ $$b = 2(d_x(o_x - c_x) + d_z(o_z - c_z))$$ $$c = (o_x - c_x)^2 + (o_z - c_z)^2 - r^2$$ 有限圆柱需要额外检查端盖。
光线-圆锥相交: 圆锥方程:$x^2 + z^2 = (r(y/h))^2$
二次方程系数: $$a = d_x^2 + d_z^2 - (r/h)^2 d_y^2$$ $$b = 2(o_x d_x + o_z d_z - (r/h)^2 o_y d_y)$$ $$c = o_x^2 + o_z^2 - (r/h)^2 o_y^2$$ 光线-环面(Torus)相交: 四次方程,使用数值方法求解: $$(x^2 + y^2 + z^2 + R^2 - r^2)^2 = 4R^2(x^2 + z^2)$$ 可以使用Sturm序列或Ferrari方法求解。
7.3.5 相交测试优化策略
-
早期拒绝(Early Rejection) - 使用简单包围体进行预测试 - 利用空间连贯性 - 背面剔除(对闭合物体)
-
SIMD并行化 - 同时测试多条光线(Ray Packets) - 批量处理三角形 - 使用SOA(Structure of Arrays)布局
-
缓存优化 - 数据结构对齐(避免false sharing) - 热数据分离(将常用数据打包) - 预取(prefetch)关键数据
-
精度考虑 - 使用稳定的数值算法 - 处理自相交问题(shadow acne) - 考虑舍入误差的传播
-
分支优化 - 减少条件分支(使用位运算) - 使用模板特化消除运行时分支 - Profile-guided optimization
-
间隔算术(Interval Arithmetic) 用于保守估计和鲁棒性:
struct Interval { float min, max; Interval operator*(const Interval& b) { float products[4] = {min*b.min, min*b.max, max*b.min, max*b.max}; return {min(products), max(products)}; } } -
自适应精度 - 远处物体使用低精度 - 重要区域使用高精度 - 基于误差估计动态调整
-
光线差分(Ray Differentials) 追踪光线的微分信息用于:
- LOD选择
- 纹理过滤
- 自适应细分 $$\frac{\partial \mathbf{p}}{\partial u} = t \frac{\partial \mathbf{d}}{\partial u} + \frac{\partial t}{\partial u} \mathbf{d}$$
本章小结
本章介绍了光线追踪的核心概念和关键技术:
核心概念:
- 光线的参数表示:$\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}$
- 递归光线追踪实现全局光照效果
- 空间加速结构将复杂度从 $O(N)$ 降至 $O(\log N)$
关键算法:
- Möller-Trumbore 三角形相交算法
- SAH启发式用于构建最优BVH
- Slab方法用于包围盒相交测试
性能优化:
- 使用BVH或KD-Tree加速结构
- SIMD并行化相交测试
- 缓存友好的数据布局
重要公式:
- SAH代价函数:$C = C_{\text{trav}} + \sum_i \frac{A_i}{A} N_i C_{\text{isect}}$
- 透视投影光线生成
- 各种几何体的相交测试公式
光线追踪为后续的全局光照、路径追踪等高级渲染技术奠定了基础。掌握本章内容是理解现代渲染技术的关键。
练习题
基础题
练习7.1 推导正交投影相机的光线生成公式。
提示
正交投影中,所有光线方向相同,只是起点不同。考虑视图体积的定义。
答案
对于正交投影,光线方向都是 $\mathbf{d} = -\mathbf{forward}$(相机前方)。
光线起点: $$\mathbf{o}_{i,j} = \mathbf{e} + u\mathbf{right} + v\mathbf{up}$$ 其中: $$\begin{aligned} u &= \frac{2i - \text{width}}{\text{width}} \cdot \frac{\text{width}_{\text{view}}}{2} \ v &= \frac{2j - \text{height}}{\text{height}} \cdot \frac{\text{height}_{\text{view}}}{2} \end{aligned}$$ $\text{width}_{\text{view}}$ 和 $\text{height}_{\text{view}}$ 是视图体积的宽高。
练习7.2 给定光线 $\mathbf{o} = (0, 0, 0)$, $\mathbf{d} = (1, 0, 0)$ 和三角形顶点 $\mathbf{v}_0 = (2, -1, -1)$, $\mathbf{v}_1 = (2, 1, -1)$, $\mathbf{v}_2 = (2, 0, 1)$,计算相交点和重心坐标。
提示
使用 Möller-Trumbore 算法或先计算平面方程。
答案
三角形在平面 $x = 2$ 上。光线沿 $x$ 轴正方向,所以 $t = 2$。
相交点:$\mathbf{p} = (2, 0, 0)$
验证重心坐标:设 $\mathbf{p} = u\mathbf{v}_0 + v\mathbf{v}_1 + w\mathbf{v}_2$,其中 $u + v + w = 1$。
解得:$u = 0$, $v = 0.5$, $w = 0.5$
因此相交点在边 $\mathbf{v}_1\mathbf{v}_2$ 的中点。
练习7.3 证明BVH中使用SAH构建的树,期望遍历代价是最优的。
提示
考虑光线均匀分布的假设,以及条件概率。
答案
假设光线均匀分布,击中子节点的概率正比于其表面积。
期望代价: $$E[C] = P(\text{hit}) \cdot C_{\text{hit}} + P(\text{miss}) \cdot C_{\text{miss}}$$ 对于内部节点: $$E[C] = C_{\text{trav}} + P(L) \cdot E[C_L] + P(R) \cdot E[C_R]$$ 其中 $P(L) = A_L/A$,$P(R) = A_R/A$。
SAH正是最小化这个期望代价的贪心策略。通过递归论证,可以证明局部最优导致全局最优(在独立性假设下)。
练习7.4 设计一个算法,检测光线是否穿过由多个三角形组成的封闭网格。
提示
考虑奇偶规则(odd-even rule)或计算有符号体积。
答案
方法1:计数法
- 从光线起点向任意方向发射测试光线
- 计算与网格的相交次数
- 奇数次相交表示起点在内部
方法2:有符号体积法
- 对每个三角形,计算光线起点与三角形构成的四面体有符号体积
- 累加所有体积
- 非零值表示在内部
方法3:法向一致性
- 检查所有相交点处的法向与光线方向的点积符号
- 一致的符号模式可判断内外
挑战题
练习7.5 推导四叉BVH(4个子节点)相对于二叉BVH的理论优势和劣势。在什么情况下四叉BVH更优?
提示
考虑树的深度、节点访问次数、SIMD利用率和缓存性能。
答案
优势:
- 树深度减少:$\log_4 N = \frac{1}{2}\log_2 N$
- SIMD友好:可以同时测试4个包围盒
- 减少遍历开销:更少的节点访问
劣势:
- 构建复杂:需要考虑更多分割组合
- 节点更大:每个节点存储4个子节点信息
- 包围盒可能更松:难以找到最优4路分割
适用场景:
- GPU实现(SIMD宽度大)
- 场景较为均匀分布
- 内存带宽充足
理论分析:设节点访问代价为 $C_n$,包围盒测试代价为 $C_b$。
- 二叉:$C_{\text{total}} = \log_2 N \cdot C_n + 2\log_2 N \cdot C_b$
- 四叉:$C_{\text{total}} = \frac{1}{2}\log_2 N \cdot C_n + 2\log_2 N \cdot C_b$
当 $C_n \gg C_b$ 时,四叉BVH更优。
练习7.6 设计并分析一种自适应的光线追踪算法,能够根据场景复杂度动态调整采样密度。
提示
考虑图像空间的梯度、几何复杂度和着色复杂度。
答案
自适应采样策略:
-
初始稀疏采样 - 以低分辨率(如每4×4像素1个样本)进行初始采样 - 记录每个样本的:深度、法线、材质ID、颜色
-
复杂度估计
复杂度 = w1·深度方差 + w2·法线差异 + w3·颜色梯度 + w4·材质边界 -
自适应细分 - 高复杂度区域:增加采样密度 - 使用四叉树结构管理采样点 - 最大细分级别限制
-
插值重建 - 平滑区域:双线性插值 - 边缘区域:最近邻或引导滤波
-
性能分析 - 最坏情况:$O(N)$(全分辨率) - 最好情况:$O(N/16)$(4×4块) - 实际:通常节省50-80%的光线
关键优化:
- 使用GPU的分层深度缓冲
- 时间连贯性:复用前帧信息
- 并行化:基于tile的处理
练习7.7 分析光线追踪中的数值精度问题,并提出一套完整的解决方案。
提示
考虑浮点误差累积、自相交、薄物体穿透等问题。
答案
主要精度问题:
-
自相交(Shadow Acne) - 原因:相交点计算的浮点误差 - 解决:偏移光线起点
ε = 1e-4 * max(|x|, |y|, |z|) origin_offset = ε * normal -
光线起点误差传播 - 使用误差界限追踪 - Welzl的误差分析:$\delta = \gamma_n \cdot |t|$ - 其中 $\gamma_n = \frac{n\epsilon}{1-n\epsilon}$
-
薄物体穿透 - 双面测试 - 保守包围盒扩展 - 使用区间算术
-
大场景的精度损失 - 局部坐标系变换 - 分层精度表示 - 使用双精度关键计算
-
完整解决方案 ``` 结构:
- 使用相对坐标系
- 包围盒适当扩展:box.min -= ε, box.max += ε
- 相交测试使用稳定算法
算法:
- Kahan求和处理累积
- 使用有理数算术验证关键决策
- 自适应精度:远处物体降低精度要求 ```
误差分析: 总误差 ≤ 相交误差 + 变换误差 + 着色误差 典型值:~1e-3 到 1e-5 相对误差
练习7.8 探讨如何将光线追踪扩展到非欧几何空间(如球面几何或双曲几何)。
提示
考虑测地线、平行传输和曲率的影响。
答案
非欧几何光线追踪:
-
球面几何(正曲率) - 光线沿大圆路径传播 - 参数化:$\mathbf{r}(t) = \cos(t)\mathbf{o} + \sin(t)\mathbf{d}$ - 相交测试需要考虑周期性
-
双曲几何(负曲率) - 使用Poincaré球模型或双曲面模型 - 测地线方程: $$\frac{d^2x^i}{dt^2} + \Gamma^i_{jk}\frac{dx^j}{dt}\frac{dx^k}{dt} = 0$$
-
算法修改 ``` 光线传播:
- 使用数值积分求解测地线方程
- 自适应步长控制
相交测试:
- 将物体变换到局部欧式坐标系
- 或直接在曲面坐标系求解
加速结构:
- 需要考虑空间的拓扑结构
- BVH需要使用测地距离 ```
-
应用场景 - 相对论可视化 - 全景渲染 - 艺术效果
-
性能考虑 - 预计算测地线查找表 - 使用局部近似 - GPU上的并行积分器
常见陷阱与错误
1. 数值精度问题
问题:自相交导致的阴影痤疮(Shadow Acne)
错误:shadow_ray.origin = hit_point
正确:shadow_ray.origin = hit_point + ε * normal
问题:光线方向未归一化导致t值含义错误
检查:assert(|ray.direction| ≈ 1.0)
2. 性能陷阱
问题:BVH构建时未考虑空间局部性
- 使用Morton编码改善缓存性能
- 考虑节点大小与缓存行对齐
问题:过早优化导致代码复杂
- 先实现正确的算法
- 使用性能分析工具定位瓶颈
3. 算法错误
问题:BVH遍历时错误的节点访问顺序
错误:总是先访问左子节点
正确:先访问距离较近的子节点
问题:忽略了背面剔除的边界情况
- 透明物体需要双面测试
- 法线翻转的处理
4. 边界情况
问题:退化三角形(共线顶点)导致除零错误
- 预处理移除退化几何
- 相交测试中添加检查
问题:极小或极大的场景规模
- 使用相对误差而非绝对误差
- 考虑分层LOD
最佳实践检查清单
设计阶段
- [ ] 选择合适的加速结构(BVH vs KD-Tree)
- [ ] 确定精度要求和误差容限
- [ ] 规划内存布局和缓存优化策略
- [ ] 考虑目标平台(CPU/GPU)特性
实现阶段
- [ ] 光线方向始终保持归一化
- [ ] 正确处理数值精度问题
- [ ] 实现鲁棒的相交测试算法
- [ ] 添加适当的debug可视化
优化阶段
- [ ] 使用性能分析确定瓶颈
- [ ] 实现SIMD加速的相交测试
- [ ] 优化内存访问模式
- [ ] 考虑光线包(Ray Packet)技术
验证阶段
- [ ] 测试各种退化情况
- [ ] 验证数值稳定性
- [ ] 比较不同场景下的性能
- [ ] 确保结果的确定性(用于调试)
维护阶段
- [ ] 文档化关键算法选择
- [ ] 提供性能调优参数
- [ ] 支持渐进式渲染
- [ ] 保持代码的可扩展性