床井研究室

しろうさぎ

いかん、また気分が落ち込んできました。こういうときは城北橋のしろうさぎのシュークリームでも買って帰ろうと思います(追記:買って帰りました)。ちなみに、ここの和風のお弁当もお気に入りです。塩を減らすために酢を使ったような煮物の味付けにはまりました。ただ、お昼に買いに出るにはちょっと遠いので、めったに食べられないのが残念です。

キューブマッピング

キューブマッピングは環境(周囲の情景)に六面体に貼り付けたテクスチャを使って環境マッピングを行う手法です。これは OpenGL 1.3 で標準機能に取り入れられました。

キューブマッピング

キューブマッピングでは、平面のスクリーンに投影した像をテクスチャに用います。スフィアマッピングのように変形する必要はありません。その代わり、テクスチャに用いる画像は6枚必要になります。

それでは試しにやってみましょう。今回は前々回に作ったスフィアマッピングのプログラムを雛形にします。

しかし、こうやって前に作ったプログラムをベースにすると、修正箇所がどんどん累積していって、プログラムがとても読みにくくなってしまいますね。やっぱり素直な雛形プログラムを毎回用意したほうがいいかなぁ。

Windows の場合

キューブマッピングは OpenGL 1.3 で標準機能に取り入れられたので、Windows の VC++ 6.0 に含まれている gl.h では、それに必要な記号定数が定義されていません。この場合は SGI” の OpenGL® Sample Implementation にある glext.h1 を使ってください。

#if defined(__APPLE__) || defined(MACOSX)
#  define GL_SILENCE_DEPRECATION
#  include <GLUT/glut.h>
#  include <OpenGL/glext.h>
#else
#  if defined(_MSC_VER)
//#    pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#    define _USE_MATH_DEFINES
#    define _CRT_SECURE_NO_WARNINGS
#  endif
#  include <GL/glut.h>
#  include <GL/glext.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

なお、Vine Linux 3.1 および Mac OS X 10.3 では、キューブマッピングに使う記号定数が gl.h で定義されているようです。ただ、ビデオコントローラに ATI RAGE 128 を使っている自宅の iMac G3 400MHz は、キューブマッピングは機能しませんでした。OpenGL のバージョンは 1.1 だそうです(泣)。やっぱり、バージョンや拡張機能のチェックは必須ですね。

テクスチャの読み込み

2次元テクスチャの場合は、1枚の画像を GL_TEXTURE_2D という<em>ターゲットに割り当てました。これに対してキューブマッピングでは、6枚の画像をそれぞれ以下の6つのターゲットに割り当てます。

これだけのターゲットをいちいち指定するのは面倒なので、これらの定数を一旦配列に格納しておくことにします。

/*
** テクスチャ
*/
#define TEXWIDTH  256                      /* テクスチャの幅    */
#define TEXHEIGHT 256                      /* テクスチャの高さ   */
static const char texture1[] = "room.raw"; /* テクスチャファイル名 */
static const GLenum target[] = {       /* テクスチャのターゲット名 */
  GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
  GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
  GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
  GL_TEXTURE_CUBE_MAP_POSITIVE_X,
  GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
  GL_TEXTURE_CUBE_MAP_POSITIVE_Z
};

そして、このひとつひとつのターゲットに glTexImage2D() を使って画像を割り当てます。この部分はループを使って書くことにします。

/*
** 初期化
*/
static void init()
{
  /* テクスチャの読み込みに使う配列 */
  GLubyte texture[TEXHEIGHT][TEXWIDTH][4];

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

  /* テクスチャ画像はワード単位に詰め込まれている */
  glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
  for (int i = 0; i < 6; ++i) {
    glTexImage2D(target[i], 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
      GL_RGBA, GL_UNSIGNED_BYTE, texture);
  }

そのほかの部分にある GL_TEXTURE_2D は、GL_TEXTURE_CUBE_MAP に置き換えます。

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

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

また、テクスチャ座標の生成モードに GL_REFLECTION_MAP を指定します。キューブマッピングではスフィアマッピングと異なり、(s, t, r) の3つの座標についてテクスチャ座標を生成します。

#if 0
  /* テクスチャ座標生成関数の設定 */
  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_REFLECTION_MAP);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);

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

シーンの描画時には、s, t に加えて r 座標に対するテクスチャ座標の自動生成を有効にします。そしてキューブマッピングによるテクスチャマッピングを有効にします。

/*
** シーンの描画
*/
static void scene()
{
  static const GLfloat color[] = { 1.0f, 1.0f, 1.0f, 1.0f };  /* 材質 (色) */

  /* 材質の設定 */
  glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);

  /* アルファテスト開始 */
  glEnable(GL_ALPHA_TEST);

  /* テクスチャマッピング開始 */
  glEnable(GL_TEXTURE_CUBE_MAP);

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

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

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

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

  /* テクスチャマッピング終了 */
  glDisable(GL_TEXTURE_CUBE_MAP);

  /* アルファテスト終了 */
  glDisable(GL_ALPHA_TEST);
}

ここまでできたら、プログラムをコンパイルして実行してみてください。

同じテクスチャをキューブマッピングに使う

