LearnOpenGL学习笔记—入门06:Transformations
0 前言
本节笔记对应的内容 变换
在入门01中我们配置好了环境
在入门02中我们可以检测输入并出现一个有颜色的窗口
在入门03中我们初步学习了图形渲染管线,尝试用不同方法画出了三角形和四边形
在入门04(上)中我们学习了shader和GLSL的相关知识,并给图形加上了变换的颜色以及彩色。
在入门04(下)中我们建立自己的shader类,并能够从外部读取shader的内容。
在入门05中我们了解了有关材质的内容
尽管我们现在已经知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,但因为它们都还是静态的物体,仍不会动
所以这一节,我们尝试用线性代数的知识让它动起来
这章中关于向量,矩阵以及相关的运算就不在这里赘述了,可以去看
3Blue1Brown的线性代数的本质系列视频——在直观层面非常好的展示了线性代数,比起直接上数字的矩阵学习可能更形象,理解也更好。
本节中理解不清楚的地方基本都可以在这个视频系列中找到直观展示
或者像考研李永乐的类似视频来扎实掌握运算
我们在这里默认已经掌握了以下知识
- 向量的定义,标量运算,向量加减,点乘,叉乘,取模,标准化
- 矩阵的定义,加减,数乘,相乘
1 矩阵与变换
1.1 2D变换
通过线性代数的知识,我们知道
空间的变换可以表示成基的变换,并且最小正交基可以组成单位矩阵
也就是说一个矩阵代表着一个变换,对基的数值变化表现在矩阵的数字上
如下图的可以表示线性代数中的 缩放变换矩阵,旋转变换矩阵
这两种矩阵都可以表示成图右边的这种,矩阵和向量相乘的形式,也就是线性组合形式
所以它们也被叫做是线性变换
但是对于位移变换来说,它的原点改变了
而线性变换是 原点不会改变的变换(保加法和保数乘的性质)
所以位移无法用线性变换表示
(关于这一点,不理解的可以去看 3Blue1Brown的线性代数的本质系列视频)
如果把以上三个变换写成矩阵运算的话
我们在线性变换的形式基础上,只能在后面放个加法来表示位移,这样表示并不方便
(这里安利一个写的很好的齐次坐标和投影以及透视除法的网址,很清晰!!)
这也就是我们引入齐次坐标的原因,因为这样我们可以解决位移带来的问题。
在引入齐次坐标表示向量后,我们可以做到只用一个矩阵来表示2维运动
因为向量因为带有平移不变性,所以齐次坐标中,二维向量的最后一个维度是0
这样可以让在齐次坐标中,向量在经过位移变换的计算后,还是它本身。
旋转缩放与平移组合到一起,组合成了仿射变换
仿射变换可以用齐次坐标表示下的矩阵来书写。
复杂的变换可以拆解成简单变换的组合,并且组合的顺序是
先缩放,再旋转,后平移,拆解变换的时候就倒过来分析
矩阵乘法没有交换律,所以我们有顺序的要求,从右到左应用变换
1.2 3D变换(欧拉角/万向节锁/四元数)
三维的情况用齐次坐标表示,缩放,平移,旋转,
这里的旋转我们先用欧拉角表示的旋转
(关于欧拉角 在第八节的 欧拉角摄像机 中也有详细描述)
利用旋转矩阵 我们可以把 任意位置向量,沿一个单位旋转轴 进行旋转。
我们也可以将多个矩阵复合,比如先沿着x轴旋转,再沿着y轴旋转。
但是这会很快导致一个问题——万向节死锁(Gimbal Lock,可以看看相关的视频来了解)。
在这里我们不会讨论它的细节,但是对于3D空间中的旋转,一个更好的模型是沿着任意的一个轴,比如单位向量 ( 0.662 , 0.2 , 0.7222 ) (0.662, 0.2, 0.7222) (0.662,0.2,0.7222)旋转,而不是对一系列旋转矩阵进行复合。
避免万向节死锁的真正解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。
关于四元数的理解,我在 入门06 附:关于四元数 里进行了比较直观的阐述。
2 实战
现在我们已经懂了变换背后的所有理论,是时候将这些知识利用起来了。
OpenGL没有自带任何的矩阵和向量知识,幸运的是,有个易于使用,专门为OpenGL量身定做的数学库,那就是GLM。
GLM是OpenGL Mathematics的缩写,它是一个只有头文件的库,也就是说我们只需包含对应的头文件就行了,不用链接和编译。
GLM可以在它们的网站上下载。把头文件的根目录复制到includes文件夹,然后就可以使用这个库了
- GLM库从0.9.9版本起,默认会将矩阵类型初始化为一个零矩阵(所有元素均为0),而不是单位矩阵(对角元素为1,其它元素为0)。
- 如果使用的是0.9.9或0.9.9以上的版本,需要将所有的矩阵初始化改为
glm::mat4 mat = glm::mat4(1.0f)。
如果想与本教程的代码保持一致,请使用低于0.9.9版本的GLM,或者改用上述代码初始化所有的矩阵。
下载完成后,项目→属性→c++→常规→附加包含目录
在main函数开头输入
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
2.1 矩阵变换实战
从单位矩阵出发,塞满里面的各个元素,这就是这节课要实现的东西。
我们会在顶点着色器里来变换位置(加了个transform)
#version 330 core
layout(location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
layout(location = 2) in vec2 aTexCoord; // uv变量的属性位置值为 2
out vec4 vertexColor;
out vec2 TexCoord;
uniform mat4 transform;
void main(){
gl_Position = transform * vec4(aPos.x, aPos.y, aPos.z, 1.0);
vertexColor = vec4(aColor,1.0);
TexCoord = aTexCoord;
}
在渲染循环使用shader的部分,glUniform1i的后面加上
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans));
- 我们首先查询uniform变量的地址glGetUniformLocation,然后用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。
- 第一个参数,它是uniform的位置值。
- 第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。
- 第三个参数询问我们我们是否希望对我们的矩阵进行置换(Transpose),也就是说交换我们矩阵的行和列。
- OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要置换矩阵,我们填GL_FALSE。
- 最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。
我们接下来会创建一个变换矩阵,之前我们在顶点着色器中声明了一个uniform,并想着把矩阵发送给了着色器,让着色器变换我们的顶点坐标。
下面来创建变换矩阵,对变换来说,是先缩放,再旋转,再位移。
我们需要算出变换矩阵的话,需要按照位移→旋转→缩放的顺序调用glm的方法来乘起来
//计算变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 0.0f, 0.0f));
trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
- 首先,我们把箱子在每个轴都缩放到0.5倍,然后沿z轴旋转45度。
- GLM希望它的角度是弧度制的(Radian),所以我们使用glm::radians将角度转化为弧度。
- 注意有纹理的那面矩形是在XY平面上的,所以我们需要把它绕着z轴旋转。
- 因为我们把这个矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。
- 最后是沿着x轴移动。
- 记住,实际的变换顺序应该与阅读顺序相反:尽管在代码中我们先位移再旋转,实际的变换却是先应用旋转再是位移的。
我们现在做些更有意思的,看看我们是否可以让箱子随着时间旋转。
要让箱子随着时间推移旋转,我们必须在游戏循环中更新变换矩阵,因为它在每一次渲染迭代中都要更新。我们使用GLFW的时间函数来获取不同时间的角度
把以下代码放入渲染循环中,就会转动了
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 0.0f, 0.0f));
//trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
2.2 四元数变换实战
2.2.1 方法
再次说明,关于四元数的理解,在 入门06 附:关于四元数 里进行了比较直观的阐述。
四元数由4个数[x y z w]构成,表示了如下的旋转
x = 旋转轴.x · sin(旋转角 / 2)
y = 旋转轴.y · sin(旋转角 / 2)
z = 旋转轴.z · sin(旋转角 / 2)
w = cos(旋转角 / 2)
以下是几种创建方法
// Creates an identity quaternion (no rotation)
quat MyQuaternion;
// 1.Direct specification of the 4 components
// You almost never use this directly
glm::quat MyQuaternion = glm::quat(w,x,y,z);
// 2.Conversion from Euler angles (in radians) to Quaternion
vec3 EulerAngles(90, 45, 0);
MyQuaternion = glm::quat(EulerAngles);
// 3.Conversion from axis-angle
// In GLM the angle must be in degrees here, so convert it.
glm::quat MyQuaternion = angleAxis(glm::radians(RotationAngle), RotationAxis);
我们运用时需要旋转矩阵,四元数转化成矩阵的方法在 入门06 附:关于四元数 里进行了说明
程序上的话,glm已经帮我们做好了
glm::mat4 RotationMatrix = glm::mat4_cast(MyQuaternion);
这下可以像往常一样把这个当做旋转矩阵来建立模型矩阵了
2.2.2 四元数的变换实现(轴角法/直接赋值法)
计算变换矩阵的代码改为
//计算变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 0.0f, 0.0f));
//欧拉角的旋转
//trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
//利用轴角的方式定义四元数
//glm::quat MyQuaternion = angleAxis(glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
//直接给四元数赋值(我们学了四元数,可能在赋值半角这里比较直观体现了吧)
glm::quat MyQuaternion = glm::quat(cos(glm::radians(22.5f)),0,0,sin(glm::radians(22.5f)));
glm::mat4 RotationMatrix = glm::mat4_cast(MyQuaternion);
trans *= RotationMatrix;
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
效果和之前一样,是转了45度以后沿着x轴走
这次的改动只有vertexSource.txt和main.cpp,vertexSource.txt改动已经在上面放出
以下是这个的main.cpp代码
#include <iostream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#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>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.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 // 左上
};
//0 1 2 2 3 0
unsigned int indices[] = {
0,1,2,
2,3,0
};
void processInput(GLFWwindow* window){
if (glfwGetKey(window, GLFW_KEY_ESCAPE )== GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//Open GLFW Window
GLFWwindow* window = glfwCreateWindow(800,600,"My OpenGL Game",NULL,NULL);
if(window == NULL)
{
printf("Open window failed.");
glfwTerminate();
return - 1;
}
glfwMakeContextCurrent(window);
//Init GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glViewport(0, 0, 800, 600);
//glEnable(GL_CULL_FACE);
//glCullFace(GL_BACK);
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
Shader* myShader = new Shader("vertexSource.txt", "fragmentSource.txt");
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
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);
glEnableVertexAttribArray(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);
stbi_set_flip_vertically_on_load(true);
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
int width, height, nrChannel;
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
{
printf("Failed to load texture");
}
stbi_image_free(data);
unsigned int TexBufferB;
glGenTextures(1, &TexBufferB);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
// 为当前绑定的纹理对象设置环绕、过滤方式
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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 加载并生成纹理
unsigned char *data2 = stbi_load("awesomeface.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
{
printf("Failed to load texture");
}
stbi_image_free(data2);
//计算变换矩阵
glm::mat4 trans;
trans = glm::translate(trans, glm::vec3(1.0f, 0.0f, 0.0f));
//trans = glm::rotate(trans, glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
//glm::quat MyQuaternion = angleAxis(glm::radians(45.0f), glm::vec3(0.0, 0.0, 1.0));
glm::quat MyQuaternion = glm::quat(cos(glm::radians(22.5f)),0,0,sin(glm::radians(22.5f)));
glm::mat4 RotationMatrix = glm::mat4_cast(MyQuaternion);
trans *= RotationMatrix;
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
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, TexBufferA);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBufferB);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
myShader->use();
glUniform1i(glGetUniformLocation(myShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(myShader->ID, "ourFace"), 1);
glUniformMatrix4fv(glGetUniformLocation(myShader->ID, "transform"), 1, GL_FALSE, glm::value_ptr(trans));
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}