galacean / engine-spine

@galacean/engine-spine is the Spine runtime module for the Galacean engine, providing efficient support for Spine animations.
https://galacean.antgroup.com/engine/docs/graphics/2D/spine/overview/
MIT License
9 stars 8 forks source link

Adjustment of spine runtime basic API #109

Open johanzhu opened 2 months ago

johanzhu commented 2 months ago

背景

过去的 Spine-runtime API 存在多个版本(SpineAnimation, SpineRenderer),杂乱且不统一。在 1.3 里程碑中,对 API 进行了初步的整合与优化。然而,当前的 API 距离最终版本仍有差距,尚需进一步的审视与优化,以确保在未来能够更好地满足开发者的需求,提供更优的开发体验。

整体 API 调研

为确保新的 Spine-runtime API 版本能够在功能性和易用性上满足开发者的需求,本次调研分析了多个流行的 Spine 运行时,包括 :Unity、Unreal Engine、Godot、Pixi 、Cocos 和 LayaAir。通过比较这些引擎在 API 设计、功能实现、扩展性等方面的不同,我们可以借鉴他们的优点,并识别现有 API 的改进方向,以实现更高效的一致性和可用性。

以下是各引擎的调研整理,**在对比不同的引擎 API 后,会整理出核心且必备的 API,然后结合目前 Galacean runtime 的 API 提出改造方案。** 以下是不同引擎API的调研和总结。

Unity Spine Runtime

概要

API 概览

Unity 对外暴露的 API 非常多且杂,但是基本上可以划分为以下几个类别:

  1. 渲染相关参数与方法
  2. 生命周期方法(初始化,销毁,各种代理)
  3. 物理加权相关参数与方法
  4. spine 动画/骨架控制
  5. 动画资产相关方法

Unity Spine Runtime 提供了 SkeletonAnimation 和 SkeletonRenderer 两个类来实现 spine 动画的渲染。二者是继承关系,且均为MonoBehaviour。

详细 API 如下:

SkeletonAnimation 对外暴露的 API 有:

SkeletonRenderer 对外暴露的 API 有:

在Unity 编辑器中

image

组件检查项中,和渲染有关的参数都统一放到了 Advanced 折叠块下,检查项与 API 是对应的~

除此之外的检查项有:


总结:

Unreal Engine

概要

API 概览

UE 同样提供了渲染组件和动画组件两个组件,蓝图提供的节点与组件暴露的方法略有差异。UE 暴露的 API 看似很多,但是都是基于 spineCore 的 AnimationState 和 Skeleton 对象的方法。

API 可以划分为这几类:

  1. spine 动画/骨架控制
  2. 生命周期方法(初始化,销毁,各种代理)
  3. 动画资产相关方法

详细 API 如下:

动画组件 API:

蓝图节点:

代理方法:

渲染组件 API:

蓝图方法:

以上蓝图节点都是基于 SpineCore.Skeleton 对象提供的

在UE 编辑器中

UE 编辑器组件中只有这四个选项,分别是骨架数据,atlas 数据,以及两个预览的名称。

在蓝图编辑器中,能够使用组件暴露的蓝图节点:

总结:

Godot

概览

API 概览

Godot 的 API 可以分为以下几个类别:

  1. 渲染相关方法(自定义混合材质)
  2. 生命周期方法(动画事件,动画生命周期)
  3. spine 动画/骨架控制
  4. 动画资产相关方法

详细 API 如下:

事件:

以下四个是四种混合模式下的材质设置与获取:

其他对外 API 是一些 debug 方法。这里就不贴了。

在编辑器中:

godot 提供的检查项有:

总结:

Pixi

概览

API 概览

Pixi 的 API 可以分为:

  1. spine 骨架/动画操作
  2. 动画资产相关方法
  3. 渲染参数(仅 tint)
  4. 生命周期方法
  5. 特殊方法:插槽内添加物体,坐标系转换(用于挂点)

详细 API:

PS: hackTexture 相关的方法只在 Pixi-spine 中存在。官方运行时并未提供相关的动态替换纹理的方法。

总结:

Cocos

概览

API 概览

cocos 暴露的 API 非常多,主要分为以下几类:

  1. Spine 动画/骨架操作 (由于存在动画缓存,cocos 基于 SpineCore API 改造并暴露了同名 API )
  2. 动画资产相关方法
  3. 渲染相关参数与方法
  4. 挂点功能相关方法
  5. debug 方法
  6. 生命周期方法(事件代理)
  7. 动画缓存相关参数与方法

详细 API:

在cocos编辑器中:

对比对外暴露的 API 能够发现,Animation,DefaultSkin 没有对应的 API。这两个检查项的 API 是 internal 的。

其余的检查项与 API 均能够对应。

总结:

Laya

概览

API 概览

Laya 的 API 主要分为以下几类:

  1. spine 动画/骨架操作
  2. 性能优化参数
  3. 动画资产相关方法
  4. 生命周期相关方法
  5. 材质相关方法(只有一个 getMateiral)
  6. 还有部分底层绘制方法

详细 API:

  1. externalSkins(get and set):设置外部皮肤
  2. resetExternalSkin:重制外部皮肤
  3. addCMDCall :可以直接修改当前材质的矩阵,透明度等信息,类似command buffer
  4. source(get and set):获取和设置 spine 动画资产
  5. skinName (get and set):获取和设置默认皮肤
  6. animationName(get and set):获取和设置默认动画
  7. loop(get and set):获取和设置默认动画是否循环
  8. url (get and set):获取和设置spine动画资产的url
  9. templet (get and set):获取和设置spine动画模板基类
  10. currentTime(get and set):获取或设置当前的动画时间
  11. playState(get):获取当前的动画播放状态
  12. useFastRender (get and set):加速开关
  13. spineItem:获取spine渲染实例
  14. play:播放动画
  15. getAnimNum:获取当前动画数量
  16. getAniNameByIndex:获取指定动画的名字
  17. getSlotByName:通过名称获取插槽
  18. playbackRate:设置动画播放速率
  19. showSkinByName:通过名字显示一套皮肤
  20. showSkinByIndex:通过索引显示一套皮肤
  21. event:触发事件
  22. stop: 停止动画
  23. paused:暂停动画
  24. resume:恢复动画播放
  25. reset:重制spine动画组件
  26. destroy:销毁spine动画组件
  27. addAnimation:添加动画
  28. setMix:设置动画混合
  29. getBoneByName:通过名称获取骨骼
  30. getSkeleton:获取骨架对象
  31. setSlotAttachment:设置插槽附件
  32. clear:清楚渲染元素
  33. changeNormal:切换渲染模式为普通
  34. drawGeos :绘制几何体(感觉不是用户用的。。
  35. updateElements:更新渲染元素(感觉不是用户用的。。
  36. getMaterial:获取材质

在Laya编辑器中:

Laya 的检查项较少,只有:

上述检查项都能找到与之对应的 API。ExternalSKins 是比较特殊的一个 API,仅laya提供了这个API。但是文档中没有介绍这个检查项的功能。

总结:

根据以上 6 个引擎的 API 以及编辑器的检查项的调研,能够总结出必备的 API 类型有:

  1. spine 骨架/动画操作相关方法
  2. 动画资产相关方法
  3. 生命周期相关方法
  4. 渲染相关参数与方法

除此之外,其他类型的 API 都是额外的一些高级功能或者特殊处理。

下文中,会结合调研,总结目前 Galacean spine 的 API 要改什么,要加什么。

目前的 API 梳理

目前运行时对外的 API 如下:

根据上述调研结果,对已有 API 进行分类:

  1. spine 骨架/动画操作:skeleton,state
  2. 动画资产相关方法:resource
  3. 生命周期相关方法:state (spine core 原生事件)
  4. 渲染相关参数与方法:setting

API 调整总览

ue 是为了提供蓝图节点,暴露的函数与原本的 API 基本一致。

cocos 由于增加了动画烘焙缓存,所以无论是动画播放,还是附件替换,这些都进行了二次封装,但是功能和原生 API 是一致的。

laya 的纯粹是为了提供使用方法,部分方法会关闭 laya 的性能优化开关,所以也进行了封装。

综上,如果引擎没有特殊的实现或者操作,无需额外封装新的方法,cocos 和 laya 封装的方法,函数名/功能和原生的SpineCore API 也是一致的。

API 修改调整方案

以下是各 API 调整的详细方案,每个API 都包含修改的方向以及背后对各个引擎针对该API 的调研。

resource

修改方向

  1. resource 不应该存储在 Renderer中。在初始化后,其职责就已经完成。渲染组件应只存储实例化出来的骨骼和动画对象。
  2. 实例化与动态修改 resource 的操作应该去掉。换成直接设置 skeleton 和 animationState ,灵活性更强。

具体方案:

/**

针对资产 API 的调研:

Unity 在运行时能够做到动态切换,这段代码在 spine 实例化时也会调用。

除此之外,还提供了运行时实例化的功能,但是并不推荐这种方式:https://zh.esotericsoftware.com/spine-unity#%E9%AB%98%E7%BA%A7-%E8%BF%90%E8%A1%8C%E6%97%B6%E5%AE%9E%E4%BE%8B%E5%8C%96

image

优势: unity 提供了对外的 API 实现实例化以及动态替换素材,灵活性强,能够满足各种需求。

劣势:动态替换素材会重新构建 mesh,反复切换素材是比较 waste 的操作,严格上来说,不算劣势。这也是不同运行时都会面临的问题。

Pixi 没有暴露资产相关的 API 不可动态替换。只能通过 from 静态方法或者 contructor 来创建。

优势:from 静态方法能够很好的和 Pixi 的 Loader 相结合使用,但是前提是需要预加载 skeleton 和 atlas 素材。而通过 contructor 创建,则需要用户手动创建出 SkeletonData,灵活性更高但是需要引入更多的 paser 来预处理素材。

劣势:无法动态替换素材,预加载的方式非常多,为了实现预加载,多 page 需要在 loader options 传入非常多额外参数。

用户学习成本很高。具体可以参考官方提供的 example ,非常繁多。

ue 的实现其实与 unity 类似,但是官方文档中,没有告知用户运行时实例化和动态修改素材的方式。在代码中,还是存在对应的接口,

这使得能够通过蓝图,替换素材:

image

但是官方文档中提供的方式还是直接设置素材,而非蓝图:

image

优势:ue 的优势主要体现在能够结合蓝图一起使用。

劣势:与 unity 一样,当切换了素材时,同样会重新 buildMesh。

godot 的 SpineSprite 同样提供了方法动态修改 resource

语意非常明确: set_skeleton_data_res,并且提供了相应的回调函数。

在 SpineSprite.cpp 的回调实现中,修改素材后,同样会重新创建 Mesh

优势:与 unreal 和 unity 相同,godot 提供的组件功能也非常全面与灵活。但是文档中并没有告知用户动态替换素材的方法。

劣势:与 unreal 和 unity 相同,切换素材时,同样会重新初始化,构建 mesh。

都有动态替换 spine 资产的 API。但是 laya 的 API (source, url)语意不太明确

cocos:

素材加载: https://docs.cocos.com/creator/3.8/manual/zh/asset/spine.html#%E5%8A%A0%E8%BD%BD%E6%96%87%E6%9C%AC%E6%A0%BC%E5%BC%8F%E7%9A%84-spine-%E8%B5%84%E6%BA%90

laya:

image

image

cocos和laya 也能够运行时替换 spine 素材,但是都需要重新load素材,然后调用API加载。重新加载时,也都会重新buildMesh。

state & skeleton

这两个 API 放到一起说。

修改方向

  1. 这两个 API 目前的暴露方式目前没有问题,暂无需修改
  2. 后续如果引擎增加了动画缓存,或者其他特殊处理,在保证 API 功能的前提下,进行二次封装。

调研:

unity 组件对外暴露了 spine-core 的这两个对象,左边是动画组件,暴露了 state:spine.AnimationState 对象,右边是渲染组件,暴露了 Spine.Skeleton 对象

ue 的实现和 unity 一样,也有两个组件一个是 skeleton 组件,一个是 animation 组件。后者继承于前者。

同样,也暴露了 skeleton state 的对象的 API,但是不是以对象的方式。而是把 API 拍平了挨个暴露出去,并且对于原本的 API 有二次封装,目的是为了更好地整合 Spine 动画系统,利用 UE 的内置特性,如蓝图、反射系统和垃圾回收机制。

提供了 get 方法来获取这两个对象。

API 与 spine-core 一致。

暴露了 spine-core 的 Skeleton 和 AnimationState 对象。

没有暴露这两个对象,但是基于这两个对象的方法,封装了常用的几个函数,比如:播放动画,替换附件,设置皮肤,还有一些 util 方法,比如:骨架归位,修改骨骼 Transform 等。之所以二次封装的理由上面也提到了,是因为运行时有一些额外的实现(动画烘焙,性能优化)。

addSeparateSlot

改造方向

目前这个方法可以删除,运行时的 API 需要在编辑器有对应功能,在添加分割插件前不需要这个 API

根据目前的调研,目前仅有 unity 提供了类似方法,用于处理一些特殊的遮挡情况:

针对拆分功能的调研

unity 的渲染组件中包含一个参数:separatorSlots

在 separatorSlots 中的插槽会用于单独创建一个独立的 subMesh。这个参数会被插件组件 SkeletonRenderSeparator 使用。SkeletonRenderSeparator 能够设置分离槽位的渲染顺序。

defaultState

改造方向

调研的引擎中,都有对应的参数来设置初始化的动画状态。不过部分引擎提供的参数只能够用于编辑器预览,无法应用到运行时。

调用后,个人认为,这些初始化的参数,直接压平放到运行时不合理,会让用户觉得这是提供出来用于修改皮肤和动画的的util API。 在保证初始化功能的前提下,为了不让用户对 API 有混淆,保留 defaultState 这一层,收拢所有初始化相关的参数。

针对默认状态 API 的调研:

unity 也能够设置初始化的 spine 状态,对应的 API 分别是:

AnimationName,还有一个单独的 loop 参数

initialSkinName

该参数在实例化时,会生效,用于设置 spine 的初始皮肤。

缩放只能够通过 flip 来设置初始的正反。

ue 没有提供对外的接口设置初始化的动画和皮肤,但是提供了两个设置项,用来预览动画和皮肤。

godot 没有提供初始化的 API ,而是提供了 preview_skin,preview_animation 用于设置预览的皮肤和动画。

如果脚本没有更新皮肤和动画,那么会直接应用 preview 这里设置的属性。

但是经过我测试,动画并没有应用成功,而且还搜索到类似的 bug:https://github.com/EsotericSoftware/spine-runtimes/issues/2530

没有提供动画,皮肤的初始化接口

没有对外暴露 初始化API,但是提供了内置的 API 且对应编辑器的接口:包括 Animation, SkinName。但是初始动画 的loop 则直接对外暴露。

setting

setting 目前管理了几个渲染相关参数,有useClipping(是否开启裁减) 和 zSpacing(层之间的间隙)。

改造方向

  1. 干掉 setting 这个参数,把 zSpacing 和 useClipping 放到外面
  2. 后续,渲染和性能优化相关参数都放在最外层。

针对渲染参数的调研:

有一个 DepthOffset 参数但是没有暴露出来是固定值

没有类似参数,阅读了代码似乎是靠 index 顺序来控制绘制顺序的

没有这两个参数,z 轴顺序是靠 mesh 的 zIndex 。

有一个 tint 开关参数,没有其他的渲染参数了

没有类似的渲染参数

新增实例化 API

目前只提供了 API 替换 spine 的 resource,但是没有提供 API 进行 atlas 的替换。

altas 素材的替换是常见的需求,详见 spine forum 帖子:

https://zh.esotericsoftware.com/forum/d/26252-swapping-atlases-based-on-screen-resolution

https://zh.esotericsoftware.com/forum/d/15659-how-to-change-the-quotactivequot-atlas-asset-at-runtime/2

https://zh.esotericsoftware.com/forum/d/18098-runtime-change-spineatlasasset/3

由于 1.3 没有实现 Spine atlas 素材的单独上传,所以替换 atlas 也没有实现。

调研 unity: unity 提供了一个 createRuntimeInstance 方法来创建一个 SkeletonDataAsset 对象,接收 skeleton 文件和 atlas 图集文件,创建新的SkeletonDataAsset: image

优势:提供了方法在运行时创建并替换 spine 动画资产,灵活性强。 劣势:但是,运行时创建资产时,需要手动指定 skeleton 和 atlas 的关联关系。

ue: ue 没有对外提供更新的方法,但是引擎内部有对应的实现,当 atlas 和 skeleton 两个数据发生改变时,会重新调用 GetSkeletonData 来加载并重新创建 Skeleton 和 AnimationState 对象,这和 unity 的操作是一样的。只不过 unity 会在 initialize 方法中执行加载和创建的逻辑 image

劣势:ue 替换素材的方法没有对外暴露,无法运行时手动创建 spine 动画资产。

godot: goto 提供了一个 set_skeleton_data_res 方法用于设置 spine 资产。当资产修改后,会在内部调用一个更新方法,重新执行加载逻辑: image

劣势:Godot 替换素材的 skeleton文件与atlas文件的关联关系无法修改,重新设置资产只能设置加载完毕的 skeleton_data_res 对象,灵活性没有 unity 高。

pixi pixi 动态替换 atlas,需要重新加载骨架和图集素材,调用 from 方法重新创建新的 spine 动画对象。 image

劣势:pixi 中,skeleton 和 atlas 的关联关系,只能手动建立。由于缺乏编辑器上传流程,假设文件不对应,会导致无法渲染或者渲染出错。

cocos cocos 替换 atlas 的方式和 unity 类似,也需要重新创建新的 skelentonData 素材: image

优势:灵活性强,能够运行时修改 spine 动画资产,还可以自定义 atlas 关联的图片素材的路径。 劣势:不同资产的关联关系需要手动建立。假设文件不对应,会导致无法渲染或者渲染出错。

laya laya 没有提供替换 atlas 的方式,只能加载新的 spine 动画素材: image

劣势:与 godot 类似,无法修改 skeleton 和 atlas 文件的关联关系。

结论: 综合调研,最好的方案需要支持以下功能:

  1. 提供运行时创建资产的能力 4.能够加载已经建立了关联关系的素材 5.提供能力手动建立素材间的关联关系

具体方案: 1和2目前已经支持: Galacean 的编辑器资产和运行时使用的资产是通过 Loader 来完成转化的。目前skeletonDataAsset,spineAtlasAsset都有对应的 Loader。即已经存在方法在运行时创建运行时使用的资产了。( 这种情况下,素材的关联关系已经在上传素材时就确定好了) 这几种资产的运行时资产如下: skeletonDataAsset 的运行时资产是SkeletonDataResource spineAtlasAsset 的运行时资产是TextureAtlas texture的运行时资产是 Texture2D

3.提供新的创建运行时使用的资产的方法,并且支持手动建立资产关联关系。

createSpineResource(skeletonFile: string, atlas: TextureAtlas ): SpineResource {}

crreateTextureAtlas(atlasFile: string, textureFiles?:string[]): TextureAtlas {}

额外科普: 为什么 spine 动画再替换 atlas 后,需要重新初始化构建 mesh 呢? 重新初始化的原因如下:

  1. Spine 实现的局限性 第一条是最关键的一点原因。Spine 动画是基于 SpineCore.SkeletonData 数据对象来创建的,SpineCore.SkeletonData 则是基于 SpineCore.TextureAtlas 创建的。也就是说,Spine的动画资源里,Spine 的骨架和atlas两个资源是绑定在一起的。所以如果需要替换图集,需要重新创建 SkeletonData 对象,并重新进行初始化。
  2. UV 变化 替换图集后,虽然顶点位置不变,但是UV很可能发生变化。这是因为每个图集中可能存在不同的纹理布局(Atlas Region),新图集的区域和旧图集的区域可能不一致。如果不重新构建mesh来适应新的UV坐标,可能会出现错误的纹理映射。 Spine 通常是使用一个大buffer来容纳顶点,uv,颜色数据的,所以UV更新时,需要重新构建 mesh
  3. 图集纹理个数发生变化 第一条提到过Spine的动画资源里,Spine 的骨架和atlas两个资源是绑定在一起的,如果图集对应的纹理个数发生变化,那么肯定要重新替换 Spine 动画资源。

至于为什么要重新构建mesh, 重新初始化后,相当于替换了一个新的 spine 素材。buffer 数据肯定会发生变化,所以调研的6款引擎都重新构建了新的 mesh。 那么可以针对替换 atlas 这种换肤场景,进行优化(不更新顶点)吗? 没必要。

  1. 一般 spine 是用一个大的 buffer 来存放全部的顶点数据,position color uv ,图集变化时,uv很可能发生变化。当 uv 变化时,肯定需要重新更新 buffer。而初始化并不是一个高频的操作,没必要为了优化 buffer 的更新而特地把部分 attribute 分配新的 buffer。
  2. Spine动画通常是批量操作顶点数据,通过重新创建mesh,可以在初始化时为整个新的资源分配一次大的buffer,避免频繁的GPU调用。由于GPU本身擅长处理大批量的数据写入,分配新buffer反而能更好地利用GPU特性,提升渲染效率。(discard)
  3. 由于 Spine 还存在动画和物理的更新,不修改顶点的优化还可能导致不可预见的渲染结果。相比之下,直接更新 buffer,更易于维护和排查问题。

生命周期方法

原生的几个方法已经能够满足开发需求了,高级的代理方法根据调研,目前只有 Unity 和 Godot 有提供。两个引擎提供的代理方法也不一样。所以现在就新增额外的生命周期方法不合适。暂时不额外添加。

singlecoder commented 1 month ago

感觉整体结构是不是有点奇怪,应该先整体看 Spine 组件需要给开发者提供什么能力,再考虑如何实现

singlecoder commented 1 month ago

addSeparateSlot 这个不要的结论推导不是很理解,首先应该是考虑开发者是否需要吧?而不是 “目前这个方法可以删除,没有对应的插件实现绘制顺序的调整,这个暂时可以删除掉”?

johanzhu commented 1 month ago

addSeparateSlot 这个不要的结论推导不是很理解,首先应该是考虑开发者是否需要吧?而不是 “目前这个方法可以删除,没有对应的插件实现绘制顺序的调整,这个暂时可以删除掉”?

这里我的理由确实没有写清楚,我补充一下。核心原因我认为是引擎API 应该与的编辑器功能有关联,unity 提供了一个专门的拆分组件,这个方法是为这个组件服务的。 目前暂时还没有增加这个拆分插件的计划,所以这个方法我认为目前不需要对外暴露。未来要不要暴露也待讨论,因为功能是希望用户通过编辑器的这个拆分插件来实现拆分,而不是用这个方法在运行时拆。

johanzhu commented 1 month ago

感觉整体结构是不是有点奇怪,应该先整体看 Spine 组件需要给开发者提供什么能力,再考虑如何实现

好的👌 RFC在结构上确实需要改进,我增加一个章节对 spine 提供的整体能力增加调研对比,以归纳出核心且必要的一些能力。基于整体API的对比调研结果,再结合我们的API来提出修改方案。