床井研究室

あけましておめでとうございます

とうとう年が明けてしまいました。昨年の大晦日にはにしんそばも無事おいしく頂くことができ、今年こそはいいことがあればなあと年の初めに思っておりました。ちなみに、大晦日は夕食に 100g あたり 580 円という高価な焼肉を頂きました。それで胃が仰天したのでしょうか、その夜消化不良を起こしてせっかく頂いたにしんそばを緑の胃薬と一緒にもどしてしまいました。やはり私の人生、所詮こんなもんかも知れません。

スフィアマッピング

それで、今回はスフィア(球体)マッピングです。環境マッピング(あるいはリフレクションマッピング)の手法の1つです。個人的には、”SGI マシン上で初めて見たときに「映り込みがリアルタイムに!」と驚かされ、NINTENDO 64 の「メタルマリオ」を見たときには「SGI のあれがこんなゲームマシンで!」と驚かされたという、結構思い入れのある手法です。

一般に映り込みを実現するには、物体表面上の一点に向かう視線ベクトル $\mathbf{u}$ と、その点における法線ベクトル $\mathbf{n}$’ から反射ベクトル $\mathbf{f}$ を求め、その先にある別の物体表面の色をサンプリングして、この物体表面上の色と合成します。

映り込みの実現

環境マッピングでは、この反射方向の物体の代わりにテクスチャを用います。OpenGL のスフィアマッピングでは、反射ベクトル $\mathbf{f} = \left(f_x, f_y, f_z\right)$ からテクスチャ座標 $(s, t)$ を生成して、テクスチャのサンプリングを行います。

スフィアマッピングを有効にする

それでは実際に OpenGL のスフィアマッピングを試してみましょう。今回はテクスチャ座標の自動生成のところで作成したプログラムを雛形に用います。

この main.cpp の初期化の関数 init() において、テクスチャ座標の生成関数を設定している部分をスフィアマッピング用の設定に置き換えます。これは glTexGen*()GL_TEXTURE_GEN_MODEGL_SPHERE_MAP を指定します。スフィアマッピングではテクスチャ座標の (s, t) のみが生成されます。

なお、この場合はテクスチャ座標の生成関数を使用しないので、テクスチャ座標の生成関数を定義している部分は不要です(これに伴って、テクスチャ座標の生成関数のパラメータを保持している配列変数 genfunc も不要になります)。

/*
** 初期化
*/
static void init()
{
  ...

  /* テクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

#if 0
  /* 混合する色の設定 */
  static const GLfloat blend[] = { 0.0, 1.0, 0.0, 1.0 };
  glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, blend);
#endif

#if 0
  /* 頂点のオブジェクト空間における座標値を使ってマッピングする */
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
  glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);

  /* テクスチャ座標生成関数の設定 */
  glTexGendv(GL_S, GL_OBJECT_PLANE, genfunc[0]);
  glTexGendv(GL_T, GL_OBJECT_PLANE, genfunc[1]);
  glTexGendv(GL_R, GL_OBJECT_PLANE, genfunc[2]);
  glTexGendv(GL_Q, GL_OBJECT_PLANE, genfunc[3]);
#endif

  /* スフィアマッピング用のをテクスチャ座標を生成する */
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);

  /* アルファテストの判別関数 */
  glAlphaFunc(GL_GREATER, 0.5);

このシーンの描画の際には、テクスチャ座標の (s, t) 軸についてのみ自動生成を有効にします。

