The Graphics Processing Unit

"《Real-Time Rendering 4th》笔记——第三章 图形处理器"

Posted by Ciel on April 3, 2019

The Graphics Processing Unit

这章介绍了GPU渲染管线的组成,可编程着色技术的发展过程,以及Vertex Shader、Tessellation Stage、Geometry Shader、Pixel Shader、Merging Stage、Compute Shader。

1 数据并行处理架构

  • 不同的处理器体系结构使用不同的策略来避免停滞。为了最小化延迟的影响,CPU芯片的大部分由快速本地缓存组成,内存中充满了接下来可能需要的数据。CPU可以有多个处理器,但是每个处理器都以串行方式运行代码(有限的SIMD向量处理是小例外)。CPU还通过使用诸如分支预测、指令重新排序、寄存器重命名和缓存预取等智能技术来避免停机

  • GPU的大部分芯片区域用于一组大的处理器,称为着色器内核,通常有数千个。GPU是一个流处理器,它依次处理有序的相似数据。因为一组顶点或像素有这种相似性,GPU可以大规模处理这些并行数据。另一个重要因素是,这些调用尽可能独立,它们不需要调用相邻数据信息,也不共享可写内存位置。这个规则有时会被打破,以允许新的和有用的功能,但是这种例外是以潜在的延迟为代价的,因为一个处理器可能会等待另一个处理器完成它的工作。

  • GPU针对吞吐量进行了优化,并根据数据处理的最大速率进行优化。然而,这种快速处理是有代价的。由于用于缓存和控制逻辑的芯片面积更小,每个着色器内核的延迟通常比CPU处理器遇到的延迟要高得多。

  • GPU将指令执行逻辑与数据分离,对固定数量的着色器程序同步执行相同命令,这种方式称为单指令多数据(single instruction multiple data,SIMD)。

  • 每个片段的像素着色器调用称为线程,这种类型的线程不同于CPU线程。使用相同着色程序的线程被捆绑成组,NVIDIA称为warps,AMD称为wavefronts。假设我们有2000个线程要执行。NVIDIA gpu上的warps包含32个线程。这将产生2000÷32 = 62.5 warps,这意味着分配了63个warps,其中一个warp是半空的。

  • 着色器程序的结构是影响效率的一个重要特性。一个主要因素是每个线程的寄存器使用量。着色器程序与每个线程关联所需的寄存器越多,可以驻留在GPU中的线程就越少,因此wraps也就越少。wraps 的短缺可能意味着无法通过交换减轻延迟。可使用的warp数量称为占用率。高占用率意味着有许多可用于处理的warp,因此处理器空闲的可能性较小。内存读取的频率也会影响延迟。另一个影响因素是由‘’if“语句和循环引起的动态分支。在着色器程序中遇到”if”语句,如果所有线程都求值并采用同一分支,则warp可以继续不需要考虑其他分支。然而如果一些甚至只有一个线程选择另一条路径,那么warp必须执行两个分支,丢弃每个特定线程中不需要的结果。这个问题称为线程发散,其中一些线程可能需要执行循环迭代,或者执行warp中的其他线程没有执行的“if”路径,而其他线程在此期间处于空闲状态。

\img\in-post\rtr3\3-1

原书图3.1,简化着色器执行示例。三角形的每个片元都是一个线程,组成了warp。每个warp简化显示为4个线程,实际上有32个线程。着色器程序有5条指令。四组GPU着色器处理器执行这些指令的第一个warp,直到“txr”命令检测到需要时间来获取它数据的情况。第二个warp是交换进来的着色程序的前三条指令,执行直到再次检测到停滞为止。在第三个warp交换并停滞后,交换到第一个warp并继续执行。如果此时它的“txr”命令的数据还没有返回,那么执行将真正停止,直到数据可用为止。每个warp依次完成。

2 GPU渲染管线概述

\img\in-post\rtr3\3-2

原书图3.2,GPU渲染管线的实现。绿色阶段是完全可编程的,虚线表示为可选阶段。黄色阶段是可配置的,但是不可编程,例如各种混合模式可以在合并阶段设置。蓝色的阶段是完全固定的。

  • GPU实现了第二章所描述的几何处理、光栅化、像素处理流水线。这些被划分为几个硬件阶段,具有不同程度的可操作性或可编程性(如图3.2所示)。注意这些物理阶段的划分与第二章中介绍的功能阶段有所不同。

  • 这里描述的是逻辑模型,通过API程序暴露出来。这个逻辑管线的实现取决于硬件供应商。逻辑模型可以帮助你推断性能的影响因素,但不应该将其误认为GPU实际实现管线的方式。

  • 顶点着色器是一个完全可编程的阶段,用于实现几何处理阶段。几何着色器是一个完全可编程的阶段,操作点、线或三角形上的顶点。它可以用于执行每个图元的着色操作,销毁图元或创建新图元。细分阶段和几何着色器是可选的,并不是所有GPU都支持它们,特别是在移动设备上。

