床井研究室

バージョンアップ

自宅の iMac の OS を Snow Leopard にしました。OS をバージョンアップをすると定常状態に落ち着くまでしばらくかかるんですけど、今回は更新前と全然変わりなく安定して動いています。いやぁ起動も速いし Firefox もサクサク動くし、Canvas X も Rosetta で一応動きます (日本語の入力ができないけど)。あと、このブログを動かしていたマシン (Celeron 1.4GHz, 箱は10年物!) も、元 M 君マシン (Athlon XP 3200+) に置き換えました。だいぶ軽くなりました。こいつの OS も VineLinux の 5.0 に更新したいんですが、なかなか取りかかれません。tDiary も 2.2.2 に更新したいんですが、なんだかエラーが出るのでちょっとペンディングしています。

クリッピング空間

レンダリングパイプラインにおいてビューポート変換 (screen mapping) は、$-1 \le x \le 1, -1 \le y \le 1, -1 \le z \le 1$ の空間の xy 平面への平行投影像を、ディスプレイ上の描画領域 (ビューポート) に写像します。ラスタライザはビューポートからはみ出た図形を描画できませんから、バーテックスシェーダから出力された図形の、この空間からはみ出た部分は、クリッピングにより削り取られます。この空間をクリッピング空間 (canonical view volume) と呼び、この座標系をクリッピング座標系と呼びます。

クリッピング空間

したがってバーテックスシェーダは、画面に表示しようとする空間がクリッピング空間に収まるように頂点の位置を座標変換して、gl_Position に出力する必要があります。この変換には、一般に平行投影 (orthogonal projection) 変換と透視投影 (perspective projection) 変換が用いられます。なお、平行投影は直交投影とも呼ばれます。また、透視投影は中心投影とも呼ばれます。

平行投影変換

平行投影変換は、x, y, z のそれぞれの軸に垂直な平面に囲まれたビュー空間 (view volume) をクリッピング空間に写像します。

平行投影のビュー空間

この変換は、x 軸方向について、ビュー空間の中心が原点となるように $-(\text{right} + \text{left}) / 2$ だけ平行移動し、その大きさを $2 / (\text{right} - \text{left})$ 倍します。y, z 軸についても同様に考えれば、次の変換行列が得られます。

\[\begin{pmatrix} \frac{2}{\text{right} - \text{left}} & 0 & 0 & 0 \\ 0 & \frac{2}{\text{top} - \text{bottom}} & 0 & 0 \\ 0 & 0 & \frac{-2}{\text{far} - \text{near}} & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} 1 & 0 & 0 & -\frac{\text{right} + \text{left}}{2} \\ 0 & 1 & 0 & -\frac{\text{top} + \text{bottom}}{2} \\ 0 & 0 & 1 & \frac{\text{far} + \text{near}}{2} \\ 0 & 0 & 0 & 1 \end{pmatrix} = \begin{pmatrix} \frac{2}{\text{right} - \text{left}} & 0 & 0 & -\frac{\text{right} + \text{left}}{\text{right} - \text{left}} \\ 0 & \frac{2}{\text{top} - \text{bottom}} & 0 & -\frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} \\ 0 & 0 & \frac{-2}{\text{far} - \text{near}} & -\frac{\text{far} + \text{near}}{\text{far} - \text{near}} \\ 0 & 0 & 0 & 1 \end{pmatrix}\]

この変換行列を OpenGL で使用する場合には、ひとつ注意点があります。いま、行列の各要素 $m_0 \sim m_{15}$ が次のように並んでいるとします。

\[\begin{pmatrix} m_0 & m_1 & m_2 & m_3 \\ m_4 & m_5 & m_6 & m_7 \\ m_8 & m_9 & m_{10} & m_{11} \\ m_{12} & m_{13} & m_{14} & m_{15} \end{pmatrix}\]

これを OpenGL の座標変換に使う場合、行列の各要素を次の順序で配列に格納します。つまり変換行列を配列に格納する際は、行列の要素 $m_0 \sim m_{15}$ の順序を (見かけ上) 転置しなければなりません。

GLfloat matrix[] = {
  m0,  m4,  m8,  m12,
  m1,  m5,  m9,  m13,
  m2,  m6,  m10, m14,
  m3,  m7,  m11, m15,
};

それでは、left, right, bottom, top, zNear, zFar を指定して平行投影変換行列を作成し、引数 matrix に与えられた配列に格納する関数 orthogonalMatrix() を作成してください。

#if defined(_WIN32)
#  define _USE_MATH_DEFINES
#  define _CRT_SECURE_NO_WARNINGS
#  include <GL/glut.h>
#elif defined(__APPLE__)
#  define GL_SILENCE_DEPRECATION
#  include <GLUT/glut.h>
#else
#  define GL_GLEXT_PROTOTYPES
#  include <GL/glut.h>
#endif
#include <math.h>

/*
** 平行投影変換行列を求める
*/
void orthogonalMatrix(float left, float right,
                      float bottom, float top,
                      float zNear, float zFar,
                      GLfloat* matrix)
{
  /* この部分を考えましょう */
}

なお、この関数も static 宣言されていない上に最初のほうに #include があるということは、ソースファイルは別にしてくれってことだからね。

透視投影変換

透視投影のビュー空間は、原点を頂点とした四角錐を z 軸に垂直な二つの平面 (前方面、後方面) で切り取った錐台になります。これを視野錐台あるいは視錐台 (view frustum) と呼びます。透視投影変換は、この空間をクリッピング空間に写像します。

透視投影の視野錐台

この変換は、以下の手順で求めることができます。空間中の点の座標を $(p_x, p_y, p_z)$ とし、その点が視点から z 方向に $-\text{near}$ の位置にある投影面上に投影された位置を $(s_x, s_y)$ とすれば、$s_x = -\text{near} \cdot p_x / p_z, s_y = -\text{near} \cdot p_y / p_z$ になります。また、これらの範囲は $\text{left} \le s_x \le \text{right}, \text{bottom} \le s_y \le \text{top}$ なので、これらを $-1 \le s_x’, s_y’ \le 1$ の範囲に収めるには、$s_x’ = 2 \cdot (s_x - \text{left}) / (\text{right} - \text{left}) - 1, s_y’ = 2 \cdot (s_y - \text{bottom}) / (\text{top} - \text{bottom}) - 1$ とします。これを整理すると、次式が得られます。

\[\begin{aligned} s_x' &= \frac{2\,\text{near}}{\text{right} - \text{left}} \left(-\frac{p_x}{p_z}\right) - \frac{\text{right} + \text{left}}{\text{right} - \text{left}} \\ s_y' &= \frac{2\,\text{near}}{\text{top} - \text{bottom}} \left(-\frac{p_y}{p_z}\right) - \frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} \end{aligned}\]

z 軸方向については、(上の図が左手系であることを考慮して) z 軸を反転して、$[-\text{near}, -\text{far}]$ の範囲を $[-1, 1]$ に写像します。このとき写像後の z 座標値 $s_z’$ は、$s_z$ の逆数に比例している必要があるので、この関数を $s_z’ = A / s_z + B$ とおきます。$s_z = -\text{near} \to s_z’ = -1, s_z = -\text{far} \to s_z’ = 1$ なので、$-1 = A / (-\text{near}) + B, 1 = A / (-\text{far}) + B$ となります。これを整理すると、次式が得られます。

\[A = \frac{2\,\text{far}\,\text{near}}{\text{far} - \text{near}}, \quad B = \frac{\text{far} + \text{near}}{\text{far} - \text{near}}\]

これにより $s_z’$ は次式で求めることができます。

