HDMA入門

ここは?

資料が英語ばかりの鬼門「HDMA」の説明らしき物です。
しかしHDMAはおろかWikiの編集も付け焼刃なのでグダグダです。
墓場のうんちくが来るまでの間に合わせってことでよろしく。

目次

?描画あれこれ


走査線/Scanline、H-blank、V-blank

SFCでは画面が1秒間に60フレームほど描かれている気がします。
そのうちの1回を取り上げて考えてみましょう。



まずは左上の1ドットから描画がスタートし、右上に向かいます。
こうして一番上の1行が描かれます。
こういった横方向の行を「走査線 Scanline」と言います。
一番上の行がScanline 0です。
二番目の行がScanline 1です。

一行目が右まで描かれると、次は二行目の左端を描くことになりますが、
それまでに僅かな休み時間があります。これがH-blankです。
同様に、各Scanlineの描画が終わるたびにH-blankが来ます。

ゲーム画面の一番下の行はScanline 225 です。
ここまで描ききった後のScanline 226〜261にあたる期間は
もうずっと休み時間です。これをV-blankと言います。
H-blankが土日だとしたら、V-blankは長期休暇みたいなものなので長いです。
その後はScanline 0に戻り、次のフレームの描画が始まります。

実際に描画するには

さて、なぜこんな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だけを入れるバージョンも作りますが。


?PPU


さんざん「描画設定を変える」とかいう表現を使ってきたわけですが、
とりあえずそれがどういうことかを知らなければなりません。

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:サイズの異なるモザイク


チャンネルの設定

まずはシンプルな1バイト入力系をやりましょう。
モザイク設定は$2106ですね。
テーブル周辺ははじめは全部00になっています。
ここにメモリビューアから入力していきます。
結論を言うと、下のスクショですね。



ここまで行く過程を説明しましょう。
まず「チャンネル3」を使うことに決めました。
図のチャンネル3テーブルに基本設定パラメータを書いていきます。

まずはチャンネルON宣言です。
テーブルの1バイト目には、下の表に従って値を入れましょう。
今回はチャンネル3なので、08ですね。

チャンネル 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を

I全BS4321
と表すとすれば、それぞれ

I/D …カラー演算 加or減
全/半 …カラー演算 1倍or半分(プロジェクターの威力半減)
BS4321 …それぞれ背景層/スプライト/BG4321。
      1としたbitに対応する層へ演算する。(プロジェクターを照射する)

口頭ではわかりにくいので、実例を見ましょう。

通常のLevelでの描画構成


スクリーン構成は
メイン…BG1,SP
サブ …BG2,BG3
$40 = 20 (+OB....)なので、加算対象は背景層のみ。

図示すると下のよう。


さて、ここで一つ考えてみましょう。
サブスクリーンが背景層に加算されていますが、
これをやめたらどうなるでしょう?
$40の値を20⇒00にしてみましょう。
「BG2、BG3、固定色」が見えなくなります。

では、背景層だけでなく、BG1にも加算するとどうなるでしょう。
$40の値を、20⇒21にしてみましょう。
プロジェクターが背景層だけでなく、レイヤー1にも照射をするという感じで、
レイヤー1にサブスクリーンの内容が映ってしまいます。
つまり、レイヤー1が透けているように見えます。

同様に、スプライトにも照射して透けさせることができますが、
スプライトのうち「前面に表示」設定のタイルは
どういうわけか特別扱いを受けているようです。
こういった加算の影響を受けません。
お化け屋敷ではこれを利用して、
透けたテレサ・透けないテレサを区別しているようです。


さて、これを見ていると、
別にBG1,2,3,スプライト全てをメインスクリーンに設定してもいい気がします。
何故こんな回りくどいことをしているのか。
それは、スプライトのうち「背面に表示」設定のタイルのためです。
こう設定すると、タイルはメインスクリーンの一番後ろに行きます。
BG2をメインスクリーンに設定すると、タイルがBG2背景の後ろに表示されます。
要するに、ピーパックンとかが背景の後ろに消えてしまうのです。

一方、レイヤー2を使ったマップでは、
別にスプライトがレイヤー2に隠れても問題ありません。
なのでここではBG2はメインスクリーンに設定されています。

?実践2:固定色層の応用を考える


さて、上の図の中に、固定色層というのがありますね。
これについて考えてみましょう。
こいつは$2132での色設定に従った単色層です。

見ての通りSMWの通常Levelでは、これはBGカラーとして使われています。
LMで設定された背景色は、RAM $0701-$0702 に保存されていて、
ここの値が毎フレーム$2132に代入されています。
ですから$0701を弄れば固定色層の色を変えることができ、
結果背景の色が変わります。

