座標変換(モデル・ビュー・射影)
3D の頂点が画面に映るまでの変換を、なぜ 4x4 行列と同次座標なのか、透視除算で遠近が付く理由、法線が壊れない掛け方まで一本の筋で追えます。
- 1.モデル・ビュー・射影の各行列を掛け合わせ、頂点をワールド→カメラ→クリップ空間へ順に写す。回転と平行移動を1つの行列で扱うために4次元の同次座標を使う。
- 2.射影行列は w に深度(-z 等)を仕込み、その後 GPU が xyz を w で割る透視除算で遠近を付け、-1〜1 の NDC に正規化する。
- 3.法線はモデル行列の逆転置で変換しないと、非等方スケールで面に対し傾く。右手系と左手系は Z 軸の向きと NDC の深度方向の約束の違い。
頂点が画面に届くまでは「空間の乗り換え」
3D モデルの頂点は、そのまま画面に描けません。まずワールド(世界)に配置し、カメラから見た座標に直し、遠近を付けてスクリーンの正規化座標へ写す——この一連の乗り換えを担うのが モデル・ビュー・射影(Model-View-Projection, MVP)行列 です。各段の変換を1つずつ 4x4 行列で表し、M_proj * M_view * M_model と 右から順に掛ける ことで、頂点 p を一気に写せます(列ベクトル規約)。GPU の頂点シェーダはこの合成行列を頂点ごとに掛けるのが本質的な仕事です。
p_clip = M_proj · M_view · M_model · p_model
M_model : モデル座標 → ワールド座標(配置・回転・拡縮)
M_view : ワールド座標 → カメラ座標(視点を原点へ)
M_proj : カメラ座標 → クリップ座標(遠近と正規化の準備)
掛ける順は「後で適用したい変換ほど左」。上式は
まず M_model、次に M_view、最後に M_proj が効く。
なぜ 3 次元なのに 4x4 行列なのか(同次座標)
回転・拡大縮小は 3x3 行列で書けますが、平行移動は行列の掛け算では表せません。原点を動かす操作は線形写像(原点を保つ)ではなくアフィン変換だからです。そこで座標を1次元増やし、点 (x,y,z) を (x,y,z,1) として扱う 同次座標(homogeneous coordinates) を導入します。4x4 にすると、平行移動が最右列に収まり、回転・拡縮・平行移動を1つの行列で合成できます。
平行移動 T(tx,ty,tz): 任意の点 (x,y,z,1) に掛けると
[ 1 0 0 tx ] (x+tx, y+ty, z+tz, 1)
[ 0 1 0 ty ] ← 4行目の 1 が tx,ty,tz を
[ 0 0 1 tz ] 座標へ「持ち込む」役割
[ 0 0 0 1 ]
同次座標の要は末尾成分 w の意味です。w ≠ 0 の (x,y,z,w) は 3D 点 (x/w, y/w, z/w) を表し、w = 0 は「無限遠の方向(ベクトル)」を表します。だから 点は w=1、方向ベクトルは w=0 で持つのが定石で、方向ベクトルに平行移動を掛けても動かない(w=0 が tx,ty,tz を無効化する)性質が自動的に得られます。この w を後段で射影に流用するのが、次のクリップ空間の肝になります。
1つのモデルに拡大 S・回転 R・平行移動 T をまとめるとき、通常は M_model = T · R · S とします。頂点にはまず S が効いて原点まわりで拡縮し、次に R で回転し、最後に T で目的地へ運ばれる。順序を変えると「回転してから移動」と「移動してから回転(=公転)」のように結果が別物になります。行列積が非可換であることの実務的な現れです。
ビュー行列 ── カメラを原点へ引き戻す逆変換
ビュー行列の考え方はシンプルです。「カメラを動かす」のではなく、世界全体をカメラの逆だけ動かして、カメラを原点・標準の向きに固定 します。したがってビュー行列はカメラのワールド姿勢(位置と回転)の 逆行列 です。回転部分 R は直交行列なので逆は転置 Rᵀ で済み、平行移動と合わせて次の形になります。
M_view = [ Rᵀ -Rᵀ·t ] R : カメラの回転(各軸がワールドでの向き)
[ 0ᵀ 1 ] t : カメラのワールド位置
典型的な lookAt(eye, center, up) は
forward = normalize(center - eye)
right = normalize(cross(forward, up))
trueUp = cross(right, forward)
の3軸で R を組み、-Rᵀ·eye を平行移動に入れて構築する。
射影行列と透視除算 ── 遠近はどこで生まれるか
透視投影は「遠いものほど小さい」を作ります。鍵は、射影行列が出力の w 成分にカメラ空間の深度(右手系なら -z)を書き込む ことです。行列積の直後の座標はまだ割り算されていない クリップ座標 で、その後 GPU が固定機能として xyz を w で割る 透視除算(perspective divide) を行います。深度で割るからこそ、遠い(w が大きい)頂点ほど中心へ寄り、遠近が付きます。
透視射影後のクリップ座標(列ベクトル、右手系の例):
x_clip = x · (f/aspect) f = 1/tan(fovy/2)
y_clip = y · f
z_clip = z·A + w·B A,B は near/far で決まる係数
w_clip = -z ← 深度が w に入る(ここが要)
透視除算(GPU が実行):
NDC = (x_clip/w_clip, y_clip/w_clip, z_clip/w_clip)
→ 各成分が概ね -1〜1 に収まる(正規化デバイス座標)
正射影(orthographic)では w_clip = 1 のまま。
割っても遠近が付かず、平行が保たれる(CAD やUI向き)。
透視除算後の空間が NDC(Normalized Device Coordinates) で、可視領域は一辺 2 の立方体(各軸 -1〜1、深度の範囲は API の約束次第)に正規化されます。この立方体に対してクリッピングと、ビューポート変換(NDC→ピクセル座標)が続きます。クリッピングを 除算前のクリップ座標で行う のは、w の符号情報(カメラ背後の点は w が負)が割り算で失われ、背後の点が画面前面に化ける「反転」を避けるためです。
z_clip/w_clip は z の逆数に比例するため、深度バッファの精度は near 側に密・far 側に粗 く分布します。near を極端に小さく取ると遠方の精度が枯れ、同一平面上の面がちらつく Z ファイティングが起きます。対策は near をできるだけ大きく取る、深度を反転させる Reversed-Z(浮動小数点深度の指数分布と精度分布を噛み合わせる)を使うなどです。near/far の比が精度を支配する点を押さえておきます。
右手系と左手系 ── 何が違い、なぜ食い違うか
座標系には 右手系と左手系 があり、X と Y を画面の右・上に取ったとき、奥行き Z の正方向が「手前向き(右手系)」か「奥向き(左手系)」かで分かれます。数学や OpenGL は右手系(カメラは -Z を向く)、Direct3D は伝統的に左手系を好むなど、API で約束が異なります。加えて NDC の深度範囲 も割れており、OpenGL は -1〜1、Direct3D・Vulkan・Metal は 0〜1 です。
| 観点 | 右手系(OpenGL 系) | 左手系(Direct3D 系) |
|---|---|---|
| カメラの視線 | -Z 方向を向く | +Z 方向を向く |
| 前方の深度 | 手前が +Z / 奥が -Z | 手前が小 / 奥が +Z |
| NDC 深度範囲 | -1 〜 1 | 0 〜 1(Vulkan/Metal も 0〜1) |
| 外積の向き | 右手の法則 | 左手の法則 |
| 主な影響先 | 射影行列の符号・面の表裏判定 | 同左(符号が逆になる) |
実務では、利き手系と深度範囲が食い違うライブラリ間でモデルを移すと、モデルが裏返る・面の表裏(フェイスカリングの向き)が反転する・深度が逆になる、といった不具合が出ます。原因の多くは 射影行列の符号と、三角形の頂点巻き順(CW/CCW) の不一致です。移植時はまず射影行列の系と NDC 深度範囲を合わせ、次にカリングの巻き順を確認するのが定石です。
法線に逆転置行列が要る理由
頂点を M で変換するとき、法線ベクトルを同じ M で変換すると、非等方スケール(軸ごとに倍率が違う変形)で法線が面に垂直でなくなります。ライティングは法線の向きで決まるため、これは陰影の破綻に直結します。正しくは法線を モデル行列(の左上 3x3 部分 A)の逆転置 (A⁻¹)ᵀ で変換します。
理由は「法線は面の接ベクトルと直交し続けねばならない」という制約から導けます。接ベクトル t は A·t へ変換される。変換後も法線 n' が A·t と直交する(内積 0)ためには、次が任意の t で成り立てばよい。
要件: (n')ᵀ · (A·t) = 0 (変換後も法線 ⟂ 接線)
仮に n' = B·n とおくと
(B·n)ᵀ · (A·t) = nᵀ·Bᵀ·A·t
元の直交 nᵀ·t = 0 を保つには Bᵀ·A = I が必要
⇒ Bᵀ = A⁻¹ ⇒ B = (A⁻¹)ᵀ (=逆転置)
A が回転のみ(直交行列)なら A⁻¹ = Aᵀ なので (A⁻¹)ᵀ = A となり、逆転置は元の行列と一致します。つまり 回転・一様スケールしか使わないなら法線をモデル行列でそのまま変換してよい(一様スケールは長さが変わるので変換後に再正規化する)。逆転置が本当に効いてくるのは非一様スケールやせん断を含むときです。GPU 側では法線行列をあらかじめ CPU で計算してユニフォームとして渡すのが一般的です。
「なぜ 4x4 か」「w は何を運ぶか」「透視除算はどの段で誰が行うか」「法線の変換行列は」の4点はほぼ定番です。要点は、平行移動を線形化するための同次座標、深度を仕込む w、除算はラスタライズ前の固定機能、法線は逆転置——と即答できること。加えて、クリッピングを除算前に行う理由(負の w で前後が反転する)を説明できると強いです。
まとめ
- MVP は モデル→ビュー→射影 の3変換を 4x4 行列で表し
M_proj·M_view·M_modelと合成する。列ベクトル規約では「後で効かせたい変換ほど左」。 - 同次座標 は座標を1次元増やして平行移動を行列積に載せる仕組み。
w=1が点、w=0が方向ベクトルで、このwを射影が深度に流用する。 - 射影行列は
wに深度を書き込み、GPU の 透視除算 がxyz/wを行って遠近を付け、可視域を NDC(-1〜1 の立方体)へ正規化する。クリッピングは符号情報を保つため除算前に行う。 - 右手系・左手系 の違いは Z の向きと NDC 深度範囲の約束差で、移植時は射影行列の符号とカリング巻き順の不一致に注意する。
- 法線はモデル行列の逆転置
(A⁻¹)ᵀで変換する。非一様スケールで法線が面から傾くのを防ぐためで、回転のみなら元の行列と一致する。
グラフィックス Article
座標変換(モデル・ビュー・射影)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
座標変換
比較で見る軸
難易度: advanced / カテゴリ: グラフィックス / タグ数: 6
導入後に効く点
射影行列は w に深度(-z 等)を仕込み、その後 GPU が xyz を w で割る透視除算で遠近を付け、-1〜1 の NDC に正規化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- グラフィックス
- タグ数
- 6
判断チェックリスト
- 自社の用途が「座標変換 / 同次座標」に近いか確認する。
- 強みである「モデル・ビュー・射影の各行列を掛け合わせ、頂点をワールド→カメラ→クリップ空間へ順に写す。回転と平行移動を1つの行列で扱うために4次元の同次座標を使う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。