最难渲染的自然现象之一

实时渲染系列
水是游戏中最难渲染的自然现象之一。它同时涉及几何变形、光学效应和流体动力学。但拆开来看,每一步都不复杂。
一、水面渲染的核心要素
一个令人信服的水面需要这几个视觉特征:
| 特征 | 实现手段 |
|---|---|
| 波浪起伏 | 顶点位移(Gerstner 波 / FFT) |
| 表面细节 | 法线贴图扰动 |
| 反射 | 平面反射 / SSR / 环境贴图 |
| 折射 | 屏幕空间折射 + 深度着色 |
| 反射与折射的比例 | 菲涅尔效应 |
| 水下雾化 | 基于深度的颜色衰减 |
| 泡沫/浪花 | 基于曲率或深度的白色叠加 |
| 焦散 | 投影纹理 / 光线追踪 |
二、波形生成
方法一:正弦波叠加(最简单)
cpp
// 多个正弦波叠加
float waveHeight(vec2 pos, float time) {
float h = 0.0;
h += 0.5 * sin(pos.x * 0.3 + time * 1.2);
h += 0.3 * sin(pos.y * 0.5 + time * 0.8);
h += 0.1 * sin((pos.x + pos.y) * 1.0 + time * 2.0);
return h;
}
问题:正弦波的波峰和波谷一样圆润,不像真实海浪(波峰尖、波谷平)。
方法二:Gerstner 波(游戏主流)
Gerstner 波不仅让顶点上下移动,还让顶点水平移动,产生真实的尖峰效果:
cpp
// Gerstner 波:顶点同时水平和垂直位移
vec3 gerstnerWave(vec2 pos, float time,
vec2 dir, float amplitude, float frequency,
float steepness) {
float phase = frequency * dot(dir, pos) + time;
float Q = steepness / (frequency * amplitude); // 控制尖锐度
vec3 offset;
offset.x = Q * amplitude * dir.x * cos(phase);
offset.z = Q * amplitude * dir.y * cos(phase);
offset.y = amplitude * sin(phase);
return offset;
}
// 叠加多个不同方向、频率的 Gerstner 波
vec3 totalOffset = vec3(0);
totalOffset += gerstnerWave(pos, t, vec2(1,0), 0.5, 0.8, 0.6);
totalOffset += gerstnerWave(pos, t, vec2(0.7,0.7), 0.3, 1.2, 0.5);
totalOffset += gerstnerWave(pos, t, vec2(-0.3,0.9), 0.2, 2.0, 0.4);
方法三:FFT 海洋(3A 级)
基于 Tessendorf 的论文,用海洋频谱(Phillips Spectrum)在频域生成波浪,再通过 FFT 变换到空间域。
Phillips 频谱 → 随机相位 → IFFT → 高度场 + 法线 + 位移
优点:物理正确,视觉效果极佳。缺点:计算量大,通常在 Compute Shader 中实现。
三、法线贴图:小尺度细节
大波浪用顶点位移,小波纹用法线贴图。两张法线贴图以不同速度和方向滚动,叠加产生复杂的水面细节:
c
// 双层法线贴图滚动
vec2 uv1 = worldPos.xz * 0.1 + time * vec2(0.02, 0.01);
vec2 uv2 = worldPos.xz * 0.05 + time * vec2(-0.01, 0.02);
vec3 n1 = texture(normalMap1, uv1).xyz * 2.0 - 1.0;
vec3 n2 = texture(normalMap2, uv2).xyz * 2.0 - 1.0;
// 混合两层法线(UDN blending)
vec3 N = normalize(vec3(n1.xy + n2.xy, n1.z * n2.z));
进阶:Flow Map 可以控制水流方向,让河流沿着河道流动而不是均匀滚动。
四、菲涅尔效应:反射与折射的比例
这是水面看起来真实的最关键因素。
物理规律:
正对水面看(垂直入射)→ 主要看到水下(折射为主)
斜着看水面(掠射角)→ 主要看到反射
c
// Schlick 近似菲涅尔
float fresnel(vec3 V, vec3 N) {
float F0 = 0.02; // 水的基础反射率
float cosTheta = max(dot(V, N), 0.0);
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// 最终颜色 = 反射和折射的混合
float F = fresnel(viewDir, normal);
vec3 color = mix(refractionColor, reflectionColor, F);
很多"看起来不对"的水面,问题就出在没有菲涅尔。加上菲涅尔,效果立刻提升一个档次。
五、反射
方法一:平面反射(最准确)
把相机关于水面做镜像,再渲染一遍场景,得到反射贴图。
镜像相机 → 裁剪水面以下 → 渲染到 FBO → 作为反射贴图采样
缺点:要多渲染一遍场景,开销翻倍。
方法二:屏幕空间反射(SSR)
在屏幕空间做光线步进,沿反射方向搜索已有像素。
优点:不需要额外渲染。缺点:只能反射屏幕上可见的东西。
方法三:环境贴图(最便宜)
用 Cubemap 提供远处的反射。适合天空和远景,近处物体反射不准确。
实际游戏通常混合使用:近处用 SSR,远处用环境贴图,特殊场景用平面反射。
六、折射与水下效果
c
// 屏幕空间折射
vec2 screenUV = gl_FragCoord.xy / screenSize;
vec2 distortion = normal.xz * refractionStrength;
vec3 refrColor = texture(sceneColorTex, screenUV + distortion).rgb;
// 基于深度的水下雾化
float waterDepth = texture(depthTex, screenUV).r - gl_FragCoord.z;
vec3 deepColor = vec3(0.0, 0.05, 0.1); // 深水颜色
float depthFactor = 1.0 - exp(-waterDepth * 0.5);
refrColor = mix(refrColor, deepColor, depthFactor);
效果:水浅的地方能看到水底,水深的地方逐渐变成深蓝/深绿色。
七、泡沫与浪花
泡沫通常出现在:
- 波峰处(Gerstner 波的 Jacobian 行列式 < 0 时)
- 水面与物体交界处(基于深度差)
- 岸边(基于水深)
c
// 基于深度的岸边泡沫
float shoreDepth = texture(depthTex, screenUV).r - fragDepth;
float foam = 1.0 - smoothstep(0.0, foamWidth, shoreDepth);
foam *= texture(foamNoise, uv * 4.0 + time * 0.1).r;
// 叠加到最终颜色
color = mix(color, vec3(1.0), foam * 0.8);
八、完整的水面着色器框架
c
// water.frag - 完整水面片段着色器框架
void main() {
// 1. 法线:从法线贴图获取扰动后的法线
vec3 N = getWaterNormal(worldPos, time);
// 2. 菲涅尔:决定反射/折射比例
float F = fresnelSchlick(viewDir, N);
// 3. 反射
vec3 reflDir = reflect(-viewDir, N);
vec3 reflection = sampleReflection(reflDir, screenUV, N);
// 4. 折射 + 水下雾化
vec3 refraction = sampleRefraction(screenUV, N, waterDepth);
// 5. 混合
vec3 color = mix(refraction, reflection, F);
// 6. 高光
color += sunSpecular(N, viewDir, lightDir);
// 7. 泡沫
color = applyFoam(color, worldPos, waterDepth, time);
fragColor = vec4(color, 1.0);
}
九、性能优化
| 技术 | 说明 |
|---|---|
| LOD | 远处水面降低网格密度和法线细节 |
| 半分辨率反射 | 反射贴图用一半分辨率渲染 |
| Tessellation | GPU 动态细分,近处密远处疏 |
| Compute Shader | FFT 海洋在 CS 中计算,不占顶点管线 |
| 时间复用 | 反射/折射隔帧更新 |
十、不同游戏的水面方案
| 级别 | 方案 | 适用场景 |
|---|---|---|
| 低端 | 法线贴图 + 菲涅尔 + 环境贴图 | 手游、独立游戏 |
| 中端 | Gerstner 波 + SSR + 深度雾 | 主流 PC/主机游戏 |
| 高端 | FFT 海洋 + 平面反射 + 体积光 + 焦散 | 3A 大作 |
水面渲染没有银弹。核心是理解每个视觉特征背后的物理原理,然后根据性能预算选择合适的近似方案。菲涅尔 + 法线扰动是性价比最高的组合。