首先,GPU 的渲染流程就是一个程序,该程序是由多个着色器组成。着色器本身也是一个程序,所以需要先进行编写、编译,然后再链接到渲染程序中,多个着色器链接之后生成最终的渲染程序。
GPU 本身是高并发设计,在渲染管线进行时,各个小的着色器可以并发执行。比如在顶点输入阶段,输入了 10 个顶点,可能就有 10 个着色器程序同时执行并输出结果。
至于在同一个渲染管线内,不同阶段之间是否能够并发,这个取决于 GPU 是 Tiled Based Render 还是 ???比如,顶点着色器运行阶段,第十个顶点还没有输出时,第一个顶点的输出有没有可能已经进入到了片段着色器阶段?如果是 TBR,因为只会渲染一次,所以需要等到每个 Tile 对应的顶点都输出完成之后才能传递到下一个阶段?这么理解对不对???
一般的渲染管线包括如下着色器:
这里需要几点需要说明:
- 可编程着色器
上图中,在 OpenGL 加入可编程着色器功能之后,顶点着色器、几何着色器、片段着色器是可编程的。但是系统有提供默认几何着色器的,所以要生成一个渲染程序,开发者至少需要提供两个着色器:顶点着色器 + 片段着色器;
- 输入和输出
上述着色器中,每个着色器的输出都会作为输入传递给下一个着色器。顶点着色器因为是第一个着色器,所以直接接受外部浮点类型数组作为输入。
Rendering Pipline 步骤如下:
1.顶点输入
float 类型的数组作为顶点输入到顶点着色器,其格式一般是这样的:
因为是在 2D 框架中,所以这个的 Z 值一般直接写 1 即可。
2. 顶点着色器处理
这个着色器是可编程着色器,可以对输入的顶点进行一些自定义的处理,后面的文章会详细讲;
另外,如何对顶点数据做解析,后面的文章也会讲到。总之,顶点着色器就是对输入的顶点做一些自定义的处理并输出给图元装配着色器;
3. 图元装配
图形装配着色器接收到顶点着色器的输出之后,按照图元参数对输入的顶点做一些处理,包含裁剪、透视分割和视口变换等,最终装配成指定的图元。
比如裁剪,如果给入的顶点超过了可视范围,那么图形装配之后生成的顶点就不一定是原来的顶点了,而是处理过后的顶点。
即:将输入的顶点处理成符合当前上下文的顶点;
图元的位置、形状等信息在计算机中仍然是通过顶点 + 图元类型来表示的,所以,这一步的输出形式依然是顶点数组。而图元的类型有点、线段、三角形,这个参数的传递是贯穿整个 pipline 的,在 draw calls 中传入,比如 drawArray 方法;
4. 图元的概念
说说自己对图元和图元枚举的概念的理解。
首先是 draw call 中的图元枚举:
这个枚举是告诉着色器,需要按照怎样的方式处理数据。比如装配阶段,如果输入是 3 个顶点且有一个顶点超出坐标系,绘制图元的类型是线段和三角形时,最终生成的顶点就会有区别:
- 线段时,仍然输出 3 个顶点;
- 三角形时,输出 4 或 5 个顶点;
图元的概念:图元其实就是一个可视化的概念,目的是方便理解 pipline,对于计算机而言,图元本质上仍然是由(顶点数据 + 图元类型)组成;
比如提供三个点绘制三角形,且有一个点超出范围时,图元装配阶段需要进行裁剪。图形装配之后,假如输出输出 5 个点:
最终其实是生成了 3 个三角形的图元,但是这三个图元本质上是由 5 个顶点 + Triangle 这个枚举来标识;
另外,OpenGL ES 是 OpenGL 的子版本,用于嵌入式设备,在各个方面进行了精简。OpenGL ES 中只有点、线、三角形,所有的图形最终会转换成三角形来进行处理,没有 OpenGL 中的矩形、多边形等概念;
5. 几何着色器处理
几何着色器阶段是对上一步的图元进行再加工,可能会生成新的顶点和图元。
最开始的 pipline 图中,一个三角形变成了两个,需要注意的是,这里变成两个三角形并不是某些渲染流程相关的潜规则,而是完全由几何着色器的代码决定的。
类似的还有很多,比如传入一个顶点数据,生成多个顶点从而生成多个基础图元,最终组装成一个小房子的形状:
上图中,传入一个顶点,在几何着色器阶段生成了 5 个顶点并构成了 3 个三角形图元,最终组成了小房子的形状,代码如下:
因为几何着色器是可编程的,所以这里开发者可以编写几何着色器对图元进行多样化的处理的。
因此,各种各样的几何着色器可以理解成实现各种功能的 Api,也就是传入顶点数据之后自动生成对应的图元数据。比如上述小房子的几何着色器就是实现传入一个顶点,从而生成一个小房子图元的功能。
iOS 中的 Tiled Based Rendering 中,将较大的三角形处理成小的三角形是不是可以理解成类似于几何着色器的功能?还是说是图元装配的过程??另外,因为 Metal 已经是和 OpenGL 一个级别的框架了,所以 Metal 中的 pipline 并不一定遵循 OpenGL 中的 pipline,所以不要对号入座,最多只能借鉴和参考;
6. 光栅化
OpenGL 中的一个片段(Fragment)是 OpenGL 渲染一个像素所需的所有数据。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),光栅化阶段会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader) 使用的片段(Fragment)。
这一步说白了就是图元到硬件(像素)的转换。
因为屏幕是由很多个像素点组成,一个图形要展示在屏幕上,就需要知道哪些像素点需要亮起来,且要用什么强度的信号来展现出怎样的颜色。光栅化这一步就是将上下文坐标系中的图元转化成硬件层面上真正要展示的像素点,即:光栅化就是计算出哪些像素需要展示。而像素具体需要展示成什么颜色则在下一步的片段着色器中计算;
光栅化阶段计算并生成像素点模型(容器),片段着色器阶段生成完整的像素点数据(向容器中填充数据)。所以,一个 Fragment 中包含该像素被渲染时所需要的所有的数据。
一种简单的划分就是根据中心点,如果像素的中心点在图元内部,那么这个像素就属于这个图元。如上图所示,深蓝色的线就是图元信息所构建出的三角形;而通过是否覆盖中心点,可以遍历出所有属于该图元的所有像素;
如上图,浅蓝色部分就是光栅化计算出来需要展示的像素点;
之前的步骤都相当于美术中的构图,只不过电脑世界的构图是以三角形作为基本图形。而光栅化这一步就相当于美术中的素描,这一步完成后就意味着将形状画到纸上了。素描完成了,接下来就是上色了,也就是美术中的水彩等阶段。
另外,在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
7.片段着色器
片段着色器(Fragment Shader)也叫做像素着色器(Pixel Shader),这个阶段的目的是给每一个像素 Pixel 赋予正确的颜色。
计算像素点的颜色需要顶点 + 场景数据。顶点可以从前面的步骤中获取。通常,片段着色器包含 3D 场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色由于需要处理纹理、光照等复杂信息,所以该阶段通常是整个系统的性能瓶颈。
片段着色器的输入是什么?工作方式是什么?多个像素点并发、单独使用一个片段着色器计算?
8. 混合阶段
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
另外,iOS 中的混合可能会相对简单,因为 iOS 是基于 TBR 方式计算了,如果没有离屏渲染,是不是混合都没有必要?或者说,混合是在 Tiler 生成之前就做完了?具体还得学习下 Metal;