深入URP之Shader篇11: 深度值专题(2)

上篇深度值专题1中主要讨论了Reversed Z,本篇讨论线性深度值。

非线性深度和线性深度

深度缓冲中的深度值是非线性的

深度缓冲中存储的是经过映射的NDC空间z坐标值。对于OpenGL,NDC的z坐标范围为[-1,1],然后经过glDepthRange映射,通常映射到[0,1];对于D3D,如果没有Reversed Z,NDC和深度缓冲的z范围为[0,1],如果Reversed Z则为[1,0]。而深度缓冲中的z值是非线性的,这是指从view space中线性的z值被变换到NDC的深度值d是非线性的变换,这个变换关系为:d = A/Z + B。其中AB是系数,值和使用的投影约定有关系,但总的来说d是1/z的函数,因此是非线性的。

image.png
上图是OpenGL的d值的函数图形,这儿n设置为0.5,f设置为1000,可以看到d值相对于z值不是线性变化的,且大部分数值集中在near plane附近。

线性深度

深度缓冲/NDC的深度值是非线性的,而视图空间(View Space)的深度值是线性的。在Shader中我们经常需要使用线性的深度值,这就需要转换。URP提供了两个函数Linear01DepthLinearEyeDepth

Linear01Depth

实现如下:

// Z buffer to linear 0..1 depth (0 at camera position, 1 at far plane).
// Does NOT work with orthographic projections.
// Does NOT correctly handle oblique view frustums.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float Linear01Depth(float depth, float4 zBufferParam)
{
    
    
    return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}

这个函数返回的是0~1之间的线性深度值,0为camera位置,1为far plane。输入depth一般是depth texture采样出来的深度。而zBuferParam是从投影矩阵获取的参数,简单说我们使用投影矩阵将eye space的z变换到clip space,然后经过透视除法变换到NDC的z,在经过映射变成深度缓冲的深度,而将这个过程反过来就可以得到eye space的z,或者是这儿的01Depth。注意这个计算仍然需要考虑ReversedZ,不过Unity已经在zBufferParam参数填充的时候处理了,是否ReversedZ会填充不同的参数,注释里面说的参数值是ReversedZ的情况。

Linear01DepthFromNear

// Z buffer to linear 0..1 depth (0 at near plane, 1 at far plane).
// Does NOT correctly handle oblique view frustums.
// Does NOT work with orthographic projection.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float Linear01DepthFromNear(float depth, float4 zBufferParam)
{
    
    
    return 1.0 / (zBufferParam.x + zBufferParam.y / depth);
}

和上面差不多,不过此时的0就是代表near plane了。

LinearEyeDepth

// Z buffer to linear depth.
// Does NOT correctly handle oblique view frustums.
// Does NOT work with orthographic projection.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float LinearEyeDepth(float depth, float4 zBufferParam)
{
    
    
    return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}

同样是通过zBufferParam重新计算出eye space Z。

Unity的投影矩阵

上面关于线性深度的计算其实是依赖于实际使用的投影矩阵,所以如果你想自己推导,必须知道Unity的投影矩阵是怎么构造的。实际上Unity的投影矩阵虽然是遵照OpenGL的惯例,但是在不同的平台上,有可能有一点点改变,就比如ReversedZ。因此使用GL.GetGPUProjectionMatrix()得到的投影矩阵是已经处理过ReversedZ的。所以如果你自己要创建投影矩阵,需要自己处理ReversedZ,在c#代码中,使用SystemInfo.usesReversedZBuffer判断平台是否是翻转Z,如果是则翻转Z的方向。

使用深度重建世界坐标

SRP Core中提供了一个方法,可以从NDC坐标和设备深度重建出世界坐标:

float3 ComputeWorldSpacePosition(float2 positionNDC, float deviceDepth, float4x4 invViewProjMatrix)
{
    
    
    float4 positionCS  = ComputeClipSpacePosition(positionNDC, deviceDepth);
    float4 hpositionWS = mul(invViewProjMatrix, positionCS);
    return hpositionWS.xyz / hpositionWS.w;
}

这个方法使用的是逆VP矩阵的方式,这个矩阵是

UNITY_MATRIX_I_VP

一般来说,我们会使用射线方式重建世界坐标,SRP Core提供的这个方法在FS中执行需要每个片段都执行一个矩阵乘法,性能不是特别好。

本篇总结

本篇简单总结了URP Shader中使用线性深度以及重建世界坐标的方法。

猜你喜欢

转载自blog.csdn.net/n5/article/details/128898695
今日推荐