本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
坐标系
在之前的章节我们学习了使用矩阵变换来操作顶点。在OpenGL中,要求所有的可见顶点的坐标在通过顶点着色器阶段后都是标准化设备坐标。我们的做法往往是自定义一片坐标空间,然后进行标准化操作。但在将顶点坐标标准化之前,我们往往会先将坐标转化为其它的一些中间坐标系的坐标,因为在进行一些特定操作的时候,在特定的坐标空间下会非常方便。这里列出5种重要的坐标系统:
- 局部空间(local space or object space)
- 世界空间(world space)
- 视图空间(view space or eye space)
- 切割空间(clip space)
- 屏幕空间(screen space)
流程
为了将坐标从一个空间转化到另一个空间,我们使用model,view,projection三个矩阵。我们的顶点坐标首先定义在局部空间,称为局部坐标,之后转到世界坐标,视图坐标,切割坐标,最终结束在屏幕坐标。下图是这段流程的介绍:
- 一个物体以自己的局部坐标作为开始。
- 经过model矩阵变换,获取在世界空间中的坐标。
- 接着通过view矩阵,获取视图空间,即摄像机视角的坐标。
- 然后通过projection矩阵,投影变换,获得在切割空间的坐标,如果不在-1到1范围内的点将被舍弃。
- 最后通过ViewPort中定义的分辨率参数将-1到1的坐标进行转换,最终的坐标将输入到光栅化阶段进行片元操作。
就像之前说的,定义这些流程操作是因为在特定的空间进行某些操作会很方便。当然我们也可以自定义矩阵来进行任意的空间转换,只是可能缺少灵活性。
局部空间
想象一下自己在类似于maya的三维建模软件中建立一个单独的模型,坐标往往在原点位置,这里就类似与局部空间坐标,之后我们就会进行场景摆放的时候就会放在模型应在的位置。假设所有建立的模型初始都在原点,那么模型就在局部空间。
世界空间
在制作动画或游戏时,我们需要将特定的模型先放在特定的位置,这里的位置就不一定是局部空间的原点了。为了进行这种变换,我们会使用上一章节的平移、旋转、缩放等操作,组合成一个名为model的矩阵。
视图空间
视图空间是我们使用摄像头进行观察到的世界。从世界空间转化到视图空间我们使用与摄像头相关的平移和旋转矩阵进行操作,我们将这些信息存储在名为view的矩阵中。
切割空间
在每个顶点着色器运行结束时,OpenGL会剔除位于特定范围外的点,这往往是通过NDC的范围来实现的。由于手动将所有的坐标都定义在-1到1的范围内不太聪明,所以我们会随意定义坐标并通过特定的变化转化到NDC的范围内,这个矩阵被称为projection。这个矩阵会将最终在屏幕上显示的顶点坐标转化到NDC的范围内,其它的将被剔除。(注意,比如一个三角形的一个角定义在屏幕显示区域外,OpenGL会自己沿边缘进行重建。)投影矩阵会创立一个视锥体的模型,不在视锥体范围内的将不会显示。在转换结束后,在切割空间中我们会进行透视除法的操作,通过将每个顶点向量的xyz与w相除。用projection矩阵进行变换时,有两种构成projection矩阵的方式,一种是正交投影,一种是透视投影。
正交投影
正交投影定义的视锥体如下,我们定义的近平面和远平面大小一样:
正交投影不会影响顶点向量的w组件。
我们可以使用GLM定义一个正交投影矩阵:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前两个参数代表视锥体左和右的位置(并定义宽度),第三个第四个参数代表下和上的位置(并定义高度),第五个参数代表视锥体近平面的距离,第六个参数代表视锥体远平面的距离。
透视投影
透视投影会操作顶点向量的w组件,离摄像机越远,w组件越大。下面是一个透视投影的视锥体:
我们可以使用GLM定义第一个透视投影矩阵:
glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.f);
第一个参数定义上图的FOV量,即透视角度。一般设为45度。第二个参数代表屏幕比,通过viewport的参数进行设置。最后两个参数同样代表近平面和远平面的距离。
作为补充,这篇文章
详细讲解了正交投影和透视投影矩阵的计算方法。
将所有变换组合起来
一个切割空间的顶点向量如下:
注意,我们从右往左阅读矩阵的乘法。最终的结果将会在顶点着色器中赋予给gl_Position,OpenGL会自动进行透视除法和切割。
渲染3D物体
我们以上一章的带纹理的平面为基础进行以下操作。首先定义一个model矩阵,。我们让平面绕x轴旋转一下,让它看起来躺在地面上:
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm:;radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
接着我们创建一个view矩阵。我们将物体向屏幕外移动一下,,由于OpenGL是右手坐标系,所以我们向+z轴移动。但在视图空间中,我们可以将摄像机向屏幕内移动来实现,也就是向-z移动:
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
最后创建一个projection矩阵,上面已经给出例子里,这里为了模拟立体效果,我们使用透视投影:
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
下面是在顶点着色器中的针对gl_Position的修改:
#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
...
}
同样,我们要对uniform变量进行赋值:
int modelLoc = glGetUniformLocation(ourShader.ID, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
...// 其余同理
运行修改后的程序结果应该如下:
绘制立方体
接下来我们绘制立方体,我们暂时不使用EBO,这里使用36个顶点来绘制立方体。
我们让立方体随时间旋转:
model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));
使用glDrawArrays绘制立方体:
glDrawArrays(GL_TRIANGLES, 0, 36);
结果是这样:结果
我们会发现一个问题,在某些时候,前后面会重叠,无法判断谁在前谁在后。应对这种情况,我们可以使用OpenGL存储深度信息的z-buffer进行深度检测。
Z-bufer
z-buffer是OpenGL自动创建的,它存储深度信息。在渲染时,OpenGL会通过z-buffer来比较片元的深度。如果当前的片元在另一个片元后面,它就会被忽略,在前面的话的就会覆盖后面的片元。这种操作被称为深度检测,OpenGL可以自动执行这一操作。当然,深度检测默认关闭,所以我们通过glEnable开启深度检测:
glEnable(GL_DEPTH_TEST);
和颜色缓冲一样,我们也需要每帧清除一次深度缓冲来更新深度:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
运行程序结果应该就正常了:结果
最后,请多多参考原文:https://learnopengl.com/Getting-started/Coordinate-Systems