GPU Gems 1 · Chapter 2. Rendering Water Caustics

从水面法线与反向折射出发,用多遍渲染和着色器生成实时水焦散。

学习目标

  • 能解释水焦散为何由曲面折射造成,并区分前向光子追踪、反向追踪与本章的美学近似
  • 能从波面高度函数的两个偏导构造法线,并用线面相交得到海底采样点
  • 能按三遍渲染流程实现海底底图、焦散叠加与水面反射,并说明屏幕空间与纹理空间的取舍
  • 能调整水深、波面坡度与焦散图分辨率,预测光斑会如何改变并用实验验证

为什么水底会出现焦散

先想象一块起伏的玻璃压在手电筒前面:光线经过平面玻璃只会整体偏移,经过弯曲表面却会被分散到不同方向;一些邻近光线恰好落到同一片地面,就形成亮斑。水焦散(caustics)就是这种由曲面反射或折射造成的光线集中,亮斑会随波面移动。

原书的目标不是建立昂贵的海洋光学仿真,而是用一个实时可行的、视觉上可信的模型。先预测:如果把水面坡度调大,你觉得海底亮斑会更均匀,还是会更集中?答案会在后面的法线、折射方向和实验里逐步出现。

水面上方海底平面 z = D波面垂直投影反向折射光线波面法线n = 1.33越接近垂直入射,太阳盘贡献越集中,焦散越亮
不是追踪全部光子,而是从地面采样点反向寻找最可能贡献焦散的光线。

1. 从完整光线输运退到可计算近似

严格计算焦散需要从太阳发射大量光子,逐次追踪它们与水面的交互,最后统计每个海底小区域收到的能量。前向光子追踪直观却浪费严重:多数光子碰不到水面,碰到水面的光子中又只有少数真正贡献给某个焦散亮斑。

正是从太阳向下发射;则从海底点出发向上猜测候选光线。后者可以用蒙特卡洛采样估计积分,但仍要丢弃大量没有打到光源的假设光线。

本章选择反向思路,却进一步采用美学近似:假定中午、太阳在正上方,海底采样点先垂直投影到水面;再由波面法线反向产生一条候选折射光线。由于太阳盘距离极远,候选方向越接近垂直,越可以把它看作获得了更多太阳能量。

水的折射率设为约 1.33。这不是把整套光学公式藏起来,而是让实现集中在“波面如何改变候选方向”这一视觉主因上。真正的物理准确度与实时预算之间,正是本章需要读者能说清楚的工程边界。

2. 用波面法线找到候选光线

设水面高度是 z = f(x, y, t),在某个海底顶点 p₀ 上方的水面点记为 p。水面沿两个水平轴的变化率分别是 fₓfᵧ把“波面往哪边上升”变成可计算的斜率,于是未归一化的表面方向可写为:

N=(fx,fy,1),N=NN.N' = (-f_x, -f_y, 1), \qquad N = \frac{N'}{\lVert N'\rVert}.

其中 N 是波面改变光线方向的关键。若原来的波函数由多层余弦或正弦叠加而成,就可以按链式法则逐层计算 fₓfᵧ;GPU 不需要先把整张高度图读回 CPU。

三遍渲染:地面 → 焦散 → 水面1地面底图先画普通海底GroundMap2焦散叠加细网格写入光照LightMap × GroundMap3水面反射最后画波面WaterMesh最终颜色 = 海底底色 + 焦散光照
本章的低成本核心:把复杂光线输运压缩成海底上的一次加法叠加。

斯涅尔定律在这里扮演什么角色

写成 η₁ sin θ₁ = η₂ sin θ₂,其中 η 是介质折射率。原书的 OpenGL 版本可以沿法线和折射率构造反向折射线;着色器版本则发现,用“沿波面法线到海底的截距”做环境映射,视觉上已非常接近完整折射计算,而且更便宜。

这个取舍很值得记住:不是因为折射定律不重要,而是因为远处太阳盘近似、垂直入射假设和水底距离共同决定了最终亮度;把可见差异很小的步骤合并,才能把预算留给每像素细节。

3. 线面相交把方向变成纹理坐标

法线给出方向,却还没有给出海底上哪个纹素应该变亮。把候选光线写成:

p(t)=p0+tv,p(t) = p_0 + t v,

把海底平面写成 n · p = D,则交点距离为:

t=Dnp0nv,phit=p0+tv.t = \frac{D - n\cdot p_0}{n\cdot v}, \qquad p_{hit}=p_0+t v.

这里 把方向 v 转成 p_hit;再将 p_hit.xy 归一化,就可以访问太阳光图。n · v 接近零时,光线几乎平行于海底,应该降低贡献或走保护分支,不能直接除零。

Caustics Lab

先预测,再改变一个光线条件

可交互
波面海底:颜色越亮表示候选光线越集中

观察:坡度增加会让亮斑更集中;水越深,传输衰减越明显。

实验操作顺序:先预测坡度滑杆从 0.24 增大到 0.45 后,亮斑是否变得更尖;再只改变一个滑杆,观察亮度网格和候选光线的变化。最后点“重置实验”,确认你看到的是参数变化,而不是视图残留。

4. 三遍渲染:把焦散叠加到海底

原书的基础 OpenGL 实现采用多遍绘制。第一遍以普通纹理四边形绘制海底;第二遍使用与波网格粒度相近的细网格,为每个顶点计算焦散值并做加法叠加;第三遍绘制水面波形,并用平面环境映射得到天空反射。焦散值可以从 0(没有集中光线)到 1(高度集中)理解。

在第二遍中,每个海底顶点执行以下链条:

  1. 从海底点向上发出垂直射线,和波面相交。
  2. 用相交点的法线与水的折射率构造反向折射方向。
  3. 让折射方向与海底平面求交,得到 p_hit
  4. p_hit 计算太阳光图坐标,并把焦散值写入细网格顶点颜色。

这种 只负责把光照贡献叠到海底底色上,不负责重新计算海底材质。水面反射仍是独立的第三遍,因此调焦散强度时不会意外改变水面几何。

5. 从顶点计算迁移到着色器

把同一波函数复制到 HLSL 或 Cg 等高层着色语言后,可以将焦散从逐顶点提升为逐像素计算。这样视觉质量不再强依赖细网格密度:每个像素都可以按自身位置计算波面梯度,再得到线面交点。

一个压缩后的实现骨架如下。代码只保留因果链,真实项目还应加入参数缓冲、边界保护、颜色空间和深度衰减:

float2 gradWave(float x, float y, float time);
 
float3 linePlaneHit(float3 p, float3 v, float3 planeN, float planeD) {
  float denominator = dot(planeN, v);
  float distance = (planeD - dot(planeN, p)) / denominator;
  return p + v * distance;
}
 
float4 shadeFloor(VertexOut input, sampler2D lightMap,
                  sampler2D groundMap, float time) {
  float2 slope = gradWave(input.position.x, input.position.y, time);
  float3 candidate = normalize(float3(slope.x, slope.y, 1.0));
  float3 hit = linePlaneHit(input.position.xyz, candidate,
                            float3(0, 0, 1), -0.8);
  float3 caustic = tex2D(lightMap, hit.xy * 0.8).rgb;
  return float4(tex2D(groundMap, input.uv).rgb + caustic, 1.0);
}

这里的 是性能与画质的交汇点:hit.xy 不是静态 UV,而是由当前波面、法线和深度共同决定的坐标。

6. 屏幕空间还是纹理空间

屏幕空间只为当前可见像素计算焦散,因此小窗口或局部水底很划算;但可见面积变大时,每帧工作量也随之膨胀。纹理空间把焦散写入固定分辨率的 render target,成本更稳定,还能利用自动 mipmap 生成与各向异性过滤降低混叠;代价是不可见区域也会被计算,并且分辨率选得过低时会显出双线或十字纹理。

屏幕空间与纹理空间屏幕空间纹理空间只算当前可见像素固定目标,可生成 mipmap选择依据:可见范围、目标分辨率、过滤与带宽预算
屏幕空间节省不可见像素;纹理空间换来稳定成本和更容易过滤的焦散图。

工程上可以按三个问题选方案:焦散覆盖多少屏幕?需要多少高频细节?过滤和带宽预算能否支撑目标分辨率?不要把“逐像素”直接等同于“永远更快”,也不要把固定 render target 当作无需调参的万能解。

本章回顾:近似如何形成可控效果

  • 焦散是曲面反射或折射后光线在局部集中形成的亮斑。
  • 反向采样从海底点找候选光线,比追踪所有光子更适合实时近似。
  • 波面偏导得到法线;线面相交把折射方向落到海底纹理坐标。
  • 三遍渲染把海底、焦散叠加和水面反射分开,便于调试与扩展。
  • 屏幕空间与纹理空间分别交换可见性收益、固定成本和过滤能力。

练习

问题 1 改 Demo 代码:把实验中的候选光线从“按波面坡度计算”改成固定竖直方向。你预计亮斑会失去什么变化?请指出应改动的向量,并用重置前后的截图验证。

问题 2 推导题:若海底平面为 z = -0.8,采样点 p₀=(0.2,0.3,0),候选方向 v=(0.1,0.2,-1),用线面相交公式求 t 并写出交点。

问题 3 场景选型题:一个小型水池只占屏幕的四分之一,另一个海底镜头几乎铺满屏幕且希望焦散图跨帧稳定。分别选择屏幕空间或纹理空间,并说明过滤策略。

名词解释

本章出现的专业名词,用大白话再讲一遍。

水焦散
曲面折射或反射后,许多光线集中到局部区域形成的动态亮斑。
前向光子追踪
从光源发射光子并跟随它们传播,统计它们最终照亮哪里的办法。
反向光线追踪
从被照亮的点反过来寻找可能来自光源的光线,减少无关光线的办法。
蒙特卡洛采样
用许多随机候选样本近似一个难以直接计算的光照积分。
偏导数
固定其他坐标后,只看曲面沿一个方向变化得有多快。
法线
垂直于表面的方向;它决定光线遇到表面后会朝哪边转。
斯涅尔定律
描述光线进入另一种介质后,入射角和折射角如何关联的公式。
线面相交
计算一条光线在什么位置穿过平面,把方向变成落点。
加法混合
把新算出的光照颜色加到已有底色上的像素混合方式。
依赖纹理读取
用计算出的交点或其他结果临时生成纹理坐标,再读取纹理。

资料与写作方式声明

本章以GPU Gems 1 · Chapter 2. Rendering Water Caustics公开完整正文核定章节范围、事实坐标与时代语境,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…