游戏里的水面是怎么做的

最难渲染的自然现象之一

实时渲染系列

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

一、水面渲染的核心要素

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

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

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

相关推荐
leoZ2314 小时前
Claude 全面解析:从基础原理到实战应用指南
人工智能·游戏
2501_943782357 小时前
【共创季稿事节】猜数字游戏:二分法思维与交互式反馈
前端·游戏·microsoft·harmonyos·鸿蒙·鸿蒙系统
星空露珠11 小时前
迷你世界UGc3.0脚本Wiki[剧情动画模块管理接口 Timeline]
开发语言·数据结构·算法·游戏·lua
yangmu320312 小时前
《星露谷物语》MOD配置与实战安装综合指南
游戏·游戏引擎·游戏程序
xcLeigh13 小时前
Unity基础:Game视图详解——游戏预览、分辨率模拟与性能显示
游戏·unity·游戏引擎·音频·视频·game·play模式
yyuuuzz14 小时前
2026独立站运营的几个技术细节问题
运维·服务器·网络·人工智能·游戏
Johnstons1 天前
游戏网络测试怎么做?从延迟到丢包,一套完整的游戏弱网测试方案
网络·游戏·php
2023自学中1 天前
imx6ull 开发板, mame 模拟器,运行游戏 测试
linux·游戏·嵌入式·开发板
专业技术员!!!!2 天前
游戏代练平台怎么开发?Uni+PHP多端代练系统|适配三角洲+王者代练
游戏