欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Modern OpenGL】转换 Transformations

程序员文章站 2023-12-27 10:44:39
...

说明:跟着learnopengl的内容学习,不是纯翻译,只是自己整理记录。
强烈推荐原文,无论是内容还是排版。 原文链接
本文地址: http://blog.csdn.net/aganlengzi/article/details/50421159

转换 Transformations

我们已经学会了怎样创建对象,并且学会利用着色或者纹理使他们呈现出表面细节,但是它们还并不是十分有趣,因为它们只是静止的对象。我们虽然可以通过在每帧中改变它们的顶点坐标值挥着通过重新配置他们的缓存区使它们动起来,但是这样做是十分繁杂并且消耗更多能量。实际上,有更好的办法可以转换一个对象:使用一个矩阵对象。

矩阵是一种十分强大的数学概念,一开始看上去是令人生畏的。但是当你习惯它们的时候,你会觉得它们非常有用。

但是,为了完全理解转换,在讨论矩阵之前,我们首先不得不深入探究一下向量。本次教程的目的是让你能有相关数学背景知识,以方便我们后面的讨论。如果你觉得这次的内容太难了,那就能理解多少就理解多少吧。当后面再次用到这里面的内容的时候,可以再回过头来看。

向量 Vectors

向量,指具有大小(magnitude)和方向的几何对象。向量可以有不同的维度,多少维都可以。如果我们使用二维向量,那它的物理意义就是二维平面上的一个方向。如果我们使用三维向量,那它的可以表示三维世界中的任意方向。

下面你将看到三个二维向量,它们在二维坐标系中用(x,y)的形式表示。因为表示二维向量更加直观,所以我们就不用三维向量作为例子了。实际上,也可以把这几个向量想象成三维向量,只是它们的z轴坐标被设置成了0而已。因为向量本质上是代表方向的,所以其起始点并不会改变它的值。在下图中,我们可以看到v和w是相等的,虽然它们的起点不同。

后面讨论的关于向量的内容在我们的初中或者高中肯定都已经学习过,主要有:
向量的表示(在坐标系中的表示和坐标表示);
向量和标量的加减乘除(向量的每个分量和标量做计算);
向量和向量的加减和意义(方向的改变);
向量的取反(反向);
向量的长度计算方法(勾股定理);
向量的单位向量(长度值为单位1时候的坐标表示);

向量之间乘法
点乘(向量表示):

【Modern OpenGL】转换 Transformations


点乘(坐标表示):两个向量a = [a1, a2,…, an]和b = [b1, b2,…, bn],
其点积定义为:a·b=a1b1+a2b2+……+anbn。
点乘得到的是一个具体数值。

 

叉乘:叉乘的结果还是一个向量,垂直原来两个所在的平面,方向也有原来两个向量决定。

【Modern OpenGL】转换 Transformations

 

现在先记这么多关于向量的知识吧,后面如果再用到的话就再回过头来看一看。

矩阵 Matrices

前面我们看了向量相关的知识,下面来看一下矩阵相关的知识:
矩阵(Matrix)是一个按照长方阵列排列的实数(还包括符号,表达式等等)集合。其中的每一个数(符号或表达式)都叫做矩阵的元素。一个m x n 的矩阵如下图所示:

【Modern OpenGL】转换 Transformations


其中的每个元素都可以通过(i,j)的形式索引到,其中i表示行,j表示列,其实就是一个二维数组,只不过注意矩阵中下标是从1开始的而不是从0开始的。
这大概就是关于矩阵定义的所有内容了。我们来看看作用在矩阵上的操作:

 

矩阵和数值的加减:每个元素都和数值相加减。
矩阵和矩阵的加减:只有相同行列数的矩阵才能够进行加减操作,对应元素相加减。
矩阵和数值的乘除:每个元素都和数值相乘除。
矩阵和矩阵相乘:两个矩阵一前一后,只有前面矩阵的列数和后面矩阵的行数相同时才能够进行相乘的操作。具体相乘操作是前面矩阵每行的每个元素和后面矩阵每列的每个元素相乘后相加得到结果矩阵中的(行,列)位置的数值。举个例子基本就清楚了:

【Modern OpenGL】转换 Transformations

 

矩阵和向量相乘

上面讲了向量,讲了矩阵,最终是要用它们。用它们做什么呢?相乘!至少形式上是可以满足矩阵和向量相乘的。一个m x n的矩阵和一个n维向量是正好可以相乘的,而且相乘的结果还是一个n维向量。换句话说,我们将一个m x n的矩阵作用在了一个n维向量上得到了作用的结果也就是二者相乘的结果。这次教程讲的是转换,这就是其所在了!矩阵就是用来对向量进行转换的工具。而对向量进行转换就只需要左乘相应的转换矩阵就好了。

先看一下最简单的转换矩阵单位矩阵。

单位矩阵

单位矩阵是个方阵(行列数相等),从左上角到右下角的对角线(称为主对角线)上的元素均为1。它的作用就像是数值乘法运算中的1。一个矩阵左乘一个单位矩阵,得到的还是本身。如下图所示:

【Modern OpenGL】转换 Transformations

 

缩放

缩放矩阵可以利用单位矩阵来理解。单位矩阵的主对角线上都是1,向量的每一个分量和其相乘后得到的值还是向量的值本身。如果将这些值改成不是1的数值,那么得到的效果就是不同的分量和这些非1值相乘的结果。因为向量表示的是一个点的坐标(目前以点的坐标举例)。如果图形上的所有点的坐标都做了相同的缩放操作(表示每个点的向量都左乘这个缩放矩阵),那么得到的整体图形就进行了缩放操作,这应该不难理解。缩放矩阵一般形式:

【Modern OpenGL】转换 Transformations


需要注意的有两点:首先,缩放矩阵有两种,一种是按比例缩放,一种是不按比例缩放。按比例缩放的缩放矩阵应该保证主对角线上除w分量(前面教程中讲过OpenGL中的向量分为x,y,z,w最多四个分量,实际上这是三维向量表示的标准统一化表示)相等;而不安比例的缩放则无需保证。其次,就是这个w分量,相当于我们在用四维向量来表示三维坐标,用四维方阵来转换4维向量。实际上,就缩放来说,没有必要用到四维,但是为什么要这样用呢?后面会讲到。

 

平移

和缩放矩阵类似,平移转换矩阵也能够从单位矩阵中推导出来。只不过缩放是对坐标值成比例(乘除)的改变。而平移是对向量分量的整体加减操作,举例来说就是:

【Modern OpenGL】转换 Transformations


可以看到,x,y,z上的平移量T_x,T_y,T_z在每次计算的时候都是和w分量相乘之后加到原来的向量分量数值上的。这个时候就体现出了向量和矩阵中的w分量的作用了。

 

为什么缩放的时候用不到w分量还要加上?实际上是为了统一表示,这就是齐次坐标w的作用所在。
关于齐次坐标,目前实际上记住:它的使用使得转换矩阵在形式上能够保证一致(行列数),这样在计算的时候不用担心不满足左边矩阵的列数不等于右边向量的行数的尴尬局面。另外,w分量的作用并不仅限于此,它的值也不仅限于1,在下一个教程中会讲到,利用w值来改变三维对象。

旋转

相较于以上介绍的缩放和平移转换矩阵,旋转转换矩阵在理解上可能会有些难度,虽然它在形式上和上面的两个转换矩阵比较相似(肯定比较相似,都是一个矩阵,只不过矩阵中的元素根据我们要实现的功能设置不同的数值)。

在学习旋转矩阵之前,我们应该首先看一下什么是向量的旋转。我们只说三维空间中的旋转。在三维空间中,所有的点都在三维坐标系中,都可以通过三维坐标来指定。其中某个点可以看成是从原点到这个点的一个实际的向量(带箭头的线段)。在三维空间中的旋转是和特定的坐标轴相关的,即旋转是绕某一个坐标轴进行一定角度的旋转。所以对于三维空间中的点的旋转转换矩阵,有三个,分别是:
绕x轴旋转变化矩阵:

【Modern OpenGL】转换 Transformations


绕y轴旋转变化矩阵:

【Modern OpenGL】转换 Transformations


绕z轴旋转变化矩阵:

【Modern OpenGL】转换 Transformations


很显然,我们在实际使用的时候不会只对绘制的对象进行按照x,y或z轴的单独的旋转,我们也可以按照先x后y最后z的方式组合达到我们的效果,但是这种方法是不推荐的,它会引入问题。推荐的方法是绕一个方向单位向量进行旋转,其形式如下,假设是绕(R_x,R_y,R_z)进行旋转:

【Modern OpenGL】转换 Transformations

 

组合矩阵

组合矩阵在本教程中的含义就是矩阵相乘。
实际上,我们在对生成的三维对象进行操作的时候,往往是对它们做一系列的转换,其中当然包括最基本的缩放、平移、旋转操作。但是,如果我们每次都要进行一系列操作(比如说缩放、平移和旋转三种操作),一种方法是:要变换的向量首先左乘缩放转换矩阵,得到的向量再左乘平移变换矩阵,得到的向量再左乘旋转变换矩阵,最终得到了我们想要的结果;另一种方法是:首先按照顺序将缩放矩阵左乘平移矩阵,得到的结果左乘旋转矩阵,然后要变换的向量左乘其结果。这两种方法的到的效果是一致的。但是从运算量上来看,第二种方法显然优于第一种,因为在第一种中,对象的每个点都要做相同的3次左乘矩阵的操作,而第二种只需要每个点完成1次左乘矩阵的操作就好了。所以,这才是使用变换矩阵和齐次坐标的意义所在。

实践一下 In practice

我们已经解释了转换背后的原理,是时候看一下我们应该怎样使用这些理论了。OpenGL本身是没有任何关于矩阵或者向量相关的内置信息的。所以我们需要自己来定义数学类和函数。在这个教程中我们使用之前就已经有的数学库来方便我们转换操作。幸运的是,GLM就是一个易用并且专为OpenGL定制过的数学库。

GLM

GLM是OpenGL Mathematics的缩写。它是一个只有头文件的库,也就一位置我们只需要包含它的合适的头文件就能够进行愉快的使用了。不需要像之前我们使用的GLEW、GLFW、SOIL等还需要编译和配置(配置还是需要的)。你可以从这儿下载到所需的文件。在配置的时候我们只需要让我们的工程能够找到需要的GLM的文件就可以了。

我的做法是将下载到的glm-0.9.7.1.zip解压到某个目录下比如说GLM_ROOT,然后:
在我的工程中—->属性—->VC++目录—->包含目录中添加GLM_ROOT就可以了。

实际上大多数情况下,我们只需要包含下面的三个头文件就已经够用了:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

首先让我们先生成一个没有什么具体含义的转换矩阵,仅仅是为了测试一下矩阵和向量相乘的结果:

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));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;

如上面的代码所示,我们想要做的是将(1,0,0)通过(1,1,0)生成的矩阵(如下图所示)转换成三个维度上的坐标分别是(2,1,0)的结果。

【Modern OpenGL】转换 Transformations


在代码中,我们通过GLM中的glm::vec4数据类型声明了一个四维向量vec;通过glm::mat4数据类型生成了一个4 x 4的矩阵trans,默认为单位矩阵;通过glm::translate函数借助于向量(1.0f, 1.0f, 0.0f)将这个矩阵变换成上图所示的转换矩阵,然后将向量vec左乘变换矩阵trans,并输出结果向量的x,y和z轴分量坐标。我运行的结果如下图所示:

【Modern OpenGL】转换 Transformations

 

接下来,让我们尝试一下更有趣的东西。
让我们对上次教程中那个由笑脸和盒子混合贴图而成的矩形进行操作:首先对其进行逆时针90度旋转,然后我们将其等比例缩放0.5。好的,开始干吧,首先我们定义转换矩阵:

glm::mat4 trans;
trans = glm::rotate(trans, 90.0f, glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));  

注意上面的转换顺序,因为转换矩阵对向量的操作都是左乘进行的,上面的glm::rotate和glm::scale函数也是默认左乘的规则生成转换矩阵,那么最终得到的trans相当于是“旋转 * 缩放”矩阵的结果,当这个组合后的变换矩阵作用到图形的每个坐标点的时候,实际上是先和缩放矩阵相乘,然后和旋转矩阵相乘的。另外,通过以上方法能够简单方便地生成组合后的最终转换矩阵,大大减少计算量(还记得上面讲到的第二种方法吧)。

有一些版本的GLM是不支持以度数来表示角度的,而是支持弧度表示,在这种情况下,可能需要手动进行一下转换。

剩下的问题就是将这个转换矩阵作用到我们图形上的每一个点了。当然应该是在vertex shader中进行坐标的转换(fragment中是进行颜色值生成的),当然你肯定也像我一样想到了用uniform,但是你可能像我一样没有记起来GLSL中也有一个mat4数据类型,表示一个4 x 4的矩阵。有了这个数据结构,我们才可以定义变量,才可以将其定义成uniform类型的变量,才可以将我们的矩阵传递进shader中,像下面这样:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 texCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 transform;

void main()
{
    gl_Position = transform * vec4(position, 1.0f);
    ourColor = color;
    TexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
} 

实际上,GLSL还有mat2和mat3数据类型,并且也支持大尺寸的矩阵指定部分给小尺寸的向量赋值的操作,和前面讲的向量中的类似的灵活操作相类似。

好的,上面的代码中,我们首先定义了用于传递转换矩阵的uniform变量,然后在主函数中将原来的点的位置向量左乘上了我们的transform矩阵。在OpenGL程序中,我们需要对这个transform矩阵进行赋值,这个应该是比较熟悉的:

GLuint transformLoc = glGetUniformLocation(ourShader.Program, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));

首先在我们的程序中查找uniform类型变量transform的位置,然后调用glUniformMatrix4fv函数来将我们定义的trans矩阵传递给这个位置。其中需要注意的是:
用于给uniform类型变量传递值的glUniform函数的后缀是Matrix4fv,它的各个参数的含义如下:
第一个参数比较简单,是上面找到的这个uniform类型变量的地址
第二个参数指定我们需要传递的矩阵的个数
第三个参数指定了我们是否需要对这个矩阵进行转置(使用GLM一般不用)
最后一个参数是实际的数据的地址,因为GLM存储的方式和OpenGL接收的方式有所不同,所以需要使用GLM内置的转换函数value_ptr进行数据格式的转换以保证数据的正确输入。

以上,我们利用GLM生成了一个转换矩阵,并且利用uniform mat4类型的变量将这个矩阵值传递到了vertex shader中,并在shader中将对象上的每个坐标向量都进行了左乘操作,结果应该就是这个样子了:

【Modern OpenGL】转换 Transformations

效果达到!

下面我们想让它动起来!让它进行旋转~
基本的步骤是相同的:
首先定义或者说利用GLM生成一个转换矩阵,其次将这个矩阵传递到vertex shader中并进行左乘操作。

glm::mat4 trans;
trans = glm::rotate(trans,(GLfloat)glfwGetTime() * 50.0f, glm::vec3(0.0f, 0.0f, 1.0f));

上面的这个矩阵是随着时间动态改变的,那个时间函数就是我们前面用到的动态改变三角形颜色的方法。定义的旋转轴是z轴。
glm::rotate函数的第一个参数是矩阵,第二个参数是角度,我们设置了随时间变化的角度;第三个参数是参照的方向向量,我们设置的是z轴。得到的效果应该是:

【Modern OpenGL】转换 Transformations

为了得到上述结果,需要注意的是吗,这个矩阵的生成需要在game loop中进行定义,否则变换矩阵并不会进行更新。也就得不到想要的旋转的效果。

所有的代码(main.cppshader)在这儿可以得到。

上一篇:

下一篇: