2D矢量绘制引擎thorvg源码剖析
2026/9/3 6:47:21 网站建设 项目流程

前言

今年因工作需求,我开始深入研究thorvg(一款 2D 矢量绘制库)。在研究过程中我越发感受到,这类 2D 矢量绘制库对于 2D UI、客户端开发等领域来说,处于非常底层的基石位置——这就好比Skia之于 Android 客户端开发一样。

更关键的是,thorvg 和 Skia 有一个相似之处:社区生态并不算强大。然而,它们却是现代互联网图形技术不可或缺的基底。与之形成鲜明对比的是,围绕它们的技术讨论群、技术博客、源码分析等资料都极为稀缺。官方文档虽然提供了一些基础资料和教程,但深度有限;而直接询问 AI 得到的大多是比较肤浅甚至错误的信息,难以满足实际工程需求。

正是基于这样的现状,我决定像去年公开 Skia 学习笔记那样,将这段时间研究 thorvg 的笔记整理并分享出来。希望能起到抛砖引玉的作用,为这个相对小众但至关重要的领域,稍微丰富一下社区资料与技术讨论氛围。

如果文章中有任何疏漏或错误,欢迎在评论区交流指正。

thorvg基本介绍:

ThorVG 是一款轻量级、高性能的2D开源矢量图形库,专为现代跨平台应用(PC 、移动端、嵌入式、web)而设计。它基于保留模式的场景图架构,支持丰富的矢量图形绘制、图像渲染、文本布局及 Lottie 动画播放,并可通过软光栅、OpenGL/ES、Vulkan 及 WebGPU 等多种后端进行硬件加速渲染。其简洁的 C++ API 与模块化设计,使其易于集成到各类嵌入式、桌面及移动端项目中,是构建流畅 UI 与动态可视化内容的理想选择。
官方资源与文档
项目仓库:​​ThorVG on GitHub
官方查看器: ​ThorVG View
API 文档:​C++ Native APIs
在线 Playground:​ThorVG Playground
VS Code 插件:ThorVG LiveView(支持在编辑器中实时预览)

构建与编译指南
构建 ThorVG 及其示例项目需要预先安装 Meson 构建系统与 Ninja 工具。在 macOS 与 Linux 环境下,通用构建指令如下:

meson setup builddir# 配置构建目录(ThorVG 主库与示例项目命令一致)ninja-Cbuilddirinstall# 编译并安装

运行示例

./builddir/src/Shapes# 运行软件光栅化后端./builddir/src/Shapes-egl# 运行 OpenGL/ES 后端(Linux 下可用)./builddir/src/Shapes-ewg# 运行 WebGPU 后端

关于 WebGPU 后端的注意事项: ThorVG 的 WebGPU 后端依赖于社区项目 wgpu-native。若在运行时遇到 Run-time dependency wgpu_native found: NO 错误,说明系统未安装该库及其头文件。 此外,WebGPU 通常依赖 Vulkan 支持。在 Linux 下,建议先sudo apt install -y vulkan-tools安装 Vulkan 工具链并vulkaninfo --summary检查环境:

ThorVG 功能覆盖完整的矢量图形与动画能力,支持能力如下:

  1. 线条与形状:内置矩形、圆形等基础图形,支持自定义 Path 路径矢量几何绘制。
  2. 填充能力:支持纯色填充、线性渐变、径向渐变与路径裁剪。
  3. 描边能力:可配置描边宽度、连接样式、线帽、虚线规则与路径修剪。
  4. 场景管理:保留式场景图架构,支持图层树管理、层级变换与界面布局管控。
  5. 画面合成:支持多种 Blend 混合模式、图层遮罩、剪切与嵌套场景合成。
  6. 文本渲染:支持 TTF/OTF 可缩放矢量字体、Unicode 字符、多行自动排版。
  7. 图像解析:原生支持 SVG、PNG、JPEG、WebP、BMP 等主流图片格式。
  8. 视觉特效:内置模糊、投影阴影、色调、单色、颜色替换、填充特效。
  9. 动画能力:原生支持 Lottie JSON 矢量动画的完整解析、渲染与播放。

thorvg框架:

了解一个引擎或库首先看框架图。ThorVG 的核心库保持约 170KB 的二进制大小,比skia的2M还要小很多,由于它的轻量化使得它除了可以在PC、移动端外还能再Soc、mcu等资源性能低配的系统中也能很好的运行!

