图形学笔记
综述
图形学知识结构
什么是计算机图形学?
“计算的目的是洞察事物的本质,而不是获得数字。” ——Richard Hamming,1962
计算机图形学的核心目标是三个基本任务:表示、交互、绘制。表示是将主、客观世界放入计算机,通过数字对二维、三维对象进行建模与储存。绘制是将计算机中的对象通过直观的人眼易读的图形图像方式表现出来。交互是通过直观的图形图像手段,改善真实用户通过计算机录入、修改、获取数据的体验。
从图形学的核心思想的角度讲,图形学就是和颜色打交道。一切数学模型,一切算法,一切数据结构的最终目的,都是在正确的位置产生正确的颜色,仅此而已。除此之外的网格、模型、几何体、光照,都不是必须的。
计算机图形学的研究方向?
计算机图形学的主要研究对象是点、线、面、体、场的数学构造方法和图形显示,以及其随时间变化的情况,它需要研究以下几方面的内容:
- 描述复杂物体图形的方法与数学算法,包括曲面、曲线的造型技术,实体造型技术,纹理、云彩、波浪等自然景物的造型和模拟,三维场景的显示如光栅图形成生成算法和线框图、真实感图形的理论和算法。
- 物体图形描述数据的输入。
- 几何和图形数据的储存、压缩和解压。
- 物体图形数据的运算,如基于图形和图像的混合绘制、自然景物仿真、图形用户接口、虚拟现实、动画技术和可视化技术等。
- 物体图形数据的输出显示,包括图形硬件和图形交互技术。
- 实时动画和多媒体技术,研究实现高速高精度动画的各种软、硬件方法,开发工具、动画语言以及多媒体接口。
- 制定与图形应用软件有关的技术标准。
底层知识背景
显卡和GPU
GPU和CPU的区别?
主流CPU(Central Processing Unit,中央处理器)芯片上有四级缓存,消耗了大量晶体管,在运行时需要大量电力;主流GPU(Graphics Processing Unit,图形处理器)芯片最多有两层缓存,且GPU可以利用晶体管上的空间和能耗做成ALU单元,因此GPU比CPU效率高。
CPU重在实时响应,对单任务速度要求高,需要针对延迟优化,所以晶体管数量和能耗都需要用在分支预测、乱序执行、低延迟缓存等控制部分;GPU主要使用于具有极高可预测性和大量相似运算的批处理,以及高延迟、高吞吐的架构运算,对缓存的要求相对很低,顺序运算效率很高,同时相对的乱序处理效率很低。
CPU除了负责浮点和整型运算,还有很多其它的指令集的负载,如多媒体解码和硬件解码,CPU注重单线程性能,保证指令流不中断,需要消耗更多晶体管和能耗用在控制部分,于是CPU分配在浮点运算的功耗会减少;GPU基本只进行浮点运算,设计结构简单,效率更高,GPU注重吞吐率,单指令能驱动更多的计算,相比较GPU消耗在控制的能耗就少得多,因此可以将资源留给浮点运算使用。GPU的浮点运算能力比CPU高10~12倍。
什么是NVIDIA/AMD?
显卡品牌。
NVIDIA公司译为英伟达,其生产的显卡又被称为N卡。AMD译为超微半导体,其生产的显卡又被称为A卡。
N卡奉行大核心战略,GPU内部采用大量1D单元,在执行效率上理论可以达到100%,实际效率也可以维持在90%以上,因为架构执行效率高,灵活性强,所以在实际应用中易发挥应有性能。但是大核心的设计复杂,内部集成的SP数量也不会太多,成本和功耗也会比较高,控制单元在晶体管的消耗上占了更大比例,在相同晶体管数量的情况下,N卡能做的运算单元相对较少。N卡在软件上具有明显优势,包括微软在内的软件商都为N卡开发优化,使得大量工具软件和游戏在N卡环境下有更好的表现。
A卡奉行小核心战略,采用VLIW5或VLIW4的设LI计,分别采用4D+1D的设计和4D设计,可以在较小的晶体管代价和较小的核心面积下装入更多的SPU,以SP的数量取胜。其理论计算能力远超N卡,但实际执行效率并不高,一旦进入GPU的图形信息是1D或3D形式这一的非标准数据形式,A卡的执行效率最低可降至25%至20%。
显卡是个人计算机最基本组成部分之一,用途是将计算机系统所需要的显示信息进行转换以驱动显示器,并向显示器提供逐行或隔行扫描信号,控制显示器的正确显示,其高效的并行计算能力现阶段也用于深度学习等运算。
GPU和显卡有什么关系?
GPU是显卡上的核心处理芯片,显卡上除了GPU,还包括显存、电路板和BIOS固件等。由于GPU在显卡上十分重要,所以时常用GPU代指显卡。
显卡也叫显示适配器,分为独立显卡和集成显卡。独立显卡由GPU、显存和接口电路组成;集成显卡和CPU共用风扇和缓存,没有独立显存,而是使用主板上的内存。
显存是什么?
显卡储存体系的设计哲学是更大的内存带宽而非更低的访问延迟,这也是显存访问的特点:高带宽,高延迟。
显存既可以是物理上的,也可以是逻辑上的。
对集成显卡如Intel HD Graphics来说,GPU使用CPU专门划分出来的一份内存空间,即**UMA(Unified Memory Architecture,一致性储存架构)作为显存,GPU和CPU用不同的虚拟地址对UMA中的同一个物理地址寻址。使用集成显卡时,CPU和GPU共享总线。在渲染时,CPU将顶点等数据存入主存,然后GPU可以通过GART(Graphic Address Remapping Table,显存地址重定位表)**访问UMA。GART的作用是将UMA虚拟地址映射到GPU寻址空间。而由于UMA属于CPU内存范围,CPU可以直接访问它。
对独立显卡来说,GPU可以使用专门的显存条,并使用显存条的物理地址进行寻址,这是最常见的显卡类型。在独立显卡结构中,GPU可以直接从显存中读写信息。而CPU访问显存条中的储存空间时,需要映射一部分GPU储存空间到CPU地址空间,典型大小为256MB或512MB,CPU地址空间的获取一般由API完成。
无论使用哪种显卡,CPU和GPU交流必然要经过总线。独立显卡中CPU与显卡的沟通,是通过异步的**DMA(Direct Memory Access,内存直接访问)**实现的。主机将DMA命令块写入内存,DMA命令块由传输来源的源地址、传输目标地址和传输的字节数组成。CPU将这个命令块的地址写入DMA控制器,然后继续其它工作。随后DMA控制块会直接操纵内存总线,脱离主CPU的帮助下实现传输,将数据提供给显卡驱动。反过来,显卡驱动发送信息给DMA控制器请求线路,DMA控制器于是占用内存总线,并发送所需地址到内存地址总线,然后发送信号到DMA确认线路。当显存收到DMA确认信号时,他就传输数据到内存,并清楚DMA请求信号。每当一次沟通结束,DMA都会触发一次CPU中断。
什么是流处理器SP?
流处理器又称流处理单元,简称**SP单元(Streaming Processor)**或SPU,有些显卡生产商也会将其称作core(核心)。流处理器的数量能直接影响显卡性能。
之前的显卡具有两个重要的运算单元——顶点处理单元和像素处理单元。但自从DirectX10开始,微软引入了流处理器这个概念,顶点处理单元和像素处理单元很快被业界抛弃。流处理器是顶点处理单元和像素处理单元的统一,负责了渲染中的顶点和像素渲染。将顶点处理单元和像素处理单元合并的概念又被称作统一着色器架构(Unified Shader Architecture)。
业界之所以抛弃之前的顶点+像素结构而使用SPU架构,是因为传统的顶点和像素分离渲染架构存在严重的资源分配不均的问题,两种单元渲染任务量不同,效率低下。而SP架构是统一结构,不再区分顶点和像素渲染,进行不同渲染任务时都能保证效率。
4D+1D/4D/1D都是什么?
D是维度Dimension的意思,在图形学中的nD指n维浮点向量运算,nD单元指由n个流处理单元整合成的n维浮点向量运算单元。
像素坐标XYZW、色彩参数RGBA以及纹理坐标参数STPQ正好都是4维运算,这导致顶点处理单元和像素处理单元都是4D单元,在引入流处理器后,主流的流处理器也是4D单元。
而1D即一维向量,也就是标量。由于流处理器合并了顶点和像素处理单元,图形渲染中标量运算成分开始增多,GPU不再像早年那样只需要处理单纯的4D向量运算了。在这样的背景下,英伟达完全抛弃4D结构,设计了G80这样的1D标量处理器,将矢量运算分解为4次或更多次标量运算,这使N卡的灵活性大幅提升,在任意维度的运算环境下都可以得到满意的性能。
AMD没有放弃4D架构,而是进行了改良,增加了一个标量运算单元,这就是4D+1D矢量标量混合架构,也就是VLIW5(Very Long Instruction Word,超长指令口令)架构,它把需要计算的指令组合成适合4D+1D架构的长指令,比如将一个2D运算和一个3D运算合并为一个4D+1D运算,这样理论上每个统一处理器每个周期都可以进行一次4D运算加一次1D运算,是N卡1D单元运算效率的4~5倍,这种将指令组合的算法被称为co-issue算法。这五个ALU只需要一个发射端口,电路设计更加简单,功效与发热也更容易控制,但缺点就是依赖指令组合,一旦非最优指令组合,这些运算单元中部分维度就只能空转,运算效率将显著降低。
什么是流多处理器SM?
SM(Streaming Multiprocessor,流多重处理器)由多个SP加上共享内存、特殊函数单元、寄存器、多边形引擎、指令缓存和L1缓存等的组合。
SM中的任务主要由SP承担,SM中SP的数量一般为32个,有时也有16或64个SP组成的SM。进行辅助计算的还有SFU(Special Function Units,特殊数学运算单元),它们用于进行三角函数和指对数等运算。
SM由Warp Scheduler(束管理器)驱动,束管理器会将指令移交给Dispatch Unit(指令分派单元),由于SM中每束处理的事务具有相似性,这个单元会从指令缓存新的读取指令,并一次性向整束的所有流处理器发送同一个指令。这种通过一条指令驱动若干线程的特性被称为SIMT(Single Instruction Multiple Thread,单指令多线程),在这个框架下,指令分派单元可以读取一条指令,然后向多个SP分派不同的参数,以让它们在不同的寄存器地址进行读写。
SM中一般配有一个多边形引擎(Polymorph Engine)。这个引擎的作用是实现属性装配、顶点拉取、曲面细分、裁剪和光栅化等渲染流水线中的固定步骤。
每个SM中具有一个足够大的寄存器,一般能达到128KB。所有SP共用这个寄存器中的空间,所以如果单个线程需要的寄存器空间过大,可能使每束的最多线程数减少,影响并行性。
L1缓存是开始时用于储存顶点数据的缓存,在顶点处理阶段结束后,SM会将处理结果送到SM外的L2缓存中。多边形将在SM外进行光栅化,然后将生成的片元重新分发到SM中,这时L1缓存中储存的就是片元数据了。
一个显卡上可能有10~20个SM,一般显卡厂商将若干SM的组合称为一个GPC(Graphic Processor Cluster,图形处理簇),每个簇可以处理一批(batch)顶点或片元。在有GPC结构的显卡上,L2缓存一般位于GPC中,而对于没有GPC结构的显卡,L2一般位于显存旁或显存中。
显卡有什么样的储存结构?
显卡中的物理储存器按存取速度的大小从快到慢依次有:寄存器、共享内存、L1缓存、L2缓存、纹理缓存、常量缓存、全局内存(显存)。
寄存器位于SM中,访问速度是1个时间周期,SP运行时可以随意的读和写寄存器。在指令分派单元的控制下,每个线程都会获得自己的寄存器空间,
共享内存和L1缓存都位于SM中,它们都可以被SM中的所有SP共用。L1缓存重点存放顶点和片元数据,而共享内存重点存放材质参数、光照、摄像机等常量数据。共享内存和L1缓存的访问速度都低于32个时间周期。
L1缓存和L2缓存重点用于顶点和片元数据的交换。L2缓存位于SM之外,它的访问速度相对L1较慢,大致需要32至64个时间周期,但由于大量SM会共用一个L2缓存,L2缓存的吞吐量是数个L1缓存的总和。在有些显卡设计中,L2缓存还可以作为纹理缓存和常量缓存的新一层缓存,供其它显卡硬件使用。
纹理缓存和常量缓存是显卡上重要的缓存类型,它们都是显存的直接缓存,访问延迟在400个时间周期以上,如果出现未命中,要等待访问显存的话,这个延迟甚至很可能超过1000个时间周期。
全局内存也就是显存,它的数据直接来自CPU总线。显卡驱动会在GPU流水线空闲时将任务数据从GPU缓冲区导入显存。显存的访问延迟在500个时间周期以上,显卡的访问一般在纹理缓存和常量缓存缺失时才会发生。但存在一种高吞吐GPU结构,使用更大量的GPU而减少了缓存的层数,这种结构中显存可能直接被共享内存访问,通过高吞吐量与高带宽来缓解缓存层级少带来的性能缺陷。
GPU是怎么应对阻塞的?
GPU不像CPU那样实现流水线,因为设计者认定GPU中的一批数据应当具有相对固定的处理过程,比如在一个屏幕后期处理的draw call中,GPU可能处理了1920x1080个片元,但对每个片元运行相同的一套算法。
GPU并不关心跳转,其性能瓶颈主要来自读取显存产生的阻塞而非跳转产生的阻塞。由于纹理数据储存在显存中,在每次遇到采样语句时,为了将显存中的数据调入核心,可能需要使处理器阻塞几百上千个时钟周期。
为了缓解这样的问题,GPU使用**换入换出操作(swapping)**来隐藏延迟。
SM中的寄存器可以备份若干份SP的执行状态。每当有一批片元在等待采样纹理数据时,可以将此时SP组的上下文备份保存在寄存器中,然后导入下一批片元进行处理,这个操作被称为换出(swap-out)。换出使得核心不需要空等采样结果,可以继续执行更多的片元。
由于GPU中的数据具有相对固定的处理过程,我们认为每一批片元都会在相同的周期后遇到一次换出操作。假设我们的GPU中只有一个核心(当然不存在这样的GPU),运行一批具有2000个片元的数据,这2000个片元都会在运行完前5个周期后遇到第一次采样语句,然后被执行换出。假设采样结果从显存中返回到核心需要1000个时间周期,那么在运行完第200个片元并将其换出后,第1个片元所需的采样数据就可以抵达核心,这时核心将第1个片元的上下文从寄存中拷贝回SP中,继续进行这个片元采样之后的运算,直到它再次被换出,这个操作被称为换入(swap-in)。如果这些片元在采样语句结束后还有5个时间周期用于算数运算,那么第1个片元执行完时第2个片元也恰好可以被执行换入。以此类推,执行完每200个片元所用的总时间是2000个时间周期,假如我们不使用换入换出操作,200个片元所用的时间将是200200个时间周期。
换入换出的延迟仅仅只有不到10个时间周期,在实际的应用中,不止是采样运算,延迟超过30个周期的很多运算都会使用换入换出操作。
当然,在上面的例子中,加入我们没有2000个片元数据,而仅有20个,那么我们不得不在运行完第20个片元的换出后等待1900个周期才能继续运算。实际的应用中,如果每个线程需要的寄存器数越多,就意味着每束处理的线程数越少,能被用于储存镜像的空间也越少,镜像短缺意味着换入换出策略的效果大幅下降,将严重影响GPU的并发执行。
GPU是怎么实现并行的?
执行一个着色器的最小单位是线程(thread)。多个线程被打包在一起称为线程束(英伟达称之为warp,AMD称之为wavefront,可以统一翻译为线程束)。多个线程束被打包为一个线程组(block),每个线程组中的所有线程可以通过共享内存来通信,不同线程组中的线程是无法通信的。
在实际运行中,处理器会将一个线程组分配给一个SM。每个SM获得线程组后,会通过束管理器将其分为若干线程束,每个线程束中线程的个数一般等于流多重处理器中流处理器的个数,如果一个SM被分为多个束,则线程束中线程的个数等于一束中流处理器的个数。如一个具有100个线程的线程组分配给一个具有32个流处理器的SM,SM会将其分为4个线程束,第031号线程分为第1束,第3263号线程分为第2束,第6495号线程分为第3束,第9699号线程分为第4束。在运行时,每个线程束中的32个线程会被分配到32个流处理器中进行并行运算。由于GPU任务的相似性,这32个线程很可能在同一时间遇到换出操作,那么SM就会安排下一个线程束进入流处理器。
动态分支语句如何影响GPU并行效率?
动态分支包括if语句和循环语句。它们在着色器语言中存在,但可能严重影响GPU性能。
在一个线程束中,如果线程中不存在动态分支语句,那么它们的所有行为都是可预测相同的。但一旦遇见一次动态分支语句,就可能产生分裂。如果线程束中所有线程执行相同的分支,那么运行结果不会有什么不同。但我们提到过,在SM中的一束线程共享同一份指令,但凡有一个线程执行另一个分支,那么整个线程束就不得不被执行两遍,将两个分支的结果都运行一次,并让每个线程扔掉它们各自不需要的结果。如果在着色器编写中出现连续的分支预测,甚至复杂的循环语句,每个线程束的执行次数可能呈指数级递增,这个效应被称为线程分歧(thread divergence)。
根据此我们也可以发现,使用循环语句运行常数次来读取某数组中的信息并不会导致线程分歧,线程分歧的严重与否关键在于相邻的元素通过动态分支语句能否得到基本相似的分支。
什么是垂直同步?
早期CRT显示器中,电子枪从上到下进行扫描,扫描完成后显示器就显示一帧画面,然后电子枪回到初始位置进行下一次扫描。为了同步显示器的使用过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号:当电子枪换行扫描时,显示器会发送一个水平同步信号,简称HSync,当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称VSync。
技术升级带来液晶屏后,视频控制器(Vedio Controller)仍在根据同步信号逐帧读取帧缓冲区的数据。为了缓解单缓冲下帧缓冲区的读取和刷新效率低的问题,GPU中常用两个缓冲区,即双缓冲机制,在一帧渲染完后,视频控制器会将两个缓冲互换。由视频控制器直接读取的缓冲区称为前缓冲区,而在后台承担渲染任务的叫后缓冲区。
双缓冲会引入一个新问题:在视频控制器读取前缓冲区只读取了一半时,GPU将新的一帧内容提交到帧缓冲区,并把两个缓冲区交换。视频控制器可能把新一帧数据的后半段渲染到了屏幕上,这使得屏幕得上半部分和下半部分分属不同得两帧,产生画面撕裂。
为了解决这个问题,GPU等待显示器发送得垂直同步信号来交换缓冲区,垂直同步信号沿用了曾经得VSync这一称谓。垂直同步解决了画面撕裂现象,也增加了画面流畅度,但需要消费更多计算资源,也会带来部分延迟。
图形API和着色语言
什么是OpenGL/DirectX?
图像编程接口(图形API),是对GPU硬件的抽象,其地位类似C语言,属于GPU编程的中低层。几乎所有GPU都既可以和OpenGL合作也可以和DirectX合作。
OpenGL和DirectX的区别?
OpenGL是纯粹的图形API;DirectX是多种API的集合体,其中DirectX包含图形API——Direct3D和Direct2D。
DirectX支持Windows和Xbox;OpenGL支持Windows、MacOC、Linux等更多平台,在Android、IOS上允许使用OpenGL的简化版本OpenGL ES。
OpenGL相对来说易上手门槛低;DirectX难上手门槛高。OpenGL渲染效率相对低,特性少;DirectX相对效率高,特性多。如DirectX12提供了底层API,允许用户一定程度上绕过显卡驱动之间操纵底层硬件。
二者在图形学领域一样重要。OpenGL在各种领域都吃香,包括许多专业领域如特效和CG建模软件。DirectX在游戏中更通用。
什么是显卡驱动?
一个应用程序向显卡接口发送渲染命令,这些接口会依次向显卡驱动发送渲染命令。显卡驱动的地位类似于C语言编译器,可以将OpenGL或DirectX的函数调用翻译成GPU能读取的机器指令,即二进制文件。显卡驱动同时也负责把纹理等数据转换成GPU支持的格式。
什么是HLSL/GLSL/CG/CUDA?
HLSL、GLSL和CG是着色语言,专门用于编写着色器。其中HLSL(High Level Shading Language)属于DirectX,GLSL(OpenGL Shading Language)属于OpenGL,而CG(C for Graphic)是NVIDIA研发的,因为英伟达与微软合作密切,CG语法与HLSL极其相似。
CUDA(Compute Unified Device Architecture,统一计算架构)也是NVIDIA研发的,和前三者类似,但并不专注于图形领域,常用在机器学习等领域,不需要像着色语言那样使用图形计算的逻辑进行数字运算。
Draw Call
什么是Draw Call?
调用一次图像编程接口(图形API),以命令GPU进行渲染的过程就称为一次Draw Call。
Draw Call的流程是什么?
Draw Call的准备工作由CPU完成。
第一步,CPU把一个网格的顶点数据从硬盘中加载到内存中(存在这一步的原因是大规模3D渲染中内存可能不足)。
第二步,CPU对这个网格设置渲染状态(每个网格不等于每个模型/图片,因为存在批处理)。所谓渲染状态包括纹理贴图、材质属性和被编译为二进制文件的着色器。随着渲染状态一起被传递到GPU的还有光照和摄像机相关的信息。图形API可以更深层次的定义渲染状态需要的数据。
第三步,CPU将网格顶点数据与渲染状态打包,将数据包按照指定格式交给DMA,由DMA将数据包传入显卡。
显存完成接受数据包后,DMA向CPU返回一个中断信号,Draw Call正式结束。
GPU是如何接受Draw Call的?
指令到达显卡驱动程序后,驱动会首先检查指令的合法性,如果指令非法,驱动会通过DMA向CPU发送错误信息。如果指令合法,驱动通过DMA确认Draw Call接收,然后将指令放入GPU缓冲。
一段时间后当显卡中存在空闲流水线,或者CPU显式发送flush命令后,驱动程序把缓冲区中的一份指令发送给GPU,GPU通过主机接口接受命令,并开始处理命令。
GPU将所有顶点存入顶点缓冲区(Vertex Buffer),GPU中的**图元分配器(Primitive Distributer)**开始通过顶点生成三角形,并将他们分成批次(batch),发送给一个或多个GPCs,如果显卡不存在GPCs,则直接分发给SMs。SM获得数据后,束管理器安排多边形引擎将三角形数据提取出来存入SM的L1缓存,随后开始顶点着色器阶段。
GPU会依次处理缓存中的每一个网格,网格的处理顺序与CPU的提交顺序有关。正因如此,CPU总是最后提交透明物体。等一帧中的所有Draw Call处理完毕后,显示器才会将图像打印在屏幕上。
什么是批处理?
对2D物体(UI)来说,如果两个或多个UI元素符合以下条件则可以被批处理:
- 使用相同的材质与纹理,即使用相同渲染状态。
- 这些元素的层级相同,或这些元素之间不夹杂使用其它渲染状态的元素的层级。
对网格(模型)来说,如果两个或多个网格符合以下条件则可以被批处理:
- 使用相同的材质与纹理,即使用相同渲染状态。
- 顶点总数不超过一个阈值,该阈值大小与使用的处理引擎有关。
为什么要进行批处理?
Draw Call的性能瓶颈是CPU而非GPU。CPU每进行一次Draw Call,都要调用一次DMA将数据输入显存。在每次调用时,对显存的映射寻址、DMA控制块的注入、等待DMA响应等系统消耗都会浪费时间周期,多次进行Draw Call就会有多次消耗。同时,DMA擅长一次传输大量数据,而不擅长多次传输少量数据。这使得降低Draw Call对于优化显示性能很有必要。
批处理可以显著降低Draw Call。通过将类似的网格当作同一网格,CPU可以在一次Draw Call中将更多的数据传输到GPU中,降低系统消耗。
渲染流水线
综述
渲染流水线的运行过程是怎样的?
渲染流水线是在显存中开启的。CPU将网格、材质、贴图、着色器等注入显存后,GPU开始渲染流水线。
渲染流水线分为几何阶段和光栅化阶段(也被称为像素阶段),并最终将运算结果送到显示器的缓冲区中。
几何阶段分为顶点着色器->曲面细分着色器(DirectX11和OpenGL4.x以上可编程)->几何着色器->裁剪->屏幕映射五个步骤。
光栅化阶段分为三角形设置->三角形遍历->片元着色器->逐片元操作四个步骤。
显卡厂商会通过硬件实现常用且功能变化不大的几个流水线阶段,因为通过硬件实现的效率远高于软件实现。这些阶段优的根据API的设计,提供了一些可以设置的参数,但总的来说不会脱离GPU的控制。
在这个过程中我们可以知道,顶点着色器和片元着色器的线程数并不是等同的。片元着色器的线程数变化幅度一般不大,而顶点着色器的线程数变化幅度随应用不同可能大幅浮动。
如何理解GPU渲染流水线和CPU指令流水线的区别?
渲染流水线重点强调的和CPU的指令流水线并不同。
在CPU指令流水线中,我们强调的是:不需要等待一个指令处理完毕再读取下一条,而是在指令处理完一个阶段后开始处理下一条。这里考虑的重点是两条指令间的流水处理。
对于渲染流水线,一种常见的误解是认为渲染流水线和指令流水线一样,不同阶段处理的是不同的Draw Call,较后的阶段处理着较早提交的Draw Call,较前的阶段处理着较晚提交的Draw Call。**这种理解是错误的。**正确的理解是,渲染流水线中,不同阶段运行的是同一次Draw Call,但一次Draw Call中先提交的数据可以不等待它之后的数据就进入下一个流水线阶段。
几何阶段
顶点着色器是如何工作的?
顶点着色器可以深度自定义,开发者需要根据显存中的原始数据(raw data)输出顶点颜色和顶点的齐次裁剪空间坐标。
束管理器将第一批顶点数据(如果一束中有32个线程,则存入32个顶点)以及其它需要的参数存入寄存器,然后将顶点着色器代码存入指令缓存,并要求指令分派单元从指令缓存中读取指令分派给SP。这个过程结束后,寄存器中必须至少保存着所有顶点的目标位置和顶点颜色。如果还有其它需要留给其它流水线部分的数据,也可以存放在寄存器中。在这之后,SM中顶点着色器阶段产生的运算结果会被存入L1缓存,然后由GPU将多个SM的结果组合冲入位于SM结构外的L2缓存。
在硬件中,将齐次裁剪空间坐标通过透视除法转化为NDC(Normalized Device Coordinates,归一设备坐标)。不同显卡的NDC格式可能不同,其中OpenGL中的NDC是左手坐标系,位于坐标范围在(-1,-1,-1)至(1,1,1)的立方体中。
曲面细分着色器是如何工作的?
曲面细分着色器又被译为镶嵌着色器,它是一个可编辑着色器,在显卡上的一个名为**视口变换器(Viewport Transform)**的硬件模块上运行实现,一个显卡上可能有1~4个视口变换器集成电路,它们有些是可编程的有些是不可编程的,在具有GPC结构的显卡中,视口变换器一般位于GPC外。
视口变换器可以从L2缓存中获取它需要的信息,并将L2缓存作为它与显存沟通的桥梁。
曲面细分着色器将复杂的曲面转换为简单的点、线、三角形。曲面细分着色器可以递归的增加网格细度,并在细分后的顶点上生成插值色彩。由于它处理了邻接顶点的信息,它处理的结果会更加平滑,而不会产生跳跃间隙。
通过细分,模型外观会更加平滑,且颜色的过度也会更加自然。
在比较老旧的API版本中曲面细分着色器由硬件实现无法编程。在DirectX11以上或OpenGL4.x以上的版本中,可以通过API编辑这个着色器的工作任务。
几何着色器是如何工作的?
几何着色器是一个可选着色器,也在视口变换器上实现。
几何着色器同样也可以实现曲面细分,但它的实现效果并不好。
几何着色器用于实现逐三角形的操作。由于相比顶点或片元数量,三角形数量并不多,所以这一步并没有分派到多SM中实现。几何着色器在投影的接受上十分重要,它还能实现扩展几何图形和绘制简单的粒子。
裁剪阶段是如何工作的?
裁剪也由视口变换器实现,但它完全不可编辑。
GPU会将完全留在视野范围内的三角形保留,将完全在视野范围外的三角形抛弃,将部分留在视野范围内的三角形修正为新的几何体,即生成新的顶点,抛弃原来在视野外的顶点。
屏幕映射是如何工作的?
屏幕映射紧跟在裁剪阶段之后,也由视口变换器负责,不可编辑。
屏幕映射简单来说就是将NDC中的坐标转换到屏幕坐标系(Screen Coordinate)。所谓屏幕坐标系,就是将范围在(-1,1)的x和y轴坐标,缩放到与目标分辨率相同大小,而z坐标则不做处理。注意,目标分辨率不一定等同于屏幕分辨率。这个阶段将输出屏幕坐标系下的顶点坐标、顶点深度、顶点法线方向、视角方向等顶点属性。
在OpenGL中,屏幕的左下角为屏幕坐标系原点,右上角为坐标最大值。而在DirectX中,屏幕左上角为最小屏幕坐标,而右下角为最大屏幕坐标。这个差异是OpenGL和DirectX很多不兼容性产生的源泉。
光栅化阶段/像素阶段
三角形设置是如何工作的?
这个阶段由视口变换器负责,不可编辑。
在这个阶段,视口变换器先将顶点坐标转化成像素坐标,也就获得三角形的像素坐标,即网格。通过顶点的像素坐标,GPU得知哪些网格与哪些顶点有关,并在扫描的同时将网格打包给对应的GPC处理。
GPU的处理策略是,处理过某批顶点的SM,尽量用来处理由同一批顶点生成的片元。
三角形遍历是如何工作的?
视口变换器将打包好的网格数据交给GPC后,GPC会将这些网格交给名为ROP(Raster Operations Units,光栅化引擎)的元件,在这里网格被进扫描变换(Scan Conversion),ROP中的元件ROPU并行地计算像素是否被网格覆盖,如果是,则产生一个片元(fragment),其中片元的状态是对网格3个顶点的信息进行插值得到的。ROP不可编程。
在生成片元的同时,ROP还会同时进行裁剪、背面剔除和早期深度剔除。这几个操作是可以通过API进行配置的,但不可编程。
片元还不是一个像素(pixel),一个片元是用于生成一个像素的数据包,它包含了坐标、颜色、深度、法线、导数和纹理坐标等一系列计算像素所需要的数据。而像素则是片元经过整个光栅化阶段后,由片元所含的数据计算得出的,仅包含坐标和颜色信息。
在生成片元后,ROP将片元分配给同一个GPC中的几个SM。
片元着色器是如何工作的?
片元着色器可以深度编程,开发者需要根据提供的片元数据输出一个像素颜色。
束管理器会将片元数据存入寄存器,然后将片元着色器代码存入指令缓存,并要求指令分派单元从指令缓存中读取指令分派给SP。每一批次中指令会从寄存器中取出若干片元数据开始处理,如果一束有32个线程,则就是32个片元,准确来说是8个2x2的片元块,2x2是片元着色器的最小工作单位。所有线程运行完后,寄存器中必须生成所有片元的目标颜色。
这些计算得到的目标颜色会和片元坐标一起存入L1缓冲,然后由GPU将多个SM的结果组合冲入L2缓存。
为什么片元着色器使用2x2的工作单位?
在片元着色器中会将四个相邻像素作为不可分割的一组送入同一个SM内的4个不同的SP中。这么做可以精简和加速像素分派的工作并精简SM的架构,降低功耗。注意,2x2块中可能存在无效像素,当网格覆盖的片元不是完整的2x2块时,比如说一个网格只覆盖了单个片元,那么在进入片元着色器时,会将它与相邻的3个空片元绑定到一起,这会导致有3个SP空转。在极端环境下,整个网格可能全部都处于这样的状态,使得SM的效率低至25%。这种为了覆盖完整2x2片元而浪费资源的情况被称为过度渲染(Over Draw)。
逐片元操作(输出合并阶段)是如何工作的?
这个步骤在显卡上的一个名为渲染输出器的元件中实现,它从L2缓存中按照三角形的原始API顺序读取片元,处理可见性测试和混合。这两个个过程是可配置的,但不可编程。
以模板测试和深度测试为例:渲染输出器会首先将片元与模板缓冲(Stencil Buffer)中的模板值比对,舍弃没有通过模板测试的片元。片元通过模板测试后,渲染输出器就会将该片元与深度缓冲(Z-Buffer)中的深度信息比对进行深度测试,舍弃掉没有通过深度测试的片元。通过深度测试的片元就会与后置缓冲区(Back Buffer)中的像素进行混合。由于数据是高度可并行的,渲染输出器中的多个渲染输出单元会并行的执行这个过程。
在像素混合时,深度和颜色的设置必须是原子操作,否则会发生同步异常。
在一次渲染结束后,视频控制器会将后置缓冲区与前置缓冲区(Front Buffer)交换,而显示器可以读取前置缓冲区中的像素进行打印。使用前置和后置两个缓冲区的这种策略被称为双重缓冲区(Double Buffer)策略,二者合称为帧缓冲区(Frame Buffer),它可以保障显示器显示的连续性,由于渲染过程始终在幕后发送,可以避免显示器打印出正在处理中的图元以致于产生屏幕撕裂。
什么是可见性测试?
透明度测试是简单的将透明度低于开发者设置的阈值的片元丢弃,透明度测试不能用于实现半透明效果,只能用于实现镂空效果。实现半透明应该使用透明度混合。
模板测试有一个对应的模板缓冲,这个缓冲区有一个大小等于目标分辨率的数据结构,对每一个像素储存了一个整数。模板测试是高度可编程的,可以通过图形API设定在模板缓冲中的什么部位写入什么数值,在渲染其它网格时可以将这些数值读取出来进行测试。比如“魔镜”效果:场景中有一些通常不能被看到的“幽灵”物体,只有透过一个
“魔镜”去观察才能看到幽灵物体。这时我们可以让“魔镜”进行模板写入,将其覆盖的部分模板值写为1,而让幽灵物体进行模板测试,只渲染模板值为1的片元,以实现这个效果。
模板测试和模板写入的编程自由度非常大,可以使用很多数学运算和逻辑运算,这使得模板测试有很多高级的用法,如渲染阴影和渲染轮廓等。
深度测试用于抛弃那些被其它片元遮挡的片元,它是高度可定制但不可编程的。与深度测试对应的是深度写入,深度缓冲记录着当前离摄像机最近的片元的深度坐标,只有深度比这个片元更小的片元才有权利通过深度测试并将新的深度写入对应区域。我们可以关闭深度测试或深度写入,比如透明或半透明物体应该关闭深度写入,因为我们不希望透明物体遮挡它背后的片元。
什么是混合?
在一个片元写入后置缓冲区中时,后置缓冲区中的对应位置有可能已经存在有像素信息了。妥善的处理旧的和新的像素信息就是混合要做的事。混合操作是高度可订制但不可编程的。
最常见的,同时也是默认混合策略是覆盖(Cover),有时叫做关闭混合(Blend Off)。覆盖策略将旧像素信息丢弃而用新像素信息重写它,这种策略在所有不透明物体上使用,因为通过了深度测试的不透明新片元显然会遮挡旧片元。
对半透明物体来说,最常见的混合策略是透明度混合(Alpha Blend),其思路是将新旧像素对透明度带权进行加法,得到新的颜色存入后置缓冲区。
总结归纳
流水线中有多少缓冲区?
- 顶点缓冲区(Vertex Buffer):由于GPU与CPU是异步的,顶点缓冲区被用于平衡两种速度不一致的硬件。通过顶点缓冲区,GPU可以访问CPU设定的顶点数组,通过图形API我们可以手动定制顶点缓冲区的大小。
- 帧缓冲区(Frame Buffer):分为前置缓冲区和后置缓冲区,通过交换两个缓冲区可以保证显示器渲染的连续性,避免屏幕撕裂。帧缓冲区的大小主要由颜色缓冲区的大小决定。
- 颜色缓冲区(Color Buffer):颜色缓冲区是帧缓冲区的一部分,和帧缓冲区、显示器中的视频控制器相连。颜色缓冲区早期用4个字节来储存颜色,俗称十六位图,但现在的计算机一般通过32位RGBA储存颜色,俗称真彩。实现了HDR技术的显示器配置的显卡,可能具有64位RGBA的颜色缓冲区。
- 深度缓冲区(Z-Buffer):如果场景中两个物体在同一个像素产生片元,GPU会比较二者的深度,保留离观察者较近的物体。如果两个片元的深度一致,由于GPU的并行性,无法确定某个片元始终处于另一个之上,进而使这两个片元出现闪烁,这个效应被称为深度冲突(Z-Fighting)。深度缓冲位数过低时,深度冲突发生的可能性就会增加,目前的深度缓冲一般使用24位或32位精度。
- 模板缓冲(Stencil Buffer):模板缓冲为每个像素保存一个无符号整数值,这个值的含义由开发者定义。模板缓冲是完全面向开发者的缓冲区设计,可以用于实现很多有趣的功能。模板测试发生在透明度测试之后,深度测试之前。一般的模板缓冲使用8位无符号整数。
- 几何缓冲(Geometry Buffer,或G-Buffer):详情见什么是前向渲染,什么是延迟渲染?
缓冲区内存该如何计算?
假设屏幕在真彩模式下显示一个2160×1080的图像,那么每个像素需要4个字节储存颜色,那么单个颜色缓冲需要的空间是:
2160
∗
1080
∗
4
B
=
8.90
M
B
2160*1080*4B=8.90MB
2160∗1080∗4B=8.90MB
使用双缓冲区技术,则空间翻倍,每个像素使用8个字节储存颜色,再加上24位的深度缓冲,8位的模板缓冲,现在占用的空间是:
2160
∗
1080
∗
(
2
∗
4
B
+
3
B
+
1
B
)
=
26.70
M
B
2160*1080*(2*4B+3B+1B)=26.70MB
2160∗1080∗(2∗4B+3B+1B)=26.70MB
如果使用抗锯齿处理,比如超级采样或多重采样,需要的储存空间会更多。
什么是前向渲染,什么是延迟渲染?
前向渲染和延迟渲染是两种光照渲染模式。
假设有一个光源和1000个具有光照反射的三角形在NDC沿z轴正方向延申摆放,法线与z轴平行,所有三角形全等,旋转和缩放相同,仅有z轴坐标不同。从屏幕上实际你只能看到一个带光照的三角形,其它的都被挡住了。
前向渲染会这样做:
- 取出一个片元
- 进行深度检测,抛弃没有通过的片元
- 片元着色器对通过的片元进行光照计算
- 更新帧缓冲区
- 返回第一步直到遍历结束
由于GPU的并行性,我们不能控制GPU取出片元的顺序。在极端条件下,1000次深度检测全部都能通过,那么光照计算会进行1000次,但由于实际上999次都被覆盖了,所以有999次多余计算。
延迟渲染引入了G-Buffer,它会这样做:
- 取出一个片元
- 进行深度检测,抛弃没有通过的片元
- 对通过的片元,将坐标、光照等信息写入G-Buffer
- 返回第一步直到遍历结束
- 从G-Buffer中取出一个像素的几何信息
- 片元着色器利用G-Buffer中的信息进行光照计算
- 更新帧缓冲区
- 返回第五步直到遍历结束
延迟渲染把参数保存了下来,没有像前向渲染那样边运行片元着色器边进行输出合并,而是先完成完整的深度检测,再运行片元着色器,对于每个像素只进行一次光照计算就实现了效果,大大节约了光照计算复杂度。光源越多、计算越复杂,节省下的性能就越明显。
然而,延迟渲染只能给屏幕上的每一个点保存一份光照数据,所以如果这些三角形都是半透明的,延迟渲染就不能体现出半透明的细节。换句话来说,延迟渲染完全不支持Blend。同理,延迟渲染也不能实现多重采样抗锯齿的功能。
一般的G-Buffer精度为64位,旧的分配方式是分别使用16位浮点数储存Normal.x、Normal.y、深度信息和漫反射颜色(十六位图)。一种新的分配模式是去掉深度,同时使用8位浮点数分别储存Normal.x、Normal.y、漫反射颜色、高光颜色,再使用24位储存RGB色彩,这样还留下了一个空闲的8位通道用作机动,并且色彩精度也提升了。新分配模式的问题是normal位数下降了很多必须通过片元着色器来代行平滑。
新的支持延迟渲染的显卡可能提供超过64位的精度,可以使延迟渲染的效果更上一层楼。