Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjustment of spine runtime basic API #109

Closed
johanzhu opened this issue Sep 19, 2024 · 4 comments
Closed

Adjustment of spine runtime basic API #109

johanzhu opened this issue Sep 19, 2024 · 4 comments

Comments

@johanzhu
Copy link
Collaborator

johanzhu commented Sep 19, 2024

背景

过去的 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

概要

  • Unity Spine Runtime 提供了最复杂的渲染参数与开关 API,目的是为了渲染优化与额外的功能实现
  • Unity 提供了运行时创建 spine 动画(GameObject + spine 组件)的 API,且能够在运行时完成 spine 资产的替换(重新初始化)
  • Unity 提供了参数控制 Transform 变化对 Spine 物理的加权

API 概览

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

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

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

详细 API 如下:

SkeletonAnimation 对外暴露的 API 有:

  • state :spineCore 的 AnimationState 用于动画控制
  • animationName (set and get):用于设置动画
  • unscaledTime:控制动画是否使用“未缩放的游戏时间”来运行
  • loop:用于控制 animationName 播放的动画是否循环
  • timeScale:设置动画的播放速度,与 spine 动画轨道的播放速度同时生效
  • AddToGameObject:运行时给一个 GameObject 添加 SkeletonAnimation 组件,并设置 spine 骨架数据
  • NewSkeletonAnimationGameObject: 运行时创建一个添加了了 SkeletonAnimation 的GameObject,并设置 spine 骨架数据
  • ClearState:清除之前生成的 mesh,并清空动画
  • Initialize :初始化构建 mesh
  • ApplyAnimation:更新动画逻辑
  • AfterAnimationApplied:动画更新后的处理逻辑
  • UpdateBonesDelegate: 四个事件代理方法

SkeletonRenderer 对外暴露的 API 有:

  • skeletonDataAsset:存储了 spine 骨骼数据
  • initialSkinName: 初始皮肤
  • initialFlipX,initialFlipY:是否反转 spine 骨架
  • updateMode:spine 更新模式,一共有五种:不更新,仅更新动画,只更新时间轴,除 mesh 均更新,全部更新
  • separatorSlots:待拆分的插槽
  • useClipping:是否开启裁减
  • immutableTriangles:是否固定 index
  • pmaVertexColors:是否开启预乘
  • clearStateOnDisable:是否在 disable 时,清除动画
  • tintBlack:是否开启 tint
  • singleSubmesh:是否假设仅有一个 submesh
  • fixDrawOrder:开启后禁用 GPU 实例化,保证渲染顺序的正确性
  • maskInteraction: 遮罩方式
  • materialsMaskDisabled:遮罩材质
  • materialsInsideMask:遮罩材质
  • materialsOutsideMask:遮罩材质
  • maskMaterials(SpriteMaskInteractionMaterials):遮罩材质
  • addNormals:是否添加法线信息
  • calculateTangents:是否添加切线信息
  • OnPostProcessVertices:顶点数据传递到 mesh 前的回调
  • CustomMaterialOverride:自定义覆盖材质
  • CustomSlotMaterials:自定义插槽覆盖材质
  • SkeletonClipping:保存了裁减工具对象
  • skeleton:SpineCore.Skeleton 用于骨架操作(包括换肤等操做)
  • valid:用于禁用组件,开启后初始化和更新方法均失效,销毁组件时会设置为 true
  • PhysicsPositionInheritanceFactor:用于控制位置变化对骨架物理约束的影响程度
  • PhysicsRotationInheritanceFactor:用于控制旋转变化对骨架物理约束的影响程度
  • PhysicsMovementRelativeTo:用于设置物理的参考 Transform
  • ResetLastPosition:重制位置(用于处理物理约束)
  • ResetLastRotation:重制旋转(用于处理物理约束)
  • ResetLastPositionAndRotation:重制位置与旋转(用于处理物理约束)
  • OnRebuild:重新构建 mesh 时触发
  • OnMeshAndMaterialsUpdated:LateUpdate 后,mesh 与 material 更新后触发
  • NewSpineGameObject:创建一个 GameObject 并添加渲染器组件,并设置 spine 骨架数据
  • AddSpineComponent:给一个GameObject 添加渲染器组件,并设置 spine 骨架数据
  • SetMeshSettings:用于设置渲染参数。参数包括:calculateTangents,immutableTriangles,pmaVertexColors,tintBlack,useClipping,zSpacing
  • ClearState: 清空动画数据,mesh 等
  • EnsureMeshGeneratorCapacity:用于扩大 vertex 数组大小
  • Initialize:初始化,构建 mesh
  • ApplyTransformMovementToPhysics:用于更新 spine 物理
  • FindAndApplySeparatorSlots:应用拆分的插槽
  • ReapplySeparatorSlotNames:重应用拆分的插槽

