
2026/09/20 4:43
Deep Divideで即座に3D
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
多くの 3D グラフィックスフレームワークは、カメラの移動背後にある数学を隠蔽していますが、その基本原理は驚くほどシンプルです:3D 世界を 2D スクリーンに投影するのは、基本的な深さに基づく除算によるものです。オブジェクトが視聴者から遠ざかると、その座標を深さ値で割るため、サイズは比例的に縮小し、平行な線は消失点へと収束してリアルな透視図法を生み出します。複雑なツールは視野角やカメラ位置などのパラメータを内部で管理していますが、最終的には視点投影行列を用いて、ワールド座標をnormalized device space に変換します。この行列は焦点スケール(垂直方向の視野角から導出される)やアスペクト比といった因子でパラメータ化され、簡略化すると本質的に $x/z$ という深さ除算に還元されます。この行列はデータを用意し、標準的なグラフィックパイプラインへと渡します。同パイプラインでは、ポイントがワールド空間からビュー空間、クリップ空間を経て、ラスタライゼーションの後に個々のピクセルに変換されます。高度なレンダリングが結局はこの基本的な除算に還元されていることを理解することは、開発者が複雑な公式を暗記したり幾何学的トリックを再発明したりすることなく、なぜ高級ツールがそのように振る舞うかを把握することを可能にします。したがって、これらの標準行列を幾何処理とカリングに活用することで、効率的にレンダリングを実装でき、開発プロセスを簡素化しつつ、様々なアプリケーションで視覚的な精度を保つことができます。
本文
3D グラフィックスの基礎:カメラと透視射影の原理
はじめに:高レベルフレームワークの限界
- ゲーム制作初期は、高レベルなフレームワークを使ってカメラを動かすだけで済ませていました。
- よりクリエイティブな技術の実装を望むようになると、必要な用語がわからず、技術探求が困難でした。
- 低レベルなグラフィックコードを書き始めることで、以下のような事実を理解しました:
- カメラの挙動は、実は非常にシンプルな数学に基づいています。
基本的な射影の原理
優れた記事『One Formula That Demystifies 3D Graphics』(@tsoding)における核心となる式は以下の通りです。
(x, y, z) \\ x' = x/z \\ y' = y/z
概念の解説
- 座標系の定義: (y) を上向き(Up)、(z) を前方(Forward)とします。
- 射影操作: 3 次元座標 ((x, y, z)) は、(x) と (y) の値を (z) で割ることで、2 次元座標 ((x', y')) に投影されます。
深さ変化による消失点への収束
深さ(depth)のみが変化する一連の例では、深さが大きくなるほど投影位置は消失点 ((0,0)) に近づきます。
| 3D 座標 (x,y,z) | 2D 投影 (x',y') |
|---|---|
| (2, 1, 2) | (1, 0.5) |
| (2, 1, 4) | (0.5, 0.25) |
| (2, 1, 8) | (0.25, 0.125) |
原理の確認と応用
- ボールの軌道: カメラ前方へオフセットされた (z) 軸上での回転により、カメラの Up 軸を中心に移動・縮尺するボールでこの原理が実証されます。
- ジオメトリの描画: この原則を用いれば、より高度な「ジオメトリ」も同様の方法で描くことが可能です。
⚠️ 注意点: これらの例は素朴で制約が大きいため、例外を除いてそのまま実用的ではありません。実際の 3D 制作では、カメラの向き(視線方向)と位置、視野角(Field of View) を扱う必要があります。
透視射影行列(Perspective Projection Matrix)
Shader に機能を追加するよりも、労力が少なく且つ実践的な解決策が存在します。それが透視射影行列です。ここにはmighty Camera の正体が秘められています。
行列の構成
用途や分野(コンピュータビジョン vs グラフィックス)によって規約が異なりますが、三角形ベースのグラフィックでは以下のパラメータを用いた一般的な規約がよく採用されます。
- 視野角(Field of View)
- アスペクト比
- ニア・ファークリッピングプレーン
これらの値は、物体の有無を判別する基準となり、オフスクリーンジオメトリの排除(カリング)や効率的なレンダリングに不可欠です。
行列構造とパラメータ定義
一般的な構造は以下のようになります(座標系規約はコンセンサスが得られていないため多少異なりますが、大まかな構造は一貫しています)。
P = \begin{bmatrix} \frac{f}{a} & 0 & 0 & 0 \\ 0 & f & 0 & 0 \\ 0 & 0 & A & B \\ 0 & 0 & 1 & 0 \end{bmatrix}
各パラメータの定義:
- (f) (焦点スケール): 垂直方向の視野角 (\theta) から導出。
f = \frac{1}{\tan(\theta/2)} - (a) (アスペクト比):
a = \frac{\text{width}}{\text{height}} - (A, B) (深さマッピング): ニアカリッピングプレーン (n) とファークリッピングプレーン (F) から導出。
A = \frac{F+n}{F-n}, \quad B = -\frac{2Fn}{F-n}
行列乗算と透视除算
3D 空間内の点に対して、以下の処理フローを行います。
- 世界座標系からビュー座標系(カメラの原点に合わせる)へ移行する。
- 透視射影行列 (P) を掛けてクリップ座標系へと変換する。
- ここで準備された状態により、後段で透视除算(perspective division) を行えるようにします。
乗算処理:
\begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} \times P = \begin{bmatrix} \frac{f}{a}x \\ fy \\ Az + B \\ z \end{bmatrix}
この結果を、最初の 3 成分を最後の成分 (w)(すなわち (z))で割った後、以下のようになります:
- シーン内の距離を表さなくなります。
- カメラの可視範囲内での相対的位置を示すようになります。
これを**正規化されたデバイス座標(NDC)**と呼びます。最終的に点の座標はスクリーン出力解像度にマッピングされます:
x_{ndc} = \frac{\frac{f}{a}x}{z}, \quad y_{ndc} = \frac{fy}{z}
深さによる除算のトリックとの関係
前述の簡易的な例では (f) と (a) が省略されており、両方を 1 に置き換えると元の「深さによる除算のトリック」が得られます。
x_{ndc} = \frac{x}{z}, \quad y_{ndc} = \frac{y}{z}
✅ 結論: 単純なシナリオにおけるカメラ相対点には有効ですが、グラフィックパイプライン全体においてはほん一つのステップに過ぎません。
グラフィックパイプラインの流れ
正式な名前がつけられた各項を踏まえると、以下の一連の流れで処理が行われます:
- world: 世界座標系
- $\rightarrow$ view: ビュー変換(カメラ相対へ)
- $\rightarrow$ clip: クリップ座標系への変換(射影行列適用)
- $\xrightarrow{/w}$ NDC: 透视除算(w で割る)
- $\rightarrow$ pixels: デバイス座標へのマッピング
- $\rightarrow$ rasterization: ラスタライズ化
まとめ:カメラの本質を理解する
この解説から最も重要なことは以下の点です。
- カメラは単にいくつかの変換を実行しているだけでなく、各変換が果たす役割を理解する必要があります。
- 要件と制約に基づいて、必要な部分だけを選択的に実装できるようになります。
- パイプライン全体が必要な場合もあれば
- 単なる深さによる除算だけで十分な場合もあるのです。