跳转到内容

OpenGL 编程/3D/矩阵

来自维基教科书,开放世界中的开放书籍

理解变换矩阵

[编辑 | 编辑源代码]

构建变换矩阵背后的概念很容易理解,但矩阵究竟代表什么?本节介绍了一种机制,用于解释包含多个平移、旋转和缩放的复合变换矩阵,并将其解释为参考系之间的转换。它提供了一个模型,用于将基本变换组织成嵌套参考系的层次结构。

参考系

[编辑 | 编辑源代码]

我们通常认为物体是“向前和向后”,“向上和向下”或“向左和向右”移动;我们通常不会考虑它们相对于任意参考系的运动。它们“转动”(偏航)、“抬头”(俯仰)和“翻滚”(滚转)、“向前移动”、“横向移动”和“向上跳跃”。那么我们如何组合变换来创建行为上更像现实世界中物体的物体呢?

让我们将物体的参考系定义为其位置和方向。这可以通过两个正交向量来唯一定义,它们指定了物体的“前进”和“向上”方向(暗示着第三个,“向右” == 前进叉乘向上),以及一个指定了物体位置的顶点。

当子参考系的参数由父参考系的坐标中的向量/顶点指定时,我们可以通过创建由四个列向量(向右、向上、向前、位置)组成的矩阵,来创建一个将向量子参考系变换到父参考系的矩阵,其中向右 == 前进叉乘向上。

让我们更具体一些。

宇宙的参考系,或者以太再探

[编辑 | 编辑源代码]

我们首先看看单位矩阵

根据我们的定义,我们将第一列 ({1,0,0,0}) 解释为“向右”方向,第二列 ({0,1,0,0}) 解释为“向上”,第三列 ({0,0,1,0}) 解释为“向前”(或者“向后”,如果你愿意),第四列 ({0,0,0,1}) 解释为“位置”顶点。请注意,位置顶点的 w 坐标为 1,而方向向量(实际上是两个顶点之间的差)的 w 坐标为 0。

这个例子看起来微不足道,但它为我们提供了基础。我们可以称其为“宇宙”参考系。向右为 +x,向上为 +y,向前(或向后,如果你愿意)为 +z,原点位于 {0,0,0,1}。

现在,我们可以看看宇宙中子对象的示例。我们可以通过矩阵来指定该对象的参考系

其中向右、向上和向前是参考系的归一化方向向量,位置是参考系的位置顶点,在父(或“宇宙”)对象的参考系中指定。当所有方向的“w”坐标为 0,而位置顶点的“w”坐标为 1 时,这最容易理解,就像“四维技巧”约定一样。

为了演示:我们在 framechild 的坐标中有一个顶点 v = {2, 0, 0, 1}。将 framechild 乘以 v,我们得到

请注意,当 rightw = 0 且 positionw = 1 时,2*rightw + positionw = 1。我们可以看到,framechild 中的顶点 v 等价于 2*frameright + frameposition,这正是 v 在父参考系中的描述。还需要注意的是,在方向不带位置 (w=0) 的情况下,会应用旋转,但不会累积位置 - 方向会被重新定向,而不会错误地分配位置。

每个 "frame" 矩阵都是从指定坐标所在的参考系到父对象的参考系的变换,最终我们得到

 frameparent*framechild*framenested_child*vertex_in_nested_child_coords ==
 frameparent*framechild*vertex_in_child_coords ==
 frameparent*vertex_in_parent_coords ==
 vertex_in_universe_coords

这个矩阵 (frameparent*framechild*framenested_child) 在与宇宙的参考系矩阵后乘后,将把坐标从 nested_child 的坐标系转换为宇宙的坐标系。这些参考系可以无限嵌套。

参考系层次结构示例

 universe -> galaxy -> solar system -> earth -> position on earth

每个参考系的运动可以被认为是相互 "独立" 的。当银河系运动时,太阳系会随之运动,地球也会随之运动,而你在地球上的位置也会随之运动。

缩放操作

[edit | edit source]

虽然作者认为应该尽量避免缩放操作,因为它们会破坏顶点法线,但有时还是需要它们。幸运的是,这很容易实现。让我们考虑缩放变换

               | αx   0  0   0  |
 scale_xform = | 0   αy  0   0  |
               | 0   0   αz  0  |
               | 0   0   0   αw |

其中 αw == 1,除非你疯了。

通常,我们希望将顶点作为执行的第一个操作进行缩放;当我们将 "x" 缩放 3 倍时,我们通常不希望缩放其方向和位置(即变换矩阵的列),因为它们是在 *父* 参考系中指定的。因此,缩放是 "参考系" 变换模型中附加到矩阵的最后一个操作。

当我们将 framechild 乘以 scale_xform 时,我们得到

               | αx*rightx  αy*upx  αz*forwardx  αw*positionx |
 final_xform = | αx*righty  αy*upy  αz*forwardy  αw*positiony |
               | αx*rightz  αy*upz  αz*forwardz  αw*positionz |
               | αx*rightw  αy*upw  αz*forwardw  αw*positionw |

缩放因子只是作为方向向量的幅度出现。

"Camera" 变换

[edit | edit source]

FIXME: 我只证明了这一点,但没有演示它是否有效。

从宇宙的参考系到相机的参考系的变换与上面描述的变换存在细微但至关重要的差异。与之前的讨论不同,"相机"(*父* 参考系)通常是在 "宇宙"(*子* 参考系)坐标中指定的。使用相机在宇宙中自由移动的传统逻辑需要额外的考虑。

为了在宇宙和相机参考系之间转换,我们需要创建一个 "固定" 坐标集,它将 "宇宙" 表示为 "相机" 的子系;我们需要在相机坐标系中得到宇宙的参考系。

FIXME: 急需插图。正在思考这个问题

  1. 如果相机与宇宙的方向相同:不进行变换。
  2. 如果相机向左倾斜,则宇宙需要向右倾斜。
  3. 如果相机向上倾斜,则宇宙需要向下倾斜。
  4. 如果相机与宇宙的方向相反,则宇宙需要朝另一个方向。
  5. 如果相机相对于宇宙是颠倒的,则宇宙需要翻转。

这些是反射的特征;整个 "相机" 参考系矩阵是相机 '宇宙' 坐标关于宇宙轴的反射的列向量组合。定义

 c = camera reference frame in universe coordinates
 u = universe reference frame (identity)
 refl(a, b) = reflection of a over b = 2*dot(a,b)*b - a:

我们可以将相机的参考系矩阵构建为

 framecamera = { refl(cright, uright), refl(cup, uup), refl(cforward, uforward), refl(cposition, uposition) }

有趣的是,这包括位置顶点,它被 "反射" 到原点。

这适用于所有 "父" 参考系存储在 "子" 参考系坐标中的参考系。作者无法想到其他任何此类情况。

投影变换

[edit | edit source]

投影变换应该只用于将相机拟合到视窗。glOrtho*()、glFrustum() 和 gluPerspective() 通常用于实现此目标。

"相机" 框架 - 无论如上所述指定,还是通过 gluLookAt() - 通常将是模型视图矩阵堆栈上的 "底部" 框架。该矩阵将宇宙坐标转换为相机坐标。投影矩阵在乘法顺序中位于模型视图矩阵之前,因此投影矩阵可以被认为是从相机坐标到屏幕坐标的变换。

使用 "相机" 框架与使用 gluLookAt() 没有区别(FIXME:检查一致性),但这两个操作的参数化略有不同。

总结

[edit | edit source]

通过将变换矩阵视为由四个列向量 { c0, c1, c2, c3 } 组成的参考系,我们可以将矩阵分解成更易用的属性

  • "右" 轴:c0.direction()
  • "上" 轴:c1.direction()
  • "前" 轴:c2.direction()
  • "位置":c3
  • "x" 缩放:c0.magnitude()
  • "y" 缩放:c1.magnitude()
  • "z" 缩放:c2.magnitude()
  • "w" 缩放:c3.magnitude()

我们可以使用这些属性更直观地实现运动

  • 向前移动距离 β:xform += { 0, 0, 0, β*c2 }
  • 向上移动距离 γ:xform += {0, 0, 0, γ*c1 }
  • 向左转 30 度:xform *= rotation_matrix(axis = c1, angle = π/6.0)

等等。

我们可以通过简单地将参考系矩阵(如上所述构建)乘以当前矩阵,从 "当前" 参考系(即在 OpenGL 上下文中由模型视图矩阵表示的参考系)转换为 "子" 参考系。完整的绘图操作可以通过以下方式执行

 glMatrixMode(GL_MODELVIEW);
 glLoadMatrixd(camera_refframe_matrix);
 glPushMatrix();
   glMultMatrixd(object_refframe_matrix);
   draw_object();
   glPushMatrix();
     glMultMatrixd(subobject_refframe_matrix);
     draw_sub_object();
   glPopMatrix();
 glPopMatrix();
 glFlush();
华夏公益教科书