UnityShader26:运动模糊

 

一、在 Unity 中使用深度纹理或法线纹理

前置:OpenGL基础29:深度测试,关于深度测试的流程以及深度值的算法、空间变换都在这里提到过

在 Unity 中,想要在着色器中获得当前摄像机的深度纹理或者法线纹理,只需要设置 Camera 组件的 depthTextureMode

//让摄像机产出一张深度纹理
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
//让摄像机产出一张深度-法线纹理
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals

深度纹理来自于深度缓存,若是无法直接获取深度缓存,Unity 就会选择渲染类型为 Opaque、渲染队列 < 2500 的物体,通过着色器替换法渲染它们到深度和法线纹理中,其中选用的 Pass 为 ShadowCaster

着色器获得深度纹理的方法如下:

sampler2D _CameraDepthTexture;
//……
fixed4 frag(vert2frag i): SV_Target
{
    //通过深度纹理和纹理坐标获取深度值,考虑了平台差异,内部实现在HLSLSupport.cginc中
    float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.zw);
    d = Linear01Depth(d);        //LinearEyeDepth(d) / Far
    //d = LinearEyeDepth(d);       //将深度值转回正常视角空间下,范围为[Near, Far]
    return float4(d, d, d, 0.0);
}
着色器得到的深度纹理,如果全黑或全白,就需要调整下摄像机的远裁平面,不能太远

同理,深度-法线纹理通过对 _CameraDepthNormalsTexture 进行采样获取深度和法线信息,可以使用 DecodeDepthNormal 函数对其进行解码,其内部实现如下:

inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal)
{
    depth = DecodeFloatRG(enc.zw);
    normal = DecodeViewNormalStereo(enc);
}

其得到的深度值是线性的,得到的法线是视角空间下的法线方向

 

二、运动模糊原理

本质上是通过在片段着色器中,得到顶点在这一帧及上一帧在屏幕上的位置,然后两点插值混合后得到当前片段的最终颜色,这样相对于之前的模糊采样,其模糊的方向就能和物体的实际运动方向相契合

实现运动模糊需要拿到每个片段的深度值,然后和屏幕 xy 坐标组成 NDC 空间下的坐标

转为具体步骤:

  1. 获得当前帧摄像机 VP 矩阵的逆矩阵 IVP
  2. 获得上一帧摄像机 VP 矩阵
  3. 每帧将 ①② 传入着色器
  4. 在片段着色器中,通过屏幕坐标和深度值拼出当前的 NDC 空间坐标
  5. 拿到 NDC 坐标后通过 IVP 逆回世界空间坐标
  6. 再拿这个世界空间坐标乘上得到的上一帧摄像机 VP 矩阵,获得上一帧的 NDC 空间坐标
  7. 好了到这里上一帧的 NDC 空间坐标、当前帧的 NDC 空间坐标都有了,就可以轻松得到每一个每个像素点上一帧挪动到这一帧的位置向量(Motion Vector)
  8. 根据这个向量计算运动模糊

对于第④步的坑:NDC 空间 xyz 的坐标范围是 [-1, 1],这是正常通过投影矩阵变换得到的结果,但是 Unity 本身处理了原生的 P 矩阵,这样 z 轴的范围就是 [0, 1] 而不是 [-1, 1],因此是否在计算 NDC 坐标时对深度值 z 进行映射有待商榷

对于第⑤步的问题:要知道根据 P 矩阵算出的结果,它的第四维 w(齐次坐标)往往不为1,是后台帮我们做了 /w(齐次剪裁)的操作,当然在根据屏幕坐标和深度值拼出的 NDC 坐标也是剪裁操作之后的,可惜的是这时并不能知道齐次坐标 w,所以需要在逆回世界空间后,再将世界空间坐标除以它的 w 那一维,这样的到的才是真正的世界空间坐标

 

三、代码示例

脚本部分:没有什么特殊之处

using UnityEngine;
using System.Collections;

public class MotionBlur: PostEffectsBase
{
	public Shader shader;
	private Material _material;
	public Material material
	{
		get
		{
			_material = CheckShaderAndCreateMaterial(shader, _material);
			return _material;
		}
	}

	private Camera _myCamera;
	public Camera myCamera
	{
		get
		{
			if (_myCamera == null)
				_myCamera = GetComponent<Camera>();
			return _myCamera;
		}
	}

	//采样间隔(单位像素比例)
	[Range(0.0f, 1.0f)]
	public float blurSize = 0.5f;
	//采样数
	[Range(1, 100)]
	public int blurNum = 20;
	private Matrix4x4 previousVPMatrix;

	void OnEnable()
	{
		//通知摄像机需要深度纹理,此后可以在着色器中使用
		//https://docs.unity3d.com/2021.1/Documentation/ScriptReference/DepthTextureMode.html
		myCamera.depthTextureMode |= DepthTextureMode.Depth;
		//上一帧的摄像机 VP 矩阵
		previousVPMatrix = myCamera.projectionMatrix * myCamera.worldToCameraMatrix;
	}

	void OnRenderImage(RenderTexture src, RenderTexture dest)
	{
		if (material != null)
		{
			material.SetFloat("_BlurSize", blurSize);
			material.SetInt("_BlurNum", blurNum);
			material.SetMatrix("_PreviousVPMatrix", previousVPMatrix);
			//当前帧摄像机 VP 矩阵
			Matrix4x4 currentVPMatrix = myCamera.projectionMatrix * myCamera.worldToCameraMatrix;
			//矩阵求逆,得到 VP 矩阵的逆矩阵 IVP
			Matrix4x4 currentIVPMatrix = currentVPMatrix.inverse;
			material.SetMatrix("_CurrentIVPMatrix", currentIVPMatrix);
			previousVPMatrix = currentVPMatrix;
			Graphics.Blit(src, dest, material);
		}
		else
		{
			Graphics.Blit(src, dest);
		}
	}
}

