【Unity】投影矩阵和线性深度推导

网络上有很多投影矩阵的推导,也有很多声称是基于 Unity 的,但和我的实测都不一致(现在看来是因为这些文章并不全面),此外有一些 Unity 本身的函数我也搞不懂它的原理,遂最终选择自行研究,总算把这些问题解决了。

现在通过这篇文章,你就可以完全搞懂 Unity 的投影矩阵是啥样,又是怎么来的。以及 Unity 逆推线性深度的函数是如何实现的。不过该文章也不是完全面向小白的,至少你应该对这些矩阵本来就有大概的了解。

渲染中的矩阵变换

渲染过程中将模型顶点转换到显卡设备的 NDC(标准设备坐标系)中,共要进行以下几个矩阵变换:

  1. 物体到世界空间矩阵(物体矩阵)
  2. 世界到视图空间矩阵(视图矩阵)
  3. 视图到剪辑空间(采用齐次坐标的 NDC 空间)矩阵(投影矩阵)

物体到世界空间矩阵就是正常的 TRS(转移,旋转,缩放)矩阵,不是本文的研究对象,在 Unity 中主要是“视图矩阵”和“投影矩阵”有特殊的地方。

视图矩阵

视图矩阵本质就是不受缩放影响的相机的 TRS 矩阵的逆矩阵。除此之外,在 Unity 中该矩阵还有个特别的地方。

虽然 Unity 是左手坐标系引擎,但它的视图空间却是用的右手坐标系的(z 轴正负与左手坐标系相反),更官方的表述是 Unity 采用的是 opengl 风格的视图矩阵。故最终会对 z 轴进行反转,使相机正前方为-z(即会对矩阵中的 m33 (z 轴系数)取反)。

虽然这一操作让人感觉有些不适,但也便于了我们后续将深度计算为 D3D 风格的 1-0(越远深度值越小),而不是传统风格的 0-1(越远深度值越大)。

投影矩阵

投影矩阵用于将视图矩阵的结果转换到剪辑空间,但具体根据当前所使用的图形 API 不同,其投影矩阵和 NDC 都会有所差异:

https://docs.unity.cn/cn/2022.3/Manual/SL-PlatformDifferences.html

对于 NDC 的 x,y 轴,全平台都是一致的:

  • 屏幕从左到右为 x 轴的-1 到 1
  • 屏幕从下到上为 y 轴的-1 到 1

对于 NDC 的 z 轴,即视图空间下的近平面到远平面的 z 轴:

  • 在 OpenGL 平台:屏幕从前到后为 z 轴的-1 到 1
  • 在 Direct3D 平台:屏幕从前到后为 z 轴的 1 到 0

如果是从相机中直接获取投影矩阵(Camera.projectionMatrix),Unity 始终返回 OpenGL 风格。但若想获取着色器中实际使用的矩阵,则需要调用GL.GetGPUProjectionMatrix,而该矩阵会随图形 API 不同而不同。

综上所述,投影矩阵在 Unity 中有多种实现方式,但考虑 Unity 的深度图是采用 Direct3D 风格存储的(包括那些解算深度图的函数),而且 Windows 平台更常用,故在此仅推导 Direct3D 风格的透视矩阵。

投影矩阵的构成

投影矩阵有两种类型:

  • “正交投影”(不实现近大远小)
  • “透视投影”(实现近大远小)。

其中透视投影比较特殊,本质上是“正交”和“透视”两种变换的复合矩阵:

  1. 透视(上图绿框变蓝框):将锥形的视野范围缩放成长方体。
  2. 正交(上图蓝框变红框):将长方体的视野范围缩放到 NDC 空间(也是长方体)。

因此只需要学会透视投影,也就能学会正交投影,而且这样子理解起来会更简单。

正交变换(等价于正交投影矩阵)

正交投影矩阵由以下参数构成:

  • size:视锥体半高度。
  • aspect:宽高比(宽度/高度),用于得出半宽度。
  • near:近平面位置。
  • far:远平面位置。

