どうも、Qiita初投稿の因幡です。
Advent Calendarなる企画にも初参加です。
たまたまRPGツクールMVのプログラマ枠が1つ空いていたので、最近作ったプラグインの話でもちょっとしてみようかなと思います。
SpriteStudioPlayerForRPGMV
今回作成したプラグインは、RPGツクールMV上(より正確にはPixi.js上)でSpriteStudioで作成したアニメーションを再生できるというものです。
作成したプラグインは、以下の場所で公開しています。PRは随時受付中。
https://github.com/InabaByakko/SSPlayerForRPGMV
ちなみに、このデモに使用しているアニメーションデータは、別制作のゲーム「WILD DRIVE」より拝借しています。こちらはRPGツクールXPにて製作中ですが、残念なことにスプライトの回転や拡縮処理が非常に重いため、作成したアニメーションはいったん連番画像にレンダリングしたうえで、ツクール標準のアニメーションセルデータに落とし込んでいます。
SpriteStudioってなんぞや?
SpriteStudioとは、株式会社ウェブテクノロジから発売されている、「超汎用」2Dスプライトアニメーションデータ作成ツールです。
同様のツールとして、SpineやSpriter、GMCMTなどがありますが、これらと比較したSpriteStudioの特徴としては
- 日本の企業が開発、販売しており、日本語での手厚いサポートが望める
- 個人や同人サークル、販売規模が小さい中小企業向けに、全機能が無料で利用できるインディーライセンスも提供されている
といったものがあります。(あとは公式Twitterアカウントがほどよくユルい、とか。)
特にインディーライセンスの存在はありがたく、普通なら1ライセンス10万近く払わないと使えない商用ツールが(半年ごとに更新の必要があるとはいえ)無料で利用できるのは、お金のない零細同人サークルには大変うれしいです。
こういう類いのツールに興味はあるけど、お金ないからなあ、という方にこそ使っていただきたいツールです。申し込みは簡単なので、ぜひどうぞ。(ダイレクトマーケティング)
ツクールMV(というかPixi.js)の描画形態について
ツクールMVの中身に触れたことのある方ならご存じの通り、描画エンジンにはPixi.jsが使われています。
2Dグラフィックスに特化し、簡素に記述することが出来かつHTML5CanvasとWebGLをうまーいこと組み合わせてくれることでハイパフォーマンスな描画処理を実現しています。
Pixi.jsに初めて触れる方は、まずこちらで簡単なチュートリアルをどうぞ。
http://studypixi.info/
ツクールに搭載されているバージョンは2系で、最新バージョンは3系なので実情と合わないところが多々あるかと思いますが、雰囲気はつかめるでしょう。たぶん。
通常、Pixi.jsで画像を画面に描画するには、以下のステップを踏みます。
- レンダラーの作成、DOMに関連づけ
- Stageを作成
- 画像URLからTextureを作成
- TextureからSpriteを作成
- StageにaddChildする
- 描画される
さらに、作成したSpriteをアニメーションさせるためには、アニメーション関数を用意してrequestAnimFrame()
に渡して定期実行してもらいます。
このあたりのイメージについては、こちらの記事の図を見ていただけるとわかりやすいかなあと。
ところが、ツクールMV上で利用する場合はさらに洗練されていて、以下の点についてはツクールのコアスクリプトでうまいことフォローされています。
- レンダラーの作成は起動時にGraphicsクラスが自動でやってくれる。DOMへの追加も気にする必要はなし。
- 各SceneクラスはStageクラスの派生。ここに作成したSpriteをどんどこ
addChild
していけばあとの描画はおまかせ。 - アニメーションについても、
addChild
したクラスがupdate
メソッドを実装していれば、毎フレーム自動で呼び出してくれる。
よって、素のPixi.jsを使うよりも、ツクールMVではさらに扱いやすくなっている、というわけです。
SpriteStudioアニメーションの実装方法
SpriteStudioPlayerForRPGMVは、ウェブテクノロジ社公式で提供されていた、HTML5用プレイヤーのソースを参考にして作られています。(ほとんどの主要ゲームエンジン用プレイヤーが公式で用意されているのも、SpriteStudioのいいところの一つですね)
そこからちょこちょこと変更を加えているため、そのあたりの解説をしてみようと思います。
ソースコードを見ながら読んでね。
パーツの描画手法の変更
元々のHTML5Playerでは、アニメーションの描画は以下の要領で実現されていました。
- SpriteStudioで作成し、エクスポートしたssaxファイルを専用のコンバータでJSONデータに変換したものを用意する。
- JSONファイルをブラウザで読み込み、そこからImageListオブジェクトを作成。必要な画像URLはここから取得できる。
- また同時にJSONファイルからSsAnimationオブジェクトを作成。パーツの管理や、実際の描画処理を司る。
- そのSsAnimationオブジェクトから、SsSpriteオブジェクトを作成。アニメーション全体の座標やスケーリングなどの情報を保持している。
- SsSpriteのdrawメソッドを定期実行し、キャンバスコンテキストと現在時刻を渡すと、現在フレーム更新とパーツの描画が行われる(パーツ描画はSsAnimationクラスに委譲)。パーツはキャンバス上に直接描画される。
これの構造を極力維持した上で、ツクールMVでの利用の実情に合うように実装したらこうなりました。
- SsSpriteクラスはrpg_core.jsのSpriteクラスを継承。直接SceneにaddChildできるようにした。各種プロパティも使えます。
- SsSpriteクラスのdrawメソッドはupdateにリネーム。毎フレーム自動で呼び出されるようになったほか、シグネチャも変更し、キャンバスコンテキストは親SceneにaddChildすることでレンダラーが描画してくれるため渡す必要がなくなり、現在時間もカレントフレーム管理がクラス内部で行われるように変更したので渡す必要がなくなった。
- SsAnimationクラスのdrawFuncメソッドもgetPartSpritesにリネーム。キャンバスコンテキストを渡してそこにパーツを直接描画する振る舞いから、パーツSpriteの集合を作成して配列として返す振る舞いに変え、呼び出し元のSsSprite.updateでは自身のchildrenを、これで作成されたSprite群に入れ替えるという方式で実現した。
という感じで、SsSpriteオブジェクトを作成したら、現在のSceneオブジェクト(ないしその子)にaddChildするだけでよしなに使えるようにしました。
欠点としては、毎フレームパーツSpriteの使い捨てを繰り返すので、使用メモリの増大が問題になり得る点でしょうか……。ひょっとしたら、GCがうまい具合に片付けてくれているのかもしれませんし、そうでないのかも知れません。教えてJSのエライ人。
頂点変形への対応
元となったHTML5Playerには、頂点変形の描画には対応していませんでした。
メッシュ変形に対応していなかったからかな、と思ったのですが、調べてみたところ一応Canvasだけで頂点変形を描画する手法はなくはないみたいです。すごい回りくどい方法をとっていますが。
http://akibahideki.com/blog/html5-canvas-1/canvas.html
しかしPixi.jsは違います!このような頂点を引っ張ってうねうね動かすためのクラスがすでに用意されています。
その名は"Strip"。ストリップショーじゃありません。
バージョン3では、Meshという名前にリネームされました。公式のドキュメントではそちらをご覧ください。
Stripクラスを使用することで、3Dのポリゴン描画のように、3点ないし4点の座標を直接指定し、そこにテクスチャのUV座標をマッピングすることで、テクスチャが貼り付けられた三角形ないし四角形が描画できる、という寸法です。
これを使えば、頂点変形付きのデータの場合はSpriteではなくStripを使用し、4点の座標に変形分の座標を加算すれば、対応完了!というわけです。
ただ、実装に当たってがっつりはまったのが「原点」をどうするか、という問題。
各パーツにはそれぞれ「原点」が設定されており、そこを座標の基準としたり、回転の軸となったりします。
Spriteクラスにはanchor
というプロパティがあり、テクスチャ中のどの位置を原点にするかを簡単に設定できます。(範囲は0~1の浮動小数点数。ピクセル数ではないのでお間違えなく。)
しかし、Stripクラスには、回転軸となるpibot
プロパティはあるものの、anchor
に相当するプロパティはありません。原点を任意に設定できないのです。
馬鹿正直に左上の座標を(0, 0)として、あとで原点を調整しようとしてもうまくいきません。
さて、どうするか。
ここは逆転の発想をして「パーツ原点を(0, 0)に合わせてしまう」という手法をとります。
つまり、各点の座標を、原点の座標分だけ引いてしまうのです。
こうすることで、position
プロパティで移動させれば原点を中心に座標が決定しますし、pibot
を変更しなくても原点中心に画像が回転します。
// テクスチャX座標
var sx = partData[iSouX];
// テクスチャY座標
var sy = partData[iSouY];
// テクスチャ切り出し幅
var sw = partData[iSouW];
// テクスチャ切り出し高さ
var sh = partData[iSouH];
// 全体原点からの差分座標X
var dx = partData[iDstX];
// 全体原点からの差分座標Y
var dy = partData[iDstY];
// 回転角
var dang = partData[iDstAngle];
// Xスケール
var scaleX = partData[iDstScaleX];
// Yスケール
var scaleY = partData[iDstScaleY];
// 原点X
var ox = (partDataLen > iOrgX) ? partData[iOrgX] : 0;
// 原点Y
var oy = (partDataLen > iOrgY) ? partData[iOrgY] : 0;
// 左右反転(-1で反転)
var fh = (partDataLen > iFlipH) ? (partData[iFlipH] != 0 ? -1 : 1)
: (1);
// 上下反転(-1で反転)
var fv = (partDataLen > iFlipV) ? (partData[iFlipV] != 0 ? -1 : 1)
: (1);
// 頂点変形座標
var translate = [
(partDataLen > iVertULX) ? partData[iVertULX] : 0,
(partDataLen > iVertULY) ? partData[iVertULY] : 0,
(partDataLen > iVertURX) ? partData[iVertURX] : 0,
(partDataLen > iVertURY) ? partData[iVertURY] : 0,
(partDataLen > iVertDLX) ? partData[iVertDLX] : 0,
(partDataLen > iVertDLY) ? partData[iVertDLY] : 0,
(partDataLen > iVertDRX) ? partData[iVertDRX] : 0,
(partDataLen > iVertDRY) ? partData[iVertDRY] : 0 ];
var texture = new PIXI.Texture(basetexture);
var tex_w = texture.width;
var tex_h = texture.height;
var spr_part = new PIXI.Strip(texture);
// 原点を(0,0)とし、そこからの各点の差分+頂点変形分を座標とする
var verts = new Float32Array([ (-ox + translate[0]),
(-oy + translate[1]), (sw - ox + translate[2]),
(-oy + translate[3]), (-ox + translate[4]),
(sh - oy + translate[5]), (sw - ox + translate[6]),
(sh - oy + translate[7]) ]);
// 反転を考慮したテクスチャ座標の設定
var uvs = new Float32Array([
(sx + (fh == -1 ? sw : 0)) / tex_w, (sy + (fv == -1 ? sh : 0)) / tex_h,
(sx + (fh == -1 ? 0 : sw)) / tex_w, (sy + (fv == -1 ? sh : 0)) / tex_h,
(sx + (fh == -1 ? sw : 0)) / tex_w, (sy + (fv == -1 ? 0 : sh)) / tex_h,
(sx + (fh == -1 ? 0 : sw)) / tex_w, (sy + (fv == -1 ? 0 : sh)) / tex_h
]);
spr_part.vertices = verts;
spr_part.uvs = uvs;
spr_part.dirty = true;
spr_part.scale = new PIXI.Point(scaleX, scaleY);
spr_part.position = new PIXI.Point(dx, dy);
spr_part.rotation = -dang;
めでたしめでたし。
まとめ
今回ありがたいことにベータテスターとして発売前の日本語版でツクツクさせていただいていたわけですが、やはり新作を触るのは楽しい。そして、まだ誰もやってないことをいち早くやるのは楽しい。大変だけどやりがいがありますね。
SpriteStudioにはいつもお世話になっているので、今回ちょっとした恩返しが出来たかなあ、と思っています。
JavaScriptは、Web2.0と呼ばれるより前の時代にちょこっとだけ触って、そこからだいぶご無沙汰だったのですが、ここまで進化していたのですね-。もうあのときのようなもっさり言語じゃないのね。しゅごい。
まだまだこのプラグインは発展途上です。再生できないアニメーションもたくさんあると思います。そのときはぜひお伝えください。可能な限り対処させていただきます。もちろんプルリクエストを送っていただいても結構です。大変助かります。
ではでは、ここまでおつきあいくださり、ありがとうございました。