片段着色器部分:

一个可能的疑问:为什么当摄像机移动的时候,会出现运动模糊现象,而当且仅当摄像机视角变化时,却不会出现运动模糊现象?

→ 原因:其实仅当摄像机视角变化时必然也会出现运动模糊现象,虽然相对于上一帧 P 矩阵没有变,但是 V 矩阵变了。而之所以看上去没有出现运动模糊现象,只是因为效果非常不明显(Motion Vector 长度过小),这在高速调整视角时可以被观察而证实

在重新构建NDC空间坐标时,一个非常坑的点:网上很多的博客包括《UnityShader入门精要》这本书在内,在构建 NDC 坐标时都对采样得到的深度 d 进行了 [0, 1] 到 [-1, 1] 的映射,但对于 DirectX11、DirectX12、PS4、XboxOne 和 Metal,现在使用的都是新的方法深度反转(Reversed direction),即NDC的z轴取值范围是[1, 0](其他的图形接口,保持传统的取值范围:OpenGL是[-1, 1],DirectX是[0, 1])。当然 Unity 也考虑了 Reversed direction,深度缓冲也是同样的道理,因此本例中 NDC 的 z 轴范围正是[1, 0]。综上所述:这里不要对采样得到的深度值d进行映射!!它正好就是NDC下的 z

Shader "Jaihk662/MotionBlur"
{
    Properties
    {
        _MainTex("Base (RGB)", 2D) = "white" {}
		_BlurSize("Blur Size", Float) = 1.0
        _BlurNum("Blur Num", int) = 20
    }

    CGINCLUDE
    #include "UnityCG.cginc"

    int _BlurNum;
    sampler2D _MainTex;
    half _BlurSize;
    //如果设置了 myCamera.depthTextureMode |= DepthTextureMode.Depth,那么这里就可以通过 _CameraDepthTexture 拿到深度图
    sampler2D _CameraDepthTexture;

    half4 _MainTex_TexelSize;
    float4x4 _CurrentIVPMatrix;
    float4x4 _PreviousVPMatrix;
    struct vert2frag
    {
        float4 pos: SV_POSITION;
		half4 uv: TEXCOORD0;
    };

    vert2frag vert(appdata_img v)
    {
		vert2frag o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.uv.xy = v.texcoord;		
        o.uv.zw = v.texcoord;
        
        //对深度纹理进行平台差异化处理
        #if UNITY_UV_STARTS_AT_TOP			
        if (_MainTex_TexelSize.y < 0.0)
            o.uv.w = 1.0 - o.uv.w;
        #endif
        return o;
	}

    fixed4 frag(vert2frag i): SV_Target
    {
        //通过深度纹理和纹理坐标获取深度值,考虑了平台差异,内部实现在HLSLSupport.cginc中
        float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.zw);
        //重新构建NDC空间坐标,其中xy都从[0,1]范围映射到[-1,1]就好
        //对于DirectX11、DirectX12、PS4、XboxOne和Metal,现在使用的都是新的方法深度反转(Reversed direction),即NDC的z轴取值范围是[1, 0](其他的图形接口,保持传统的取值范围:OpenGL是[-1,1],DirectX是[0,1])
        //同理:Unity也考虑了Reversed direction,深度缓冲也是同样的道理,因此本例中NDC的z轴范围是[1, 0]。综上所述:这里不要对采样得到的深度值d进行映射!!它正好就是NDC下的z(网上的大多数博客,包括是《UnityShader入门精要》这本书,对于新版本的Unity这里就都已经不对了)
        //https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
        float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d, 1);
        //从NDC空间逆回世界空间,当然这个IVP矩阵是前一帧的IVP矩阵
        float4 D = mul(_CurrentIVPMatrix, H);
        //除以w分量拿到真正的世界空间坐标
        float4 worldPos = D / D.w;
        
        //再转回来,不过这次的VP矩阵是当前帧的VP矩阵
        float4 currentPos = H;
        float4 previousPos = mul(_PreviousVPMatrix, worldPos);
        previousPos /= previousPos.w;
        
        //得到相邻两帧屏幕坐标的差,再乘上采样间隔
        //这个地方为什么要除以2:因为currentPos和previousPos都是NDC空间坐标,所以在计算时需要将它们转回屏幕空,而化简后的结果就是相差除以2
        float2 velocity = (currentPos.xy - previousPos.xy) / 2 * _BlurSize;
        
        //在这两帧NDC空间坐标中间进行采样
        float2 uv = i.uv.xy;
        float4 color = float4(0.0, 0.0, 0.0, 0.0);
        for (int it = 0; it < _BlurNum; it++)
        {
            float4 currentColor = tex2D(_MainTex, uv);
            color += currentColor;
            uv += velocity / _BlurNum;
        }
        color /= _BlurNum;

        //d = Linear01Depth(d);
        return fixed4(color.rgb, 1.0);
    }
    ENDCG

    Subshader
    {
        ZTest Always Cull Off ZWrite Off
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
			#pragma fragment frag
            ENDCG
        }
    }
    FallBack Off
}

 

参考资料:

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值