主要参考《Unity Shader入门精要》一书,外加自己的一些总结
前言
什么是流水线
采用流水线的好处在于大大提高了单位时间的额生产量。
什么是渲染流水线
在学习Shader(着色器)之前,先了解一下渲染流水线,因为Shader是渲染流水线中的一个环节。
渲染流水线分为两种,一种是可编程渲染流水线,另一种是固定渲染流水线。现在的渲染流水线基本都是可编程的,以实现不同的效果。
渲染流水线分为三个阶段:应用阶段、几何阶段、光栅化阶段。
图1 渲染流水线中的三个概念阶段
a.应用阶段(CPU 处理)
这个阶段是由我们开发者主导的,主要有3个任务:
1), 准备好场景数据,比如摄像机位置、视锥体、模型、光源等;
2),粗粒度剔除工作,把不可见的物体剔除出去;
3),设置好每个模型的渲染状态(使用的材质、使用的纹理、使用的Shader等)。
这一阶段最重要的输出是渲染所需的几何信息,即渲染图元,传递给下一个阶段,几何阶段。图元:实际上就是点、线、面。
b.几何阶段(GPU 处理)
几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。该阶段的重要任务是把顶点坐标转换到屏幕空间中,再交给光栅器处理。
通过对渲染图元进行处理后,将输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等信息,并传递给下一阶段。该阶段可以分为更小的流水线阶段。
c.光栅化阶段(GPU处理)
使用几何阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。该阶段可以分为更小的流水线阶段。
光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。
CPU与GPU之间的通信
所有渲染所需的数据都需要从硬盘(HDD)中加载到系统内存(RAM)中,然后网格和纹理等数据又被加载到显卡上的存储空间,即显存(VRAM)中。这是因为显卡对显存的访问速度更快,而且大多数显卡对RAM没有直接的访问权利。
数据加载到显存中之后,系统内存RAM中的数据就可以移除了,但如果CPU需要这些数据来进行碰撞检测等,则系统内存里的数据就会保留。
图2 渲染所需的数据(两张纹理以及3个网格)从硬盘最终加载到显存中。在渲染时,GPU可以快速访问这些数据
(2)设置渲染状态:
渲染状态是指场景中的网格是怎样被渲染的,使用的哪个Vertex Shader 、Fragment Shader、光源属性、材质等。
(3)调用Draw Call:
实际上,Draw Call 就是一个命令,它的发起者是CPU,接收者是GPU。一个Draw Call指向本次需要被渲染的图元列表。给定一个Draw Call后,GPU会根据渲染状态和所有的顶点数据来进行计算,最终输出成屏幕上显示的像素,这个计算过程,就是GPU流水线。
图3 CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程。一个Draw Call会指向本次调用需要渲染的图元列表
GPU流水线
从上图可以看出,GPU的渲染流水线接受顶点数据作为输入,这些顶点数据是由应用阶段加载到显存中的,再由Draw Call指定的。这些数据随后被传递到顶点着色器。顶点着色器是完全可编程的,主要用于实现顶点的空间变换、顶点着色等。
(2)曲面细分着色器:
是一个可选的着色器,用于细分图元。
(3)几何着色器:
也是一个可选的着色器,用于执行逐图元的着色操作,或被用于产生更多的图元。
(4)剪裁:
这一阶段是可配置的。这一流水阶段的目的是把哪些不在视野内的顶点剪掉,并剔除某些三角形图元的面片。
一个图元与摄像机视野的关系有 3 种:完全在视野内、部分在视野内、完全在视野外。
完全在视野内的图元会继续传递给下一个流水线阶段,完全在视野外得图元不会继续向下传递,部分在视野内的图元需要做剪裁处理。
(5)屏幕映射:
这一阶段是不可配置和编程的,主要负责把每个图元的坐标(三维坐标系)转换成屏幕坐标(二维坐标系)。
光栅化阶段
(1)三角形设置:
上一阶段输出的信息是屏幕坐标系下的顶点位置以及相关的额外信息,如深度值(z坐标)、法线方向、视角方向等。
光栅化阶段的重要目标是:计算每个图元覆盖了哪些像素 和 为这些像素计算它们的颜色。
三角形设置这一阶段,会计算光栅化一个三角网格所需的信息。上一阶段输出的是三角网格的顶点,如果我们想得到整个三角网格的覆盖情况,就必须计算每条边上的像素坐标而得到三角形边界的表达方式。这样一个得到三角形边界表示方式的过程就是三角形设置。
(2)三角形遍历:
这一阶段会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元。这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历。
三角形遍历阶段会根据上一阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。
注:一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了屏幕坐标、深度信息、及从几何阶段输出的顶点信息,如法线和纹理坐标等。
图5 三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图2.12中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0
(3)片元着色器:
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,输出是一个或者多个颜色值。片元着色器是一个非常重要的可编程着色器阶段。这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。片元着色器可以完成很多重要的效果,但它仅可以影响单个片元。
图6 根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色
(4)逐片元操作:
主要任务有两个:
a, 决定每个片元的可见性。这涉及了很多测试工作,如深度测试、模板测试等。
b, 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
图7 逐片元操作阶段所做的操作。只有通过了所有的测试后,新生成的片元才能和颜色缓冲区中已经存在的像素颜色进行混合,最后再写入颜色缓冲区中
什么是OpenGL/DirectX
OpenGL和DirectX 就是图形应用编程接口,这些接口用于渲染二维或三维图形,这些接口架起了上层应用程序和底层GPU的沟通桥梁。概括来说,我们的应用程序运行在CPU上,应用程序通过调用OpenGL或DirectX的图形接口将渲染所需的数据,如顶点数据、纹理数据、材质参数等数据存储在显存中的特定区域。随后,开发者可以通过图形编程接口发出渲染命令(Draw Call),它们将会被显卡驱动翻译成GPU能够理解的代码,进行真正的绘制。
图8 CPU、OpenGL/DirectX、显卡驱动和GPU之间的关系
什么是HLSL、GLSL、CG
HLSL:High Level Shadeing Language,DirectX的着色器语言,由微软控制着色器的编译,就算使用不同的硬件,同一个着色器的编译结果也是一样的,但也因此支持HLSL的平台相对比较有限,几乎都是微软自己的产品,如Windows、Xbox 360、PS3等,这是因为在其他平台上没有可以编译HLSL的编译器。
GLSL:OpenGL Shading Language,OpenGL的着色器语言,优点在于跨平台性,可以在Windows、Linux、Mac甚至移动平台等多平台上工作,但这种跨平台性是由于OpenGL没有提供着色编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持对GLSL的编译它就可以运行。
CG:NVIDIA的着色器语言,真正意义上的跨平台,它会根据平台的不同,编译成相应的中间语言。
什么是Draw Call
深入了解Draw Call之前,先了解一下CPU和GPU是如何实现并行工作的。如果没有流水线,那么CPU需要等到GPU完成上一个渲染任务才能再次发送渲染命令。这种方法显然会造成效率低下。因此,要像隔壁老王的洋娃娃厂一样,我们需要让CPU和GPU可以并行工作。解决方法就是使用一个命令缓冲区。
命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是相互独立的。命令缓冲区中的命令由很多种类,而Draw Call就是其中一种,如下图:
图9 命令缓冲区。CPU通过图像编程接口向命令缓冲区中添加命令,而GPU从中读取命令并执行。黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。我们使用红色方框来表示改变渲染状态的命令,
是因为这些命令往往更加耗时
因为GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没什么区别,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量时间花费在提交DrawCall上,造成CPU的过载。因此要减少Draw Call,就要把很多小的DrawCall合并成一个大的DrawCall,这就是批处理的思想。批处理技术更适合那些静态的物体,比如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。当然,对动态物体也可以进行批处理,但是,由于这些物体是不断运动的,因此每一帧都需要重新进行合并并再发送给GPU,这对空间和时间都会造成一定的影响。
游戏开发过程中,为了减少Draw Call的开销,需要注意下面两点:
1), 避免使用大量很小的网格。如果需要使用很小的网格,考虑是否可以合并它们。
2), 避免使用过多的材质。尽量在不同的网格之间公用同一个材质。