找回密码
 注册帐号

扫一扫,访问微社区

士郎 Unity用shader来实现2D涟漪/水波纹特效

67
回复
5459
查看
  [ 复制链接 ]
排名
1
昨日变化

7752

主题

8308

帖子

3万

积分

Rank: 16

UID
1231
好友
186
蛮牛币
10604
威望
30
注册时间
2013-7-29
在线时间
3983 小时
最后登录
2019-5-23

活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

2018-11-22 14:24:37 显示全部楼层 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
· 看看效果?
动图:
1.1.gif
Show me the CODE
直接先上代码吧!注释可能不够详尽,也可以先参考下一节,如果想直接运行项目看效果可以跳到后面章节下载运行
首先是shader部分:[RippleEffect.shader]
[AppleScript] 纯文本查看 复制代码
Shader "Hidden/Ripple Effect"
{
    Properties
    {
        _MainTex("Base", 2D) = "white" {}
        _GradTex("Gradient", 2D) = "white" {}
        _Reflection("Reflection Color", Color) = (0, 0, 0, 0)
        _Params1("Parameters 1", Vector) = (1, 1, 0.8, 0)
        _Params2("Parameters 2", Vector) = (1, 1, 1, 0)
        _Drop1("Drop 1", Vector) = (0.49, 0.5, 0, 0)
        _Drop2("Drop 2", Vector) = (0.50, 0.5, 0, 0)
        _Drop3("Drop 3", Vector) = (0.51, 0.5, 0, 0)
        _SeaLevel("SeaVevel", Float) = -1.0
    }
    CGINCLUDE
    #include "UnityCG.cginc"

    sampler2D _MainTex;//输入源图像
    float2 _MainTex_TexelSize;
    sampler2D _GradTex;//涟漪振幅
    half4 _Reflection;
    float4 _Params1;    // [ aspect, 1, scale, 0 ]
    float4 _Params2;    // [ 1, 1/aspect, refraction, reflection ]
    float3 _Drop1;//涟漪1
    float3 _Drop2;//涟漪2
    float3 _Drop3;//涟漪3
    float _SeaLevel;

    float wave(float2 position, float2 origin, float time) //当前点位置, 出发点位置, 时间
    {
        float d = length(position - origin);
        float t = time - d * _Params1.z;
                if (_SeaLevel > 0 && position.y > _SeaLevel)// 超过海平面则不再扩散
                {
                        return 0;
                }
                return (tex2D(_GradTex, float2(t, 0)).a - 0.5f) * 2;
    }
    float allwave(float2 position)// 计算当前点在三个涟漪下的共同作用效果(因为涟漪之间可能相交)
    {
                return
                        wave(position, _Drop1.xy, _Drop1.z) +
                        wave(position, _Drop2.xy, _Drop2.z) +
                        wave(position, _Drop3.xy, _Drop3.z);
    }
    half4 frag(v2f_img i) : SV_Target
    {
        const float2 dx = float2(0.01f, 0);//delta x
        const float2 dy = float2(0, 0.01f);// delta y

        float2 p = i.uv * _Params1.xy;//根据比例变换uv
        float w = allwave(p);//振幅,用振幅来对当前点做UV上面的偏移,即可产生涟漪效果
        float2 dw = float2(allwave(p + dx) - w, allwave(p + dy) - w);//xy上振幅
        float2 duv = dw * _Params2.xy * 0.2f * _Params2.z; //ux上振幅
        half4 c = tex2D(_MainTex, i.uv + duv);//在原图上做偏移
        float fr = pow(length(dw) * 3 * _Params2.w, 3);
        return lerp(c, _Reflection, fr);//lerp来实现反射效果,优化表现
    }
    ENDCG
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma fragmentoption ARB_precision_hint_fastest 
            #pragma target 3.0
            #pragma vertex vert_img
            #pragma fragment frag
            ENDCG
        }
    } 
}



控制部分c#脚本:[rippleeffect.cs]
[AppleScript] 纯文本查看 复制代码
using UnityEngine;
using System.Collections;

public class RippleEffect : MonoBehaviour
{
    public Camera c;
    public float seaLevel = -1.0f;
    public AnimationCurve waveform = new AnimationCurve(
        new Keyframe(0.00f, 0.50f, 0, 0),new Keyframe(0.05f, 1.00f, 0, 0),new Keyframe(0.15f, 0.10f, 0, 0),new Keyframe(0.25f, 0.80f, 0, 0),
        new Keyframe(0.35f, 0.30f, 0, 0),new Keyframe(0.45f, 0.60f, 0, 0),new Keyframe(0.55f, 0.40f, 0, 0),new Keyframe(0.65f, 0.55f, 0, 0),
        new Keyframe(0.75f, 0.46f, 0, 0),new Keyframe(0.85f, 0.52f, 0, 0),new Keyframe(0.99f, 0.50f, 0, 0)
    );//预设的涟漪振幅曲线
    [Range(0.01f, 1.0f)]
    public float refractionStrength = 0.5f;//折射强度,也就是涟漪效果强度
    public Color reflectionColor = Color.gray;//反射默认色 灰色
    [Range(0.01f, 1.0f)]
    public float reflectionStrength = 0.7f;//反射效果强度,可以理解为涟漪的阴影
    [Range(0.0f, 3.0f)]
    public float waveSpeed = 1.25f;//传播速度
    [SerializeField]
    Shader shader;
    class Droplet
    {
        Vector2 position;
        float time = 1000.0f;
        public Droplet() { }
        public void Reset(Vector2 pos){
            position = new Vector2(0.5f, 0.5f);//涟漪起点
            time = 0;
        }
        public void Update(){
            time += Time.deltaTime;
        }
        public Vector4 MakeShaderParameter(float aspect){
            return new Vector4(position.x * aspect, position.y, time, 0);
        }
    }
    Droplet[] droplets;
    Texture2D gradTexture;
    Material material;
    float timer;
    int dropCount;
    void UpdateShaderParameters()//更新shader参数
    {
        material.SetVector("_Drop1", droplets[0].MakeShaderParameter(c.aspect));
        material.SetVector("_Drop2", droplets[1].MakeShaderParameter(c.aspect));
        material.SetVector("_Drop3", droplets[2].MakeShaderParameter(c.aspect));
        material.SetFloat("_SeaLevel", seaLevel);
        material.SetColor("_Reflection", reflectionColor);
        material.SetVector("_Params1", new Vector4(c.aspect, 1, 1 / waveSpeed, 0));
        material.SetVector("_Params2", new Vector4(1, 1 / c.aspect, refractionStrength, reflectionStrength));
    }
    void Start()
    {
        droplets = new Droplet[3];
        for(int i = 0;i < droplets.Length;i++)
        {
            droplets = new Droplet();
        }//初始化涟漪数据
        gradTexture = new Texture2D(2048, 1, TextureFormat.Alpha8, false);
        gradTexture.wrapMode = TextureWrapMode.Clamp;
        gradTexture.filterMode = FilterMode.Bilinear;
        for (var i = 0; i < gradTexture.width; i++)
        {
            var x = 1.0f / gradTexture.width * i;
            var a = waveform.Evaluate(x);
            gradTexture.SetPixel(i, 0, new Color(a, a, a, a));
        }//初始化振幅贴图(也就是把waveform曲线初始化到gradTexture上面)
        gradTexture.Apply();
        material = new Material(shader);
        material.hideFlags = HideFlags.DontSave;
        material.SetTexture("_GradTex", gradTexture);
        UpdateShaderParameters();
    }
    void Update()
    {
        foreach (var d in droplets) d.Update();//更新每个涟漪
        UpdateShaderParameters();
    }
    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, material); //效果作用于画面
    }
    public void SetSeaLevel(float seaLevel_)
    {
        seaLevel = seaLevel_;
    }
    public void Emit(Vector2 pos)// call this to emit a ripple
    {
        droplets[dropCount++ % droplets.Length].Reset(pos);
    }
}



