参考资料

OpenGL的主要工作是:

  • 3D坐标转化为2D坐标
  • 2D坐标转换为有颜色的屏幕像素

OpenGL将这个复杂过程进行了抽象,分成了几个阶段,每个阶段把前一个阶段的输出作为输入,就像工厂生产流水线一样,这整个流程就是OpenGL图形渲染管线(Rendering Pipeline),具体分如下几个阶段:
点击查看【processon】
每个阶段对应一个在GPU上运行的Shader程序(着色器),用OpenGL着色语言(OpenGL Shading Language,GLSL)编写,由GPU编译运行。其中:

  • 顶点着色器(Vertex Shader)
  • 几何着色器(Geometry Shader)
  • 片段着色器(Fragment Shader)

我们可以用自己编写的程序代替默认的着色程序。
OpenGL 2.0以后要求,我们必须提供顶点着色器片段着色器

一、顶点规范(Vertex Specification)

Specification的意思是指(自)定义数据的格式,让GPU能够准确的读取这些数据。再说直白一点就是定义顶点数据。
在这个阶段创建一系列有序的顶点数据,作为渲染管线的输入数据,这些顶点定义了图元的边界。

在这里会创建相关的对象(object)用于渲染需要,最典型的就是:

  • VAO(Vertex Array Object
    • 顶点数组对象,定义了每个顶点数据的结构
  • VBO(Vertex Buffer Object
    • 顶点缓冲对象,管理着在显卡内存中存储着的大量顶点数据。通过VBO,可以一次性发送大批顶点到显卡,提升性能。

每个顶点的数据是一系列的属性(attribute),属性又是一个小的数据集合(像一个简单的C Struct),顶点的属性值可以是任意的,不一定必须包含3D坐标和法向量。

二、顶点处理阶段(Vertex Processing)

顶点数据处理的阶段,这个阶段几乎都是可以自定义着色器程序(programable),我们可以决定如何处理每一个顶点。

1、顶点着色器(Vertex Shader)

以一个单独的顶点作为输入,主要的工作:

  • 坐标变换
    • MVP矩阵:Model-View-Projection,模型视图投影矩阵。物体的移动、缩放、旋转、翻转都是通过这个完成。
    • 顶点着色器最终输出一个裁剪坐标。
  • 光照计算
    • 通过光照判断顶点的颜色。
  • 传递着色器(pass-through shader)
    • 啥都不干
    • 顶点数据复制并传递到下一个着色阶段,叫传递着色器。

最终输出一个顶点(outgoing vertex),我们还可以自定义输出供下一个阶段使用。
这里有一个限制,每个输入顶点必须映射到一个指定的输出顶点,简单说就是相同的输入顶点,得到的输出顶点数据完全相同,这样相同的输入顶点无需计算两次。
顶点着色器是必须要实现的。
OpenGL_渲染管线概述 - 图2

分输入数据和输出数据两部分来讲。

  • 输入数据
    • 属性(Attribute)
      • 顶点数据就是由属性组成,如空间位置、法向量、纹理坐标以及顶点颜色。
      • 属性只在顶点着色器中才有,片段着色器中没有属性。
      • OpenGL ES 2.0 规定了所有实现应该支持的最大属性个数不能少于 8 个。
    • 常量(Uniforms)
      • 由应用程序传递给着色器的只读常量数据,对顶点、片段着色器均可见。
      • 在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。
      • OpenGL ES 2.0 也规定了所有实现应该支持的最大顶点着色器 uniform 变量个数不能少于 128 个,最大的片段着色器 uniform 变量个数不能少于 16 个。
    • 采样器(Samplers):
      • 一种特殊的 uniform,用于呈现纹理。sampler 可用于顶点着色器、片段着色器。
  • 输出数据

    • 内建
      • gl_Position(必需),表示顶点的最终位置,如果后续没有顶点处理操作,那么它最终输出的是裁剪坐标。
      • gl_FrontFacing (可选)
      • gl_PointSize(可选)
    • 自定义
      • Varying,可变变量,存储顶点着色器的输出数据,也存储片段着色器的输入数据。
      • 顶点着色器中声明的 varying 变量都应在片段着色器中重新声明为同名同类型的 varying 变量。
      • 会在光栅化处理阶段被线性插值。
      • OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。

        2、细分着色器(Tessellation Shader)

        通过两个细分着色器和在这两个阶段之间的a fixed-function tessellator来完成图元细分,用Patch来细化模型,使模型更平滑真实,几何图元增加,顶点增加。
  • 细分控制着色器

    • Tessellation Control Shader,TCS
    • 决定了每个图元的细分数量
    • 确保相邻细分图元之间的连通性(connectivity )
  • a fixed-function tessellator
    • 固定不变的细分器,生成细分图元。
  • 细分求值着色器

    • Tessellation Evaluation Shader,TES
    • 为细分图元进行插值和一些其他操作(used to compute user-defined data values)

      3、几何着色器(Geometry Shader)

      输入是图元,输出也是图元,输出的图元数量可能减少(删除图元)或者增加(细分图元)。
      这里接收的输入图元是图元装配的子过程(subset)产生的输出图元,所以如果输入一个三角形带,会输出一系列的三角形。
      可以删除图元,细分图元,修改顶点值(做点顶点着色器的工作),或者做图元细分时的插值工作。还可以改变图元类型,输入点图元,输出三角图元。
      几何着色器是可选的。

      三、顶点后处理阶段(Vertex Post-Processing)

      经过一系列着色器级别的顶点处理之后,将进入一些固定不变的处理阶段。

      1、变换反馈(Transform Feedback)

      几何着色器和图元装配的输出将被写入前面创建的缓冲对象中,这就是变换反馈模式。

      2、裁剪(Clipping)

      裁减掉视景体(view volume)外面的图元部分:

    • 如果图元完全在外面则全部裁剪

    • 如果图元完全在内部则保留
    • 如果图元一部分在外面,则裁剪掉外面的部分。

通过透视除法(Perspective Divide)和视口变换(Viewport Transform),顶点坐标将从裁剪坐标变换为窗口坐标。

四、图元装配(Primitive Assembly)

将上一个阶段输出的一系列顶点数据组装成一系列的图元(点、线、三角形)。
如果输入是一个12顶点的三角形带,则输出10个三角形。
如果细分和几何着色器是开启状态,那么在顶点处理阶段之前也会执行一部分(a limited form of)的图元装配,这是因为有些特殊的着色器接受的是单独的图元而不是顶点。

背面剔除(Face Culling)

通过三角形法向量与屏幕法向量的点乘来判断三角形图元是否朝向还是背向窗口,对于闭合曲面(closed surface),我们可以避免渲染那些背面窗口的三角形,因为它们永远不会被看到。

五、光栅化(Rasterization)

将图元光栅化成一系列的片段(Fragments)。
一个片段包含了创建一个输出到帧缓冲的像素(或者采样如果开启了多重采样)的全部数据。数据包括:

  • 2D屏幕坐标
  • 采样覆盖(sample coverage),如果开启了多重采样
  • 前面顶点和几何着色器输出的任意数据(arbitrary data)

比如图元表示了一条线段,那么就会生成一些像素用来在正确的位置显示这条线段,这些像素还不一定最后能放置在帧缓存中,可能会被剔除(Culling)

This last set of data is computed by interpolating between the data values in the vertices for the fragment. The style of interpolation is defined by the shader that outputed those values.

六、片段着色器(Fragment Shader)

OpenGL_渲染管线概述 - 图3

顶点着色器决定图元的屏幕位置,片段着色器决定片段的颜色。
一个片段,我们可以看成是一个“候选”像素,如果通过了各项测试,它将最终替换屏幕上对应像素。

处理前面光栅化输出的每个片段,输出结果是:

  • a list of colors,它们将被写入颜色缓冲区,通过纹理映射(texture mapping)的方式对顶点处理阶段的颜色值进行补充。
  • 一个深度值(a depth value)
  • 一个模板值(a stencil value)

片段着色器无法设置模板数据,但是能够控制颜色值和深度值。

如果我们没有提供片段着色器,则深度值和模板值可以正常获取,但颜色值将是未定义。在执行遮挡查询测试(Occlusion tests)时,这非常有用。

七、逐片段操作(Per-Fragment Operation)

或者也可以是逐Sample操作(Per-Sample Operation),这里将进行一系列的操作(a sequence of operations):

  1. 剔除测试(Culling test):如果测试不通过,则缓冲区像素将不会更新,很多测试都需要我们手动开启。
    1. 像素所有权测试(Pixel ownership test):不通过表示这个像素不属于OpenGL窗口(比如被其他窗口覆盖)。
    2. 剪切测试(Scissor test):如果片段有部分或全部像素在一个指定屏幕矩形框之外,则测试不通过,需手动开启。
    3. 模板测试(Stencil test):如果测试提供的模板值和我们在从模板缓冲区中指定的模板值不匹配,则测试不通过,需手动开启,注意即使不通过还是能改变缓冲区中的模板值。
    4. 深度测试(Depth test):如果片段深度值和我们从深度缓冲区中指定的深度值不匹配,则测试不通过,需手动开启。
    5. 注意:这些测试也可以发生在片段着色器之前,可以节省性能。
  2. 颜色混合(Blending):通过指定算法,将片段颜色值与对应缓冲区颜色值混合。也可以用Logical Operations代替混合,这些操作是在两者之间进行位操作。
  3. 蒙板操作(Mask Operation):最后片段数据将被写入到帧缓冲中,蒙板操作可以阻止写入到某些特定值,颜色、颜色单独某个通道、深度值、模板值都可以被masked on and off。