3 可编程着色阶段

  • 现代着色器程序使用统一着色器(unified shader)设计。这意味着顶点、像素、几何和细分相关的着色器共享一个公共的编程模型。它们具有相同的指令集体系结构(instruction set architecture,ISA)。在DirectX中,实现该模型的处理器称为共着色器内核,具有这种内核的GPU具有统一的着色器体系结构。GPU可以根据自己的需要分配这些处理器,决定如何平衡负载。

  • 着色器使用类c语言编程,比如DirectX使用HLSL(High-Level Shading Language),OpenGL使用GLSL(OpenGL Shading Language)。HLSL可以编译成汇编语言,也称为中间语言(intermediate language,IL),以提供硬件独立性。

  • 32位单精度浮点的标量和矢量是其基本数据类型,之后也支持32位整型和64位浮点型。浮点向量通常包含位置(xyzw)、法线、矩阵、颜色(rgba)或纹理坐标(uvwq)等数据。整数通常用于表示计数器、索引或位掩码。还支持综合数据类型,如结构体、数组和矩阵。

  • 一个Draw Call会调用图形API来绘制一组图元,从而使图形管线执行并运行着色器。每个可编程着色阶段都有两种类型的输入:uniform 输入,值在一个draw call期间保持不变(但是可以在draw call间进行更改);varying 输入,数据来自三角形的顶点或者光栅化阶段。例如像素着色器可以提供一个光源的颜色作为一个uniform值,三角形表面的位置随着像素的变化而变化(varying)。纹理是一种特殊的uniform输入,它曾经是应用于表面的彩色图像,但是现在可以看作是任何存储大量数据的数组。

  • 底层虚拟机为不同类型的输入和输出提供特殊的寄存器。可用于常数的寄存器数量远远大于可用于不同输入或输出的寄存器。这是因为不同的输入和输出需要分别存储在每个顶点或像素上,统一的输入只存储一次并在一次draw call中的所有顶点或像素上重用。

\img\in-post\rtr3\3-3

原书图3.3,Shader Model 4.0下虚拟机架构和寄存器布局。每个资源旁边都显示了最大可用数量。三个数字从左到右分别是顶点、集合、像素着色器中的限制。

  • 着色语言表示出了大多数常见的操作(比如加法乘法用运算符+和*来表示)。其余的操作有为GPU优化的函数比如atan(),dot(),log()等。更复杂的操作也存在于内部函数,比如向量归一化(vector normalization)、反射(reflection)、叉乘(cross product)、矩阵转置(matrix transpose)和行列式(determinant)等。

  • 流控制指的是使用分支指令来改变代码执行流程的操作。这些指令用于实现高级语言结构如if和case语句,以及各种类型的循环。着色器支持两种类型的流控制。静态流控制是基于统一输入的值的。这意味着代码的流在调用时是常量。静态流控制的主要好处是允许在不同情况下使用相同的着色器(例如,不同数量的光源),所有的调用都采用相同的代码路径。动态流控制基于不同输入的值,这意味着每个片元可以独立地执行代码。这比静态流控制功能强大得多,但是会降低性能,特别是当着色器调用之间发生代码流变化时。

4 可编程着色和API的发展

可编程着色器框架思想可以追溯到1984年Cook的shade trees。

\img\in-post\rtr3\3-4

原书图3.4,一个简单的铜着色器和其对应的着色语言代码。

  • 80年代中后期,RenderMan着色语言根据这个可编程着色框架思想开发出来,目前仍然广泛用于电影制作的渲染中,以及其他不断发展的规范,比如Open shading Language(OSL)项目。

\img\in-post\rtr3\3-5

原书图3.5,一些API和图形硬件的发布时间表

  • 1996年10月1日,消费级图形硬件由3dfx Interactive公司推出。该硬件实现了一个固定功能的管道。在GPU原生支持可编程着色器之前,有一些通过多个渲染通道实现实时可编程着色的尝试。

  • 1999年,《雷神 3:竞技场》脚本语言是在这个领域上第一个成功广泛商用的语言。NVIDIA的GeForce256是第一个被称为GPU的硬件,它是不可编程的,但它是可配置的。

  • 2001 年,NVIDIA’s GeForce 3 发布,它是第一个支持可编程顶点着色器的GPU,向DirectX 8.0 开放,并以扩展的形式提供给OpenGL。此时着色器不允许流控制(分支),因此条件必须通过计算条件和在结果之间选择或插值来模拟。DirectX提出了着色器模型(SM)的概念,以区分具有不同着色器功能的硬件。

  • 2002年,DirectX 9.0发布,包括了Shader Model 2.0,它支持真正的可编程顶点和像素着色。类似的功能也在OpenGL下使用各种扩展实现。增加了对任意依赖纹理读取和16位浮点值存储的支持,增加了对着色器资源(如指令、纹理、寄存器)的上限,因此着色器能进行更复杂的转换。还增加了流控制,着色器长度和复杂性的增加使得编程越来越麻烦。幸而着色编程语言HLSL也随着DirectX 9.0发布。这种着色语言是微软与英伟达合作开发的。大约在同一时间,OpenGL ARB(Architecture Review Board)发布了GLSL,这是一种和OpenGL相当类似的语言。这些语言在很大程度上受到C语言的语法和设计理念的影响,并包含了RenderMan着色语言中的元素。

  • 2004年,Shader Model 3.0发布,添加了动态流控制,使得着色器更加强大。它还将可选功能进行了实现,进一步增加了资源上限,并在顶点着色器中添加了有限的纹理读取支持。

  • 2005年以及2006年,微软和索尼分别推出了新一代游戏主机Xbox 360和PLAYSTATION 3,它们都配备了Shader Model 3.0级别的GPU。2006年底,任天堂的Wii发布,是最后一款值得注意的搭载固定功能管线GPU的主机。在当时上,纯粹的固定功能管线过时很久了。着色器语言已经发展到使用各种工具来创建和管理它们了。