在Unity 编辑器中

image

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

除此之外的检查项有:

  • initial Skin 初始皮肤名称
  • Animation Name 默认播放的动画名称
  • Loop 默认动画是否循环
  • TimeScale 动画播放速率
  • Unscaled Time 控制动画是否使用“未缩放的游戏时间”来运行
  • RootMotion :用于添加 rootMotion 插件组件
  • Sorting Layer
  • Order Layer
  • Mask interaction
    后三个是 unity 的 2D 配置项,以上检查项在API中都能够找到对应项。

总结:

  • 从功能丰富性来看,Unity 的 API 是无出其右的。提供了非常多高级功能来满足各种场景的需要(高级功能也用 Advanced 折叠块进行了折叠,对用户也是比较友好的)
  • 对小白来说点开 Advanced 后,看到这么多高级功能肯定会一脸懵逼。熟练掌握需要花费时间。

Unreal Engine

概要

  • Unreal Spine Runtime 倾向于利用 Unreal 的蓝图系统和 C++,对蓝图系统的依赖较强。
  • Unreal 无法运行时修改 spine 资产。

API 概览

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

API 可以划分为这几类:

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

详细 API 如下:

动画组件 API:

  • GetAnimationState:返回 SpineCore 的 AnimationState 对象
  • BeginPlay: 开始播放
  • TickComponent:更新函数
  • FinishDestroy:销毁

蓝图节点:

  • SetAutoPlay:设置自动播放
  • SetPlaybackTime:这是回放时间
  • SetTimeScale:设置时间缩放系数
  • GetTimeScale:获取时间缩放系数
  • SetAnimation:播放动画
  • AddAnimation:添加动画
  • SetEmptyAnimation:设置空动画
  • AddEmptyAnimation:添加空动画
  • GetCurrent:获取当前播放的动画轨道
  • ClearTracks:清空动画轨道
    以上都是基于 SpineCore 的 AnimationState 的 API 提供的蓝图节点

代理方法:

  • AnimationStart:动画开始事件
  • AnimationInterrupt:动画中断事件
  • AnimationEvent:动画自定义事件
  • AnimationComplete:动画完成事件
  • AnimationEnd:动画结束事件
  • AnimationDispose:动画销毁事件
  • PreviewAnimation:编辑器配置项,预览动画名
  • PreviewSkin:编辑器配置项,预览皮肤

渲染组件 API:

  • Atlas:spine 的 atlas 资产
  • SkeletonData:spine 的骨架资产数据
  • GetSkeleton: 返回 skeleton 对象

蓝图方法:

  • GetSkins:获取皮肤数组
  • SetSkins:设置皮肤数组
  • SetSkin:获取当前皮肤
  • HasSkin:判断是否存在某个名称的皮肤
  • SetAttachment:替换附件
  • GetBoneWorldTransform:获取骨骼世界变幻
  • SetBoneWorldPosition:设置骨骼世界位置
  • UpdateWorldTransform:更新世界变幻
  • SetToSetupPose:重制回初始姿势
  • SetBonesToSetupPose:重制骨骼回初始姿势
  • SetSlotsToSetupPose:重制插槽为初始状态
  • SetScaleX:设置X轴缩放
  • GetScaleX:获取X轴缩放
  • GetBones:获取骨骼
  • HasBone:判断是否存在某个名称的骨骼
  • GetSlots:获取插槽
  • HasSlot:判断是否存在某个插槽
  • SetSlotColor:设置插槽颜色
  • GetAnimations:获取动画
  • HasAnimation:判断是否存在某个动画
  • GetAnimationDuration:获取动画持续时间

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

在UE 编辑器中

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

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

总结:

  • UE 对外暴露的 API 实际上不多,更多的是基于 SpineCore API 而暴露的一些蓝图节点,更加依赖蓝图操作
  • UE 没有没有对外暴露运行时创建 spine 动画的 API,也无法运行时修改资产
  • 没有提供高级功能,但是能够满足常规的开发诉求

Godot

概览

  • Godot 的实现也相对简单的,遵循 Godot 的 Node-based 系统,接口也比较轻量化。

API 概览

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

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

