🧚

PixiJS の Sprite 調べてみた

2021/06/24に公開
2

PixiJS入門編 って記事書きまして、その続きです。

今回は画像ファイル読み込んで表示する系統のやつをやっていこうかなと思います。
例によって『RPGツクールMZ』で応用するつもりなので PixiJS のバージョン5.2.4 でいきます。
そんでもって適宜、公式のリファレンスにリンクしていきます。

PIXI.Sprite を選択

PixiJSで画像表示する場合は PIXI.Sprite を使うのが基本みたいです。
クラス継承関係的には次の通り。

PIXI.Container の下にありますから、子オブジェクトをぶら下げられる系オブジェクトっぽいです。
ただまー、コンテナならコンテナだけの役割して表示までしない方が、何かと使いやすくわかりやすいんじゃないかなとは思います。
また PixiJS Examples SPRITE にスプライト関連のサンプルがありますので、こっちも参考リンクしていきましょう。

画像ファイル読み込んで表示系のオブジェクトは、表示はもちろんですが読み込み部分がかなり重要なので、その辺りも突っ込んで調べてみようかと思います。

ともかく表示

とはいえ、とりあえず表示したいですよね。
PixiJS では、PNG、GIF、JPEG、SVG といったブラウザが使える画像は一通り使えるみたいですが、基本的にはPNGでしょうかね。
静止画を読み込んでアニメーションさせることもできます。 PixiJS Examples SPRITE/ Basic を見ると、うさぎがくるくる回ってます。
サンプルはサンプルとして、こっちでも適当な画像を用意して読み込んでみましょうか。

const sprite = PIXI.Sprite.from( "$Hime.png" );

"$Hime.png" は『RPGツクールMV』および『RPGツクールMZ』規格の歩行グラフィックです。
ツクール付属のデータは、ツクールシリーズおよびツクールシリーズで作られたゲーム(ソフト)のスクリーンショットや動画以外では使えないので、自前でぽちぽち打ったドットを使うのです。
皆さんもお好きな画像ファイルを用意して表示してみましょー!

ともかくこんな感じでいきなり画像ファイルを指定すれば PIXI.Spriteインスタンスを生成して返してくれるのが from()静的メソッドです。
PIXI.Sprite以外でも from()静的メソッドを使ってインスタンスを生成するのが PixiJS の常套手段です。

前回の PIXI.Text と同じノリで、app.stage.addChild() すれば表示されます。

const app = new PIXI.Application();
document.body.appendChild( app.view );
const sprite = PIXI.Sprite.from( "$Hime.png" );
app.stage.addChild( sprite );

このPIXI.Spriteの偉いところは読み込み終わってなくても、とりあえずオブジェクトを使えることです。
即時読み込まれている体でスクリプト書いておけば、読み込み終わったところで表示してくれます。いろいろ考えなくていいので便利ですね。

読み込んでいるかチェックしよう

ただし、SVG の場合は読み終わってからでないと動作しない操作もあるようです。
これは SVG がベクターデータであり PixiJS が扱うのがラスタデータである特性上、読み込み終わった時点で変換する必要があるんですね。
なんで先に操作したものを後から一気に適用してもうまくいかない場合もあるのかなと。よくわかんないですが。
単に読んで表示するだけなら、よほど変換に手間のかかるデータでない限り、ほとんど気にする必要はないのかも。

ともかくそんな場合、読み込み終わったのを判定する必要があります。
いろいろ方法があるみたいなんですが、どうも一番素朴なのは sprite._textureIDundefined かどうかで判定する方法のようです。
PIXI.Texture の説明に書いてありました。

なんかもっと別に isReadyToRender() みたいなメソッドあったりしそうなんですが、あったかなぁ…僕ほんと探すの下手くそなんですよ。
額にメガネかけてメガネを探すとかメガネあるあるですが、僕なんか普通にメガネかけててメガネ探しますからね! ちゃんと見えてんだからかけてるってわかるだろ! って思うでしょうが、それに気づかないのが検索能力最底辺の人間なのです。ヤバイですね!

