从数学原理到 HLSL 实现,手把手拆解如何在 Unity Shader 中利用 UV 偏移模拟真实的水波涟漪动画。
涟漪效果的基本原理
当一块石头投入平静的水面,水面受到扰动后,以投入点为圆心向外传播圆形机械波 。在渲染中,我们无需模拟真实的流体动力学,只需用正弦函数来模拟这种周期性的起伏,并将其作用于纹理的 UV 坐标,就能欺骗眼睛,呈现出逼真的水波视觉效果。
📌 核心思路
涟漪 = UV 偏移 (让纹理"扭曲流动")+ 正弦函数 (控制波形形状)+ 时间变量(让波纹随时间运动)
下面这张图展示了水面波纹的传播示意,圆圈代表等相位线(波峰或波谷),颜色深浅表示振幅强弱:
图 1:涟漪传播示意 --- 同心圆形波以扰动源为圆心向外辐射,振幅随距离衰减

UV 坐标基础回顾
UV 坐标是纹理空间中的二维坐标系,U 对应水平轴(0→1 从左到右),V 对应垂直轴(0→1 从下到上)。在 Unity Shader 中,顶点着色器输出 texcoord,片元着色器用它对纹理采样。
标准 UV 范围
Unity 中 UV 默认在 [0, 1] 范围内,超出部分由 Wrap Mode(Repeat / Clamp)决定行为。
UV 偏移与缩放
通过 TRANSFORM_TEX 宏自动应用材质的 Tiling 和 Offset,涟漪效果需要在此基础上额外叠加偏移量。
图 2:UV 坐标可视化 --- 红色对应 U 分量,绿色对应 V 分量,中心点为 (0.5, 0.5)

正弦波驱动 UV 偏移
涟漪的本质是对 UV 坐标施加依赖距离和时间的正弦偏移。我们分两步来理解:先做单方向的波纹,再扩展到圆形涟漪。
3.1 单向波纹
最简单的情形:让纹理沿 U 轴做正弦摆动。核心公式如下:
uv.y += sin(uv.x × frequency + time × speed) × amplitude
参数含义:
| 参数 | 作用 | 典型值 |
|---|---|---|
| frequency | 控制波峰密度(越大波越密) | 5 ~ 20 |
| speed | 波的传播速度 | 1 ~ 3 |
| amplitude | 波的振幅(偏移强度) | 0.01 ~ 0.05 |
3.2 环形涟漪(圆形传播)
要做圆形涟漪,需要把"距离扰动中心的距离 dist"替换掉上面公式中的 uv.x。波纹沿径向传播,UV 偏移方向也应该是径向的:
vec2 dir = normalize(uv - center)
float dist = length(uv - center)
float wave = sin(dist × frequency - time × speed) × amplitude × attenuation
uv += dir × wave
其中 attenuation(衰减)通常取 1.0 / (dist × k + 1.0),让远处的涟漪逐渐消失,更符合自然规律。
图 3:左侧为单向正弦波纹,右侧为环形径向涟漪(实时动画)

