之前研究Unlit shader的时候就遇到一些Z值相关的问题,一笔带过了,比如ComputeFogFactor
中的UNITY_Z_0_FAR_FROM_CLIPSPACE
。今天就把URP Shader中出现的Z相关的问题做一个专题一起研究下。
深度缓冲的方向和UNITY_REVERSED_Z
先说这个关于z的宏,因为这是平台级别的,比较底层,其他深度相关的宏也会用它。在不同的平台,比如DX11/12,Metal,OpenGL等深度缓冲的方向
是不一样的,分为两种:
DX11/DX12, Metal, Vulkan 等新一代图形API以及一些较新的主机平台如NS使用翻转的方向
- 深度缓冲中,1.0表示的是近裁剪面的深度值,0.0表示的是远裁剪面的深度值。
- clip space的Z值范围为
[near,0]
,即从near plane的深度值递减到far plane的0.0
其他平台,如DX9, OpenGL,OpenGL ES 2.0/3.0使用传统的方向
- 深度缓冲中,0.0表示的是近裁剪面的深度值,1.0表示的是远裁剪面的深度值。
- clip space的Z值范围根据不同平台有两类
- D3D-Like的平台,clip space的Z值范围为
[0,far]
,即从near plane的0.0递增到far plane的深度值 - OpenGL-Like的平台,clip space的Z值范围为
[-near, far]
, near plane的深度值为负值,递增到far plane。
- D3D-Like的平台,clip space的Z值范围为
使用翻转深度值方向的优势
如果深度缓冲是浮点缓冲,此时使用翻转的深度值方向,可以显著的提升深度缓冲的精度。这可以减轻z值的冲突以及提高阴影质量,特别是当使用很小的near plane和很大的far plane时。
UNITY_REVERSED_Z
的定义
当平台使用翻转深度值方向时,Unity就会定义UNITY_REVERSED_Z
为1:
在这些平台上,由于深度缓冲是翻转的,那么_CameraDepthTexture
的范围也是1(near)到0(far)。同时clip space的z值范围是[near,0]
。在shader中就要使用UNITY_REVERSED_Z
区分出这种情况做相应的处理。例如,当我们要从_CameraDepthTexture
中采样深度值时,需要这么写:
float z = tex2D(_CameraDepthTexture, uv);
#if defined(UNITY_REVERSED_Z)
z = 1.0f - z;
#endif
而如果我们要手动操作clip space Z值时,也要多加注意了,比如下面这个情况。
UNITY_Z_0_FAR_FROM_CLIPSPACE
这个宏是URP封装好了,方便我们手动处理clip space的z值的,传入的参数是clip space的z,返回的也是clip space的z,但是根据不同的情况做了处理,保证返回的z值范围是0到far。我搜索了一下,这个宏目前只有ComputeFogFactor用了:
real ComputeFogFactor(float z)
{
float clipZ_01 = UNITY_Z_0_FAR_FROM_CLIPSPACE(z);
}
看一下定义:
#if UNITY_REVERSED_Z
#if SHADER_API_OPENGL || SHADER_API_GLES || SHADER_API_GLES3
//GL with reversed z => z clip range is [near, -far] -> should remap in theory but dont do it in practice to save some perf (range is close enough)
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(-(coord), 0)
#else
//D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far]
//max is required to protect ourselves from near plane not being correct/meaningfull in case of oblique matrices.
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
#endif
#elif UNITY_UV_STARTS_AT_TOP
//D3d without reversed z => z clip range is [0, far] -> nothing to do
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#else
//Opengl => z clip range is [-near, far] -> should remap in theory but dont do it in practice to save some perf (range is close enough)
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#endif
这里面还有个很重要的unity内置的uniform_ProjectionParams
:
// x = 1 or -1 (-1 if projection is flipped)
// y = near plane
// z = far plane
// w = 1/far plane
float4 _ProjectionParams;
这儿有四种情况:
- 平台定义了翻转Z,但API是OpenGL。GL原本的clip z范围是
[-near,far]
,平台翻转了就是[near,-far]
。这儿直接取了个负,其实是变成[-near,far]
了,然后使用max(0)来直接截断-near到0。这个其实是性能的折中,按道理应该映射到[0,far]
的。 - 平台定义了翻转Z,且API是D3D,这是主要需要处理的情况,核心计算就是这个:
(1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z
这个函数是将[near,0]
映射到[0,far]
。我换一种形式可能更好懂:
Z1 = far - Z*far/near
当z为near时,返回0;z为0时,返回far; z取中间的某个值,如 near/k 时,返回 far/k。
- 如果平台没定义翻转Z,但是定义了
UNITY_UV_STARTS_AT_TOP
,这个宏一般用来判断API是D3D。此时D3D没有使用翻转Z,那么它的clip z范围就是[0,far]
因此什么都不做。 - 如果平台没定义翻转Z,也不是D3D,那么就是OpenGL,由于OpenGL的clip z范围是
[-near, far]
,理论上应该重新映射,但实际上为了节约性能消耗什么都不做。这儿也没用max。
关于这儿的平台和API的区别,我只能猜测一下,比如NS这种主机,是定义了翻转Z的,但是它又能用OpenGL,是不是这时候就会出现第一种情况?
本篇小结
关于Z的话题果然一篇讲不完,这才只是研究了一下Reversed Z。下篇继续。