\img\in-post\rtr3\3-6

原书图3.6,一个用于着色器设计的可视化着色器图形系统。各种操作都封装在左边的函数库中。当函数框被选择时,右图显示各种可调节的参数。每个函数框中的输入输出互相连接得到最终结果,如图中右下角显示。

  • 2006年年底,Shader Model 4.0发布,包含于DirectX10.0中,引入了几个主要特性,比如几何着色器和流输出。Shader Model 4.0为所有着色器(顶点、像素和几何)提供了统一的编程模型,进一步增加了资源上限,并添加了对整数数据类型(包括位操作)的支持。OpenGL 3.3引入的GLSL 3.30中提供了一个类似的着色器模型。

  • 2009年,DirectX 11和Shader Model 5.0发布,添加了细分着色器(tessellation stage shaders )和计算着色器(compute shader,也称为DirectCompute)。OpenGL在4.0版本中添加了细分曲面,在4.3版本中添加了计算着色器。

  • 2013年,AMD公司引入了Mantle API。Mantle是与电子游戏开发商DICE合作开发的,其理念是去掉大部分图形驱动程序的开销,并将这种控制直接交给开发者。除了这种重构,还进一步支持有效的CPU多处理。这类新的API着重于大大减少CPU在驱动程序上花的时间。

  • 2014年,苹果公司发布了自己的低开销API Metal。Metal首次出现在iPhone 5s和iPad Air等移动设备上,除了效率之外,减少CPU使用还可以省电,这是移动设备上的一个重要因素。这个API有自己的着色语言,用于图形和GPU计算程序。

  • 2015年,微软发布DirectX 12。DirectX 12是对API的彻底重新设计,是一个更好地映射到现代GPU的架构。低开销驱动程序适用于CPU驱动程序成本导致瓶颈的应用程序,或者使用更多CPU处理器处理图形可以提高性能的应用程序。

  • 2016年,AMD将Mantle技术工作交给了 Khronos Group,后者在2016年初发布了自己的新API Vulkan。与OpenGL一样,Vulkan也支持多个操作系统。Vulkan使用了一种新的高级中间语言SPIRV,它既用于着色器表示,也用于通用GPU计算。预编译着色器是可移植的,因此可以在任何支持所需功能的GPU上使用。Vulkan也可以用于非图形GPU计算,因为它不需要显示窗口。Vulkan与其他低开销驱动程序的一个显著区别是,它适用系统广泛,从工作站到移动设备。

  • 在移动设备上通常使用OpenGL ES。“ES”代表嵌入式系统(Embedded Systems),因为这个API是为移动端开发的。当时,标准OpenGL的一些调用结构相当笨重和缓慢,并且需要支持很少使用的功能。OpenGL ES 1.0与2003年发布,是OpenGL 1.3的精简版,描述了固定功能管线。虽然DirectX的发布与支持它们的图形硬件同步,但是为移动设备开发图形支持并不是以相同的方式进行的。例如,2010年发布的第一代iPad实现了OpenGL ES 1.1。2007年,OpenGL ES 2.0规范发布,提供可编程的着色器。它基于OpenGL 2.0,但没有固定的函数组件,因此不能向后兼容OpenGL ES 1.1。OpenGL ES 3.0于2012年发布,提供了多目标渲染(render targets)、纹理压缩(texture compression)、转换反馈(transform feedback)、实例化(instancing)以及更广泛的纹理格式和模式,以及着色语言的改进。OpenGL ES 3.1增加了计算着色器,3.2增加了几何和细分着色器等功能。

  • OpenGL ES的一个分支是基于浏览器的API WebGL,通过JavaScript调用。该API的第一个版本于2011年发布,在大多数移动设备上都可以使用,因为它的功能相当于OpenGL ES 2.0。与OpenGL一样,使用扩展访问更高级的GPU特性。WebGL 2规范基于OpenGL ES 3.0。WebGL特别适合在课堂上进行特性试验或使用:

    • 它是跨平台的,适用于所有的个人电脑和几乎所有的移动设备。

    • 驱动程序由浏览器处理。即使一个浏览器不支持特定的GPU或扩展,通常另一个浏览器支持。

    • 代码是解释的,而不是编译的,开发只需要一个文本编辑器。

    • 大多数浏览器都内置了调试器,可以检查任何网站上运行的代码。

    • 程序可以通过上传到网站或Github来部署。

  • 更高级的场景图和效果库,如three.js,为各种更复杂的效果(如阴影算法、后处理效果、基于物理的渲染和延迟渲染)提供了方便的代码访问。

5 顶点着色器

  • 在顶点着色阶段之前发生了一些数据操作。比如在DirectX 中叫做输入装配(Input Assembler)的阶段,会将一些数据流组织在一起,以形成顶点和基元的集合,发送到管线。在输入装配中也支持执行实例化。这允许使用每个实例的不同数据多次绘制对象,所有这些数据都使用一个draw call。
  • 三角形网格由一组顶点表示,每个顶点与模型表面上的特定位置相关。除了位置(position)之外,还有其他可选属性如颜色(color)、纹理坐标(texture coordinates)、曲面法线(surface normals)。从数学上讲,每个三角形都有一个定义好的曲面法线。在渲染时,三角形网格通常用于表示底层曲面,顶点法线用于表示该曲面的方向,而不是三角形网格本身的方向。

\img\in-post\rtr3\3-7

原书图3.7,三角形网格(黑色带有顶点法线)表示曲面(红色)。左边平滑的顶点法线用来表示一个光滑的表面。右边中间的顶点被复制,并给出了两条法线,表示一条折痕。

  • 顶点着色器是处理三角形网格的第一阶段,它只处理传入的顶点,并提供方法来修改、创建或忽略与每个三角形顶点相关的值,如颜色、法线、纹理坐标和位置。通常顶点着色器将顶点从模型空间转换为齐次裁剪空间。顶点着色器至少要输出这个位置数据。

  • 每个传入的顶点都由顶点着色器程序处理,然后输出的值在三角形或直线上进行差值。顶点着色器既不能创建也不能销毁顶点,一个顶点生成的结果不能传递给另一个顶点。由于每个顶点都是独立处理的,GPU上任何数量的着色处理器都可以并行的应用于传入的顶点流。

  • 顶点着色器的其他用途包括:

    • 对象生成,创建一次网格,并让它根据顶点着色器变形。

    • 动画人物身体和脸部皮肤部分变形技术。

    • 程序变形,如旗子、布料或水的运动。

    • 粒子创建,通过沿管道向下发送退化网格并根据需要给它们一个区域。

    • 镜头变形、热雾、水波、页面卷曲等,通过使用整个框架内容作为纹理,在屏幕对齐的网格上进行程序化变形。

    • 通过顶点纹理获取应用地形高度。

\img\in-post\rtr3\3-8

原书图3.8,左边是一个普通的茶壶。中间是由顶点着色器执行简单的剪切操作产生的茶壶。右边是通过噪声产生的一个发生形变的茶壶。

  • 顶点着色器的输出可以以几种不同的方式使用。通常是用于每个实例的基本类型,如三角形的生成和栅格化,以及生成的单个像素片段发送到像素着色器程序进行继续处理。在一些GPU上,数据也可以发送到细分阶段或几何着色器或存储在内存中。

6 细分阶段

  • 细分阶段允许我们渲染曲面。GPU的任务是获取每个表面描述并将其转换为一组有代表性的三角形。此阶段是一个可选的GPU功能,在DirectX 11中首次可用(并且是必需的)。它在OpenGL 4.0和OpenGL ES 3.2中也受支持。

  • 使用曲面细分阶段有几个优点。曲面描述通常比提供相应的三角形本身更紧凑。除了节省内存外,该特性还可以防止CPU和GPU之间的传输成为动画角色或对象的瓶颈,因为动画角色或对象的形状每一帧都在变化。通过为给定视图生成适当数量的三角形,可以有效地呈现曲面。例如,如果一个球离摄像机很远,只需要几个三角形;近距离看,用成千上万个三角形来表示可能更好。这种控制细节级别(LOD)能力还允许应用程序控制其性能,例如,在较弱的GPU上使用较低质量的网格来保持帧速率。通常由平面表示的模型可以转换为三角形的精细网格,然后根据需要进行变形。或者可以对它们进行细分,以减少执行高消耗着色计算的频率。

  • 细分阶段通常由三个元素组成。在DirectX中,它们是外壳着色器(hull shader)、细分器(tessellator)、域着色器(domain shader)。在OpenGL中,外壳着色器对应细分控制着色器(tessellation control shader ),域着色器对应细分评估着色器(tessellation evaluation shader),它们更具有描述性,但是比较冗长。固定函数tessellator在OpenGL中称为原始生成器(primitive generator)。

  • 首先,对hull shader的输入是一个特殊的补丁(patch)原语。它由几个控制点组成,这些控制点定义了细分曲面,贝塞尔曲面或其他类型的曲面元素。hull shader有两个功能:①告诉tessellator应该生成多少个三角形,以及在什么条件下生成三角形。②它对每个控制点执行处理。hull shader 可以根据需要修改传入的补丁描述,添加或删除控制点。hull shader将控制点集以及细分控制数据输出到domain shader。

