OpenGL ES 总结(五)OpenGL 中 pipeline 机制

1,992 阅读13分钟
原文链接: mp.weixin.qq.com

导读: OpenGL ES是在图形图像中,非常优秀的渲染库,其中pipeline可以说是非常重要一个环节。

前一篇介绍是渲染视频,今天主要介绍OpenGL中pipeline机制,看下Agenda:

OpenGL ES中pipeline机制

  • pipeline是什么

  • Vertex Specification

  • Tessellation

  • Geometry Shader

  • Vertex Post Processing

  • Primitive Assembly

  • Transform Feedback

  • 光栅化Rasterization

  • Fragment shader

  • Per-Sample操作

pipeline是什么?

管线是图形系统中前一道的输出作为下道工序的输入。主CPU发出一个绘图指令,然后可能由硬件部件完成坐标变换,裁剪,添加颜色或是材质,最后在屏幕上显示出来。简单理解就是:按照特定的顺序对图形信息进行处理。

下图显示了OpenGL ES 1.x 固定管道的结构图: 




OpenGL ES 2.0 允许提供编程来控制一些重要的工序,一些“繁琐”的工序比如栅格化等仍然是固定的。


OpenGL从我们提供的几何数据(顶点和几何图元)出发,首先使用了一系列shader阶段来处理它: 
vertex shading, 
tessellation shading(它本身就包含了两种shaders),接着是geometry shading,然后再传递给光栅化程序(rasterizer);光栅化程序将会为每个在裁剪区域(clipping region)内部的图元生成fragments,然后再为每个fragment执行一个fragment shader。

只有vertex和fragment shaders是我们必须实现的。Tessellation和geometry shaders对于我们是可选的。

下面看下这些pipeline的阶段 


 


Vertex Specification

  • 准备顶点数组数据(vertex array data)

即进行顶点规格定义。应用将会建立一个有序的顶点列表,然后发送给OpenGL。这些顶点定义了图元的边界。图元是基本的绘制形状,如点、线、三角形。这些顶点列表是如何被组织成一个个图元的会在后面的阶段里进行处理。

这个阶段会处理一些顶点数组对象(Vertex Array Objects)和顶点缓存对象(Vertex Buffer Objects)。VAO定义了每个顶点包含的信息,而VBO则是顶点本身的数据所在。

一个顶点的数据就是一系列顶点属性(vertex attributes)。每一个attribute都是一个数据的集合,用于后面的阶段进行处理。虽然这些attributes定义了一个顶点,但没有要求说一个顶点的attributes集合中必须包含了位置和法线信息。实际上,attribute数据是完全任意的,它们仅仅是“数据”,在这个准备阶段不会有人在意你传递的到底是什么attribute,它们的真正含义会在顶点处理阶段进行解读。

OpenGL要求所有的数据都必须存储在缓存对象中(buffer objects)。缓存对象是OpenGL管理的一些内存空间。把数据放到这些缓存里有很多方法,但最常见的是使用glBufferData()来实现。

  • 向OpenGL发送数据

在我们定义了顶点相关信息后,我们可以通过调用OpenGL的绘图操作来要求按一个个几何图元绘制到屏幕上。这些操作例如有glDrawArrays()。我们后面会讲到。这个绘制的过程意味着我们把顶点数据传递给OpenGL服务器。

Tessellation

Tessellation,读 泰斯类什,可以翻译成曲面细分。在vertex shader处理了每一个顶点的相关信息后,如果tessellation shader阶段被激活的话,它就会继续处理这些数据。在后面我们会看到tessellation使用patchs来描述一个对象的形状,并且允许细化(tessellate)相对简单的patch集合,来增加几何图元的数量,提高模型的平滑度和真实度。Tessellation Shading阶段可以使用两个shaders来控制patch数据,以及中间一个固定函数的tessellator来生成最终的形状。

Geometry Shader

这一阶段允许在光栅化之前处理单独的几何图元,包括创建新的图元。

Geometry shader会处理每一个输入的图元,然后返回0个或更多的输出图元。它的输入是primitive assembly的输出图元。因此如果我们按triangle strip看待一个图元,那么geometry shader看到的就会使一系列三角形。

然而也有一些输入的图元类型是专门为geometry shaders定义的。这些相邻的图元可以让GS了解关于相邻顶点的信息。

GS的输出可以是0个多更多的简单图元。GS可以移除图元,或者根据一个输入图元来输出更多的图元来细分(tessellate)它们。GS甚至可以改变图元的类型,比如把点图元编程三角形,把线图元变成点。

Vertex Post Processing

对于通过绘制命令绘制的每一个顶点,OpenGL将调用一个vertex shader来处理关于这个顶点的相关信息。Vertex shader接受从上个阶段传递来的attribute作为输入,然后把每一个输入顶点转换成一个输出顶点。和输入的顶点信息不同,输出的顶点数据有一些必需的要求——vertex shader必须填充一个位置信息。

Vertex shaders的复杂性可以变化非常大,有的很简单,就是仅仅复制数据然后传递给下一个流水线阶段,我们称这种为pass-through shader;有的很复杂,会执行很多操作来计算顶点的屏幕位置,还可能会进行光照计算来计算顶点的颜色,或者其他技术。

通常,一个应用会包含多个vertex shader,但同一时间只有一个会被激活(active)。

Primitive Assembly

Primitive assembly就是把vertex shader输出的顶点数据集合在一起,并把它组合成一个图元的过程。用户渲染的图元的类型决定了这个过程是如何工作的。

这个过程的输出是一个有序的简单图元(点、线或者三角形)序列。

Transform Feedback

Geometry shader或者primitive assembly的输出会被写入一系列的缓存对象。这被称为transform feedback模式。它允许我们通过vertex和geometry shaders来变换数据,然后再等待后续使用。

通过舍弃光栅化的结果,pipeline可以在这步就停止了。这允许transform feedback成为渲染的唯一输出。

光栅化(Rasterization)

在裁剪完成后,更新后的图元就会被发送给光栅化程序去生成fragments。那么什么是fragment呢?一个fragment可以看成是一个“候选像素”。这类像素在帧缓存中的一块区域中。一个fragment仍可以被拒绝(reject),并且永远不会更新它的相关像素位置。

Wiki上把fragment成为是一个状态的集合,用于计算一个像素的最终数据。一个fragment的状态包含了它在屏幕空间的位置信息,样本覆盖(sample coverage,如果开启了multisampling的话),以及一些其他由vertex或者geometry shader输出的数据。这些数据集合是通过该fragment对应的顶点数据进行插值计算而得的。这个插值计算是由输出这些数据的shader定义的。

处理fragments是后面两个阶段的任务——fragment shading和per-fragment操作。

Fragment shader

最后一个我们可以编程控制颜色的阶段就是fragment shader。在这个阶段,我们使用一个shader来决定该fragment的最终颜色(其实下个阶段,per-fragment操作仍可以进行最后的颜色修改)和它的深度值(depth value)。在fragment shaders里我们可以进行非常强大的纹理映射的工作。如果一个fragment shader认为某个fragment不应该绘制出来,它还可以终结一个fragment的处理过程。这个过程称为fragment discard。

一个fragment shader会输出一个颜色列表、一个深度值和一个stencil值。Fragment shaders不可以为一个fragment设置stencil,但是它们可以控制颜色和深度值。

我们可以想来区分vertex shading(包括tessellation和geometry shading)和fragment shading:vertex shading决定了一个图元在屏幕上的位置,而fragment shading使用这些信息来决定该fragment的颜色。

Per-Sample操作

从fragment处理器输出的fragment数据会再通过一系列的步骤。

第一个步骤就是各种剔除检验(culling tests)。如果开启了stencil test,如果一个fragment没有通过检验它就会被剔除,而不会写入到帧缓存中;如果开启了depth test,如果一个fragment没有通过检验它就会被剔除,而不会写入到帧缓存中。只要没有通过任何一个检验,fragments都会被剔除,不会添加到帧缓存中。

如果一个fragment成功通过了所有的检测,它就会直接写入帧缓存中,更新它的像素颜色(也可能是深度值)。如果blending被开启了,该fragment的颜色会和当前的像素颜色进行混合去产生一个新的颜色,再写入帧缓存中。

最后,fragment数据被写入帧缓存中。Masking operation允许用户避免写入特定的值。写颜色、深度和stencil都可以被mask成on或者off;单独的颜色通道也可以。

注意:GLSL是要注意的。

下图就是一个顶点数据经过几个步骤后转化成显示在屏幕上像素的过程(一般也叫做GLSL的流水线工作流程),蓝色图形部分是我们可以通过写shader文件参与的,目前参与比较多的主要是顶点和片段shader。

  • 1、 Vertex Data[]传到顶点shader程序

  • 2、 顶点着色程序对顶点数据进行转换,把Vertex Data中的3D坐标转换成对应的不同于Vertex Data中的3D坐标,在这里,顶点着色程序允许用户进行一些基本的处理和顶点属性设置。

  • 3 、shape assembly(也叫 primitive assembly) 是一个对顶点数据进行简单的组合过程,比如上图就是把3个点组合成一个三角形。

  • 4、 简单组合后竟然几何着色程序,根据输入的vertices(顶点数据集)能生成新的或者说是其他的图形,在上图中生成了一个新的三角形.

  • 5 、经过几何着色后就会进入细分着色程序。在这里有能力生成更小更多的小的简单的图形,在上图中我们自己让它生成更小更多的三角形。

  • 6、 之后就是进入光栅化过程,在这里会把原始的图形最终转换成在屏幕上显示的相对应的像素,在这里会进行对显示内容进行判断(比如有些超出屏幕显示就会被截掉等),其中最终生成的片段会在片段着色器中使用。

  • 7、 片段着色器会计算每个像素在屏幕上显示的最终颜色

