面向电子游戏的 GPU 编程--三维顶点变换

0 阅读6分钟

三维顶点变换

变换管线

变换首先要确定 3D 美术师在 Maya 或 Blender 等软件中使用的坐标系。将这些物体转换成世界坐标系,方法是获取它们并根据需要进行调整。这种世界变换将它们放入世界坐标系中,而世界坐标系正是游戏玩法所在的位置。物理计算和人工智能程序的执行也可能在这里进行。

image.png

关卡设计师在游戏过程中使用这个坐标系。摄像机可能会移动,因此试图变换的作用是将这些世界坐标空间转换成摄像机使用的概念空间。摄像机可能与玩家重合(例如第一人称射击游戏),也可能在第三人称游戏中稍微后移。

image.png

投影变换将这些三维坐标放到计算机屏幕上最终过程的一部分,但我们将在下一节课中讲解。我们将尝试用矩阵乘法来表示这些过程。

image.png

我们使用矩阵代数方法的原因是,我们可以预先乘一些矩阵,从而节省计算时间。所以,你的美术师正在使用比如在 3ds Max 之类的软件中,他们可能会将 a000 设置为正在创建的物体的中心,例如立方体或茶壶。如果是人形物体,通常是 000 会位于角色的脚下。

image.png

首先,你需要将物体旋转到正确的方向,然后将其平移到世界空间中所需的位置。正如我们稍后会看到的,你需要按这个顺序进行操作。你可能还需要进行缩放操作,因为关卡设计师经常会使用物体骨架,并为其添加不同的纹理,然后将某个物体重新用于场景中的其他用途。

image.png

视图变换的作用是考虑摄像机的影响。摄像机位于空间中的特定点,向下看空间中的特定点,并且具有特定的方向。你可以将其想象成飞机的滚转角。定义视图变换的方法有很多种,但一个很好的概念方法是想象摄像机本身是场景中的另一个物体,例如盒子和茶壶。如果你创建等效的世界变换矩阵,那么取一台摄像机,将其放置在场景中的某个特定位置,并调整到正确的方向。然后,你可以想象将视图矩阵设为该变换的逆矩阵。接着,如果将该矩阵应用于场景中的所有物体,它们就会围绕这该逆矩阵旋转并重新排列,就像摄像机沿着 z 轴向下拍摄,并带有一个朝向新的 y 轴方向的向上向量。

image.png

投影变换将在下一篇文章中讲解。这是将三维坐标转换为屏幕上的二维坐标之前的最后一步。

平移

关于齐次坐标的概念,有一套丰富的数学技巧。我们在这里不会使用太多复杂的数学结构,它们主要是为了方便起见。我们希望用矩阵乘法来表示对这些 3D 顶点的所有操作,但平移除外。这样,我们就可以通过在将这些变换应用于 10000 个顶点之前将这些矩阵预乘来处理一系列变换。平移本身不能简单地用矩阵乘法来表示。虽然旋转可以通过这种方式轻松处理,但我们使用齐次坐标的技巧在于添加第四个坐标,这样所有坐标就变成了四维坐标而不是三维坐标。这在以后的课程中处理透视投影时会发挥作用,我们需要除以距离,通常的做法是将距离代入 W 向量。但现在,我们只需将数字 1 代入 W 即可,这足以表示我们需要的平移。

来看一个具体的平移,假设我们需要平移这个矩形并追踪左下角这个特定坐标的生命周期。我们希望能够将这些元素从 XYZ 平移到新的位置,平移后的新位置是(xt,yt,zt)(x_t, y_t, z_t)。可以看到,这里的红点已经移动了。

image.png

问题是,我们如何用矩阵表示这个平移?通过添加这个元素,我们可以实现齐次坐标。注意,我们这里使用行坐标约定,所以我们有一个水平行,它与这个 4x4 变换矩阵进行后乘,得到一个新的行向量。微软的 Direct3D 和 XNA 都使用这种系统。此外,HLSL 或等效的 CG 的大部分文档也使用这种行向量约定。正如我们稍后会看到的,我没用 HLSL 编写的着色器代码,可以利用 HLSL 内置的各种函数对数据进行计算这一事实。对于如何设置函数的输入,我们可以想象我们的向量要么是行表示法,要么是列表示法,因为 HLSL 本身并不真正知道你使用的是哪种约定,它只是拥有这些四维向量。我们稍后会详细介绍,OpenGL、Unity 以及大多数其他软件都使用列坐标约定。

image.png

缩放

image.png

缩放是一种很容易用矩阵变换表示的运算,实际上,你可以用一个 3x3 的矩阵来处理三维向量,不需要像平移那样引入额外的 W 坐标。假设我们想将这个矩阵放大四倍,我们可以通过将 x、y 和 z 坐标分别乘以一个标量来轻松表示。这里我们得到一个对角变换矩阵。

image.png

旋转

旋转在这里要复杂得多,我们必须考虑如何旋转物体,有很多不同的方法来表示旋转。一些游戏引擎,比如 Unity,实际上使用了一种相当复杂的结构,称为四元数,它是复数的思维扩展,用于表示旋转。处理四元数,提供了一种自然的插值摄像机角度的方法,还可以处理称为万向节锁的问题,我们以后可能会讨论也可能不会讨论这个问题。人们设计飞机控制系统,我们将讨论俯仰、滚转和偏航。Unity 和许多其他游戏引擎会考虑绕 x 轴旋转,然后绕 y 轴旋转,最后绕 z 轴旋转。

image.png

