|
Shader的core是一个小型处理器,它执行一些相对独立的任务,比如将顶点从它在世界上的位置
转换成屏幕坐标,或者计算一个三角形覆盖的像素的颜色。每一帧都有数千或数百万个三角形被发送到屏幕上,每一秒都可能有数十亿个着色器调用, 每一个都是着色器程序的单独实例。
延迟是所有处理器都要面对的问题。访问数据需要花费一些时间。 考虑延迟的一个基本方法是,信息离处理器越远,等待的时间就越长。第 23.3 节更详细地介绍了延迟。存储在内存芯片中的信息比存储在本地寄存器 中的信息需要更长的存取时间。第 18.4.1 节更深入地讨论了内存访问。关键问题是,等待数据被检索意味着处理器停止工作,这降低了性能。
1 、 Data-Parallel Architectures
不同的处理器架构使用不同的策略来避免停顿。Cpu 经过优化,可以处理各 种各样的数据结构和大型代码库。Cpu 可以有多个处理器,但每个处理器都以基本上是串行的方式运行代码,有限的 SIMD 向量处理只是个小例外。为 了最小化延迟的影响,CPU 芯片的大部分由快速本地缓存组成,这些缓存中充满了下一步可能需要的数据。Cpu 还通过使用诸如分支预测、指令重排序、寄存器(register)重命名和缓存预取[715]等巧妙的技术来避免停顿。
Gpu 采取了不同的方法。Gpu 的大部分芯片区域都专门用于一大组处理器, 称为Shader cores,通常编号数以千计。Gpu 是一个流处理器,它依次处理相似数据的有序集。由于这种相似性----例如,一组顶点或像素----GPU 可以以大规模并行处理的方式处理这些数据。另一个重要因素是,这些调用尽可能独立,因此它们不需要来自相邻调用的信息,也不共享可写内存位置。这个规则有时会被打破以允许新的或有用的功能,但是这样的异常是以潜在的延迟为代价的,因为一个处理器可能等待另一个处理器完成它的工作。
Gpu 对吞吐量(throughput)进行了优化,定义为数据处理的最大速率。然而,这种快速处理是有成本的。由于专用于缓存内存和控制逻辑的芯片面积较少,每个Shader core的延迟通常比 CPU 处理器遇到的延迟要高得多[462]。
在单指令、多数据(SIMD)架构中,通过让 GPU 切换到另一个片段来保持忙碌,延迟是隐藏 的。Gpu 通过将指令执行逻辑与数据分离,使这种设计更进一步。它在固定数量的着色器程序上执行锁步中的相 同命令。SIMD的优点是,与使用单独的逻辑和调度单元运行每个程序相比,用于处理数据和交换的硅(和功耗)要少得多。将我们的 2000 个片段示例转换为现代 GPU 术语,每个片段的像素着色器调用称为线程(thread)。这种类型的线程与CPU thread不同。它包含一些内存用于着色器的输入值,以及着色器执行所需的任何寄存器空间。使用相同着色程序的线程被捆绑成组,NVIDIA 称之为warps,AMD 称之为wavefronts。一个warps/wavefronts被安排由一些 GPU 着色核心来执行,从 8 到 64,使用 SIMD 处理。每个线程都映射到一个 SIMD 通道。
假设我们有两千个线程要执行。NVIDIA GPU 上的warps包含 32 个threads。这产生 2000/32 = 62.5 warps,这意味着分配 63 个warps,有一个warps的一半是空的。一个 warp的执行类似于我们的单一 GPU 处理器例子。着色程序在所有 32 个处理器上以锁步执行。当遇到内存提取时,所有线程都会同时遇到它,因为对所有线程执行相同的指令。获取信号表示threads的warp将停止,所有threads都在等待它们(不同的)结果。而不是拖延时间。
warp被换成 含有32 个线程的warp,然后由 32 个核心执行。这种交换的速度和我们的单处理器系统一样快,因为当warp进行交换或交换时,每个线程中没有数据被触及。每个线程都有自己的寄存器,每个warp跟踪它正在执行的指令。在一个新的warp中进行交换只是将一组核心指向另一组要执行的线程的问题;没有其他开销。
在书上的简单示例中,为纹理获取内存的延迟可能导致warp交换。实际上,由于交换的成本非常低,waprs可以通过交换得到更短的延迟。还有其他一些技术用于优化执行[945],但是warp交换是所有 gpu 使用的主要延迟隐藏机制。这个过程如何有效运作涉及到几个因素。例如,如果有很少的线程,那么可以创建很少的warp,从而降低延迟。
shader程序的结构是影响着色效率的一个重要特征。一个主要因素是每个线程的寄存器使用量。在书上的示例中,我们假设两千个线程可以同时驻留在GPU 上。与每个线程相关联的着色程序需要的寄存器越多,线程就越少,因此可以驻留在 GPU 中的warp也就越少。缺少warp可能意味着一个池(Stall)不能通过交换来减轻。 Warps 的占用被 称为“in flight”,这个数字称为占用率(occupancy)。高占用率意味着有许多可用于处理的warp,因此空闲处理器的可能性较小。占用率低通常会导致性能低下。内存提取的频率也会影响延迟。Lauritzen[993]概述了占用率如何受寄存器数量和着色器使用的共享内存的影响。Wronski[1911,1914]讨论了理想的占用率如何根据着色器执行的操作类型而变化。
另一个影响总体效率的因素是由“if”语句和循环引起的动态分支。假设在着色程序中遇到“if”语句。如果所有的线程计算并采用同一个分支,则warp可以继续,而不需要考虑其他分支。但是,如果一些线程,甚至一个线程采用替代路径,那么warp必须执行完两个分支,丢弃每个特定线程不需要的结果[530,945]。这个问题称为线程发散(thread divergence),其中一些线程可能需要执行循环迭代或执行“if”路径,而其他warp中的线程则不需要这样做,因此在此期间它们处于空闲状态。
所有的 gpu 都实现了这些架构思想,导致系统有严格的限制,但每瓦的计算能力巨大。理解这个系统是如何运作的将帮助你作为一个程序员更有效地利用它提供的能力。在接下来的章节中,我们将讨论 GPU 如何实现渲染流水线,可编程着色器如何操作,以及每个 GPU 阶段的演变和功能。
上图,简化的着色器执行示例。一个三角形的碎片,称为threads,被收集成warps。每个warp显示为四个线程,但实际上有 32 个线程。要执行的着色程序有五个指令长。由四个 GPU 着色器组成的处理器执行这些指令进行第一次跃迁,直到在“txr”命令上检测到stall状态,这需要时间来获取数据。第二个warp被交换,着色程序的前三个指令被应用到它上面,直到再次检测到stall。在交换第三次stall并停止后,从第一个warp开始执行剩下的两个指令将继续执行并交换,但如果其“txr”命令的数据此时还没有返回,那么在这些数据可用之前,执行将真正停止。这些warp依次完成。
上图,渲染流水线的 GPU 实现。这些阶段是根据用户对其操作的控制程度进行颜色编码的。绿色阶段是完全可编程的。虚线表示可选的阶段。黄色阶段是可配置但不可编程的,例如,可以为merge阶段设置各种blend模式。蓝阶段的功能是完全固定的。
2、GPU Pipeline Overview
GPU实现了几何处理、光栅化和像素处理流水线阶段。这些被分成几个具有不同程度的可配置性或可编程性的硬件阶段。根据他们的可编程或可配置程度显示了不同阶段的颜色编码。请注意,这些物理阶段的划分与Chapter2中介绍的功能阶段有所不同。
我们在这里描述了 GPU 的逻辑模型,这个模型是通过 API 公开给程序员的。正如第 18 章和第 23 章所讨论的,这个逻辑管线(物理模型)的实现取决于硬件供应商。逻辑模型中固定功能的阶段可以在 GPU 上通过向相邻的可编程阶段添加命令来执行。管道中的单个程序可以被分解为由单独的子单元执行的元素,或者完全通过单独的传递执行。逻辑模型可以帮助您推理影响性能的因素,但是不应该把它误认为 GPU 实际实现流水线的方式。
顶点着色器是一个完全可编程的舞台,用于实现几何处理阶段。几何着色器是一个完全可编程的阶段,它工作在primitive(点、线或三角形)的顶点上。它可以用于执行每个primitive的阴影操作、销毁primitive或创建新的。曲面细分阶段和几何着色器都是可选的,并不是所有的 gpu 都支持它们,尤其是在移动设备上。
裁剪、三角形设置和三角形遍历阶段由固定功能的硬件实现。屏幕映射受窗口和视区设置的影响,内部形成一个简单的比例尺和重新定位。像素着色阶段是完全可编程的。虽然合并阶段是不可编程的,它是高度可配置的,可以执行各种各样的操作。它实现了“合并”功能阶段,负责修改颜色,z buffer,混合,stencil,和任何其他输出相关的缓冲区。像素着色器的执行和合并阶段构成了概念像素处理阶段。
随着时间的推移,GPU 流水线已经从硬编码的操作演变的更加灵活性和控制。可编程着色器阶段的引入是这一演变中最重要的一步。下一节描述各个可编程阶段的共同特性。
3、The Programmable Shader Stage
现代的着色器程序使用统一的着色器设计。这意味着顶点、像素、几何和曲面细分相关的着色器共享一个公共的编程模型。在内部,他们拥有相同的指令集架构(instruction set architecture)ISA。在 DirectX 中,实现这种模型的处理器被称为common-shader core,具有这种核心的 GPU 被称为具有统一着色器架构。这种架构背后的思想是着色处理器可以用于多种角色,GPU 可以根据自己的需要分配这些角色。例如,一组具有微小三角形的网格需要比两个三角形组成的大正方形需要更多的顶点着色处理。具有顶点和像素着色核心的独立池(separate pools)的 GPU 意味着保持所有核心忙碌的理想工作分布是严格预先确定的。有了统一的着色器核心,GPU 可以决定如何平衡这种负载。
着色器使用类似 c 的着色语言编程,比如 DirectX 的High-Level Shading Language(HLSL)和 OpenGL的OpenGL Shading Language(GLSL)。Directx 的 HLSL 可以被编译成虚拟机字节码,也称为中间语言(intermediate language,IL 或 DXIL),以提供硬件独立性。中间表示法还允许脱机编译和存储着色程序。这个中间语言被驱动程序转换为特定 GPU 的 ISA。控制台编程通常避免中间语言/安全步骤,因为系统只有一个 ISA。
基本的数据类型是 32 位单精度浮点标量和向量,尽管向量只是着色器代码的一部分,并且在上面概述的硬件中不受支持。在现代的 gpu 上,32 位整数和 64 位浮点数也得到了原生支持。浮点向量通常包含诸如位置(xyzw)、normals、矩阵、颜色(rgba)或纹理坐标(uvwq)等数据。整数通常用于表示计数器、索引或位掩码。还支持聚合数据类型,如结构、数组和矩阵。
draw call调用图形 API 来绘制一组primitives,这样就会导致绘图管线执行和运行着色器。每个可编程着色器阶段都有两种类型的输入:uniform inputs,在整个绘制调用过程中保持不变的值(但可以在绘制调用之间更改);varying inputs,来自三角形顶点或光栅化的数据。例如,像素着色器可以将光源的颜色作为一个uniform的值,并且三角形表面的位置每个像素都会改变,因此是变化的。纹理是一种特殊的均匀输入,它曾经是应用于表面的彩色图像,但现在可以认为是任何大数组的数据。
底层虚拟机为不同类型的输入和输出提供特殊的寄存器。可用于的uniforms常数寄存器(constant registers)的数目远远大于可用于varying inputs or outputs的constant registers。这是因为varying inputs or outputs需要为每个顶点或像素分别存储,因此这成为了一个自然的限制。uniform inputs一次性存储,并在draw call中的所有顶点或像素之间重复使用。虚拟机还具有通用(general-purpose)的临时寄存器(temporary registers),用于存储从头开始的空间scratch space。所有类型的registers都可以使用temporary registers中的整数值进行数组索引。着色器虚拟机的输入和输出可以在图形计算中常见的操作z这些都是在现代 gpu 上高效执行的。shader语言通过运算符(如*和+)。其余部分通过内部函数(intrinsic functions),例如,atan()、sqrt()、log()和许多其他为 GPU 优化的函数。函数也可用于更复杂的运算,如向量 normalization 和 reflection、叉积、矩阵转置和行列式计算。
术语flow control是指使用分支指令来更改代码执行的流。与流控制相关的指令用于实现高级语言结构,如“if”和“case”语句,以及各种类型的循环。着色器支持两种类型的流量控制。静态流量控制(Static flow control)分支是基于uniform inputs。这意味着代码流在 draw 调用上是常量。static flow control的主要好处是允许在各种不同的情况下使用相同的着色器(例如,不同数量的灯)。没有线程分歧,因为所有调用都采用相同的代码路径。动态流程控制是基于varying inputs,这意味着每个碎片可以以不同的方式执行代码。这比静态流控制要强大得多,但是会降低性能,特别是如果代码流在着色器调用之间发生不规则的变化。
上图,Unified的虚拟机架构(virtual machine)和registers布局,Shader Model 4.0。在每个资源旁边指出最大可用数量。用斜杠分隔的三个数字表示顶点、几何体和像素着色器的限制(从左到右)。
4、The Evolution of Programmable Shading and APIs
可编程shading框架的想法可以追溯到 1984 年库克的shade trees[287]。一个简单的着色器和它相应的树荫显示在 RenderMan 规范中[63,1804]是在 20 世纪 80 年代后期从这个想法发展起来的。直到今天,它仍然被用于电影制作渲染,以及其他不断发展的规范,例如Open Shading Language[608]。
上图,为树荫下的一个简单的铜shader,及其相应的着色器语言程序。
上图,一些API和图形硬件发布的时间线
1996 年 10 月 1 日,3dfx In-teractive 首次成功地引入了消费级图形硬件。查看今年的时间表。他们的巫术图形卡的能力,以高品质和优秀的游戏Quake导致其迅速采用。这个硬件实现了一个固定功能的流水线。在 gpu 本地支持可编程着色器之前,有几种方法尝试通过多重渲染通道实现可编程的实时着色操作。Quake III:在1999 年脚本语言Arena是第一个广泛传播的。正如在第一章开头提到的,NVIDIA 的GeForce256 是第一个被称为 GPU 的硬件,但它不是可编程的。但是,它是可配置的。
在 2001 年初,NVIDIA 的 GeForce3 是第一个支持可编程顶点着色器[1049]的 GPU,通过 DirectX8.0 和 OpenGL 的扩展公开。这些着色器是用一种类似汇编语言编写的,驱动程序在运行过程中将其转换为微代码。Directx8.0 也包括像素着色器,但是像素着色器不具备实际的可编程性ー驱动程序将支持的有限的“programs”转换为纹理混合状态,然后驱动程序将硬件“register combiners”连接在一起这些“程序”不仅长度有限(12 条指令或更少),而且缺乏重要的功能。根据对 RenderMan 的研究,Peercy 等人[1363]认为相关的纹理读取和浮点数据对于真正的可编程性至关重要。
着色器在这个时候不允许流控制(分支),所以条件句必须通过计算两个项并在结果之间选择或插值来仿真。Directx 定义了Shader Model(SM)的概念来区分具有不同着色器功能的硬件。2002 年发布了包括shader Model 2.0 在内的DirectX9.0,它具有真正可编程的顶点和像素着色器。类似的功能也可以在OpenGL 下使用不同的编程方法展示出来。支持任意相关的纹理读取和存储的16 位浮点值被添加,最终完成由 Peercy 等人确定的需求集。限制着色器资源,如指令,纹理,registers的增加,所以着色器变得更加复杂的效果。还增加了对flow control的支持。着色器日益增长的长度和复杂性使得装配编程模型越来越繁琐。幸运的是,DirectX9.0 HLSL。这款着色语言是由微软和 NVIDIA 合作开发的。大约在同一时间,OpenGLARB(架构审查委员会)发布了 GLSL,这是一种非常相似的OpenGL[885]语言。这些语言深受 c 编程语言的语法和设计哲学的影响,并且包含了 RenderMan 规范语言的元素。
Shader Model 3.0 于 2004 年推出,并增加了动态流控制,使着色器更加强大。它还将可选特性转化为需求,进一步增加了资源限制,并增加了对顶点着色器中纹理读取的有限支持。当新一代游戏机在 2005 年末(Mi-crosoft 的Xbox360)和 2006 年末(索尼电脑娱乐公司的 PLAYSTATION3 系统)推出时,它们配备了 ShaderModel3.0 级 gpu。任天堂的 Wiicon-sole 是最后一款引人注目的固定功能 gpu 之一,最初于 2006 年底发布。在这一点上,纯粹的固定功能管道早已不复存在。着色器语言已经发展到可以使用各种工具来创建和管理它们的地步。一个这样的工具的屏幕截图,使用Cook's shade tree 概念3.6节。
在可编程性的下一个大步骤也来到了 2006 年底。Shader Model 4.0,包括在directx10.0[175]中,引入了几个主要特性,如几何着色器和流输出。Shader Model 4.0 包括一套制服
上图,一个可视化着色器图形系统的设计。各种操作被封装在左侧可选择的功能框中。选中后,每个函数框都有可调参数,如右图所示。每个功能框的输入和输出相互连接,形成最终结果,如中间框的右下角所示。(来自“mentalmill,”mentalimagesinc.)的截图。
所有着色器(顶点、像素和几何)的编程模型,前面描述的统一着色器设计。进一步增加了资源限制,并增加了对 inte-ger 数据类型(包括按位操作)的支持。Opengl3.3 中引入的 GLSL3.30 提供了一个类似的着色器模型。
2009 年,DirectX11 和 Shader Model5.0 发布,增加了tessellation阶段着色器和compute shader,也称为 DirectCompute。该版本还关注于更有效地支持 CPU 多处理,这是 Sec-tion18.5 中讨论的主题。Opengl 在 4.0 版本中增加了Tessellation,并在4.3 版本中计算着色器。Directx 和 OpenGL 发展的不同。两者都为特定的版本发布设置了一定的硬件支持级别。微软控制着 DirectXAPI,因此直接与独立的硬件供应商合作,比如 AMD,NVIDIA,Intel,以及游戏开发商和计算机辅助设计软件公司,来决定要公开哪些特性。Opengl 是由硬件和软件供应商联盟开发的,由非营利性的 Khronos 集团管理。由于涉及的公司数量众多,API 特性通常在引入 DirectX 之后的某个时候出现在 OpenGL 的发行版中。然而,OpenGL 允许扩展,特定于供应商或者更通用的,允许在官方支持之前使用最新的 GPU 功能。
2013 年 AMD 推出的 Mantle API 引领了 API 接下来的重大变化。通过与视频游戏开发者 DICE 合作开发,Mantle 的想法是去除大部分图形驱动的开销,并将控制权直接交给开发者。除了这种重构,还进一步支持有效的 CPU 多处理。这类新的 api 关注于大大减少 CPU 花在驱动程序上的时间,以及更高效的 CPU 多处理器支持(第 18 章)。在Mantle开创的想法被微软采用,并在2015 年以 DirectX12 的形式发布。请注意,directx12 并不专注于显示新的GPU 功能ーdirectx11.3 显示了相同的硬件特性。这两个 api 都可以用来发送图形到虚拟现实系统,如 Oculus Rift 和 HTC Vive。然而,directx12 是 API 的彻底重新设计,一个更好地映射到现代 GPU 架构。低开销的驱动程序对于 CPU驱动程序成本造成瓶颈的应用程序非常有用,或者对于图形使用更多 CPU 处理器可以提高性能的应用程序非常有用[946]。从早期的 api 移植可能会很困难,一个初级的实现可能会导致较低的性能[249,699,1438]。
苹果在 2014 年发布了自己的低成本 API,称为 Metal。Metal 首次出现在iPhone5S 和 iPadAir 等 移 动 设 备 上 , 一 年 后 , 新 版 mac 电 脑 通 过OS X El Capitan 获得了使用权。除了效率之外,降低 CPU 使用率还能节省电力,这是移动设备上的一个重要因素。这个 API 有它自己的着色语言,图形处理器和 GPU 计算程序。
Amd 公司捐赠了它的 Mantle 工作给 Khronos 集团,Khronos 集团在 2016年初发布了自己的新 API,称为 Vulkan。与 OpenGL 一样,Vulkan 在多个操作系统上工作。Vulkan 使用了一种叫做 SPIR-V 的新型高级中间语言,它既可以用于着色器表示,也可以用于一般的 GPU 计算。预编译着色器是可移植的,因此可以在任何支持所需功能的 GPU 上使用[885]。Vulkan 也可以用于非图形 GPU 计算,因为它不需要显示窗口[946]。Vulkan 与其他低开销驱动程序的一个显著区别是,它适用于从工作站到移动设备的广泛系统。
在移动设备上,标准是使用 OpenGL ES。“ES”代表嵌入式系统,因为这个API 是为了移动设备而开发的。当时的标准 OpenGL 在一些调用结构中相当笨重和缓慢,并且需要对很少使用的功能提供支持。OpenglES1.0 发布于2003 年,是 OpenGL1.3 的精简版,描述了一个固定功能的流水线。虽然DirectX 的发布与支持它们的图形硬件同步,但是开发移动设备的图形支持并没 有 以 同 样 的 方 式 进 行 。 例 如 , 2010 年 发 布 的 第 一 代 iPad 实 现 了OpenGLES1.1。2007 年发布了 OpenGLES2.0 规范,提供了可编程的shading。它基于 OpenGL2.0,但没有fixed-function component,因此不能向后兼容 OpenGLES1.1。OpenglES3.0 在 2012 年发布,提供多个渲染目标、纹理压缩、转换反馈、实例化、更广泛的纹理格式和模式等功能,以及着色器语言的改进。3.1 增加了计算着色器,3.2 增加了几何和tessellation着色器等特性。第 23 章详细讨论了移动设备的体系结构。
OpenGL ES 的一个分支是基于浏览器的 API WebGL,通过 JavaScript 调用。该 API 的第一个版本于 2011 年发布,可用于大多数移动设备,因为它的功能相当于 OpenGLES2.0。和 OpenGL 一样,扩展访问更高级的 GPU 特性。WebGL 2 假定 OpenGLES3.0 支持。
WebGL 特别适合在课堂上进行功能或使用的试验:
- 它是跨平台的,可以在所有的个人电脑和几乎所有的移动设备上工作。
- 驱动程序批准由浏览器处理。即使一个浏览器不支持某个特定的 GPU 或扩展,通常另一个浏览器也支持。
- 代码是解释的,不是编译的,开发只需要一个文本编辑器。
- 大多数浏览器都内置了调试器,可以检查在任何网站上运行的代码。
- 例如,程序可以通过上传到网站或 Github 来部署。
更高级别的场景图和效果库,如 three.js[218],可以方便地访问各种更复杂的效果的代码,如阴影算法、后处理效果、基于物理的shading和延迟渲染。
5、The Vertex Shader
顶点着色器是函数流水线中的第一个级别。虽然这是程序员直接控制的第一个阶段,但值得注意的是,有些数据操作发生在这个阶段之前。在 DirectX 调用的输入汇编程序(input assembler)[175,530,1208]中,几个数据流可以交织在一起,形成沿管道发送的顶点和primitives的集合。例如,一个对象可以由一个位置数组和一个颜色数组表示。input assembler通过创建具有位置和颜色的顶点来创建这个对象的三角形(或线或点)。第二个对象可以使用相同的位置数组(以及不同的模型转换矩阵)和不同的颜色数组来表示。数据表示将在 16.4.5 节中详细讨论。在input assembler中还支持执行实例化(instancing)。这允许使用每个实例的一些可变数据多次绘制一个对象,所有这些操作都使用一个draw call。instancing的使用见 18.4.2节。
三角形网格由一组顶点表示,每个顶点与模型表面上的特定位置相关联。除了位置,还有其他与每个顶点相关的可选属性,如颜色或纹理坐标。表面法线定义在网格顶点以及,这可能看起来是一个奇怪的选择。数学上,每个三角形都有一个明确的表面法线,直接使用三角形的法线进行shading可能更有意义。然而,在渲染时,通常用三角形网格来表示下面的曲面,用顶点法线来表示曲面的方向,而不是用三角形网格本身。第 16.3.4 节将讨论计算顶点法线的方法。显示了两个三角形网格表示曲面,一个光滑和一个具有尖锐的折痕侧视图。
上图,三角形网格的边视图(黑色,顶点法线)表示曲面(红色)。在左边的光滑顶点法线是用来表示一个光滑的表面。在右边,中间的顶点已经被复制并且给了两个法线,代表一个折痕。
顶点着色器是处理三角形网格的第一步。数据描述什么三角形形成是不可用的顶点着色器。顾名思义,它专门处理传入的顶点。顶点着色器提供了一种方法修改、创建或忽略与每个三角形顶点关联的值,如颜色、法线、纹理坐标和位置。通常顶点着色程序将顶点从模型空间转换为齐次剪辑空间(Section 4.7节)。至少,顶点着色器必须始终输出这个空间的位置。
顶点着色器与前面描述的unified shader非常相似。每个传入的顶点都由顶点着色程序处理,然后输出一些值,这些值被插值到一个三角形或直线上。顶点着色器既不能创建也不能破坏顶点,并且由一个顶点生成的结果不能传递到另一个顶点。由于每个顶点都是独立处理的,GPU 上的任意数量的着色处理器都可以并行地应用于进入的顶点流。
输入程序集通常是在执行顶点着色器之前进行的。这是一个物理模型经常与逻辑模型不同的例子。在物理上,获取数据来创建顶点可能会发生在顶点着色器中,驱动程序会悄悄地在每个着色器前加上适当的指令,程序员是看不见的。
接下来的章节将解释几个顶点着色器效果,例如用于动画关节的顶点混合和轮廓渲染。顶点着色器的其他用途包括:
- Object generation, by creating a mesh only once and having it be deformed by the vertex shader.
- Animating character’s bodies and faces using skinning and morphing techniques.
- Procedural deformations, such as the movement of flags, cloth, or water [802, 943].
- Particle creation, by sending degenerate (no area) meshes down the pipeline and having these be given an area as needed.
- Lens distortion, heat haze, water ripples, page curls, and other effffects, by using the entire framebuffffer’s contents as a texture on a screen-aligned mesh undergo-ing procedural deformation.
- Applying terrain height fields by using vertex texture fetch [40, 1227].
使用顶点着色器进行的一些变形如下图所示。
上图,左边是一个普通的茶壶。由顶点着色程序执行的简单剪切操作生成中间图像。在右边,一个噪声函数创建了一个扭曲模型的场。 (图片由FX Composer 2 制作,英伟达提供)
顶点着色器的输出可以以几种不同的方式处理。通常的路径是为每个实例的primitives,例如,三角形的生成和光栅化,生成的单个像素片段发送到像素着色程序继续处理。在一些 gpu 的数据也可以发送到tessellation阶段或几何着色器或存储在内存。这些可选的阶段将在下面的小节中讨论。
6、The Tessellation Stage
曲面细分阶段允许我们渲染曲面。Gpu 的任务是将每个表面描述转化为一组具有代表性的三角形。这个阶段是一个可选的 GPU 特性,它最早在 directx11 中可用(并且是 directx11 所需要的)。Opengl4.0 和 OpenGLES3.2 也支持它。
使用曲面细分阶段有几个好处。曲面归属通常比提供对应的三角形本身更加紧凑。除了节省内存之外,这一特性还可以防止 CPU 和 GPU 之间的总线信息交流成为每帧形状不断变化的动画人物或对象的瓶颈。通过为给定的视图生成适当数量的三角形,可以有效地渲染曲面。例如,如果一个球离相机很远,只需要几个三角形。仔细观察,它可能用成千上万个三角形来表示最好。这种控制细节级别的能力也允许应用程序控制其性能,例如,在较弱的 gpu 上使用低质量的网格以保持帧速率。通常由平面表示的模型可以转换成三角形的精细网格,然后根据需要进行扭曲[1493],或者它们可以进行曲面细分以减少昂贵的shading计算[225]。
曲面细分阶段总是由三个要素组成。使用 DirectX 的 terminology,这些是hull shader、tessellator和domain shader。在 OpenGL 中,hull shader 是tessellation control shader和domain shader 是 tessellation evaluation shader,这是一个更详细的描述。在 OpenGL 中,fixed-function tessellator称为 primitive generator,正如我们将要看到的,它确实是这样做的。
上图,曲面细分阶段。hull shader在一个patch定义的contro points。它将tessellation factors(TFs)和类型发送到fixed-function tessellator。控制点集按照hull shader的要求进行转换,并与 TFs 和相关的patch constants一起发送到domain shader。Tessellator 创建一组顶点以及它们的质心坐标(barycentric coordinates)。这些在domain shader 进行处理,生产三角形网格(控制点显示为参考)。
--将vertex shader 处理完的顶点传入hull shader 这步成为input patch,hull shader 将TFs和type传给tessellator进而生成一组顶点以及它们的质心坐标,hull shader还会将control points set按照hull shader的要求进行转换,并与 TFs 和相关的patch constants一起发送到domain shader。之后在domain shader 将tessellator生成的一组顶点和hull shader传入的那些要求进行处理,生成所需的三角形mesh。
--patch description,根据需要添加或删除控制点。hull shader输出-把它的控制点集, 随着tessellation控制数据,最后到domain shader。
tessellator是一个固定的功能阶段在管道,只用于曲面细分着色器。它的任务是为域着色器添加几个新的顶点来进行处理。hull shader发送surface信息,哪种类型的surface是理想的:三角形,四边形,或等值线。等值线是一组线条,有时用于头发渲染[1954]。其他重要的值发送的hull shader是tessellation factors(在 OpenGL 是tessellation levels)。这些都是两种类型:内部和外部边缘。这两个内部因素决定有多少镶嵌发生在三角形或四边形。外部因素决定了每个外部边缘分裂的程度(17.6节)。增加tessellation factors的一个例子显示在下图。通过允许单独的控件,我们可以有相邻曲面的边缘匹配tessellation,不管内部如何tessellation。匹配的边缘避免裂缝或其他shading的斑块交汇。这些质心坐标被分配到不同的顶点上(22.8 节),这些值指定了期望曲面上每个点的相对位置。
上图,改变tessellation factors的效果。犹他州的茶壶由 32 个小块组成。内部和外部tessellation factors,从左到右,是 1,2,4,和 8。(图片由 Rideout 和 VanGelder 生成[1493]。)
hull shader总是输出一个patch,一组控制点位置。但是,它可以通过向tessellator 发送一个零或更少的外部镶嵌级别(或非数字 not-a-number,NaN)来发出将丢弃patch的信号。否则,tessellator 生成一个网格并将其发送到domain shader。每次调用domain shader都使用来自hull shader的曲面控制点来计算每个顶点的输出值。hull shader具有类似于顶点着色器的数据流模式,处理来自该着色器的每个输入顶点并生成相应的输出顶点。形成的三角形然后通过管道传递下去。
虽然这个系统听起来很复杂,但是为了提高效率,它是以这种方式构建的,而且每个着色器都可以相当简单。patch通过一个hull shader往往会经历很少或没有修改。这个着色器也可以使用patch的估计距离或屏幕大小来动态计算tessellation factors,比如地形渲染[466]。或者,hull shader可以简单地为应用程序计算和提供的所有patch传递一组固定的值。Tessellator 执行一个复杂但固定的函数过程,生成顶点,给出它们的位置,并指定它们形成的三角形或线条。这个数据放大步骤在着色器之外执行,以提高计算效率[530]。domain shader获取为每个点生成的质心座标,并在patch的评估方程中使用这些来生成所需的位置、法线、纹理坐标和其他顶点信息。看一个例子。
上图,左边是大约 6000 个三角形的底层网格。在右边,利用 PN 三角剖分对每个三角形进行tessllation和移位。(图片来自 NVIDIASDK11[1301]样本,由英伟达提供,模型来自 Metro2033by4AGames。)
7、The Geometry Shader
几何着色器可以将primitives转换成其他primitives,而这是tessellation阶段无法做到的。例如,一个三角形网格可以转换成一个线框视图通过每个三角形创建线边缘。另外,线条可以被面向viewer的四边形代替,这样就可以制作出边缘较厚的线框渲染图。2006 年底,几何着色器随着 DirectX10 的发布被添加到硬件加速的绘图管线中。它位于tessellation shader 后面,其使用是可选的。虽然这是着色器模型 4.0 所需要的部分,但在早期的着色器模型中并没有使用。Opengl3.2 和 OpenGLES3.2 也支持这种类型的着色器。
几何着色器的输入是单个对象及其相关联的顶点。物体通常由一个三角形、一条线段或一个点组成。extended primitives可以定义和处理的几何着色器。特别是,三角形外部的三个附加顶点可以传入,并且可以使用折线上的两个相邻顶点。看下图。使用 directx11 和 Shader Model5.0,你可以通过更精细的patch,最多 32 个控制点。也就是说,tessellation阶段是更有效的patch生成[175]。
上图,几何着色器输入的几何着色器程序是一些单一类型:点,线段,三角形。最右边的两个primitives包括邻近直线和三角形对象的顶点。更复杂的patch类型也是可以的。
几何着色器处理这个primitive并输出零个或多个顶点,这些顶点被视为点、多边形线或三角形条。请注意,几何着色器根本不能生成输出。通过这种方式,可以通过编辑顶点、添加新的primitives和删除其他primitives来有选择地修改网格。
几何着色器是为修改传入数据或制作有限数量的副本而设计的。例如,一种用途是生成六个经过转换的数据副本,以同时呈现立方体地图的六个面;请参阅第 10.4.3 节。它还可以用于有效地创建级联阴影图,用于高质量的阴影生成。其他利用几何着色器优势的算法包括从点数据创建可变大小的粒子、沿着轮廓挤压鳍以进行毛皮渲染,以及为阴影算法寻找物体边缘。看看下图的更多的例子。这些和其他用途将在本书的其余部分中讨论。
上图,几何着色器(GS)的一些应用。在左边,metaball 等表面曲面细分是在飞行中使用的 GS。在中间部分,利用 GS 和源流进行线段的分形细分,利用 GS生成广告牌以显示闪电。在右边,布料模拟是使用顶点和几何着色器与流出。(Images from NVIDIA SDK 10 [1300] samples, courtesy of NVIDIA Corporation.)(图片来自 NVIDIASDK10[1300]样本,由英伟达提供)
Directx11 增加了几何着色器使用实例化的能力,其中几何着色器可以在任何给定的primitives上运行一组次数[530,1971]。在Opengl4.0 这是通过调用计数指定的。几何着色器也可以输出多达四个流。一个流可以沿着呈现管道发送以进行进一步处理。可以选择将所有这些流发送到流输出呈现目标。
几何着色器保证以与输入相同的顺序输出primitivs的结果。这会影响性能,因为如果多个着色器核心并行运行,结果必须保存和排序。这个因素和其他因素不利于使用几何着色器在一个调用中复制或创建大量的几何图形[175,530]。在绘制调用发出之后,在管道中只有三个地方可以在 GPU 上创建工作:栅格化、镶嵌阶段和几何着色器。其中,在考虑资源和内存需求时,几何着色器的行为是最难预测的,因为它是完全可编程的。在实践中,几何着色器通常看不到什么用处,因为它不能很好地映射到 GPU 的优势。在一些移动设备上,它是通过软件实现的,所以它的使用在那里是被积极劝阻的[69]。
1.Stream Output
Gpu 流水线的标准用途是通过顶点着色器发送数据,然后对生成的三角形进行光栅化,并在像素着色器中处理这些三角形。过去,数据总是通过管道传递,中间结果无法访问。在 ShaderModel4.0 中引入了流输出的概念。在顶点被顶点着色器(以及tessellation和几何着色器)处理之后,除了被发送到光栅化阶段之外,这些顶点可以被输出到一个流中,也就是一个有序的数组。实际上,栅格化可以被完全关闭,然后流水线被用作纯粹的非图形流处理器。数据以这种方式处理的数据可以通过流水线发送回来,从而允许迭代处理。这种类型的操作可以用于模拟流动的水或其他粒子效应,如第 13.8 节所讨论的。它还可以用于对模型进行蒙皮处理,然后使这些顶点可重用(Section 4.4)。
流输出仅以float的形式返回数据,因此可能会有显著的内存开销。流输出在primitives上工作,而不是直接在顶点上。如果网格沿着管道发送,每个三角形生成自己的三个输出顶点集。原始网格中的任何顶点共享都将丢失。出于这个原因,更典型的用法是将顶点作为点集primitive通过管道发送。在 OpenGL中,流输出阶段称为转换反馈(transform feedback),因为它的主要用途是转换顶点并返回这些顶点进行进一步处理。primitive保证按照输入的顺序发送到流输出目标,这意味着顶点顺序将得到维护[530]。
8、The Pixel Shader
在顶点、曲面细分和几何着色器执行它们的操作之后,primitive被裁剪并设置为光栅化,正如前一章所解释的那样。管道的这一部分在其处理步骤中是相对固定的,也就是说,不符合语法,但是可以进行某种程度的配置。遍历每个三角形以确定它所覆盖的像素。rasterizer还可以粗略地计算三角形覆盖每个像素的单元区域(Section 5.4.2)的大小。三角形中部分或完全重叠于像素的这一部分称为片元(fragment)。
三角形顶点上的值,包括在 z 缓冲区中使用的 z 值,被插值到三角形表面的每个像素上。这些值传递给像素着色器,然后由像素着色器处理片段。在OpenGL 中,像素着色器被称为片段着色器,这可能是一个更好的名字。我们使用“像素着色器”在整个本书的一致性。沿着管道发送的点和线primitive也会为覆盖的像素创建片段。
三角形执行的插值类型由像素着色程序指定。通常我们使用透视校正插值,这样当一个物体在世界空间距离越来越远时,像素表面会显示的越来越小近大远小(道理我都懂,但鸽子为什么这么大)。一个例子是绘制延伸到地平线的铁路轨道。铁轨枕木间距离越近,他们在图像中的距离越远。还有其他插值选项,例如屏幕空间插值,这里没有考虑透视投影。Directx11 进一步控制了插补的执行时间和方式[530]。
在编程术语,顶点着色程序的输出,插值跨三角形(或直线),有效地成为像素着色程序的输入。随着 GPU 的发展,其他输入已经暴露出来。例如,片段的屏幕位置对于 ShaderModel3.0 以及更高版本的像素着色器是可用的。还有三角形的哪一边是一个输入标志,一般是通过三角形顶点构建的顺序是逆时针还是顺时针判断,OpenGL默认逆时针为正面。这些知识对于在一次传递中在每个三角形的正面和背面呈现不同的材质非常重要。
有了输入,像素着色器通常会计算和输出片段的颜色。它还可能产生不透明度值,并可选择修改其 z 深度。在合并过程中,这些值用于修改存储在像素上的内容。在光栅化阶段生成的深度值也可以通过像素着色器修改。模板缓冲区值通常是不可修改的,但是它会被传递到 merge 阶段。Directx11.3 允许着色器改变这个值。在 SM4.0[175]中,诸如雾计算和 alpha 测试等操作已经从合并操作转变为像素着色计算。
像素着色器还具有丢弃传入片段的独特能力,也就是说,不生成输出。如何使用片段丢弃的一个示例如下图所示。裁剪平面功能过去是固定功能管道中的可配置元素,后来在顶点着色器中指定。随着片段丢弃的可用,这个功能可以在像素着色器中以任何想要的方式实现,比如决定clipping volumes应该是“AND”还是“OR”合并在一起。
上图,用户定义剪切平面。在左侧,一个单独的水平剪切平面将对象切片。在中间,曲面细分的球体由三个平面剪切而成。在右边,球体的表面只有在它们都在三个clip planes之外时才会被clipped。(摘自 three.js 示例 webgl clipping 和webgl clipping intersection[218]。)
最初像素着色器只能输出到合并阶段,以便最终显示。随着时间的推移,像素着色器可以执行的指令数量大大增加了。这种增加产生了多重渲染目标(multiple render targets , MRT)的想法。而不是发送结果的像素着色器的程序只是颜色和 z 缓冲区,多套值可以生成的每个片段,并保存到不同的缓冲区,每个称为渲染目标。渲染目标通常具有相同的 x 维和 y 维;有些 api 允许不同的大小,但渲染区域将是其中最小的。有些体系结构要求呈现目标具有相同的位深度,甚至可能具有相同的数据格式。根据 GPU 的不同,渲染目标的数量可以是四个或者八个。
即使有这些限制,MRT 功能是一个强大的援助,在执行渲染算法更有效。一次渲染过程可以在一个目标中生成彩色图像,在另一个目标中生成物体标识符,在第三个目标中生成世界空间距离。这种能力也产生了一种不同类型的渲染管道,称为延迟着色渲染管道(deferred shading),在这种管道中可见性和阴影是分开进行的。第一次传递在每个像素处存储关于对象的位置和材质的数据。连续的传递可以有效地应用照明和其他效果。这类呈现方法在第 20.1 节中进行了描述。
像素着色器的局限性在于,它通常只能将它的片段位置写入渲染目标,而不能从邻近的像素读取当前结果。也就是说,当一个像素着色程序执行时,它不能直接将输出发送到邻近的像素,也不能访问其他像素最近的变化。相反,它计算的结果只影响自己的像素。然而,这种限制并不像听起来那么严重。在一次传输中创建的输出图像可以在以后的传输中让像素着色器访问它的任何数据。邻近的像素可以使用图像处理技术进行处理,详见第12.1 节。
像素着色器不能知道或影响邻居像素结果的规则有例外。一种是像素着色器可以在计算梯度或导数信息期间立即访问相邻片段的信息(尽管是间接的)。像素着色器提供了沿 x 和 y 屏幕轴每个像素的任何间隔值变化的数量。这些值对于各种计算和纹理寻址非常有用。这些渐变对于诸如纹理滤镜变换(Section 6.2.2)这样的操作特别重要,我们想知道一个图像覆盖了像素的多少。所有现代的 gpu都通过处理 2 x 2 个片段的组来实现这一特性,称为 quad。当像素着色器请求渐变值时,将返回相邻片段之间的差值。看下图。一个unified core可以访问相邻的数据(保存在同一个warp上的不同thread中),因此可以计算梯度,用于像素着色器。这种实现的一个结果是梯度信息无法在受动态流控制影响的部分着色器中访问,即“if”语句或迭代次数可变的循环。组中的所有片段必须使用相同的指令集进行处理,以便所有四个像素的结果对计算梯度有意义。这是甚至在离线渲染系统中也存在的一个基本限制[64]。
上图,在左边,一个三角形被栅格化成四边形,2 x 2 个像素。用黑点标记的像素的梯度计算如右图所示。V 的值显示为四边形中四个像素位置的每个位置。请注意,三个像素没有被三角形覆盖,但是它们仍然被 GPU 处理,因此可以找到渐变。X 和 y 屏幕方向上的梯度是通过使用左下角的两个quad邻居计算出来的。
Directx11 引入了一种缓冲类型,允许对任何位置进行写访问,即无序访问视图(unordered access view,UAV)。最初只针对像素和compute shader,UAVs的访问扩展到了 DirectX 11.1[146]中的所有着色器。Opengl4.3 将其调用为着色器存储缓冲区对象(shader storage buffer object , SSBO)。这两个名字都有各自的描述方式。像素着色器以任意顺序并行运行,并且这个存储缓冲区在它们之间共享。
通常需要一些机制来避免数据竞争条件data racec condition(也称为数据危险data hazard),在这种条件下,两个着色器程序都可能“racing”影响相同的值导致了武断的结果。例如,如果一个像素着色器的两次调用试图(比如说)在大约同一时间添加相同的检索值,则可能发生错误。两者都会检索原始值,都会在本地修改它,但是最后一个写结果的调用会消除另一个调用的贡献ーー只会有一个添加。Gpu 通过使用着色器可以访问的专用原子单位来避免这个问题。然而,原子意味着一些着色器在等待访问另一个着色器读/修改/写入的内存位置时可能会失败。
虽然原子可以避免数据风险,但许多算法需要特定的执行顺序。例如,您可能希望绘制一个更远的透明蓝色三角形,然后用红色透明三角形覆盖它,在蓝色之上混合红色。一个像素可以对一个像素有两个像素着色器调用,每个像素对应一个三角形,以红色三角形的着色器在蓝色之前完成的方式执行。在标准管道中,fragment结果在合并阶段进行排序,然后再进行处理。在directx11.3 中引入了光栅化订单视图(Rasterizer order views,ROVs),以强制执行命令。这些就像UAVs;它们可以被着色器以同样的方式读写。关键的区别在于,ROVs 保证以正确的顺序访问数据。这大大增加了这些着色器可访问缓冲区的有用性[327,328]。例如,ROVs 使像素着色器可以编写自己的混合方法,因为它可以直接访问和写入 ROV 中的任何位置,因此不需要合并阶段[176]。代价是,如果检测到无序访问,像素着色器调用可能会停止,直到处理完前面绘制的三角形为止。
9、The Merging Stage
正如Section 2.5.2所讨论的,合并阶段是将单个片段(在像素着色器中生成)的深度和颜色与帧缓冲区结合在一起的阶段。DirectX 将这个阶段称为输出合并(output merger);OpenGL 将其称为每个样本操作(per-sample operations)。在大多数传统的流水线图(包括我们自己的流水线图)中,这个阶段是进行模板缓冲区和 z 缓冲区操作的地方。如果片段是可见的,在这个阶段发生的另一个操作是颜色混合。对于不透明的表面,没有真正的混合涉及,因为片段的颜色只是替换以前存储的颜色。片段和存储颜色的实际混合通常用于透明和合成操作(Section 5.5)。
想象一下,通过光栅化生成的片段在像素着色器中运行,然后在应用 z 缓冲区时被先前渲染的片段隐藏起来。在像素着色器中完成的所有处理都是不必要的。为了避免这种浪费,许多 gpu 在执行像素着色器之前执行一些合并测试[530]。片段的 z-depth(以及其他任何正在使用的东西,比如模板缓冲区或scissoring)用于测试可见性。如果隐藏,则对片元进行剔除。这个功能称为early-z[1220,1542]。像素着色器能够更改片段的 z 深度或者完全丢弃片段。如果在像素着色程序中发现类似的操作,那么 early-z 通常不能使用并被关闭,这通常会降低流水线的效率。Directx11 和 OpenGL4.2 允许像素着色器强制执行 early-z 测试,尽管有一些限制[530]。有关 early-z 和其他 z 缓冲区优化的更多信息,请参见第 23.7 节。有效地使用 early-z 可以对性能产生很大的影响,这将在第 18.4.5 节中详细讨论。
合并阶段位于固定功能阶段(如三角形设置)和完全可编程着色器阶段之间的中间地带。虽然它不是可编程的,但是它的操作是高度可配置的。特别是可以设置色彩混合来执行大量不同的操作。最常见的是涉及颜色和 alpha 值的乘法、加法和减法的组合,但也可以进行其他操作,如最小值和最大值,以及按位逻辑操作。Directx10 添加了将像素着色器中的两种颜色与 framebuffer 颜色混合的功能。这种能力称为双源颜色混合(dualsource-color blending),不能与MRT一起使用。MRT 在其他方面支持混合,而 directx10.1 引入了在每个单独的缓冲区上执行不同混合操作的能力。
正如上一节末尾所提到的,directx11.3 提供了一种通过 ROVs 使混合可编程的方法,尽管在性能上有一定的代价。ROVs和合并阶段都保证了绘制顺序,也就是输出不变性。无论生成像素着色器结果的顺序如何,API 都要求结果按照输入、对象和三角形的顺序排序并发送到合并阶段。
10、The Compute Shader
图形处理器可以用来做的不仅仅是实现传统的绘图管线。在计算股票期权的估计价值和用于深度学习的训练神经网络等各种领域中,有许多非图形的用途。这种使用硬件的方式称为 GPU computing。像 CUDA 和 OpenCL 这样的平台被用来控制 GPU 作为一个巨大的并行处理器,没有真正的需要或访问图形特定的功能。这些框架通常使用带有扩展的 c 或 c++等语言,以及为 GPU 设计的库。
在 DirectX11 中引入的compute shader是 GPU 计算的一种形式,因为它是一个着色器,不会被锁定在绘图管线中的某个位置。它与呈现过程密切相关,因为它是由图形 API 调用的。它与顶点、像素和其他着色器一起使用。它使用与管道中使用的相同的统一着色处理器池。它与其他着色器一样,具有一些输入数据集,可以访问用于输入和输出的缓冲区(如纹理)。warp和thread在计算着色器中更加明显。例如,每个调用都会获得一个它可以访问的线程索引。还有一个线程组的概念,它由 directx11 中的 1 到 1024 个线程组成。这些线程组由 x 坐标、y 坐标和 z 坐标指定,主要是为了在着色器代码中简单使用。每个线程组都有少量内存,这些内存在线程之间共享。在 directx11 中,这相当于 32kb。计算着色器是由线程组执行的,因此组中的所有线程都保证能够并发运行[1971]。
compute shader的一个重要优点是它们可以在 GPU 上访问数据。从 GPU 向CPU 发送数据会引起延迟,因此如果处理和结果能够保持在 GPU[1403]上,性能可以得到提高。后期处理(以某种方式修改渲染图像)是计算着色器的一个常见用途。共享内存意味着来自采样图像像素的中间结果可以与邻近的线程共享。例如,使用计算着色器来确定图像的分布或平均亮度被发现运行速度是在像素着色器[530]上执行此操作的两倍。
计算着色器对粒子系统、网格处理(如人脸动画[134]、剔除[1883,1884]、图像过滤[1102,1710]、提高深度精度[991]、阴影[865]、景深[764])以及任何其他可以使用 GPU 处理器的任务也很有用。威利达尔[1884]讨论了如何计算着色器可以比tessellation hull shader更有效率。看看下图的其他用途。
上图,计算着色器例子。在左边,计算着色器是用来模拟头发受风影响,头发本身呈现使用tessellation阶段。在中间,计算着色器执行快速模糊操作。在右边,海浪是模拟的。(图片来自 NVIDIASDK11[1301]样本,由英伟达提供)
这就结束了我们对 GPU 渲染管道实现的回顾。有许多方法可以使用和组合 gpu 函数来执行各种与呈现相关的过程。为了利用这些能力而调整的相关理论和算法是本书的中心主题。我们的焦点现在移动到transforms和shading。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|