渲染流水线
综述
渲染流水线的运行过程是怎样的?
渲染流水线是在显存中开启的。CPU将网格、材质、贴图、着色器等注入显存后,GPU开始渲染流水线。
渲染流水线分为几何阶段和光栅化阶段(也被称为像素阶段),并最终将运算结果送到显示器的缓冲区中。
几何阶段分为顶点着色器->曲面细分着色器(DirectX11和OpenGL4.x以上可编程)->几何着色器->裁剪->屏幕映射五个步骤。
光栅化阶段分为三角形设置->三角形遍历->片元着色器->逐片元操作四个步骤。
显卡厂商会通过硬件实现常用且功能变化不大的几个流水线阶段,因为通过硬件实现的效率远高于软件实现。这些阶段优的根据API的设计,提供了一些可以设置的参数,但总的来说不会脱离GPU的控制。
在这个过程中我们可以知道,顶点着色器和片元着色器的线程数并不是等同的。片元着色器的线程数变化幅度一般不大,而顶点着色器的线程数变化幅度随应用不同可能大幅浮动。
如何理解GPU渲染流水线和CPU指令流水线的区别?
渲染流水线重点强调的和CPU的指令流水线并不同。
在CPU指令流水线中,我们强调的是:不需要等待一个指令处理完毕再读取下一条,而是在指令处理完一个阶段后开始处理下一条。这里考虑的重点是两条指令间的流水处理。
对于渲染流水线,一种常见的误解是认为渲染流水线和指令流水线一样,不同阶段处理的是不同的Draw Call,较后的阶段处理着较早提交的Draw Call,较前的阶段处理着较晚提交的Draw Call。**这种理解是错误的。**正确的理解是,渲染流水线中,不同阶段运行的是同一次Draw Call,但一次Draw Call中先提交的数据可以不等待它之后的数据就进入下一个流水线阶段。
可见,GPU的并行是一次Draw Call中各个元素间的并行,而非多个Draw Call之间的并行。GPU处理连续的多个Draw Call是串行的,已被GPU接受但还来不及处理的Draw Call以及其它CPU指令被暂存在GPU的指令缓冲区中,等待当前GPU任务运行结束后读取新的指令。指令的运行顺序和CPU的提交顺序是一致的。
几何阶段
顶点着色器是如何工作的?
顶点着色器可以深度自定义,开发者需要根据显存中的原始数据(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编辑这个着色器的工作任务。
几何着色器是如何工作的?
几何着色器是一个可选着色器,也在视口变换器上实现。
几何着色器位于顶点和片元着色器之间,它以一个或多个顶点或三角形等基本图形作为输入,输出另外的一个顶点序列或三角形序列,以此改变图形的形状。仅仅是改变形状那它的作用就和顶点着色器没区别了,几何着色器最大的特点是能改变顶点的数量。对于每一个输入,几何着色器可以返回0到40个输出,但如果一个输入对应的输出数大于27,性能会明显的下降。
以HLSL为例,典型的几何着色器定义如下:
#pragma geometry geom
[maxvertexcount(6)]
void geom(triangle v2g input[3], inout PointStream<g2f> outstream)
{
for(int i = 0; i < 3; i++)
{
g2f o = (g2f)0;
o.vertex = input[i].vertex;
o.uv = input[i].uv;
outstream.Append(o);
}
outstream.RestartStrip();
}
函数的第一个参数类型决定了可以输入的基本图形,它们包括:
point:每批处理一个顶点,声明格式为point v2g input[1]。
line:每批处理一条线端上的两个顶点,声明格式为line v2g input[2]。
triangle:每批处理一个三角形上的三个顶点,声明格式为triangle v2g input[3]。最常用的输入类型就是triangle。
lineadj:每批处理一条线端及其两端的两条线端,共三条线端四个顶点,声明格式为lineadj v2g input[4]。
triangleadj:每批处理一个三角形及其邻接的三个三角形,共四个三角形六个顶点,声明格式为triangleadj v2g input[6]。
函数的第二个参数决定了几何着色器的输出类型,它们可以是TriangleStream、LineStream和PointStream,即HLSL中的流输出对象类型(Stream-Output Object),它们分别对应了3个、2个和1个顶点。
通过标签[maxvertexcount(N)]来定义几何着色器单次调用输出的最大顶点个数,这个输出顶点个数应当是输出类型对应的顶点数的整数倍。在几何着色器中,通过使用outstream.Append(o)来将一个g2f顶点输出,每次调用几何着色器的输出个数应当等于maxvertexcount定义的顶点个数。如果在运行过程中想要丢弃本次运算输出的顶点,在return前使用outstream.RestartStrip()来保证输出流的格式正确。
裁剪阶段是如何工作的?
裁剪也由视口变换器实现,但它完全不可编辑。
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还会同时进行裁剪、背面剔除和早期深度测试(Early-Z Testing)。这几个操作是可以通过API进行配置的,但不可编程。
在片元着色器中的大量片元都可能因为不能通过深度测试而被抛弃,所以浪费了大量性能用于不必要的光照运算。实现Early-Z技术的GPU在三角形遍历阶段维护G-Buffer,提前抛弃了不需要渲染的片元——详情见【什么是前向渲染,什么是延迟渲染】。
片元还不是一个像素(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的片元,以实现这个效果。
模板测试和模板写入的编程自由度非常大,可以使用很多数学运算和逻辑运算,这使得模板测试有很多高级的用法,如渲染阴影和渲染轮廓等。
深度测试用于抛弃那些被其它片元遮挡的片元,它是高度可定制但不可编程的。与深度测试对应的是深度写入,深度缓冲记录着当前离摄像机最近的片元的深度坐标,只有深度比这个片元更小的片元才有权利通过深度测试并将新的深度写入对应区域。我们可以关闭深度测试或深度写入,比如透明或半透明物体应该关闭深度写入,因为我们不希望透明物体遮挡它背后的片元。
什么是模板测试?
**模板测试(Stencil Test)**的名字起源于印刷行业中的版面模子(Stencil),在印刷时我们需要在模子上抠出需要的图案,然后将模子盖在要被印刷的材质上,透过抠出的洞涂颜色,而不需要上色的部分则会被模子挡住,避免了涂色越界。
模板测试就是将屏幕上的部分像素用一个8bit的通道盖住,在渲染前,剔除该通道上的值不等于测试值的像素,类似于在模子上挖洞,只渲染通过该测试的像素。
在主流的GPU架构中,模板缓冲区和深度缓冲区是连在一起的,在一个32位的储存区域中有24位记录深度信息,紧邻着8位的模板信息。正是因为模板缓冲区和深度缓冲区关系如此之紧密,我们可以在模板测试中访问深度测试。如前面所说的,模板测试先于深度测试。
在Unity Shader中可以用以下语法进行模板缓冲区的读写:
Pass
{
Stencil
{
//当前像素stencil值与0进行比较
Ref referenceValue //0-255
//一口气比较好多位stencil
ReadMask readMask
//一口气写好多位stencil
WriteMask writeMask
//测试条件:测试是否相等
//测试条件包括Greater、GEqual、Less、LEqual、Equal、NotEqual、Always、Never
Comp Equal
//如果测试通过对此stencil值进行的写入操作:保持当前stencil值
Pass keep //default:keep
//如果测试失败对此stencil值进行的写入操作:保持当前stencil值
Fail keep //default:keep
//如果深度测试失败对此stencil值进行的写入操作:循环递增
ZFail IncrWrap //default:keep
//操作方式包括Keep、Zero、Replace、IncrSat、DecrSat、Invert、IncrWrap、DecrWrap
//其中Replace把Ref写进去,Sat是有边界加/减,Wrap是溢出加/减
}
}
举个例子,使用模板测试实现简易描边:
Stencil
{
Ref 0 //0-255
Comp Equal //default:always
Pass IncrSat //default:keep
Fail keep //default:keep
ZFail keep //default:keep
}
在默认情况下第一个pass前屏幕上所有像素模板都为0,则物体所有像素通过模板测试并使模板值加1,同时成功渲染物体。然后在第二个pass中,在法线方向放大所有顶点的位置,那么只有之前没有渲染过的新增的部分可以通过测试,这样就可以实现描边了。
模板缓冲在每一帧的整幅图像只清空一次,所以依靠模板缓冲可以轻易做到跨Pass和跨Shader的信息沟通。如果想在一帧中重复使用多次模板缓冲,可以设定好渲染顺序,在模板缓冲使用结束后用一个额外的Pass来手动清空模板缓冲。在一个大型团队中,你难以预料是否有其他人和你使用了同样的模板缓冲参考值,这可能导致bug。最好的解决方法是全团队进行协商分配不同的参考值序列,但如果这么做对你们的团队来说不可能,那就无论如何都额外写一个Pass来清理你的模板缓冲区,这可能导致性能下降,但总比渲染错误好。
什么是混合?
在一个片元写入后置缓冲区中时,后置缓冲区中的对应位置有可能已经存在有像素信息了。妥善的处理旧的和新的像素信息就是混合要做的事。混合操作是高度可订制但不可编程的。
最常见的,同时也是默认混合策略是覆盖(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位的精度,可以使延迟渲染的效果更上一层楼。
什么是抗锯齿?
在三角形设置阶段结束后我们将三角形交给了ROP,我们将三角形画在这样的像素图上比对:
可以发现,有的像素点全部在三角形中,优的部分在其中,有的完全没在其中。但在屏幕上绘制时却不能只绘制像素点的一部分,要么全部不绘制,要么全部都绘制。假设我们判断像素是否绘制的标准是像素的中点是否被三角形覆盖,则得到以下的片元分布图:
这个图形和一个完美的三角形还有很大的差距,这种和平滑图形差距很大的问题就是我们要解决的“锯齿”。
锯齿的出现可以理解为像素精度不够,那么我们该如何巧妙地提升像素精度呢?
**SSAA(Super-Sampling Anti-Aliasing,超级采样抗锯齿)**的思路简单粗暴——我们只要在采样时将分辨率翻倍就好了。假设屏幕分辨率是1920×1080,那么4×SSAA采样就会在3840×2160的分辨率等级上生成片元,并将片元渲染到3840×2160的缓冲区上,等所有渲染过程结束后,再向下采样取得1920×1080的图像。SSAA可以得到最好的抗锯齿效果,但问题也很明显,就是带来了四倍的光栅化阶段工作量和内存消耗。这种奢侈的算法几乎被所有显卡厂商抛弃了。
**MSAA(Multi-Sampling Anti-Aliasing,多重采样抗锯齿)**在SSAA的基础上进行了优化,放弃了扩大分辨率,而是将一个像素点与多个采样点对应。如图:
抗锯齿2
注意到,为了使采样结果尽可能的多样,我们不能将采样点整齐的摆放成正方形。理想的采样点摆放是稀疏的,对于N个采样点,任意两个采样点不应该出现在一个N×N网格的同一行、列及对角线上。通过著名的N皇后问题算法可以解出满足这种稀疏摆放条件的采样点。
于是我们可以根据三角形包围的像素点的数量来加权的计算颜色值,得到如下的效果:
这个抗锯齿的性能比SSAA好了不少,且效果也不差,但它又有一个致命的问题,就是对满大街都是的延迟渲染结构不友好——在前向渲染中片元会被光栅化到G-Buffer上,而不是直接生成片元。假如一个像素具有4个采样点,那么G-Buffer上每个像素都可能需要保存最多4种不同的颜色,为了保存这些颜色,G-Buffer需要4倍的空间储存颜色,这使得MSAA需要的缓存又回到了和SSAA差不多的数量级。抛开延迟渲染问题不谈,MSAA是商业运用中产生效果最好的一种抗锯齿算法。
为了适应延迟渲染,现代引擎里一般使用**FXAA(Fast Approximate Anti-Aliasing,快速近似抗锯齿)和TXAA (Temporal Anti-Aliasing,时间滤波抗锯齿)**这类技术。它们的普遍特点是抛弃精度,通过相邻像素模糊来实现平滑和抗锯齿,开启了这类抗锯齿效果的场景会有明显的边界不清问题,但它们应该是性能最好的抗锯齿效果了。
以FXAA为例。MSAA和SSAA都属于硬件设计范畴,而FXAA属于软件优化范畴,在三角形遍历阶段通过ROP解决。而FXAA可以被理解为一次额外的屏幕后处理。FXAA的核心思路是,对每个像素计算它与周围像素的梯度,确认该像素是否位于边缘,如果是,判断边缘的方向,对边缘方向的连续两个或三个像素进行加权颜色混合,以实现模糊的效果。