Chapter 3 – The Graphics Processing Unit

3.8 像素着色器

在顶点着色器,tessellation着色器以及几何着色器完成操作之后,图元将被剪裁并设置为光栅化。剪裁与光栅化是不可编程的。管线将遍历每一个三角形以决定三角形覆盖了哪些像素。光栅化还会粗略地计算覆盖了每个像素多大的区域。与三角形部分或者完全重叠的像素则被称为fragment。

三角形的顶点所存储的值,包括z-buffer中的z轴坐标值,都会基于三角形的表面进行插值。这些数据将被传输至像素着色器,而像素着色器将处理每一个fragment。在OpenGL中,像素着色器被称为fragment shader。点与线段的图元会被管线处理并生成fragment。像素着色器程序将设定三角形表面插值的类型。一般我们会使用透视纠正(perspective-correct)插值。例如,我们绘制铁轨,并延伸至地平线。随着铁轨越来越远,两根轨道的间距会越来越近。我们还能使用其他插值类型,例如屏幕空间插值。DX11支持开发者自行选择在哪个节点以及如何进行插值。

以编程的术语来说,顶点着色器的输出将被基于三角形进行插值,之后将变为像素着色器的输入。随着GPU的进化,还有其他类型的输入可以为我们使用。例如,在Shader Model 3.0中,fragment在屏幕中的位置可以在像素着色器中读取。还有三角形的哪一面是可见的也能作为像素着色器的输入。通过这一特性,我们可以在一个渲染通道中在三角形的正面或者反面绘制不同的材质。

基于像素着色器的输入,我们将计算并输出每个fragment的颜色。其也能创建一个关于透明度的值或者修正z轴的变量。在之后的合并阶段,这些变量将用于改变像素的颜色。又光栅化阶段生成的深度值也能在像素着色器中进行修正。stencil buffer中的数据一般无法修改,但可以决定是否将这一变量传入之后的合并阶段。DX 11.3允许着色器改变这一变量。雾天效果的计算,alpha测试现在一般在像素着色器中进行处理而不是将其放在合并阶段。

像素着色器还能够决定是否舍弃一个输入fragment,也就是不生成任何输出。而剪裁平面的功能之前一直作为固定功能管线中一个可设置的元素,而之后我们将其放在顶点着色器中。但是现在fragment能够被直接舍弃,我们可以在像素着色器中实现这一功能。

最初,像素着色器的输出只能传输至合并阶段,用于呈现在显示器上。但是,像素着色器中可以运行的指令随着时间不断增加。这也就给了实现多渲染对象(multiple render target,MRT)的机会。我们无需将像素着色器中每一个fragment的数据传入color buffer和z-buffer,而是将这些数据传入不同的buffer,这些buffer则被称为渲染对象(render target)。渲染对象一般拥有相同的x轴与y轴尺寸,有些API则允许不同的尺寸。有些GPU结构需要每个渲染对象拥有相同的位深度(bit depth),甚至相同的数据格式。根据GPU的不同,渲染对象的数量一般是4或者8。

即使有这这些限制,多渲染对象仍然是非常有用的工具,其能够支持我们进行更为高效的渲染算法。只通过一个渲染通道,我们就能在一个渲染对象中生成一张图像(代表颜色),在另一渲染对象中生成场景中物体的标识符,在第三个渲染对象中生成世界空间内的距离。这个能力还能产生不同类型的渲染管线,其被称为延迟着色(deferred shading),也就是说物体是否可见以及物体的着色在一个独立的渲染通道中进行处理。第一个渲染通道将存储每个像素所对应的物体的位置以及物体的材质。之后的渲染通道可以直接基于这些数据进行光照计算和其他效果的计算。我们将在第二十章中学习这一类型的渲染方法。

但像素着色器也有着一些限制,一般来说,它每次只能写入一个fragment,并不能读取周边像素的计算结果。也就是说,当像素着色器运行时,它不能将其输出直接传输给周边的像素也不能读取其他像素块的数据。但这个问题并不是特别严重,由渲染管线输出的图像将包含所有的数据,也就是说在之后的渲染通道中我们可以在像素着色器中读取这一图像,这样就能计算某个像素块周边的像素了。具体的实现,我们将在第十二章中进行学习。

但是对于我们上述提到到的规则,也有例外。像素着色器在计算gradient(梯度)或者derivative(导数)信息时可以即可读取周边像素块的信息。像素着色器拥有基于屏幕空间的相邻像素间输出的改变(前提是该数据是插值数据)。这些数据可以用于各种计算以及贴图address模式,特别是在我们进行贴图filtering时,我们需要知道图像中的每个像素覆盖了多少屏幕中的像素。所有的现代GPU通过处理一组2×2的像素来实现上述功能,如下图所示。具体请参考第三章。

上图中,左半边的三角形被光栅化为6组2×2的像素。右图则显示了,像素之间的“梯度”。

一个统一的着色器核心可以读取同一个warp中不同线程的数据,因此可以计算出像素着色器所需要的gradient数据。但这一种实现模式也导致在着色器的动态flow control中,例如if或者循环中,我们不能读取gradient信息。线程组中的所有像素块都需要以同一组指令进行处理,这样四个像素才能用于计算gradient。这个限制也同样存在于离线渲染系统中。

DX11引入了一种新的buffer类型unordered access view(UAV),这种buffer允许被写入任何位置。起初,这种类型的buffer只能用于像素着色器以及计算着色器,之后DX11.1中所有的着色器都能读取这一类型的buffer。像素着色器是以随机的顺序并行运作,所以所有的像素着色器将共享这个UAV。

一般来说,我们需要使用某些机制来避免data race condition,也就是说两个着色器程序“两次竞争”写入同一数据。例如,调用两次像素着色器,并且试着在同一时刻加上同一个值。两个像素着色器会同时修改本地值,但是最后一个写入的像素着色器总会把之前那个像素着色器的写入值抹去,也就是说结果与只运行一次像素着色器是相同的。GPU通过使用atomic来避免这一情况,这也意味着一些着色器将等待另一些着色器处理完一个内存空间(读取/写入/修正)。

虽然atomic能够避免数据的错误,但是许多算法需要一个特定的执行顺序。例如,你想要先绘制一个距离较远的透明蓝色三角形,并且使其与一个红色透明三角形相重叠,而红色三角形位于蓝色三角形的前面,之后进行融合。一个像素很有可能会调用两次像素着色器,每次都是为了绘制一个三角形;这就有可能导致红色三角形的着色器先于蓝色三角形的着色器完成。在标准的管线中,像素块的结果将先在合并阶段进行排序。DX11.3则引入了rasterrizer order view(ROV),其会对执行顺序进行强制排序。对于UAV这一类型的资源,着色器可能以相同的方式进行读写。但是,ROV则保证了数据以正确的顺序进行读取。这也增加了这一类“着色器可使用”的buffer的使用率。例如,ROV让像素着色器可以使用自己想要的形式进行融合,这样就不需要合并阶段了。但这样的操作也是有代价的,如果GPU检测到着色器的操作并未按照规定的顺序,那么像素着色器将会停止,知道顺序上优先的三角形完成处理。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据