Unity Shader - Motion Trail Effect(模型運動軌跡/拖尾效果)

我們以前玩:拳皇(我是拳皇手殘黨),98裏的盧卡爾的某個招式,快速滑動,攻擊碰到的敵人,滑動過程,人物會有拉伸效果,如下圖:
在這裏插入圖片描述

但是這個是2D的,而且是手繪的關鍵幀。(手繪的方式來實現每一幀的過程,效果當然是最理想的,但是人力成本相當的大)

那下面我們就在3D中實現類似的效果吧。

先看看效果

先看看靜態效果:
在這裏插入圖片描述

Gif效果,可以調整顏色,過程強度
在這裏插入圖片描述

實現思路

  • 兩個pass
  • 第一個pass繪製實體
  • 第二個pass繪製運動拖拽的拉伸模型

第一個pass繪製實體就沒啥好說的

關鍵說一下第二個pass吧

本來想只有一個pass

一個pass實現會有一個問題,但是也是可以通過與美術溝通後,可以避免的。
那就是,如果模型頂點的索引組成的面,基本沒有共用索引的話,那麼頂點拉伸會導致模型給拉開了,導致後面看過去是,模型穿幫了,如下面的GIF
在這裏插入圖片描述

所以我還是使用兩個pass的方式來製作。
如果要用一個pass的方式來製作,起始就保留第二個pass繪製就好了,第一個pass不需要了。這樣還可以提高性能合批上也會好很多

但如果要只用一個pass的話代碼還是要重寫一遍。因爲與兩個pass的方式不太一樣。
當然是用效果與兩個pass的,還是會有不同的。

確定運動向量

向量有方向,大小(模)

我們給shader添加了一個uniform變量:float4的,xyz存着運動向量的單位向量,w存着運動向量的模

腳本中更新運動向量傳入到shader即可。

shader

...
float4 _MoveDir; // xyz存着運動向量的單位向量,w存着運動向量的模
...

csharp

private void update()
{
	...
	Material mat ...
	mat.SetVector4("_MoveDir", ....);
	...
}

拉伸模型

我們上面有運動向量數據了,其實就可以拿這個數據來處理模型的拉伸

  • 確定拉伸方向(運動反向)
  • 確定拉伸強度(運動向量的模)
  • 屏蔽拉伸頂點(模型法線與運動反向向量求點積:dot(normal, _MoveDir.xyz))
  • 僞隨機(我這使用perlinNoise,比較平滑)因數來影響每個頂點的拉伸強度。

偏移模型頂點

vertex shader

    v2f_motion vert_motion (appdata v) {
        v2f_motion o;
        float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
        worldPos.xyz += _MoveDir.xyz * _MoveDir.w;
        o.vertex = mul(unity_MatrixVP, worldPos);
        ...
        return o;
    }

整體代碼思路就是:

  • 先將模型座標轉換到世界座標float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
  • 在對世界座標拉伸邊緣(_MoveDir.w我們前面有介紹)worldPos.xyz += _MoveDir.w;
  • 再對偏移後的座標轉回裁剪座標o.vertex = mul(unity_MatrixVP, worldPos);

看看運行效果:
在這裏插入圖片描述

上圖中,紅色框是模型真是座標,我們偏移了_MoveDir.w的強度

_MoveDir = (-0.03816666, 0, -0.9992714, 5.7642)

加上perlinNoise僞隨機再偏移模型頂點

vertex shader

    v2f_motion vert_motion (appdata v) {
        v2f_motion o;
        float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
        worldPos.xyz += _MoveDir.xyz * ((perlin_noise(v.vertex.xy) * 0.5 + 0.5) * _MoveDir.w);
        o.vertex = mul(unity_MatrixVP, worldPos);
        ...
        return o;
    }

再運行看看
在這裏插入圖片描述

但是需要的拉伸效果,並不是扭曲一下就好了。
所以需要保留:面向運動方向的頂點不拉伸

前門提到過:屏蔽拉伸頂點(模型法線與運動反向向量求點積:dot(normal, _MoveDir.xyz))

屏蔽面向運動方向頂點+perlinNoise僞隨機再偏移模型頂點

vertex shader

    v2f_motion vert_motion (appdata v) {
        v2f_motion o;
        float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
        float3 worldNormal = UnityObjectToWorldNormal(v.normal);
        fixed MDDotN = max(0, dot(_MoveDir.xyz, worldNormal));
        half offsetFactor = (perlin_noise(v.vertex.xy) * 0.5 + 0.5) * MDDotN;
        o.normal.z = (offsetFactor * _MoveDir.w);
        worldPos.xyz += _MoveDir.xyz * o.normal.z;
        o.vertex = mul(unity_MatrixVP, worldPos);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
        o.normal.xyz = worldNormal;
        return o;
    }

屏蔽的主要代碼fixed MDDotN = max(0, dot(_MoveDir.xyz, worldNormal));

再來看看運行效果
在這裏插入圖片描述

OK,僞隨機拉伸就好了。
就下來對拉伸的頂點顏色控制一下就好了

