Real-Time Rendering 翻译 3.图形处理单元

历史上,图形硬件是从渲染管线的末端功能开始发展,最开始进行三角形的光栅化操作。经过成功的迭代,图形硬件从后往前接管渲染管线的功能,直达应用阶段直接向硬件提交数据。硬件相对于软件的唯一优势是速度,但速度恰好是最关键的。

过去十年,图形硬件发生了翻天覆地的变化。第一代引入的消费级图形芯片是1999年发布的NVIDIA GeForece256。NVIDIA制造了术语GPU(graphics processing unit) 用来区分之前只能光栅化的芯片。经过几年的发展,GPU从可配置的复杂的固定管线发展为开发者可以实现自己算法的可编程管线。各种各样的可编程的着色器是控制GPU的主要手段。顶点着色器对每个顶点执行各种各样的操作(包括变换和变形)。同样的,片源着色器操作每个像素,可以在此执行各种复杂的着色计算。计算结果可以写入高精度的缓存中,并以顶点或者纹理的方式重用。为了效率考虑,有些管线功能还只是可配置的,而不是可编程的,但是未来的趋势是可编程和灵活的。


图3.1 GPU实现的渲染管线。每个阶段的颜色是根据用户可控制度来确定的。绿色阶段是完全可编程的,黄色阶段是可配置的但不可编程,例如裁剪阶段可以通过用户定义的裁剪平面来进行裁剪。蓝色阶段是完全固定的。

3.1 GPU管线预览

GPU实现了第二章当中的几何和光栅化阶段。它们被分为多个硬件阶段,每个阶段有着自己的可配置性和可编程性。图3.1中每个阶段的颜色是根据自己的可编程性和可配置性来决定的。注意物理阶段的划分和第二章提到的功能阶段的划分有细微的差别。

顶点着色器是完全可编程的,所以经常用来实现模型视图变换、顶点着色和投影变换等功能。几何着色器是可选的,完全可编程的,用来操作图元的顶点。可以用来做逐图元的着色计算、销毁或者创建图元。裁剪、屏幕映射、三角形设置和三角形遍历都是固定功能阶段。与顶点和几何着色器一样,片源着色器也是完全可编程的,用来执行片源着色操作。最后,合并阶段介于完全可编程和固定操作之间。尽管不能编程控制,但是是高度配置化的,可以实现多种多样的操作。合并阶段也实现了很多功能,比如负责修改颜色、Z-buffer、混合、模板和其他先关的缓存操作。[2016/06/16]

随着时间变化,GPU管线从固定操作进化到更为灵活的控制。这个进化过程中,引入可编程着色器是最重要的一步。下一节描述各个可编程阶段的特性。

3.2 可编程着色阶段

现代着色阶段(支持Shader Model4.0,DirectX10及后续)使用一个通用的着色器内核(common-shader core)。这就意味着顶点着色器、片源着色器和几何着色器共享同一个编程模型。[2016/06/18]我们在本书中区分通用着色器内核、程序员所看到的功能、统一的着色器。通用着色器内核是API,拥有统一的着色器是GPU的一个特性。早期的GPU没有几何着色器,顶点着色器和片源着色器之间也大不相同。尽管如此,统一着色器的某些设计元素也可以在旧硬件上找到,旧硬件有可能设计的较简化或者没有设计,并不是彻底的不同。我们主要讨论的是Shader Model 4.0,较早GPU的着色器模型后面会讨论。

完全描述编程模型超出了本书的范畴,有很多的文档书籍和网站做了这件事情。一些简单的讨论还是需要的。着色器使用类C的着色器语言,例如HLSL,Cg和GLSL。它们被编译成独立于机器的汇编语言,也称之为中间语言(IL)。早期的着色器模型允许直接使用汇编语言编程,到了DirectX10,汇编语言只有在调试输出中看到。这些汇编语言转换为实际的机器码是一个独立的过程,通常是在GPU驱动中完成。这种设计允许在不同的硬件中保持兼容性。这种汇编语言可以被视作虚拟机的定义,主要针对着色语言的编译器。

