床井研究室

バンプマッピングと環境マッピングの合わせ技

バンプマッピングはテクスチャを使って法線ベクトルを「揺らす」テクニックですが、この法線ベクトルを使って視線の反射ベクトルを求めて環境のテクスチャをサンプリングすれば、映り込みにバンプマッピングによる凹凸の影響を反映することができます。このテクニックを環境マップバンプマッピング (Environment-Mapped Bump Mapping, EMBM)1 といいます。

陰影計算を視点座標系で行う

前回のバンプマッピングでは、法線マップが接空間で定義されているために、視線ベクトルと光線ベクトルを接空間に移して陰影を求めていました。

しかし環境マッピングでは、環境のテクスチャを視点座標系でサンプリングしなければなりません。そのためには、視点座標系における視線の反射ベクトルが必要になります。そこで今回は、法線マップから得た法線ベクトルを、視点座標系に移して処理することにします。

環境のテクスチャを追加する

まず、環境マッピングのテクスチャを準備します。環境マッピングの手法にはキューブマッピングを用います。これをバンプマッピングと組み合わせるために、マルチテクスチャも使います。

まず、シェーダのプログラムオブジェクトのリンクが成功したら(ソースプログラムのファイル名は embm.vert と embm.frag にしています)、そのプログラムオブジェクトから頂点の接線ベクトルの attribute 変数 tangent の場所と、テクスチャのサンプリングに使うサンプラ変数 color に加えて、環境マップのサンプリングに使うサンプラ変数 environment の場所を取り出しておきます。

/* OpenGL / GLSL 関連の宣言 */
#include "glsl.h"

...

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

/*
** 接線ベクトルを格納する attribute 変数の場所
*/
static GLint tangentLoc;

/*
** テクスチャのサンプラの uniform 変数の場所
*/
static GLuint colorLoc;

/*
** 環境マップのサンプラ変数の uniform 変数の場所
*/
static GLint envronmentLoc;

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

/*
** 初期化
*/
static void init()
{
  /* GLSL の初期化 */
  if (glslInit()) exit(1);

  /* シェーダオブジェクトの作成 */
  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);

  /* シェーダのソースプログラムの読み込み */
  if (readShaderSource(vertShader, "embm.vert")) exit(1);
  if (readShaderSource(fragShader, "embm.frag")) exit(1);

  ...

  /* シェーダプログラムのリンク結果 */
  GLint linked;

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

  /* テクスチャのサンプラの uniform 変数の場所を得る */
  textureLoc = glGetUniformLocation(gl2Program, "color");

  /* 接ベクトルを渡すために使う attribute 変数の場所を得る */
  tangentLoc = glGetAttribLocation(gl2Program, "tangent");

  /* 環境マップのサンプラ変数の uniform 変数の場所を得る */
  envronmentLoc = glGetUniformLocation(gl2Program, "environment");

テクスチャユニット0には高さマップから生成した法線マップを格納します。

  /* テクスチャユニット0を指定する */
  glActiveTexture(GL_TEXTURE0);

  /* テクスチャオブジェクトの作成と結合 */
  GLuint tex;
  glGenTextures(1, &tex);
  glBindTexture(GL_TEXTURE_2D, tex);

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

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

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

  /* 法線マップの作成 */
  makeNormalMap(texture, TEXWIDTH, TEXHEIGHT, 20.0, texture_file);

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

以前はキューブマッピングを行うためにテクスチャ座標の自動生成を行っていましたが、今回はシェーダプログラム内で生成した座標(反射ベクトル)を使ってテクスチャをサンプリングするため、ここではテクスチャの読み込みとサンプリングに関連した設定だけを行います。この設定はテクスチャユニット1に対して行います。環境のテクスチャには、テクスチャマッピング入門の第12回で使った room2.zip を用います。

  /* 環境マップにはテクスチャユニット1を指定する */
  glActiveTexture(GL_TEXTURE1);

  /* 環境マップのテクスチャオブジェクトの作成と結合 */
  GLuint env;
  glGenTextures(1, &env);
  glBindTexture(GL_TEXTURE_CUBE_MAP, env);

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

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

  for (int i = 0; i < 6; ++i) {
    /* キューブマップのテクスチャファイル名 */
    static const char *cubemaps[] = {
      "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,
    };
    /* キューブマップの読み込みに使う配列 */
    GLubyte cubemap[128][128][4];

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

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

最後に、残りの設定を行います。

  /* 初期設定 */
  glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
  glEnable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);

  /* 光源の初期設定 */
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol);
  glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol);
  glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb);
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
}

あと、display() 関数で、uniform 変数 environment の場所にテクスチャユニット1の番号を設定しておきます。

static void display()
{
  /* シェーダプログラムの適用 */
  glUseProgram(gl2Program);

  /* テクスチャのサンプラにテクスチャユニット0を指定する */
  glUniform1i(colorLoc, 0);

  /* 環境マップのサンプラ変数にテクスチャユニット1を設定 */
  glUniform1i(envronmentLoc, 1);

  ...
}

バーテックスシェーダの変更

環境マップのサンプリングはワールド座標系か視点座標系で行わなければならないので、ここでは視点座標系における光線ベクトルと視線ベクトルをラスタライザに送るようにします。また、接空間の基底ベクトル tbn もラスタライザに送って、フラグメントシェーダ側で補間値を参照できるようにします。

#version 120