拉伸片段着色

  • shader添加了一個 float _alpha的uniform,控制透明度
  • 再添加一個fixed4 _MotionTintColor,對拖尾着色
  • _InvMaxMotion 是我們保留拖拽最大長度的一個數值,因爲我們不需要太長的拖尾,然後也方便用於控制alpha過渡
  • i.normal.z保存的是我們頂點着色器中的,該頂點確定的拉伸強度值
    fixed4 frag_motion (v2f_motion i) : SV_Target {
        fixed4 col = tex2D(_MainTex, i.uv);
        col.rgb += _MotionTintColor.rgb * _alpha;
        col.a = _alpha * saturate(1 - (i.normal.z * _InvMaxMotion));
        return col;
    }

來看看運行效果
在這裏插入圖片描述

再加第一個pass

運行看看效果
在這裏插入圖片描述
然後調整透明度,看一下GIF
在這裏插入圖片描述

整合到Timeline中,看看運行效果

在這裏插入圖片描述

將拖尾消失時間變短,再一邊變換顏色看看效果
在這裏插入圖片描述

用鼠標將人物拖來拖去,看看效果
在這裏插入圖片描述

完整的Code

CSharp

using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// author  :   jave.lin
/// date    :   2020.03.01
/// 簡單實現運動拖尾,模型的頂點儘量共面使用
/// 如果頂點很多是不共面的索引使用,那麼模型拉伸效果就很糟糕
/// </summary>
public class MotionEffectScript : MonoBehaviour
{
    public Shader targetShader;

    public Color motionColor = Color.white;

    [Range(1, 100f)]
    public float tweenSpeed = 10;
    [Range(1, 100f)]
    public float maxMotion = 10;

    private List<Material> matList;
    private int moveDir_hash = 0;
    private int motionColor_hash = 0;
    private int invMaxMotion_hash = 0;
    private int alpha_hash = 0;

    private Vector3 lastPos;

