当前位置: 首页 > ai >正文

Skia如何渲染 Lottie 动画

lottie 动画原理

  1. Lottie 是一个开源轻量级动画库,Lottie 通过 JSON 文件在动画中定义复杂的动画特性,如颜色、位置、旋转、缩放、图层、形状、遮罩、文本等(甚至还有位图图像)。

  2. Lottie 动画播放器会读取并解析这个 JSON 文件,并按照 JSON 描述的信息渲染图像。

  3. Lottie 动画的每一帧都是根据时间线和关键帧计算得出的,Lottie 会根据当前时间计算出应该显示的动画状态。

    描述 Lottie 动画的 JSON 文件如下图所示:

    lottie_json.png

增加依赖库

要想使用 Skia 渲染 Lottie 动画,必须增加几个依赖库:

skottie.lib
sksg.lib
skunicode_core.lib
skunicode_icu.lib
skshaper.lib
harfbuzz.lib 

其中与 Lottie 动画有直接关系的库为: skottie.lib ,其他几个库都是 skottie.lib 库依赖的库。

线程锁

在播放 gif 动画时,用一个全局的 SkBitmap 对象来存储 gif 动画每一帧的信息。

子线程解码 gif 动画时,向 SkBitmap 对象写入像素数据,主线程播放 gif 动画时,读取 SkBitmap 对象的像素数据,并把它们写入窗口画布中。

即使改变窗口大小时(改变窗口画布对应的像素数据),也不存在多线程数据竞争的问题。

这样的话,改变窗口大小时,主线程会操作窗口画布像素数据。子线程绘制 Lottie 动画描述的图像时,子线程也会操作窗口画布像素数据。这就涉及到多线程竞争访问数据资源的问题。

所以,要使用线程锁解决这个问题。

如下代码所示:

// #include <mutex>// std::mutex locker;
case WM_SIZE:
{w = LOWORD(lParam);h = HIWORD(lParam);if (surfaceMemory) {std::unique_lock guard(locker);delete[] surfaceMemory;surfaceMemory = new SkColor[w * h]{ 0xff000000 };guard.unlock();}break;
}

这是窗口大小改变时执行的代码,在这段代码中,使用线程锁来保护重置窗口画布数据的工作。

std::mutex locker 是一个互斥对象,std::unique_lock 对象可以锁住这个互斥对象。

当某个线程尝试锁住互斥对象 locker 时,会先检查是否有其他线程锁住了互斥对象 locker ,如果有,则等待其他线程释放互斥对象 locker 。如果没有,则立即锁住互斥对象 locker ,并继续执行。

当 guard 对象执行 unlock 方法时,会解锁互斥对象。如果此时正有某个线程等待这个互斥对象解锁,那么这个线程可以继续执行它的工作了。是不是有点像排队上厕所呢?

渲染动画

渲染 Lottie 动画的核心代码如下所示:

// #include "modules/skottie/include/Skottie.h"
// #include <thread>void animateLottie() {auto t = std::thread([&]() {std::wstring imgPath = L"D:\\project\\SkiaInAction\\动画Lottie\\demo4.json";auto pathStr = wideStrToStr(imgPath);sk_sp<skottie::Animation> animation = skottie::Animation::MakeFromFile(pathStr.data());auto size = animation->size();auto fps = animation->fps();auto duration = std::chrono::milliseconds((int)(1000 / fps));auto frameCount = animation->duration()*fps;auto frameIndex{ 0 };while (true){auto x = (w - size.fWidth) / 2;auto y = (h - size.fHeight) / 2;auto rect = SkRect::MakeXYWH(x, y, size.fWidth, size.fHeight);animation->seekFrame(frameIndex);std::unique_lock guard(locker);auto canvas = getCanvas();canvas->clear(0xff000000);animation->render(canvas, &rect);guard.unlock();InvalidateRect(hwnd, nullptr, false);std::this_thread::sleep_for(duration);frameIndex += 1;            if (frameIndex > frameCount) {frameIndex = 0;}}});t.detach();    
}

