1.4 OpenGL渲染管线
OpenGL实现了我们通常所说的渲染管线(rendering pipeline),它是一系列数据处理过程,并且将应用程序的数据转换到最终渲染的图像。图1-2所示为OpenGL 4.5版本的管线。自从OpenGL诞生以来,它的渲染管线已经发生了非常大的改变。
OpenGL首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,这些阶段包括顶点着色、细分着色(它本身包含两个着色器)以及最后的几何着色,然后它将被送入光栅化单元(rasterizer)。光栅化单元负责对所有剪切区域(clipping region)内的图元生成片元数据,然后对每个生成的片元都执行一个片元着色器。
正如你所了解的,对于OpenGL应用程序而言着色器扮演了一个最主要的角色。你可以完全控制自己需要用到的着色器来实现自己所需的功能。我们不需要用到所有的着色阶段,事实上,只有顶点着色器和片元着色器是必需的。细分和几何着色器是可选的步骤。
现在,我们将稍微深入到每个着色阶段当中,以了解更多的背景知识。可以理解,现在的阶段多少会让人感到望而却步,但是请不要担心。通过对一些概念的进一步理解,你将会很快习惯OpenGL的开发过程。
1.4.1 准备向OpenGL传输数据
OpenGL需要将所有的数据都保存到缓存对象(buffer object)中,它相当于由OpenGL维护的一块内存区域。我们可以使用多种方式来创建这样的数据缓存,不过最常用的方法就是使用例1.1中的glNamedBufferStorage()命令同时设置缓存的大小及内容。我们可能还需要对缓存做一些额外的设置,相关的内容请参见第3章。
1.4.2 将数据传输到OpenGL
当将缓存初始化完毕之后,我们可以通过调用OpenGL的一个绘制命令来请求渲染几何图元,例1.1中的glDrawArrays()就是一个常用的绘制命令。
OpenGL的绘制通常就是将顶点数据传输到OpenGL服务端。我们可以将一个顶点视为一个需要统一处理的数据包。这个包中的数据可以是我们需要的任何数据(也就是说,我们自己负责定义构成顶点的所有数据),通常其中几乎始终会包含位置数据。其他的数据可能用来决定一个像素的最终颜色。
第3章会更详细地介绍绘制命令的内容。
1.4.3 顶点着色
对于绘制命令传输的每个顶点,OpenGL都会调用一个顶点着色器来处理顶点相关的数据。根据其他光栅化之前的着色器的活跃情况,顶点着色器可能会非常简单,例如,只是将数据复制并传递到下一个着色阶段,这叫做传递着色器(pass-through shader);它也可能非常复杂,例如,执行大量的计算来得到顶点在屏幕上的位置(一般情况下,我们会用到变换矩阵(transformation matrix)的概念,参见第5章),或者通过光照的计算(参见第7章)来判断顶点的颜色,或者其他一些技法的实现。
通常来说,一个复杂的应用程序可能包含许多个顶点着色器,但是在同一时刻只能有一个顶点着色器起作用。
1.4.4 细分着色
顶点着色器处理每个顶点的关联数据之后,如果同时激活了细分着色器(tessellation shader),那么它将进一步处理这些数据。正如在第9章将会看到的,细分着色器会使用面片(patch)来描述一个物体的形状,并且使用相对简单的面片几何体连接来完成细分的工作,其结果是几何图元的数量增加,并且模型的外观会变得更为平顺。细分着色阶段会用到两个着色器来分别管理面片数据并生成最终的形状。
1.4.5 几何着色
下一个着色阶段(几何着色)允许在光栅化之前对每个几何图元做更进一步的处理,例如创建新的图元。这个着色阶段也是可选的,但是我们在第10章里会体会到它的强大之处。
1.4.6 图元装配
前面介绍的着色阶段所处理的都是顶点数据,此外,这些顶点构成几何图元的所有信息也会被传递到OpenGL当中。图元装配阶段将这些顶点与相关的几何图元之间组织起来,准备下一步的剪切和光栅化工作。
1.4.7 剪切
顶点可能会落在视口(viewport)之外(也就是我们可以进行绘制的窗口区域),此时与顶点相关的图元会做出改动,以保证相关的像素不会在视口外绘制。这一过程叫做剪切(clipping),它是由OpenGL自动完成的。
1.4.8 光栅化
剪切之后马上要执行的工作,就是将更新后的图元传递到光栅化(rasterizer)单元,生成对应的片元。光栅化的工作是判断某一部分几何体(点、线或者三角形)所覆盖的屏幕空间。得到了屏幕空间信息以及输入的顶点数据之后,光栅化单元就可以直接对片元着色器中的每个可变变量进行线性插值,然后将结果值传递给用户的片元着色器。我们可以将一个片元视为一个“候选的像素”,也就是可以放置在帧缓存中的像素,但是它也可能被最终剔除,不再更新对应的像素位置。之后的两个阶段将会执行片元的处理,即片元着色和逐片元的操作。
OpenGL实现光栅化和数据插值的方法是与具体平台相关的。我们无法保证在不同平台上的插值结果总是相同的。
光栅化意味着一个片元的生命伊始,而片元着色器中的计算过程本质上意味着计算这个片元的最终颜色,它绝不等价于OpenGL对这个片元所执行的全部操作。
1.4.9 片元着色
最后一个可以通过编程控制屏幕上显示颜色的阶段叫做片元着色阶段。在这个阶段中,我们使用着色器来计算片元的最终颜色(尽管在下一个阶段(逐片元的操作)时可能还会最终改变一次颜色)和它的深度值。片元着色器非常强大,在这里我们会使用纹理映射的方式,对顶点处理阶段所计算的颜色值进行补充。如果我们觉得不应该继续绘制某个片元,在片元着色器中还可以终止这个片元的处理,这一步叫做片元的丢弃(discard)。
如果我们需要更好地理解处理顶点的着色器和片元着色器之间的区别,可以用这种方法来记忆:顶点着色(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某个片元的颜色应该是什么。
1.4.10 逐片元的操作
除了我们在片元着色器里做的工作之外,片元操作的下一步就是最后的独立片元处理过程。在这个阶段里会使用深度测试(depth test,或者通常也称作z缓存)和模板测试(stencil test)的方式来决定一个片元是否是可见的。
如果一个片元成功地通过了所有激活的测试,那么它就可以被直接绘制到帧缓存中了,它对应的像素的颜色值(也可能包括深度值)会被更新,如果开启了融混(blending)模式,那么片元的颜色会与该像素当前的颜色相叠加,形成一个新的颜色值并写入帧缓存中。
从图1-2中可以看到,像素数据的传输也有一条路径。通常来说,像素数据来自图像文件,尽管它也可能是OpenGL直接渲染的。像素数据通常保存在纹理贴图当中,通过纹理映射的方式调用。在纹理阶段我们可以从一张或者多张纹理贴图中查找所需的数据值。我们将在第6章了解有关纹理映射的内容。
现在我们已经了解OpenGL管线的基础知识,接下来回到例1.1,用渲染管线的方式讲解其中的操作。