深度探索OpenGL图形编程艺术

更新:10-28 名人轶事 我要投稿 纠错 投诉

今天给各位分享深度探索OpenGL图形编程艺术的知识,其中也会对进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!

2 向量和矩阵

向量和矩阵是空间几何的基础知识,也是3D图形渲染中最常用的两个元素。

2.1 向量

顶点(Vertex)是OpenGL的主要输入对象。每个顶点可以看作三维空间中的一个向量。 OpenGL从顶点数组创建三角形图元,通过连接三维空间中的顶点形成有向矢量三角形。在着色器语言(GLSL)中,二维、三维和四维向量分别表示为vec2、vec3 和vec4。对于向量,OpenGL关心的是它的方向(Direction)和长度(Magnitude)。另外,OpenGL中也经常使用归一化值和归一化向量。归一化向量的定义是长度为1的向量,如(1,0,0)。

OpenGL通常只需要使用三个坐标分量(x,y,z)来表示三维空间中的方向向量,例如用于计算光照的平面法向量。然而,在处理三维物体的变形时,需要与44矩阵进行乘法运算。因此,在进行矩阵变化时,必须使用顶点并且必须用4维向量来表示。这将在本文稍后介绍齐次坐标系时讨论。

2.1.1 基本运算

向量之间的加减乘除要求要运算的两个向量的元素个数相等,并且计算时每个元素都能进行相应的运算。向量和标量的加法、减法、乘法和除法的基本元素可以直接对标量和每个向量元素执行。

2.1.2 点乘

向量的点积(Dot Product)也称为向量的内积(Inner Product)。其运算结果等于各元素相乘之和(v1 * v2=v1.x * v2.x + v1.y * v2.y + v1.z * v2.z),从几何意义上可以可以理解为两个向量的长度和它们的角度的余弦的乘积。在漫射照明环境中,常常需要计算平面的单位法向量与面向光源的单位向量的点积来确定片段的颜色值。在着色器语言中,调用内置函数dot()来计算两个向量的点积。

2.1.3 叉乘

向量的叉积也称为向量积。在进行线性代数运算时,将两个向量和一个单位向量组合起来形成一个矩阵,借助部分行列式的运算规则得到一个新的向量。向量的叉积也是向量。在几何意义上,它被表示为垂直于由所计算的两个向量组成的平面的向量。另外,对于向量v1和v2叉积得到的向量v3来说,v3的长度等于v1的长度、v2的长度以及它们之间的正弦积,即平行四边形的面积由两个向量组成。另外,与点积不同的是,向量叉积的阶数会影响其最终的计算结果,即会影响最终的向量方向。

矢量a和矢量b的叉积可以通过以下行列式计算。通常I、j、k取单位向量(1,1,1)。 ab=((aybz - azby) i, (azbx - axbz) j, (axby - ay bx) k)。在着色器语言中,调用函数cross()来计算两个向量的叉积。

向量v2v1的叉积的几何意义可以表示为下图,其中v3垂直于平面v1-v2,遵循右手螺旋定则。

2.1.4 长度

向量的长度(Length)也称为向量的模。三维向量的长度计算公式如下。在着色器语言中,可以调用函数length()来计算向量的长度。

2.1.5 折射和反射

在光照着色公式中,常常需要计算某个向量的折射和反射向量,以确定片段的最终颜色。下图中,Rin为入射标准向量,N为界面标准法向量,Rreflect为反射标准向量。 Rrefract 是不同折射率的标准折射矢量。

OpenGL计算反射矢量时,入射矢量和反射面法线矢量都必须使用标准矢量,这样计算结果只包含方向信息。反射矢量计算如下。在shader语言中,可以直接调用OpenGL函数reflect()来计算反射向量。

OpenGL在计算折射矢量时需要指定折射率。计算方法如下。在shader语言中,可以直接调用OpenGL函数refract()来计算反射向量。

2.2 矩阵

在OpenGL中,图形的变化几乎都是通过矩阵(Matrices)运算来实现的。 vn 的x 坐标分量,即顶点v 绕任意点任意方向旋转得到的新顶点vn,不仅与原顶点v 的x 坐标分量和旋转参数有关,还与y 坐标分量有关。 v 的z 轴坐标分量。数学上矩阵以行列式的形式表示,而在OpenGL 中矩阵以二维数组的形式表示。矩阵之间可以进行加法和乘法运算。将右向量表示的顶点乘以仿射矩阵即可得到转换后的新点。

在44矩阵中,即OpenGL中的数组,前4个元素是矩阵的第一列,接下来的4个元素是矩阵的第二列,以此类推。在将a点从当前坐标系转换到目标坐标系的场景中,44矩阵也可以理解为三维坐标系的三轴单位向量及其原点,在目标坐标系中的坐标。例如下图中的仿射矩阵,暂时忽略最后一行。前三列表示x、y、z轴单位向量在目标坐标系中的坐标,最后一列表示当前坐标系在目标坐标系中的原点。地点。左上角的3*3矩阵可以实现物体的旋转和缩放,最后一列可以实现物体的平移。

在坐标系CooperativeSystemO中,用一个44矩阵Matrix来表示其与坐标系CooperativeSystemN的关系。 4 维向量VecO 表示CooperativeSystemO 中的任何顶点。 Matrix与VecO相乘,得到新的4维向量VecN,即顶点。顶点对应于坐标系CooperativeSystemN中的位置。

当顶点通过多个矩阵变换到新的坐标空间时,也可以先将每次坐标变换的矩阵相乘,直接得到从原始坐标系到最终坐标系的仿射矩阵。即A*(B*v)=(A*B)*v,但是这里需要注意乘法的顺序,因为矩阵的乘法只遵守乘法结合律,不遵守交换律法律。

