文章目录
-
- [0. 效果预览](#0. 效果预览)
- [1. 原理简述](#1. 原理简述)
- [2. 功能点](#2. 功能点)
- [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
- [4. 使用方法](#4. 使用方法)
- [5. 参数说明](#5. 参数说明)
- [6. 变体与扩展](#6. 变体与扩展)
-
- [6.1 简化版:纯 Sin 波(不需要 Gerstner)](#6.1 简化版:纯 Sin 波(不需要 Gerstner))
- [6.2 顶点色控制波浪强度](#6.2 顶点色控制波浪强度)
- [6.3 径向波纹(从中心扩散)](#6.3 径向波纹(从中心扩散))
- [7. 常见问题](#7. 常见问题)
- [8. 性能建议](#8. 性能建议)
0. 效果预览

顶点波浪动画是最直观的 Shader 动态效果之一:在顶点着色器里用数学波函数偏移顶点位置,让平面网格变成起伏的水面、飘动的旗帜、摇摆的植物。不需要骨骼动画、不需要物理模拟,一个数学函数就能让静态模型动起来。
本文直接上 Gerstner 波------比普通 Sin 波更真实:顶点不仅上下移动,还会水平聚拢,产生波峰尖锐、波谷平缓的自然海浪感。
1. 原理简述
Gerstner 波的本质:顶点沿波浪法线方向做圆周运动------水平方向用 cos 聚拢,垂直方向用 sin 起伏,组合出波峰尖、波谷宽的真实水面形态。
普通 Sin 波 vs Gerstner 波的区别:
普通 Sin 波只改 Y(上下):
hlsl
positionOS.y += sin(x * freq + t * speed) * amplitude;
// 波峰波谷完全对称,像果冻一样匀称
Gerstner 波同时改 XZ(水平聚拢)和 Y(上下):
hlsl
float phase = dot(waveDir, positionOS.xz) * freq + t * speed;
positionOS.xz -= waveDir * steepness * amplitude * sin(phase); // 水平聚拢
positionOS.y += cos(phase) * amplitude; // 垂直起伏
steepness(陡度)控制水平聚拢的程度:0 退化为普通 Sin 波,越大波峰越尖。但超过 1 会导致网格自交(波峰翻转),实际使用 0.3~0.7。
多波叠加时,每一层用不同的方向、频率、振幅,海面的复杂感就出来了。
2. 功能点
- Gerstner 波顶点位移:水平聚拢 + 垂直起伏,波峰尖锐波谷平缓
- 双波叠加:主波 + 副波不同方向叠加,打破单一波浪的规律感
- 陡度可调 :
_Steepness控制波峰尖锐度,从微波到尖浪 - 波浪方向可调 :
_WaveDirAngle用角度控制波浪传播方向 - 实时动画 :用
_Time驱动波浪滚动,无需任何脚本 - 法线解析修正:基于 Gerstner 波的解析导数精确计算法线,光照随波浪起伏变化
- 主贴图支持:保留 UV 采样,波浪表面可以有纹理细节
- GPU Instancing:支持多实例渲染
3. 完整 Shader(可直接用)
hlsl
Shader "Custom/VertexWave_URP"
{
Properties
{
// 主贴图(表面纹理)
_BaseMap ("Base Map", 2D) = "white" {}
// 主颜色叠乘
_BaseColor ("Base Color", Color) = (1,1,1,1)
// ===== 主波参数 =====
// 波浪振幅(高度)
_Amplitude ("Amplitude", Range(0, 2)) = 0.3
// 波浪频率(密度,值越大波越密)
_Frequency ("Frequency", Range(0.1, 10)) = 2.0
// 波浪移动速度
_Speed ("Speed", Range(0, 10)) = 1.5
// 波峰陡度(0=普通Sin,0.5=自然海浪,>1会自交)
_Steepness ("Steepness", Range(0, 1)) = 0.5
// 主波方向角度(度,0=沿+X,90=沿+Z)
_WaveDirAngle ("Wave Direction Angle", Range(0, 360)) = 0.0
// ===== 副波参数(叠加第二层波,打破规律感) =====
// 副波振幅
_SubAmplitude ("Sub Amplitude", Range(0, 1)) = 0.1
// 副波频率
_SubFrequency ("Sub Frequency", Range(0.1, 20)) = 4.0
// 副波速度
_SubSpeed ("Sub Speed", Range(0, 10)) = 2.5
// 副波方向角度(度)
_SubWaveDirAngle ("Sub Wave Dir Angle", Range(0, 360)) = 60.0
}
SubShader
{
Tags
{
"RenderPipeline" = "UniversalRenderPipeline"
"Queue" = "Geometry"
"RenderType" = "Opaque"
}
Pass
{
Name "VertexWavePass"
Tags { "LightMode" = "UniversalForward" }
Cull Off
ZWrite On
Blend Off
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// GPU Instancing 支持
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// =========================================================
// 贴图声明
// =========================================================
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
// =========================================================
// 材质属性(与 Properties 一一对应)
// =========================================================
float4 _BaseMap_ST;
float4 _BaseColor;
float _Amplitude;
float _Frequency;
float _Speed;
float _Steepness;
float _WaveDirAngle;
float _SubAmplitude;
float _SubFrequency;
float _SubSpeed;
float _SubWaveDirAngle;
struct Attributes
{
float4 positionOS : POSITION; // 模型空间顶点
float3 normalOS : NORMAL; // 模型空间法线
float2 uv : TEXCOORD0; // UV 坐标
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION; // 裁剪空间位置
float2 uv : TEXCOORD0; // 传递 UV
float3 normalWS : TEXCOORD1; // 世界空间法线
float3 positionWS : TEXCOORD2; // 世界空间位置
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
// =========================================================
// 角度转方向向量
// =========================================================
float2 AngleToDir(float angleDeg)
{
float rad = angleDeg * PI / 180.0;
return float2(cos(rad), sin(rad));
}
// =========================================================
// 单层 Gerstner 波:返回顶点偏移和法线贡献
// dir: 波浪传播方向(归一化)
// freq: 频率 speed: 速度 amp: 振幅 steep: 陡度
// pos: 顶点 XZ 坐标
// outOffset: 输出顶点偏移 (x, y, z)
// outNormal: 累加到法线的分量
// =========================================================
void GerstnerWave(float2 dir, float freq, float speed, float amp, float steep,
float2 pos, out float3 outOffset, out float3 outNormal)
{
// 相位 = 方向·位置 * 频率 + 时间 * 速度
float phase = dot(dir, pos) * freq + _Time.y * speed;
float s = sin(phase);
float c = cos(phase);
// Gerstner 位移:水平聚拢 + 垂直起伏
outOffset.x = -dir.x * steep * amp * s;
outOffset.z = -dir.y * steep * amp * s;
outOffset.y = amp * c;
// Gerstner 解析法线分量(累加后再归一化)
// 参考:GPU Gems Chapter 1 - Effective Water Simulation
outNormal.x = dir.x * freq * amp * s;
outNormal.z = dir.y * freq * amp * s;
outNormal.y = freq * amp * steep * c;
}
// =========================================================
// 顶点着色器:Gerstner 波浪位移
// =========================================================
Varyings vert(Attributes v)
{
Varyings o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
float2 pos = v.positionOS.xz;
// ===== 主波 =====
float2 mainDir = AngleToDir(_WaveDirAngle);
float3 mainOffset, mainNrm;
GerstnerWave(mainDir, _Frequency, _Speed, _Amplitude, _Steepness,
pos, mainOffset, mainNrm);
// ===== 副波 =====
float2 subDir = AngleToDir(_SubWaveDirAngle);
float3 subOffset, subNrm;
GerstnerWave(subDir, _SubFrequency, _SubSpeed, _SubAmplitude, _Steepness,
pos, subOffset, subNrm);
// ===== 叠加偏移 =====
v.positionOS.xyz += mainOffset + subOffset;
// ===== 法线:从累加的切线分量计算 =====
float3 nrmSum = mainNrm + subNrm;
// Gerstner 法线公式:N = normalize(float3(-sum.x, 1 - sum.y, -sum.z))
float3 correctedNormal = normalize(float3(-nrmSum.x, 1.0 - nrmSum.y, -nrmSum.z));
v.normalOS = correctedNormal;
// ===== 标准变换 =====
o.positionHCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
o.normalWS = TransformObjectToWorldNormal(v.normalOS);
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
return o;
}
// =========================================================
// 片元着色器:半 Lambert 光照
// =========================================================
half4 frag(Varyings i) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(i);
// 采样主贴图
half4 baseCol = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);
baseCol *= (half4)_BaseColor;
// 半 Lambert 光照
Light mainLight = GetMainLight();
float3 normalWS = normalize(i.normalWS);
float NdotL = dot(normalWS, normalize(mainLight.direction));
float halfLambert = NdotL * 0.5 + 0.5;
half3 lighting = halfLambert * mainLight.color;
// 环境光补偿
lighting += half3(0.1, 0.1, 0.15);
half3 finalColor = baseCol.rgb * lighting;
return half4(finalColor, baseCol.a);
}
ENDHLSL
}
}
}
4. 使用方法
-
在 Unity 项目的
Assets/Shaders/下新建文件VertexWave_URP.shader,粘贴上方完整代码。 -
新建材质(Create → Material),Shader 选择
Custom/VertexWave_URP。 -
创建高细分平面(关键):
- Unity 默认的 Plane 只有 10×10 个顶点,波浪会非常粗糙
- 推荐用 ProBuilder 创建一个 50×50 或更高细分的平面
- 或者从外部导入一个高细分的平面网格(Blender → 细分 → 导出 FBX)
-
将材质赋给高细分平面。
-
在 Inspector 中调节参数,推荐预设:
自然海面 :
Amplitude= 0.3,Frequency= 2,Speed= 1.5,Steepness= 0.5,Wave Dir= 0°, 副波SubAmplitude= 0.1,SubFrequency= 4,SubSpeed= 2.5,Sub Dir= 60°平静湖面 :
Amplitude= 0.05,Frequency= 3,Speed= 0.8,Steepness= 0.2, 副波全关(SubAmplitude= 0)汹涌大浪 :
Amplitude= 0.8,Frequency= 1.5,Speed= 2,Steepness= 0.7, 副波SubAmplitude= 0.3

- 运行场景,观察波浪动画效果。旋转光源方向,确认波浪表面的光影随起伏变化。
5. 参数说明
| 参数 | 类型 | 范围/默认值 | 说明 |
|---|---|---|---|
_BaseMap |
2D | white | 表面贴图 |
_BaseColor |
Color | (1,1,1,1) | 主颜色叠乘 |
_Amplitude |
Range(0,2) | 0.3 | 主波振幅(波的高度) |
_Frequency |
Range(0.1,10) | 2.0 | 主波频率(波越密值越大) |
_Speed |
Range(0,10) | 1.5 | 主波移动速度 |
_Steepness |
Range(0,1) | 0.5 | 波峰陡度:0=圆润Sin,0.5=自然海浪,>0.8=极尖 |
_WaveDirAngle |
Range(0,360) | 0 | 主波方向角度(度),0=+X,90=+Z |
_SubAmplitude |
Range(0,1) | 0.1 | 副波振幅 |
_SubFrequency |
Range(0.1,20) | 4.0 | 副波频率 |
_SubSpeed |
Range(0,10) | 2.5 | 副波速度 |
_SubWaveDirAngle |
Range(0,360) | 60 | 副波方向角度(度) |
6. 变体与扩展
6.1 简化版:纯 Sin 波(不需要 Gerstner)
如果不需要波峰尖锐效果(如旗帜、草地摇摆),可以退化为纯 Sin 波,只改 Y 轴:
hlsl
// 替换 vert 中的 Gerstner 部分
float waveCoord = lerp(v.positionOS.x, v.positionOS.z, 0.5);
float wave = sin(waveCoord * _Frequency + _Time.y * _Speed) * _Amplitude;
v.positionOS.y += wave;
// 法线用 cos 近似
float slope = cos(waveCoord * _Frequency + _Time.y * _Speed) * _Frequency * _Amplitude;
v.normalOS = normalize(float3(-slope, 1, 0));
计算量更小,适合大量实例(如草地)。
6.2 顶点色控制波浪强度
让模型的某些区域不受波浪影响(如旗帜的固定端):
hlsl
// 在 Attributes 中加入顶点色
float4 color : COLOR;
// 在 vert 中用顶点色 R 通道做遮罩
float mask = v.color.r; // 0=不动,1=完全波动
v.positionOS.xyz += (mainOffset + subOffset) * mask;
在建模软件中把固定端的顶点色涂黑(R=0),自由端涂白(R=1),就能实现"一端固定、一端飘动"的效果。
6.3 径向波纹(从中心扩散)
模拟水滴落水后的同心圆波纹:
hlsl
// 在 vert 中替换波浪坐标为到中心的距离
float2 center = float2(0, 0); // 波纹中心(模型空间)
float dist = length(v.positionOS.xz - center);
// 用减号让波纹从中心向外扩散
float wave = sin(dist * _Frequency - _Time.y * _Speed) * _Amplitude;
// 可选:距离越远振幅越小
wave *= saturate(1.0 - dist * 0.2);
v.positionOS.y += wave;
7. 常见问题
Q: 波浪看起来像锯齿/阶梯,不平滑?
A: 网格细分不够。Unity 默认 Plane 只有 10×10 顶点,波浪在低细分网格上会被"采样不足"变成锯齿。用 ProBuilder 创建 50×50 以上的平面,或导入高细分网格。
Q: 波峰出现翻转/网格自交?
A: _Steepness 过高。Gerstner 波的数学限制是所有波的 steepness * frequency * amplitude 之和不能超过 1,否则波峰处顶点会交叉。降低 _Steepness 到 0.3~0.5 通常就能解决。
Q: 光照在波浪面上没有变化,看起来是平的?
A: 检查法线修正是否生效。Shader 中用 Gerstner 解析导数计算了法线,但如果 _Amplitude 很小,法线变化也很小。调大振幅或用更强的方向光测试。
Q: 想让旗帜一端固定、一端飘动?
A: 用顶点色遮罩(见变体 6.2)。固定端顶点色 R=0,自由端 R=1,Shader 用 color.r 乘以偏移量。
Q: 多个波浪物体的波纹不对齐?
A: 当前 Shader 用模型空间坐标,每个物体各自独立。大面积水面拼接需改为世界空间:把 float2 pos = v.positionOS.xz; 改为 float2 pos = TransformObjectToWorld(v.positionOS.xyz).xz;。
8. 性能建议
- 顶点数是瓶颈:波浪计算在顶点着色器中,50×50 平面(2500 顶点)移动端没问题,200×200(40000 顶点)需要注意帧率。
- 副波可选 :性能敏感时把
_SubAmplitude设为 0 关闭副波,省掉一半的 Sin/Cos 计算。 - 简化法线:如果不需要波浪表面的光照变化(无光照的卡通风格),注释掉法线计算部分,省掉额外的三角函数。
- LOD 策略:远处的波浪物体用低细分网格 + 法线贴图伪造波纹,只有近处用真正的顶点位移。
- 合批友好:单 Pass Opaque Shader,支持 SRP Batcher 和 GPU Instancing。大量水面片可开启 Instancing。