图形处理单元The Graphics Processing Unit
硬件图形加速一开始用于管线的尾端,用于执行三角形扫描线的光栅化,然后慢慢扩展到更高层的应用程序阶段。专用硬件相对于软件的优势仅仅是速度,但速度至关重要。
NVIDIA创造了GPU这一术语,以此区别GeForce256与之前只有光栅化功能的芯片,这是一个分界线。从此,GPU从可配置的复杂固定功能的管线逐渐演变为高度可编程的“白板”,开发者可以在其上实现自己的算法了。
各种可编程着色器是控制GPU的主要手段。
顶点着色器允许在每个顶点上执行各种操作(包括转换和变形)。
像素着色器处理单个像素,允许对每个像素计算复杂的着色方程。
几何着色器允许GPU创建和销毁飞速写入的几何图元(点、线、三角形)。计算得到的数据可以被写入多个高精度缓冲区,并作为顶点或纹理数据复用。
为提高效率,管线的某些部分仍然是可配置的,不是可编程的,但大体趋势是可编程和更灵活。
1. GPU管线概述
几何阶段与光栅化阶段都是分为几个具有不同程度的可配置性和可编程性的硬件阶段。
下图用不同的颜色表示了几何阶段和光栅阶段中每个子阶段的可编程或可配置性。
- 绿色:完全可编程;
- 黄色:可配置但不可编程,例如裁剪阶段可以选择执行删除或添加用户定义的裁剪平面;
- 蓝色:完全固定,不可编程也不可配置。
顶点着色器是一个完全可编程的阶段,通常用于实现“模型和视图转换”、“顶点着色”和“投影”这几个功能阶段。
几何着色器是一个可选的,完全可编程的阶段,主要对图元(点、线、三角形)的顶点进行操作。它可以用来执行逐图元的着色操作,销毁图元或者创建新的图元。
裁剪、屏幕映射、三角形设置和三角形遍历阶段都是固定功能阶段,实现同名的功能阶段的功能。
像顶点着色器和几何着色器一样,像素着色器也是完全可编程的,用来实现“像素着色”这一功能阶段。
最后,合并阶段介于完全可编程的着色器阶段和其他固定功能的阶段之间。虽然它是不可编程的,它是高度可配置,可以被设置去执行各种各样的操作。当然,它实现了“合并”这一功能阶段,负责修改颜色缓冲区,Z缓冲区,交融缓冲区,模板缓冲区,和其他相关的缓冲区。
随着时间的推移,GPU管线已经远离硬编码的运算操作,而朝着提高灵活性和控制性改进。编程着色器的引入是这个进化的最重要的一步。
2. 可编程着色阶段
现代着色器阶段(例如,支持Shader Model 4.0的DirectX 10以及以后)使用通用着色器核心(common-shader core)。这是说顶点,像素和几何着色器共享一个编程模型。
我们使用类似C语言的着色器语言来进行着色器的编程,如HLSL,Cg和GLSL,这些程序被编译成机器无关的汇编语言,也称为中间语言(IL, intermediate language)。之前的着色器模型允许直接使用汇编语言编程,但是在DirectX 10上汇编语言仅可做为调试输出使用。这些汇编语言在单独的阶段,通常是在驱动中,被转化成实际的机器语言。这样的安排可以兼容不同的硬件实现。这些汇编语言可以被看做是定义一个作为着色语言编译器的虚拟机。
这个虚拟机是一个处理多种类型寄存器和数据源、预编了一系列指令的处理器。由于很多图形操作使用短向量(最长4个分量),所以该处理器包含了4路SIMD(单指令多数据流),每个寄存器都包含4个独立的值。32位的单精度浮点数的标量和向量是基本的数据类型,对32位整数的支持是最近才被加上的。浮点向量通常包含的数据有位置(xyzw),法线,矩阵行,颜色(rgba)和纹理坐标(uvwq)。整数通常被用于计数器,索引以及位掩码(bit mask),也支持聚合的数据类型如结构体,数组以及矩阵。而为了便于使用向量,向量操作如调和(swizzling,也就是向量分量的重新排序或复制),和掩码(masking,只使用指定的矢量元素),也是支持的。
绘制调用是指调用图形API来绘制一组图元,这会驱使图形管线的运行。每个可编程着色阶段拥有两种类型的输入:
- uniform输入,在整个绘制调用期间保持不变(多个绘制调用之间可被改变);
- varying输入,每个顶点和像素的值是不同的。
纹理是一类特殊的uniform输入,曾经一直是一张贴在表面的彩色图片,但现在可以认为是任意存储着大量数据的数组。需要强调的是,尽管着色器有各种各样的输入,可以使用不同的方式进行处理,但输出却被严格限制,这也是着色器与通用处理器执行程序的最大不同。
底层虚拟机给不同类型的输入输出提供了特殊的寄存器,uniform输入只能通过只读常量寄存器或常量缓冲区来访问,这也是为什么在整个绘制调用期间它们的值是不变的。可用的常量寄存器的数量远大于用于varying输入输出的寄存器数量。这是因为varying的输入输出需要单独存储每个顶点和像素,而uniform类型的输入只被存储一次,且在整个绘制调用期间被所有的顶点和像素复用。该虚拟机也包含了通用功能的临时寄存器,被用作暂存空间(scratch space),所有类型的寄存器都可以在临时寄存器中使用整数进行数组索引。着色器虚拟机的输入和输出如下图所示。
图形计算中的常见操作在现代GPU上执行速度非常快。通常情况下,最快的操作是标量和向量的乘法,加法以及他们的组合,如乘加运算(multiply-add)和点积运算(dot-product)。其它操作如取倒数,平方根,sin,cos,指数以及对数往往会稍微贵一点,但依然很速。纹理操作是高效的,但是他们的性能可能受到诸如等待检索结果的时间等因素的限制。着色语言使用类似于 *和+的操作符来表示常用操作(如加法和乘法),其它操作使用固有函数(intrinsic function)的方式来表示,如atan(), dot(),log()等等。对于更复杂的操作,例如向量归一化和反射,叉积,矩阵转置,以及求行列式等,也用固有函数来表示。
流控制(flow control)这个术语是指使用分支指令来改变代码执行流程的操作。这些指令用于实现高级语言结构,如“if”和“case”语句,以及各种类型的循环。着色器支持两种类型的流控制:
- 静态流控制(Static flow control)基于uniform输入。这意味着代码的流在调用时是恒定的。静态流控制的主要好处是允许在不同的情况下使用相同的着色器(例如,不同数量的光源)。
- 动态流控制(Dynamic flow control)基于varying输入。动态流控制远比静态流量控制更强大但同时开销也更大,特别是在多个着色器调用之间,代码流的改变很不规律的时候。
评估一个着色器的性能,要看其在一段时间内处理顶点或像素的个数。如果流对某些元素选择“if”分支,而对其他元素选择“else”分支,那么对于所有元素,两个分支都必须进行评估(未使用的分支将被丢弃)。
着色器程序可以在程序加载或运行时离线编译。和任何编译器一样,有生成不同输出文件和使用不同优化级别的选项。一个编译过的着色器被存储为为字符串或者文本,并通过驱动程序传递给GPU。
3. 可编程着色的演变
可编程着色的框架的思想可以追溯到1984年Cook的着色树(shade tree)[1]。
一个简单的铜着色器的着色树和其对应的着色语言代码如下图所示。
RenderMan 着色语言(RenderMan Shading Language)是从80年代中后期根据这个可编程着色的框架思想开发出来的,目前仍然广泛运用于电影制作的渲染中。在GPU原生支持可编程着色之前,有一些通过多个渲染通道实现实时可编程着色的尝试。
- 1999年,《雷神III:竞技场》的脚本语言是在这个领域上第一个成功广泛商用的语言。
- 2000年,Peery等人,描绘了一个将RenederMan着色器翻译成在多通道上并且运行在图形硬件上的系统。
- 2001年,NVIDIA的GeForce 3发布,它是第一个支持可编程顶点着色器的GPU,向DirectX 8.0开放,并以扩展的形式提供给OpenGL。
- 2002年, DirectX 9.0发布,里面包括了Shader Model 2.0(及其扩展版本2.X),Shader Model 2.0 支持真正的可编程顶点和像素着色。着色编程语言 HLSL也是随着DirectX 9.0的发布而问世。
- 2004年,Shader Model 3.0推出。SM3.0是一个增量改进,将之前的可选功能进行了实现,进一步增加资源上限,并在顶点着色器中添加了有限的纹理读取支持。新一代主机的问世,2005(Microsofts Xbox 360),2006 (Sony Play Station),它们配备了Shader Model 3.0级别的GPU。
- 2006年底,任天堂发布了搭载固定功能管线GPU的Wii主机 ,固定功能管线。
- 2007年,Shader Model4.0发布,包含于DirectX 10.0中,OpenGL以扩展的方式支持。SM 4.0新增了几个着色器和流输出等新特性。SM 4.0包括支持所有着色器类型(顶点、几何、像素着色器)的一套统一着色模型(uniform programming model)。
着色模型对比
下表比较了各种着色模型的能力。在这个表格中,“VS”代表顶点着色器和“PS”代表像素着色器(而SM 4.0 引入了几何着色器,其能力与顶点着色相似)。如果没有出现“VS”和“PS”,那么该行适用于顶点和像素着色器。
4. 顶点着色器
顶点着色器(Vertex Shader)是完全可编程的阶段,是专门处理传入的顶点信息的着色器,顶点着色器可以对每个顶点进行诸如变换和变形在内的很多操作。
顶点着色器是功能管线上的第一个阶段,虽然是进行任何图形处理的第一个阶段,但其实一些数据操作在这个阶段之前就发生了。在DirectX所称的输入装配(input assembler)的阶段,会将一些数据流组织在一起,形成顶点和基元的集合,发送到管线。例如一个物体可以由一个位置数组和颜色数组表示。输入装配将通过创建具有位置和颜色的顶点来创建这个物体的三角形(或线、点)。第二个对象可以使用相同的位置数组(连同不同的模型转换矩阵)和不同的颜色数组来表示。这允许一个物体被绘制多次,每个实例有一些不同的数据,所有这些都只需一次调用。DirectX 10中的输入装配还对每个实例、图元和顶点加上一个标识符号,该标识符号可以被任何后续的着色器阶段访问。对于早期的着色器模型,必须将这些数据显式地添加到模型中。
三角形网格由一组顶点和附加信息描述,这些信息描述顶点如何形成三角形。顶点着色器是处理三角形网格的第一阶段。描述三角形成的附加数据对于顶点着色器来说是没什么用的;顾名思义,顶点着色器只处理传入的顶点。一般来说,顶点着色器提供了一种修改、创建或忽略与每个多边形顶点相关的值的方法,比如它的颜色、法线、纹理坐标和位置。通常顶点着色器程序将顶点从模型空间(Model Space)转换到齐次裁剪空间(Homogeneous Clip Space);至少,顶点着色器必须始终输出此变换位置。
顶点着色器在2001年首次由DirectX 8引入。由于是管线的第一个阶段,调用相对较少,因此可以选择在GPU还是CPU上实现。如果在CPU上实现最后依然需要将输出数据送到GPU进行光栅化。这使得从旧硬件到新硬件到转换只是速度问题而不是功能性问题。目前几乎所有的GPU都支持顶点着色。
顶点着色器本身与通用核心虚拟机(Common Core Virtual Machine)非常相似。传入的每个顶点由顶点着色器程序处理,然后输出一些在三角形或直线上进行插值后获得的值。顶点着色器既不能创建也不能消除顶点,并且由一个顶点生成的结果不能传递到另一个顶点。由于每个顶点都被独立处理,所以GPU上的任何数量的着色器处理器都可以并行地应用到传入的顶点流上。
接下来的章节将会解释一些顶点着色器的效果,比如阴影体的创建,动画的顶点混合和剪影渲染。顶点着色器的其他用途包括:
- 镜头效果,使屏幕呈现鱼眼,水下,或其他方式的扭曲。
- 物体定义,只创建一个网格,并让它由顶点着色器变形。
- 物体扭曲、弯曲和锥度操作。
- 程序性变形,如旗帜、布或水的移动。
- 通过沿着管线发送退化的网格来创建图元,并根据需要赋予区域。这个功能在新的GPU中被几何着色器所取代。
- 页面卷曲、热雾、水波纹和其他效果可以通过使用整个帧缓冲区的内容经过程序性变形然后作为对准屏幕网格上的纹理的方式来实现。
- 顶点纹理提取(SM 3上可用),可用于将纹理应用于顶点网格,允许海洋表面和地形高度字段被廉价应用。
下图是使用顶点着色器产生变形的例子:
左图,一个普通茶壶。中图,经过顶点着色程序执行的简单剪切(shear)操作产生的茶壶。右图,通过噪声(noise)产生的发生形变的茶壶。
顶点着色器的输出可以以许多不同的方式来使用,通常是随后用于每个实例三角形的生成和光栅化,然后各个像素片段被发送到像素着色器,以便继续处理。而在SM 4.0中,数据也可以发送到几何着色器(Geometry Shader)或输出流(Streamed Output)或同时发送到像素着色器和几何着色器两者中。
5. 几何着色器
几何着色器随着2006年底 DirectX 10的发布被加入到硬件加速图形管线中。它位于管线中的顶点着色器之后,是可选的。几何着色器作为SM 4.0的一部分,并不能用于早期的着色模型(<=SM 3.0)。
几何着色器的输入是单个对象及其相关顶点,对象通常是网格中的三角形,线段或简单的点。另外,几何着色器还可以处理扩展图元,比如下图中最右边的两个图元就是扩展图元。
几何着色器处理如上类型的图元,并输出零个或多个图元,但输出的形式只能为点、折线和三角形条。例如,可以通过一次调用几何着色器程序来输出多个三角形条。同样重要的是,几何着色器也可以没有任何输出。如此,我们可以通过编辑顶点、添加新图元和删除其他内容来选择性地修改网格。(当输出的图元减少或者不输出时,实际上起到了裁剪图形的作用,当输出的图元类型改变或者输出更多图元时起到了产生和改变图元的作用。)
几何着色器程序的输入对象和输出对象的类型不必匹配。例如,输入一个三角形,输出很可能只是三角形的中心点。即使输入和输出对象类型匹配,几何着色器也可以省略或扩展每个顶点上的数据。例如,可以计算三角形的平面法线,并将其添加到每个输出顶点的数据中。与顶点着色器类似,几何着色器必须为每个顶点输出一个齐次裁剪空间的位置。
几何着色器必须保证输出与输入的顺序是相同的。这会影响性能,因为如果多个着色器单元并行运行,则结果必须保存并排序。考虑到性能和效率的权衡,在SM 4.0中有一个限制,每次执行总共只能生成1024个32位数值。因此,如果需要生成一千个灌木叶子,只有一个叶子作为输入是不可行的,这不是几何着色器的推荐用法。将简单的表面细分成更复杂的三角网格的做法也是不推荐的。这个阶段更多的是以编程方式修改传入的数据或复制有限数量的副本,而不是大规模复制或细化。其他可以利用几何着色器的算法包括从点数据创建各种大小的粒子,沿着轮廓挤压出鳍状物来绘制毛发,为阴影算法寻找物体边缘等。下图是一些一些几何着色器的应用例子:
左图,使用几何着色器实现元球的等值面曲面细分(metaball isosurface tessellation)。中图,使用了几何着色器和流输出进行线段细分的分形(fractal subdivision of line segments)。右图,使用顶点和几何着色器的流输出进行布料模拟。(图片来自NVIDIA SDK 10的示例)
流输出
GPU的管线的标准使用方式是发送数据到顶点着色器,然后对所得到的三角形进行光栅化处理,之后再在像素着色器中进行处理。 数据总是通过管线传递,中间结果是无法访问的。流输出这一概念在SM 4.0中被引入。在顶点着色器(以及可选的几何着色器中)处理顶点之后,除了将数据发送到光栅化阶段之外,也可以输出到流,例如,一个有序数组。事实上,可以完全关掉光栅化,然后管线纯粹作为非图形流处理器来使用。以这种方式处理的数据可以通过管线回传,从而允许迭代处理。这种操作特别适用于模拟流动的水或其他粒子特效。
6. 像素着色器
在顶点和几何着色器执行完其操作之后,图元会被裁剪、屏幕映射,结束几何阶段,到达光栅化阶段,在光栅化阶段中先经历三角形设定和三角形遍历,这些都是相对固定的处理步骤,是不可编程的。之后是可编程的像素着色器阶段。
像素着色器(Pixel Shader),在OpenGL中被称为片元着色器(Fragment Shader),似乎更为准确。一个三角形可以完全或部分地覆盖每个像素单元,所描绘的材质可以是不透明或透明的。光栅化并不直接影响像素中存储的颜色,而是在较大或较小的程度上生成解释三角形如何覆盖像素单元的数据。然后在合并过程中,此片元数据被用于修改存储在像素上的内容。
顶点着色器程序的输出实际上就是像素着色器程序的输入。在SM 4.0.5中总共可以有16个向量(每个向量有4个值)从顶点着色器传到像素着色器。当启用几何着色器时,可以输出32个向量传到像素着色器。
在SM 3.0中额外的输入被专门添加到像素着色器中,例如,三角形的哪个边可见的信息,被作为输入标志添加进去。这一知识对于一次渲染正面和背面是不同材质的三角形是很重要的。而且像素着色器也可以获得片元的屏幕位置。
像素着色器的局限性在于它只能影响片元。也就是说,当一个像素着色器程序执行时,它不能直接将结果发送给相邻像素。相反,它使用从顶点插值的数据以及存储的常量和纹理数据来计算结果,该结果也只影响单个像素。然而,这个限制并不像听起来那么严重。邻近像素最终会受到图像处理技术的影响。
像素着色器可以访问到的相邻像素的信息(虽然是间接的),是梯度或导数信息的计算。像素着色器可以通过获取的值计算出它在x和y屏幕轴上每个像素的变化量。这对于各种计算和纹理寻址是很有用的。梯度对于某些操作,如过滤,尤为重要。大多数的GPU是通过处理以2×2或更多为一组的像素来实现此功能的组。当像素着色器请求一个梯度值时,相邻像素之间的差值被返回。这样的一个结果是,着色器中受动态流控制的部分是不能访问梯度信息的——同一组中的所有像素必须被同样的指令所处理。这是离线渲染系统中存在的一个基本限制。访问梯度信息的能力是像素着色器独有的,并且不与其他可编程着色器阶段共享。
像素着色程序通常在最终合并阶段设置片段颜色以进行合并,深度值也可以由像素着色器修改。模板缓冲(stencil buffer)值是不可修改的,而是将其传递到合并阶段(merge stage)。在SM 2.0以及以上版本,像素着色器也可以丢弃(discard)传入的片元数据,即不产生输出。这样的操作会消耗性能,因为通常在这种情况下不能使用由GPU执行的优化。诸如雾计算和alpha测试的操作已经从合并操作转移到SM 4.0 中的像素着色器里计算。
当前像素着色器已经能够进行大批量的处理。在单个渲染过程中计算任意数量的值的需求引出了多渲染目标(MRT, multiple render targets)这一概念。与将像素着色器程序的结果保存到单个颜色缓冲区相反,每个片元生成多个向量,并保存到不同的缓冲区中。这些缓冲区必须是相同的维度,一些架构还要求它们具有相同的位深度(尽管出于需求,它们都是不同的格式)。上文着色模型对比表中PS Output Registers的数量是指可访问的单独缓冲区的数量,即4或8。与可显示的颜色缓冲区不同,这些缓冲区还有由于额外目标而带来的其他限制。例如,通常不执行抗锯齿。即使有这些限制,MRT功能也是更有效地执行渲染算法的有力工具。如果要从同一组数据中计算出若干中间结果图像,则只需要一个渲染通道,而不是每次输出缓冲都要一个通道。与MRTs相关的另外一个关键能力是读取图像作为纹理。
7. 合并阶段
作为光栅化阶段名义上的最后一个阶段,合并阶段(Merging Stage)是将像素着色器中生成的各个片段的深度和颜色与帧缓冲结合在一起的地方。这个阶段也就是进行模板缓冲(Stencil-Buffer)和Z缓冲(Z-buffer)操作的地方。最常用于透明处理(Transparency)和合成操作(Compositing)的颜色混合(Color Blending)操作也是在这个阶段进行的。
虽然合并阶段不可编程,但却是高度可配置的。在合并阶段可以设置颜色混合来执行大量不同的操作。最常见的是涉及颜色和Alpha值的乘法,加法,和减法的组合。其他操作也是可能的,比如最大值,最小值以及按位逻辑运算。
8. 特效(effects)
GPU渲染管线中的可编程阶段有顶点、几何和像素着色器三个部分,它们需要相互结合在一起使用。正因如此,不同的团队研发出了不同的特效语言,例如HLSL FX,CgFX,以及COLLADA FX,来将他们更好的结合在一起。
一个特效文件通常会包含所有执行一种特定图形算法的所有相关信息,而且通常定义一些可被应用程序赋值的全局参数。例如,一个单独的特效文件可能定义渲染塑料材质需要的VS(顶点着色器)和PS(像素着色器),它可能暴露一些参数例如塑料颜色和粗糙度,这样渲染每个模型的时候可以改变效果而仅仅使用同一个特效文件。一个特效文件中能存储很多techniques。这些techniques通常是一个相同effect的变体,每种对应于一个不同的SM(SM2.0,SM3.0等等)。如下图,就是使用着色器实现的特效。
参考文献
[1] Akenine-Möller T, Haines E, Hoffman N. Real-Time Rendering, Third Edition[J]. Crc Press, 2008.