详细 API 如下:

  • set_skeleton_data_res:设置 Spine 资产
  • get_skeleton_data_res:获取 Spine 资产
  • get_skeleton:获取骨架对象,用于骨架操作
  • get_animation_state:获取动画对象,用于动画控制
  • on_skeleton_data_changed:Spine 资产更新回调
  • update_skeleton:更新方法
  • get_global_bone_transform:获取骨骼世界位置
  • set_global_bone_transform:设置骨骼世界位置
  • get_update_mode:获取更新模式
  • set_update_mode:设置更新模式
  • new_skin:设置皮肤

事件:

  • animation_started, 当动画开始时触发.
  • animation_interrupted, 当清空动画轨道或设置了某个新动画时触发.
  • animation_completed, 当某条轨道上的动画完成了一个循环时触发.
  • animation_ended, 当不再应用某个动画时触发.
  • animation_disposed, 当销毁动画轨道条目时触发.
  • animation_event, 当用户定义的事件发生时触发.
  • before_animation_state_update, 在以当前delta时间更新动画状态前触发.
  • before_animation_state_apply, 在skeleton姿态应用动画状态前触发.
  • before_world_transforms_change, 在skeleton的世界变换(world transforms)被更新前触发.
  • world_transforms_changed, 在skeleton的世界变换(world transforms)更新后触发.

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

  • get_normal_material
  • set_normal_material
  • get_additive_material
  • set_additive_material
  • get_multiply_material
  • set_multiply_material
  • get_screen_material
  • set_screen_material

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

在编辑器中:

godot 提供的检查项有:

  • SkeletonData Res: spine 资产
  • Update Mode:更新模式
  • Materials:不同混合模式的材质
  • Debug:编辑器内的 debug 配置
  • Preview:预览
    检查项与API对应

总结:

  • Godot 的对外提供的 API 也比较轻量化,但是能够满足常规的的开发诉求
  • Godot 没有对外暴露运行时创建 spine 动画的 API,也无法运行时修改资产
  • Godot 的 Preview 相关 API 并没有对外暴露,在编辑器设置后,若运行时未设置动画,则会生效

Pixi

概览

  • Pixi 提供的 API 也非常简单,遵循 PixiJS 的 API 结构,使用起来简单方便。
  • 动画资产的加载依赖于 Pixi 本身的 Loader API

API 概览

Pixi 的 API 可以分为:

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

详细 API:

  • skeleton:SpineCore.Skeleton
  • state:SpineCore.AnimationState
  • debug: debug 模式设置
  • autoUpdate:是否自动更新
  • update:更新方法
  • updateTransform:更新变换
  • destroy:销毁
  • addSlotObject:给插槽内添加一个 Pixi 对象
  • getSlotObject:获取插槽内的 Pixi 对象
  • removeSlotObject:移除插槽内的 Pixi 对象
  • setBonePosition:设置骨骼位置
  • getBonePosition:获取骨骼位置
  • skeletonToPixiWorldCoordinates:spine 坐标系转 Pixi 世界坐标系
  • PixiWorldCoordinatesToSkeleton:Pixi 世界坐标系转 spine 坐标系
  • PixiWorldCoordinatesToBone:Pixi 世界坐标系转 spine 骨骼坐标系
  • from:通过资产创建 spine 动画
  • tint:设置 tint

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

总结:

  • Pixi 的 API 也比较简单,没有提供高级功能,但是能够满足常规的开发诉求
  • Pixi 运行时没无法运行时修改资产,只能重新加载并创建新的动画
  • 官方的 Pixi 运行时提供了一系列方法用于挂点操作
  • Pixi 三方运行时增加了 hackTexture 的方法用于动态替换附件的贴图

Cocos

概览

  • Cocos Spine Runtime 基于 Cocos 引擎的组件系统 API 多且杂
  • 提供了动画烘焙以及内存共享功能以优化性能,但是烘焙动画不支持混合。
  • cocos runtime 最高只支持 spine 3.8.99

API 概览

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

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

