«第4回 GLSL によるバンプマッピング 最新 第6回 異方性反射»

床井研究室

※このブログは遅くとも 2027 年 3 月に管理者の定年退職により閉鎖します (移転先は管理者本人共々模索中)

■ 2005年10月15日 [OpenGL][GLSL] 第5回 環境マップバンプマッピング

2007年04月12日 10:12更新

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

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

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

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

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

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

バーテックスシェーダでは,varying 変数 light と view に設定する値を視点座標系における光線ベクトルと視線ベクトルに変更します.またフラグメントシェーダで接空間の座標系から視点座標系への変換を行うので,その変換行列の元になる視点座標系における法線ベクトルと接線ベクトルを varying 変数 n と t に設定します.

// bump.vert
 
attribute vec3 tangent;
 
varying vec3 light;
varying vec3 view;
varying vec3 n;
varying vec3 t;
 
void main()
{
  // 視線ベクトルと光線ベクトルを求める
  view = vec3(gl_ModelViewMatrix * gl_Vertex);
  light = gl_LightSource[0].position.xyz - view;
  
  // 法線ベクトルと接線ベクトルを求める
  n = normalize(gl_NormalMatrix * gl_Normal);
  t = normalize(gl_NormalMatrix * tangent);
  
  // テクスチャ座標と頂点座標を出力する
  gl_TexCoord[0] = gl_MultiTexCoord0;
  gl_Position = ftransform();
}

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

フラグメントシェーダでは,まずバーテックスシェーダで設定した varying 変数 n と t を使って,接空間から視点座標系への変換行列 toView を求めます.mat3 は3×3要素の実数からなる行列を宣言します.

// bump.frag
 
uniform sampler2D texture;
 
varying vec3 light;
varying vec3 view;
varying vec3 n;
varying vec3 t;
 
void main (void)
{
  // 接空間から視点座標系への変換行列を求める
  vec3 fn = normalize(n);
  vec3 ft = normalize(t);
  vec3 fb = cross(fn, ft);
  mat3 toView = mat3(ft, fb, fn);

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

  // 法線マップから得たベクトルを視点座標系に変換する
  vec4 color = texture2DProj(texture, gl_TexCoord[0]);
  vec3 fnormal = toView * (vec3(color) * 2.0 - 1.0);
  
  vec3 flight = normalize(light);
  float diffuse = dot(flight, fnormal);
  
  gl_FragColor = gl_FrontLightProduct[0].ambient;
  if (diffuse > 0.0) {
    vec3 fview = normalize(view);
    vec3 halfway = normalize(flight - fview);
    float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess);
    gl_FragColor += gl_FrontLightProduct[0].diffuse * diffuse
                 + gl_FrontLightProduct[0].specular * specular;
  }
}

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

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

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

次に環境マッピングを組み込みます.環境マッピングの手法にはキューブマッピングを用います.これをバンプマッピングと組み合わせるために,マルチテクスチャも有効にします.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
#if defined(WIN32)
//#  pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#  include "glut.h"
#  include "glext.h"
PFNGLACTIVETEXTUREPROC glActiveTexture;
#elif defined(__APPLE__) || defined(MACOSX)
#  include <GLUT/glut.h>
#else
#  define GL_GLEXT_PROTOTYPES
#  include <GL/glut.h>
#endif
 
・・・

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

・・・
 
/*
** 初期化
*/
static void init(void)
{
  ・・・
  
  /* テクスチャユニット0に法線マップを割り当てる */
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
    GL_RGBA, GL_UNSIGNED_BYTE, texture);
  
  /* テクスチャユニット1用のテクスチャオブジェクトを作成する */
  GLuint texname[1];
  glGenTextures(1, texname);
  
#if defined(WIN32)
  glActiveTexture =
    (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture");
#endif
  
  /* テクスチャユニット1に切り替える */
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_CUBE_MAP, texname[0]);
  
  /* テクスチャを拡大・縮小する方法の指定 */
  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);
  
  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;
    
    /* テクスチャ画像の読み込み */
    if ((fp = fopen(cubemaps[i], "rb")) != 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);
  }
  
  /* テクスチャユニット0に戻す */
  glActiveTexture(GL_TEXTURE0);
  
  /* 初期設定 */
  glClearColor(0.3, 0.3, 1.0, 0.0);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  
  ・・・

あと,環境マッピングに使うテクスチャユニットの番号をシェーダプログラムに伝えるために,関数 init() の最後あたりで,シェーダプログラムの uniform 変数 environment にテクスチャユニットの番号 1 を設定しておきます.

  ・・・
  
  /* シェーダプログラムの適用 */
  glUseProgram(gl2Program);
  
  /* テクスチャユニット0を指定する */
  glUniform1i(glGetUniformLocation(gl2Program, "texture"), 0);
  
  /* 環境マップにはテクスチャユニット1を指定する */
  glUniform1i(glGetUniformLocation(gl2Program, "environment"), 1);
  
  /* 接線ベクトルを渡すために使う attribute 変数のハンドルを得る */
  tangent = glGetAttribLocation(gl2Program, "tangent");
}
 
・・・

環境のテクスチャのサンプリング

それではフラグメントシェーダで環境のテクスチャのサンプリングを行って,映り込みを実現してみましょう.まず,環境のテクスチャを保持しているテクスチャユニットを指定するために,samplerCube 型の uniform 変数 environment の定義を追加します.

// bump.frag
 
uniform sampler2D texture;
uniform samplerCube environment;
 
varying vec3 light;
varying vec3 view;
varying vec3 n;
varying vec3 t;
 
void main (void)
{
  // 接空間から視点座標系への変換行列を求める
  vec3 fn = normalize(n);
  vec3 ft = normalize(t);
  vec3 fb = cross(fn, ft);
  mat3 toView = mat3(ft, fb, fn);

そして GLSL の組み込み関数 textureCube() を使い,unform 変数 environment で指定したテクスチャユニットから,環境のテクスチャをサンプリングします.GLSL の組み込み関数 refrect(view, fnormal) は,法線ベクトルが fnormal の面に入射するベクトル view の反射ベクトルを求めます.

環境のテクスチャをサンプリングして得た色は,物体表面に映り込ませるために,鏡面反射光の色として使います.その際,光源の映り込みであるハイライトが現れると妙なので,specular の計算は省いてしまいます.

  // 法線マップから得たベクトルを視点座標系に変換する
  vec4 color = texture2DProj(texture, gl_TexCoord[0]);
  vec3 fnormal = toView * (vec3(color) * 2.0 - 1.0);
  
  vec3 flight = normalize(light);
  float diffuse = dot(flight, fnormal);
  
  gl_FragColor = gl_FrontLightProduct[0].ambient;
  if (diffuse > 0.0) {
    // 視線の反射ベクトルでキューブマップをサンプリングして環境の色を得る
    vec4 envcolor = textureCube(environment, reflect(view, fnormal));
    gl_FragColor += gl_FrontLightProduct[0].diffuse * diffuse
                 + gl_FrontMaterial.specular * envcolor;
  }
}

これでこういう結果が得られます.

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

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

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

編集 «第4回 GLSL によるバンプマッピング 最新 第6回 異方性反射»