Unity Shader實現運動模糊(一) : 攝像機運動產生模糊
阿新 • • 發佈:2018-12-15
運動模糊是個經常會用到的效果,常見的實現步驟是:
- 對深度紋理進行取樣,取得當前片元的深度資訊
- 根據深度資訊建立當前片元的NDC空間的座標curNDCPos
- 把curNDCPos乘以當前VP矩陣的逆矩陣(即View*Projection)-1,得到當前片元的世界空間座標WorldPos
- 把WorldPos乘以上一幀的VP矩陣(即View*Projection),得到上一幀在裁切空間中的位置 lastClipPos
- 把lastClipPos除以其w分量,得到NDC空間位置lastNDCPos
- 用當前片元NDC空間位置 減去 上一幀NDC空間位置(即 curNDCPos-lastClipPos),得到速度的方向speed
- 沿speed方向進行多次取樣,求出平均值作為當前片元的顏色
在Unity中實現運動模糊需要後處理的配合,在後處理程式碼中需要把 攝像機的depthTextureMode 設定為 DepthTextureMode.Depth(這樣在shader中才能使用深度紋理),還要當前VP逆矩陣和上一幀的Vp矩陣傳遞給shader。
效果圖:
C#程式碼:
using UnityEngine; public class MotionBlur_CameraMove : MonoBehaviour { [Range(0, 0.5f)] public float BlurSize; private Material m_mat; private const string ShaderName = "MJ/PostEffect/MotionBlur_CameraMove"; private Matrix4x4 m_curVP_Inverse; // 當前 VP矩陣的逆矩陣 // private Matrix4x4 m_lastVP; // 上一幀的Vp矩陣 // private Camera m_cam; void Start() { Shader shader = Shader.Find(ShaderName); if (shader == null) { enabled = false; return; } m_mat = new Material(shader); m_cam = Camera.main; if (m_cam == null) { enabled = false; return; } m_cam.depthTextureMode = DepthTextureMode.Depth; } void OnRenderImage(RenderTexture srcRT, RenderTexture dstRT) { if (m_mat == null || m_cam == null) { return; } Matrix4x4 curVP = m_cam.projectionMatrix*m_cam.worldToCameraMatrix; m_curVP_Inverse = curVP.inverse; m_mat.SetFloat("_BlurSize", BlurSize); m_mat.SetMatrix("_CurVPInverse", m_curVP_Inverse); m_mat.SetMatrix("_LastVP", m_lastVP); Graphics.Blit(srcRT, dstRT, m_mat, 0); m_lastVP = curVP; } }
Shader程式碼:
Shader "MJ/PostEffect/MotionBlur_CameraMove" { Properties { _MainTex ("Main Texture", 2D) = "white" {} _BlurSize("Blur Size", Range(0, 10)) = 1 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" } Cull Off ZWrite Off ZTest Always LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float2 uv_depth : TEXCOORD1; }; sampler2D _MainTex; float2 _MainTex_TexelSize; float4 _MainTex_ST; sampler2D _CameraDepthTexture; uniform float _BlurSize; uniform float4x4 _CurVPInverse; uniform float4x4 _LastVP; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.uv_depth = TRANSFORM_TEX(v.uv, _MainTex); #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) { o.uv_depth.y = 1-o.uv_depth.y; } #endif return o; } float4 frag (v2f i) : SV_Target { float2 uv = i.uv; float depth = tex2D(_CameraDepthTexture, i.uv_depth); // float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); // depth = Linear01Depth(depth); float4 curNDCPos = float4(uv.x*2-1, uv.y*2-1, depth*2-1, 1); float4 worldPos = mul(_CurVPInverse, curNDCPos); worldPos /= worldPos.w; // 為了確保世界空間座標的w分量為1 // // worldPos.w = 1; float4 lastClipPos = mul(_LastVP, worldPos); float4 lastNDCPos = lastClipPos/lastClipPos.w; // 一定要除以w分量, 轉換到 NDC空間, 然後再做比較 // float2 speed = (curNDCPos.xy - lastNDCPos.xy)*0.5; // 轉到ndc空間做速度計算 // float4 finalColor = float4(0,0,0,1); for(int j=0; j<4; j++) { float2 tempUV = uv+j*speed*_BlurSize; finalColor.rgb += tex2D(_MainTex, tempUV).rgb; } finalColor *= 0.25; return finalColor; } ENDCG } } Fallback Off }
根據 [官網文件] (https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html) 中的說明 建議寫上一些幾句:
Cull Off
ZWrite Off
ZTest Always
由於後處理shader中使用了一張以上的紋理(_MainTex和_CameraDepthTexture),因此需要手動把uv的y座標翻轉下,以保持兩張圖uv的y座標方向保持一致:
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
o.uv_depth.y = 1-o.uv_depth.y;
}
#endif
對深度紋理進行取樣可以使用 unity自帶的方法SAMPLE_DEPTH_TEXTURE 也可以直接對 _CameraDepthTexture 進行取樣:
float depth = tex2D(_CameraDepthTexture, i.uv_depth);
或
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
兩種方式都能獲取到深度值,大部分平臺上都可以用直接取樣的方式獲取深度值,但是一些平臺需要做些特殊處理例如PSP2,因此使用 SAMPLE_DEPTH_TEXTURE 方式更安全,因為unity內部對各種巨集進行了判斷,能確保在不同的平臺都能正確地得到深度值。
效果圖:
最後感謝馮樂樂大神的書和部落格。
package檔案
提取碼:vpud