详细 API:

  • paused:动画暂停
  • skeletonData(get & set):获取与设置spine 资产
  • defaultCacheMode:设置动画缓存模式
  • premultipliedAlpha:预乘开关
  • loop:默认动画是否循环
  • timeScale:设置动画时间缩放系数
  • useTint: 染色开关
  • enableBatch:合批开关
  • sockets:挂点代理对象
  • debugSlots:debug方法
  • debugBones:debug方法
  • debugMesh:debug方法
  • socketNodes:挂点代理节点
  • customMaterial:自定义材质
  • __preload:预加载开关
  • getState:获取动画对象(AnimationState)
  • clearAnimation:清空动画
  • clearAnimations:清空多个动画
  • setSkeletonData:设置 spine 资产
  • setSlotsRange:设置插槽范围
  • getAttachment:获取附件
  • setAttachment:替换附件
  • getTextureAtlas:获取图集对象
  • setAnimation:播放动画
  • addAnimation:添加动画
  • findAnimation:寻找对应名称的动画
  • getCurrent:获取当播放的动画轨道
  • setSkin:设置皮肤
  • updateAnimation:更新spine动画
  • markForUpdateRenderData:renderer通用方法
  • isAnimationCached:动画是否开启缓存
  • setAnimationCacheMode:设置动画缓存模式
  • setToSetupPose:设置重制回初始状态
  • setSlotsToSetupPose:设置插槽回初始状态
  • invalidAnimationCache:禁用动画缓存
  • findBone:寻找骨骼
  • findSlot:寻找插槽
  • setMix:设置动画混合时间
  • clearTracks:清空多个动画轨道
  • clearTrack:清空动画轨道
  • querySockets:获取挂点骨骼路径
  • setVertexEffectDelegate:设置顶点动画代理
  • setStartListener:设置动画开始事件监听
  • setInterruptListener:设置动画中断事件监听
  • setEndListener:设置动画结束事件监听
  • setDisposeListener:设置动画释放事件监听
  • setCompleteListener:设置动画完毕事件监听
  • setEventListener:设置自定义动画事件监听
  • setTrackStartListener:设置某轨道的动画开始事件监听
  • setTrackInterruptListener:设置某轨道的动画中断事件监听
  • setTrackEndListener:设置某轨道的动画结束事件监听
  • setTrackDisposeListener:设置某轨道的动画释放事件监听
  • setTrackCompleteListener:设置某轨道的动画完毕事件监听
  • setTrackEventListener:设置某轨道的自定义动画事件监听
  • setSlotTexture:设置插槽贴图

在cocos编辑器中:

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

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

总结:

  • cocos 暴露的 API 非常多,不过大部分是直接从 SpineCore.Skeleton, SpineCoreAnimationState 中经过封装然后暴露出去的,功能完全相同。只不过内部处理了动画缓存相关的逻辑
  • cocos 提供了 2 个方法动态替换 spine 动画资产。一个的入参是编辑器资产类型,一个的入参是 SpineCore.SkeletonData 原始的对象。
  • cocos 提供了挂点相关的 API,并且在编辑器内也有对应的挂点功能
  • cocos 支持运行时动态替换素材
  • cocos 能够设置动画烘焙以提升性能
  • cocos 提供了一些用于骨骼挂点的 API

Laya

概览

  • Laya 的提供了 API 开启性能优化
  • Laya 增加了暂停,恢复等额外的 API 用户控制 spine 动画
  • Laya 最高支持 spine 4.1 版本

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 的检查项较少,只有:

  • Source
  • Skin name
  • Animation Name
  • Loop
  • Preview
  • ExternalSKins

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

总结:

  • laya 也提供了很多基于 SpineCore.Skeleton, SpineCore.AnimationState 中封装然后暴露出去的同名 API。
  • laya 仅在运行时提供了性能优化接口用于开启性能优化,在编辑器内是没有对应选项的,这一点和 unity 和 cocos 不同。
  • laya 能够在运行时加载并替换 spine 动画资产。

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

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

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

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

目前的 API 梳理

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

  • resource (getter & setter):用于设置 spine 动画素材
  • state :用于动画播放
  • skeleton:用于骨架控制与换装
  • addSeparateSlot:用于添加拆分的插槽
  • defaultState:用于设置动画初始状态
  • setting:用于设置一些渲染的基本参数

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

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

API 调整总览

  • spine 骨架/动画操作:目前的两个 API 能够满足开发者的诉求了,无需调整。理由如下:

    1. 骨架与动画操作底层都依赖 SpineCore 的 Skeleton 和 AnimationState API,直接暴露出这两个对象的是最简单最直接的方式。这种情况下,用户没有额外的学习成本。

    2. 调研的引擎中,cocos ,laya,ue 没有直接暴露 Skeleton 和 AnimationState 这两个对象,而是直接暴露的二次封装的方法,他们这样做的原因如下:

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

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

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

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

  • 动画资产相关方法:根据调研,资产相关方法,目前存在的问题有:
  1. 骨架和 atlas 的映射关系是固定的,无法通过手动的方式进行替换,灵活性不足。这点 cocos 和 unity 都是能做到的。
  2. 另外,渲染组件中不应该存储 resource 资源。通过 SkeletonDataResource 获取 SkeletonData 的方式也非常别扭。
  • 生命周期相关方法:缺少较底层的生命周期方法,但使用场景不多,可以暂时先不增加。Spine 默认的动画事件足够满足开发诉求了。

  • 渲染相关参数与方法: tint 和 pma 缺失,pma 非常需要,tint 不太常用。pma 本次里程碑会支持

  • 其他:本次会增加一些性能优化的参数开关(预乘)

