【Unity Shader URP】顶点波浪动画(Vertex Wave)实战教程

文章目录

    • [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. 使用方法

  1. 在 Unity 项目的 Assets/Shaders/ 下新建文件 VertexWave_URP.shader,粘贴上方完整代码。

  2. 新建材质(Create → Material),Shader 选择 Custom/VertexWave_URP

  3. 创建高细分平面(关键):

    • Unity 默认的 Plane 只有 10×10 个顶点,波浪会非常粗糙
    • 推荐用 ProBuilder 创建一个 50×50 或更高细分的平面
    • 或者从外部导入一个高细分的平面网格(Blender → 细分 → 导出 FBX)
  4. 将材质赋给高细分平面。

  5. 在 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

  1. 运行场景,观察波浪动画效果。旋转光源方向,确认波浪表面的光影随起伏变化。

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。
相关推荐
魔士于安2 小时前
Unity 简单水面效果URP
游戏·unity·游戏引擎·贴图·模型
专注VB编程开发20年3 小时前
VBA/VB6连接、读取Mdb access数据库最快的方法
前端·ui·ado·vb6
mxwin3 小时前
Unity Shader 毛发 & 草海渲染Alpha‑to‑Coverage 抗锯齿技术详解
unity·游戏引擎·shader
ai_coder_ai3 小时前
自动化脚本ui编程之单选控件(radio)
ui·autojs·自动化脚本·冰狐智能辅助·easyclick
qq_452396233 小时前
【工程实战】第四篇:UI 自动化 —— Playwright 异步模式深度实战:告别 Selenium 的“脆”与“慢”
selenium·ui·自动化
空中海3 小时前
第二章:UI 开发——View 系统与 Jetpack Compose
android·ui
程序猿追3 小时前
把手机变成调色盘:用 ArkUI 搓一个带放大镜效果的“UI 灵感色卡取色器”
ui·智能手机
张老师带你学13 小时前
unity TerrainSampleAssets
科技·游戏·unity·游戏引擎·模型
亿元程序员13 小时前
亿元Cocos小游戏实战合集2.0
游戏·游戏引擎