床井研究室

クリッピング空間の奥行き

OpenGL の解説には、しばしば「初期状態では、視線は Z 軸の負の方向に向いている」と書かれています (私もそう書いてしまいます↓)。xy 平面については確かにその通りなんですけど、z 方向についてこれを鵜呑みにすると、混乱することがあります (私はしました)。

クリッピング空間上では、z 軸の正の方向 (視線の反対方向) が奥、負の方向が手前になります。したがって、クリッピング空間の $z = 1$ の平面が後方面であり、$z = -1$ の平面が前方面になります。このためクリッピング座標系上で隠面消去を行うと、視線方向に対して手前にあるものが、奥のものに隠されてしまいます。ビュー空間を設定する際に $\text{near} < \text{far}$ とすれば、頂点の座標値の z 値の符号が反転するので、奥のものが手前のものに隠されるようになります。

ビュー変換行列

平行投影変換の次は透視投影変換をやりたいところですが、サンプルのプログラムは図形も視点も xy 平面上にあるので、視点を動かさないと透視投影変換ができません。なので先に視点の移動を行います。

クリッピング空間では、視点は原点にあり、視線は z 軸の負の方向を向いています (^_^;) これを任意の位置から任意の方向を見ることができるようにします。このような変換をビュー変換 (viewing transform) と呼びます。いま、視点が $\mathbf{e} = (ex, ey, ez)$ の位置にあり、目標点 $\mathbf{t} = (tx, ty, tz)$ の方向を向いているとします。また視点の「上方向」は $\mathbf{u} = (ux, uy, uz)$ とします。

視点の位置と方向

変換行列 $\mathbf{T}$ を用いて、この視点が原点になるように平行移動します。$\mathbf{e}$, $\mathbf{t}$ は位置なので、これらを同次座標で表せば、それぞれ $(ex, ey, ez, 1)^T$, $(tx, ty, tz, 1)^T$ となります。これに対して $\mathbf{u}$ はベクトルなので、同次座標は $(ux, uy, uz, 0)^T$ になり、$\mathbf{T}$ をかけても変化しません。

\[\mathbf{T} = \begin{pmatrix} 1 & 0 & 0 & -ex \\ 0 & 1 & 0 & -ey \\ 0 & 0 & 1 & -ez \\ 0 & 0 & 0 & 1 \end{pmatrix}\]

また、視点座標系の基底ベクトル (軸ベクトル) $\mathbf{x}’$, $\mathbf{y}’$, $\mathbf{z}’$ を求めます。$\mathbf{z}’$ は視線の逆ベクトルを正規化して $\mathbf{z}’ = (\mathbf{e} - \mathbf{t}) / \vert\mathbf{e} - \mathbf{t}\vert$, $\mathbf{x}’$ は $\mathbf{u}$ と $\mathbf{z}’$ に対して垂直なので $\mathbf{x}’ = (\mathbf{u} \times \mathbf{z}’) / \vert\mathbf{u} \times \mathbf{z}’\vert$, $\mathbf{y}’$ は $\mathbf{z}’$ と $\mathbf{x}’$ に垂直なので $\mathbf{y}’ = \mathbf{z}’ \times \mathbf{x}’$ となります。

視点座標系の基底ベクトル

この視点座標系の基底ベクトルが、x, y, z 軸と一致するように回転します。

視点座標系の基底ベクトルを x, y, z 軸と一致させる

この変換行列 $\mathbf{R}$ は、次のようになります (ううう、式を間違えた - 9月8日修正)。なんでそうなるかは、たとえば x, y, z の各軸ベクトル $(1, 0, 0)$, $(0, 1, 0)$, $(0, 0, 1)$ を、それぞれ $(x’_x, x’_y, x’_z)$, $(y’_x, y’_y, y’_z)$, $(z’_x, z’_y, z’_z)$ に変換する行列を考えてみるとわかります。

\(\mathbf{R} = \begin{pmatrix} 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{pmatrix}\) \(\begin{aligned} \mathbf{z}' &= (z'_x \quad z'_y \quad z'_z) = \frac{(ex - tx \quad ey - ty \quad ez - tz)}{\sqrt{(ex - tx)^2 + (ey - ty)^2 + (ez - tz)^2}} \\ \mathbf{x}' &= (x'_x \quad x'_y \quad x'_z) = \frac{(u_x \quad u_y \quad u_z) \times (z'_x \quad z'_y \quad z'_z)}{\sqrt{|(u_x \quad u_y \quad u_z) \times (z'_x \quad z'_y \quad z'_z)|^2}} \\ \mathbf{y}' &= (y'_x \quad y'_y \quad y'_z) = (z'_x \quad z'_y \quad z'_z) \times (x'_x \quad x'_y \quad x'_z) \end{aligned}\)

