Skip to content

*)ワールド座標からNDCの算出方法

最終更新日時: 2025年08月25日 12:57

  • nothing

3D座標変換処理: ワールド座標からNDCへの流れ

Section titled “3D座標変換処理: ワールド座標からNDCへの流れ”

3D空間の点をスクリーン上に描画するためには、一連の座標変換が必要となる。本レポートでは、まず3D座標変換の理論と数式を説明し、次に具体例として点(1,1,1)がカメラ位置(5,0,0)からどのように変換されるかを示す。最後にこれらの変換を実装するサンプルコードを提示する。

3Dグラフィックスでは、右手座標系を標準として使用することが多い。

  • 右手座標系:
    • X軸: 右方向が正
    • Y軸: 上方向が正
    • Z軸: 奥から手前へ向かう方向が正(観察者に向かう方向)

この右手座標系に基づいて議論をする

  • ワールド座標系
    • 3D空間全体の基準となる右手座標系
  • ビュー座標系
    • カメラ座標系とも呼ばれることがある
    • カメラを原点とし、カメラの向きに基づいた座標系。
    • カメラのZ軸は一般的にカメラから見た奥行きの逆方向(つまりカメラに向かう方向)を表す
  • クリップ空間
    • 射影変換後の4次元座標空間
  • 正規化デバイス座標(NDC)
    • クリップ空間から透視除算後の3次元座標空間

3Dレンダリングパイプラインにおける座標変換の流れ

Section titled “3Dレンダリングパイプラインにおける座標変換の流れ”
  • ビュー行列による変換
    • ワールド座標系 → ビュー座標系
  • 射影行列による変換
    • ビュー座標系 → クリップ空間
  • 透視除算による変換
    • クリップ空間 → 正規化デバイス座標(NDC)
  • ビュー座標系のZ軸
    • カメラから対象を見る方向の逆(カメラに向かう方向、正規化)
  • ビュー座標系のX軸
    • 上方向ベクトルとZ軸の外積(正規化)
  • ビュー座標系のY軸
    • Z軸とX軸の外積

これらの軸と位置を元に、ビュー行列が構築される:

V=[xxyxzx0xyyyzy0xzyzzz0pxpypz1]V = \begin{bmatrix} x_x & y_x & z_x & 0 \\ x_y & y_y & z_y & 0 \\ x_z & y_z & z_z & 0 \\ -\mathbf{p} \cdot \mathbf{x} & -\mathbf{p} \cdot \mathbf{y} & -\mathbf{p} \cdot \mathbf{z} & 1 \end{bmatrix}

ここで、x=(xx,yx,zx)\mathbf{x}=(x_x,y_x,z_x)y=(xy,yy,zy)\mathbf{y}=(x_y,y_y,z_y)z=(xz,yz,zz)\mathbf{z}=(x_z,y_z,z_z) はカメラ座標系の各軸ベクトル、p\mathbf{p}はカメラの位置ベクトルを表す。

  • ビュー変換は同次座標系を用いたアフィン変換として定義できる。
  • 具体的には「ワールド座標系からカメラ座標系への回転変換」と「カメラ位置を原点へ移動させる平行移動変換」の合成写像として表現される。

カメラ位置 p\mathbf{p} を原点に移動させる行列:

T=[100001000010pxpypz1]T = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ -p_x & -p_y & -p_z & 1 \end{bmatrix}

カメラ座標系からワールド座標系への回転行列:

Rworld_from_camera=[xxxyxz0yxyyyz0zxzyzz00001]R_{world\_from\_camera} = \begin{bmatrix} x_x & x_y & x_z & 0 \\ y_x & y_y & y_z & 0 \\ z_x & z_y & z_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

ワールド座標系からカメラ座標系への回転行列(直交行列なので転置):

Rcamera_from_world=Rworld_from_cameraT=[xxyxzx0xyyyzy0xzyzzz00001]R_{camera\_from\_world} = R_{world\_from\_camera}^T = \begin{bmatrix} x_x & y_x & z_x & 0 \\ x_y & y_y & z_y & 0 \\ x_z & y_z & z_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

ビュー行列 VV は回転と平行移動の合成:

V=Rcamera_from_worldTV = R_{camera\_from\_world} \cdot T

行列の積を計算:

V=[xxyxzx0xyyyzy0xzyzzz00001][100001000010pxpypz1]V = \begin{bmatrix} x_x & y_x & z_x & 0 \\ x_y & y_y & z_y & 0 \\ x_z & y_z & z_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ -p_x & -p_y & -p_z & 1 \end{bmatrix}

積の結果:

V=[xxyxzx0xyyyzy0xzyzzz0(xxpx+xypy+xzpz)(yxpx+yypy+yzpz)(zxpx+zypy+zzpz)1]V = \begin{bmatrix} x_x & y_x & z_x & 0 \\ x_y & y_y & z_y & 0 \\ x_z & y_z & z_z & 0 \\ -(x_x p_x + x_y p_y + x_z p_z) & -(y_x p_x + y_y p_y + y_z p_z) & -(z_x p_x + z_y p_y + z_z p_z) & 1 \end{bmatrix}

内積表記で整理:

V=[xxyxzx0xyyyzy0xzyzzz0pxpypz1]V = \begin{bmatrix} x_x & y_x & z_x & 0 \\ x_y & y_y & z_y & 0 \\ x_z & y_z & z_z & 0 \\ -\mathbf{p} \cdot \mathbf{x} & -\mathbf{p} \cdot \mathbf{y} & -\mathbf{p} \cdot \mathbf{z} & 1 \end{bmatrix}

この行列によりワールド座標系の点をカメラ座標系へ変換できる。

透視射影変換は、カメラの視野角、アスペクト比、近平面、遠平面に基づいて定義される:

P=[faspect0000f0000far+nearnearfar1002farnearnearfar0]P = \begin{bmatrix} \frac{f}{aspect} & 0 & 0 & 0 \\ 0 & f & 0 & 0 \\ 0 & 0 & \frac{far+near}{near-far} & -1 \\ 0 & 0 & \frac{2 \cdot far \cdot near}{near-far} & 0 \end{bmatrix}

ここで:

  • f=cot(fov2)=1tan(fov2)f = \cot(\frac{fov}{2}) = \frac{1}{\tan(\frac{fov}{2})}
  • fovfov は視野角
  • aspectaspect はアスペクト比
  • nearnear は近平面の距離
  • farfar は遠平面の距離

クリップ空間の座標 (x,y,z,w)(x, y, z, w) から正規化デバイス座標(NDC)への変換は次式で行われる:

NDC=(xwywzw)NDC = \begin{pmatrix} \frac{x}{w} \\ \frac{y}{w} \\ \frac{z}{w} \end{pmatrix}

通常、NDC座標は [1,1]3[-1, 1]^3 の立方体内に収まるように設計される。

  • 右手座標系を使用
    • X軸: 右方向が正
    • Y軸: 上方向が正
    • Z軸: 奥から手前へ向かう方向が正(観察者に向かう方向)
  • カメラ設定
    • 位置: (5,0,0)(5,0,0)
    • 注視点: 原点(0,0,0)(0,0,0)
    • 上方向: (0,1,0)(0,1,0)
  • 変換対象の点: (1,1,1)(1,1,1)
  • 射影パラメータ
    • 視野角: 45°45°
    • アスペクト比: 1.01.0
    • 近平面: 0.10.1
    • 遠平面: 100.0100.0
  1. Z軸の計算: カメラから注視点への方向の逆ベクトル: (5,0,0)(0,0,0)=(5,0,0)(5,0,0) - (0,0,0) = (5,0,0) 正規化すると: (1,0,0)(1,0,0)

  2. X軸の計算: 上方向(0,1,0)(0,1,0)とZ軸(1,0,0)(1,0,0)の外積: (0,0,1)(0,0,-1)

  3. Y軸の計算: Z軸(1,0,0)(1,0,0)とX軸(0,0,1)(0,0,-1)の外積: (0,1,0)(0,1,0)

上記の計算結果から、ビュー行列の値は次のようになる:

V=[0010010010000001]V = \begin{bmatrix} 0 & 0 & -1 & 0 \\ 0 & 1 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

パラメータを元に計算すると f=1tan(22.5°)2.414f = \frac{1}{\tan(22.5°)} \approx 2.414 となり、射影行列は:

P=[2.41400002.41400001.0021000.2000]P = \begin{bmatrix} 2.414 & 0 & 0 & 0 \\ 0 & 2.414 & 0 & 0 \\ 0 & 0 & -1.002 & -1 \\ 0 & 0 & -0.200 & 0 \end{bmatrix}
  1. ワールド座標系の点(1,1,1)(1,1,1)を同次座標(1,1,1,1)(1,1,1,1)で表現

  2. ビュー変換:

    V(1111)=(1116)V \cdot \begin{pmatrix} 1 \\ 1 \\ 1 \\ 1 \end{pmatrix} = \begin{pmatrix} -1 \\ 1 \\ 1 \\ 6 \end{pmatrix}
  3. 射影変換:

    P(1116)=(2.4142.4147.0020.200)P \cdot \begin{pmatrix} -1 \\ 1 \\ 1 \\ 6 \end{pmatrix} = \begin{pmatrix} -2.414 \\ 2.414 \\ -7.002 \\ -0.200 \end{pmatrix}
  4. 透視除算によるNDC変換:

    \begin{pmatrix} \frac{-2.414}{-0.200} \\ \frac{2.414}{-0.200} \\ \frac{-7.002}{-0.200} \end{pmatrix} = \begin{x} 12.059 \\ -12.059 \\ 34.975 \end{pmatrix}
  • 得られたNDC座標(12.059,12.059,34.975)(12.059, -12.059, 34.975)[1,1]3[-1, 1]^3の範囲を大きく超えており、点(1,1,1)(1,1,1)はビューフラスタムの外部にある
  • またww成分が負値(0.200)(-0.200)であることから、この点はカメラの背後に位置していることがわかる

以下は上記の変換を実装するJavaScriptコードである:

// ベクトルと行列の計算に使用する関数
function multiplyMatrixVector(matrix, vector) {
const result = [0, 0, 0, 0];
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
result[i] += matrix[i * 4 + j] * vector[j];
}
}
return result;
}
function normalizeVector(vector) {
const length = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
return [vector[0] / length, vector[1] / length, vector[2] / length];
}
function crossProduct(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
];
}
// カメラの設定
const cameraPosition = [5, 0, 0];
const target = [0, 0, 0]; // 原点を見る
const up = [0, 1, 0]; // 上方向ベクトル
// カメラ座標系の計算
// Z軸: カメラから対象への方向の逆(右手座標系ではカメラに向かう方向が正)
const zAxis = normalizeVector([
cameraPosition[0] - target[0],
cameraPosition[1] - target[1],
cameraPosition[2] - target[2]
]);
// X軸: 上方向ベクトルとZ軸の外積
const xAxis = normalizeVector(crossProduct(up, zAxis));
// Y軸: Z軸とX軸の外積
const yAxis = crossProduct(zAxis, xAxis);
// ビュー行列の作成
const viewMatrix = [
xAxis[0], yAxis[0], zAxis[0], 0,
xAxis[1], yAxis[1], zAxis[1], 0,
xAxis[2], yAxis[2], zAxis[2], 0,
-(xAxis[0] * cameraPosition[0] + xAxis[1] * cameraPosition[1] + xAxis[2] * cameraPosition[2]),
-(yAxis[0] * cameraPosition[0] + yAxis[1] * cameraPosition[1] + yAxis[2] * cameraPosition[2]),
-(zAxis[0] * cameraPosition[0] + zAxis[1] * cameraPosition[1] + zAxis[2] * cameraPosition[2]),
1
];
// 射影行列の作成(透視投影)
const fov = 45 * Math.PI / 180; // 視野角(ラジアン)
const aspect = 1.0; // アスペクト比
const near = 0.1; // 近平面
const far = 100.0; // 遠平面
const f = 1.0 / Math.tan(fov / 2);
const projectionMatrix = [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) / (near - far), -1,
0, 0, (2 * far * near) / (near - far), 0
];
// 元の点の設定と変換
const originalPoint = [1, 1, 1, 1]; // 同次座標系で表現
// ビュー変換
const pointInViewSpace = multiplyMatrixVector(viewMatrix, originalPoint);
// 射影変換
const pointInClipSpace = multiplyMatrixVector(projectionMatrix, pointInViewSpace);
// NDCへの変換
const ndcPoint = [
pointInClipSpace[0] / pointInClipSpace[3],
pointInClipSpace[1] / pointInClipSpace[3],
pointInClipSpace[2] / pointInClipSpace[3]
];
console.log("ビュー空間での点:", pointInViewSpace);
console.log("クリップ空間での点:", pointInClipSpace);
console.log("NDC座標:", ndcPoint);
  • ビュー行列(View Matrix): ワールド座標からカメラ座標への変換行列
  • 射影行列(Projection Matrix): カメラ座標からクリップ空間への変換行列
  • クリップ空間(Clip Space): 射影変換後の4次元空間で、透視除算前の座標空間
  • 正規化デバイス座標(NDC): クリップ空間からw成分による除算後の座標で、通常は [1,1]3[-1, 1]^3 の立方体内に収まる
  • 透視除算(Perspective Division): クリップ空間座標のxyz成分をw成分で割る操作
  • ビューフラスタム(View Frustum): カメラから見える3D空間の領域を表す切頭四角錐