GPU Gems 3 · Chapter 17. Robust Multiple Specular Reflections and Refractions
从分层距离 cubemap 出发,解释如何用 ray marching、secant search、Fresnel 和折射率在 GPU 上稳定追踪多阶反射与折射。
学习目标
- 能解释为什么 fragment shader 需要分层距离地图来代替直接访问场景网格,并区分 ray marching 与 secant search 的职责
- 能修改 Robust Secondary Ray Lab 的路径类型、最大深度、地图层数、搜索加速和步长,观察命中可信度与实时预算
- 能回答:为什么 Fresnel、折射率和提前终止必须同时参与多阶反射/折射循环,而不是只把环境 cubemap 采样几次
先问:为什么一个反射 cubemap 不够
想象一个玻璃球放在镜子前:球面会看到镜子,镜子里又会看到球;如果球还是透明的,光还要继续穿过表面。只保存“从相机直接看见的颜色”,这些被遮挡的交点就永远不存在。
本章解决的是:怎样把场景中可搜索的几何信息放进 GPU 可访问的纹理,再沿反射和折射方向找到第二、第三甚至更高阶的命中点。没有这条路径,反射只能停在第一跳,透明物体会把背景当作一张不随几何变化的贴图。
1. 把场景表面存进分层距离地图
↡以一个参考点为中心,把每个方向上可见表面的距离、法线和材质存入多个 cubemap 层的场景采样表示。fragment shader 能查纹理,却不能直接遍历完整的三角网格。因此先从参考点向六个 cubemap 方向采样场景,把每个 texel 的距离、命中法线和材质信息写入纹理。普通深度 cubemap 只保存第一层;反射物体前后或被遮挡的表面则需要额外层。
分层的意义不是把场景变成精确体素,而是让二次光线拥有一个可查询的“几何近似”。距离使用世界空间欧氏距离时,方向与距离可以一起描述命中点,也能减轻 cubemap 面之间插值时的边界特殊处理。空 texel 可以写入负距离,后续搜索时明确识别为无效。
简单的反射物体可以把参考点放在自身中心,分别渲染前后表面得到两层;复杂场景则需要 depth peeling 控制层数,把更远的表面合并到有限的层里。层数越多,遮挡证据越完整,但每帧更新纹理和内存成本也越高。
↡通过多次深度剥离逐层取出当前最近表面,把被前层遮挡的表面写入后续距离地图层。2. 先不漏交点,再把交点找准
↡沿二次光线按固定步长逐步读取距离地图,寻找射线与采样表面之间的符号变化或穿越区间。从当前表面点 x 沿反射或折射方向 R 走,先用 ray marching 逐步查询距离地图。步长不能大于相邻 texel 所能表达的距离,否则细薄物体可能完整地落在两个采样点之间而被跳过。它的职责是建立安全的命中区间,而不是在大步进阶段追求最终精度。
找到一对距离符号相反或跨越表面的样本后,再切换到 ↡在已知命中区间内用两端函数值估计交点,并反复收缩区间以获得更精确命中的搜索方法。。它比继续用很小的固定步长更快,也能把命中位置收敛到足以支撑法线和颜色查找的精度。
两阶段的边界要清楚:ray marching 负责“不要漏”,secant search 负责“不要糊”。当距离值变化非常剧烈时,要把线性步长调细;当表面平滑且 texel 代表的距离变化缓慢时,可以用更大的步长换取速度。
3. 在命中点分配反射与折射能量
↡描述光在界面上被反射的比例;它随入射角变化,掠射角通常反射更多能量,也可以用金属的基准反射率近似。命中点有法线 N、视线方向 V 和材质数据。理想反射使用 reflect 生成新的方向;透明表面使用 refract,并需要 ↡光在真空中的传播速度与在当前介质中传播速度之比;它决定折射方向和透射路径。 决定弯折程度。Fresnel term 用角度把能量分成反射部分 F 与透射部分 1 − F。
金属可以用基准反射率、粗略的 extinction 相关参数和 Schlick 风格近似;玻璃或水则需要正的折射率。工程上还可以用折射率的符号区分路径类别:负值表示 reflector,零表示 diffuse 终点,正值表示 refractor。这样动态循环不用额外的材质枚举也能选择下一条路径。
不要把 Fresnel 当作最后的颜色滤镜。它会影响下一次路径的 radiance;如果命中点又是镜面或透明表面,就要携带新的方向、法线、折射率和能量继续追踪。
4. 用有限循环追踪多阶路径
每次命中后,循环都要更新四类状态:当前点与方向、路径辐射量、当前材质的 Fresnel 参数、已经走过的深度。命中 diffuse 表面时把它的 radiance 加入结果并停止;命中 reflector 或 refractor 时重新计算方向并继续。
float3 radiance = float3(1, 1, 1);
float3 direction = normalize(viewDirection);
for (int depth = 0; depth < MAX_DEPTH; ++depth) {
float3 hitNormal, hitRadiance;
float hitDistance;
if (!Hit(distanceMaps, origin, direction, hitDistance, hitRadiance, hitNormal)) break;
float3 F = Fresnel(hitNormal, direction, materialFp0);
if (materialIor == 0) { radiance *= hitRadiance; break; }
bool mirror = materialIor < 0;
direction = mirror ? reflect(direction, hitNormal) : refract(direction, hitNormal, materialIor);
radiance *= mirror ? F : (1.0 - F);
origin += direction * hitDistance;
}
return radiance;实际 shader 还需要处理路径离开 cubemap、命中无效 texel、全黑 radiance 和动态分支支持。MAX_DEPTH 是正确性护栏,也是性能旋钮;增加它不一定改善画面,因为更高阶路径可能已经低于可见阈值。
5. 用 min-max 和层数做工程预算
复杂场景每帧更新多层 cubemap 会很贵。min-max pair 可以为每层距离数据保存上下界,光线先跳过不可能相交的区间,再把剩下的区间交给精确搜索。它不是放宽命中标准,而是减少明显不可能命中的 texture lookup。
实现时要同时看三个预算:层数控制被遮挡表面的完整性,线性步长控制薄物体漏检概率,secant 迭代次数控制命中精度。简单物体可用两层前后表面;镜子、玻璃和复杂遮挡使用更多层;远处或小屏幕占比物体则可以降低层数和最大深度。
6. 三步把二次光线接入实时管线
先生成可搜索的分层场景表示
选择参考点,写入方向 cubemap 中的距离、法线和材质;简单对象使用前后两层,复杂遮挡再用 depth peeling 增加层数,并标记无效 texel。
Robust Secondary Ray Lab
让二次光线既不漏,又能实时跑
调整路径类型、最大光线深度、距离地图层数、搜索加速和步长,比较命中可信度、路径亮度、纹理工作量与薄物体漏检风险。
hit confidence
69%
path radiance
61%
texture work
48 units
thin-object risk
24%
ray marching 负责不漏交点,secant search 负责把命中点收准。这些读数是解释算法取舍的稳定示意,不是特定硬件的 benchmark。
本章小结
- 分层距离地图把被遮挡表面变成 fragment shader 可查询的几何近似
- ray marching 负责不漏交点,secant search 负责提高交点精度
- Fresnel 和折射率共同决定反射/透射方向与能量
- 多阶路径必须有最大深度、能量和无效命中终止条件
- 层数、步长、min-max 与屏幕占比共同决定实时预算
练习
问题 1|修改 Demo 代码。 当前命中搜索只使用固定大步长。请增加“找到命中区间后再细化”的阶段,并说明如何减少薄物体漏检。
for (int i = 0; i < MAX_STEPS; ++i) {
float distance = SampleDistance(rayOrigin + rayDir * t);
if (distance < 0) return t;
t += STEP_SIZE;
}问题 2|诊断反射/折射能量错误。 玻璃球的反射和透射都很亮,且掠射角没有明显变亮。你会检查哪些输入和更新?
问题 3|场景选型。 近距离玻璃球、远处金属装饰和包含细电线的镜面场景分别怎样选择层数、步长和最大深度?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- layered distance map
- depth peeling
- ray marching
- secant search
- Fresnel term
- index of refraction