渲染管线
概述
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。
图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。
正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
有些着色器允许开发者自己配置,这就允许我们用自己写的着色器来替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了,而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间。OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。
下面,你会看到一个图形渲染管线的每个阶段的抽象展示。要注意蓝色部分代表的是我们可以注入自定义的着色器的部分。
图形渲染管线包含很多部分,每个部分都将在转换顶点数据到最终像素这一过程中处理各自特定的阶段。
首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据。
图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状;本节例子中是一个三角形。
图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
渲染管线各个步骤详解
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 图中,一个三角形变成了两个,需要注意的是,这里变成两个三角形并不是某些渲染流程相关的潜规则,而是完全由几何着色器的代码决定的。
因为几何着色器是可编程的,所以这里开发者可以编写几何着色器对图元进行多样化的处理的。
因此,各种各样的几何着色器可以理解成实现各种功能的 Api,也就是传入顶点数据之后自动生成对应的图元数据。比如上述小房子的几何着色器就是实现传入一个顶点,从而生成一个小房子图元的功能。
6. 光栅化
OpenGL 中的一个片段(Fragment)是 OpenGL 渲染一个像素所需的所有数据。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),光栅化阶段会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader) 使用的片段(Fragment)。
这一步说白了就是图元到硬件(像素)的转换。
因为屏幕是由很多个像素点组成,一个图形要展示在屏幕上,就需要知道哪些像素点需要亮起来,且要用什么强度的信号来展现出怎样的颜色。光栅化这一步就是将上下文坐标系中的图元转化成硬件层面上真正要展示的像素点,即:光栅化就是计算出哪些像素需要展示。而像素具体需要展示成什么颜色则在下一步的片段着色器中计算;
一种简单的划分就是根据中心点,如果像素的中心点在图元内部,那么这个像素就属于这个图元。如上图所示,深蓝色的线就是图元信息所构建出的三角形;而通过是否覆盖中心点,可以遍历出所有属于该图元的所有像素;
如上图,浅蓝色部分就是光栅化计算出来需要展示的像素点;
之前的步骤都相当于美术中的构图,只不过电脑世界的构图是以三角形作为基本图形。而光栅化这一步就相当于美术中的素描,这一步完成后就意味着将形状画到纸上了。素描完>成了,接下来就是上色了,也就是美术中的水彩等阶段。
另外,在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
7.片段着色器
片段着色器(Fragment Shader)也叫做像素着色器(Pixel Shader),这个阶段的目的是给每一个像素 Pixel 赋予正确的颜色。
计算像素点的颜色需要顶点 + 场景数据。顶点可以从前面的步骤中获取。通常,片段着色器包含 3D 场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色由于需要处理纹理、光照等复杂信息,所以该阶段通常是整个系统的性能瓶颈。
8. 混合阶段
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。