【游戏技术群】959392658  【游戏出海群】12067810
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2375

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 2848|回复: 63

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

[复制链接]  [移动端链接]
排名
3
昨日变化

7088

主题

7615

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
11121
威望
30
注册时间
2013-7-29
在线时间
3646 小时
最后登录
2018-12-17

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛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 很给力!

查看全部评分


跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

7日久生情
1500/5000
排名
1996
昨日变化

2

主题

396

帖子

1500

积分

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

UID
213483
好友
0
蛮牛币
2585
威望
0
注册时间
2017-3-21
在线时间
438 小时
最后登录
2018-12-17
发表于 2018-11-22 14:49:27 | 显示全部楼层
跟我念“站长妹纸萌萌哒!”我说站长,你说YO!

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1324/1500
排名
3192
昨日变化
16

7

主题

378

帖子

1324

积分

Rank: 6Rank: 6Rank: 6

UID
40014
好友
16
蛮牛币
2535
威望
0
注册时间
2014-8-15
在线时间
315 小时
最后登录
2018-12-17
QQ
发表于 2018-11-22 15:42:03 | 显示全部楼层
2D涟漪/水波纹特效
[发帖际遇]: seany321 乐于助人,奖励 2 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

3偶尔光临
185/300
排名
14922
昨日变化
2

0

主题

110

帖子

185

积分

Rank: 3Rank: 3Rank: 3

UID
301584
好友
1
蛮牛币
532
威望
0
注册时间
2018-10-28
在线时间
25 小时
最后登录
2018-12-6
发表于 2018-11-22 16:21:17 | 显示全部楼层
2D涟漪/水波纹特效

回复 支持 反对

使用道具 举报

5熟悉之中
604/1000
排名
5946
昨日变化
35

0

主题

275

帖子

604

积分

Rank: 5Rank: 5

UID
266294
好友
1
蛮牛币
13
威望
0
注册时间
2018-1-25
在线时间
105 小时
最后登录
2018-12-17
发表于 2018-11-22 17:03:57 | 显示全部楼层
thanks for the share!!!

回复 支持 反对

使用道具 举报

排名
494
昨日变化

14

主题

275

帖子

2550

积分

Rank: 11Rank: 11Rank: 11Rank: 11Rank: 11

UID
19893
好友
5
蛮牛币
4850
威望
0
注册时间
2014-4-1
在线时间
649 小时
最后登录
2018-12-6

活力之星

QQ
发表于 2018-11-22 17:05:01 | 显示全部楼层
shader是我一直想学却一直坚持学不下去的...

励志成为大神的男人!!!
回复 支持 反对

使用道具 举报

2初来乍到
134/150
排名
12369
昨日变化
2

0

主题

13

帖子

134

积分

Rank: 2Rank: 2

UID
232458
好友
0
蛮牛币
274
威望
0
注册时间
2017-7-16
在线时间
49 小时
最后登录
2018-12-11
发表于 2018-11-22 17:35:36 | 显示全部楼层
哇哇哇,厉害了大大。
学习一下

回复 支持 反对

使用道具 举报

排名
30733
昨日变化
25

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日久生情
1589/5000
排名
1435
昨日变化
1

0

主题

468

帖子

1589

积分

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

UID
87577
好友
0
蛮牛币
5366
威望
0
注册时间
2015-3-31
在线时间
271 小时
最后登录
2018-12-16

锦衣玉食

发表于 2018-11-23 08:33:50 | 显示全部楼层
too good too strong!

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1040/1500
排名
2618
昨日变化

4

主题

51

帖子

1040

积分

Rank: 6Rank: 6Rank: 6

UID
186015
好友
0
蛮牛币
2857
威望
0
注册时间
2016-11-22
在线时间
449 小时
最后登录
2018-12-17
发表于 2018-11-23 09:05:59 | 显示全部楼层
thanks you share !!  shader is good

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1053/1500
排名
2578
昨日变化
1

0

主题

234

帖子

1053

积分

Rank: 6Rank: 6Rank: 6

UID
68040
好友
0
蛮牛币
1416
威望
0
注册时间
2015-1-13
在线时间
275 小时
最后登录
2018-12-17
发表于 2018-11-23 09:06:05 | 显示全部楼层
666 学习了

回复

使用道具 举报

3偶尔光临
227/300
排名
15824
昨日变化

0

主题

138

帖子

227

积分

Rank: 3Rank: 3Rank: 3

UID
301614
好友
0
蛮牛币
290
威望
0
注册时间
2018-10-29
在线时间
45 小时
最后登录
2018-12-14
发表于 2018-11-23 09:11:36 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
768/1000
排名
16534
昨日变化

0

主题

500

帖子

768

积分

Rank: 5Rank: 5

UID
199204
好友
0
蛮牛币
355
威望
0
注册时间
2017-1-5
在线时间
228 小时
最后登录
2018-12-14
发表于 2018-11-23 09:19:56 | 显示全部楼层
谢谢分享

回复

使用道具 举报

6蛮牛粉丝
1179/1500
排名
2180
昨日变化

0

主题

253

帖子

1179

积分

Rank: 6Rank: 6Rank: 6

UID
83972
好友
2
蛮牛币
445
威望
0
注册时间
2015-3-24
在线时间
310 小时
最后登录
2018-12-14
发表于 2018-11-23 09:51:01 | 显示全部楼层
尔尔额额

回复

使用道具 举报

3偶尔光临
218/300
排名
12570
昨日变化
1

0

主题

95

帖子

218

积分

Rank: 3Rank: 3Rank: 3

UID
248391
好友
0
蛮牛币
207
威望
0
注册时间
2017-10-12
在线时间
51 小时
最后登录
2018-12-17
发表于 2018-11-23 09:56:41 | 显示全部楼层
不知道有没有那种3D的?

回复 支持 反对

使用道具 举报

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

本版积分规则

快速回复 返回顶部 返回列表