我們以前玩:拳皇(我是拳皇手殘黨),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