光栅化擅长"把三角形画到屏幕上",却在反射、折射、软阴影与全局光照(GI)上捉襟见肘——这些效果只能靠各种 hack 近似(SSR 屏幕空间反射、级联阴影、Light Probe)。光线追踪则反过来:从每个像素发射光线,追踪其在场景中的弹跳路径,直接按光学规律计算颜色——物理正确,但昂贵。NVIDIA RTX 系列将 BVH 遍历与求交硬件化后,实时光追从实验走入工业。本文从数学出发,讲到硬件、混合策略与降噪,构建完整的光追知识地图。
一、光线求交
一句话:光线追踪的最基本操作是"光线与三角形求交"——一条参数化光线遍历场景所有三角形找最近交点,现代加速结构让这个"所有"变成"极少数"。
1.1 光线表示
光线用原点与方向参数化:R(t) = O + t·D,t >= 0。与三角形求交常用 Möller–Trumbore 算法:用重心坐标求 t 与重心坐标 (u, v):
O + t·D = (1 - u - v)·V0 + u·V1 + v·V2
t = dot(Q, E1) / dot(P, E2), P = D × E2, Q = (O - V0) × E1
u = dot(P, (O - V0)) / dot(P, E2)
v = dot(Q, D) / dot(P, E2)
其中 E1 = V1 - V0、E2 = V2 - V0。当 0 < u, v, u+v < 1 且 t > 0 时命中。
// GLSL:Möller–Trumbore 三角形求交
bool intersectTriangle(vec3 O, vec3 D,
vec3 v0, vec3 v1, vec3 v2,
out float t, out float u, out float v) {
vec3 e1 = v1 - v0, e2 = v2 - v0;
vec3 p = cross(D, e2);
float det = dot(e1, p);
if (abs(det) < 1e-8) return false; // 平行/退化
float invDet = 1.0 / det;
vec3 s = O - v0;
u = dot(s, p) * invDet;
if (u < 0.0 || u > 1.0) return false;
vec3 q = cross(s, e1);
v = dot(D, q) * invDet;
if (v < 0.0 || u + v > 1.0) return false;
t = dot(e2, q) * invDet;
return t > 0.0;
}
1.2 求交的变体
- AABB 求交(slab 方法):包围盒测试用三个轴对齐平面组(slab),返回 t_min/t_max,是 BVH 遍历的基础。
- Embree:Intel 的开源光线内核,包含高度优化的 SIMD 求交与 BVH 构建,是影视渲染(如 Cycles、Arnold)与众多实时引擎的底层库。
二、加速结构:BVH
一句话:裸遍历是 O(N)(N=三角形数),BVH 把三角形递归分到包围盒树里,一次求交降到 O(log N),场景越大收益越夸张。
2.1 Bounding Volume Hierarchy
BVH(包围体层次结构)是一棵二叉树:叶子节点装少量三角形(通常 1~4 个),内部节点存其子树的 AABB。求交时自顶向下:若光线与节点 AABB 不相交则剪掉整棵子树;相交则递归子节点。由于 AABB 求交极廉价,光线实际只与路径上的少数 AABB 和叶子的少数三角形求交。
遍历示例(光线 r):
root AABB 命中
├─ L: AABB 未命中 → 整棵剪掉(数百三角形免测)
└─ R: AABB 命中
├─ ... 递归 ...
└─ 叶子: 与 4 个三角形求交,记录最近 t
2.2 构建策略
BVH 构建的经典策略:
- SAH(Surface Area Heuristic,表面积启发):以"光线命中概率 ∝ 包围盒表面积"为代价函数,贪心选择分割位置最小化期望遍历代价。实时构建首选,质量高。
- Median / Equal count:按最长轴中位数切分,构建 O(N log N) 但质量略逊。
- LBVH(Linear BVH):基于 Morton 码排序 + 并行构建,GPU 友好,常用于动画场景的逐帧重建。
2.3 GPU/硬件加速结构:BLAS + TLAS
实时 API(Vulkan KHR / DXR)要求两级结构:
- BLAS(Bottom-Level AS):每个网格一个,装三角形/包围盒,含顶点与索引(只读只重建,可做紧凑化 Compaction)。
- TLAS(Top-Level AS):由多个 Instance 组成,每个 Instance 引用一个 BLAS 并带变换矩阵(支持矩阵动画实例)。
// C++/Vulkan:TLAS 实例描述
VkAccelerationStructureInstanceKHR instance = {};
instance.transform = toTransformMatrix(modelMatrix); // 模型矩阵
instance.instanceCustomIndex = 0; // 可自定义索引(材质 ID)
instance.mask = 0xFF; // 实例掩码(可裁剪/碰撞组)
instance.instanceShaderBindingTableRecordOffset = 0; // SBT 偏移
instance.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR;
instance.accelerationStructureReference = blasDeviceAddress; // 引用的 BLAS 地址
实例化复用:大量相同网格(森林)共享同一 BLAS,TLAS 每帧只需重建少量实例变换(或部分更新),这是光追场景高性能的关键。
三、Whitted 光追 vs 路径追踪
一句话:Whitted 光追只沿镜面方向递归产生反射/折射,是"特效级"近似;路径追踪用蒙特卡洛在半球随机采样完整光传输路径,是"物理级"真实——也是影视渲染的标准。
3.1 Whitted Ray Tracing(递归镜面)
1980 年 Turner Whitted 提出:光线击中表面时,除直接光照外,沿反射方向与折射方向各发射一条光线递归追踪,直到最大深度(5~8 层)或未命中。效果:完美镜面反射、玻璃折射、硬阴影。缺点:
- 只处理镜面/折射,漫反射表面仍是直接光 + 环境近似;
- 深度固定,远处镜面链依然昂贵;
- 每个分支要么全有要么全无,产生"爆点"(fireflies)与硬边。
3.2 Path Tracing(路径追踪,蒙特卡洛)
路径追踪在漫反射表面也按 BRDF 重要性采样随机发射光线,用蒙特卡洛积分估计辐射度:
L_o = Σ ( BRDF × L_i × (N·L) ) / pdf,逐 bounce 累积
每个像素发射 N 条路径(每路径多次弹跳),期望收敛到无偏真实解。参数:
- 每像素采样数(SPP):SPP=1 时噪声极大;影视离线通常 SPP=1000~10000 收敛。
- 俄罗斯轮盘赌(Russian Roulette):按路径能量随机终止,保证无偏且终止高效。
- Next Event Estimation(NEE,直接光采样):每个顶点额外直接对光源采样,大幅降低噪声。
3.3 两者对比
| 特性 | Whitted | Path Tracing |
|---|---|---|
| 物理正确性 | 近似(镜面特例) | 无偏(MC 积分) |
| 噪声 | 少(确定性分支) | 多(随机采样) |
| 漫反射 GI | 不支持 | 天然支持 |
| 采样成本 | 递归镜面链 | 全路径 MC |
| 应用 | 实时光追起步、特效 | 离线渲染(Cycles/Arnold) |
一句话:实时光追在"Whitted 式确定性 + 蒙特卡洛采样 + 降噪"之间折中:对每个像素发射 1 条(或 2 条)光线 + 复用光栅化的 G-Buffer 做 BRDF 评估,然后交给时序降噪器。
四、DXR / RTX 硬件光追
一句话:RTX 的核心是把 BVH 遍历与三角形求交做成硬件单元(RT Core),着色器专注"命中了怎么着色",光线发射/遍历/命中以固定功能极速完成。
4.1 硬件加速单元
NVIDIA RTX 引入 RT Core(AMD RDNA2 的 Ray Accelerator 同理):光线与 AABB 遍历、三角形求交在硬件管线中执行,吞吐量比纯着色器软件求交高一个数量级(RTX 20 系列起)。每颗 RT Core 与 CUDA Core 协同:CUDA Core 执行着色器,RT Core 执行加速遍历。
4.2 光追着色器组合(Ray Tracing Pipeline)
与图形管线类似,光追管线由可编程阶段 + 固定功能阶段组成:
| 着色器类型 | 触发时机 | 职责 |
|---|---|---|
| Ray Generation | 每像素一次 | 计算相机光线,调 traceRay(),写输出 |
| Closest Hit | 光线命中最近三角形 | 执行 BRDF、发射次级光线 |
| Any Hit | 命中任意三角形 | 透明剔除、alpha 测试、阴影开关 |
| Miss | 未命中任何几何 | 采样天空盒 / 环境 |
| Intersection | 自定义图元 | 非三角形求交(AABB、球、SDF) |
// GLSL(Ray Generation Shader):逐像素发一条光线
layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba32f) uniform writeonly image2D outputImage;
layout(location = 0) rayPayloadEXT vec3 hitColor;
void main() {
vec2 uv = vec2(gl_LaunchIDEXT.xy) / vec2(gl_LaunchSizeEXT.xy) * 2.0 - 1.0;
// 从相机出发:origin + 方向(可由 G-Buffer 重建,做混合渲染)
vec3 origin = uCamPos;
vec3 dir = normalize(uCamToWorld * vec4(uv, 1.0, 0.0)).xyz;
traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xFF, 0, 1, 0,
origin, 0.001, dir, 1000.0, 0);
imageStore(outputImage, ivec2(gl_LaunchIDEXT.xy), vec4(hitColor, 1.0));
}
4.3 SBT(Shader Binding Table)
光追允许每个实例/每材质选择不同着色器,通过 SBT(Shader Binding Table):一张"着色器句柄 + 参数"表,traceRay 用 record offset 索引。因此不同材质(金属/玻璃/皮肤)可用不同 Closest Hit 着色器,这是光追管线比固定管线灵活的核心机制。
// C++/Vulkan:SBT 记录布局
// | RayGen | Miss(1) | HitGroup(材质0) | HitGroup(材质1) | ...
// traceRay 的 sbtRecordOffset = 命中实例的材质索引
五、混合渲染:光栅化 + 光追
一句话:全路径追踪实时化太贵,工业界用"光栅化算主体,光追补特效"——G-Buffer 提供几何/材质,光追只解决反射、阴影、AO、GI 中光栅化无能为力的部分。
5.1 混合策略架构
现代引擎(UE5 Lumen、Metro Exodus、Cyberpunk)的典型混合管线:
光栅化:不透明场景 → G-Buffer(位置/法线/材质/深度)+ 直接光照
光追: ① 反射:对像素发射 1 条反射光线,命中处用材质着色
② 阴影:对光源发射 1 条阴影光线(替代/补充 CSM)
③ AO:发射少量 AO 光线评估环境遮蔽
④ GI:与 G-Buffer 的 SVGF/ReSTIR 结合做全局光
降噪:时序累积 + 空间滤波 → 高质量低噪声结果
每个光追效果都瞄准一个光栅化痛点:
| 光栅化近似 | 痛点 | 光追替代 |
|---|---|---|
| SSR 屏幕空间反射 | 屏幕外/背面无反射 | 光追反射(任意方向、可多次弹跳) |
| CSM 级联阴影 | 精度/级联接缝 | 光追阴影(每像素精确) |
| SSAO 屏幕空间 AO | 仅屏幕空间、接触阴影 | 光追 AO(场景级、可长距离) |
| Light Probe / 探针 GI | 粗糙近似、漏光 | Lumen / 光追 GI |
5.2 分辨率与成本分配
光追 pass 通常半分辨率或 1/4 分辨率计算,再上采样(FSR/DLSS),因为降噪后的信息可以重建高频。成本分配示例(RTX 30 系中端):
| 光追效果 | 光线数/像素 | 分辨率 | 帧预算占比 |
|---|---|---|---|
| 反射 | 1~2 | 半分辨率 | 15%~25% |
| 阴影 | 1 | 全分辨率 | 5%~10% |
| AO | 1~4 | 半分辨率 | 5%~10% |
| GI (Lumen) | 复用时序 | 低分辨率 | 20%~35% |
5.3 光追开关与降级
混合渲染必须提供逐级降级:Ultra(全光追)→ High(反射+阴影)→ Medium(仅阴影/AO)→ Low(纯光栅化 + CSM + SSR)。每个效果单独开关、单独预算,玩家根据硬件裁剪。
六、实时降噪:TAA 与时序滤波
一句话:实时光追每像素只发 1~2 条光线,原始结果是"噪点海洋";降噪器用"时序累积(复用历史帧)+ 空间滤波(边缘保持)“把它变成干净画面。
6.1 为什么必须降噪
SPP=1 的路径追踪噪声巨大,直接输出无法观看。降噪器分两类:
- 时序降噪(Temporal):用运动向量把当前帧结果对齐到历史帧并混合,等效 SPP 随帧数累积。
- 空间降噪(Spatial):在同一帧内对邻域做边缘感知滤波(法线/深度/位置加权),保留几何边。
6.2 经典降噪器
| 降噪器 | 方法 | 特点 |
|---|---|---|
| SVGF(Spatiotemporal Variance-Guided Filtering) | 方差引导的时序+空间滤波 | 开源、质量高、实时 |
| ReBLUR(OIDN) | 时空间结合 + 置信度 | Intel 开源,影视级 |
| 简单 TAA + 双边滤波 | 时序混合 + 双边模糊 | 引擎标配、便宜但易鬼影/糊 |
核心思路(SVGF 简化):
// 伪代码:SVGF 式降噪核心
// 1. 运动向量重投影历史帧
vec2 historyUV = uv - motionVector;
vec3 history = sampleHistory(historyUV);
// 2. 方差估计:用 3x3 邻域的均值/方差决定滤波强度
float variance = computeNeighborhoodVariance(uv);
float filterStrength = estimateFromVariance(variance, sampleCount);
// 3. 边缘感知空间滤波:法线/深度/位置差异作为权重
vec3 result = bilateralFilter(history, current, normal, depth, filterStrength);
// 4. 时序混合:颜色钳制抑制鬼影
history = clamp(history, neighborhoodAABBMin(uv), neighborhoodAABBMax(uv));
result = mix(current, history, 0.9 * confidence);
6.3 降噪陷阱
- 鬼影(Ghosting):运动物体残影——用邻域 AABB 钳制历史 + 置信度衰减。
- 糊化(Over-blur):高频细节被滤掉——用方差引导的可变滤波半径(低方差少滤波)。
- 闪烁(Flicker):LOD/物体切换——与 TAA 共享同一运动向量基础设施,统一处理。
- 半透明:光追与降噪对半透明极难——通常单独处理或关闭光追于半透明。
6.4 重采样与 ReSTIR
高端方案 ReSTIR(Reservoir-based Spatiotemporal Importance Resampling)用水库采样在时空间重用路径,实现"每像素 1 条光线却有 1000 SPP 等效质量"的 GI/阴影。它通过维护每像素的候选光线水库 + 时空间重采样,是实时 GI 的前沿标准(Lumen 的核心)。
七、生产级光追管线
一句话:上生产要解决的不只是"能跑”,还有构建开销、内存、跨平台与调试——BLAS 复用、动态场景处理、TLAS 部分更新都是必修课。
7.1 构建开销管理
- 静态网格:BLAS 只在加载/网格变化时构建,构建后可**紧凑化(Compaction)**减少显存。
- 动态网格:每帧重建 BLAS 代价高——优先用 TLAS 实例变换表达动画(实例矩阵可每帧更新);必须形变的网格才重建 BLAS,且用 LBVH 并行构建。
- TLAS:每帧重建 TLAS 是常规操作(涉及实例变换更新),但实例列表稳定时可增量更新(
VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT)。
// C++/Vulkan:光追构建流程(每帧)
// 1. 更新实例缓冲(模型矩阵)
// 2. vkCmdBuildAccelerationStructuresKHR(TLAS, instanceBuffer)
// 3. 分配 scratch buffer(构建临时内存,复用)
// 4. traceRay 前加 memory barrier:AS 可见性
7.2 内存与预算
| 资源 | 量级参考(中型场景) |
|---|---|
| BLAS 顶点/索引 | 与光栅化 VB 共享(Buffer Device Address) |
| BLAS 加速结构 | 约 1.5~2× 顶点数据 |
| TLAS | 每帧小(实例数 × 64 B + 少量节点) |
| SBT | 材质数 × 64 B |
| 降噪中间缓冲 | 半分辨率 float 若干张 |
共享技巧:光追的顶点/索引缓冲直接复用光栅化的顶点缓冲(通过 Buffer Device Address),不复制数据,是内存优化的关键。
7.3 跨平台与 API
- Vulkan
VK_KHR_ray_tracing_pipeline:可编程、跨厂商(NVIDIA/AMD/Intel)。 - DirectX 12 DXR:微软标准,与 Vulkan 语义对应(TLAS/BLAS、RayGen/CH/AnyHit/Miss)。
- Metal Ray Tracing(Apple):Apple Silicon 光追加速,Metal 3 API。
- WebGPU:
VK_KHR_ray_tracing在 Web 端尚未标准化(作为扩展探索中),目前 Web 光追走软件(Three.js 示例)或原生插件。
7.4 调试要点
- AS 可视化:RenderDoc/Nsight 可查看 BVH 节点命中次数,定位遍历热点。
- 分步验证:先跑纯光追反射(无降噪)看原始噪声;再加降噪看糊化;再加光栅化混合看接缝。
- 射线调试:对特定像素高亮光追路径(黄色路径可视化),快速定位"反射里多出来的物体"。
八、常见问题(FAQ)
Q1:光追反射里有"黑缝/漏光",哪里出了问题?
A1:优先检查 TLAS 实例的 mask 与透明处理:alpha 测试物体若未走 Any-Hit 剔除,会在反射里产生洞;而漏光常见于 Instance 变换矩阵与光栅化模型矩阵不一致(TLAS 每帧更新时用了旧矩阵)。再检查 Closest-Hit 的 BRDF 是否对法线朝向做了正确处理(背面命中应走 Miss 或黑色)。
Q2:降噪后画面糊成一片,保留不了细节?
A2:先确认是否对"原始噪点"直接做空间滤波——正确顺序是时序累积(复用历史帧)为主、空间滤波为辅。糊化通常来自:运动向量错误导致历史帧重投影错位、方差估计过高触发大半径滤波、或滤波时权重没有区分法线/深度边缘。把历史混合权重调高、把空间滤波半径绑定到方差,可显著保留细节。
Q3:动态物体每帧重建 BLAS 太贵,怎么优化?
A3:能表达为刚体运动的物体(平移/旋转/缩放)一律用 TLAS 实例变换,不要重建 BLAS;只有骨骼形变等必须逐顶点变化的网格才重建。对必须重建的网格,用 LBVH 并行构建 + 双缓冲(前一帧构建与当前帧遍历重叠)。静态场景的 BLAS 构建后做 Compaction 省显存,且构建一次后完全复用。
Q4:混合渲染里光追反射与 SSR 接缝明显,如何过渡?
A4:这是混合渲染的经典难题。方案:用 SSR 结果 + 光追作为"补漏"(光追只覆盖 SSR 失效区,如屏幕外/背面),接缝处用粗糙度/距离加权渐变过渡(粗糙度大、距离远时更信任 SSR)。同时统一两套方案的运动向量与降噪参数,让时序累积在同一空间工作,接缝会随帧数收敛消失。
总结
光线追踪从 1980 年的 Whitted 递归,到 2018 年 RTX 硬件化,再到今天的 Lumen/ReSTIR 混合管线,经历了"学术 → 硬件 → 工业"三阶段:
| 层次 | 核心技术 | 核心权衡 |
|---|---|---|
| 求交数学 | Möller–Trumbore、AABB slab | 精度 vs 速度 |
| 加速结构 | SAH BVH、BLAS/TLAS | 构建质量 vs 构建开销 |
| 采样策略 | Whitted 确定性 / Path Tracing MC | 噪声 vs 物理正确 |
| 硬件 | RT Core、DXR/VK_KHR_ray_tracing | 固定功能遍历 vs 可编程着色 |
| 混合渲染 | 光栅化 + 光追分工 | 质量 vs 帧预算 |
| 降噪 | SVGF/ReSTIR 时序重采样 | 干净 vs 鬼影/糊化 |
实践建议:先写一个 CPU 上的路径追踪器(几百行,含 BVH 与 MC 积分)真正理解收敛与噪声;再用 Vulkan/DXR 跑通"发一条反射光线 + 降噪"的最小混合管线;最后按预算表逐效果开关、用 profiler 验证帧率。实时光追的工程门槛高,但知识路径清晰——数学、硬件、采样、降噪四块逐一攻克即可。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。