这种虚拟机是一个拥有寄存器、数据源和程序指令的处理器。因为很多图形操作都是以短向量(最多4个分量)为单位进行操作,很多处理器拥有4路的SIMD(single-instruction multiple-data单指令多数据)。每个寄存器包含4个独立的数据,32位单精度浮点数和向量是最基础的数据类型,最近添加了32位的整数。向量一般包含的数据如下,位置(xyzw)、法线、矩阵的行、颜色(rgba)或者纹理坐标(uvwq)。整数一般用来表示计数器、索引或者标志位。同时也支持聚合数据类型,例如结构体、数组和矩阵。为了处理向量,也可复制向量的任何部分。也就是,向量的分量可以被重排或者复制。同样的,也支持masking操作,就是只使用向量的指定分量。

Draw Call是指调用图形API来绘制一组图元,会引起图形管线来执行绘制。每个可编程着色器拥有两类输入:统一变量(uniform),单次Draw Call保持不变(Draw Call之间可能会变);易变变量(varying),对每个顶点或者片源都不一样。纹理是一种特殊的统一变量,以前被认为是一张贴在物体表面的图像,现在认为是一组大量的数据。特别注意,尽管着色器有很多不同的输入,可以使用多种方式访问,但是输出被严格限制了。这与执行在通用处理器上的程序的输入大有不同。底层的虚拟机提供很多寄存器来访问这些各种各样的输入和输出。统一变量存储在只读的常量寄存器(constant registers)或者常量缓存(constant buffers)中,之所以这么命名是因为在一次Draw Call中这些内容是不变的。可用的常量寄存器远比易变变量多。这是因为输入的统一变量只需要存储一次,可以为所有的顶点或者片源所使用(一次Draw Call中)。虚拟机还拥有很多通用的临时寄存器,用作暂存空间。临时寄存器可以作为索引来访问所有的寄存器。图3.2描述了着色器虚拟机的输入和输出。


图3.2 DirectX10中通用着色器虚拟机的架构和寄存器分布。每种资源标注了最大数量。以斜杠分割的数据分别表示顶点、几何和片源着色器的限制。

图形计算操作在现代GPU上可以高效的执行。通常来说,最快的操作包括标量和向量的乘法、加法和他们的组合,例如乘加和点乘。其他操作,例如倒数、开方、sine、cosine、指数和对数,会消耗一些,但是还是非常快。纹理操作非常高效,但是也受到其他因素的影响,比如等待访问纹理的时间。着色语言开放了大部分常用操作(例如加法和乘法)。其他的操作以内置函数方式暴露出来,例如atan(),dot(),log()等等。内置函数也有一些复杂的操作,例如向量标准化和反射、叉乘、矩阵转置和行列式值计算等等操作。[2016/06/20]

流程控制指的是通过分支指令修改代码的执行流程。这些流程控制指令用来实现高级语言的if和case语句,同样用来实现各种类型的循环。着色器支持两种类型的流程控制。静态流程控制基于输入的统一变量(uniform)。这就意味着在单次Draw Call中代码的执行流程是固定的。静态流程控制允许一个shader用于各种情形(例如,不同的光源数量)。动态流程控制基于易变变量(varying)的值。动态流程控制比静态控制更为灵活,但是开销更大。着色器代码同时执行在多个顶点或者像素上。如果流程控制中的if分支对一些顶点或者像素其作用,else分支执行在其他的顶点或者像素,但是所有的顶点或者像素都会经过if-else条件判断以判断执行哪个分支。

着色器程序可以离线编译或者运行时编译。可其他编译器一样,有很多关于输出或者优化等级的选项。编译后的着色器存储为字符串,通过驱动传递给GPU执行。

3.3 着色器语言的进化

关于可编程着色器的框架最早可追溯到1984年的shader trees。图3.3描述了一个简单的shader与其相关的shader trees。RenderMan Shading Language是根据这种思想来开发的,这种语言至今任然用于电影渲染。在GPU真正支持可编程着色器之前,有很多方法通过多遍渲染来实现可编程的着色操作。Quake III的Arena脚本语言是第一种使用这种方法的商业产品。Peercy描述了一个系统,这个系统可以将RenderMan着色语言转换为图形硬件可执行的多遍渲染。他们发现要实现这种系统,GPU缺乏两个通用的特性,一是不能使用计算结果作为纹理坐标,二是缺乏各种范围和精度的数据类型用于纹理和颜色缓存。比如使用16位的浮点数的表示方法。这一阶段,没有商业GPU产品支持可编程着色语言,尽管有些产品已经有了高度可配置的渲染管线。