完整 Shader 实现
下面是一个完整的 Unity ShaderLab 实现,包含涟漪 UV 偏移、振幅衰减、以及通过 _Time 驱动动画:
HLSL / ShaderLabRippleEffect.shader
Shader "Custom/RippleEffect"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_RippleCenter("Ripple Center", Vector) = (0.5, 0.5, 0, 0)
_Frequency ("Frequency", Float) = 15.0
_Speed ("Speed", Float) = 2.5
_Amplitude ("Amplitude", Float) = 0.03
_Attenuation("Attenuation",Float) = 4.0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// 材质属性声明
sampler2D _MainTex;
float4 _MainTex_ST;
float2 _RippleCenter;
float _Frequency;
float _Speed;
float _Amplitude;
float _Attenuation;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 应用 Tiling / Offset
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 frag(v2f i) : SV_Target
{
float2 uv = i.uv;
// --- 1. 计算到涟漪中心的向量与距离 ---
float2 delta = uv - _RippleCenter;
float dist = length(delta);
// --- 2. 避免中心点除零,加极小偏移 ---
float2 dir = delta / (dist + 0.0001);
// --- 3. 计算振幅衰减(距离越远越弱)---
float attn = 1.0 / (1.0 + dist * _Attenuation);
// --- 4. 正弦波计算偏移量 ---
float wave = sin(dist * _Frequency - _Time.y * _Speed)
* _Amplitude * attn;
// --- 5. 沿径向方向偏移 UV ---
uv += dir * wave;
// --- 6. 采样纹理并输出 ---
float4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
✅ 关键点
_Time.y 是 Unity 内置的全局时间变量(单位:秒),不需要在 C# 中手动传入,直接在 Shader 里用即可驱动动画。
代码逐步拆解
-
计算到涟漪中心的向量
delta = uv - _RippleCenter,把 UV 空间的当前像素坐标减去涟漪中心坐标,得到偏移向量。
-
归一化得到径向方向
dir = delta / (dist + ε),归一化使方向向量长度为 1,加上极小量 ε 防止中心点除以零。
-
计算衰减系数
attn = 1 / (1 + dist × k),远离中心时振幅逐渐衰减为零,物理上类似能量散失。
-
用正弦函数生成波形
sin(dist × frequency − _Time.y × speed),dist × frequency 决定空间频率,_Time.y × speed 让波纹随时间向外传播。
-
偏移 UV 并采样
uv += dir × wave 将当前 UV 沿径向偏移,然后用扭曲后的 UV 采样纹理,产生涟漪视觉扭曲。
// 05
多层叠加提升真实感
真实水面并非只有一个扰动源,而是多个涟漪叠加的结果。Unity Shader 中我们可以用两层不同频率、不同方向的正弦波叠加来大幅提升真实感:
HLSL多层叠加片段
// 第一层:主涟漪(低频、大振幅)
float wave1 = sin(dist * 10.0 - _Time.y * 2.0) * 0.03 * attn;
// 第二层:细碎波纹(高频、小振幅,速度更快)
float wave2 = sin(dist * 30.0 - _Time.y * 5.0) * 0.008 * attn;
// 第三层:斜向扰动(增加不规则感)
float wave3 = sin(uv.x * 20.0 + uv.y * 15.0 - _Time.y * 3.0) * 0.005;
// 将三层叠加
uv += dir * (wave1 + wave2) + float2(wave3, wave3 * 0.7);
⚠️ 性能提示
每增加一层 sin() 运算对移动端 GPU 有一定开销。建议在低端设备上通过 Shader LOD 或 Material Quality 关闭高频层,只保留主涟漪层。
图 4:多层叠加效果演示 --- 多中心涟漪互相叠加形成自然水面扰动(点击画面可添加新涟漪源)

互动演示:点击激发涟漪
下面的 Canvas 模拟了完整的涟漪效果。点击任意位置即可在该处激发新的涟漪扰动,感受不同频率与衰减参数的视觉差异:
图 5:互动涟漪演示 --- 点击激发涟漪,多波源叠加;模拟 Shader 中的 UV 扭曲效果

对应到 Unity Shader,可以将点击坐标从屏幕空间转换为 UV 空间后,通过 Material.SetVector("_RippleCenter", pos) 动态传入,实现交互式涟漪。
C#RippleController.cs
using UnityEngine;
public class RippleController : MonoBehaviour
{
[SerializeField] private Material rippleMat;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
// 将碰撞点的纹理坐标传给 Shader
Vector2 uvPos = hit.textureCoord;
rippleMat.SetVector("_RippleCenter",
new Vector4(uvPos.x, uvPos.y, 0, 0));
}
}
}
}
常见问题与优化建议
边缘拉伸问题
当涟漪中心靠近纹理边缘时,Repeat 模式会出现接缝。解决方案:将 Wrap Mode 改为 Mirror,或在 Shader 中用 frac() 平滑处理边界。
振幅过大失真
Amplitude 设置过高会导致画面严重扭曲失去辨识度。建议将范围限制在纹理尺寸的 2%~5% 以内(即 0.02~0.05)。
移动端性能
在移动端减少 sin() 调用层数,或使用预计算的 Normal Map 贴图替代实时正弦计算,可大幅降低 GPU 负载。
与法线贴图结合
涟漪扭曲只改变 UV,若配合动态 Normal Map(如用 UV 偏移采样法线贴图),可同时影响光照,让水面涟漪更具立体感。
进阶扩展方向
- 使用 RenderTexture + 物理模拟 替代正弦函数,实现真正的水波传播(适合高端项目)
- 结合 反射探针,在涟漪扭曲 UV 时同步扭曲反射采样,强化水面质感
- 用 Shader Graph 可视化搭建以上节点,便于美术同学调参迭代
- 加入 泡沫遮罩(foam mask):在振幅峰值处显示白色泡沫纹理,模拟浪尖效果
🎯 总结
UV 涟漪效果的本质是 「对采样坐标做时变的径向正弦偏移」。掌握了这个核心思路,就可以延伸出水面反射、旗帜飘动、热浪扭曲等各类基于 UV 动画的 Shader 效果。