床井研究室

球に Dot3 バンプマッピング

前回の最後で「Dot3 バンプマッピングは面倒」みたいなことを書いたんですが,一応,接空間の算出が容易な球に対してバンプマッピングを行うサンプルを書いてみました.

こんな感じです.

球にバンプマッピング(1) 球にバンプマッピング(2)
球にバンプマッピング(1) 球にバンプマッピング(2)
球にバンプマッピング(3) 球にバンプマッピング(4)
球にバンプマッピング(3) 球にバンプマッピング(4)

接空間の設定

前回は単一のポリゴンに対して Dot3 バンプマッピングを行ったので,ポリゴンをローカル座標系の xy 平面上に置いて,法線マップの座標系をポリゴンの接空間と一致させました.しかし,複数のポリゴンで構成された物体に対して Dot3 バンプマッピングを施す場合は,そういう手抜きができません.

そこで今回は,ポリゴンの一つ一つの頂点に,法線マップの貼付け方に合わせて接空間を設定する方法について説明します.例として,ここでは球に対して下図のように法線マップを貼り付ける場合について考えます.この貼り付け方では極点部分がつぶれてしまいますが,そのあたりは大目に見てください.

法線マップの貼り付け方

この球面上の一点における接空間は,次のようにして設定します.まず,この点における法線単位ベクトル $\mathbf{n}$ を求めます.これをこの接空間の基底ベクトル(軸ベクトル)の z 軸 ( $\mathbf{z}_t$ ) に用います.

次に物体の中心軸を y 軸 とし,これと $\mathbf{z}_t$ との外積を求め,正規化します.このベクトルはこの点における接線ベクトルになります.これを接空間の基底ベクトルの x 軸 ( $\mathbf{x}_t$ ) とします.

最後に $\mathbf{z}$t と $\mathbf{x}$t の外積を求めて正規化し,この点における従法線ベクトルを求めます.これを接空間の基底ベクトルの y 軸 ( $\mathbf{y}$t ) とします.

接空間の設定

この手順をプログラムにすると,こんな具合になります(ファイル sphere.cpp).このプログラムでは光源位置の変換に使う行列(後述)の算出まで行っています.

/*
** ローカル座標系から n を法線ベクトルとする接空間の座標系への変換行列 t を求める
*/
static void localToTangent(const double n[3], double t[16])
{
  double l = n[0] * n[0] + n[2] * n[2];
  double a = sqrt(l);

  /* 接空間のX軸 = (0, 1, 0) × n */
  if (a > 0) {
    t[ 0] = n[2] / a;
    t[ 8] = -n[0] / a;
  }
  else {
    t[ 0] = 0.0;
    t[ 8] = 0.0;
  }
  t[ 4] = 0.0;
  t[ 3] = 0.0;

  /* 接空間のY軸 = Z軸 (= n) × X軸 */
  t[ 1] = -n[1] * n[0];
  t[ 5] = l;
  t[ 9] = -n[1] * n[2];
  a = sqrt(t[ 1] * t[ 1] + t[ 5] * t[ 5] + t[ 9] * t[ 9]);
  if (a > 0) {
    t[ 1] /= a;
    t[ 5] /= a;
    t[ 9] /= a;
  }
  else {
    t[ 1] = 0.0;
    t[ 5] = 0.0;
    t[ 9] = 0.0;
  }
  t[ 7] = 0.0;

  /* 接空間のZ軸 (= n) */
  t[ 2] = n[0];
  t[ 6] = n[1];
  t[10] = n[2];
  t[11] = 0.0;

  t[12] = 0.0;
  t[13] = 0.0;
  t[14] = 0.0;
  t[15] = 1.0;
}

接空間における光源位置の算出

接空間の基底ベクトルが求められたら,それから接空間における光源の位置を求める変換行列 $\mathbf{T}$ を計算します.

球面上の接空間のベクトルを $\mathbf{x}_t$,$\mathbf{y}_t$,$\mathbf{z}_t$ とするとき,

\[\mathbf{x}_t = \begin{pmatrix} x_x\\ y_x\\ z_x \end{pmatrix} ,\; \mathbf{y}_t = \begin{pmatrix} y_x\\ y_x\\ y_x \end{pmatrix} ,\; \mathbf{z}_t = \begin{pmatrix} z_x\\ z_x\\ z_x \end{pmatrix}\]

接空間の規定行列 $\mathbf{M}_t$ は,

\[\mathbf{M}_t = \begin{pmatrix} \mathbf{x}_t & \mathbf{y}_t & \mathbf{z}_t \end{pmatrix} = \begin{pmatrix} x_x&x_y&x_z\\ y_x&y_y&y_z\\ z_x&z_y&z_z \end{pmatrix}\]

となります.したがって,球のローカル座標系における光源方向を,

