PAG (Portable Animated Graphics) 是一套完整的动画工作流。它提供从AE导出插件,到桌面预览工具,再到各端的跨平台渲染SDK,助力于将AE动画方便快捷的应用于各平台终端。PAG目前是公司AVGenerator OTeam开源协同小组的核心组件之一,广泛应用于公司内外40余款主流APP或业务,涵盖UI动画、视频编辑、特效模板、服务端特效渲染等多个场景,于2022年1月开源至GitHub。
PAG(Portable Animated Graphics)是腾讯自主研发的一套完整的动画工作流解决方案,助力于将 AE 动画方便快捷的应用于各平台终端。和 Lottie、SVGA 相比,支持的 AE 特性更多,支持的平台更广(增加了 mac OS、Windows 和 Linux),性能方面也做了深层次的优化,支持图层编辑,可以与视频编辑场景紧密结合。目前已经广泛应用于公司内外几十款 APP,包括国民级 APP 微信、QQ、腾讯视频、QQ 音乐、QQ 空间等。
本文将会对 PAG 与 Lottie、SVGA 的工作流程、实现方案、性能等进行对比,并且将会介绍 PAG 特有的一些能力,为一些想要了解或接入 PAG 的开发同学提供一些参考。
下面将对 Lottie、SVGA、PAG 的工作流程进行对比。
下面是 Lottie 的实现流程图,设计师使用 AE 设计好动画, 通过 bodymovin 插件将 AE 工程文件导出为 json 文件,在客户端(使用 Lottie SDK)解析,最后通过各平台原生渲染方案进行渲染,其中在 Android 平台上通过 Canvas 进行绘制,在 iOS 上通过 CALayer 进行绘制,在 web 端支持 SVG、Canvas 和 HTML 绘制。
图1 Lottie工作流程图
rLottie 与 lottie 工作流一致,在 SDK 上实现不一样,rLottie 没有使用平台特定实现,是统一 C++实现。
SVGA 流程如图 2 所示,大体流程与 Lottie 类似,使用 SVGAConverter 插件导出,文件是 PB 序列化以后 zip 压缩的格式,在具体实现上通过设置帧率来生成一个配置文件,使得每一帧一个配置,每一帧都是关键帧,从而在绘制的过程中不用解析高阶插值。SVGA 在 Lottie 方案基础上进行了优化和完善,但是不支持复杂的矢量形状图层和特效。
图2 SVGA工作流程图
PAG 的流程类似 Lottie,设计师使用 AE 设计好动画以后,通过 PAGExporter 插件读取 AE 工程文件,根据具体需求选择矢量导出、BMP 预合成、混合导出方式中的一种导出一个 PAG 二进制文件,客户端对该 PAG 二进制文件进行解码、渲染,各端共享一套 C++实现,平台端只做接口封装。(导出插件:PAGExporter;桌面预览工具:PAGViewer;客户端渲染 SDK:PAG SDK)
图3 PAG工作流程图
以上 3 个库的工作流程大体相似,不同之处在于导出和渲染。
下面对 Lottie、SVGA 和 PAG 的实现方案进行对比。
Lottie 最早从UI动画场景出发解决矢量动画渲染的问题,从官方社区来看,我们能容易发现 Lottie 的矢量基因,社区作品大多是矢量图形类动画。SVGA 是 YY 直播的开发工程师 2017 年发布的一套跨平台动画解决方案,诞生于直播场景,SVGA 不支持复杂矢量图形动画,对位图动画的支持超过 Lottie,其最初的目标是为了改善和弥补Lottie。不可否认,两者都是业界优化的动画解决方案。PAG诞生于2016年,最初的原因是为了解决更为复杂的视频编辑场景下动画渲染问题,同时又完美覆盖了UI动画和直播场景。
一个有意思的共同点,以上三种方案的作者都有比较丰富的 Flash 相关背景,都在把Flash完善动画工作流的实现方式带到移动端,三者出发的场景不同,因此实现的方式也会存在一些差异。
Lottie 和 SVGA 都使用 AE Script SDK 来导出 AE 工程,但是 AE Script SDK 本身存在一定限制,不能访问 AE 文件中的所有属性,PAG 则使用 AE C++ SDK,能访问 AE 文件中所有属性和一些高级 API,能够实现对 AE 文件的完整导出。
Lottie 和 SVGA 渲染层面的实现依赖平台端接口,因此不同平台会存在支持的 AE 特性有所差异、渲染效果不一致等问题。PAG 渲染层面使用 C++实现,所有平台共享同一套实现,平台端只是封装接口调用,提供渲染环境,因此 PAG 所有平台支持特性一致,渲染效果一致。rLottie 跟 PAG 类似,底层共享一套 C++实现,素材支持 lottie 的 json 文件,矢量渲染性能还不错,但缺少各平台封装,支持的 AE 特性不全,也不支持文本、序列帧等。
Lottie 和 SVGA 依赖平台端的接口绘制,只能依赖平台侧接口的渲染缓存,PAG 内部有三级缓存机制,从素材结构到渲染结构都有缓存,实现了非常高性能的绘制效率。
Lottie 导出素材格式是 json 文本,可读性高,但是承载 AE 特性能力差,文件体积大,解码速度慢。SVGA 使用 ProtoBuffer 序列化,解码速度快,最终生成的文件直接使用 zip 压缩。PAG 采用二进制的编码方法,配套自研编解码器,动态比特位压缩,冗余信息极少,文件体积最小,解码速度最快,且支持图片和音频信息编码。
目前 Lottie 仅支持 Android、iOS、web、mac OS,SVGA 支持 Android、iOS 和 web 端,PAG 可以支持到 Android、iOS、web、mac OS、windows、Linux,涵盖到所有平台。
表1 动画文件对比
如上表所示,PAG 采用了动态比特位的压缩技术,动画文件可以做到足够小。相同的 AE 工程,PAG 导出的动画文件大小是 Lottie 动画文件的 51%,SVGA 动画文件的 22%。
表2 矢量动画渲染性能对比
如表所示,在矢量图形渲染方面,PAG 优化 Lottie 和 SVGA,内存占用方面会偏大一些。
PAG 从第一行代码写下到现在已经经历了 5 年,期间经历了多个版本迭代:在 PAG 1.0 版本中,我们重点设计了高压缩率的文件格式,以及游戏引擎级别的跨平台的渲染架构。虽然还支持了带动画的文本编辑能力,但 1.0 版本跟 Lottie 一样仅覆盖了 AE 的纯矢量导出能力,很多复杂动画效果无法被完整还原。
于是在 PAG 2.0 版本中,我们引入了 BMP 预合成的混合导出能力,同时解决了 AE 全特性的支持和可编辑性的问题。2.0 版本还引入了占位图替换的能力,为照片模板和视频模板的生产带来了工业化量产的能力。
到 3.0 版本时,固定时间轴的模板已经越发没法满足需求,PAG 在编辑性上又进行了一步探索突破,开放了图层级别的原子编辑组合能力,支持了从原子特效组件动态构建模板,很好的支撑了游戏战报和一键出片等动态模板的需求。
截止到本月,PAG 4.0 版本的开发也接近收尾。这个版本耗时了近一年时间完成了在渲染架构上最大的一次升级,彻底脱离了谷歌的 Skia 2D 绘图库,PAG SDK 包体也直线下降了约 60%,并完成了包括 Web 平台在内的全平台覆盖。
下面详细介绍一下各个版本迭代过程中的重点技术演进细节:
PAG 方案最早就是诞生在视频编辑的场景下,要让动画能够在视频编辑场景下无缝整合使用,需要解决两个问题:支持离屏渲染绘制、子线程渲染。Lottie 的动画方案之所以无法应用在视频合成中,主要是因为依赖了平台相关的 UI 框架,开发成本较低,但也导致了它只能渲染到 UI 视图上,并且无法在子线程中使用。
PAG 的整套动画方案就是基于 C++跨平台架构研发的,一直从最底层的动画插值器,还原到上层的时间轴和图层渲染树系统,虽然开发成本较高,但是所有端共享同一套代码,天然的能保障跨端渲染一致性。最重要的是能直接渲染到离屏纹理上,并完美支持子线程动画渲染。
图4 PAG与视频渲染相结合
在解决完整合视频渲染的问题后,还需要考虑怎么优化动画的性能。视频编辑的场景本身资源耗费比较高,每帧并行地存在多个视频解码以及各种特效处理,此时留给 PAG 的渲染时间就不太多。我们需要把 PAG 的渲染性能优化到极致,来满足视频编辑场景的实时预览需求。
分析动画文件的特效,我们发现大部分的动画素材实际上并不是整个时间轴上都在变化,或多或少会存在一些画面静止的区间。而 PAG 在刷新时,如果遇到这些静态区间,会直接返回上一帧的动画内容,自动跳过任何重复的绘制。极限情况下,假设有一个一分钟的动画素材,但实际上全程都是静止的,它对 PAG 来说就相当于一张静态图片,整个刷新的过程中都是 0 开销。而在 Lottie 方案中,整个刷新过程都是全量的开销,因为它每帧都会清空屏幕重新刷新。
这里的解决思路是用空间来换时间。
第一个层面是文件缓存,主要解决 PAG 文件从文件解码到内存过程的耗时,同一个动画文件只需要解码一次,就可以放在多个动画实例中渲染,避免多个相同动画的重复解码。
第二个层面是绘制缓存,解码后的文件有多个时间轴属性,我们将生成的绘制数据缓存到共享文件中,一个文件的任何一帧,只要绘制过一次,第二次绘制就可以得到加速。同时还利用了静态区间的特点来优化内存,将每个图层拆分成多个属性组,每个属性组计算出静态区间的列表后,只缓存每个静态区间第一帧数据。
第三个层面是内容缓存,这个层级的加速效果是最明显的。通常情况下,图层的内容绘制是最耗时的,因为要经历栅格化等操作。但是内容一般不会随着时间轴变化,反而是轻量的矩阵参数会频繁的变化。根据这个原理,如果一个图层内容是静止的,我们会把他的内容缓存成一张纹理。这样整个时间轴上,只会经历一次栅格化的过程,后续每帧的绘制都可以复用第一帧的纹理,快速套用矩阵变换,接近零成本地渲染出动画效果。这里的内容缓存我们同样考虑了内存优化问题。例如一个动画文件预设的大小是 500x500,但是实际使用中,整体被缩放到了 50x50 的大小,那么内部创建的内容缓存,会对应的缩小相应的面积倍数。这样可以做到在保证清晰度的前提下,只缓存最小的面积。
在纯矢量的导出模式下,无论是那种实现方案,在众多的 AE 特性面前,都只支持将有限的 AE 特性导出渲染。因为在有桌面显卡的情况下,有部分 AE 特性都还需要跑进度条才能完成预览,在移动端根本没可能做到实时。另外还存在第三方 AE 插件的效果无法导出的问题。这在一定程度上限制了设计师的创造力。另一方面,由于相同的动画在 AE 中有很多实现方式,但性能却千差万别。于是我们思考如何解决众多 AE 特性支持的问题,通过分析 AE 提供的 SDK 的能力,我们发现 AE SDK 可以直接截图,可以导出任何效果,且包含第三方 AE 插件的效果,但缺点也很明显,图片无法进行编辑,如果通过截图的方式,文件会比较大。
图5 BMP预合成导出实现
针对截图后文件比较大的问题(动画一般不低于 24 帧),我们首先想到了视频编码的极限帧间压缩能力,相对于原始的图片序列帧,可以压缩到百分之一点几的大小,另外视频格式还可以使用硬件解码,从而获得比较高的渲染性能。但这里遇到的一个问题是:动画一般都是透明的,而视频格式却不支持透明通道。于是我们视频编码的同时,扩展了透明通道,如上图所示,左边为 RGB 的视频内容,右边为 Alpha 通道的灰度图,最终渲染的时候再合并回 RGBA 的图片,从而实现对透明通道的支持。渲染的过程中,由于启用了硬件加速解码,可以直接得到一个 YUV 的纹理。我们在这里的优化点主要是不使用常见的 FFmpeg 来执行 YUV 到 RGB 转换,从而避免纹理在 CPU 和 GPU 之间来回拷贝,而是自定义了一个 Shader 脚本,利用硬件加速在一次绘制过程中,同时完成 YUV 转换和 Alpha 通道合并。这里平均就能够提高 10%的渲染性能。
针对 BMP 预合成无法编辑的特点,我们将 BMP 预合成支持的粒度由文件延伸到合成,支持矢量和 BMP 预合成混合导出,从而实现了支持所有的 AE 特性又能保持运行时的可编辑性。
在照片模板和视频模板不断地量产过程中,固定时间轴和尺寸的模板已经逐渐出现了在应用上的瓶颈。特别是当一键出片、王者战报等智能模板需求的出现,整个模板不是由固定的时间轴组成,而是可能由多个原子特效组件拼装而成,设计师即使投入非常高的人力,也无法针对每一种情况进行排列组合输出。这里对 PAG 的编辑能力也提出了进一步的挑战:就是要能对多个 PAG 文件,同时具有空间位置和时间轴的组合能力。由业务方去控制组合的规则。基于这个需求,我们引入了图层渲染树的编辑架构,不仅支持文本和占位图比编辑,还支持图层级别的编辑。一个文件就是一棵渲染树,支持图层级别的任意修改位置甚至增删图层,也可以把别的 PAG 文件添加到这棵渲染树中作为子树。能在空间维度上进行自由的排列摆放。而在时间轴的组合上,我们提供了 PAG 时间伸缩的能力,包含循环,变速,定格等多种自适应模式。每个图层又提供了起始时间的调整能力,能够自由设置在时间轴上的相对位置,能够灵活适配用户视频的时长。
图6 PAG图层编辑
经过这些改造,新的接口不仅满足了智能模板的编辑性需求,也简化了原有业务调用的复杂度。例如原先业务上除了要构建外部的视频时间轴,还需要在渲染的过程中不断手动更新每个视频片段和 PAG 进度的对应关系。现在无论哪种使用场景,都可以简化为两个步骤:利用空间和时间的组合能力构建一个渲染树,然后播放或者导出即可。
在 PAG 的前 3 个大版本的迭代过程中,大部分的业务痛点问题都已经得到了很好的解决和覆盖。但是接入过程中始终一直还存在一个难以回避的痛点:SDK 包体能否进一步压缩?例如在某些头部的 App 对接过程中,甚至要求接入后包体 0 增量。对大部分应用来说,包体直接影响增长拉新的数据,因此包体优化确实是个刚需。在之前的版本里,我们的渲染架构由于依赖了谷歌的 Skia 2D 绘图库。我们也已经针对性做了非常多的定制和裁剪,但是 Skia 依然占据了 PAG SDK 75%左右的包体,无法在进一步进行裁剪。
而在性能方面,3.0 版本上层的 PAG 渲染架构已经做了游戏引擎几乎所有能做的优化策略。但是由于 Skia 需要兼容历史遗留的 CPU 绘制模式,在 API 上暴露会比较保守,很多针对现代 GPU 绘制管线可以进一步优化性能的接口都没暴露出来。另外由于 Skia 是针对 UI 这种随机绘制设计的引擎,内部做了大量的缓存来确保随机渲染的性能。而对于动画这种可预测的渲染模式没有很好的优化,如果针对性优化可以有效降低平均的内存占用。整体上由于渲染对 Skia 的依赖,导致我们在性能上想要进一步突破也遇到了瓶颈。
为了彻底打破包体和性能的限制,我们花了将近一整年时间自研实现了一套轻量的纯 GPU 绘图引擎。在包体方面,我们最大化利用了平台端提供的所有可用能力,例如复杂矢量图形的栅格化, iOS 直接使用平台自带的 CoreGraphics,文本方面利用起 CoreText ,Android 端图片解码直接利用 Java 反射等。最终实现以 500K 左右的包体覆盖了 Skia 绝大部分功能。
而在接口设计上,我们充分暴露了针对 GPU 渲染的优化能力给到调用层,例如提交纹理后统一不再重复缓存一份 CPU 图片,暴露传入纹理遮罩缓存的能力实现一次性上屏,并在移动端全面开启了 HardwareBuffer 接口的使用来加速纹理提交。在减小包体和内存占用的同时进一步提升了渲染性能的天花板。在接口易用性方面也自带线程安全的设计,所有 GPU 资源统一管理,外部任意线程释放引用都可以确保正确销毁,降低了使用 Skia 的 GPU 绘制模式时,容易出错并需要大量封装平台相关上下文代码的门槛。
表3 PAG 4.0 包体优化数据
另外在 PAG 4.0 版本中,我们也提供了对 Web 平台的支持。之前迟迟未覆盖这最后一个平台,部分原因也是在等待新的渲染引擎升级完成后,可以减少 Web 端的包体加载压力。在 Web 端,Lottie 和 SVGA 使用 Web 的 HTML、CSS 和 Javascript 重新实现了一遍。而 PAG 依然保持了全平台共享一套 C++ 代码的架构。通过 WebAssembly 将全新的渲染引擎直接绑定到 WebGL 接口上进行渲染,仅在文本和栅格化等模块上对 Web 平台做了针对性的优化适配。
目前这个新的绘图引擎仍然内置在 PAG 4.0 版本内,未来有可能会进一步抽离成独立的 2D 绘图库,应用到动画工作流以外更多的渲染场景中。目标实现成针对现代 GPU 渲染优化的,包体和性能达到最佳平衡的 2D 绘图引擎。
除了本文描述的 PAG 技术能力,相对于 Lottie 和 SVGA,PAG 的辅助工具也非常完善,如果需要了解,大家可以访问 PAG 官网:https://pag.io/
如果大家对改进 PAG 项目有任何的想法或建议,欢迎访问 Github 主页:
https://github.com/Tencent/libpag
提交 issue 或 pull request,一起参与到开源项目建设中,帮助 PAG 动画方案做到更好。
官方交流QQ 群: 893379574