"The display is the computer."——Jen-Hsun Huang。那个显示器就是电脑。

图形加速的第一步是针对三角形进行像素扫描的时候进行颜色插值,并将这些值显示出来。除此之外,还包括采样纹理图片中的数据并将其粘合到物件表面上。添加用于插值和检测深度值的硬件,提供内置的可见性检测。由于它们会被频繁使用,所以使用专用的硬件来提高性能。越来越多的管线环节以及每个环节中越来越多的功能都是在几代中逐步添加的。专用图形硬件相比较CPU的唯一处理优势就是速度,然而速度也是最重要的。

在过去的二十多年中,图形硬件发生了翻天覆地的变化。第一个包含硬件几何处理器的消费级图形芯片(Nvidia的GeForce256)于1999年上市。Nvidia创建了GPU这个概念,将GeForce256与之前只有光栅化的芯片区分开来。在之后的几年中,GPU从可配置的复杂的固定管线发展成为开发者可以实现自己算法的高度可编程的空白板。各种可编程的着色器从此成为了控制GPU的主要手段。为了效率,目前管线中仍然有一部分是通过配置,而非编程,但是整体的趋势是向可编程以及灵活性发展。

GPU专注于实现一系列高度可并行的任务,并以此获得超高的速度。GPU中包含了专门用于访问Z缓冲区、快速访问纹理图片以及其他缓冲区、以及查找三角形内所有像素的硅(芯片)。第23章会详细介绍这些元素如何完成它们的功能。对于我们,首先需要了解的是,GPU如何实现可编程着色器的并行性。

第3.3节解释了着色器的工作原理。现在我们需要知道的是,着色器core是一个用于执行一些相对独立任务的小型处理器,比如讲顶点的位置从世界空间转移到屏幕空间,或者是计算三角形内某个像素的颜色。由于每帧都会有百万级的三角形传递给屏幕,那么每秒会有十亿级别的着色器调用,所以需要将着色器调用按照像素独立开来,去并行的运算一个个着色器实例。

首先,所有处理器都要面临延迟这个问题。访问数据需要一定的时间。一般情况,信息离处理器越远,等待的时间越长。第23.3节详细的介绍了延迟。访问内存芯片中的数据要比访问本地寄存器的数据消耗的时间要长。第18.4.1节详细的介绍了内存访问。关键是等待数据意味着处理器停止工作,也就会降低性能。

3.1 数据并行架构

各种不同的处理器架构使用各种不同的策略去用于避免停顿。经过优化的CPU可以处理各种数据结构和大型代码库。CPU可以有多个处理器,但是每个处理器都以串行的方式运行代码,尽可能少的SIMD适量处理把异常控制到最小范围。为了减少延迟带来的影响,大部分CPU芯片都包含一些快速的本地缓存,其中包含了下一步可能就会用上的数据。CPU通常还会使用一些巧妙的技巧去避免停顿,比如分支预测、指令重新排序、寄存器重命名以及预取缓存等。

GPU的方案则不同。GPU芯片中的大部分区域都是由一大组,甚至数以千计被称为shader core的处理器组成。GPU是流式处理器,依次按照顺序处理一些类似的数据。也正是因为顶点/像素这些数据的类似性,GPU才可以以大规模并行方式处理这些数据。另外一个重要原因则是这些单元相对独立,并不需要邻里之间互通信息,也不共享可写内存位置。然而这些规定有时候也会因为一些新的、有用的功能被打破,但是这种打破就会因为一个处理器需要等待其他处理器完成工作,从而导致可能出现延迟。

CPU针对吞吐量进行了优化,从而可以得到数据处理的最大速率。但是这种快速处理有一个成本,也就是芯片中只有较少区域用于缓存区和控制逻辑,导致每个shader core的延迟通常比CPU处理器的延迟要高。

假设一个mesh被光栅化成2000个像素,也就是说下面的像素着色器要被调用2000次。假设我们使用一个世界上最烂的GPU,只有一个着色器处理单元。它将从第1个片元开始执行着色器程序,一直到第2000个。着色器处理器针对寄存器上的数值执行了一些算数运算,由于寄存器是本地的,访问速度很快,所以不会出现停顿。然后,着色器要去执行一个纹理采样的指令,通过表面位置信息获取到图片中的颜色并将其运用给mesh。由于纹理是一些完全独立的资源,并非像素着色器的本地内存,所以需要去访问纹理。然而由于一次内存访问需要数百数千个时钟周期,而这个阶段GPU处理器无事可做。这时着色器处理器也就会停顿下来,去等待所需要的纹理颜色。

为了优化这种糟糕的GPU,可以给每个片元一个小的存储空间用来当做本地寄存器。现在,我们决定不停下来去等待纹理采样,目前的着色器处理器使得我们可以切换并执行另外一个片元,比如2000个片元中的第2个。这个切换很快,而且不会影响第一个或者第二个片元的任何内容,只需要注意目前第一个片元执行到哪条指令了即可。然而开始执行第2个片元,与第1个片元类似,执行一些算数函数,知道再次遇到纹理采样,然后依然切换到下个片元,第3个片元,直到2000个片元都通过这种方式处理完了(Patrick:我记得纹理会被采样过来放在缓存中,但是也不会采样很多吧,比如对应2000个片元那么多,所以是不是执行一部分,比如200个,就知道纹理被采样过来一部分了,然后就去继续进行第1个片元了?不然2000个结束后,发现第1个片元的纹理ok了,但是第2个片元的纹理没被拿过来就尴尬了。)此时,着色处理器返回到第一个片元。这个时候着色器所需要的纹理颜色已经获取到了,着色器处理器可以继续执行第1个片元的操作,这样下去,直到又遇到一个已知的会导致停顿的执行指令的到来,或者整个程序执行结束。从单个片元的执行时间来说,比不切换的模式要慢一点,但是总体来说,所有片元的总执行时间将大大缩短。

在这个体系结构中,通过不停的切换到另一个片元,使得GPU保持忙碌,从而隐藏了延迟。GPU通过把逻辑和数据分离,进一步实现了这一设计。这个技术被称为SIMD,single instruction multiple data。这样的安排,使得可以在固定数量的着色程序中通过lock-step技术执行相同的命令。相比较针对每个program使用一个独立的逻辑、调度,SIMD的优势在于调用了相当少的硅/power去处理这个事情。将2000个片元用于现在的GPU架构中,针对每个片元所调用的片元着色器都被称为一个线程。而这里的线程和CPU线程不同。它包含了一位内存用于存放该shader的输入,以及材质球执行所需要的任何寄存器空间。使用相同shader program的线程被合并成了一个group,在NV被称为warps,在AMD被称为wavefronts。一个Warps/Wavefronts被安排通过8-64个GPU Shader cores执行,使用SIMD技术。每个线程都被映射到了一个SIMD通道中。

假如我们有2000个thread需要被执行,NV GPU中的一个warps包含32个线程。也就是说将产生2000/32=62.5个warps,也就是说会分配63个warps,其中有一个warp有一半是空的。warp的执行类似于一个单一GPU执行的例子。因为这些线程执行的是相同的指令,当遇到内存提取时,所有的线程都会同时遇到。然后这个提取的信号会告知warps中的所有线程都停止等待他们(不同的)结果。而warps不会停止,而是转移到另外一个包含32线程的warp,然后使用这32个core继续执行。由于交换的时候没有任何数据被涉及,所以这种交换速度和我们的单处理器系统一样快。每个线程都有自己的寄存器,每个warp都会记录它执行到了哪个指令了。交换到一个新的warp只是将运算核心指向到另外一组线程继续执行了,而没有额外的开销。warps不停的执行和交换,直到全部结束。参考图3.1。

