腾讯Libpag动画库研究2Pag实现原理

Pag实现原理

上一篇我们介绍了Pag的基本用法,这一篇开始我们来看一看Pag的核心实现原理。
由于都是利用AE制作动画,AE动画的基本元素也是各种Layer,Composition等。所以Pag本身的数据组成结构基本是和Lottie基本一样的,甚至源码实现上也借鉴了不少Lottie的实现。总的来说Pag最大的区别就是自己实现了一套自己的渲染系统。

环境搭建

我目前使用的编译环境是版本

  • OS:MaxOS 12.3.1
  • XCode:13.2.1
  • AndroidStudio:BumbleBee 2021.1.1
  • NDK:21.4.7075529
  • 源码Sha:5e10390e472e2fe269bf238dc057fa463585de51
    源码下载和环境搭建步骤如下:
  1. Github下载源码Pag的源码
  2. 进入libpag工程目录后,运行./sync_deps.sh。这个命令会去下载node cmake ninja yasm git-lfs emcc这几个编译需要用到的工具。还会使用depsync去安装vendor.json文件内的所有Pag使用到的依赖库
  3. 各平台IDE导入
    • libpag使用CMakeList组织编译结构,可以用CLion直接打开工程根目录,打开后如果sync报错The local md5 cache is out of date! Please run the 'update_baseline.sh' to regenerate the cache.,根据提示在终端运行update_baseline.sh后再次sync即可成功
    • MacOS和IOS可以使用XCode打开macosios目录的workspace项目文件即可
    • Android平台直接用AndroidStudio根目录下的android目录即可

由于我对Android平台最熟悉,这里就用Android平台来进行分析了。其他平台的核心逻辑是一模一样的,只是创建PagSurface的方式大同小异罢了。

准备知识

  • 从Pag的源码结构可以知道它是一个C++语言为核心的动画库,所以我们需要一定的C++知识储备
  • 从介绍里面可以知道Pag是通过图形硬件API为核心来实现一套动画库,需要对OpenGL之类的图形API有一定的了解
  • Pag的依赖里面有用一些多媒体相关的依赖库,如果对多媒体图像有一定了解更好

情景分析

我分析一个软件的原理比较喜欢从一个简单功能场景入手,抽丝剥茧似的从上到下分析。从上一篇…/pag_introduce/pag_introduce.md可以知道使用Pag的方法仅仅如下即可:

// 1. 加载Pag文件
PAGFile pagFile =  PAGFile.Load("/path/xxx.pag");
// 2. 创建Pag画布
PAGSurface pagSurface = PAGSurface.FromSurface(mEncoder.createInputSurface());
// 3. 创建Pag播放器
pagPlayer = new PAGPlayer();
// 4. 绑定播放渲染画布
pagPlayer.setSurface(pagSurface);
// 5. 设置Pag播放数据源
pagPlayer.setComposition(pagFile);
// 6. 在渲染线程循环播放Pag
pagPlayer.setProgress(progress);
pagPlayer.flush();

表示为时序图大概如下:

在这里插入图片描述

加载Pag文件

我们先看看PAGFile.Load()方法,里面有三种实现,提供了路径、assets、bytes三种加载方式。,都大同小异,我们就只分析加载本地文件路径的这个实现了。里面的基本流程追溯上比较简单我这里总结了一张调用时序图。里面的核心是文件的解析部分,基本也能猜得出来这里肯定就是把文件读入内存,分析出图层,可编辑文字,Composition等信息。

在这里插入图片描述

简单的来说Load方法核心就是填充PagFile内部的文件信息。PagFile我们可以理解为一个AE合成,可以直接用于渲染,这部分需要后续渲染的时候再具体分析。可以通过下面这个类图关系加深一些理解。

在这里插入图片描述

此外官方有在这里也给Pag的文件规范:pag-spec 通过这个规范我们能理解到Pag文件的数据组成,这里就不过多多文件解析做赘述。

创建Pag画布

和加载Pag过程十分类似,创建画布也有好几种方式:FromSurfaceTexture、FromSurface、FromTexture、FromTextureForAsyncThread。总结下其实就是两种:1. 传入Android Surface画布 2. 传入OpenGL纹理(需要在GL线程)。其中传入Surface的方式可以指定GLContext用于共享OpenGL上下文。几种方式流程大同小异,我们这里就直接分析FromSurface。这个流程很简单,我直接上图了:

在这里插入图片描述

里面其实最核心的一步是调用GPUDrawable::FromWindow创建了一个GPUDrawable对象。这个Drawable可不是Android里面的那个Drawable对象,一般在3D领域叫做Renderable。更接近于Android里面的Surface的概念,我觉得应该叫BeDrawable跟贴切。这里创建GPUDrawable的代码如下:

std::shared_ptr<GPUDrawable> GPUDrawable::FromWindow(ANativeWindow* nativeWindow,
                                                     EGLContext sharedContext) {
  if (nativeWindow == nullptr) {
    LOGE("GPUDrawable.FromWindow() The nativeWindow is invalid.");
    return nullptr;
  }
  return std::shared_ptr<GPUDrawable>(new GPUDrawable(nativeWindow, sharedContext));
}

GPUDrawable::GPUDrawable(ANativeWindow* nativeWindow, EGLContext eglContext)
    : nativeWindow(nativeWindow), sharedContext(eglContext) {
  updateSize();
}
void GPUDrawable::updateSize() {
  if (nativeWindow != nullptr) {
    _width = ANativeWindow_getWidth(nativeWindow);
    _height = ANativeWindow_getHeight(nativeWindow);
  }
}

可以看到创建GPUDrawable真是十分简单,就是创建了一个对象,然后赋值ANativeWindow和EGLContext后更新了尺寸而已。从这里可以得知,我们可以在任意线程通过Surface来创建PAGSurface。这里顺便提一下另外两个通过纹理创建PAGSurface的方法:FromTexture、FromTextureForAsyncThread。这两个必须传入拥有这个纹理的上下文线程,最终会把Pag渲染到这个纹理上面。第二个方法会创建一个新共享当前线程的OpenGL上下文,这样就可以实现异步渲染的功能,比较适合视频预览+录制的场景使用。

创建播放器

创建播放器的过程十分简单,也都是实例化和赋值操作,如下图:

在这里插入图片描述

这里我们需要注意Native层创建了两个非常重要的对象:PAGStage和RenderCache。Stage是舞台的意思,舞台是干嘛的?肯定是用来实际做渲染的舞台对象,而之前提到的那些PagLayer应该就是上面的舞者了。RenderCache顾名思义是渲染缓存的意思,很明显就是用来防止多次计算的缓存系统。这两个之后看具体的渲染流程的时候再好好分析。

绑定播放渲染画布

这步同样比较简单,也就是一层一层调用到Native层的PagPlayer#setSurface()。我们直接看这个函数代码就可以了

void PAGPlayer::setSurfaceInternal(std::shared_ptr<PAGSurface> newSurface) {
  if (pagSurface == newSurface) {
    return;
  }
  if (newSurface && newSurface->pagPlayer != nullptr) {
    LOGE("PAGPlayer.setSurface(): The new surface is already set to another PAGPlayer!");
    return;
  }
  if (pagSurface) {
    pagSurface->pagPlayer = nullptr;
    pagSurface->rootLocker = std::make_shared<std::mutex>();
  }
  pagSurface = newSurface;
  if (pagSurface) {
    pagSurface->pagPlayer = this;
    pagSurface->contentVersion = 0;
    pagSurface->rootLocker = rootLocker;
    updateStageSize();
  } else {
    stage->setContentSizeInternal(0, 0);
  }
}

可以看到其实就是判断是否已经被设置了一下画布对象如果有的话就解绑后重新设置。

设置播放数据源

PagPlayer的数据源设置和设置PagSurface的过程十分类似:

void PAGPlayer::setComposition(std::shared_ptr<PAGComposition> newComposition) {
  LockGuard autoLock(rootLocker);
  auto pagComposition = stage->getRootComposition();
  if (pagComposition == newComposition) {
    return;
  }
  if (pagComposition) {
    auto index = stage->getLayerIndexInternal(pagComposition);
    if (index >= 0) {
      stage->doRemoveLayer(index);
    }
    delete reporter;
    reporter = nullptr;
  }
  pagComposition = newComposition;
  if (pagComposition) {
    stage->doAddLayer(pagComposition, 0);
    reporter = FileReporter::Make(pagComposition).release();
    updateScaleModeIfNeed();
  }
}

可以看到最核心的代码就是:stage->doAddLayer(pagComposition, 0);了。即把PagComposition设置到了stage的Layer的最底层。这个PAGComposition也就是前面我们创建的PagFile了,之前我们也介绍过,PAGComposition本质也是PagLayer。所以设置播放源其实也就是把PagFile设置到了PagPlayer的PagStage的最底层。

渲染线程播放

这个部分比较复杂,也是Pag的核心,libpag里面还内置了一套用于2D图形渲染的引擎叫做TGFX,根据官方介绍说是为了替换skia自研实现的。
我准备把渲染部分分为Pag渲染过程,Pag图层组织,TGFX渲染架构,TGFX 2D渲染实现等内容来讲。下一篇先讲最容易的Pag图层组织吧。

总结

通过对PagFile、PagComposition、PagLayer、PagPlayer、PagStage、Drawable等结构的组织,我们大概脑海里能有这样一张流程图:

在这里插入图片描述

简单的理解也就是一套配置 + 渲染的Pipeline模型。

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...