7 - 坐标空间

本文介绍了OpenGL坐标系统的一些知识,包括各种坐标系和MVP变换,然后进入3D世界,介绍Z-Buffer等知识。

坐标系统简介

有关坐标系统和MVP变换的详细内容,可在GAMES101相关文章中找到。

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 将局部坐标变换为世界空间坐标(M变换),世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 将世界坐标变换为观察空间坐标(V变换),使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 将观察坐标投影为裁剪坐标(P变换)。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 将裁剪坐标变换为屏幕坐标(视口变换)。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅化器,将其转化为片段。

投影

为了将顶点坐标从观察空间变换到裁剪空间,需要进行投影变换,主要有两种投影(详细介绍可在GAMES101的相关文章中找到):

正交投影

正交投影的示意图如下:

上面的视锥体定义了可见的坐标,由宽、高、近平面和远平面所指定。任何出现在近平面前或远平面后的坐标都会被裁剪掉。

可以通过glm::ortho创建一个正交投影矩阵:

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

前两个参数指定视锥体的左右坐标,第三、四个参数指定视锥体的底部和头部,最后两个参数定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。

透视投影

透视投影的示意图如下:

可以通过glm::perspective()来创建一个透视投影矩阵:

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

其中,第一个参数指定fov,第二个参数指定宽高比,最后两个参数分别指定近、远平面的位置。

进入3D世界

知道如何将模型的3D坐标通过MVP变换和视口变换转换成屏幕2D坐标后,我们就能真正使用3D物体了,这里以前面画的小矩形平面为例:

MVP变换

首先,我们先创建一个模型矩阵(M),这个矩阵通常包含位移、缩放与旋转操作,并把顶点坐标转换为世界坐标。这里我们绕x轴旋转,让要画的平面躺在地板上:

glm::mat4 model;
// 先让他绕x轴旋转
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));

然后我们再创建一个观察矩阵(V),让场景朝-z方向移动,以让我们看清楚:

glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));

最后创建一个透视投影矩阵(P)即可:

glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(45.0f), static_cast<float>(SCR_WIDTH / SCR_HEIGHT), 0.1f, 100.0f);

创建好MVP变换的矩阵后,将它们传入顶点着色器:

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	// 注意矩阵乘法是从右往左乘
	
	gl_Position = projection * view * model * vec4(aPos, 1.0);
	ourColor = aColor;
	TexCoord = aTexCoord;
}
int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
int viewLoc = glGetUniformLocation(ourShader.ID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
int projLoc = glGetUniformLocation(ourShader.ID, "projection");
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));

最后结果如下:

深度测试

接下来我们画一个立方体(去掉颜色数据):

// 定义顶点和索引信息
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,  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,  1.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.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,  0.0f, 0.0f,
    0.5f, -0.5f, -0.5f,  1.0f, 1.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,  1.0f, 1.0f
};

unsigned int indices[] = {
    // 注意索引从0开始! 
    0, 1, 2, 2, 3, 0,
    4, 5, 6, 6, 7, 4,
    8, 16, 9, 9, 4, 8,
    10, 2, 11, 11, 12, 10,
    9, 13, 5, 5, 4, 9,
    3, 2, 10, 10, 14, 15

};

然后让它随着时间旋转:

model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));

发现效果是这样的,不是我们想要的立方体:

由于没有用Z-buffer进行深度测试,导致openGL不知道像素间是怎么被覆盖的。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。

启动深度测试要修改两个地方:

// 渲染前开启深度测试
glEnable(GL_DEPTH_TEST);

// 渲染时清除深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

最终我们得到了满意的结果:

更多立方体

接下来我们一口气画十个立方体。

首先,我们定义它们在世界空间中的位置:

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)  
};

然后把他们画出来:

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;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    int modelLoc = glGetUniformLocation(ourShader.ID, "model");
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}

最终结果如图:

参考资料

  • 坐标系统 - LearnOpenGL CN (learnopengl-cn.github.io)