T03

如上图所示,是一个简单的着色器执行实例。一个三角形的所有片元,被称为线程,gather成warps。这里每个warps显示包含4个线程,其实实际上是包含32个线程。该shader program包含5条指令。GPU开始对第一个warps执行这些指令,直到遇到了“txr”指令触发stall,因为这个指令需要时间去获取数据。然后切换到第二个warp,并开始执行该shader program的前三条指令,直到再次遇到了stall。下面第三个warp被执行和stall,然后就会切换回第一个warp继续执行。如果这个时候“txr”指令所需要的数据还没回来,则真的stall直到数据准备ok。每个warp将依次完成。

在我们的简单示例中,纹理的内存获取延迟会导致warps的切换。而实际上,由于warps的切换成本很低,所以更短的延迟都有可能会触发warps的切换。还有一些其他技术可以用于优化(参考文献[945]),而warp的切换是所有GPU最主要的用于消除延迟带来影响的机制。这个过程的具体工作还涉及到几个因素。比如,如果线程很少,warps也很少,那么可能就无法消除延迟带来的影响。

shader program的结构也是影响效率的重要特性之一。一个主要因素是每个线程的寄存器使用量。在我们的示例中,我们假设2000个线程可以同时驻留在GPU上。而每个线程相关联的着色器所需要的寄存器越多,可驻留在GPU中的线程就越少,warps也就越少。warps越少,也就意味着无法通过warps切换缓解延迟。入住的warps呗称为“in flight”,这个数据被称为入住率。高入住率也就代表着有足够多用于处理的warps,导致处理器空闲的可能性也就越小。而低入住率通常会导致性能不佳。内存获取的频率也会影响到底有多少延迟需要被消除。Lauritzen(参考文献[993])描述了shader使用的共享内存以及寄存器的数量时如何影响入住率。Wronski(参考文献[1911、1914])针对shader program中执行的操作类型对应的理想入住率。

另外一个影响整体效率的因素是由于if语句和循环引起的动态分支。假如在着色程序中有“if”语句,如果所有线程评估后都走了相同线程,那么warps就可以继续,而不用考虑另外一个分支。然而,如果一些线程,即使只有一个,走了另外一个分支,这样的话该warp久需要执行全部分支,丢弃每个特定线程不需要的结果(参考文献[530、945])。这个问题被称为线程发散,其中一些线程可能需要执行循环迭代活着执行“if”路径,而warps中的其他一些线程不需要,从而使它们在此期间处于空闲状态。

所有的GPU都已经实现了这套体系架构,严格限制的系统,带来了每瓦的巨大的计算能力。了解这套系统可以帮助你更有效的使用它所提供的能力。在接下来的章节中,将会介绍GPU如何实现这些管线、可编程着色程序如何操作,以及每个GPU阶段的演进和功能。

3.2 GPU管线总览

GPU中实现了第二章中描述的几何处理、光珊化、像素处理这些管道的阶段。它们分为若干个硬件阶段,具有不同程度的可配置性或可编程性。图3.2显示了根据其可编程性或可配置性进行颜色编码的各个阶段。需要注意的是,这些物理阶段与第2章钟介绍的功能阶段有所不同。

T03

GPU实现了整个渲染管线。整个阶段根据用户操作可控制的程度以颜色标记出来。绿色阶段为完全可编程。虚线代表着可选阶段。黄色阶段代表着可配置但不可编程,比如在合并阶段可以使用大量的混合操作。蓝色阶段为完全fixed。

我们这里聊的是GPU的理论模型,通过API的形式暴露给开发者的。正如第18和23章所说,这些理论模型实际上的物理实现,都是由硬件vendor设计的。比如理论模型中的一个fixed-function可以通过GPU中相邻的可编程的阶段添加命令实现的。管道中一个简单的程序也可以被几个独立的单元拆分成若干元素,或者通过一个独立的pass完成。理论模型可以帮助你理解影响性能的因素,但是并非说GPU实际上就是这么实现的。

顶点着色器是一个完全可编程的阶段,用于实现几何处理阶段。几何着色器也是一个完全可编程阶段,用于操作一个图元(点、线、三角形)上的顶点。可用于逐图元的着色器运算,比如删除图元或者创建新的图元。曲面细分阶段和几何着色器都是可选的,并非所有的GPU都支持它们,特别是移动平台。

裁剪、图元组装、图元遍历都是通过fixed-function硬件实现的。屏幕映射受到窗口和视窗设置的影响,在内部行程了一个简单的比例缩放和重新定位。像素着色器阶段是完全可编程阶段。尽管merger阶段并非可编程,但是是高度可配置的,从而可以设置去执行各种操作。它实现了merging功能阶段,负责修改颜色、zbuffer、混合、stencil以及其他输出相关的buffer。像素着色器与merger阶段一同完成了第2章中介绍的像素处理阶段。

随着时间的推移,GPU管道已经从硬编码操作发展到增强灵活性和可控性阶段。引入可编程着色阶段是这一演变过程中最重要的一步。下一节将介绍各种可编程阶段的共同特性。

3.3 可编程着色器阶段

现代的shader program使用一套统一的shader design。也就是说,VS、PS、GS、TS使用的是同一套编程模型。内部使用相同的指令集体系结构(ISA)。DX中实现该模型的处理器被称为公共着色核心,拥有该核心的GPU局域统一的着色体系结构。这种体系结构背后的原因是,这样的话着色器处理器就可以被用于各种不同的角色(VS\PS\TS\GS),GPU就可以根据它所看到的来进行分配。比如一块拥有大量小三角形的mesh比拥有两个大三角形的mesh需要更多的VS处理器。而如果实现GPU把VS和PS的处理器分开的话,那么就很难使得所有的核心都处于工作状态。使用统一的着色核心,GPU就可以决定如何平衡这个负载了。

关于整个着色器编程模型远超这本书的范围,且还有大量的书、网站已经对其进行介绍了。shader是通过一种类似C语言的着色器语言编写,比如DX中的HLSL,OpenGL中的GLSL。DX的HLSL可以被编译成虚拟机字节码,也被称为中间语言(IL/DXIL),以提供硬件独立性(这样的话硬件实现和shader语法就分离了)。同时,中间代码表示法也可以允许着色程序离线编译和存储。驱动只需要将此中间语言转换成特定GPU的ISA即可。console平台一般都会避免这种中间代码阶段,因为这样的话该平台就只有一种ISA了。

基本的数据类型为32位单精度的float以及vector,尽管vector是shader code的一部分,但是实际上上述硬件并不支持。在现在GPU上,32位整形和64位浮点型也native支持了。浮点型vector基本用语保存数据,比如位置、法线、矩阵的行、颜色以及纹理坐标。整型基本被用于数量、索引或者位操作。聚合数据类型,比如结构体、数组、矩阵都已经被支持了。

一个drawcall会触发图形API去渲染一组图元,也就是说触发管线去执行shader命令。每个着色器阶段都会有两种输入:uniform用于保存针对一个DC的数据(不同DC不同),varying是用于保存光栅化得到的数据。比如,光源的颜色会通过uniform传入,而三角形的位置会是varying。纹理是一种特殊的uniform,之前总被用作物件颜色使用,现在可以看作是一个用于包含任何数据的大数组。

