一、不同的平台 API,不同的策略
知乎上一篇很好的文章:反向Z(Reversed-Z)的深度缓冲原理 - 知乎
先只考虑投影后裁剪空间的深度坐标 z 范围,有:
- Direct3D / Metal / 各类游戏主机:[0, 1] / [0, far]
- OpenGL 类:[-1, 1] / [-near, far]
也就是说,OpenGL 平台的投影矩阵会和其它平台不一样,而几篇经典的文章(一个例子)都是推的 OpenGL 的投影矩阵:
而对应 D3D 等其它平台的投影矩阵为
当然投影矩阵还会跟参考坐标系有关系,例如 OpenGL 使用的是右手坐标系,但是在NDC中使用的是左手坐标系,因此构造投影矩阵时,需要将 near 和 far 取负
1.1 反向 Z(Reversed-Z)
而如今的游戏中,往往会使用近处 1.0,远处 0.0 的深度分布,Unity 引擎中,某些 Graphics API 上会默认开启这样的改动,这一块可以直接参考官方手册:
- DirectX 11 /?DirectX 12 /?Metal: Reversed direction:[1, 0] / [near, 0]
- OpenGL:[-1, 1] / [-near, far](这个范围使用了反向 Z 也没有任何作用;但是可以通过4.5版本的 API 修改剪裁范围为 [0.0, 1.0],从而同样可以使用反向 Z)
- 其它不使用反向 z 的?Direct3D 类平台:[0, 1] / [0, far]
使用反向 z 的好处是可以均匀深度值?z 的精度,使得无论接近近裁平面还是远裁平面,精度都能得到一定的保证:这主要是从这两点考量的:
- 本身投影矩阵计算完后,NDC 空间下深度 z 与实际深度就不是线性的关系,离近裁平面越近,z 的分布密度越小,精度越高
- 还有一点很容易被忽视,那就是浮点数原理:对于越接近 0 的浮点数,精度越高
而反向 Z 就可以完美的使这两个性质做到互补,而非要不都满足精度高的条件,要不都不满足以带来更大的精度不平衡
1.2 回到游戏开发,你应该注意到的
Unity shader 开发时,在使用深度发生反转的平台上的着色器满足条件:
- 定义了 UNITY_REVERSED_Z。
- _CameraDepth 纹理的纹理范围是 [1, 0]
- 裁剪空间范围是?[1, 0] / [near, 0]
因此如果要手动提取深度缓冲区值,则可能需要检查缓冲区方向
float z = tex2D(_CameraDepthTexture, uv);
# if defined(UNITY_REVERSED_Z)
z = 1.0f - z;
# endif
如果你想自己计算投影矩阵,就要非常小心了,如果处于反向 Z 的平台上,则?GL.GetGPUProjectionMatrix()?输入当前矩阵可以给你提供一个还原反向 Z 的矩阵,但如果要手动从投影矩阵中进行合成(例如,对于自定义阴影或深度渲染),则需要通过脚本按需自行还原深度 方向
当然还会有一些情况:比如我的深度需要用于参与计算,而非用于深度检测,这时就需要你指定一个正确的投影矩阵,并且要求是一个平台无关的投影矩阵,就像下面这个例子一样:
// 无视平台的转换投影矩阵到GL格式
private static Matrix4x4 GetGPUProjMatrix(Matrix4x4 p)
{
p[2, 0] = p[2, 0] * (-0.5f) + p[3, 0] * 0.5f;
p[2, 1] = p[2, 1] * (-0.5f) + p[3, 1] * 0.5f;
p[2, 2] = p[2, 2] * (-0.5f) + p[3, 2] * 0.5f;
p[2, 3] = p[2, 3] * (-0.5f) + p[3, 3] * 0.5f;
return p;
}
public static void GetShadowMatrix(Vector2 size, Vector2 worldHeight, Vector2 worldOffset, Vector2 lightDirection, out Matrix4x4 m, out Matrix4x4 p)
{
m = Matrix4x4.TRS(new Vector3(-size.x * 0.5f + worldOffset.x, -size.y * 0.5f + worldOffset.y, 0.0f), Quaternion.Euler(-90, 0, 0), Vector3.one);
p = Matrix4x4.Ortho(-size.x * 0.5f, size.x * 0.5f, -size.y * 0.5f, size.y * 0.5f, worldHeight.x, worldHeight.y);
float z = Mathf.Sqrt(1 - lightDirection.x * lightDirection.x - lightDirection.y * lightDirection.y);
p[0, 2] = -(lightDirection.x/z) / size.x * 2.0f;
p[1, 2] = -(lightDirection.y/z) / size.y * 2.0f;
// ESM 的阴影是在DX11下烘焙的 这里将ESM_Matrix转换成了GL的矩阵格式
// GLES3.0 的绘制模式下没有做翻转,但是实际需要使用翻转后的矩阵
// GL.GetGPUProjectionMatrix 接口只会转DX到GL 遇到GL时直接不做处理
// 为了平台数据一致 直接统一转换
// p = GL.GetGPUProjectionMatrix(p, false);
p = GetGPUProjMatrix(p);
}
由于 Matrix4x4.Ortho?函数返回的投影矩阵默认裁剪后 z 范围为 [-1, 1],并且不会考虑反向 Z,因此如果你想使最后深度在 [0, 1] 的范围内,就需要自行进行转换,由于深度计算和平台无关,因此不能直接调用?GL.GetGPUProjectionMatrix()?,它在 OpenGL 平台上会不起作用
|