資料が英語ばかりの鬼門「HDMA」の説明らしき物です。
しかしHDMAはおろかWikiの編集も付け焼刃なのでグダグダです。
墓場のうんちくが来るまでの間に合わせってことでよろしく。
SFCでは画面が1秒間に60フレームほど描かれている気がします。
そのうちの1回を取り上げて考えてみましょう。
さて、なぜこんなblankの話をしてるかというと、
描画情報の変更はこのblank中でやらなければならないからです。
だからLevelASMとかで直接描画情報を変えようとしても、
その瞬間が奇跡的にblank中だった場合しかうまく行かないことになります。
ただしいくつかのエミュではこういうのを考慮してないので
blank無視しても正常に動作してしまう罠。
実機で動かなくてもエミュで動けばいいじゃんと見るかどうかは人によるけど…
最新のエミュでは、ちゃんと(?)blank無視した変更は反映されないようになってます。
じゃあblankのタイミングを狙うにはどうすればいいのか?
1つは、NMIという割り込みを利用する方法です。
画面下、Scanline 225を描いてV-blankに突入した瞬間、
流れているプログラムは一旦ストップ。
Snes:$00816Aから始まるNMIルーチンが割り込んできます。
この中でblank中でなければならない処理を
まとめてやってしまうというのです。
しかしblankはV-blankだけではありません。
H-blank中に描画設定を変えるとどうなるでしょう。
たとえば、Scanline0の前で明るい画面に設定し、
Scanline112の後のH-blankで暗い画面に設定すれば、
画面上半分(0〜112)は明るく、画面下半分(113〜225)は暗くなります。
このようにScanlineとScanlineの間で描画設定を変えることができます。
こういった、指定したH-blankで描画設定を変えるという処理を
自動的にやってくれる装置があります。
これがHDMAです。
まずはHDMAを実装しましょう。いくつか方法があります。
・CからBMF98567氏のHDMAを持ってきてインストール
・自作物展示場や、あっぷろだXのASM_Supporter
余計な機能がつきすぎるのが嫌でなければ、
ASM_Supporterをオススメします。
後先考えるとインスタントNMIがあった方が便利っちゃあ便利。
xkasの使い方は、あっぷろだXを参照。
いや、CMのつもりでは…
要望があれば、HDMAだけを入れるバージョンも作りますが。
さんざん「描画設定を変える」とかいう表現を使ってきたわけですが、
とりあえずそれがどういうことかを知らなければなりません。
SFCにはPPUというユニットがあり、
こちらのプログラムとは独立して、描画処理を行ってます。
このPPUにこちらから働きかけます。
難しそうですが、結局は$21xxへのストアです。
$21xxに値をストアするというのがPPUとの手動通信です。
とりあえずすずめ愛好会のこのページを見てみましょう。
対してHDMAは自動通信です。
HDMAは「Scanline毎に$21xxの値を自動で変えてくれる物」
だということになります。
ついに実際に簡単なHDMA効果を作ってみます。
しかし、いきなりLevelASMから作るのは大変なので、
チートで作ることにしましょう。
テストもしやすいですし。
というわけで、代入可能なメモリビューアがついているエミュを用意しましょう。
ちなみに私が使っているのは音楽再現度的な意味でSNESGTです。
最新のβ版ではblankも考慮されている上、
指定アドレスにジャンプができるので旧版より使いやすいです。
まずHDMAをインストールしたROMを起動して好きなLevelに行き、ポーズ。
メモリビューアを開いて、HDMAテーブルのある位置を見ましょう。
初期設定ではテーブルの位置は、BMF98567氏のHDMAでは$7FFF00
ASM_Supporterでは$7FFE00と決められています。(現在の設定では$7F8190ですが変更可能)
この先は後者として話を進めていきますので、
前者の場合は、各アドレスの3桁目をE⇒Fと脳内変換してください。
これからこのテーブルという領域に、
HDMAに必要な手続きを行っていきます。
そしたら先ほどインストールしたHDMA内部機構が
手続きに従い、自動的にHDMAを実行してくれるのです。
HDMAには0〜7の8つのチャンネルがあります。
各チャンネルに「Scanline毎に$21xxの値を変える」という
一まとまりの仕事を割り振ることができます。
つまり最大同時に8種類の仕事をさせることができるのです。
ただ本家でもHDMAが使われている箇所があるので、
本家HDMAの使用チャンネルとかぶるとキャンセルされてしまうかも。
チャンネル3辺りを使えば心配無いでしょう。
本家HDMAの使用状況を知りたければ、
ここにあるデバッグ用Snes9xでHDMAをトレースしてみましょう。
自分が理解するのに詰まったところなので勝手に補足。
さて、上記のようにHDMAには8つのチャンネルがあり、同時に最大8種類の効果を出すことができます。
色々弄れば色々効果が出せるわけですが難しい。そこで「必要事項を指定の場所に入れてくれれば後はやってやんよ」
というのがBMF98567氏の作ったものです。
まず「チャンネルの設定パラメータを入れるRAMアドレス」(=チャンネルごとのテーブル)がチャンネルごとに6つずつあります。
ここに転送方法とか弄る対象とか、設定を適宜入れていきます。
ここで設定した「転送したいデータを入れるRAMアドレス」(=転送内容のテーブル)に、弄る走査線の数などを入れていきます。
前者はxkasなどでプログラムを挿入するときに決めるもので、後から変更はできず、
後者は毎回自分の好きなところに指定する必要があります。
次項からその実践です。
まずはシンプルな1バイト入力系をやりましょう。
モザイク設定は$2106ですね。
テーブル周辺ははじめは全部00になっています。
ここにメモリビューアから入力していきます。
結論を言うと、下のスクショですね。
| チャンネル | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 1バイト目の値 | 01 | 02 | 04 | 08 | 10 | 20 | 40 | 80 |
ただし、今すぐ入力するのはやめてください。
まだ設定が終わってないのにスイッチONにするのは自殺行為ですね。
軽くバグります。
2バイト目は、転送方式の設定です。
一番難しいところです。下の表を見ましょう。
| 2バイト目の値 | 1回分の処理の挙動 | 使用例 | 使用例で何が起こるか |
| 00 | 1アドレスへ1byte書き | $2106にXXを代入 | モザイク設定がXXに |
| 01 | 2アドレスへ1byte書き | $2126にXX⇒$2127にYY | ウィンドウ1左端XX、右端YY |
| 02 | 1アドレスへ2byte書き | $210FにXX、ついでYY | レイヤー2 x座標がYYXXに |
| 03 | 2アドレスへ2byte書き | 拡大縮小回転マトリクス |
2chに分けて画面に濃淡の演出 使用してるゲーム例 アダムスファミリー |
| 04 | 4アドレスへ1byte書き | 思いつかん |
$2106は単純に1バイトを入力するものなので
00を入力します。
3バイト目は、弄る対象です。$21xxのxxを入力します。
今回は$2106モザイクなので、06です。
4〜6バイト目に、転送内容テーブルのありかを入力します。
空き場所ならどこでもいいですが、
今回は近くの$7FFE40に「転送内容」を書いていくことにします。
よって40 FE 7F
さて、ついに基礎設定が完了しました。
次はいよいよ「転送内容」を書いていきます。
さっき$7FFE40に指定したので、そこに書いていきましょう。
基本は「上から数えるScanline数⇒代入値」の繰り返しです。
こっからは好きなようにしていいですが…
とりあえず上から40行分にサイズAの大モザイクをかけてみましょう。
$2106の設定はAFです。
よってまず「40 AF」と書いていきます。
画面全体にモザイクがかかるでしょうが、無視して次行きましょう。
次の40行分はモザイクをサイズ6と、少し小さくしてみます。
続きに「40 6F」と書きます。
次第に小さくして、最終的に「40 AF 40 6F 40 2F 40 00 (00)」としました。
その結果がスクショです。そのとおりになってますね。
ちなみに(00)は終了宣言です。
こうして、チートでHDMAを作ることができました。
あとは今回のチート入力を再現するLevelASMを組むだけです。
組み方は65C816プログラミングの方を見ましょう。
このへんからだいぶ怪しい説明になります。
変なこと言ってたら直しちゃってください。
次のステップに進む前に
メインスクリーン、サブスクリーンとかを知っておくといいです。
もう一度すずめ愛好会を見ましょう。
今回見るべきは、
$212C(メインスクリーン構成)
$212D(サブスクリーン構成)
$2131(カラー演算対象設定 $40から転記)
$2132(固定色層の色)
ただし、$2131に関しては、
毎フレームのNMIで$40からコピーされているで、
$2131を弄ってもすぐ潰されてしまいます。
かわりに$40を弄ればOKです。
描画される画面は、
スプライト・固定色・BG1・BG2・BG3・BG4といった層の
重ね合わせで構成されているのですが、
細かく言うともう少し複雑です。
これらの層をメインスクリーン・サブスクリーンにグループ分け。
私たちにはメインスクリーンだけが見えます。
内部でサブスクリーンというのを別に構成しておきます。
この結果を、「メインスクリーンのうちの背景層」に足し加える。(カラー演算)
そういう足し算の結果、メインスクリーンの後ろにサブスクリーンがあるように見えます。
足し算というとわかりにくいですね。
サブスクリーンの内容を、プロジェクターで映し出すような感じです。
$40に関してはここでも説明しておきましょう。
$40の8つのbitを
さて、上の図の中に、固定色層というのがありますね。
これについて考えてみましょう。
こいつは$2132での色設定に従った単色層です。
見ての通りSMWの通常Levelでは、これはBGカラーとして使われています。
LMで設定された背景色は、RAM $0701-$0702 に保存されていて、
ここの値が毎フレーム$2132に代入されています。
ですから$0701を弄れば固定色層の色を変えることができ、
結果背景の色が変わります。
$2132を弄るHDMAをつけてみましょう。
Scanline毎に固定色層の色を変えるって事です。
こうなると固定色層はもはや単色ではなく、
グラデーションのかかったきれいな層となります。
やり方は前回とほとんど同じです。
しかしCとかで流行っているグラデーション演出は、
さっきのとは少し違ったはずです。
なんていうか、背景ではなく画面全体に色が映っているかのような。
?での知識を活かして、こちらをやってみましょう。
まずBG1-3、SPといったレイヤーを全てメインスクリーンに移してしまいます。
こうするとサブスクリーンにあるのは固定色層のみになります。
ここでこのサブスクリーンを、メインスクリーンの全ての層、
レイヤー123・スプライト・背景に映し出せばOKです。
$40に37を代入しましょう。
この時ピーパックンなどがレイヤー2の後ろに行ってしまう問題は、
どうしようもありません。副作用だと思ってください。
この場合はピーパックンを使わないか、
ピーパックンのグラフィックルーチンを弄って
フラグによっては前面に出るようにするか。
インスタントNMIを使って$0303,xにしらみつぶしにTSB #$30しまくるか。
どのみちこの設定ではレイヤー1と2の間にスプライトを挟むことはできないので
まあ使わないのが一番でしょう。
さて、今回はHDMAチートの前に準備が必要です。
まずメイン/サブスクリーンの設定をしなくてはなりません。
これらはSMWでは『$2131←$40』のようにサポートされていないので、
自作のLevelASMでやる必要があります。
まずはそこから作ってみましょう。
$212C(メインスクリーン構成)に17を代入し、
$212D(サブスクリーン構成)に00を代入します。
エミュでは普通にLevelASMから直接代入してもできるんですが、
本家SMWでPPUアクセスは全てNMIに回されているのを見るに、
恐らく実機では動かないのでしょう。
従って、インスタントNMIでやります。(CMじゃry)
講座も来たことだし、xkas用でいいよね!
!NMI = インスタントNMIのJSLのありかとします。
CodeStart: LDA $06B6 ;処理は開始1回だけで十分 BEQ INIT ;ステージ開始時は00になっているからINITへ RTL INIT: INC $06B6 ;2回は行わないようにする ;INCすることで、2回目以降は ;BEQ INITされない STZ $24 ;ステータスバー特別扱い解除 LDA #$01 ; TSB $06AD ; LDA #$37 ;固定色層を、全レイヤーに投射 STA $40 JSL !NMI ;次のV-blankのときに JSL NMIset ;JSL NMIsetが行われるよう予約 RTL NMIset: LDA #$17 ;予約内容 STA $212C ;メインスクリーン:全部 STZ $212D ;サブスクリーン :固定色のみ RTL
このLevelASMを入れたステージに行ってみましょう。
画面全体が背景色を帯びていることだと思います。(マリオは無事)
$701-2を弄って、色が変わることを確認しましょう。
本来背景色だったものを前面に持ってくるギミックなので、
もともと背景色があまり使われていないスイッチ宮殿や、城等でやると映えます。
さて、単色では物足りないなら、ここでHDMAです。
背景グラデーションの時と全く同じなので、図だけを投下しておきます。
今回のテーマは「『転送内容データ』を動的に変化させる」です。
「背景としてのレイヤー2」を3D的にスクロールさせることが目的です。
今回は下の背景を3Dスクロールさせてみましょう。
(既にHDMAグラデーションをかけてあります。)
$210Fはレイヤー2スクロールのx座標。
むしろレイヤー2を映すカメラのx座標と言ったほうが分かりやすいかも。
LunarMagicで見るような、レイヤー2の一枚絵を
正面から映しているカメラを想像しましょう。
xが大きいほど、カメラは右にあることになります。
xを増やすと、カメラは右に進み、
すると映像ではレイヤー2が左に流れていくことになります。
図も載せときます。
太陽・星・雲などがそうです。ここまで遠くにあると、
マリオがどこにいても同じように見えるはずです。
よって観測者の位置(レイヤー1座標)とは関係無く動くはずです。
今回は画面上の雲がそれにあたります。
この背景では雲が上中下3列あるので、
試しに各列が違う速度で左に流れるようにしてみましょう。
左に流すにはカメラを右に動かせばいいので、
「$210Fの値がどんどん増える。」ようにします。
各列の幅を計ったら、1F,20,20でうまくいったので、
「転送内容データ」のはじめの部分は
1F xxxx 20 yyyy 20 zzzz。
xxxx、yyyy、zzzzそれぞれに「違う速度で増えていく値」
がストアされるようにすればいいのです。
組み方わかる人は次の見出しまで飛ばしましょう。
まずはASMの側でカウンターを用意しましょう。
空きRAMならどこでもいいのですが…
今回は$7FFEF0を使ってみます。
毎フレームこれに1を足す。
これで$7FFEF0は「毎フレーム1増える値」として機能します。
次に$7FFEF0を使って雲をスクロールさせます。
「毎フレーム1増える」というのはマリオの歩行速度並なので
雲にしては速すぎです。
では、『$7FFEF0の1/2倍』という数字はどうでしょうか。
これは「2フレームに1増える値」として機能します。
スピードが半分になりました。
同様に、$7FFEF0の1/4倍、3/8倍という数字を使えば、
流れるスピードは1/4倍、3/8倍になります。
そこで今回は、
「上段の雲…スピード1/2」
「中段の雲…スピード3/8」
「下段の雲…スピード1/4」
としてみます。
1/2とか1/4をやるなら、ASLやLSR命令を覚えましょう。
12345という数字の各桁を右に動かすと、1234.5。1/10になってしまいます。
各桁を左に動かすと、123450。10倍になってしまいます。
これは10進法だからです。
2進法の世界では、桁を右に動かすと1/2、左に動かすと2倍になります。
LSR(右)、ASL(左)という命令がそれにあたります。
$7FFEF0にLSRした値を「上段の雲があるScanlineでの$210F値」に設定。
もう一度LSRした値を「下段の雲があるScanlineでの$210F値」に設定。
さて、中段の3/8倍という処理ですが、
$7FFEF0を『3倍してから8で割る』ことによって可能です。
注意すべきは、『8で割ってから3倍する』ではダメなこと。
なぜなら、『8で割った』値は、「8フレームに1回1増える」。
それを3倍すると、「8フレームに1回3増える」。
こういうカックカクな動きになってしまうからです。
前者をやれば「8フレームに3回1増える」ので、滑らかです。
$7FFEF0をLDA ⇒ ASL ⇒ $7FFEF0をADC ⇒ LSR3回
によって中段の値が得られます。
星や雲ほど遠くにある物でないならば、
こちらの動きによって見え方がかわります。
今回は海がそうです。
視点を動かしていくと、
近くのものほど速く動き、遠くのものほど遅く動きます。
視点からの遠さが同じものは、同じ速さで動くはずです。
背景の物体がもし、マリオやレイヤー1と同じ遠さにあるならば、
レイヤー1と同じ速さで(視点の動きに対し)スクロールするはずです。
このとき、$210F = レイヤー1x座標
これで視点の動きに対するスクロールが再現できます。
次に、その背景物体自体が動いてた場合。
視点の動きに加え、さらに物体自体の動きも反映させるには、
$210F = レイヤー1 x座 - 物体の移動値
なぜ引き算かというと、雲の時と同じです。
レイヤー2の静止した一枚絵が右に動く様子を再現するには、
逆にカメラを左に動かすことになるからです。
さて、以上は物体がマリオのそば、レイヤー1と同じ層にいた時の話です。
もっと遠くにあれば、視点に対してもっとゆっくりスクロールするはず。
$210F =(レイヤー1 x座 − もしレイヤー1の距離にいた場合の物体の移動値)× 距離補正 ~
これが一応最終公式となります。
今回の海に適用するとどうなるでしょう。
レイヤー1座標は$1462から得られます。
次にもし海がレイヤー1の距離、つまりマリオの足元にあった場合の移動値を考えます。
今回は波が左にマリオの歩行距離程度の速度で流れるという風にしてみましょう。
波の移動値は毎フレーム-1です。これを引くということは、
「毎フレーム+1する値を足す」のと同じです。
雲の時のカウンタ$7FFEF0を流用しましょう。
また、波なので、左に流れながらも多少ゆらゆらさせてもいいかもしれません。
「ゆらゆらする値」をさらに足しこめばOKです。
「ゆらゆらする値」の取り方の一例
LDA $14 (タイマ)
AND #$0F
TAX
LDA WAVE,x
:
:
WAVE db $00,$00,$00,$01,$01,$02,$03,$03
db $04,$04,$04,$03,$03,$02,$01,$01
こうして、$1462 + $7FFEF0 + WAVE,x という
『波がマリオのそばにあったときの値』を得ました。
とりあえずこれを$7FFEF2に保存しておきます。
その後最後に、距離補正を加えるために数字を掛け、
各Scanlineの$210F値としてストアします。
この掛ける数をScanline毎に色々変えることで、
視点からの距離の奥行きを表現することができます。
これがHDMAで3Dを擬似的に再現できる仕組みです。
(Scanline毎にしか奥行きを変えれないので、
同じScanlineに異なる遠さの物が置けない制限はあるが)
さて、海という大変なテーマを選んでしまいました。
どのScanlineも遠さが違います。
かなり多くの段階に分けて距離補正値を変えなくてはなりません。
めんどくさいので20段階くらいに留めます。十分でしょう。
一番近い所でも、マリオよりは遠くにあるので、1倍よりは小さい。
一番遠い所、つまり水平線付近は相当遠いので、1/32倍くらいでOKしょう。
その間を20段階くらいにわけて設定する。
計算方法は自由ですが、自分が思いつく中で一番早いやり方を紹介します。
7/8, 3/4, 5/8, 1/2
7/16, 3/8, 5/16, 1/4
7/32, 3/16, 5/32, 1/8
7/64, 3/32, 5/64, 1/16
7/128,3/64, 5/128,1/32
ラインナップは上の通り。右に読んでいけば大きい(近い)順です。
分子が同じものをひとまとめに計算していきます。
まず$7FFEF2(元の値)から、
元の値の2倍⇒$7FFEF4 (ASL)
元の値の3倍⇒$7FFEF6 (更に$7FFEF2を足す)
元の値の5倍⇒$7FFEF8 (更に$7FFEF4を足す)
元の値の7倍⇒A (更に$7FFEF4を足す)
と計算します。
あとはLSRLSR...で分子7のものを全部計算しつつストア。
$7FFEF8をLDAしてLSRLSR...で分子5のものを全部計算しつつストア。
同様に分子3、分子1も全部計算ストアしてミッションコンプリート。
完成品と、LevelASMコードを
http://mario.ellize.com/up/src/smw_2022.zipにおいておきます。
紛失したようです。持ってる人居たら、WikiにUPお願いします。
もっといい計算法があったら言ってね。
いきなりハードな内容で、うわぁああああとなった人が多そうなので、
まとめ兼1番簡単な多重スクロールの説明を勝手に補足。
まず雲のような、マリオとは関係なく画面を流れていくものについて。
レイヤー2の位置を毎フレーム1増やせばOK、と考える。(増やすと左に流れますね。)
さて、「?:弄る走査線の本数」「?:$210Fに入れたい値」をテーブルに書き込んでいくわけですが、
要するに?に毎フレーム1足せばいいだけです。
REP #$20 ;2byte扱うんで必要ですね。 LDA ? INC A STA ? SEP #$20 ;元に戻す。
これだけですね。とっても簡単。
INCの数を増やせば馬鹿みたいに早くなるし、
上を参考に何フレームに何回、という風にすればスピードも自由自在です。
次にマリオと関係して動くもの。
上の例では独立して動きつつ、マリオと関係しても動く海なので難しいですね。
ここでは地面でも山でもいいから、勝手には動かないもので練習。
レイヤー1と関係させて動かすことにしましょう。
$1462(レイヤー1の位置)の値をそのまま?に入れれば、レイヤー1とまったく同じ動きをします。
その走査線の部分だけ「レイヤー2スクロール固定」とおんなじことですね。
半分の速度で動かすには…
REP #$20 ;2byte(ry LDA $1462 LSR A ;この辺を変えることで、スクロールの早さが変わります。 STA ? SEP #$20 ;元に(ry
これだけです。LSRの回数を増やしたりして、スクロールスピードを変えることができますね。
ちなみに$1462でなく$1466(レイヤー2位置)でもOK。
その場合LMで設定したレイヤー2のスピードに影響を受けます。
以上を理解してもう一回上を内容を読めば、理解しやすいかも?
工事中
いよいよ講義1単位分程度のボリュームになってきた(笑)。