\img\in-post\rtr3\3-9

原书图3.9,细分阶段。hull shader接受一个由控制点定义的补丁。它发送细分因子(TFs)和类型到固定功能的tessellator。控制点集根据hull shader的需要进行变形,并与TFs和相关的常量一起发送到domain shader。tessellator创建一组顶点及其重心坐标。然后由domain shader处理,生成三角形网格(控制点供参考)。

  • tessellator是管线中的固定功能,仅用于细分着色器。它的任务是为domain shader添加几个新的顶点去处理。hull shader发送有关所需细分曲面类型的信息:三角形、四边形或等值线(isoline)。等值线是一组线条,有时用于头发绘制。hull shader发送的其他重要值是细分因子(tessellation factors,在OpenGL中是tessellation levels)。有两种类型:内边缘和外边缘(inner and outer edge)。两个内部因素决定了三角形或四边形内部发生了多少细分。外部因素决定每条外部边缘被分割多少。通过允许单独控制,我们可以使相邻曲面的边缘在曲面细分中匹配,而不管内部是如何细分的。匹配边缘避免了补丁遇到的裂缝或其他伪影。顶点被赋予重心坐标,这些坐标是指定表面上每个点的相对位置值

\img\in-post\rtr3\3-10

原书图3.10,改变细分因素的效果。茶壶由32块贴片组成。从左到右,内部和外部细分因子分别为1、2、4、8。

  • hull shader总是输出一组控制点的位置。它可以通过向tessellator发送一个外部细分等级(零或更低或NaN)来发出一个要丢弃补丁的信号。否则,tessellator将生成一个网格并将其发送到domain shader。hull shader 中曲面控制点用于每次调用domain shader时计算每个顶点的输出值。domain shader具有与vertex shader类似的数据流模式,来自tessellator的每个输入定点都被处理并生成相应的输出顶点。然后形成的三角形沿着管线传递下去。

  • 传递到hull shader的补丁通常很少或不修改,可以使用补丁的距离或屏幕大小来实时计算细分因子,比如地形渲染。另外,hull shader 可以简单地为应用程序计算和提供的所有补丁传递一组固定的值。tessellator执行一个复杂但固定的函数过程,生成顶点,给出它们的位置,并指定它们形成怎样的三角形或直线。为了提高计算效率,这个数据放大步骤是在着色器之外执行的。domain shader为每个点生成质心坐标,并在补丁的计算方程中使用这些质心坐标生成位置、发现、纹理坐标和其他所需的顶点信息。

\img\in-post\rtr3\3-11

原书图3.11,左边大约是6000个三角形网格。右边使用PN三角形对每个三角形进行细分和位移。

7 几何着色器

\img\in-post\rtr3\3-12

原书图3.12,几何着色器程序的几何着色器输入是某种单一类型:点,线段,三角形。最右边的两个基元包括与线相邻的顶点和三角形对象。更精细的补丁类型是可能的。

  • 几何着色器可以将图元转换为其他图元,这是细分阶段无法做到的。例如,通过让每个三角形创建线边,可以将三角形网格转换为线框视图。或者被面向观察者的四边形所替代,绘制出边缘更粗的线框。随着DirectX 10在2006年末的发布,几何着色器被添加到硬件加速图形管线中。它位于细分阶段之后,是可选的。虽然是shader model 4.0中必要的部分,但是早期着色器模型中并没有使用它。OpenGL 3.2和OpenGL ES 3.2也支持这种着色器。

  • 几何着色器的输入是单个对象及其关联的顶点。对象通常由三角形,线段或简单的点组成。 可以通过几何着色器定义和处理扩展图元。 特别地,可以传入三角形外部的三个附加顶点,并且可以使用折线上的两个相邻顶点。参见图3.12。使用directx11和着色器模型5.0,可以传递更精细的补丁,最多有32个控制点。这说明,细分阶段生成补丁更有效率。

  • 几何着色器处理这个图元并输出零个或多个顶点,这些顶点被视为点、折线 或三角形条。几何着色器本身不能生成任何输出。但可以通过编辑顶点、添加新的图元和删除来选择性地修改网格。

  • 几何着色器是为修改传入数据或复制有限数量的数据而设计的。例如:用一个面的数据同时呈现一个立方体的六个面;高效创建cascaded shadow maps ,以生成高质量的阴影;创建粒子;毛皮渲染;为阴影算法寻找对象边缘。

\img\in-post\rtr3\3-13

原书图3.12,一些几何着色器的应用。左图。使用几何着色器实现等值面曲面细分;中图,使用几何着色器和流输出进行线段细分,利用几何着色器生成广告牌用于显示闪电;右图,使用顶点和几何着色器的流输出进行布料模拟。

  • Directx11增加了几何着色器使用实例化的能力,其中几何着色器可以在任何给定的图元上运行一组次数。在OpenGL 4.0中,这由调用数指定。几何着色器还可以输出多达四个流。可以在渲染管线向下发送一个流以进行进一步处理。可以选择将所有这些流发送到流输出渲染目标。

  • 几何着色器保证按图元的输入顺序输出图元的结果。这将影响性能,因为如果多个着色器内核并行运行,则必须保存并排序结果。这和其他因素都不利于使用几何着色器在单个调用中复制或创建大量几何图形。

  • 在绘制调用(draw call)发出后,在管线中只有三个地方可以再GPU上创建工作:光栅化、细分曲面阶段和几何着色器。其中,考虑到所需的资源和内存,几何着色器的行为是最不可预测的,因为它是完全可编程的。在实践中,几何着色器通常很少使用,因为它不能很好地映射GPU的优势。在某些移动设备上,它是用软件实现的,所以在那里不鼓励使用它。

  • GPU管线的标准用途是通过顶点着色器发送数据,然后对生成的三角形进行栅格化,并在像素着色器中处理这些数据。过去,数据总是通过管线传递,中间结果无法访问。在着色器模型4.0中引入了流输出(Stream output )的思想。顶点着色器(以及可选的细分和几何着色器)处理顶点之后,可以将它们输出到流中,即除了被发送到栅格化阶段外,还有一个有序数组。事实上,可以完全关闭栅格化,然后将管道纯粹用作非图形化流处理器。以这种方式处理的数据可以通过管线返回,从而允许迭代处理。这种类型的操作可用于模拟流水或其他颗粒效应(第13.8节)。它还可以用来对模型进行剥皮,然后让这些顶点可供重用(第4.4节)。

  • 流输出只以浮点数的形式返回数据,因此它可能有明显的内存开销。流输出在图元上工作,而不是直接在顶点上工作。如果网格沿着管道发送,每个三角形都会生成它自己的三个输出顶点集。原始网格中的任何顶点共享都将丢失。由于这个原因,更典型的用法是将顶点作为点集图元通过管线发送。在OpenGL中,流输出阶段称为转换反馈(transform feedback),因为它的主要用途是转换顶点并返回它们进行进一步处理。保证图元按照输入的顺序发送到流输出目标,这意味着将保持顶点顺序。

8 像素着色器

  • 几何着色器之后会裁剪图元并光栅化。光栅化在处理步骤中相对封闭,不可编程但是可配置。遍历每个三角形以确定它覆盖的像素。光栅化器还可以粗略计算三角形覆盖每个像素的单位面积(第5.4.2节)。这些三角形中部分或完全重叠的像素称为片元。

  • 三角形顶点处的值,包括z-buffer中使用的z值,在三角形表面上为每个像素插入。这些纸被传递到像素着色器,然后像素着色器处理片元。在OpenGL中,像素着色器被称为片元着色器(更贴切)。在本书中使用像素着色器来保持一致性。沿着管线发送的点和线也会为所覆盖的像素创建片元。

  • 通过三角形执行的插值类型由像素着色器程序指定。通常我们使用透视校正插值,这样随着物体在距离上的后退,像素表面位置之间的世界空间距离会增加。例如渲染铁轨延伸到地平线,铁轨越远,铁轨之间的距离就越近,因为每一个接近地平线的连续像素移动的世界空间距离就越大。其他插值选项是可用的,如屏幕空间插值不考虑透视投影。directx11提供了对何时以及如何执行插值的进一步控制。

  • 顶点着色器程序的输出,通过三角形(或直线)插值,成为像素着色器程序的有效输入。随着GPU的发展,其他输入也被暴露。例如,shader model 3.0或更高版本中,像素着色器可以使用片元的屏幕位置。此外,三角形的哪条边是可见的也是输入标志。这一知识对在单个遍历中呈现每个三角形的正面和背面的不同材质非常重要。

  • 通常像素着色器计算和输出一个片元的颜色。还可能产生一个不透明度值,并可选的修改其深度值。在合并期间,这些值用于修改存储在像素中的内容。光栅化阶段生成的深度值可以通过像素着色器进行修改。模板缓冲区值通常不可修改,而是传递到合并阶段。DirectX 11.3允许着色器更改这个值。在shader model 4.0中,雾计算和alpha测试之类的操作从合并操作转变为像素着色器计算。

    \img\in-post\rtr3\3-14