えーと、なんでしたっけ。_textureID をチェックしておけば画像が読み込まれたかどうかチェックできるんで、undefined の時は操作しない、みたいなプログラム書けばいいということです。

あれ?なんか試してみると_textureID の規定値 undefinedじゃなくて-1 で、読み込まれたら正の数字に変わってますね。
…リファレンス嘘書いてる?バージョン違い?こっちの解釈間違い??

読み終わるまで待とう

結局こういうのは最初にリソースを全部読み終わるまで待つのが、一番間違いのない方法というのは相場が決まってます。
とはいえ、流石にずっとループして _textureIDを監視したら全部の動作が止まっちゃいますし、「もしかして永久ループしてませんか?」的なダイアログをブラウザが出してきちゃいます。
もうちょっとまともにやるなら、前回使った ticker の仕組みを利用して毎フレームチェックすることになるでしょうか。
それでも結構面倒臭いですね。

PixiJS は読み込み待ち用の仕組みが用意してあります。「そりゃそうだろ」って思うかもしれませんが、案外当たり前に用意してほしい仕組みが用意されてないライブラリとか、そこらにゴロゴロしてます。

でも PixiJS はちゃんと用意してるんです。やさすぅぃーん!
ほら前回 PIXI.Application が、一通り用意してくれると書いてた中にあった loader ですよ!

const app = new PIXI.Application();
document.body.appendChild( app.view );

app.loader.add( "$Hime.png" ).load( setup );   // ①

function setup() {
    const sprite = PIXI.Sprite.from( app.loader.resources[ "$Hime.png" ].texture );   // ②
    app.stage.addChild( sprite );
}

① こんな感じに読み込むファイルを add() して、load() で読み込み完了ハンドラ(コールバック関数)を設定する。
すると読み込み完了したところで、設定した関数(ここでは setup())が呼ばれる。
② ローダが持ってるリソースからファイル名で取り出したテクスチャデータを PIXI.Sprite.from() に渡してやれば、キュアスプライト!で・き・あ・が・りっ! (キュアってなんだよキュアって)

ちなみに②の部分は、ローダのリソースから渡さず PIXI.Sprite.from( "$Hime.png" ) と直接ファイル名を書いても、ちゃんとキャッシュされたデータを使ってくれるようです。

add() には複数のファイルを配列で渡したり、さらに凝った書式で渡すこともできます。
またハンドラは完了だけでなく読み込みの進捗も設定できます。万全の体制ですね。
詳細はリファレンス PIXI.Loader を見てください。

ちなみに loader は画像ファイルに限らず、JSON とかのテキストファイルや、音声など一通りのものの読み込みに使えるみたいです。
この辺りも、そのうち調べるかもしれません。

app.loader.resources と似た感じのやつで PIXI.utils.TextureCache というものもあって、こちらは TextureCache の名の通りテクスチャだけ保持しているので、画像しか扱わない場合は使いやすいと思います。

    const sprite = PIXI.Sprite.from( app.loader.resources[ "$Hime.png" ].texture );

って書いてたところを次のように書き換えても動作する、ということです。

    const sprite = PIXI.Sprite.from( PIXI.utils.TextureCache[ "$Hime.png" ] );

お好みで!!オコノミークラスで!!

ただ PIXI.utils見ると、「TODO:プロパティの使い方の説明書く」みたいに書いてあって、TextureCache に若干の不安があります。

似たようなのがいくつも

ところでローダなんですが app.loader の他に PIXI.loaderPIXI.Loader.shared というのがあって、どう違うんだかよくわかりません。
…というかコードは読んでないんですが、内部的には同じものが渡されてると思います。
どうも前は PIXI.loaderPIXI.Loader.shared を使ってたんだけど、他の主要オブジェクトと同様に PIXI.Application にまとめられたという経緯っぽい。
もちろん新規に const myLoader = new PIXI.Loader(); みたいな感じで作ってもいいんだろうけど、大抵の場合は規定値のままで使っても問題なくて新規に作る必要はないみたい。
新規に作るのは resources に全部のリソースがあると管理しづらくなる場合、とかかなぁ。
それもリソースのパスを保持しておく配列、とかの工夫をした方が使いやすい気がする。

PIXI.LoaderResource

読み込んだデータが保持されている resources のデータは PIXI.ILoaderResource で定義されています。
そのスーパークラスの PIXI.LoaderResource は、 PixiJS が専用に作ったやつじゃなくて resource-loader ってライブラリを取り込んだもののようです。
ファイル読み込み関連は、丸っと resource-loader が担当しているみたいで PIXI.LoaderResource = resource-loader.Resource ということのようです。
それで PIXI.ILoaderResource で新たにクラスを作るのではなく、いくつかプロパティを追加するという感じで PixiJS では使ってるんじゃないかなー。
この辺を詳しく調べれば、より細かいローディングやキャッシュデータの制御ができそうですね。
でもとりあえず resource-loader 使ってるんだということがわかればいいでしょう。

テクスチャ

ところで from()メソッドを使わずに、普通のオブジェクトと同様の new で生成する場合 PIXI.Texture をコンストラクタに渡す必要があります。

    const sprite = new PIXI.Sprite( texture );

PIXI.Texture は画面に表示するためのものではなく、データを保持しておくためのオブジェクトです。
そういえばさっきローダを使ったスクリプトでは texture プロパティを使っていましたね。
あれがどうも PIXI.ILoaderResource のリファレンス見た所 PIXI.Texture のようなので、new で生成する際に使えそうです。

  const sprite = new PIXI.Sprite( app.loader.resources[ "$Hime.png" ].texture );

書き換えてみたら動きました。
今度は PIXI.Texture 自体を生成してみましょう。どうやら PIXI.Texture にも from() メソッドがあるみたいです。
ローダを使わないので、コード全体を書いておきましょう。

const app = new PIXI.Application();
document.body.appendChild( app.view );

const texture = PIXI.Texture.from( "$Hime.png" );   // ①
const sprite = new PIXI.Sprite( texture );
app.stage.addChild( sprite );

表示されました。正直①が増えた分「回りくどくなっただけ」って感じですね。
ただ、さっき app.loader.resources[ "$Hime.png" ].texture のところで使ったように、 PIXI.Texture を変数に保持しておくほうが、PIXI.Sprite の状態で持っておくより便利そうです。
オブジェクトサイズが小さくできたり管理が楽な面もありそうですが、同じ画像を複数表示する場合にデータがひとつで済むメリットが大きいと思います。

PixiJS Examples SPRITE/Texture Swap をご覧ください。
一度表示したスプライトのテクスチャを入れ替えることで、同じスプライトのまま画像を切り替えています。
こういう場合、データ部分が分離していた方が入れ替えが容易です。
そうでないとファイル読み込みからやり直しか、スプライト自体を入れ替えるかする必要が出てきて面倒くさいです。

さて from() で生成しているなら new の方式はどうなんだというのが気になりますね。
PIXI.Texture のコンストラクタに渡す引数は PIXI.BaseTexture です。
なんかゴーディアンみたいですね(通じます?バイカンフーみたいですね?いやどーなの、言い換えとして成立してる?)
あっ!! マトリョーシカみたいですね!

画像表示用の PIXI.Sprite とデータ保持用の PIXI.Texture があるのはわかるとしても、PIXI.BaseTexture が別に必要なのか疑問です。
ここで疑問ですというのは、必要ないだろという意味ではなく、なんで必要なんだかわからない、という純疑問です。
プロパティもメソッドも PIXI.TexturePIXI.BaseTexture はほぼ一緒だし、次のコードを見ると生成のために「この段階踏む必要がある?」って気持ちがメラメラ湧いてきます。

const app = new PIXI.Application();
document.body.appendChild( app.view );

const baseTexture = new PIXI.BaseTexture( "$Hime.png" );    //← いる?
const texture = new PIXI.Texture( baseTexture );
const sprite = new PIXI.Sprite( texture );
app.stage.addChild( sprite );

