Notifications
Article
Shader案例:顶点运动模糊
Published 9 months ago
1.8 K
12
Shader案例:顶点运动模糊

前言

通过后处理我们可以实现效果很好的运动模糊效果,但是怎耐性能却吃不消,于是,让我们变通一下,换个思路来实现另一种风情的运动模糊。
效果如下:

实现思路
  1. 通过在顶点着色器中对顶点进行偏移,加上适当的噪波来实现随机性。
  2. 对象在移动的时候,利用脚本实时更新偏移参数来实现最终的效果。

顶点着色器部分(一)

顶点着色器是本效果的核心实现,所以我们先来看下顶点着色器中的逐步分解与实现。
首先呢,我们建一个默认的Unlit Shader.
其中顶点着色器代码如下:
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }
其中,UnityObjectToClipPos(v.vertex)表示的是将模型的顶点本地坐标转换到齐次裁剪空间下,那我们就选择在顶点的本地空间下来做偏移。
顶点偏移
同时由于最后顶点的偏移不是固定的一个方向,所以我们需要引入一个vector向量用来承载变化的方向方量,如下面的_Direction
_Direction("Direction",vector) = (0,0,0,1)
对顶点本地坐标进行偏移运算,由方向是三维向量,所以我们指定的是xyz分量,同时又由于_Direction是四维向量,刚好最后一个分量w我们可以利用起来,用来做整体偏移的强度。
v.vertex.xyz += _Direction.xyz * _Direction.w;
此时当我们调节材质面板中的_Direction属性时,可以看到对象的顶点已经产生了偏移,只不过现在是整体偏移了而已(注意橙色框中是角色原来的位置)。
偏移随机
好,那接下来呢,我们要实现随机偏移效果
原理很简单,我们只需要让每个顶点的坐标加上的值不一样即可,有两种方式可实现:
  1. 在顶点着色器中采样一张噪波贴图
  2. 通过噪波算法实现
由于第一种方式在SM2.0上不支持,所以我们这里采用第二种方式.
我们采用常见的噪波公式算出噪波并应用于本地顶点上。
float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453); v.vertex.xyz += _Direction.xyz * _Direction.w * noise;
此时的效果如下:

偏移部分
顶点偏移也有了,随机拉伸感也出来了,但是我们希望的并不是整个对象都被拉伸了,而是只需要部分拉伸。
而这里的部分到底是指哪部分呢?
假如对象向正前方移动,那我们所希望的是对象正面不拉伸,背部那部分才会拉伸,其它方向同理,那这个要如何实现呢?
首先哪部分拉伸哪部分不拉伸,这个我们需要用黑白来区分,这样用一个乘法就可以实现了。
那么问题变成了如何根据方向来求出对象表面的黑白效果.
这时大家可以在场景中新建一个默认球体,然后仔细观察它与平行光的关系,是不是恍然大悟。。。
fixed NdotD = max(0,dot(v.normal,_Direction));
然后将NdotD乘到顶点偏移中去。
v.vertex.xyz += _Direction.xyz * _Direction.w * noise * NdotD;
OK,效果已基本成型,片断着色器中暂时不做任何处理。

C#脚本部分

当运行时,我们需要动态的获取到对象当前帧的位置坐标与上一帧的位置坐标,这样我们就可以计算出对象运动的方向与速度,然后我们就可以利用这些信息去修改我们上面的Shader,以便产生动态的顶点偏移效果。
脚本代码部分都有详细注释,就不再另做解释,直接贴上代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MotionVertexController : MonoBehaviour { private Transform trans; private Material[ ] mats; private Vector3 lastPosition; private Vector3 newPosition; private Vector3 direction; private float t = 0; void Start () { trans = transform; lastPosition = newPosition = trans.position; //获取对象及子对象中的所有渲染器(MeshRenderer或者SkinnedMeshRenderer) var renderers = trans.GetComponentsInChildren<Renderer> (); //获取所有的材质球(针对有些对象有多个部件多个材质的情况) mats = new Material[renderers.Length]; for (int i = 0; i < renderers.Length; i++) { mats[i] = renderers[i].sharedMaterial; } } void Update () { newPosition = trans.position; //如果上一帧的位置追到了当前帧的位置,则重置t if (newPosition == lastPosition) t = 0; t += Time.deltaTime; //上一帧的位置通过t来做插值 lastPosition = Vector3.Lerp (lastPosition, newPosition, t / 2); //求出移动的方向 direction = lastPosition - newPosition; //遍历修改所有材质的_Direction属性 foreach (var m in mats) { m.SetVector ("_Direction", new Vector4 (direction.x, direction.y, direction.z, m.GetVector ("_Direction").w)); } } }
使用方法:直接将脚本拖到对象的最外层GameObject上。
运行,然后移动角色观察下效果,这时会发现效果很奇怪,主要表现在顶点拉伸的方向不对。这是为什么呢?
这其实是由于我们在脚本中使用的是模型在世界空间下的坐标,而Shader中的顶点偏移计算却是在模型的本地空间下进行的,两者的坐标空间不一致导致的原因。
因为,我们选择修改Shader,将相关的计算从模型的本地空间改成世界空间。

顶点着色器部分(二)

由于UnityObjectToClipPos封装的原因,我们需要自行拆出世界空间,如下:
float4 wPos = mul(unity_ObjectToWorld,v.vertex); o.vertex=mul(UNITY_MATRIX_VP,wPos);
然后我们的顶点偏移改成在世界空间下来做,同时也需要把顶点法线转换到世界空间下。
float4 wPos = mul(unity_ObjectToWorld,v.vertex); half3 wNormal = UnityObjectToWorldNormal(v.normal); fixed NdotD = max(0,dot(wNormal,_Direction)); float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453); wPos.xyz += _Direction.xyz * _Direction.w * noise * NdotD; o.vertex=mul(UNITY_MATRIX_VP,wPos);
再次运行,效果就正确了

片断着色器

最后我们可以给整个效果加点修饰,比如给顶点偏移叠加点颜色,这里可以好好利用下顶点着色器中计算出来的NdotD。
完整的代码如下:
Shader "taecg/MotionVertex" { Properties { _Color("Color",color) = (1,0,0.65,1) _MainTex ("Texture", 2D) = "white" {} _Direction("Direction",vector) = (0,0,0,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; half3 normal:NORMAL; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; fixed NdotD:TEXCOORD1; }; fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; half4 _Direction; v2f vert (appdata v) { v2f o; float4 wPos = mul(unity_ObjectToWorld,v.vertex); half3 wNormal = UnityObjectToWorldNormal(v.normal); fixed NdotD = max(0,dot(wNormal,_Direction)); o.NdotD = NdotD; float noise = frac(sin(dot(v.uv.xy, float2(12.9898, 78.233))) * 43758.5453); wPos.xyz += _Direction.xyz * _Direction.w * noise * NdotD; o.vertex=mul(UNITY_MATRIX_VP,wPos); // o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); col += i.NdotD * _Color; UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }

最后

欢迎大家关注更多干货的公众号:Unity技术美术 ( ID:gh_8b69cca044dc )
Tags:
taecg
TA - Artist
21
Comments
寒寒
8 months ago
强强强
1
33
8 months ago
大赞
0
Q(kuang)
9 months ago
。。。
0
j
jasonbao
9 months ago
不错
0
KsGin
9 months ago
程序猿
很棒
0