\[\mathbf{l} = \begin{pmatrix} l_x\\ l_y\\ l_z \end{pmatrix}\]

とするとき,$\mathbf{l}$ の接空間における方向 $\mathbf{l}_t$ は,

\[\mathbf{l}_t = \mathbf{M}_t^{-1} \mathbf{l}= \mathbf{M}_t^{\top} \mathbf{l} = \begin{pmatrix} x_x&y_x&z_x\\ x_y&y_y&z_y\\ x_z&y_z&z_z \end{pmatrix} \begin{pmatrix} l_x\\ l_y\\ l_z \end{pmatrix}\]

となります.

関数 normalizeTexCoord() は,この $\mathbf{M}_t^{\top} = \mathbf{T}$ を使って接空間における光源位置を求め,それを正規化マップのテクスチャ座標に設定します.

まず,光源が平行光線でなければ,ローカル座標系における光源位置(引数 ll)の実座標を求めておきます.そして,これを $\mathbf{T}$(変数 t)により変換して,この接空間における光源位置を求めます.これをテクスチャ座標に設定します.

/*
** 正規化マップのテクスチャ座標を設定する
**  n: その点における法線ベクトル
**  p: その点の位置
**  ll: ローカル座標系における光源位置
*/
static void normalizeTexCoord(double n[], double p[], double ll[])
{
  /* 接空間における光源位置 */
  double lt[4] = { ll[0], ll[1], ll[2], ll[3] };

  /* 平行光線でなければ光源位置の実座標を求めておく */
  if (lt[3] != 0.0) {
    lt[0] = lt[0] / lt[3] - p[0];
    lt[1] = lt[1] / lt[3] - p[1];
    lt[2] = lt[2] / lt[3] - p[2];
  }

  /* ローカル座標系から接空間の座標系への変換行列 */
  double t[16];

  /* ローカル座標系から接空間の座標系への変換行列を求める */
  localToTangent(n, t);
  
  /* 接空間における光源位置を求める */
  transform(lt, t, lt);
  
  /* 接空間における光源位置をテクスチャ座標に設定する */
  glMultiTexCoord3dv(GL_TEXTURE1, lt);
}

球の描画

これらを用いて球を描画します.まず,現在のモデルビュー変換行列を取り出し,その逆行列を求めます.これを用いて球のローカル座標系における光源位置(変数 ll)を求めます.

/*
** 正規化マップのテクスチャ座標を設定する
**  n: その点における法線ベクトル
**  p: その点の位置
**  ll: ローカル座標系における光源位置
*/
static void normalizeTexCoord(double n[], double p[], double ll[])
{
  /* 接空間における光源位置 */
  double lt[4] = { ll[0], ll[1], ll[2], ll[3] };

  /* 平行光線でなければ光源位置の実座標を求めておく */
  if (lt[3] != 0.0) {
    lt[0] = lt[0] / lt[3] - p[0];
    lt[1] = lt[1] / lt[3] - p[1];
    lt[2] = lt[2] / lt[3] - p[2];
  }

  /* ローカル座標系から接空間の座標系への変換行列 */
  double t[16];

  /* ローカル座標系から接空間の座標系への変換行列を求める */
  localToTangent(n, t);
  
  /* 接空間における光源位置を求める */
  transform(lt, t, lt);
  
  /* 接空間における光源位置をテクスチャ座標に設定する */
  glMultiTexCoord3dv(GL_TEXTURE1, lt);
}

球の描画に先立って,まず,この球のローカル座標系における光源位置 ll を求めます.