如下面三图所示,立方体首先绕其z轴逆时针旋转一定角度,然后沿其x轴平移一定距离。在接下来的三张图片中,应用相反的变换过程,可以清楚地看到我们得到了完全不同的图像。

3 坐标空间

OpenGL等图形渲染语言要求对象从三维空间经过一系列仿射变换和投影,最终获得二维可显示的图形。在了解OpenGL 中如何执行这些任务之前,我们需要首先了解OpenGL 中的各种坐标系。在OpenGL中,使用基于欧几里德几何的模型空间、世界空间、相机空间和标准设备空间,以及基于投影几何的投影空间。

我们可以想象,场景在我们眼中呈现的图像取决于场景中各个模型的相对位置以及我们观察它的姿势。同样,OpenGL中图形渲染的第一步就是确定这些模型的相对位置。 OpenGL中的每个模型都有自己的坐标系(模型坐标系)来确定每个模型的形状,并定义了一个世界坐标系来确定模型的相对位置。 Position还定义了一个相机坐标系(视点坐标系)来确定我们的观察位置,这样将每个顶点的坐标转换到相机坐标系就可以确定其在我们视野中的位置。然后通过下面描述的投影变换在裁剪空间中裁剪掉不可见的部分,并将场景渲染到屏幕上。

3.1 模型空间

模型空间用于描述物体空间信息并确定模型本身的形状。通常也称为对象坐标空间(Object Space)。通常选择物体的重心或特定点来建立三维空间坐标系,并使用一系列顶点来描述三维模型。坐标原点通常不在模型之外。在场景中,每个模型都有自己的模型空间,如下所示。

3.2 世界空间

世界空间(World Space)以全局坐标原点为中心建立。在OpenGL中,全局坐标原点是固定的。之前的模型空间都是在世界空间中以新原点建立的3D坐标系。所有模型都将放置在世界空间中。一些光照和物理计算是参考世界空间或相机空间来执行的。世界空间图如上。

3.3 视图空间

View Space是与观察者相关的空间,有时也称为Camera或Eye Space。与模型空间类似,视图空间也是基于世界坐标系建立的。其正x 和y 方向代表相机的右侧和顶部,其负z 轴方向代表相机的镜头方向。

3.4 剪切空间

剪辑空间是一个截锥体空间,由负责投影的矩形近平面和负责控制的矩形远平面组成。两个平面之间的区域是裁剪空间。位于裁剪空间中的每个顶点包含4个分量,称为4维齐次坐标,其第四个分量与顶点到相机的距离成正比。进行裁剪时,顶点的四个分量将除以第四个分量w。当w不为1时,其余三个分量将被放大或缩小。这种远近效果通过透视和投影来体现。影响。

这里需要注意的是,它与之前的模型空间、世界空间、相机空间不同,它们都是基于笛卡尔右手坐标系的。裁剪空间不是基于欧几里得几何的坐标系。它是基于投影几何的坐标系。与平行直线不相交的欧几里得坐标系不同,在投影几何中,平行直线的投影在无穷远处相交。

3.5 标准设备空间

为了适应不同的设备,OpenGL使用了标准设备空间(Normalized Device Space)。 x轴正方向为水平向右,y轴正方向为垂直向上,z轴正方向为垂直向内。它们的取值范围都是[-1, 1]。每个顶点的真实像素坐标只有在实际显示在屏幕上时才会被计算。经过剪切步骤的顶点将转换为标准设备空间。标准设备坐标空间示意图如上。

另外,这里要注意的是,它与之前的模型空间、世界空间、相机空间不同,它们都是基于笛卡尔右手坐标系的。标准设备空间是基于笛卡尔左手直角坐标系建立的。

4 仿射变换

在数学中,绝对值不能用来描述物体的位置,只能描述其相对于参考系的位置。同样,模型在空间上的变换也是相对位置的变化。描述模型变换的描述方法主要有三种:矩阵形式、欧拉角和四元数。 OpenGL中最常用的矩阵是矩阵,也可以使用四个元素。但欧拉角的表示方法存在死锁问题,因此很少使用。

4.1 矩阵形式

OpenGL中模型的平移、选择和缩放操作可以通过仿射矩阵与模型的所有顶点向量相乘来获得。 iOS和MacOS中的系统库GLKit提供了一系列矩阵初始化和计算接口。其他平台上也有开源的C++数学库可用,但深入理解其原理对于学习3D图形变换非常有帮助。

4.1.1 单位矩阵

单位矩阵(Identity Matrix)的行数和列数相等。除右对角线元素为1 外,其余元素均为0。将单位矩阵与任何模型顶点相乘不会改变其位置。在GLKit框架中,通过常量GLKMatrix4Identity可以得到一个44的单位矩阵。单位矩阵和向量的乘法计算如下所示。

4.1.2 平移矩阵

前面提到过,44仿射矩阵的最后一列表示当前坐标系原点在目标坐标系中的坐标。在OpenGL中平移模型可以看作是模型自身坐标系的平移,即模型从旧坐标系转换到新坐标系的过程。将平移矩阵应用于模型的结果如下所示。

在OpenGL中,通常使用齐次坐标w分量(第四分量)为1的四维向量来表示空间中的点。当其w分量不为1.0时,其平移效果会根据变换矩阵产生缩放效果。另外,使用w分量为0的三维向量或四维向量来表示方向向量。通过平移仿射矩阵处理顶点的计算公式如下。在iOS和MacOS中,可以通过函数GLKMatrix4MakeTranslation(float tx, float ty, float tz)获得平移矩阵。

4.1.3 旋转矩阵

绕坐标轴旋转的旋转矩阵对于旋转矩阵,首先考虑模型绕x 轴的简单旋转。旋转后模型的x轴坐标不会改变。如上所述,四阶仿射矩阵的前三列表示当前坐标系的轴的三个基向量在目标坐标系中的表示。如下图所示,虚线为当前坐标系,实线为目标坐标系。将当前坐标系绕X轴正方向旋转90度。当前坐标系的基向量在目标坐标系中的坐标如下。

