- OpenGL ES 3.x游戏开发(上卷)
- 吴亚峰
- 6953字
- 2020-06-28 05:37:03
3.2 着色器与渲染管线
前面一节已经通过一个旋转三角形的案例为读者简单介绍了如何使用OpenGL ES 3.0进行3D场景的开发,相信读者对OpenGL ES 3.0应用的开发已经有了初步的了解。但要想真正地掌握OpenGL ES 3.0,则首先要掌握着色器和渲染管线相关的知识,本节将向读者介绍这方面的内容。
3.2.1 OpenGL ES 2.0的渲染管线
学习OpenGL ES 3.0的渲染管线之前,应该首先了解一下OpenGL ES 2.0的渲染管线,这对于进一步深入了解OpenGL ES 3.0的渲染管线是很有裨益的,本小节将对此内容进行介绍。
渲染管线有时也称为渲染流水线,一般是由显示芯片(GPU)内部处理图形信号的并行处理单元组成。这些并行处理单元两两之间是相互独立的,在不同型号的硬件上独立处理单元的数量也有很大的差异。一般越高端型号的硬件,其中独立处理单元的数量也就越多。
提示
与普通应用程序通过CPU串行执行不同的是,渲染工作是通过渲染管线中多个相互独立的处理单元进行并行处理的,这种模式极大地提升了渲染效率。
从另一个角度看,OpenGL ES中的渲染管线实质上指的是一系列绘制过程。这些过程输入的是待渲染3D物体的相关描述信息数据,经过渲染管线,输出的是一帧想要的图像。OpenGL ES 2.0的渲染管线如图3-16所示。
▲图3-16 OpenGL ES 2.0可编程渲染管线
1.基本处理
该阶段设定3D空间中物体的顶点坐标、顶点对应的颜色、顶点的纹理坐标等属性,并且指定绘制方式,如点绘制、线段绘制或者三角形绘制等。
2.顶点缓冲对象
这部分功能在应用程序中是可选的,对于某些在整个场景中顶点的基本数据不变的情况。可以在初始化阶段将顶点数据经过基本处理后送入顶点缓冲对象,在绘制每一帧想要的图像时就省去了顶点数据IO的麻烦,直接从顶点缓冲对象中获取顶点数据即可。相比于每次绘制时单独将顶点数据送入GPU的方式,可以在一定程度上节省GPU的IO带宽,提高渲染效率。
3.顶点着色器
顶点着色器是一个可编程的处理单元,功能为执行顶点的变换、光照、材质的应用与计算等顶点的相关操作,其每顶点执行一次。其工作过程为首先将原始的顶点几何信息及其他属性传送到顶点着色器中,经过自己开发的顶点着色器处理后产生纹理坐标、颜色、点位置等后继流程需要的各项顶点属性信息,然后将其传递给图元装配阶段。
顶点着色器替代了原有固定管线的顶点变换、光照计算,开发人员可以根据自己的需求自行开发顶点变换、光照等功能,大大增加了程序的灵活性。但凡事有利皆有弊,增加灵活性的同时也增加了开发的难度。顶点着色器的工作原理如图3-17所示。
▲图3-17 OpenGL ES 2.0顶点着色器工作原理
❑ 顶点着色器输入主要为待处理顶点相应的attribute(属性)变量、uniform(一致)变量、采样器以及临时变量,输出主要为经过顶点着色器后生成的varying(易变)变量及一些内建输出变量。
❑ attribute变量指的是3D物体中每个顶点各自不同的信息所属的变量,一般顶点的位置、颜色、法向量等每个顶点各自不同的信息都是以attribute变量的方式传入顶点着色器的。
❑ uniform变量指的是对于同一组顶点组成的单个3D物体中所有顶点都相同的量,一般为场景中当前的光源位置、当前的摄像机位置、投影系列矩阵等。
❑ varying变量(易变变量)是从顶点着色器计算产生并传递到片元着色器的数据变量。顶点着色器可以使用易变变量来传递需要插值到片元的颜色、法向量、纹理坐标等任意值。
❑ 内建输出变量gl_Position、gl_FrontFacing和gl_PointSize等。gl_Position是经过变换矩阵变换、投影后顶点的最终位置,gl_FrontFacing指的是片元所在面的朝向,gl_PointSize指的是点的大小。
需要特别注意的是,易变变量在顶点着色器赋值后并不是直接将赋的值送入到后继的片元着色器中,而是在光栅化阶段由管线根据片元所属图元各个顶点对应的顶点着色器对此易变变量的赋值情况及片元与各顶点的位置关系插值产生,图3-18说明了这个问题。
▲图3-18 易变变量的工作原理
说明
有一些数学知识的读者可能会想到一个问题,对每个片元进行一次插值计算将会非常耗费时间,严重影响性能。幸运的是,OpenGL ES 2.0的设计者也考虑到了这个问题,这些插值操作都是由GPU中的专用硬件实现的,因此速度很快,不影响性能。
4.图元装配
这个阶段主要有两个任务,一个是图元组装,另一个是图元处理。所谓图元组装是指顶点数据根据设置的绘制方式被结合成完整的图元。例如,点绘制方式下每个图元仅需要一个单独的顶点,此方式下每个顶点为一个图元;线段绘制方式每个图元则需要两个顶点,此方式下每两个顶点构成一个图元;三角形绘制方式下需要3个顶点构成一个图元。
图元处理最重要的工作是剪裁,其任务是消除位于半空间(half-space)之外的部分几何图元,这个半空间是由一个剪裁平面所定义的。例如,点剪裁就是简单地接受或者拒绝顶点,线段或多边形剪裁可能需要增加额外的顶点,具体取决于直线或者多边形与剪裁平面之间的位置关系,如图3-19所示。
▲图3-19 剪裁三角形3个顶点生成6个新的顶点
说明
图3-19给出了一个三角形图元(图中为点划线绘制)被4个剪裁平面剪裁的情况。4个剪裁平面分别为:上面、左侧面、右侧面、后面。
之所以要进行剪裁是因为随着观察位置、角度的不同,并不总能看到(这里可以简单地理解为显示到设备屏幕上)特定3D物体某个图元的全部。例如,当观察一个正四面体并离某个三角形面很近时,可能只能看到此面的一部分,这时在屏幕上显示的就不再是三角形了,而是经过裁剪后形成的多边形,如图3-20所示。
▲图3-20 从不同角度、距离观察正四面体
剪裁时,若图元完全位于视景体以及自定义剪裁平面的内部,则将图元传递到后面的步骤进行处理;如果其完全位于视景体或者自定义剪裁平面的外部,则丢弃该图元;如果其有一部分位于内部,另一部分位于外部,则需要剪裁该图元。
提示
关于视景体剪裁的问题会在第5章介绍投影的部分进行详细介绍,这里读者简单了解即可。
5.光栅化
虽然虚拟3D世界中的几何信息是三维的,但由于目前用于显示的设备都是二维的。因此,在真正执行光栅化工作之前,首先需要将虚拟3D世界中的物体投影到视平面上。需要注意的是,由于观察位置的不同,同一个3D场景中的物体投影到视平面可能会产生不同的效果,如图3-21所示。
▲图3-21 光栅化阶段投影到视口
另外,由于虚拟3D世界当中物体的几何信息一般采用连续的数学量来表示,因此投影的平面结果也是用连续数学量表示的。但目前的显示设备屏幕都是离散化的(由一个一个的像素组成),因此还需要将投影的结果离散化。将其分解为一个一个离散化的小单元,这些小单元一般称为片元,具体效果如图3-22所示。
▲图3-22 投影后图元离散化
其实每个片元都对应于帧缓冲中的一个像素,之所以不直接称为像素是因为3D空间中的物体是可以相互遮挡的。而一个3D场景最终显示到屏幕上虽然是一个整体,但每个3D物体的每个图元是独立处理的。这就可能出现这样的情况,系统先处理的是位于离观察点较远的图元,其光栅化成为了一组片元,暂时送入帧缓冲的对应位置。
但后面继续处理离观察点较近的图元时也光栅化出了一组片元,两组片元中有对应到帧缓冲中同一个位置的,这时距离近的片元将覆盖距离远的片元(如何覆盖的检测是在深度检测阶段完成)。因此,某片元就不一定能成为最终屏幕上的像素,称为像素就不准确了,可以将其理解为候选像素。
提示
每个片元包含其对应的顶点坐标、顶点颜色、顶点纹理坐标以及顶点的深度等信息,这些信息是系统根据投影前此片元对应的3D空间中的位置及与此片元相关的图元的各顶点信息进行插值计算而生成的。
6.片元着色器
片元着色器是用于处理片元值及其相关数据的可编程单元,其可以执行纹理的采样、颜色的汇总、计算雾颜色等操作,每片元执行一次。片元着色器主要功能为通过重复执行(每片元一次),将3D物体中的图元光栅化后产生的每个片元的颜色等属性计算出来送入后继阶段,如剪裁测试、深度测试及模板测试等。
从渲染管线的图中可以看出,可编程片元着色器替代了老版本中固定管线纹理采样、颜色求和、雾以及Alpha测试等阶段。与顶点着色器类似,被其替代的功能系统将不再提供,需要完全由开发人员用着色器语言编程完成。这在提高了灵活性的同时也增加了开发的难度,尤其是对于初学者,其基本工作原理如图3-23所示。
▲图3-23 OpenGL ES 2.0片元着色器工作原理
❑ Varying0~n指的是从顶点着色器传递到片元着色器的易变变量,如前面所介绍,由系统在顶点着色器后的光栅化阶段自动插值产生。其个数是不一定的,取决于具体的需要。
❑ gl_FragColor内建变量指的是片元的最终颜色。一般在片元着色器的最后都需要对gl_FragColor进行赋值。
提示
经过光栅化、顶点着色器与片元着色器的介绍,可以看出顶点着色器每顶点执行一次,而片元着色器每片元执行一次,片元着色器的执行次数明显大于顶点着色器的执行次数。因此在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。
7.剪裁测试
如果程序中启用了剪裁测试,OpenGL ES会检查每个片元在帧缓冲中对应的位置,若对应位置在剪裁窗口中则将此片元送入下一阶段,否则丢弃此片元。
8.深度测试和模板测试
❑ 深度测试是指将输入片元的深度值与帧缓冲区中存储的对应位置片元的深度值进行比较,若输入片元的深度值小则将输入片元送入下一阶段准备覆盖帧缓冲中的原片元或与帧缓冲中的原片元混合,否则丢弃输入片元。
❑ 模板测试的主要功能为将绘制区域限定在一定的范围内,一般用在湖面倒影、镜像等场合,后面的章节会详细介绍。
9.颜色缓冲混合
若程序中开启了Alpha混合,则根据混合因子将上一阶段送来的片元与帧缓冲中对应位置的片元进行Alpha混合;否则送入的片元将覆盖帧缓冲中对应位置的片元。
10.抖动
抖动是一种简单的操作,其允许只使用少量的颜色模拟出更宽的颜色显示范围,从而使颜色视觉效果更加丰富。例如,可以使用白色以及黑色模拟出一种过渡的灰色。
但使用抖动也是有缺点的,那就是会损失一部分分辨率,因此,对于现在主流的原生颜色就很丰富的显示设备一般是不需要启用抖动的。
提示
当下的一些系统中虽然在API方面支持开启抖动,但这仅是为了API的兼容,其可能根本不会去进行事实上的抖动操作。
11.帧缓冲
OpenGL ES中的物体绘制并不是直接在屏幕上进行的,而是预先在帧缓冲区中进行绘制,每绘制完一帧再将绘制的结果交换到屏幕上。因此,在每次绘制新的一帧时都需要清除缓冲区中的相关数据,否则有可能产生不正确的绘制效果。
同时需要了解的是,为了应对不同方面的需要,帧缓冲是由一套组件组成的,主要包括颜色缓冲、深度缓冲以及模板缓冲,各组件的具体用途如下所列。
❑ 颜色缓冲用于存储每个片元的颜色值,每个颜色值包括RGBA(红、绿、蓝、透明度)4个色彩通道,应用程序运行时在屏幕上看到的就是颜色缓冲中的内容。
❑ 深度缓冲用来存储每个片元的深度值,所谓深度值是指以特定的内部格式表示的从片元处到观察点(摄像机)的距离。在启用深度测试的情况下,新片元想进入帧缓冲时需要将自己的深度值与帧缓冲中对应位置片元的深度值进行比较,若结果为小于才有可能进入缓冲,否则被丢弃。
❑ 模板缓冲用来存储每个片元的模板值,供模板测试使用。模板测试是几种测试中最为灵活和复杂的一种,后面将用专门的章节进行介绍。
提示
本小节只是对渲染管线中的每一个模块进行了简单介绍,更为具体的情况会在本书中的后继章节进行更为详细讨论,这里读者只要在概念上有个整体的把握即可。
3.2.2 OpenGL ES 3.0的渲染管线
OpenGL ES 2.0采用的是可编程的渲染管线,编程人员可以插入一些特殊操作,方便高效地完成OpenGL ES 1.x固定渲染管线难以完成的任务。而OpenGL ES 3.0采用的也是可编程渲染管线,只是OpenGL ES 3.0增加了一些新特性。
通过增加的新特性,能够让移动平台的游戏画面更加逼真、细腻,提高设备3D渲染的性能,OpenGL ES 3.0可编程渲染管线如图3-24所示。
▲图3-24 OpenGL ES 3.0可编程渲染管线
从图3-24中可以看出,总体上OpenGL ES 3.0可编程渲染管线与OpenGL ES 2.0的基本相同,但其实顶点着色器与片元着色器内部还是有不少区别的,详细内容如下。
1.顶点着色器
此处的顶点着色器与OpenGL ES 2.0的顶点着色器相同,也是一个可编程的处理单元,其功能也是执行顶点的变换、光照、材质的应用与计算等顶点的相关操作,每顶点执行一次。其工作过程也是首先将原始的顶点几何信息及其他属性传送到顶点着色器中,经过自己开发的顶点着色器处理后产生纹理坐标、颜色、点位置等后继流程需要的各项顶点属性信息,然后将其传递给图元装配阶段。
▲图3-25 OpenGL ES 3.0顶点着色器工作原理
通过可编程的顶点着色器,开发人员可以根据自己的需求自行开发顶点变换、光照等功能,下面给出顶点着色器的工作原理图,如图3-25所示。
❑ 顶点着色器的输入主要为待处理顶点相应的in变量、uniform(一致)变量、采样器以及临时变量,输出主要为经过顶点着色器后生成的out变量及一些内建输出变量。
❑ 顶点着色器中的in变量指的是3D物体中每个顶点各自不同的信息所属的变量,一般顶点的位置、颜色、法向量等每个顶点各自不同的信息都是以in变量的方式传入顶点着色器的。例如,前一节“初识OpenGL ES 3.0应用程序”中顶点着色器里的aPosition(顶点位置)和aColor(顶点颜色)变量等。
❑ uniform变量指的是对于同一组顶点组成的单个3D物体中所有顶点都相同的量,一般为场景中当前的光源位置、当前的摄像机位置、投影系列矩阵等。例如,前一节“初识OpenGL ES 3.0应用程序”案例中顶点着色器里的uMVPMatrix(总变换矩阵)变量等。
❑ 顶点着色器中的out变量是从顶点着色器计算产生并用于传递到片元着色器的数据变量。顶点着色器可以使用out变量来传递需要插值或不需要插值到片元的颜色、法向量、纹理坐标等任意值。例如,3.1.2节“初始OpenGL ES 3.0应用程序”中由顶点着色器传入片元着色器中的vColor变量。
❑ 内建输出变量gl_Position、gl_PointSize以及内建输入变量gl_VertexID、gl_InstanceID等。gl_Position是经过变换矩阵变换、投影后的顶点的最终位置,gl_PointSize指的是点的大小。gl_VertexID用来记录顶点的整数索引。gl_InstanceID是指实例ID,只在顶点着色器中使用,对于指定的每一组图元,该ID相应递增。
out变量在顶点着色器赋值后并不是直接将赋的值传递到后继的片元着色器对应的in变量中,在此存在两种情况:
❑ 如果out限定符之前含有smooth限定符或者不含有任何限定符,则传递到后继片元着色器对应的in变量的值,是在光栅化阶段由管线根据片元所属图元各个顶点对应的顶点着色器对此out变量的赋值情况及片元与各顶点的位置关系插值产生,图3-26所示说明了这个问题。
▲图3-26 顶点着色器中out变量的工作原理
❑ 如果out限定符之前含有flat限定符,则传递到后继片元着色器对应的in变量的值不是在光栅化阶段插值产生的,而是由图元的最后一个顶点对应的顶点着色器对此out变量所赋的值决定的,此种情况下图元中每个片元的值均相同。
2.片元着色器
与OpenGL ES 2.0相同,OpenGL ES 3.0的片元着色器同样是用于处理片元值及其相关数据的可编程单元,其可以执行纹理的采样、颜色的汇总、计算雾颜色等操作,每片元执行一次。
▲图3-27 OpenGL ES 3.0片元着色器工作原理
与OpenGL ES 2.0不同的是,片元着色器内的varying变量变成了in变量,并且内建输出变量gl_FragColor不存在了。其基本原理图如图3-27所示。
❑ in0~in(n)指的是从顶点着色器传递到片元着色器的变量,如前面所介绍,由系统在顶点着色器后的光栅化阶段自动产生,其个数是不一定的,取决于具体的需要。例如,3.1.2节“初识OpenGL ES 3.0应用程序”中片元着色器里的vColor变量。
❑ out变量一般指的是由片元着色器写入计算完成的片元颜色值的变量,一般在片元着色器的最后,都需要对其进行赋值,最后将其送入渲染管线的后继阶段进行处理。例如,3.1.2节“初识OpenGL ES 3.0应用程序”中片元着色器里创建的fragColor变量。
提示
原来OpenGL ES 2.0中片元着色器的内建输出变量gl_FragColor在OpenGL ES 3.0中不存在了,如果需要输出颜色值,则需要自己声明out(类型为vec4)变量,用声明的变量替代gl_FragColor。在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。
3.2.3 OpenGL ES中立体物体的构建
前面的几个小节向读者介绍了不同版本OpenGL ES的渲染管线,同时也给出了一个非常简单的旋转三角形案例。但到目前为止,读者可能还是不太清楚虚拟3D世界中的立体物体是如何搭建出来的。其实这与现实世界搭建建筑物并没有本质区别,请读者考察图3-28和图3-29中国家大剧院远景和近景的照片。
▲图3-28 国家大剧院远景
▲图3-29 国家大剧院近景
从两幅照片中可以对比出,现实世界的某些建筑物远看是平滑的曲面,其实近看是由一个一个的小平面组成的。3D虚拟世界中也是如此,任何立体物体都是由多个小平面搭建而成的。这些小平面切分得越小,越细致,搭建出来的物体就越平滑。
当然OpenGL ES的虚拟世界与现实世界还是有区别的,现实世界中可以用任意形状的多边形来搭建建筑物,例如图3-28中的国家大剧院就是用四边形搭建的,而OpenGL ES中仅允许采用三角形来搭建物体。其实这从构造能力上来说并没有区别,因为任何多边形都可以拆分为多个三角形,只需开发时稍微注意一下即可。
提示
OpenGL ES中之所以仅支持三角形而不支持任意多边形是出于性能的考虑,就目前移动嵌入式设备的硬件性能情况来看,这是必然的选择了。
图3-30更加具体地说明了,在OpenGL ES中如何采用三角形来构建立体物体。
▲图3-30 用三角形搭建立体物体
说明
从图3-30中可以看出用三角形可以搭建出任意形状的立体物体,这里仅是给出了几个简单的例子,本书的后继章节中还有很多其他形状的立体物体。
了解了OpenGL ES中立体物体的搭建方式后,下面就需要了解OpenGL ES中的坐标系统了。OpenGL ES中采用的是三维笛卡尔坐标系,如图3-31所示。
▲图3-31 OpenGL ES中的坐标系
从图3-31中可以看出,OpenGL ES中采用的是左手标架坐标系。一般来说,初始情况下y轴平行于屏幕的竖边,x轴平行于屏幕的横边,z轴垂直于屏幕平面。
提示
空间解析几何中有两种坐标系标架,即左手标架和右手标架。本书非讨论空间解析几何的专门书籍,关于标架的问题不予详述,需要的读者可以参考空间解析几何的相关书籍或资料。