Real-Time Rendering 4th 译文《三 图形处理单元》

2024-01-23 01:30

本文主要是介绍Real-Time Rendering 4th 译文《三 图形处理单元》,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第三章 图形处理单元

从历史上来看,图形学的加速发展是与处理三角形重叠的像素扫描线上进行颜色插值,然后显示这些值开始的,其中包括访问能够让纹理应用到物体表面的图像数据的能力,其中还添加了用于插值和测试z深度的硬件,由于它们的频繁使用,因此将此类过程用于专用硬件以提高性能。连续几代硬件中添加了渲染管线的更多部分,以及每个部分的更多功能。专用图形硬件相对于CPU的唯一计算优势是速度,但速度至关重要。

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

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

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

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

3.1 数据并行结构

不同的处理器体系结构使用各种策略来避免停顿。对CPU进行了优化,以处理各种数据结构和大型代码库。CPU可以具有多个处理器,但是每个CPU都以串行方式运行代码,尽可能少的SIMD(单指令多数据结构)把异常控制到最小范围。为了最大程度地减少延迟的影响,CPU的许多芯片都由快速本地缓存组成,这些缓存中填充了下一步可能需要的数据。CPU还通过使用诸如分支预测,指令重新排序,寄存器重命名和缓存预取之类的巧妙技术来避免停顿。

GPU的方案则不同。GPU芯片中的大部分区域都是由一大组,甚至数以千计的,被称为着色器内核的处理器组成。GPU是流式处理器,依次按照顺序处理一些类似的数据。也正是因为顶点/像素这些数据的相似性,GPU才可以以大规模并行方式处理这些数据。另外一个重要原因则是这些单元相对独立,并不需要邻里之间互通信息,也不共享可写内存位置。然而这些规定有时候也会因为一些新的、有用的功能被打破,但是这种打破就会因为一个处理器需要等待其他处理器完成工作,从而导致可能出现延迟。

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

假设一个网格被光栅化成2000个像素,也就是说下面的像素着色器要被调用2000次。假设我们使用一个世界上最烂的GPU,只有一个着色器处理单元。它将从第1个片元开始执行着色器程序,一直到第2000个。着色器处理器针对寄存器上的数值执行了一些算数运算,由于寄存器是本地的,访问速度很快,所以不会出现停顿。然后,着色器要去执行一个纹理采样的指令,通过表面位置信息获取到图片中的颜色并将其运用给网格。由于纹理是一些完全独立的资源,并非像素着色器的本地内存,所以需要去访问纹理。然而由于一次内存访问需要数百数千个时钟周期,而这个阶段GPU处理器无事可做。这时着色器处理器也就会停顿下来,去等待所需要的纹理颜色。

为了使这个糟糕的GPU变得更好,我们为每个片元提供一些用于其本地寄存器的存储空间。现在,允许着色器处理器切换并执行另一个片元,即两千个第二个片元,而不是停止纹理获取。此切换速度非常快,除了注意第一条指令正在执行哪条指令之外,第一段或第二段中的内容均不受影响。现在执行第二个片元。与第一个相同,执行一些算术函数,然后再次遇到纹理获取。着色器核心现在切换到另一个片元,即第三个片元。最终,所有两千个片元都以这种方式处理。此时,着色器处理器将返回片元编号一。此时,纹理颜色已被获取并且可以使用,因此着色器程序可以继续执行。处理器以相同的方式进行处理,直到遇到另一个已知会暂停执行的指令,或者程序完成。与着色器处理器(shader processor)始终专注于一个片元相比,执行单个片元所需的时间更长,但是整个片元的总体执行时间将大大减少。

在这种架构中,通过切换到另一个片元使GPU保持忙碌来隐藏延迟。GPU通过将指令执行逻辑与数据分离开来,使该设计更进一步。称为单指令多数据(SIMD,single instruction, multiple data)的这种安排可以在固定数量的着色器程序上以锁定步骤执行同一命令。SIMD的优点是,与使用单独的逻辑和调度单元运行每个程序相比,用于处理数据和交换的芯片(和功率)要少得多。将我们的2000片元示例转换为现代GPU术语,每个片元的像素着色器调用都称为线程。这种类型的线程与CPU线程不同。它由用于着色器输入值的一点内存以及着色器执行所需的任何寄存器空间组成。使用相同着色器程序的线程被分为几组,被NVIDIA称为warp,被AMD称为wavefronts。一个 warp/wavefront 被 计划用于SIMD处理,由8至64之间的任意数量的GPU着色器内核执行。每个线程都映射到SIMD通道。

假设我们有两千个线程要执行。NVIDIA GPU的 warps 包含32个线程。这将产生2000/32 = 62.5个 warps,这意味着分配了63个warps,其中一个 warps 是一半为空。warp 的执行类似于我们的单个GPU处理器示例。着色器程序在所有32个处理器上以固定步骤执行。因为对所有线程执行相同的指令,遇到内存提取时,所有线程都会同时遇到它。提取信号表明线程warp将停止,所有线程都在等待它们的(不同的)结果。此时不会停顿,而是将warp换成32个线程的另一个warp,然后由32个内核执行。这种交换的速度与我们的单处理器系统一样快,因为在将warp换入或换出时,每个线程内的数据都不会被触及。每个线程都有自己的寄存器,每个warp都跟踪其正在执行的指令。交换新线程只是将一组核心指向另一组要执行的线程即可。没有其他开销。warp执行或换出,直到全部完成。参见图3.1。

在这里插入图片描述
图3.1。简化的着色器执行示例。三角形片元(称为线程,threads)被收集成 warps。每个 warp 显示为四个线程,但实际上有32个线程。要执行的着色器程序长五个指令。四个GPU着色器处理器的集合在第一次 warp 时执行这些指令,直到在“txr”命令上检测到停顿条件为止,这需要时间来获取其数据。交换第二个 warp,并对其应用着色器程序的前三个指令,直到再次检测到停顿为止。交换第三个 warp 并使其停止后,通过交换第一个 warp 并继续执行。如果此时尚未返回其“txr”命令的数据,则执行将真正停止,直到这些数据可用为止。每个 warp 依次完成。

在我们的简单示例中,纹理获取内存的等待时间可能导致warp换入换出。实际上,因为交换成本非常低,所以可以将warp换成较短的延迟。还有其他几种用于优化执行的技术,但 warp交换(warp-swapping)是所有GPU使用的主要延迟隐藏机制。此过程的效率如何涉及多个因素。例如,如果线程很少,那么几乎不会创建任何warp,从而使延迟隐藏成为问题。

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

影响整体效率的另一个因素是由“ if”语句和循环引起的动态分支。假设在着色器程序中遇到 “if” 语句。如果所有线程求值并采用同一分支,则warp可以继续进行而不必担心其他分支。但是,如果某些线程甚至一个线程采用了替代路径,那么warp必须执行两个分支,从而丢弃每个特定线程不需要的结果。这个问题称为线程发散,其中一些线程可能需要执行循环迭代或执行warp中其他线程不执行的 “if” 路径,从而使它们在此期间处于空闲状态。

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

