👻

Spine アニメーションを Web ページで表示してみる

2022/07/17に公開

Spine とは

2D ゲームに特化したアニメーション制作ツールです。
Spine ギャラリーにもある通り、様々なゲームで利用されています。
公式サイトはコチラ

Spine の利用例として、特に有名なのはアイドルマスター シャイニーカラーズでしょうか。
コンピュータ・エンターテインメント・デベロッパーズ・カンファレンス 2018 にて、Spine を採用した件に触れていて、話題になっていましたね。

Spine ランタイム

Spine 公式が様々なプラットフォーム向けにランタイムを提供しています。
> Spine ランタイム

その中で、HTML5 WebGL をはじめとして Web 用のランタイムも用意されています。
今回は HTML5 WebGL – JavaScript/TypeScript を利用していきます。

余談ですが、公式ランタイムの他にサードパーティ製ランタイムもあります。
Web 系で言えば、Three.js や PixiJS 向けランタイムが有志によって提供されています。
公式汎用ランタイムを一から導入するよりも、サードパーティ製ランタイムを使った方が早く構築できる場合もあるため、プロジェクトに合わせて検討してみると良いでしょう。

サンプルページとリポジトリ

さっそくですが、下記が Spine アニメーション実装のサンプルページとリポジトリになります。
> Spine web-gl サンプルページ
> Spine web-gl リポジトリ

サンプルページの画面スクショはこんな感じです。
画面イメージ

サンプルページを表示するとオバケ 👻(誰がなんと言おうとオバケです)がクネクネと動いていると思います。
Spine web-gl ランタイムを利用して、Spine エクスポートデータを読み込み、Canvas 上でアニメーションをループ再生しています。

アニメーションは【Spine】ゲーム制作におけるアニメーション基礎を参考に製作しました。
Spine の使い方をとても丁寧に解説されており、本当に良きチュートリアル動画です。

ソースコード周りの解説

Spine 自体の使い方や、データのエクスポート方法等は、公式サイトを初めとして数々の解説記事がありますので、そちらに譲ります。
本記事では Spine アニメーションを Web ページ上で動かすための開発環境、構築方法、ソースコード周りを書き留めていきます。

実装の流れは以下の通りです

  1. 開発環境の用意
  2. @esotericsoftware/spine-webgl をインストール
  3. Spine エクスポートデータを開発環境へ格納
  4. Spine アニメーション実行のソースコード実装

開発環境の用意

まずは開発環境ですが TypeScript x 何かしらの JS バンドルツールがあれば OK です。
今回構築した Spine web-gl リポジトリ は、ベースが私個人のフロントエンド開発スターターキットでして、Pug x Scss x TypeScript x Webpack の構成になっています。
単純に私がいつも使っている汎用スターターキットなだけで、Pug や Scss が入っている事に深い意味はありません。TypeScript と Webpack があれば十分です。
TypeScript のコンパイルとバンドルができれば、rollup でも vite でも良いと思います。

ちなみに、今回の開発環境の概要は下記の通りです。

バージョン
Node.js v16.14.0
yarn 1.22.17
typescript 4.4.2
webpack 5.51.1

@esotericsoftware/spine-webgl をインストール

開発環境が用意できましたら @esotericsoftware/spine-webgl のインストールしていきます。
Web 系のランタイムは、2021 年 9 月に npm レジストリにて公開されたため、npmyarn でインストールできます。

以下のコマンドを実行して、@esotericsoftware/spine-webgl をインストールします。

yarn add @esotericsoftware/spine-webgl

もしくは

npm install @esotericsoftware/spine-webgl

Spine ランタイムのバージョンについて

Spine ランタイムアーキテクチャ にて言及されている通り、Spine はエディタとランタイムのバージョンを同期させるよう推奨しています。
もし、Spine アニメーションが正しく再生されない場合は、Spine エディタのバージョン、エクスポート時のバージョン、Spine ランタイムのバージョンを確認しましょう。

今回の Spine 関連のバージョンは、以下の通りです。

バージョン
Spine エディタ 4.1.06
Spine エクスポートデータ 4.1
Spine ランタイム
(@esotericsoftware/spine-webgl)
4.1.19

Spine エクスポートデータを開発環境へ格納

今回は下記スクショの形式で Spine データをエクスポートしました。
「パック設定」はデフォルトのママです。
よりエクスポートデータのサイズを減らしたい場合は json ではなく、バイナリを選択すると良いです。

spine export 画面

Spine データをエクスポートすると .atlas.json.png の 3 点が出力されます。
この 3 点セットは、後ほど Ajax で取得することになりますので、開発環境の静的ファイル格納先にまとめておきましょう。
3 点のうち、いずれか 1 つでも欠けてしまうと Spine アニメーションが正しく表示されませんので、ご注意ください。

Spine アニメーション実行のソースコード実装

今回実装したソースコードは、Spine web-gl 公式サンプル | mix-and-match を参考にしています。
(…というか、公式サンプルを TypeScript に書き直して、少し手直ししただけです。)

では、肝心のソースコードを見ていきます。
主要な処理は下記の 2 ファイルに記述しています。

spineApp.ts

/src/ts/modules/spineApp.ts では SpineCanvasApp インターフェースを利用して、SpineApp クラスを定義してます。
この SpineApp クラスが、今回のソースコードの最も重要な点です。
SpineApp クラスのインスタンス(以降、Spine アプリとします)が、アニメーション描画の核のような存在でして、Spine エクスポートデータのロード、状態の更新、描画等を担います。

Spine アプリにはライフサイクルが存在し、発火順に loadAssetsinitializeupdaterender となっています。
詳細は以下の通りです。

  • loadAssets
    • 一番初めに発火します。
      assetManager を介して、Spine エクスポートデータを読み込みます。
  • initialize
    • すべての Spine エクスポートデータが読み込まれた時点で発火します。
      主に読み込んだデータの初期設定を行います。
  • update
    • 定期的な画面更新時(requestAnimationFrame)に発火します。
      画面描画以外の処理(Spine アプリの状態更新など…)を行います。
      画面描画処理は、下記の render が担います。
  • render
    • 定期的な画面更新時(requestAnimationFrame)に発火します。
      画面描画処理を行います。
  • error
    • Spine エクスポートデータが正しく読み込まれなかった場合にのみ発火します。

SpineApp クラスには、それぞれのライフライクルにあった処理を記述していけば OK です。
下記がソースコード全文です。

// spine
import * as spine from '@esotericsoftware/spine-webgl'

export class SpineApp implements spine.SpineCanvasApp {
  private skeleton: unknown // type: spine.Skeleton
  private state: unknown // type: spine.AnimationState

  loadAssets = (canvas: spine.SpineCanvas) => {
    // atlas ファイルをロード
    canvas.assetManager.loadTextureAtlas('model.atlas')
    // skeleton(json 形式) をロード
    canvas.assetManager.loadJson('model.json')
  }

  initialize = (canvas: spine.SpineCanvas) => {
    // spine のアセットマネージャーを取得
    const assetManager = canvas.assetManager

    // テクスチャアトラスを生成
    const atlas = canvas.assetManager.require('model.atlas')
    // AtlasAttachmentLoader(リージョン、メッシュ、バウンディングボックス、パスのアタッチメントを解決するための要素)のインスタンスを生成
    const atlasLoader = new spine.AtlasAttachmentLoader(atlas)
    // skeleton(json 形式) を読み込むためのオブジェクトを生成
    const skeltonJson = new spine.SkeletonJson(atlasLoader)
    // skeleton 情報を読み込み
    const skeltonData = skeltonJson.readSkeletonData(
      assetManager.require('model.json')
    )
    // skeleton インスタンスを生成して、メンバにセット
    this.skeleton = new spine.Skeleton(skeltonData)

    if (this.skeleton instanceof spine.Skeleton) {
      // skeleton の位置を画面中央にセット
      this.skeleton.x = 0
      this.skeleton.y = (-1 * Math.floor(this.skeleton.data.height)) / 2
      // skeleton の大きさを等倍にセット
      this.skeleton.scaleX = 1
      this.skeleton.scaleY = 1
    }

    // skeleton 情報からアニメーション情報を取得
    const stateData = new spine.AnimationStateData(skeltonData)
    // アニメーションをセット
    this.state = new spine.AnimationState(stateData)
    if (this.state instanceof spine.AnimationState) {
      this.state.setAnimation(0, 'animation', true)
    }
  }

  update = (canvas: spine.SpineCanvas, delta: number) => {
    if (!(this.skeleton instanceof spine.Skeleton)) return
    if (!(this.state instanceof spine.AnimationState)) return

    // アニメーションを更新
    this.state.update(delta)
    this.state.apply(this.skeleton)
    this.skeleton.updateWorldTransform()
  }

  render = (canvas: spine.SpineCanvas) => {
    if (!(this.skeleton instanceof spine.Skeleton)) return

    // レンダラー取得
    const renderer = canvas.renderer

    // 画面リサイズ(ブラウザサイズが変更された時の対応)
    renderer.resize(spine.ResizeMode.Expand)
    // 画面クリア
    canvas.clear(0.2, 0.2, 0.2, 1)
    // 描画開始
    renderer.begin()
    // skeleton を描画
    renderer.drawSkeleton(this.skeleton)
    // 描画終了
    renderer.end()
  }

  error = (canvas: spine.SpineCanvas) => {
    // エラーがあれば、以降が発火する
    console.log('error!!')
    console.log(canvas)
  }
}

main.ts

/src/ts/main.ts では SpineCanvas クラスのインスタンスを生成しています
SpineCanvas のコンストラクタの第 1 引数には canvas 要素を。 第 2 引数には先程の Spine アプリを入れます。
コードを見て察しがつくと思いますが、SpineCanvas クラスの役割は、描画先の canvas 要素と Spine アプリの紐付けです。

// spine
import * as spine from '@esotericsoftware/spine-webgl'
// modules
import { SpineApp } from './modules/spineApp'

window.onload = () => {
  // canvas 要素
  const canvasEl = document.getElementById('canvas') as HTMLCanvasElement

  // canvas 要素と SpineApp インスタンスを紐付ける
  new spine.SpineCanvas(canvasEl, {
    pathPrefix: 'assets/spine-data/', // Spine エクスポートデータ 3 点セットの格納先
    app: new SpineApp()
  })
}

たったこれだけのコードで Web ページ上で Spine アニメーションを再生できます。
意外とカンタンですね。

最後に

今回はシンプルなループアニメーション表示だけでしたが、Spine デモ にもある通り、パス・コンストレイントやインバース・キネマティクス等のリッチなアニメーションも Web ページ上で再生できます。
また、JavaScript でのアニメーション制御も可能なため、単純なアニメーション再生だけでなく、他の Canvas 系の JS ライブラリと組み合わせる事も可能です。

キャンペーンサイトやプロモーションサイトに組み込むと、中々面白そうな演出が作れそうですね。
私もクリエイティブコーディング x Spine アニメーションで、何か作品を作ってみたいと画策しているところです。
また、Spine の知見が貯まりましたら、記事にしてみたいと思います。

Discussion