底层虚拟机针对不同类型的输入和输出提供不同的寄存器。用于存放uniform的寄存器远比存放varying的寄存器大得多。原因是每个像素的varying数据都不同,所以需要加以限制。而uniform的数据只读,且针对当前drawcall中全局通用。虚拟机还有一些通用的临时寄存器,用于暂存空间。所有类型的寄存器都可以使用临时寄存器中的整数值进行数组索引。着色器虚拟机的输入输出如下图3.3所示。

T03

上图为虚拟机和寄存机在SM4.0下的布局图。每个资源旁都指示了最大可用数。用斜线分割的三个数字表示VS\GS\PS的限制。

现代GPU中会高效的执行图形学中常见的计算。shading语言将最常用的操作暴露出来,比如加法+和乘法*。其他被优化的操作会通过内部函数暴露,比如atan、sqrt、log以及其他。还会有一些更加复杂的函数,比如向量归一化、反射、叉乘、矩阵转置、行列式计算等。

分支控制使用分支指令去切换不同分支代码的执行。分支控制相关的指令被用于实现高级语言中的“if”、“else”等指令。着色器支持两种不同类型的分支控制。静态分支控制是通过uniform的值。也就是说在当前DC使用哪个分支是固定的。这个的优势在于针对各种不同的情况(比如不同数量的灯光)每个DC当前所执行的着色器不会有分支。由于所有的实现都使用相同的代码,这样的话就不会有分支差异。动态分支是根据varying的输入,也就是说每个片元所执行的代码都不同。这个比静态分支厉害,但是也消耗性能,特别是分支变化不稳定的情况下。

3.4 着色器语言和API的发展

可编程着色器框架的想法起源于1984年的Cook's shade trees(参考文献[287])。图3.4展示了一个简易的shader以及其相应的shade tree。RenderMan着色器语言(参考文献[63、1804])于二十世纪80年代末根据这一思想发展而来。它与其他不断发展的规范(比如Open Shading Language OSL)一起,至今仍然用于电影制作渲染。

T03

1996年10月1号,3DFX Interactive推出了第一代消费级图形硬件,在下图3.5可以看到这些年的时间轴。它们的voodoo图形卡可以将游戏Quake以很高的质量和性能渲染出来,从而使其快速被市场采用。这个图形硬件全局使用的都是固定管线。在GPU原生支持可编程渲染管线之前,有多次尝试通过多个渲染pass实时实现可编程着色器操作。1999年 Quake III所使用的Arena脚本语言是这一领域第一次广泛的被成功商业化。正如本章最开始所说,NV的GeForce256是第一个被称为GPU的硬件,然而它不可编程,不过它可配置。

T03

在2001年早期,NV的GeForce3是第一个支持可编程顶点着色器的GPU(参考文献[1049]),通过DX8.0的接口以及OpenGL的扩展展现给开发者。这种着色器是通过一种类似汇编的语言编写,然后由驱动解析成微代码。DX8实际上还包含了像素着色器,但是当时的PS还不够可编程。只能通过驱动支持纹理混合,将寄存器合并在一起。(Patrick:这句话没看懂)这种编程不仅短(只有12个或者更少的指令),而且还缺乏重要功能。通过对RenderMan的学习,Peercy et al.(参考文献[1363])认为真正的可编程中的重要部分是纹理读取和浮点数支持。

这个时候的Shader还不支持分支控制,所以只能通过计算两条分支的结果,然后选择一条或者将两个结果进行插值,这种方式来模拟条件语句。DX定义了Shader Model的概念用于区分包含不同能力shader的硬件。2002年DX9支持了SM2.0,其中包含了真正可编程的VS和PS。类似的功能在OpenGL中也通过各种扩展暴露给开发者。增加了读取任意贴图以及存储16bit 浮点数的功能,也就终于完成了Peercy et al.所提出的一系列要求。针对shader resource,比如指令、纹理、寄存器的限制也得到了提升,从而使得shader可以实现更加复杂的效果。分支判断的功能也加进去了。然而随着shader的越来越复杂,导致汇编编程模型变的越来越繁琐。还好,DX9引入了HLSL。这个着色器语言是由微软和NV一同完成的。同时,OpenGL ARB(体系结构审查委员会)发布了GLSL,一个类似的语言用于针对OpenGL(参考文献[885])。这些语言受到C语言编程的语法和设计理念的严重影响,并包含了renderman着色语言的元素。

SM3.0是于2004年发布的,加入了dynamic flow control,使得Shader功能更加强大。同时也将一些可选的特性转为了必须支持的特性,提高了shader resource的最大数量限制,并且增加了VS中读取纹理的功能。2005年末发布的新一代游戏console(微软的Xbox360)以及2006年末发布的(Sony的PS3)都需要SM3.0级别的GPU。任天堂的Wii console是最后一款有名的使用Fixed-function GPU的机器,在2006年末发布。在当时,纯固定管线已经不复存在了。着色器语言已经发展到了可以通过大量的工具创建和管理的时代。图3.6显示了其中一种使用了Cook's shade tree概念的工具。

	下面为apple设备信息
