2011/02/03 とりあえず完成。
(日記はここで終わっている)
ではなくて…。
基本的にデータの垂れ流しなので特筆すべきことはないのですが、興味を持たれる奇特な方もいらっしゃるかもしれないので
いつも通り徒然なるままに制作過程を書いてみたいと思います。
GB と書いてあるのは全て Gameboy Color のことです。
これまでのあらすじ(転載)。
ファミコン版は
Little Limit さん。いつもフォントを使わせて頂いています。
ver 2.5 で MMC3 使用の 30fps を実現!
髪や鎖、花吹雪、はごろもフーズなどの動きがなめらかで心地よいですね。
データ圧縮と展開に並々ならぬご苦労をされたようです。おつかれさまでした。
自分ならどうするか、ということを暫し考えていたのですが MMC5 使えるなら使いたいところですね…。←意志が弱い。
どこかで今回の技術解説記事を読めると良いなぁ。
・皮 算 用
一通り見たところで、おもむろに AviUtl で動画を連番 BMP 画像に分解。
(オリジナル)動画解像度 512x384 / 動画枚数 6573 枚 / 30fps / 再生時間 3分39秒
GB の ROM は最大容量 8MB を使うとして…。
GB の解像度は 160x144 です。元動画の解像度が 4:3 なので、変換すると少し縦長になります。
GB の色分解能は 8x8ドット中(=1タイル) 4 色なので、元画像から白黒 4 階調に変換します。
影絵だからって白黒2色と思ったら大間違い。オリジナルは綺麗なグレースケールなのです。
4色 = 2bit なので、GB の 1 画面に必要な容量は次のようになります。
160 * 144 * 2 = 46080 bit = 5760byte
|
で、これに動画枚数 6573 枚をかけると
5760 * 6573 = 37860480byte = 約 36MB
|
約 36MB 必要ということになります。
GB のスペックでも 36MB なのが PC 上では MP4 エンコードで、音声を付けても 15.9MB になってしまうんですね。やるな。
さて、最大容量が 8MB なのでこのままでは入りませんし、音を入れる余地も考えないといけません。
というわけで、先に音声の容量を確定させてしまうことにします。
というのも、音声の方が再生に要する手間(VRAM の書き込み禁止時間など)が圧倒的に少ないので
容量をはじき出すのに悩むポイントがあまり無いからです。
容量が厳しかったり CPU リソースが逼迫していれば容量を減らして音質を悪くすればよいし、余裕があればリッチにできるということです。
音声再生は 波形メモリ音源を使った 4bitPCM ということにします。
再生レートは 4096Hz だと音質が悪すぎだったので、8192Hz でいってみましょう。
4bit * 8192Hz * 3分39秒 = 7176192 bit = 897024byte
|
約 1MB と見積もります。8192Hz というのは 1秒間に 8192 データを再生するのですが
GB の波形メモリ音源は 32 データ単位で書き込みと再生が可能なので、実質 8192 / 32 = 256
つまり 1/256 秒単位で音声データ 32データ=16byte を書き込みに行けば良いということです。
動画が 30fps とすると、画面を 1 枚表示している間に約 8〜9 回音声処理が入る、と。
音声のデータを約 1MB と設定したので、残り ROM 容量は 7MB。
36MB の画像データは 1/6 程度に圧縮すればなんとかなりそうです。
ちなみにオリジナルの音声は WAV フォーマットに直すと 36.8MB もあります。
6MB 動画データ + 1MB 音声データ = 7MB
|
・動 画 圧 縮
1/6 に圧縮すると決めたわけですが、他に考えるべきことがあります。
これまで 30fpsを前提として計算してきましたが、たとえばこれを 20fps にしてしまえば、
6573 枚の動画枚数も 2/3、つまり 4382 枚になるわけで、なめらかさを犠牲にするとはいえ、圧縮の条件も緩くなります。
そもそも GB の CPU パワーで 30fps 動画は可能なのでしょうか。
GB の CRTC は当然 60fps で動いており、各フレーム V-Blank および H-Blank 中にしか VRAM にアクセスできません。
30fps は GB の CRTC が 2 回画面を描画する間 (2/60frame) に 次の 1 画面分のデータを転送し終わらなければなりません。
アクセス違反を犯さない最も安全かつ高速な転送方法は DMA 転送ですが、
これだと 1画面 = 5760byte を転送するには 2.5/60frame かかってしまいます。
20fps ならなんとか間に合いそうですが 30fps は無理。
CPU 転送だともっと無理。
しかしこれは画像を 160x144 の 1 枚の画像データとして考えた場合の話。
ゲーム機のハードに即して考えればやりようはあります。
GB は 8x8 ドットのタイル領域と 20x18 のマップ領域(ファミコンで言うネームテーブル)を持っています。
8x8 を 20x18 個並べて 160x144 を構成しているわけです。
以下は昔の RPG などでよくあるマップですが、この 1 画面を構成するのに必要な情報量はどれくらいでしょうか。
海と陸と森のキャラデータ 3 個と、どこにそれを配置してあるかがわかれば良さそうですね。
もし画面が全部海だったらもっとデータは削減できそうです。
海・陸・森のキャラデータは必要ですが、配置データを別に持つことで 1 画面全体の画像とは比較にならないほど小さくて済みます。
同じように、1 枚の画像データも「どの 8x8 キャラデータと同じか」という情報を持っていればデータ量はかなり減らせます。
あらためて Bad Apple !! を見ると、白で塗りつぶされた部分と
黒で塗りつぶされた部分が多いですね。
RPG のマップのように 8x8 というマス目を意識すると、
黒と白の境界=輪郭の部分以外は■と□でカバーできそうです。
ということで、削減の方針は出来たので、 30fps が可能かどうかはさておき(なにしろやってみるまで分からない)
データを圧縮してみることにします。
36MB → 6MB つまり 1/6 にしたいので、6 フレーム分を一度に調査します。
6 フレーム分の画像を読み込んで、
8x8 ドット単位で「全く同じ」箇所を探します。
同じ 8x8 キャラクタがあればあるほど使い回しが効くことになります。
そして 6 フレーム毎に、使い回す 8x8 のキャラクタセットを総入れ替えするというわけです。
RPG で言うと、洞窟と城でキャラデータのセットが違うようなものです。
全体では 20 * 18タイル * 6フレームで 2160 タイルです。
大抵■と□におさまりますが、それ以外にも、連続したフレーム故に全く動いていない(変更のない)部分もあり、結構削減できます。
収まらない部分、つまり
「他では代用できない 8x8 ドット」についてどれくらいあるかを調査します。
これを便宜上「ユニークタイル」と呼ぶことにします。
以上調査結果。6 フレームずつ計測しているので横軸は 1/6 になっています。
赤線を引いてあるのはユニークタイル 256 個のラインです。
ユニークタイルを 256 個選別し、これを 6 フレームで使い回すことにします。256 個の数字の根拠は後述。
1 画面 20x18 タイルなので本来なら 360 タイルで 1 画面を構成するのですが、今回はそれ以下ということで果たしてどうなることか。
とりあえず、256 以下が安全でそれより上が危険水域。
結構 256 個超えが多いですね。ワーストケース 900 近いんですけど…。
さて、256 個以内だった 6 フレーム分はそのまま圧縮完了!として、256 個を超えた部分はどうするか。
「使い回しのきかない 8x8 ドット」のキャラクタが 256 個以上あるわけですが、そのままではどうにもなりません。
答えは
「似た 8x8 ドットキャラクタを探して置き換える」です。
手順は次の通りです。
1)あるタイルと自分自身以外の全タイルを比較して類似率を求める。
2)最も似ているタイルの類似率が閾値以内であれば4)へ
3)最も似ているタイルの類似率が閾値外であれば、置き換え不能なユニークタイルとして新規登録。6)へ
4)発見した類似タイルが既にユニークタイルとして登録されていれば、そこに自分自身を仲間として登録する。
5)発見した類似タイルが未だユニークタイルとして登録されていなければ、自分と類似タイルをペアでユニークタイルとして新規登録。
6)全てのタイルを調査し、最終的にユニークタイルが 256個に収まらなければ閾値を一つあげて1)へ
似ているタイル同士でクラスタを作るような感じです。クラスタの総数が最終的に 256 個以内に収まっていれば良し。
類似率の算出方法は以下の通り。閾値もこれを基準に決めます。
調査方法は以下の通り。
完全に同じ 8x8 ドットの場合が 0 で、全く違うと 192 になります。閾値は 0 → 192 と変化させていきます。
この類似率算出方法ですが、ぱっと見でおかしなことになると気づかれるかもしれません。
実はいくつか改善方法を導入してみたのですが、あまりにも重かったのでやめました。
さらに、発見した類似タイル群(各クラスタ)の中から代表タイルを選別しなければならないのですが、
ここで手抜きをして、[0]番のタイル(最初にそのクラスタに登録したタイル)を無条件で選んでいます。
動画の中でノイズのようなゴミが現れることがありますが、コレが原因です。
人気投票をやってからソートして、人気の高い順に類似検索すればマシになるのか・・・な・・・。
・必 死 転 送
動画のコメントにも書きましたが、走査線から逃げながらいかにデータを転送するかがキモであり、
それができなければ fps 数を落とすか解像度を落とすかしかないわけです。
で、GB 版では 6フレームで使うベースとなるタイルを 256 個に制限しています。
もう少し詳しく説明すると…
まず、GB は BG 用として 256 個のタイルを 2 バンク使えます。このことは 30fps 実現の上で大きなアドバンテージになります。
もし 1 バンクしかなければ、表示直前の V-Blannk 期間にすべてのタイルデータを転送しなければならず、
それは実質 60fps と変わらないことになるからです。
しかし 2 バンクあるので、ある画面を表示しつつ、裏バンクに 2 フレームかけて転送しても表示中の画面には影響ありません。
転送が終わったところで表裏を切り替えればチラつかずにきれいなアニメーションになるわけです(いわゆるページフリップ)。
ユニークタイル数を 256 個に制限した主な理由はここにあります。
1 画面を構成するタイルは表裏両方同時にも使える(つまり 256*2=512 個)のですがページフリップのため 256 個制限というわけです。
さらに転送スピードとのバランスも一因です。
タイルデータは 8x8 ドットデータ(=16byte) * 256個 = 4096byte になります。
6フレーム分を 256 個でまかなうので、4096byte を最長 6 フレームかけて転送する余裕があることになります。
上記の通り、裏バンクに転送するので表示中のキャラデータには影響がありません。
ちなみにこの場合の 6 フレームというのは論理フレームなので、60fps 基準で言うと 6*2/60 frame といういことです。
これに加えて上にも書いたようにどのタイルをどこに使うか、というマップデータも VRAM に転送しなければなりません。
これは 1 フレーム(2/60frame)ずつ転送しないと 30fps が実現できません。データ量は 20x18=360byte。
その他、裏・表どちらのバンクを使うか、というタイルアトリビュート情報も書き換える必要があります。これも 20x18=360byte。
4096byte のタイルデータは 6 フレームかけて良いので 4 フレームに分け、1 フレームにつき 1024byte ずつ転送することにします。
結局、1フレームでの最大転送量は 1024byte + 360byte + 360 byte ということになります。
割と少なめに見えるかもしれませんが、実は GB では、マップデータの 360byte すら V-Blank 中に転送しきれません。
ですが幸いなことにマップ領域も 2 つ持っているので、こちらも交互に使っていくことにします。
実際の所、転送は結構忙しく、以下の図のようになります。
V-Blank 中に転送が収まりきらないので、H-Blank にも転送を行います。
上で安全最速は HDMA による転送だと書きましたが、HDMA中は転送元の ROM バンクを切り替えてはいけないというルールがあります。
HDMA は H-Blank にしか動かないので走査線が走っている期間は CPU が こっそり ROM バンクを操作しても大丈夫そうに思えたのですが、
実際にやってみるとタイルデータが乱れまくりでとても見苦しいものでした。
原因は PCM 用の割り込み処理が HDMA 転送中断中に PCM データのあるバンクに切り替え、
それを動画バンクに戻し終わる前に HDMA が再発してしまうことにあるようでした。
どうやら、H-Blank 期間が来ると CPU の割り込み状態などおかまいなしに PCM データ用に切り替わった ROM バンクから転送を再開しようとするようです。
ということでタイルデータ・マップデータ・アトリビュートまで含めてすべて CPU による転送になったのでした。
VRAM にアクセスするコードは、ループにせず全展開 1024byte + 360byte + 360byte 転送になっています。
CRTC とバッティングしないよう走査線を避けながら書き込むため結構遅く、H-Blank で 7〜8byte、V-Blank で 200byte 程度しか書き込みできません。
さらにここに音源操作のためのタイマー割り込みが 1/256 秒毎にかかるため、全体では結構負荷がかかります。
上で書いたような明らかな代表タイル選定ミス以外にも画面にゴミが載ることがありますが、これはほとんど音源割り込みによる影響です。
タイマ割り込みの後でも Blank を待ってみるべきかなぁ。
プログラム自体はシンプルなので色々改造してみても面白いかも。
(チャート図を書いて流れを追わないと激しく混乱しますが)
まだまだ最適化の余地はありますし、最適化すればタイミングが原因の不具合が発生する確率も下がると思います。
マップの転送で 1byte 毎に範囲チェックしているのは無駄の極み…とか。
エミュレータで VRAM の様子を眺めていると中々笑えます。