unity 特效_unity用shader来实现2D涟漪/水波纹特效

ef1ee72dbf4f74f4d525f96126e4dee0.png
在之前做的一个游戏中,主角鲸鱼有一个技能可以在水底发射声波,因此需要做一个扩散的水波纹特效,为了达到最好的效果,我们使用了shader来实现。

本文参考github项目:RippleEffect 部分内容参考水波涟漪的实现

· 看看效果?

动图:

b7b01fc8a942e2315af7874ea8e10021.gif

视频:

d271f6c166175278e0627e8fe7176d32.png
shader实现的2D涟漪特效https://www.zhihu.com/video/1036737154977935360

· Show me the CODE!

直接先上代码吧!注释可能不够详尽,也可以先参考下一节,如果想直接运行项目看效果可以跳到后面章节下载运行

首先是shader部分:[RippleEffect.shader]

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; //uv上振幅
        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]

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[i] = 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);
    }
}

· 试图对原理进行解释:

如何在画面中产生一个涟漪的效果?

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

a6a027d7a83812787f4b66b8cb24a543.png
我们自定义的波形

有了波形,然后呢?

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

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

d4c292030b9e3a5971019109aade3f5b.png

看图中的涟漪效果,之所以人眼看起来像涟漪,是因为在涟漪处空间发生了轻微的扭曲(珊瑚、鲸鱼的身体),而“空间扭曲”,也就是贴图偏移

OK,核心思想我们已经阐述完毕,所以我们现在可以自信的说出:

涟漪 ≈ 波形 ≈ 振幅 ≈ 画面的扭曲 ≈ 贴图偏移

回过头来看代码片段:

首先是将预设的波形传给shader:

        //伪代码 [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));
        }

然后是如何得到振幅:

    //伪代码 [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;//在波形曲线上得到振幅
    }

最后是根据振幅算出贴图偏移:

    //伪代码 [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来实现灰色的反射效果,优化表现
    }

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

至此,整个原理也基本讲完了,如果还有什么地方文字描述的不够具体的,可以回过头来看看上一节的代码,会让你豁然开朗。当然,如果还有解释的不够明白的地方(或者错误),欢迎在下方评论区留言交流~

· 现在就用,现在!

下载项目试一试?

示例项目地址:https://github.com/wasd6267016/RippleEffectExample

clone到本地后unity打开即可,Unity 2018.2.3f1实测没问题,理论上unity5也可以。

想用到自己的工程?

unitypackage[微云下载]涟漪特效_2DCameraWithRippleEffect.unitypackage(链接失效欢迎随时告诉我来维护)

导入的prefab为一个2D camera,可以把相同的脚本挂到自己的相机上面。

· A little more

在实际应用上,还会遇到一些问题:

这个shader是作用于主相机的,所以涟漪会扩散到整个屏幕范围内,但是有时候水面也会显示在屏幕内,我们并不希望涟漪在水平面之外继续扩散

在shader里面可以这样做:

//[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,这样就可以控制涟漪扩散的范围。

当然,也可以设计更丰富的控制逻辑,比如把涟漪设计成只在指定方向传播,这样甚至可以把这个效果设计成一个技能:“水波攻击”(好吧 只能想出这么中二的名字了),见下图:

d77f5e46941f7940481065efaadcda12.gif

· 结语

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

感谢阅读!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值