床井研究室

マスク

最近、花粉症対策のマスクをしています。ほこりでアレルギー性鼻炎を起こすことはあるのですが、花粉症にはまだ縁はありません。でも今年の花粉の飛散は悲惨だというので、予防のつもりマスクをかけてみました。そうしたら、妙に気分が落ち着くのです。素顔を見られない安心感からでしょうか。部屋の中でマスクをしていたら、学生さんに不審がられました。

ちなみに、あんまり吐き気・胸のつかえ・胸の痛み、あるいは気分の落ち込みのようなものが続くので、先日ついに病院に行きました。今風に言えば「メンタルクリニック」とかいうところです。「気分の悪さが吐き気なのか不安感なのか区別がつかない」とお医者さんに話したら、「そういうものをかつて心身症と呼んでいた」と言われました。なるほど。メイラックスとドグマチールという薬を処方してもらいました。効いているようです。

キューブマッピングで Phong シェーディング

さてさて今回は、前回の途中でくじけたキューブマッピングによる Phong シェーディングの実装を行います。雛型にはキューブマッピングでテクスチャを回転したときに作成したものを用います。

材質の設定

まず、キューブマッピングによる映り込み(環境マッピング)の代わりに、描画する物体のマテリアル(材質)を決めておきます、元々の材質は移り込みに影響を与えないように白にしてありました。ここではスフィアマッピングで Phong シェーディングをやったときと同様に青色にしてみます。

/*
** マテリアル
*/
static const GLfloat kdiff[] = { 0.0f, 0.1f, 0.3f, 1.0f };  /* 拡散反射係数 */
static const GLfloat kspec[] = { 0.6f, 0.6f, 0.6f, 1.0f };  /* 鏡面反射係数 */
static const GLfloat knone[] = { 0.0f, 0.0f, 0.0f, 1.0f };  /* 鏡面反射無効 */
static const GLfloat kshi = 20.0;                           /* 輝き係数   */

鏡面反射光強度の算出

次に、このマテリアルを使って鏡面反射光強度を算出する手続きを、関数 specular() として定義しておきます。これは初期化の関数 init() の前に置いてください。

/*
** 鏡面反射光強度
*/
static void specular(float fx, float fy, float fz, const float* l, GLubyte* t)
{
  /* 光線ベクトルと反射ベクトルの内積を求める */
  float lf = l[0] * fx + l[1] * fy + l[2] * fz;

  if (lf > 0.0) {
    /* 鏡面反射率×255を求める */
    float rs = powf(lf, kshi) * 255.0f;

    /* 鏡面反射光強度を求める */
    t[0] = (GLubyte)(kspec[0] * rs * lightcol[0]);
    t[1] = (GLubyte)(kspec[1] * rs * lightcol[1]);
    t[2] = (GLubyte)(kspec[2] * rs * lightcol[2]);
  }
  else {
    t[0] = t[1] = t[2] = 0;
  }
}

鏡面反射光強度分布の画像を作る

この関数 specular() を使って鏡面反射光強度分布の画像を作成する関数 makeTexture() を、この直後(つまり関数 init() との間)で定義します。キューブマッピングなので6枚分の画像を作ります。

キューブマッピングにおける法線ベクトル

/*
** テクスチャの作成
*/
static void makeTexture(GLubyte* tex[], int width, int height)
{
  int i = 0;

  /* 反射ベクトルと光線ベクトルとの内積値でテクスチャを作る */
  for (int v = 0; v < height; ++v) {
    float y = (float)(v + v - height) / (float)height;
    float y2 = y * y;

    for (int u = 0; u < width; ++u) {
      float x = (float)(u + u - width) / (float)width;
      float x2 = x * x;

      /* 反射ベクトル */
      float r = 1.0 / sqrt(x2 + y2 + 1.0);
      float s = x * r;
      float t = y * r;

      /* 6面のテクスチャについてそれぞれ鏡面反射光強度を求める */
      specular(-r, -t,  s, lightpos, tex[0] + i);  /* negative x */
      specular( s, -r, -t, lightpos, tex[1] + i);  /* negative y */
      specular(-s, -t, -r, lightpos, tex[2] + i);  /* negative z */
      specular( r, -t, -s, lightpos, tex[3] + i);  /* positive x */
      specular( s,  r,  t, lightpos, tex[4] + i);  /* positive y */
      specular( s, -t,  r, lightpos, tex[5] + i);  /* positive z */

      i += 3;
    }
  }

テクスチャを割り当てる

テクスチャに使う画像を格納する配列 texture を6枚分用意します。makeTextures() の引数 texGLubyte 型のポインタの配列 (GLubyte* tex[]) にしてしまったので、texture の個々の要素は TEXHEIGHT * TEXWIDTH * 3 バイトの1次元にします。ちょっとケチってチャンネル数を * 3 として奇数にしてしまったので、GL_UNPACK_ALIGNMENT は 1 にしておきます。

そして texture の各要素のポインタを要素として持つポインタの配列 textures を宣言します。この texturesmakeTextures() の引数 tex に指定します。

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

  /* テクスチャの読み込みに使う配列 */
  static GLubyte texture[6][TEXHEIGHT * TEXWIDTH * 3];
  static GLubyte* textures[]{ texture[0], texture[1], texture[2], texture[3], texture[4], texture[5] };

  /* テクスチャの作成 */
  makeTexture(textures, TEXWIDTH, TEXHEIGHT);

テクスチャに使う画像を作成したので、この後に続く画像ファイルの読み込み部分は不要になります。#if 0#endif ではさむなどして、無効にしておいてください。

あと、今回テクスチャに割り当てる画像にはアルファチャンネルを付けていないので、テクスチャを割り当てる際には画像の書式に GL_RGB を指定してください。また、6枚の画像が配列変数 textures のひとつひとつの要素に格納されているので、textures に添え字 [i] を付けておいてください。

  for (int i = 0; i < 6; ++i) {
#if 0
    /* テクスチャの読み込みに使う配列 */
    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]);
    }
#endif

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

テクスチャとしてマッピングする鏡面反射光成分を、OpenGL による陰影付けで得た拡散反射光成分と合成するために、glTexEnvi() を使って GL_TEXTURE_ENV_MODEGL_ADD を指定します。

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

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

ここまでできたら一度コンパイルして実行してみてください。

キューブマッピングで Phong シェーディング

拡散反射光も回そう

マウスをドラッグしてみてください。鏡面反射光成分(ハイライト)がマウスの移動方向と反対に動くのが少し気色悪いですけど、ここはこのままいきましょう。 一方、拡散反射光成分は今回も動いていません。そこで、今回はこの拡散反射光成分も、光源の位置にあわせて動くようにしてみます。拡散反射光成分をマウスの動きにあわせて回転させるには、光源の位置を設定しているところで、物体の回転と同じように回転の行列を乗じます。

static void display()
{
  ...

  /* 光源の位置を設定 */
  glPushMatrix();
  glMultMatrixd(trackballRotation());
  glLightfv(GL_LIGHT0, GL_POSITION, lightpos);
  glPopMatrix();

これでコンパイルして実行すると、多分、面白いことが起こります。

鏡面反射光成分と拡散反射光成分が逆に動く

鏡面反射光成分と拡散反射光成分が逆回転している

これは鏡面反射光成分と拡散反射光成分が反対方向に回転している状態です。正しく回転させるには、どちらかを逆回転させる必要があります。回転行列の場合、逆回転、すなわち逆変換行列は、元の変換行列を転置するだけで求められます。しかし、OpenGL の Version 1.3 では、転置行列を扱う拡張機能 (GL_ARB_transpose_matrix) が標準機能になっていますから、ここではこれを使うことにします。

Windows の場合

なお Windows でこの機能を使う場合には、少しおまじないが必要になります。先にその部分を説明します。まず、プログラムの最初の部分で glext.h#include している部分の後で、glMultTransposeMatrixd() という関数の関数ポインタ変数 glMultTransposeMatrixd を宣言しておきます。

#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>
#  if defined(_WIN32)
PFNGLMULTTRANSPOSEMATRIXDPROC glMultTransposeMatrixd;
#  endif
#endif

そして、初期化の関数 init() の最後あたりで、この glMultTransposeMatrixd に関数のエントリポイント(プログラムの実体が格納されている場所)を代入しておきます。

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

  /* キューブマッピングする */

  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);

#if defined(_WIN32)
  glMultTransposeMatrixd =
  (PFNGLMULTTRANSPOSEMATRIXDPROC)wglGetProcAddress("glMultTransposeMatrixd");
#endif
}

実際に関数 glMultTransposeMatrixd() が使えるかどうかは、glGetString( GL_VERSION ) で OpenGL のバージョンを調べたり、glGetString( GL_EXTENSIONS ) で返される文字列の中に GL_ARB_transpose_matrix が含まれることを確認したりする必要があります。少なくとも、上記の手続きで得られた関数ポインタ glMultTransposeMatrixd が NULL でないことを確認しておかないと、この機能がサポートされていなかった場合にプログラムが異常終了してしまいます。ですが、ここでは手を抜いて「使えるもの」として話を進めます。

鏡面反射光成分のテクスチャを逆回転する

最後に、テクスチャを回転している部分の glMultMatrixd()glMultTransposeMatrixd() に置き換えて、鏡面反光射成分が逆方向に回転するようにします。

static void display()
{
  /* テクスチャ変換行列の設定 */
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();

  /* トラックボール処理による回転 */
  glMultTransposeMatrixd(trackballRotation());

  /* モデルビュー変換行列の設定 */
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

鏡面反射光成分と拡散反射光成分を同じ方向に回した場合

テクスチャを回した場合 ティーポットを回した場合
テクスチャを回した場合 ティーポットを回した場合