光线追踪与混合渲染:BVH、路径追踪与 RTX

光线追踪按物理规律反向追踪光子路径,能天然产生反射、折射、软阴影与全局光照,是画质的终极答案,但计算量巨大。本文讲解光线求交数学、BVH 加速结构、Whitted 与路径追踪的差异、DXR/RTX 硬件光追架构、光栅化与光追的混合渲染策略、实时降噪(TAA/时序),以及生产级光追管线的工程权衡。

光栅化擅长"把三角形画到屏幕上",却在反射、折射、软阴影与全局光照(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 两者对比

特性WhittedPath 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%
AO1~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 验证帧率。实时光追的工程门槛高,但知识路径清晰——数学、硬件、采样、降噪四块逐一攻克即可。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机图形学」更多文章

  1. 骨骼动画与蒙皮:骨骼层级、动画混合与 GPU 蒙皮
  2. 大地形与开放世界渲染:LOD、分块与实例化
  3. 后处理特效管线:HDR、Bloom、色调映射与抗锯齿