闭式覆盖误差约为Skia的十三分之一,拟合单步三点二毫秒

Windfoil: Closed-Form Coverage for Real-Time and Differentiable Vector Graphics

Matt DesLauriers

cs.GR, cs.CV

2026-10-02

用闭式公式计算二次贝塞尔轮廓的盒滤波缠绕数,覆盖误差约为Skia的十三分之一,图像拟合单步3.2毫秒。

这篇在解决什么

矢量图靠缠绕数定内外。从一点向右发一条水平射线,数边界穿过几次,由此判断点在不在内部。每像素只测一次,斜边就是台阶。抗锯齿另做:多重采样、近似覆盖,或把距离场烘成贴图。GPU 路径为了速度,常把曲线折成线段,或只探几条射线。

可微渲染更难。缠绕测试几乎处处是常数,梯度为零。DiffVG 用蒙特卡洛或光滑近似,Bézier Splatting 用大量高斯代替曲线。拟合时的覆盖,和导出到 Skia 之后的覆盖,各算各的。

Windfoil 对闭合二次贝塞尔给出盒滤波缠绕数的闭式,显示和反传用同一公式。

方法

形状由闭合、有向的二次贝塞尔组成,可以不止一条。像素中心外放轴对齐盒子,默认一个像素大,放大边缘就更软。盒子内缠绕数取平均,套上填充规则,乘不透明度,再按画家顺序合成。

格林公式把面积分变成边界积分。每根水平细条里,上升穿越加上左缘到曲线的宽度,下降穿越减去。二次曲线先在 x、y 极值拆成最多三段,两轴都单调。像素只处理与自己垂直范围相交的段,解二次方程找左右交点:左侧贡献零,右侧贡献全宽乘高度,中间的多项式闭式积分。

填充发生在平均之后。缠绕数在脚印里至多变化 1 时,结果与「逐点填充再滤波」相同;自交和重叠会分叉。nonzero 截断绝对值,even-odd 看离最近偶数多远。

显示端是片元着色器。单调段按水平行带装桶,片元只读碰到的桶。一条带轴就够,Slug 要横竖各一套。缩到宽高约四个像素以下,改用预计算墨量。

可微端把画面切成 tile,按绘制顺序列出可能贡献的形状。前向算覆盖,损失是均方误差。反向用解析 VJP 把图像梯度送回控制点、颜色和不透明度。CLIP 这类外部损失只要提供图像梯度。预处理在 CPU,每步几何变化都要重跑。正的滤波宽度下,覆盖随曲线连续变化。

结果

语料对照是 24 倍超采样后的盒式降采样。Slug 为公开着色器的 WebGPU 移植,Skia 走 Node 绑定。缩小近似已关闭。

方法平均绝对覆盖误差对照
Windfoil1.2×10⁻⁴与参考采样误差同级
Slug1.1×10⁻³约 9 倍
Skia1.6×10⁻³约 13 倍

图 2 的 @(Lato)边缘基本贴住参考。自交玫瑰结的竖缝被画成洞,Slug 同样,Skia 与 Figma 正确。参考图是每像素 64×64 点采样,和上表的 24 倍降采样不是同一份。

帧时间相对 Slug:放大快 1.2–2.6 倍,中段缩放慢 1.2–2.8 倍,小于约四像素快 5–6 倍。M2 上几千个字形能进 60 fps。Ghostscript tiger 有 304 个形状、约 2.2 万段二次曲线。

拟合平台是 RTX 2000 Ada,Windfoil 走 Node.js 与 Dawn,基线走 PyTorch。与 DiffVG 同初始、每形 8 段;Bézier Splatting 用原生初始化。DiffVG 优化采样 2×2,计分 8×8。PSNR 由均方误差换算,越高越接近目标。

设置WindfoilDiffVGBézier Splatting
Kodak 均值,768×512,512 形,800 步26.60 dB,2.57 秒,3.2 ms/步26.41 dB,269 秒,336 ms/步22.80 dB,23.0 秒,29 ms/步
Färlev,512×288,4096 形,800 步26.77 dB,10.1 秒21.31 dB,1658 秒22.61 dB,132 秒
Färlev,4096×2304,256 形,60 秒20.96 dB,2421 步19.96 dB,34 步19.77 dB,893 步
Färlev,512×288,4096 形,1 万步27.88 dB,149 秒未测23.02 dB,1683 秒

在达到 DiffVG 终分的 20/24 张 Kodak 上,中位加速 243 倍,另有 4 张没达到。Bézier Splatting 是 24/24,中位 196 倍。加速比按第一次碰到对方终分计算,不是单步时间比。4096 形、800 步高出 5.46 dB 与 4.16 dB。一万步日程上,512 形只高 0.53 dB,4096 形高 4.86 dB。5 万形、4096×2304、800 步为 25.46 dB、176 秒,1 万形为 23.23 dB、65.6 秒,基线未测。

300 秒墙钟含启动。512 形跨过 21.4 dB 用时 1.0 秒、38 秒、94 秒,结束于 22.0、21.8、21.5 dB,步数约 11.5 万、1500、1.08 万。CLIP 草图只报告损失更低,没给数字。印刷分块试到 30000×30000,像素互不累加,拼缝不接出一条线。

为什么重要

实时矢量要画得准,向量化和文字草图要推得动控制点。两套覆盖各近似一次,损失看到的边和上屏的边会对不齐。这里它们是同一个盒滤波。

Kodak 512 形只高 0.19 dB,质量打平。3.2 ms 对 336 ms 的单步,才让浏览器里的拟合成为交互。4096 形时 DiffVG 掉到 21.31 dB,优势来自场景变大。Bézier Splatting 单步约 29 ms,初始化不同,3.80 dB 不能全算成公式的功劳。

滤镜宽度可调,宽核退火到单像素就是由粗到细。帐篷核和三次 B 样条用求积,更慢,适合压摩尔纹。WebGPU 同一代码覆盖浏览器、Node 与 Deno。

局限与存疑

平均后再填充,只在脚印内缠绕变化不超过 1 时等于逐点滤波。自交竖缝会被画成洞,图 2 里能看见。旋转、剪切、透视下盒子不精确。各形状分开滤波,接缝和整图滤波不同。多采样或先整理轮廓能减轻,主实验没做。

只支持闭合二次轮廓,描边和三次曲线要先转换。没有蒙版、渐变、纹理。非盒核是求积。定点原子加会量化,也可能溢出。预处理在 CPU。误差数字不含缩小近似。Slug 对比的是移植。拟合各一次,没有误差棒。残差已接近参考噪声,十三倍是相对 Skia。

5 万形在 4096×2304 上 800 步要 176 秒,谈不上这一档分辨率的交互。配方由 Claude Code(Fable)在约一小时定向搜索中提出,作者实现并核对。论文称文献中未见相同做法。

术语

原文与代码

社区讨论

相关论文

全部论文解读