OpenGL 编程/3D/数学
许多关于我们做事方式的约定是任意的。例如,在道路的左侧行驶并不比在右侧行驶好或坏。但是,如果同一条道路的用户不同意都在同一边行驶,那么结果就会很糟糕。
计算机图形也是如此。例如,坐标系可以是右手或左手;如果你想象将你的眼睛放在 (0, 0, 0) 点并依次沿着正 X 轴、正 Y 轴和正 Z 轴的方向看,如果你的视线描述了一个顺时针旋转,那么坐标系就是右手坐标系,而逆时针则意味着它是左手坐标系。记住哪一个是哪一个的方法是将你的拇指、食指和中指伸出成直角,将拇指方向称为正 X,食指指向正 Y,中指指向正 Z;如果你用右手这样做,那么坐标系(自然地)就是右手坐标系;而你的左手定义了一个左手坐标系。
建议的约定(在大多数 3D 软件中使用)是定义你的模型/场景为右手坐标系。
在计算机图形中,通常会使用多个不同的坐标系。例如,汽车模型是根据其自己的模型坐标系定义的。要将汽车放置在场景中,也许是在道路上移动,需要将这些模型坐标转换为场景的世界坐标。然后,“相机”(实际上是观看场景的人的眼睛位置的表示)的放置就涉及到将这些世界坐标(通过视图变换)转换为眼睛坐标,这些坐标相对于观察者定义,使得 X 轴是水平的并且向右增加,Y 轴是垂直的并且向上增加,Z 轴是水平的并且直接向观察者增加。最后,它们被转换为规范化设备坐标并映射到用户显示器上的像素。
并且只是为了增加乐趣,汽车模型本身可能有多个坐标系。例如,每个车轮都可以在它自己的子坐标系中定义,在该坐标系中,它相对于其父级(即汽车的车身)旋转。因为车轮相对于汽车进行变换,所以它会自动获得汽车的变换,因此通过场景移动汽车就足以使车轮一起移动,它们不需要单独重新定位。
因此,变换流水线(就眼睛坐标而言)看起来像这样(其中方括号中的步骤是变换,而没有表示坐标值)
model coords → [parent xform 1] → ... → [parent xform n] → [viewing xform] → eye coords
然后眼睛坐标进一步转换如下
eye coords → [projection xform] → clip coords → [÷ w] → normalized dev coords → [viewport xform] → window coords
我之前说过,建议使用右手坐标系。这在眼睛坐标阶段之前是正确的。投影变换通常会翻转 Z 值以使坐标系成为左手坐标系,这样 Z 值现在就会远离观察者增加,而不是朝向他们增加。这样做是为了使 Z 值能够转换为深度缓冲区中存储的值,其中越来越大的值表示越来越大的深度。
眼睛坐标的标准范围是每个轴上的 [-1 .. +1] 区间。当然,你可以使用计算机浮点表示形式能够处理的任何数字定义你的模型和场景,并且你可以随意命名单位 - 米、英尺、微米、光年,无论什么。你所要做的就是确保所有模型和视图变换的组合将你想要看到的场景部分带入那个 [-1 .. +1] 范围的眼睛坐标值内。
然后,规范化设备坐标只是将这些眼睛坐标映射到 (x, y) = (-1, -1) 在视图区域(窗口、全屏,无论什么)的左下角,而 (x, y) = (+1, +1) 在右上角(z 值,当然,最终会进入深度缓冲区)。最终的视口变换将这些重新映射到实际像素的单位中,这些像素对应于你的视图区域的实际大小。
应用于计算机图形的普通变换类型(也是 OpenGL 支持的唯一类型)称为线性变换。这是因为在变换之前是直线的直线在变换后仍然是直线。这种变换可以方便地用矩阵来表示。一个 4×4 矩阵可以表示三维空间中任何可能的线性变换。
多个变换可以通过乘法它们各自的矩阵来组合(“连接”)。与传统标量数字的乘法不同,变换的顺序很重要:缩放后进行平移(重新定位)与先进行平移不同,因为缩放是在不同的中心点进行的。
你会经常看到三维位置向量写成 (x, y, z) 而不是 (x, y, z, w)。相应的变换矩阵是 4×4 而不是 3×3。为什么呢?
这是为了使所有线性变换都可以在矩阵乘法的形式下统一表示。否则,平移将不得不作为加法步骤单独分离出来
(x', y', z') = [scaling+rotation+shear] × (x, y, z) + [translation]
使用齐次坐标,所有内容都可以合并到一个矩阵中
(x', y', z', w') = [scaling+rotation+shear+translation] × (x, y, z, w)
通常将w 设置为 1.0,至少在开始时是这样。大多数变换将产生w = 1.0 的向量,如果它们一开始就是这样给出的。但是,注意产生w ≠ 1.0 的向量的变换(特别是透视变换),如果你想对它们分别进行任何计算,你必须归一化x、y 和z 值(用w 除它们),否则你可能会得到错误的结果!
这里我们又遇到了需要选择一个约定并坚持下去的情况。用变换M 将向量V 变换为向量V' 可以写成列向量的前乘
V' = M × V
或行向量的后乘
V' = V × M
建议你坚持使用前乘约定,因为这与 OpenGL 的工作方式更一致。例如,在(OpenGL 3.0 之前的)固定功能流水线中,调用的顺序(在伪代码中)将是
apply matrix M; draw vector V
这对应于前乘公式中的从左到右的顺序。如果M 分解为单独的组成部分,例如M1 × M2,那么前乘公式变为
V' = M1 × M2 × V
相应的固定功能调用是
apply matrix M1; concatenate matrix M2; draw vector V