PIXI.Texture はフレーミング(トリミング)機能を持っているんで、PIXI.BaseTexture に元データを持たせて、それをいくつかに分割して使う場合に PIXI.Texture を使うみたいな活用をします。
このあたりは、これから説明するスプライトシートがそれですね。

スプライトシート

特にインターネットでは細かいファイルをいくつも読み込むのは様々な負担が発生するので、できるだけまとめておきたい。
てなわけで複数(小さめの)の画像をまとめて一枚の画像として読み込んで、使うときに一部だけ表示する手法をスプライトシートなどと呼びます。テクスチャアトラスとかも呼ばれます。
CSSスプライトというとWeb系の諸兄にはおなじみかもしれません。
正確に言うとそれぞれの用語は微妙に違うんですが、どれも複数の画像を一枚の画像にまとめて読み込み一部だけ取り出して使う手法で、基本的な概念は共通です。

さてもうお気づきかと思いますが PixiJS はその仕組みを持っています。やさすぅぃーん!(本日2回目)

スプライトシートを作ろう

まずスプライトシート用の JSONデータというのが世の中にはありましてそのデータを作ります。
ただこのスプライトシートのJSON規格、どこに仕様書があるのかよくわかりません。「規格に沿って作る」とか言いつつ、規格がどこにあるんだかわかってないとゆー(笑)
知ってる方がいたら教えてください。なんかみんな「なんとなくこんな感じじゃないのー」みたいな雰囲気で実装しているような感じがします。

ただ規格がわからなくても大丈夫(…なのか?)なんと素晴らしいことに、多くの画像エディタでスプライトシートと設定JSONが作れます。
またスプライトシートを作る専用のツール(TexturePacker)なんかもあります。

個人的には Aseprite を愛用しているので、俺は Aseprite で行く!俺が!俺たちが Aseprite だ!!
ということで、Aseprite のメニューから[File]-[Export SpriteSheet] です。
フレームで分けているのを結合して1ファイルにしてくれたり、JSONファイルの出力設定もできます。
逆にすでに結合している画像は [File]-[Inport SpriteSheet] で分割できます。
細かい設定はAseprite の公式サイトのマニュアルとか有志のマニュアルサイトをご覧ください。

スプライトシートを読み込もう

さてさてさーて、スプライトシート画像と JSONファイルを作ったら、それを PixiJS に読み込みます。
"$Hime.json"ってのが、さっき Aseprite で作ったJSONファイルです。同じフォルダに1枚に結合したスプライトシート画像も置いてます。

const app = new PIXI.Application();
document.body.appendChild( app.view );

app.loader.add( "$Hime.json" ).load( setup );   // ①

function setup() {
  const sprite = PIXI.Sprite.from( "Hime_front0.png" );   // ②
  app.stage.addChild( sprite );
}

① スプライトシート用の JSON を読み込ませると、自動的に解析してくれます。
JSON の中には分割した画像につける仮想的なファイル名が設定してあり、読み込むと同時に解析とこのファイル名と画像の結びつけも行われます。
② その後は仮想ファイル名(ここでは "Hime_front0.png" )を普通のファイル名のように使うと、分割した画像を単一のファイルのように扱ってくれます。

スプライトシートのデータを持ってるオブジェクトは PIXI.Spritesheet なんですけど、上記コード中に出てきてませんね。
app.loader.resources[ "$Hime.json" ]ってやると参照できますが、単体のスプライトだと直接使う機会はないんじゃないかなぁ。
一応、次のように書き換えても動くのですが、無駄にコードが長くなるだけですよね。

function setup() {
  const texture = app.loader.resources[ "$Hime.json" ].textures[ "Hime_front0.png" ];
  const sprite = new PIXI.Sprite( texture );
  app.stage.addChild( sprite );
}

PIXI.SpritesheetbaseTextureプロパティを持っていて、それを分割したものを textures に持っているという作りです。
ここで切り替える画像は前出のサンプル Texture Swap のように複数のファイルを読み込んでもいいのですが、スプライトシート一枚にまとまっているならば確実に両方が読み込まれているわけですから、このような動的な変更を安心して行えます。

ちなみに『RPGツクールMZ』はスプライトシートの概念をもってますが、PixiJS の機能は使わずに独自に実装しているので、この辺の知識は特に役立ちません。JSON ファイルも使わず画像の大きさから、固定された比率で分割してます。
キャラクタの場合1キャラ3×4パターンで、さらに8キャラを4×2でまとめたスプライトシートが使われます。
ついでに言うとローダの類も独自実装しているので app.loader のあたりからの話も役に立ちません。
ふふふふふ『RPGツクールMZ』で活用しようと思って PixiJS 調べ始めたのに。無駄足!!

スプライトアニメーション

スプライトの画像を切り替える。これなんでしょう、そうアニメーションですね。
画像を切り替えることができるなら、そのタイミングを調整すればいいだけです。
タイミングに関しては既に前回 app.tickerを使って回転アニメーションをさせました。
とはいえ、ひとつぐらいならともかく沢山の画像を切り替えてアニメーションさせるのは面倒臭い。

そしてそろそろパターン見えましたね。そう PixiJS にはアニメ用の仕組みが用意されています。やさすぅいーん(本日3回目)
PIXI.AnimatedSprite がそれです。

要は PIXI.AnimatedSprite のコンストラクタに PIXI.Texture の配列を渡してやればいいのです。
その際前述のスプライトシートを使うのが基本かと思います。実際サンプルの PixiJS Examples SPRITE/Animated Sprite - Jet とかそうですね。
ゲーム的には PixiJS Examples SPRITE/Animated Sprite - Explosion の爆発のような、エフェクト系に使いやすい気はします。
他の手法としては、複雑な図形をdrawXxx系メソッドで描いてキャッシュしたやつを登録する、とかでしょうか。

アニメーション

ちょっと同じコード書くの(コピペですけど)飽きてきたので、ちょっとローディングの仕組みを変えてみます。

const app = new PIXI.Application();
document.body.appendChild( app.view );

app.loader.add( "hime", "$Hime.json" ).load( setup );   // ①

function setup( loader, resources ) {  // ②
  const textures = resources.hime.textures;  // ③
  const textureArray = Object.keys( textures ).map( e => textures[ e ] );  // ④
  const sprite = new PIXI.AnimatedSprite( textureArray );   // ⑤
  app.stage.addChild( sprite );
  sprite.animationSpeed = 0.05;
  sprite.play();
}

① 読み込みの add() 引数を増やしました。こうすると第一引数が別名として登録されます。
識別子として使えないパス(階層「/」があるとか)などに、適当に使いやすい名前をつけるわけです。
具体的には app.loader.resources[ "$Hime.json" ] と書いてたのを app.loader.resources.hime と書けます。
別に変数用意すりゃいいといえばいいんですが、標準的な方法があるなら使った方が見通しが良くなります。
② 読み込み終わりのハンドラに引数つけました。こうするとローダとリソースを渡してくれます。
app のような大域変数は極力使わないのがスマートなコーディングというもの、それを助ける機能です。
③ というわけで ① と ② を組み合わせて、 app.loader.resources[ "$Hime.json" ]resources.hime になりました。
ちなみにここでは loader引数の方は使ってません。

ローディングの仕組みの入れ替えはここまで。引き続き PIXI.AnimatedSprite の使い方の説明に入ります。
texturesオブジェクトを配列に map() してます。⑤ で使う引数が配列なのでオブジェクトのままでは渡せないのです。
PIXI.AnimatedSprite のコンストラクタに ④ で作った配列を渡して、インスタンスドミネーション!もとい、インスタンス生成です。
結局ステージに追加するところで app 使っちゃってますが(笑)、まぁサンプルなので。
そしてあとは見たまんまですが、速度を遅くしてアニメーション再生です。
姫さまが4方向にトコトコ歩くアニメーションします。可愛いですね。