3.2 GPU管线总览

在这里插入图片描述

GPU中实现了第二章中描述的几何处理、光珊化、像素处理这些管道的阶段。它们分为若干个硬件阶段,具有不同程度的可配置性或可编程性。图3.2显示了根据其可编程性或可配置性进行颜色编码的各个阶段。需要注意的是,这些物理阶段与第2章钟介绍的功能阶段有所不同。

整个渲染管线都是在GPU中实现的。上图中,根据用户可操作可控制的程度,使用不同颜色对每个阶段进行了标记。绿色阶段为完全可编程。虚线方框代表着可选阶段。黄色阶段代表着可配置但不可编程,比如在合并阶段可以使用大量的混合操作。蓝色阶段为完全固定阶段。

我们这里聊的是GPU的理论模型,这些会通过API的形式暴露给开发者。但是,正如第18和23章所说,这些理论模型对应的实际上的物理实现,都是由硬件提供商设计的。比如理论模型中的一个固定功能,可以通过GPU中相邻的可编程的阶段添加命令实现。管道中一个简单的程序也可以被几个独立的单元拆分成若干元素,或者通过一个独立的pass完成。所以,理论模型可以帮助你理解影响性能的因素,而并非说GPU实际上就是这么实现的。

顶点着色器是一个完全可编程的阶段,用于实现几何处理阶段。几何着色器也是一个完全可编程阶段,用于操作一个图元(点、线、三角形)上的顶点。可用于逐图元的着色器运算,比如删除图元或者创建新的图元。曲面细分阶段和几何着色器都是可选的,并非所有的GPU都支持它们,特别是移动平台。

裁剪、三角形设置、三角形遍历都是通过固定功能硬件实现的。屏幕映射则受到窗口和视窗设置的影响,在内部进行了一个简单的比例缩放和重新定位。像素着色器阶段是完全可编程阶段。尽管合并阶段并不是可编程的,但是是可配置的,可以通过设置去执行各种操作。合并功能负责修改颜色、z缓冲、混合、模板缓冲以及输出其他相关的缓冲。像素着色器与合并阶段一同完成了第2章中介绍的像素处理阶段。

随着时间的推移,GPU管道已经从硬编码操作发展到增强灵活性和可控性阶段。引入可编程着色阶段是这一演变过程中最重要的一步。下一节将介绍各种可编程阶段的共同特性。

3.3 可编程着色器阶段

现代的着色编程使用一套统一的着色设计。也就是说,顶点,像素,几何和细分相关的着色器使用的是同一套编程模型。内部使用相同的指令集体系结构(ISA-instruct set architecture)。DirectX中实现该模型的处理器被称为公共着色核心,拥有该核心的GPU具有统一的着色体系结构。这种体系结构背后的原因是,这样的话着色器处理器就可以被用于各种不同的角色,GPU就可以根据它所看到的来进行分配。比如一块拥有大量小三角形的网格比拥有两个大三角形的网格需要更多的顶点着色处理器。而如果实现GPU把顶点着色和像素着色的处理器分开的话,那么就很难使得所有的核心都处于工作状态。使用统一的着色核心,GPU就可以决定如何平衡这个负载了。

关于整个着色器编程模型远超这本书的范围,且还有大量的书、网站已经对其进行介绍了。shader是通过一种类似C语言的着色器语言编写,比如DirectX中的HLSL,OpenGL中的GLSL。DX的HLSL可以被编译成虚拟机字节码,也被称为中间语言(IL/DXIL),以提供硬件独立性(这样的话硬件实现和shader语法就分离了)。同时,中间代码表示法也可以允许着色程序离线编译和存储。驱动只需要将此中间语言转换成特定GPU的ISA(instruct set architecture)即可。操纵台平台一般都会避免这种中间代码阶段,因为这样的话该平台就只有一种ISA了。

基本的数据类型为32位单精度的浮点型以及向量,尽管向量是着色编码的一部分,但是实际上上述硬件并不支持。在现在GPU上,32位整形和64位浮点型也从开始就支持了。浮点型向量vector基本上是用于保存数据,比如位置、法线、矩阵的行、颜色以及纹理坐标。整型基本被用于数量、索引或者位操作。复合的数据类型,比如结构体、数组、矩阵都已经被支持了。

一个drawcall会触发图形API去渲染一组图元,也就是会触发管线去执行着色命令。每个着色器阶段都会有两种输入:uniform输入,通过drawcall的时候值不变,varying(变化的)输入,来源于三角形顶点或者来源于光栅化。比如,像素着色器会将光源的颜色以uniform的形式传入,而三角形的位置会变化所以是以varying的形式。纹理是一种特殊的uniform类型,之前总被用作物件表面颜色使用,现在可以看作是一个用于包含任何数据的大数组。

底层虚拟机针对不同类型的输入和输出提供不同的寄存器。用于存放uniform的寄存器远比存放varying的寄存器大得多。原因是每个像素的varying数据都不同,所以需要加以限制。而uniform的数据只读,且针对当前drawcall中全局通用。虚拟机还有一些通用的临时寄存器,用于暂存空间。所有类型的寄存器都可以使用临时寄存器中的整数值进行数组索引。着色器虚拟机的输入输出如下图3.3所示。

底层虚拟机针对不同类型的输入和输出提供了特殊的寄存器。用于存放uniform的寄存器远比存放varying的寄存器多得多。原因是每个像素的varying数据都不同,所以需要加以限制。而uniform的数据只读,且针对当前drawcall中全局通用。虚拟机还有一些通用的临时寄存器,用于暂存空间。所有类型的寄存器都可以使用临时寄存器中的整数值进行数组索引。着色器虚拟机的输入输出如下图3.3所示。

在这里插入图片描述
图3.3 上图为统一的虚拟机和寄存机在shader model4.0下的布局图。每个资源旁都指示了最大可用数。用斜线分割的三个数字表示顶点\几何\像素 着色的的限制数量。

现代GPU中会高效的执行图形学中常见的计算。shading语言将最常用的操作暴露了出来,比如加法和乘法。其他被优化的操作会通过内部函数暴露,比如atan()、sqrt()、log()以及其他。还会有一些更加复杂的函数,比如向量归一化、反射、叉乘、矩阵转置、行列式计算等。

分支控制使用分支指令去切换不同分支代码的执行。分支控制相关的指令被用于实现高级语言中的“if”、“else”等指令。着色器支持两种不同类型的分支控制。静态分支控制是通过uniform的值。也就是说代码流在drawcall过程中是固定的,静态控制的优势在于针对各种不同的情况(比如不同数量的灯光)可以使用相同的着色器,这不是线程分散的,因为所有的调用使用的是相同的代码路径,动态分支是根据varying的输入,也就是说每个片元所执行的代码都不同。这个比静态分支厉害,但是也消耗性能,特别是分支变化不稳定的情况下。

3.4 着色器语言和API的发展

可编程着色器框架的想法起源于1984年的Cook’s shade trees(参考文献[287])。图3.4展示了一个简易的着色器以及其相应的着色树。RenderMan着色器语言(参考文献[63、1804])于二十世纪80年代末根据这一思想发展而来。它与其他不断发展的规范(比如Open Shading Language OSL)一起,至今仍然用于电影制作渲染[608]。

在这里插入图片描述
图3.4 简单铜着色器的着色树,和他对应的着色器语言程序。

1996年10月1号,3DFX Interactive推出了第一代消费级图形硬件,在下图3.5可以看到这些年的时间轴。它们的voodoo图形卡可以将游戏Quake(雷神之锤)以很高的质量和性能渲染出来,从而使其快速被市场采用。这个图形硬件全局使用的都是固定管线。在GPU原生支持可编程渲染管线之前,有多次尝试通过多个渲染pass实时实现可编程着色器操作。1999年 Quake III所使用的Arena脚本语言是这一领域第一次广泛的被成功商业化。正如本章最开始所说,NVIDIA的GeForce256是第一个被称为GPU的硬件,然而它不可编程,不过它可配置。

在这里插入图片描述
在2001年早期,NVIDIA的GeForce3是第一个支持可编程顶点着色器的GPU(参考文献[1049]),通过DirectX8.0的接口以及OpenGL的扩展展现给开发者。这种着色器是通过一种类似汇编的语言编写,然后由驱动解析成微代码。DirectX8.0实际上还包含了像素着色器,但是当时的像素着色器还不可编程。只能通过驱动支持纹理混合,将寄存器合并在一起。这种编程不仅长度短(只有12个或者更少的指令),而且还缺乏重要功能。通过对RenderMan的学习,Peercy et al.(参考文献[1363])认为真正的可编程中的重要部分是纹理读取和浮点数支持。

这个时候的Shader还不支持分支控制,所以只能通过计算两条分支的结果,然后选择一条或者将两个结果进行插值,通过这种方式来模拟条件语句。DirectX定义了Shader Model(SM)的概念用于区分包含不同能力shader的硬件。2002年DirctX9支持了SM2.0,其中包含了真正可编程的顶点着色和像素着色。类似的功能在OpenGL中也通过各种扩展提供给开发者。增加了读取任意已经依赖的贴图以及存储16bit 浮点数的功能,也就终于完成了Peercy et al.所提出的一系列要求。针对着色资源的限制,比如指令、纹理、寄存器的限制也得到了提升,从而使得shader可以实现更加复杂的效果。分支判断的功能也加进去了。然而随着shader的越来越复杂,导致汇编编程模型变的越来越繁琐。还好,DirectX9引入了HLSL。(high level shader language)这个着色器语言是由微软和NVIDA一同完成的。同时,OpenGL ARB(体系结构审查委员会)发布了GLSL(GL shader language),一个类似的语言用于针对OpenGL(参考文献[885])。这些语言受到C语言编程的语法和设计理念的严重影响,并包含了RenderMan着色语言的元素。

SM(shader model)3.0是于2004年发布的,加入了动态流程控制,使得Shader功能更加强大。同时也将一些可选的特性转为了必须支持的特性,提高了着色资源的最大数量限制,并且增加了顶点着色器中读取纹理的功能。2005年末发布的新一代游戏平台(微软的Xbox360)以及2006年末发布的(Sony的PS3)都需要SM3.0级别的GPU。任天堂的Wii console是最后一款有名的使用Fixed-function GPU的机器,在2006年末发布。在当时,纯固定管线已经不复存在了。着色器语言已经发展到了可以通过大量的工具创建和管理的时代。图3.6显示了其中一种使用了Cook’s 着色树概念的工具。

在这里插入图片描述
图3.6 上图为mental image公司的工具“mental mill”。显示了用于实现shade设置r的一个可视化shader编辑器。各种操作封装在功能栏中,可从左侧进行选择。选中后,每个功能栏都有可调的参数,如上图右侧所示。每个函数的输入和输出链接在一起,形成最终结果显示在上图中间框的右下角。

在2009年DirectX11和SM5.0发布,增加光栅化阶段的渲染和计算渲染,也被称为直接计算。该版本还着重于更有效的支持CPU多线程,这个课题将在18.5进行讨论。OpenGL在4.0加入了光栅化,在4.3加入了计算渲染。DirectX和OpenGL发展日新月异。两者都设定了特定版本发布所需要的一定级别的硬件支持。微软控制着DirectX API,因此直接与独立硬件供应商(IHV),比如AMD、NV、Intel,以及游戏开发人员和计算机辅助设计软件公司,来确定公开哪些功能。OpenGL则是由硬件和软件供应商组成的联盟开发,由非营利组织khronos组织管理。由于涉及到的公司众多,所以功能经常在DirectX发布之后的一段时间之后才出现在OpenGL的一个版本中。但是OpenGL允许扩展,扩展分为特定供应商的或者通用的扩展,这样的话,就可以在下一代版本正式发布之前,使用到最新的GPU技术。

API的下一个重大变化是在2013年,AMD公司推出了Mantle。是与视频游戏开发者DICE合作开发,Mantle的理念为去掉大部分图形驱动程序的开销,直接将控制权交给开发者。除此之外,重构还进一步有效的支持CPU多线程。这种新型的API专注于极大地降低了CPU在驱动中的耗时,并更有效的支持了CPU多线程,详见18章。Mantle的这种新理念被微软所采纳,并与2015年加入了DirectX12中。需要注意的是DirectX12已经没有再增加新特性了,与DirectX11.3所支持的硬件特性完全一样。这两个API都可以用于将图形发送到VR系统,比如Oculus Rift以及HTC Vive。然而DirectX12对API进行彻底的重新设计,它可以更好的映射到现代GPU架构。低开销的驱动程序对于由于CPU驱动消耗导致瓶颈的应用程序非常有帮助,或者对于使用大量CPU处理器进行图形处理的应用程序,可以提高性能(参考文献[946])。从早期的API进行移植可能会很难,而且如何使用错误的方式实现甚至会导致性能更低(参考文献[249、699、1438])。

2014年苹果发布了自己的低开销API metal。metal最早出现在移动平台iPhone5s和iPad Air,一年后通过OS X发布在Mac上。除了上述优势之外,降低CPU消耗还节省了功耗,这是移动平台的一个重要指标。这个API有自己的着色器语言,用于图形和GPU计算程序。

AMD将Mantal赠送给了khronos组织,后者随后与2016年初发布了新的API Vulkan。和OpenGL一样,Vulkan支持多操作系统。Vulkan使用一种高级的中间语言SPIR-V,用于着色显示和通用的GPU运算。预编译的shader可被用于任何支持该功能的GPU上(参考文献[885])。Vulkan同样可以用于非图形学GPU运算,因为它并非需要一个显示窗口(参考文献[946])。Vulkan与其他低开销驱动的一个显著不同是,它适用于从工作站到移动设备的各种系统。

在移动设备上,标准是使用OpenGL ES。"ES"的意思是嵌入式系统(Embedded System),因为这个API就是为了移动设备设计的。标准的OpenGL在某些调用结构中相当庞大和缓慢,并且需要对一些很少会被使用到的功能进行支持。2003年发布的OpenGL ES1.0是OpenGL1.3的精简版,使用的是固定管线。虽然DirectX的发布是与那些支持它的硬件同步发布的,但是针对移动设备的图形支持并非如此。比如第一代iPad,发布于2010年,使用的是OpenGL ES1.1。然而2007年OpenGL ES2.0就发布了,并且提供了可编程着色器。它是基于OpenGL 2.0,但是并没有固定管线部分,也就是说无法向后兼容OpenGL ES1.1。OpenGL ES3.0在2012年发布,提供了多个渲染对象、纹理压缩、位置反馈、实例化以及大量的纹理模式和模型的功能,着色器语言也得到了进一步发展。OpenGL ES3.1加入了计算着色,ES3.2加入了几何着色和光栅化着色以及其他功能。第23章会更加详细的讨论移动设备架构。

OpenGL ES是基于浏览器的API WebGL的一个分支,通过JavaScript调用。于2011年发布,这个API的第一个版本在大多数移动设备都可用,因为它在功能上相当于OpenGL ES2.0。与OpenGL一样,可以通过扩展使用更高级别的GPU特性。WebGL 2需要OpenGL ES3.0的支持

WebGL特别适用于如下场景:

跨平台,在所有个人电脑和几乎所有移动设备上工作。
驱动由浏览器支持。即使一个浏览器不支持一个特定的GPU或者扩展,通常另外一个浏览器支持。
代码是解释的,而非编译的,开发只需要一个文本编辑器。
调试工具是内置于大多数浏览器中,在任何网站运行的代码都可以被检查。
可以通过代码上传到网站或者github进行部署
高级场景、效果库,比如three.js(参考文献[218])可以很方便的访问各种复杂效果,比如阴影算法、后效、PBS(基于物理渲染)、延迟渲染等。

3.5 顶点着色器

顶点着色器是图3.2中管线中的第一阶段。虽然这是直接由开发者控制的第一阶段,但是需要注意的是,一些数据操作发生在这个阶段之前,比如创建一个含有位置信息的缓冲,VBO()等。在DirectX中被称为输入装配程序(参考文献[175、530、1208]),可以将多个数据流编织在一起,形成沿着管道继续进行的点,然后再组成图元。比如,一个物件是由一组顶点和一组颜色组成。输入装配器将通过创建具有位置和颜色的顶点,来创建一个对象的三角形(或者线或者点)。第二个对象可以使用相同的位置数组(以及不同的模型转换矩阵)和不同的颜色数组来表示。第16.4.5将详细介绍数据展示。通过输入装配器还可以实现实例化。这样的话就可以将一个对象根据每个实例不同的数据,一次性绘制出来。第18.4.2介绍了实例如何使用。

一个三角形网格是由一组顶点组成,每个顶点都与模型表面上的特定位置相关联。除了位置,每个顶点还可以关联一些可选的属性,比如颜色、纹理坐标。曲面法线也通过顶点属性表示,这看上去是个奇怪的选择。因为在数学上,每个三角形都有一个定义明确的曲面法线,看上去直接用三角形的面法线进行着色更有意义。然而,渲染的时候,三角形网格用于表示底层的曲面,顶点法线用于表示这个曲面的方向,而非三角形本身。第16.3.4描述了计算顶点法线的方法。下图3.7展示表示曲面的两个三角形网格的侧视图,一个是平滑的,另一个有锐利的折痕。

在这里插入图片描述
图3.7 上图展示了三角形网格(黑色,带顶点法线)的侧视图(红色)。在左侧,平滑顶点法线用于表示平滑曲面。在右边,中间的顶点被复制,给出两条法线,表示一条折痕。

顶点着色器是处理三角形网格的第一步。顶点着色器无法访问关于如何组装成三角形的数据。正如它的名字所示,它只处理输入的顶点。顶点着色器提供修改、创建、忽略每个顶点相关数据(比如顶点的颜色、法线、纹理坐标和位置)的方法。通常,顶点着色器会将顶点从模型空间转成齐次裁剪空间,见4.7节。顶点着色器至少要始终输出这个坐标。

顶点着色器与前面描述的统一着色器基本相同。每个传入的顶点都会经过顶点着色器的处理,然后输出一系列值用于在三角形和直线上进行插值使用。顶点着色器无法创建、销毁顶点,一个顶点产生的数据无法传递到另外一个顶点。由于每个顶点都是相对独立的,所以GPU上任何数量的着色器处理器都可以与顶点的传入流进行并行运算。

输入装配集通常是在顶点着色执行之前进行的。这也是一个物理模型不等同逻辑模型的例子。在物理上,顶点着色器获取数据创建顶点,而驱动会使用恰当的指令悄悄的预处理每个着色器程序,,则对程序是不可见的。

接下来的章节会聊一下几种着色器效果,比如用于动画关节的顶点混合,轮廓渲染等。顶点着色器的其他用途还包括:

对象生成,创建一次性的网格,将其通过顶点着色器进行变形
通过蒙皮或者变形为角色的身体和面部设置动画
程序化变形,比如旗帜、布料、水(参考文献[802、943])
创建粒子。将一个的网格发送到管线,并根据需要为其分配一个区域。
通过将整个帧缓存的内容作为一张纹理,作用于正在进行程序变形的屏幕对齐网格,实现透镜变形,热雾,水波,页面卷曲和其他效果
通过使用顶点纹理获取VTF,得到地形高度(参考文献[40、1227])

上述的一些通过顶点着色进行的变形如图3.8所示

在这里插入图片描述
上图左侧为一个正常的茶壶。顶点着色器执行一些简单的操作得到中间的图。右侧的图为使用噪声扰动这个模型得到的图。(图片由NVIDA公司提供,通过Fx Composer2制作)

顶点着色的输出可以以几种不同的方式使用。通常的做法是组建成三角形,光珊化,然后将生成的单个像素片段发送到像素着色器以继续处理。在某些GPU中,数据也可以发给曲面细分阶段、几何着色,或者通过存储到内存。这些可选的阶段将在后面的章节进行讨论。

3.6 细分曲面阶段

细分曲面阶段使得我们可以绘制曲面。这个阶段的任务是根据针对曲面的描述,在GPU中将一个面数较低的网格转成一个面数可控的一组三角形。这个可选的GPU功能,首先是在DirectX11中被引入,也是DX11所必需的一个特性,OpenGL4.0和OpenGL ES3.2也支持它。

使用曲面细分有很多优点。由于曲面通常比较复杂,除了节约内存,针对动画角色或者每帧形状都在变化的物件,这个特性还可以以防CPU和GPU之间的带宽成为瓶颈(因为每帧都会有大量的VBO(vertex buffer object)传输)。通过为指定视图生成的适当数量的三角形,就可以有效的渲染曲面。比如,当一个球离摄像机很远的时候,只需要很少的三角形。当离得很近的时候,最好是有数千个三角形看上去才好看。这种控制LOD(level of detail 层次细节)的能力使得应用程序可以自行控制其性能。比如,在差一点的GPU上,可以使用低精度的网格保持帧率。所以说,可以直接使用一个精细的三角形网格(参考文献[1493]),或者也可以通过曲面细分的方式,根据需要去执行一些复杂的着色计算。

曲面细分通常由三部分组成,用DirectX的术语来说,分为外壳着色器、曲面细分着色器、区域着色器。OpenGL中外壳着色器就是曲面细分控制着色器,它控制着色,区域着色器被称为曲面细分评估着色器,名字更长,但是更容易理解。而固定管线曲面细分着色器在OpenGL被称为图元生成,也正如我们将要看到的,它确实是干这个的。

第17章详细的讨论了如何说分和细分 曲线和曲面。在这里,我们简单的总结每个细分阶段的目的。首先,外壳着色的输入是一个特殊的图块图元。它包含了定义细分曲面、bezier(贝塞尔)曲线或者其他类型曲线所需要的多个控制点。轮廓着色有两个功能。首先,告诉细分器应该生成多少三角形,以及按照什么配置生成。其次,它对每个控制点进行处理。除此之外,外壳着色器还可以根据需要修改曲面图块,增加或者删除控制点。轮廓着色器将控制点集和细分控制数据传给区域着色器。如下图3.9所示。

在这里插入图片描述
上图为细分阶段。轮廓着色拿到一组由控制点组成的图块,将细分因子(TFs)和类型发送给固定管线细分器。控制点集由轮廓着色根据需要进行转移,然后由TF以及相关的图块 传给区域着色器。细分器根据它们的质心坐标系创建一组顶点。然后区域着色器会对这些进行处理,生成三角形网格(作为参考,控制点已经显示出来了。)

曲面细分器在管线中是一个固定管线,只被用于曲面细分着色器。它的任务是为区域着色器增加一些新的顶点。外壳着色器输出需要细分的曲面类型:(三角形、四边形、isoline等值线)。isoline(等值线)是一组线条,有时会被用于头发渲染(参考文献[1954])。外壳着色器还会输出细分因子(在OpenGL被称为细分级别)。由两种类型:内缘和外缘。这两个内部因子决定了三角形或四边形内的细分数量。外部因素决定了每个外部边缘的分割程度,详见第17.6节。图3.10是一个逐步增加细分因子的例子。通过这种内外因子分开的方式,我们可以使得相邻曲面的边在细分的过程中分割方式保持一致,而不用关心其内部是如何被细分的。这样的话就会避免因为细分后相邻面边不一样的问题,导致裂缝或者其他着色瑕疵。顶点位于质心/重心坐标系,详见22.8,指定所需曲面上每个点的相对位置的值。

在这里插入图片描述
上图展示了改变细分因子的效果。最初的茶壶是由32个图块组成。从左到右,内缘、外缘细分因子分别为1、2、4、8(图片由Rideout和Van Gelder(参考文献[1493])中的demo提供)

外壳着色输出一个图块和一组控制点位置。而且,它还可以通过发送一个小于或者等于0(或者直接发送NaN)的外缘细分等级来表示将该图块丢弃。否则的话,细分器会产生一个网格并将其发送给区域着色器。每次调用域着色器时,都会使用外壳着色器中曲面的控制点来计算每个顶点的输出值。区域着色的数据流模式和顶点着色一样,处理来自细分器的每个输入顶点并生成相应的输出顶点。然后形成三角形沿着管线向下传递。

尽管这个系统听起来很复杂,但其实它这样结构的目的是为了提高效率,使得每个着色器都很简单。比如外壳着色器中的图块通常不会有修改或者只有很少的修改。该着色器还可以估算图块到摄像机的大概距离或者屏幕尺寸来实时计算细分因子,比如地形渲染(参考文献[466])。或者,外壳着色器也可以对所有图块提供一套相同的数值。细分器执行一套复杂但是固定的函数过程,生成顶点,得到它们的位置,并指定它们形成三角形或者线。这个数据放大的步骤在着色器外部执行,以提高计算效率(参考文献[530])。区域着色器获取每个点生成的质心坐标,并在面片的计算公式中,使用这些坐标来生成位置、法线、纹理坐标和其他所需的顶点信息。示例如图3.11所示。
在这里插入图片描述
上述左图为一个6000面的模型。右侧使用PN三角形细分对每个三角形进行细分。(图片来自NV SDK 11(参考文献[1301]示例,由NV提供,模型来自Metro 2033 by 4A Games))

在这里插入图片描述
图3.12 如上图所示,几何着色的输入为一些简单的类型:点、线段、三角形。上图右侧两张图中的图元,包含了针对线段和三角形的额外顶点。除此之外还可以支持更复杂的图块

3.7 几何着色器

几何着色器可以将图元转变成其它图元,这是细分阶段无法做到的。比如,可以通过给每个三角形创建线边,将三角形网格转换成线框视图。或者,不使用线,而使用面向观察者的四边形,这样就可以用较厚的边进行线框渲染(参考文献[1492])。几何着色是于2006年底的DirectX10中被引入图形硬件加速管线中。它在管线中的位置在曲面细分阶段之后,也是可选的。它是SM4.0的必备特性,在之前的SM中并不支持它。OpenGL 3.2和OpenGL ES3.2也支持这种类型的shader。

几何着色的输入是一个物体和它相关的顶点。物件通常由三角形、线段、点组成。然后可以通过几何着色定义和处理扩展的图元。比如说,可以传入三角形外部的三个附加顶点,以及多段线上的两个相邻顶点。如图3.12所示。DirectX11和SM(shader model)5可以传入更多更复杂的图块,最高可以达到32个控制点。也就是说,几何处理可以更有效的生成图块(参考文献[175])。

几何着色除了点、线段、三角形这些图元,然后输出0个或者多个顶点。需要注意的是,几何着色可以不输出任何输出。通过这种方式,可以选择性的改变顶点、增加新的图元,或者删除图元,以此改变整个网格。

几何着色可以用于修改传入的数据或者制作有限数量的复制品。例如:一个用途是生成六个不同转换过位置的副本,用于同时呈现立方体的六个面,详见10.4.3。也可以被用于创建大量阴影图来获得高质量的阴影。还可以利用几何渲染的优势通过一些点的数据创建可变尺寸的粒子、沿轮廓拉伸以进行毛发渲染以及为阴影算法查找对象边缘。图3.13展示了一些例子。这些以及更多的用法都将在本书的剩下部分详细介绍。

在这里插入图片描述
图3.13 上图为几何渲染的一些用法。左侧为使用几何渲染动态执行圆球的表面细分。中间图为通过几何渲染和流输出对直线段进行分段细分,然后通过几何渲染生成的广告牌效果展示闪电。右侧为通过顶点着色和几何渲染以及流输出实现布料模拟。(图片来自NVIDA SDK 10(参考文献[1300])的实例,由NVIDA提供)

DirectX11为几何渲染增加了实例化的功能,这样的话几何渲染就可以针对给的任意图元运行多次(参考文献[530、1971])。在OpenGL4.0中这是用一个调用计数来表示被调用。几何渲染可以输出多达四个流。一个流可以按照管线继续后面的运算。所有这些流都可以选择性的发送到流输出的渲染目标上。、

几何渲染中保证图元输出结果的顺序与输入的顺序一致。这样影响了性能,因为如果多个渲染核并行运算,那样的话结果就要被保存下来并进行排序。这个因素和其他因素不利于几何体着色器在一次调用中复制或创建大量几何体。(参考文献[175、530])。
在一个Drawcall中,整个管线只有三个部分会在GPU中创建工作:光栅化、TS和GS。而考虑到所需要的资源和内存,GS是最不可控的,因为它是完全可编程的。在实践中,GS很少会被使用,因为它无法很好的利用GPU的优势。在一些移动平台,它甚至是在软件层开发的,所以严重阻碍了它的使用(参考文献[69])

3.7.1 流输出

GPU管线的标准用法是将数据通过顶点着色、光栅化,然后在几何着色阶段对其进行处理。之前数据都是会贯穿整个管线,而中间数据无法被访问。流输出在SM4.0被引入。在顶点被顶点着色器中处理完之后(以及可选的曲面细分阶段、几何阶段),除了可以将其传输给光栅化阶段之外,还可以输出一个流,比如一个有序数组。甚至说,可以将光栅化以及之后的流程全部关闭,然后整个管线就被当时一个纯粹的非图形学工作流。这里输出的数据还可以被送回重新进行管线,从而允许迭代处理。如13.8所述,这种操作针对模拟流水或者其他粒子特效都会比较有用。它还可以用于对模型进行蒙皮,然后这些顶点可重用,详见4.4节

流输出仅以浮点数的形式返回数据,所以会有一个明显的内存开销。流输出是以图元为单位,而非顶点。也就是说,当一个网格选择这条路的话,每个三角形都会生成自己相关的三个顶点作为输出。原始网格中的顶点共享都会丢失。因此,比较常见的办法是以点为图元进行这个管线。在OpenGL中流输出被称为transform feedback,因为它的作用就是将顶点输出,然后用于之后进行进一步处理。图元会被按照顺序传递给流输出目标,也就是说顶点的顺序保持不变(参考文献[530])。

3.8 像素着色器

在完成了顶点着色、曲面细分着色、几何着色之后,图元会再去经历裁剪以及光栅化。这两个部分的处理步骤是相对固定的,不可编程,但是有些可配置。每个三角形都被遍历以确定它所覆盖的像素。光栅化还可以粗略计算三角形覆盖每个像素的单元区域的大小,详见5.4.2。三角形全部或者部分覆盖部分的像素被称为片元。

三角形顶点处的值,包括Z缓冲(深度缓冲)上的Z值,都会逐像素的在三角形曲面上进行插值。这些值都会被传给顶点着色去逐片元处理。在OpenGL PS也被称为片元着色,这个称呼其实更加合理。但是为了统一性,我们这本书将使用片元着色来称呼。顺着这个管线的点和线的图元也会根据像素覆盖率创建片元。

处理三角形所用的插值算法是由像素着色指定的。一般我们都使用透视矫正插值,这样的话当一个物体远离摄像机的时候,两个像素表面位置之间的世界空间距离会增加。比如,绘制延伸到地平线的铁路轨道。当铁轨距离比较远的时候,从屏幕上看铁轨枕木之间的距离越来越近,这也就是因为地平线上每个连续像素的距离越大。还有其他插值算法,比如屏幕空间插值,不考虑透视投影。DirectX11给开发者权限可以选择插值的时间和方式(参考文献[530])。

在编程方面,顶点着色器程序的输出,经过三角形、线段的插值,被传输给像素着色器作为输入。随着GPU的发展,像素着色器可以获取到更多的输入。比如在SM3.0及以上,像素着色器可以获取到片元在屏幕上的位置,还有,当前绘制的片元属于三角形的正面还是背面,也可以通过一个内置flag获取到。通过这个flag,着色器可以轻松的在一个pass(shader语言中的一个属性)中实现正面和背面效果不同的材质球。

根据这些输入,特别是像素着色器可以计算和输出一个片元的颜色。像素着色还可以生成不透明的值,以及可以修改它的Z值。在合并阶段,这些值用于修改该像素原本存储的值。光栅化生成的深度值,可以在像素着色器中被修改。模板缓冲通常是无法被修改的,而是直接将其传递到合并阶段。而DirectX11.3也允许着色器去修改这个值了。在SM4.0中,雾的计算以及alpha test(透明度测试)都从合并阶段,搬到了像素着色阶段(参考文献[175])

像素着色具有丢弃片元(即不生成输出)的独有功能。图3.14展示了如何丢弃一个片元。裁剪面之前是固定管线中的一个可配置的元素,后来被加入了顶点着色中。后来像素支持了丢弃后,这个功能就可以在像素着色中以多种多样的形式出现了,比如裁剪体应该是AND(和)关系还是OR(或)关系。

在这里插入图片描述
图3.14 展示了用户自定义的裁剪平面。最左侧,一个水平裁剪板被用于裁剪物件。中间那个图展示了,一个球被三个板子进行裁剪。右侧,球被裁剪后,只剩下了三个板子之外的部分了。(图片来自three.js中的webgl_clipping、webgl_clipping_intersection例子(参考文献[218]))

最开始,像素着色器的输出只能传递到合并阶段,用于显示。随着时间的推移,像素着色可以执行的指令数量大大增加。然后就出现了多个渲染目标(MRT)的概念。也就是说像素着色器的输出不再仅限于颜色和z缓冲了,而是可以将针对每个片元的多套数据写入不同的缓冲中,每一个都被称为一个渲染目标。渲染目标一般都有相同的x和y维度,一些API支持不同长度,但是渲染区域将是使用最小的那个。一些架构要求渲染目标的位数也一样,甚至要求相同的数据格式。根据GPU不同,多重渲染目标的数量一般为4个或者8个。

即使有这些限制,多个渲染目标依然是一个可以提高渲染性能的强大功能。一个简单的pass就可以将颜色、物件标识符、世界空间距离等信息生成在多个渲染目标上。这个功能也使得产生了新的渲染管线,叫做延迟渲染,其中可见性和着色是在两个单独的过程中完成的。第一个过程逐像素的存储了物件的位置、材质信息,第二个pass可以将光照等其他效果高效的计算出来。这种渲染方式将在20.1节详细介绍。

像素着色器的限制是它只能改变当前所在片元位置的渲染,而不能从相邻像素读取当前结果。也就是说,当一个像素着色器执行的时候,不能将其输出发送到相邻的像素,也不能访问其他像素最近的改动。它只能影响当前像素的结果。然而,这个限制并没有听起来那么可怕。因为一个pass产生的图片可以在下一个pass的像素着色器中随意读取。相邻的像素还可以通过图像处理技术进行处理,这个将在12.1节详细介绍。

有一个办法可以获取周围像素的结果。一种是,像素着色器可以在计算梯度或导数信息时立即访问相邻片段信息(尽管是间接的)。像素着色器提供沿X和Y屏幕轴每像素内插值的变化量。这些值对于各种计算和纹理寻址很有用。这些渐变信息对于纹理过滤(详见6.2.2)等操作尤为重要,这样可以知道一个像素对应图片中的多少信息。所有的当代GPU都是通过以22(quad)的的片元来实现这个特性。当顶点着色器需要一个渐变值的时候,将会返回相邻片元的差,如下图3.15所示。一个统一的内核可以访问相邻(同一个warp内不同线程)的数据,然后就可以计算出用于顶点着色的使用的渐变值。一个实现的结果就是:在有动态分支(比如if语句或者可变次数的循环语句)的shader中,无法访问这个渐变信息。只有当一组内的所有片元都是走的同一套指令,这样的话用于计算渐变信息的四个像素数据才有意义。这个基础的限制在离线渲染系统中依然存在(参考文献[64])
在这里插入图片描述
图3.15 如上左图,一个三角形被光栅化为几个四边形,每个quad为2
2的像素。在上右图显示了用黑色标记的像素的渐变计算。V的值(黑点边上的数字)用于显示四边形中的4个像素位置。需要注意的是即使那个四边中的那3个像素没有被覆盖,但是依然被GPU处理,以便计算渐变。X、Y轴上的渐变通过同一个四边形中左下角的像素以及它的两个邻居计算出来

DX11引入了一种缓冲区类型,允许对任意位置进行访问,被称为无序访问视图(unordered acess view-UAV)。最初只是用于像素和计算着色,在DirectX11.1中被扩展到可以被用于所有shader。OpenGL 4.3将这个缓冲称为着色器存储缓冲对象(shader storage buffer objectSSBO)。这俩名字都不错。像素着色器是并行运行的,按照一个任意的顺序,这个存储缓冲区在它们之间共享。

通常一些机器需要一些机制来避免数据争抢,也就是数据危险。比如有两个着色器程序都在争取去影响同一个变量,就可能导致各种结果。比如,像素着色器的两个指令同时去获取同一个值,得到初始值后各自进行修改,然后不论是哪个后写回,都会导致另外一个计算结果丢失。GPU通过一个专用的原子单元来处理这个事情(参考文献[530])。但是原子也就意味着一些着色器可能会等待另外一个正在读取/修改/写入数据的着色器。

虽然原子操作可以避免数据危害,但是很多算法还是需要特定的顺序执行。比如,我希望先绘制一个更远距离的蓝色的透明三角形,然后再在上面绘制一个红色的透明三角形。可以通过2个像素着色器来分两次绘制,每个像素着色器绘制一个三角形,这样的话肯定没有问题。在标准管线中,片元结果会在合并阶段中先进行排序,然后再被处理。DirectX11.3引入的光栅化顺序(Rasterizer order views-ROVs)就是去强制执行顺序。有点类似UAV,可以以相同的方式由着色器读写。关键不同是ROV保证数据是按照正确顺序访问的,这个大大提高了这些着色器可用缓冲的实用性(参考文献[327、328])。比如ROV使像素着色可以写自己的混和算法成为了可能,因为它可以直接读写在ROV中的任何位置,而不需要合并阶段(参考文献[176])。代价就是,如果检测到一个无序访问,像素着色器的调用可能会被暂停直到前面的三角形绘制完全处理结束。

3.9 合并阶段

正如2.5.2节所描述的,合并阶段是用于将单独的片元的深度和颜色(由像素着色器生成)与帧缓存上的信息进行合并。DirectX将这个阶段称为输出合并,OpenGL将这个阶段称为预取样操作。在大多数传统管线中(包括我们的),这个阶段包含模板缓冲和z缓冲。如果片元可见,则就会触发颜色混和操作。针对不透明物件,其实并没有真正的混和,只是单纯的用片元颜色代替之前存储的颜色。真正将片元和帧缓存上存储的颜色进行混和的操作,通常被用于半透明物件以及合成操作,详见5.5节。

想象一下,一个由光栅化生成的片元经过像素着色器后,在进行深度测试的时候发现自己被前面绘制的片元挡住了,这样的话整个像素着色器的操作都白费了。为了避免这种情况,需要GPU将合并阶段中的一些测试放到像素着色之前(参考文献[530])。帧的深度缓冲(以及其他被使用的,比如模板缓冲或者裁剪)被用于测试可见性,如果不可见,该片元则被剔除掉。这个功能被称为early-z(参考文献[1220、1542])。像素着色器可以改变z,或者直接抛弃该片元。如果像素着色器中有这两个操作,early-z就会被关闭,这样的话整个管线就变的低效了。DirectX11和OpenGL4.2允许像素着色器强制打开early-z,尽管还会有一些限制(参考文献[530])。23.7节会详细介绍early-z以及其他z 缓冲的优化。使用early-z会获得一个很大的性能提升,这个会在18.4.5中描述。

合并阶段发生在固定管线之间,比如三角形设置,以及可编程管线中间。尽管它并非可编程,但是高度可配置。比如颜色混和就可以使用多种算法。最常见的有涉及到颜色和透明度的乘法、加法、减法等,还有其他操作,比如最大、最小、按位操作等。DirectX10增加了将像素着色器中两个颜色与帧缓存上颜色混合的方法。这个能力被称为双源颜色混合,不能和多渲染目标一起使用。多渲染目标支持颜色混和,DirectX10.1还增加了针对每个缓冲使用不同混和算法的功能。

如前一节末尾所述,DirectX11.3提供了一个方法,可以通过ROV(光栅化顺序)实现可编程渲染,尽管它在性能上会有一些代价。ROV和合并阶段都是按照顺序渲染的,也就是输出不变性。不管像素着色器的输出顺序如何,API要求结果按照输入顺序,一个个对象和一个个三角形的排序并发送到合并阶段。

3.10 计算着色器

GPU不仅可以用于实现传统的图形管线。也可以用于非图形化应用,比如计算股票期权估值,以及训练神经网络进行深度学习等领域。以这种方式使用硬件被称为GPU计算,像CUDA(Compute Unified Device Architecture)、OpenCL这样的API被用来控制GPU作为一个巨大的并行处理器来使用,而不需要使用图形特定的功能。这些框架通常使用C\C++的扩展,以及搭配上GPU特定的类库。

DirectX11中引入了计算着色器的概念,这个着色器不固定在管线中的某个位置。然而它又与渲染过程密切相关,因为它是由图形API调用。它也是根据顶点、像素以及其他着色器一起执行。它与管线中的其他着色器共享同一个统一着色器处理器池子。它与其他着色器类似,有一些输入,可以访问缓冲(比如纹理)作为输入/输出。在CS中warp和线程的概念更加可视化。比如,每个调用都能获取到它的线程索引。还有线程组的概念,在DirectX11中由1-1024个线程组成。这些线程组分为x、y、z三个轴表示,主要是为了在着色器中使用方便。每个线程组都有一个小的内存用于线程间共享。在DirectX11中,这个大小为32KB。计算着色器是由线程组为单位执行,这样保证一组中的所有线程都并发运行(参考文献[1971])

计算着色器的一个重要优势在于,它们可以访问GPU生成的数据。将数据从GPU传给CPU会有延迟,因此如果处理和结果都在GPU上进行,可以提高性能(参考文献[1403])。后处理,即将渲染后的数据进行修改,通过计算着色器实现性能更优。所谓的共享内存,也就是指采样的图像数据可以与相邻线程共享。比如,使用计算着色器计算图像的分布或者亮度的执行速度,是像素着色器的两倍。

计算着色器还可以被用于粒子特效、网格处理比如面部动画(参考文献[134])、剔除(参考文献[1883、1884])、图像过滤(参考文献[1102、1710])、提高深度的精度(参考文献[991])、阴影(参考文献[865])、区域深度(参考文献[764])以及任何其他可以使用GPU处理的任务。Wihlidal(参考文献[1884])还讨论了计算着色器如何比光栅化外壳着色器更加高效。图3.16可以看到其他使用。
在这里插入图片描述
上图为计算着色器的使用实例。左图为使用计算着色器计算头发受风的影响,头发本身是有曲面细分计算。中间为使用计算着色器执行一个快速的模糊算法。右图为模拟海洋波浪。(图片为NVIDA SDK11的实例,由NVIDA提供)

以上是我们对GPU渲染管线的总览。可以通过各种方法使用和组合GPU的特性来实现各种渲染相关的处理。本书的中心主题是使用这些能力调整相关理论和算法。下面我们聊一下位置转换和着色。

更多资源

Giesen的图形管线指南(tour of graphics pipeline)(参考文献[530])描述了GPU的许多方面,解释了各个元素的工作方式。Fatahalian和Bryant在一系列详细的演讲幻灯片中讨论了GPU的并行性。Kirk 和 Hwa的书(参考文献[903])重点讨论了CUDA的GPU计算,另外前言部分还讨论了GPU的进化和设计理念。

想要了解着色器需要学习更多的内容。比如OpenGL Superbible(参考文献[1606])以及OpenGL Programming Guide(参考文献[885])介绍了着色器的编程。OpenGL Shading Language(参考文献[1512])不包括计算着色器和曲面细分着色器,但是包含了着色器相关的算法。访问本书的网站http://www.realtimerendering.com可以看到最新以及推荐的书籍。

这篇关于Real-Time Rendering 4th 译文《三 图形处理单元》的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/634838

相关文章

MySQL中时区参数time_zone解读

《MySQL中时区参数time_zone解读》MySQL时区参数time_zone用于控制系统函数和字段的DEFAULTCURRENT_TIMESTAMP属性,修改时区可能会影响timestamp类型... 目录前言1.时区参数影响2.如何设置3.字段类型选择总结前言mysql 时区参数 time_zon

Python 标准库time时间的访问和转换问题小结

《Python标准库time时间的访问和转换问题小结》time模块为Python提供了处理时间和日期的多种功能,适用于多种与时间相关的场景,包括获取当前时间、格式化时间、暂停程序执行、计算程序运行时... 目录模块介绍使用场景主要类主要函数 - time()- sleep()- localtime()- g

如何使用 Bash 脚本中的time命令来统计命令执行时间(中英双语)

《如何使用Bash脚本中的time命令来统计命令执行时间(中英双语)》本文介绍了如何在Bash脚本中使用`time`命令来测量命令执行时间,包括`real`、`user`和`sys`三个时间指标,... 使用 Bash 脚本中的 time 命令来统计命令执行时间在日常的开发和运维过程中,性能监控和优化是不

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

linux 下Time_wait过多问题解决

转自:http://blog.csdn.net/jaylong35/article/details/6605077 问题起因: 自己开发了一个服务器和客户端,通过短连接的方式来进行通讯,由于过于频繁的创建连接,导致系统连接数量被占用,不能及时释放。看了一下18888,当时吓到了。 现象: 1、外部机器不能正常连接SSH 2、内向外不能够正常的ping通过,域名也不能正常解析。

UMI复现代码运行逻辑全流程(一)——eval_real.py(尚在更新)

一、文件夹功能解析 全文件夹如下 其中,核心文件作用为: diffusion_policy:扩散策略核心文件夹,包含了众多模型及基础库 example:标定及配置文件 scripts/scripts_real:测试脚本文件,区别在于前者倾向于单体运行,后者为整体运行 scripts_slam_pipeline:orb_slam3运行全部文件 umi:核心交互文件夹,作用在于构建真

python内置模块datetime.time类详细介绍

​​​​​​​Python的datetime模块是一个强大的日期和时间处理库,它提供了多个类来处理日期和时间。主要包括几个功能类datetime.date、datetime.time、datetime.datetime、datetime.timedelta,datetime.timezone等。 ----------动动小手,非常感谢各位的点赞收藏和关注。----------- 使用datet

第六章习题11.输出以下图形

🌏个人博客:尹蓝锐的博客 希望文章能够给到初学的你一些启发~ 如果觉得文章对你有帮助的话,点赞 + 关注+ 收藏支持一下笔者吧~ 1、题目要求: 输出以下图形

一个图形引擎的画面风格是由那些因素(技术)决定的?

可能很多人第一直覺會認為shader決定了視覺風格,但我認為可以從多個方面去考慮。 1. 幾何模型 一個畫面由多個成分組成,最基本的應該是其結構,在圖形學中通常稱為幾何模型。 一些引擎,如Quake/UE,有比較強的Brush建模功能(或應稱作CSG),製作建築比較方便。而CE則有較強的大型地表、植被、水體等功能,做室外自然環境十分出色。而另一些遊戲類型專用的引擎,例

「大数据分析」图形可视化,如何选择大数据可视化图形?

​图形可视化技术,在大数据分析中,是一个非常重要的关键部分。我们前期通过数据获取,数据处理,数据分析,得出结果,这些过程都是比较抽象的。如果是非数据分析专业人员,很难清楚我们这些工作,到底做了些什么事情。即使是专业人员,在不清楚项目,不了解业务规则,不熟悉技术细节的情况下。要搞清楚我们的大数据分析,这一系列过程,也是比较困难的。 我们在数据处理和分析完成后,一般来说,都需要形成结论报告。怎样让大