iOS 接入 Spine 骨骼动画预研

iOS 接入 Spine 骨骼动画预研

Spine 是一套骨骼动画的成熟框架, 可以方便的接进各种游戏引擎, 在spine-runtimes 可以找到各种平台/游戏引擎的运行环境. 跟 Spine 类似的还有DragonBones.

关于 Spine

对于骨骼图的理解, 先从这张图开始,

  1. 骨骼动画中的人物是由骨骼bones,插槽slot,附件attachment 三种概念组成, 都是 1 对 N 的关系
    1. 附件其实人物外表的展示,主要有三种类型:图片,蒙皮,权重蒙皮, 但是只有一种能生效.
    2. 蒙皮可以定义形变, 就像下面这个长枪
      82a729d59af040cba7529696eeda26a4
      1. 蒙皮能自由形变, 是因为它有顶点,边缘,三角区域这三个概念, 能对图片某个区域变形, 看下面的图解, 只要移动了那个顶点就能拉长鼻子
  2. 骨架Skeleton 指代的是数据的集合,包含构成此骨架的所有骨骼、插槽、附件及其他信息。
  3. 皮肤skin 可以看做是attachment的集合,或者可以认为是attachment的一个映射查询表,一个人物可以由多套skin,通过切换skin的方式去查询不同的附件映射表,便可以变相的实现人物的全身换装。
  4. 骨骼动画中的动画,是基于时间轴的,定义某个时间点显示那个附件,骨骼的位移和旋转等等
  5. 引擎播放骨骼动画流程
    1. 引入SPINE编辑器导出的json文件
    2. 引擎自动引入同名的atlas和png文件
    3. 解析json和atlas文件,生成spine对象
    4. 加入到容器里面
    5. 时器渲染,播放动作

spine架构和核心类解读

spine整体架构分层如下:

spine核心类如下:

读懂上面这张官方所提供的类图,将会对spine的整体架构设计有更加明确的了解和认识。

1、Loading模块:是针对资源加载的处理,一个spine形象的骨架信息导出后,一般会导出为json或者二进制文件的形式,由于json形式纯文本文件过大,所以官方提供了二进制文件导出的形式,并且辅以运行库的代码针对二进制文件进行解析。其次,Loading模块中的atlasAttachmentLoader将会负责atlas文件的解析,由于atlas文件本身是字符串的形式,内部包含雪碧图中素材的位置信息,所以需要解析后与素材建立”关联关系“。例如:Eyes-close素材在picture1.png图片中的x,y位置 旋转角度为z,而构造出来的这种映射关系将用于被实例化attachment的时候消费。

2、Spine Texture Atlas模块:一张素材图映射一个atlasPage,一张素材图中的某个区域块映射一个atlasRegion,而region的详细绘制信息本质上已经在上个模块完成。

3、Rending模块:由渲染层遍历slot进行渲染,这里不做详解,渲染层并非spine核心库所负责的部分,上屏渲染可以由canvas、webGL或者其他第三方渲染库渲染,例如pixijs。

4、SetupPoseData模块:或者称之为SkeletonData模块,数据源从这里输入进行处理,但是并不是最终数据,可以理解为这里对数据做了一层预处理,会将骨骼数据先处理为boneData,插槽数据处理为slotData,当然也有部分数据不需要被再次处理,在这里,也会根据前面生成的atlasRegion去构造出对应的附件实例,存储进skin中,skin本质上为附件映射表。

其次,数据对象本身和实例对象是有差别的。

数据对象是无状态的,可在任意数量的骨架实例间共用。有对应实例数据的数据对象类名称以“Data”结尾,没有对应实例数据的数据对象则没有后缀,如附件、皮肤及动画。

实例对象有许多属性与数据对象相同。数据对象中的属性代表装配姿势,通常不会改动。实例对象中的相同属性表示播放动画时该实例的当前姿势。每个实例对象保有一个其数据对象参考,用于将实例对象重置回装配姿势。

例如,SkeletonData是数据对象,而Skeleton是实例对象。

5、Instance Data模块:或者称之为Skeleton模块,Skeleton实例本身是渲染层上屏渲染的真实直接数据源,渲染层将读取Skeleton实例上的插槽信息,渲染对应的附件,在这里,许多数据对象已经被处理成对应的实例对象,例如boneData已经被处理为Bone实例,slotData已经被处理为Slot实例;其次,如图中所展示的,Skeleton实例中有两个比较关键的方法,updateWorldTransform和setToSetUpPose。

updateWorldTransform为更新世界变换,本质是触发骨骼位置的计算,由于骨骼位置可能发生旋转偏移,其对应的子骨骼也会受到影响,因此需要更新世界变换重新计算所有骨骼的最新坐标位置。

setToSetUpPose为更新实例到当前初始状态,一般才初始化时或重置人物状态时调用,会将人物形象骨骼装扮等切换为初始最初的状态。

