Skip to main content

3.4 着色器管理

在传统的渲染管线中,渲染是以几何体为单位进行的,相同类型的几何体可以使用对应类型的着色器。但是在延迟着色中,每一次渲染是以屏幕上的一块区域为单位进行着色计算的,这块区域内的像素点可能分别来自不同类型的几何体,它们可能分别具有不同类型的材质参数,所以使用延迟着色,另一个挑战是怎样管理好各种类型物体的着色计算。所幸的是,随着近几年基于物理的渲染模型的广泛使用,如第(1)章讲述的那些BRDF/BSDF模型,游戏场景中大量的物体表面都可以用比较统一的一些材质参数来表述,剩下仅有一些特殊的物体如毛发,皮肤等使用其他特殊的材质模型。

多种类型的几何体混在一起使用相同的着色器,这就避免不了分支的处理,而GPU中的分支计算可能会对程序性能造成很严重的影响。本节要介绍的是顽皮狗工作室在神秘海域4[a:DeferredLightinginUncharted4]中使用的管理着色器的方案。

图(1):顽皮狗神秘海域4中使用的基本的两个G-buffer,每个G-buffer中每个像素仅包含16位,它充分使用GCN架构的位解码技术来尽量压缩每个参数的长度(图片数据来自顽皮狗神秘海域4)

首先,G-buffer必须要支持所有类型的材质参数,每种不同类型的材质需要使用不同的渲染方法,所以这就可能导致巨大的G-buffer,因为其中一些材质参数对另一些类型的物体并不适用。为了尽可能减少G-buffer的体积,他们进行两个方面的优化,首先是对于特殊的材质,他们使用相互独立的材质参数,如图(1)所示,两个基本的G-buffer用来存储所有物体都需要的材质参数,例如法线,位置,粗糙度等等,第三个可选的G-buffer用来存储每种类型特殊的一些材质参数,这些物体包括织物,头发,皮肤以及丝绸等等,这个特殊的G-buffer对每个物体只使用一种类型,例如一个表面不可能同时包含织物和皮肤的材质参数,这是由材质创作管线限制的。

其次,他们充分使用GCN架构内置的的位解码方法来充分压缩每个参数的长度,使得图(1)中每个G-buffer中每个像素仅占用16位的存储空间。传统的着色器编程语言通常使用固定的包含1到4个分量内部图像格式,某些参数在实际中可能占用比一个分量更少的位置,这就会导致一定的浪费。GCN架构提供的解码方法[a:Low-levelShaderOptimizationforNext-GenandDX11]可以对任何整数按位进行解码,这使得我们可以在帧缓存中存储任何长度(甚至超过4个分量)的数据,并且这个操作可以在1个GPU时钟周期内完成,例如以下代码分别取s变量二进制表示的121612\sim 166116 \sim 11151\sim 5位,它们分别是5位、6位和5位的颜色分量:

int r = s & 0x1F; 			  // 1 cycle
int g = (s >> 5) & 0x3F; // 1 cycle
int b = (s >> 11) & 0x1F; // 1 cycle

在着色器编程中有一个术语叫大型着色器(uber shader),这种着色器往往是将各种材质类型以及光照类型的功能全部写在一个着色器中,这样就导致着色器中会包含大量的分支计算,而通过第(2)章中的内容可知,GPU中的分支计算会严重影响其性能,最坏的情况下,每个着色器的每个分支都要被执行一面,尽管有大量的着色器实例的计算是无效的。

然而我们已经说过,由于延迟着色将各种不同类型材质的物体混在一起,这就很难避免大型着色器,这就留给我们似乎也是唯一的方向:那就是我们能不能将这些混在一起的像素点,根据其材质类型将其分离出来,然后对每种类型使用一个独立的渲染通道?

这正是顽皮狗在神秘海域4[a:DeferredLightinginUncharted4]中使用的渲染技术,实际上这也是Battlefield 3[a:SPU-basedDeferredShadingforBattlefield3onPlaystation3]中已经使用过的技术,我们此处以神秘海域4中的内容为准。

神秘海域4中的延迟着色阶段是使用计算着色器以块为单位进行渲染的,其中每个块包含16×1616\times 16个像素点。在延迟着色阶段,计算着色器分两步处理块中像素的着色计算:

  1. 对每个块执行材质类型定义(material classification),以得到一个材质类型组合。
  2. 对每个材质类型组合分别执行一次计算着色器。

材质定义阶段的目的是找出每个块使用的计算着色器类型。为此,他们对每个不同材质类型的组合建立一个单独的计算着色器通道,所有可能的材质类型组合存储在一个查询表(lookup table)中,这样每个块通过其材质组合的索引值就能找到对应的计算着色器,如果在材质类型定义阶段像这样区分了所有的像素点,第二阶段就可以对每个材质类型组合使用一个独立的计算着色器,其仅包含少量(由下面的内容可知,这个数量取决于该块内材质类型的数量,在一个局部的块内,大部分的材质类型都是相同的,即使具有不同类型的材质,其分支的数量也比一个大型着色器的分支要少得多。)甚至无分支计算。

那每个块怎样计算这个材质类型组合的索引值呢?他们称这个材质类型组合的值为一个块的材质ID(注意,这里材质ID的概念是针对块的,而不是针对一个物体。由于块包含多个像素点,所以材质ID是一个材质的组合,而不是只单个材质。这里可以理解为,每个物体本身包含的材质是基本的原子的着色类型(由于他们假设所有材质相互独立),而材质ID是由于一个块包含多个原子功能之后总的看起来的着色类型。)(material ID)的属性,它实际上是一个位平面,每个位代表一个着色器特性(shader feature),或者我们可以称为着色器中计算功能的原理单位(其内部不包含分支的),神秘海域4中的材质ID使用12位来表示,由于这些材质类型之间是相互独立的,所以其可以被压缩到8位进行存储。

由于材质ID是一个位平面,所以每个块内的材质ID可以通过其内每个像素点材质对应位的或操作得来,然后我们就可以通过这个材质ID从查询表中查询到该块计算着色器的类型,将这个信息保存起来,就可以在后续的阶段被使用相应的计算着色器正确执行,材质类型定义阶段的代码如下:

uint materialMask = DecompressMaterialMask(materialMaskBuffer.Load(int3(screenCoord, 0)));

uint orReducedMaskBits;
ReduceMaterialMask(materialMask, groupIndex, orReducedMaskBits);

short shaderIndex = shaderTable[orReducedMaskBits];

if (groupIndex == 0)
{
uint tileIndex=AtomicIncrement(shaderGdsOffsets[permutationIndex]);
tileBuffers[shaderIndex][tileIndex]=groupId.x|(groupId.y<<16);
}

这里的shaderTable就是材质类型组合的查询表,它表示的是所有可能的包含各组材质组合的着色器,它可以根据材质ID返回表示一个独立的计算着色器的索引值shaderIndex。groupId是每个块的坐标值,每个块会被添加到一个全局的管理所有块材质ID的缓存对象tileBuffers中,后续的着色阶段就会分别根据每个shaderIndex对被添加到给着色器类型的所有块进行着色计算。

由于这里使用计算着色器以块为单位进行材质类型定义的工作,所以AtomicIncrement表示一个被申领的块,这是一个原理计算器,它保证每个计算着色器实例不会处理同一个块,这里的思路是和[a:SPU-basedDeferredShadingforBattlefield3onPlaystation3}中相似的。

由于一个块包含16×1616\times 16个像素,所以块内每个像素仍然可能使用不同的材质类型或组合,这就导致每个计算着色器内部还是会存在分支处理,尽管它比大型着色器的分支数量要小得多。这里进一步的优化是将那些块内每个像素拥有完全一样材质类型的块使用的着色器的分支计算去掉,所以这里使用了一个额外的包含无分支排列组合的查询表(branchless permutation table)。我们可以使用下面的语句修改上述第6行代码为:

bool constantTileValue = IsTileConstantValue( … );
short shaderIndex = constantTileValue? branchlessShaderTable[orReducedMaskBits]: shaderTable[orReducedMaskBits];

相对于传统的大型着色器,神秘海域4中使用的着色器管理技术可以带来20%30%20\%\sim 30\%的性能提升,如图(2)所示,场景中大部分块内每个像素都拥有相同的材质类型,即它们不包含分支处理,将这些分支去掉,以及以材质类型为单位减少大型着色器的分支处理,延迟着色的性能得到大大提升。

图(2):图中每个颜色代表不同的材质ID,它们分别使用具有较少分支甚至无分支的着色器(图片来自顽皮狗工作室)

另外,所有这些着色器的分配工作都是由游戏引擎工具自动完成,例如完成不同材质类型组合着色器的编译,以及不同块之间按材质ID进行划分都是对美术师透明的,这大大简化了对物体着色的流程以及渲染的灵活性,并大幅提升了渲染性能。同时,添加新的材质类型并不会影响到着色器管理的复杂度。