x 轴旋转时,我们这里使用的是左手坐标系。所以,伸出你的左手,拇指指向 x 轴,然后弯曲其他手指来指示旋转方向。同样地,绕 y 轴旋转时,拇指向上,弯曲其他手指,然后将拇指指向屏幕,再次弯曲其他手指。你可以看到物体在这些情况下是如何旋转的,基本上都是逆时针旋转。如果你想沿着某个轴向下看,你也可以使用右手旋转系统,在这种情况下,绕轴旋转是顺时针的,所以你可能需要检查你的游戏引擎使用的约定,或者像大多数人一样,直接输入一个数字,如果旋转方向错误,就输入一个负号。现在,你执行这些操作的顺序很重要,而且在三维空间中也很重要。

image.png

幸运的是,在二维空间中,如果你只是在制作一个 2D 游戏,争议就少得多。我们再次来追踪某个点的生命周期。还记得上次我讲解了人们使用的各种不同的三维坐标系么?因为它们很容易让人困惑。处理二维图形时,争议往往较小。我们将使用 xB(水平)和 YB(垂直)坐标。与传统的 3D 游戏编程方式相比,唯一略有不同的是,传统的 2D 游戏以 y 为初始值,数值越小,y 值越小。但在传统的数学中,以及我们在这里进行的大部分数学运算中(直到某个特定点),我们通常认为正 y 值代表向上,这没问题。所以,如果我们从这里开始,想要旋转它,比如旋转角度 θ\theta,传统上正 θ\theta 表示逆时针旋转。为了方便计算,我们可以将这些二维坐标乘以一个二维矩阵,以保留这个额外的 W 坐标的传统。我们稍后会把它写进去。本质上,你得到一个包含与旋转角度相关的正弦和余弦值的矩阵。你需要注意的一点是负号的位置,正如我们稍后会看到的,这实际上可能有点棘手,所以对于这种传统的数学符号,下面是负号,上面是正号。将这个矩阵应用到我们的坐标上,就能得到我们想要的旋转。现在,当我们将其应用到三维空间时,我们需要考虑绕哪个轴旋转。

image.png

你可以运用大量的几何和三角函数知识来绕任意轴旋转,但在这里,我们用绕 z 轴、绕 y 轴和绕 x 轴旋转来表示旋转。如果我们按照一致的顺序进行操作,就能得到一致的符号。这种方法足够丰富,可以给出我们需要的任何物体方向。所以整体结构非常相似:绕 z 轴旋转时,我们将保持 z 轴固定,然后绕 x 轴和 y 轴旋转;绕 y 轴旋转时,我们将保持第二个坐标固定(这就是为什么你在这里看到 1)。然后,它会使用你之前看到的原始旋转矩阵,现在我们将其应用到 x 轴和 z 轴上。需要记住的一点是,符号的位置现在互换了,我所说的符号是指 sin(x)=sin(y)sin(x) = sin(y),而不是 sin(y)sin(y)。现在我们保持 x 轴不变,并进行 y 轴和 z 轴的 2x2 旋转矩阵运算。但现在符号又变回去了。假设我们生活在四维或五维宇宙中,或者如果这样的宇宙中存在生物并制作了电子游戏,那么这种符号的来回翻转就会持续很久。很久以前,我查阅过符号翻转的原因,但后来忘记了。你可以相信我,你需要这样做才能使用不同旋转方向的符号约定保持一致。正如我之前提到的,并且我会继续强调,顺序很重要。

image.png

让我们来看一个例子,这个例子来自笨拙的 PowerPoint 图形动画。这里有一个圆柱体,在左边,我们首先要绕 y 轴逆时针旋转 90 度,这样它就到了这里。现在,如果我们绕 x 轴顺时针旋转它,它就会看起来几乎是垂直向下。现在让我们尝试同样的实验,但交换操作顺序。如果我们旋转沿 x 轴逆时针旋转,它看起来和我们最初看到的一样。现在,如果我们沿 y 轴逆时针旋转,它最终会沿着 z 轴向下看。

image.png

这是对正在发生的事情的一种几何视觉解释,但我们可以通过乘以相关的旋转矩阵来看到一般的效果。

image.png

非交换律

现在让我们考虑缩放和平移,第一个例子将进行平移,然后进行缩放,所以我们先平移立方体,稍微向上移动一点,然后再缩放。但如果我们先缩放再平移,最终缩放的幅度会小一些。

image.png

另外需要注意的是,我们并没有均匀地缩放所有维度。关卡设计师在尝试将一个物体重新用于其他用途时,可能会经常用到这种方法。通常情况下,你应该使用右侧的方法,即先缩放物体,然后再平移它。左侧方法的问题在于,缩放同时作用于原始物体和平移,因此很难预测这些操作将如何组合。以上是一个几何可视化示例。

image.png

我们还可以通过矩阵乘法的结果来看出顺序的重要性。我们先平移再缩放,偏移量也会乘以缩放值。而如果我们先进行缩放,这些偏移量就不会受到缩放的影响。

image.png

  • 顺序很重要;
  • 进行矩阵运算和 3D 图形处理时,由于不同的约定,常常会让人感到困惑。例如,如果使用行约定,则将矩阵放在行之后,然后从左到右进行运算;如果使用列约定,则将矩阵放在列向量左侧,然后从右到左进行运算。

视图变换

image.png

视图变换的计算相当复杂,矩阵的表达方式也多种多样。通常的做法是指定摄像机的位置(即摄像机的视线方向),以及另一个向量(即摄像机的"向上"向量)。推导省略,以下是各种 api。

image.png