根据上图,这个仿射变换的坐标系可以直接写成如下。在GLKit库中,可以使用函数GLKMatrix4MakeXRotation(float radians)直接获取旋转矩阵。

同理,绕y轴和z轴旋转的矩阵可以导出如下。在GLKit中,可以使用函数GLKMatrix4MakeYRotation(float radians)和GLKMatrix4MakeZRotation(float radians)来直接获取这些旋转矩阵。

当模型以物体坐标系为参考绕x、y、z轴旋转时,也就实现了绕任意经过原点的向量的旋转。 GLKit的内部代码会根据三个轴上的旋转角度构造一个类似的模型。仿射矩阵如下所示。在GLKit中,可以通过函数GLKMatrix4MakeRotation(float radians, float x, float y, float z)获得相应的矩阵。

注意,以世界坐标系为参考进行仿射变换时,最后一个变换需要放在整个矩阵乘法公式的最右边,这样模型才能多次从初始空间变换到最终空间。另外,如果以物体坐标系为参考,这个仿射变换就相当于以世界坐标系为参考的逆变换,即需要将第一个仿射变换矩阵放在方程的右边。

绕任意轴旋转的旋转矩阵如下图,向量v在空间中绕单位向量n旋转-,得到向量v‘。

首先得到下面的公式。

根据前四个公式,可以推导出以下公式。

可以计算出,对于基向量p=[1, 0, 0],旋转向量如下

可以计算出,对于基向量q=[0, 1, 0],旋转向量如下

可以计算出,对于基向量r=[0, 0, 1],旋转向量如下

如前所述,仿射矩阵的前三列是三个单位基向量的方向,因此绕任意轴旋转的仿射矩阵可以写成如下形式。使用GLKit中的函数GLKMatrix4MakeRotation(float radians, float x, float y, float z)获取对应的矩阵。该函数在内部标准化x-y-z。

4.1.4 缩放矩阵和镜像矩阵

沿坐标轴缩放矩阵和镜像矩阵沿坐标轴缩放矩阵(缩放矩阵)相对简单。只需将缩放后的新基向量直接组合成仿射矩阵即可。但需要注意的是,x-y-z 轴上不等比例缩放会导致模型变形。可以使用函数GLKMatrix4MakeScale(float sx, float sy, float sz) 获得。当缩放因子为-1时,得到镜像变化的仿射矩阵。缩放矩阵表示如下。需要注意的是,这里的基向量仍然是其自身坐标系中的单位向量。

沿任意方向缩放矩阵空间中的向量v沿单位向量n的方向以k为缩放因子进行缩放,如下图所示。请注意,使用n 的倒数会得到相同的结果。

首先我们可以推导出以下4个公式。

我们可以根据上面的公式来计算。

那么对于基向量p=[1, 0, 0],可以表示为

那么对于基向量q=[0, 1, 0],可以表示为

那么对于基向量r=[0, 0, 1],可以表示为

上一节提到,用于物体变换的仿射矩阵可以根据当前坐标系的基向量在目标坐标系中的坐标来构造,因此可以如下得到仿射矩阵。另外,如果这里设置缩放因子k=-1,则得到绕任意轴镜像的仿射矩阵。

4.1.5 连接仿射矩阵

通常需要从模型到目标多坐标进行多次仿射变换。这时候我们只需要将各个仿射变换的矩阵相乘即可。我们只需要记住在进行仿射变换时使用世界坐标系作为参考即可。最后一个变换需要放在整个矩阵乘法公式的右侧。以物体坐标系为参考,需要将第一个仿射变换矩阵放在方程的右侧。

4.1.6 矩阵蠕变

使用矩阵时,需要注意矩阵蠕变的现象,即输入错误的数据,或者浮点运算引起的累积误差使矩阵不再正交。这时就需要进行矩阵正交化,消除误差,重新获得一个最接近原矩阵的正交矩阵。

正交矩阵当矩阵M与其转置矩阵MT的乘积等于单位矩阵时,矩阵M称为正交矩阵。也就是说,M 的转置等于M 的逆。

在空间几何中,假设M是一个33矩阵,则用r1、r2、r3分别表示矩阵的第1到3列,其中r1、r2、r3分别表示空间中的3个基向量。只要每个向量都是单位向量,并且任意两个向量同时垂直,那么M就可以称为正交矩阵。 OpenGL中所有仿射矩阵左上角的33个子矩阵都满足上述两个条件,因此都是正交矩阵。

矩阵正交化施密特正交化的思想是逐列处理矩阵。第一列不进行变换,并且从所有先前列表示的向量的方向上的分量中减去每个后续列表示的向量。对于33阶矩阵M,r1、r2、r3分别代表其1到3列。正交化计算过程如下。

此时得到的新的基向量已经相互垂直,但不是单位向量。这时候就需要对它们分别进行标准化。然而,施密特正交化是有偏差的,因为r1 不能总是被校准。一种改进的方法是旋转一个小因子k,每次只减去k倍的投影,最后通过多次迭代得到正交矩阵。计算过程如下。

4.1.7 矩阵的优点

可以立即进行向量旋转:矩阵形式和四元数可以直接对向量进行运算,得到旋转后的向量坐标,但欧拉角不能。矩阵形式被图形API使用:您可以使用欧拉角和四元数在程序中保存方向,但OpenGL在内部执行旋转时必须以矩阵形式提供它。多个角位移连接:变换坐标系时,可以先合成变换矩阵,最后变换坐标。矩阵的逆:使用逆矩阵可以撤销原来的操作。由于旋转矩阵是正交的,因此其逆矩阵等于其转置矩阵。

4.1.8 矩阵的缺点