原书图3.14,用户自定义裁剪平面。在左侧,单个水平剪裁平面切割对象。 在中间,嵌套的球体被三个平面裁剪。 在右侧,球体的表面仅在它们位于所有三个剪裁平面之外时才会被剪裁。

  • 像素着色器还具有丢弃传入片元的独特能力,即不生成输出。剪切平面功能过去是固定功能管线的一个可配置元素,后来在顶点着色器中被指定。有了片元丢弃功能,就可以在像素着色器中以任何想要的方式实现剪切。比如决定剪切值是应该“与”还是“或”。

  • 最初,像素着色器只能输出到合并阶段,以便最终显示。随着时间的推移,像素着色器可以执行的指令数大大增加。这种增加产生了多重渲染目标(multiple render targets,MRT)的思想。不同于仅将像素着色器程序的结果发送到颜色和z缓冲区,每个片元可以生成多组值并保存到不同的缓冲区,每组值称为渲染目标(RT)。渲染目标通常具有相同的x维和y维;有些api允许不同的大小,但是呈现的区域将是其中最小的。有些架构要求呈现目标具有相同的位深度,甚至可能具有相同的数据格式。根据GPU的不同,可用的渲染目标数量是4个或8个。

  • 即使有这些限制,MRT在更有效地执行渲染算法方面仍是一个强大的帮助。一个渲染遍历可以在一个目标中生成彩色图像,在另一个目标中生成对象标识符,在第三个目标中生成世界空间距离。这种能力还产生了一种不同类型的渲染管线,称为延迟着色,其中可见性和着色是在单独的通道中完成的。第一个通道存储关于对象在每个像素处的位置和材质的数据。连续通过可以有效地应用照明和其他效果。这类渲染方法在第20.1节中进行了描述。

  • 像素着色器的限制是,它通常只能在提交给它的片元位置写入渲染目标,而不能从相邻像素读取当前结果。也就是说,当一个像素着色器程序执行时,它不能将其输出直接发送到相邻的像素,也不能访问其他像素最近的更改。相反,它计算的结果只影响它自己的像素。然而,这种限制并没有听起来那么严重。在一次传递(one pass)中创建的输出图像可以让像素着色器在以后的传递中访问它的任何数据。相邻像素可以使用图像处理技术进行处理,如第12.1节所述。

  • 也有例外,像素着色器不能知道或影响邻近像素的结果。一种情况是像素着色器可以在计算梯度或导数信息时立即访问相邻片元的信息(尽管是间接的)。像素着色器提供了沿x和y屏幕轴每像素内插值的变化量。这些值对于各种计算和纹理寻址都很有用。这些梯度对于纹理过滤(第6.2.2节)之类的操作尤其重要,在这些操作中,我们想知道图像覆盖像素的程度。所有现代GPU都通过处理2×2组(称为quad)的片元来实现此功能。当像素着色器请求渐变值时,将返回相邻片段之间的差值。参见图3.15。一个统一的内核可以访问同一wrap上不同线程中保存的相邻数据,因此可以计算渐变以供像素着色器使用。这种实现的一个结果是渐变信息不能在受动态流控制影响的着色器中访问,即一个迭代次数可变的“if”语句或循环。组中的所有片段必须使用相同的指令集进行处理,这样所有四个像素的结果对于计算梯度都是有意义的。这是即使在离线渲染系统中也存在的一个基本限制。

\img\in-post\rtr3\3-15

原书图3.15,在左边,一个三角形被栅格化成四边形,每个四边形由2×2个像素组成。然后在右边显示用黑点标记的像素的梯度计算。v的值显示在四边形的四个像素位置中的每一个。注意,其中三个像素没有被三角形覆盖,但它们仍然由GPU处理,以便找到梯度。通过使用其两个四边形邻居来计算左下像素的x和y屏幕方向上的梯度。

  • DirectX 11引入了一种缓冲区类型,允许对任何位置进行写访问,即无序访问视图(unordered access view,UAV)。最初仅用于像素和计算着色器,在DirectX 11.1扩展到了所有着色器。OpenGL 4.3将其称为着色器存储缓冲区对象(shader storage buffer object,SSBO)。像素着色器以任意顺序并行运行,并且这些存储缓冲区在它们之间共享。

  • 通常需要一些机制来避免数据竞争条件(也就是数据危险),其中两个着色器程序都“竞争”以影响相同的值,可能导致任一情况。例如,如果像素着色器的两次调用,尝试在大约相同的时间添加到相同的检索值,则可能会发生错误。两者都将检索原始值,两者都会在本地修改它,但是无论哪个调用最后写入其结果都会消除其他调用的贡献,只会发生一次添加。GPU通过使用着色器可以访问的专用原子单元来避免此问题。但是,原子意味着某些着色器可能会在等待访问由另一个着色器进行读取/修改/写入的内存位置时停止。

  • 虽然原子可以避免数据风险,但许多算法需要特定的执行顺序。例如,在用红色透明三角形覆盖之前,您可能想要绘制一个更遥远的透明蓝色三角形,将红色混合在蓝色之上。一个像素可能有两个像素着色器调用一个像素,每个三角形一个,执行的方式是红色三角形的着色器先于蓝色三角形的着色器完成。在标准管线中,片元结果在处理前的合并阶段进行排序。在DirectX 11.3中引入了光栅化顺序视图(Rasterizer order views,ROVs)来强制执行顺序。这些就像UAVs;它们可以被着色器以同样的方式读写。关键的区别在于,ROVs确保数据按正确的顺序访问。这大大提高了这些着色器可访问缓冲区的实用性。例如,ROVs使像素着色器能够编写自己的混合方法,因为它可以直接访问ROV中的任何位置并对其进行写入,因此不需要合并阶段。代价是,如果检测到UAVs,像素着色器调用可能会暂停,直到处理前面绘制的三角形为止。