· 试图对原理进行解释:如何在画面中产生一个涟漪的效果?
仔细看上面的效果视频,不难发现扩散的涟漪就是高中物理学过的波,事实上游戏中也通常使用正弦函数/正弦波来逼近真实世界中的涟漪的效果。正弦函数/正弦波是最基础的波形,如果想要更加复杂的表现效果可以通过修改波的公式或者修改计算的坐标空间。(我们非常机智的使用了自定义的曲线来定义了波形)


1.jpg

我们自定义的波形

有了波形,然后呢?



有了波形并不意味着就能产生涟漪的效果,画面中的折射、反射、扭曲效果还需要我们实现。但如果仔细观察效果并提炼规律,其实也不难得到涟漪效果的原理:



对于涟漪(水波)上的某一点,我们很轻松的就能根据上面的波形曲线得到它的振幅。此处的振幅就对应着这一处对附近空间的扭曲,可能文字说起来有点难以想象,可以看下面的图示:




2.png




看图中的涟漪效果,之所以人眼看起来像涟漪,是因为在涟漪处空间发生了轻微的扭曲(珊瑚、鲸鱼的身体),而“空间扭曲”,也就是贴图偏移。
OK,核心思想我们已经阐述完毕,所以我们现在可以自信的说出:


涟漪 ≈ 波形 ≈ 振幅 ≈ 画面的扭曲 ≈ 贴图偏移回过头来看代码片段:
首先是将预设的波形传给shader:
      
[AppleScript] 纯文本查看 复制代码
 //伪代码 [rippleeffect.cs]
        //初始化波形贴图(也就是把waveform曲线初始化到gradTexture上面)
        //初始化之后gradTexture的color.a即为波形曲线上对应的值
        gradTexture = new Texture2D;
        for (var i = 0; i < gradTexture.width; i++)
        {
            var x = 1.0f / gradTexture.width * i;
            var a = waveform.Evaluate(x);
            gradTexture.SetPixel(i, 0, new Color(a, a, a, a));
        }



然后是如何得到振幅:
   
[AppleScript] 纯文本查看 复制代码
 //伪代码 [RippleEffect.shader]    
    float wave(float2 position, float2 origin, float time) //当前点位置, 出发点位置, 时间
    {
        float d = length(position - origin);//计算当前点到出发点的距离
        float t = time - d * _Params1.z;//计算已扩散时间
        return (tex2D(_GradTex, float2(t, 0)).a - 0.5f) * 2;//在波形曲线上得到振幅
    }

最后是根据振幅算出贴图偏移:
   
[AppleScript] 纯文本查看 复制代码
 //伪代码 [RippleEffect.shader]  
    //_MainTex是原来没有涟漪效果的贴图
    half4 frag(v2f_img i) : SV_Target
    {
        const float2 dx = float2(0.01f, 0);//delta x
        const float2 dy = float2(0, 0.01f);// delta y
        float2 p = i.uv * _Params1.xy;//根据相机比例变换当前点的UV坐标
        float w = allwave(p);//振幅,用振幅来对当前点做UV上面的偏移,即可产生涟漪效果
        float2 dw = float2(allwave(p + dx) - w, allwave(p + dy) - w);//xy上振幅
        float2 duv = dw * _Params2.xy * 0.2f * _Params2.z; //UV上的振幅
        half4 c = tex2D(_MainTex, i.uv + duv);//在原图上做偏移,到这一步涟漪效果已经出来了
        float fr = pow(length(dw) * 3 * _Params2.w, 3);
        return lerp(c, _Reflection, fr);//lerp来实现灰色的反射效果,优化表现
    }



最核心的代码就是上面这一块了,在这个过程中将原贴图的某些区域进行了一些像素的偏移,使得这些区域看起来就像高低起伏的水面一样,产生了涟漪的效果。


至此,整个原理也基本讲完了,如果还有什么地方文字描述的不够具体的,可以回过头来看看上一节的代码,会让你豁然开朗。

