• 投影矩阵、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]

    因此如果要手动提取深度缓冲区值,则可能需要检查缓冲区方向

    1. float z = tex2D(_CameraDepthTexture, uv);
    2. # if defined(UNITY_REVERSED_Z)
    3. z = 1.0f - z;
    4. # endif

    如果你想自己计算投影矩阵,就要非常小心了,如果处于反向 Z 的平台上,则 GL.GetGPUProjectionMatrix() 输入当前矩阵可以给你提供一个还原反向 Z 的矩阵,但如果要手动从投影矩阵中进行合成(例如,对于自定义阴影或深度渲染),则需要通过脚本按需自行还原深度 方向

    当然还会有一些情况:比如我的深度需要用于参与计算,而非用于深度检测,这时就需要你指定一个正确的投影矩阵,并且要求是一个平台无关的投影矩阵,就像下面这个例子一样:

    1. // 无视平台的转换投影矩阵到GL格式
    2. private static Matrix4x4 GetGPUProjMatrix(Matrix4x4 p)
    3. {
    4. p[2, 0] = p[2, 0] * (-0.5f) + p[3, 0] * 0.5f;
    5. p[2, 1] = p[2, 1] * (-0.5f) + p[3, 1] * 0.5f;
    6. p[2, 2] = p[2, 2] * (-0.5f) + p[3, 2] * 0.5f;
    7. p[2, 3] = p[2, 3] * (-0.5f) + p[3, 3] * 0.5f;
    8. return p;
    9. }
    10. public static void GetShadowMatrix(Vector2 size, Vector2 worldHeight, Vector2 worldOffset, Vector2 lightDirection, out Matrix4x4 m, out Matrix4x4 p)
    11. {
    12. 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);
    13. p = Matrix4x4.Ortho(-size.x * 0.5f, size.x * 0.5f, -size.y * 0.5f, size.y * 0.5f, worldHeight.x, worldHeight.y);
    14. float z = Mathf.Sqrt(1 - lightDirection.x * lightDirection.x - lightDirection.y * lightDirection.y);
    15. p[0, 2] = -(lightDirection.x/z) / size.x * 2.0f;
    16. p[1, 2] = -(lightDirection.y/z) / size.y * 2.0f;
    17. // ESM 的阴影是在DX11下烘焙的 这里将ESM_Matrix转换成了GL的矩阵格式
    18. // GLES3.0 的绘制模式下没有做翻转,但是实际需要使用翻转后的矩阵
    19. // GL.GetGPUProjectionMatrix 接口只会转DX到GL 遇到GL时直接不做处理
    20. // 为了平台数据一致 直接统一转换
    21. // p = GL.GetGPUProjectionMatrix(p, false);
    22. p = GetGPUProjMatrix(p);
    23. }

    由于 Matrix4x4.Ortho 函数返回的投影矩阵默认裁剪后 z 范围为 [-1, 1],并且不会考虑反向 Z,因此如果你想使最后深度在 [0, 1] 的范围内,就需要自行进行转换,由于深度计算和平台无关,因此不能直接调用 GL.GetGPUProjectionMatrix() ,它在 OpenGL 平台上会不起作用

  • 相关阅读:
    Python_面向对象
    深度学习入门-与学习相关的技巧
    leetcode LCR 007 mid. 三数之和。对撞指针解法
    2022年最新运维常用脚本学习
    <Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
    java操作数据库的工具类简介说明
    【Lychee图床】本地电脑搭建私人图床,公网远程访问
    React学习笔记
    vue3/vue2的兄弟组件的传值
    中创人民云|党管数据是如何保证国家数据安全的
  • 原文地址:https://blog.csdn.net/Jaihk662/article/details/126876203