OpenGL中级篇(一)
坐标系统概述
将坐标变换为标准化设备坐标,再转化为屏幕坐标的过程通常是分步进行的,类似于流水线。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统
比较重要的总共有5个不同的坐标系统:
从构造模型的局部坐标系 (Local / Object Space) 经过一系列处理最终渲染到屏幕坐标 (Screen Space) 下,这过程中需要经历的5个坐标系统。
- 局部空间 (Local Space,或称物体空间 (Object Space))
- 世界空间 (World Space)
- 观察空间 (View Space,或称视觉空间 (Eye Space) )
- 裁剪空间 (Clip Space)
- 屏幕空间 (Screen Space)
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,分别是:
- 模型矩阵(Model Matrix)
- 观察矩阵(View Matrix)
- 投影矩阵(Projection Matrix)
顶点坐标起始于局部空间,在这里它称为局部坐标(Local Coordinate),之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),最后以屏幕坐标(Screen Coordinate)的形式结束。
裁剪空间
由投影矩阵创建的观察箱 (Viewing Box) 被称为平截头体 (Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程被称之为投影 (Projection),因为使用投影矩阵能将3D坐标投影 (Project) 到2D的标准化设备坐标系中。
一旦所有顶点被变换到裁剪空间,最终的操作——透视除法 (Perspective Division) 将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;
透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。
在这一阶段之后,最终的坐标将会被映射到屏幕空间中,并被变换成片段。
将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正交投影矩阵 (Orthographic Projection Matrix) 或一个透视投影矩阵 (Perspective Projection Matrix)。
透视投影
透视投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在 - w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在 - 1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:
顶点坐标的每个分量都除以w分量,距离观察者越远顶点坐标就越小。这也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。
glm::mat4 proj = glm::perspective(glm::radians(45.0f),
(float)width / (float)height,
0.1f,
100.0f);
第一个参数定义了视野(Field of View)的值。如果想要一个真实的观察效果,它的值通常设置为45.0f。
第二个参数设置了宽高比,由视口的宽除以高所得。
第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
当把透视矩阵的 near 值设置太大时(如10.0f),处于0.0f和10.0f之间的物体都会裁剪掉,靠近摄像机的坐标也会裁剪掉,因此,会导致一个在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候视线会直接穿过去。
正交投影
平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。
正交平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。
glm::mat4 proj =
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
- 第一、第二个参数指定了平截头体的左右坐标
- 第三、第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小。
- 第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。
正交投影和透视投影的对比
可以看到,使用透视投影的话,远处的顶点看起来比较小,而在正交投影中每个顶点距离观察者的距离都是一样的。
变换矩阵组合
在上面每一个步骤中,都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:
注意:矩阵运算的顺序是相反的(需要从右往左阅读矩阵的乘法)。最后的顶点被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。
绘制3D平面
创建模型矩阵
开始进行3D绘图时,首先创建一个模型矩阵。这个模型矩阵包含了位移、缩放与旋转操作,它们会被应用到所有物体的顶点上,以变换它们到全局的世界空间。变换一下平面,将其绕着x轴旋转,使它看起来像放在地上一样。模型矩阵创建如下:
通过将顶点坐标乘以这个模型矩阵,将该顶点坐标变换到世界坐标。平面看起来就是在地板上,代表全局世界里的平面。
创建观察矩阵
接下来需要创建一个观察矩阵。想要在场景里面实现稍微往后移动的效果,以使得物体变成可见的(当在世界空间时,我们位于原点(0, 0, 0))。
通过观察矩阵,因为我们想要往后移动,以相反于摄像机移动的方向移动整个场景,并且OpenGL是一个右手坐标系(Right - handed System),所以通过将场景沿着z轴负方向平移来实现。
创建投影矩阵
因为在场景中需要使用透视投影,所以声明一个投影矩阵如下:
变换矩阵传入着色器
创建好变换矩阵后,需要将它们传入着色器。首先,在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:
最后将矩阵传入着色器(这通常在每次的渲染迭代中进行,因为变换矩阵会经常变动):
首先用 glGetUniformLocation()查询了 uniform 变量地址。
然后用 glUniformMatrix4fv()把矩阵数据发送给着色器。
最终效果
渲染一个立方体
若想渲染一个立方体,一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点)。且让渲染的立方体随着时间旋转:
model = glm::rotate(model,
(float)glfwGetTime() * glm::radians(50.0f),
glm::vec3(0.5f, 1.0f, 0.0f));
使用glDrawArrays()来绘制立方体,总共有36个顶点。
glDrawArrays(GL_TRIANGLES, 0, 36);
Z缓冲
OpenGL存储它的所有深度信息于一个Z缓冲(Z - buffer) 中,也被称为深度缓冲 (Depth Buffer) 。
GLFW会自动生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。
首先要在OpenGL内启用深度测试;它默认是关闭的。可以通过glEnable()来开启深度测试。glEnable()和glDisable()允许我们启用或禁用某个OpenGL功能。这个功能会一直保持启用 / 禁用状态,直到另一个调用来禁用 / 启用它。现在想启用深度测试,需要开启GL_DEPTH_TEST:
接着,因为使用了深度测试,所以要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,可以通过在glClear()中指定DEPTH_BUFFER_BIT来清除深度缓冲: