深度和法线纹理为游戏开发过程中常用的信息,通常情况下存储在深度法线纹理中,使用时需要进行对应的转换。
简介
- 在游戏开发过程中,深度和法线信息起着重要的作用。在渲染管线中,深度和法线通常会存储在深度法线纹理中,供各个阶段使用。其中,纹理的生成规则和使用方式相互关联,为了能够了解其使用方法,需要认识其生成过程。
深度纹理
深度值
正交投影
- 经过投影变换和齐次除法后,可以得到裁剪空间下的深度
z_NDC
其中 z_view为观察空间下的深度。 - 在像素着色器中,每一个像素的坐标都需要通过插值计算得到。然而,计算结果的值存储在
z-buffer中,渲染管线通常不能直接进行操作,因此需要存入到深度纹理中。由于深度纹理的范围为[0, 1],因此深度纹理中的z_depth值为
- 由于 OpenGL 中观察空间为右手坐标系,而裁剪空间下是左手坐标系,所以观察空间下的
z_view值需要取反,才为左手坐标系下的线性z_eye值,即
- 在
com.unity.render-pipelines.core/ShaderLibrary/ShaderVariablesFunctions.hlsl中,获取线性深度的方法为
// ShaderVariablesFunctions.hlsl
...
float LinearDepthToEyeDepth(float rawDepth)
{
#if UNITY_REVERSED_Z
return _ProjectionParams.z - (_ProjectionParams.z - _ProjectionParams.y) * rawDepth;
#else
return _ProjectionParams.y + (_ProjectionParams.z - _ProjectionParams.y) * rawDepth;
#endif
}
...
- 其中,用于在 shader 中计算深度的
_ProjectionParams参数值为
透视投影
- 透视投影下的裁剪空间深度
z_NDC
- 在像素着色器中,每一个像素的坐标都需要通过插值计算得到。而在透视投影下,需要使用透视校正插值。如果深度纹理使用观察空间下的
z_view,则在插值过程顶点坐标需要除以z_view,因此深度纹理使用z_NDC可以减少计算复杂度。 - 齐次除法后的
z_NDC值已经不为观察空间下z_view值的线性变换,因此使用深度纹理的深度信息时,需要变换观察空间下才能进行其他计算,即
则 z_eye为
- 当相机位置为 0 ,远裁剪平面为 1 时,即在左手坐标系下整体缩小
f,则深度值z_linear01为
- 当近裁剪平面为 0 ,原裁剪平面为 1 时,即在左手坐标系下先移动
-n,再整体缩小f,则深度值z_linear01FromNear为
- 在
com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl中,获取线性深度的方法为
// Common.hlsl
...
// 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 (UNITY_REVERSED_Z) = { f/n - 1, 1, (1/n - 1/f), 1/f }
// zBufferParam = { 1 - f/n, f/n, (1/f - 1/n), 1/n }
float Linear01DepthFromNear(float depth, float4 zBufferParam)
{
#if UNITY_REVERSED_Z
return (1.0 - depth) / (zBufferParam.x * depth + zBufferParam.y);
#else
return depth / (zBufferParam.x * depth + zBufferParam.y);
#endif
}
// 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 (UNITY_REVERSED_Z) = { f/n - 1, 1, (1/n - 1/f), 1/f }
// zBufferParam = { 1 - f/n, f/n, (1/f - 1/n), 1/n }
float Linear01Depth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}
// Z buffer to linear view space (eye) depth.
// Does NOT correctly handle oblique view frustums.
// Does NOT work with orthographic projection.
// zBufferParam (UNITY_REVERSED_Z) = { f/n - 1, 1, (1/n - 1/f), 1/f }
// zBufferParam = { 1 - f/n, f/n, (1/f - 1/n), 1/n }
float LinearEyeDepth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}
...
- 其中,用于在 shader 中计算深度的
_ZBufferParams参数值为
Reversed-Z 优化
- 正交投影下,
z_NDC和z_view是线性关系,而透视投影下,由于z_NDC是按1/z_view关系分布的,因此,z_NDC区间的绝大部分值,都映射到接近近裁剪平面附近,精度较高,而靠近远裁剪平面部分,z_NDC的变化很小,精度较低,即在较大区间范围内的所有z_view都对应同一个z_NDC值,因此容易出现 z-fighting 问题。而在近裁剪平面附近,通常也不需要这么高的精度。
引自 Depth Precision Visualized - 在 D3D 中,
z_NDC的范围为[0, 1],对应z_view的[near, far],而深度值通常使用 4 bit 来存储浮点数。根据浮点数的量化规则,当浮点数越接近 0 时,同样位数的存储能有更大的精度,即能表示更小的浮点数,但这些值对应的z_view变化范围非常小。
引自 Depth Precision Visualized - 如果将深度值按
1 - z_NDC存储,即远裁剪平面对应z_NDC为 0,利用浮点数的特点,可以在靠近远裁剪平面获得更大的精度,一定程度上抵消非线性深度的问题,这种方式称为Reversed-Z。
引自 Depth Precision Visualized - 然而,在 OpenGL 中,由于
z_NDC的范围为[-1, 1],浮点数的高精度部分集中在中间区域,远裁剪平面无法通过使用Reversed-Z优化。尽管z_NDC在后续会将值映射到[0, 1]存入深度纹理,但由于初始映射到[-1, 1]的过程已经破坏了远裁剪平面部分的精度,所以Reversed-Z作用不大。
引自 Depth Precision Visualized
计算观察坐标
- 根据近裁剪平面上的顶点坐标信息,加上深度纹理的
uv坐标和深度信息,可以计算出目标点在观察空间下的坐标。 - 对于正交投影,目标点观察空间下的
x、y坐标,可由近裁剪平面的左上顶点的x、y坐标,加上uv偏移得到,z坐标则可由近裁剪平面的z坐标,加上当前和近裁剪平面的距离得到,即
- 对于透视投影,根据变换的关系,可以得到观察空间下目标点的坐标和其投影到近裁剪平面上的点的坐标关系
同样使用近裁剪平面的左上顶点和 uv偏移,则有
法线纹理
- 在 Built-In 管线中,法线没有单独的纹理,如果需要法线,则会将法线和深度一起存储到深度法线纹理中,存储时通过
EncodeDepthNormal方法,将深度编码到RG通道,将法线编码到BA通道,读取时再通过DecodeDepthNormal进行解码,得到深度和法线。其中,法线的编码方法EncodeViewNormalStereo为
// UnityCG.cginc
...
inline float2 EncodeViewNormalStereo( float3 n )
{
float kScale = 1.7777;
float2 enc;
enc = n.xy / (n.z+1);
enc /= kScale;
enc = enc*0.5+0.5;
return enc;
}
- 单位法线向量的点落在单位球面上,以
(0, 0, -1)为投影中心,z = 0为投影平面,将所有点投影到投影平面上,则有
x_0、y_0为投影平面上点的坐标,z_o为投影中心点的z坐标,即
- 在 URP 中,法线则是通过
DepthNormalOnlyPass,渲染所有对象的DepthNormals或DepthNormalsOnly标签的pass,结果写入到深度图_CameraNormalsTexture中。
总结
- 深度和法线纹理,随着版本的变更,也出现了一些新的调整,但其原理基本上是相同的。了解深度法线纹理的生成过程,对于复杂效果的实现有比较大的帮助。