OpenGL 原生库5 变换
前言
尽管我们现在已经知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,但因为它们都还是静态的物体,仍是不够有趣。我们可以尝试着在每一帧改变物体的顶点并且重配置缓冲区从而使它们移动,但这太繁琐了,而且会消耗很多的处理时间。我们现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。当然,这并不是说我们会去讨论武术和数字虚拟世界
矩阵是一种非常有用的数学工具,尽管听起来可能有些吓人,不过一旦你理解了它们后,它们会变得非常有用。在讨论矩阵的过程中,我们需要使用到一些数学知识。对于一些愿意多了解这些知识的读者,我会附加一些资源给你们阅读。
为了深入了解变换,我们首先要在讨论矩阵之前进一步了解一下向量。这一节的目标是让你拥有将来需要的最基础的数学背景知识。如果你发现这节十分困难,尽量尝试去理解它们,当你以后需要它们的时候回过头来复习这些概念。
向量
向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。你可以把向量想像成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。那么这个藏宝图的指示一共有3个向量。向量可以在任意维度(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。
下面你会看到3个向量,每个向量在2D图像中都用一个箭头(x, y)表示。我们在2D图片中展示这些向量,因为这样子会更直观一点。你可以把这些2D向量当做z坐标为0的3D向量。由于向量表示的是方向,起始于何处并不会改变它的值。下图我们可以看到向量v¯
和w¯
是相等的,尽管他们的起始点不同:
由于向量是一个方向,所以有些时候会很难形象地将它们用位置(Position)表示出来。为了让其更为直观,我们通常设定这个方向的原点为(0, 0, 0),然后指向一个方向,对应一个点,使其变为位置向量(Position Vector)(你也可以把起点设置为其他的点,然后说:这个向量从这个点起始指向另一个点)。比如说位置向量(3, 5)在图像中的起点会是(0, 0),并会指向(3, 5)。我们可以使用向量在2D或3D空间中表示方向与位置.
和普通数字一样,我们也可以用向量进行多种运算(其中一些你可能已经看到过了)。
向量相乘
两个向量相乘是一种很奇怪的情况。普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。但是在相乘的时候我们有两种特定情况可以选择:一个是点乘(Dot Product),记作v¯⋅k¯
,另一个是叉乘(Cross Product),记作v¯×k¯
。
点乘
两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:
叉乘
叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。接下来的教程中这会非常有用。下面的图片展示了3D空间中叉乘的样子:
两个正交向量A和B叉积:
完整变换公式:
其中(Rx,Ry,Rz)
代表任意旋转轴
代码:
#include <glad/glad.h> // GLAD: 用于管理 OpenGL 函数指针
#include <GLFW/glfw3.h> // GLFW: 用于创建窗口及处理用户输入
#include <stb_image.h> // STB_IMAGE: 用于加载图像
#include <glm/glm.hpp> // GLM: 用于数学运算(如向量和矩阵)
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <learnopengl/shader_s.h> // 自定义 Shader 类,用于管理着色器程序
#include <iostream> // 用于标准输入输出
// 回调函数声明
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
// 设置窗口宽度和高度
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
// 初始化 GLFW
glfwInit();
// 设置 OpenGL 版本为 3.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
// 使用核心模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
// MacOS 需要设置这个以便获得 OpenGL 3.2 及以上版本
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// 创建一个 GLFW 窗口
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// 注册窗口大小变化回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 初始化 GLAD:加载所有 OpenGL 函数指针
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 编译和构建着色器程序
Shader ourShader("5.1.transform.vs", "5.1.transform.fs");
// 顶点数据和配置顶点属性
float vertices[] = {
// 位置 // 纹理坐标
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f // 左上
};
unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 设置顶点属性指针
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 纹理坐标属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 加载和创建纹理
unsigned int texture1, texture2;
// 纹理 1
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// 设置纹理环绕方式
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);
// 加载图像,创建纹理并生成 mipmaps
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // 翻转加载的纹理
unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 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 << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// 纹理 2
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);
// 设置纹理环绕方式
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);
// 加载图像,创建纹理并生成 mipmaps
data = stbi_load(FileSystem::getPath("resources/textures/awesomeface.png").c_str(), &width, &height, &nrChannels, 0);
if (data)
{
// awesomeface.png 有透明通道,因此使用 GL_RGBA
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
// 告诉 OpenGL 每个采样器属于哪个纹理单元(只需要做一次)
ourShader.use();
ourShader.setInt("texture1", 0);
ourShader.setInt("texture2", 1);
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 处理输入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
// 创建变换矩阵
glm::mat4 transform = glm::mat4(1.0f); // 确保矩阵初始化为单位矩阵
transform = glm::translate(transform, glm::vec3(0.5f, -0.5f, 0.0f));
transform = glm::rotate(transform, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
// 获取变换矩阵的 uniform 位置并设置矩阵
ourShader.use();
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
// 渲染容器
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// 交换缓冲区并查询 IO 事件(如按键、鼠标移动等)
glfwSwapBuffers(window);
glfwPollEvents();
}
// 可选:释放所有资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
// 终止 GLFW,释放所有分配的资源
glfwTerminate();
return 0;
}
// 处理所有输入:查询 GLFW 是否在这一帧中按下/释放了相关按键,并做出相应处理
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// GLFW 回调函数:窗口大小改变时执行
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 确保视口与新的窗口尺寸匹配;注意 width 和 height 会显著大于在视网膜显示器上的指定值。
glViewport(0, 0, width, height);
}
5.1.transform.vs
#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.0);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
5.1.transform.fs
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
// texture samplers
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
// linearly interpolate between both textures (80% container, 20% awesomeface)
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}