推导投影矩阵

推导投影矩阵

在任何 3D 图形程序员工具包中的基本矩阵变换中,投影矩阵都比较复杂。平移和缩放一目了然,任何对三角学有基本了解的人都可以想象出旋转矩阵,但投影有点棘手。如果您曾经查找过此类矩阵的公式,就会知道常识不足以告诉您它的来源。然而,我还没有在网上看到很多资源来描述如何推导投影矩阵。这就是我将在本文中讨论的主题。

对于那些刚开始使用 3D 图形的人,我应该提一下,了解投影矩阵的来源可能是我们中间有数学倾向的人的好奇心,但这不是必需的。你可以只用公式;如果您正在使用像 Direct3D 这样的图形 API 来为您构建投影矩阵,那么您甚至不需要它。所以,如果这篇文章的细节看起来有点让人不知所措,不要害怕。只要您了解投影的作用,如果您不想,您就不必担心它是如何工作的。这篇文章是为那些想要了解更多细节而不是绝对必要的程序员而写的。

概述:什么是投影?

计算机显示器是一个二维表面,因此如果要显示三维图像,则需要一种将 3D 几何图形转换为可以呈现为 2D 图像的形式的方法。而这正是投影所做的。举一个非常简单的例子,将 3D 对象投影到 2D 表面的一种方法是简单地丢弃每个点的 z 坐标。对于多维数据集,它可能类似于图 1。

推导投影矩阵_d3

图 1:通过丢弃 z 坐标投影到 xy 平面上。

当然,这过于简单,在大多数情况下并不是特别有用。首先,你根本不会投射到飞机上;相反,您的投影公式会将您的几何图形转换为一个新的体积,称为规范视图体积。规范视图体积的确切坐标可能因图形 API 的不同而有所不同,但出于本次讨论的目的,将其视为从 (–1, –1, 0) 扩展到 (1, 1, 1) 的框),这是 Direct3D 使用的约定。一旦所有顶点都映射到规范视图体积中,只有它们的 x 和 y 坐标用于将它们映射到屏幕。然而,z 坐标并非无用;它通常由深度缓冲区用于可见性确定。这就是您转换为新体积而不是投影到平面上的原因。

请注意,图 1 还描绘了一个左手坐标系,其中相机向下看正 z 轴,y 轴指向上方,x 轴指向右侧。这也是 Direct3D 使用的约定,我将在整篇文章中使用。对于右手坐标系或稍微不同的规范视图体积,计算没有显着不同,因此即使您选择的 API 使用的约定与 Direct3D 使用的约定不同,所讨论的所有内容仍然适用。

这样,您就可以进入实际的投影变换。有很多不同的投影方法,我将介绍两种最常见的:正交和透视。

正投影

正投影,因为所有的投影线都垂直于最终的绘图表面而得名,是一种相对简单的投影技术。视图体积(即包含您要显示的所有几何图形的眼空间区域)是一个轴对齐的框,您可以将其转换为规范的视图体积,如图 2 所示。

推导投影矩阵_投影矩阵_02

图 2:正交投影。

如您所见,视图体积由六个平面定义:

推导投影矩阵_透视投影_03

 因为视图体积和规范视图体积都是轴对齐的框,所以在这种类型的投影中没有距离校正。事实上,最终结果与图 1 中的结果非常相似,其中您只是删除了每个点的 z 坐标。3D 空间中相同大小的对象在投影中显示的大小相同,即使一个对象离相机的距离比另一个远得多。在 3D 空间中平行的线在最终图像中保持平行。对于第一人称射击游戏之类的游戏来说,使用这种投影是不可能的——想象一下,在无法判断距离有多远的情况下尝试玩其中一个游戏!——但它确实有它的用途。例如,您可以在基于图块的游戏中使用它,尤其是在相机以固定角度放置的游戏中。图 3 显示了一个简单的示例。

推导投影矩阵_3d_04

图 3:正射投影的简单示例。

因此,事不宜迟,开始弄清楚这是如何工作的。最简单的方法可能是分别考虑三个轴中的每一个,并计算如何将沿该轴的点从原始视图体积映射到规范视图体积。您从 x 坐标开始。视图体积内的一个点将在 [l, r] 范围内具有 x 坐标,并且您希望将其转换为 [–1, 1] 范围。

