一.矩阵变换
-
缩放
我们下面会构造一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为(S1,S2,S3)我们可以为任意向量(x,y,z)定义一个缩放矩阵:
注意,第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量的作用是为了仿射变换的时候建立齐次坐标用的。 -
位移
位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。如果我们把位移向量表示为(Tx,Ty,Tz),我们就能把位移矩阵定义为:
-
旋转
在3D空间中旋转需要定义一个角和一个旋转轴(Rotation Axis)。物体会沿着给定的旋转轴旋转特定角度。- 沿x轴旋转:
- 沿y轴旋转:
- 沿z轴旋转
利用旋转矩阵我们可以把任意位置向量沿一个单位旋转轴进行旋转。也可以将多个矩阵复合,比如先沿着x轴旋转再沿着y轴旋转。但是这会很快导致一个问题——万向节死锁,关于万向节死锁的产生见另外一篇博客:https://blog.csdn.net/Tom870223050/article/details/120625072?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163417618116780357295341%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=163417618116780357295341&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_v2~rank_v29-1-120625072.pc_v2_rank_blog_default&utm_term=%E4%B8%87%E5%90%91&spm=1018.2226.3001.4450 - 矩阵的组合
让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。
- 沿x轴旋转:
当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。
二.实践
注意,OpenGl没有自带的矩阵变换的数学库,但是有为OpenGl定做的数学库,那就是GLM。
现在,我们来让之前的那个箱子逆时针旋转90度。然后缩放0.5倍,使它变成原来的一半大。
- 导入头文件
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
- 创建矩阵
//首先,我们把箱子在每个轴都缩放到0.5倍,然后沿z轴旋转90度。
//注意,由于矩阵变换是左乘运算,所以越早定义的越后操作
glm::mat4 trans;
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));
3.把矩阵传递给顶点着色器
//这里使用uniform定义了一个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 = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
- 肯定的,我们需要在程序中对mat4赋值
//查找transform位置值
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
//将矩阵数据发送给着色器,p1为位置值,p4为trans数据,由于要把GLM的数据转为OpenGl的,使用value_ptr()函数
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
完整代码如下:
//窗口创建头文件
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//设置一个窗口回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
//检测用户输入
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
//顶点着色器源码
//" VertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n"
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout(location = 1) in vec3 aColor;\n"
"uniform mat4 transform;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = transform * vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
//片段着色器源码
const char* fragmentShaderSource = "#version 330 core\n"
"in vec3 ourColor; \n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"FragColor = vec4(ourColor,1.0f);\n"
"}\n";
//实例化窗口
int main()
{
//*********************L1********************************//
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
//使用核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//glfCreateWindow函数设置窗口的宽和高,以及标题
//GLFWwindow类存放窗口对象的指针
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOPenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "faild" << std::endl;
return -1;
}
//将当前窗口的上下文设置为当前线程的上下文,上下文:OpenGL在其中存储渲染信息的一个数据结构
glfwMakeContextCurrent(window);
//初始化GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Faild" << std::endl;
return -1;
}
//设置渲染窗口,p1,p2设置窗口左下角的位置,p3,p4设置窗口的宽度和高度(按像素算)
glViewport(0, 0, 800, 600);
//使用这个函数可以调用设置视口的函数
//glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//***************************L1***********************************//
//***************************L2**********************************//
//创建一个顶点着色器,用ID来引用
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//把着色器源码赋到着色器对象上,然后编译它
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//创建一个片段着色器,用ID来引用
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//把着色器源码附加到着色器对象上,然后编译它
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//把编译后的两个着色器链接到一个程序对象上.
//注意,当链接到下一个着色器时,它会把这个着色器的输出作为下一个着色器的输入
unsigned int shaderProgram;
shaderProgram = glCreateProgram(); //创建一个程序对象
glAttachShader(shaderProgram, vertexShader);//附加着色器
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); //链接着色器
//然后激活就可以使用了,这时我们调用着色器进行渲染的时候就是使用这个程序对象了
glUseProgram(shaderProgram);
//**************************L5**********************************//
glm::mat4 trans;
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));
unsigned int transformLoc = glGetUniformLocation(shaderProgram, "transform");
glUniformMatrix4fv(transformLoc,1,GL_FALSE,glm::value_ptr(trans));
//之前定义的着色器对象就可以删除了
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
/*
int vertexColorLocation = glGetUniformLocation(shaderProgram,"ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, 1.0f, 0.0f, 1.0f);*/
//缓存弄好了,接下来就是将数据弄到缓存上去了
float vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下
0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部
};
//定义一个顶点缓存对象
//GPU上有一个特定的缓冲ID,通过引用申请一个VBO对象
unsigned int VBO;
glGenBuffers(1, &VBO);
//下一步就是把缓冲对象绑定到对应的缓存中去,这里我们绑定的缓存类型为GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//定义一个顶点数组对象
//该对象也可以被绑定,任何随后的顶点属性都会被绑定在这个VAO中
unsigned int VAO;
glGenVertexArrays(1, &VAO);
//下一步也就是把这个数组对象绑定到缓存上去
glBindVertexArray(VAO);
//GL_STATIC_DRAW :数据不会或几乎不会改变。
//GL_DYNAMIC_DRAW:数据会被改变很多。
//GL_STREAM_DRAW :数据每次绘制时都会改变。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //绑定数据到缓存
//由于顶点着色器的输入很灵活,我们必须定义输入的值对应的顶点属性
//glVertexAttribPointer()函数定义了Opengl中该如何分析我们输入的顶点数据
//p1表示位置值,p2表示顶点属性的大小vec3对应的就是3,p3对应我们输入的数据类型,p4对应我们是否要标准化
//也就是对应是否要将数据映射到-1到1的空间中去,p5表示我们读取一个顶点属性的位移,p6表示数据在缓冲区的位移
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//启用顶点属性函数,参数为对应的顶点属性位置
//添加对RGB顶点属性的读取
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); //激活
//加入循环
while (!glfwWindowShouldClose(window)) //用于检查window对象是否还在,也就是还没退出渲染
{
//*****************************************L1*************************************//
//在渲染中不断检测用户动作
processInput(window);
//清空缓存并设置一个默认缓存色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//这里是对颜色缓存进行清空,其他还有深度缓等GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
//*****************************************L1*************************************//
//***************************************L2************************************//
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
//注意,这里定义的图元为一个三角形
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);//用于交换前缓存和后缓存:也就是绘制图像的过程
glfwPollEvents();//用于检测有没有什么触发事件,并更新窗口状态
}
//清空顶点数据
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
//释放资源
glfwTerminate();
//...
return 0;
}
结果如下: