上篇深度值专题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的函数,因此是非线性的。
上图是OpenGL的d值的函数图形,这儿n设置为0.5,f设置为1000,可以看到d值相对于z值不是线性变化的,且大部分数值集中在near plane附近。
线性深度
深度缓冲/NDC的深度值是非线性的,而视图空间(View Space)的深度值是线性的。在Shader中我们经常需要使用线性的深度值,这就需要转换。URP提供了两个函数Linear01Depth
和LinearEyeDepth
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中使用线性深度以及重建世界坐标的方法。