Edited at
DxLibDay 23

画像前処理で描画速度とメンテナンス性を両立する


はじめに

この記事はDxLib Advent Calendar 2018 23日目の記事です。

12/23 : 分かりにくい部分の訂正

<< 19日目 | DXライブラリ+nuklearでGUIを実現する || 24日目 | 五芒星つくった>>


進捗確認

年末ですが、これを読んでいるみなさん、進捗どうですか?年内に完成させるとか言ってしまった(DXライブラリ製の)任意スクロール弾幕STGは、ラストダンジョンにイベントを置いている最中で、絵もあと2枚できてないしEXダンジョンも未着手でテストプレイも終わって無くてウルトラモードの調整も未着手で立ち絵の表情差分もまだ作っていません。それと修論1も終わっていません。 何でAdvent Calendar書いてるの?


DirectXとDerivationGraph()の仕様の復習

同人の弾幕STGは圧倒的にc/c++で作られたものが多いです。これは、DirectXでは、同じテクスチャにいろいろな弾幕の画像がまとめてあると、複数の弾の描画を一度のDrawPrimitiveUP()2でやってくれるため高速化します。DXライブラリでは、同じ画像(DerivationGraph()で同じ親ハンドルから得られるものも含む)を連続で描画すると、自動でこの最適化をやってくれるようになっています。(因みにDrawPrimitiveUP()を呼んだ回数はGetDrawCallCount()で取得可能)

ちなみに、5年くらい前にjavaで弾幕STGを作ろうとしたことがありますが、300発くらいで処理落ちを始めて断念しました。

一般に、弾幕STGを作るためには200~2000発の画像を60fpsで描画する必要があります。

今までは弾幕の話をしてましたが、RPGツクール/ウディタのようなRPG系の2Dゲームでも同じ問題が起こり得ます。

典型的な画面サイズで640x480、マップチップ32x32ピクセルが3レイヤ、とすると一度に表示するマップチップの最大数は

(640/32)×(480/32)×3=20×15×3でなんと900枚です。大往生の二周目(210発)より多いね!

要するに:一枚の画像ファイルにまとまっていると速く描画できる


高速化案:画像を一つにまとめる

作ってるゲームがRPG系のマップ上で1000発くらい弾が飛んでくるゲームなのですが、いくらDXライブラリであっても適当に書くと上記の原因で処理落ちしてゲームになりません。RPG系のマップは、例えば大きな木の後ろにキャラが入ると隠れるなどの挙動があるせいで画像の描画順にも気をつけないといけないので、ますます難易度が上がります。そこで、使う全部の画像を2048x2048等の大きな.pngファイル3にまとめてしまえばいいのではないか?との結論にたどり着きます


画像を一つにまとめた場合の弊害

理論上は最速で描画できます。機械には優しいですが、メンテナンス性が極端に悪いので毛髪が瞬く間に禿げ上がります。

・ship.pngという画像が巨大画像のどの位置に格納されているかを手打ちでどこかに持っておく必要がある

・この画像ちょっと暗いな、色味補正しよう -> 色味補正が全体に適用されて死んだ!

・大き目の画像で背景が透明の部分に、未使用領域と勘違いして別の画像を配置してしまう

・敵の画像の種類を増やしたい -> 画像の隣接した領域が空いているとは限らない -> 敵画像の位置が散り散りになってしまう


前処理で解決

もう何がしたいか分かった聡明な方もいると思います。全画像を例えばdata/image直下等に置いて、ゲームを起動するたびにプログラムから画像を一つにまとめるような仕組みを作ります4こうすれば、敵画像はdata/image/enemyにまとめるだとか、管理はすごく楽です。DXライブラリ的には、まずc++標準関数でdata/image直下の.pngファイルを全部列挙し5、ソフトイメージ系の関数で画像を合成して書き出します6。個々の画像の埋め込み位置と元.pngの最終更新日時(FileRead_getInfo()や標準関数で取得する)をまとめて.jsonにでも書き出しておいておけば、ゲームコードからは


game.cpp

const auto healItem = graph_maker::getHandle("システム/回復アイテム");


みたいに簡潔な形に呼び出せます。グラフィックハンドルの分割とか破棄はゲームエンジンが開始/終了時にまとめてやるので、ハンドルの所有権の管理に手を煩わせることもありません。自分の環境で、この合成には5秒くらいかかるので、全.pngの最終更新日時を取ってきて更新が不要ならサボる(cmakeみたいに)機能をつけるとベターです。ちなみに、DirectXの仕様で、画像描画時に周囲の画像が1px未満だけにじみ出ることがあるので(DXライブラリ側ではどうにもできない)画像合成のときには1ピクセルずつ透明色のパディングを挟んでおくと安心です。


余談:この議論の例外

DXライブラリで(大規模な)ノベルゲームを作る人はどれだけいるか知りませんが、(ノベルゲームエンジンを作ってた人はいましたね)

例えばノベルゲームのイベントCGが基本50枚、差分含め200枚(800x600サイズ)とかだと一度しか使わないイベントのCGが起動時から終了までメモリを使用し続けます。こういう用途では、むしろ描画速度は一切必要ないので、合成せずに普通に持っておいた方がいいです。でもゲームコードではさっきの使用例と統一的なインターフェースになるようにしたいですね!





  1. 非情報系 



  2. DXライブラリの関数ではなくDirectXの関数 



  3. だからといって16384x16384とかの巨大サイズだとグラボが対応してない可能性があるので、2048くらいで数枚の画像にわけるのが無難ではないでしょうか 



  4. DXアーカイブとの兼ね合いがあるので、開発中のみ画像合成し、リリース前にアーカイブにしてから、完成品では画像合成によってできた大きい画像のみ同梱するようにする 



  5. 開発中のみの機能で、data/imageはアーカイブになってないことが保証されるのでc++標準関数で問題ない 



  6. Dxlib 3.19f現在、ソフトイメージ系の関数で画像をただ読み込んで書き出すだけでも乗算済みαの影響を受けてしまう(読み込みと書き出しの画像が変化してしまう)罠があるので、ソフトイメージ使用前には必ず乗算済みαをオフにしておくこと