平滑圆角 3.0:技术视角

August 26

技术目标

平滑圆角在系统中是一个极其底层的图元。 它会出现在按钮、卡片、遮罩、图标容器、面板切角等大量位置上,而且半径跨度极大,从 0.5px0.5 px 到 1000px1000 px 都可能出现。

这意味着技术方案必须同时满足四个条件:

  • 形状稳定,不能因为缩放而产生明显变形
  • 边缘平滑,不能在高对比场景里出现肉眼可见的毛刺
  • 运行时成本可控,不能让系统里每一个圆角都重复做重计算
  • 适配范围足够大,能覆盖从 2D UI 到更复杂实时渲染场景的长期演进

技术结构

技术结构图

整个技术链路可以概括成四步:

  1. 使用高连续性的曲线定义理想圆角形状
  2. 用高质量抗锯齿方案把矢量曲线栅格化为缓存贴图
  3. 对缓存图生成多级 Mipmap,保证不同缩放比例下的稳定显示
  4. 在 GPU 中按实际 LOD 对材质进行滤波采样并输出到屏幕

这套结构的关键在于,把“昂贵但高质量”的工作尽量前置到离线或预计算阶段,把运行时的责任压缩到 GPU 最擅长的采样环节。

曲线建模:为什么选 Degree 4 NURBS

NURBS 曲线的可控性很强,可以比较直接地控制局部曲率变化。 在这个项目里,曲线建模需要同时满足两个要求:

  • 达到 G4 连续:圆角既是视觉元素,也是可能进入光照、法线生成等链路的几何基础。连续性做得越高,后续适配空间越大。
  • 形状尽可能接近正圆:系统历史上大量元素已经与正圆圆角建立了认知绑定,升级时不能让整体气质突然偏移。

G4 连续示意

正圆比较

如果只看数学上的“更平滑”,超椭圆也是一个很自然的候选。 但在系统设计里,曲线不是孤立存在的,多个圆角并排时形成的负形空间同样会改变用户对平台气质的感知。 因此最终的技术选型其实是一个被设计语言约束过的工程决策。

栅格化:为什么选 SSAA

现代屏幕本质上是像素阵列,矢量曲线最终仍然需要落到像素上。 栅格化方案并不缺,真正的问题是:在“圆角形状已经固定”的前提下,哪一种方案值得被预计算并缓存。

技术方案简介优点缺点
SSAA (超级采样抗锯齿)渲染高分辨率图像后降采样, 获得更平滑的边缘画质最佳, 几乎无锯齿, 适用所有场景性能消耗大, 内存占用高
MSAA (多重采样抗锯齿)对几何边界进行多重采样, 提升边缘平滑度性能与画质平衡, 对几何边界效果好对透明纹理和复杂像素效果支持有限
FXAA (快速近似抗锯齿)基于像素着色器的图像后处理, 检测边缘并模糊处理性能消耗极低, 易于集成细节丢失, 边缘模糊, 对高对比度场景效果有限
TAA (时域抗锯齿)利用多帧历史数据进行采样和混合, 消除锯齿能有效消除锯齿, 适合动态场景容易产生拖影和鬼影, 动态场景下表现不佳
CMAA (保守混合抗锯齿)结合边缘检测和混合, 优化性能与效果性能消耗低, 兼容性好效果有限, 部分场景下锯齿仍明显

抗锯齿示意

因为圆角形状是可缓存的系统资产,而不是每一帧都在变化的复杂几何,我们可以直接选画质最好的 SSAA,先栅格化,再复用结果。 这类“高质量但昂贵”的方案,一旦被前置到缓存阶段,运行时负担就会显著下降。

重采样:为什么是 Lanczos + 9 层 Mipmap

单张高分辨率圆角图并不能解决所有问题。 如果只依赖一张 512px×512px512 px \times 512 px 的缓存图在运行时做大范围缩小,图像会因为采样频率失配出现明显混叠。

这也是我最终把问题转化为典型纹理管线问题的原因:

  • 先做高质量母版缓存
  • 再生成多级 Mipmap
  • 让运行时采样在更合适的频段上完成

下表是做方案评估时的几个候选重采样算法:

