游戏里的水面是怎么做的

最难渲染的自然现象之一

实时渲染系列

水是游戏中最难渲染的自然现象之一。它同时涉及几何变形、光学效应和流体动力学。但拆开来看,每一步都不复杂。

一、水面渲染的核心要素

一个令人信服的水面需要这几个视觉特征:

特征 实现手段
波浪起伏 顶点位移(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 大作

水面渲染没有银弹。核心是理解每个视觉特征背后的物理原理,然后根据性能预算选择合适的近似方案。菲涅尔 + 法线扰动是性价比最高的组合。

相关推荐
noipp15 小时前
推荐题目:洛谷 P6231 [JSOI2013] 公交系统
c语言·数据结构·c++·算法·游戏·洛谷·luogu
金銀銅鐵20 小时前
[Python] 用 turtle 来绘制国际象棋棋盘(不含棋子)
python·游戏
Python私教1 天前
Godot怎么下载和安装?零基础完成第一次启动
人工智能·游戏·godot
FairGuard手游加固1 天前
2026年7月份国产游戏审批信息
游戏
星空露珠2 天前
28种颜色对应名称,
开发语言·数据库·算法·游戏·lua
中国搜索直付通2 天前
棋牌游戏支付与防沉迷:二级商户如何构建“免疫系统”
游戏
2601_962924762 天前
遗忘之海手游战术家怎么玩 遗忘之海手游战术家玩法攻略
游戏
雅客李2 天前
2026年第三季度云手机实测 游戏多开挂机托管三维度数据公开
游戏·智能手机
xcLeigh2 天前
Unity基础:创建你的第一个游戏物体——Cube、Sphere与基本3D物体
游戏·3d·unity·教程
_Tenk_2 天前
自研开源小工具 SwitchDeck:一键切换分辨率/刷新率/音频输出(附 WinForms 高 DPI 踩坑实录)
游戏·分辨率·cs2·看电影·一键切换·音响耳机音频·州瓦go