6、Animation模块:动画模块被单独抽离,不仅更方便维护和更新实例的状态信息,整体架构逻辑也简洁明了,由动画state实例去触发skeleton实例的更新,接下来skeleton实例调用updateWorldTransform更新世界变化,之后重新上屏渲染。

一个动画实例中由多个timeline构成,这些timeline实例来自于不同的变种Timeline类,根本上都继承与底层的TimeLine类,由于一个动画过程中可能涉及多种变化,因此需要对不同的动画进行划分区别,处理旋转的单独一条timeline,处理缩放的单独一条timeline,等等。而虽然不同类别动画会抽离成不同的timeline,但是最终某个时间节点生效触发,所有的timeline”作用”都是同时的。

spine源码解读

1、Slot:存储插槽的当前姿势。插槽为{@link Skeleton#drawOrder}目的组织附件,并提供存储附件状态的位置。状态不能存储在附件本身中,因为附件是无状态的,可以跨多个骨架共享。
(deform属性是针对mesh附件的处理信息。)
(通过setToSetUpPose设置初始动作)
(在slot实例里可以直接getAttachment和setAttachment)

2、SlotData:slot实例里用的数据的数据格式,包含index、插槽名称、附件名称、boneData等。

3、BoneData:骨头实例里用的数据格式,包含index(骨头也有index)、骨头名称、父骨头数据、骨头本地转换数据、世界转换的模式。

4、Bone:关键方法updateWorldTransformWith,更新骨骼的世界坐标。包含其他的一些方法,世界坐标和本地坐标的转换,旋转转换等。

5、SkeletonData: Skeleton实例对应的数据格式。包含bones、slots、skins、events、animations、各种约束。提供了
由于是数据对象,仅提供了一些findbone、findslot的方法。

6、Skeleton:根据data新建Bone和Slot。bone有index按顺序建立关联关系。调用setToSetupPose将bone和slot设置到初始位置。会遍历调用bone和slot对应的方法。updateWorldTransform调用bone的updateWorldTransform更新骨骼位置。提供了一些Bone、slot、attachment的get、set方法。
有个update方法,更新time时间。

7、SkeletonBinary:用于读取二进制的skeleton文件。

8、SkeletonBounds:收集每个可见的BoundingBoxAttachment,并计算其多边形的世界顶点。主要用于碰撞或者命中检测。由渲染层调用。

9、SkeletonCilpping:主要针对ClippingAttachment的处理,由渲染层调用。

10、SkeletonJson:用于解析处理spine导出的skeleton json。需要对应传入一个attachmentLoader让其能构造对应的attachment实例,处理bone、slots、ik、skins、animation等数据。
处理bone和slot构造对应的实例data。
处理skins借助loader生成对应附件实例。
处理 animations生成不同的timeline实例对象。
最终构造出对应的SkeletonData实例。

11、Skin:一套皮肤下的所有attachment都在skin实例下,提供了操作skin和attachment的方法。这里操作的方法相当于dictionary,不是改变人物装扮的。

12、attachment目录:各种附件的处理处理方法,继承于Attachment基类。实际由对应的AttachmentLoader调用对应的附件类方法。
AtlasAttachmentLoader实现了对应方法。

13、Texture:定义了Texture抽象类,定义一些抽象方法需要被实现。

14、TextureAtlas:针对atlas文本进行解析处理,实现TextureAtlasReader进行逐行读取,texture
借助外部传入的textureLoader回调来获取对应的纹理。
每块小素材对应一个TextureAtlasPage,素材信息读取解析后构造对应TextureAtlasRegion。

15、AnimationStateData:存储AnimationState动画更改时要应用的混合(交叉淡入淡出)持续时间。

16、AnimationState:随着时间调用动画,动画入队等待播放,允许多个动画叠加。
分多个track存储动画、区分不同动画的timeline,针对event事件的处理逻辑等。

17、Animation:实现了各种timeline类,Animation负责调用apply方法触发更新,其apply方法会调用各个timeline的apply方法更新。timeline类中实现找到对应关键帧 决定如何渲染。

18、AssetManager:静态资源管理,包括拉取文本资源、拉取二进制资源、加载纹理。调用TextureAtlas处理atlas文本等。

渲染库代码解读

canvas:
1、AssetManager:没有做啥,直接沿用core里的AssetManager
2、canvasTexture:继承Texture。
3、SkeletonRender:传入skeleton数据,渲染画布,
drawImage会遍历drawOrder中的slot,逐个渲染region附件,借助ctx.drawImage API来裁剪和渲染图片。
drawTriangles会计算顶点,渲染调试模式的绿色线条。

threejs:
1、ThreeJsTexture:针对threesjs本身的texture做了一层包裹,处理了一下filter。
2、MeshBatcher:MeshBatcher继承自Threejs本身的Mesh。调用SkeletonMeshMaterial获取材质。
3、SkeletonMesh:SkeletonMeshMaterial继承自ShaderMaterial,这里包含了着色器代码,顶点着色器和片元着色器。
SkeletonMesh继承自Object3D类。
核心渲染函数updateGeometry,skeleton更新世界变化后,调用渲染函数,遍历drawOrder。
RegionAttachment和MeshAttachment会进行渲染,渲染借助MeshBatcher,纹理作为素材传入batchMaterial。

webgl:
1、GLTexture:获取画布,渲染对应的image到画布上。
2、Camera:设置相机位置
3、WebGL:定义了ManagedWebGLRenderingContext,其实就是获取webgl的context上下文。
4、Input:对元素做事件监听,鼠标、touch事件。
5、Shader:自行实现的着色器,片元和顶点。
6、SkeletonRenderer:负责skeleton的上屏渲染,渲染函数需要借助PolygonBatcher来上屏渲染,同样的,只会对RegionAttachment和MeshAttachment会进行渲染。
7、PolygonBatcher:在这里绑定着色器,设置混合模式,绑定一个Mesh实例对象,Mesh为单独封装的mesh类,最终调用的是Mesh暴露的渲染方法。
8、Mesh:单独封装的Mesh类,允许设置指数和顶点,上屏渲染借助context的drawElements和drawArrays方法。
9、SceneRenderer:最上层的调用类,实例化batcher、webgl上下文、shader,实例化SkeletonRenderer,暴露不同的渲染方法,包括drawSkeleton,drawSkeletonDebug、drawTexture、drawRegion等。

iOS 接入 Spine

最简单的接入就是使用 WKWebView 加载含有 Spine 动画的 url(让前端同学来写页面), iOS 如果想自行接入也有三个办法

  1. spine-cocos2dx 基于 C++(spine-cpp) 实现, 类似于 cocos2dx 使用 C++ 来写代码就好, 目前已经支持到 Spine4.1 版本
    1. Spine 的兼容是向上下均不兼容, 比如我尝试使用 4.1 版本 sdk 加载 3.8 版本的素材, spine-cpp 代码直接不解析 json 了.
    2. 最终也选了这个方案的 3.8 版本
  2. spine-cocos2d-objc 基于 C(spine-c) 实现, 可以直接使用 OC 来编写 Spine 相关代码, 但是版本只兼容到 Spine 3.9.x.
    1. 我们设计导出的Spine素材在使用spine-cocos2d-objc加载时候, 出现了莫名其妙的崩溃, 短时间解决不了, 就放弃了.
  3. Spine / YSC_SpineSwiftTest 等基于SpriteKit 实现的方案, 但是存在需要调整素材 / 特性支持不全等问题, 自行解决太耗时, 应该没必要

spine-cocos2dx 与 WKWebView 加载性能简单对比

spine 接入 手机 CPU(%) 平均帧数(fps) 内存(M)
iPhone7P cocos-2dx 75.4 58.2 65
iPhone7P WKWebView 91 60 43
iPhone11Pro cocos-2dx 18 60 66
iPhone11Pro WKWebView 24,3 60 44

使用 cocos-2dx 加载大 json 的解析存在优化空间.

iOS加载手机CPU负载比 WKWebView 低 18%~25%,
但是 WKWebView 的内存消耗少 20M, 在低端机上帧率也更稳定

后续优化方向

  1. json 按需加载(比如只加载特定动作/皮肤/动画)
  2. 渲染降级, 动作停止时候不再渲染

高效JSON片段解析组件

从一个非常大的JSON文件中解析一块比较小片段的JSON,应该采用什么方式?

常规的方法有:

  1. 先全部解析,然后提取部分.这个方法最大的问题在于它不仅耗时巨大,而且会短时间消耗巨大的内存.
  2. 边解析边找,找到了就不再解析.虽然内存消耗不大,但是耗时还是很大.
  3. 预先将JSON文件切割成很多小块,想要获取部分JSON的时候再直接提取.首先,文件太多不好管理.其次,也不好切割,因为有时候两个需要切割的对象具有包含关系.最后,多次打开关闭文件即消耗时间,也对硬盘损害不小.
    以上三个方案缺点很明显,可以使用一种基于文本预建立索引的方式去解决问题

预处理逻辑:

预处理实现方案

性能如下:

解析器工作流程图如下:

解析器性能如下:

Cocos-2dx 是什么? 跟 CocosCreator 什么关系?

简单说 CocosCreator 是一个游戏引擎编辑器, 也可以使用 JS/TS 编辑部分游戏逻辑, 游戏元素都是所见即所得, 类似于 Uinty, 是一整套游戏开发方案.

Cocos-2dx 则是基于 C++ 开发的游戏引擎, 并且内置了 Spine 的运行环境(虽然 Spine 官方不希望Cocos-2dx 内置, 因为内置的是较低版本的 Spine 3.7, 如果需要升级Spine 就很麻烦, 不过有一说一, Spine 的兼容也做的很差), Cocos-2dx 一般以 SDK 的形式接入到游戏开发流程中, 能开发的游戏效果比 CocosCreator 更多.

参考文档

2D动画spine渲染原理解析与源码解读

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道