坐标系统(Coordinate Systems)
- 将坐标转换为标准设备坐标(NDC) 一般是一步一步完成的,在最终将一个图形的顶点转换到NDC之前,我们会经过多个坐标系统。对我们来说比较重要的5种坐标系统是:
- 本地空间或对象空间(Local space or Object space)
- 世界空间(World space)
- 视空间(View space)
- 裁剪空间(Clip space)
- 屏幕空间(Screen space)
1. 坐标空间总览
-
在坐标转换过程中我们使用到多个变换矩阵,其中最重要的就是模型(model),视(view)和投影(projection)矩阵。顶点坐标到屏幕坐标大致的转换流程如下图所示(图片取自书中):
- 本地坐标是与本地原点相关的对象坐标。
- 世界空间坐标是与场景中的全局原点相关的坐标,涉及多个对象。
- 视空间坐标是从相机或观察者角度看到的视界中的坐标。
- 裁剪坐标被转换到-1.0到1.0范围内,并决定了那些顶点会显示在屏幕上。
- 最后,我们将裁剪坐标转换为屏幕坐标。(见第2小节中的视口转换)
视空间也是我们常讲的OpenGL相机(有时也称为相机空间)。视空间是将世界空间坐标转换到用户视角坐标的结果。因此,视空间就是从相机的角度看到的空间。一般将世界空间到视空间的变换组合到一个视矩阵(view matrix)。
在每个顶点着色器运行的结尾,OpenGL期待指定范围内的坐标,而任何处在范围外的坐标将被裁剪(clipped)。被裁剪的坐标将被丢弃,保留下的坐标则成为片元最终显示屏幕上。
将顶点从视空间转换到裁剪空间,会定义一个投影矩阵(projection matrix)。投影矩阵在每个维度指定了一个坐标范围(例如-1000到1000),并只将该范围内的坐标转到NDC,所有该范围外的坐标将被裁剪。
注意:如果一个元图形(primitive),如一个三角形的一部分处在裁剪体积(clipping volume) 之外,OpenGL会将该三角形重建为一个或多个三角形来适应裁剪范围。
投影矩阵所创建的视框(viewing box) 称为截锥(frustum)。
将指定范围内的坐标转换到NDC——可以映射到2D视平面坐标的整个过程称为投影(projection)。
投影矩阵将视坐标转换为裁剪坐标一般采用两种不同的形式,每种都定义一个唯一的截锥:正射投影矩阵(orthographic projection matrix) 和透视投影矩阵(perspective projection matrix)。
-
正射投影矩阵定义一个立方截锥体。我们创建正射投影矩阵需要指定可见截锥体的宽度、高度和长度(图片取自书中)。
正射投影截锥体直接将截锥体内坐标映射为标准设备坐标而不产生任何特殊的副作用,因为它不影响变换矢量的w分部。使用GLM内置函数创建正射投影矩阵如下:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
1. 前两个参数指定截锥体左右的坐标。
2. 第3和4个参数指定截锥体的上下坐标。
3. 第5和6个参数指定远近两个平面之间的距离。
- 现实生活中我们发觉远处的物体会显得较小,这种奇怪的效果我们成为透视(perspective)。
- 透视投影矩阵将指定截锥体范围内的坐标映射到裁剪空间,同时对每个顶点坐标的w分部进行处理,对于离视点越远的顶点坐标,坐标的w分部越大。
- 因为OpenGL要求顶点着色器最终输出的坐标必须在范围-1.0到1.0内,因此一旦坐标映射到裁剪空间,就会将透视除法(perspective division) 应用于坐标:
- 使用GLM创建透视投影矩阵(图片取自书中):
glm::mat4 proj = glm::perspecrive(glm::radians(45.0f), (float)width / (float)height, 0.1f, 100.0f);
1. 第1个参数定义了fov值(视野:field of view),设定视空间的大小,一般设置为45度。
2. 第2个参数设置纵横比(aspect ratio),通过将视口的宽除以高计算得到。
3. 第3和4个参数设置截锥体的远近截面,一般将近截面设置为0.1,将远截面设置为100.0。
2. 组合到一起
- 我们创建一个变换矩阵将上述步骤组合起来:模型,视和投影矩阵。顶点坐标转换到裁剪坐标的过程如下公式所示(注意:矩阵的顺序是相反的):
- OpenGL使用透视除法将裁剪空间坐标转换为标准设备坐标,然后使用
glViewPort
函数的参数将标准设备坐标映射成为屏幕上的点,这个过程称为视口转换(viewport transform)。
3. 开始3D
-
右手坐标系(Right-handed system):指的是正向x轴指向你的右侧,正向y轴指向上且正向z轴指向你后面。如下图所示:
3.1 创建一个模型矩阵:绕x轴旋转
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
- 3.2 创建视矩阵:
glm::mat4 view = glm::mat4(1.0f);
// 注意我们将场景按反方向移动
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
- 3.3 创建投影矩阵
glm::mat4 projection = glm::mat4(1.0f);
projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
- 3.4 修改顶点着色器,定义接受变换矩阵的uniform变量
#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 = aTexCoord;
}
- 3.5 将变换矩阵传递到顶点着色器
unsigned int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
unsigned int viewLoc = glGetUniformLocation(ourShader.ID, "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
unsigned int projectionLoc = glGetUniformLocation(ourShader.ID, "projection");
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
-
3.6 渲染效果
4. 更多3D
4.1 渲染一个立方体
- 设置顶点数据(含36个顶点)
float vertices[] = {
// 1
-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,
// 2
-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,
// 3
-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,
// 4
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,
// 5
-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,
// 6
-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
};
- 修改模型矩阵,让立方体随时间旋转
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
- 使用
glDarwArrays
函数绘制(注意这里没有使用元素缓冲区)
glDrawArrays(GL_TRIANGLES, 0, 36);
-
渲染效果
4.2 改进立方体渲染——深度测试
- OpenGL将深度信息存储在一个缓冲区,叫做z-buffer,也称为深度缓冲区(depth buffer)。
- 每个片元都存储了深度信息(片元的z值),且无论任何片元输出颜色前,OpenGL都会将片元的深度值与z-buffer进行对比,如果当前片元位于其他片元之后则被丢弃,否则覆盖。这个过程称为深度测试(depth test),由OpenGL自动完成。
- 深度测试默认是禁用的,启用代码如下:
glEnable(GL_DEPTH_TEST);
- 因为启动深度测试,因此渲染循环迭代需要与颜色缓冲区一样进行清除:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
启用深度测试后立方体的渲染效果
4.3 绘制多个立方体
- 如果我们要在屏幕显示10个立方体,每个立方体看起来一样只是处于场景中的不同位置,那么我们无需修改缓冲区的顶点数据,唯一需要修改的是将立方体对象转换到世界空间的模型矩阵。
- 为每个立方体定义一个平移矢量
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)
};
- 在渲染循环中使用不同的模型矩阵绘制立方体
glBindVertexArray(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;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
unsigned int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 36);
}
-
渲染效果