游戏里的水面是怎么做的

最难渲染的自然现象之一

实时渲染系列

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

一、水面渲染的核心要素

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

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

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

相关推荐
程序员-Benothing4 小时前
H3 Max开放世界RPG:AI原生游戏,从“能生成“到“能玩“差了多远
游戏·ai-native
CoderYanger14 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
无别05216 小时前
【做游戏】做完游戏Demo后没地方发布?
游戏·玩游戏
lilian23316 小时前
HarmonyOS 7 新特性(二十九)|游戏伴随服务:悬浮协作与质量降级
游戏·语音识别·harmonyos
人民广场吃泡面17 小时前
DLSS 5 深度解析:当 AI 学会“创造”画面,实时游戏渲染迎来“GPT 时刻”
人工智能·gpt·游戏
学习星球19 小时前
AI 一句话生成 3D 游戏世界:腾讯 HY-World 2.0 开源深度解析与本地实战
开发语言·人工智能·游戏·3d·ai·课程设计·ai编程
乌萨达20 小时前
梦幻新诛仙轻享电脑版怎么玩?模拟器安装与设置指南
游戏·安卓模拟器·雷电模拟器
qq_369224331 天前
ddraw.dll丢失是什么原因引起的?老游戏运行报错,多种可行修复手段汇总分享
windows·游戏·dll·dll修复·dll丢失·dll错误
xiaohe06011 天前
🎮 豆包完胜 DeepSeek ?!零玩家竞技场,AI Agent 专属对弈!
游戏·llm·agent
白搞电子2 天前
ESP-Mosaico 掌上游戏开发:LVGL 抛硬币动画、音频混音与 BMI270 摇晃触发
单片机·嵌入式硬件·物联网·游戏·音视频