Skip to content

矢量合成管线

2D 图形渲染引擎的核心任务是矢量路径的光栅化多图层合成。理解这两个核心机制,是掌握 2D 渲染原理的基础。

矢量绘制

2D 图形主要分为两种:位图和矢量,2D 渲染引擎需要同时支持两者。但是,其实主要工作量集中在矢量的绘制上。

位图(如 PNG、JPG)已经是像素数据,渲染时直接作为纹理上传到 GPU 或拷贝到目标表面,几乎不需要额外计算。

矢量(如 SVG、字体字形、基本几何)则由数学描述组成——直线、二次/三次贝塞尔曲线、圆弧等。引擎需要实时将矢量路径转换为像素,这个过程称为光栅化(Rasterization)。

矢量光栅化与 3D 引擎中的三角形光栅化类似,但面临更大挑战。2D 路径可能非凸、多环、自交,还需处理复杂的填充规则(nonzero 规则、even-odd 规则)和抗锯齿(计算每个像素的 alpha 覆盖率)。常用算法包括:

  • 扫描线算法(Scanline):经典 CPU 方式,使用活跃边表(AET)+ 边缘列表逐行扫描,Skia 的 CPU 后端采用此方法
  • GPU 方式:将路径细分(tessellation)成三角形,或使用 stencil buffer + cover,以及符号距离场(SDF)

文字本质上是矢量的一种——字形来自字体文件的贝塞尔曲线轮廓。但由于文字光栅化极其耗时,实际渲染中通常特殊处理:预生成字形纹理图集(Glyph Atlas)或 SDF 纹理,避免每次都从路径光栅化。

图层合成

2D 引擎大量使用分层 + 合成策略。不同数据源(位图、矢量路径、文字)可能在各自的绘制管线中并行处理,最终按顺序合成到目标表面。这种设计带来三个优势:

  • 支持叠加顺序(z-index)和混合模式(blend modes:SrcOver、Multiply、Screen 等)
  • 提供缓存机制,复杂子树只渲染一次,后续复用
  • 支持并行/异步渲染,多线程光栅化 + GPU 合成

画家算法

2D 渲染采用画家算法(Painter's Algorithm)处理遮挡关系:从后向前(back-to-front)逐层绘制,后绘制的内容覆盖先绘制的内容。这与 3D 引擎的深度缓冲(Z-buffer)有本质区别。

3D 引擎通过深度测试自动处理遮挡,无需严格排序。而 2D 引擎通常不使用深度测试,完全依赖绘制顺序保证正确遮挡。只有在特殊场景(如 stencil 裁剪)时才会显式启用深度相关功能。

典型合成流程

以 Skia 和 Flutter 为例,典型的合成流程如下:

  1. 构建显示列表:记录所有绘制命令(drawRect、drawPath、drawImage、drawText 等),每条命令携带 Paint 参数(颜色、笔刷、混合模式、变换矩阵)
  2. 分层决策:复杂元素(滤镜、裁剪、透明度 < 1、特殊混合模式)通常推入独立图层(SkLayer 或类似结构)
  3. 按画家算法顺序绘制:从后向前逐层渲染,透明元素单独排序后处理
  4. GPU 合成:各层作为纹理,执行合成 pass(混合 + 滤镜)

缓存优化

图层缓存是 2D 引擎的关键优化。复杂子树(如复杂的 SVG 路径、带有滤镜的容器)光栅化后缓存为位图,后续帧直接复用,避免重复计算。缓存策略需要权衡内存占用和渲染性能,动态内容需要及时失效缓存,静态内容则尽可能长地保留。

文字排版

文字的处理是 2D 渲染引擎中最复杂的部分之一,通常分为字符编码解析、字体整形(Shaping)、光栅化和合成四个阶段。

与简单图形渲染不同,文字渲染面临三个核心难题:

  • 语义与图形的转换——一个 Unicode 编码不对应唯一的字形;
  • 上下文相关性——尤其在阿拉伯语或印地语中,同一字母在单词不同位置的形状完全不同;
  • 精度要求极高——人眼对文字边缘模糊和间距异常极其敏感;

成熟的 2D 引擎通常集成以下开源库来构建文字子系统:

  • FreeType:读取 TTF/OTF 字体文件,提取贝塞尔曲线轮廓,转换为位图或 SDF 数据
  • HarfBuzz:决定每个字形在屏幕上的精确坐标,处理字距微调(Kerning)、连字(Ligatures)、从右往左(RTL)排版
  • FriBidi:处理 Unicode 双向算法(Bidi),当文字同时包含左到右和右到左语言时决定显示顺序

字形缓存

光栅化将曲线转为像素的过程极其耗时,渲染引擎使用字形纹理图集(Glyph Atlas)来优化。首次遇到字符时调用 FreeType 生成位图存入 GPU 纹理,记录 UV 坐标,后续渲染直接从纹理采样。针对动态文本,引擎会维护 LRU 缓存自动清理长时间不用的字形。

API 分层

以 Skia 为例,排版引擎提供两个层级的接口:

  • 低级接口drawGlyphs() 直接在指定坐标绘制已计算好位置的字形 ID,需要开发者自己处理排版逻辑
  • 高级接口:提供类似 HTML/CSS 的能力,设置字体大小、行高、对齐方式、自动换行、图文混排。SkParagraph 会自动调用 HarfBuzz 进行布局计算,开发者只需给出字符串

与 3D 渲染引擎的对比

2D 和 3D 渲染引擎在设计上有根本差异,这些差异源于各自的核心任务。

方面2D 渲染引擎(Skia / Cairo / Flutter)3D 渲染引擎(OpenGL / Vulkan / UE)
几何源2D 路径(贝塞尔、直线、矩形、圆等)3D 网格(三角形)
变换2D 仿射变换(translate/rotate/scale/skew)3D 投影 + 视图 + 模型变换(透视/正交)
光栅化对象矢量路径(scanline / tessellation / SDF)三角形(硬件光栅化)
遮挡/排序画家算法(back-to-front 顺序)深度缓冲(Z-test)或排序(透明物体)
光照基本无(或简单着色器)复杂 PBR / GI / 阴影
图层/合成核心特性(多层、缓存、blend modes)次要(后处理 pass、多 pass 渲染)
性能瓶颈路径复杂度、图层数量、CPU 光栅化顶点/片元数量、带宽、光追

一句话概括:2D 引擎的核心是"矢量 → 光栅 + 分层合成",强调精确的 2D 路径填充、混合顺序和缓存复用;3D 引擎的核心是"3D 几何 → 投影 + 光栅 + 深度/光照",强调空间变换和真实感模拟。

为什么 2D 更依赖图层

3D 引擎中图层是"应该较少使用"的特性,因为会破坏早期 Z 剔除和批处理优化,带来 overdraw、draw call 爆炸、带宽压力等性能问题。但在 2D 引擎中,图层是核心特性,原因在于:

2D 渲染没有深度缓冲,必须靠图层顺序 + 合成保证正确叠加;2D 合成成本低(简单混合 + GPU 纹理 blit);2D 引擎的绘制顺序本身就要求严格的画家算法,图层结构与此天然契合。因此,2D 引擎的分层设计不是性能负担,而是实现正确渲染和高效缓存的基础架构。