为什么要从深度图重建世界坐标

一个很大的应用情景是在后处理的阶段,或是计算一些屏幕空间的效果(如SSR、SSAO等),只能获取到一张深度贴图,而不是每一个几何体的顶点数据,很多的计算中却又需要用到世界坐标或者是视空间的坐标,这时我们就需要通过深度图来重建世界空间的坐标。

重建世界坐标的流程

  1. 首先要获取屏幕空间的UV,这里记为positionSS,范围是(0, 1)(0, 1)。
  2. 使用UV采样深度贴图,获取到当前的深度值。
  3. 使用UV和深度值,得到标准化设备坐标,这里记为positionNDC。
  4. 使用裁剪空间到视空间的变换矩阵乘以positionNDC,除以W分量,得到视空间坐标,这里记为positionVS。
  5. 使用视空间到世界空间的变换矩阵乘以positionVS,得到世界空间坐标,这里记为positionWS。

这里使用DepthToPositionShader.shader,假装是屏幕后处理的shader,来演示一下重建世界坐标的流程,这样比直接写屏幕后处理的shader能够更好的去了解Unity的空间变换的方式。

这个shader有以下几个需要注意的点:

  1. 为了使用_CameraDepthTexture这张深度贴图,需要在srp的设置中开启Depth Texture这个选项。这样子在渲染的时候会在DepthPrePass用shader中的Depth Only这个pass去先渲染出深度贴图。我们就能够在渲染物体的时候直接拿到包含当前物体的深度贴图了。
  2. 顶点着色器和片元着色器中的SV_POSITION并不完全相同。对于顶点着色器来说,SV_POSITION就是之前所说的\((\frac X {\tan {\frac {fovy} 2} \cdot \frac x y }, -\frac Y {\tan {\frac {fovy} 2}}, \frac {Zn} {f - n} + \frac {fn} {f - n}, -Z)\);但是在片元着色器中,SV_POSITION的XY分量会乘上屏幕的宽高,Z分量则是已经除以W之后的深度值。屏幕的宽高信息保存在_ScreenParams这个内置的变量中,它的前两位是屏幕的宽高像素数,后两位是宽高的像素数的倒数加一。
  3. 要针对DX11和OpenGL不同的透视变换矩阵来调整UV的Y分量的数值,也就是要注意UNITY_UV_STARTS_AT_TOP这个宏的使用。出现获得的坐标跟随着摄像机的移动发生奇怪的倾斜的时候,往往都是忘记对Y分量的平台差异进行处理。
  4. 最后得到的视空间和世界空间的坐标值,要记得除以这个坐标值的W分量,相当于是做了一次归一化,才能得到正确的坐标。

DepthToPositionShader.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);
        output.positionCS = vertexPositionInputs.positionCS;
        output.texcoord = input.texcoord;
        return output;
    }

    float4 Frag(Varyings input) : SV_TARGET
    {
        float2 positionSS = input.positionCS.xy * (_ScreenParams.zw - 1);
        float depth = tex2D(_CameraDepthTexture, positionSS).r;
        float3 positionNDC = float3(positionSS * 2 - 1, depth);

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

#if REQUIRE_POSITION_VS
        float4 positionVS = mul(UNITY_MATRIX_I_P, float4(positionNDC, 1));
        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
        }
    }
}