本章主要解决这个问题:
如何对物体进行位置变换?
想要操作物体的位置,我们就要使用数学工具对其位置进行计算。先来看看回顾一下需要用到的基本数学知识:
向量
在最初的定义中,向量就是用来表示方向的。向量包括大小和方向两个要素。你可以把向量想象成在藏宝图上的箭头指示:向左走10米,然后,往北走3米,再然后,往右走5米。这个左右南北就是方向,10米就是向量的大小。理论上,向量可以是任意维数的,不过我们不关心这个,我们关心的是我们最最常用的2到4维向量。2维向量表示平面上的方向,3维向量表示3D世界里的方向。
用一张直观的图来表示一下。
因为向量只有大小和方向两个要素,起点并不算在内,所以我们可以认为向量w和向量v是相同的。本文中向量用粗斜体字来表示,比如之前的向量w和向量v。为了显示的明显一点,我们用垂直行列式的方式来表示一个向量,例如:
因为向量含有位置的信息,不知从什么时候开始,大家都用向量来表示位置了。这种向量被称作位置向量,不过不用担心,我们很熟悉这种表示方式,就是把它看做是在三维空间中的一个坐标而已。
向量运算
标量
和一个标量进行计算是最简单的一种运算了,加减乘除都可以(除数不能是0哦)。计算方式就是用向量的每个分量和标量进行一次运算,得出的一个新向量就是计算的结果。
向量取反
向量取反操作直接把向量的方向变反了,对向量的大小并没有什么影响。运算方式非常简单,直接看图:
向量加减
向量相加操作就是把两个向量的相应分量相加,得到一个新的向量。
向量相减操作就是把两个向量的对应分量相减,得到一个新的向量。
长度
长度计算通过最普通的勾股定理就能算出来
向量乘法
普通的乘法(对应分量相乘)并没有什么实际意义,所以也没有必要去研究。不过,向量乘法两种特殊的乘法运算,点乘(w · v)和叉乘(w x v)。我们分别研究一下这两种乘法有啥作用:
点乘
两个向量的点乘结果是:两个向量长度的乘积再乘上夹角的cos值。像这样:
特别地,如果两个向量为单位向量(单位向量是指长度为1的向量),那么这个公式就退化成:
看到没,就只是两向量夹角的cos值,这个功能在进行光照计算的时候非常有用,我们计算光照的时候优势不需要知道它的大小,只需要知道其法向量和光照方向的夹角就好了。
从公式上来看,两个向量的点乘每个分量相乘的结果在求和。
叉乘
叉乘只在3D空间中有意义,因为叉乘的结果向量方向是与两个相乘的向量都垂直的。在2D空间里,你无法找到一个能与两条相交的直线都垂直的直线,但在3D空间中易如反掌!
如果你是在左手坐标系中,那么伸出左手,四指从v弯曲到k,大拇指的方向就是叉乘结果向量的方向。等等,好像不太对啊,图上的结果向量是朝外的!哈哈,这说明图上的坐标系是右手坐标系,这时候就要用右手做弯曲动作确定方向了。
不过,不管是左手坐标系还是右手坐标系,向量乘积的结果都是一样的!向量叉乘公式:
验证一下我们的公式:A = (1, 0, 0), B = (0, 1, 0) , A x B = (0 * 0 - 0 * 1, 0 * 0 - 1 * 0, 1 * 1 - 0 * 0) = (0, 0, 1)。公式正确!
矩阵
关于向量我们已经了解的差不多了,除了最后的向量叉乘有点难理解之外,其余的几乎都是小菜一碟。是时候来点挑战性了!欢迎各位勇士来到矩阵的世界!先说好,矩阵就不会像向量那样“温馨”了。
所谓矩阵,基本上就是一个矩形数组,这个数组中可能有数字、符号或者表达式。矩阵中的每一项被称为矩阵的一个元素。举一个2x3的矩阵例子来看:
矩阵可以通过索引获取其某个位置的元素,例如 :索引(i,j)表示第i行第j列的元素。这也就是为什么上面的矩阵被称为2 x 3的矩阵,因为它是2行3列。引用矩阵元素可能会和表示坐标上的点相混淆(笔者就经常弄混),一个(2,1)的位置表示x=2,y=1,而一个(2,1)的矩阵索引表示row = 2, column = 1,刚好和位置坐标相反。
光有矩阵并没啥意思,我们最看中地是它的运算方式。它也和向量一样有很多有趣的运算方法。
加法和减法
矩阵可以和一个简单的标量相加减,也可以和一个矩阵加减,运算的方式不太一样,我们分别来看!
1、和标量
矩阵可以和一个标量相加或相减。方式是用矩阵的每一个分量去加或者减这个标量:
2、和矩阵
当一个矩阵和另一个矩阵相加或者相减的时候,情况也很简单,只要将两个矩阵对应的分量相加或者相减就行了。
你可能会有疑问,如果两个矩阵的维数(所谓维数,就是矩阵的行数和列数)不同,那该怎么加减呢?没错,不同维数的矩阵的加减操作没有意义,所以在数学上,我们就禁止不同维数的矩阵进行加减操作!
乘法
和加减操作一样,矩阵乘法也有和标量、和矩阵之分,运算方式大不相同,我们仔细来看!
和标量
和标量的乘法非常简单,只需要把标量和矩阵的每个元素相乘,得到一个新的矩阵就行了。
和矩阵
和矩阵相乘就不是那么令人愉快了。在矩阵相乘之前,有两条规则我们要来看看,这是两个最基本的原则:
- 相乘的两个矩阵,第一个矩阵的列数必须要等于第二个矩阵的行数!
- 矩阵相乘不满足交换律,也就是说A · B != B · A !
满足这两个条件后,我们再来看两个矩阵是如何相乘的。
我们可以看到,矩阵的乘法是第一个矩阵的行,乘以第二个矩阵的列,对应元素相乘然后求和(这就是为什么有第一条原则的原因了!)。画个圈圈可以看得更清楚
提示:自己在纸上算一遍更好理解哦!
这里我们就给出了行列数相同的矩阵乘法示例,而根据规则,矩阵行列数可以不同,但也能进行运算。我们只稍微说说,两个可以相乘的矩阵(注意哦,前提是可以相乘!)的运算结果也是一个矩阵,这个结果矩阵的行数等于第一个矩阵,列数等于第二个矩阵。(想象一下用一个矩阵乘以一个向量,得到的结果也必定是一个向量。)
矩阵和向量相乘
严格来说,矩阵和向量相乘并不能单独作为一节来说,因为就像前面说的那样,把向量当成一个列数为1的矩阵,就可以根据矩阵运算规则算出来了。但是,矩阵与向量的运算太重要了,以至于它完全值得我们单独列出来对他大肆捯饬一番。
单位矩阵
所谓单位矩阵就是除了从左上角到右下角对角线上的元素都是1,其余元素都是0的矩阵。一个单位矩阵和一个向量相乘,结果还是那个向量,就像任何数乘以1都不会对其有啥改变。
你可能会想知道,既然单位矩阵不会改变向量的值,那还有个卵用啊?别急,虽然用处不大,但还是有点用滴,不然谁会发明这个东西啊。单位矩阵通常都是其他矩阵的“起点”,很多矩阵都是从它开始算出来的。另外,如果我们对线性代数研究地更深一点,就会发现,它对于提供理论证明,解决线性相等问题有很大的帮助。
当然这些都是题外话,光啃干货不舒服,扯皮用的。
比例变化
我们可以构造一个矩阵来对向量进行缩放,除了对各个坐标进行统一缩放,我们还能通过给不同的坐标设置不同缩放因子这种方法对各个坐标进行不统一的缩放。是不是听上去很神奇?我们来看看就知道了
如果S1,S2,S3不相同,那就是不统一缩放(改变方向),如果相同,那就是统一缩放(不改变方向)。注意,第四个缩放因子必须是1,因为在3D空间中,对w分量进行缩放的操作不知道会出啥问题!
平移
根据我们的已有知识,要想平移一个向量,只要在这个4x4的矩阵的最后一列放上我们需要平移的量就行了,当然最后一个必须还是1.像这样:
很简单吧!
旋转
旋转的内容有点复杂。首先我们要了解的是,如何定义一个旋转?有人可能就不明白了,旋转还要定义吗?直接往左或者往右转个90度不就完了吗?这个还真的得说两句,因为在数学世界中,角度有两种表示方法:角度或者弧度。我们熟悉的都是用角度来表示,一圈有360度。而在数学世界里,弧度也是非常常用的,一圈是2PI。为了便于理解,我们用角度来说明。
角度和弧度可以相互转换,具体的公式是:
角度= 弧度 * (180.0f / PI)
弧度= 角度 * (PI / 180.0f)
PI的精度最好高一点,以免出现误差,通常把它设置为:3.14159265359。
在3D世界中,当我们需要将一个向量进行旋转,我们就需要确定三样东西:
- 绕着什么旋转
- 往哪个方向旋转
- 旋转多少度
理论上,我们可以绕任意轴旋转(实际上也是一样_),不过在计算的时候,我们通常把绕任意轴旋转的操作分解成绕三条主轴旋转的操作。通过一些三角变换函数,计算出绕某一个主轴的变换结果,然后将这些操作结合起来,组成绕任意轴旋转的操作。下面直接给出绕3个轴旋转的变换公式:
首先是绕X轴旋转的公式:
然后是绕Y轴旋转的公式:
最后是绕Z轴旋转的公式:
这些公式不需要记,你可以非常快地在网上查到,或者把这篇文章收藏一下,直接就能看到。在这个互联网时代,笔者的主张是不需要记那么多的知识点,但是你必须要记住在哪能查到这些知识!明白了吗?知道在哪比单纯的记住更加重要!
言归正传,当我们把这些变换组合起来的时候,很快就会遇到一个问题,那就是万向锁(Gimbal lock)。
简单讲讲万向锁:
先不要被锁这个字给吓唬住了,出现万象锁现象并不是说你不能再旋转了,而是这种情况下,某些旋转不是按照我们想要的方式来。笔者看了许多文字描述的万向锁,但是都没搞明白,所以不打算用文字解释,直接推荐一个视频。仔细看万向锁的部分,就能明白了。
先来说一个解决方法,绕任意一个单位轴进行旋转,例如(0.662, 0.2, 0.722),不过记住一定要是单位向量。转换公式像这样(Rx, Ry, Rz是坐标值):
跟其他的转换矩阵相比,是不是顿时有种鹤立鸡群的感觉!What the f**k?幸好我们有网络这个东西,不用死记硬背实在是太幸福了。顺便一提,解决万向锁还可以用一种四元数的东西,以后我们会涉及到。
实战演练
讲了这么多基础知识,终于可以动手操作了,是不是等不及了?先别急,我们是要学OpenGL的,花太多的时间在实现数学库上显然和我们的初衷背道而驰,所以,我们可以采用“拿来主义”,直接找一个数学库用。幸好,OpenGL的“周边”就有一个好的数学库,叫做:GLM。
到GLM的网站去下载0.9.8版本的数学库(不要下最新的0.9.9版本,和我们的代码不兼容)。没法FQ的可以到我的网盘里去下载。下载解压后,把头文件根目录(glm目录,不是解压缩后的glm文件夹,而是在里面的glm文件夹)复制到你的includes目录下面就可以了。
设置完后,我们需要在代码中包含需要的头文件。
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
我们先来试试这个库有没有效。
//Test
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f)); //从这里就能看出单位矩阵的作用了。初始化的trans是一个单位矩阵,让它平移到(1.0f, 1.0f, 0.0f)的位置产生了一个平移矩阵。
vec = trans * vec;
std::cout << "(" << vec.x << "," << vec.y << "," << vec.z << ")" << std::endl;
嗯,输出正确。
让我们袖子干吧!把前面章节中显示的图片缩小成原来的一半,然后再绕着z轴逆时针旋转90度。
先来生成矩阵:
glm::mat4 trans;
trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f));
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
矩阵生成完后,我们如何让它在顶点着色器中生效呢?想来你很快就能得到答案,没错,就是用uniform关键字。先声明一个变量uniform mat4 transform
,然后在主函数中调用gl_Position = transform * vec4(aPos, 1.0f)
。
接下来,我们要在程序里设置这个值。但原有的shader类中没有设置mat4类型的接口,所以我们要添加一个:
void Shader::setMat4(const std::string& name, float value[]) const {
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, value);
}
介绍一下glUniformMatrix4fv
函数的参数:
- 参数一:变量位置
- 参数二:我们想传的矩阵个数,这里我们只设置一个,所以是1
- 参数三:我们是否想转换矩阵,把行和列交换。OpenGL中的矩阵是列主序的矩阵(和DX中的不同),不过GLM中生成的矩阵也是列主序的,所以我们设置成GL_FALSE,表示不用转换。
- 参数四:矩阵数组。这里我们要把矩阵转换成数组的格式传递。
接口写好后,我们就能在主循环中使用了:
shader.setMat4("transform", glm::value_ptr(trans));
完成后,编译运行:
跟我们想象的一样!
慢着,这样就满足了吗?NO!我们还要让它动起来。方法也很简单,我们传入一个glfwGetTime()作为旋转的弧度就可以了。像这样:
trans = glm::rotate(trans, /*glm::radians(90.0f)*/(float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
如果之前你是在循环外面生成的转换矩阵,那么你就要把它放到循环里面去了,这样随着每次运行,旋转的角度也不一样。嗯,编译运行。
效果不错!如果你的程序不这样的显示,可以点击这里下载代码进行比对。
总结
好了,艰苦的数学旅程告一段落,我们来回忆一下都学了些什么。首先是向量,以及向量能做的一些运算;然后是矩阵,以及矩阵的一些运算;接着,我们看到了实际有用的一些运算矩阵;最后,我们使用了一个现成的库GLM来实现变换坐标的效果。呼~休息!
参考资料:
www.learningopengl.com(很好的学习网站,建议多去看看)