图3.3 着色器语言的语法树

2001年,NVIDIA的GeForce3是第一种支持可编程顶点着色器的GPU,通过DirectX 8.0和OpenGL的扩展暴露出来。[2016/06/21]这些着色器语言类似于汇编语言,在运行时通过GPU驱动转换为GPU可执行的机器码。DirectX 8.0同时也引进了片源着色器,不过Shader Model 1.1的片源着色器的可编程性不高,片源着色器最终被转换为纹理绑定。这些程序不仅在长度方面有所限制(12条指令以内),而且缺少真正可编程的两个特性——纹理读取和浮点数据。

此时的着色器还不支持流程控制,所以条件控制通过如下模拟来完成,计算出所有的可能的结果,然后选择或者在各种结果中插值。DirectX定义Shader Model来区分不同硬件的着色器的能力。GeForce3支持顶点着色器Shader Model 1.1和片源着色器Shader Model 1.1(Shader Model 1.0打算用于硬件,但是重来没用过)。2001年,GPU朝着可编程片源着色器更进了一步。DirectX 8.1添加了片源着色器Shader Model 1.2和1.4,扩展了片源着色器的能力,添加了额外指令和纹理读取操作。

2002年,DirectX 9.0引入了Shader Model 2.0,支持着完全可编程的着色器。OpenGL扩展同样引入了这些功能。添加了纹理读取操作和16位浮点数,完全满足了可编程着色器的定义。着色器资源(指令,纹理,寄存器)的限制也提高了,所以着色器有能力完成更为复杂的特效。流程控制也加入进来了。随着着色器更大更复杂,使用汇编语言编写着色器变的笨重起来。幸运的是,DirectX 9.0还引入了新的着色器编程语言HLSL(High Level Shading Language)。HLSL是由Microsoft和NVIDIA共同完成,还发布了一个跨平台的变种Cg。与此同时,OpenGL ARB(Architecture Review Board)发布了类似的语言GLSL。这些语言的语法和设计哲学都到C语言的深度影响,也引入了RenderMan Shading Language的设计元素。

在2004年引入的Shader Model 3.0,又有着显著的提高,进一步提高了资源上限,并在顶点着色器中增加了纹理读取操作。新一代的主机游戏机(2005, Xbox360, 2006 Playstation3)都装备了支持Shader Model3.0的GPU。固定管线并没有完全消失,2006年的Wii还是使用固定管线的GPU。基本上最新的主机游戏机,甚至移动手机都可以使用可编程着色器。

其他着色器开发的语言和环境也有所发展。例如Sh语言,使用通过C++库来生成GPU着色器代码。这个开源项目运行在多个平台上。也出现了一些可视化工具,允许艺术家设计着色器。这些工具拥有图形化编辑器,使用内置的着色器组合来设计新的效果,最终编译器会将结果转换为着色器语言(例如HLSL)。图3.4所示的是一个这样的工具的截图。McGuire描述了一个可视化的着色器编程系统,主要针对高层次和抽象的概念。

2007年可编程性又前进了一步。Shader Model 4.0引入了多个重要特性,例如几何着色器和流式输出。Shader Model 4.0为所有的着色器(顶点、片源和几何)引入了统一的编程模型,就是之前描述的统一着色器内核。也提高了资源的上限,整数类型(包括位操作)也添加进来了。Shader Model 4.0只支持高级着色器语言(HLSL和GLSL),用户不能编写之前的汇编语言。

GPU制造商、微软和OpenGL ARB持续改进和扩展着色器语言的能力。除了新版本的API,新的编程模式也可用于非图形应用,例如NVIDIA的CUDA和AMD的CTM。GPU通用编程将在18.3.1介绍。

3.3.1 Shader Model之间的比较

尽管这章主要针对Shader Model 4.0(本书编写时最新),但是开发者经常需要面对支持老的Shader Model的硬件。基于这个原因,我们简单的描述几种Shader Model(2.0,3.0,4.0)之前的区别。本书不会列出他们之前的详细区别,详细信息可以再MSDN和DirectX SDK中找到。