したがってビュー変換行列 $\mathbf{RT}$ は、次式により求められます。

\[\begin{aligned} \mathbf{RT} &= \begin{pmatrix} 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{pmatrix} \begin{pmatrix} 1 & 0 & 0 & -ex \\ 0 & 1 & 0 & -ey \\ 0 & 0 & 1 & -ez \\ 0 & 0 & 0 & 1 \end{pmatrix} \\ &= \begin{pmatrix} x'_x & x'_y & x'_z & -ex\,x'_x - ey\,x'_y - ez\,x'_z \\ y'_x & y'_y & y'_z & -ex\,y'_x - ey\,y'_y - ez\,y'_z \\ z'_x & z'_y & z'_z & -ex\,z'_x - ey\,z'_y - ez\,z'_z \\ 0 & 0 & 0 & 1 \end{pmatrix} \end{aligned}\]

それでは、視点位置 ex, ey, ez, 目標点位置 tx, ty, tz, 上方向のベクトル ux, uy, uz としてビュー変換行列を作成し、引数 matrix に与えられた配列に格納する関数 lookAt() を作成してください。

/*
** ビュー変換行列を求める
*/
void lookAt(float ex, float ey, float ez,
            float tx, float ty, float tz,
            float ux, float uy, float uz,
            GLfloat* matrix)
{
  /* この部分を考えましょう */
}

この関数も orthogonalMatrix() と同じファイルに書いといてね。

行列の積

視点の移動を行うには、頂点の座標値に orthogonalMatrix() で作った投影変換行列をかけたものに、lookAt() で作ったビュー変換行列をかける必要があります。この計算はバーテックスシェーダで行うこともできますが、先に投影変換行列とビュー変換行列の積を求めておけば、バーテックスシェーダの負担を減らすことができます。4行4列の行列の積は、次式により求められます。

\[\begin{pmatrix} r_{00} & r_{01} & r_{02} & r_{03} \\ r_{10} & r_{11} & r_{12} & r_{13} \\ r_{20} & r_{21} & r_{22} & r_{23} \\ r_{30} & r_{31} & r_{32} & r_{33} \end{pmatrix} = \begin{pmatrix} p_{00} & p_{01} & p_{02} & p_{03} \\ p_{10} & p_{11} & p_{12} & p_{13} \\ p_{20} & p_{21} & p_{22} & p_{23} \\ p_{30} & p_{31} & p_{32} & p_{33} \end{pmatrix} \begin{pmatrix} q_{00} & q_{01} & q_{02} & q_{03} \\ q_{10} & q_{11} & q_{12} & q_{13} \\ q_{20} & q_{21} & q_{22} & q_{23} \\ q_{30} & q_{31} & q_{32} & q_{33} \end{pmatrix}\] \[r_{ij} = \sum_{k=0}^3 p_{ik} q_{kj}\]

それでは、引数 m0 に指定された配列に格納されている行列と引数 m1 に指定された配列に格納されている行列の積を求め、引数 matrix に与えられた配列に格納する関数 multiplyMatrix() を作成してください。

/*
** 行列 m0 と m1 の積を求める
*/
void multiplyMatrix(const GLfloat* m0, const GLfloat* m1, GLfloat* matrix)
{
  /* この部分を考えましょう */
}

ついでだから、この関数も orthogonalMatrix() と同じファイルに書いといてね。

視点の移動

作った関数 lookAt() を使って、実際に視点を移動します。まず、lookAt()multiplyMatrix() を呼び出すために、これらの関数の宣言をメインプログラムに追加します。

...

/*
** 直交投影変換行列を求める
*/
extern void orthogonalMatrix(float left, float right,
                             float bottom, float top,
                             float zNear, float zFar,
                             GLfloat* matrix);

/*
** ビュー変換行列を求める
*/
extern void lookAt(float ex, float ey, float ez,
                   float tx, float ty, float tz,
                   float ux, float uy, float uz,
                   GLfloat* matrix);

/*
** 行列の積を求める
*/
extern void multiplyMatrix(const GLfloat* m0,
                           const GLfloat* m1,
                           GLfloat* matrix);

/*
** 投影変換行列
*/
static GLfloat projectionMatrix[16];

/*
** 投影変換行列の uniform 変数の場所
*/
static GLint projectionMatrixLocation;

/*
** attribute 変数 position の頂点バッファオブジェクト
*/
static GLuint buffer;

...

そして、初期化の段階でビュー変換行列を求め、投影変換行列に掛け合わせます。このために、ビュー変換行列を一時的に格納する配列変数 viewing と、投影変換行列を一時的に保存しておく配列変数 perspective を用意しておきます(平行投影変換の場合は orthogonal など)。

