Unity3D中根据深度值重建像素点对应的世界坐标

Table of Contents

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

1 深度图介绍

就是将深度信息(Z坐标值)保存在了一张贴图上的R通道上,因为R通道的值范围是[0, 1],所以我们可以用ndc空间下的Z坐标值做下处理就能变成[0, 1]范围了( (Zndc+1)*0.5

2 C#接口取得深度图纹理的方法

Shader.GetGlobalTexture("_CameraDepthTexture")

3 在NDC空间下深度值的计算公式

在NDC空间下深度值的计算公式

4 从片元着色器中根据深度图反推出片元世界坐标且输出的shader

Shader "zznewclear13/DepthToPositionShader"
{
    Properties
    {
       [Toggle(REQUIRE_POSITION_VS)] 
	   _Require_Position_VS("Require Position VS", float) = 0
    }

    HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/core.hlsl"
    #pragma multi_compile _ REQUIRE_POSITION_VS

    sampler2D _CameraDepthTexture;
    
    struct Attributes
    {
        float4 positionOS   : POSITION;
        float2 texcoord     : TEXCOORD0;
    };

    struct Varyings
    {
        float4 positionCS   : SV_POSITION;
        float2 texcoord     : TEXCOORD0;
    };

    Varyings Vert(Attributes input)
    {
        Varyings output = (Varyings)0;
		// 根据顶点在其模型坐标系下的值,计算得到在裁剪空间中的齐次坐标
        VertexPositionInputs vertexPositionInputs = 
		                     GetVertexPositionInputs(input.positionOS.xyz);
							 
		// vertexPositionInputs.positionCS的是齐次裁剪空间坐标,即一个 float4 值,
		// 形式为 (x, y, z, w),其中 w 是用于透视除法的分量。透视除法会在后续的处理
		// 步骤中进行,得到标准化设备坐标(NDC),最终决定物体在屏幕上的位置。
		// 在透视除法之前,齐次裁剪空间的 x 和 y 值可以超出任何特定范围,因为它们与
		// w 是关联的。最终,它们会通过透视除法得到以下标准化的范围
		// 透视除法后(标准化设备坐标)的取值范围:
		// x / w 和 y / w 的取值范围为 [-1, 1]。
		// -1 表示屏幕的最左边(x 方向)或最下方(y 方向)。
		// 1 表示屏幕的最右边(x 方向)或最上方(y 方向)。
        output.positionCS = vertexPositionInputs.positionCS;
        output.texcoord = input.texcoord;
        return output;
    }

    float4 Frag(Varyings input) : SV_TARGET
    {
	    // 根据片元的在裁剪空间中的坐标值,算出片元对应的在屏幕空间的坐标值
		// _ScreenParams的xy分量是渲染目标纹理的宽度和高度(以像素为单位),
		// z分量是1.0 + 1.0/宽度,w为1.0 + 1.0/高度。所以屏幕空间的坐标值为
		//【齐次裁剪空间坐标】的坐标值的xy,分别除以渲染目标纹理的高宽
		
		// 因为input.positionCS.xy的取值范围是[0,渲染目标纹理的宽]和
		// [0,渲染目标纹理的高],故而positionSS的xy分量,经过上一步的运算后,
		// 分别得到的取值范围是[0,1]。就是一个有效的纹理坐标uv值
        float2 positionSS = input.positionCS.xy * (_ScreenParams.zw - 1);
		
		// 利用纹理坐标uv值,对深度图纹理进行采样,得到的值,就是当前屏幕空间中
		// 片元对应的深度值,即NDC空间中的坐标值z分量
		// _CameraDepthTexture是Unity3D的内置着色器变量
        float depth = tex2D(_CameraDepthTexture, positionSS).r;
		
		// 因为NDC空间的取值范围是:
		// X和Y轴的取值范围:[-1, 1]。
		// -1表示屏幕的最左侧或最底部。1表示屏幕的最右侧或最顶部。
		// Z轴的取值范围是[0, 1]。0和1分别表示相机的近截平面和远截平面
		// 而positionSS的取值范围是[0,1],所以需要做【乘2减1】的操作,将其映射到
		// [-1,1]的取值范围内。
        float3 positionNDC = float3(positionSS * 2 - 1, depth);

#if UNITY_UV_STARTS_AT_TOP
        positionNDC.y = -positionNDC.y;
#endif

		// 得到裁剪空间的NDC坐标之后,就可以反向地推出片元对应的屏幕空间了
#if REQUIRE_POSITION_VS
		// UNITY_MATRIX_I_P是投影矩阵的逆矩阵(inverse projection matrix)
        float4 positionVS = mul(UNITY_MATRIX_I_P, float4(positionNDC, 1));
		
		// 当你通过逆投影矩阵将点从 clip space 变换回 view space 时,生成的点仍
		// 然是齐次坐标 (x, y, z, w),因此需要除以 w 来将其转换回标准的三维欧几里
		// 得坐标。这是因为:在齐次坐标系下,w 分量影响了 x、y 和 z 分量的比例。
		// 如果不除以 w,这些分量的值将无法正确反映物体在视图空间中的位置,导致不正
		// 确的几何表示。view space中的坐标应该是三维欧几里得坐标,而不是齐次坐标。
		// 透视除法确保得到的 x'、y'、z' 是正确的三维位置。
        positionVS /= positionVS.w;
		
		// 再通过观察矩阵的逆矩阵,将顶点变换回世界坐标
        float4 positionWS = mul(UNITY_MATRIX_I_V, positionVS);
#else
        float4 positionWS = mul(UNITY_MATRIX_I_VP, float4(positionNDC, 1));
        positionWS /= positionWS.w;
#endif

        return positionWS;
    }

    float4 DepthFrag(Varyings input) : SV_TARGET
    {
        return 0;
    }

    ENDHLSL

    SubShader
    {
        Tags{ "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
            Tags{"LightMode"="UniversalForward"}
            ZWrite On
            ZTest LEqual
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            ENDHLSL
        }

        Pass
        {
            Tags{"LightMode" = "DepthOnly"}
            ZWrite On
            ZTest LEqual
            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment DepthFrag
            ENDHLSL
        }
    }
}

5 在Unity3D默认渲染管线中在后处理阶段用深度图重建世界坐标

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class PostEffectUseDepthTex : MonoBehaviour
{

    private Camera m_Camera;
    private Material m_GetWorldPosByDepthMat;

    void Start()
    {
        m_Camera = GetComponent<Camera>();
        m_GetWorldPosByDepthMat = 
		          new Material(Shader.Find("My/DepthTex/GetWorldPosByDepth"));
    }

    void OnRenderImage(RenderTexture src, RenderTexture dst)
    {
		// 用摄像机的观察矩阵和投影矩阵相乘,然后得出其逆矩阵
		// 将其传递给shader的_ClipToWorldMatrix,即从齐次裁剪空间变换回世界空间中
        var VPMatrix = m_Camera.projectionMatrix * m_Camera.worldToCameraMatrix;
        var clipToWorldMatrix = VPMatrix.inverse;
        m_GetWorldPosByDepthMat.SetMatrix("_ClipToWorldMatrix", clipToWorldMatrix);
        Graphics.Blit(src, dst, m_GetWorldPosByDepthMat);
    }

}

下面是shader代码

// 后处理中根据深度图重建世界坐标, 注意: 只适用于后处理OnRenderImage那边,即使用内置管线
Shader "My/DepthTex/GetWorldPosByDepth"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }

    SubShader
    {
        ZTest Always
        ZWrite Off

        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 _CameraDepthTexture; //深度图
            sampler2D _MainTex; //后处理提供的屏幕图
            float4 _MainTex_TexelSize;
            float4x4 _ClipToWorldMatrix; //裁剪空间到世界空间转换矩阵, 需要c#部分提供

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
				// 因为传递进来的就是一个覆盖全屏幕的矩形,所以顶点的纹理采样uv
				// 就可以直接用作对深度图的纹理采样UV
                o.uv_depth = v.uv;
            #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
            #endif
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
				// 对深度图纹理进行采样
                float depth = SAMPLE_DEPTH_TEXTURE(
				                    _CameraDepthTexture, i.uv_depth);
				
				// 在Unity中,UNITY_REVERSED_Z 是一个内置宏,用来指示是否使用了反向Z缓冲(Reversed Z-buffering)。
				// Unity引擎从Unity 5.5开始支持反向Z缓冲,这种方式可以在远平面处提高深度缓冲的精度。
				// 通常,在深度缓冲中,靠近相机的物体会有较大的深度值,远离相机的物体会有较小的深度值。
				// 然而,在启用了反向Z缓冲的情况下,Z值的方向是反的:近处的物体会有较小的Z值,远处的物
				// 体会有较大的Z值。这种反转可以减少Z-fighting(Z轴上的对象深度接近时的渲染问题),
				// 特别是在需要较远的远裁剪面时提高精度。
				// 在Shader中使用时,可以通过这个宏来决定如何处理深度值。例如:
            #if defined(UNITY_REVERSED_Z)
                depth = 1 - depth;
            #endif
			    // 后处理的那张贴图就对应整个屏幕, 所以可以直接用片元的纹理映射坐标,即齐次裁剪空间下的坐标,
				// 对应创建创建ndc坐标(参见前面的shader)
                float4 ndcPos = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, 
				                       depth * 2 - 1, 1);

				//ndcPos和clipPos只是差一个倍数(就是w分量)
                float4 worldPos = mul(_ClipToWorldMatrix, ndcPos); 
                worldPos /= worldPos.w;

                //用世界坐标做相关处理
                fixed4 c = tex2D(_MainTex, i.uv);
                return c;
            }
            ENDCG
        }
    }

    FallBack Off
}

参考网页

世界坐标恢复

Unity使用深度信息重建世界坐标

【UnityShader预备知识】内置变量和函数

深度图及后处理中用深度图重建世界坐标

Reconstruct world space positions in a shader in URP

从深度纹理重建像素的世界空间位置

Unity中URP下深度图的线性转化

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus