RealTime-Rendering 第三章 自我学习和翻译

第三章 图像处理单元

从历史来看,图形加速始于在被三角形覆盖的每个像素扫描线上内插计算颜色然后展示这些值。包括访问图像数据的功能,可以将贴图应用在表面上。使用了用于内插和深度测试的硬件,提供了内置的可见性检查。 由于它们的频繁使用,这个过程主要使用专用硬件以提高性能。 连续几代中添加了渲染管线的更多部分,以及每个部分的更多功能。 专用图形硬件相对于CPU的唯一计算优势是速度,但速度至关重要。

过去二十年,图形硬件经历了难以置信的变化。1999年推出的第一款包含硬件顶点处理的消费型图形芯片(NVIDIA的GeForce256)。NVIDIA创造了图形处理单元(GPU)一词,以将GeForce 256与以前可用的仅光栅化的芯片区分开来。 在接下来的几年中,GPU从复杂的固定功能管道的可配置实现演变为高度可编程的空白板,开发人员可以实现自己的算法。 各种可编程着色器是控制GPU的主要方法。 为了提高效率,流水线的某些部分保持可配置性,而不是可编程的,但趋势是朝着可编程性和灵活性的方向发展。

GPU通过专注于一组高度可并行化的任务而获得了飞快的速度。 他们拥有专用于实现z缓冲区,快速访问纹理图像和其他缓冲区以及查找例如由三角形覆盖的像素的硅。 这些元素如何执行功能将在第23章中介绍。更重要的是,尽早了解GPU是如何实现其可编程着色器的并行性。

3.3节介绍了着色器的功能。目前需要知道的是,着色器核心是一个小型处理器,可以执行一些相对独立的任务,例如将顶点从其在世界上的位置转换为屏幕坐标,或者计算被三角形覆盖的像素的颜色。每帧有成千上万个三角形发送到屏幕,每秒可能有数十亿次着色器调用,即运行着色器程序的单独部分。

首先,延迟是所有处理器都面临的问题。访问数据需要一些时间。考虑延迟的一种基本方法是,信息离处理器越远,等待时间就越长。第23.3节更详细地介绍了延迟。存储在存储芯片中的信息将比本地寄存器中的信息花费更长的时间。 18.4.1节将更深入地讨论内存访问。关键是等待数据检索意味着处理器停止运行,这会降低性能。

3.1 数据并行架构

不同处理器使用了各种策略来避免停顿。优化CPU以处理各种数据结构和大型代码库。CPU可以有多个处理器,但是每个处理器都以串行方式运行代码,有限的SIMD矢量处理是例外,但不重要。 为了最大程度地降低延迟的影响,CPU的许多芯片都由快速本地缓存组成,内存中填充了接下来可能需要的数据。 CPU还通过使用巧妙的技术来避免停顿,例如分支预测,指令重排序,寄存器重命名和缓存预取。

GPU采取了不同的方式。大多数GPU芯片区域是专用于大量称为着色内核处理器通常由成千上万个。GPU是流处理器,依次处理相似数据的有序集合。 由于这种相似性(例如一组顶点或像素),GPU可以大规模并行地处理这些数据。另一个重要的元素是这些调用尽可能地独立,这样它们就不需要来自相邻调用的信息,并且不共享可写的内存位置。 有时会打破该规则以允许使用新的有用的功能,但是这种例外会以延迟为代价,因为一个处理器可能会等待另一个处理器完成其工作。

GPU针对吞吐量进行了优化,吞吐量的定义为可以处理数据的最大速率。 但是,这种快速处理有成本。 由于专用于高速缓存存储器和控制逻辑的芯片面积较小,因此每个着色器内核的等待时间通常比CPU处理器遇到的等待时间高得多。

假如网格已栅格化,并且两千个像素具有要处理的片段,像素着色器程序要调用两千次。想象一下如果是世界上最弱的GPU,只有一个着色处理器。它开始为两千个像素的第一个片元执行着色器程序,着色器处理器对寄存器中的值执行一些算术运算。 寄存器是本地的,可以快速访问,因此不会发生停顿。 然后,着色器处理器会执行一条指令,例如访问纹理; 例如,对于给定的表面位置,程序需要知道应用于网格的图像的像素颜色。 纹理是一个完全独立的资源,而不是像素程序本地内存的一部分,并且有时可能涉及到纹理访问。 内存提取可能需要数百到数千个时钟周期,在此期间GPU处理器不执行任何操作。 此时,着色器处理器将停止运行,等待纹理的颜色值返回。

为了让这个糟糕的GPU提高性能,在本地寄存器中为每一个片段提供一小块存储空间。现在不再需要在抓取纹理时候停顿,着色器程序可以切换执行另一个片元,两千个中的第二个。这种切换速度是很快的,除了注意第一条指令正在执行之外,在第一或第二个片元中的任何内容都不会被影响,现在执行第二个片元,和第一个片元一样,先执行一些算术函数,然后又到了纹理抓取,着色器核现在切换到第三个片元。最后所有两千个片元都这样计算。然后着色器处理器就回到第一个片元重新开始处理。这时,纹理颜色已经抓取完毕可以拿来使用了,因此着色器程序可以继续执行。处理器以相同的方式进行处理,直到遇到另一个会暂停执行的指令,或者程序完成。与着色器处理器始终专注于一个片元相比,单个片元的执行时间会更长,但是整个片段的总体执行时间将大大减少。

在这个架构中,通过使GPU持续在片元之间切换将延迟隐藏了。GPU通过将指令执行逻辑和数据区分开来,这是所谓的单指令,多数据,这种安排在固定数量的着色器程序上以锁步方式执行同一命令。 SIMD的优势在于,与使用单独的逻辑和调度单元运行每个程序相比,用于处理数据和交换的硅(和功率)要少得多。将我们的2000片元示例转换为现代GPU术语,每个片段的像素着色器调用都称为线程。这种类型的线程与CPU线程不同。它由用于着色器输入值的一点内存以及着色器执行所需的任何寄存器空间组成。

假设我们有两千个线程要执行。 NVIDIA GPU的wrap包含32个线程。这将产生 2000 / 32 = 62.5 2000/32 = 62.5 2000/32=62.5个wraps,这意味着分配了63个wraps,其中一个wraps有一半是空的。wrap的执行类似于我们的单个GPU处理器示例。着色器程序在所有32个处理器上以锁定步骤执行。当一个线程遇到内存提取时,所有线程都会同时遇到它,因为对所有线程执行相同的指令。提取表明线程wrap将停止,所有线程都在等待它们的(不同)结果。将warp换成32个线程的不同warp,然后由32个内核执行,这样不会停顿。这种交换的速度与我们的单处理器系统一样快,因为在将warp交换进出时,每个线程内的数据都不会被影响。每个线程都有自己的寄存器,每个线程都跟踪其正在执行的指令。交换新线程仅仅是将一组核指向要执行的不同线程集,没有其他开销。wrap执行或交换出去,直到全部完成。见图3.1。
在这里插入图片描述
在示例中,纹理获取内存的等待时间可能导致wrap掉出。 事实上,因为交换的成本很低,所以可以将wrap换成较短的延迟。 还有其他几种用于优化执行的技术,但wrap交换是所有GPU使用的主要延迟隐藏机制。 此过程的有效运作方式涉及几个因素。 例如,如果线程很少,那么几乎不会创建任何wrap,从而使延迟隐藏成为问题。

着色器编程的结构很大程度会影响性能。一个主要因素是每个线程使用的寄存器数量。在示例中,我们假设一次可以将2000个线程全部驻留在GPU上。与每个线程相关联的着色器程序所需的寄存器越多,线程中可以驻留的线程越少,因此wrap也就越少。wrap不足可能意味着无法通过交换来减少停顿。驻留的wrap被称为“内在”,这个数字称为占用率。高占用率意味着有许多可用于处理的wrap,因此空闲处理器的可能性较小。占用率低通常会导致性能不佳。内存访存的频率还影响需要多少延迟隐藏。 Lauritzen概述了如何通过着色器使用的寄存器数量和共享内存来影响占用率。 Wronski讨论了理想的占用率如何根据着色器执行的操作类型而变化。

另外一个因素是由 “ i f ” “if” if语句和循环引起的动态分支。假设在着色器编程中遇到了 “ i f ” “if” if语句,假设所有的线程都要计算并采用相同的分支,wrap可以不用关心其他分支继续执行。然而,如果有些线程甚至只有一个线程采用了另一条道路,wrap必须执行全部分支,从而丢弃每个特定线程不需要的结果。 这个问题称为线程发散,其中一些线程可能需要执行循环迭代或执行 “ i f ” “if” if路径,而warp中的其他线程则不这样做,从而使它们在此期间处于空闲状态。

所有GPU都实现了这些架构思想,从而导致系统受到严格的限制,但每瓦的计算能力却很大。 了解此系统的运行方式将有助于您作为程序员充分利用其提供的功能。 在以下各节中,我们讨论GPU如何实现渲染管线,可编程着色器如何工作以及每个GPU阶段的演变和功能。

3.2 GPU管线一览

GPU实现了第二章介绍的几何处理,光栅化,像素处理阶段。这些阶段被分为几个具有不同可配置性或者可编程性的硬件阶段。如图3.2所示,用不同的颜色表示了各个阶段的可配置性或可编程性。注意这些物理阶段与第2章中介绍的功能阶段有所不同。
在这里插入图片描述我们在这描述GPU的逻辑模型,通过API变成编程公开在你面前。正如第18章和第23章所讨论的那样,此逻辑管道(物理模型)的实现取决于硬件供应商。 通过将命令添加到相邻的可编程阶段,可以在GPU上执行逻辑模型中具有固定功能的阶段。 流水线中的单个程序可以分为由单独的子单元执行的元素,也可以由单独的遍历完全执行。 逻辑模型可以帮助您确定会影响性能的原因,但是不要误认为GPU实际上实现了流水线的方式。

顶点着色器是完全可编程阶段,用来实现几何处理阶段。几何着色器是操作图元的顶点(点,线或三角形)的完全可编程阶段,用来进行逐图元着色操作,销毁图元或者创建一个新的图元。 曲面细分阶段和几何着色器都是可选的,并非所有GPU都支持它们,尤其是在移动设备上。

裁剪,三角形设置,三角形遍历阶段由固定功能的硬件实现。屏幕映射被窗口和视点的设置影响,在内部形成简单的比例并重新定位。像素着色阶段是完全可编程的。尽管混合阶段不是可编程的,但是是高度可配置的,可以设置以执行各种操作。它实现了混合功能阶段,主要修改颜色,z缓冲,混合,模板等 ,或者其他与输出有关的缓冲。像素着色器的执行与合并阶段一起构成了第2章中介绍的概念性像素处理阶段。

随着时间的流逝,GPU管道已从硬编码操作演变为增加灵活性和控制能力。 可编程着色器阶段的引入是这一发展过程中最重要的一步。 下一节将介绍各个可编程阶段的通用功能。

3.3 可编程着色阶段

现代着色编程使用统一的着色设计,意味着顶点,像素,几何体或者其他有关的着色器共用一个通用编程模型。在内部具有相同的指令集体系结构(ISA)。 实现此模型的处理器在DirectX中称为“通用着色器核心”,据说具有这种核心的GPU具有统一的着色器体系结构。 这种类型的体系结构背后的想法是,着色器处理器可以在各种角色中使用,GPU可以根据需要分配它们。 例如,与每个由两个三角形组成的大正方形相比,一组带有小三角形的网格将需要更多的顶点着色器处理。 具有单独的顶点和像素着色器核心池的GPU意味着严格确定了使所有核心繁忙的理想工作分配。 使用统一的着色器内核,GPU可以决定如何平衡此负载。

基础数据类型是32位单精度浮点值标量和向量,尽管向量只是着色器代码的一部分,并且如上所述在硬件中不受支持。 在现代GPU上,本机还支持32位整数和64位浮点数。 浮点向量通常包含诸如位置(xyzw),法线,矩阵行,颜色(rgba)或纹理坐标(uvwq)之类的数据。 整数最常用于表示计数器,索引或位掩码。 还支持聚合数据类型,例如结构,数组和矩阵。

draw call调用图形API绘制一系列图元,使图形管线来执行并运行着色。每一个可编程着色阶段都有两种类型的输入:统一输入,整个draw call结束之前值都保持不变(但是在draw call之间可以变化),以及可变输入,数据来源于三角形顶点或光栅化。例如像素着色阶段可以提供光源的颜色作为统一输入,但是三角形表面的位置每个像素变化,因此是不同的。 纹理是一种特殊的统一输入,曾经一直是应用于表面的彩色图像,但是现在可以认为是任何大型数据数组。

底层虚拟机为不同类型的输入和输出提供特殊的寄存器。为统一输入提供的可用的不变的寄存器数量远远大于为可变输入或者输出提供的可用寄存器的数量。这是由于每个顶点或者像素需要分开存储可变输入和输出,因此对于需要的数量有自然的限制。 统一输入存储一次,并在draw call中的所有顶点或像素之间重复使用。 虚拟机还具有用于暂存空间的通用临时寄存器。 可以使用临时寄存器中的整数值对所有类型的寄存器进行数组索引。 着色器虚拟机的输入和输出如图3.3所示。
在这里插入图片描述
图形计算中常见的操作可以在现代GPU上有效执行。 着色语言通过 ∗ * + + +等运算符公开了这些操作中最常见的操作(例如加法和乘法)。 其余的通过内置函数公开,例如 a t a n ( ) , s q r t ( ) , l o g ( ) atan(),sqrt(),log() atan()sqrt()log()以及为GPU优化的许多其他函数。 对于更复杂的操作,也存在函数,例如向量归一化和反射,叉积,矩阵转置和行列式计算。

术语“流控制”是指使用分支指令来更改代码执行流。 与流控制相关的指令用于实现高级语言构造,例如“ if”和“ case”语句,以及各种类型的循环。 着色器支持两种类型的流控制。 静态流控制分支基于统一输入的值。 这意味着代码流在draw call中是恒定的。 静态流控制的主要好处是可以在各种不同的情况下(例如,数量不同的灯光)使用同样的着色器。 没有线程分歧,因为所有调用都采用相同的代码路径。 动态流控制基于变化的输入的值,这意味着每个片段都可以不同地执行代码。 它比静态流控制功能强大得多,但会降低性能,尤其是在着色器调用之间代码流不规律地更改的情况下。

3.4 可编程着色和API

可编程着色框架的想法可以追溯到1984年库克的着色树。一个简单的着色器及其对应的着色树如图3.4所示。RenderMan着色语言是在20世纪80年代后期根据这一思想发展起来的。它至今仍被用于电影制作渲染,以及其他不断发展的语言,如开放式着色语言(OSL)项目。

在这里插入图片描述

3.5 顶点着色器

顶点着色器是功能性管线的第一个阶段,如图3.2所示。虽然这是可直接编程控制的第一个阶段,值得注意的是在这个阶段之前还有一些数据操作。在DirectX所称的输入汇编器中,可以将一些数据流编织在一起,形成沿管道发送的顶点和原语集。例如,一个对象可以用一个位置数组和一个颜色数组来表示。输入汇编程序将通过创建具有位置和颜色的顶点来创建该对象的三角形(或线或点)。第二个对象可以使用相同的位置数组(以及不同的模型变换矩阵)和不同的颜色数组来表示它。数据表示将在16.4.5节中详细讨论。在输入汇编器中也支持执行实例化。这允许用不同的数据对一个对象进行多次绘制产生不同的实例,所有这些都是通过单个绘制调用完成的。实例化的使用在18.4.2节中有介绍。

三角形网格是由一组与模型表面特定位置相关联的顶点表示。除了位置,每个顶点还有其他相关联的可选属性,例如颜色或者贴图坐标。表面法线也是网格顶点定义的。数学上,每个三角形都有一个定义明确的表面法线,可能使用三角形的法线着色看起来更有意义。然而在渲染时,三角形网格通常用于表示底层的曲面,顶点法线用于表示曲面的方向,而不是三角形网格本身的方向。第16.3.4节将讨论计算顶点法线的方法。图3.7显示了两个三角形网格的侧视图,它们代表曲面,一个平滑,另一个有尖锐的折痕。

顶点着色器是处理三角形网格的第一个阶段。顶点着色器不能使用描述三角形形成的数据,顾名思义,它只处理传入的顶点。顶点着色器提供方法来修改,创造或忽略三角形顶点的相关值,例如颜色,法线,纹理贴图和位置。通常顶点着色程序会把顶点从模型空间转换到同构裁剪空间。至少,顶点着色器必须总是输出这个位置。

顶点着色器与前面描述的统一着色器非常相似。传入的每个顶点都由顶点着色程序处理,然后输出大量的值,这些值在三角形或直线上进行插值。顶点着色器既不能创建顶点或销毁顶点,并且由一个顶点生成的结果不能传递给另一个顶点。由于每个顶点都是独立处理的,GPU上任意数量的着色器处理器都可以并行地应用于传入的顶点流。

输入程序通常是在顶点着色器执行之前进行的。他的物理模型与逻辑模型不同。物理上,获取数据来创建一个顶点可能会发生在顶点着色器中,驱动程序会悄悄地在每个着色器前添加适当的指令,而程序员是不可见的。

后面章节会介绍几种顶点着色器的效果,如顶点混合的动画关节,和剪影渲染。顶点着色器的其他用途包括:
• 对象生成,只创建一个网格让它被顶点着色器处理。
• 动画人物的身体和脸使用皮肤和变形技术。
• 程序性变形,如旗帜、布料或水的运动。
• 粒子创建,通过发送退化(无区域)网格下的管道,并有这些被给予一个需要的区域。
• 镜头畸变、热雾、水波纹、页面卷曲和其他效果,使用整个帧缓冲的内容作为屏幕对齐网格上的纹理,进行程序变形。
• 通过使用顶点纹理获取应用地形高度字段。
使用顶点着色器做的一些变形如图3.8所示。
在这里插入图片描述

顶点着色器的输出可以用几种不同的方式使用。通常的路径是每个实例的基元,例如三角形,然后被生成和光栅化,而产生的单个像素片段被发送到像素着色程序进行继续处理。在一些GPU上,数据也可以发送到曲面细分阶段或几何着色器或存储在内存中。下面几节将讨论这些可选阶段。

3.6 曲面细分阶段

曲面细分阶段允许我们渲染曲面。GPU的任务是获取表面描述然后转换成一系列三角形进行代表。

使用曲面细分阶段有几个特点。曲面描述通常比提供与他们有关的三角形要紧凑。除了内存节省外,这个特性还可以避免CPU和GPU之间的总线成为动画角色或对象的瓶颈,因为动画角色或对象的形状在每一帧中都会发生变化。通过为给定的视图生成适当数量的三角形,可以有效地渲染表面。例如,如果一个球离摄像机很远,只需要几个三角形。近距离看,可能需要几千个三角形才有比较好的效果。这种控制细节级别的能力也允许应用程序控制它的性能,例如,在较弱的GPU上使用低质量的网格来维持帧率。通常由平面表示的模型可以被转换成精细的三角形网格,然后按照需要进行扭曲,或者可以进行细分,以减少昂贵的着色计算。

曲面细分阶段包含三个元素。用DirectX的术语,叫做外壳着色器,细分器,和域着色器。在OpenGL中,外壳着色器是细分控制着色,域着色器是细分求值阶段,虽然冗长,但是是更有描述性的。固定函数细分器在OpenGL中被称为基元生成器,这确实是它的工作。

如何指定和细分曲线和曲面会在17章详细讨论。这里我们只给每个曲面细分阶段一个简短的总结。最开始,外壳着色器的输入是一个特殊的补丁基元。这包含了几种定义细分曲面的控制点,B’ezier贴片,或其他类型的曲线元素。 首先,会告诉细分器有多少三角形生成以及三角形配置,其次,它对每个控制点执行处理。此外,可以根据需要选择性地在外壳着色器修改传入的补丁描述,添加或删除控制点。外壳着色器输出它的控制点集,连同细分控制数据,到域着色器。参见图3.9。
在这里插入图片描述细分器是管线的固定功能阶段,只和曲面细分阶段一起使用。他的任务是为域着色器添加新的顶点。外壳着色器发送关于什么类型的细分表面是想要的的细分器信息:三角形,四边形,或等值线。等值线是一组线条,有时用于头发绘制[1954]。外壳着色器会发送其他很重要的值是细分因素(在OpenGL中称为细分等级)。这是两个类型:内边和外边。内在因素决定在三角形或四边形中会发生多少细分。外在因素决定每个外部边缘被分割的程度。如图3.10所示是一个增加细分因子的例子。通过允许单独的控件,我们可以在细分中有相邻曲面的边缘匹配,而不管内部是如何细分的。匹配的边缘避免裂缝或其他阴影工件的补丁满足。顶点被分配为质心坐标(章节22.8),这些值指定了所需表面上每个点的相对位置。
在这里插入图片描述

外壳着色器总是输出一个补丁,一组控制点位置。然而,它可以通过向细分器发送一个零或更小的外部细分级别(或非数字, NaN)来表示一个补丁是否要被丢弃。否则,细分器会生成一个网格并将其发送到域着色器。域着色器调用外壳着色器的曲面控制点来计算每个顶点的输出值。域着色器有一个类似于顶点着色器的数据流模式,每个来自细分器的输入顶点都被处理并生成相应的输出顶点。形成的三角形随后沿着管道传递。

尽管这个系统听起来有点复杂,但他这种结构是很有效率的,每个着色器可以相当简单。通过外壳着色器的补丁通常很少或没有修改。这个着色器也可以使用补丁的估计距离或屏幕大小来计算动态的细分因子,比如地形渲染。另一种方法是,外壳着色器可以简单地为应用程序计算和提供的所有补丁传递一组固定的值。细分器执行一个复杂但功能固定的过程,生成顶点并给它们定位,并指定它们构成什么三角形或线。为了提高计算效率,该数据放大步骤是在着色器之外执行的。域着色器获取为每个点生成的质心坐标,并在补丁的求值方程中使用这些坐标来生成位置、法线、纹理坐标和其他需要的顶点信息。示例见图3.11。
在这里插入图片描述
在这里插入图片描述

3.7 几何着色器

几何着色器能够把图元转换到其他图元,这是细分着色器做不到的。例如,三角形网格可以转换到每个三角形都用线条描边的线框视图。或者,线条可以用面向观察者的四边形取代,这样就可以制作一个边缘较粗的线框渲染。2006年底,随着directx10的发布,几何着色器被添加到硬件加速图形管道中。它位于细分着色器的管道后,是可选阶段。

几何着色器的输入是单独的对象和它关联的顶点,对象包括三角形,线段或者一个点。扩展的图元可以在几何着色器定义和处理。特别地,可以传入三角形三个额外的顶点,并且可以使用折线上的两个相邻顶点。见图3.12。使用directx11和Shader模型5.0,你可以通过最多有32个控制点的更精细的补丁。也就是说,细分阶段的patch生成效率更高。

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

几何着色器的设计用来修改输入的数据或者进行有限的拷贝。例如,如图10.4.3所示,一种用法是产生六种转换后的数据的拷贝,同时渲染立方体贴图的六个面;也可以用来高效生成获得高质量阴影的级联阴影贴图。利用几何着色器的其他算法包括从点数据创建可变粒子,为毛皮渲染沿着轮廓挤压,以及为阴影算法寻找物体边缘。更多示例见图3.13。这些用法和其他用法将在本书的其余部分进行讨论。
在这里插入图片描述
几何着色器能保证输出的图元和输入保持相同的顺序。这会影响性能,因为假如着色器内核并行运行,结果必须被保存并重排。这个和其他的因素不利于几何着色器在一次单独的调用中复制或创建大量几何体。

在绘制调用后,在管线中只有三个地方可以在GPU上工作:光栅化,细分阶段和几何着色器。其中,当考虑资源和内存需要时几何着色器的行为是最不可预测的,因为它是完全可编程的。在实践中,几何着色器很少使用,因为它没有很好地使用到GPU的能力。在一些移动设备上,它是通过软件实现的,因此不鼓励使用它[69]。

3.7.1 输出流

GPU管道的标准使用时通过顶点着色器发送数据,然后经过光栅化生成三角形并在像素阶段处理。 过去,总是通过管道传递数据,而中间结果无法访问。 在顶点着色器(以及可选的细分和几何着色器)处理了顶点之后,除了可以发送到栅格化阶段之外,还可以将它们输出到流(即有序数组)中。 实际上,光栅化可以完全关闭,然后将管道纯粹用作非图形流处理器。可以将通过这种方式处理的数据通过管道发送回去,从而允许进行迭代处理。 如第13.8节所述,这种类型的操作对于模拟流动的水或其他粒子效果很有用。 它也可以用于为模型蒙皮,然后使这些顶点可重复使用(第4.4节)。

输出流只以浮点数的形式返回数据,因此会有明显的内存消耗。输出流不是直接在顶点上工作,而是在图元上工作。如果沿管道输出网格,则每个三角形将生成自己的三个输出顶点集。 原始网格中共享的所有顶点都将丢失。 因此,更典型的用途是仅通过管道发送顶点作为点集图元。 在OpenGL中,流输出阶段称为变换反馈,因为它的重点是变换顶点并将其返回以进行进一步处理。 保证按输入顺序将基元发送到流输出目标,这意味着将保持顶点顺序。

3.8 像素阶段

在顶点着色器,细分着色器和几何着色器执行他们的操作后,图元会被裁剪并在光栅化阶段进行设置,就和前面的章节提到的那样。流水线的这一部分在其处理步骤中是相对固定的,即不是可编程的,但在某种程度上是可配置的。 遍历每个三角形来决定是否有像素覆盖。光栅化也用来粗略计算每个三角形覆盖了多少像素(第5.4.2节)。 部分或完全重叠像素的三角形称为片段。

三角形顶点的值,包括深度缓冲中的z值,在每个像素的三角形表面上插值计算。这些值传入像素着色阶段,之后由其处理片元。在OpenGL中像素着色器称为片元着色器,我们在这本书中统一使用像素着色器。沿管线发送的点和线图元也会为所覆盖的像素创建片段。

整个三角形执行的插值类型由像素着色器程序指定。通常,我们使用透视校正内插法,以便像素表面位置之间的世界空间距离随着对象后退距离而增加。一个示例是渲染延伸到地平线的铁轨。铁轨在较远的地方间隔更近,因为每个接近像素的行进距离都越来越远。其他插值选项也可用,例如屏幕空间插值,其中不考虑透视投影。 DirectX 11可进一步控制何时以及如何执行插值。

用编程术语来说,顶点着色器程序的输出(在三角形(或线)上进行插值)有效地成为像素着色器程序的输入。 随着GPU的发展,其他输入也已公开。 例如,片段的屏幕位置可用于Shader Model 3.0及更高版本中的像素着色器。 同样,三角形的哪一侧可见是输入标志,这对于通过每个三角形的正面和背面渲染不同的材质非常重要。

有了输入,像素着色器会计算和输出片元的颜色,也可能产生不透明度值并可以选择修改它的Z值。在混合阶段,这些值可以用来修改存储在像素中的值。深度值在光栅化阶段产生,也可以在像素着色阶段修改。模板缓冲的值通常是不可修改的,而是合并到传递阶段。 DirectX 11.3允许着色器更改此值。雾计算和alpha测试等操作已从SM 4.0中的合并操作变为像素着色器计算。

像素着色器还具有丢弃传入片段(即不生成任何输出)的独特功能。图3.14所示,是一个如何使用片元丢弃的示例。剪切平面功能以前是固定功能管道中的可配置元素,后来在顶点着色器中指定。有了片段丢弃功能之后,就可以以像素着色器中所需的任何方式来实现此功能,例如确定剪切量是应该使用“与”还是“或”。
在这里插入图片描述

最初,像素着色器只能输出到合并阶段,以进行最终显示。 随着时间的推移,像素着色器可以执行的指令数量大大增加。 这种增加引起了多个渲染目标(MRT)的想法。可以为每个片段生成多组值并将其保存到不同的缓冲区中,每个缓冲区均称为渲染目标,而不是将像素着色器程序的结果仅发送到颜色和z缓冲区。 渲染目标通常具有相同的x和y维度; 有些API允许使用不同的大小,但是渲染区域将是其中最小的。 某些体系结构要求渲染目标必须具有相同的位深,甚至可能具有相同的数据格式。可用的渲染目标数量为四个或八个,这取决于GPU。

尽管有了这些限制,MRT在渲染计算时仍然是很高效的。 一次渲染通道可以在一个目标中生成彩色图像,在另一个目标中生成对象标识,而在第三个目标中生成世界空间距离。 此功能还引起了另一种类型的渲染管道,称为延迟着色,其中可见性和着色在单独的pass中完成。 第一遍存储每个像素处对象位置和材质的数据。 然后,后续的pass可以有效地施加照明和其他效果。 此类渲染方法在第20.1节中进行了描述。

像素着色器的局限性在于通常只能在传递给目标的片段位置上写入渲染目标,而不能从相邻像素读取当前结果。 也就是说,执行像素着色器程序时,它无法将其输出直接发送到相邻像素,也无法访问其他人的最新更改。 而是,它计算仅影响其自身像素的结果。 但是,此限制并没有那么严重。 一遍pass的输出图像可以在下一遍中由像素着色器访问其任何数据。 可以使用第12.1节中所述的图像处理技术来处理相邻像素。

像素着色器无法知道或影响相邻像素的结果也是有例外的。像素着色器可以在计算梯度或导数信息时立即获取相邻片段的信息(尽管是间接的)。像素着色器能够沿着屏幕坐标的X和Y插值计算每个像素的值,这些值用于各种计算和贴图寻址。这种梯度变化在纹理过滤时尤其重要,在这个操作中我们能知道一个像素覆盖了多少图像。所有现代GPU通过以2x2为一组处理片段来实现这个功能。当像素着色器请求梯度值时,会返回相邻片段的差异,如图3.15。统一内核具有访问相邻数据(保留在同一扭曲上的不同线程中)的功能,因此可以计算出用于像素着色器的梯度。此实现的一个结果是,无法在由动态流控制影响的着色器的部分中访问渐变信息,即,“ if”语句或具有可变迭代次数的循环。一组中的所有片段都必须使用相同的指令集进行处理,以便所有四个像素的结果对于计算梯度都有意义。这是一个基本的限制,甚至在常规渲染系统中也存在。
在这里插入图片描述需要某种机制来避免数据争用,即两个着色器程序争用以影响相同的值,这会导致任意的结果。例如,如果像素着色器的两次调用试图同时添加相同的值可能会发生错误。两次会检索原始值,都会在本地修改它,最后无论哪个调用将其写入结果,都会抹去另一个调用的结果,只有一个调用有效。GPU通过使用着色器可以访问的原子单元来避免这个问题。但是,原子意味着某些着色器可能在等待访问另一个着色器进行读/修改/写操作的存储位置时等待。

尽管原子性避免了数据争用,但是许多算法需要特定的执行顺序。例如,你想要先绘制一个较远的蓝色透明三角形,再绘制一个红色透明三角形覆盖他,将红色混合在蓝色上面。这可能导致一个像素的两次着色器调用,每个三角形调用一次,红色三角形着色在蓝色三角形着色之前执行。在标准管线中,片段的结果会在处理之前在合并阶段进行排序。在DirectX 11.3中引入了栅格化程序顺序视图(ROV)以强制执行顺序。这些就像无人机。着色器可以以相同的方式读取和写入它们。关键区别在于ROV确保以正确的顺序访问数据。这大大增加了这些可访问着色器的缓冲区的有用性。例如,ROV使像素着色器可以编写自己的混合方法,因为它可以直接访问和写入ROV中的任何位置,因此不需要合并阶段。代价是,如果检测到乱序访问,像素着色器调用可能会停顿,直到处理了先前绘制的三角形。

3.9 合并阶段