API 修改调整方案

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

resource

修改方向

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

具体方案:

  • SpineAnimationRenderer 中的resource 的 getter 和 setter 标记为 deprecated。
  • SkeletonDataResource 名称修改为 SpineResource,内部存储: SkeletonData 和 AnimationData。
  • 组件通过直接设置 Skeleton 和 AnimationState 的方式初始化
const { skeletonData, animationData } = spineResource;
const skeleton = new Skeleton(skeletonData);
const state = new AnimationState(animationData);
const entity = new Entity('spine-entity');
const spineAnimation = entity.addComponent(SpineAnimationRenderer);
spineAnimation.skeleton = skeleton;
spineAnimation.state = state;
root.addChild(entity);
  • SpineAnimationRenderer 提供两个语法糖方法帮助快速初始化spine组件:
/**
   * Creates a new `Entity` with a `SpineAnimationRenderer` component attached and initializes it
   * with the specified `SpineResource`. 
   * @param resource - The `SpineResource` used to initialize the `SpineAnimationRenderer`, 
   * @returns The newly created `SpineAnimationRenderer` component attached to the new `Entity`.
   * 
   * @example
   * ```typescript
   * const spineAnimation = SpineAnimationRenderer.createWithEntity(spineResource);
   * root.addChild(spineAnimation.entity); // Add the new entity with animation to the scene root
   * ```
  */
  static createWithEntity(resource: SpineResource): SpineAnimationRenderer {}

/**
    * Quickly attaches a `SpineAnimationRenderer` component to an existing `Entity`
    * and configures it with the specified `SpineResource`.
    * 
    * @param entity - The existing `Entity` to attach the component to.
    * @param resource - The `SpineResource` used to initialize the `SpineAnimationRenderer`.
    * @returns The newly created `SpineAnimationRenderer` component.
    * @example
    * ```typescript
    * const spineAnimation = SpineAnimationRenderer.attachToEntity(existingEntity, spineResource);
    * ```
  */
  static attachToEntity(entity: Entity, resource: SpineResource): SpineAnimationRenderer {}

针对资产 API 的调研:

  • unity

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

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

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

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

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

  • ue

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

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

image

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

image

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

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

  • godot

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

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

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

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

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

  • cocos 和 laya

都有动态替换 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:

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

  • ue

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

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

  • godot

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

API 与 spine-core 一致。

  • Pixi

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

  • cocos 和 laya

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

addSeparateSlot

改造方向

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

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

针对拆分功能的调研

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

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

defaultState

改造方向

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

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

针对默认状态 API 的调研:

  • unity

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

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

initialSkinName

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

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

  • ue

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

  • godot
    image

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

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

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

  • Pixi

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

  • cosos

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

  • laya
    有 animationName,loop,skinName 三个 getter setter,用于设置皮肤与动画。

setting

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

改造方向

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

针对渲染参数的调研:

  • unity
    类似的渲染参数是直接平铺在 Renderer 内的。

  • ue

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

  • godot

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

  • Pixi

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

  • cocos

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

  • laya

没有类似的渲染参数

新增实例化 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.提供新的创建运行时使用的资产的方法,并且支持手动建立资产关联关系。

  • 在 LoaderUtils 中增加一个 createSpineResource 方法,用于创建 SpineResource 对象。参数是 skeleton 的文件地址以及 TextureAtlas对象。

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

  • 在 LoaderUtils 中增加一个 crreateTextureAtlas 方法,用于创建 TextureAtlas 对象。参数是 atlas 地址和 texture 的地址。

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 有提供。两个引擎提供的代理方法也不一样。所以现在就新增额外的生命周期方法不合适。暂时不额外添加。

@johanzhu johanzhu changed the title Spine-runtime API Enhancement Spine-runtime basic API Enhancement Sep 19, 2024
@johanzhu johanzhu changed the title Spine-runtime basic API Enhancement Adjustment of spine runtime basic API Sep 19, 2024
@singlecoder
Copy link
Member

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

@singlecoder
Copy link
Member

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

@johanzhu
Copy link
Collaborator Author

johanzhu commented Oct 9, 2024

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

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

@johanzhu
Copy link
Collaborator Author

johanzhu commented Oct 9, 2024

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants