使用3D API进行2D描线的一个抗锯齿方法

背景

这篇文章针对的是需要从3D硬件加速API自撸2D绘制引擎的情况。

在3D引擎中绘制比较复杂的2D线条,常见的方法是在CPU中先算出线条的具体轮廓(在此过程中考虑复杂的线条样式,如线条拐角与端点的形状等等)并三角化,然后将三角形图元发送到显卡管线进行渲染。显卡起的作用其实主要是光栅化器和执行着色逻辑。

使用3D API进行2D描线的一个抗锯齿方法_第1张图片

线条位置,线条轮廓,三角化图元,光栅化。

那么我们立即就会遇到抗锯齿(antialias,以下简称AA)的问题。3D管线内置的MSAA效果比较稳定,但是计算代价较大,而更先进的FXAA、深度学习抗锯齿更多地是为了3D场景渲染设计,如果应用到2D绘制,一是未必有它们需要的信息(比如深度、光照),二是这些近似算法可能不太适合用在要求精确的2D绘制上。而且不论如何,我们经常会遇到不能打开硬件内置AA的情况(硬件不支持/需要重开窗口而应用场景无法重开窗口)。于是一个自制的AA总是有必要的。

此处介绍一种比较简便的抗锯齿描线方法,它大体上是对一个传统的三次draw call描线方法(分别绘制线条中心和半透明的两个边沿)的一个改进,主要有这些好处:

  • 三角形数量大致上只有原始形状的两倍,而不是三倍。
  • 只需要一次draw call。
  • 思路比较简单。
  • 计算代价相对较低。
  • 不依赖任何独有的硬件特性。
  • 效果可以接受。

方法

在栅格化的时候,AA操作的实质,是计算被矢量图形部分覆盖的像素应当是什么“强度”,比如:如果像素有一半的面积被覆盖,那它的“强度”应当是50%;有三分之一被覆盖,它的“强度”就应当是33%。那么如何搞出一个比较合适的“强度”呢?这个方法的思路是:让像素按照自己中心距离线条边界的距离逐渐淡出:处于边界内部0.5像素尺寸或更靠内的像素有完全的“强度”,处于边界外部0.5像素尺寸的像素是完全淡出的。这是对像素覆盖率的一个近似。

为了达到这个目的,首先我们在生成线条的几何轮廓的时候,进行下列两点修正:

  1. 光栅化的时候,不考虑任何硬件AA,那么只有中心在几何体之内的像素才会参与渲染。为了保证仅部分覆盖的像素也参与渲染,线条的几何轮廓需要稍微搞大一点。如果线条宽度为w,半径r = w/2,那么改为生成r+1半径的轮廓(实际上应当+0.707像素尺寸就够了)。这是为了保证淡出范围的像素被覆盖。
  2. 生成轮廓的时候,劈成左右两半边,并且赋予一个顶点属性记录自己离线条中心有多远:在线条中心的顶点设置为0,在边缘的顶点设置为r+1,让它插值到fragment shader。这样自然就知道每个像素离中心多远了。

这个过程相当于构建了一个矢量的有向距离场。

使用3D API进行2D描线的一个抗锯齿方法_第2张图片

粗实线为线条中心,深灰色标识原始线条宽度,浅灰色标识“扩展”的线条宽度。

最终在fragment shader中,依据这个属性计算出片元离边界的距离,归一化之后直接给片元颜色额外的alpha系数,两者关系如下图所示:

使用3D API进行2D描线的一个抗锯齿方法_第3张图片

下图为使用了这个方法进行AA的实际渲染效果。其中绿色折线宽度为2像素,蓝色曲线宽度为1像素。
使用3D API进行2D描线的一个抗锯齿方法_第4张图片

一些显然的改进

DPI-aware

其实非常简单,算alpha的时候假设像素尺寸不是1就好了:假如DPI缩放是2,那么像素尺寸就是0.5,并依此计算像素“强度”。于是alpha值和中心距离的关系稍作修改,如下图所示:

使用3D API进行2D描线的一个抗锯齿方法_第5张图片

DPI缩放值没法直接从shader中获得,需要从另外的地方拿到(比如窗口系统),并在调用时塞到uniform里。

更正确的像素“强度”

上述简化的AA算法的像素“强度”估算,其实只在线条方向平行于像素边沿时才是正确的,其它方向都会有一些偏差,会导致细曲线看上去有些奇怪。

完全精确的像素“强度”其实也并不那么困难,但需要额外的信息:在生成几何体的时候,把线条的切线方向传进顶点,并插值到片元。这样一来,片元同时知道自己离边缘的距离和切线的方向,是能正确计算出自己的覆盖率的,依此算出的像素“强度”基本上是精确的。

你可能感兴趣的