(机翻,转载自:Article - World, View and Projection Transformation Matrices)
介绍
在本文中,我们将尝试详细了解任何3D引擎的核心机制,即矩阵变换链,它允许在2D监视器上表示3D对象。我们将尝试进入矩阵如何构建的细节以及为什么,所以这篇文章不是绝对的初学者。 我将假设向量数学和矩阵数学的一般知识。
我们先来谈谈转换和向量空间之间的关系。然后我们将展示如何以矩阵形式表示变换。从那里,我们将展示您将需要应用的典型的转换顺序,从 模型 到 世界空间,然后到 相机 ,然后是 投影。
矢量空间:模型空间和世界空间
向量空间是由给定数量的线性无关向量定义的数学结构,也称为基向量(例如图1中有三个基向量); 线性独立向量的数目定义了向量空间的大小,因此三维空间有三个基向量,而二维空间则有两个。这些基矢量可以缩放并添加到空间中以获得所有其他矢量。向量空间是一个相当广泛的话题,本文的目标不是详细解释它们,我们所需要知道的是,我们的模型存在于一个特定的向量空间中,这个向量空间被称为模型空间,它用典型的 3D坐标系统表示 (图1)。 当一个艺术家创作一个3D模型时,他创建了所有相对于他正在工作的工具的三维坐标系的顶点和面,这就是模型空间。所有的顶点都是相对于模型空间的原点,所以如果我们在模型空间的坐标(1,1,1)有一个点,我们就知道它在哪里(图2)。 游戏中的每一个模型都存在于它自己的模型空间中,如果你希望它们处于任何空间关系中(比如你想把一个茶壶放在桌子上),你需要把它们转换成一个共同空间称为 世界空间)。 |
图1:标准的右手三维坐标系 |
|
图2:茶壶的顶点(1,1,1) |
让我再次强调这一点。理解一个矢量只在坐标系中才有意义是很重要的。如果我们不指定空间,我们不能代表任何一点。一旦模型从工具导出到游戏引擎,所有的顶点都被表示在模型空间中。现在,如果我们想把我们刚刚导入的对象放在游戏世界中,我们需要移动它并且/或者把它旋转到所需的位置,这会把对象放到World Space中。 移动,旋转或缩放 对象就是我们所说的 转换。当所有的物体都变成一个共同的空间(世界空间)时,它们的顶点将与世界空间本身相关。
转型
我们可以看到矢量空间中的变换只是从一个空间到另一个空间的变化。这是向量转换中最棘手的部分之一,所以我们尽可能使其尽可能清晰。
我们可以将三维矢量空间想象成三个正交轴(如图1所示)。我们总是需要有一个“活跃的”空间,这个空间是我们用来作为参考的空间(几何或其他空间)。如果我们有两个模型,每个模型都有自己的模型空间,那么在我们定义一个共同的“活动”空间之前,我们不能绘制它们。
现在让我们说,我们从一个活跃的空间开始,称之为空间 A.,里面有一个茶壶。我们现在想要应用一个将空间A中的所有东西都转换到新位置的转换; 但如果我们移动空间A,则需要定义一个新的“活动”空间来表示变换后的空间A.让我们称之为新的活动空间空间B (图3)。在转换之前,空间A中描述的任何点都与该空间的起源有关(如图3左侧所示)。在我们应用了变换之后,所有的点现在都相对于新的活动空间,即空间B(图3右)。任何重新定义空间A相对于空间B的操作都是一种转换。请注意,在变换之后,空间A现在“迷失”到空间B中,或者更准确地说,它被重新映射到空间B中,所以我们没有办法对其应用任何其他变换(除非我们撤消变换并使空间再次是“活跃”空间)。
另一种看待这种情况的方式是,假设空间中的任何东西都随着基矢量而移动,并且想象空间A开始在空间B上完全重叠。当我们应用该变换时,我们将空间A从空间B移开,并且空间A随着它移动。一旦我们移动了所有的顶点,我们将它们全部表示为相对于空间B,并且我们已经完成了这个变换。
如果我们需要再次在空间A中操作,则可以将变换的逆应用于空间B.这样做,空间B将被重新映射到空间A(并且在这一点上,我们“丢失”空间B)。如果我们知道这两个变换和它们的倒数,我们总是可以将这两个空间重新映射到另一个空间。
图3:空间转换 |
我们可以在矢量空间中使用的转换是缩放,平移和旋转。重要的是要注意,每一个转换总是与原点有关,这使得我们所使用的转换本身非常重要。如果我们旋转90°左右,然后平移,我们会得到与我们首先平移然后旋转90°时所得到的东西截然不同的东西(图4,我省略了除了活动的空间之外的任何空间)。
图4:以不同顺序应用相同转换的不同结果 |
图4也可以帮助我们更多地理解转换的逆过程。因此,如果将图4左上角,可以将90°旋转到左侧的变换与其相反,这是向右旋转90°。注意转换的逆是一个转换本身,所以没有理由不把它应用到完全不相关的空间中的对象。左边90°变换的倒数是右边90度变换,显然可以应用于任何空间的任何事物。
转换矩阵
现在我们知道一个转换是从一个空间到另一个空间的转变,我们可以达到数学。如果我们想要表示从一个3D空间到另一个3D空间的转换,我们将需要一个4x4矩阵。我将从这里假设一个列向量符号,就像在OpenGL中一样。如果你是行向量,你只需要转置矩阵,并预乘我向后乘的向量。为了应用变换,我们必须将所有想要变换的向量乘以变换矩阵。如果向量在空间A中,并且转换描述了空间A相对于空间B的新位置,则在乘法之后,所有向量将在空间B中描述。
现在,让我们看看如何表示矩阵形式的一般转换:
凡Transform_XAxis是在新空间中的X轴方向, 变换_YAxis是在新空间中的Y轴方向, 变换_ZAxis是在新的空间,Z轴方向和翻译 中,说明了新的空间将是相对的活跃空间中的位置。
有时我们想做简单的转换,比如翻译或者旋转; 在这些情况下,我们可以使用以下矩阵,它们是我们刚才介绍的通用形式的特例。
翻译矩阵:
凡翻译是一个3D矢量,代表我们想要移动我们的空间的位置。一个平移矩阵使所有的轴都像活动空间一样旋转。
比例矩阵:
其中规模是沿每个轴代表一个尺度3D矢量。如果你阅读第一列,你可以看到新的X轴是如何面向相同的方向的,但是它是由标量scale.x来缩放的。所有其他轴也是如此。另请注意,翻译栏全是零,这意味着不需要翻译。
X轴周围的旋转矩阵:
其中theta是我们想要用于我们的旋转的角度。注意第一列如何不会改变,这是我们围绕X轴旋转所期望的。另请注意,如何将θ改为90°,将Y轴重新映射到Z轴,将Z轴重新映射到-Y轴。
Y轴周围的旋转矩阵:
Z轴周围的旋转矩阵:
Z轴和Y轴的旋转矩阵的行为与X轴矩阵相同。
我刚才介绍给你的矩阵是最常用的矩阵,它们都是你需要描述刚性变换的。您可以通过将矩阵依次相乘来将几个变换链接在一起。结果将是编码完整转换的单个矩阵。正如我们在转型部分看到的那样,我们使用转化的顺序是非常重要的。这是通过矩阵乘法不可交换的事实来反映在数学上的。因此一般来说Translate x Rotate与Rotate x Translate不同。
由于我们使用的是列向量,所以我们必须从右向左阅读一个变换链,所以如果我们想要围绕Y轴向左旋转90°,然后沿Z轴平移10个单位,则链将是[沿X转换10] x [RotateY 90°] = [ComposedTransformation]。
让我们把一些数字,以便我们可以看到它是如何工作的。假设我们想要变换图5中的球体。为了简单起见,我们将仅将变换应用于在模型空间中位于(0,1,0)的球体的顶部顶点。我们将计算它将在世界空间中的位置。首先我们定义变换矩阵。假设我们希望将球体放置在世界空间中,它将围绕Y轴顺时针旋转90°,然后围绕X轴旋转180°,然后转换为(1.5,1,1.5)。这意味着转换矩阵将是:
请注意结果矩阵如何完美地符合我们所提出的通用变换公式。在世界空间中,X轴现在定位为该空间的Z轴,因此现在是(0,0,1)。现在Y轴翻转,因此(0,-1,0)。现在Z轴定位为X轴(1,0,0)。最后,翻译矢量(1.5,1,1.5)。
一旦我们有了结果,我们可以乘以球体的任何顶点,从模型空间变成世界空间。让我们做我们的顶点(0,1,0)。请注意,由于我们使用4x4矩阵,所以我们需要使用齐次坐标,因此我们需要一个4维向量,在最后一个分量中有1个。
图5:球体从模型空间转换到世界空间 |
模型空间,世界空间,视图空间
现在我们把所有的难题都放在一起。当我们想要渲染3D场景的第一步是把所有的模型放在同一个空间世界空间。由于每一个对象在世界上都处于自己的位置和方位,所以每一个对象都有一个不同的模型到世界的转换矩阵。
图6:三个茶壶,每个都在自己的模型空间中 |
图7:世界空间中的三个茶壶 |
所有的对象在正确的位置,我们现在需要把它们投影到屏幕上。这通常分两步完成。第一步将所有对象移动到另一个称为视图空间的空间中。第二步使用投影矩阵执行实际的投影。最后一步与其他步骤有所不同,我们将在一会儿详细地看到它。
为什么我们需要一个视图空间?视图空间是一个辅助空间,我们用它来简化数学,保持优雅和编码成矩阵。这个想法是,我们需要渲染一个相机,这意味着投影所有的顶点到相机屏幕上,可以在空间任意取向。如果我们可以将摄像头放在原点的中心并观察三个轴中的一个,那么数学就简化了很多,比方说Z轴遵守惯例。那么为什么不创造一个这样做的空间,重新映射世界空间,以便相机在原点,沿着Z轴向下看?这个空间是视图空间(有时称为 相机空间),我们应用的转换将所有的顶点从世界空间移动到视图空间。
我们如何计算View Space的变换矩阵?现在,如果您想要将相机放在“世界空间”中,则可以使用位于相机所在的位置的变换矩阵,以便Z轴正在朝向相机目标。如果将这个转换应用到世界空间中的所有对象,逆转会将整个世界转移到视图空间。请注意,我们可以将两个转换“模型转换为世界”和“查看世界”转换为一个单一转换模型以查看。
图8:在世界空间的左边两个茶壶和一个照相机; 在右边,一切都转化为视图空间(世界空间仅代表帮助形象化转换) |
投影空间
这个场景现在处于投影的最友好的空间,视野空间。我们现在要做的就是将其投影到相机的虚拟屏幕上。在展现图像之前,我们还需要进入另一个最后的空间 - 投影空间。这个空间是一个长方体,每个轴的尺寸在-1到1之间。这个空间非常方便剪辑(1:-1范围以外的任何内容都在摄像机视图区域之外)并简化了平坦化操作(我们只需要放下z值即可获得平坦的图像)。
从视图空间到投影空间,我们需要另一个矩阵,视图到投影矩阵,这个矩阵的值取决于我们想要执行什么类型的投影。两个最常用的预测是正投影与透视投影。
为了做正投影,我们必须定义相机可以看到的区域的大小。这通常用x和y轴的宽度和高度值以及z轴的近和远z值来定义(图9)。
图9:正交投影 |
给定这些值,我们可以创建将盒子区域重新映射到长方体的变换矩阵。下面的矩阵将视图空间中的矢量转换成正交投影空间,并假设右手坐标系统。
图10:从图9中的茶壶获得的投影空间 |
另一个投影是透视投影。这个想法与正射投影相似,但是这次视区是一个平截头体,因此重映射有点棘手。不幸的是,这种情况下的矩阵乘法是不够的,因为在乘以矩阵后,结果不在相同的投影空间上(这意味着对于每个顶点w分量不是1)。为了完成这个转换,我们需要用w分量本身来划分向量的每一个分量。目前的图形API为你做分工,因此你可以简单的把所有的顶点乘以透视投影矩阵,并把结果发送给GPU。
图11:透视投影 |
GPU负责除以w,在立方体区域之外剪切这些顶点,展平放置z分量的图像,重新映射从-1到1范围的所有内容到0到1范围,然后将其缩放到视口宽度,高度,并将三角形光栅化到屏幕上(如果您正在CPU上执行光栅化操作,则必须亲自处理这些步骤)。因此,如果我们通过OpenGL或DirectX渲染,我们可以将这些最后的步骤视为理所当然,所以透视空间是我们链变革的最后一步。
最后,模型可以被转换为渲染链接[View To Projection] x [World To View] x [Model to World] = [ModelViewProjectionMatrix]。