矩阵占用了更多的内存:为了保存方向,矩阵需要使用9 个数字,欧拉角仅使用3 个数字,四元数仅使用4 个数字。难于理解:矩阵无法直观地表示对象的方向。矩阵可能是病态的:基体中发生基体蠕变。矩阵无法插值:给定两个矩阵,它们之间不能执行曲面线性插值。

4.2 欧拉角形式

欧拉角的基本思想是,3D物体的旋转被分解为围绕三个相互垂直的轴的三个旋转的序列。这组描述三个角度和旋转信息的数字称为欧拉角。

欧拉角不指定旋转顺序和旋转轴。它们只要求两个连续的旋转轴必须垂直,因此旋转轴的组合有很多种。另外,欧拉角还分为静态定义和动态定义。静态定义是指旋转轴为世界坐标系,动态定义是指旋转轴为模型坐标系。静态定义不会造成万向节死锁(universal joint deadlock)。 (稍后解释),动态定义就会。这是通过经典力学中使用的动态Z-X-Z 范数以图形方式说明的。

上图中,蓝色部分xyz为世界坐标系,红色XYZ为模型坐标系,N为XYO平面与xyO平面的交线。那么模型变换过程可以描述为,以模型坐标系为参考,先绕Z轴旋转度,然后绕X轴旋转度,最后绕Z轴旋转度。

然而,对于3D图形旋转,通常采用Y-X-Z,即先进行Yaw(偏航),然后进行Pitch(俯仰),最后依次进行Roll(滚动)。如下图所示。

4.2.1 万向节死锁

欧拉角有一个致命且无法避免的缺点,那就是万向节死锁。即当第一旋转的轴和第三旋转的轴相对于世界坐标系重合时,欧拉角将失去一个自由度,第一旋转和第三旋转都绕同一轴旋转相对于世界坐标系。上图中,如果第二个Pitch的角度为90,第一个Yaw的角度和第三个Roll的角度将产生滚筒旋转。在处理这个问题时,通常当第二次旋转为90时,第一次强制旋转为0,这样所有的Roll效果都是通过第三次旋转来实现的。

之所以被称为万向节死锁,是因为在导航的早期,需要一个类似于下图所示的陀螺仪来确定当前的方向。当船体以90度角倾覆时,中间点的两个圆环处于同一垂直平面上。当此时再次发生滚动运动时,陀螺仪就失去了调节功能,所以这种情况称为万向节死锁。

4.2.2 欧拉角的优点

易于使用:欧拉角以图形方式表示模型的方向。使用内存最少:欧拉角只需要三个数字即可保存位置。任意三个数都是合法的:任意三个角度都能找到对应的方向。

4.2.3 欧拉角的缺点

别名问题:同一方向上的欧拉角可以有无限多种组合。造成别名问题的原因有以下三个。首先,将角度添加360 后,方向不会改变,但值会改变。

其次,三个角度之间存在强耦合,这使得(Yaw 0,Pitch 135,Roll 0)和(Yaw 180,Pitch 45,Roll 180)代表相同的方向。

为了保证任意欧拉角都有唯一的表示,通常对偏航、翻转和滚转的角度进行限制,使得Yaw和Roll在180到-180之间,Pitch在90到-90之间。

第三,由于万向节的死锁,(Yaw , Pitch 90, Roll )和(Yaw , Pitch 90, Roll )的欧拉角组合,只要-等于eta -,这两个欧拉角代表相同的方向。为了解决这个问题,规定只要俯仰为90,横滚始终等于0。

插值问题:造成插值困难的因素有3个。首先,例如,10和300之间的插值将围绕最佳弧进行插值,并且不是最短路径。这个问题可以通过限制欧拉角来解决。

其次,使用限制欧拉角后,170和-170之间的插值仍然会绕着最优弧走。解决方案是将其角度差映射到-180和180之间。公式为wrap()= - 360((+180)/360)。括号内的除法产生一个整数。

第三,当万向节死锁时,两个欧拉角之间无法进行插值,无法解决问题。球面插值只能通过使用四元数表示方位角来实现。

4.3 四元数

