概要
モード7とはスーパーファミコン(SFC)に搭載された「背景画像(BG)に2次元のアフィン変換1を適用する変形機能」のことで、その名前はSFCでBGにアフィン変換を適用できるモードが7番であったことに由来します。この機能は当時、レースゲームのコースやRPGのワールドマップなどで印象的に利用されたため、“モード7”といえば「擬似的に3次元平面を表現する手法」として界隈に認知されています。
この記事では、この「疑似3D平面表現」としての”モード7”について、どのように実現されるのかを考えてみたいと思います。
仕組みと制約
SFCはメモリマップドI/O方式の計算機で、接続されるテレビのタイミングに従って動作します。つまり、H-Blank2やV-Blank3の時にプログラムを実行してI/Oレジスタの読み書きを行い、それ以外の時にI/Oレジスタに従ってピクセルを出力します。
“モード7”は、そのH-Blankで走査線4ごとにBG変形の変換行列を適切に与えることで実現します。ただし、この変形は2次元のアフィン変換なので、その走査線を3次元的に変形して”正しくパースペクティブを付ける”ことはできません。つまり、走査線の中ですべてのピクセルの奥行きが同じでない場合には見た目がおかしくなります5。なので、常にそうならないような制約を考えなければなりません。
そこで、今回は「平面を固定し、カメラを平行移動・ヨー回転・ピッチ回転のみに制限する」という状況を考えます。
導出
BG変形の変換行列は、3次元平面上にBGがマッピングされていると考えて、画面上の点からその平面上の点への変換として求めることができます。このとき、平面はY軸に垂直でにあって、一切変形しないとします。また、カメラはの位置にあって、だけピッチ回転して、続いてだけヨー回転するとします。空間座標系はを右、を上、を前とします。
透視投影
まず、ビュー空間6の点を幅・高さ・中心のスクリーンへ投影することを考えます。このとき、投影された点は
となります。ここで、はそのプロジェクション行列を表します。また、は奥行きを正規化する関数を表しており、の範囲がの範囲に投影される場合には、となります。逆に、の範囲に投影される場合には、となります。
次に、点を幅・高さで左上を原点とする画面の座標系へ変換することを考えます。このとき、変換された点は
となります。ここで、はその変換行列を表します。同様に、点についても考えると
となります。
そして、今度は逆に、点から点を求めると
となります。ここで、はの逆の操作を行う関数を表しており、の範囲をの範囲に戻す場合には、となります。逆に、の範囲をの範囲に戻す場合には、となります。
最後に、このベクトルを透視除算7して、共通項を括り出すと以下のようになります。
ここで、はスケーリング行列を表します。このスケーリング係数は後ほど取り扱います。
BGへのマッピング
続いて、画面上の点からワールド空間8上の点への変換を考えます。ビュー空間上の点への変換はすでに求まっているので、それにカメラが取り得る変換(平行移動・ヨー回転・ピッチ回転)を適用すると
となります。ここで、はワールド空間でのカメラの位置を、は平行移動行列を表します。また、は「X軸で回転したのちY軸で回転する」ような回転行列を表し、以下のようになります。
今回考えている平面はXZ面上にあるので、点が平面上にあるとしてを解くと
となり、スケーリング係数が求まります。ここで、はの2行目のベクトルを、はカメラ位置の成分を表します。
以上により、すべての値が画面上の点から計算できるようになりました。そして最後に、のX軸とZ軸に関する変換を取り出してまとめれば、2次元の変換行列が得られます。
おまけ:水平線の高さ
以下のように式を変形すると
に対するを求めることができます。このとき、とすると、は水平線の高さを表します。
実装
H-Blank割込ごとにPA、PB、PC、PD、X、Yを導出した数式に従って更新するだけです。これらのパラメータは変換行列として以下のようになっています。
ただし、この計算をH-Blank中に行うのはいささか重すぎるので、代わりに、V-Blank中にすべてのパラメータを配列として用意しておいて、H-Blank割込でDMAを介してコピーすると良いでしょう。
BG変形のパラメータは左上を基準とするので、走査線の左端を入力として計算します。
疑似コード
void mode7(float y, float screen_width, float screen_height, vec3 camera_pos, float camera_near, vec2 camera_angle) {
const vec3 screen_pos{-screen_width / 2, -y + screen_height / 2, camera_near};
const auto sin_x = sin(camera_angle.x);
const auto cos_x = cos(camera_angle.x);
const auto sin_y = sin(camera_angle.y);
const auto cos_y = cos(camera_angle.y);
// 回転行列(1行目と3行目のみ)
const vec3 rot_x{cos_y, sin_x * sin_y, cos_x * sin_y};
const vec3 rot_z{-sin_y, sin_x * cos_y, cos_x * cos_y};
// スケーリング係数
const auto zoom = -camera_pos.y / (screen_pos.y * cos_x - screen_pos.z * sin_x);
// BG変形を計算する
PA = zoom * rot_x.x;
PB = zoom * rot_x.z;
PC = zoom * rot_z.x;
PD = zoom * rot_z.z;
X = camera_pos.x + zoom * dot(screen_pos, rot_x);
Y = camera_pos.z + zoom * dot(screen_pos, rot_z);
}固定小数点数の精度
SFCでは固定小数点数で実数を扱いますが、場面場面で必要な精度が異なります。三角関数はの範囲なので、整数部は1ビットあれば良いはずです。また、位置は程度の範囲なので、整数部は9ビットか10ビットが必要になります。
また、精度が落ちないように計算順序を入れ替えたほうが良い場合もあるそうです。
参考文献
Footnotes
-
アフィン変換とは「線形変換(拡大縮小、回転、せん断)と平行移動から成る変換」のことです。 ↩
-
H-Blankとは「同じフレーム内で、直前の走査線の右端から直後の走査線の左端へ描画対象を移すのにかかる期間」のことです。 ↩
-
V-Blankとは「前のフレームの最後の走査線の右端から次のフレームの最初の走査線の左端へ描画対象を移すのにかかる期間」のことです。 ↩
-
走査線とは「画面1行分の画素の集まり」のことです。 ↩
-
走査線上のピクセルの奥行きが異なると、それらに対応する平面上の点が等間隔に並ばないので、2次元のBG変形では正しく表現できなくなります。 ↩
-
ビュー空間とは「カメラ(視点)を基準とする空間座標系」のこと。 ↩
-
これらのベクトルは4次元同次座標系なので、で割ることができます。 ↩
-
ここでの「ワールド空間」は絶対座標を与える空間という意味合いで使っています。 ↩