透视投影
介绍
正交投影中,物体的大小与它们和摄像机的距离无关,而透视投影中,离你近的物体看起来会比离你远的物体更大。目前我们讨论的所有投影变换都基于一个前提:我们已经完成了视图变换。也就是说,摄像机沿着 z 轴向下拍摄,所有物体都位于中心。正交投影会将定义在视图空间(也称为摄像机空间或眼睛空间)中的三维体转换为归一化的三维体和裁剪空间。
投影变换则截然不同,这里我们处理的是一个梯形,就像一个巨大的金字塔,顶部被切掉了一样。这里是近红外(NIR)图像,这叫做视椎体,我们要做的就是把它投影到一个矩形体上。现在我们已经超出了线性变换的范畴,需要用到新的数学知识来进行这种变换。你可以想象一下,你看到外面有很多物体,你在一个黑暗的房间里看着外面的东西。你可以把这个窗户想象成近平面,物体将被投影到这个近平面上。
正交投影和透视投影中,选择近平面和远平面都会产生一些实际影响。首先,远平面之外的物体将不可见。其次,如果物体位于近平面和远平面之间,就会出现问题。比如,你正朝着某个物体移动,当远平面与该物体相交时,该物体会突然出现在视野中。如果你玩过黄金眼等游戏,你可能会见过类似的问题:如果你悄悄走到一个守卫身后,并且足够靠近,近平面会切掉守卫的后脑勺,你可以看到他们的后脑勺,脸部看起来有点奇怪。因为在远处的平面上,有些东西会突然出现在你身后,忽隐忽现。有时人们会添加雾效,来试图掩盖这种情况,但这仍然会发生。从实际角度来看,选择近平面和远平面在正交投影和透视投影中都很重要,因为视觉信息用于确定哪些物体比其他物体更近,特别是哪些物体遮挡了其他物体。如果近平面和远平面之间的距离设置得太远,可能没有足够的分辨率和 Z 轴来很好地解析,从而产生一种奇怪的闪烁效果,称为 Z 轴冲突。
因此,我们将使用这个特定的视椎体,这个被截断的金字塔,这个看起来像梯形的金字塔,我们将把这个本质上不是 3D 矩形体的东西映射到一个 3D 矩形体上。左下角和右上角的近坐标在这个我们将要投影 3D 空间的窗口中是有意义的。我不会消失,我只是写 Z 轴等于远平面,因为右上角和左下角的坐标是外的。这里,这个点的位置不对,顶部的位置也会有所不同,这取决于不同的角度。现在,我按照 Direct3D 的约定绘制了这个图形。如果这是 OpenGL,左下角的位置实际上应该是 (-1, -1, -1)。我将在推导过程中展示的矩阵是 Direct3D 风格的,但你可以稍微修改一下推导过程,得到 OpenGL 风格的矩阵。如果你出于某种原因需要的话,我们将空间中的物体投影到这个近平面上。在近平面上,我们将从左到右映射到 [-1, 1],从下到上映射到 [-1, 1],就像我们之前对正交投影所做的那样。但请记住,这只适用于在近平面上的情况。正交投影则适用于整个 3D 空间,但这次的情况截然不同。
假设我们有一个点,比如说是 XYZ,在我们的 3D 空间中,我们已经确定了我们要变换的是空间中的某个点,我们希望空间中的所有点都经过原点 。这里我们假设相机已经移动到了原点。我们将空间中的所有点沿着经过原点的直线移动,然后观察它们与近平面的交点。基本思路是,所有点都必须构成相似三角形,所有角度都必须匹配。例如,如果我计算近平面上某一点的 x 坐标(记为 )与原 x 坐标的比值,那么这个比值必须等于近平面上某一点的 z 坐标与原 z 坐标的比值。稍作整理,我就可以得到如下公式:
注意,这个变换本质上是非线性的,需要谨慎处理。我们要做的就是尽量保持大部分计算都可以在线性矩阵代数空间中进行,这样我们就可以将其与其他矩阵运算结合起来。但是,我们需要进行除法运算,而这无法通过简单的矩阵乘法来实现。
完成变换后,,。此时,我们可以使用上节课推导出的正交投影公式,将它们映射到 的裁剪空间中。
现在我们需要思考一下如何处理 。我们已经弄清楚了如何变换 x 和 y,但还需要处理 z,因为它包含一些深度信息。我们需要计算遮挡,所以为了保持一致性,我们将 z 映射到新的 乘以 z,并尝试找到一个合理的表达式。
假设近平面映射到 0,即 ,远平面映射到 。
如果我想使用 OpenGL 的约定,我可以将 设置为 -1,但这里我们使用的是 Direct3D 的约定。如果我想进行那种映射,我知道我可以在这里加上 n。所以如果我想让近平面映射到 0,我可以代入 n,得到第一个方程。现在,如果我切换颜色,我们来看看另一种情况。我想用 f 代替 z,我希望 的值是 1。如果我这样做,最终会得到这个表达式。
现在,如果我把前面三个表达式结合起来,我会得到这个。
我稍后会解释为什么这里有 乘以 z。所以我要把所有元素都放到一个矩阵中,把 x、y 和 z 分别映射到 、 和 。
定义视锥
通常情况下,视野范围是用弧度或角度来表示的。从关卡设计师的角度来看,通常是角度,他需要决定如何设置摄像机。视野范围是在这个垂直方向上定义的。然后,你需要定义一个宽高比来计算宽度。当然,我们还需要定义近平面和远平面。弄清楚视野如何映射到高度和宽度,如下所示:
3D API 中内置了创建这些矩阵的命令。Direct3D 和 Unity 默认都使用左手坐标系。
- In Direct3D:
D3DXMatrixPerspectiveFovLH(*o, a, r, n, f); - In Unity:
Matrix4x4.Perspective(a, r, n, f);
当然,某些 API 中也存在右手坐标系版本。这有点奇怪,因为这里底层的数学原理也适用于右手坐标系,你只需要将 f 和 n 的值从正数改为负数即可。但是,即使远平面、平面和右手坐标系都应该用负坐标来定义,所有情况仍然如此。API 默认你传入的是正数,所以你在这里看到的 n - f,它帮你做了取反运算,这是 Direct3D 内置的:
- In Direct3D:
D3DXMatrixPerspectiveFovRH(*o, a, r, n, f) - In XNA:
Matrix.CreatePerspectiveFieldOfView(a, r, n, f) - In OpenGL:
gluPerspective(a, r, n, f)
在 Unity 中自定义投影
你可以创建自己的投影矩阵,Unity 可以使用我们讨论过的那些例程,或者你可以编写一些代码来生成一些非常特殊的自定义矩阵。你的摄像机有一个瞬态变量投影矩阵,你可以将它赋值给它,但你只有在做一些比较特殊的事情时才需要这样做。文档中提到了斜投影,这是一种我们之前没有讨论过的非标准投影,它显然被水体渲染系统使用。同样,你只有在需要处理一些特殊情况时才需要这样做。如果你要做一些特别奇怪的操作,Unity 会获取摄像机的各种参数,并根据你在 Inspector 面板中的设置,按需为你创建这些矩阵。你也可以通过脚本来修改这些设置(如果有需要的话)。
视口变换
- The actual 2D projection to the viewer
- Copy to your back buffer(frame buffer)
- Can be programmed, scaled,...
最后还有一个转换,就是将一对一的像素值映射到屏幕上的实际像素数(整数)。如果你习惯于使用整数来显式索引特定像素,那么在使用现代硬件进行 3D 图形编程时,需要一段时间才能适应这种转换。当然,这种情况最终还是会发生。你的操作系统和 API 中可能还有其他东西,它们会根据像素定义屏幕上各个窗口的位置,然后将这些一对一的像素值映射到不同的位置。正如我们稍后将看到的,情况可能会更加复杂,因为你可能正在进行抗锯齿处理,实际上渲染到一个像素密度比实际显示更高的缓冲区中。如今,很多这类处理都在后台完成。场景部分我们先交给 API 来完成。Unity 定义了几种坐标约定,这些约定是 Unity 特有的。如果你使用其他引擎,可能会有不同的命名规则。它定义了一个视口系统,左下角是 ,右上角是 。视口系统没有 z 坐标的概念。
此外,还有屏幕空间坐标,从左下角 开始,逐渐过渡到更接近整数像素的数值,例如像素宽度和像素高度。在这个设置中,还有一个 z 坐标,这是来自摄像机的"世界单位坐标"。
请注意,它们的单位不同。Unity 内置图形用户界面(GUI)的坐标是 ,现在左上角是摄像机的像素,右下角是摄像机的像素坐标。
前两种坐标更接近我们到目前为止看到的二维坐标映射约定,其中 y 坐标增加意味着你在屏幕上向上移动,而 GUI 坐标系的基准是向下,这更接近传统的 2D 图形编程,我们增加的是 y 坐标,也就是在屏幕上向下移动。