PIXI.AnimatedSprite.fromFrames() を使えば、スプライトシートのフレーム名文字列を配列で渡して生成できます。
PIXI.AnimatedSprite.fromImages() を使えば、画像ファイルパスの文字列を配列で渡して生成できます。
この辺はWebサイトの装飾的なもののような、表示できてなくてもとりあえず前に進んでた方がいい系の処理では使いやすいですね。

PIXI.AnimatedSpriteインスタンスは gotoAndPlay() とか gotoAndStop()メソッドが使えて非常にムービークリップっぽい!往年の Flashクリエイターは昔取った杵柄をブルンブルン振るえます!!
といって Flash は今も『Adobe Animate』って製品として存在してますから現役ですけど!
割とふつーにアニメ作りに使われてますけど!!Flash は不滅ですけどー!!!

ただ、ゲームで使う場合インタラクティブにプレイヤーの入力に反応する必要があるので、実のところ PIXI.AnimatedSprite の活躍箇所は限られてくるようにも思います。
マップで揺れる背景の草木とかに使えばいいのかな。goto() メソッドでパターン制御するという手もありますが texture 入れ替えでやる方が扱いやすい気がします。

変動速アニメーション

ところで PIXI.AnimatedSprite は画像表示時間を1枚ごとに決めることができます。
方法としてはコンストラクタに渡す引数の形式を [ texture,... ] から [ { texture, duration },... ] に変えるだけ。
duration の方に表示時間を入れる。
非常に Flash のタイムラインっぽい。

PixiJS Examples SPRITE/Animated Sprite - Speed がこの方式です。
Aseprite が出力する JSON ファイルも duration に対応していて、ほぼサンプルコードのままでアニメーションが実行できます。

ただこれも、使い所はかなり限られる感じはします。

動画表示

mp4 とか動画も表示できて、音声も含めて再生されます。
しかし、ちょっと僕の興味があまりにないので調べるのやめました(笑)
PixiJS Examples SPRITE/Video、サンプル見てください!(シャクティ風に)

タイル

あと、スプライトは PIXI.TilingSprite とかあります。
四角い画像を敷き詰める感じのスプライトです。
サンプル的には PixiJS Examples SPRITE/Tiling Sprite です。
これは『RPGツクールMZ』でも、継承したクラスがマップの遠景とか戦闘時の背景に使われてます。
ただ意外ですが『RPGツクールMZ』の場合、キャラが移動するマップ自体に PIXI.TilingSprite は使われてません。
従来のマップの処理から変更するのが難しかったというか、あえて切り替えるメリットがなかったというか。

まとめ

柔軟な利用法が用意してありますが、逆にそれが「どれをどう使っていいかわからない」みたいな状態になってる感じあります。
この記事で、多少なりとも使い分けが理解できるようになれば幸いです。
といっても、僕自身がまだぜんぜん使えてませんが。

ファイル名を指定してしまえばローディングのことを意識せずに、雑に作っても割と問題なく動くのは凄いですね。
その上で、かなり細かくローディング制御できるのもありがたいところです。
紹介してませんが、もうちょっと別のタイミングで呼ばれるハンドラもあるみたいです。

ただ、スプライトシートのフォーマットが良くわからなかったのがモヤモヤします。
規格のあるページがわかりましたら、コメント等で教えていただけると幸いです。

Discussion

chiichii

QiitaにPixiJSの入門記事(初心者による超初心者のための~)を投稿した者です。
本記事を拝見しまして、分かりやすさに感激しました。

特にLoader周りは私も理解が不十分で自身の記事内で解説ができておらず、
これはぜひ入門記事を見た方々にも読んでもらわねばと思い、
勝手に記事にリンクを掲載させていただきました。
問題がありましたら削除いたします(事後承諾すみません…)

素晴らしい記事をありがとうございました。

とんび@鳶嶋工房とんび@鳶嶋工房

コメントありがとうございます。
公開してあるページはリンクされてナンボです!リンクもありがとうございます。