运动模糊是个经常会用到的效果,常见的实现步骤是:
- 对深度纹理进行采样,取得当前片元的深度信息
 - 根据深度信息建立当前片元的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
[图片上传失败...(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内部对各种宏进行了判断,能确保在不同的平台都能正确地得到深度值。
效果图:
最后感谢冯乐乐大神的书和博客。
package文件
提取码:vpud
参考链接:
: https://docs.unity3d.com/Manual/PostProcessingWritingEffects.html
: https://docs.unity3d.com/Manual/SL-PlatformDifferences.html