我们要想改变物体的位置,现有解决办法是,每一帧改变物体的顶点并且重配置缓冲区从而使物体移动,但是这样太繁琐,更好的解决方式是使用矩阵(Matrix)来更好的变换(Transform)一个物体。
一、向量
向量是有方向的量,向量有一个方向(Direction)和大小(Magnitude,也叫强度或长度)。
数学家喜欢在字母上面加一横表示向量,比如说
。当用在公式中时它们通常是这样的:
1、向量与标量运算
标量只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加、减、乘、除一个标量时,可以把向量的每个分量分别与这个标量进行运算。如下是向量与标量的加法运算:
2、向量取反
对一个向量取反会将其方向逆转,一个指向东北的向量取反后就指向西南方向了。在一个向量的每个分量前加负号就可以实现取反:
3、向量加减
向量的加法可以被定义为分量的相加,即将一个向量中的每个分量加上另一个向量对应的分量:
两个向量想加的结果向量是从第一个向量(v=(4,2))起点指向第二个向量(k=(1,2))的终点
向量的减法等于第一个向量加上第二个向量的相反向量:
两个向量相减会得到这两个向量指向位置的差:从第二个向量(v=(3,2))的终点指向第一个向量(v=(0.5,3.5))的终点,再把起点位置移动到(0,0)处,最终向量是(-2.5,1.5)
4、长度
使用勾股定理来获取向量的长度/大小,如果你把向量的x与y分量画出来,该向量会和x与y分量为边形成一个三角形:
因为两条边(x和y)是已知的,如果希望知道斜边的长度,可以直接通过勾股定理计算:
||v¯||表示向量的长度,例子中向量(4,2)的长度等于4.47:
有一个特殊类型的向量叫做单位向量(Unit Vector)。单位向量有一个特别的性质:长度是1。可以用任意向量的每个分量除以向量的长度得到它的单位向量:
这种方法叫做一个向量的标准化,单位向量头上有一个^样子的记号。
二、向量相乘
普通的乘法在向量上时没有定义的,因为它在视觉上没有意义。但是在相乘的时候有两种特定情况可以选择:一个是点乘,另一个是叉乘。
1、点乘
点乘是通过将对应分量逐个相乘,然后再把所有得积相加来计算的:
同时,两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值,公式如下:
它们之间的夹角记作
θ。当两个向量都是单位向量时,公式简化为:
现在点乘只定义了两个向量的夹角,0度的余弦值是1,90度的余弦值是0,使用点乘可以很容易测试两个向量是否是正交或平行(想了解更多关于正弦或余弦函数的知识,推荐看可汗学院的基础三角学视频:
https://www.khanacademy.org/math/geometry-home/right-triangles-topic/intro-to-the-trig-ratios-geo/v/basic-trigonometry
)。
也可以通过点乘的结果计算两个非单位向量的夹角,点乘的结果除以两个向量的长度之积,得到的结果就是夹角的余弦值,即cos
θ:
2、叉乘
叉乘需要两个不平行向量作为输入,生成一个正交与两个输入向量的第三个向量。
如果输入的两个向量也是正交的,那么叉乘之后将会产生3个相互正交的向量:
叉乘公式如下:
三、矩阵
简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。
下面是一个2
×
3的矩阵:
矩阵可以通过(i,j)进行索引,i是行,j是列,这就是上面的矩阵叫做
2
×
3的矩阵的原因(2行3列,也叫做矩阵的维度)。
1、矩阵的加减
矩阵与标量之间的加减定义如下,标量值要加减到矩阵的每一个元素上:
矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,只有同维度的矩阵才能加减:
2、矩阵的数乘
矩阵与标量的乘法是矩阵的每一个元素分别乘以该标量:
四、矩阵相乘
矩阵相乘有一些限制:
- 只有当左侧矩阵的列数与右侧矩阵的行数相等时,才能相乘;
- 矩阵相乘不遵守交换律,也就是说A·B≠B·A;
下面是两个2×2矩阵相乘的例子:
- 首先用左侧矩阵的第1行的第1个数乘以右侧矩阵的第1列的第1个数加上左侧矩阵的第1行第2个数乘以右侧矩阵的第1列第2个数,得到的结果放在第1行第1列上;
- 然后用左侧矩阵的第1行的第1个数乘以右侧矩阵的第2列的第1个数加上左侧矩阵的第1行第2个数乘以右侧矩阵的第2列第2个数,得到的结果放在第1行第2列上;
- 然后用左侧矩阵的第2行的第1个数乘以右侧矩阵的第1列的第1个数加上左侧矩阵的第2行第2个数乘以右侧矩阵的第1列第2个数,得到的结果放在第2行第1列上;
- 最后用左侧矩阵的第2行的第1个数乘以右侧矩阵的第2列的第1个数加上左侧矩阵的第2行第2个数乘以右侧矩阵的第2列第2个数,得到的结果放在第2行第2列上;
- 结果得到一个维度是(n,m)的矩阵,n等于左侧矩阵的行数,m等于右侧矩阵的列数(n×k矩阵 乘以 k×m矩阵得到一个n×m矩阵)。
(想了解更多的矩阵知识,推荐看可汗学院的矩阵教程:
https://www.khanacademy.org/math/algebra2/algebra-matrices)
五、矩阵与向量相乘
向量可以看成是N×1的矩阵,N表示向量分量的个数。如果我们有一个M×N矩阵,可以用这个矩阵乘以N×1向量,
因为这个矩阵的列数等于这个向量的行数,所以他们能相乘。
1、单位矩阵
在OpenGL中,由于某些原因我们通常使用4
×4的变换矩阵,其中最重要的原因就是大部分的向量都是4分量的。
最简单的变换矩阵就是单位矩阵,单位矩阵是一个除了对角线以外都是0的N
×N矩阵。
下面可以看到这个变换矩阵使一个向量完全不变:
2、缩放矩阵
对一个向量进行缩放就是对这个向量的长度进行缩放,它的方向保持不变。
先尝试缩放向量
v
¯
=
(
3
,
2
),
把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一,
沿着y轴把向量的高度缩放为原来的两倍,得到向量
s
¯
:
OpenGL通常在3D空间进行操作,对于2D的情况我们当做把z轴缩放1倍,这样z轴的值保持不变。
刚刚的缩放操作时,
每个轴的缩放因子都不一样,所以
是不均匀缩放;如果每个轴的缩放因子都一样就叫均匀缩放。
下图为向量(x,y,z)定义一个缩放矩阵:
S1、S2、S3分别表示对轴x、y、z的缩放倍数。注意,第四个缩放向量(w分量)仍然是1,这个w分量有其他用途。
3、位移矩阵
位移是在原来向量的基础上加上另一个向量从而获得一个不同位置的新向量的过程。
在
4
×4矩阵上的第四列最上面的3个值用来实现向量的位移操作,位移矩阵定义如下:
齐次坐标:
向量的w分量也叫齐次坐标,想要从齐次向量得到3D向量,我们可以把x、y、z坐标分别除以w坐标。使用齐次坐标的好处:它允许我们在3D向量上进行移动(如果没有w分量我们不能位移向量),而且下一章我们会用w值创建3D视觉效果。
如果一个向量的齐次坐标是0,这个坐标就是方向向量,这个向量不能移动。
4、旋转矩阵
如果想了解旋转矩阵是如何构造出来的,推荐看可汗学院线性代数的视频:
https://www.khanacademy.org/math/linear-algebra/matrix_transformations
2D或3D空间中旋转用角来表示。角可以是角度制或弧度制的,周角是360角度或2PI弧度。
大多数旋转函数需要用弧度制的角,角度制的角可以和弧度制相互转化:
- 弧度制角度:角度 = 弧度 * (180.0f / PI);
- 角度制弧度:弧度 = 角度 * (PI / 180.0f);
PI约等于3.14159265359。
转半圈会旋转360/2 = 180度,向右旋转1/5圈表示向右旋转360/5 = 72度。
在3D空间中旋转需要定义一个角和一个旋转轴,物体会沿着给定的旋转轴旋转特定角度。
使用三角学,给定一个角度,可以把一个向量变换为一个经过旋转的新向量,这通常使用一系列正弦和余弦函数进行巧妙的组合得到(如何生成旋转矩阵超出本教程的范围,直接看旋转矩阵即可)。
旋转矩阵在3D空间中每个单位轴上有不同定义,旋转角度用
θ表示,沿x轴旋转:
沿y轴旋转:
沿z轴旋转:
利用旋转矩阵可以把任意向量沿着一个单位轴进行旋转,也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转,但是这会导致万向节死锁(万向节死锁的知识推荐看这个视频:
https://www.youtube.com/watch?v=zc8b2Jo7mno
)。一个更好的模型是沿着任意的一个轴进行旋转,而不是一系列旋转矩阵进行复合,这样的矩阵是存在的,见下面的公式,其中
(
R
x
,
R
y
,
R
z
)
代表任意旋转轴:
在数学上讨论如何生成这样的矩阵仍然超过本节内容,但是,这样一个矩阵也只是会极大地避免万向节死锁的问题,不能完全的解决
万向节死锁的问题。避免
万向节死锁的真正解决方案是使用四元数(关于四元数,推荐看这个:
https://krasjet.github.io/quaternion/quaternion.pdf
),它不仅更安全,而且计算会更有效率。
5、矩阵的组合
使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,可以把多个变换组合到一个矩阵中。
假设有一个顶点(x,y,z),我们希望将其缩放2倍,然后位移(1,2,3)个单位,我们需要一个位移和缩放矩阵来完成这些变换,结果的变换矩阵像这样:
注意,当矩阵相乘时我们先写位移再写缩放变换,矩阵乘法是不遵守交换律的,这意味这它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个向量相乘的,所以你应该从右向左读这个乘法。建议在组合矩阵时先进行缩放操作,然后旋转,最后才是位移(位移一般要放在最后;假设现在需要旋转和位移,如果是先旋转再位移,旋转的中心就是物体的中点点;
如果是先位移再旋转,旋转的中心是原点
)。
最终的变换矩阵左乘向量得到以下结果:
这样,向量先缩放2倍,然后位移了(1,2,3)个单位。
上面的矩阵可以写成如下样式,这样的话向量会和第二个矩阵进行运算,运算结果再和第一个矩阵进行运算,这样的计算结果是先缩放再位移:
如果写成下面这样,计算结果是先位移再缩放(下一节的练习1中展示这种效果)
六、实践
更改程序,实现矩形位移和旋转功能。
更改顶点着色器代码,创建一个
uniform mat4 RotationMatrix变量,用于接收变换矩阵
#version 330 core
layout (location = 0) in vec3 aPos; //位置变量的属性位置值为0
layout (location = 1) in vec3 aColor; //颜色变量的属性位置值为1
layout (location = 2) in vec2 aTexture; //纹理变量的属性位置值为2
out vec3 ourColor; //向片段着色器输出一个颜色坐标
out vec2 ourTexture; //向片段着色器输出一个纹理坐标
uniform mat4 RotationMatrix; //变换矩阵
void main()
{
//矩阵与向量相乘,使向量进行旋转、缩放、位移等
gl_Position = RotationMatrix * vec4(aPos, 1.0);
ourColor = aColor;
ourTexture = aTexture;
}
在myopenglwidget.h文件中创建定时器对象m_timer和定时器响应槽函数函数onTimeout
#ifndef MYOPENGLWIDGET_H
#define MYOPENGLWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QTimer>
class MyOpenGLWidget : public QOpenGLWidget,QOpenGLFunctions_3_3_Core
{
Q_OBJECT
public:
explicit MyOpenGLWidget(QWidget *parent = nullptr);
~MyOpenGLWidget();
protected:
virtual void initializeGL();
virtual void resizeGL(int w, int h);
virtual void paintGL();
void keyPressEvent(QKeyEvent *event);
private slots:
void onTimeout();
private:
QOpenGLShaderProgram m_shaderProgram;
QOpenGLTexture *m_textureWall;
QOpenGLTexture *m_textureSmile;
QOpenGLTexture *m_textureSmall;
float mixValue = 0.5;
QTimer m_timer;
};
#endif // MYOPENGLWIDGET_H
在myopenglwidget.h文件中构造函数进行定时器的信号与槽函数的绑定,并启动100ms的定时器;析构函数中停止定时器:
#include "myopenglwidget.h"
#include <QDebug>
#include <QKeyEvent>
#include <QTime>
unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
unsigned int EBO; //元素缓冲对象
MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
setFocusPolicy(Qt::StrongFocus);
connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::onTimeout);
m_timer.start(100); //100ms
}
MyOpenGLWidget::~MyOpenGLWidget()
{
if(m_timer.isActive())
m_timer.stop();
makeCurrent();
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &EBO);
doneCurrent();
}
void MyOpenGLWidget::onTimeout()
{
update();
}
void MyOpenGLWidget::initializeGL()
{
//初始化OpenGL函数
initializeOpenGLFunctions();
//创建VBO,并赋予ID
glGenBuffers(1, &VBO);
//绑定VBO对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //右上角
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下角
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下角
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //左上角
};
//把顶点数据复制到显存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//创建VAO对象,并赋予ID
glGenVertexArrays(1, &VAO);
//绑定VAO对象
glBindVertexArray(VAO);
//创建EBO对象,并赋予ID
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
unsigned int indices[] = {
0, 1, 3, //第一个三角形
1, 2, 3 //第二个三角形
};
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); //开启VAO管理的第二个属性值
//纹理属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2); //开启VAO管理的第三个属性值
//解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
//解绑VAO
glBindVertexArray(0);
//创建一个程序对象
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
bool success = m_shaderProgram.link();
if(!success)
qDebug()<<"ERR:" << m_shaderProgram.log();
m_shaderProgram.bind();
m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);
m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器
m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器
m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器
m_shaderProgram.setUniformValue("mixValue", mixValue);
}
void MyOpenGLWidget::resizeGL(int w, int h)
{
Q_UNUSED(w);
Q_UNUSED(h);
//glViewport(0, 0, w, h);
}
void MyOpenGLWidget::paintGL()
{
//设置墨绿色背景
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
glClear(GL_COLOR_BUFFER_BIT); //状态使用
//变换矩阵
QMatrix4x4 matrix; //创建单位矩阵
unsigned int time = QTime::currentTime().msec();
matrix.translate(0.5, -0.5, 0.0); //位移
matrix.rotate(time, 0.0f, 0.0f, 1.0f); //旋转
m_shaderProgram.setUniformValue("RotationMatrix", matrix);
//绘制
m_shaderProgram.bind(); //激活程序对象
glBindVertexArray(VAO); //绑定VAO
m_textureWall->bind(0); //绑定激活纹理单元0
m_textureSmile->bind(1); //绑定激活纹理单元1
m_textureSmall->bind(2); //绑定激活纹理单元2
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); //绘图
}
void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
if(event->key() == Qt::Key_Up)
mixValue+=0.1;
else if(event->key() == Qt::Key_Down)
mixValue-=0.1;
else
return;
if(mixValue > 1.0)
mixValue = 1.0;
if(mixValue < 0.0)
mixValue = 0.0;
makeCurrent();
m_shaderProgram.setUniformValue("mixValue", mixValue);
doneCurrent();
update();
QOpenGLWidget::keyPressEvent(event);
}
定时器的槽函数中进行界面更新
void MyOpenGLWidget::onTimeout()
{
update();
}
更改一下顶点数据中位置数据,使矩形变小(此处不是要实现的缩放功能,只是为展示效果更看)
//顶点数据
float vertices[] = {
//位置 //颜色 //纹理
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //右上角
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //右下角
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, //左下角
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f //左上角
};
在paintGL()函数中创建matrix变量,对matrix进行旋转和位移操作,之后把值传递给顶点着色器的RotationMatrix变量。
//变换矩阵
QMatrix4x4 matrix;
unsigned int time = QTime::currentTime().msec();
matrix.translate(0.5, -0.5, 0.0); //位移
matrix.rotate(time, 0.0f, 0.0f, 1.0f); //旋转
m_shaderProgram.setUniformValue("RotationMatrix", matrix);
在上面的代码中旋转代码在位移代码的下一行,变量matrix会先和旋转矩阵进行运算然后再和位移矩阵进行计算,达到先旋转后位移的效果。
运行结果如下,矩形会移动到界面的右下角,同时每隔100ms进行一次旋转
注:观看OpenGL中文官网(https://learnopengl-cn.github.io/)和阿西拜的现代OpenGL入门(https://ke.qq.com/course/3999604#term_id=104150693)学习OpenGL