iPhone 12Pro 6GB A14(六核) iOS14 2532 x 1170 2020.10.14
iPhone 12 4GB A14(六核) iOS14 2532 x 1170 2020.10.14
iPhone 12mini 4GB A14(六核) iOS14 2340 x 1080 2020.10.14
iPhone SE 2 3GB A13 iOS13 1134 x 750 2020.4.17
iPad Pro(12.9 英寸,第 4 代) 6GB(八核) A12Z 2732 x 2048 2020.3.18
iPad Pro(11 英寸,第 2 代) 6GB(八核) A12Z 2388 x 1668 2020.3.18
iPhone 11ProMax 6GB A13(八核) iOS13 2688×1242 2019.9.11
iPhone 11Pro 6GB A13(八核) iOS13 2436×1125 2019.9.11
iPhone 11 4GB A13(八核) iOS13 1792 x 828 2019.9.11
iPad7(2019) 3GB A12 2160x1620 2019.9.11
iPad Air 2019 3GB A12+M12 2224x1668 2019.3.18
iPad mini5 3GB A12 2048x1536 2019.3.18
iPad Pro(12.9 英寸,第 3 代) 4GB A12X+M12 2732 x 2048 2018.10.30
iPad Pro(11 英寸) 4GB A12X+M12 2388 x 1668 2018.10.30
iPhone Xs Max 4GB A12(7nm、四核) iOS12 2688×1242 2018.9.13
iPhone Xs 4GB A12(7nm、四核) iOS12 2436*1125 2018.9.13
iPhone XR 3GB A12(7nm、四核) iOS12 1792x828 2018.9.13
iPad6(2018) 2GB A10 iOS12 2048x1536 2018
iPhone X 3GB A11 iOS11 2436×1125 2017.9.13
iPhone 8 Plus 3GB A11 iOS11 1920x1080 2017.9.13
iPhone 8 2GB A11 iOS11 1334x750 2017.9.13
iPad5(2017) 2GB iOS11 A9 2048x1536 2017
iPad Pro(12.9 英寸,第 2 代) 4GB A10X+M10 2732 x 2048 2017.6.5
iPad Pro(10.5 英寸) 4GB A10X+M10 2224 x 1668 2017.6.5
iPhone 7 Plus 3GB A10(四核) iOS10 1920*1080 2016.9.09
iPhone 7 2GB A10(四核) iOS10 1334×750 2016.9.09
iPad Pro(9.7英寸)2GB A9x 2048 x 1536 2016.3.22
iPhone SE 2GB A9(双核) iOS9.3 1136 x 640 2016.3.22
iPad Pro(12.9 英寸,第 1 代)4GB A9X iOS9 2732 x2048 2015.9.09
iPhone 6s Plus 2GB A9(双核) iOS9 1920x1080 2015.9.09
iPhone 6s 2GB A9(双核) iOS9 1334x750 2015.9.09
iPad mini 4 2GB A8 iOS9 2048x1536 2015.9.09
iPad Air 2 2GB A8X+M8 2048×1536 2014.10.17
iPad mini 3 1GB A7+M7 iOS8 2048x1536 2014.10.17
iPhone 6 Plus 1GB A8 iOS8 1920*1080 2014.9.09
iPhone 6 1GB A8 iOS8 1334*750 2014.9.09
iPad Air 1GB A7+M7 iOS7 2048x1536 2013.10.23
iPad mini 2 1GB A7+M7 iOS7 2048x1536 2013.10.23
iPhone 5s 1GB A7(双核)iOS7 1136*640 2013.9.20 opengles 3.0 metal
iPhone 5c 1GB A6(双核)iOS7 1136x640 2013.9.20
iPad 4 1GB A6X iOS6 2048 x 1536 2012.10.23
iPad mini 512MB A5 iOS6 1024*768 2012.10.23
iPhone 5 1GB A6(双核)iOS6 1136×640 2012.9.21 opengles 2.0
iPad 3 1GB A5X(双核CPU+四核GPU)iOS5 2048×1536 2012.3.8
iPhone 4s 512MB A5(双核) iOS5 960×640 2011.10.14
iPad 2 512MB A5(双核) iOS5 1024x768 2011.3.3
iPhone 4 512MB A4 iOS4 960x640 2010.6.24
iPad 1 256MB A4 2010.1.27
iPhone 3Gs 2009.6.19
iPhone 3G 2008.7.11
iPhone 2007.6.29
以2019.4.1-2019.6.30的数据,内存4G以上占据5%,3G以上占据36%,2G以上占据36%,1G以上支持OpenGL ES3.0占据17%,1G不支持OpenGL ES3.0占据1%
T03

上图为mental image公司的工具“mental mill”。显示了用于实现shader的一个可视化shader编辑器。各种操作封装在功能栏中,可从作则进行选择。选中后,每个功能栏都有可调的参数,如上图右侧所示。每个函数的输入和输出链接在一起,形成最终结果显示在上图中间frame的右下角。

可编程的下一个重大节点是2006年底,DX10(参考文献[175])提出了SM4.0,带来了一些重大特性,比如几何着色器GS,流输出stream output(Patrick:应该就是TFBO),SM4.0包含了一种针对所有着色器(VS、PS、GS)的统一编程模型,也就是前面所说的统一着色器设计。资源的限制数量进一步提升,并且支持了整形数据(包含按位操作)。OpenGL 3.3和GLSL3.30也提供了一个类似的SM。

在2009年DX11和SM5.0发布,增加TS和CS,也被称为DirectCompute。该版本还着重于更有效的支持CPU多线程,这个课题将在18.5进行讨论。OpenGL在4.0加入了TS,在4.3加入了CS。DX和OpenGL发展日新月异。两者都设定了特定版本发布所需要的一定级别的硬件支持。微软控制着DX API,因此直接与独立硬件供应商(IHV),比如AMD、NV、Intel,以及游戏开发人员和计算机辅助设计软件公司,来确定公开哪些功能。OpenGL则是由硬件和软件供应商组成的联盟开发,由非营利组织khronos组织管理。由于涉及到的公司众多,所以功能经常在DX发布之后的一段时间出现在OpenGL的一个版本中。但是OpenGL允许扩展,针对特定供应商或者通用的,这样的话就可以在正式发布之前使用到了最新的GPU技术。

API的下一个重大变化是在2013年,AMD公司推出了Mantle。是与视频游戏开发者Dice合作开发,Mantle的理念为去掉大部分图形驱动程序的开销,直接将控制权交给开发者。除此之外,重构还进一步有效的支持CPU多线程。这种新型的API专注于极大地降低了CPU在驱动中的耗时,并更有效的支持了CPU多线程,详见18章。Mantle的这种新理念被微软所采纳,并与2015年加入了DX12中。需要注意的是DX12已经没有再增加新特性了,与DX11.3所支持的硬件特性完全一样。这两个API都可以用于将图形发送到VR系统,比如Oculus Rift以及HTC Vive。然而DX12对API进行彻底的重新设计,它可以更好的映射到现代GPU架构。低开销的驱动程序对于由于CPU驱动消耗导致瓶颈的应用程序非常有帮助,或者对于使用大量CPU处理器进行图形处理的应用程序,可以提高性能(参考文献[946])。从早期的API进行移植可能会很难,而且一个傻瓜的实现甚至会导致性能更低(参考文献[249、699、1438])。

2014年苹果发布了自己的低开销API metal。metal最早出现在移动平台iPhone5s和iPad Air,一年后通过OS X发布在Mac上。除了上述优势之外,降低CPU消耗还节省了功耗,这是移动平凡的一个重要因素。这个API有自己的着色器语言,用于图形和GPU计算程序。

AMD将Mantal赠送给了khronos,后者随后与2016年初发布了新的API Vulkan。和OpenGL一样,Vulkan支持多操作系统。Vulkan使用一种高级的中间语言SPIR-V,用于着色显示和通用的GPU运算。预编译的shader可被用于任何支持该功能的GPU上(参考文献[885])。Vulkan同样可以用于非图形学GPU运算,因为它并非需要一个显示窗口(参考文献[946])。Vulkan与其他低开销驱动的一个显著不同是,它适用于从工作站到移动设备的各种系统。

在移动设备上,标准是使用OpenGL ES。"ES"的意思是嵌入式系统,因为这个API就是为了移动设备设计的。标准的OpenGL在某些调用结构中相当庞大和缓慢,并且需要对一些很少会被使用到的功能进行支持。2003年发布的OpenGL ES1.0是OpenGL1.3的精简版,使用的是固定管线。虽然DX的发布是与那些支持它的硬件同步发布的,但是针对移动设备的图形支持并非如此。比如第一代iPad,发布于2010年,使用的是OpenGL ES1.1。然而2007年OpenGL ES2.0就发布了,并且提供了可编程着色器。它是基于OpenGL 2.0,但是并没有fixed-function部分,也就是说无法先后兼容OpenGL ES1.1。OpenGL ES3.0在2012年发布,提供了MRT、纹理压缩、TFBO、Instance以及大量的纹理格式和模式的功能,着色器语言也得到了进一步发展。OpenGL ES3.1加入了CS,ES3.2加入了GS和TS以及其他功能。第23章会更加详细的讨论移动设备架构。

OpenGL ES的一个分支是基于浏览器的API WebGL,通过JavaScript调用。于2011年发布,这个API的第一个版本在大多数移动设备都可用,因为它在功能上相当于OpenGL ES2.0。与OpenGL一样,可以通过扩展使用更高级别的GPU特性。WebGL 2需要OpenGL ES3.0的支持。

