前言
你需要记住的是:在OpenGL中图形渲染的本质,是将顶点数据经过渲染管线处理后组成我们想要的图像数据并展示在屏幕对应的位置上。
背景知识
对于初次接触OpenGL程序员来说,它的坐标系统是相对难以理解的概念。了解过OpenGL渲染机制的应该知道,一个图形要完成渲染并最终呈现在屏幕上,需要将其坐标转换到不同的坐标系并在其中完成相关的操作才能被显示。之所以要进行这些变换,是因为OpenGL有个非常重要的特点,那就是只将NDC坐标(x,y,z)位于[-1,1]之间的顶点进行渲染,而超出此范围之外的图元则会被裁剪,不会出现在屏幕上。
试想一下如果OpenGL只有一个坐标系的话,想让图像显示在设备的屏幕上就变得十分困难。不仅要将其顶点的坐标范围限制在[-1,1]之间,图像的顶点坐标和屏幕像素之间的映射也会难以处理。即便有方案处理这种映射关系,来简单实现场景里的2D图形渲染,但对于复杂的3D图形及动画效果,就显得捉襟见肘了。
假如有一种方案,我们只需要输入物体的各个顶点坐标而不必限制其在[-1,1]之间,这些坐标只是用来组成一个物体(暂不考虑纹理、法线坐标),我们可以随意的对这些顶点进行编辑来改变它的形状大小,而不会影响到其他的物体。然后通过某种方式将各个物体组织起来,形成一个大的场景。这个场景能够正确的显示在屏幕上。我们还希望动态的改变这些物体的位置、并且能够从不同的方向上来观察这个场景,而对于超出屏幕之外的物体不进行渲染。
这种方案看起来还不错。它基本上包含了我们能够想到的所有情况了。没错,OpenGL正是采用的这个思路。那么它又是如何将这种思路付诸实践的呢?OpenGL借鉴了流水线的思路,在不同的处理阶段使用不同的坐标系(也叫坐标空间),专注完成特定的功能。这样不仅能简化操作,还能提高渲染效率。
1. 初识坐标系统
我们以一张经典的坐标系统示例图开始:
图1-1
如图1-1所示,共有五个坐标空间,分别是:
- 局部空间
- 世界空间
- 观察空间
- 裁剪空间
- 屏幕空间
接下来一一讲解下每个空间的作用和使用场景。
开始之前,先简单说明下坐标系。OpenGL中的坐标系是笛卡尔坐标系,也称为参考系。在坐标系中通过坐标可以准确清晰的描述物体位置,运动方向,物体之间的位置关系等。这一点在图形学中非常重要。
1.1 局部空间
图 1.1-1
如图1.1-1所示,局部空间是基于物体自身的坐标系,一般是以自身的中心(当然这不是固定的)为原点,会根据实际的场景动态调整。
图 1.1-2
如图1.1-2所示,以2D空间的三角形为例,它的中心为局部坐标系的原点,这样我们就可以通过顶点坐标来描述三角形的三个顶点在局部坐标系中的位置了。
顶点1的坐标(0, 0.5, 0),顶点2的坐标(-0.5, -0.25,0), 顶点3的坐标为(0.5, -0.25, 0)。在局部坐标系中顶点坐标(x,y,z)可以是任意的, 你可以设置为(10,20,-100)都没有问题。
功能: 用来描述物体的各个顶点之间的位置及关系,基于这个坐标空间,可以非常方便的控制物体的形状大小,而不影响到其他的物体。
1.2 世界空间
世界空间的坐标系如下图1.2-1所示。
图 1.2-1
它是用来承载场景中的所有物体的空间。各个物体经过各种平移、旋转或者缩放之后,摆放在世界空间当中。
图 1.2-2
如图1.2-2所示,世界空间中有两个经过旋转和缩放后的立方体,静静地展示在各自的位置上。
在局部空间中,立方体的顶点A坐标可能为(0.5,0.2,1.5)。一旦它转换到世界空间中,顶点A的坐标就变成基于世界坐标系原点的坐标了,此时顶点A在世界空间中的坐标可能为(2.0, 10, 0.2)。如果我们对所有的顶点都应用同样的转换,那么立方体上所有的顶点都变成了基于世界空间原点的坐标了。
因为立方体的所有顶点都是经过相同的转换,意味着各顶点之间的位置关系并没有发生变化,立方体的形状也不会因此而改变。
功能: 是物体所处的最终空间,它用来描述场景中各个物体的位置的,不会改变物体自身属性,
1.3 观察空间
但是世界空间只是决定了各个物体在场景中的位置,它无法决定屏幕上显示哪个物体。决定渲染结果的是观察空间+投影形成的视椎体+硬件裁剪。
观察空间(也叫摄像机空间,它的原理就像我们用摄像机拍摄整个场景一样)顾名思义是一种基于观察点的坐标系。它用来描述当我们基于观察点来观察整个场景时,场景中各个物体以及物体上各个顶点的坐标的。
图 1.3-1
讨论一个坐标系之前,首先需要明确它的原点和坐标轴的方向。如图1.3-1所示,原点在摄像机的图片的位置,三个坐标轴互相垂直,注意z轴的正方向的朝向。蓝色部分对应着整个场景,也就是前面所说的世界空间。
思考一个问题,我们为什么需要摄像机空间?
回到上面提到的世界空间,它是物体最终变换到的空间。也就是说,一旦物体确定了在世界空间中的位置,除非我们去修改,否则它的位置也就固定下来了。因为在背景知识中介绍的原因,很多的物体可能被裁剪而不显示在屏幕上。如果我们想要看到屏幕之外的物体,就需要手动的修改每一个物体的世界坐标将它们移动到屏幕内,同时还需要保持原有的位置关系。这不仅要消耗大量的CPU算力,物体间的位置关系的计算也变得极其困难。
现实世界中当我们的眼睛看不到一个物体时(可能这个物体被其他的物体遮挡,也可能这个物体不在我们的视线范围内)是怎么做的呢?我们会换一个位置和角度就能够解决了。在OpenGL中也一样,我们只需要换个位置和角度去观察整个场景,就能够看到那些不可见的物体了。
有了观察空间,我们只需要修改摄像机的位置、方向就可以观察到原本看不见的物体了。
作用: 用来决定屏幕上最终渲染的效果。
1.4 裁剪空间
前面说过,OpenGL有个特点就是只显示NDC坐标在[-1,1]范围内的顶点,那观察空间中物体的的坐标,是如何变成NDC的坐标的呢?我们需要借助裁剪空间来完成这样的转换。
首先你要知道的是裁剪空间的坐标是一个齐次坐标(x,y,z,w),齐次坐标的x、y、z分量除以w分量后,才会得到NDC坐标(x/w, y/w, z/w).观察空间的三维坐标的w分量默认为1,而裁剪空间中的齐次坐标的w分量是通过投影矩阵计算得到的。
裁剪空间的有效范围是[-w,w]之间,超出这个范围就会被裁剪掉。
在介绍观察空间的时候,我们提到了视椎体。那视椎体具体是什么东西呢?
图 1.4-1
如图1.4-1所示,视锥体由近裁剪面、远裁剪面以及四周侧面合围而成,透视投影为四棱台形态,正交投影为长方体柱体,该几何体定义了观察空间的理论可视范围。。我们可以根据远近平面来生成投影矩阵,来最终计算出裁剪空间中的齐次坐标的w分量。
作用: 根据视椎体空间的大小,生成齐次坐标的w分量,来得到NDC坐标。
1.5 屏幕空间
这是OpenGL的渲染管线中,光栅化之前的最后一个坐标空间。也是GPU根据视口变换,将NDC坐标转成成屏幕上真实窗口的像素坐标的过程。x、y代表在屏幕上的位置,z代表它的深度值(深度测试会讲到).这一阶段是GPU自动完成的,我们不必深究其细节。
作用: 屏幕空间的唯一目的就是将NDC坐标转成像素坐标,也就是图像在屏幕上最终显示的位置。
总结
至此,OpenGL中的坐标空间的特点、使用场景以及作用差不多梳理完了。从CPU中输入顶点坐标开始,到最终在屏幕上显示,整个流程大概是这样的:
模型空间 → 世界空间 → 观察空间 → 裁剪空间 → 透视除法 → NDC [-1,1] 立方体 → 视口变换 → 屏幕空间.
举例
文章的最后,举个例子来巩固一下我们所学习的内容。如果我们要在屏幕上展示一个三角形,大概要经历以下几个步骤:
- 编码CPU通过向GPU中传入三角形的顶点坐标。
- 顶点着色器接收到顶点坐标后,经过坐标转换(MVP矩阵处理),得到裁剪坐标(齐次坐标:x,y,z,w)。
- 图元装配时将顶点数据组装成具体的几何图形,并在装配完成后,执行透视除法得到NDC坐标([-1,1]),随后进行视锥体裁剪,剔除图元屏幕外的部分。
- 光栅器在生成片段的过程中,结合视口转换(将NDC坐标映射到屏幕窗口坐标),决定片段在屏幕上的具体位置。
- 片段着色器对片段进行上色(颜色插值或者从纹理采样)
- 进行模板、深度和混合测试后,将片段对应的数据写入帧缓冲区
- 渲染到屏幕