C/C++ (NATIVE): 面向原生应用开发,如桌面软件、移动端 App (Android/iOS/鸿蒙)、嵌入式系统等。这是性能最高、控制最精细的接口。
JS/TS (WEB): 面向 Web 开发,允许在浏览器环境中使用 JavaScript 或 TypeScript 调用 ThorVG 的功能,通过 WebAssembly (Wasm) 技术实现,Emscripten 把 C++ 核心编译成 WebAssembly,再由独立仓库thorvg.web

C API:在路径E:\thorvg\src\bindings\capi\thorvg_capi.h 下是对C++ 很薄的一层封装。C++ 是源码主 API,C 是在其上派生出的绑定,底层确实大量依赖 reinterpret_cast类型转换.

PIMPL 变体 / C ABI 稳定性技巧:C 头文件(ABI 契约)不携带任何 C++ 类布局信息,因此 C++ 内部类的成员变更、虚函数表变化都不会破坏 C ABI 兼容性,只要 tvgCapi.cpp 里的 reinterpret_cast 目标类型和实际分配对象类型保持一致即可。代价是类型安全完全由约定维护——如果调用方把 SwCanvas 创建出来的指针传给期望 GlCanvas* 的函数,reinterpret_cast 不会报错,只会在运行时表现为未定义行为

Paint 树(场景图)
Paint是thorvg2D矢量绘制的场景管理核心, 所有可见元素都派生自 Paint。提供统一接口 上层 API 都可以通过 Paint* 指针来操作,多态性提供 :渲染器可以遍历一个 Paint 列表,并调用每个对象的 render() 方法,而无需关心其具体类型。具体的渲染逻辑由各自的子类实现。 每个 Paint::Impl 持有变换矩阵、透明度、混合方式(BlendMethod)、蒙版(maskData)、裁剪(clipper)、指向后端渲染器的 RenderMethod* 指针,以及脏位标记 RenderUpdateFlag