算法/类型做法优点缺点/备注
Nearest Neighbor (最近点)取最近整数坐标像素值, 无插值。硬件支持 实现极快, 代码简单明显块状、锯齿, 质量最低
Bilinear (双线性)取周围 2x2 像素加权平均, 线性插值。硬件支持 平滑、速度快边缘模糊, 细节丢失
Bicubic (三次插值)取 4x4 像素, 用三次卷积核插值。清晰度与平滑度平衡, 细节较好计算量较大, 可能轻微振铃
Lanczos 重采样 (窗化 sinc)取更大邻域 (如 6x6) , 用 sinc 核窗化插值。锐利且混叠抑制优秀成本高, 可能出现振铃 (ringing)
Mitchell-Netravali三次核, 参数可调, 常用于高质量缩放。细节与平滑度兼顾, 适合内容管线算法复杂度中等
Area/Box (区域平均)输出像素对应输入区域平均, 常用于缩小图像。缩小时抗混叠好, 速度快放大时模糊, 细节损失
Gaussian (高斯核)用高斯权重模糊后采样。平滑自然, 混叠抑制好细节损失, 计算较重
Catmull-Rom三次插值家族, 锐度高, 常用于内容管线。边缘清晰, 细节好振铃风险, 成本中等

重采样比较

最终选择 Lanczos 的原因有两个:

  1. 它对混叠抑制足够好,缩小时边缘更干净
  2. 它对原始信号的形状保持更完整,不容易把设计好的曲率关系“抹软”

我们可以选用 a=3a = 3 的窗口 Lanczos 归一化离散核对原始图 L0L_0 进行卷积以获得第一层重采样图像 L1L_1: (I∗La)[i,j]=∑m=i−a+1i+a−1∑n=j−a+1j+a−1I[m,n]  La(i−m)  La(j−n)(I * L_a)[i,j] = \sum_{m=i-a+1}^{i+a-1} \sum_{n=j-a+1}^{j+a-1} I[m,n]\; L_a(i - m)\; L_a(j - n)

接着对 L1L_1 递归操作,获得剩余 Mipmap 层级: Ln=I∗Ln−1L_{n} = I * L_{n-1}

使用多级 Mipmap 而不是对单图直接缩小,是必要的。否则在大缩放比场景中会更容易出现 摩尔纹。

mipmap 比较

图片来自维基百科: https://upload.wikimedia.org/wikipedia/commons/5/59/Mipmap_Aliasing_Comparison.png

下面是 Nearest、Linear、Mitchell 和 Lanczos 的实机拍摄对比。可以看到 Nearest 和 Linear 有明显“毛刺”,Mitchell 与 Lanczos 都已经足够平滑,但继续放大后,Lanczos 对原始曲线形状的保留更好。

重采样实测1

重采样实测2

运行时采样:为什么只用 Trilinear

技术框架的最后一步是在多级 Mipmap 中进行材质采样和滤波。 常见选项如下:

滤波方式简介优点缺点
Nearest-neighbor选择最近像素点的颜色作为采样结果实现简单, 性能高画面锯齿明显, 细节丢失
Nearest-neighbor with Mipmapping最近点采样结合多级 Mipmap性能高, 减少远距离采样伪影仍有锯齿, 过渡不自然
Bilinear对采样点周围四个像素做线性插值边缘平滑, 性能较好细节模糊, 远距离仍有伪影
Trilinear with Mipmapping在 Mipmap 层间做线性插值 (双线性+层间插值)过渡自然, 锯齿和伪影减少性能消耗增加, 细节仍有限
Anisotropic (各向异性滤波)根据视角优化采样, 提升斜角和远距离纹理清晰度细节保留好, 远距离和斜角表现最佳性能消耗较高, 硬件支持要求高

考虑到目标场景主要是移动端 UI,而不是大透视角度的斜向纹理观察,因此各向异性并不划算。 最终选择 Trilinear with Mipmapping,本质上是一个性价比很高的终端解法:画质已经足够稳定,且现代 GPU 对它有成熟硬件支持。

最终效果

实际结果

用显微镜观察屏幕次像素的表现可以明显感知到,新的平滑圆角在直边转向圆角边的过渡更加平滑,基本消除了原先的 bump 感。

NEW (OS3)
OLD (OS2)

后续演进

经过三年多断断续续的研究和对行业其他实现的分析,目前已经可以把高性能平滑圆角抽象成一条比较稳定的技术架构。 之后如果图形学某个中间环节出现更优算法,只需要替换该阶段,而不需要重做整条链路。

比如在重采样部分,John Costella 提出过 Magic Kernel 这样的卷积核,它在某些低分辨率设备上可能会有潜在价值。 对我来说,这个项目真正重要的不是“这次用了哪一种算法”,而是把圆角问题转译成了一条可以持续升级的图形管线。