我们这里主要针对DirectX,因为它有着明显的版本区别,而不像OpenGL有着不同的扩展,有些是由OpenGL ARB带来的,有些是由制造商带来的。这种扩展系统允许独立硬件制造商(IHV)的最新功能可以立即被使用。[2016/06/23]DirectX 9及之前版本通过”capability bits”(能力标识)来判断GPU是否支持IHV的特性。到了DirectX 10,微软抛弃了上面的做法,要求所有IHV都必须支持标准模型。尽管我们这里主要讨论DirectX,但是接下来讨论的东西也和OpenGL相关,同时期的GPU都有相同的特性。

表3.1比较了不同Shader Model之间的区别。表中VS表示顶点着色器,PS表示片源着色器(Shader Model 4.0引入了几何着色器,和顶点着色器类似)。如果没标记VS或者PS,表示这行对顶点着色器和片源着色器都适用。因为虚拟机是4路SIMD,每个寄存器都能存储1到4个独立的值。”Instruction Slots”表示着色器能容纳指令的最大数量。[2016/06/23]”Max. Steps Executed”指的是可以执行的指令的最大数量,包括分支和循环指令。”Temp. Register”指的是为了存储临时变量的通用寄存器数量。”Constant Register”指的是着色器输入变量的数量。”Flow Control, Predication”指的是着色器执行分支和循环指令的能力。”Textures”指的是着色器可以访问的纹理的数量(每个纹理可能被访问多次)。”Integer Support”指的是支持整数类型的能力,包括位运算和整数的算术运算。”VS Input Register”指的是顶点着色器可访问的易变变量的数量。”Interpolator Registers”指的是顶点着色器传输给片源着色器所使用的寄存器。之所以怎么命名,是因为顶点着色器传给片源着色器的值是经过插值的。最后,”PS Output Register”指的是存储片源着色器输出的寄存器,每个代表的是一个缓存或者是render target。

XX SM 2.0/2.X SM 3.0 SM 4.0
Introduced DX 9.0, 2002 DX 9.0c, 2004 DX 10,2007
VS Instruction Slots 256 512 4096
VS Max. Steps Executed 65536 65536
PS Instruction Slots 96 512 65536
PS Max. Steps Executed 96 65536
Temp. Registers 12 32 4096
VS Constant Registers 256 256 14X4096
PS Constant Registers 32 224 14X4096
Flow Control, Predication Optional Yes Yes
VS Textures None 4 128X512
PS Textures 16 16 128X512
Integer Support No No Yes
VS Input Register 16 16 16
Interpolator Registers 8 10 16/32
PS Output Registers 4 4 8

表3.1 DirectX Shader Model各个版本的能力

3.4 顶点着色器

顶点着色器是渲染管线的第一个阶段,如图3.1所示。这是第一个和图形处理相关的阶段,需要了解一下这个阶段之前所做的数据操作。DirectX称顶点着色器之前的操作为输入装配器(input assembler),大量的数据交织在一起形成顶点和图元数据,传送给渲染管线。例如,一个对象由一组顶点位置数据和一组颜色数据所表示。input assembler会创建这个对象的三角形数据,本质上还是使用这个对象的顶点位置和颜色数据(三角形使用到这些顶点和颜色)。[2016/06/28]另一个对象也可以使用同一组顶点位置数据(使用另一个模型变换矩阵)和另一组颜色数据。数据表示会在12.4.5详细讨论。输入装配器还会执行实例化操作。这就意味着在一个Draw Call中,一个模型可以有多个绘制实例,每个实例可以有些不同的数据。DirectX 10中的输入装配器还会给每个实例、图元和顶点一个ID,在其他着色阶段可以访问到这个ID。对于早期的着色器模型,这样的数据只能附着在模型上。

一个三角形网格是由如下数据表示,顶点数据和描述如何形成三角形的数据。顶点着色器是第一个处理三角形网格的渲染管线阶段。顶点着色器是不能访问描述如何形成三角形的数据,正如它的名字所言,它只处理输入的顶点数据。更专业的描述,顶点着色器提供一个修改/创建/忽略顶点相关属性的方法,这些顶点属性包括颜色、法线、纹理坐标和位置。一般来说,顶点着色器将顶点从模型空间转换到齐次裁剪空间,最少也会输出变换后的位置信息。

顶点着色器是由DirectX 8在2001年引入的。因为这是渲染管线的第一个阶段,调用也相对不频繁,顶点着色器既可以GPU也可以在CPU上实现,将最终的结果送到GPU光栅化。新硬件在GPU上实现顶点着色器是为了速度,而不是功能。现代所有的GPU都支持顶点着色器。

顶点着色器本身类似于3.2所描述的通用的虚拟机。每个顶点都由顶点着色器程序处理,输出的数据会被插值。顶点着色器不能产生或者销毁顶点,一个顶点的计算结果不能被另一个顶点访问。每个顶点都被独立对待,GPU上的多个着色器处理器可以并行处理输入的顶点。

接下来的章节会讨论很多顶点着色器所产生的效果,例如阴影体(Shadow Volume)的创建、动画中的顶点融合和轮廓渲染。还有一些顶点着色器的应用如下:

  • 镜头特效,例如鱼眼,水下效果或者扭曲效果。
  • 模型定义,例如创建一个网格,然后被顶点着色器畸变。
  • 模型扭曲,弯曲或者锥化。
  • 程序上的形变,例如旗子、布料或者水面的运动。
  • 创建图元,产生一些新的网格数据并传递到接下来的渲染管线中。在新一代的GPU中,这一个功能由几何着色器完成。
  • 纸面的卷曲,热霾,水波等效果可以使用如下方法完成,使用帧缓存的数据作为纹理,然后在网格上产生程序上的形变。
  • 顶点着色器中可以读取纹理(Shader Model 3.0以上),这样一来海洋表面和地形可以花很少的代价来实现。

一些由顶点着色器完成的形变效果可以参见图3.5。


图3.5 最左边是一个正常的茶壶。一个由顶点着色器完成的简单的切变操作产生了中间的图像。右边是由噪声函数所产生的扭曲的模型。

顶点着色器的输出数据可以有多种处理方式。最常用的方式是对每个三角形进行光栅化操作,所产生的片源输入到片源着色器进行接下来的处理。在Shader Model 4.0中,顶点着色器的输出可以送到几何着色器。这些操作是接下来的主题。

3.5 几何着色器

2006年发布的DirectX 10引入了几何着色器,成为了硬件加速的图形渲染管线的一部分。它处在顶点着色器的下一个阶段,通常是可选的。几何着色器是Shader Model 4.0中的一部分,之前的Shader Model是不包含的。

几何着色器的输入是一个模型和它的顶点。这个模型通常是网格中的一个三角形,一个线段或者一个顶点。几何着色器定义扩展出来的图元。特别的是,三角形的外部的三个顶点也会传进来,线段的连个邻接顶点也可以在几何着色器中使用。如图3.6所示


图3.6 几何着色器的输入是如下类型:点、线段和三角形。最右边的两个图元,包括线段和三角形的邻接顶点。

几何着色器处理这些图元,输出多个图元。输出类型包括点、线段和三角形带。[2016/07/04]例如,执行一次几何着色器可以输出多个三角形带。值得注意的是,几何着色器也会不输出任何东西。使用这种方式,可以编辑网格的顶点、添加新图元或者移除某些图元。

几何着色器的输出模型类型和输入模型类型没有必然联系。例如,输入是三角形但是输出可以是三角形的重心点。即使输出模型类型和输入模型类型相同,每个顶点的属性数据也可以减少或者扩展。例如,可以计算出三角形平面的法线并添加到顶点属性上。和顶点着色器类似,几何着色器的输出的每个顶点的坐标也必须是齐次裁剪坐标。

几何着色器保证输入图元和输出图元的顺序一致。这个特性影响到了性能,因为大量的着色器处理器并行运行着,输出结果必须保存起来然后排序。为了在能力和效率之间达到平衡,Shader Model 4.0限制几何着色器一次只能输出1024个32位的数据。所以,在几何着色器中使用一张树叶来创造出数千张树叶的这种做法是不可行的,也不是推荐的使用方式。也不推荐使用几何着色器将一个简单的表面细分为稠密的三角形网格。这个阶段的功能更多是使用程序手段来修改输入的顶点数据,或者有限制的复制顶点数据,而不是大量的复制或者扩大。例如,一个用途就是产生6份数据,同时渲染立方体贴图的6个面。还有一些算法可以使用几何着色器从一个个的点来创建大规模的粒子特效,沿着轮廓挤出面片以渲染皮毛,阴影算法中寻找物体的边缘。如图3.7所示。


图3.7

3.5.1 输出流

