自学OpenGL(九)-数学基础
前言
计算机图形学中大量使用了数学原理,尤其是矩阵和矩阵代数。虽然我们倾向于认为3D图像学编程是现代的技术领域之一,但是它用到的很多技术实际上可以追溯到上百年前。其中一些甚至是文艺复兴时期的伟大哲学家们就已经理解并记录的。
3D 图形学中几乎每个方面、每种效果--移动、缩放、透视纹理、光照、阴影等等,都是在很大程度是以数学方式实现。
这里,我们假定大家都具备基础的矩阵运算知识。如果在阅读过程中发现不理解的地方,自行去学习相关知识,个人推荐去哔哩哔哩搜索宋浩老师,笔者在大学没有开数学课,所有数学理论知识,包括线性代数、微积分都是在宋浩老师那里学的。看视频的同时再配合使用微信读书去搜索相关教材,把习题做一遍,效果翻倍。
3D坐标
3D 中间通常用3个坐标轴X、Y和Z来表示。这3个轴可以以两种方式来布置:左手或者右手
知道图形编程化境使用的坐标系是很重要的。例如,OpenGL中的坐标系大体是右手坐标系,而其他3D系统的坐标系则有可能是左手的。在本博客中,如果没有特别说明,我们默认是右手坐标系。
点
3D中间中的点可以通过使用形如(2,8,-3)的符号,列出X、Y、Z的值来表示。不过如果用齐次坐标来表示点会更有用。n维空间中点的坐标用n+1个分量来表示。三维空间中的齐次坐标有4个值,前三个值表示X、Y、Z、,第四个W总是非零值 ,通常情况为1,因此,我们会将之前的点表示为(2,8,-3,1)。正如我们稍后将要看到的,齐次坐标会是我们的图形学计算更加的高效。
用来存储齐次3D坐标的GLSL数据类型是vec4。GLM库包含适用在C++/OpenGL应用中创建3元和4元的点,分别叫做vec3和vec4.
关于齐次坐标不太熟悉的同学可以看这里:https://zhuanlan.zhihu.com/p/258437902
矩阵
矩阵是矩形值阵列,它的元素通常使用下表访问。第一个下标行号,第二个下表为列号,下标从0开始。我们在3D徒刑计算中要用到的矩阵大多数大小为4X4
A00 A01 A02 A03
A10 A11 A12 A13
A20 A21 A22 A23
A30 A31 A32 A33
GLSL 语言中的mat4数据类型用来储存4X4的矩阵。同样,GLM中有mat4类泳衣是梨花并存储4X4矩阵。
单位矩阵用一条对角线的值为1,其余值全为0:
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
转置
GLM 库和GLSL库都有转置函数,分别是glm::transpose(mat4)和 transpose(mat4)
加法
加法非常简单,就是逐分量相加:
点与矩阵相乘
点左乘一个矩阵得到一个新的点
矩阵相乘
这里我们拿一个4X4的矩阵相乘举个例子:
矩阵相乘也称为矩阵合并,这块知识完全遵循线性代数里面的矩阵运算规则,如果线性代数这方面知识的同学,建议先去学习一下线性代数。
变换矩阵
在图形学中,矩阵通常用来进行物体的变换,所以我们一般称之为变换矩阵。变换矩阵的重要特征之一就是他们都是4X4矩阵。这是因为我们决定使用齐次坐标系,否则,变换举证可能会有不同的维度并且无法相乘。正如我们所见,确保变换矩阵大小想相同并不只是为了方便,同时让他们可以任意组合,进行预先计算变换矩阵以提高性能。
平移矩阵
平移矩阵用于将物体从一位置到另一个位置,矩阵表示为:
GLM 中有一些函数适用于构建与点相乘的平移矩阵:
- glm::translate(x, y, z)构建的平移矩阵;
- mat4 X vec4
缩放矩阵
缩放矩阵用于改变物体的大小或者将点想远点反方向移动。因此,缩放物体涉及缩放它的点的集合,缩放矩阵变换表示为:
GLM 中有一些函数适用于构建与点相乘的缩放矩阵:
- glm::scale(x, y, z) 构建缩放矩阵;
- mat4 X vec4
旋转矩阵
旋转扫尾复杂一些,因为在3D空间中旋转需要指定旋转周和旋转的角度或弧度,任何旋转都可以表示为绕着X、Y、Z轴旋转的组合。围绕这三个轴的旋转角度被称为欧拉角。旋转变换有3种,见下图:
GLM 中也有一些用于构建旋转矩阵的函数。
- glm::rotate(mat4, θ, x,y, z)构建绕X、Y、Z旋转θ度的矩阵;
- mat4 X vec4;
实践中,当3D空间中旋转轴部穿过原点时,物体使用欧拉角旋转需要额外的步骤。一般有:平移旋转轴以使它经过原点;绕X、Y、Z轴旋转适当的欧拉角;复原步骤(1)中的平移。旋转变换中的三个旋转变换矩阵都有自己的特性,即反响旋转的矩阵等于其转置矩阵。
旋转矩阵时蒸饺矩阵,蒸饺矩阵的逆等于矩阵的转置,反响旋转其实就是再乘以旋转矩阵的逆
向量
向量表示大小和方向,他们没有特定位置。移动向量并不能改变它代表的含义。
在GLM和GLSL中有许多3D图形学中经常用到的向量操作。如加减法、归一化、点积、插积
加减法
A ± B=(u ± x, v ± y, w ± z)
1.glm: vec3 ± vec3
2.GLSL: vec3 ± vec3
归一化(长度为1)
A=A/|A|=A/sqrt(u2+v2+w2),其中|A| ≡向量A的长度
glm: normalize(vec3) 或normalize(vec4)
GLSL: normalize(vec3) 或normalize(vec4)
点积
A·B=ux+vy+wz
- glm: dot(vec3,vec3) 或dot(vec4,vec4)
- GLSL: dot(vec3,vec3) 或dot(vec4,vec4)
插积
A × B=(vz-wy, wx-uz, uy-vx)
- glm: cross(vec3,vec3)
- GLSL: cross(vec3,vec3)
点积的应用
点击最重要也最基本的应用时求解两向量夹角。设向量V和W,计算其夹角为θ。因此,如果V和W是单位向量(归一化向量),则有:
有趣的是,我们后面会看到通常用到的是cos(θ),而非θ,因此连个推到出的共识都很有用。
- 求向量的大小根号下V.V ;
- 求解两向量是否正交,若正交,则V.W=0;
- 求解两向量是否平行,若平行,则 V.W = |V||W|;
- 求解两向量是否平行,但方向相反,若满足,则 V.W = -|V||W|;
- 求解两向量夹角是否在-90~90度之间 V.W > 0 ;
- 求解点P到平面S的最小距离;
叉积的应用
两个向量叉积的一个重要特性是,他会生成一个新的向量,新的向量正交于前两个向量所定义的平面。任意两个不贡献的向量都定义了一个平面,例如考虑两个人一向量V和W。由于向量可以在不改变任意含义的情况下移动,因此,可以将他们移动到起点相交的位置,下图展示了V和W定义的平面,以及其插积所得到法向量。其所得法向量的方向遵循右手定则。即右手手指从V向W卷会是的大拇指指向法向量R。
通过叉积来获得法向量的能力对我们后面要学习的光照部分非常重要。为了确定光照效果,我们需要知道所渲染模型的外向法向量。图3.8中展示了一个例子,其中有一个6个点(顶点)构成的简单模型,使用叉积计算来获得其中一面的外向法向量。
局部空间和世界空间
局部空间
当建立物体的3D模型是,我们通常以最方便的定位方式描述模型。如果模型是个球形,那么我们很可能将球心定位于原点,并赋予它一个方便的半径,比如1。模型定义的空间叫做局部空间。OpenGL文档使用的术语是物体空间(object space)。
之后这个球形可能用于一个大模型的部分,如成为机器人的头部。这个机器人,当然,定义在他自己的局部空间。我们可以用下图的举证变换通过缩放、旋转、平移,将球形模型房子啊机器人模型的空间。通过这种方式,可以分成次地构建复杂模型,使用这种方式,通过设定物体在模型世界中的朝向和大小,将物体放在模拟这个世界的空间中,这个空间叫做世界空间。将对象定位及定向在世界空间的矩阵成为模型矩阵或M。
视觉空间和合成相机
到此为止,我们所接触的变换矩阵全都在3D空间中操作,但是我们最终需要将3D空间中的物体展示在2D显示器上。为达到这一个目标,我们需要找到一个有利点。正如我们在现实世界通过眼睛观察一样,我们也必须找到一点并确定观察方向作为我们观察虚拟世界的窗口。这个点叫做“视图“或”视觉“空间,或”合成相机“。
观察3D世界需要如下步骤:
- 将相机放入世界的某个位置;
- 调整相机的角度,通常需要一套自己的直角坐标系;
- 定义一个视体(view volume);
- 将视体内的对象投影到投影平面(projection plane)上。
OpenGL 有一个固定在原点并看向Z轴副方向的相机
为了应用OpenGL相机,我们需要做的是将它抹你移动到何时的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置,如下图
需要做的变换如下:
- 将Pw平移,其向量为负的期望相机位置;
- 将Pw旋转,其角度为负的期望相机旋转的欧拉角。我们可以构建一个单一变换举证已完成旋转和平移,这个矩阵叫做视图变换矩阵V。矩阵V通过合并矩阵T和R。
在本例中,从右向左,我们先平移世界空间中的点Pw,之后旋转
Pc=R(T*Pw)=(R*T)Pw;
将R*T用V来表示,即:
Pc=V*Pw;
通常,V举证与模型矩阵M合并成为一个模型-视图矩阵,即:
MV=V*M;
之后,点Pm在自己的模型空间通过如下一个步骤就可以直接转换至合成相机空间:
Pc=MV*Pm;
在复杂场景中,当我们需要对每个顶点,而非知识一个点做这个变换的时候,这种方法的好处就很明显了。通过预先计算MV,对于空间中每一个点的变换只需要我们进行一次矩阵乘法计算。之后我们将看到,我们可以将这个过程延伸到与计算机更多的合并矩阵,以大量减少每个顶点的计算量。
投影矩阵
当我们设置好相机之后,就可以学习投射矩阵了,我们需要学习两个重要的投射矩阵是:
- 透视投射;
- 正射投射;
透视投影矩阵
透视投影举证通过使用透视概念模仿我们看真实世界的方式,尝试让2D的图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。
我们通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果。这个矩阵叫做透视矩阵,通过定义四个参数来进行视体的构造。其中4个参数是众横比,视场、投射平面和近裁剪平面、远裁剪平面。只有在远近裁剪平面间的物体才会被渲染。近裁剪平面同时也是物体所投射到的平面,通常放在离眼睛或相机较近的位置。视场是可视空间的纵向角度。纵横比是远近裁剪平面的宽度比高度。通过这些元素所形成的形状叫做视锥,如下图:
透视矩阵用于将3D空间中的点变换至近裁剪平面上合适的位置,它的构建需要先计算q、A、B、C的值,之后用这些值来构建透视矩阵,如下图所示:
生成透视变换矩阵很容易,只需要将所描述的公式插入一个4X4矩阵。GLM库也包含了一个用于构建透视矩阵的函数glm::perspective()。
正射投影矩阵
在正射投影中,平行线仍然是平行的,即不使用透视。正射与透视相反,在视体中的物体不因距相机距离做任何调整,而直接进行投影。
正射投影是一种平行投影,其中所有的投射都与投影平面垂直。正射矩阵通过如下参数构建:
- 从相机到投影平面的距离Znear;
- 从相机到远平面的距离Zfar;
-
L、R、T、B的值,其中L、R分别是投射平面左右边界的X坐标,T和B分别是投射平面上下边界的Y坐标,如下图:
并非所有平线投影都是正射投影,其他投影暂时不讲。
平行投影与我们眼睛所见到的真实世界不同。但是他们在很多情况下都有用处,比如投射阴影、进行3D裁剪以及CAD中用在CAD中因为无论物体如何摆放,其尺寸都不变。
LookAt 矩阵
当想要吧相机放在某处并看像一个特定的位置时,就需要用到它,但是这个操作非常频繁,因此为它专门构建一个矩阵通常比较有用。
LookAt 变换由相机旋转决定。我们通过指定大致旋转朝向的向量。通常,可以通过一系列叉积获得相机旋转的正面、侧面以及上面。
我们可以将这个过程构建一个C++/OpenGL使用函数,通过指定相机位置、目标位置以及向上向量Y,构建一个LookAt矩阵的矩阵。由于GLM中已经有一个用来构建LookAt矩阵的函数glm::lookAt(),我们用它就可以了。