床井研究室

就職活動

現在、うちの研究室の学部4年生が就職活動にいそしんでいます。すでに活動の「第1波」が過ぎたようで、よその研究室からは内定を取ったという話がちらほらと入ってきます。でもうちの4年生は、なんだかのんきに構えているように見えます。大丈夫かなぁ(内心、焦っているのかもしれませんが)。

それにしても就職活動というのは、本当にお見合いとよく似ていて、もちろん本人の能力や資質も問われますけど、それ以上に「縁」とか「相性」とか「運」みたいなものが成否を左右します。ところが学生さんは、偏差値という物差しを使って大学までやってきたせいか、同じような物差しが企業の間にもあるような錯覚をしていることがあって、就職活動の成否によって自分が「ランク付け」されているように感じることがあるみたいです。そのため、うまくいかないとどんどん落ち込んでいったりします。そこで、「うまくいかなかったのは相性が合わなかっただけだから、また自分も相手(就職先)もハッピーになれるような縁を探しに行こう」などとお仲人さんのようなことを言ったりします。あ、そういや先方が不採用を通知してくるとき、「ご縁が無かったということで…」って言うよな。

シャドウマッピング

今回は以前にテクスチャマッピングのテクニックとして紹介したシャドウマッピングを、GLSL を使って実装してみたいと思います。雛形のプログラムはこのときに用意したものを用います。このテクニックの肝となるテクスチャ変換行列の設定方法については、シャドウマッピングのところを参照してください。

このアーカイブには main() を含んだソースプログラムが三つ含まれていますが、ここではその中の main1.cpp を使います。これは次のような真っ黒い影を作ります。

影をつけたシーン

テクスチャ座標の自動生成は不要

シャドウマッピングでは物体表面上の三次元位置を求めるために、テクスチャ座標の自動生成機能を使っていました。シェーダを使う場合は、バーテックスシェーダで頂点の座標値を得ることができるので、この設定は不要になります(というか、シェーダを使うとテクスチャ座標の自動生成が機能しない?)。まず main1.cpp から次のテクスチャ座標の自動生成の設定を削除します。

  /* テクスチャ座標に視点座標系における物体の座標値を用いる */
  glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
  glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

  /* 生成したテクスチャ座標をそのまま (S, T, R, Q) に使う */
  static const GLdouble genfunc[][4] = {
    { 1.0, 0.0, 0.0, 0.0 },
    { 0.0, 1.0, 0.0, 0.0 },
    { 0.0, 0.0, 1.0, 0.0 },
    { 0.0, 0.0, 0.0, 1.0 },
  };
  glTexGendv(GL_S, GL_EYE_PLANE, genfunc[0]);
  glTexGendv(GL_T, GL_EYE_PLANE, genfunc[1]);
  glTexGendv(GL_R, GL_EYE_PLANE, genfunc[2]);
  glTexGendv(GL_Q, GL_EYE_PLANE, genfunc[3]);

代わりに、シェーダプログラムの読み込み処理を追加します。glsl.h と glsl.cpp は、前回のサンプルプログラムのものなどを用いてください。シェーダプログラムのリンクに成功したら、シャドウマップのサンプラの uniform 変数 texture の場所を取り出しておきます。

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

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

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

  ...

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

  /* シャドウマップのサンプラの uniform 変数の場所を得る */
  colorLoc = glGetUniformLocation(gl2Program, "texture");

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

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

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

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

  /* 書き込むポリゴンのテクスチャ座標値のRとテクスチャとの比較を行うようにする */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

  /* もしRの値がテクスチャの値以下なら真(つまり日向) */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

  /* 比較の結果を輝度値として得る */
  glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);

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

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

  /* 光源の初期設定 */
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb);
}

シーンの描画

シーンの描画時には、シェーダプログラムを適用し、シャドウマップのサンプラにテクスチャユニット番号を設定します。また、テクスチャ座標の自動生成を有効にしていた部分を削除します。

static void display()
{
  ...

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

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

  /*
  ** 第1ステップ:デプステクスチャの作成
  */

  ...

  /*
  ** 第2ステップ:全体の描画
  */

  ...

  /* テクスチャマッピングとテクスチャ座標の自動生成を有効にする */
  glEnable(GL_TEXTURE_2D);
#if 0
  glEnable(GL_TEXTURE_GEN_S);
  glEnable(GL_TEXTURE_GEN_T);
  glEnable(GL_TEXTURE_GEN_R);
  glEnable(GL_TEXTURE_GEN_Q);
#endif

  /* 光源の明るさを日向の部分での明るさに設定 */
  glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol);
  glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol);

  /* シーンを描画する */
  scene(t);

  /* テクスチャマッピングとテクスチャ座標の自動生成を無効にする */
#if 0
  glDisable(GL_TEXTURE_GEN_S);
  glDisable(GL_TEXTURE_GEN_T);
  glDisable(GL_TEXTURE_GEN_R);
  glDisable(GL_TEXTURE_GEN_Q);
#endif
  glDisable(GL_TEXTURE_2D);

  /* シェーダプログラムの適用解除 */
  glUseProgram(0);

  /* ダブルバッファリング */
  glutSwapBuffers();
}

シェーダプログラム内で gl_Color を参照できるように、物体の拡散反射係数(色)は glMaterial*() の代わりに glColor*() を使って指定するようにします。鏡面反射係数と輝き係数は、これまで通り glMaterial*() で指定します。

// scene.cpp

/*
** シーンの描画
*/
void scene(double t)
{
  static const double r = 1.5;
  double wt = 2.0 * M_PI * t;

  /* タイルを描く */
  glPushMatrix();
  glTranslated(-3.0, -2.0, -3.0);
  tile(1.0, 1.0, 6, 6);
  glPopMatrix();

  /* 箱を描く */
  glPushMatrix();
  glTranslated(-1.0, -1.5, -1.0);
  box(2.0, 1.0, 2.0);
  glPopMatrix();

  /* 球を描く */
  glPushMatrix();
  glTranslated(r * cos(wt), 1.0, r * sin(wt));
  static const GLfloat red[] = { 0.8f, 0.2f, 0.2f, 1.0f };
  static const GLfloat specular[] = { 0.1f, 0.1f, 0.1f, 1.0f };
  glColor3fv(red);
  glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
  glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 20.0f);
  glutSolidSphere(0.9, 32, 16);
  glPopMatrix();
}

バーテックスシェーダ

バーテックスシェーダでは、Gouraud シェーディングと同様に拡散反射光強度と鏡面反射光強度の計算を行いますが、環境光強度はフラグメントシェーダ側で合成するため頂点色には含めません。また、頂点座標にテクスチャ変換行列を掛けてテクスチャ座標を求め、環境項の反射光強度を ambient という varying 変数でフラグメントシェーダに送ります。

#version 120

// shadow.vert

// 環境項の反射光強度
varying vec4 ambient;

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

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

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

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

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

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

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

  // 視点座標系の中間ベクトル
  vec3 halfway = normalize(light + view);

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

  // 環境項の反射光強度をフラグメントシェーダに送る
  ambient = gl_LightSource[0].ambient * gl_Color;

  // 環境光強度はフラグメントシェーダで設定するので頂点の色に含めない
  gl_FrontColor = gl_LightSource[0].diffuse * gl_Color * diffuse
                + gl_LightSource[0].specular * gl_FrontMaterial.specular * specular;
}

フラグメントシェーダ

フラグメントシェーダでは GLSL の組み込み関数 shadow2DProj() を使ってシャドウマップをサンプリングします。これにより、環境光(影の部分でも常に残る成分 ambient)に、シャドウマップの比較結果(日向なら 1、影なら 0)を乗じた直接光(拡散反射光+鏡面反射光 gl_Color)を加算してフラグメントの色を決定します。

#version 120

// shadow.frag

// シャドウマップ
uniform sampler2DShadow texture;

// 環境項の反射光強度の補間値
varying vec4 ambient;

void main ()
{
  // フラグメントの色
  gl_FragColor = shadow2DProj(texture, gl_TexCoord[0]) * gl_Color + ambient;
}

GLSL によるシャドウマッピング

日向と影の描き分けが不要

以前に解説したシャドウマッピングでは、影の部分に陰影をつけるために、日向と影の部分を別々にレンダリングしていました。シェーダを使った場合は日向と影の陰影を同時に計算できるため、このような描き分けが不要になり、レンダリング回数を1回減らすことができます。しかし、GeForce FX 5200 だとフラグメントシェーダが遅いためか、かえって処理が遅くなってしまいました。

余談

GLSL の組み込み関数 shadow2DProj() の実装が ATI と nVIDIA で異なっていて、最初これらの間で同じ結果が得られずにえらく悩みました。ATI の方はオレンジブックに則したもので、私の期待したとおり 0 か 1 の値を返してくれたのですが、nVIDIA の方はなぜか中間的な値を返してくるのです。 いろいろ調べた結果、nVIDIA の GLSL のリリースノートに「glTexParameteri()GL_TEXTURE_COMPARE_MODEGL_COMPARE_R_TO_TEXTURE を指定していれば、テクスチャの値として比較の結果をサンプリングするので、shadow2DProj()texture2DProj() と同じである」なんてことが書いてありました。私は最初この glTexParameteri() をはずしていたので、nVIDIA の shadow2DProj() の動作が期待と異なるものになってました。 なお、GL_TEXTURE_COMPARE_FUNCGL_DEPTH_TEXTURE_MODE の設定も、shadow2DProj() が返す値に影響を与えます。GL_TEXTURE_COMPARE_FUNCGL_LEQUAL を設定したときは、参照点がテクスチャの値以下(すなわち日向の)時に shadow2DProj() は 1 を返します。また GL_DEPTH_TEXTURE_MODEGL_LUMINANCE を設定すれば、shadow2DProj() が返す値の RGB に結果が格納されます。これは ATI、nVIDIA とも同じでした。