1、什么是 shader
shader 中文名为着色器,全称为着色器程序,是专门用来渲染图形的一种技术。通过 shader,我们可以自定义显卡渲染画面的算法,使画面达到我们想要的效果。小到每一个像素点,大到整个屏幕。通常来说,程序是运行在 CPU 中的,但是着色器程序比较特殊,它是运行在 GPU 中的,所以当我们在编写 shader 程序的时候,实际上也是在编写 GPU 程序。在 OpenGL 中,对应的着色器语言是 GLSL(OpenGL Shading Language)。通过 shader 编程,我们可以实现很多渲染风格,如马赛克效果、素描风格等。
2、OpenGL 图形渲染流程
当我们使用 OpenGL 时,都是基于 3D 空间去编程的,但是最终呈现到屏幕或者窗口时却是二维的像素数组,所以简单来说 OpenGL 的渲染流程其实就是将 3D 坐标转换成适配屏幕的 2D 像素,而这个过程实际上是由 OpenGL 的图形渲染管线管理的,大致可以划分成两步:
- 将 3D 坐标转换成 2D 坐标。
- 将 2D 坐标转换成实际有颜色的像素。
如下图所示,图形渲染管线可以被划分为顶点着色器、图元装配、几何着色器、光栅化、片段着色器和测试混合六个阶段,每一个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在 GPU 上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序就是 shader。
2.1. 顶点着色器
3D 图形都是由一个个三角面片组成的,顶点着色器就是计算每个三角面片上的顶点,并为最终像素渲染做准备。在顶点着色器中,可以访问到顶点的三维位置、颜色、法向量等信息。可以通过修改这些值,或者将其传递到片元着色器中,实现特定的渲染效果。
可以作为顶点着色器的输入有:
- 用
attribute
修饰的属性,可以传递顶点数据、纹理坐标等。 - 用
uniform
修饰的属性,可以传递变换矩阵等。
顶点着色器常见的输出有:
gl_Position
, 将变换后的顶点数据进行输出。gl_PointSize
, 设置点的大小。
在顶点着色器进行的业务处理有:
- 矩阵变换的计算
- 计算光照公式生成逐顶点颜色
- 生成 / 变换纹理坐标
2.2. 图元装配
图元装配,即将从顶点着色器中输出的顶点根据 primitive (原始的连接关系)还原成网格结构。网格由顶点和索引组成,在这个阶段是根据索引将顶点连接在一起,组成线、面单元。之后就是对超出屏幕外的三角形进行裁剪。
这里的裁剪怎么理解呢?假设有一个三角形,三角形的一个顶点在屏幕外,两个顶点在屏幕内,这个时候就需要将超出屏幕外的三角形裁剪掉,所以我们能看到的其实是一个四边形,然后再将这个四边形的顶点装配成两个三角形图元的形状。
同时在图元装配这个阶段还需要根据三角形面片的顶点顺序 —— 也就是三角形的法向量朝向来判断是否要进行去除操作。一般顶点按照逆时针排序,根据右手定则来决定三角面片的法向量,如果该法向量朝向视点(法向量与到视点的方向的点积为正),该面是正面。如果该面是反面,则进行背面去除操作。
这里注意:在这个阶段进行的所有裁剪剔除计算都是为了减少需要绘制的顶点个数。
2.3. 几何着色器
几何着色器位于顶点和片段着色器之间,如果没有使用时,则顶点着色器输出到片元着色器,在使用几何着色器后,顶点着色器输出组成一个基础图元的顶点信息到几何着色器,经过几何着色器处理后,再输出到片元着色器。几何着色器能够产生 0 个以上的基础图元 (primitive),它能起到一定的裁剪作用、同时也能产生比顶点着色器输入更多的基础图元。
几何着色器在启用后,它将获得顶点着色器以组成一个基础图元为一组的顶点输入,通过对输入的顶点进行处理,几何着色器将决定输出的图元类型和个数。当输出的图元减少或者不输出时,实际上起到了裁剪图形的作用,当输出的图元类型改变或者输出更多图元时起到了产生和改变图元的作用。
2.4. 光栅化
光栅化阶段会接收来自几何着色器的图元数据输出。在这个阶段会把图元映射为最终屏幕上相应的像素,生成供片段着色器 (Fragment Shader) 使用的片段 (Fragment)。在片段着色器运行之前会执行裁切
(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。光栅化分为三角形设置与三角形遍历两个阶段:
- 三角形设置: 光栅化的第一个流水线阶段是三角形设置,这个阶段会计算光栅化一个三角网格所需的信息。具体来说,上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。它的输出是为了给下一个阶段做准备。
- 三角形遍历: 三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元,而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历。三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格 3 个顶点的顶点信息对整个覆盖区域的像素进行插值。下图展示了三角形遍历阶段的简化计算过程。
这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了 (但不限于) 它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
2.5. 片段着色器
在片段着色器阶段的主要目的是计算一个像素的最终颜色,这也是所有 OpenGL 高级效果产生的地方。通常,片段着色器包含 3D 场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
这里注意:光栅化阶段后得到的是一个个 “片元”。片元和像素已经非常接近了,但两者仍是有区别的。用一种通俗的说法来解释的话,就是比如三维空间内有两个从摄像机角度看过去一前一后的三角形,它们重叠部分的显示区域,每个像素对应两个片元;不重叠的部分,像素和片元一一对应。当然,这个例子是简化过的,真实的对应关系可能更复杂一些。片段着色器也是我们能够在图形渲染过程中进行编程的一个阶段。
2.6. Alpha 测试和混合
Alpha test 是一种类似 depth test 一般的存在,简单粗暴,通过多个条件来判断当前的片元是否通过测试,只要有一个条件不通过,即被舍弃而不会对后续渲染产生任何影响。当前片元的透明度是其中一个重要的指标,通常我们设定一个阈值,如果透明度小于这个阈值,那么就会被直接舍弃,相当于这个片元透明到 "看不到"、"消失" 了一般;而高于这个阈值的面片则会被当作不透明的物体来进行处理。这种简单粗暴的方法无法实现真正透明的效果。
Alpha blending 则能够真正实现透明的效果。它将当前面片的 alpha 通道值(透明度)作为混合因子,参与该面片本身的颜色与颜色缓冲区中本身颜色的混合。需要注意的是,alpha 混合过程中需要关闭深度写入,但不关闭深度测试。不关闭深度测试意味着,当一个不透明的物体在另一个物体前面的时候,能够通过深度测试正常渲染更近的不透明的物体。
所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
3、参考文章
- 卡通渲染(上)
- 光栅化阶段:三角形设置、三角形遍历、像素着色、合并
- OpenGL - 渲染流程
- 透明度测试和透明度混合
紧追技术前沿,深挖专业领域
扫码关注我们吧!