PixiJS x Spineで作るキャラクターアニメーション【舞い散る桜と少女】
今回も 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 のフィルターは、 Container
の filters
配列に追加すると適用されます。
今回のソースコードでは 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