参考资料:
https://learnopengl.com/
https://learnopengl-cn.github.io/
这次要实现的是Transform变换功能。OpenGL没有自带任何的矩阵和向量知识,可以使用已经做好了的数学库,如GLM。从 这里 可以下载GLM库,使用时需导入以下头文件:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
要对图像进行位移、旋转、缩放等操作,离不开矩阵,因此在顶点着色器编码中,定义一个四维矩阵变量,与 gl_Position 相乘实现变换功能。
//顶点着色器编码
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord;
uniform mat4 transform;
out vec4 vertexColor;
out vec2 TexCoord;
void main() {
gl_Position = transform * vec4(aPos.x, aPos.y, aPos.z, 1.0);
vertexColor = vec4(aColor, 1.0f);
TexCoord = aTexCoord;
}
在main函数中的循环绘制阶段,可以使用glUniformMatrix4fv函数把矩阵数据发送给着色器。第一个参数是uniform的位置值,使用glGetUniformLocation获取对应着色器中所需的uniform变量位置值,我们定义了一个uniform mat4 transform,因此获取transform变量的位置值。第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。第三个参数询问我们是否希望对我们的矩阵进行转置(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,因此填GL_FALSE。最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans)); //把矩阵数据发送给着色器
接下来,只需要定义这个trans矩阵,也就是真正的变换矩阵了,它能够决定对图像的变换方式。使用glm::mat4声明一个四维矩阵。glm::translate可以将其转换为位移变换矩阵,也就是第四列中前三行对应元素的值,它的参数要求填入要变换的矩阵,以及三维的位移变换向量。位移矩阵如下:
glm::rotate可以将其转换为旋转变换矩阵,填入要变换的矩阵,要变换的角度(实际需要的是弧度值,使用glm::radians将角度转为弧度),和决定绕哪个轴旋转的三维旋转变量。绕x、y、z三轴旋转的变换矩阵如下:
glm::scale转换为缩放矩阵,传入要变换的矩阵,和三维缩放向量(即矩阵前三行前三列对角线上的元素值)。缩放矩阵如下:
值得注意的是,矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。举例来说,如果先旋转后位移,那么因为旋转之后模型的正面朝向改变了,模型就会向新的方向位移,这不符合原本的预期。为了让变换更符合直观感受,应当先进行缩放操作,然后是旋转,最后位移。而矩阵是以左乘的形式与向量进行运算的,所以几个矩阵相乘时,“最右边的”矩阵会优先对向量造成影响,因此应当以 Mt * Mr * Ms * v 这种由位移变换矩阵、旋转变换矩阵、缩放操作矩阵依次相乘的形式作为最终的变换矩阵。因此变换矩阵的代码如下:
//变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.3f, 0.3f, 0.2f)); //位移
trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0, 0, 1.0f)); //旋转
trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f)); //缩放
这样trans矩阵将会对图像造成的影响为:先将xyz三个维度均缩放为原来的0.5倍,然后绕Z轴进行45度的旋转,再将图像在x方向移动0.3,在y方向移动0.3,z方向移动0.2。主程序文件的代码和运行后的结果如下所示:
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
//顶点数据
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 // 左上
};
//顶点索引
unsigned int indices[] = {
0, 1, 2, //第一个三角形使用的顶点
2, 3, 0 //第二个三角形使用的顶点
};
//检查输入函数
void processInput(GLFWwindow* window)
{
//按下ESC键时,将WindowShouldClose设为true,循环绘制将停止
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
//视口改变时的回调函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height); //OpenGL渲染窗口的尺寸大小
}
int main()
{
glfwInit(); //初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //告诉GLFW要使用OpenGL的版本号
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //主版本号、次版本号都为3,即3.3版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //告诉GLFW使用核心模式(Core-profile)
//打开 GLFW Window
GLFWwindow* window = glfwCreateWindow(1600, 1200, "My OpenGL Game", nullptr, nullptr);
if (window == nullptr) //若窗口创建失败,打印错误信息,终止GLFW并return -1
{
printf("Open window failed.");
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window); //创建OpenGL上下文
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); //用户改变窗口大小的时候,视口调用回调函数
//初始化GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK) //若GLEW初始化失败,打印错误信息并终止GLFW窗口
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
Shader* myShader = new Shader("vertexSource.txt", "fragmentSource.txt");
//创建VAO(顶点数组对象)
unsigned int VAO;
glGenVertexArrays(1, &VAO); //生成一个顶点数组对象
glBindVertexArray(VAO); //绑定VAO
//创建VBO(顶点缓冲对象)
unsigned int VBO;
glGenBuffers(1, &VBO); //生成缓冲区对象。第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组,由于只需创建一个VBO,因此不需要用数组形式
glBindBuffer(GL_ARRAY_BUFFER, VBO); //把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,GL_ARRAY_BUFFER是一种顶点缓冲对象的缓冲类型。OpenGL允许同时绑定多个缓冲,只要它们是不同的缓冲类型。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //把定义的数据复制到当前绑定缓冲的函数。参数1:目标缓冲类型,参数2:指定传输数据的大小(以字节为单位),参数3:我们希望发送的实际数据,参数4:指定显卡如何管理给定的数据,GL_STATIC_DRAW表示数据不会或几乎不会改变。
//创建EBO(元素缓冲对象/索引缓冲对象)
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); //告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。参数1:要配置的顶点属性(layout(location = 0),故0),参数2:指定顶点属性的大小(vec3,故3),参数3:指定数据的类型,参数4:是否希望数据被标准化,参数5:在连续的顶点属性组之间的间隔,参数6:表示位置数据在缓冲中起始位置的偏移量(Offset)。
glEnableVertexAttribArray(0); //以顶点属性位置值作为参数,启用顶点属性,由于前面声明了layout(location = 0),故为0
//颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
//UV属性
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
//使用纹理单元0绑定TexBufferA
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
int width, height, nrChannel;
stbi_set_flip_vertically_on_load(true); //翻转图像y轴
//加载并生成第一张纹理
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "图片加载失败" << std::endl;
}
stbi_image_free(data); //释放
//使用纹理单元3绑定TexBufferB
unsigned int TexBufferB;
glGenTextures(1, &TexBufferB);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
//加载并生成第二张纹理
unsigned char* data2 = stbi_load("awesomeface1.png", &width, &height, &nrChannel, 0);
if (data2)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "图片加载失败" << std::endl;
}
stbi_image_free(data2);
//变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(0.3f, 0.3f, 0.2f)); //位移
trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0, 0, 1.0f)); //旋转
trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f)); //缩放
//让程序在手动关闭之前不断绘制图像
while (!glfwWindowShouldClose(window))
{
//检测输入
processInput(window);
//渲染指令
glClearColor(0.f, 0.5f, 0.5f, 1.0f); //设置清空屏幕所用的颜色
glClear(GL_COLOR_BUFFER_BIT); //清空屏幕的颜色缓冲区
//绑定纹理到对应的纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
glBindVertexArray(VAO); //绘制物体的时候就拿出相应的VAO,绑定它
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //绑定EBO
myShader->use();
//告诉OpenGL每个着色器采样器属于哪个纹理单元
glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 3);
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans)); //把矩阵数据发送给着色器
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //绘制,参数1:绘制模式(三角形),参数2:绘制顶点数(两个三角形6个顶点),参数3:索引的类型,参数4:指定EBO中的偏移量。
//检查并调用事件,交换缓冲区
glfwSwapBuffers(window); //交换颜色缓冲区,前缓冲区保存最终输出的图像,后缓冲区负责绘制渲染指令,当渲染指令执行完毕后,交换前后缓冲区,使完整图像呈现出来,避免逐像素绘制图案时的割裂感
glfwPollEvents(); //检查触发事件,如键盘输入、鼠标移动等
}
glfwTerminate(); //关闭GLFW并退出
return 0;
}