本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
变换
经过一段时间的学习,我们已经清楚如何创建对象,赋予颜色,或者赋予纹理。现在我们来试着让创建的物体动起来,实现这一目的的其中一个方式是矩阵。为了方便讲解,先复习一下相关的数学知识。
向量
一个向量包含方向和大小,向量有二维和三维。下面是一些典型的二维向量:
三维向量同理,只不过增加了一个维度。我们经常用例如的符号来表示向量,如 = (x, y ,z)-1。由于向量是用来描述方向的,所以比较难用来描述位置,当然我们默认将向量的起点设为原点,那么终点就是位置,这样就是位置向量。由此我们可以同时用向量来表示位置和方向。
标量运算
如向量与标量的加法公式为:-1 -1-1-1。减和乘除同理。
反向
反向是反转向量的方向:- = -(vx, vy, vz)-1 = (-vx, -vy, -vz)-1
向量加减法
两向量的加法是分组件的加法,每个相同的组件进行加法操作:
= (1, 2, 3)-1, = (4, 5, 6)-1 → + = (1 + 4, 2 + 5, 3 + 6)-1 = (5, 7, 9)-1。
几何上的演示如下:
减法同理:
向量大小
为获取向量大小我们使用以下勾股定理,如下图的二维向量:
对于二维向量,我们用下面的公式定义它的大小:
,三维向量同理。
同时,有一个被称为单位向量的特殊向量,大小为1,我们可以这样定义:
我们可以把这一操作成为标准化一个向量,由于大小为1,我们只需关心方向。
向量乘法
针对向量的乘法有两种,一种是点乘:;一种是叉乘:。
点乘
点乘公式为:,为两向量的夹角。
特别地,如果两向量的长度均为1, 那么,这样的话就可以很方便的判断两向量是正交还是平行。
若不清楚夹角,计算公式为:。(若为三维向量)
叉乘
叉乘只定义在三维空间中,并将两个不平行的向量作为输入,得到正交于这两个向量的第三个向量。若对两个正交的向量叉乘,那么最后得到的三个向量将两两正交,下面是一个演示图:
不同于其它操作,叉乘的公式比较复杂:
对于两个向量A和B:。
矩阵
矩阵是一个方形阵列,一个矩阵长这样:
我们可以通过索引(i, j)来寻找矩阵的元素,前一个代表行数,后一个代表列数。
矩阵加减
只有行列数均相同的矩阵才可以实现加减法操作,如:
,减法同理。
矩阵标量乘法
同样是矩阵的每个元素和标量相乘,如:
矩阵与矩阵相乘
矩阵乘法有一定的规则:
1.进行相乘的两个矩阵,左侧的矩阵的列数必须等于右侧矩阵的行数。
2.矩阵之间的乘法是不满足交换律的。
下面是两个矩阵乘法的例子,注意行与列的变化:
当然,如果只靠这种公式去理解矩阵的乘法,同时还有上面的向量叉乘和点乘,包括接下来要解释的矩阵变换,这是远远不够的。为了能够更好的了解相关的几何意义,这里贴出B站搬运的一段视频,帮助大家更好的理解矩阵和向量:线性代数的本质(链接可能打不开,建议复制链接到浏览器搜索,或B站搜索“线性代数的本质”,或B站查找UP主“3Blue1Brown”)。当然,这里也贴出原文作者推荐的可汗学院的视频地址供学习参考:Khan Academy videos。
矩阵和向量乘法
经过上面的学习,我们了解了向量和矩阵的一些操作。其实向量本身就可以看作是一个的矩阵,所以它也拥有矩阵的相关特性,这样会方便我们理解很多操作。比如,我们想要对一个向量进行变换操作,那么我们可以将类似于平移、旋转、缩放的这些操作存储在一个矩阵中,并应用矩阵乘法来实现这些操作。
单位矩阵
在OpenGL中,我们只关注变换矩阵,因为大多数向量都是4个维度。最简单的变换矩阵是单位矩阵,单位矩阵的对角线全为1,其余为0。下面我们用单位矩阵与某一向量相乘:
我们可以看到向量没有变化,之后将慢慢介绍单位矩阵的作用。
缩放
当我们缩放向量时,我们通过一个值来改变向量的每一个元素,但方向不变。但如果我们向每个元素单独缩放该怎么做。下面是一个实例。
我们有一个向量,我们让它的x分量缩小一半,y分量扩大一倍,即缩放为向量:
注意OpenGL在3维空间进行操作,所以我们将z轴的缩放值置为1。现在我们来构建缩放矩阵,还记得上面介绍过的单位矩阵吗,我们像下面这样就可以完成缩放:
由此,我们可以很简单的获得一般情况下的缩放矩阵:
我们简单的将第四个缩放量置为1,这个w组件有其他的用途。
平移
有了上面的经验,我们仍使用单位矩阵来构造平移变换矩阵,经过推算,一般情况如下:
我们可以发现,由于我们将向量的w组件设为1,这样平移变换的量就与我们所设置的一致,如果只使用矩阵是做不到的。
齐次坐标
我们把上述的带有w组件的向量称为齐次向量,w组件称为齐次坐标。我们通过将x、y、z与w做除法来从齐次向量获取三维向量。如果没有齐次坐标,我们不能进行平移操作,同时齐次坐标在进行透视变换的时候非常有用,我们将会在之后进行了解。
旋转
首先强烈建议把上面推荐的视频看一遍或者搜索一下其它线性代数的教程学习一下,以方便理解接下来的内容。
向量的旋转被表示为角度,角度制或弧度制,这里我们使用角度制,当然,相互之间的转换非常方便,这里不赘述。
下图展示了从顺时针绕(0, 0, 1)旋转72°的结果:
我们通过旋转的角度的正弦和余弦来表示旋转的量,下面给出绕x、y、z三个轴旋转的一般情况下的变换矩阵:
绕X轴旋转:
绕Y轴旋转:
绕Z轴旋转
上述三个式子大家可以自己在纸上推导一下,看看为什么要在矩阵特定的位置安放正弦和余弦。
下面是绕任意一个轴的一般情况下的看起来很复杂的旋转矩阵:
但这种旋转方式是一种不安全的方式,会造成万向轴锁死的情况,如果想要避免万向轴锁死,这里推荐用四元数的方式来进行旋转,在之后会单独写一篇文章进行讲解。
将平移、缩放、旋转矩阵结合起来
通过矩阵乘法,我们可以把这三个矩阵结合起来,并变换一个向量:。注意,一定要把平移变换矩阵放到最左边。在变换时,从右往左看,若先进行平移变换的话,在进行例如缩放 变换操作后,会改变之前平移变换操作的结果,这是我们不愿意看到的。建议的顺序是先进行缩放,接着旋转,最后平移,写成表达式就是我们上面所展示的那样。
举个例子,我们有一个向量,我们向先放大2倍,再让它按方式平移,变化矩阵如下:
再与向量相乘:
,结果确实如此。
程序实战
经过上面的学习,我们会发现针对向量和矩阵的操作计算量非常大,幸运的是,现在有很多优秀的数学库供我们使用,如GLM,从官网上下载好并保存在你的包含文件目录下就可以开始学习了。
要使用GLM库,我们大部分时候针对向量和矩阵操作使用下面几个头文件就可以:
#include
#include
#include
下面是一个平移变换向量的例子:
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;
如上图,我们呢通过GLM内建的vector类建立一个齐次向量,接着定义一个4阶单位矩阵作为初始变换矩阵,之后通过内建的translate方法为这个变换矩阵赋平移变换的量,与向量相乘,向量就完成了平移变换。结果应该是
下面是旋转和缩放的例子:
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
我们通过GLM内建的rotate和scale方法来设置相应的变换矩阵。注意,GLM接受弧度制角度,所以我们通过内建的radians来将角度制转换为弧度制。同时,rotate接受的旋转轴是单位向量,所以记得标准化坐标。
我们按照之前的顺序设置好各个变换的量,GLM会自动帮我们将这些量组装为一个变换矩阵。
接下来的一个问题是如何将这个变换矩阵传递至顶点着色器。之前提到过,着色器支持mat4类型的变量,所以我们修改一下着色器代码:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = aTexCoord;
}
注意我们使用uniform声明了transform,我们需要设置这个变量:
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transform, 1, GL_FLASE, glm::value_ptr(trans));
像之前一样,我们获取这个变量的位置,由于是4阶矩阵,我们使用带有Matrix4fv后缀的glUniform方法,第一个参数是uniform变量的位置,第二个参数是传入矩阵数量,第三个参数是是否要转置矩阵,第四个参数是矩阵的数据(由于GLM的数据存储方式有所不同,我们用内建的value_ptr进行一下类型的转化)。
运行修改后的程序,会得到这样的结果:
可以看到,和预期一样,缩小了一半,并向左旋转了90度(注意:由于屏幕坐标以左上角为原点,横向为x轴,纵向为y轴,按照右手坐标系的规则,Z轴是指向屏幕内部的,之前我们设置的按z轴顺时针旋转90度,在屏幕上的效果就是向左侧旋转了90度。)
接下来我们让这个盒子不停地旋转:
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
我们在渲染循环中放上这段代码,通过glfwGetTime来设置动态的旋转角度,同时记住要在渲染循环中对顶点着色器中的uniform变量进行赋值。
这里给出原文的参考效果视频:https://learnopengl.com/video/getting-started/transformations.mp4
如果成功的话,盒子会绕着某一个点逆时针旋转。
这里给出原文代码参考:code,以及针对这一节做出修改的shader class。
最后,请多多参考原文内容:https://learnopengl.com/Getting-started/Transformations
再次贴出更深入了解线性代数的视频地址:线性代数的本质(链接可能打不开,建议复制链接到浏览器搜索,或B站搜索“线性代数的本质”,或B站查找UP主“3Blue1Brown”),或参考油管原视频:Essence of Linear Algebra。