· 现在就用,现在!
下载项目试一试?
clone到本地后unity打开即可,Unity 2018.2.3f1实测没问题,理论上unity5也可以。
想用到自己的工程?
unitypackage:[微云下载]涟漪特效_2DCameraWithRippleEffect.unitypackage(链接失效欢迎随时告诉我来维护)
导入的prefab为一个2D camera,可以把相同的脚本挂到自己的相机上面。
· A little more
在实际应用上,还会遇到一些问题:
这个shader是作用于主相机的,所以涟漪会扩散到整个屏幕范围内,但是有时候水面也会显示在屏幕内,我们并不希望涟漪在水平面之外继续扩散:
在shader里面可以这样做:
[AppleScript] 纯文本查看 复制代码
//[RippleEffect.shader] 
     float wave(float2 position, float2 origin, float time) //当前点位置, 出发点位置, 时间
    {
        float d = length(position - origin);
        float t = time - d * _Params1.z;
        if (_SeaLevel > 0 && position.y > _SeaLevel)// 超过海平面则不再扩散
        {
                return 0;
        }
        return (tex2D(_GradTex, float2(t, 0)).a - 0.5f) * 2;
    }

而控制海平面的参数_SeaLevel是属于(0,1)的,表示在视口中的比例位置,由控制部分c#脚本在没帧计算后传入shader,这样就可以控制涟漪扩散的范围。
当然,也可以设计更丰富的控制逻辑,比如把涟漪设计成只在指定方向传播,这样甚至可以把这个效果设计成一个技能:“水波攻击”(好吧 只能想出这么中二的名字了),见下图:
1.2.gif


· 结语
本文介绍的shader应用非常简单,在我们的minigame中应用的效果也相当不错,所以才花了不少时间将这个应用记录下来,分享给大家。由于自己也不是主要做Unity的,对Unity中的shader也是仅仅入门而已,所以文章中难免有纰漏和不足,恳请各位指正。如果有什么更好的建议,欢迎评论里交流。
感谢阅读!知乎@Kelvin



参与人数 5鲜花 +6 收起 理由
shark8530 + 1 赞一个!
Man-Max + 1 赞一个!
下雨了 + 2 赞一个!
wujianguo + 1
deram + 1 很给力!

查看全部评分总评分 : 鲜花 +6

回复

使用道具 举报

7日久生情
1658/5000
排名
1851
昨日变化

2

主题

430

帖子

1658

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
213483
好友
0
蛮牛币
2519
威望
0
注册时间
2017-3-21
在线时间
504 小时
最后登录
2019-5-23
2018-11-22 14:49:27 显示全部楼层
跟我念“站长妹纸萌萌哒!”我说站长,你说YO!
回复 支持 反对

使用道具 举报

7日久生情
2094/5000
排名
2353
昨日变化

8

主题

770

帖子

2094

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
40014
好友
16
蛮牛币
2825
威望
0
注册时间
2014-8-15
在线时间
542 小时
最后登录
2019-5-23
QQ
2018-11-22 15:42:03 显示全部楼层
2D涟漪/水波纹特效
回复 支持 反对

使用道具 举报

3偶尔光临
190/300
排名
15298
昨日变化

0

主题

112

帖子

190

积分

Rank: 3Rank: 3Rank: 3

UID
301584
好友
1
蛮牛币
562
威望
0
注册时间
2018-10-28
在线时间
28 小时
最后登录
2019-5-21
2018-11-22 16:21:17 显示全部楼层
2D涟漪/水波纹特效
回复 支持 反对

使用道具 举报

6蛮牛粉丝
1045/1500
排名
4092
昨日变化

0

主题

461

帖子

1045

积分

Rank: 6Rank: 6Rank: 6

UID
266294
好友
1
蛮牛币
159
威望
0
注册时间
2018-1-25
在线时间
222 小时
最后登录
2019-5-20
2018-11-22 17:03:57 显示全部楼层
thanks for the share!!!
回复 支持 反对