WebGL特别适用于如下场景:

  • 跨平台,在所有个人电脑和几乎所有移动设备上工作。
  • 驱动由浏览器支持。即使一个浏览器不支持一个特宁的GPU或者扩展,通常另外一个浏览器支持。
  • 代码是解释的,而非编译的,开发只需要一个文本编辑器。
  • 调试与内置于大多数浏览器中,在任何网站运行的代码都可以被检查。
  • 可以通过代码上传到网站或者github进行部署

高级场景、效果库,比如three.js(参考文献[218])可以很方便的访问各种复杂效果,比如阴影算法、后效、PBS、延迟渲染等。

3.5 顶点着色器

顶点着色器是图3.2中管线中的第一阶段。虽然这是直接由开发者控制的第一阶段,但是需要注意的是一些数据操作发生在这个阶段之前。在DX中被称为输入装配器input assembler(参考文献[175、530、1208]),可以将多个数据流编织在一起,形成沿着管道继续进行的顶点和图元。比如,一个物件是由一组顶点和一组颜色组成。输入装配器将通过创建具有位置和颜色的顶点来创建对象的三角形(或者线或者点)。第二个对象可以使用相同的位置数组(以及不同的模型转换矩阵)和不同的颜色数组来表示。第16.4.5将详细介绍数据展示。通过输入装配器还可以实现instancing。这样的话就可以将一个对象根据每个实例不同的数据,一次性绘制出来。第18.4.2介绍了instancing如何使用。

一个三角形mesh是由一组顶点组成,每个顶点都与模型表面上的特定位置相关联。除了位置,每个顶点还可以关联一些可选的属性,比如颜色、纹理坐标。曲面法线也通过顶点属性表示,这看上去是个奇怪的选择。因为在数学上,每个三角形都有一个定义明确的曲面法线,看上去直接用三角形的面法线进行着色更有意义。然而,渲染的时候,三角形mesh用于表示底层的曲面,顶点法线用于表示这个曲面的方向,而非三角形本身。第16.3.4描述了计算顶点法线的方法。下图3.7展示表示曲面的两个三角形网格的侧视图,一个是平滑的,另一个有锐利的折痕。

T03

上图展示了三角形mesh(黑色,带顶点法线)的侧视图(红色)。在左侧,平滑顶点法线用于表示平滑曲面。在右边,终结的顶点被复制,给出两条法线,表示一条折痕。

顶点着色器是处理三角形网格的第一步。vs无法访问关于如何组装成三角形的数据。正如它的名字所示,它只处理输入的顶点。顶点着色器提供修改、创建、忽略每个顶点相关数据(比如顶点的颜色、法线、纹理坐标和位置)的方法。通常,顶点着色器会将顶点从模型空间转成齐次裁剪空间,见4.7节。顶点着色器至少要始终输出这个坐标。

顶点着色器与前面描述的统一着色器基本相同。每个传入的顶点都会经过顶点着色器的处理,然后输出一系列值用于在三角形和直线上进行插值使用。顶点着色器无法创建、销毁顶点,一个顶点产生的数据无法传递到另外一个顶点。由于每个顶点都是相对独立的,所以GPU上任何数量的着色器处理器都可以与顶点的传入流并行运算。

输入装配器通常是在VS执行之前进行。这也就是一个物理模型同理论模型不同的例子。在无力上,顶点着色器获取数据创建顶点,而驱动会悄悄的通过一些看不到的适当指令预处理每个着色器程序。(Patrick:比如?)

接下来的章节会聊一下几种着色器效果,比如用于动画关节的顶点混合,轮廓渲染等。顶点着色器的其他用途还包括:

  • 对象生成,只创建一次mesh,将其通过顶点着色器进行变形(Patrick:这是啥?)
  • 通过蒙皮或者变形morphing为角色的身体和面部设置动画。
  • 程序化变形,比如旗帜、布料、水(参考文献[802、943])
  • 创建粒子。将无区域的网格发送到管线,根据需要将这些网格制定一个区域。
  • 通过将整个framebuffer的内容作为一张纹理,作用于正在进行程序变形的屏幕对齐网格,实现透镜变形Lens distortion,热雾heat haze,水波water ripples,页面卷曲page curls和其他效果。(Patrick:这不在PS做么。。)
  • 通过使用顶点纹理获取VTF,得到地形高度(参考文献[40、1227])

上述的一些通过VS进行的变形如图3.8所示

T03

上图左侧为一个正常的茶壶。顶点着色器执行一些简单的操作得到中间的图。右侧的图为使用噪声扰动这个模型得到的图。(图片由NV公司提供,通过Fx Composer2制作)

VS的输出可以以集中不同的方式使用。通常的做法是组建成三角形,光珊化,然后将生成的单个像素片段发送到像素着色器以继续处理。在某些GPU中,数据也可以发给TS、GS,或者通过TFBO存储到内存。这些可选的阶段将在后面的章节进行讨论。

3.6 细分曲面阶段

细分曲面阶段使得我们可以绘制曲面。GPU的任务是根据每个曲面,将其转成一组三角形。这个可选的GPU功能,首先是在DX11中被引入,也是DX11所必需的一个特性,OpenGL4.0和OpenGL ES3.2也支持它。

使用曲面细分有很多优点。由于曲面通常比其对应的三角形面要复杂。除了节约内存,针对动画角色或者每帧形状都在变化的物件,这个特性还可以以防CPU和GPU之间的带宽成为瓶颈(因为每帧都会有大量的VBO传输)。通过为指定视图生成的适当数量的三角形,就可以有效的渲染曲面。比如,当一个球离摄像机很远的时候,只需要很少的三角形。当离得很近的时候,最好是有数千个三角形看上去才好看。这种控制LOD的能力使得应用程序可以自行控制其性能。比如,在差一点的GPU上,可以使用低精度的mesh保持帧率。所以说,可以将平面表示的模型改成精细的三角形网格,然后根据需要进行扭曲(参考文献[1493]),或者也可以通过曲面细分的方式,根据需要去执行一些复杂的着色计算。

曲面细分通常由三部分组成,用DX的术语来说,分为hull shader、tessellator、domain shader。OpenGL中hull shader被称为tessellation control shader曲面细分控制着色,domain shader被称为tessellation evaluation shader曲面细分评估着色,名字更长,但是更容易理解。而fixed-function tessellator在OpenGL被称为primitive generator图元生成,也正如我们将要看到的,它确实是干这个的。

第17章详细的讨论了如何制定和细分曲线和曲面。在这里,我们简单的总结每个细分阶段的目的。首先,hull shader的输入为一个特殊的patch图元。它包含了定义细分曲面、bezier曲线或者其他类型曲线所需要的多个控制点。hull shader有两个功能。首先,告诉tessellator应该生成多少三角形,以及按照什么配置生成。其次,它对每个控制点进行处理。除此之外,hull shader还可以根据需要修改patch,增加或者删除控制点。hull shader将控制点集和细分控制数据传给domain shader。如下图3.9所示。

T03

上图为细分阶段。hull shader拿到一组由控制点组成的patch,将细分因子(TF)和类型发送给fixed-function tessellator。控制点集由hull shader根据需要进行transform,然后雨TF以及相关的patch常数传给domain shader。tessellator根据它们的质心坐标系创建一组顶点。然后domain shader会对这些进行处理,生成三角形mesh(作为参考,控制点已经显示出来了。)

