床井研究室

反射率は入射角に依存する

前回,非金属の物体への映り込みの実現を実現するために,環境の映り込みと拡散反射光を比例配分する手法を示しました.この方法では物体の表面に環境の映り込みが「乗っている」ような結果が得られますが,どうもこう,物体表面に光沢のあるフィルムが貼られているような不自然さがあります.これは映り込みと拡散反射光の配分比を固定していることが原因になっています.

鏡面反射光は光源の映り込みを模したものなので,環境マッピングを行う場合は,鏡面反射光を置き換えることになります.このとき,映り込みと拡散反射光の配分比を固定していると,物体の面がどの方向を向いていようとも同じように環境が映り込んでしまいます.しかし,鏡面反射光強度は光の入射角によって変化します.光の入射角が浅いほど鏡面反射光強度は強まり,全反射に近づいてゆきます.この現象はフレネル (Fresnel) 反射と呼ばれます.

観測角の違いによる映り込みの違い

そこで前回のプログラムを雛形にして,フレネル反射の実装を行ってみます.

フレネルの式

フレネル反射を考慮した映り込みを実装するには,入射角 $\theta_i$ と境界面(物体表面)の両側の媒質における屈折率の比 $n$ から次のフレネルの式により反射率 $F$ を求め,これを鏡面反射率に反映します.

\[\begin{aligned} c &= \cos\theta_i \\ g &= \sqrt{n^2 + c^2 - 1} \\ F &= \frac{1}{2}\frac{\left(g-c\right)^2}{\left(g+c\right)^2}\left(1+\frac{\left(c\left(g+c\right)-1\right)^2}{\left(c\left(g-c\right)+1\right)^2}\right) \end{aligned}\]

ただ,この式はちょっと複雑なので,ゲームなどでは Schlick の近似がよく用いられます.$\overline{\cos}\theta_i=\max\left(\cos\theta_i, 0\right)$ です.

\[F \approx F_0+\left(1-F_0\right)\left(1-\overline{\cos}\theta_i\right)^5\]

この $F_0$ は入射角 $\theta_i=0$ のときの反射率です.

\[F_0=\left(\frac{n-1}{n+1}\right)^2\]

ですが,今回は,これをテクスチャコンバイナを使って,テクスチャマッピングだけで実装しようと思います.そのために,反射率 $F$ のテーブルを作り,それを1次元テクスチャにします.この処理は事前に行うため,$F$ の計算に少々時間がかかっても気にならないと思うので,元のフレネルの式を使うことにします.

視線ベクトルを $\mathbf{v}$,面の法線ベクトルを $\mathbf{n}$ とすれば,$\cos\theta_i=\mathbf{v}\cdot\mathbf{n}$ となります.ここで視線ベクトルを $\mathbf{v} = \left(0, 0, 1\right)$ に固定してしまえば,$c$ に法線ベクトル $\mathbf{n}$ の Z 成分をそのまま使えば良いことになります.

したがって,この1次元テクスチャを,面の法線ベクトルの Z 成分を使ってサンプリングします.こうして得たテクスチャの値を映り込みと拡散反射光の配分比に使用すれば,フレネル反射が実現できます.多分.

フレネル反射の実装

このプログラムは,下地のテクスチャ,$F$ のテーブルの1次元テクスチャ,キューブマッピングによる環境マッピングのテクスチャの3つのテクスチャを使います.したがって,テクスチャ名も3つ用意します.

/*
** テクスチャ
*/
#define TEXWIDTH  256                               /* テクスチャの幅    */
#define TEXHEIGHT 256                               /* テクスチャの高さ   */
static const char texture_file[] = "dot.raw";       /* テクスチャファイル名 */
static GLuint texname[3];                           /* テクスチャ名(番号) */

反射率 $F$ を求める関数を定義します.

/*
** フレネル関数
*/
static float fresnel(float c)
{
  const float n = 1.5f; /* 屈折率の比 */
  const float g = sqrtf(n * n + c * c - 1.0f);
  const float gpc = g + c;
  const float gmc = g - c;
  const float gpc1 = c * gpc - 1.0f;
  const float gmc1 = c * gmc + 1.0f;
  const float gc = gmc / gpc;
  const float gc1 = gpc1 / gmc1;
  return 0.5 * gc * gc * (1.0f + gc1 * gc1);
}

まず,テクスチャ名を3つ作ります.

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

#if defined(_WIN32)
  glActiveTexture =
    (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture");
#endif

  /* テクスチャ名を3つ作る */
  glGenTextures(3, texname);
}

テクスチャコンバイナを使って複数のテクスチャを合成してマッピングする場合は,テクスチャユニットを下から GL_TEXTURE0GL_TEXTURE1GL_TEXTURE2 → … というように,重ねるテクスチャの順番に合わせて割り当てる必要があります.したがって,一番下の下地のテクスチャは,GL_TEXTURE0 に割り当てます.

  /* 1つ目のテクスチャ名には2次元テクスチャを割り当てる */
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, texname[0]);

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

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

このテクスチャはポリゴンの陰影の影響を受けるので,テクスチャ環境に GL_MODULATE を設定して,テクスチャの色を下地の色に乗算します.

  /* テクスチャユニット0のテクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

2番目のテクスチャユニット GL_TEXTURE1 には,$F$ の1次元テクスチャを割り当てます.このテクスチャのフォーマットは GL_LUMINANCE_ALPHA とし,アルファチャンネル A を $F$ のテーブルとして利用します.

  /* 2つ目のテクスチャにはフレネル関数のテーブルを割り当てる */
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_1D, texname[1]);

  /* フレネル関数のテーブル */
  GLfloat table[128][2];

  /* フレネル関数のテーブル作成 */
  for (int i = 0; i < 128; ++i) {
    table[i][0] = 1.0f;
    table[i][1] = fresnel((float)i / 127.0f);
  }

  /* フレネル関数のテーブルを一次元テクスチャとして割り当て */
  glTexImage1D(GL_TEXTURE_1D, 0, GL_LUMINANCE_ALPHA, 128, 0,
    GL_LUMINANCE_ALPHA, GL_FLOAT, table);

フレネル関数の値はなだらかな曲線ですが,このテクスチャをサンプリングするときは一応線形補間することにしておきます.また,このテクスチャの両端の値が境界色とブレンドされてしまっては困るので,テクスチャの繰り返し方法には GL_CLAMP_TO_EDGE を指定します.

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

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

下地のアルファ値(この場合はグロスマッピング用のテクスチャ)にフレネル関数のテーブルの値を合成するために,このテクスチャユニットのテクスチャ環境にも GL_MODULATE を指定します.

このテクスチャは LUMINANCE すなわち RGB が 1 なので,これは下のテクスチャの値をそのまま使います.アルファ値 A も乗算になるので,下のテクスチャのアルファ値 A に,このテクスチャのアルファチャンネル A に格納した反射率 $F$ を掛けたものを使うことになります.

  /* テクスチャユニット1のテクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

このテクスチャのサンプリングに法線ベクトルの z 成分を使うので,テクスチャ座標の自動生成機能を使って(頂点の法線ベクトルの反射点における補間値の)法線ベクトルを得ます.使用するのは z 座標値だけなので,z 成分 (GL_R) のみ自動生成を行います.

  /* 法線ベクトルのz成分をテクスチャ座標として補間する */
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_NORMAL_MAP);

  /* テクスチャ座標の自動生成を有効にする */
  glEnable(GL_TEXTURE_GEN_R);

しかし,このままでは自動生成された法線ベクトルの z 成分がテクスチャ座標 (s, t, r, q) の r に格納されます.1次元テクスチャでは s をテクスチャ座標として用いるので,r を s に移す必要があります.このために,テクスチャ座標の変換行列 (GL_TEXTURE) に r と s を交換する変換行列を設定します.

なお,テクスチャ座標の変換行列は,テクスチャユニットごとに個別に保持されています.下記の変換行列 mat による変換は,この時点で有効になっているテクスチャユニット (GL_TEXTURE1) に割り当てられたテクスチャのみに適用されます.

  /* テクスチャのパラメータ r を s と交換する */
  static const GLdouble mat[] = {
    0.0, 0.0, 1.0, 0.0,
    0.0, 1.0, 0.0, 0.0,
    1.0, 0.0, 0.0, 0.0,
    0.0, 0.0, 0.0, 1.0,
  };
  glMatrixMode(GL_TEXTURE);
  glLoadMatrixd(mat);
  glMatrixMode(GL_MODELVIEW);

3番目のテクスチャユニット GL_TEXTURE2 には,キューブマッピングによる環境マッピングの設定を行います.ローエンドのビデオカードでは,テクスチャユニットが2個しか使えないものがあるので(てゆーか,演習室のマシンがそうだった),ビデオカードによっては正しくマッピングされなくなるかもしれません.また,3個以上のテクスチャユニットが使えても,実際に3個以上のテクスチャユニットを使うと,パフォーマンスが悪化する場合があります1

  /* 3つ目のテクスチャにはキューブマップを割り当てる */
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_CUBE_MAP, texname[2]);

このテクスチャユニット (GL_TEXTURE2) のテクスチャ環境 (GL_TEXTURE_ENV_MODE) にテクスチャコンバイナを指定 (GL_COMBINE) し,テクスチャユニットが出力する色の RGB の決定方法 (GL_COMBINE_RGB) に,このテクスチャの色と下地のテクスチャの色の線形補間 (GL_INTERPOLATE) を用いる設定をします.

次に,下地のテクスチャ (GL_PREVIOUS) を,入力の2番のソース $\alpha_2$ の RGB (GL_SOURCE2_RGB) に指定します.また,この $\alpha_2$ の RGB (GL_OPERAND2_RGB) に下地のテクスチャのアルファ値 (GL_SRC_ALPHA) を設定することによって,$\alpha_2$ の RGB のすべてのチャンネルに下地のテクスチャのアルファ値 A,すなわち $F$ が使用されます.

0番目のソース $\alpha_0$ の RGB (GL_SOURCE0_RGB) には GL_TEXTURE すなわちキューブマッピングのテクスチャ,1番目のソース $\alpha_1$ の RGB (GL_SOURCE1_RGB) には GL_PREVIOUS すなわち下地のテクスチャがそれぞれデフォルトで設定されています.

GL_INTERPOLATE はテクスチャの色を決定する際に $\alpha_0\alpha_2 + \alpha_1\left(1-\alpha_2\right)$ という計算をするので,このテクスチャユニットが出力する色は,キューブマッピングのテクスチャの色 $\alpha_0$ と下地のテクスチャの色 $\alpha_1$ を,下地のテクスチャのアルファ値 $\alpha_2$ によって比例配分したものになります.

  /* テクスチャユニット2のテクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
  glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
#if 0
  static const GLfloat blend[] = { 1.0f, 1.0f, 1.0f, 0.5f };
  glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, blend);
#else
  glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB, GL_PREVIOUS);
  glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_SRC_ALPHA);
#endif

上記の設定について補足します.テクスチャコンバイナ (GL_COMBINE モード) では,通常3つのソース (GL_SOURCE0_RGB, GL_SOURCE1_RGB, GL_SOURCE2_RGB) を組み合わせて色を計算します.それぞれのソースには,計算に使いたい色データを指定します.主に以下のような定数を指定します.

GL_SOURCE2_RGB で指定した色データをそのまま使うか,色を反転させて使うかを指定します.

そして,描画時にテクスチャユニット0 (GL_TEXTURE0) を有効にして下地のテクスチャ (texname[0]) を割り当て,テクスチャユニット1 (GL_TEXTURE1) を有効にして反射率 $F$ のテーブルを格納した1次元テクスチャ (texname[1]) を割り当て,テクスチャユニット2 (GL_TEXTURE2) を有効にしてキューブマッピングのテクスチャ (texname[2]) を割り当てます.

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

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

  /* テクスチャユニット0をアクティブにする */
  glActiveTexture(GL_TEXTURE0);

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

  /* テクスチャユニット1をアクティブにする */
  glActiveTexture(GL_TEXTURE1);

  /* 1次元テクスチャマッピング開始 */
  glEnable(GL_TEXTURE_1D);

  /* テクスチャユニット2をアクティブにする */
  glActiveTexture(GL_TEXTURE2);

  ...

  /* テクスチャユニット1に切り替える */
  glActiveTexture(GL_TEXTURE1);

  /* 1次元テクスチャマッピング終了 */
  glDisable(GL_TEXTURE_1D);

  /* テクスチャユニット0に戻す */
  glActiveTexture(GL_TEXTURE0);

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

この結果は次のようになります.右側は図形をティーポットに変え,カメラや照明のパラメータを多少変更したものです.この図ではあまり映り込みがはっきり見えないのですが,これは自分でプログラムを実行して,物体をぐるぐる回してみると,それらしく見えます(多分).

フレネル反射のある物体 物体をティーポットに変更

このプログラムにおいて視線の入射角が浅い場合でも映り込みがあまりはっきりと現れていないのは,環境のテクスチャが明るくない割に下地のテクスチャが明るいことが一番大きな原因だと思うのですが,視線ベクトルを (0, 0, 1) に固定していることも原因の一つになっている気がします.もし,正確な視線ベクトルを用いることができれば,より物体の「内側」に視線の入射角が浅い面が現れるはずです.

視線のモデルによる入射角の違い

グロスマッピングをやめてみる

グロスマッピングをやめてしまうことで,環境の映り込みが多少わかりやすくなります.グロスマッピングのテクスチャ(下地のテクスチャのアルファ値)にフレネル関数のテーブルを合成しないで,フレネル関数のテーブルの値をそのまま映り込みと拡散反射光の配分比に使います.

これはテクスチャコンバイナを使って,RGB (GL_COMBINE_RGB) には乗算 (GL_MODULATE) を設定し,アルファ値 A (GL_COMBINE_ALPHA) には置き換え (GL_REPLACE) を設定します.

  /* テクスチャユニット1のテクスチャ環境 */
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
  glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE);

グロスマッピングをオフにした場合 物体をティーポットに変更

うーん,やっぱりあまりよくわかりませんね.スクリーンショットを撮りそこなっていますけど,すごく浅い角度にしたら何か写っているように見えます.また,白い車よりは黒い車の方が映り込みがはっきり見えたりするので,そのうちモデルなり色なりを変えていろいろ試してみたいと思います.ここではとにかくプログラムを動かして,自分で眺めてください.なお,なぜか Windows では,マルチテクスチャを使うと glutSolidTeapot() がエラーになります2.Vine Linux 3.1 や Mac OS X 10.3 では問題ありませんでした.

  1. 今のビデオカードは,そんなことはありません. (2026 年 5 月 10 日追記) 

  2. これはオリジナルの GLUT の問題みたいで,FreeGLUT なら問題ありませんでした.(2026 年 5 月 12 日追記)