跳转至

深度重建世界坐标

在后处理 Shader 还有一些屏幕空间效果中,常需要使用深度还原世界坐标。

逆矩阵法

推导

根据 uv 和 depth 可以还原出 positionNDC.xyz。由

positionCS = mul(MatrixVP, positionWS);
positionNDC = positionCS / positionCS.w; // positionNDC.w 一定为 1

可得

positionWS = mul(MatrixInvVP, positionCS.w * positionNDC);

w 分量的公式为

positionWS.w = dot(MatrixInvVP[3], positionCS.w * positionNDC);

因为 positionWS.w == 1 所以

positionCS.w = 1 / dot(MatrixInvVP[3], positionNDC);

带回去得到

positionWS = mul(MatrixInvVP, positionNDC) / dot(MatrixInvVP[3], positionNDC);

化简得到

positionWS = mul(MatrixInvVP, positionNDC);
positionWS /= positionWS.w;

URP 实现

Packages/com.unity.render-pipelines.universal/ShaderLibrary/Common.hlsl 里有相关的实现。

注意此处 URP 的 positionNDC.xy 是屏幕 uv,范围是 [0, 1],与前面推导时用的 positionNDC 的定义不同。

float4 ComputeClipSpacePosition(float2 positionNDC, float deviceDepth)
{
    float4 positionCS = float4(positionNDC * 2.0 - 1.0, deviceDepth, 1.0);

#if UNITY_UV_STARTS_AT_TOP
    // Our world space, view space, screen space and NDC space are Y-up.
    // Our clip space is flipped upside-down due to poor legacy Unity design.
    // The flip is baked into the projection matrix, so we only have to flip
    // manually when going from CS to NDC and back.
    positionCS.y = -positionCS.y;
#endif

    return positionCS;
}

float3 ComputeWorldSpacePosition(float2 positionNDC, float deviceDepth, float4x4 invViewProjMatrix)
{
    // 这个 positionCS 才是前面推导时的 positionNDC
    float4 positionCS  = ComputeClipSpacePosition(positionNDC, deviceDepth);
    float4 hpositionWS = mul(invViewProjMatrix, positionCS);
    return hpositionWS.xyz / hpositionWS.w;
}

使用方法

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

// ...

float deviceDepth = SampleSceneDepth(input.texcoord.xy);

#if !UNITY_REVERSED_Z
    deviceDepth = deviceDepth * 2.0 - 1.0;
#endif

float3 positionWS = ComputeWorldSpacePosition(input.texcoord.xy, deviceDepth, unity_MatrixInvVP);

相似三角形法

这个方法来自 SIGGRAPH 2011 中的 Secrets of CryENGINE 3  Graphics Technology。

推导

原理图(原图来自 Secrets of CryENGINE 3  Graphics Technology 的 ppt)
原理图(原图来自 Secrets of CryENGINE 3  Graphics Technology 的 ppt)

假设要重建点 C 的世界坐标。由

\[ \triangle \text{ABC} \sim \triangle \text{ADE} \]

得到

\[ \dfrac{\left | \text{AB} \right |}{\left | \text{AD} \right |} = \dfrac{\left | \text{AC} \right |}{\left | \text{AE} \right |} \]

如果深度是在 Linear01 空间的话,\(\left | \text{AD} \right | =1\)\(\left | \text{AB} \right |\) 就是点 C 的 Linear01Depth,所以

\[ \overrightarrow{\text{AC}} = \left | \text{AB} \right | \overrightarrow{\text{AE}} \]

等号两边同时变换到世界空间

\[ \overrightarrow{\text{AC}}_\text{ws} = \left | \text{AB} \right | \overrightarrow{\text{AE}}_\text{ws} \]

再加上点 A(相机)的世界坐标就能算出点 C 的世界坐标。

URP 实现

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

先在 Vertex Shader 里计算 \(\overrightarrow{\text{AE}}_\text{ws}\)

// 在 Blit Shader 中 texcoord 需要使用 API 计算出来,参考 Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl
float3 positionWS = ComputeWorldSpacePosition(texcoord, UNITY_RAW_FAR_CLIP_VALUE, unity_MatrixInvVP);
output.viewRayWS = positionWS - GetCameraPositionWS();

在 Fragment Shader 里,还原世界坐标

float depth = Linear01Depth(SampleSceneDepth(input.texcoord.xy), _ZBufferParams);
float3 positionWS = depth * input.viewRayWS + GetCameraPositionWS();

这个方法的矩阵运算在 Vertex Shader 里进行,计算量小一点,但是 URP 的 Blit Vertex Shader 改起来相对麻烦一点。第一种方法只需要改 Fragment Shader 就行了,更好写。

参考资料


相关文章