OpenGL: Transform知识
一、变换
二、向量
三、向量与标量运算
四、向量取反
五、向量加减
六、长度
七、向量相乘
八、矩阵
九、矩阵的加减
十、矩阵的数乘
十一、矩阵相乘
1、矩阵相乘限制:
* 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
* 矩阵相乘不遵守交换律。A * B != B * A。
2、矩阵相乘:
* 取左侧矩阵的行,取右侧矩阵的列,决定了结果矩阵的输出值位置(行列)。
*
| 1 2 | | 5 6 | | 1*5+2*7 1*6+2*8 | | 19 22 || | * | | = | | = | || 3 4 | | 7 8 | | 3*5+4*7 3*6+4*8 | | 43 50 |
* 结果矩阵维度(m, n), m是左侧矩阵的行数, n是右侧矩阵的列数。
十二、矩阵乘以向量
1、M*N矩阵和N*1向量相乘,可以变换这个向量。
十三、单位矩阵
1、单位矩阵是一个对角线全是1,其它全是0的N*N矩阵。
2、单位矩阵乘以向量,使向量不变。
十四、缩放(Scale)
1、不均匀(Non-uniform)缩放,每个轴的缩放因子(Scaling Factor)都不一样,会改变向量的方向和大小。
2、均匀缩放(Uniform Scale),每个轴的缩放因为(Scaling Factor)都一样,不改变向量的方向,但改变向量的大小。
3、我们把缩放因子表示为(S1, S2, S3),则为向量(x,y,z)定义一个缩放矩阵:
| S1 0 0 0 | | x | | S1 * x || | | | | || 0 S2 0 0 | | y | | S2 * y || | * | | = | || 0 0 S3 0 | | z | | S3 * z || | | | | || 0 0 0 1 | | 1 | | 1 |
十五、位移(Translation)
1、位移是在原始向量的基础上加上另一个向量,从而获得一个在不同位置的新向量的过程。
2、我们把位移向量表示为(Tx, Ty, Tz),则为向量(x,y,z)定义一个位移矩阵:
| 1 0 0 Tx | | x | | x + Tx || | | | | || 0 1 0 Ty | | y | | y + Ty || | * | | = | || 0 0 1 Tz | | z | | z + Tz || | | | | || 0 0 0 1 | | 1 | | 1 |
十六、旋转(Rotation)
1、旋转用角(Angle)来表示,角可以是角度制或弧度制。
2、弧度转角度: 角度 = 弧度 * 180 / PI。
角度转弧度: 弧度 = 角度 * PI / 180。
3、在3D空间中,旋转需要定义一个角和一个旋转轴(Rotation Axis),物体会沿着给定的旋转轴旋转特定角度。
4、沿X轴旋转theta角:
| 1 0 0 0 | | x | | x || | | | | || 0 cosT -sinT 0 | | y | | cosT * y - sinT * z || | * | | = | || 0 sinT cosT 0 | | z | | sinT * y + cosT * z || | | | | || 0 0 0 1 | | 1 | | 1 |
5、沿Y轴旋转theta角:
| cosT 0 sinT 0 | | x | | cosT * x + sinT * z || | | | | || 0 1 0 0 | | y | | y || | * | | = | || -sinT 0 cosT 0 | | z | | -sinT * x + cosT * z || | | | | || 0 0 0 1 | | 1 | | 1 |
6、沿Z轴旋转theta角:
| cosT -sinT 0 0 | | x | | cosT * x - sinT * y || | | | | || sinT cosT 0 0 | | y | | sinT * x + cosT * y || | * | | = | || 0 0 1 0 | | z | | z || | | | | || 0 0 0 1 | | 1 | | 1 |
十七、可以多个矩阵复合,注意万向节死锁(Gimbal Lock)问题,避免这个问题是使用四元数(Quaternion),更安全更有效率。
十八、矩阵的组合
1、使用矩阵进行变换的强大之处在于,根据矩阵之间的乘法,可以把多个变换组合到一个矩阵中。
2、假设有一个顶点(x,y,z),希望将其缩放2倍,然后位移(1,2,3)个单位,则结果的变换矩阵为:
| 1 0 0 1 | | 2 0 0 0 | | 2 0 0 1 || | | | | || 0 1 0 2 | | 0 2 0 0 | | 0 2 0 2 || | * | | = | || 0 0 1 3 | | 0 0 2 0 | | 0 0 2 3 || | | | | || 0 0 0 1 | | 0 0 0 1 | | 0 0 0 1 |
3、因为矩阵乘法不遵守交换率,意味着它们的顺序很重要。
当矩阵相乘时,最右边矩阵是第一个与向量相乘的,所以最右边矩阵代表着第一个变换,我们应该从右向左读矩阵乘法。
4、注意!!!!!!!!!!!!!!!!!
组合矩阵时,建议先进行缩放操作,然后旋转,最后才是位移,否则它们会消极地互相影响。(比如,先位移再缩放,位移的向量也会被缩放。)
十九、实践
1、OpenGL没有自带任何的矩阵和向量知识,我们必须定义自己的数学类和函数。
2、有个易于使用,且专门为OpenGL量身定做的数学库,GLM。
3、GLM:
* GLM, OpenGL Mathematics的缩写。
* GLM是一个只有头文件的库。
* GLM从0.9.9版本起,矩阵类型默认初始化为一个零矩阵,而不是单位矩阵。
用glm::mat4 mat = glm::mat4(1.0f)初始化为单位矩阵。
* 我们需要的GLM大多数功能可以在下面三个头文件中找到:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
4、实际变换中,先把箱子缩放0.5倍,再逆时针旋转90度,看代码效果。
* glm::radians()将角度转为弧度。
* 修改顶点着色器,添加uniform mat4类型的矩阵变量,再将该矩阵乘以位置向量。
#version 330 corelayout(location = 0) in vec3 aPos;layout(location = 1) in vec3 aColor;layout(location = 2) in vec2 aTexCoord;out vec3 ourColor;out vec2 TexCoord;uniform mat4 transform;void main(){gl_Position = transform * vec4(aPos, 1.0);ourColor = aColor;TexCoord = aTexCoord;}
* 将变换矩阵传递给着色器。
unsigned int transformLoc = glGetUniformLocation(shaderProgramID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
* 注意,代码看着顺序是先旋转再缩放,但是实际应用却是先变换缩放,再变换旋转!!!!!!
因为OpenGL的向量是列向量,旋转矩阵*缩放矩阵*向量,向量先和缩放矩阵乘,再和旋转矩阵乘!!!
二十、实例代码
//MLOpenGLWidget.h#pragma once#include <QOpenGLWidget>
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLShaderProgram>class MLOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{Q_OBJECTpublic:MLOpenGLWidget(QWidget * parent = nullptr);~MLOpenGLWidget();enum Shape { None, Rect, Circle, Triangle };void DrawShape(Shape shape);void WireFrame(bool bWire);protected:void initializeGL() override;void resizeGL(int w, int h) override;void paintGL() override;private:unsigned int VAO;unsigned int VBO;unsigned int EBO;unsigned int textureID1;unsigned int textureID2;Shape m_shape = None;QOpenGLShaderProgram m_shaderProgram;float time = 10.0f;
};//MLOpenGLWidget.cpp#include "MLOpenGLWidget.h"
#include <QDebug>
#include "stb_image.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"MLOpenGLWidget::MLOpenGLWidget(QWidget * parent) : QOpenGLWidget(parent)
{
}MLOpenGLWidget::~MLOpenGLWidget()
{makeCurrent();glDeleteBuffers(1, &VBO);glDeleteBuffers(1, &EBO);glDeleteVertexArrays(1, &VAO);glDeleteTextures(1, &textureID1);glDeleteTextures(1, &textureID2);doneCurrent();
}void MLOpenGLWidget::DrawShape(Shape shape)
{//触发重新绘制m_shape = shape;update();
}void MLOpenGLWidget::WireFrame(bool bWire)
{makeCurrent();if (bWire){glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);}else{glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);}doneCurrent();update();
}void MLOpenGLWidget::initializeGL()
{initializeOpenGLFunctions();//创建VAO、VBO、EBO,并赋予IDglGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);//绑定VAO、VBO、EBO对象glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //注意, 因为VAO已经绑定,此句会被VAO的最后一个指针偷偷记录下来, 指向EBO。//顶点数据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, 0.3f, 0.5f, 0.8f, 0.0f, 1.0f, // 左上角};glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//索引数据unsigned int indices[] = {0, 1, 3, // 第一个三角形1, 2, 3 // 第二个三角形};glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);//配置VAO位置属性指针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);//(1)、创建和生成纹理1glGenTextures(1, &textureID1);glBindTexture(GL_TEXTURE_2D, textureID1);//为当前绑定的纹理对象设置环绕、过滤方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//加载并生成纹理1int width, height, nrChannels;stbi_set_flip_vertically_on_load(true);unsigned char * data1 = stbi_load("E:/02.LearnSummary/16.OpenGL+Qt/41_Transform/41_Transform/textures/container.jpg", &width, &height, &nrChannels, 0);if (data1){//当前绑定的纹理会被附加上纹理图像数据。glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data1);//为当前绑定的纹理自动生成所有需要的多级渐远纹理。glGenerateMipmap(GL_TEXTURE_2D);}else{qDebug() << "Failed to load texture1\n";}stbi_image_free(data1);glBindTexture(GL_TEXTURE_2D, 0);//(2)、创建和生成纹理2glGenTextures(1, &textureID2);glBindTexture(GL_TEXTURE_2D, textureID2);//为当前绑定的纹理对象设置环绕、过滤方式glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//加载并生成纹理2unsigned char * data2 = stbi_load("E:/02.LearnSummary/16.OpenGL+Qt/41_Transform/41_Transform/textures/awesomeface.png", &width, &height, &nrChannels, 0);if (data2){glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data2);glGenerateMipmap(GL_TEXTURE_2D);}else{qDebug() << "Failed to load texture2\n";}stbi_image_free(data2);glBindTexture(GL_TEXTURE_2D, 0);//(1)着色器程序: 添加纹理//(2)着色器程序: 多个纹理采样器多个纹理m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/1_RotateScaleShader.vert");m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/1_RotateScaleShader.frag");m_shaderProgram.link();
}void MLOpenGLWidget::resizeGL(int w, int h)
{
}//绘制都是在此函数中,其它地方都是触发绘制
void MLOpenGLWidget::paintGL()
{glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);//将纹理对象绑定到纹理单元glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, textureID1);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, textureID2);m_shaderProgram.bind();//将纹理采样器绑定到纹理单元m_shaderProgram.setUniformValue("ourTexture1", 0);m_shaderProgram.setUniformValue("ourTexture2", 1);//一、//定义一个向量(1, 0, 0)/*glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);*///定义一个矩阵变量/*glm::mat4 trans = glm::mat4(1.0f);*///平移(1,1,0)单位,单位矩阵+平移向量,构造平移变换矩阵/*trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));*///平移向量/*vec = trans * vec;*///结果为: 2 1 0/*qDebug() << vec.x << vec.y << vec.z;*///二、先把箱子逆时针旋转90度,再缩放0.5倍。//(1)glm方式//创建单位变换矩阵//glm::mat4 trans = glm::mat4(1.0f);//单位矩阵+逆时针旋转90度+绕Z轴 得旋转矩阵//trans = glm::rotate<float>(trans, glm::radians(90.0), glm::vec3(0.0, 0.0, 1.0));//旋转矩阵+缩放因子0.5 得旋转矩阵*缩放矩阵//trans = glm::scale<float>(trans, glm::vec3(0.5, 0.5, 0.5));//(2)Qt方式//创建单位矩阵变量//QMatrix4x4 trans4;//单位矩阵+逆时针旋转90度+绕Z轴 得旋转矩阵//trans4.rotate(90.0, QVector3D(0.0, 0.0, 1.0));//旋转矩阵+缩放因子 得旋转矩阵*缩放矩阵//trans4.scale(0.5);//将变换矩阵传递给顶点着色器//m_shaderProgram.setUniformValue("transform", trans4);switch (m_shape){case MLOpenGLWidget::None:break;case MLOpenGLWidget::Rect:{//三、随着时间旋转//注意!!!代码看着先平移,再旋转。//但实际变换是先旋转变换,再平移变换,因为平移矩阵*旋转矩阵*向量,向量先和旋转矩阵乘,再和平移矩阵乘,OpenGL的向量是列向量。QMatrix4x4 trans4;trans4.translate(QVector3D(0.5f, -0.5f, 0.0f));trans4.rotate(time, QVector3D(0.0, 0.0, 1.0));m_shaderProgram.setUniformValue("transform", trans4);glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);time += 10;break;}case MLOpenGLWidget::Circle:break;case MLOpenGLWidget::Triangle:break;default:break;}
}//MainWindow.h#pragma once#include <QtWidgets/QMainWindow>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindowClass; };
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget * parent = nullptr);~MainWindow();private:void ActDrawRectTriggered(bool checked);void ActClearTriggered(bool checked);void ActWireFrameTriggered(bool checked);private:Ui::MainWindowClass * ui;
};//MainWindow.cpp#include "MainWindow.h"
#include "ui_MainWindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindowClass())
{ui->setupUi(this);setCentralWidget(ui->openGLWidget);connect(ui->actDrawRect, &QAction::triggered, this, &MainWindow::ActDrawRectTriggered);connect(ui->actClear, &QAction::triggered, this, &MainWindow::ActClearTriggered);connect(ui->actWireFrame, &QAction::triggered, this, &MainWindow::ActWireFrameTriggered);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::ActDrawRectTriggered(bool checked)
{ui->openGLWidget->DrawShape(MaLanOpenGLWidget::Rect);
}void MainWindow::ActClearTriggered(bool checked)
{ui->openGLWidget->DrawShape(MaLanOpenGLWidget::None);
}void MainWindow::ActWireFrameTriggered(bool checked)
{ui->openGLWidget->WireFrame(checked);
}
MainWindow.ui