structPaint::Impl{// 1. 变换与外观属性Matrix transform;// 本地变换矩阵:平移、缩放、旋转、斜切floatopacity;// 0‑1 透明度BlendMethod blendMethod;// 混合模式:正常、叠加、相乘等Paint*maskData;// 蒙版,另一个Paint作为蒙版Paint*clipper;// 裁剪节点,按该Paint轮廓裁剪本节点// 2. 渲染后端桥接RenderMethod*renderMethod;// 指向渲染后端对象(CPU/GL/WebGPU),真正执行光栅化// 3. 脏标记【增量渲染核心】 RenderUpdateFlaguint32_tupdateFlag;// 位标记:矩阵变更、透明度变更、路径变更、子节点变更、蒙版变更……// 4. 场景树关系Paint*parent;// 父节点std::vector<Paint*>children;// 子节点列表,仅Scene/Picture这类容器才有有效子节点// 5. 计算缓存Matrix worldTransform;// 缓存:累计父节点的全局世界矩阵,避免每一帧重复递归计算Rect dirtyRegion;// 当前节点产生的脏区域,用于脏矩形合并......};

src/loaders/ 下的 Lottie、SVG、图片(PNG/JPG/WebP)、字体(TTF)、媒体(Media)等各类加载器,职责是把外部格式(JSON/XML/二进制)解析后统一构建成 Paint/Scene 树,核心渲染逻辑完全不关心原始文件格式,加载器独立性原则,Picture 通过 LoaderMgr 静态管理器识别并调度对应加载器。

thorVG 的场景图本质是一棵带引用计数、支持惰性求值( 延迟计算策略:不在数据变化的那一刻立刻重新计算结果,而是打一个"脏"标记(dirty flag),真正需要用到这个结果时才去计算,并把结果缓存下来,下次如果没有再变脏就直接复用缓存,避免重复劳动)的合成树。SceneImpl 用 list<Paint*> 组织子节点顺序(决定绘制顺序),update() 递归下发变换/透明度/裁剪并按需临时放行"包围边界"的局部渲染优化( 把内部不再变化的复杂子树,预先渲染成一张缓存图,后续只做整体变换,避免每帧重复计算内部细节),render() 递归渲染并按 cmpFlag(是否需要蒙版/混合/后处理/半透明合成)决定是否申请离屏目标做中间合成,bounds() 用脏标记 vdirty 避免每帧都重新合并所有子节点包围盒。整棵树通过 parent 指针 + refCnt 引用计数管理生命周期与所有权唯一性(一个节点不能同时属于两个父场景)。

良好的可移植性:

其他渲染器和 ThorVG 之间切换绘图上下文,从而轻松使用 ThorVG 的 API,thorvg可以很好的跟其他的引擎通信。程序中包含我们自研或其他主要的渲染器,可以通过在主渲染器和 ThorVG 之间切换绘图上下文(比如同一个opengl的context)来无缝地使用 ThorVG API。在这些 API 调用过程中,它通过其渲染后端引擎执行同步或异步渲染。
渲染引擎间的通信全解:原理、方案与实战

跟skia相比:官网还说thorvg平均比skia矢量图形引擎快 2.3 倍。在矩形、笔触、旋转和圆形渲染等几何密集场景中,这种优势尤为明显。

脏区域渲染更新机制:

脏区域机制是 ThorVG 在 v1.1 版本中引入的一项关键性能优化特性。它允许渲染器仅重新绘制场景中发生变化的部分,而不是每一帧都重绘整个画布。这对于高分辨率显示或复杂场景中的局部动画更新尤为重要,能显著降低 CPU 负载和功耗。脏区域更新机制在现代的Ui/2D渲染的性能至关重要,不管是skia、还是QT都引入了该机制。ThorVG的脏区域(dirty region)增量渲染机制目前只在CPU/软光栅渲染器(SwCanvas / SwRenderer)中真正生效。GL、WebGPU等GPU引擎虽然在接口层声明了 damage() / partial(),但实际上是空实现,不会做真正的局部重绘。底下就来讲一下通用的脏区域相关的知识:

显示列表/ui控件树:
分成两种一种是容器类似“盒子",移动/缩放容器时,内部子项整体跟随变换,普通显示对象为叶子节点(控件、文本、图片等)。每个节点包含两个属性:自身矩阵(在父容器中的位置/缩放)和自身矩形(未缩放时的原始大小),最终屏幕位置 = 自身矩形 × 屏幕矩阵(自身矩阵 × 所有父级矩阵的连乘结果)。

全屏刷新 vs 局部刷新
全屏刷新: 每帧流程:清空整个屏幕 → 遍历显示列表 → 绘制所有可见对象,通常只有很老旧的系统在软光栅上是全屏刷新,货3D应用上,在GPU上是全屏刷新,因为在gpu上局部刷新往往是得不偿失的,所以在图形应用不管是直播、图片、视频编辑、游戏都是全屏刷新。
优点:原理简单,实现容易,是大多数GPU渲染引擎的默认方案。
缺点:即使只有极小区域变化,也要清空并重绘整个屏幕,软光栅开销大,gpu端开销小;
局部刷新: 每帧流程:计算变化区域(重绘区) → 只清空重绘区 → 只重绘相交对象,通常主要在ui系统或2D系统的软光栅比如thorvg与skia的swCanvas等。
优点:CPU软光栅下大幅提升渲染性能(复杂UI场景下可轻松满帧),节省电量(实测:耗电量降至原先的 30%),降低发热功耗等,并且无变化时直接跳过绘制。
缺点:仅限于2D不适用于3D场景,只适用于软光栅。

获取重绘区域
获取重绘区域与合并区域是最关键的脏区域实现。
屏幕矩阵 = 自身矩阵 × 所有父级容器矩阵(逐层连乘)
2
屏幕矩形 = 自身矩形 × 屏幕矩阵

通知机制到 产生重绘区,每当显示对象的屏幕矩阵或自身矩形发生变化时:

  1. 重新计算屏幕矩形
    2、得到两个矩形:
    • 改变前的屏幕矩形(旧位置oldPos)
    • 改变后的屏幕矩形(新位置newPos)
  2. 这两个矩形就是本次的重绘区

合并绘制区域:
一帧中可能有多个对象同时变化,产生大量重绘矩形。如果逐个处理,开销巨大所以才需要合并绘制区域。

合并两条核心规则

  1. 优先收益合并:两个矩形合并之后总面积 < 两个矩形面积之和,则合并;优先选相交面积最大的一对合并
  2. 强制合并兜底:规则 1 不满足,但剩余脏矩形数量 > 3,强制继续合并,优先选合并后面积增量最小的两个矩形
    经验阈值:把脏矩形数量控制在 ≤3 个 ,是性能平衡点;兼顾重绘开销与计算开销。
    特殊兜底场景:如果画面大量元素变动,零碎脏区合并后得到全屏矩形,自动退化为全屏刷新模式,所以不管是thorvg还是skia都是一套算法兼容局部刷新 + 全屏刷新两种模式。

thorvg具体实现:ThorVG 当前只有 CPU软光栅后端 (tvgSwRenderer) 真正实现了脏区域(dirty region)/局部重绘机制,涉及区域收集、按分区排序合并、退化全屏兜底等步骤。

  1. UI 树遍历收集脏区(Damage):UI 树在 update() 遍历 tvgPaint/Scene 树时调用 damage(),同时记录节点的旧位置(prv)与新位置(cur),作为脏区域的来源。
  2. 区域入队(Add):渲染任务完成时(SwTask::complete),将新旧两个包围盒(Box)通过 dirtyRegion->add() 加入脏区域收集队列。
  3. 智能合并(Commit):使用**扫描线算法(Sweep-line)**按 X 轴排序,在 16 个分区 (PARTITIONING = 16)内对重叠、包含或相邻的矩形进行切分与简化,最大限度减少重绘次数。
  4. 降级兜底(Fallback):swRender内部有 fulldraw 标志,当缓冲区被清空或脏区域功能被停用 (dirtyRegion.deactivated()) 时,直接走全屏绘制路径而非局部区域绘制,另外 Scene::update() 中当场景是 fixed(固定尺寸/viewport裁剪)类型时,会临时调用 renderer->partial(true) 关闭子节点的局部渲染优化,改为按整体 viewport 处理。

thorvg线程模型:

ThorVG 的线程模型并不是想着是opengl就是用“单一渲染线程通过事件驱动”的架构,它采用的是基于内部内置了一个线程池的任务调度机制。将处理中的各个阶段(如动画的解码、解析json配置等、渲染等)拆分为独立的任务,并分配给线程池中的多个线程并行处理。调用 tvg_engine_init(threads) / Initializer::init() 时,会创建一个固定数量的工作线程池,init确定之后不能更改。TaskSchedulerImpl 会为每个线程分配一个独立的 TaskQueue(每个线程各自的任务队列),并启动对应数量的 std::thread 去执行通用任务比如编码、解码、动画等。

同步渲染流程
Initializer::init(0) //threads == 0 时只使用主线程不使用线程池,所有任务在当前线程执行,同步模式下 sync() 本质上是空操作,也验证了thorvg并不强求使用多线程的灵活性。

异步渲染流程
Initializer::init(大于0) //threads > 0 时只使用主线程异步执行。异步sync()主线程在此阻塞,直到所有工作线程完成渲染,buffer 中的像素数据就绪

无论同步还是异步,应用侧调用的 API 序列都是固定的四步:add → update → draw → sync 对应源码是/renderer/tvgCanvas.h的struct Canvas::Impl 。区别在于 任务调度器TaskScheduler::request() 内部是把渲染任务立即在当前线程跑完,还是丢给工作线程队列异步处理最后调用sync()阻塞等待其他完成后进行。
对应好的demo源代码如下,
同步渲染流程

#include<thorvg.h>intmain(){tvg::Initializer::init(0);//0 同步执行// 假设已经创建好 GL context 并 make currentautocanvas=tvg::GlCanvas::gen();canvas->target(nullptr,nullptr,glContext,0,400,400,tvg::ColorSpace::ABGR8888S);autoshape=tvg::Shape::gen();shape->appendCircle(200,200,100,100);shape->fill(255,0,0);canvas->add(shape);canvas->update();canvas->draw(true);canvas->sync();// 0线程下依然同步阻塞,但底层是GPU光栅化tvg::Initializer::term();return0;}

异步渲染流程

#include<thorvg.h>#include<webgpu/webgpu.h>intmain(){//4 个工作线程 => 异步执行tvg::Initializer::init(4);// 需要事先创建好 WGPUInstance/WGPUAdapter/WGPUDevice/WGPUTextureautocanvas=tvg::WgCanvas::gen();canvas->target(device,instance,texture,800,800,tvg::ColorSpace::ABGR8888,/*type*/1);autopicture=tvg::Picture::gen();picture->load("complex_scene.svg");canvas->add(picture);canvas->update();//推入 worker 线程队列,立即返回canvas->draw(true);//光栅化在 worker 线程后台进行,立即返回doOtherWork();canvas->sync();//阻塞直到 GPU 命令提交/完成tvg::Initializer::term();return0;}

加载解析动画开启多线程
如果使用gl是只有一个渲染线程,开多线程对渲染线程是没帮助,主要的是初始化更快解析lottice的json动画更快,不然就thorvg主线程全部加载->解析->gl初始化->上传GPU资源->绘制

如上图结合“struct LottieLoader : AnimLoader, Task” 首次加载(open/read)会被丢进线程池并发加载解析了lottie动画。

初始化动画:threads == 0 时,header() 会直接同步完整解析整个 JSON(调用 prepare());threads > 0 时,header() 只做一次轻量的手写字符扫描(找 “fr”/“ip”/“op”/“w”/“h”),拿到动画基本信息就立刻返回,而完整的 JSON 解析被推迟到后续 read() 中通过 TaskScheduler::request() 异步进行。如下源码

注释与代码写的很清楚。而完整的 JSON 解析被推迟到后续 read() 中通过 TaskScheduler::request() 异步进行。综上所述当开启动画等最好thorvg开启线程池,使用多线程

RHI层:

Thorvg有点类似skia的几套实现,一套是软光栅Raster、另一套是基于opengl\opengles的实现,再后面就是基于WGPU实现的RHI图形后端。ThorVG 的 WebGPU 后端(WG 引擎)在原生 Linux/Windows 环境下,恰恰就是基于 wgpu-native 实现的。ThorVG 没有原生的 Vulkan 后端、也没有原生 DirectX 、metal后端,但通过 WG 引擎间接调用 Vulkan(Linux)和 D3D12(Windows),底层由 wgpu-native 自动适配。同理在macOS/iOS 的 Metal 也是同一条路径。这和 Skia 的 Graphite 走 Dawn(WebGPU 实现)的思路是同构的——区别在于 Skia 同时保留 Ganesh 那套 GL/Vulkan/Metal/D3D 原生后端,
原生端只有 3 套可编译启用的光栅化引擎如下表

引擎标识全称底层依赖
cpu软件渲染引擎纯 CPU 计算,可选 OpenMP 、SIMD加速
glOpenGL/ES 引擎系统 OpenGL 3.3+ / OpenGL ES 3.0+ 驱动
wgWebGPU 引擎原生端依赖 wgpu-native;网页端依赖浏览器 WebGPU

不过要特别注意:GL 引擎也能跑在网页端(WebGL2),不只是 wg 有浏览器路径。这里不展开介绍说明Wgpu的实现与教程有兴趣的可以看 学习 wgpu
而 ThorVG 刻意不做原生后端,用一套 WG 代码覆盖全部原生 API。收益是维护一份 GPU 管线代码即可(官方基准显示 WG 比 GL 平均快约 1.8 倍)同样是GPU硬件加速官网给出了wgpu与opengl的区别,但凡了解过现代图形API的都不会惊讶,都是意料之内的事情

wg结构图如下
对于上下文抽象WG 有独立的 WgContext 结构体统一持有 WGPUInstance/Adapter/Device/Queue。

GL结构图如下
对于上下文抽象GL 直接把裸的 mDisplay/mSurface/mContext 存在 GlRenderer 里,没有独立的 context 类,且 target() 接口签名(GL 是 void* display, void* surface, void* context, int32_t id…)。对于 RenderPass 管理GL 用显式的 mRenderPassStack(栈式管理多层离屏 FBO,用于 mask/blend 嵌套合成),每个 GlRenderPass 绑定一个 GlRenderTarget(FBO)来实现。

如上GL与WG两者在架构分层的抽象层级上是一致的(都是三角化 → 着色器/管线 → 凸多边形/凹多边形处理 → 离屏合成 → 批处理),但 GL 引擎因为要兼容传统 OpenGL 立即模式(IMR)状态机,实现上多了一层显式的渲**染任务对象(GlRenderTask)+ 渲染通道栈(mRenderPassStack)**模拟现代管线语义;而 WG 引擎因为 WebGPU 本身就是命令式/声明式 API,不需要这层额外封装,直接用 WGPUCommandEncoder/WGPURenderPassEncoder 表达。

参考资料:

1、 优化 Vulkan GPU 渲染器
2、 ThorVG 集成 3D 渲染管线可行性分析
3、v1.0 大版本重构
4、 v1.1 线程、WebCanvas 多线程架构图
5、显示列表与脏矩形渲染
6、Qt底层原理:深入解析QWidget的绘制技术细节(1)_qwidget 绘制
7、 学习wgpu中文版

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询