\[s_z' = -\frac{2\,\text{far}\,\text{near}}{\text{far} - \text{near}} \left(-\frac{1}{p_z}\right) + \frac{\text{far} + \text{near}}{\text{far} - \text{near}}\]

ここで点 $(s_x’, s_y’, s_z’)$ を $w = -p_z$ として同次座標で表せば、$(-s_x’ \cdot p_z, -s_y’ \cdot p_z, -s_z’ \cdot p_z, -p_z)$ となります。

\[\begin{aligned} -s_x'\,p_z &= \frac{2\,\text{near}}{\text{right} - \text{left}} \, p_x + \frac{\text{right} + \text{left}}{\text{right} - \text{left}} \, p_z \\ -s_y'\,p_z &= \frac{2\,\text{near}}{\text{top} - \text{bottom}} \, p_y + \frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} \, p_z \\ -s_z'\,p_z &= -\frac{\text{far} + \text{near}}{\text{far} - \text{near}} \, p_z - \frac{2\,\text{far}\,\text{near}}{\text{far} - \text{near}} \end{aligned}\]

これを行列で表せば、次の変換行列が得られます1

\[\begin{pmatrix} \frac{2\,\text{near}}{\text{right} - \text{left}} & 0 & \frac{\text{right} + \text{left}}{\text{right} - \text{left}} & 0 \\ 0 & \frac{2\,\text{near}}{\text{top} - \text{bottom}} & \frac{\text{top} + \text{bottom}}{\text{top} - \text{bottom}} & 0 \\ 0 & 0 & -\frac{\text{far} + \text{near}}{\text{far} - \text{near}} & -\frac{2\,\text{far}\,\text{near}}{\text{far} - \text{near}} \\ 0 & 0 & -1 & 0 \end{pmatrix}\]

それでは、left, right, bottom, top, zNear, zFar を指定して透視投影変換行列を作成し、引数 matrix に与えられた配列に格納する関数 perspectiveMatrix() を作成してください。

/*
** 透視投影変換行列を求める
*/
void perspectiveMatrix(float left, float right,
                       float bottom, float top,
                       float zNear, float zFar,
                       GLfloat* matrix)
{
  /* この部分を考えましょう */
}

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

バーテックスシェーダでの座標変換

バーテックスシェーダで座標変換を行うには、頂点位置が与えられている attribute 変数に変換行列をかけて、gl_Position に代入します。この変換行列や、光源情報や材質情報などのように、1回の描画単位 (glDrawArrays()glDrawElements() などの呼び出し) の間に変化しない値は、uniform 変数に格納します。uniform 変数はバーテックスシェーダ、フラグメントシェーダ、およびジオメトリシェーダから共通して参照できる大域変数(みたいなもの)です。

位置は同次座標 (vec4 型) で計算するので、この変換行列は 4x4 要素である必要があります。このような行列変数は、mat4 型で宣言します。ここではバーテックスシェーダで mat4 型の uniform 変数 projectionMatrix を宣言し、これを用いて attribute 変数 position を座標変換します。

#version 120
//
// simple.vert
//
invariant gl_Position;
attribute vec2 position;
uniform mat4 projectionMatrix;

void main(void)
{
  gl_Position = projectionMatrix * vec4(position, 0.0, 1.0);
}

変換行列の uniform 変数への格納

メインプログラムで平行投影変換行列を求め、それを uniform 変数 projectionMatrix に格納します。まず、前に定義した関数 orthogonalMatrix() の宣言と、投影変換行列を格納する配列 projectionMatrix の宣言、および uniform 変数 projectionMatrix の場所を記憶する変数 projectionMatrixLocation の宣言を追加します。

/*
** プログラムオブジェクト
*/
static GLuint gl2Program;

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

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

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

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

...

次に、初期化の時点で投影変換行列を求めます。ビュー空間は、この図形の右上だけを見ることとして、left = 0, right = 1, bottom = 0, top = 1, zNear = -1, zFar = 1 に設定します。また、同時に uniform 変数 projectionMatrix の場所を探します。uniform 変数が見つからなかったときはエラーなんだけど、いつものごとくエラー処理しとらん。

...

  /* プログラムオブジェクトのリンク結果を得る変数 */
  GLint linked;

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

  /* 平行投影変換行列を求める */
  orthogonalMatrix(0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, projectionMatrix);

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

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

...
GLint glGetUniformLocation(GLuint program, const GLchar* name);
シェーダプログラムで使われている uniform 変数の場所を探します。program にはシェーダプログラムの名前を指定します。name には uniform 変数の変数名を指定します。戻り値は uniform 変数の場所です。name に指定した uniform 変数が見つからなければ、これは -1 になります。

そして図形の描画時に、変換行列の内容を uniform 変数 projectionMatrix に格納します。ここでは glUniformMatrix4fv() の引数 transposeGL_FALSE を指定して、配列の内容を転置せずに格納します。

/*
** 画面表示
*/
static void display(void)
{
  /* 画面クリア */
  glClear(GL_COLOR_BUFFER_BIT);

  /* プログラムオブジェクトを適用する */
  glUseProgram(gl2Program);

  /* 投影変換行列の uniform 変数 projectionMatrix に変換行列の値を設定する */
  glUniformMatrix4fv(projectionMatrixLocation, 1, GL_FALSE, projectionMatrix);

  /* 頂点バッファオブジェクトとして buffer を指定する */
  glBindBuffer(GL_ARRAY_BUFFER, buffer);

  /* index が 0 の attribute 変数を有効にする */
  glEnableVertexAttribArray(0);

  /* index が 0 の attribute 変数に頂点バッファオブジェクトの場所と書式を設定する */
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);

  /* 図形を描く */
  glDrawArrays(GL_LINE_LOOP, 0, 4);

  /* index が 0 の attribute 変数を無効にする */
  glDisableVertexAttribArray(0);

  /* 頂点バッファオブジェクトを解放する */
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  /* 固定機能に戻す*/
  glUseProgram(0);

  glFlush();
}
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);
mat4 型 (4x4 の行列) の uniform 変数に値を格納します。locationglGetUniformLocation() で得られた uniform 変数の場所を指定します。count はデータの個数です。格納する uniform 変数が配列なら、その要素数までの数が指定できます。普通の変数なら 1 です。transposeGL_TRUE にすると引数 value に指定した配列の内容を転置して uniform 変数に格納します。value は uniform 変数に格納するデータの配列です。mat4 型の uniform 変数の要素数は 16 ですから、この配列の要素数は count * 16 個になります。

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

ビュー空間を指定して描画

  1. Eric Lengyel 著、狩野 智英 訳、”ゲームプログラミングのための 3D グラフィックス数学,” pp. 97-99, ボーンデジタル