四元数源自复数。复数对(a, b) 定义复数P(a+bi),其共轭复数定义为P’(a-bi)。复数的模定义为||P||=平方(P*P"), ||P||=平方(a^2 + b^2)。以复数的实部为二维笛卡尔坐标系的横轴,虚部为其纵轴,构造一个复平面,任意复数都可以表示为平面上的点。此时引入了一个新的复数q,平面上的旋转可以通过复数乘法来表达。

爱尔兰数学家William Hamilton 一直致力于将复数从2D 扩展到3D。最后,他扩展了复数系,定义了复数P(w,xi,yj,zk)。其中,i、j、k之间的关系如下。该复数可以用四元数M[w,(x,y,z)]表示。与复数如何旋转2D 向量类似,四元数可以旋转3D 向量。

四元数包含标量分量和3D矢量分量,并具有以下两种表示形式。

4.3.1 四元数和轴-角对

3D 中的任何角位移都可以表示为绕单个轴的旋转。这种描述方法称为轴角描述方法。绕轴n旋转度的角位移可以由轴-角度对(n,)表示。四元数可以解释为角位移的轴角对。即,上述轴-角度对可以由以下四元数表示。

4.3.2 负四元数

对四元数取反就是对它的每个分量取反。在空间几何中,四元数M和-M表示相同的角位移。

4.3.3 单位四元数

数学单位四元数为M[1, 0],任何四元数乘以单位四元数都等于其自身。在空间几何中,还有一个单位四元数N[-1, 0],表示不发生角位移。

4.3.4 四元素运算

四元数的模对于任何四元数,模数公式如下。

四元数的共轭和逆四元数的共轭表示为q*,可以通过对四元数的向量部分求反得到。四元数的倒数表示为q-1。 q-1=q 的共轭除以q 的模。所以对于一个单位四元数,它的共轭和逆四元数是相等的。

四元数的对数、指数对于以下四元数

其对数公式定义如下

指数以相反的方式定义,假设有一个四元数如下

那么它的指数公式表达如下

四元数幂

算当四元数q的指数从0变化为1时,其值从 [1, 0] 变化为q。这里需注意当q代表的角位移角度为30°时,q2代表60°,q4代表的是240°,(q4)1/2表示的是120°,可以看出(q4)1/2不等于q2,因为数学上关于指数运算的代数公式对于四元数的求幂运算都不适用。四元数和标量乘法运算四元数和标量的乘法运算较为简单,直接将标量和四元素的各个分量相乘即可。四元数间的叉乘四元数的叉乘公式如下,需注意的是四元数的乘法满足结合律,不满足交换律。另外由乘法公式可以推导四元数乘积的模等于模的乘积。另外如未写两个四元数乘法的符号,默认都是叉乘。四元数的点乘四元数的点乘运算方式类似于向量的点乘,其公式如下。其几何意义可以表示为四元数a和b的点乘绝对值越大,它们代表的角位移越相似。四元数的差四元数的差被定义为一个方位到另一个方位的角位移,给定方位a和b,从a经过角位移d变化到b可用公式表示为ad = b。此时可计算出d = a-1b
4.3.5 四元数旋转空间向量
对于空间任意向量P,在用四元素计算将其绕单位向量n旋转θ度得到向量p‘时,先将其扩充到四维向量p = [0, P],再引入角轴对四元数q [cos(θ/2), nsin(θ/2)]。其计算公式为p’ = qpq-1,可以通过乘法展开,并将p"和使用旋转矩阵方式得到的结果比较从而证明该等式成立。实际上,该方法的时间成本和将四元数转换为旋转矩阵后再旋转向量的时间成本相当。 四元数的乘法可以连接多个角位移,当p分别经过轴角对a和b旋转,其旋转公式表示如下。需要注意这里的四元数乘法是叉乘。
4.3.6 四元数的插值
Slerp插值欧拉角和一般线性插值都不能用于空间球面插值,四元数的Slerp(Spherical Linear Interpolation)可以用于空间球面平滑插值。两个标量之间的一般线性插值可以如下定义。d = a1- a0; at= a0+ td同样的对于四元数的插值也可以定义如下。lerp(a0, a1, t) = a0+ td首先计算两个值的差:上文讲过计算四元数q0到q1的差可以由角位移d = q0-1q1得出。 然后计算差的一部分:四元数的幂运算可以将四元数等分,dt = dt。 然后在初值上加上偏移量:通过四元数的乘法可以组合角位移。 最后,得出四元数的插值公式为。slerp(q0, q1, t) = q0 (q0-1q1)t在实际应用中,可以通过空间几何推导出四元数插值一般公式。对于向量v0,旋转w可以得到向量v1,对于任意0到1之间的时间点t,其旋转后的向量表示为vt。 首先可以得到等式:vt= k0v0+ k1v1根据上图由空间几何可以推导出k1= sin(tw) / sin(w)再推导出k0= sin((1-t)w) / sin(w)最后可以推导出vt= (sin((1-t)w) / sin(w)) v0+ (sin(tw) / sin(w)) v1将该公式推导至四元数,因此四元数的插值一般公式可以表示为slerp(q0, q1, t) = (sin((1-t)w) / sin(w)) q0+ (sin(tw) / sin(w)) q1这里需要注意,当q0和q1点乘为负时,其角位移方向为优弧表示的方向,因此需要对其中一个取负,另外当q0和q1非常接近时sinw的值会引起不必要的计算误差,此时需使用四元数插值的理论公式。Squad插值Slerp提供了在两个方位之间插值,Squad允许在一个方位序列中进行平滑插值,方位序列由一组四元数控制点构成。 此外,还需要引入辅助四元数序列si,将它作为临时控制点,为了为每个控制点都扩展一个对应的临时控制点,另外控制点前后分别增加虚拟控制点q0= q1和qn+1= qn。 引入插值变量h,当h从0变化到1时,squad描述了qi到qi+1之间的曲线,整条插值曲线分别用如下公式获得。此处并未深入讨论Squad的细节,如需了解,参考书籍Quaternions, Interpolation and Animation
4.3.7 四元数的优点
平滑插值:Slerp和Squad提供了方位间的平滑插值。快速连接和角位移求逆:四元数叉乘能够将角位移序列转换为单个角位移,比矩阵连接更快。单位四元数的逆可以通过共轭四元数得到,相较于正交矩阵通过转置求逆更快。可以和矩阵快速互相转换内存占用较小:四元数存储一个方位只需要使用四个数。
4.3.8 四元数的缺点
四元数可能不合法:坏数据的输入或者浮点数的累积误差都可能引起四元数不合法,可用四元数标准化解决这个问题,即将四元数除以它的模,确保其为单位大小。四元数对于空间方位描述有着不可替代的作用,在GLKit中可以使用GLKQuaternion相关方法对四元数进行运算。

4.4 方位表达方式之间的互相转换

前文已经说过,OpenGL的底层是通过矩阵变化来旋转物体的,因此无论我们用的是哪一种形式表达刚体的方位,最终都需要转化为矩阵形式。
4.4.1 从欧拉角到仿射旋转矩阵
欧拉角转化到矩阵比较简单,上文旋转矩阵中已经分别介绍了绕各个轴单独的旋转矩阵,只需按规定欧拉角的旋转顺序将各个矩阵相乘即可得到,如x-y-z旋转顺序动态欧拉角对应的模型坐标系向世界坐标系转化的仿射旋转矩阵表示如下。由于是以物体坐标系为参考,因此第一次的绕x轴变换在等式最右侧。从世界坐标系向模型坐标系转换的仿射矩阵使用相反的乘法,即使用下述矩阵的转置矩阵。
4.4.2 从仿射旋转矩阵到欧拉角
根据上一小结中的旋转矩阵,可以直接使用反三角函数求出三个欧拉角,得出Ψ = arcTan(-m12/ m11),θ = arcSin(m13),Φ = arcTan(-m23/ m33)。当θ = ±90°时,上述4个分量都为0,此时计算出θ = arcSin(m13),再令Ψ = 0,在通过矩阵计算出Φ的值。
4.4.3 从四元数到矩阵
前文已经得到绕任意轴旋转的矩阵表达式如下。 表示绕任意轴旋转的四元数q = [w, x, y, z]可以表示如下。 此时通过数学上的一些技巧可以将矩阵中各个元素用四元数分量表示出来。对于对角线上的元素如m11,可以通过下面的推导公式求出: 进一步求解如下 对于非对角线的元素如m12,可以通过以下推导获得。 因此最后可以写出用四元数转换为的旋转仿射矩阵表示如下
4.4.4 从矩阵到四元数
根据上述公式,各个元素之间代数运算可得到下述关系 首先解平方根确定一个分量w,再根据如下等式计算剩余分量,这样会有两个不同的解,但是它们代表了相同的角位移。
4.4.5 从欧拉角到四元数
将绕x-y-z轴旋转p、h、b度的四元数表示如下。 假定欧拉角为y-x-z的动态旋转顺序,则表示描述物体向世界坐标系转换的四元数q = BPH表示如下。
4.4.6 从四元数到欧拉角
以物体坐标系为参照,分别绕x-y-z轴旋转ϕ-θ-ψ度的四元数可以通过上面公式解方程求出p-h-b(即ϕ-θ-ψ)的值,但是通常利用矩阵转换到欧拉角到公式如下。 再使用如下四元数到矩阵的转换公式得出矩阵元素的值,最后计算出欧拉角。注意,不同旋转顺序其欧拉角有不同的解。

5 OpenGL中的空间变换

OpenGL在渲染3D模型时需要将其从模型坐标系转换到窗口坐标系,其转换过程如下。

5.1 模型坐标系向世界坐标系转换

一个向量经过多个连续的空间变换可以通过矩阵相乘合成一个变换,然后右乘列向量。但是这里需要注意的是,矩阵乘法的顺序非常重要,当基于模型坐标系做动态变化时,矩阵的乘法顺序和变化顺序相反,当基于世界坐标系做静态变化时,矩阵的乘法顺序和变化顺序相同。例如基于世界坐标系绕y轴旋转一个模型,然后再将其平移(4,10,-20)的代码应该如下表示。 GLKVector4 inputVector; GLKMatrix4 rotationMatrix = GLKMatrix4MakeRotation(M_PI_2, 0, 1, 0); GLKMatrix4 translationMatrix = GLKMatrix4MakeTranslation(4, 10, -20); GLKMatrix4 compositeMatrix = GLKMatrix4Multiply(rotationMatrix, translationMatrix); GLKVector4 outputVector = GLKMatrix4MultiplyVector4(compositeMatrix, inputVector);

5.2 模型坐标系向视图坐标系转换

在OpenGL中,模型首先被转换到世界坐标系中,再将其转换到视图坐标系中。将模型从模型空间转换到视图空间称为模型-视图转换(Model-View Transform),对应的矩阵成为模型视图矩阵(Model-View Matrix)。由模型-世界转换(Model-World Transform)以及世界-视图转换(World-View Transform)合成,可以通过世界视图矩阵(World-View Matrix)右乘模型世界矩阵(Model-World Matrix)计算得到。
5.2.1 观察矩阵
世界视图矩阵也可以称为观察矩阵(Lookat Matrix),通过定义相机位置,即观察者位置构建。在世界坐标系定义一个相机即观察点Eye,观察中心Center,相机向上参考向量up,它可以不严格和Y轴正方向重合,它们在有xyz构建的世界坐标系中表示如下。 则视图坐标系的基向量XYZ在世界坐标系中的表示可以由如下公式推导。 根据前面讲到的仿射矩阵的构建知识,从相机坐标系到世界坐标系的3️3转换矩阵R相机->世界可以由相机坐标系3个轴的基向量在世界坐标系总的坐标构建如下。 而由世界坐标系向相机坐标系的3️3转换矩阵R世界->相机等于R相机->世界的转置矩阵,可以表示如下。 最后我们需要使用的4️仿射矩阵可以定义如下。 由世界坐标系中相机的位置在相机坐标系中是原点,因此通过以下计算能够计算出abc的值,从而得到最终的由世界坐标系向相机坐标系转换的仿射矩阵T世界->相机。 在GLKit中可以调用如下函数构造该仿射矩阵。 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ, float centerX, float centerY, float centerZ, float upX, float upY, float upZ)

