Unity Shader - Motion Trail Effect(模型运动轨迹/拖尾效果)

141 篇文章 35 订阅
74 篇文章 5 订阅


我们以前玩:拳皇(我是拳皇手残党),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

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值