和2.5.2章讨论的一样,合并阶段会把每个片段的深度和颜色与帧缓冲中的合并。 DirectX将此阶段称为输出合并; OpenGL将其称为每个样本的操作。 在大多数传统的管线图(包括我们自己的管线图)上,此阶段是模板填充和Z填充操作的地方。 如果片段可见,则此阶段中发生的另一种操作是颜色混合。 对于不透明的表面,不涉及真正的混合,因为片段的颜色会简单地替换以前存储的颜色。 片段和所存储颜色的实际混合通常用于透明度和合成操作(第5.5节)。

想象一下光栅化阶段产生的一个片段通过了像素着色阶段,然后和z缓冲中之前已经渲染的片段的值进行比较发现该片元不可见。在像素着色器做的所有处理都没有必要。为了避免这种消耗,许多GPU执行在像素着色器之前执行合并测试。片元的Z值用来判断可见性。如果片元不可见就会被丢弃。这种功能称为Early-Z,像素着色器具有改变片元Z值或完全丢弃片元的能力。如果发现像素着色器程序中存在这两种类型的操作,则通常不能使用Early-Z,将其设为Off,通常会使管线效率降低。 DirectX 11和OpenGL 4.2允许像素着色器强制进行Early-Z测试,尽管有很多限制。有关Early-Z和其他z缓冲区优化的更多信息,请参见第23.7节。有效地使用Early-z会对性能产生很大的影响,这将在18.4.5节中详细讨论。

合并阶段占据了固定功能阶段(三角形设置)以及完全编着色阶段之间的中间地带。尽管不是可编程的,但是他是可高配置的。可以设置颜色混合执行大量不同的运算。最常见的是颜色和透明度值的加减乘混合运算,还有最小值最大值以及逻辑运算。DirectX 10具有混合像素着色器得到的两种颜色和帧缓冲颜色的能力,此功能称为双源颜色混合,不能与多个渲染目标一起使用。 MRT确实支持混合,DirectX 10.1引入了对每个单独的缓冲区执行不同混合操作的功能。

如上一节末尾所述,DirectX 11.3提供了一种通过ROV进行混合编程的方法,尽管这是以性能为代价的。 ROV和合并阶段均保证绘制顺序,也就是输出不变性。不论生成像素着色器结果的顺序如何,API要求都按照输入结果的顺序对结果进行排序并将其发送到合并阶段(对象按对象,三角形按三角形)。

3.10 计算着色

GPU的作用不仅仅是执行传统图形管线,还可以用于更多用途,有许多非图形用途,例如计算股票期权的估计值和训练用于深度学习的神经网络。以这种方式使用硬件称为GPU计算。诸如CUDA和OpenCL之类的平台可作为大型并行处理器来控制GPU,而无需真正的需求或无法访问图形专用功能。这些框架通常使用带有扩展功能的语言(例如C或C ++)以及为GPU制作的库。

DirectX 11中引入了计算着色器,它是GPU计算的一种形式,它是未锁定在图形管道中某个位置的着色器。它与渲染过程紧密相关,是由图形API调用的。它与顶点,像素和其他着色器一起使用。它使用与流水线中使用的统一着色器处理器池相同的池。与其他着色器一样,它具有一组输入数据,并且可以访问缓冲区(例如纹理)进行输入和输出。在计算机着色器中,扭曲和线程更明显。例如,每个调用都会获得一个可以访问的线程索引。还有一个线程组的概念,它由DirectX 11中的1到1024个线程组成。这些线程组由x,y和z坐标指定,主要是为了简化在着色器代码中的使用。每个线程组都有少量的线程共享的内存。在DirectX 11中,这等于32 kB。计算着色器由线程组执行,因此保证该组中的所有线程可以同时运行。

计算着色器的一个重要的优点是他们可以访问GPU产生的数据。把数据从GPU传到CPU会产生延迟,因此如果可以将处理和结果保留在GPU上,则可以提高性能。后处理是使用某种方法改变渲染好的图像,是计算着色器最常见的用途。共享内存意味着采样图像像素的中间结果可以与相邻线程共享。例如,已经发现使用计算着色器确定图像的分布或平均亮度的运行速度是在像素着色器上执行此操作的两倍。

计算着色器还可用于粒子系统,网格处理(例如面部动画,剔除,图像过滤,提高深度精度,阴影,字段深度,以及可以承担一组GPU处理器的任何其他任务。 Wihlidal 讨论了如何使计算着色器比曲面细分船体着色器更有效。其他用途请参见图3.16。
在这里插入图片描述至此,我们对GPU渲染管线实施的介绍结束了。 有多种方法可以使用和组合GPU功能来执行各种与渲染相关的过程。 调整利用这些功能的相关理论和算法是本书的中心主题。 现在,我们将重点放在变换和着色上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值