🌸

PixiJS x Spineで作るキャラクターアニメーション【舞い散る桜と少女】

2023/12/04に公開
1

今回も PixiJS / pixi-spine / Spine を利用して、キャラクターアニメーションを作ってみました。

成果物

画面キャプチャ
舞い散る桜と少女

成果物のURLはコチラです。
> 舞い散る桜と少女 | PixiJS x Spine

ページを表示するとセーラー服の少女と、舞い散る桜のアニメーションが再生されます。
今回は Spine アニメーションのリサイズ処理も実装しています。ウィンドウ幅を変更しても、少女のアニメーションが画面外に飛び出さずに再生できます。
※PC での閲覧推奨です。 スマホでも再生はできますが、処理が重く、カクつくと思います。)

前回前々回と同様、キャラクターアニメーションは Spine で作成。桜が舞い散る様子や差し込む光は PixiJS で作成しています。
イラストは Stable Diffusion で生成して、Photoshop + ペンタブで書き足したりして、調整しました。

主な npm パッケージとバージョン

主な npm パッケージとバージョンは以下の通りです。
また、プロジェクトは vite で作成しています。

パッケージ バージョン
lil-gui ^0.19.1
@pixi/filter-godray ^5.1.1
pixi-spine ^4.0.4
pixi.js ^7.3.2
typescript ^5.2.2

ソースコードの解説

リポジトリはコチラです。
> t-tonyo-maru/pub_web_pixijs_sakura-girl-animation

いろいろとファイルが格納されていますが、主要なファイルは下記リンクの main.ts のみです。
> /src/main.ts

以降は main.ts の要所をピックアップして、解説していきます。

キャラクターアニメーション

大まかな処理は前回と同じです。
前回と異なる点は Spine アニメーションの縮尺率(scale)を調整している点です。
PixiJS の描画範囲の高さ(app.screen.height) の 8 割 = Spine アニメーションの縮尺率 1 倍として計算しています。…(1)
リサイズ時にも縮尺率の再計算をしているため、ウィンドウサイズになっても Spine アニメーションが見きれず表示されるわけですね。

import 'pixi-spine'
import { Spine } from 'pixi-spine'

let spineAnimationHeight = 0 // Spine アニメーションの初期の高さ

// Spine エクスポートファイルの読み込み
const spineAnimation = await PIXI.Assets.load(
  `path/to/assets/spine-data/model.json`
)
  .then((res) => {
    // 読み込み成功時
    // pixi-spine より Spine Animation インスタンスを生成
    const animation = new Spine(res.spineData)

    // Spine Animation の位置をセット
    animation.x = app.screen.width / 2
    animation.y = app.screen.height + 2 // Spine アニメーションの粗隠しのため、+2 しています

    // トラック:0 に idle アニメーションを追加
    animation.state.setAnimation(0, 'idle', true)
    // トラック:1 に bright アニメーションを追加
    animation.state.setAnimation(1, 'bright', true)
    // トラック:2 に blink アニメーションを追加
    animation.state.setAnimation(2, 'blink', true)

    // Spine アニメーションの初期の高さを取得
    spineAnimationHeight = animation.height

    // Spine アニメーションのスケールを調整 … (1)
    const scaleRatio = 1 / (spineAnimationHeight / (app.screen.height * 0.8))
    animation.scale.set(scaleRatio >= 1 ? 1 : scaleRatio)

    return animation
  })
  .catch((err) => {
    console.log(err)
  })

舞い散る桜のアニメーション

舞い散る桜のアニメーションは、PixiJS の ParticleContainer を利用して、生成しています。
処理の内容としては、前回に作成したホタルのアニメーションと似ています。

ポイントとしてはアニメーション関数(app.ticker.addの引数)にて、桜の縮尺率(scale)を更新している点でしょうか。…(1)
やっている事は単純で、桜の縦・横の縮尺率を、cos・sin で周期的に更新しているだけです。
たったコレだけでも、桜の花びらの表と裏が翻って"ヒラヒラを舞っている感じ"が作れます。

// 桜の花びら単体の型
type Sakura = {
  sprite: PIXI.Sprite // 桜の Sprite
  speed: number // 桜の落下速度
  positionPhase: number // 位置をズラすための位相
  scalePhase: number // 縮尺率をズラすための位相
  rotationSpeed: number // 桜の回転速度
}
// 桜の総数
const MAX_PARTICLE_COUNT = 100
// 微調整用の係数(マジックナンバー回避のため、定数化)
const SCALE_PHASE_COEFFICIENT = 0.16 // scale 用
const POSITOIN_PHASE_COEFFICIENT = 3 // position 用

// …略…

// 舞い散る桜用のパーティクルコンテナを生成
const particleContainer = new PIXI.ParticleContainer(MAX_PARTICLE_COUNT, {
  scale: true,
  position: true,
  rotation: true,
  uvs: true,
  alpha: true
})
// 桜を管理するための配列
const partilces: Sakura[] = []
// 桜のテクスチャ読み込み
const sakuraTexture = PIXI.Texture.from(
  `path/to/assets/images/sakura_hanabira.png`
)
// 桜の生成
for (let i = 0; i < MAX_PARTICLE_COUNT; i++) {
  // 縮尺率にランダム性を加える
  const scalePhase = Math.random() * Math.PI * 2

  // 桜の Sptite を生成
  const sakuraSprite = PIXI.Sprite.from(sakuraTexture)
  // アンカーを中心にセット
  sakuraSprite.anchor.set(0.5)
  // 桜の縮尺率をセット
  sakuraSprite.scale.set(
    Math.cos(Date.now() / 1000 + scalePhase) * SCALE_PHASE_COEFFICIENT,
    Math.sin(Date.now() / 1000 + scalePhase) * SCALE_PHASE_COEFFICIENT
  )
  // 桜をランダムな位置に配置
  sakuraSprite.position.set(
    Math.random() * app.screen.width,
    Math.random() * app.screen.height
  )
  // 桜の角度をセット
  sakuraSprite.rotation = Math.random() * Math.PI * 2

  // 管理用配列に詰め込み
  partilces.push({
    sprite: sakuraSprite,
    speed: Math.random() * 2 + randRange(0.1, 1), // random speed
    rotationSpeed: Math.random() * 0.01, // random rotation speed
    positionPhase: Math.random() * Math.PI * 2, // random position phase
    scalePhase // random scale
  })
  // パーティクルコンテナに詰め込み
  particleContainer.addChild(partilces[i].sprite)
}

// アニメーション関数
app.ticker.add((delta) => {
  // 桜の状態を更新して、舞い散っているように見せる
  for (let i = particleContainer.children.length - 1; i >= 0; i--) {
    const sakura = particleContainer.children[i] as PIXI.Sprite
    // 桜の横位置の更新
    sakura.position.x +=
      Math.sin(Date.now() / 1000 + partilces[i].positionPhase) *
      POSITOIN_PHASE_COEFFICIENT
    // 桜の縦位置の更新
    sakura.position.y += partilces[i].speed
    // 桜の角度を更新
    sakura.rotation += partilces[i].rotationSpeed
    // 桜の縮尺率を更新 … (1)
    sakura.scale.set(
      Math.cos(Date.now() / 1000 + partilces[i].scalePhase) *
        SCALE_PHASE_COEFFICIENT,
      Math.sin(Date.now() / 1000 + partilces[i].scalePhase) *
        SCALE_PHASE_COEFFICIENT
    )

    // 桜が画面の左右両端を超えた場合
    if (sakura.position.x < 0) {
      sakura.position.x = app.screen.width
    } else if (sakura.position.x > app.screen.width) {
      sakura.position.x = 0
    }

    // 桜が画面下端を超えた場合
    if (sakura.position.y - sakura.height > app.screen.height) {
      sakura.position.y = sakura.height * -1
    }
  }
})

差し込む光のアニメーション

差し込む光は PixiJS の Godray Filter を利用しました。
PixiJS のフィルターは、 Containerfilters 配列に追加すると適用されます。
今回のソースコードでは Container に画面を構成する要素すべて(背景画像・舞い散る桜のパーティクル・Spine アニメーション…の 3 点)を入れているため、画面全体にフィルター効果が適用されます。

import { GodrayFilter } from '@pixi/filter-godray'

// godray filter のパラメータ
const godrayParameter = {
  gain: 0.6,
  lacunarity: 2.75,
  alpha: 1,
  parallel: true,
  angle: 30,
  center: new PIXI.Point(100, -100)
}
// godray filter のインスタンスを生成
const godrayFilter = new GodrayFilter(godrayParameter)

const container = new PIXI.Container()
// Container の filters に godray filter のインスタンスをセット
container.filters = [godrayFilter]

app.stage.addChild(container)

試しに、画面左上の GUI の「godray filter」から、パラメータを変更してみてください。
変更したパラメータに応じて、フィルター効果が変わると思います。

PixiJS のフィルターは簡単に設定できて、大変便利ですね!

まとめ

PixiJS と Spine の組み合わせは、少ないコードからリッチな演出を作成できるので、とても楽しいですね。

次回は Spine のトランスフォーム・コンストレイントあたりを使ったアニメーションを作成してみようと思います。
では、また!

Discussion

できますできます

Hi, I am looking for a pixi.js development engineer with a monthly salary of US$7k-15k