在 Web 中使用 Spine 动画

4,417 阅读5分钟

Spine 是什么

Spine 是什么?

Spine 是一款针对游戏开发的 2D 骨骼动画编辑工具。 Spine 旨在提供更高效和简洁的工作流程,以创建游戏所需的动画。

关于 Spine 如何制作动画,制作动画的原理,这里不涉及。设计师给到前端的导出文件有多种方式:

  • json
    • .json
    • .atlas
    • .png
  • skel
    • .skel
    • .atlas
    • .png
  • skel.bytes
    • .skel.bytes
    • .atlas.txt
    • .png

总结一下,主要是 3 类文件:

  • .json 文件和 .skel 文件:这个文件包含了 Spine 动画的骨架数据、动画数据以及附带的所有信息。它是一个文本文件,包含了动画的所有关节、插槽、皮肤、事件等信息。而 .skel 文件为二进制格式的 Spine 骨架文件,功能和 .json 文件类似,但是二进制更加紧凑和高效。
  • .atlas 文件:这个文件包含了贴图集的元数据,指示哪些图像文件被用作素材以及这些图像在纹理图集中的位置。
  • .png 文件:这些是实际的纹理图像文件,包含了骨骼动画中使用的所有图片资源。

.skel.bytes 文件通常在特定的引擎(如 Unity)中用来加载和解析动画数据。

在 Web 中的使用

在官网对运行时页面:zh.esotericsoftware.com/spine-runti… 有介绍各种平台和框架中使用的方法。

网上搜了一下 Spine 在 Web 前端中的应用,没有多少应用,掘金的一篇前端播放在线spine方案,介绍了在 Cocos Creator 和 h5 中的使用,其中在 h5 中使用了 pixi,而且是直接 script 引入的,没有 npm 使用的案例。

首先我们肯定要用一下官方运行时,其次项目是使用 Vite + Vue3,自然尽量使用 npm 的形式而不是直接在 script 中引入,这样和项目更融合,使用更方便。最后基本确定先尝试使用 spine-player

缺少 npm 使用案例

确实,网上也很难找到 npm 使用的案例。

官方的 Github 仓库:github.com/EsotericSof… 虽然介绍了很多技术方案的使用案例,但是还是不熟悉:

  • 下载官方的代码一直失败
  • spine-ts 中的 spine-core 似乎是其他方式的依赖
  • example 中的代码都是 script 引入的方式,没有 npm 使用方式
  • example 中的各种文件功能不清晰

官方的 Spine Web Player 同样看着问题很多,没有 npm 的使用案例。直接原样复制,竟然也会报错,最后使用 spine-player 的时候才发现,是参数不对,没有 skeletonatlas

NPM 上也有包 www.npmjs.com/package/@es… 也是来自 GitHub,没有更多的信息。

即使能找到 Spine goes NPM 的官方文章,依然有很多的问题。

多种文件的区别

官方案例中也有多种文件的使用,各不相同,让人不明白到底什么时候应该用什么文件。

比如 en.esotericsoftware.com/blog/Spine-… 示例中使用 .skel 文件;

比如 github.com/EsotericSof… 中也是用 .skel 文件

比如 github.com/EsotericSof… 中既有使用 .skel 文件,也有使用 .json 文件方式。

版本一致问题

使用的过程中,发现报错:Spine web player: Animation bounds are invalid: fadein

搜索发现:esotericsoftware.com/forum/d/250…

这可能是动画的 Spine Editor 版本和库的版本不一致问题,问设计师,才发现,我们也有使用 3.8.99,不过也能用 4.1.xx 制作,于是要了一个 4.1 版本的动画素材尝试。

参数问题

不得不说,除了文档不够清晰之外,示例用法和版本似乎也不能对应,可能是文档没更新。

查看源代码 github.com/EsotericSof…

/* The URL of the skeleton JSON (.json) or binary (.skel) file */
skeleton?: string;

/* @deprecated Use skeleton instead. The URL of the skeleton JSON file (.json). Undefined if binaryUrl is given. */
jsonUrl?: string

/* @deprecated Use atlas instead. The URL of the skeleton atlas file (.atlas). Atlas page images are automatically resolved. */
atlasUrl?: string

/* The URL of the skeleton atlas file (.atlas). Atlas page images are automatically resolved. */
atlas?: string;

确实存在多种写法,但是明明说的是 jsonUrlatlasUrl 已经废弃了?实际上只能使用这两个,而不是文档中的 skeletonatlas

动画无法自动播放

最后在使用 spine-player 显示动画之后,动画不能自动播放。

咨询 ChatGPT 之后,查看源码,并没有设置自动播放的参数 play 之类的。ChatGPT 也是各种给出错误答案,比如:

play: true // 自动播放

// 监听加载完成事件
player.on('ready', () => { player.play(); });

并表示:由于 SpinePlayer 库的文档和类型声明可能不完全一致。

解决:传入 animate 动画名称。如果有多个动画,传入参数 animates 动画列表数组。

animates: ['in', 'loop']

但是 spine 动画是否是自动播放的?设计反馈之前对接其他项目并不需要告知动画名称,开发只需要文件即可。

如果需要传入动画名称,那文档也有问题:

默认情况下, Web Player会播放在skeleton中找到的第一个动画. 默认动画也可以在配置中明确指定:

动画似乎有锯齿

最后,终于让动画动起来了,但是设计认为动画有锯齿,有点画质不好的感觉,图像的边缘应该是光滑的。

难道是背景透明的问题,尝试其他背景色,但是我们的效果肯定需要背景透明的。

偶然又看了一眼 Spine goes NPM,有个参数 premultipliedAlpha,看源码意思为:Whether the skeleton's atlas images use premultiplied alpha,默认值为 true,尝试设置为 false,然后锯齿问题就解决了。

我们建议对所有Spine Web Player需显示的资产均使用premultiplied alpha. 它减少了不使用premultiplied alpha时可能出现的伪影和接缝问题.

premultipliedAlpha 叫预乘 alpha,这个参数的含义,可以自行搜索。具体使用,可能还要看具体的动画效果而定。

参考代码

<template>
  <div ref="spineContainer" class="spine h-full w-full"></div>
</template>

<script setup lang="ts">
import {
  SpinePlayer,
  type SpinePlayerConfig,
} from '@esotericsoftware/spine-player'

interface Props {
  jsonPath: string
  atlasPath: string
  animations: string[]
}

const props = defineProps<Props>()
const spineContainer = ref<HTMLElement | null>(null)

onMounted(() => {
  if (!spineContainer.value) return

  const config: SpinePlayerConfig = {
    jsonUrl: props.jsonPath,
    atlasUrl: props.atlasPath,
    animations: props.animations,
    alpha: true,
    premultipliedAlpha: false,
    backgroundColor: '#00000000',
    preserveDrawingBuffer: false,
    showControls: false,
  }

  const player = new SpinePlayer(spineContainer.value, config)

  // 清理函数,当组件卸载时销毁 player
  return () => {
    player.dispose()
  }
})
</script>

最终代码很简单,spine-player 使用了最新版本。

参考