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。

效果图:


image

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

[图片上传失败...(image-bd99aa-1544771820108)]

由于后处理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

你可能感兴趣的:(Unity Shader实现运动模糊(一) : 摄像机运动产生模糊)