// embm.vert

// 頂点の接線ベクトル
attribute vec3 tangent;

// ラスタライザに送る視点座標系の光線ベクトル
varying vec3 light;

// ラスタライザに送る視点座標系の視線ベクトル
varying vec3 view;

// ラスタライザに送る接空間の基底ベクトル
varying vec3 t, b, n;

lightview、および tbn を varying 変数として宣言しましたから、ここで行っていた変数宣言の vec3 は削除します。

void main()
{
  // 頂点のクリッピング座標値
  gl_Position = ftransform();

  // テクスチャ座標
  gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;

  // 視点座標系の頂点の位置
  vec4 position = gl_ModelViewMatrix * gl_Vertex;

  // 視点座標系の法線ベクトル
  vec3 normal = normalize(gl_NormalMatrix * gl_Normal);

  // 視点座標系の光線ベクトル
  light = normalize((gl_LightSource[0].position * position.w
    - gl_LightSource[0].position.w * position).xyz);

  // 視点座標系の視線ベクトル
  view = -normalize(position.xyz);

  // 法線ベクトルと接線ベクトルから接空間への変換行列
  n = normal;
  t = normalize(gl_NormalMatrix * tangent);
  b = cross(n, t);
}

フラグメントシェーダの変更

フラグメントシェーダでは、まずバーテックスシェーダで設定した varying 変数 lightview、および tbn を受け取ります。

#version 120

// embm.frag

// ラスタライザから受け取る視点座標系の光線ベクトルの補間値
varying vec3 light;

// ラスタライザから受け取る視点座標系の視線ベクトルの補間値
varying vec3 view;

// ラスタライザから受け取る接空間の基底ベクトルの補間値
varying vec3 t, b, n;

また、uniform 変数として、バンプマップのテクスチャのサンプラの uniform 変数 color に加えて、環境マップのテクスチャのサンプラの uniform 変数 environment を宣言します。

// テクスチャのサンプラ
uniform sampler2D color;

// 環境マップのサンプラ
uniform samplerCube environment;

バーテックスシェーダから受け取った接空間の基底ベクトル tbn を正規化し、それらを使って接空間から視点座標系への変換行列 toView を求めます。mat3 は3×3要素の実数からなる行列を宣言します。

void main ()
{
  // 視点座標系への変換行列
  mat3 toView = mat3(normalize(t), normalize(b), normalize(n));

求めた変換行列 toView を用いて法線マップをサンプリングして得た法線ベクトルを変換し、視点座標系における法線ベクトル fnormal を求めます。。

  // 法線マップから視点座標系の法線ベクトルを得る
  vec4 fcolor = texture2DProj(color, gl_TexCoord[0]);
  vec3 fnormal = toView * (fcolor.rgb * 2.0 - 1.0);

この法線ベクトル fnormal と視点座標系における光線ベクトル flight から、拡散反射率 diffuse を求めます。また、中間ベクトル fhalfway も視点座標系の光線ベクトル flight と視線ベクトル view を正規化したものから求めます。

  // 視点座標系の光線ベクトル
  vec3 flight = normalize(light);

  // 拡散反射率
  float diffuse = max(dot(fnormal, flight), 0.0);

  // 視点座標系における中間ベクトル
  vec3 fhalfway = normalize(flight + normalize(view));

  // 鏡面反射率
  float specular = pow(max(dot(fnormal, fhalfway), 0.0), gl_FrontMaterial.shininess);

  // フラグメントの色
  gl_FragColor = gl_FrontLightProduct[0].ambient
               + gl_FrontLightProduct[0].diffuse * diffuse
               + gl_FrontLightProduct[0].specular * specular;
}

ここまでの変更で、バンプマッピングの陰影計算を視点座標系で行うようになります。プログラムをコンパイル・実行すると、前回と同じような画像が得られるはずです。

バンプマッピングの陰影計算を視点座標系で行った結果

映り込みを実現するには、まず GLSL の組み込み関数 reflect() を使って、視線ベクトル view の正反射ベクトルを求めます。ここで view は正規化する必要はありません。

そして GLSL の組み込み関数 textureCube() を使って、キューブマップのサンプラの unform 変数 environment を指定して、この正反射ベクトルでテクスチャをサンプリングし、得られた色を鏡面反射率 specular として使います。

なお、中間ベクトル fhalfway は使わないので、この計算は削除してしまって構いません。

  // 視線の反射ベクトルでキューブマップをサンプリングして環境の色を得る
  vec4 specular = textureCube(environment, reflect(fnormal, view));

  // フラグメントの色
  gl_FragColor = gl_FrontLightProduct[0].ambient
               + gl_FrontLightProduct[0].diffuse * diffuse
               + gl_FrontLightProduct[0].specular * specular;
}

以上でこういう結果が得られます2

環境マップバンプマッピングの結果

ちなみに、鏡への映り込みじゃないと環境マッピングのありがたみを感じないというなら、フラグメントシェーダの最後のところで、gl_FragColor に直接 specular を代入してください。

環境のテクスチャをサンプリングした色をそのままフラグメントの色に使った結果

  1. 今ググると “a classic real-time computer graphics technique” と言われてしまいますが。(2026 年 7 月 2 日追記) 

  2. この画像はこの記事を執筆したときのものですが、サンプルプログラムの表示とは映り込み方が違います。多分、当時のプログラムが間違っています。(2026 年 7 月 2 日追記)