tessellator在管线中是一个fixed—function,只被用于曲面细分着色器。它的任务是为domain shader增加一些新的顶点。hull shader输出需要细分的曲面类型:(三角形、四边形、isoline等值线)。isoline等值线是一组线条,有时会被用于头发渲染(参考文献[1954])。hull shader还会输出细分因子(在OpenGL被称为细分级别)。由两种类型:inner内缘和outer edge外缘。这两个内部因子决定了三角形或四边形内的细分数量。外国因素决定了每个外部边缘的分割程度,详见第17.6节。图3.10是一个逐步增加细分因子的例子。通过这种内外因子分开的方式,我们可以使得相邻曲面的边在细分的过程中分割方式保持一致,而不用关心其内部是如何被细分的。这样的话就会避免因为细分后相邻面边不一样的问题,导致裂缝或者其他着色瑕疵。顶点位于质心/重心坐标系,详见22.8,指定所需曲面上每个点的相对位置的值。

T03

上图展示了改变细分因子的效果。最初的茶壶是由32个patch组成。从左到右,inner、outer细分因子分别为1、2、4、8(图片由Rideout和Van Gelder(参考文献[1493])中的demo提供)

hull shader输出一个patch和一组控制点位置。而且,它还可以通过发送一个小于或者等于0(或者直接发送NaN)的outer细分等级来表示将该patch丢弃(Patrick:这里真的把patch丢弃了,这些patch也无法进入后面的了?)。否则的话,tessellator会产生一个mesh并将其发送给domain shader。每次调用domain shader的时候,都会使用hull shader输出的针对曲面的控制点,来计算每个顶点的输出值。domain shader的数据流模式和VS一样,处理来自tessellator的每个输入顶点并生成响应的输出顶点。然后形成三角形沿着管线向下传递。

尽管这个系统听起来很复杂,但其实它这样结构的目的是为了提高效率,使得每个着色器都很简单。比如hull shader中的patch通畅不会有修改或者只有很少的修改。该shader还可以使用patch到摄像机的大概距离或者屏幕尺寸来实时计算细分因子,比如地形渲染(参考文献[466])。或者,hull shader也可以针对所有patch提供一套相同的数值。tessellator执行一套复杂但是固定的函数过程,生成顶点,得到它们的位置,并指定它们形成三角形或者线。这个数据膨胀步骤在着色器外部执行,以提高计算效率(参考文献[530])。domain shader获取为每个点生成的质心坐标,并在面片的计算公式中,使用这些坐标来生成位置、法线、纹理坐标和其他所需的顶点信息。示例如图3.11所示

T03

上述作图为一个6000面的模型。右侧使用PN三角形细分对每个三角形进行细分。(图片来自NV SDK 11(参考文献[1301]示例,由NV提供,模型来自Metro 2033 by 4A Games))

3.7 几何着色器

几何着色器可以将图元转变成其他图元,这是细分阶段无法做到的。比如,可以通过给每个三角形创建线边,将三角形网格转换成线框视图。或者,不使用线,而使用面向观察者的四边形,这样就可以用较厚的边进行线框渲染(参考文献[1492])。GS是于2006年底的DX10中被引入图形硬件加速管线中。它在管线中的位置在TS之后,也是可选的。它是SM4.0的必备特性,在之前的SM中并不支持它。OpenGL 3.2和OpenGL ES3.2也支持这种类型的shader。

GS的输入是一个物体和它相关的顶点。物件通常由三角形、线段、点组成。然后可以通过GS定义和处理扩展的图元。比如说,可以传入三角形外部的三个附加顶点,以及多段线上的两个相邻顶点。如图3.12所示。DX11和SM5可以传入更多更复杂的patch,最高可以达到32个控制点。也就是说,GS可以更有效的生成patch(Patrick:原文说的是TS更高效,我觉得是打印错误)

T03

如上图所示,GS的输入为一些简单的类型:点、线段、三角形。上图右侧两张图中的图元,包含了针对线段和三角形的额外顶点。除此之外还可以支持更复杂的patch。

GS除了点、线段、三角形这些图元,然后输出0个或者多个顶点。需要注意的是,GS可以不输出任何输出。通过这种方式,可以选择性的改变顶点、增加新的图元,或者删除图元,以此改变整个mesh。

GS可以用于修改传入的数据或者制作有限数量的副本。例如:一个用途是生成六个不同transform的副本,用于同时呈现立方体的六个面,详见10.4.3。也可以被用于创建cascade shadow map来获得高质量的阴影(Patrick:这个怎么做)。还可以利用GS的优势通过一些点的数据创建可变尺寸的粒子、沿轮廓拉伸以进行毛发渲染以及为阴影算法查找对象边缘。图3.13展示了一些例子。这些以及更多的用法都将在本书的剩下部分详细介绍。

T03

上图为GS的一些用法。左侧为使用GS动态执行元球的表面细分。中间图为通过GS和流输出对直线段进行分段细分,然后通过GS生成的billboard展示闪电。右侧为通过VS和GS以及流输出实现布料模拟。(图片来自NV SDK 10(参考文献[1300])的实例,由NV提供)

DX11为GS增加了instance的功能,这样的话GS就可以针对给的任意图元运行多次(参考文献[530、1971])。在OpenGL4.0中这是用调用计数指定的。GS可以输出多达四个流。一个流可以按照管线继续后面的运算。所有这些流都可以选择性的发送到流输出的RT上。

GS根据图元输出结果的顺序需要与输入的顺序一致。这样会影响性能,以为如果多个shader core并行运算,那样的话结果就要被保存下来并进行排序。这个和GS的其它因素都针对单次调用复制或者创建大量几何体。(参考文献[175、530])

在一个DC中,整个管线只有三个部分会在GPU中创建工作:光栅化、TS和GS。而考虑到所需要的资源和内存,GS是最不可控的,因为它是完全可编程的。在实践中,GS很少会被使用,因为它无法很好的利用GPU的优势。在一些移动平台,它甚至是在软件层开发的,所以严重租来了它的使用(参考文献[69])

3.7.1 流输出

GPU管线的标准用法是将数据通过VS、光栅化,然后在PS中对其进行处理。之前数据都是会贯穿整个管线,而中间数据无法被访问。流输出在SM4.0被引入。在顶点被VS处理完之后(以及可选的TS、GS),除了可以将其传输给光栅化阶段之外,还可以输出一个流,比如一个有序数组。甚至说,可以将光栅化以及之后的流程全部关闭,然后整个管线就被当时一个纯粹的非图形学工作流(Patrick:这样的话和CS有什么区别,我觉得1.CS和核心是不是更多?算起来更快。2.CS得到的数据存在GPU的buffer中,这个数据CPU应该无法直接访问?还是说这个buffer VS、PS等也无法直接访问?)这里输出的数据还可以被送回重新进行管线,从而允许迭代处理。如13.8所述,这种操作针对模拟流水或者其他粒子特效都会比较有用。它还可以用于对模型进行蒙皮,然后这些顶点可重用(Patrick:在哪里重用?意义是啥?),详见4.4节

流输出仅以浮点数的形式返回数据,所以会有一个明显的内存开销。流输出是以图元为单位,而非顶点。也就是说,当一个mesh选择这条路的话,每个三角形都会生成自己相关的三个顶点作为输出。原始mesh中的顶点共享都会丢失。因此,比较常见的办法是以点为图元进行这个管线。在OpenGL中流输出被称为fransform feedback,因为它的作用就是将顶点输出,然后用于之后进行进一步处理。图元会被按照顺序传递给流输出目标,也就是说顶点的顺序保持不变(参考文献[530])。

3.8 像素着色器

在完成了VS、TS、GS之后,图元会再去经历clip以及光栅化。这两个部分的处理步骤是相对固定的,不可编程,但是有些可配置。每个三角形都被遍历以确定它所覆盖的像素。光栅化还可以粗略计算三角形覆盖每个像素的单元区域的大小,详见5.4.2。三角形全部或者部分覆盖部分的像素被称为片元fragment。

三角形顶点处的值,包括Zbuffer上的Z值,都会逐像素的在三角形曲面上进行插值。这些值都会被传给PS去处理这些片元。在OpenGL PS也被称为fragment shader,这个称呼其实更加合理。但是为了统一性,我们这本书将使用PS来称呼。顺着这个管线的点和线的图元也会根据像素覆盖率创建片元。

处理三角形所用的插值算法是由PS指定的。一般我们都使用透视矫正插值,这样的话当一个物体远离摄像机的时候,两个像素表面位置之间的时间空间距离会增加。比如,绘制延伸到地平线的铁路轨道。当铁轨距离比较远的时候,从屏幕上看铁轨枕木之间的距离越来越近,这也就是因为地平线上每个连续像素的距离越大。(Patrick:插值不是在屏幕映射之后么,这个时候透视/正交已经被考虑好了哇,一个物体在屏幕上占多少像素也就知道了哇,还需要这样?)。还有其他插值算法,比如屏幕空间插值,不考虑透视投影。DX11给开发者权限可以选择插值的时间和方式(参考文献[530])。

在编程方面,VS的输出,经过三角形、线段的插值,被传输给PS作为输入。随着GPU的发展,PS可以获取到更多的输入。比如在SM3.0及以上,PS可以获取到fragment在屏幕上的位置(Patrick:不是通过varying,还有内含token?),还有,当前绘制的片元属于三角形的正面还是背面,也可以通过一个内置flag获取到。通过这个flag,shader可以轻松的在一个pass中实现正面和背面效果不同的材质球。

根据这些输入,PS可以计算得到一个片元的颜色。PS还可以生成不透明的值,以及可以修改它的Z值(Patrick:不会吧,这怎么可能,Z值不是只能在VS修改么,难道是用glPolygonoffset?)。在merging阶段,这些值用于修改该像素原本存储的值。光栅化生成的深度值,可以在PS中被修改(Patrick:接上又强调一遍,我不信。。)stencil buffer通常是无法被修改的,而是直接将其传递到merge阶段。而DX11.3也允许shader去修改这个值了。在SM4.0中,雾的计算以及alpha test都从merge阶段,搬到了PS阶段(参考文献[175])

PS具有丢弃片元(即不生成输出)的独有功能。图3.14展示了如何丢弃一个片元。clip plane之前是fixed-function管线中的一个可配置的元素,后来被加入了VS中(Patrick:这个厉害了,我居然还在一个用CPU的裁剪板。。)。后来PS支持了discard后,这个功能就可以在PS中以多种多样的形式出现了,比如裁剪volume应该是AND关系还是OR关系。

T03

上图展示了用户自定义的裁剪平面。最左侧,一个水平裁剪板被用于裁剪物件。中间那个图展示了,一个球被三个板子进行裁剪。右侧,球被裁剪后,只剩下了三个板子之外的部分了。(图片来自three.js中的webgl_clipping、webgl_clipping_intersection例子(参考文献[218]))

最开始,PS的输出只能被传递到merging阶段,用于显示。随着时间的推移,PS可以执行的指令数量大大增加。然后就出现了MRT的概念。也就是说PS的输出不再仅限于color和zbuffer了(Patrick:PS为啥可以写zbufer。。。在TBDR里面,zbuffer在earyz的时候就写好了。。),而是可以将针对每个片元的多套数据写入不同的buffer中,每一个都被称为一个RT。RT一般都有相同的x和y长度,一些API支持不同长度,但是渲染区域将是使用最小的那个。一些架构要求RT的位数也一样,甚至要求相同的数据格式。根据GPU不同,MRT的数量一般为4个或者8个。

即使有这些限制,MRT依然是一个可以提高渲染性能的强大功能。一个pass就可以将颜色、物件标识符、世界空间距离等信息生成在多个RT上。这个功能也使得产生了新的渲染管线,延迟渲染,其中可见性和着色是在两个单独的过程中完成的。第一个过程逐像素的存储了物件的位置、材质信息,第二个pass可以将光照等其他效果高效的计算出来。这种渲染方式将在20.1节详细介绍。

PS的限制是它只能改变当前所在片元位置的RT,而不能从相邻像素读取当前结果。也就是说,当一个PS执行的时候,不能将其输出发送到相邻的像素,也不能访问其他像素最近的改动。它只能影响当前像素的结果。然而,这个限制并没有听起来那么可怕。因为一个pass产生的图片可以在下一个pass的PS中随意读取。相邻的像素还可以通过图像处理技术进行处理,这个将在12.1节详细介绍。

有一个办法可以获取周围像素的结果。通过计算渐变或衍生信息立即访问相邻片段的信息(尽管是间接的方式)。PS提供沿X和Y屏幕轴每像素内插值的变化量。这些值对于各种计算和纹理寻址很有用。这些渐变信息对于纹理过滤(详见6.2.2)等操作尤为重要,这样可以知道一个像素对应图片中的多少信息。所有的当代GPU都是通过以2*2(quad)的的片元来实现这个特性。当PS需要一个渐变值的时候,将会得到相邻片元的区别,如下图3.15所示。一个unit何鑫可以访问相邻(同一个warp内不同线程)的数据,然后就可以计算出渐变用于PS的使用。一个限制就是:在有动态分支(比如if语句或者可变次数的循环语句)的shader中,无法访问这个渐变信息。只有当一组内的所有片元都是走的同一套指令,这样的话用于计算渐变信息的四个像素数据才有意义(Patrick:为啥,这个渐变值到底是啥。。)。这个限制在离线渲染系统中依然存在(参考文献[64])

T03

如上左图,一个三角形被光栅化为几个quads,每个quad为2*2的像素。在上右图显示了用黑色标记的像素的渐变计算。V的值用于显示quad中的4个像素位置(Patrick:没看懂这个V从哪里来的。。)。需要注意的是即使quad中的那3个像素没有被覆盖,但是依然被GPU处理,以便计算渐变。X、Y轴上的渐变通过同一个quad中左下角的像素以及它的两个邻居计算出来

DX11引入了一种缓冲区类型,允许对任意位置进行访问,被称为unordered acess view(UAV)(Patrick:和RT有啥区别)。最初只是用于PS和CS计算,在DX11.1中被扩展到可以被用于所有shader。OpenGL 4.3将这个buffer称为shader storage buffer object(SSBO)。这俩名字都不错。PS是并行运行的,按照一个任意的顺序,这个存储缓冲区在它们之间共享。

通常需要一些机制来避免数据争抢,也就是数据危险。比如偶有两个着色器程序都在争取去影响同一个变量,就可能导致各种结果。比如,PS的两个指令同时去获取同一个值,得到初始值后各自进行修改,然后不论是哪个后写回,都会导致另外一个计算结果丢失。GPU通过一个专用的原子单元来处理这个事情(参考文献[530])。但是原子也就意味着一些shader可能会等待另外一个正在读取/修改/写入数据的shader。

