投影法

立体の空間を平面の画面に映す方法(投影法)と、奥行きのある地面を描くための透視投影の計算について説明します。

<注意>
このページでは、数式を表示するためにMathJaxを使用しています。ブラウザの設定でスクリプト実行をブロックしている場合、ページ内の数式が正しく表示されません。

投影とは

立体の空間の点\(\ (X,\ Y,\ Z)\ \)を、画面の上の点\(\ (x,\ y)\ \)に対応させることを投影と言います。
2Dのゲームでも、奥行きのある見せ方をするときには、何らかの投影を考えています。

投影は、大きく2種類に分けられます。

種類光の進み方遠くのものの大きさ例
平行投影平行な光で映す近くのものと同じ見下ろし型の画面、クォータービュー
透視投影1点(視点)に集まる光で映す遠いほど小さい奥行きのある地面、3D

同じ立方体を、4つの方法で投影したもの

平行投影

平行投影では、空間の座標から画面の座標への変換は、掛け算と足し算だけ(1次式)で表せます。
そのため、行列変換の行列1つで表せます。

ここでは、\(\ X\ \)を右、\(\ Y\ \)を奥、\(\ Z\ \)を上とします。

正投影

奥行きの座標\(\ Y\ \)を捨てて、\(\ (x,\ y) = (X,\ Z)\ \)とします。
横から見た画面(横スクロールのゲームなど)は、これにあたります。
奥行きは見えないので、重なりの順番(どちらが手前か)は別に決めます。

斜投影

奥行きを、斜めの方向に縮めてずらします。

\[ x = X + kY\cos\alpha, \qquad y = Z + kY\sin\alpha \]

正面の形はそのままで、奥行きが分かるようになります。
\(\ \alpha\ \)は奥行きを表す向き、\(\ k\ \)は奥行きを縮める割合です(図は\(\ \alpha = 45^\circ,\ k = 1/2\ \))。

クォータービュー

空間を斜め上から見下ろす投影で、ゲームでよく使われるのが次の式です。

\[ x = X - Y, \qquad y = \frac{X + Y}{2} + Z \]

床のマス目が、横2:縦1のひし形に見えます(画面の\(\ y\ \)は上向きにとっています。SNESの画面の座標では符号を逆にします)。
「横に2ドット進むと縦に1ドット進む」線は、ドット絵ではギザギザが揃ってきれいに見えるので、この比がよく使われます。

クォータービューで物を描く順番は、奥から手前へ、つまり\(\ X + Y\ \)の小さい順にします(同じなら\(\ Z\ \)の小さい順)。
奥のものから順に描けば、手前のものが上に重なります。

透視投影

視点から距離\(\ f\ \)の位置に画面を置き、空間の点と視点を結ぶ直線が画面と交わる点に映します。

透視投影を横から見た図

視点を原点にとり、奥行きの方向の距離を\(\ Z\ \)とすると、相似な三角形から次の式が得られます。

定理 (透視投影)
視点から奥行き\(\ Z\ \)の距離にある点\(\ (X,\ Y)\ \)は、画面の中心から次の位置に映る。

\[ x' = \frac{fX}{Z}, \qquad y' = \frac{fY}{Z} \]

同じ大きさのものでも、距離\(\ Z\ \)が2倍になると、画面の上では半分の大きさに映ります。
平行投影と違って\(\ Z\ \)で割る計算が入るので、行列の掛け算だけでは表せません。

視野と焦点距離

\(\ f\ \)は、画面にどれだけ広い範囲が映るか(視野角)を決めます。
画面の幅を\(\ W\ \)、左右の視野角を\(\ \phi\ \)とすると、次の関係があります。

\[ \tan\frac{\phi}{2} = \frac{W/2}{f} \]

\(\ f\ \)を小さくすると広い範囲が映り(広角)、大きくすると狭い範囲が大きく映ります(望遠)。

物の大きさ

遠くにいるキャラクターは、距離に反比例して小さく描きます。

\[ \text{画面上の大きさ} = \frac{f}{Z} \times \text{本当の大きさ} \]

SNESのスプライトは拡大縮小できないので、大きさを変えた絵を何種類か用意しておき、距離に応じて選びます。
描く順番は、遠いもの(\(\ Z\ \)の大きいもの)から手前のものへの順です。

奥行きのある地面

視点の高さ\(\ h\ \)から、水平に前を向いて平らな地面を見る場合を考えます。

視点、画面、地面の位置関係

地面は視点より\(\ h\ \)だけ下にあるので、透視投影の式で\(\ Y = -h\ \)とすると、奥行き\(\ Z\ \)の地面は画面の中心から\(\ fh/Z\ \)だけ下に映ります。
画面の中心の高さ(地平線)を\(\ y_0\ \)ライン目とし、画面の\(\ y\ \)ライン目(\(\ y > y_0\ \))に映る地面の奥行きを求めると、次のようになります。

定理
画面の\(\ y\ \)ライン目に映る地面の奥行きは

\[ Z = \frac{fh}{y - y_0} \]

であり、そのラインでは、画面の1ピクセルが地面の\(\ Z/f = h/(y - y_0)\ \)の幅にあたる。

ラインごとの、1ピクセルあたりの地面の幅

地平線のすぐ下では、1ピクセルが地面の広い範囲にあたり(遠くが小さく見え)、画面の下に行くほど狭い範囲にあたります(近くが大きく見える)。
地平線より上(\(\ y \le y_0\ \))には地面は映らないので、空などを描きます。

この式でラインごとに地面の模様を拾うと、次のような画面になります。

式から描いた、奥行きのある地面

地面に32×32の市松模様を描き、\(\ f = 200,\ h = 40,\ y_0 = 60\ \)として、画面の各点に映る地面の位置を式から求めて描いたものです。
地平線に近いところで模様が細かく乱れて見えるのは、1ピクセルに地面の広い範囲が入り、模様を飛び飛びに拾っているためです(エイリアシング、サンプリングとデジタルフィルタ)。

モード7で地面を描く

SNESのモード7は、BGを拡大縮小・回転できますが、レジスタを書き換えなければ、画面全体が同じ倍率で表示されます(モード7)。
奥行きのある地面にするには、上の式のとおり、ラインごとに倍率と、表示するBGの位置を変える必要があります。
そこで、HDMAでM7A~M7Dなどをラインごとに書き換えます(HDMAを使う)。

ラインごとの値は、視点の高さ\(\ h\ \)と地平線の位置\(\ y_0\ \)が変わらなければ、毎フレーム同じです。
\(\ 1/(y - y_0)\ \)の表をあらかじめ作っておけば、割り算をしなくて済みます(時間計算量と空間計算量)。
視点が向きを変える場合は、さらに行列変換の回転を組み合わせます。

参照

行列変換
ベクトル
三角関数
サンプリングとデジタルフィルタ
モード7
HDMAを使う