[Unity Shader]凌波微步效果

相信很多人都看过天龙八部,里面的段誉有一个技能就是凌波微步:移动的时候人先到,衣角跟随其后。说白了就是移动时有一个残影跟着他。下面先看下最终效果

Unity软体_Unity软体

下面我们看如何实现上面的效果。

思路:

1.既然需要移动,那么就需要一个3维(x,y,z三个方向)的数据存储,同时还需要一个变量用来表示偏移强度。

2.需要一个2d贴图来做采样

因此Shader代码很快就出来了

Shader "QShader/UnlitShader_04_1"
{
	Properties
	{
		_MainTex ("MainTex", 2d) = "white"{}
		_Direction ("Direction", vector) = (0,0,0,1)
	}
	SubShader
	{		
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

            #include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _Direction;
 			float4 _MainTex_ST;

			struct appdata
			{
				float4 position : POSITION;
				float2 uv : TEXCOORD0;
			};
			
			struct v2f
			{
				float4 position : SV_POSITION;
				float2 uv:TEXCOORD0;
			};
		 
			v2f vert (appdata v)
			{
				v2f o;
				v.position.xyz += _Direction.xyz * _Direction.w;
				o.position = UnityObjectToClipPos(v.position);
				o.uv = TRANSFORM_TEX(v.uv,_MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{ 
				fixed4 col = tex2D(_MainTex,i.uv);
				return col;
			}
			ENDCG
		}
	}
}

注意里面的 TRANSFORM_TEX 是为了即时将变化在屏幕上显示出来。

我们先看下效果

Unity软体_贴图_02

我们创建两个材质球,第一个材质球不做任何处理,然后将第二个材质球的Direction变量的X修改为2,将两个物体做个对比观察。

Unity软体_2d_03

我们发现物体向右边移动了。接下来我们想要的残影效果还没有,我们使用噪波算法实现随机偏移的效果。

//噪波算法
float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453);

Unity软体_2d_04

我们看到物体是整体都会被拉伸了,但是我们只需要根据他的移动方向做拉伸就好,也就是他的前进方向做拉伸,背面不做拉伸。怎么做呢?

物体在阳光下会有投影,物体的投影,也就是他的反射光线是可以根据入射光线以及他的法线来算出。这里就可以将他的不做拉伸的背面理解为他的反射光线。

那么我们就将这个反射光线作为参数传入进去

//变换拉伸
fixed NdotD = max(0,dot(_Direction,v.normal));
v2f vert (appdata v)
			{
				v2f o;	 
				//噪波算法
				float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453);
				//变换拉伸
				fixed NdotD = max(0,dot(_Direction,v.normal));
				v.position.xyz += _Direction.xyz * _Direction.w * noise * NdotD;
				o.position = UnityObjectToClipPos(v.position);
				o.uv = TRANSFORM_TEX(v.uv,_MainTex);
				return o;
			}

实际就如上所示。至此完整的Shader代码已经出来了。我们增加了一个Color变量用来在贴图上面添加一个好看的颜色,这里仅是为了美观,可以去掉。

Shader "QShader/UnlitShader_04_2"
{
	Properties
	{
		_Color ("Color",Color) = (0,0,0,1)
		_MainTex ("MainTex", 2d) = "white"{}
		_Direction ("Direction", vector) = (0,0,0,1)
	}
	SubShader
	{		
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

            #include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _Direction;
 			float4 _MainTex_ST;
			float4 _Color;

			struct appdata
			{
				float4 position : POSITION;
				float2 uv : TEXCOORD0;
				half3 normal:NORMAL;
			};
			
			struct v2f
			{
				float4 position : SV_POSITION;
				float2 uv:TEXCOORD0;
			};
		 
			v2f vert (appdata v)
			{
				v2f o;	 
				//噪波算法
				float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453);
				//变换拉伸
				fixed NdotD = max(0,dot(_Direction,v.normal));
				v.position.xyz += _Direction.xyz * _Direction.w * noise * NdotD;
				o.position = UnityObjectToClipPos(v.position);
				o.uv = TRANSFORM_TEX(v.uv,_MainTex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{ 
				fixed4 col = tex2D(_MainTex,i.uv);
				col+=_Color;
				return col;
			}
			ENDCG
		}
	}
}

这个时候我们需要一个脚本文件来将物体移动的方向作为参数传给Shader的Direction变量,用来动态显示残影。因此新建AfterglowEffect.cs代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AfterglowEffect : MonoBehaviour {

    private Material[] mats;
    private Vector3 prePosition;
    private Vector3 curPosition;
    private float deltaTime;

    // Use this for initialization
    void Start()
    {
        prePosition = curPosition = transform.position;
        Renderer[] renderers = transform.GetComponentsInChildren<Renderer>();
        mats = new Material[renderers.Length];
        for (int i = 0; i < renderers.Length; i++)
        {
            Renderer renderer = renderers[i];
            mats[i] = renderer.sharedMaterial;
        }
    }

    // Update is called once per frame
    void Update()
    {
        curPosition = transform.position;

        if (curPosition == prePosition)
        {
            deltaTime = 0;
            return;
        }

        deltaTime += Time.deltaTime;
        prePosition = Vector3.Lerp(prePosition,curPosition,deltaTime);

        Vector3 direction = prePosition- curPosition;
        for (int i = 0; i < mats.Length; i++)
        {
            mats[i].SetVector("_Direction", new Vector4(direction.x, direction.y, direction.z, mats[i].GetVector("_Direction").w));
        }
    }
}

这里有两个需要注意的地方

prePosition = Vector3.Lerp(prePosition,curPosition,deltaTime);

我们根据之前的位置和当前的位置通过Lerp函数做插值,动态传入就让残影移动的比较平滑。还有一个要注意的是我们的移动方向

Vector3 direction = prePosition- curPosition;

一般情况下移动方向是新位置减去之前的位置,但是这样会导致残影优先移动了过去,什么意思呢?就是下面这个情况

Unity软体_#pragma_05

我们的移动方向是向右边,但是残影的方向其实应该是向左边,也就是反过来,这样才是对的。