/*
** シーンの描画
*/
static void scene()
{
  ...

  /* テクスチャ座標の自動生成を有効にする */
  glEnable(GL_TEXTURE_GEN_S);
  glEnable(GL_TEXTURE_GEN_T);
#if 0
  glEnable(GL_TEXTURE_GEN_R);
  glEnable(GL_TEXTURE_GEN_Q);
#endif

  /* 箱を描く */
  box(1.0, 1.0, 1.0);

  /* テクスチャ座標の自動生成を無効にする */
  glDisable(GL_TEXTURE_GEN_S);
  glDisable(GL_TEXTURE_GEN_T);
#if 0
  glDisable(GL_TEXTURE_GEN_R);
  glDisable(GL_TEXTURE_GEN_Q);
#endif

あと、雛形に使ったプログラムは、テクスチャ座標を回転した後、投影マッピングを行うためにテクスチャ変換行列に透視変換行列やビュー変換行列を乗じていますから、その部分を無効にしておきます。

static void display()
{
  ...

  /* トラックボール処理で図形を回転 */
  glMultMatrixd(trackballRotation());

  /* テクスチャ行列の設定 */
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
#if 0
  glTranslated(0.5, 0.5, 0.0);
  glRotated(t * 360.0, 0.0, 0.0, 1.0);
  glScaled(0.5, 0.5, 1.0);

  /* テクスチャ行列に透視変換行列と視野変換行列を掛ける */
  gluPerspective(60.0, 1.0, 0.1, 10.0);
  gluLookAt(0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
#endif

これをコンパイルして実行すると、こんな図形が表示されます。しかし、なんだか変ですね。

最初の実行結果

スフィアマッピング用のテクスチャを使う

そこで、テクスチャ用の画像ファイルを次の room.raw に取り替えてみてください。

スフィアマッピング用のテクスチャ

この画像の上下が反転しているのは、テクスチャ空間の原点が画像の左下隅にあるからです。中央でカメラを構えているのは、情けないですけど私です。右上に「へぇボタン」なんかも写ってたりします。この画像を使うために、main.cpp の下の部分を変更します。ついでに変数 genfunc も不要なので無効にしておいてください。

/*
** テクスチャ
*/
#define TEXWIDTH  256                               /* テクスチャの幅    */
#define TEXHEIGHT 256                               /* テクスチャの高さ   */
static const char texture_file[] = "tire.raw";      /* テクスチャファイル名 */

#if 0
/* テクスチャ生成関数のパラメータ */
static double genfunc[][4] = {
  { 1.0, 0.0, 0.0, 0.0 },
  { 0.0, 1.0, 0.0, 0.0 },
  { 0.0, 0.0, 1.0, 0.0 },
  { 0.0, 0.0, 0.0, 1.0 },
};
#endif

これをコンパイルして実行してみてください。

スフィアマッピング用のテクスチャを使った場合

なんだかそれらしいものが出てきたのではないかと思います。もし映り込みのテクスチャが粗いのが気になるなら、第2回のテクスチャの割り当て の「MIPMAP について」あたりを参照して、テクスチャの補間を行ってみてください。

表示形状を変えてみる

形状が箱だとスフィアマッピングのありがたみがよくわからないので、表示する形を変えてみましょう。まず最初に球を描いてみます。main.cpp の中の関数 scene() で箱を描いている部分を、glutSolidSphere() に置き換えてください。

/*
** シーンの描画
*/
static void scene()
{
  ...

  /* テクスチャ座標の自動生成を有効にする */
  glEnable(GL_TEXTURE_GEN_S);
  glEnable(GL_TEXTURE_GEN_T);
#if 0
  glEnable(GL_TEXTURE_GEN_R);
  glEnable(GL_TEXTURE_GEN_Q);
#endif

#if 0
  /* 箱を描く */
  box(1.0, 1.0, 1.0);
#endif

  /* 球を描く */
  glutSolidSphere(1.0, 32, 16);

  /* テクスチャ座標の自動生成を無効にする */
  glDisable(GL_TEXTURE_GEN_S);
  glDisable(GL_TEXTURE_GEN_T);
 #if 0
  glDisable(GL_TEXTURE_GEN_R);
  glDisable(GL_TEXTURE_GEN_Q);
#endif

これをコンパイルして実行すると、こういう図形が表示されます。

球にスフィアマッピング

球なので回転しても形が変わらないのは当然ですが、映り込んでいるテクスチャも動きません。これじゃ余計にありがたみがないので、球ではなくティーポットを描いてみます。

/*
** シーンの描画
*/
static void scene()
{
  ...

#if 0
  /* 箱を描く */
  box(1.0, 1.0, 1.0);
#endif

  /* ティーポットを描く */
  glutSolidTeapot(1.0);

これをコンパイルして実行すると、こういう図形が表示されます。回してみてください。クロームメッキのティーポットらしいでしょ。

ティーポットにスフィアマッピング

ここまでの内容をサンプルプログラムにまとめてあります。ただし、例によってテクスチャが回転するようになってます。

スフィアマッピング

スフィアマッピングにおけるテクスチャ座標

それでは、このスフィアマッピング用のテクスチャをどうやって作るか考えてみたいと思います。反射ベクトル $\mathbf{f} = \left(f_x, f_y, f_z\right)$ からテクスチャ座標 $(s, t)$ を生成する手順は、マニュアルには次のように書いてあります。まずはじめに、この式の意味について考えてみます。

\[\begin{aligned} m &= 2\sqrt{f_x^2 + f_y^2 + \left(f_z + 1\right)^2} \\ s &= \frac{f_x}{m} + \frac{1}{2} \\ t &= \frac{f_y}{m} + \frac{1}{2} \end{aligned}\]

ここで視点座標系における視線の方向単位ベクトルを $\mathbf{u} = (0, 0, -1)$ とします。この近似は、OpenGL の陰影付けにおいてデフォルトで用いられているものです(glLightModeli( GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE ); とすれば、より正確な陰影付けを行うことができます)。物体表面の(視点座標系における)法線ベクトルは -$\mathbf{u}$ と $\mathbf{f}$ の中間ベクトルですから、$\mathbf{n}’$ は次のようにして求められます。

視線ベクトルと反射ベクトルから法線ベクトルを求める

この法線単位ベクトルの xy 成分は下図の単位円内にあります。

法線ベクトルのxy成分

この領域をテクスチャ空間と一致するようにスケーリングと平行移動を行えば、マニュアルに示されている式が得られます。

テクスチャ座標

したがってスフィアマッピングでは、テクスチャ座標 $(s, t)$ に物体表面の法線ベクトル $\mathbf{n}’$ の xy 成分を(スケーリングと平行移動を行って)用いていると言えます。

スフィアマッピング用のテクスチャの作り方

スフィアマッピング用のテクスチャは、テクスチャ座標 $(s, t)$ に対応した法線ベクトル $\mathbf{n}’$ をもとに反射ベクトル $\mathbf{f}$ を求め、レイトレーシング的に周囲をサンプリングして作成することができます。しかし、こんな方法では手間も時間もかかってしまいます。OpenGL 的に作成することもできなくはないとは思われますが、$\mathbf{f}$ から $(s, t)$ への変換が非線形なので、やっぱり一筋縄ではいきません。

しかし球形の鏡を使えば、近似的ではありますが、非常に簡単にスフィアマッピング用のテクスチャを撮影することができます。テクスチャ座標 $(s, t)$ が法線単位ベクトル $\mathbf{n}’$ の xy 成分に対応しているので、この法線単位ベクトル群の包絡面となる単位半球表面への映り込みを st 平面上に投影して、テクスチャを得ることができます。

単位球への映り込み

このような画像は、焦点距離が無限に長いカメラを使い、無限の彼方から球形の鏡を撮影すれば得ることができます。現実にはそのような撮影は不可能ですから、近似的に普通のカメラ(それでも、できれば望遠のもの)を使って撮影を行います。

球形の鏡を使った撮影

それで球形の鏡を探したんですが、意外とこれが見つかりません。ある画像処理が専門の先生は光源分布の撮影用にそういう鏡を作ったとおっしゃってましたが、私にはそんなお金はありません。そこでホームセンターに行って見つけたのがこれ。350 円。

反射鏡つきの電球

普通の電球とは違って、電球の「頭」に反射鏡が付いています。間接照明用なのでしょう。他にもお玉とかボウルとか色々候補はあったんですが、一番真球に近そうで、何より(もとが鏡なので)映り込みに余計な色が付きそうにないと思えたので、これにしました。このてっぺんに描いてあったマークをマニキュアの除光液で消して、よーく磨いて撮影したのが、さっき使った room.raw です。

この方法ではカメラが写ってしまうのが難点ですが、望遠を使えばカメラの映り込みは小さくできますし、何なら Photoshop かなんかを使って写っているカメラを消すくらい、それほど大きな手間ではないでしょう。

スフィアマッピングの使い道

スフィアマッピングはピクセルに色をつける際、そのピクセルのところの物体表面の法線ベクトルをもとにテクスチャを拾います。このテクスチャは、何も映り込みの画像に限定する必要はありません。

例えば Phong のスムーズシェーディングでは、法線ベクトルを補間した後にピクセル単位に陰影を計算しますが、スフィアマッピングではこの法線ベクトルの補間が既に行われています。

そこで、テクスチャ座標に対応した法線ベクトルを使って、法線ベクトルに対する拡散反射光と鏡面反射光の分布をあらかじめテクスチャとして作成しておきます。そしてレンダリング時にスフィアマッピングによるテクスチャ座標を生成し、サンプリングしたテクスチャの色をマルチテクスチャ機能を使って合成することにより、一般に時間がかかる(とされる)Phong のスムーズシェーディングを、テクスチャマッピングにより実現できます。

他にも、物体表面の法線ベクトルをインデックスとして利用する様々な処理にスフィアマッピングは利用できます。

スフィアマッピングの問題点とキューブマッピング

スフィアマッピングでは視線の方向を固定しているために、物体ではなく視線の方向を移動したときに、テクスチャを作り直さなければなりません。また、そのテクスチャの生成に手間がかかるために、レンダリングした画像を映り込みのテクスチャに使うなどのテクニックに応用することが困難です。キューブマッピングではこのような問題は発生しません。キューブマッピングは処理がピクセル単位になるので、これを使ってバンプマッピングなどの凝った処理が行える半面、高速に実行するには多くのハードウェアリソースが必要になります。でも、これは昨今のグラフィックスハードウェアなら屁でもないんじゃないかと思います。