9 合并阶段

  • 合并阶段是将单个片元的深度和颜色(在像素着色器中生成)与帧缓冲区结合在一起的阶段。DirectX将此阶段称为输出合并; OpenGL将其称为每个样本操作。在大多数传统流水线图(包括我们自己的流程图)上,此阶段是模板缓冲区和z缓冲区操作发生的位置。如果片元是可见的,则在此阶段进行的另一个操作是颜色混合。对于不透明的表面,没有真正的混合,因为片元只是替换了之前存储的颜色。片元和存储颜色的实际混合通常用于透明和合成操作(第5.5节)。
  • 假设一个由光栅化生成的片元在像素着色器中运行,然后在应用zbuffer时被一些先前渲染的片元所隐藏。所有在像素着色器中完成的处理都是不必要的。为了避免这种浪费,许多gpu在执行像素着色器之前执行一些合并测试。片元的z-depth(以及使用的其他任何东西,例如模板或剪切)用于测试可见性。如果隐藏,则删除片元。这个功能称为early-z。像素着色器能够改变片段的z-depth或完全丢弃片段。如果发现在像素着色器程序中存在这两种操作,early-z通常不能使用,并且会被关闭,这通常会降低管线效率。DirectX 11和OpenGL 4.2允许像素着色器强制进行early-z测试,尽管有一些限制。有关early-z和其他z缓冲区优化的更多信息,请参见第23.7节。有效地使用early-z可以对性能产生很大的影响,这将在第18.4.5节中详细讨论。
  • 合并阶段占据固定功能阶段(例如三角形设置)和完全可编程着色器阶段之间的中间地带。虽然它不可编程,但其操作具有高度可配置性。特别是可以设置颜色混合以执行大量不同的操作。最常见的是涉及颜色和alpha值的乘法,加法和减法的组合,但是其他操作也是可能的,例如最小值和最大值,以及按位逻辑运算。DirectX10增加了将像素着色器中的两种颜色与framebuffer颜色混合的功能。此功能称为双源-颜色混合,不能与多个呈现目标一起使用。MRT也支持混合,DirectX10.1引入了在每个单独缓冲区上执行不同混合操作的功能。
  • ROVs和合并阶段都保证了绘制顺序,也就是输出不变性。不管生成像素着色器结果的顺序如何,API要求按照输入的顺序(对象对对象,三角形对三角形)对结果进行排序并将结果发送到合并阶段。

10 计算着色器

  • GPU不仅可以用来实现传统图形流水线,在各个领域中存在许多非图形用途,如计算股票期权的估计值和培训用于深度学习的神经网络。以这种方式使用硬件被称为GPU计算(GPU computing)。 诸如CUDA和OpenCL之类的平台用于将GPU控制为大规模并行处理器,无需实际需要或访问特定于图形的功能。 这些框架通常使用带有扩展的C或C ++等语言,以及为GPU制作的库。

  • 在DirectX11中引入的计算着色器是GPU计算的一种形式,因为它是一个不锁定在图形管道中的位置的着色器。它与呈现过程密切相关,因为它是由图形API调用的。它与顶点、像素和其他着色器一起使用。它使用与管道中使用的相同的统一着色器处理器池。它和其他着色器一样是一个着色器,因为它有一些输入数据集,可以访问用于输入和输出的缓冲区(比如纹理)。Warps和Threads在计算着色器中更明显。例如,每次调用都获得一个可访问的线程索引。还有线程组的概念,它在Directx11中由1到1024个线程组成。这些线程组由x、y和z坐标指定,主要是为了便于在着色器代码中使用。每个线程组都有一小部分内存在线程之间共享。在Directx11中,这相当于32kB。 计算着色器由线程组执行,以便保证组中的所有线程同时运行。

  • 计算着色器的一个重要优势是它们可以访问GPU上生成的数据。将数据从GPU发送到CPU会产生延迟,因此如果处理和结果能够驻留在GPU上,那么性能可以得到提高。后处理(以某种方式修改渲染的图像)是计算着色器的常见用途。共享内存意味着采样图像像素的中间结果可以与相邻的线程共享。例如,使用计算着色器来确定图像的分布或平均亮度,其运行速度是在像素着色器上执行此操作的两倍。

  • 计算着色器对于粒子系统、网格处理(如人脸动画、剔除、图像滤波、提高景深精度、阴影景深)以及任何一组GPU处理器可以处理的其他任务也很有用。

\img\in-post\rtr3\3-16

原书图3.16,计算着色器的例子。左图,一个计算着色器被用来模拟受风影响的头发,头发本身使用细分阶段渲染。中间,一个计算着色器执行快速模糊操作。右图是模拟海浪。