    void Start()
    {
        if (targetShader == null) targetShader = Shader.Find("Custom/MotionEffect");
        // 提供update, material set uniform variables的速度,所以先拿到uniform字符的hash
        moveDir_hash = Shader.PropertyToID("_MoveDir");
        motionColor_hash = Shader.PropertyToID("_MotionTintColor");
        invMaxMotion_hash = Shader.PropertyToID("_InvMaxMotion");
        alpha_hash = Shader.PropertyToID("_alpha");
        matList = new List<Material>();
        // 收集材質
        CollectMats(gameObject.GetComponentsInChildren<MeshRenderer>(true));
        CollectMats(gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true));
        // 記錄上次位置
        lastPos = transform.position;
    }

    private void CollectMats<T>(T[] renderers) where T : Renderer
    {
        if (renderers == null || renderers.Length == 0)
            return;

        foreach (var item in renderers)
        {
            // 判斷是用shared的,否則底層會因爲調用了material屬性的getter而新建一個Material
            if (item.sharedMaterial.shader == targetShader)
            { // 這裏不要用sharedMaterial,否則會影響到其他使用了相同材質的引用對象
              // 所以我們使用material的變量
                matList.Add(item.material);
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        Vector4 moveDir = Vector4.zero;
        var curPos = transform.position;
        float alpha = 0;
        if ((curPos - lastPos).magnitude > 0.05f)
        {
            lastPos = Vector3.Lerp(lastPos, curPos, Time.deltaTime * tweenSpeed);
            var desPos = lastPos - curPos;
            moveDir = desPos.normalized;
            moveDir.w = desPos.magnitude;
            alpha = Mathf.Clamp01(moveDir.w / maxMotion);
        }

        foreach (var item in matList)
        {
            item.SetVector(moveDir_hash, moveDir);              // 移動向量,xyz:單位向量,方向,w:模
            item.SetColor(motionColor_hash, motionColor);       // 運動拖尾條的尾部顏色
            item.SetFloat(invMaxMotion_hash, 1f / maxMotion);   // 最大的位移距離倒數,用來控制拖尾的後半部分alpha
            item.SetFloat(alpha_hash, alpha);                   // 整體alpha
        }
    }
}

Shader

// jave.lin 2020.02.29
Shader "Custom/MotionEffect" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}                       // 主紋理
        _MoveDir ("Move Direction", Vector) = (0,0,0,0)             // xyz:移動方向,w,移動強度
        _MotionTintColor ("Motion Tint Color", Color) = (1,1,1,1)   // 移動着色
    }
    CGINCLUDE
    #pragma multi_compile_fog
    #include "UnityCG.cginc"
    #include "AutoLight.cginc"
    #include "Lighting.cginc"
    #pragma multi_compile_fwdbase_fullshadows
    #pragma fragmentoption ARB_precision_hint_fastest
    struct appdata {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
    };
    struct v2f {
        float4 vertex : SV_POSITION;
        float2 uv : TEXCOORD0;
        float3 normal : TEXCOORD1;
    };
    struct v2f_motion {
        float4 vertex : SV_POSITION;
        float2 uv : TEXCOORD0;
        float4 normal : TEXCOORD1;
    };
    sampler2D _MainTex;         // 主紋理
    float4 _MainTex_ST;         // tiling and offset
    float4 _MoveDir;            // 移動向量,xyz:單位向量,方向,w:模
    fixed4 _MotionTintColor;    // 運動拖尾條的尾部顏色
    float _InvMaxMotion;        // 最大的位移距離倒數,用來控制拖尾的後半部分alpha
    float _alpha;               // 整體alpha
    float2 hash22(float2 p) {
        p = float2(dot(p,float2(127.1,311.7)),dot(p,float2(269.5,183.3)));
        return -1.0 + 2.0*frac(sin(p)*43758.5453123);
    }
    float2 hash21(float2 p) {
        float h=dot(p,float2(127.1,311.7));
        return -1.0 + 2.0*frac(sin(h)*43758.5453123);
    }
    //perlin
    float perlin_noise(float2 p) {				
        float2 pi = floor(p);
        float2 pf = p - pi;
        float2 w = pf * pf*(3.0 - 2.0*pf);
        return lerp(lerp(dot(hash22(pi + float2(0.0, 0.0)), pf - float2(0.0, 0.0)),
            dot(hash22(pi + float2(1.0, 0.0)), pf - float2(1.0, 0.0)), w.x),
            lerp(dot(hash22(pi + float2(0.0, 1.0)), pf - float2(0.0, 1.0)),
                dot(hash22(pi + float2(1.0, 1.0)), pf - float2(1.0, 1.0)), w.x), w.y);
    }
    half3 getLDir() {
        return normalize(_WorldSpaceLightPos0.xyz);
    }
    v2f vert (appdata v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);          // clip pos
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);               // uv tiling and offset
        o.normal = UnityObjectToWorldNormal(v.normal);      // normal
        return o;
    }
    v2f_motion vert_motion (appdata v) {
        v2f_motion o;
        // 頂點先變換到世界座標系
        float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
        // 法線轉換到世界座標下
        float3 worldNormal = UnityObjectToWorldNormal(v.normal);
        // 找出與motion運動‘反’向的(拖尾方向)的頂點,根據頂點法線來找
        // 不需要負數,所以clamp的min到0(就時最小值,夾到0)
        // MDDotN 保存的是同向性的因數0.0~1.0
        // MDDotN 時求反向面的係數,與運動方向相反
        fixed MDDotN = max(0, dot(_MoveDir.xyz, worldNormal));
        // perlinoise返回-1~1,偏移到0~1,作爲僞隨機,但又很平滑的效果
        // offsetFactor是頂點偏移係數
        half offsetFactor = (perlin_noise(v.vertex.xy) * 0.5 + 0.5) * MDDotN;
        // 平移強度 = 頂點偏移係數 * 移動方向向量模
        // 將偏移強度存在 normal.z,提高有限的寄存器利用率
        o.normal.z = (offsetFactor * _MoveDir.w);
        // motion根據_MoveDir.xyz方向偏移 * _MoveDir.w向量模
        worldPos.xyz += _MoveDir.xyz * o.normal.z;
        // 再講世界座標轉到clip座標
        o.vertex = mul(unity_MatrixVP, worldPos);
        // uv的平鋪於偏移
        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
        // 頂點法線
        o.normal.xyz = worldNormal;
        return o;
    }
    fixed4 frag (v2f i) : SV_Target {
        fixed4 col = tex2D(_MainTex, i.uv);
        #if MUTE_LIGHT // 暫時屏蔽燈光
        col.rgb *= _LightColor0.rgb;
        col.rgb *= dot(getLDir(), i.normal) * 0.5 + 0.5;    // half-lambert
        #endif
        return col;
    }
    fixed4 frag_motion (v2f_motion i) : SV_Target {
        fixed4 col = tex2D(_MainTex, i.uv);
        #if MUTE_LIGHT // 暫時屏蔽燈光
        // col.rgb *= _LightColor0.rgb;
        // col.rgb *= dot(getLDir(), i.normal) * 0.5 + 0.5;    // half-lambert
        #endif
        col.rgb += _MotionTintColor.rgb * _alpha;
        col.a = _alpha * saturate(1 - (i.normal.z * _InvMaxMotion));
        return col;
    }
    ENDCG
    SubShader {
        Tags { "Queue"="Geometry" "RenderType"="Opaque" "LightMode"="ForwardBase" }
        LOD 100
        // sold : 實體
        Pass {
            Name "sold"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
        // motion trial : 運動拖尾
        pass {
            Name "motion trial"
            // Tags { "Queue"="Transparent" "RenderType"="Transparent" }
            ZWrite off Cull off
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert_motion
            #pragma fragment frag_motion
            ENDCG
        }
    }
    Fallback "Diffuse"
}

Project

備份測試工程:UnityShader_MotionTrailEffectTesting_運動人物模型拖尾效果_2018.3.0f2

References

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章