1. 程式人生 > >Unity Shader實現運動模糊(一) : 攝像機運動產生模糊

Unity Shader實現運動模糊(一) : 攝像機運動產生模糊

運動模糊是個經常會用到的效果,常見的實現步驟是:

  1. 對深度紋理進行取樣,取得當前片元的深度資訊
  2. 根據深度資訊建立當前片元的NDC空間的座標curNDCPos
  3. 把curNDCPos乘以當前VP矩陣的逆矩陣(即View*Projection)-1,得到當前片元的世界空間座標WorldPos
  4. 把WorldPos乘以上一幀的VP矩陣(即View*Projection),得到上一幀在裁切空間中的位置 lastClipPos
  5. 把lastClipPos除以其w分量,得到NDC空間位置lastNDCPos
  6. 用當前片元NDC空間位置 減去 上一幀NDC空間位置(即 curNDCPos-lastClipPos),得到速度的方向speed
  7. 沿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需要包含的一些狀態

由於後處理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內部對各種巨集進行了判斷,能確保在不同的平臺都能正確地得到深度值。

HLSLSupport.cginc檔案中的描述

效果圖:

最後感謝馮樂樂大神的書和部落格。

package檔案
提取碼:vpud

參考連結:
https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html
https://docs.unity3d.com/Manual/SL-PlatformDifferences.html