7 - 坐标空间
本文介绍了OpenGL坐标系统的一些知识,包括各种坐标系和MVP变换,然后进入3D世界,介绍Z-Buffer等知识。
坐标系统简介
有关坐标系统和MVP变换的详细内容,可在GAMES101相关文章中找到。
- 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
- 将局部坐标变换为世界空间坐标(M变换),世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
- 将世界坐标变换为观察空间坐标(V变换),使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
- 将观察坐标投影为裁剪坐标(P变换)。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
- 将裁剪坐标变换为屏幕坐标(视口变换)。视口变换将位于-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)