基礎知識
戦闘関係
スーパーファミコンにおける画像処理機能のひとつ、モザイク処理について説明する。
「ライブ・ア・ライブ」では、戦闘で「テレポート」コマンドを使った時に背景にモザイクが徐々にかかり、テレポートで飛んだ後はモザイクが徐々に取れる場面や、回想シーンの前後などで使用されている。
モザイク処理はI/Oポートアドレス$00:2106(MOSAIC・モザイク処理)に値を書き込むことで行われる。
| Bit | 内容 |
|---|---|
| Bit0 | BG1モザイク有効化フラグ (0=無効, 1=有効) |
| Bit1 | BG2モザイク有効化フラグ (0=無効, 1=有効) |
| Bit2 | BG3モザイク有効化フラグ (0=無効, 1=有効) |
| Bit3 | BG4モザイク有効化フラグ (0=無効, 1=有効) |
| Bit4~7 | モザイクサイズ (%0000 = 1×1ピクセル ~ %1111 = 16×16ピクセル) |
以上から、モザイクをかけることが可能なのはBG1~BG4、つまり背景だけということがわかる。背景別に設定することが可能。
スプライトにはモザイク処理ができない。
テレポートの時、BG2に背景として設定される敵キャラにはモザイクがかかるが、スプライトの味方キャラにはモザイクがかかっていないのは、演出的な目的に加えてスプライトにモザイクをかけることができないという事情があるからであろう。
回想シーン前後も、スプライト部分のキャラにはモザイクがかからないのが見ていてもわかる。近未来編冒頭(公園のシーン)あたりがわかりやすい。
(という仕様のため、ざきの一部の技でざきにモザイクがかかるのは、モザイクがかかったようなスプライトを用意して使っているだけである)
Bit4~7のモザイクサイズは%0000~%1111の16段階が設定可能で、%0000は実質モザイクなし(1×1ピクセル)だから、%0001(2×2ピクセル)~%1111(16×16ピクセル)まで設定できることになる。
画面の描画中でなければ、画面左上を原点として、モザイクサイズで画面を分割し、分割した正方形の左上ピクセル位置の色で正方形を塗りつぶす、という方法でモザイクを作る。
画面描画中だと、描画している箇所が原点扱いになってモザイク処理を行う。
本作の場合、基本的には画面の描画終了時に画面全体にモザイク処理をしている(はず)。
フレーム毎にモザイクサイズを変更することでモザイクの度合いを変化させることが可能。
例としていくつか処理を紹介する。
まず、戦闘開始時に、[$00:2106]に$00を書き込んでモザイク処理なしの設定をしておく。
$C1/F345 LDA #$00 ;Aに$00をロード $C1/F347 STA $2106 [$00:2106] ;Aを[$00:2106]に書き込み
「テレポート」コマンドを使うと各種処理が入るが、モザイク処理含め画像関係の処理は$C1/7046~$C1/70B9で行っている。
上に書いた通り、画面描画が終わったタイミング、つまりVBlankに入ったタイミングでモザイク処理を入れる。
テレポート使用時の画像処理のタイミングは[$00:0360]に入る値で調整をしているようなのだが(要検証)、まず、モザイク処理をかける1フレーム前で[$00:0360]に$00を書き込む。
$C1/7098 STZ $60 [$00:0360] ;[$00:0360]に$00を書き込み $C1/709A LDA #$02 ;Aに$02をロード $C1/709C JSR $27D7 [$C1:27D7] ;[$C1:27D7]へジャンプ
後はフレーム毎に以下でループ処理になる。
$C1/709F LDA $60 [$00:0360] ;Aに[$00:0360]をロード $C1/70A1 ORA #$03 ;Aと$03で論理和 $C1/70A3 STA $2106 [$00:2106] ;Aを[$00:2106]に書き込み $C1/70A6 LDA $60 [$00:0360] ;Aに[$00:0360]をロード $C1/70A8 CLC ;キャリーフラグクリア $C1/70A9 ADC #$10 ;A + $10 $C1/70AB STA $60 [$00:0360] ;Aを[$00:0360]に書き込み $C1/70AD BNE $EB [$709A] ;ゼロフラグが立っていないとき[$709A]分岐 ;ゼロフラグON(モザイク処理終了) $C1/70AF LDA #$20 ;Aに$20をロード $C1/70B1 JSR $27D7 [$C1:27D7] ;[$C1:27D7]へジャンプ
$C1/70A3で、[$00:2106]に値を書き込む。
上の処理からして、$03, $13, $23, ……, $F3と順に増加していくはずだ。
Bit0~1には必ず%1が入るから、常にBG1, BG2にモザイク処理がかかる。
戦闘におけるBG1, BG2は、一番下の背景と敵キャラであり、このふたつの背景にはモザイクがかかるが、テキストウィンドウにあたるBG3にはモザイクがかからない、ということである。
そして、モザイクサイズは%0001(2×2ピクセル)~%1111(16×16ピクセル)まで順に+1されていくということになる。
モザイクが徐々に大きくなっていくということである。
また、[$00:0360]の値もモザイク処理とともに+$10されていくことがわかる。
$C1/70ADでゼロフラグが立つのは、[$00:0360]に$F0が入っていて、$C1/70A9で$F0 + $10 = $100となりキャリーオーバーし(キャリーフラグクリア後に計算をしているためキャリーフラグが立つことになる)、Aに下1バイトの$00が入った時である。
つまり、[$00:2106]に$F3が入って、モザイクサイズが最大の16×16ピクセルになった時点で、モザイク処理終了である。
終了といっても[$00:2106]には$F3が入ったままであり、モザイクサイズが最大となったタイミングで味方キャラが戦闘離脱して、戦闘画面が暗転するまでモザイク処理は続いている。
この後はフィールドの画面が表示される。
フィールドに切り替わったタイミングで、一度[$00:2106]は初期化されて、$00が入る。
$C0/0E1F STZ $2106 [$00:2106] ;[$00:2106]に$00を書き込み
実際には、テレポートで飛んだ後、フィールドに戻った時にモザイクがかかっており、モザイクが徐々に解除される処理が行われる。
$C0/BE00 LDA #$01 ;Aに$01をロード $C0/BE02 STA $0239 [$00:0239] ;Aを[$00:0239]に書き込み $C0/BE05 STA $2100 [$00:2100] ;Aを[$00:2100](INIDISP ディスプレイ制御レジスタ)に書き込み $C0/BE08 LDA #$F7 ;Aに$F7をロード $C0/BE0A STA $2106 [$00:2106] ;Aを[$00:2106](MOSAIC モザイク処理)に書き込み $C0/BE0D LDA #$02 ;Aに$02をロード $C0/BE0F BRA $1E [$BE2F] ;フラグにかかわりなく常に分岐[$BE2F] $C0/BE2F STA $2E [$00:002E] ;Aを[$00:002E]に書き込み $C0/BE31 JSR $0AF7 [$C0:0AF7] ;[$C0:0AF7]へジャンプ
ここがモザイク処理のかかるタイミング。
[$00:2100]はディスプレイ制御レジスタで、画面明るさの制御である。
| Bit | 内容 |
|---|---|
| Bit0-3 | 明るさ (%0000=黒, %0001~%1111:明るさ度合い %1111が通常) |
| Bit4-6 | 不使用 |
| Bit7 | FBlankフラグ (FBlank = Forced blank 強制ブランク, 0=何もしない, 1=画面黒) |
上ではモザイク処理寸前に、[$00:2100]に$01を書き込んでおり、画面の明るさが最低(1~15の中で1)に設定されている。
[$00:2106]には$F7 (%1111 0111)が書き込まれているから、BG1~BG3に、モザイクサイズ最大の16×16ピクセルを設定している。
フィールドにおいては、セリフウィンドウなどが表示されるBG3にもモザイク処理が入るということになる。
といっても、テレポートで逃げた直後はセリフ表示がないので、実質BG1とBG2にモザイクをかけることになる。
また、モザイク処理進行のフラグ関係で[$00:002E]に$02を書き込んでいる。
この後、モザイク解除と画面を徐々に明るくする処理が1フレーム毎に同時並行で行われていく。
$C0/BE3A LDA $0160 [$00:0160] ;Aに[$00:0160]をロード $C0/BE3D BNE $03 [$BE42] ;ゼロフラグが立っていないとき[$BE42]分岐 $C0/BE42 CMP #$01 ;Aと$01を減算比較(ステータスレジスタ変更のみ) $C0/BE44 BNE $03 [$BE49] ;ゼロフラグが立っていないとき[$BE49]分岐 $C0/BE49 CMP #$02 ;Aと$02を減算比較(ステータスレジスタ変更のみ) $C0/BE4B BNE $03 [$BE50] ;ゼロフラグが立っていないとき[$BE50]分岐 $C0/BE50 CMP #$03 ;Aと$03を減算比較(ステータスレジスタ変更のみ) $C0/BE52 BNE $03 [$BE57] ;ゼロフラグが立っていないとき[$BE57]分岐 $C0/BE57 CMP #$04 ;Aと$04を減算比較(ステータスレジスタ変更のみ) $C0/BE59 BNE $03 [$BE5E] ;ゼロフラグが立っていないとき[$BE5E]分岐 $C0/BE5B BRL $004C [$BEAA] ;フラグにかかわりなく常に分岐[$BEAA] $C0/BEAA LDA #$1E ;Aに$1Eをロード $C0/BEAC SEC ;キャリーフラグON $C0/BEAD SBC $2E [$00:002E] ;A - [$00:002E](初期値$02) $C0/BEAF AND #$1E ;Aと$1Eで論理積 $C0/BEB1 ASL A ;Aを算術左シフト *2 $C0/BEB2 ASL A ;Aを算術左シフト *2 $C0/BEB3 ASL A ;Aを算術左シフト *2 $C0/BEB4 ORA #$07 ;Aと$07で論理和 $C0/BEB6 STA $2106 [$00:2106] ;Aを[$00:2106](MOSAIC モザイク処理)に書き込み $C0/BEB9 LDA $2E [$00:002E] ;Aに[$00:002E](初期値$02)をロード $C0/BEBB LSR A ;Aを論理右シフト (/2) $C0/BEBC STA $0239 [$00:0239] ;Aを[$00:0239]に書き込み $C0/BEBF STA $2100 [$00:2100] ;Aを[$00:2100](INIDISP ディスプレイ制御レジスタ)に書き込み $C0/BEC2 LDA $2E [$00:002E] ;Aに[$00:002E]をロード $C0/BEC4 INC A ;Aをインクリメント +1 $C0/BEC5 STA $2E [$00:002E] ;Aを[$00:002E]に書き込み $C0/BEC7 CMP #$1F ;Aと$1Fを減算比較(ステータスレジスタ変更のみ) $C0/BEC9 BEQ $03 [$BECE] ;ゼロフラグが立っているとき[$BECE]分岐 ;ゼロフラグON(ループ処理) $C0/BECB BRL $FF63 [$BE31] ;フラグにかかわりなく常に分岐[$BE31] ;ゼロフラグOFFでループを抜ける $C0/BECE STZ $2106 [$00:2106] ;[$00:2106]に$00を書き込み $C0/BED1 BRA $3C [$BF0F] ;フラグにかかわりなく常に分岐[$BF0F] $C0/BF0F LDA #$01 ;Aに$01をロード $C0/BF11 STA $0161 [$00:0161] ;Aを[$00:0161]に書き込み $C0/BF14 RTS ;サブルーチン戻り
$C0/BEAAからがモザイクと輝度処理である。
[$00:002E]の初期値は$02で、
(($1E - [$00:002E]) & $1E) * $08 | $07
(&は論理積演算子、|は論理和演算子)
を[$00:2106]に書き込んでいる。
さらに、[$00:002E]は$C0/BEC4でインクリメント+1されており、[$00:002E]が$1Fになるまでループ処理、となっている。
これはいったい何を計算しているのか。
答えを先に言うと、[$00:2106]に、$E7, $D7, $C7……, $07と、-$10ずつしていく処理なのだが、-$10するのは2フレームに1回だけにするため、上のような処理をしている。
手で計算するのは面倒なので、下のようにJavaScriptを書いてwhileループで計算してもらうと、
let result = []; let n = 2; while (n < 0x1F) { // 計算式 let m = (((0x1E - n) & 0x1E) * 0x08) | 0x07; // 結果を16進数変換 m = '0' + m.toString(16).toUpperCase(); result.push('$' + m.slice(-2)); n++; } console.log(result.toString(","));
出力される答えがこんな感じになる。
"$E7, $D7, $D7, $C7, $C7, $B7, $B7, $A7, $A7, $97, $97, $87, $87, $77, $77, $67, $67, $57, $57, $47, $47, $37, $37, $27, $27, $17, $17, $07, $07"
計算においては、最後に$07を論理和することで常に下3ビットに1が立つが、これはBG1~BG3のモザイクを常にONにするため。
その上でモザイクサイズのBit4~7だけを2フレームに1減らすという処理である。
最後に2回$07を書き込んだ時点でBit4~7が0だから、モザイクなし状態である。
最終的にループ処理をぬけて、$C0/BECEで[$00:2106]に$00を書き込み、モザイクなしの通常状態に戻る。
これがテレポート実行時のモザイク処理となる。
ちなみに[$00:2100]は、毎フレーム trunc([$00:002E]/2) が書き込まれているので、2フレームに一度+1される、つまりBit0~3の明るさが+1されて、最終的にループの最後に[$00:2100]に$0Fが書き込まれ、通常の輝度に戻る。
ここでは、中世編のルクレチア城テラス、マップの右上に立つといつでも見られるアリシアの誘拐の回想シーンの処理を見てみる。
まず、テラスに入った時点で、各種I/Oアドレスの初期化が$C0/0E1A~$C0/0ED7で行われており、このタイミングで、
$C0/0E1F STZ $2106 [$00:2106] ;[$00:2106]に$00を書き込み
という処理があって、[$00:2106]には$00が入る。
回想シーンが始まるとモザイク処理が挟まるが、最初の処理は以下の通り。
$C0/BF1B LDA $0160 [$00:0160] ;Aに[$00:0160]をロード $C0/BF1E CMP #$01 ;Aと$01を減算比較(ステータスレジスタ変更のみ) $C0/BF20 BNE $03 [$BF25] ;ゼロフラグが立っていないとき[$BF25]分岐 $C0/BF25 CMP #$02 ;Aと$02を減算比較(ステータスレジスタ変更のみ) $C0/BF27 BEQ $75 [$BF9E] ;ゼロフラグが立っているとき[$BF9E]分岐 $C0/BF29 CMP #$03 ;Aと$03を減算比較(ステータスレジスタ変更のみ) $C0/BF2B BEQ $71 [$BF9E] ;ゼロフラグが立っているとき[$BF9E]分岐 $C0/BF2D CMP #$04 ;Aと$04を減算比較(ステータスレジスタ変更のみ) $C0/BF2F BEQ $5C [$BF8D] ;ゼロフラグが立っているとき[$BF8D]分岐 $C0/BF8D LDA #$0F ;Aに$0Fをロード $C0/BF8F STA $0239 [$00:0239] ;Aを[$00:0239]に書き込み $C0/BF92 STA $2100 [$00:2100] ;Aを[$00:2100]に書き込み $C0/BF95 LDA #$07 ;Aに$07をロード $C0/BF97 STA $2106 [$00:2106] ;Aを[$00:2106]に書き込み $C0/BF9A LDA #$1D ;Aに$1Dをロード $C0/BF9C BRA $19 [$BFB7] ;フラグにかかわりなく常に分岐[$BFB7] $C0/BFB7 STA $2E [$00:002E] ;Aを[$00:002E]に書き込み $C0/BFB9 JSR $0AF7 [$C0:0AF7] ;[$C0:0AF7]へジャンプ
[$00:2106]の初期値は$07、つまりBG1~BG3にモザイクをかけるが、モザイクサイズはまだ1×1なので実質モザイクはかかっていない。
また、[$00:2100]に$0Fを書き込んでおり、ここでも明るさ(輝度)とモザイク処理を並行して行うことがわかる。
$C0/BFC2 LDA $0160 [$00:0160] ;Aに[$00:0160]をロード $C0/BFC5 BNE $03 [$BFCA] ;ゼロフラグが立っていないとき[$BFCA]分岐 $C0/BFCA CMP #$01 ;Aと$01を減算比較(ステータスレジスタ変更のみ) $C0/BFCC BNE $03 [$BFD1] ;ゼロフラグが立っていないとき[$BFD1]分岐 $C0/BFD1 CMP #$02 ;Aと$02を減算比較(ステータスレジスタ変更のみ) $C0/BFD3 BEQ $79 [$C04E] ;ゼロフラグが立っているとき[$C04E]分岐 $C0/BFD5 CMP #$03 ;Aと$03を減算比較(ステータスレジスタ変更のみ) $C0/BFD7 BEQ $75 [$C04E] ;ゼロフラグが立っているとき[$C04E]分岐 $C0/BFD9 CMP #$04 ;Aと$04を減算比較(ステータスレジスタ変更のみ) $C0/BFDB BEQ $4D [$C02A] ;ゼロフラグが立っているとき[$C02A]分岐 $C0/C02A LDA $2E [$00:002E] ;Aに[$00:002E]をロード $C0/C02C LSR A ;Aを論理右シフト (/2) $C0/C02D BEQ $1A [$C049] ;ゼロフラグが立っているとき[$C049]分岐 $C0/C02F STA $0239 [$00:0239] ;Aを[$00:0239]に書き込み $C0/C032 STA $2100 [$00:2100] ;Aを[$00:2100]に書き込み $C0/C035 LDA #$1E ;Aに$1Eをロード $C0/C037 SEC ;キャリーフラグON $C0/C038 SBC $2E [$00:002E] ;A - [$00:002E] $C0/C03A AND #$1E ;Aと$1Eで論理積 $C0/C03C ASL A ;Aを算術左シフト *2 $C0/C03D ASL A ;Aを算術左シフト *2 $C0/C03E ASL A ;Aを算術左シフト *2 $C0/C03F ORA #$07 ;Aと$07で論理和 $C0/C041 STA $2106 [$00:2106] ;Aを[$00:2106]に書き込み $C0/C044 DEC $2E [$00:002E] ;[$00:002E]をデクリメント -1 $C0/C046 BRL $FF70 [$BFB9] ;フラグにかかわりなく常に分岐[$BFB9] $C0/BFB9 JSR $0AF7 [$C0:0AF7] ;[$C0:0AF7]へジャンプ ;$C0/C02DでゼロフラグON(モザイクサイズ最大) $C0/C049 STZ $2106 [$00:2106] ;[$00:2106]に$00を書き込み $C0/C04C BRA $3F [$C08D] ;フラグにかかわりなく常に分岐[$C08D] $C0/C08D LDA #$8F ;Aに$8Fをロード $C0/C08F STA $0239 [$00:0239] ;Aを[$00:0239]に書き込み $C0/C092 STA $2100 [$00:2100] ;Aを[$00:2100]に書き込み $C0/C095 STZ $0161 [$00:0161] ;[$00:0161]に$00を書き込み $C0/C098 RTS ;サブルーチン戻り
後は1フレームに1回モザイクと輝度の処理を上ループで繰り返す。
$C0/C02A~$C0/C098が該当部分である。
今回は[$00:002E]の初期値に$1Dが入っているから、
(($1E - [$00:002E]) & $1E) * $08 | $07
を計算して、[$00:002E]をデクリメント(-1)していくという、先程のテレポート後のモザイク復元と逆パターンであることがわかる。
JavaScriptだと、先ほどのサブルーチンをちょっと変えて以下のようにしたら良い。
let result = []; let n = 0x1D; while (n > 0) { // 計算式 let m = (((0x1E - n) & 0x1E) * 0x08) | 0x07; // 結果を16進数変換 m = '0' + m.toString(16).toUpperCase(); result.push('$' + m.slice(-2)); n--; } console.log(result.toString(","));
結果は以下。
"$07, $17, $17, $27, $27, $37, $37, $47, $47, $57, $57, $67, $67, $77, $77, $87, $87, $97, $97, $A7, $A7, $B7, $B7, $C7, $C7, $D7, $D7, $E7, $E7"
2フレームに1回、モザイクサイズが増加していき、最後は$E7まで増える。
同じタイミングで[$00:2100]に$01が入るので、明るさはほぼ最低値である。
この後にループを抜けるから、$C0/C049に進み、以下のように値が入る。
[$00:2106] : $00[$00:2100] : $8Fモザイクは初期化解除になるが、[$00:2100]に$8F(%1000 1111)が入ることで強制ブランク(Fblank, Forced blank)となり画面が真っ暗となる。
つまり回想シーン前にモザイクがかかってから暗転するまでの処理となる。
回想シーンに入った直後のモザイク処理は、テレポートでジャンプ後の$C0/BE00~$C0/BF14と共通のため省略。
以上から、フィールドにおけるモザイク処理のサブルーチンは、モザイクを徐々に大きくする(かつ輝度を下げる)場合$C0/C02A~$C0/C098、モザイクを徐々に小さくする(かつ輝度を上げる)場合$C0/BE00~$C0/BF14を使う、ということがわかる。
以上、モザイク処理の説明とする。
ちなみに回想シーンで色味がセピアカラーになるのはカラーマス(Color math)処理ではない。
セピア系のカラーパレットをCGRAMに転送して使用している。