推导投影矩阵_投影矩阵_05

 现在,为了准备将范围缩小到您想要的大小,您从所有项中减去 l 以在左侧产生一个零。您可以在此处采用的另一种方法是转换范围,使其以零为中心,而不是将其一个端点设为零,但这种方式代数更简洁,因此我会这样做的可读性。

推导投影矩阵_正投影_06

 现在您的范围的一端位于零,您可以将其缩小到您想要的大小。您希望 x 值的范围为两个单位宽,从 1 到 –1,因此您乘以 2/(r – l)。请注意,r – l 是您的视图体积的宽度,因此始终为正数,因此您不必担心不等式会改变方向。

推导投影矩阵_d3_07

 接下来,从所有项中减去一个以产生所需的 [–1, 1] 范围。

推导投影矩阵_d3_08

 一些基本代数允许您将中心项写为单个分数:

推导投影矩阵_投影矩阵_09

 最后,您将中心项分成两个部分,使其采用 px + q 的形式;您需要以这种方式对您的术语进行分组,以便您推导出的方程可以轻松转换为矩阵形式。

推导投影矩阵_3d_10

 这个不等式的中心项现在为您提供了将 x 转换为规范视图体积所需的等式。

推导投影矩阵_3d_11

 获得 y 的公式所需的步骤完全相同——只是用 y 代替 x,用 t 代替 r,用 b 代替 l——所以在这里不再重复它们,我只展示结果:

推导投影矩阵_透视投影_12

 最后,您需要推导出 z 的公式。在这种情况下有点不同,因为您将 z 映射到范围 [0, 1] 而不是 [–1, 1],但这应该看起来很熟悉。这是您的起始条件,范围 [n, f] 上的 z 坐标:

推导投影矩阵_d3_13

 您从所有项中减去 n,因此范围的下端位于零处:

推导投影矩阵_投影矩阵_14

现在,剩下的就是除以 f – n 以产生最终的范围 [0, 1]。和以前一样,请注意 f – n 表示您的观看量的深度,因此永远不会是负数。

推导投影矩阵_正投影_15

最后,你把它分成两部分,所以它采用 pz + q 的形式:

推导投影矩阵_正投影_16

 这为您提供了转换 z 的公式:

推导投影矩阵_正投影_17

 现在,您已准备好编写正交投影矩阵。回顾您迄今为止的工作,以下是您推导出的三个投影方程:

推导投影矩阵_d3_18

 如果你以矩阵形式写这个,你会得到:

推导投影矩阵_正投影_19

 而已!Direct3D 提供了一个名为 D3DXMatrixOrthoOffCenterLH() 的函数(多口啊!),它基于相同的公式构造一个正交投影矩阵;您可以在 DirectX 文档中找到它。那个笨拙的函数名称中的“LH”指的是您使用的是左手坐标系。但是,“OffCenter”究竟是什么意思?

该问题的答案将引导您获得正交投影矩阵的简化形式。考虑几点:首先,在眼睛空间中,您的相机位于原点并直接向下看 z 轴。其次,您通常希望您的视野向左和向右延伸同样远,并且在 z 轴上方同样远,如下所示。如果是这种情况,z 轴将直接通过您的视域中心,因此您有 r = –l 和 t = –b。换句话说,您可以完全忘记 r、l、t 和 b,而只需根据宽度 w 和高度 h 以及其他裁剪平面 f 和 n 来定义您的视图体积。如果你将这些替换到上面的正交投影矩阵中,你会得到这个相当简化的版本:

推导投影矩阵_透视投影_20

 该等式由 Direct3D 函数 D3DXMatrixOrthoLH() 实现。你几乎总是可以使用这个矩阵而不是你上面派生的更一般的“偏离中心”版本,除非你对你的投影做了一些奇怪的事情。

在完成本节之前,还有一点。值得注意的是,这个矩阵可以表示为两个更简单变换的串联:一个平移,后跟一个比例。如果您从几何角度考虑它,这对您来说应该是有意义的,因为您在正交投影中所做的就是将点从一个轴对齐的框移到另一个;观看体积不会改变它的形状,只会改变它的位置和大小。具体来说,你有:

推导投影矩阵_正投影_21

 这种投影的产品形式可能更直观一些,因为它可以让您更轻松地想象正在发生的事情。首先,观察体沿 z 轴平移,使其近平面与原点重合;然后,应用一个比例将其降低到规范视图体积的尺寸。这很容易理解,对吧?偏心正交投影的矩阵也可以表示为变换和比例的乘积,但它与上面显示的结果非常相似,我不会在这里列出。

透视投影

透视投影是一种稍微复杂的投影方法,使用频率更高,因为它会产生距离的错觉,从而产生更逼真的图像。从几何上讲,这种方法与正射投影的区别在于,在透视投影中,视体积是一个截锥体——即一个截棱锥——而不是一个轴对齐的盒子。您可以在图 4 中看到这一点。

推导投影矩阵_透视投影_22

图 4:透视投影。

如您所见,视锥体的近平面从 (l, b, n) 延伸到 (r, t, n)。远平面的范围是通过从原点通过近平面上的四个点中的每一个跟踪一条线直到它们与平面 z = f 相交来找到的。因为视锥体从原点延伸得越远,它就越宽;并且因为您正在将该形状转换为规范的视图体积,即一个盒子;视锥体的远端比近端被压缩的程度更大。因此,视锥体中更靠后的物体看起来更小,这会给您带来距离的错觉。

因为在这种变换中体积的形状发生了变化,透视投影不能像正投影那样简单地表示为平移和缩放。你必须想出一些不同的东西。但是,这并不意味着您在正投影上所做的工作毫无用处。数学中一种方便的问题解决技术是将问题简化为您已经知道如何解决的问题。所以,这就是你可以在这里做的。上一次,您一次检查一个坐标,但这次您将一起检查 x 和 y 坐标,然后再考虑 z。您对 x 和 y 的攻击计划可以分为两个步骤:

步骤 1:给定视锥体内的一个点 (x, y, z),将其投影到近平面 z = n。因为投影点在近平面上,它的 x 坐标将在 [l, r] 范围内,它的 y 坐标将在 [b, t] 范围内。

第 2 步:使用您在正投影研究中推导出的公式,将新的 x 坐标从 [l, r] 映射到 [–1, 1],将新的 y 坐标从 [b, t] 映射到 [– 1, 1]。

听起来不错?然后,请看图 5。

推导投影矩阵_正投影_23

图 5:使用相似三角形将点投影到 z = n 上。

在此图中,您从点 (x, y, z) 到原点绘制了一条线,并记下该线与平面 z = n 相交的点 — 用黑色标记的那个点。从这些点开始,您将两个垂直于 z 轴的垂线放下,突然之间您就有了一对相似的三角形。如果您压抑了对高中几何的记忆,那么相似三角形就是形状相同但大小不一定相同的三角形。要证明两个三角形相似,证明它们对应的角相等就足够了,在这种情况下不难做到。角 1 由两个三角形共享,显然它等于自身。角2和角3是两条平行线相交所成的对应角,所以它们相等。而且,直角当然彼此相等,

您感兴趣的相似三角形的性质是它们的对应边对都以相同的比例存在。您知道沿 z 轴的边长;它们是 n 和 z。这意味着其他对边也以 n / z 的比率存在。所以,考虑一下你所知道的。根据勾股定理,从 (x, y, z) 向下到 z 轴的垂线长度如下:

推导投影矩阵_透视投影_24

 如果您知道从投影点到 z 轴的垂线长度,您就可以计算出该点的 x 和 y 坐标。但是,这很容易!因为你有相似的三角形,所以长度只是 L 乘以 n / z:

推导投影矩阵_透视投影_25

 因此,您的新 x 坐标是 x * n / z,而您的新 y 坐标是 y * n / z。步骤 1 就这样结束了。步骤 2 只是要求您执行与上一节中所做的相同的映射,因此是时候重新审视您在正投影研究中推导出的公式了。回想一下,您将我们的 x 和 y 坐标映射到规范视图体积中,如下所示:

推导投影矩阵_正投影_26

 您现在可以再次调用这些相同的公式,除非您需要考虑您的投影;所以,你用 x * n / z 替换 x,用 y * n / z 替换 y:

推导投影矩阵_投影矩阵_27

 现在,你乘以 z:

推导投影矩阵_正投影_28

 这些结果有点奇怪。要将这些方程直接写入矩阵,您需要将它们写成以下形式:

