接上节。由于这一整章都是概括内容,所以有个印象即可,具体算法后面还会继续说。这节我会加点其他的知识和自己的注释,一般放在括号里了。如果有误欢迎指出~
3.5 顶点着色器
我们再拿出这张图看一眼。会发现顶点着色器处于第一阶段。
1. 实际上在顶点着色前,一些数据就已经被操作过了,DX把这个叫做input assembler,数据流可以被编制在一起,来构建一系列到管线中的顶点和片元。比如一个object需要顶点位置和顶点色,那么就把这种需要顶点位置和颜色的数据结构提前整合到一起,这样第二个物体也可以使用这种结构了。同样,DX也支持实例绘制。
2. 每个模型都由三角形组成,三角形网格上有表示位置信息的顶点。除了位置之外,每个顶点还可以包含其他可选属性,比如颜色和纹理坐标。三角形顶点也可以包含法线,这好像挺奇怪的,为啥不直接用三角面片的法线呢?毕竟从数学上讲,每个三角形都有一个明确定义的表面法线,直接使用三角形的法线进行光照似乎更合理。然而,在渲染时,三角网格通常用于表示一个潜在的曲面,而顶点法线用于表示该曲面的取向,而不是三角网格本身的取向。(嗯……这么翻译看起来好像挺费解,其实有了顶点法线我们就可以插值了对吧?当然插值还分为光栅化前插值和光栅化后插值,错误的插值时刻会导致错误结果,这个我们后面会讲)
3. 顶点着色器是处理三角形网格的第一个阶段。用于描述三角形结构的数据对顶点着色器不可用(顶点着色器是并行的,因此不关心顶点之间的关系)。顾名思义,顶点着色器专门处理输入的顶点。顶点着色器可以对每个三角形顶点的相关值进行修改、创建或忽略,例如颜色、法线、纹理坐标和位置。通常,顶点着色器程序将顶点从模型空间转换为齐次裁剪空间。顶点着色器至少始终输出位置信息。(我们写shader的时候,顶点着色器输入是POSITION语义,输出是SV_POSITION语义,因为输出是经过MVP变换后的位置,也就是经过齐次除法之后的位置)
4. 每一个顶点都被顶点着色器单独处理,然后输出一些列数据用来形成三角面或者线段。顶点着色器既不能创建也不能销毁顶点,一个顶点的处理结果也不能传递给另一个顶点,因为每个顶点都被独立处理,因此任何在GPU中的进程都可以被应用到顶点数据的输入流中。(注意物理上输入数据是在顶点着色阶段被处理的)
(比如我们看下列代码就是一个包含顶点位置、法线、切线、纹理坐标的结构体)
struct Vertex
{
Vertex() {}
Vertex(
const DirectX::XMFLOAT3& p,
const DirectX::XMFLOAT3& n,
const DirectX::XMFLOAT3& t,
const DirectX::XMFLOAT2& uv) :
Position(p),
Normal(n),
TangentU(t),
TexC(uv){}
Vertex(
float px, float py, float pz,
float nx, float ny, float nz,
float tx, float ty, float tz,
float u, float v) :
Position(px, py, pz),
Normal(nx, ny, nz),
TangentU(tx, ty, tz),
TexC(u, v) {}
DirectX::XMFLOAT3 Position;
DirectX::XMFLOAT3 Normal;
DirectX::XMFLOAT3 TangentU;
DirectX::XMFLOAT2 TexC;
};
顶点着色器可以制作顶点动画,轮廓渲染等。顶点着色器的其他用途包括:
- 物体生成,通过顶点着色器扭曲顶点来制作一次mesh
- 利用骨骼和morthing技术来制作身体和脸部的动画
- 程序化变形,比如旗帜,衣服和水流等
- 粒子生成,通过发送退化(无面积)网格到渲染管线中,根据需要赋予它们面积来创建粒子。
- 通过将整个帧缓冲区的内容作为纹理应用到屏幕对齐的网格上,并对该网格进行程序化变形,可以实现镜头畸变、热浪、水波、页面卷曲等效果。(这里感觉用后处理更合适一点)
- 制作地形
3.6 细分阶段
细分阶段用来把三角形细分(废话嘛这不是)。
它有什么用呢?
比起直接存储高面模型,使用细分可以存储低面模型,减少存储需求。同时,还可以减少CPU与GPU之间的**数据传输**。(比如动画播放时,每帧都需要CPU把数据传给GPU,如果直接使用高面模型会有数据传输瓶颈)。还可以**控制LOD**,在离相机远时加载低面模型,近时加载高面。对于较弱的GPU加载低面以**保证帧率**。
细分阶段通常包含三个基本元素。在DX中叫 hull shader, tessellator,domain shader。在OpenGL中把hull shader称为tessellation control shader,domain shader则叫做 tessellation evaluation shader。
更细节的部分我们会放在后面讲解,这里只是大概总结一下
hull shader(外壳着色器)的输入是一种特殊的补丁图元(patch primitive),该图元包含几个控制点,定义了细分曲面、Bézier补丁或其他类型的曲线元素。Hull shader 具有两个主要功能:
1. 指定 tessellator(细分器)应生成多少个三角形及其排列方式;
2. 对每个控制点进行处理。
此外,hull shader 还可以选择性地修改输入的补丁描述,根据需要增加或移除控制点。最后,hull shader 会将控制点集和细分控制数据传递给domain shader(域着色器)。
tessellator是固定函数,只能用细分shader。这个阶段创造一系列新的顶点。hull shader给tessellator发送自己需要的细分信息:是需要三角形,四边形还是isoline。isoline是一系列的线,有时会用于制作头发的细分。另外一个hull shader给tessellator发送的重要信息是tessellation factors(OpenGL中叫tessellation levels),包括两种模式:内部和外部的边缘线,两种inner factors定义了细分出现在三角形或者四边形的内部,outer factors定义了多少外部边缘被切分。通过允许独立控制,我们可以让相邻曲面在边缘的细分上保持一致,而不受内部细分方式的影响。顶点被赋予重心坐标。
Hull shader 总是输出一个补丁(patch),即一组控制点位置。然而,如果 hull shader 向 tessellator(细分器)发送的外部细分级别为零或更小(或者为非数字值 NaN),则 tessellator 将丢弃该补丁。否则,tessellator 会生成一个网格并将其发送给 domain shader(域着色器)。**Domain shader** 中每次调用都会利用 hull shader 提供的曲面控制点来**计算每个顶点的输出值**。Domain shader 的数据流模式类似于顶点着色器,来自 tessellator 的每个输入顶点都会被处理并生成相应的输出顶点。形成的三角形随后传递到渲染管线的下一个阶段。
总而言之,细分阶段结构复杂,但各部分设计简洁高效,通常仅对曲面描述进行少量修改或使用固定细分因子。虽然增加了计算量,但通过优化设计,能够显著提高曲面渲染质量与性能。如下图,左边是没细分的,右边是细分后的
3.7 几何着色器
几何处理器可以把一种图元变成其他图元。比如,三角mesh可以转化为线框模式;再比如线又可以被替换成面向摄像机的方形。几何着色器随着DirectX 10在2006年引入,作为Shader Model 4.0的一部分,同时也在OpenGL 3.2和OpenGL ES 3.2中受到支持。它是图形管线中可选的一环,位于细分着色器之后。DirectX 11和Shader Model 5.0进一步增强了功能,允许处理更复杂的图元补丁(最多32个控制点)。
输入: 接收单个对象及其关联的顶点,包括扩展图元(例如三角形外的附加顶点或折线上的相邻顶点)。
输出: 可以生成0个或多个顶点,输出的形式包括点、折线或三角形条带。它允许通过编辑顶点、新增或删除图元来选择性地修改网格。
应用场景:
- 渲染六个方向的立方体贴图、生成级联阴影贴图、处理点数据生成可变大小的粒子。
- 渲染技术如毛发渲染(沿轮廓挤出“鳍”),或用于阴影算法的边缘检测。
- 实例化:几何着色器可以在一个图元上运行多次,并支持将结果输出到最多四个数据流以供后续处理或存储。
几何着色器可以在一个图元上运行多次,并支持将结果输出到最多四个数据流以供后续处理或存储。
几何着色器的问题:
- 几何着色器会按照与输入顺序相同的顺序输出图元的结果。这会影响性能,因为如果多个着色器核心并行执行,则必须保存结果并进行排序,因此在处理大规模几何复制时效率较低。
- 在发出绘制调用后,GPU的管线中只有三个地方可编程的:光栅化阶段、细分阶段和几何着色器。在这三者中,几何着色器的资源消耗是最不可预测的。实际上,几何着色器的使用较少,因为它并不适配GPU的优势。在某些移动设备上,几何着色器甚至是通过软件实现的,因此不推荐在这些设备上使用。
3.7.1 流式输出阶段
这还是个挺重要的阶段捏。总结一下
什么是流式输出呢?一般情况下,在进入渲染流水线后会经过顶点着色器处理,然后光栅化处理,然后送到像素着色器继续操作。 Shader Model 4.0引入了流输出功能,允许顶点数据在经过顶点、细分和几何着色器处理后,作为有序数组输出,而不仅限于传递到光栅化阶段。流输出可以完全绕过光栅化阶段,将GPU管线用作非图形数据处理器。数据输出后可重新输入管线,适用于模拟流动水或粒子效果等连续性场景。
需要注意以下几点
- 流输出仅支持浮点数格式,可能会导致较高的内存消耗。
- 流输出针对图元而非单个顶点。例如,传递一个网格时,每个三角形都会生成各自的顶点集合,原始网格中的顶点共享信息会丢失。
- 通常情况下,更典型的用法是将顶点作为点集合图元传递,而不是完整的网格。在OpenGL中,该阶段称为**变换反馈(transform feedback)**,主要关注顶点的变换和重用。 输入图元的顺序在输出目标中得到保证,即顶点顺序保持不变。
3.8 像素着色器
这个阶段大家应该已经特别熟悉了,毕竟写Shader的时候大多数都是在这里进行的。
顶点、细分和几何着色器处理完毕后,图元经过裁剪并进入栅格化阶段,为像素着色器准备数据,光栅化阶段不可编程但是可配。每个三角形被遍历,确定每个图元覆盖的像素,生成片元(fragment),并将顶点属性(如深度值、法线等)插值到片元上。(注意,片元与像素的区别就在这里了。像素就是像素,片元是携带各种顶点属性可以使用的)。OPENGL把像素着色器叫片元着色器。
像素着色器功能:
- 负责处理片元的颜色、透明度及深度值,并决定是否丢弃片元。在光栅化阶段计算好的深度也可以在像素着色器中修改。
- 使用插值方法计算三角形表面的片元属性,默认使用透视校正插值(Perspective-Correct Interpolation)以确保远近关系真实,例如渲染铁轨远近的视觉效果。也有其他插值方法,如屏幕空间插值,这种方式透视投影没有被考虑在内。
输入:
- 在像素着色器阶段,输入一般是顶点着色器的输出。
- 随着GPU的进化,其他的输入也可以使用,比如片元的屏幕空间坐标在Model3.0以上可以使用。
- 同样,三角形的可见性可以作为标志输入,用于渲染前后材质。
输出:
- 最初像素着色器只能输出到合并阶段。
- 如今像素着色器可以将数据写入多个渲染目标,称为**MRT**(multiple render target),这些渲染目标都有同样的xy大小,有些API虽然支持不同大小,但是渲染区会是最小的。一些结构允许渲染目标有一些深度,甚至数据表。根据GPU性能来看,渲染目标一般是4个或者8个。
- 尽管有些局限,多渲染目标可以应用到延迟渲染(Deferred Shading)技术。通过第一个Pass,每个目标保存的不同的信息(如颜色、深度、世界坐标等),在第二个Pass中完成可见性和着色渲染。(Pass可以理解为一次渲染操作)。
最开始我们讲GPU时提到,GPU是并行处理的,因此相邻像素之间理论上是无法访问的。实际上,现代GPU已经针对这个问题做出了一些改进,比如可以访问导数或者插值变化量,以及DX11之后引入的UAV。
- 像素着色器通常无法直接访问或修改邻近像素的数据,只能处理当前片元的信息。(但是也有例外,比如在计算相邻像素的梯度或者导数时。)
- 像素着色器提供了沿x和y屏幕轴每个像素的插值变化量,这些值可以用于各种计算和纹理寻址。所有现代GPU通过将片段分组为2×2的组(称为quad)来实现这一功能。
- 当像素着色器请求梯度值时,会返回相邻片段之间的差异。uniform core 可以访问相邻数据(保存在同一个warp的不同线程中),因此可以在像素着色器中计算梯度。如下图。
- 梯度信息不能在受动态流控制影响的着色器部分访问,例如“if”语句或具有变量迭代次数的循环。所有片段组必须使用相同的指令集进行处理,以便所有quad的结果对于计算梯度是有意义的。
- DirectX 11引入了**UAV**,这是一种允许对任何位置进行写入访问的缓冲区类型。最初,UAV只适用于像素着色器(pixel shaders)和计算着色器(compute shaders)。
- 后来,在DirectX 11.1中,UAV的访问权限被扩展到了所有类型的着色器。在OpenGL 4.3中,UAV对应的概念被称为Shader Storage Buffer Object(SSBO)。
- 像素着色器是并行运行的,执行顺序是任意的。这种存储缓冲区(即UAV或SSBO)在像素着色器之间是共享的。
数据竞争:
- 数据竞争,也称为数据隐患(data hazard),发生在多个着色器程序“竞相”影响同一值时,可能导致不确定的结果。例如,如果两个像素着色器的实例尝试同时向同一个检索到的值添加数据,就可能发生错误。两个实例都会检索到原始值,分别进行修改,但最后写入结果的那个实例会覆盖另一个实例的贡献,导致只有一个加法操作被执行。
- GPU通过提供专用的原子单元来避免这个问题,着色器可以访问这些原子单元。原子操作意味着在其他着色器进行读/修改/写操作时,一些着色器可能需要等待访问内存位置,这可能导致它们暂停执行。
- 通过原子操作来避免数据竞争意味着许多算法需要特定的执行顺序。例如,在绘制透明三角形时,可能需要先绘制较远的蓝色三角形,然后再在其上覆盖红色透明三角形,并混合红色到蓝色上。
- ROVs在DirectX 11.3中引入,用于强制执行顺序。它们类似于无序访问视图(UAVs),可以被着色器以相同的方式读写。ROVs保证数据按照正确的顺序被访问,这大大增加了这些着色器可访问缓冲区的用途。
- ROVs使得像素着色器能够编写自己的混合方法,因为它可以直接访问和写入ROV中的任何位置,因此不需要合并阶段。
- 如果检测到非顺序访问,像素着色器调用可能会暂停,直到先前绘制的三角形被处理。
3.9 合并阶段
合并阶段是图形渲染管线中将像素着色器生成的片段深度和颜色与帧缓冲区融合的关键步骤。DirectX 将此阶段称为输出合并器,OpenGL 则称为逐样本操作。
此阶段包含模板缓冲(stencil buffer)和深度缓冲(z-buffer)的操作,判断片段是否可见。如果片段可见,则可能执行颜色混合:
- 不透明表面:片段的颜色直接替换帧缓冲区中已存储的颜色,无需混合。
- 透明表面:颜色混合被广泛用于透明度和合成操作,例如按比例融合片段颜色和存储颜色。
为了减少不必要的像素着色器处理,GPU 在像素着色器执行之前进行早期 Z 测试(Early-Z):
- 功能:利用片段深度值和模板缓冲等信息提前测试可见性,剔除隐藏片段。
- 限制:如果像素着色器修改了片段的深度值或丢弃片段,则早期 Z 测试会被禁用,从而降低管线效率。
- 改进:DirectX 11 和 OpenGL 4.2 提供强制启用早期 Z 测试的功能,但存在一些限制。
尽管合并阶段本身不可编程,但其操作具有高度可配置性:
- 颜色混合操作:支持多种运算方式,如加法、减法、乘法,以及最小值、最大值和按位逻辑运算。
- 双源颜色混合:DirectX 10 引入,允许将像素着色器生成的两个颜色值与帧缓冲区颜色进行混合,但不支持与多渲染目标(MRT)结合使用。
- 独立缓冲混合:DirectX 10.1 允许对每个缓冲区执行不同的混合操作。
- 可编程混合:DirectX 11.3 引入 ROV(渲染目标视图),支持可编程混合操作,但可能导致性能下降。
- 无论像素着色器生成结果的顺序如何,API 要求所有结果按输入顺序(对象、三角形)传递到合并阶段。这种输出不变性确保了渲染的正确性。
3.10 计算着色器
计算着色器(Compute Shader) 是 GPU 计算的一种形式,拓展了 GPU 在传统图形管线之外的应用范围。GPU 的功能不仅限于传统图形渲染,还被广泛应用于非图形任务,例如:估算股票期权价值和深度学习的神经网络训练。这种应用方式称为GPU 计算,通过 CUDA 和 OpenCL 等平台将 GPU 用作大规模并行处理器,脱离对图形功能的依赖。
计算着色器于 DirectX 11 中首次引入,作为一种灵活的着色器,它并不固定在图形管线的某个阶段。
功能概述:
- 与顶点着色器和像素着色器共享同一套统一的着色处理器。
- 能够访问输入和输出缓冲区(如纹理)。
线程管理:
- 每个线程可以通过索引访问数据。
- 支持线程组,每组最多 1024 个线程,线程组内共享内存(DirectX 11 中为 32kB)。
- 保证线程组内所有线程同时执行,提高并行处理效率。
性能优势
- 计算着色器能够直接访问 GPU 内部生成的数据,避免 GPU 与 CPU 之间的数据传输延迟,从而显著提升性能。
- 共享内存支持线程间共享中间结果,例如在图像后处理任务中利用计算着色器计算图像的亮度分布或平均亮度,比像素着色器快两倍。
计算着色器适用于多种高性能任务,包括但不限于:
- 后处理(如景深和阴影处理)
- 粒子系统和网格处理(如面部动画)
- 图像过滤和剔除操作
- 深度精度改进
细分曲面控制着色器(Tessellation Hull Shader),计算着色器在某些任务上更高效,特别是在需要高效并行计算的场景。
呼,这一章总算结束了。最后的总结用一张脑图来代替吧。虽然看起来内容很多,但是这一章仅仅是个目录,所以看不懂没关系,细节可以看看后面。ψ(`∇´)ψ