使用道具 举报

7日久生情
2577/5000
排名
515
昨日变化

14

主题

281

帖子

2577

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
19893
好友
5
蛮牛币
4871
威望
0
注册时间
2014-4-1
在线时间
656 小时
最后登录
2019-3-28

活力之星

QQ
2018-11-22 17:05:01 显示全部楼层
shader是我一直想学却一直坚持学不下去的...
回复 支持 2 反对 0

使用道具 举报

3偶尔光临
193/300
排名
10112
昨日变化

0

主题

18

帖子

193

积分

Rank: 3Rank: 3Rank: 3

UID
232458
好友
0
蛮牛币
399
威望
0
注册时间
2017-7-16
在线时间
67 小时
最后登录
2019-4-28
2018-11-22 17:35:36 显示全部楼层
哇哇哇,厉害了大大。
学习一下
回复 支持 反对

使用道具 举报

排名
31429
昨日变化

0

主题

20

帖子

32

积分

Rank: 1

UID
302421
好友
1
蛮牛币
93
威望
0
注册时间
2018-11-4
在线时间
2 小时
最后登录
2018-11-23
2018-11-22 22:03:34 显示全部楼层
shader应用非常简单
回复 支持 反对

使用道具 举报

7日久生情
1862/5000
排名
1192
昨日变化

0

主题

546

帖子

1862

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
87577
好友
0
蛮牛币
6828
威望
0
注册时间
2015-3-31
在线时间
338 小时
最后登录
2019-5-24
2018-11-23 08:33:50 显示全部楼层
too good too strong!
回复 支持 反对

使用道具 举报

6蛮牛粉丝
1206/1500
排名
2486
昨日变化

4

主题

55

帖子

1206

积分

Rank: 6Rank: 6Rank: 6

UID
186015
好友
0
蛮牛币
3200
威望
0
注册时间
2016-11-22
在线时间
569 小时
最后登录
2019-5-22
2018-11-23 09:05:59 显示全部楼层
thanks you share !!  shader is good
回复 支持 反对

使用道具 举报

6蛮牛粉丝
1371/1500
排名
1980
昨日变化

0

主题

270

帖子

1371

积分

Rank: 6Rank: 6Rank: 6

UID
68040
好友
0
蛮牛币
1947
威望
0
注册时间
2015-1-13
在线时间
417 小时
最后登录
2019-5-24
2018-11-23 09:06:05 显示全部楼层
666 学习了
回复

使用道具 举报

5熟悉之中
506/1000
排名
9487
昨日变化

0

主题

237

帖子

506

积分

Rank: 5Rank: 5

UID
301614
好友
0
蛮牛币
556
威望
0
注册时间
2018-10-29
在线时间
149 小时
最后登录
2019-5-23
2018-11-23 09:11:36 显示全部楼层
回复

使用道具 举报

6蛮牛粉丝
1073/1500
排名
16954
昨日变化

1

主题

716

帖子

1073

积分

Rank: 6Rank: 6Rank: 6

UID
199204
好友
1
蛮牛币
765
威望
0
注册时间
2017-1-5
在线时间
316 小时
最后登录
2019-5-24
2018-11-23 09:19:56 显示全部楼层
谢谢分享
回复

使用道具 举报

6蛮牛粉丝
1360/1500
排名
1828
昨日变化

0

主题

258

帖子

1360

积分

Rank: 6Rank: 6Rank: 6

UID
83972
好友
2
蛮牛币
838
威望
0
注册时间
2015-3-24
在线时间
374 小时
最后登录
2019-5-24
2018-11-23 09:51:01 显示全部楼层
尔尔额额
回复

使用道具 举报

4四处流浪
327/500
排名
10342
昨日变化

0

主题

144

帖子

327

积分

Rank: 4

UID
248391
好友
0
蛮牛币
231
威望
0
注册时间
2017-10-12
在线时间
73 小时
最后登录
2019-5-5
2018-11-23 09:56:41 显示全部楼层
不知道有没有那种3D的?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则