今は1枚のスフィアマッピング用の画像をキューブマッピング用の6つのターゲットにそのまま割り当てているので、こういう画像が表示されます。上、下、右、左、および手前の5つのテクスチャがティーポットに映り込んでいます。

回転を止めよう

しかし、このプログラムは例によってテクスチャを回転していますから、またわけがわかんないことになっています。ややこしいので、一旦この回転を止めることにします。テクスチャ行列を設定してテクスチャを回している処理をすべて #if 0#endif ではさんでしまいます。

/*************************
** GLUT のコールバック関数 **
*************************/

/* アニメーションのサイクル */
#define FRAMES 360

static void display()
{
#if 0
  /* フレーム数をカウントして時間として使う */
  static int frame = 0;                      /* フレーム数        */
  double t = (double)frame / (double)FRAMES; /* 時間とともに 0→1 に変化 */

  /* アニメーションのサイクルごとにフレーム数をリセットする */
  if (++frame >= FRAMES) frame = 0;
#endif
#if 0
  /* テクスチャ行列の設定 */
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();
  glTranslated(0.5, 0.5, 0.0);
  glRotated(t * 360.0, 0.0, 0.0, 1.0);
#if 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);
#else
  glTranslated(-0.5, -0.5, 0.0);
#endif
#endif

キューブマッピング用のテクスチャ

さて、キューブマッピング用のテクスチャをどうやって用意するか、考えてみます。これはカメラを三脚に乗せて、上下左右前後の6方向の写真を撮影し、それぞれが周辺部分で連続するようにトリミングすればできそうです。でも気分が落ち込んでいると、その程度の手間すら煩わしく感じます。

それぞれのテクスチャには平面のスクリーンへの投影像を用いればいいので、これらはコンピュータに作ってもらうことにします。これをシーンのレンダリング時に動的に作成するというのが OpenGL などによるリアルタイムレンダリングの醍醐味なのですが、そんな説明は将来に回すとして(そこまで行き着くかなぁ?)、ここでは CG ソフト (LightWave) で作った下の画像を使ってください。

キューブマッピング用のテクスチャ

この画像はカメラの位置を固定し、画角を 90°に設定して、6つの方向に向けてレンダリングしたものです。いずれの画像も正方形(縦横の画素数が同じ)である必要があります。6枚あるので、room2.zip というファイルにまとめてあります。まず、これらのファイル名を、変数 target の内容と対応付けて、別の配列変数 textures に設定します。

なお、ひとつひとつのテクスチャのサイズは以前のものより小さくしているので、記号定数 TEXWIDTHTEXHEIGHT も変更します。また以下のプログラムでは、もとあったスフィアマッピング用のテクスチャの設定は削除しています。

/*
** テクスチャ
*/
#define TEXWIDTH  128                      /* テクスチャの幅    */
#define TEXHEIGHT 128                      /* テクスチャの高さ   */
static const char* textures[] = {          /* テクスチャファイル名 */
  "room2nx.raw",
  "room2ny.raw",
  "room2nz.raw",
  "room2px.raw",
  "room2py.raw",
  "room2pz.raw"
};
static const GLenum target[] = {       /* テクスチャのターゲット名 */
  GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
  GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
  GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
  GL_TEXTURE_CUBE_MAP_POSITIVE_X,
  GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
  GL_TEXTURE_CUBE_MAP_POSITIVE_Z
};

この6つの画像を、それぞれ対応するターゲットに割り当てます。プログラムは順番を入れ替え、ファイル名を指定しているところに textures[i] を使うように変更するだけです。

/*
** 初期化
*/
static void init()
{
  /* テクスチャ画像はワード単位に詰め込まれている */
  glPixelStorei(GL_UNPACK_ALIGNMENT, 4);

  for (int i = 0; i < 6; ++i) {

    /* テクスチャの読み込みに使う配列 */
    GLubyte texture[TEXHEIGHT][TEXWIDTH][4];

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

    /* テクスチャの割り当て */
    glTexImage2D(target[i], 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
    GL_RGBA, GL_UNSIGNED_BYTE, texture);
  }

テクスチャの拡大フィルタ (GL_TEXTURE_MAG_FILTER) とに縮小フィルタ (GL_TEXTURE_MIN_FILTER) では線形補間 GL_LINEAR を行うようにします。ただし、この時はテクスチャのラッピングモード (GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T) を GL_CLAMP_TO_EDGE にしないと、テクスチャの範囲外がマッピングされている領域の色が、テクスチャの最外周の色と違ってきます。

  /* テクスチャを拡大・縮小する方法の指定 */
  glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_CUBE_MAP, 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);

上のプログラムでは、ついでにテクスチャの拡大・縮小方法に線形補間を指定しています。これをコンパイルして実行すると、こんな具合になります。

ティーポットにキューブマッピング

図形を立方体に変えたときの品質は、スフィアマップより良好です。スクリーンに対して垂直に近い面において、テクスチャ座標が狂って正しくマッピングされないという現象も発生しません。

立方体にキューブマッピング

以下のプログラムでは、不要な部分を削除してあります。

キューブマッピング

  1. これは OpenGL Registry のものです。(2026 年 4 月 25 日 追記)