$2132を弄るHDMAをつけてみましょう。
Scanline毎に固定色層の色を変えるって事です。
こうなると固定色層はもはや単色ではなく、
グラデーションのかかったきれいな層となります。

やり方は前回とほとんど同じです。


▲ウホッ、いい空…

1バイト目はスイッチ。
2バイト目は転送設定。「00」
3バイト目は弄る対象が$2132だから「32」
そして転送内容データの置き場所を設定して、
そこに「走査線数」⇒「値」⇒「走査線数」⇒「値」…
HDMA自体の基本は全く同じ。

今回注意したいことは、$2132の使い方が少し変則的なことです。

$2132に入力するのは、「固定色層の色」という値ではなく、
「PPUよ、R/G/Bの値をxxに変えよ!」という命令なのです。
PPUの内部で固定色層の色が(R:12)(G:02)(B:0F)となっていたとします。

図にも書いてある通り、
20 + xx の値を$2132に入力すると、
PPUはRの値をxxにしろという命令を受け、
(R:xx)(G:02)(B:0F)というふうに変えます。

60 + xx の値を$2132に入力すると、
PPUはRとGの値をxxにしろという命令を受け、
(R:xx)(G:xx)(B:0F)というふうに変えます。

もう気づいたでしょうが、$2132への入力一発では、
RGBに同時に異なる値を設定することはできません。

転送設定「00」では、1スキャンラインで入力できるのは一回だけ。
これが不自由だと思うなら、転送設定「02」を使えばいいかも。
その場合、転送内容のところには
「Scanline数」⇒「値(1byte目)」⇒「値(2byte目)」⇒「Scanline数」…
となるので、3バイト1グループになることに注意。

さて、図のグラデーションの組み方ですが、
まずScanline0の描画開始前に「LMで設定した背景色$0701」の値が
(R)(G)(B)に代入されています。今回は淡い青です。

Scanline 0 …「60」RとGを00にして深い青にする
Scanline 2 …「9F」Bを1F(max)にして濃い青にする
Scanline 5 …「62」RとGを02にして少し明るくする
Scanline 9 …「63」RとGを03にしてもう少し明るくする
  :
  :
とまあ、こんな感じでグラデーションをやっていってます。
狙い通りの色合いを出すなら、やはり見ながら調整できるチートが便利。
完成したらLevelASMを組みましょう。


?実践3:固定色層の応用を考える?


しかし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です。
背景グラデーションの時と全く同じなので、図だけを投下しておきます。



LevelASMコードも投下しておきます。
ただし、CONTENTの内容がとても長いので、
外部ファイルGRAD.binとして入れるようにしています。
自信のある人は解読してみてください。


?実践3:マルチレイヤースクロール

目的

今回のテーマは「『転送内容データ』を動的に変化させる」です。
「背景としてのレイヤー2」を3D的にスクロールさせることが目的です。
今回は下の背景を3Dスクロールさせてみましょう。
(既にHDMAグラデーションをかけてあります。)



そうそう、前回言い忘れてましたが、
加算グラデーションをかける時は、元の色を暗めにしておきましょう。
加算なので、RGB各成分を元の色より下げることはできませんし。


さて、普通の設定では、
雲も海のどの部分も、同じ分だけスクロールしてしまいます。
これは奇妙。
近景は速く、遠景は遅く動いて見えるようにしたいものです。

RAM $210F

$210Fはレイヤー2スクロールのx座標。
むしろレイヤー2を映すカメラのx座標と言ったほうが分かりやすいかも。
LunarMagicで見るような、レイヤー2の一枚絵を
正面から映しているカメラを想像しましょう。
xが大きいほど、カメラは右にあることになります。
xを増やすと、カメラは右に進み、
すると映像ではレイヤー2が左に流れていくことになります。
図も載せときます。



普段はRAMの$1466から転記されているこの値。
これをScanlineごとに変えればいいのですが、
変える値は前回までと違って、リアルタイムに変化します。

「転送内容データ」は、今までどおり

「走査線数」⇒「転送内容(2byte)」⇒

の繰り返しです。
RAMに置いてあるこいつを、 ASMによって「転送内容(2byte)」の部分を
絶えず書き換えることが必要になります。


で、どういう値に書き換えればいいのか。

極めて遠くにあるもの


太陽・星・雲などがそうです。ここまで遠くにあると、
マリオがどこにいても同じように見えるはずです。
よって観測者の位置(レイヤー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単位分程度のボリュームになってきた(笑)。


コメント