...

/*
** 初期化
*/
static void init(void)
{
  /* シェーダプログラムのコンパイル/リンク結果を得る変数 */
  GLint compiled, linked;

  /* 頂点バッファオブジェクトのメモリを参照するポインタ */
  typedef GLfloat Position[2];
  Position* position;

  ...

ビュー変換行列を viewing に求めます。視点の位置は (4, 5, 6), 目標点の位置は (0, 0, 0) にします。また上方向のベクトルは (0, 1, 0) にします。

...

  /* シェーダプログラムのリンク */
  glLinkProgram(gl2Program);
  glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked);
  printProgramInfoLog(gl2Program);
  if (linked == GL_FALSE) {
    fprintf(stderr, "Link error.\n");
    exit(1);
  }

  /* ビュー変換行列を求める */
  GLfloat viewing[16];
  lookAt(4.0f, 5.0f, 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, viewing);

ビュー空間は、xy 平面上では $-1 \le x, y \le 1$ とします。zNearzFar は、視点の原点からの距離が約 9 なので、$\pm 2$ の範囲をとって、それぞれ 7 と 11 とします。得られた投影変換行列は orthogonal に格納します。

  /* 平行投影変換行列を求める */
  GLfloat orthogonal[16];
  orthogonalMatrix(-1.0f, 1.0f, -1.0f, 1.0f, 7.0f, 11.0f, orthogonal);

その後、ビュー変換行列 viewing と投影変換行列 orthogonal をかけて、projectionMatrix に格納します。この projectionMatrix に格納した行列は、uniform 変数 projectionMatrix に渡されます。

  /* ビュー変換行列と投影変換行列の積を projectionMatrix に入れる */
  multiplyMatrix(viewing, orthogonal, projectionMatrix);

  /* uniform 変数 projectionMatrix の場所を得る */
  projectionMatrixLocation = glGetUniformLocation(gl2Program, "projectionMatrix");

  /* 頂点バッファオブジェクトを1つ作る */
  glGenBuffers(1, &buffer);

  ...

これで下のような図形が描かれれば OK です。

視点を移動して描画

透視投影変換による描画

それでは、当初の目的である透視投影変換による描画を行います。まず、透視投影変換行列を求める関数 perspectiveMatrix() を呼び出すために、この関数の宣言をメインプログラムに追加します。

...

/*
** 直交投影変換行列を求める
*/
extern void orthogonalMatrix(float left, float right,
                             float bottom, float top,
                             float zNear, float zFar,
                             GLfloat* matrix);

/*
** 透視投影変換行列を求める
*/
extern void perspectiveMatrix(float left, float right,
                              float bottom, float top,
                              float zNear, float zFar,
                              GLfloat* matrix);

/*
** ビュー変換行列を求める
*/
extern void lookAt(float ex, float ey, float ez,
                   float tx, float ty, float tz,
                   float ux, float uy, float uz,
                   GLfloat* matrix);

/*
** 行列の積を求める
*/
extern void multiplyMatrix(const GLfloat* m0,
                           const GLfloat* m1,
                           GLfloat* matrix);

/*
** 投影変換行列
*/
static GLfloat projectionMatrix[16];

/*
** 投影変換行列の uniform 変数の場所
*/
static GLint projectionMatrixLocation;

/*
** attribute 変数 position の頂点バッファオブジェクト
*/
static GLuint buffer;

...

そして、平行投影変換行列 orthogonalMatrix() を呼び出している部分を、perspectiveMatrix() に置き換えます。

...

  /* シェーダプログラムのリンク */
  glLinkProgram(gl2Program);
  glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked);
  printProgramInfoLog(gl2Program);
  if (linked == GL_FALSE) {
    fprintf(stderr, "Link error.\n");
    exit(1);
  }

  /* 透視投影変換行列を求める */
  GLfloat perspective[16];
  perspectiveMatrix(-1.0f, 1.0f, -1.0f, 1.0f, 7.0f, 11.0f, perspective);

  /* ビュー変換行列を求める */
  GLfloat viewing[16];
  lookAt(4.0f, 5.0f, 6.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, viewing);

  /* ビュー変換行列と投影変換行列の積を projectionMatrix に入れる */
  multiplyMatrix(viewing, perspective, projectionMatrix);

  /* uniform 変数 projectionMatrix の場所を得る */
  projectionMatrixLocation = glGetUniformLocation(gl2Program, "projectionMatrix");

  /* 頂点バッファオブジェクトを1つ作る */
  glGenBuffers(1, &buffer);

  ...

これで透視投影変換による描画が行われるはずです。下のような図形が描かれれば OK です。

透視投影変換による描画

一応、ここまでのプログラムをまとめたものを、以下に用意しておきます。