学习目录:https://blog.csdn.net/weixin_42513339/article/details/83242032
(若下文中有概念不懂可能看我之前的学习记录。)
目录
GPU从CPU得到渲染命令以后,就会进行一系列的流水线操作,最终把图元渲染到屏幕上。
而且GPU渲染的过程就是GPU流水线。
对于GPU操作是在于几何阶段和光栅化阶段,但是开发者没有绝对的控制权。虽然我们无法控制这两个阶段的具体细节,但是GPU向开发者开放了很多的控制权。
上一讲的几何阶段和光栅化阶段可以分成若干个更小的流水线阶段。如下图:
- 绿色:代表该流水线阶段是完全可编程控制的。(实线框表示该shader必须由开发者编程实现,虚线框表示该shader是可选的)
- 黄色:代表该流水线阶段可以配置但不是可编程的。
- 蓝色:代表该流水线阶段可由GPU固定实现的,开发者没有任何控制权。
GPU接收顶点数据作为输入。顶点数据是由应用阶段加载到显存中,再由DrawCall指定的。这些数据随后被传递给顶点着色器。
- 顶点着色器完全可编程(Vertex Shader)是完全可编程的,通常用于实现顶点的空间变化、顶点着色等功能。
- 曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。
- 几何着色器(Gerometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者用于产生更多的图元。
- 剪裁(Clipping)这一阶段的目的是将那些不再摄像机视野的顶点裁剪掉,并剔除某些三角图元的面片。(可配置)
- 屏幕映射(Screen Mapping)这一阶段并不能配置和编程,他负责把每个图元的坐标转换到屏幕坐标系中。
- 在光栅化概念中三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也是固定函数阶段。(不能改)
- 接下去的片元着色器(Fragment Shader)则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。
- 最后,逐片元操作(Per-Fragment Opeartions)阶段负责执行多个重要步骤,例如修改颜色、深度缓冲、进行混合等,有很高的可配置性。
顶点着色器
输入来自于CPU,顶点着色器的处理单位是顶点,即每输入一个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点和顶点之间的关系。
但是GPU可以利用本身的特性并行化处理每一个顶点,这意味着这一阶段的处理速度会变快。
顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照。当然,顶点着色器还可以输出后续阶段所需要的数据。
顶点着色器最基本需要完成的工作:顶点着色器把顶点坐标从模型空间转换到齐次裁剪空间。
接着通常再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates,NDC)
数学推导后面会讲到。转换过程如下:
上图的坐标范围同时是OpenGL同时也是Unity使用的NDC,它的 z 分量范围在 [-1,1]之间,而DirectX中,NDC的 z 分量范围确实[0,1]。
而输出的话,可以经光栅化后交给片元处理器进行处理或者还可以发送给曲面细分着色器或几何着色器。(这里跳过曲面细分着色器、几何着色器)。
剪裁
剪裁就是为了去除”不在摄像机视野范围内的物体”。
一个图元和摄像机视野的关系有三种:完全在视野内、部分在视野内、完全在视野外。
完全在视野内的图元就继续传递给下一个流水线阶段。
完全在视野外的图元不会继续向下传递,无需被渲染了。
部分在视野内的图元需要进行一个处理,这就是裁剪。
由于在上一步,我们已经得知在NDC下的顶点位置,即顶点位置在一个立方体内,因此裁剪就变得简单:只需要将图元裁剪到单位立方体内即可。如下图:
这一步不可编程,但是我们可以自定义一个裁剪的操作来对这一步进行配置。
屏幕映射
这一步的输入仍然是三维坐标系下的坐标(范围在单位立方体内)。
屏幕映射(Screen Mapping)的任务就是把每个图元的 x 和 y 坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
由于我们输入的坐标是-1到1,转换到屏幕窗口上实际上就是一个缩放的过程。(平米映射不会对z坐标做任何处理)
屏幕坐标系和 z 光标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates)。这些值会一起被传递到光栅化阶段。
PS:屏幕坐标系在OpenGL和DirectX之间具有差异
三角形设置
这一步开始进入光栅化阶段。从上一个阶段输出的信息是屏幕坐标系下的顶点位置以及和它们相关的额外信息,如深度值(z)、法线方向、视角方向等。
光栅化两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色
三角形设置这个阶段会计算光栅化一个三角网格所需的信息。
上一阶段屏幕映射输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。
但如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标。而为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方法。
这样一个计算三角网格表示数据的过程就叫做三角形设置。
三角形遍历
三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。
如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也称为扫描变换(Scan Conversion)。
三角形遍历阶段会根据上一阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值。如下图:
上图是三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。
这一步输出就是得到一个片元序列。PS:一个片元并不是真正意义的像素,而是包含了很多状态的集合(如:屏幕坐标、深度信息等)
片元着色器
片元着色器(Fragment Shader)是另一个非常重要的可编程着色器阶段。在DirectX中,片元着色器被称为像素着色器。
片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。
这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
PS:虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。
逐片元操作
逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这个阶段被称为输出合并阶段(Output-Merger)。
通过这两个名称也可以看出这个阶段目的就是合并,操作的那位是片元。
这个阶段的主要任务:
- 决定每个片元的可见性,这个涉及很多测试工作,例如:深度测试、模板测试等。
- 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并。
PS:逐片元操作阶段是高度可配置性的,即我们可以设置每一步的操作细节。
测试的过程实际上是个比较复杂的过程,而且不同的图形接口的实现细节也不尽相同。下图是最基本的深度测试和模板测试简化流程图。
模板测试:如果开启模板测试,GPU首先会读取(使用读取掩码)模板缓冲区中该片元位置的模板值,然后将该值和读取(使用读取掩码)到的参考值进行比较,这个比较函数可以由开发者指定,例如:小于时舍弃该片元,或者大于等于时舍弃该片元。如果该片元没有通过测试,则该片元会被舍弃。
不管一个片元有没有通过模板测试,我们都可以根据模板测试和下面的深度测试结果来修改模板缓冲区,这个修改操作也是由开发者指定的。开发者可以设置不同结果下的修改操作。
深度测试:如果开启了深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。 这个比较函数可由开发者设置,例如小于时舍弃或者大于时舍弃该片元。 通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区的值,那么就会舍弃它。这是因为我们总想只显示出离摄像机最近的物体,而那些被遮挡的就不需要在屏幕上。
和模板测试不同,如果一个片元没有通过深度测试,它就没有权利更改深度缓冲区的值,如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖原有的深度值,这是通过开启/关闭深度写入做到的。
PS:透明效果和深度测试以及深度写入的关系非常密切。
如果一个片元通过了上面的所有测试,那么就来到了 混合操作 的前面。
对于不透明物体,开发者可以关闭混合(Blend)操作,这样片元着色器计算得到的颜色值就会直接覆盖点颜色缓冲区中的像素值。
对于半透明物体,那么我们就需要使用混合操作来让这个物体看起来是透明的。混合操作简化版流程图如下:
从上述流程图中,可以看出,混合操作可以高度配置:开发者可以旋转开启/关闭混合功能。
如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,而这也是许多初学者发现无法得到透明效果的原因。(未开启混合功能)
如果开启混合功能,GPU会取出源颜色和目标颜色,将两个颜色进行混合。(源颜色指的是片元着色器得到的颜色值,目标颜色指的是已经存在于颜色缓冲区的颜色值)。
之后便会使用一个混合函数进行混合操作。这个混合函数通常和透明通道息息相关,例如根据透明通道的值进行相加、相减、相乘等。
上面的测试顺序不唯一,这也是为了效率问题,作为GPU,它会希望尽可能早知道哪些片元会被舍弃,对于这些片元就不需要使用片元着色器来计算它们的颜色。在Unity给出的渲染流水线中,我们也可以发现深度测试是在片元着色器之前的。这种技术成为Early-Z技术。
为了避免我们看到正在光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。这意味着,对场景的渲染是幕后发生的,即在后置缓冲中,一旦场景已经被渲染到后置缓冲中,GPU会交换后置缓冲区和前置缓冲区中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此保证我们看到的图像是连续的。
图示场景中包含了两个对象:球和长方体,绘制顺序是先绘制球(在屏幕上显示为圆),再绘制长方体(在屏幕上显示为长方形)。如果深度测试在片元着色器之后执行,那么在渲染长方体时,虽然它的大部分区域都被遮挡在球的后面,即它所覆盖的绝大部分片元根本无法通过深度测试,但是我们仍然需要对这些片元执行片元着色器,造成了很大的性能浪费
疑惑地方
1.OpenGL和DirectX
渲染命令也被称为Draw Call
2.HLSL、GLSL、Cg
在可编程管线出现之前,为了编写着色器代码,需要用汇编语言。为了给开发者打开方便之门,就出现了更高级的着色语言(Shading Language)。
常见的着色语言有 DirectX 的 HLSL (High Level Shading Language) 、OpenGL 的 GLSL (OpenGL Shading Language)、NVIDIA 的 Cg (C for Graphic)。
GLSL优点:跨平台性,可以在 Windows、Linux、Mac 甚至移动等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持GLSL的编译就可以运行,由于供应商完全了解自己的硬件构造,所以能发挥出最大作用。但是这也说明不同硬件供应商对GLSL的编译实现不尽相同,可能会导致编译结果不一样。
HLSL:微软控制着色器编译,就算用不同的硬件,同一个着色器的编译结果也是一样的(版本相同)。但也因此支持HLSL的平台相对有限,几乎都是微软自己的产品,如Windows、Xbox360等。
Cg:则是真正意义上的跨平台。它会根据不同平台的不同,编译成相应的中间语言。Cg可以无缝移植HLSL的代码(与微软合作),但缺点是可能无法发挥出OpenGL的最新特性。
3.Draw Call
本身含义就是CPU调用图像编程接口。
CPU和GPU的流水化线的实现:
CPU通过图像编程接口向命令缓冲区中添加命令,而GPU从中读取命令并执行。黄色方框内的命令就是Draw Call,而红色方框内的命令用于改变渲染状态。我们使用红色方框来表示改变渲染状态的命令, 是因为这些命令往往更加耗时 。
如果Draw Call多了则会影响帧率?
由于CPU会把大量时间花费在提交Draw Call上,造成CPU过载。
命令缓冲区中的虚线方框表示GPU已经完成的命令。此时,命令缓冲区中没有可以执行的命令了,GPU处于空闲状态,而CPU还没有准备好下一个渲染命令。
如何减少Draw Call ?
使用 批处理 ,把很多小的Draw Call 合并成一个大的 Draw Call
总结
Shader :
- GPU流水线上可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的;
- 有一些特定类型的着色器,如顶点着色器、片元着色器等;
- 依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。