你有没有注意过在一些游戏里角色的边缘建筑的轮廓远处的栏杆看起来像是用锯子锯出来的。一格一格的粗糙生硬不自然。你把画面截图放大会看到那条本来应该是平滑曲线的边缘实际上是一堆小方块拼出来的阶梯。这个现象叫做锯齿英文叫Aliasing。它让画面看起来廉价让精心设计的场景显得粗糙。而消灭它的技术叫做抗锯齿Anti-Aliasing。其中最经典、最广泛使用的一种叫做多重采样抗锯齿MSAAMultisample Anti-Aliasing。今天我们用一个切蛋糕的比喻把这件事说清楚。一、锯齿是怎么来的要理解抗锯齿先要理解锯齿是怎么产生的。屏幕是由像素组成的。像素是屏幕上最小的发光单元是一个小方块。你的屏幕可能是1920×1080的分辨率也就是横向1920个像素纵向1080个像素总共两百多万个小方块。每个像素只能显示一种颜色。现在你要在屏幕上画一条斜线。这条斜线在数学上是无限细的它可以精确地穿过任何位置。但屏幕上的像素是离散的是有固定大小的方块。斜线穿过的地方有些像素被完全覆盖有些像素被部分覆盖有些像素完全没有被覆盖。问题来了那些被部分覆盖的像素怎么处理最简单的方式是做一个二元判断如果这个像素的中心点在线条内部就把这个像素涂成线条的颜色。如果中心点在线条外部就不涂。这个方式简单粗暴但结果就是那些阶梯状的锯齿。因为你把一个连续的、平滑的线条强行映射到了一个离散的、方块状的网格上信息丢失了平滑性丢失了。这个过程在信号处理里叫做采样锯齿是采样率不足导致的走样现象。二、切蛋糕的比喻现在我们用切蛋糕来理解这个问题。想象你有一个大蛋糕你要把它切成很多小块每一小块代表屏幕上的一个像素。切完之后你需要决定每一小块蛋糕的颜色。规则是看这一小块蛋糕的正中心中心是什么颜色这块蛋糕就是什么颜色。现在有一条巧克力酱画出来的斜线穿过蛋糕。对于那些中心点恰好在巧克力酱上的蛋糕块颜色是棕色。对于那些中心点在奶油上的蛋糕块颜色是白色。但有很多蛋糕块巧克力酱只覆盖了它的一部分中心点在奶油上所以整块被判定为白色。结果那条本来平滑的斜线变成了一串棕色方块拼成的阶梯。这就是锯齿的来源。问题的根本是你只看了每块蛋糕的一个点就决定了整块蛋糕的颜色。这一个点不能代表整块蛋糕的真实情况。三、最直觉的解法把蛋糕切得更小既然一个点不够代表整块蛋糕那就把蛋糕切得更小让每块更小的蛋糕更接近于一个点。这就是超级采样抗锯齿SSAASuper Sampling Anti-Aliasing。把渲染分辨率提高到屏幕分辨率的四倍每个屏幕像素对应四个渲染像素。渲染完成后把四个渲染像素的颜色平均一下得到最终的屏幕像素颜色。效果非常好锯齿几乎消失。但代价极大。分辨率提高四倍渲染的像素数量提高四倍GPU的工作量提高四倍帧率可能直接腰斩。这就像为了让蛋糕切得更准确你把每块蛋糕切成原来的四分之一大小然后再把四小块合并成一块。结果是准了但你切蛋糕的工作量变成了原来的四倍。太贵了。四、MSAA的聪明之处只在边缘多切几刀MSAA的设计者发现了一个关键的事实锯齿只出现在边缘。在一个纯色的区域内部不管你怎么采样结果都是一样的颜色不会有锯齿。锯齿只出现在两种颜色的交界处也就是几何体的边缘。所以没有必要对整个画面都做超级采样。只需要在边缘处多采样几次就够了。这就是MSAA的核心思想。回到切蛋糕的比喻。MSAA说我不需要把整块蛋糕都切成四份。我只需要在巧克力酱经过的地方也就是边缘处多取几个采样点看看这块蛋糕里有多少比例被巧克力酱覆盖然后按比例混合颜色。对于那些完全在奶油里或者完全在巧克力酱里的蛋糕块还是只取一个点不需要额外工作。只有那些被边缘穿过的蛋糕块才需要多取几个点。这样工作量大幅减少但效果接近于超级采样。五、MSAA的具体工作流程说完原理来看看MSAA具体是怎么工作的。以4x MSAA为例每个像素有4个采样点。第一步确定采样点的位置。每个像素内部有4个采样点它们的位置是预先确定的不是随机的也不是均匀分布在四个角而是经过精心设计的位置能最好地覆盖像素内部的不同区域。常见的一种排列叫做旋转网格采样4个点分布在像素内部的不同位置互相错开避免对齐产生的规律性误差。第二步几何体覆盖测试。对于每个像素GPU检查这4个采样点哪些点在几何体内部哪些在外部。注意这里只是做覆盖测试判断点在不在几何体里还没有计算颜色。第三步着色计算。这是MSAA最关键的优化所在。对于一个像素不管它的4个采样点有几个在几何体内部着色计算只做一次。着色就是计算这个像素应该是什么颜色包括光照计算、纹理采样等是GPU工作量最大的部分。MSAA只在像素中心做一次着色得到一个颜色值。第四步按覆盖率混合颜色。假设4个采样点里有3个在几何体内部1个在外部。覆盖率是3/4也就是75%。最终这个像素的颜色是75%的几何体颜色加上25%的背景颜色。这个混合让边缘变得平滑不再是非黑即白的硬边。第五步resolve。所有像素处理完成后把多采样的结果合并成最终的单采样图像输出到屏幕。六、用数字感受一下效果来看一个具体的例子。一条斜线穿过一排像素。没有抗锯齿的情况像素1中心点在线条外颜色 背景色白 像素2中心点在线条内颜色 线条色黑 像素3中心点在线条外颜色 背景色白 像素4中心点在线条内颜色 线条色黑结果白黑白黑硬边锯齿明显。4x MSAA的情况像素14个采样点1个在线条内 覆盖率 1/4 25% 颜色 25%黑 75%白 浅灰 像素24个采样点3个在线条内 覆盖率 3/4 75% 颜色 75%黑 25%白 深灰 像素34个采样点2个在线条内 覆盖率 2/4 50% 颜色 50%黑 50%白 中灰 像素44个采样点4个在线条内 覆盖率 4/4 100% 颜色 100%黑 黑结果浅灰、深灰、中灰、黑有过渡边缘平滑。这个过渡就是抗锯齿的本质用颜色的渐变模拟几何体边缘的平滑性。七、MSAA的内存代价MSAA不是免费的它有内存代价。4x MSAA每个像素有4个采样点每个采样点需要存储颜色值和深度值。这意味着颜色缓冲区和深度缓冲区都需要扩大到原来的4倍。一个1080p的游戏颜色缓冲区大约需要8MB。开启4x MSAA颜色缓冲区变成32MB。深度缓冲区同样扩大4倍。这些额外的内存对现代GPU来说不是大问题但在内存带宽上会有一定的压力。读写更大的缓冲区需要更多的内存带宽这会影响GPU的整体性能。八、MSAA的局限它解决不了所有锯齿MSAA很好但它有一个明显的局限。它只能处理几何体边缘产生的锯齿。对于着色产生的锯齿它无能为力。什么是着色产生的锯齿比如一个表面上有高频纹理纹理里有很细的条纹。这些条纹不是几何体的边缘而是着色计算的结果。MSAA对每个像素只做一次着色所以这种锯齿MSAA处理不了。还有透明物体比如铁丝网、树叶。这些物体用透明度来模拟细节透明边缘产生的锯齿MSAA也处理得不好。这就是为什么后来出现了FXAA、TAA等其他抗锯齿技术它们用不同的方式解决MSAA解决不了的问题。但MSAA作为最经典的抗锯齿方案在几何边缘的处理上至今仍然是最可靠的选择之一。九、在代码里MSAA是怎么开启的如果你用OpenGL或者Vulkan做图形开发开启MSAA的方式大致是这样的。OpenGL里// 创建支持多重采样的帧缓冲glBindFramebuffer(GL_FRAMEBUFFER,framebuffer);// 创建多重采样的颜色缓冲glBindRenderbuffer(GL_RENDERBUFFER,colorBuffer);glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,// 采样数4x MSAAGL_RGBA8,// 颜色格式width,height);glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_RENDERBUFFER,colorBuffer);// 创建多重采样的深度缓冲glBindRenderbuffer(GL_RENDERBUFFER,depthBuffer);glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,// 同样是4xGL_DEPTH_COMPONENT24,width,height);glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,depthBuffer);// 渲染完成后resolve到普通帧缓冲glBindFramebuffer(GL_READ_FRAMEBUFFER,msaaFramebuffer);glBindFramebuffer(GL_DRAW_FRAMEBUFFER,resolveFramebuffer);glBlitFramebuffer(0,0,width,height,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST);关键的函数是glRenderbufferStorageMultisample第二个参数是采样数。4就是4x MSAA8就是8x MSAA采样数越高效果越好性能开销越大。最后的glBlitFramebuffer就是resolve步骤把多采样的结果合并成最终图像。回到那个切蛋糕的比喻。普通渲染是看每块蛋糕的中心点一刀切非黑即白。SSAA是把蛋糕切成更小的块工作量翻倍但结果更准确。MSAA是只在边缘处多取几个点内部区域还是一个点用最小的额外工作量换来边缘的平滑。这个聪明的权衡让MSAA在性能和效果之间找到了一个很好的平衡点。它不是最完美的方案但它是最经典的方案。理解了它你就理解了抗锯齿这件事的核心逻辑锯齿是采样不足的结果。抗锯齿是用更多的采样信息还原边缘的真实形态。多采样几刀蛋糕就切得更准。
游戏锯齿:为什么边缘像锯子?
你有没有注意过在一些游戏里角色的边缘建筑的轮廓远处的栏杆看起来像是用锯子锯出来的。一格一格的粗糙生硬不自然。你把画面截图放大会看到那条本来应该是平滑曲线的边缘实际上是一堆小方块拼出来的阶梯。这个现象叫做锯齿英文叫Aliasing。它让画面看起来廉价让精心设计的场景显得粗糙。而消灭它的技术叫做抗锯齿Anti-Aliasing。其中最经典、最广泛使用的一种叫做多重采样抗锯齿MSAAMultisample Anti-Aliasing。今天我们用一个切蛋糕的比喻把这件事说清楚。一、锯齿是怎么来的要理解抗锯齿先要理解锯齿是怎么产生的。屏幕是由像素组成的。像素是屏幕上最小的发光单元是一个小方块。你的屏幕可能是1920×1080的分辨率也就是横向1920个像素纵向1080个像素总共两百多万个小方块。每个像素只能显示一种颜色。现在你要在屏幕上画一条斜线。这条斜线在数学上是无限细的它可以精确地穿过任何位置。但屏幕上的像素是离散的是有固定大小的方块。斜线穿过的地方有些像素被完全覆盖有些像素被部分覆盖有些像素完全没有被覆盖。问题来了那些被部分覆盖的像素怎么处理最简单的方式是做一个二元判断如果这个像素的中心点在线条内部就把这个像素涂成线条的颜色。如果中心点在线条外部就不涂。这个方式简单粗暴但结果就是那些阶梯状的锯齿。因为你把一个连续的、平滑的线条强行映射到了一个离散的、方块状的网格上信息丢失了平滑性丢失了。这个过程在信号处理里叫做采样锯齿是采样率不足导致的走样现象。二、切蛋糕的比喻现在我们用切蛋糕来理解这个问题。想象你有一个大蛋糕你要把它切成很多小块每一小块代表屏幕上的一个像素。切完之后你需要决定每一小块蛋糕的颜色。规则是看这一小块蛋糕的正中心中心是什么颜色这块蛋糕就是什么颜色。现在有一条巧克力酱画出来的斜线穿过蛋糕。对于那些中心点恰好在巧克力酱上的蛋糕块颜色是棕色。对于那些中心点在奶油上的蛋糕块颜色是白色。但有很多蛋糕块巧克力酱只覆盖了它的一部分中心点在奶油上所以整块被判定为白色。结果那条本来平滑的斜线变成了一串棕色方块拼成的阶梯。这就是锯齿的来源。问题的根本是你只看了每块蛋糕的一个点就决定了整块蛋糕的颜色。这一个点不能代表整块蛋糕的真实情况。三、最直觉的解法把蛋糕切得更小既然一个点不够代表整块蛋糕那就把蛋糕切得更小让每块更小的蛋糕更接近于一个点。这就是超级采样抗锯齿SSAASuper Sampling Anti-Aliasing。把渲染分辨率提高到屏幕分辨率的四倍每个屏幕像素对应四个渲染像素。渲染完成后把四个渲染像素的颜色平均一下得到最终的屏幕像素颜色。效果非常好锯齿几乎消失。但代价极大。分辨率提高四倍渲染的像素数量提高四倍GPU的工作量提高四倍帧率可能直接腰斩。这就像为了让蛋糕切得更准确你把每块蛋糕切成原来的四分之一大小然后再把四小块合并成一块。结果是准了但你切蛋糕的工作量变成了原来的四倍。太贵了。四、MSAA的聪明之处只在边缘多切几刀MSAA的设计者发现了一个关键的事实锯齿只出现在边缘。在一个纯色的区域内部不管你怎么采样结果都是一样的颜色不会有锯齿。锯齿只出现在两种颜色的交界处也就是几何体的边缘。所以没有必要对整个画面都做超级采样。只需要在边缘处多采样几次就够了。这就是MSAA的核心思想。回到切蛋糕的比喻。MSAA说我不需要把整块蛋糕都切成四份。我只需要在巧克力酱经过的地方也就是边缘处多取几个采样点看看这块蛋糕里有多少比例被巧克力酱覆盖然后按比例混合颜色。对于那些完全在奶油里或者完全在巧克力酱里的蛋糕块还是只取一个点不需要额外工作。只有那些被边缘穿过的蛋糕块才需要多取几个点。这样工作量大幅减少但效果接近于超级采样。五、MSAA的具体工作流程说完原理来看看MSAA具体是怎么工作的。以4x MSAA为例每个像素有4个采样点。第一步确定采样点的位置。每个像素内部有4个采样点它们的位置是预先确定的不是随机的也不是均匀分布在四个角而是经过精心设计的位置能最好地覆盖像素内部的不同区域。常见的一种排列叫做旋转网格采样4个点分布在像素内部的不同位置互相错开避免对齐产生的规律性误差。第二步几何体覆盖测试。对于每个像素GPU检查这4个采样点哪些点在几何体内部哪些在外部。注意这里只是做覆盖测试判断点在不在几何体里还没有计算颜色。第三步着色计算。这是MSAA最关键的优化所在。对于一个像素不管它的4个采样点有几个在几何体内部着色计算只做一次。着色就是计算这个像素应该是什么颜色包括光照计算、纹理采样等是GPU工作量最大的部分。MSAA只在像素中心做一次着色得到一个颜色值。第四步按覆盖率混合颜色。假设4个采样点里有3个在几何体内部1个在外部。覆盖率是3/4也就是75%。最终这个像素的颜色是75%的几何体颜色加上25%的背景颜色。这个混合让边缘变得平滑不再是非黑即白的硬边。第五步resolve。所有像素处理完成后把多采样的结果合并成最终的单采样图像输出到屏幕。六、用数字感受一下效果来看一个具体的例子。一条斜线穿过一排像素。没有抗锯齿的情况像素1中心点在线条外颜色 背景色白 像素2中心点在线条内颜色 线条色黑 像素3中心点在线条外颜色 背景色白 像素4中心点在线条内颜色 线条色黑结果白黑白黑硬边锯齿明显。4x MSAA的情况像素14个采样点1个在线条内 覆盖率 1/4 25% 颜色 25%黑 75%白 浅灰 像素24个采样点3个在线条内 覆盖率 3/4 75% 颜色 75%黑 25%白 深灰 像素34个采样点2个在线条内 覆盖率 2/4 50% 颜色 50%黑 50%白 中灰 像素44个采样点4个在线条内 覆盖率 4/4 100% 颜色 100%黑 黑结果浅灰、深灰、中灰、黑有过渡边缘平滑。这个过渡就是抗锯齿的本质用颜色的渐变模拟几何体边缘的平滑性。七、MSAA的内存代价MSAA不是免费的它有内存代价。4x MSAA每个像素有4个采样点每个采样点需要存储颜色值和深度值。这意味着颜色缓冲区和深度缓冲区都需要扩大到原来的4倍。一个1080p的游戏颜色缓冲区大约需要8MB。开启4x MSAA颜色缓冲区变成32MB。深度缓冲区同样扩大4倍。这些额外的内存对现代GPU来说不是大问题但在内存带宽上会有一定的压力。读写更大的缓冲区需要更多的内存带宽这会影响GPU的整体性能。八、MSAA的局限它解决不了所有锯齿MSAA很好但它有一个明显的局限。它只能处理几何体边缘产生的锯齿。对于着色产生的锯齿它无能为力。什么是着色产生的锯齿比如一个表面上有高频纹理纹理里有很细的条纹。这些条纹不是几何体的边缘而是着色计算的结果。MSAA对每个像素只做一次着色所以这种锯齿MSAA处理不了。还有透明物体比如铁丝网、树叶。这些物体用透明度来模拟细节透明边缘产生的锯齿MSAA也处理得不好。这就是为什么后来出现了FXAA、TAA等其他抗锯齿技术它们用不同的方式解决MSAA解决不了的问题。但MSAA作为最经典的抗锯齿方案在几何边缘的处理上至今仍然是最可靠的选择之一。九、在代码里MSAA是怎么开启的如果你用OpenGL或者Vulkan做图形开发开启MSAA的方式大致是这样的。OpenGL里// 创建支持多重采样的帧缓冲glBindFramebuffer(GL_FRAMEBUFFER,framebuffer);// 创建多重采样的颜色缓冲glBindRenderbuffer(GL_RENDERBUFFER,colorBuffer);glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,// 采样数4x MSAAGL_RGBA8,// 颜色格式width,height);glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_RENDERBUFFER,colorBuffer);// 创建多重采样的深度缓冲glBindRenderbuffer(GL_RENDERBUFFER,depthBuffer);glRenderbufferStorageMultisample(GL_RENDERBUFFER,4,// 同样是4xGL_DEPTH_COMPONENT24,width,height);glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,depthBuffer);// 渲染完成后resolve到普通帧缓冲glBindFramebuffer(GL_READ_FRAMEBUFFER,msaaFramebuffer);glBindFramebuffer(GL_DRAW_FRAMEBUFFER,resolveFramebuffer);glBlitFramebuffer(0,0,width,height,0,0,width,height,GL_COLOR_BUFFER_BIT,GL_NEAREST);关键的函数是glRenderbufferStorageMultisample第二个参数是采样数。4就是4x MSAA8就是8x MSAA采样数越高效果越好性能开销越大。最后的glBlitFramebuffer就是resolve步骤把多采样的结果合并成最终图像。回到那个切蛋糕的比喻。普通渲染是看每块蛋糕的中心点一刀切非黑即白。SSAA是把蛋糕切成更小的块工作量翻倍但结果更准确。MSAA是只在边缘处多取几个点内部区域还是一个点用最小的额外工作量换来边缘的平滑。这个聪明的权衡让MSAA在性能和效果之间找到了一个很好的平衡点。它不是最完美的方案但它是最经典的方案。理解了它你就理解了抗锯齿这件事的核心逻辑锯齿是采样不足的结果。抗锯齿是用更多的采样信息还原边缘的真实形态。多采样几刀蛋糕就切得更准。