使用GPU渲染管线的标准方式是,将数据送到顶点着色器处理后,接下来光栅化这些三角形,再经过片源着色器处理。数据流过渲染管线,中间结果不能被访问。Shader Model 4.0引入了输出流。顶点经过顶点着色器(可选的几何着色器)处理之后,输出的数据组成流,比如一个有序的数组,然后送到光栅化阶段。事实上,光栅化阶段可以被关闭,整个渲染管线变成了一个纯粹的非图形的流处理器。经过这种范式处理后的数据还可以被送回渲染管线,这样形成迭代处理。这种使用方式特别适合模拟流动的树或者其他粒子效果。

3.6 片源着色器

经过顶点着色器和几何着色器处理,图元被裁剪,准备光栅化。[2016/07/04]上述(裁剪,光栅化)管线阶段的操作是固定不变的,不是可编程的。先遍历每个三角形,然后在三角形区域以内,使用顶点数据进行插值(其实是有了顶点数据[位置、颜色和法线等等],计算三角形区域内每个像素的数据)。接下来的阶段就是片源着色器阶段。OpenGL中称之为片源着色器。光栅化的原理是,三角形覆盖(完全或者不完全)很多像素,材质描述这些像素是透明或者不透明的。光栅化阶段不会直接影响到像素的颜色,其输出的数据是描述三角形如何覆盖一个个的像素点。在后面的合并阶段,片源数据才会最终影响到像素的颜色。

顶点着色器的输出数据成为了片源着色器的输入。Shader Model 4.0中,顶点着色器可以向片源着色器传递16个向量(4元素向量)。如果使用了几何着色器,它可以输出32个向量到片源着色器。

Shader Model 3.0给片源着色器添加了特殊的输入数据。例如,三角形的可见面信息传递进来了。当需要为三角形正反面渲染不同材质时,这个信息就非常有用。片源的屏幕位置在片源着色器也可以访问到。

片源着色器的限制是,它只能影响它处理的点。不能将一个片源的处理结果传递给旁边的片源。片源着色器使用的数据包括,顶点插值后的数据、常量和纹理,依据这些数据计算的结果只能影响到当前正在处理的片源。但是这个限制并没有听起来那么严重。图像处理的技术可以影响到邻近像素点。

只有一个例子是片源着色器可以访问到邻近像素点的信息(尽管是间接的),那就是计算梯度或者导数信息。片源着色器可以获取到像素点沿着X或者Y轴的变换信息。这对各种计算和纹理寻址很重要。梯度对于过滤这类的操作很重要。现代GPU实现过滤的方法都是以2x2的方式来访问纹理。当片源着色器需要访问当前片源的梯度时,会返回相邻像素的差值。这种2x2的纹理访问方式的限制就是,不能在片源着色器中通过动态流程来控制纹理访问方式,所有的访问方式都是同一指令。这种底层的限制甚至在离线渲染系统中都存在。这种可以访问梯度信息的能力只存在于片源着色器中,其他可编程着色器都不能做到。

片源着色器一般都是设置片源的颜色,在此之后会进到合并阶段。光栅化所产生的深度信息,在片源着色器中也可以修改。模板缓存是不可修改的,只能传递到合并阶段。在Shader Model 2.0中,片源着色器也可以丢掉片源数据,不产生任何输出。这种操作是耗时的,因为很多GPU的优化操作不能进行。一些操作从合并阶段移到片源着色器阶段,例如雾的计算和Alpha测试。

当前的片源着色器有能力进行大量的计算。[2016/07/07]由于片源着色器有有能力在一次处理中计算出片源的多种数据,这就产生了多渲染目标(multiple render targets MRT)。片源着色器不仅仅是将颜色输出到颜色缓存中,可以将多种数据保存到不同的缓存中。这些缓存必须有着相同的维度和尺寸,还有一些体系结构要求他们拥有相同的深度位(尽管实际上他们的格式不尽相同)。表3.1指明了片源着色器的输出寄存器的数量,以及各种缓存的数量,例如4或者8。不像用来展示的颜色缓存,其他缓存有着不同的限制。例如,一般来说都没抗锯齿功能。尽管有着这些限制,MRT大大提高某些渲染算法的效率。如果很多中间数据都是通过相同的数据源计算出来的,其实只需要一遍渲染就能完成,而不是一遍渲染得到一个输出缓存。使用渲染结果作为纹理也是MRT的重要能力之一。

