Skip to content
Go back

モード7

· Updated:

概要

モード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軸に垂直でy=0y = 0にあって、一切変形しないとします。また、カメラはppの位置にあって、θx\theta_xだけピッチ回転して、続いてθy\theta_yだけヨー回転するとします。空間座標系は+X+Xを右、+Y+Yを上、+Z+Zを前とします。

透視投影

まず、ビュー空間6の点pv=[xv,yv,zv,wv]p_v = [x_v, y_v, z_v, w_v]を幅bwb_w・高さbhb_h・中心[0,0,zn][0, 0, z_n]のスクリーンへ投影することを考えます。このとき、投影された点ppp_p

pp=Mppv=[2bw1znxv,2bh1znyv,fz(zv,wv),zv]p_p = M_p p_v = [2 b_w^{-1} z_n x_v, 2 b_h^{-1} z_n y_v, f_z(z_v, w_v), z_v]

となります。ここで、MpM_pはそのプロジェクション行列を表します。また、fz(zv)f_z(z_v)は奥行きを正規化する関数を表しており、[zn,zf][z_n, z_f]の範囲が[0,1][0,1]の範囲に投影される場合には、fz(zv,wv)=zfzfznzvzfznzfznwvf_z(z_v, w_v) = \frac{z_f}{z_f - z_n} z_v - \frac{z_f z_n}{z_f - z_n} w_vとなります。逆に、[1,0][1, 0]の範囲に投影される場合には、fz(zv,wv)=znzfznzv+zfznzfznwvf_z(z_v, w_v) = -\frac{z_n}{z_f - z_n} z_v + \frac{z_f z_n}{z_f - z_n} w_vとなります。

次に、点pp=[xp,yp,zp,wp]p_p = [x_p, y_p, z_p, w_p]を幅bwb_w・高さbhb_hで左上を原点とする画面の座標系へ変換することを考えます。このとき、変換された点pdp_d

pd=Mdpp=[0.5bwxp+0.5bw,0.5bhyp+0.5bh,zp,wp]p_d = M_d p_p = [0.5 b_w x_p + 0.5 b_w, -0.5 b_h y_p + 0.5 b_h, z_p, w_p]

となります。ここで、MdM_dはその変換行列を表します。同様に、点pvp_vについても考えると

pd=MdMppv=[znxv+0.5bw,znyv+0.5bh,fz(zv,wv),zv]p_d = M_d M_p p_v = [z_n x_v + 0.5 b_w, -z_n y_v + 0.5b_h, f_z(z_v, w_v), z_v]

となります。

そして、今度は逆に、点pd=[xd,yd,zd,wd]p_d = [x_d, y_d, z_d, w_d]から点pvp_vを求めると

pv=Mp1Md1pd=[zn1(xd0.5bw),zn1(yd+0.5bh),wd,fw(zd,wd)1]p_v = M_p^{-1} M_d^{-1} p_d = [z_n^{-1} (x_d - 0.5 b_w), - z_n^{-1} (y_d + 0.5 b_h), w_d, f_w(z_d, w_d)^{-1}]

となります。ここで、fw(zd,wd)f_w(z_d, w_d)fz(zv,wv)f_z(z_v, w_v)の逆の操作を行う関数を表しており、[0,1][0, 1]の範囲を[zn,zf][z_n, z_f]の範囲に戻す場合には、fw(zd,wd)=zfznwdzfwd(zfzn)zdf_w(z_d, w_d) = \frac{z_f z_n w_d}{z_f w_d - (z_f - z_n) z_d}となります。逆に、[1,0][1, 0]の範囲を[zn,zf][z_n, z_f]の範囲に戻す場合には、fw(zd,wd)=zfznwdznwd+(zfzn)zdf_w(z_d, w_d) = \frac{z_f z_n w_d}{z_n w_d + (z_f - z_n) z_d}となります。

最後に、このベクトルを透視除算7して、共通項を括り出すと以下のようになります。

pv=S(fw(zd,wd)zn1)[xd0.5bw,yd+0.5bh,znwd,1]p_v = S(f_w(z_d, w_d) z_n^{-1}) [x_d - 0.5 b_w, -y_d + 0.5 b_h, z_n w_d, 1]

ここで、S(fw(zd,wd)zn1)S(f_w(z_d, w_d) z_n^{-1})はスケーリング行列を表します。このスケーリング係数fw(zd,wd)zn1f_w(z_d, w_d) z_n^{-1}は後ほど取り扱います。

BGへのマッピング

続いて、画面上の点pdp_dからワールド空間8上の点pw=[xw,yw,zw,ww]p_w = [x_w, y_w, z_w, w_w]への変換を考えます。ビュー空間上の点pvp_vへの変換はすでに求まっているので、それにカメラが取り得る変換(平行移動・ヨー回転・ピッチ回転)を適用すると

pw=T(p)Ryx(θy,θx)S(fw(zd,wd)zn1)[xd0.5bw,yd+0.5bh,znwd,1]p_w = T(p) R_{yx}(\theta_y, \theta_x) S(f_w(z_d, w_d) z_n^{-1}) [x_d - 0.5 b_w, -y_d + 0.5 b_h, z_n w_d, 1]

となります。ここで、ppはワールド空間でのカメラの位置を、T(p)T(p)は平行移動行列を表します。また、Ryx(θy,θx)R_{yx}(\theta_y, \theta_x)は「X軸で回転したのちY軸で回転する」ような回転行列を表し、以下のようになります。