由这些参数可以简单得出以下变量:

  • h:半高度(size)
  • w:半宽度(size*aspect)
  • n:近平面(near)
  • f:远平面(far)

正交投影矩阵是线性变换,所以可以直接通过直线公式(\(y=Ax+B\))来拟合(如下图),具体而言是要实现以下映射:

  1. \((-w,w)=>(-1,1)\)
  2. \((-h,h)=>(-1,1)\)
  3. \((-n,-f)=>(1,0)\)(受视图矩阵的 z 反转影响,故远近平面取反)

对于第一第二点,只要设置直线斜率(即对输入的 x,y 坐标直接除以 w,h 即可)。对于第三点则可以通过带入 z=-n 和 z=-f 两个线段端点成以下公式:

  • \(-An+B=1\)
  • \(-Af+B=0\)

进一步推导可得:

\(
\begin{aligned}
(-An+B)-(-Af+B) &= 1-0 \\
-An+B+Af-B &= 1 \\
Af-An &= 1 \\
A(f-n) &= 1 \\
A &= \frac{1}{f-n} \\
\end{aligned}
\)

\(
\begin{aligned}
-(\frac{1}{f-n})f+B&=0\\
B &= \frac{f}{f-n}\\
\end{aligned}
\)

最终根据上述结论,可用相关参数可构成正交投影矩阵:

\[\begin{bmatrix}
\frac{1}{w} & 0 & 0 & 0 \\
0 & \frac{1}{h} & 0 & 0 \\
0 & 0 & \frac{1}{f-n} & \frac{f}{f-n} \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
\]

透视变换(透视投影矩阵的一部分)

透视变换(后也称透视矩阵)的目的是实现近大远小,即根据 z 位置缩放 xy 轴,使任何位置的 x,y 都等于近平面的 x',y'(映射关系如下图)。

上图根据相似三角形定理可得对于 y 轴的透视变换如下公式:

\(
\begin{aligned}
\frac{y'}{n} &=\frac{y}{z} (n,z此处为长度,故不是负数)\\
y'&=\frac{yn}{z}\\
x'&=\frac{xn}{z}(x轴同理)
\end{aligned}
\)

现在要将上述公式反应在矩阵变换上:

  • 对于 n,这是一个定值,直接利用缩放矩阵的原理就可以实现。
  • 对于 z,这是一个变量,肯定无法直接写在矩阵中,但可以借助其次坐标 w 归一化的特性,将向量的 w (位置在 m43)设为 z 即可。

于是便可得出初步矩阵:

\(
\begin{bmatrix}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
? & ? & ? & ? \\
0 & 0 & -1 & 0
\end{bmatrix}
\)

注意因为视图矩阵中 z 被反转,此处为保证 xy 不受影响,因此需要将 m43 设置为 -1 来获取 +z。

此外 z 的系数都被标记为?,因为 z 也会受 w 归一的影响,而我们实际需要 z 保持不变,故需要对这些能对 z 产生作用的系数进行推导,以确保最终计算出的向量归一化前的 z 分量为\(-z^2\)(齐次坐标是实现除 z 而不是-z,所以为保持最终结果依然是视图空间的 -z ,z 分量应该是负数 z)。

由于前两个系数(m31,m32)是与 x,y 相乘,我们不需要所以始终为 0。而剩余的两个系数(m33,m34)设分别为 A,B 时,再加上视图空间向量(投影变换的输入向量)的 w 分量(B 的乘数)默认为 1,带入 z=-n 和 z=-f 两个特例后可得以下公式:

  • \(-An+B=-n^2\)
  • \(-Af+B=-f^2\)

推导可得:

\(
\begin{aligned}
(-An+B)-(-Af+B) &= (-n^2)-(-f^2) \\
-An+B+Af-B &= f^2-n^2 \\
Af-An &= (f-n)(f+n) \\
A(f-n) &= (f-n)(f+n) \\
A &= f+n \\
\end{aligned}
\)

\(
\begin{aligned}
-(f+n)f+B&=-f^2\\
B &= -f^2+(f+n)f\\
B &= -f^2+f^2+nf\\
B &= nf\\
\end{aligned}
\)

最终根据上述结论,可用相关参数可构成透视矩阵:

\[\begin{bmatrix}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
0 & 0 & f+n & nf \\
0 & 0 & -1 & 0 \\
\end{bmatrix}
\]

透视投影矩阵

将正交变换和透视变换的矩阵相结合可得如下矩阵:

\(
\begin{aligned}
&=\begin{bmatrix}
\frac{1}{w} & 0 & 0 & 0 \\
0 & \frac{1}{h} & 0 & 0 \\
0 & 0 & \frac{1}{f-n} & \frac{f}{f-n} \\
0 & 0 & 0 & 1 \\
\end{bmatrix}
*\begin{bmatrix}
n & 0 & 0 & 0 \\
0 & n & 0 & 0 \\
0 & 0 & f+n & nf \\
0 & 0 & -1 & 0 \\
\end{bmatrix}\\
&=\begin{bmatrix}
\frac{n}{w} & 0 & 0 & 0 \\
0 & \frac{n}{h} & 0 & 0 \\
0 & 0 & \frac{f+n}{f-n}-\frac{f}{f-n} & \frac{nf}{f-n} \\
0 & 0 & -1 & 0 \\
\end{bmatrix}\\
&=\begin{bmatrix}
\frac{n}{w} & 0 & 0 & 0 \\
0 & \frac{n}{h} & 0 & 0 \\
0 & 0 & \frac{n}{f-n} & \frac{nf}{f-n} \\
0 & 0 & -1 & 0 \\
\end{bmatrix}
\end{aligned}
\)

在透视投影中,Unity 不直接提供 h(半高),需要利用 fov(视野角度)计算。利用三角函数可以轻松得出:

\(
h = \tan(fov/2)*n\\
w = h * aspect
\)

重新整理后可得最终透视投影矩阵:

\[\begin{bmatrix}
\frac{1}{\tan(fov/2)*aspect} & 0 & 0 & 0 \\
0 & \frac{1}{\tan(fov/2)} & 0 & 0 \\
0 & 0 & \frac{near}{far-near} & \frac{near*far}{far-near} \\
0 & 0 & -1 & 0 \\
\end{bmatrix}
\]

线性深度推导

经过透视投影后得到的深度不是线性的(如上图),但很多特效实现都有利用 NDC 深度重建世界信息的需求,因此还需要研究一下如何逆推得到线性深度。

以下都是对 Unity 中相关线性深度求解函数的解析,利用下方链接可以查看每个函数的函数图,以便直观的感受深度变化效果:

https://www.geogebra.org/calculator/nxrfrkzj

LinearEyeDepth

将 NDC 中的深度反推为视图空间中的非反转深度(即原始的 z 轴坐标)。

该函数的实现可分成两个步骤,先执行 \(透视投影\) 的 \(逆函数\) 得出视图空间中的深度。由于视图空间中的深度为反转的 z 轴,故对该深度二次反转,以得到非反转深度。

即 \(LinearEyeDepth(z) = -逆透视投影(z)\)

  1. 根据之前的矩阵计算可得:

    \(
    \begin{aligned}
    透视投影
    &= \frac{(\frac{nz}{f-n}+\frac{nf}{f-n})}{-z}\\
    &=\frac{n(z+f)}{z(n-f)}\\
    \end{aligned}
    \)

  2. 再对该函数求逆:

    \(
    \begin{aligned}
    z'&=\frac{n(z+f)}{z(n-f)} \\
    z(n-f)z'&=nz+nf\\
    z(n-f)z'-nz&=nf\\
    z((n-f)z'-n)&=nf\\
    z&=\frac{nf}{(n-f)z'-n}\\
    \end{aligned}
    \)

    即:

    \(
    逆透视投影=\frac{nf}{(n-f)z-n}
    \)

  3. 加入反转,再简化:

    \(
    \begin{aligned}
    &= -\frac{nf}{(n-f)z-n}\\
    &= \frac{nf}{(f-n)z+n}\\
    &= \frac{1}{\frac{f-n}{nf}z+\frac{1}{f}}\\
    \end{aligned}
    \)

故最终结论为:

\[LinearEyeDepth(z)=\frac{1}{\frac{f-n}{nf}z+\frac{1}{f}}
\]

Linear01Depth

将 NDC 中的深度反推为线性 0-1 深度(相机位置为 0,远平面为 1)。

很容易想到,只需要对 \(LinearEyeDepth\) 的结果除以远平面大小即可,即:

\(
\begin{aligned}
&= \frac{LinearEyeDepth(z)}{f}\\
&= \frac{1}{\frac{f-n}{nf}z+\frac{1}{f}} * \frac{1}{f}\\
&= \frac{1}{\frac{f-n}{n}z+1}\\
\end{aligned}
\)

故最终结论为:

\[Linear01Depth(z)=\frac{1}{\frac{f-n}{n}z+1}
\]

Linear01DepthFromNear

求解线性 0-1 深度(近平面为 0,远平面为 1)。(Unity 中的注释是这样写的,但实测根本不是)。

该函数的本质为:

\(
\begin{aligned}
&=Linear01Depth(z)*z\\
&=\frac{1}{\frac{f-n}{n}z+1}*z\\
&=\frac{1}{\frac{f-n}{n}+\frac{1}{z}}\\
\end{aligned}
\)

其计算出的深度确实是线性,但近平面等于 \(Linear01Depth\)(z 等于 1,相乘后不变),远平面等于 0(z 等于 0,相乘后等于 0)。

若要实现真正的 \(Linear01DepthFromNear\) ,应对 \(逆透视投影函数\) 的结果直接进行 \(正交变换\),然后调换深度为 0-1 方向,即:

\(
\begin{aligned}
&= 1 - 正交变换(逆透视投影(z))\\
&= 1-\frac{1}{f-n}LinearEyeDepth(z)+\frac{f}{f-n}\\
&= 1-\frac{1}{f-n}(\frac{nf}{(n-f)z-n}+f)\\
\end{aligned}
\)

【Unity】投影矩阵和线性深度推导的更多相关文章

  1. (转)投影矩阵的推导(Deriving Projection Matrices)

    转自:http://blog.csdn.net/gggg_ggg/article/details/45969499 本文乃<投影矩阵的推导>译文,原文地址为: http://www.cod ...

  2. OpenGL中投影矩阵的推导

    本文主要是对红宝书(第八版)第五章中给出的透视投影矩阵和正交投影矩阵做一个简单推导.投影矩阵的目的是:原始点P(x,y,z)对应后投影点P'(x',y',z')满足x',y',z'∈[-1,1]. 一 ...

  3. 【脚下生根】之深度探索安卓OpenGL投影矩阵

    世界变化真快,前段时间windows开发技术热还在如火如荼,web技术就开始来势汹汹,正当web呈现欣欣向荣之际,安卓小机器人,咬过一口的苹果,winPhone开发平台又如闪电般划破了混沌的web世界 ...

  4. 介绍Unity中相机的投影矩阵与剪切图像、投影概念

    这篇作为上一篇的补充介绍,主要讲Unity里面的投影矩阵的问题: 上篇的链接写给VR手游开发小白的教程:(三)UnityVR插件CardboardSDKForUnity解析(二) 关于Unity中的C ...

  5. [OpenGL](翻译+补充)投影矩阵的推导

    1.简介 基本是翻译和补充 http://www.songho.ca/opengl/gl_projectionmatrix.html 计算机显示器是一个2D的平面,一个3D的场景要被OpenGL渲染必 ...

  6. OpenGL投影矩阵(Projection Matrix)构造方法

    (翻译,图片也来自原文) 一.概述 绝大部分计算机的显示器是二维的(a 2D surface).在OpenGL中一个3D场景需要被投影到屏幕上成为一个2D图像(image).这称为投影变换(参见这或这 ...

  7. OpenGL投影矩阵

    概述 透视投影 正交投影 概述 计算机显示器是一个2D平面.OpenGL渲染的3D场景必须以2D图像方式投影到计算机屏幕上.GL_PROJECTION矩阵用于该投影变换.首先,它将所有定点数据从观察坐 ...

  8. 关于Opengl投影矩阵

    读 http://www.songho.ca/opengl/gl_projectionmatrix.html 0.投影矩阵的功能: 将眼睛空间中的坐标点 [图A的视椎体]     映射到     一个 ...

  9. OpenGL投影矩阵【转】

    OpenGL投影矩阵 概述 透视投影 正交投影 概述 计算机显示器是一个2D平面.OpenGL渲染的3D场景必须以2D图像方式投影到计算机屏幕上.GL_PROJECTION矩阵用于该投影变换.首先,它 ...

  10. 投影矩阵、最小二乘法和SVD分解

    投影矩阵广泛地应用在数学相关学科的各种证明中,但是由于其概念比较抽象,所以比较难理解.这篇文章主要从最小二乘法的推导导出投影矩阵,并且应用SVD分解,写出常用的几种投影矩阵的形式. 问题的提出 已知有 ...

随机推荐

  1. HASHCTF2024

    第一届山东大学HASHCTF部分Misc题解 下面是我在本次比赛出的题目的WriteUp Secret of Keyboard 签到脚本题,有些同学的脚本解出来大小写不正确可能是由于脚本无法识别shi ...

  2. Flex 弹性布局备忘录

    概述 Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性 这也是我目前用的最多的一种布局方案,相比Grid布局此种布局方案相对较简单, ...

  3. 在腾讯云 EMR 上使用 GooseFS 加速大数据计算服务

    GooseFS 是腾讯云对象存储团队最新推出的高性能.高可用以及可弹性伸缩的分布式缓存系统,依靠对象存储(Cloud Object Storage,COS)作为数据湖存储底座的成本优势,为数据湖生态中 ...

  4. android emulator 设置代理

    android emulator 设置代理 由于开发的 app 需要访问 google 服务,那么跑虚拟机的时候就需要设置网络代理,试了几种方法都没成功,记录一下 因为已知我开发电脑的代理地址和端口, ...

  5. eShopOnContainer 中 unauthorized_client error 登录错误处理

    在准备好 eShopOnContainer 环境,运行起来之后,不幸的是,我遇到了不能登录的错误. 从错误信息中,可以看到 unauthorized_client 的内容.这是为什么呢? 从 eSho ...

  6. 我们需要什么样的 ORM 框架

    了解我的人都知道, 本人一直非常排斥 ORM 框架, 由于对象关系阻抗不匹配, 一直觉得它没有什么用, 操作数据库最好的手段是 sql+动态语言. 但这两年想法有了重大改变. 2013 年用 js 实 ...

  7. 管理员应了解的 SIEM解决方案七大功能 !

    ​SIEM解决方案已成为企业网络安全武器库中不可或缺的一部分.但由于SIEM功能过于复杂且架构难以理解,企业往往SIEM的潜在功能.遗憾的是,他们忽视的潜在功能正是解开企业网络合规的重要部分. 例如, ...

  8. Qt通用方法及类库5

    函数名 //设置标签颜色 static void setLabStyle(QLabel *lab, quint8 type, const QString &bgColor = "&q ...

  9. Qt开源作品37-网络中转服务器

    一.前言 用Qt做开发10年了,其中做过好多项目,基于现在web和移动互联网发展如此迅猛,大量的应用场景需要一个网络中转服务器,可以实现手机app或者其他客户端远程回控设备,现在物联网发展非常迅猛,这 ...

  10. 编译Ubuntu 24.04 LTS 内核(BuildYourOwnKernel)

    1.配置环境 修改apt源 修改 /etc/apt/sources.list.d/ubuntu.sources ,添加 "deb-src"到 Types:,修改后的文件内容如下: ...