翻译我的OpenGL学习进阶之旅世界(World)视图(View)和投影变换矩阵(Projection Transformation Matrices)
Posted 字节卷动
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了翻译我的OpenGL学习进阶之旅世界(World)视图(View)和投影变换矩阵(Projection Transformation Matrices)相关的知识,希望对你有一定的参考价值。
PS: 本文翻译自原文: Article - World, View and Projection Transformation Matrices
一、介绍
在本文中,我们将尝试详细了解任何 3D 引擎的核心机制之一,即允许在 2D 监视器上表示 3D 对象的矩阵变换链。我们将尝试详细介绍矩阵的构造方式和原因,因此本文不适合绝对的初学者。我将假设向量数学和矩阵数学的一般知识。
我们将首先讨论变换
和向量空间
之间的关系。然后我们将展示如何以矩阵
形式表示变换
。从那里我们将展示您需要应用的典型转换序列,即从模型(Model)
到世界空间(World Space)
,然后到相机(Camera)
,然后是投影(Projection)
。
二、向量空间(Vector Spaces):模型空间(Model Space )和世界空间( World Space)
向量空间(Vector Spaces)
是一种数学结构
,由给定数量的线性无关向量定义,也称为基向量
(例如在图 1 中有三个基向量);
线性无关向量的数量定义了向量空间的大小,因此 3D 空间有三个基向量,而 2D 空间则有两个。
这些基向量可以缩放
并相加
以获得空间中的所有其他向量。
向量空间是一个相当广泛的主题,本文的目的不是详细解释它们,为了我们的目的,我们需要知道的是我们的模型存在于一个特定的向量空间中,它以模型空间和 它用规范的 3D 坐标系表示(图 1)。
- 图 1:标准右手 3D 坐标系
当艺术家创作 3D 模型时,他会相对于他正在使用的工具的 3D 坐标系[ 模型空间(Model Space )
]创建所有顶点和面。 所有顶点都相对于模型空间的原点,因此如果我们在模型空间中的坐标 (1,1,1) 处有一个点,我们就可以准确地知道它在哪里(图 2)。
游戏中的每个模型都生活在自己的模型空间(Model Space )
中,如果您希望它们具有任何空间关系(例如,如果您想将茶壶放在桌子上),则需要将它们转换为公共空间
(这通常是 称为世界空间(World Space)
)。
- 图 2:茶壶在位置 (1,1,1) 的顶点
让我再次强调这一点。
重要的是要了解矢量仅在坐标系内有意义; 如果我们不指定空间,我们就不能代表任何点。
模型从工具导出到游戏引擎后,所有顶点都在模型空间(Model Space )
中表示。
现在,如果我们要将刚刚导入的对象放入游戏世界中,我们需要将其移动
和/或旋转
到所需位置,这会将对象放入世界空间(World Space)
。 移动(moving)、旋转(rotating )或缩放(scaling )
对象这就是我们所说的变换(transformation)
。 当所有对象都被转换到一个公共空间(世界空间 World Space
)时,它们的顶点将相对于世界空间(World Space)
本身。
三、变换(Transformation)
我们可以将向量空间中的变换简单地看作是从一个空间到另一个空间的变化。 这是向量变换中最棘手的部分之一,所以让我们尽量把它说清楚。
我们可以将 3d 中的向量空间想象为三个正交轴(如图 1 所示)。 我们总是需要有一个“活动”空间,这是我们用作其他一切(几何或其他空间)参考的空间。 如果我们有两个模型,每个模型都在自己的模型空间中,那么在定义一个共同的“活动”空间之前,我们无法同时绘制它们。
现在假设我们从一个活动空间开始,称为空间 A
,其中包含一个茶壶。
我们现在想要应用一个变换,将空间 A
中的所有东西移动到一个新位置; 但是如果我们移动空间 A
,那么我们需要定义一个新的“活动”空间来表示转换后的空间 A
。我们将新的活动空间称为空间 B
(图 3)。
在转换之前,空间 A
中描述的任何点都相对于该空间的原点(如左侧图 3
中所述)。 在我们应用变换之后,所有点现在都相对于新的活动空间空间 B
(图 3,右
)。
任何相对于空间 B
重新定义空间 A
的操作都是一种转换
。 请注意,在转换之后,空间 A
现在如何“丢失”到空间 B
中,或者更准确地说,它被重新映射
到空间 B
中,因此我们无法对其应用任何其他转换(除非我们撤消转换并使 Space 再次是“活动”空间)。
另一种看待这一点的方式是,想象空间中的任何东西都随着基向量移动,并想象空间 A
开始与空间 B
完美重叠。当我们应用变换时,我们将空间 A
移离空间 B
,以及空间 A
中的任何东西 随之移动
。 一旦我们移动了所有顶点,我们就将它们全部表示为相对于空间 B
,我们就完成了转换
。
如果我们需要再次在空间 A
中操作,可以将变换的逆应用于空间 B
。这样做,空间 B
将再次重新映射到空间 A
(此时,我们“失去”了空间 B
)。 如果我们知道变换和它们的逆,我们总是可以将两个空间重新映射到另一个。
- 图 3:空间变换
我们可以在向量空间中使用的变换
是缩放、平移和旋转
。 重要的是要注意,每个变换
总是相对于原点
,这使得我们用来应用变换本身的顺序
非常重要。
如果我们向左旋转 90°
然后平移
,我们得到的结果与我们首先平移然后旋转 90°
得到的结果非常不同(图 4,我省略了活动空间之外的任何空间)。
- 图 4:以不同顺序应用相同变换的不同结果
图 4 还可以帮助我们更多地理解变换的逆。
因此,如果您采用图 4 的左上角,向左旋转 90°
的变换
可以通过它的逆变换移除,即向右旋转 90°
。
注意变换的逆是变换本身,所以我们没有理由不把它应用到完全不相关的空间中的对象。
向左 90° 变换
的逆是向右 90° 变换
,这显然可以应用于任何空间中的任何事物。
四、变换矩阵 (Transformation Matrix)
现在我们明白了变换是从一个空间到另一个空间的变化,我们可以开始数学了。
如果我们想表示从一个 3D 空间
到另一个空间
的转换
,我们将需要一个 4x4 矩阵
。
我将从这里开始假设列向量表示法,就像在 OpenGL
中一样。 如果你是行向量,你只需要转置矩阵并在我发布乘法的地方预乘向量。
为了应用变换,我们必须将所有要变换的向量与变换矩阵相乘。 如果向量在空间 A
中,并且变换描述的是空间 A
相对于空间 B
的新位置,那么在乘法之后,所有向量都将在空间 B
中描述。
现在,让我们看看我们如何以矩阵形式表示通用变换:
其中
Transform_XAxis
是新空间中的XAxis
方向,Transform_YAxis
是新空间中的YAxis
方向,Transform_ZAxis
是新空间中的ZAxis
方向,Translation
描述了新空间相对于活动空间的位置。
有时我们想做简单的变换,比如平移
或旋转
; 在这些情况下,我们可能会使用以下矩阵,它们是我们刚刚介绍的通用形式的特殊情况。
- 翻译矩阵 Translation Matrix
其中平移是一个 3D 向量,表示我们要将空间移动到的位置。 平移矩阵使所有轴完全按照活动空间旋转。
-
缩放矩阵 Scale Matrix
其中scale
是一个3D 矢量
,表示沿每个轴的比例。
如果您阅读第一列,您可以看到新的 X 轴如何仍然面向相同的方向,但它是由标量scale.x
缩放的。 同样的情况也发生在所有其他轴上。 还要注意翻译列是如何全为零的,这意味着不需要翻译。 -
绕X轴旋转矩阵:Rotation Matrix around X Axis
其中 theta
是我们想要用于旋转的角度。
注意第一列永远不会改变,这是预期的,因为我们围绕 X 轴旋转
。
还要注意将 theta
更改为 90°
如何将 Y 轴
重新映射到 Z 轴
,将 Z 轴
重新映射到 -Y 轴
。
- 绕 Y 轴的旋转矩阵:Rotation Matrix around Y Axis
- 绕 Z 轴的旋转矩阵:Rotation Matrix around Z Axis
Z 轴和 Y 轴的旋转矩阵的行为方式与 X 轴矩阵相同。
我刚刚向您展示的矩阵是最常用的矩阵,它们是您描述刚性变换所需的全部。 您可以通过将矩阵一个接一个相乘来将多个变换链接在一起。 结果将是编码完整转换的单个矩阵。 正如我们在转换部分看到的,我们用来应用转换的顺序非常重要。 这在数学中反映为矩阵乘法不是可交换的。 因此,通常 Translate x Rotate
不同于 Rotate x Translate
。
由于我们使用的是列向量,因此我们必须读取从右到左的变换链,因此如果我们想 围绕Y 轴向左旋转 90°
,然后 沿 Z 轴平移 10 个单位
,则该链将是
[Translate 10 along X] 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 矩阵
,因此我们需要使用齐次坐标(homogeneous coordinates)
,因此我们需要一个 4 维向量
,其最后一个分量为 1
。
- 图 5:球体从模型空间转换到世界空间
五、模型空间(Model Space)、世界空间(World Space)、视图空间(View Space)
现在我们有了拼图的所有部分,让我们把它们放在一起。
当我们想要渲染 3D 场景时,第一步是将所有模型放在同一个空间中,即世界空间(World Space)
。
由于每个对象在世界中都有自己的位置和方向,因此每个对象都有不同的模型到世界转换矩阵。
- 图 6:三个茶壶,每个茶壶都在自己的模型空间中
- 图 7:设置在世界空间(World Space) 中的三个茶壶
将所有对象放在正确的位置后,我们现在需要将它们投影到屏幕上。
这通常分两步完成。
- 第一步将所有对象移动到另一个称为视图空间的空间中。
- 第二步使用投影矩阵执行实际投影。 这最后一步与其他步骤略有不同,我们稍后会详细介绍。
5.1 为什么我们需要一个视图空间(View Space)
?
视图空间(View Space)
是我们用来简化数学并保持所有内容优雅并编码为矩阵的辅助空间。
这个想法是我们需要渲染
到相机
,这意味着将所有`顶点投影到可以在空间中任意定向的相机屏幕上。
如果我们可以让相机以原点为中心并向下观察三个轴中的一个,那么数学就会简化很多,假设 Z 轴遵循惯例。那么为什么不创建一个执行此操作的空间,重新映射世界空间,使相机位于原点并沿 Z 轴向下看?这个空间是视图空间(View Space)
(有时称为相机空间
),我们应用的变换将所有顶点从世界空间(World Space)
移动到视图空间(View Space)
。
5.2 我们如何计算视图空间(View Space)
的变换矩阵?
现在,如果您想将相机放置在世界空间(World Space)
中,您将使用一个位于相机所在位置的变换矩阵
,该矩阵的方向使 Z 轴
指向相机目标。如果应用到世界空间中的所有对象,则此变换的逆变换会将整个世界移动到视图空间中。请注意,我们可以将
Model To World: 即 模型空间(Model Space)
转成 世界空间(World Space)
和
World to View : 即 世界空间(World Space)
转换成 视图空间(View Space)
这两个转换
组合成
Model To View: 即 模型空间(Model Space)
转换成 视图空间(View Space)
。
- 图 8:左边是世界空间中的两个茶壶和一个相机; 在右侧,一切都被转换为视图空间(世界空间仅用于帮助可视化转换)
六、 投影空间 (Projection Space)
场景现在位于最适合投影的空间中,即视图空间( View Space)
。 我们现在要做的就是将其投影到相机的假想屏幕上。 在展平图像之前,我们仍然需要进入另一个最终空间,即 投影空间 (Projection Space)
。
这个空间是一个长方体,每个轴的尺寸都在 -1 到 1
之间。 这个空间对于裁剪非常方便(1:-1
范围之外的任何东西都在相机视图区域之外)并简化了展平操作(我们只需要删除 z 值
以获得平面图像)。
为了从 视图空间( View Space)
进入 投影空间 (Projection Space)
,我们需要另一个矩阵,即 View to Projection 矩阵
,这个矩阵的值取决于我们想要执行的投影类型。 两种最常用的投影是正交投影(Orthographic Projection)
和透视投影(Perspective Projection)
。
6.1 正交投影(Orthographic Projection)
要进行正交投影(Orthographic Projection)
,我们必须定义相机可以看到的区域的大小。
这通常定义为 x 和 y 轴
的宽度和高度值
,以及 z 轴的近和远 z 值
(图 9)。
- 图 9:正交投影
给定这些值,我们可以创建将框区域重新映射到长方体的变换矩阵。 后面的矩阵将向量从视图空间转换为正交投影空间,并假设一个右手坐标系。
6.2 透视投影(Perspective Projection)
另一种投影是透视投影(Perspective Projection)
。 这个想法类似于正交投影(Orthographic Projection)
,但这次视图区域是一个平截头体,因此重新映射有点棘手。
不幸的是,这种情况下的矩阵乘法是不够的,因为乘以矩阵后的结果不在同一个投影空间上(这意味着 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]
七、总结一下
下面总结来源于:
OpenGL中的坐标处理过程包括模型变换、视变换、投影变换、视口变换等过程,如下图所示:
在上面的图中,注意,OpenGL只定义了裁剪坐标系、规范化设备坐标系和屏幕坐标系,而局部坐标系(模型坐标系)、世界坐标系和照相机坐标系都是为了方便用户设计而自定义的坐标系,它们的关系如下图所示(来自Chapter 7. World in Motion):
图中左边的过程包括模型变换、视变换,投影变换,这些变换可以由用户根据需要自行指定,这些内容在顶点着色器中完成;而图中右边的两个步骤,包括透视除法、视口变换,这两个步骤是OpenGL自动执行的,在顶点着色器处理后的阶段完成。
矩阵变换公式:
其中,中间的三项有个非常霸气的名字:MVP 矩阵
!
为什么不是 PVM 啊?
因为从逻辑上来说,是先把模型点坐标向量乘以模型矩阵,然后乘以视图矩阵,然后乘以投影矩阵,然后乘以视口矩阵的。所以先后顺序的确是 MVP。
以上是关于翻译我的OpenGL学习进阶之旅世界(World)视图(View)和投影变换矩阵(Projection Transformation Matrices)的主要内容,如果未能解决你的问题,请参考以下文章
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)
我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)