投影矩阵、NDC 空间与 Reversed-Z

一、不同的平台 API,不同的策略

知乎上一篇很好的文章:反向Z(Reversed-Z)的深度缓冲原理 - 知乎

先只考虑投影后裁剪空间的深度坐标 z 范围,有:

  • Direct3D / Metal / 各类游戏主机:[0, 1] / [0, far]
  • OpenGL 类:[-1, 1] / [-near, far]

也就是说,OpenGL 平台的投影矩阵会和其它平台不一样,而几篇经典的文章(一个例子)都是推的 OpenGL 的投影矩阵:

\left(\begin{array}{cccc} \frac{2 n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2 n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2 f n}{f-n} \\ 0 & 0 & -1 & 0 \end{array}\right)

而对应 D3D 等其它平台的投影矩阵为

\left(\begin{array}{cccc} \frac{2 n}{r-l} & 0 & \frac{-r-l}{r-l} & 0 \\ 0 & \frac{2 n}{t-b} & \frac{-t-b}{t-b} & 0 \\ 0 & 0 & \frac{f}{f-n} & \frac{-f n}{f-n} \\ 0 & 0 & 1 & 0 \end{array}\right)

当然投影矩阵还会跟参考坐标系有关系,例如 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 平台上会不起作用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值