OpenGL 3.3以后,OpenGL程序就必须要定义一个vertex shader 和 fragment shader(因为在GPU中没有默认定义的 vertex/fragment shaders)。

OpenGL渲染管线按照特定的顺序对图形信息进行处理,这些图形信息可以分为两个部分:顶点信息(坐标、法向量等)和像素信息(图像、纹理等),图形信息最终被写入帧缓存中,存储在帧缓存中的数据(图像),可以被应用程序获得(用于保存结果,或作为应用程序的输入等,见下图中灰色虚线)。 


 


Display List(显示列表)

显示列表中存储的是一组OpenGL命令,用于几何图元(顶点坐标、法向量等)与像素信息都可以保持到显示列表中,

显示列表中存储的是一组稍后执行的OpenGL命令,所有的数据,包括几何图元(顶点坐标、法向量等)和像素信息都可以保持到显示列表中,因为命令和数据都存储在显示列表中(类似计算机的缓存)所以一定程度上可以提高程序的性能。

Vertex Operation(顶点操作)

每个顶点和法向量坐标均经过模型视图变换(从物体坐标变换到视图坐标系下);如果启用了光照,将根据变换后的顶点坐标和法向量信息,计算每个顶点的光照信息,计算得到的光照信息将用来更新顶点的颜色(通常是光照计算后的颜色 = 环境光 + 反射光 * 顶点颜色 + 聚焦光)。

Primitive Assembly(图元组装)

当顶点操作后,基本图元(点,线,多边形)将再次通过投影矩阵然后裁剪通过viewing volume clipping planes;从几何坐标到剪辑坐标。表面分隔通过w坐标和视口变换应用3d底图场景映射到window坐标,图元组装里最后做的事就是一旦culling可行,将进行culling test(选择测试)

Pixel Transfer Operation(像素转换操作)

从client的内存中读取像素,这些数据被执行,scaling, bias, mapping and clamping. 这些操作就是像素转换操作,转换后的像素数据存储中纹理内存或者是光栅化过的片段(fragment)中。

Texture Memory(纹理内存)

纹理图被加载到纹理内存,并应用到几何对象上(如矩形)。

Raterization(光栅化)

光栅化就是把几何(顶点坐标等)和像素数据转换为片段(fragment)的过程,每个片段对应于帧缓冲区中的一个像素,该像素对应屏幕上一点的颜色和不透明度信息。片段是一个矩形数组,包含了颜色、深度、线宽、点的大小等信息(反锯齿计算等)。如果渲染模式被设置为GL_FILL,多边形内部的像素信息在这个阶段会被填充。 




如上图中的三角形,输入三角形的三个顶点坐标以及其颜色,顶点操作会对三角形的顶点坐标以及法向量进行变换,颜色信息不需要经过变换,但光照计算会影响顶点的颜色信息,经过光栅化后,三角形被离散为一个个点,不在是三个坐标表示,而是由一系列的点组成,每个点存储了相应的颜色、深度和不透明度等信息。


Fragment Operation(片段操作)

片段操作是最后一个操作,将片段转化成像素到framebuffer中。在这阶段的第一过程是 texel generation,从纹理内存中生成纹理元素,这些纹理元素将被用于每个fragment(片段),然后fog计算将开始,接着,这些片段将按顺序执行:Scissor Test ⇒ Alpha Test ⇒ Stencil Test ⇒ Depth Test. 
最后blending, dithering, logical operation and masking通过bitmask执行,然后真实的像素被存储到framebuffer中。

Feedback(反馈)

通过glGet*()及glIsEnabled()命令,可以知道OpenGL的当前的状态及信息。我们可以通过glReadPixels()从framebuffer中读取矩形区域的像素数据。通过glRenderMode(GL_FEEDBACK)能得到全部的顶点数据。虽然glCopyPixels()不能返回系统中具体的像素数据。但是我们可以复制它们回到另一个framebuffer中。如从front buffer到back buffer(ps: surfaceView中也有这个buffer);

第一时间获得博客更新提醒,以及更多 android,源码分析,最新开源项目推荐,更多有价值的思考 ,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码