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内部对各种宏进行了判断,能确保在不同的平台都能正确地得到深度值。

扫描二维码关注公众号,回复: 4510176 查看本文章

HLSLSupport.cginc文件中的描述

效果图:

最后感谢冯乐乐大神的书和博客。

package文件
提取码:vpud

参考链接:
https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html
https://docs.unity3d.com/Manual/SL-PlatformDifferences.html

猜你喜欢

转载自blog.csdn.net/h5502637/article/details/84986197