推导投影矩阵_d3_29

 但很明显,现在不会发生这种情况,因此您似乎陷入了僵局。该怎么办?好吧,如果您能找到一种方法来获得 z'z 的公式,就像 x'z 和 y'z 的公式一样,您可以编写一个矩阵变换,将 (x, y, z) 映射到 (x'z, y 'z,z'z)。然后,您只需将该点的分量除以 z,最终得到 (x', y', z'),这正是您想要的。

因为您知道 z 到 z' 的转换不以任何方式依赖于 x 或 y,所以您知道您需要 z'z = pz + q 形式的公式,其中 p 和 q 是常数。而且,您可以很容易地找到这些常量,因为您知道如何在两种特殊情况下获得 z': 因为您将 [n, f] 映射到 [0, 1],所以您知道 z' = 0 当 z = n , 当 z = f 时 z' = 1。当您将第一组值插入 z'z = pz + q 时,您可以根据 p 求解 q:

推导投影矩阵_投影矩阵_30

 现在,您插入第二组值,并得到:

推导投影矩阵_3d_31

 将 q 的值代入该方程,您可以轻松求解 p:

推导投影矩阵_d3_32

 现在您有了 p 的值,并且您之前发现 q = –pn,您可以求解 q:

推导投影矩阵_正投影_33

 最后,如果将 p 和 q 的这些表达式代入原始公式,您将得到:

推导投影矩阵_d3_34

 您现在几乎完成了,但是您解决这个问题的方法的不寻常性质要求您对齐次坐标 w 做一些事情。通常,您只是满足于设置 w' = 1——您可能已经注意到基本变换中的底行几乎总是 [0 0 0 1]——但现在您正在编写一个变换到点 (x 'z, y'z, z'z, w'z),所以不是写 w' = 1,而是写 w'z = z。因此,您将用于透视投影的最后一组方程是:

推导投影矩阵_投影矩阵_35

 而且,当你以矩阵形式写出这组方程时,你会得到:

推导投影矩阵_3d_36

 当您将其应用于点 (x, y, z, 1) 时,它会产生 (x'z, y'z, z'z, z)。但是随后,您应用了除以齐次坐标的通常步骤,因此最终得到 (x', y', z', 1)。这就是透视投影。Direct3D 在函数 D3DXMatrixPerspectiveOffCenterLH() 中实现了上述公式。与正交投影一样,如果您假设视锥体是对称的并以 z 轴为中心(意味着 r = –l 和 t = –b),您可以通过将矩阵写成视锥体的矩阵来大大简化事情宽度 w 和它的高度 h:

推导投影矩阵_正投影_37

 Direct3D 也有这个矩阵的函数,称为 D3DXMatrixPerspectiveLH()。

最后,还有一种经常派上用场的透视投影表示法。在这种形式中,您不必严格考虑视锥体的尺寸,而是根据相机的视野来定义它。有关此概念的说明,请参见图 6。

推导投影矩阵_d3_38

图 6:根据垂直视场角 a 定义的视锥体高度。

垂直视场角为a。这个角度被 z 轴一分为二,所以通过一些基本的三角学,你可以写出以下方程,将 a 与近平面 n 和屏幕高度 h 相关联:

推导投影矩阵_3d_39

 此表达式允许您替换投影矩阵中的高度。此外,您将宽度替换为纵横比 r,定义为显示区域的宽度与其高度的比率。所以你有了:

推导投影矩阵_投影矩阵_40

 因此,根据垂直视场角 a 和纵横比 r,您有一个透视投影矩阵:

推导投影矩阵_透视投影_41

 在 Direct3D 中,您可以通过调用 D3DXMatrixPerspectiveFovLH() 来获得这种形式的矩阵。这种形式特别有用,因为您可以将 r 设置为您正在渲染的窗口的纵横比,而 p / 4 的视角通常就可以了。因此,您真正需要担心定义的唯一事情是视锥体沿 z 轴的范围。

概括

这就是您需要了解的有关投影变换背后的数学知识的全部内容。还有一些其他较少使用的投影方法,当然如果您使用右手坐标系或不同的规范视图体积,情况会略有不同,但是您应该能够通过使用结果以本文为基础。如果您想了解有关投影和其他变换的更多信息,请查看Tomas Moller 和 Eric Haines 的Real-Time Rendering;或Computer Graphics:Principles and Practice by James D. Foley、Andries van Dam、Steven K. Feiner 和 John F. Hughes;这是我在撰写本文时参考的两本关于计算机图形学的优秀书籍。

封面:

推导投影矩阵_正投影_42