虽然原子操作可以避免数据危害,但是很多算法还是需要特定的顺序执行。比如,我希望先绘制一个蓝色的透明三角形,然后再在上面绘制一个红色的透明三角形。可以通过2个PS来分两次绘制,每个ps绘制衣蛾三角形,这样的话肯定没有问题。在标准管线中,片元结果会在merge阶段中先进行排序,然后再被处理。DX11.3引入的Rasterizer order views(ROVs)就是去强制执行顺序。有点类似UAV,可以以相同的方式由shader读写。关键是ROV保证数据是按照正确顺序访问的,这个大大提高了这些shader accessible buffer的实用性(参考文献[327、328])。比如ROV是的PS可以写自己的blending算法,因为它可以读写ROV中的任何位置,而不需要merge阶段(参考文献[176])。代价就是,如果检测到一个无序访问,PS可能会被暂停直到前面的三角形绘制完全结束。(Patrick:感觉挺厉害的,UAV只是一个可读写的buffer,而这个可以获取到任意像素的当前数据?)

3.9 合并阶段

正如2.5.2节所描述的,合并阶段是用于将片元的深度和颜色(PS的输出)与framebuffer上的信息进行合并。DX将这个阶段称为output merger,OpenGL将这个阶段称为per-sample 操作。在大多数传统管线中(包括我们的),这个阶段包含stencil buffer和z buffer。如果片元可见,则就会触发color blending操作。针对不透明物件,其实并没有真正的blending,只是单纯的用fragment颜色代替之前存储的颜色。真正的将片元和framebufer上存储的颜色进行blending的操作,通常被用于半透明物件以及合成操作,详见5.5节。

想想一下,一个由光栅化生成的片元经过PS后,在进行depth test的时候发现自己被前面绘制的片元挡住了,这样的话整个PS操作都白费了。为了避免这种情况,需要GPU将合并阶段中的一些测试房到PS之前(参考文献[530])。fragment的zbuffer(以及其他被使用的,比如stencil bufer或者scissor)被用于测试可见性,如果不可见,该片元则被cull掉。这个功能被称为early-z(参考文献[1220、1542])。PS可以改变z,或者直接抛弃该片元。如果PS中有这两个操作,early-z就会被关闭,这样的话整个管线就变的低效了。DX11和OpenGL4.2允许PS强制打开early-z,尽管还会有一些限制(参考文献[530])。23.7节会详细介绍early-z以及其他z bufer的优化。使用early-z会获得一个很大的性能提升,这个会在18.4.5中描述。

合并阶段发生在fixed-function,比如图元装配,以及可编程管线中间。尽管它并非可编程,但是高度可配置。比如color blending就可以使用多种算法。最常见的有涉及到颜色和alpha的乘法、加法、减法等,还有其他操作,比如最大、最小、按位操作等。DX10增加了将PS中两个颜色与framebuffer上颜色混合的方法。这个能力被称为双源颜色混合,不能和MRT一期使用。MRT支持color blending,DX10.1还增加了针对每个buffer使用不同blend算法的功能。

如前一节末尾所述,DX11.3提供了一个方法,可以通过ROV实现progrrammable blending,尽管它在性能上会有一些代价。ROV和合并阶段都是按照顺序渲染的,也就是output invariance输出不变性。不管PS的输出顺序如何,API要求结果按照输入顺序,逐物件和三角形的排序并发送到合并阶段。

3.10 compute shader

GPU不仅可以用于实现传统的图形管线。也可以用于非图形化应用,比如计算股票期权估值,以及训练神经网络进行深度学习等领域。以这种方式使用硬件被称为GPU计算,像CUDA、OpenCL这样的API被用来控制GPU作为一个巨大的并行处理器来使用,而不需要使用图形特定的功能。这些框架通常使用C\C++的扩展,以及搭配上GPU特定的类库。

DX11中引入了computer shader的概念,这个shader不固定在管线中的某个位置。然而它又与渲染过程密切相关,因为它是由图形API调用。它也是根据顶点、像素以及其他shader一起执行。它与管线中的其他shader共享同一个统一shader 处理器池子。它与其他shader类似,有一些输入,可以访问buffer(比如纹理)作为输入/输出(Patrick:既然使用相同的buffer,那么假如dest RT不变的话,将管线切换到CS消耗大么,那么消耗主要是渲染状态的切换?)。。在CS中warp和thread的概念更加可视化。比如,每个调用都能获取到它的thread index。还有线程组的概念,在DX11中由1-1024个thread组成。(Patrick:不同GPU支持的数量不同,一般按照32/64个使用吧,可以通过API获取到)。这些线程组分为x、y、z三个轴表示,主要是为了在shader中使用方便。每个线程组都有一个小的内存用于线程间共享。在DX11中,这个大小为32KB。CS是由线程组为单位执行,这样保证一组中的所有线程都并发运行(参考文献[1971])

CS的一个重要优势在于,它们可以访问GPU生成的数据(Patrick:PS不行?)。将数据从GPU传给CPU会知道延迟,因此如果处理和结果都在GPU上进行,可以提高性能(参考文献[1403])。后处理,将渲染后的数据进行修改,通过CS实现性能更优。共享内存以为这来自采样图像像素的中间结果可以与相邻线程共享。(Patrick:PS只有texture cache,没有CS那么厉害,如果没理解错的话,CS直接是采样出来的数据共享)。比如,使用CS计算图像的分布或者亮度的执行速度,是PS的两倍。

CS还可以被用于粒子特效、网格处理比如面部动画(参考文献[134])、剔除(参考文献[1883、1884])、图像过滤(参考文献[1102、1710])、提高depth精度(参考文献[991])、阴影(参考文献[865])、DOF(参考文献[764])以及任何其他可以使用GPU处理的任务。Wihlidal(参考文献[1884])还讨论了CS如何比Tessellation hull shader更加高效。图3.16可以看到其他使用。

T03

上图为CS的使用实例。左图为使用CS计算头发受风的影响,头发本身是有tessellation计算。中间为使用CS执行一个快速的blur算法。有图为模拟海洋波浪。(图片为NV SDK11的实例,由NV提供)

以上是我们对GPU渲染管线的总览。可以通过各种方法使用和组合GPU的特性来实现各种渲染相关的处理。本书的中心主题是使用这些能力调整相关理论和算法。下面我们聊一下transform和shading。

更多资源

Giesen的tour of graphics pipeline(参考文献[530])描述了GPU的许多方面,解释了各个元素的工作方式。Fatahalian和Bryant在一系列详细的演讲幻灯片中讨论了GPU的并行性。Kirk 和 Hwa的书(参考文献[903])重点讨论了CUDA的GPU计算,另外前言部分还讨论了GPU的进化和设计理念。

想要了解shader需要学习更多的内容。比如OpenGL Superbible(参考文献[1606])以及OpenGL Programming Guide(参考文献[885])介绍了shader programming。OpenGL Shading Language(参考文献[1512])不包括GS和TS,但是包含了shader的spec。访问本书的网站www.realtimerendering.com可以看到最新以及推荐的书籍。


虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2019 年 4 月 10 日发表,原文链接(http://geekfaner.com/shineengine/Translation3_RealTime_Rendering_4th_Edition1.html)