3.7 合并阶段

如2.4.4节讨论的,合并阶段将单个片源(由片源着色器产生)的深度和颜色信息合并起来,成为帧缓存。模板缓存和Z-buffer等相关操作发生在这个阶段。颜色混合操作也发生在这个阶段,常用来实现透明和融合操作。

合并阶段发生在固定功能阶段和完全可编程着色器阶段之间。尽管不是可编程的,但是它相关的操作是高度可定制化的。颜色混合可以完成大量不同的操作。最常见颜色和Alpha的混合方式包括,乘、加和减,也有其他操作,例如取最大值和最小值操作,还有位操作。DirectX 10还允许混合片源着色器和颜色帧缓存中的颜色,这称之为双颜色混合。

如果MRT功能实现了,多个帧缓存之间也可以实现混合操作。DirectX 10.1允许不同的帧缓存实现不同的混合操作。在之前的版本,所有的帧缓存之间只能执行相同的混合操作。

3.8 效果

到此为止,我们已经介绍了管线中所有的可编程阶段。例如顶点、几何和片源着色器用来控制这些可编程阶段,他们之间不存在真空。一个孤立的着色器其实不太有用,片源着色器需要顶点着色器的输出数据。所有的着色器完成好之后才能工作。开发者需要对接好顶点着色器的输出和片源着色器的输入。多个着色器的多遍渲染才能达到一个特定的渲染效果。除了着色器之外,还需要将将各种状态设置好才能正确的工作。例如 ,渲染状态包括如何使用Z-buffer和模板缓存,片源着色器如何影响到现有的像素(替换、相加或者混合)。

由于以上原因,各种组织开发了多个特效语言,例如HLSL FX、CgFX和COLLADA FX。这种特效语言尝试封装特定渲染算法的所有相关信息。它定义多种可由应用访问的全局参数。例如,一个特性文件可以定义一个渲染真实塑料材质所用到的顶点和像素着色器。它还会暴露各种参数出来,例如塑料颜色和粗糙度等参数,这样可以使用同样的特效文件但是不同的参数来渲染各个模型。

为了展示特效文件的特性,我们看一个NVIDIA FX Composer 2特效系统的一些特效语言片段。Direct X 9 HLSL特效文件实现了一个很简单的Gooch shading。Gooch shading中使用表面的法线来和光源的位置来进行比较。如果法线指向光源,这个表面着暖色,如果相反,着冷色。[2016/07/08]法线介于两者之间的点的颜色在暖色和冷色之间插值得到。这种渲染技术是一种非真实渲染技术。效果如图3.8。

特效文件起始部分定义了所需变量。这些变量是不可变的,和相机位置相关的参数会自动设置好(在特效文件代码执行之前)。

float4x4 WorldXf   : World;
float4x4 WorldITXf : WorldInverseTranspose;
float4x4 WvpXf     : WorldViewProjection;

这种语法格式是 type id : samatic。上述语句中,float4x4是矩阵类型,名字是用户自定义的,samantic是内置名字。正如名字所示,WorldXf表示模型到世界坐标的转换矩阵,WorldITXf是WorldXf的逆转置矩阵,WvpXf是模型空间到相机空间的转换矩阵。这些由samatic标记的变量是由系统设置的,不用用户关心。

接下来是用户定义的变量:

float3 Lamp0Pos : Position <
    string Object = "PointLight0";
    string UIName = "Lamp 0 Postion";
    string Space = "World";
> = {-0.5f, 2.0f, 1.25f};

float3 WarmColor <
    string UIName = "Gooch Warm Tone";
    string UIWidget = "Color";
> = {1.3f, 0.9f, 0.15f};

float3 CoolColor <
    string UIName = "Gooch Cool Tone";
    string UIWidget = "Color";
> = {0.05f, 0.05f, 0.6f};

“<>”之间定义了其他符号,最后的”=”表示赋予默认值。”<>”之间的符号是应用申明的,与效果或者着色器无关。这些符号可以被应用查询到。这个例子中,这些符号描述了这些变量在用户界面如何显示。

接下来定义了着色器的输入和输出数据结构。

struct appdata{
    float3 Position : POSITION;
    float3 Normal   : NORMAL;
};