Ryx(θy,θx)=[cosθysinθxsinθycosθxsinθy00cosθxsinθx0sinθysinθxcosθycosθxcosθy00001]R_{yx}(\theta_y, \theta_x) = \begin{bmatrix} \cos\theta_y & \sin\theta_x \sin\theta_y & \cos\theta_x \sin\theta_y & 0 \\ 0 & \cos\theta_x & -\sin\theta_x & 0 \\ -\sin\theta_y & \sin\theta_x \cos\theta_y & \cos\theta_x \cos\theta_y & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

今回考えている平面はXZ面上にあるので、点pwp_wが平面上にあるとしてyw=0y_w = 0を解くと

yw=0    y+fw(zd,wd)zn1(Ryx,2(θy,θx)[xd0.5bw,yd+0.5bh,znwd,1])=0    y+fw(zd,wd)zn1((yd+0.5bh)cosθxznwdsinθx)=0    fw(zd,wd)zn1=y/((yd+0.5bh)cosθxznwdsinθx)\begin{array}{ll} & y_w = 0 \\ \implies & y + f_w(z_d, w_d) z_n^{-1} (R_{yx,2}(\theta_y, \theta_x) [x_d - 0.5 b_w, -y_d + 0.5 b_h, z_n w_d, 1]) = 0 \\ \implies & y + f_w(z_d, w_d) z_n^{-1} ((-y_d + 0.5 b_h) \cos\theta_x - z_n w_d \sin\theta_x) = 0 \\ \implies & f_w(z_d, w_d) z_n^{-1} = -y / ((-y_d + 0.5 b_h) \cos\theta_x - z_n w_d \sin\theta_x) \end{array}

となり、スケーリング係数fw(zd,wd)zn1f_w(z_d, w_d) z_n^{-1}が求まります。ここで、Ryx,2(θy,θx)R_{yx,2}(\theta_y, \theta_x)Ryx(θy,θx)R_{yx}(\theta_y, \theta_x)の2行目のベクトルを、yyはカメラ位置ppYY成分を表します。

以上により、すべての値が画面上の点pdp_dから計算できるようになりました。そして最後に、pwp_wのX軸とZ軸に関する変換を取り出してまとめれば、2次元の変換行列が得られます。

おまけ:水平線の高さ

以下のように式を変形すると

fw(zd,wd)zn1=y/((yd+0.5bh)cosθxznwdsinθx)    yd+0.5bh=(fw(zd,wd)1zny+znwdsinθx)/cosθx    yd+0.5bh=fw(zd,wd)1zny/cosθx+znwdtanθx\begin{array}{ll} & f_w(z_d, w_d) z_n^{-1} = -y / ((-y_d + 0.5 b_h) \cos\theta_x - z_n w_d \sin\theta_x) \\ \implies & -y_d + 0.5 b_h = (-f_w(z_d, w_d)^{-1} z_n y + z_n w_d \sin\theta_x) / \cos\theta_x \\ \implies & -y_d + 0.5 b_h = -f_w(z_d, w_d)^{-1} z_n y / \cos\theta_x + z_n w_d \tan\theta_x \end{array}

fw(zd,wd)f_w(z_d, w_d)に対するydy_dを求めることができます。このとき、fw(zd,wd)=zff_w(z_d, w_d) = z_fとすると、ydy_dは水平線の高さを表します。

実装

H-Blank割込ごとにPAPBPCPDXYを導出した数式に従って更新するだけです。これらのパラメータは変換行列として以下のようになっています。

[PAPBXPCPDY001]\begin{bmatrix} PA & PB & X \\ PC & PD & Y \\ 0 & 0 & 1 \end{bmatrix}

ただし、この計算をH-Blank中に行うのはいささか重すぎるので、代わりに、V-Blank中にすべてのパラメータを配列として用意しておいて、H-Blank割込でDMAを介してコピーすると良いでしょう。

BG変形のパラメータは左上を基準とするので、走査線の左端pd=[0,y,zn,1]p_d = [0, y, z_n, 1]を入力として計算します。

疑似コード

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,+1][-1, +1]の範囲なので、整数部は1ビットあれば良いはずです。また、位置は[512,512][-512, 512]程度の範囲なので、整数部は9ビットか10ビットが必要になります。

また、精度が落ちないように計算順序を入れ替えたほうが良い場合もあるそうです。

参考文献

Footnotes

  1. アフィン変換とは「線形変換(拡大縮小、回転、せん断)と平行移動から成る変換」のことです。

  2. H-Blankとは「同じフレーム内で、直前の走査線の右端から直後の走査線の左端へ描画対象を移すのにかかる期間」のことです。

  3. V-Blankとは「前のフレームの最後の走査線の右端から次のフレームの最初の走査線の左端へ描画対象を移すのにかかる期間」のことです。

  4. 走査線とは「画面1行分の画素の集まり」のことです。

  5. 走査線上のピクセルの奥行きが異なると、それらに対応する平面上の点が等間隔に並ばないので、2次元のBG変形では正しく表現できなくなります。

  6. ビュー空間とは「カメラ(視点)を基準とする空間座標系」のこと。

  7. これらのベクトルは4次元同次座標系なので、wwで割ることができます。

  8. ここでの「ワールド空間」は絶対座標を与える空間という意味合いで使っています。