这段代码有以下几点需要注意:

  1. animateLottie 方法也是在窗口创建成功之后执行的。

  2. 与播放 gif 动画一样,也是在一个子线程中渲染每一帧 Lottie 动画的。

  3. skottie::Animation::MakeFromFile 方法负责从一个 json 文件创建动画对象,动画对象的类型为:skottie::Animation

  4. 动画对象的 size 方法用于获取此动画的大小,fps 方法用于获取此动画的帧率(每秒播放多少帧),duration 方法用于获取动画播放一次使用的时间(秒)

  5. 变量 duration 是每帧动画的时间间隔(毫秒),这是根据动画的帧率算出来的。

  6. 变量 frameCount 是帧数量,这是用帧率播放时间算出来的。

  7. 变量 frameIndex 是当前播放的帧(第几帧)。

  8. 循环伊始,首先计算动画的位置和大小,因为用户可能随时调整窗口的大小,所以要在每次循环时都计算它们。

  9. 动画对象的 seekFrame 方法用于设置当前播放第几帧,由于每一帧都是根据 json 文件计算出来的,所以这里可以使用小数来设置当前播放第几帧。

  10. 渲染动画前先加线程锁,因为这里要修改画布像素数据,这与前面介绍的线程锁是呼应的。

  11. getCanvas 方法用于获取画布对象,它能返回一个与窗口对应的画布(SkCanvas)对象。

  12. 在渲染动画的每一帧前,必须清空了画布,因为 lottie 动画每一帧都可以独立渲染成一幅图像,如果不做清空工作,则可能和上一帧图像叠加到一起了。

    这与绘制 gif 动画不同,绘制 gif 动画每一帧 前一定不能清空画布,必须让它们叠加渲染才行。

  13. 动画对象的 render 方法负责在 rect 指定的位置和区域绘制一帧图像。绘制完成后立即解锁互斥锁(以后的工作不再涉及到线程竞争访问资源的问题了)。

  14. 请求重绘窗口、等待帧与帧之间的时间间隔,判断是否到了最后一帧等工作与渲染 gif 动画相同,这里就不再赘述了。

获取画布对象

本示例中,由于画布对象与窗口像素(surfaceMemory)有关,所以要单独处理,如下代码所示:

SkCanvas* getCanvas() {static std::unique_ptr<SkCanvas> canvas;if (!canvas.get()) {SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);canvas = SkCanvas::MakeRasterDirect(info, surfaceMemory, 4 * w);return canvas.get();}auto info = canvas->imageInfo();if (w != info.width() || h != info.height()) {SkImageInfo newInfo = SkImageInfo::MakeN32Premul(w, h);canvas = SkCanvas::MakeRasterDirect(newInfo, surfaceMemory, 4 * w);}return canvas.get();
}

这段代码中,在方法内定义了一个静态变量 canvas

方法内的静态变量的生命周期与应用程序的生命周期相同,不仅仅是其所在方法被调用期间,只有到程序结束时才会被销毁。

虽然这个静态变量的生命周期与整个应用程序的声明周期相同,但其作用域仍然局限于声明它的函数或方法内。这意味着在该方法之外无法直接访问这个静态变量。

每次函数被调用时,这个静态局部变量会保留其上次函数调用结束时的值。

当首次调用 getCanvas 方法时(渲染第一帧时),canvas.get() 方法返回的是一个空指针,所以会执行第一个if分支,根据窗口大小创建 SkCanvas 对象。

当再次调用 getCanvas 方法时(渲染第二帧或第N帧时),canvas.get() 方法返回的不是空指针,所以第一个if分支 不会执行,

此时会判断 canvas 的大小是否与窗口大小一致,如果一致(用户没有改变窗口大小),则直接返回之前创建的canvas 指针。

如果不一致(用户改变了窗口大小),则根据新的窗口大小和 surfaceMemory 创建 canvas 对象。

值得注意的是,这个方法是在子线程中执行的,是受线程锁保护的,执行这个方法时,永不会执行 WM_SIZE 处的逻辑。

运行程序,得到的结果如下图所示:

http://www.xdnf.cn/news/20309.html

相关文章:

  • 《Java线程池面试全解析:从原理到实践的高频问题汇总》
  • 深入剖析Spring Boot启动流程
  • 基于Node.js和Three.js的3D模型网页预览器
  • JP4-7-MyLesson后台前端(二)
  • 轻量应用服务器具体指的是什么?
  • Redis《RedisSerializer》
  • 脑电数据预处理十五:小波变换从原理到实践
  • Codeforces Round 1046 (Div. 2) vp补题
  • C++ 详细讲解vector类
  • 检查CDB/PDB 表空间的说明
  • Linux网络接口命名详解:从eth0到ens33
  • [光学原理与应用-431]:非线性光学 - 能生成或改变激光波长的物质或元件有哪些?
  • GPIO的配置中开漏输出与推挽输出的差别
  • C++零基础第四天:顺序、选择与循环结构详解
  • Protobuf
  • 人工智能辅助荧光浓度检测系统:基于YOLO与RGB分析的Python实现
  • 【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
  • AP1272:新一代高性能LDO稳压器,为精密电子系统提供更优电源解决方案
  • 《秦时明月》系列经典语录分享
  • 云原生的12个要素是什么?
  • 【Linux指南】动静态库与链接机制:从原理到实践
  • 疯狂星期四文案网第62天运营日记
  • 消失的6个月!
  • 从文本到知识:使用LLM图转换器构建知识图谱的详细指南
  • Java多线程学习笔记
  • Nginx 实战系列(二)—— Nginx 配置文件与虚拟主机搭建
  • QML Charts组件之LineSeries、SplineSeries与ScatterSeries
  • 正态分布 - 正态分布的经验法则(68-95-99.7 法则)
  • Modbus通信的大端和小端字节序
  • OpsManage 项目启动脚本与 Docker 配置深度分析