struct vertexOutput{
    float4 HPosition  : POSITION;
    float3 LightVec   : TEXCOORD1;
    float3 WorldNormal: TEXCOORD2;
};

appdata定义了模型中每个顶点所需的数据,也就定义了顶点着色器的输入数据。vertexOutput定义了顶点着色器的输出和片源着色器的输入。TEXCOORD*用于输出的名字是渲染管线进化的结果。最开始,一个表面会有多个纹理与之相关,所以这些字段称之为纹理坐标。实践中,这些字段其实可以存放从顶点着色器到片源着色器的任何数据。

接下来定义的是着色器代码。只有一个顶点着色器程序:

vertexOutput std_VS(appdata IN){
    vertexOutput OUT;
    float4 No = float4(IN.Normal, 0);
    OUT.WorldNormal = mul(No, WorldITXf).xyz;
    float4 Po = float4(IN.Position, 1);
    float4 Pw = mul(Po, WorldXf);
    OUT.LightVec = (Lamp0Pos - Pw.xyz);
    OUT.HPosition = mul(Po, WvpXf);
    return OUT;
}

程序首先将法线向量变换到世界坐标系。我们不会解释为什么这里会使用逆转置矩阵,变换是下一张的主题。顶点位置也被转换到世界坐标系中。[2016/07/08]使用模型到世界坐标的转换矩阵将顶点变换到世界坐标系中。然后使用光源位置减去顶点位置得到从顶点指向光源的向量。最后顶点位置转换到投影坐标系,用于光栅化。所有的顶点着色器程序都必须输出投影空间的顶点信息。

有了世界坐标系中的光源方向(顶点指向光源的向量)和法线,使用如下片源着色器计算片源的颜色。

float4 gooch_PS(vertexOutput IN) : COLOR
{
    float3 Ln = normalize(IN.LightVec);
    float3 Nn = normalize(IN.WorldNormal);
    float ldn = dot(Ln, Nn);
    float mixer = 0.5 * (ldn + 1.0);
    float4 result = lerp(CoolColor, WarmColor, mixer);
    return result;
}

向量Ln是光源方向向量的单位向量,Nn是法线的单位向量。由于是单位向量,Ln和Nn的点乘表示向量之间角度的cosine值。我们使用这个值在冷色和暖色之间进行线性插值。函数lerp()的mixer参数在0和1之间,0表示最终值为CoolColor,1表示最终值为WarmColor,在0到1之间的值表示最终值为CoolColor和WarmColor的混合值。由于cosine的值在位于区间[-1,1],所以需要将mixer变换到[0,1]。mixer用来混合出最终的颜色值,也就是片源着色器的输出。这些着色器都是汗多。一个特效文件可以包含多个函数,并能引用其他其他特效文件的函数。

一个pass一般包含一个顶点着色器,一个片源着色器和一个可选的几何着色器,还包含一些状态设置。一个technique是一个或者pass的集合,用于产生出期望的效果。如下文件包含一个technique,其中包含一个pass:

technique Gooch < string Script = "Pass=p0;"; >{
    pass p0 < string Script = "Draw=geometry;"; >{
        VertexShader = compile vs_2_0 std_VS();
        PixelShader = compile pas_2_a gooch_PS();
        ZEnable = true;
        ZWriteEnable = true;
        ZFunc = LessEqual;
        AlphaBlendEnable = false;
    }
}

这些状态设置Z-buffer为常见的使用方式,开启读和些,只有深度小于等于存储的深度值的片源才能通过深度检测。关闭Alpha混合,因为假设使用此technique的模型都是不透明的。这些规则意味着,深度值小于等于存储的深度值的片源的颜色会替代相应像素的颜色值。换而言之,就是标准的Z-buffer使用方式。

一个特效文件可能会包含多个technique。这些technique都是一个效果的变种,每个technique面对的是不同的Shader Model。由此可以产生大量的特效。图3.9展示了拥有现代可编程着色器的渲染管线的威力。这些效果包含了相关的technique。有很多方法用于管理Shader。

至此为止,我们完全展示了GPU。GPU还能做很多其他工作,有多种方式使用它的功能和组合。如何利用GPU能力的相关理论和算法是本书的主要主题。有了这些基础之后,我们接下来会主要关注变换和视觉表现,这些都是渲染管线的关键点。

你可能感兴趣的:(图形)