5.3 视图坐标系向标准设备坐标系转换

当模型顶点坐标被转换到视图坐标系后,OpenGL会进行投影变换操作,该操作后模型的坐标就会被映射到裁剪空间。投影变换(Projection Transformations)可以分为正交投影和透视投影两种,通常我们在项目中为了体现出模型近小远大的自然特征而采用后者。
5.3.1 齐次坐标系
在正式了解投影变换之前我们需要首先了解再投影变换中使用到的齐次坐标系。首先我们考虑一个描述“两条平行的直线没有交点”,这个描述在高中学习的基于欧式几何建立的笛卡尔坐标系下是正确的。但是几何分为欧式几何和非欧几何两个大类,其中非欧几何泛指非欧式几何。而欧式几何不同于其他几何的地方就是平行公理,即过直线外一点有且仅有一条平行线。 我们在投影变换中使用的齐次坐标系就是非欧式几何的一种,考虑实际生活中的一个场景,假如我们站在️上并望向远方,我们会发现平行的两根轨道会在无穷远处相交,这也是投影几何中的特点,即平行线相交于其直线方向的无穷远点。 那么我们在笛卡尔坐标系中如何定义无穷点了,一种可能的表示方式为P(∞, ∞, ∞)。这样表示有两个问题,第一符号∞没有实际意义,第二该坐标不能表示无穷远点的方向。为了解决这两个问题,我们对笛卡尔坐标进行扩展,新增第四维度来表示三维空间中的点。 在笛卡尔坐标系中,直线的参数函数可以表示如下。 令t=1/w,则上述函数可以转换为如下形式 则直线上的任意点P的笛卡尔坐标为(m/w, n/w, q/w),齐次坐标为(m, n, q, w),可以发现当w趋于0时,其笛卡尔坐标趋于无穷大,因此当使用齐次坐标点P(m, n, q, 0)表示在向量V(m, n, q)方向上的无穷远点。这种表示方法使用了有意义的符号,并同时描述了无穷远点的方向,从而弥补了笛卡尔坐标的劣势。由各个无穷远点组成的面称为无穷远面。

刚刚讲到在笛卡尔坐标系中两条平行线无交点,即下面的函数无解。 使用齐次坐标系改写函数如下。 则上述函数存解P(x, y, z, 0),其中xyz需要满足函数Ax+By+Cz=0。几何意义上就是在直线两端无穷远处它们相交。
5.3.2 正交投影
在正交投影(Orthographic Projection)中,模型将被垂直投影到屏幕上,即无论观察者距离物体的距离有多远,它投影后的大小都相同。正交投影通常用于二维图形和文字的处理。正交矩阵构建好的正交矩阵如下图所示 GLKit中使用如下函数构建正交矩阵 GLKMatrix4 GLKMatrix4MakeOrtho(float left, float right, float bottom, float top, float nearZ, float farZ)
5.3.3 透视投影
透视投影(Perspective Projection)的特点是对于同样尺寸的模型,其距离观察者的距离越远,它被投影到屏幕上的尺寸越小。透视矩阵(Perspective Matrices)透视矩阵又被称为平截椎体矩阵(Frustum Matrix),在OpenGL中通过指定其近矩形平面的四个方向距离中心的距离,以及4个剪切平面(及平截椎体除去近、远剪切面)的坐标可以直接构造如下透视矩阵。这里需要注意left、right、top、bottom是坐标,即有正负,而near、far是观察点到近投影面和远投影面的距离,都为正值。 GLKit中使用如下函数构建透视矩阵 GLKMatrix4MakeFrustum(float left, float right, float bottom, float top, float nearZ, float farZ)另外,还可以通过指定垂直方向上的可视角度(以pi为单位),指定水平和垂直宽度比,以及远近剪切面的距离直接构建对称的透视矩阵 GLKMatrix4 GLKMatrix4MakePerspective(float fovyRadians, float aspect, float nearZ, float farZ)

5.4 从投影坐标系转换到标准设备坐标系

经过投影变换,顶点的坐标就呗转换到投影空间中,前面讲过将齐次坐标系转换为笛卡尔坐标系的方法是讲其xyz分量都除以w分量即,P点点齐次坐标(x, y, z, w)转换得到的笛卡尔坐标为(x/w, y/w, z/w),这个操作称为透视除法。经过这个变换后模型的顶点坐标就被转换到了标准设备坐标系中。 标准设备坐标系以设备中心为原点,水平向右为x轴正方向,垂直向上为y轴正方向,垂直屏幕朝内为z轴正方向,它们的取值范围都为[-1, 1]。前面章节将图形渲染管道的时候还讲过,这一阶段OpenGL将会裁剪掉不在视野范围内的图元以提升图形渲染性能,即会裁剪掉坐标超出[-1, 1]的图元。

5.5 从标准设备坐标系转换到窗口坐标系

图像最终是需要显示在窗口上的,这意味着我们在渲染的最终阶段必须得到每个片段的窗口坐标,经过前面的几节我们已经讲模型的各个顶点从模型坐标系转换到标准设备坐标系之中了,接下来我们需要讲其转换到窗口坐标系中,如下图。 这个过程在OpenGL中称为视口变换(Viewport Transformation),其计算过程如下。 其中(xd, yd, zd)表示点P在标准设备坐标系中的坐标,(xw, yw, zw)表示该点在窗口坐标系中的坐标。OxOypxpy分别是在设置窗口位置时调用的函数glViewport(_ x: GLint, _ y: GLint, _ width: GLsizei, _ height: GLsizei)时设置的四个参数,n和f则是设置深度区间时调用函数glDepthRange(_ zNear: GLclampd, _ zFar: GLclampd)时设置的两个参数。

6 插值和曲线

6.1 插值

在OpenGL中常用到插值计算(Interpolation)来确定中间状态的值,如前文讲到的使用四元数的插值公式实现曲面插值。此外在绘制一个颜色过渡的三角形图元时,通常我们设置了三个顶点的颜色值,而在片段着色器中我们需要确定每个像素的颜色,此时就需要进行插值计算。插值计算分为线性插值计算和非线性插值计算。线性插值:对于因变量Y和自变量t的函数,t的取值范围为[0, 1],Y从A逐渐变换到B,可以表示为y=f(t),如果这个函数是线性变换的则称将这个计算过程称为线性插值计算。OpenGL在颜色混合等场景中会用到,着色器中可以使用内置函数vec4 mix(vec4 A, vec4 B, float t);计算。非线性插值:非线性插值和线性插值的区别在于插值函数y=f(t)是非线性的。OpenGL在片段着色器中得到的输入变量默认情况下都是图像渲染管线前端计算得到的顶点属性在投影平面非线性插值的结果,具体逻辑会在片段着色器章节详细讲解。

6.2 曲线

OpenGL中常用的曲线类型是贝塞尔曲线和样条曲线,通过这些光滑的曲线我们可以模拟出一些有趣的现象,如迎风招展的旗子。
6.2.1 贝塞尔曲线
贝塞尔曲线(Bezier Curve)由一个起点一个终点以及其中一系列控制点组成。由起点A,终点C,控制点B描绘出贝塞尔曲线如下图。 当t从0变化到1时,对于P点,其满足下列公式:D = A + t(B - A)E = B + t(C - B)P = D + t(E -D)可以得到:P = A + 2t(B - A) + t2(C - 2B + A)在着色器中可以使用多个线性插值函数绘制由1个控制点构成的2次贝塞尔曲线: vec4 quadratic_bezier(vec4 A, vec4 B, vec4 C, float t) { vec4 D = mix(A, B, t); vec4 E = mix(B, C, t); vec4 P = mix(D, E, t); return P; }由起点A,终点D,控制点BC描绘的贝塞尔曲线如下图。 当t从0变化到1时,对于P点,其满足下列公式:E = A + t(B - A)F = B + t(C - B)G = C + t(D - C)H = E + t(F - E)I = F + t(G - F )P = H + t(I - H)在着色器中可以使用多个线性插值函数绘制由两个控制点构成的3次贝塞尔曲线: vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t) { vec4 E = mix(A, B, t); //E=A+t(B-A) vec4 F = mix(B, C, t); //F=B+t(C-B) vec4 G = mix(C, D, t); //G=C+t(D-C) vec4 H = mix(E, F, t); //H=E+t(F-E) vec4 I = mix(F, G, t); //I=F+t(G-F) vec4 P = mix(H, I, t); //P=H+t(I-H) return P; }可以简写为: vec4 cubic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, float t) { vec4 E = mix(A, B, t); vec4 F = mix(B, C, t); vec4 G = mix(C, D, t); return quadratic_bezier(E, F, G, t); }更高阶的贝塞尔曲线都可以按照这样的方式构建,但是通常高于包含4个点的时曲线的描绘就不再使用贝塞尔曲线了,而使用样条曲线。含有5个点的贝塞尔曲线构建方式如下: vec4 quintic_bezier(vec4 A, vec4 B, vec4 C, vec4 D, vec4 E, float t) { vec4 F = mix(A, B, t); vec4 G = mix(B, C, t); vec4 H = mix(C, D, t); vec4 I = mix(D, E, t); return cubic_bezier(F, G, H, I, t); }
6.2.2 样条曲线
样条曲线(Splines)是由多个小的曲线(如贝塞尔曲线)组成,每个曲线称为片段(Segment),片段两头的控制点称为焊点(Welds),片段内部的点称为节点(Knots),焊点一定被相邻片段共享,有时节点也会被相邻片段共享。 一个由三个贝塞尔曲线组成的B类立方样条曲线(Cubic B-spline)(ABCD)-(DEFG)-(GHIJ)如下图所示。注意这里CDE共线,FGH共线,其中D、G都为各自线段的中点。 当t从0分别变化到1、2、3时,P分别变化到D、G和J。其样条曲线生成函数可以如下构建。使用改方程时,匀速变化的t会得到匀速变化的p,反之p将不会匀速插值。 vec4 cubic_bspline_10(vec4 CP[10], float t) { if (t<= 0.0) { return CP[0]; } if (t >= 1.0) { return CP[9]; } float f = t * 3.0; int i = int(floor(f)); // 取整数 float s = fract(f); // 取小数 vec4 A = CP[i * 3]; vec4 B = CP[i * 3 + 1]; vec4 C = CP[i * 3 + 2]; vec4 D = CP[i * 3 + 3]; return cubic_bezier(A, B, C, D, s);

用户评论

龙吟凤

OpenGL 的基本概念和原理

    有7位网友表示赞同!

江山策

OpenGL 应用场景、使用方法和技巧

    有14位网友表示赞同!

剑已封鞘

OpenGL 在游戏开发、动画制作等领域的应用

    有14位网友表示赞同!

←极§速

OpenGL 与其他图形库或 API 的比较与区别

    有7位网友表示赞同!

人心叵测i

OpenGL 学习资源及社区建设

    有9位网友表示赞同!

良人凉人

最近 OpenGL 新功能的介绍

    有17位网友表示赞同!

莫飞霜

OpenGL 发展历史和趋势

    有18位网友表示赞同!

非想

OpenGL 编程示例和代码分析

    有9位网友表示赞同!

北朽暖栀

如何使用 OpenGL 创建简单的3D图形

    有10位网友表示赞同!

如你所愿

高效利用 OpenGL 实现实时渲染

    有17位网友表示赞同!

浮光浅夏ζ

OpenGL 中常见问题及解决方法

    有20位网友表示赞同!

我的黑色迷你裙

OpenGL 的未来发展方向

    有9位网友表示赞同!

安陌醉生

比较主流的 OpenGL 编程语言

    有17位网友表示赞同!

浅嫣婉语

OpenGL 相关的开源项目和工具

    有17位网友表示赞同!

单身i

如何学习和掌握 OpenGL 技能

    有5位网友表示赞同!

一笑抵千言

OpenGL 在不同平台上的兼容性

    有10位网友表示赞同!

殃樾晨

OpenGL 和虚拟现实技术的结合

    有14位网友表示赞同!

有阳光还感觉冷

OpenGL 对于图形设计师的重要性

    有7位网友表示赞同!

哭着哭着就萌了°

OpenGL 的优点和缺点分析

    有12位网友表示赞同!

【深度探索OpenGL图形编程艺术】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活

上一篇:内心独白:探寻未被诠释的世界 下一篇:面对邻居孩子成绩炫耀,如何帮助孩子保持自信?有效应对策略分享