前言
坐标转换
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。
下面一个Cube从模型坐标本投射到屏幕坐标上完整流程展示:
顶点位置和法向量等几何数据在OpenGL流水线中通过顶点操作和原始装配操作进行转换,然后再进行网格化处理。
渲染流水线中顶点的空间变换过程:
局部空间
局部空间(Local Space,或者称为物体空间(Object Space)),是指物体所在的坐标空间,即对象最开始所在的地方。
你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。
世界空间
世界空间(World Space):是指顶点相对于(游戏)世界的坐标。通常,我们会把世界空间的原点放置在游戏空间的中心。
顶点变换的第一步
如果将顶点坐标从模型空间变换到世界空间中,需要通过模型变换 (model transform)
、模型矩阵(Model Matrix)
来实现。
具体做法:
- 先将物体的本地坐标系转成世界坐标系中,用变换矩阵表示;
- 然后,物体本地坐标*变换矩阵=物体世界坐标;
这里有个B站视频方便理解:【CTO教程系列】计算机图形管线 3.模型变换
观察空间
观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。
观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。
观察空间和屏幕空间区别:观察空间是一个三维空间 ,而屏幕空是一个二维空间。从观察空间到屏幕空间的转换需要经过一个操作, 那就投影 (projection) 。
顶点变换的第二步
是将顶点坐标从世界空间变换到观察空间中,这个变换通常叫做观察变换 (view transform)。
这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)
里,它被用来将世界坐标变换到观察空间。
这里有个B站视频方便理解:【CTO教程系列】计算机图形管线 4.ModelView变换
需要注意的是在OpenGL观察空间使用的是右手坐标系,因此需要在z轴值取反操作。
裁剪空间
裁剪空间(Clip Space)
在一个顶点着色器运行的最后,OpenGL所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。
被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。
可以使用投影矩阵(Projection Matrix)
将顶点坐标从观察变换到裁剪空间。
投影矩阵可以为两种不同的形式:
正交投影
正交投影定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。平行投影,没有远近距离关系。
它的平截头体看起来像一个容器:
正交投影矩阵(Orthographic Projection Matrix)
要创建一个正交投影矩阵,我们可以使用GLM的内置函数glm::ortho:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
正交投影矩阵直接将坐标映射到2D屏幕上,但会产生不真实的结果。
很多地方也会用的到,比如游戏中王者、LOL的小地图,2D游戏等等。
这里有个B站视频方便理解:
【CTO教程系列】计算机图形管线 9.正交投影 in OpenGL
透视投影
透视投影是一种更贴近实际生活的投影,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵(Perspective Projection Matrix)来完成的。
这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。
OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:
o u t = ( x / w y / w z / w ) out= \begin{pmatrix} x/w \\ y/w \\ z/w \end{pmatrix} out= x/wy/wz/w
顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。
在GLM中可以这样创建一个透视投影矩阵:
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
同样,glm::perspective所做的其实就是创建了一个定义了可视空间的大平截头体,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。
侧切面:
这里有个B站视频方便理解:
【CTO教程系列】计算机图形管线 12.透视投影 in OpenGL
屏幕空间
屏幕空间(Screen Space)
由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上
OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。
总结
上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:
V c l i p = M p r o j e c t i o n ⋅ M v i e w ⋅ M m o d e l ⋅ V l o c a l V_{clip}=M_{projection}⋅M_{view}⋅M_{model}⋅V_{local} Vclip=Mprojection⋅Mview⋅Mmodel⋅Vlocal
注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。
OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。
OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点。这个过程称为视口变换。
这里有个B站视频方便理解:
【CTO教程系列】计算机图形管线 14.视口变换 in OpenGL】
扩展实践:3D绘图
深度缓冲(Depth Buffer)
OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。
深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing)
,它是由OpenGL自动完成的。
现在我们想启用深度测试,需要开启GL_DEPTH_TEST:
glEnable(GL_DEPTH_TEST);
因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
实践
-
启用深度测试:
glEnable(GL_DEPTH_TEST);
-
构建和编译着色程序:
- 顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoord; out vec2 TexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0f); TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y); }
- 片元着色器
#version 330 core out vec4 FragColor; in vec2 TexCoord; uniform sampler2D texture1; uniform sampler2D texture2; void main() { FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2); } ```
-
设置顶点数据(和缓冲区)并配置顶点属性
要想渲染一个立方体,我们一共需要36个顶点:// 1.设置立方体顶点输入 一共需要36个顶点 float vertices[] = {...}; // 立方体的世界空间位置 glm::vec3 cubePositions[] = {...};
-
清除深度缓冲,创建变换、观察矩阵,投影矩阵
... // 渲染 // 清除颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 清除深度缓冲 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 在相应的纹理单元上绑定纹理 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); ...
-
创建变换、观察矩阵,投影矩阵
- 正交投影
... ourShader.use(); // 创建变换、观察矩阵,投影矩阵 glm::mat4 view = glm::mat4(1.0f); // 模型矩阵 view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); // 正交投影 glm::mat4 ortho = glm::mat4(1.0f); ortho = glm::ortho(-width / 200.0f, width / 200.0f, -height / 200.0f, height / 200.0f, 0.1f, 100.0f); // 正交投影 ourShader.setMat4("projection", ortho); // 观察矩阵 ourShader.setMat4("view", view); ...
- 透视投影
... ourShader.use(); // 创建变换、观察矩阵,投影矩阵 glm::mat4 view = glm::mat4(1.0f); // 模型矩阵 view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); // 透视投影 glm::mat4 projection = glm::mat4(1.0f); // 投影矩阵 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); // 透视投影 // 将矩阵传递给着色器 ourShader.setMat4("projection", projection); // 观察矩阵 ourShader.setMat4("view", view); ···
-
绘制立方体
... // 绘制三角形 glBindVertexArray(VAO); // 绑定VAO for (unsigned int i = 0; i < 10; i++) { glm::mat4 model = glm::mat4(1.0f); // 模型矩阵 model = glm::translate(model, cubePositions[i]); float angle = 20.0f * i; angle = (float)glfwGetTime() * 25.0f; model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f)); ourShader.setMat4("model", model); glDrawArrays(GL_TRIANGLES, 0, 36); // 绘制立方体 } ...
完整源码
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <shader_s.h>
void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
GLFWwindow *window;
int main()
{
InitGLFW(); // 初始化GLFW
bool isCreated = CreateWindow(); // 创建一个窗口对象
if (!isCreated)
return -1;
bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!isGLAD)
return -1;
// 启用深度测试
glEnable(GL_DEPTH_TEST);
// 构建和编译着色程序
Shader ourShader("shader/P1_Basic/06_Coordinate/coordinate.vs", "shader/P1_Basic/06_Coordinate/coordinate.fs");
// 设置顶点数据(和缓冲区)并配置顶点属性
// 1.设置立方体顶点输入 一共需要36个顶点
float vertices[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f};
// world space positions of our cubes
glm::vec3 cubePositions[] = {
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3(2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3(1.3f, -2.0f, -2.5f),
glm::vec3(1.5f, 2.0f, -2.5f),
glm::vec3(1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)};
// 2.设置索引缓冲对象
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO); // 生成一个VAO对象
glGenBuffers(1, &VBO); // 生成一个VBO对象
glBindVertexArray(VAO);
// 绑定VAO
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中
// 4.配置位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// 5. 纹理属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// 加载和创建纹理
unsigned int texture1, texture2;
// 加载第一张纹理图片
glGenTextures(1, &texture1); // 生成纹理
glBindTexture(GL_TEXTURE_2D, texture1); // 绑定纹理
// 设置纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // x轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // y轴
// 设置纹理过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大
// 加载纹理图片,创建纹理和生成多级渐远纹理
int width, height, nrChannels;
stbi_set_flip_vertically_on_load(true); // stb_image.h能够在图像加载时帮助我们翻转y轴
unsigned char *data = stbi_load("image/04_Textures/container.jpg", &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);
// 加载第二张纹理图片
glGenTextures(1, &texture2); // 生成纹理
glBindTexture(GL_TEXTURE_2D, texture2); // 绑定纹理
// 设置纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // x轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // y轴
// 设置纹理过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 缩小
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大
// 加载纹理图片,创建纹理和生成多级渐远纹理
data = stbi_load("image/04_Textures/awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
// 注意,awesomeface.png具有透明度,因此有一个alpha通道,所以一定要告诉OpenGL数据类型是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 | GL_DEPTH_BUFFER_BIT);
// 在相应的纹理单元上绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
ourShader.use();
// 创建变换、观察矩阵,投影矩阵
glm::mat4 view = glm::mat4(1.0f); // 模型矩阵
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
// 透视投影
glm::mat4 projection = glm::mat4(1.0f); // 投影矩阵
projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); // 透视投影
// 将矩阵传递给着色器
ourShader.setMat4("projection", projection);
// 正交投影
// glm::mat4 ortho = glm::mat4(1.0f);
// ortho = glm::ortho(-width / 200.0f, width / 200.0f, -height / 200.0f, height / 200.0f, 0.1f, 100.0f); // 正交投影
// ourShader.setMat4("projection", ortho);
// 观察矩阵
ourShader.setMat4("view", view);
// 绘制三角形
glBindVertexArray(VAO); // 绑定VAO
for (unsigned int i = 0; i < 10; i++)
{
glm::mat4 model = glm::mat4(1.0f); // 模型矩阵
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
angle = (float)glfwGetTime() * 25.0f;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36); // 绘制立方体
}
// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}
// 可选:一旦资源超出其用途,就取消分配所有资源:
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// 释放/删除之前的分配的所有资源
glfwTerminate();
return 0;
}
void InitGLFW()
{
// 初始化GLFW
glfwInit();
// 配置GLFW 第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
// 第二个参数接受一个整型,用来设置这个选项的值。
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
}
bool CreateWindow()
{
// 创建一个窗口对象
window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
// 创建失败,终止程序
glfwTerminate();
return false;
}
// 将我们窗口的上下文设置为当前线程的主上下文
glfwMakeContextCurrent(window);
// 设置窗口大小改变时的回调函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
return true;
}
bool InitGLAD()
{
// 初始化GLAD,传入加载系统相关opengl函数指针的函数
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
// 初始化失败,终止程序
return false;
}
return true;
}
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// 设置窗口的维度
glViewport(0, 0, width, height);
}
// 输入
void processInput(GLFWwindow *window)
{
// 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
// 关闭应用程序
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
效果图
正交投影
透视投影