/*
** 球の描画
*/
void sphere(double radius, int slices, int stacks, const float l[])
{
  /* 現在のモデルビュー変換行列の逆行列を求める */
  double m[16];
  glGetDoublev(GL_MODELVIEW_MATRIX, m);
  inverse(m, m);
  
  /* ローカル座標系における光源位置を求める */
  double ll[4] = { l[0], l[1], l[2], l[3] };
  transform(ll, m, ll);

この球は円柱の上下をすぼめて作っているので,基本的な描画手順は円柱の場合と変わりありません.また,各頂点の位置を求める際,同時にその頂点における法線ベクトルも算出しておきます.

  /* 球を描く */
  for (int j = 0; j < stacks; ++j) {
    double t0 = (double)j / (double)stacks;
    double t1 = (double)(j + 1) / (double)stacks;
    double r0 = sin(M_PI * t0);
    double r1 = sin(M_PI * t1);
    double n[2][3], p[2][3];
    
    /* 法線単位ベクトルの y 成分 */
    n[0][1] = -cos(M_PI * t0);
    n[1][1] = -cos(M_PI * t1);
    
    /* 頂点の y 座標値 */
    p[0][1] = radius * n[0][1];
    p[1][1] = radius * n[1][1];
    
    /* 法線マップのテクスチャ座標の算出 */
    t0 *= 4.0;
    t1 *= 4.0;
    
    glBegin(GL_QUAD_STRIP);
    for (int i = 0; i <= slices; ++i) {
      double s = (double)i / (double)slices;
      double a = -2.0 * M_PI * s;
      
      /* 法線単位ベクトルの x, z 成分 */
      n[0][0] = r0 * cos(a);
      n[0][2] = r0 * sin(a);
      n[1][0] = r1 * cos(a);
      n[1][2] = r1 * sin(a);
      
      /* 頂点の x, z 座標値 */
      p[0][0] = radius * n[0][0];
      p[0][2] = radius * n[0][2];
      p[1][0] = radius * n[1][0];
      p[1][2] = radius * n[1][2];
      
      /* 法線マップのテクスチャ座標の算出 */
      s *= 8.0;

法線マップのテクスチャ座標には,球を描くために求めた緯度方向のパラメータ(変数 t0, t1)と経度方向のパラメータ(変数 s)をスケーリングしたものを用います.正規化マップのテクスチャ座標は,その頂点の法線ベクトル(変数 n)と位置(変数 p),および球のローカル座標系における光源位置(変数 ll)を用いて,前述の関数 normalizeTexCoord() により設定します.

      /* 法線マップのテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE0, s, t0);
      
      /* 正規化マップのテクスチャ座標を設定する */
      normalizeTexCoord(n[0], p[0], ll);
      
      /* 頂点位置 */
      glVertex3dv(p[0]);
      
      /* 法線マップのテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE0, s, t1);
      
      /* 正規化マップのテクスチャ座標を設定する */
      normalizeTexCoord(n[1], p[1], ll);
      
      /* 頂点位置 */
      glVertex3dv(p[1]);
    }
    glEnd();
  }
}

拡散反射率(色)のテクスチャを合成する

この球に拡散反射率のテクスチャを合成します.テクスチャを一つ追加するので,main.cpp の init() で合計3つのテクスチャを作成します.

  /* テクスチャ名を3つ作る */
  GLuint texname[3];
  glGenTextures(3, texname);

3つ目のテクスチャは2次元テクスチャとしてマッピングします.

  /*
  ** テクスチャユニット2に拡散反射率マップを設定する
  */
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, texname[0]);

  /* テクスチャ画像の読み込み */
  FILE *fp;
  if ((fp = fopen(texture_file, "rb")) != NULL) {
    fread(texture, sizeof texture, 1, fp);
    fclose(fp);
  }
  else {
    perror(texture_file);
  }

  /* テクスチャの割り当て */
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
    GL_RGBA, GL_UNSIGNED_BYTE, texture);

  /* テクスチャを拡大・縮小する方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

  /* テクスチャの繰り返し方法の指定 */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

このテクスチャは下地の色(バンプマッピングの出力)と合成するので,テクスチャ環境を GL_MODULATE に設定します.

  /* テクスチャユニット2のテクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

sphere.cpp の sphere() で,球の頂点に拡散反射係数のテクスチャ座標を指定します.

/*
** 球の描画
*/
void sphere(double radius, int slices, int stacks, const float l[])
{
  ...

  /* 球を描く */
  for (int j = 0; j < stacks; ++j) {
    ...

    glBegin(GL_QUAD_STRIP);
    for (int i = 0; i <= slices; ++i) {
      double s = (double)i / (double)slices;
      double a = -2.0 * M_PI * s;
      
      /* 法線単位ベクトルの x, z 成分 */
      n[0][0] = r0 * cos(a);
      n[0][2] = r0 * sin(a);
      n[1][0] = r1 * cos(a);
      n[1][2] = r1 * sin(a);
      
      /* 頂点の x, z 座標値 */
      p[0][0] = radius * n[0][0];
      p[0][2] = radius * n[0][2];
      p[1][0] = radius * n[1][0];
      p[1][2] = radius * n[1][2];
      
      /* 法線マップのテクスチャ座標の算出 */
      s *= 8.0;
      
      /* 法線マップのテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE0, s, t0);
      
      /* 正規化マップのテクスチャ座標を設定する */
      normalizeTexCoord(n[0], p[0], ll);
      
      /* 拡散反射係数のテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE2, s, t0);
      
      /* 頂点位置 */
      glVertex3dv(p[0]);
      
      /* 法線マップのテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE0, s, t1);
      
      /* 正規化マップのテクスチャ座標を設定する */
      normalizeTexCoord(n[1], p[1], ll);
      
      /* 拡散反射係数のテクスチャ座標を設定する */
      glMultiTexCoord2d(GL_TEXTURE2, s, t1);
      
      /* 頂点位置 */
      glVertex3dv(p[1]);
    }
    glEnd();
  }
}

球にバンプマッピング