床井研究室

板書

私が担当しているコンピュータグラフィックスという講義では、板書で解説をしています。いつもチョークまみれになってしまいますし、内心では「画像や3次元図形を板書するのは無理があるなぁ」と思っているのですが、もう意地になって板書しています。しかし先日、講義が終わった後に、一人の学生さんから「どうして PowerPoint 使わないんですか?」と言われてしまいました。

さすがに「意地で」とは言えないので、「PowerPoint を使うと自分でノートを整理してくれないし、私が手で書いているんだから、みんなにも手でノートを取ってほしいと思って」とか建前を答えたんですが、こちらから「やっぱり何かまずいか?」と聞いてみたら、「いえ、なんかいつも息を切らして授業されてるから」と言われてしまいました orz。はい、キツイんです。すみません。こういう講義をしているせいかどうも空気が重たいので、この間、天津木村を真似て「あると思います!」と言ってみたんですが、さらに冷たい風が吹きました。そのうち講義が終わったら「ひぐちカッター!」とかやってみたいんですが、勇気が出ません。

Depth Peeling

Depth Peeling(深度剥離)とは、重なり合うポリゴンの層を奥行き方向に1枚1枚はがしていく手法で、Order-Independent Transparency において半透明処理を実現するために導入されました。この手法は半透明処理以外にも、集合演算処理ポリゴンモデルのボクセル化など、形状処理にもいろいろ使えそうな手法なので、だいぶ前からそのうちやってみようと思っていたのですが、なかなかプログラムを書いてみる気にならなくてほったらかしにしてました。

Depth Peeling の原理

まず、デプスバッファを使って図形の隠面消去処理を行います。これにより、視点に最も近い面を取り出すことができます。これをレイヤー0と呼ぶことにします。その際、デプスバッファの内容を保持しておきます(緑色の部分)。

デプスバッファによる隠面消去処理

もう一度図形を描画します。その際、保存しておいたデプスバッファの内容(黄色の部分)を参照し、その値よりも奥にある図形についてのみ通常のデプスバッファを用いて隠面消去処理を行います(緑色の部分)。そうすると、視点に最も近い面に隠されていた、2番目に視点に近い面を取り出すことができます。これをレイヤー1と呼ぶことにします。

2番目に視点に近い面の取り出し

このデプスバッファの内容を保存しておき、もう一度同じ処理を繰り返せば、3番目に視点に近い面を取り出すことができます。これをレイヤー2と呼ぶことにします。

3番目に視点に近い面の取り出し

このように、Depth Peeling を実現するには、2つのデプスバッファが必要になります。ところが、OpenGL では(シェーダを使わなければ)同時に2つのデプスバッファを使うことができません。そこで2つ目のデプスバッファとして、シャドウマッピングに用いるデプステクスチャを使います。

Depth Peeling の手順

まず、初期設定において、デプステクスチャを準備します。

void init(void)
{
  ...

  /* テクスチャの割り当て-このテクスチャを前方のデプスバッファとして使う */
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, TEXWIDTH, TEXHEIGHT, 0,
    GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);

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

この glTexImage2D() の最初の GL_DEPTH_COMPONENT は、GL_DEPTH_COMPONENT32 など、精度を指定したほうがいいかもしれません。次にシャドウマッピングの設定を行います。シャドウマッピングの判定の結果はアルファ値として得て、アルファテストを使います。ポイントは、フラグメントとデプステクスチャとの比較関数を GL_GREATER に設定するあたりでしょう。

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

  /* もしRの値がテクスチャの値を超えていたら真(フラグメントを描く) */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_GREATER);

  /* 比較の結果をアルファ値として得る */
  glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_ALPHA);

  /* アルファテストの比較関数(しきい値) */
  glAlphaFunc(GL_GEQUAL, 0.5f);

シャドウマッピングを行うので、テクスチャ座標を自動生成します。これは視点座標系で生成します。

  /* テクスチャ座標に視点座標系における物体の座標値を用いる */
  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]);

あと、デプスバッファとデプステクスチャの精度が完全に一致していないと、アルファテストに失敗して一つ前の面のフラグメントが残ってしまうので、ポリゴンオフセットを使ってポリゴンを少しずらすことにします。

  ...

  /* Polygon Offset の設定 */
  glPolygonOffset(1.0f, 1.0f);

  ...
}

シーンは、以下の手順で描画します。まず、テクスチャ変換行列に透視変換行列を設定し、視点座標がデプステクスチャのテクスチャ座標に変換されるようにしておきます。影付けの場合と異なり、今回は視点の位置と影の投影中心の位置が一致しているので、これにモデルビュー変換行列をかける必要はありません。

static void display(void)
{
  GLdouble projection[16]; /* 透視変換行列の保存用 */
  int i;

  ...

  /* テクスチャ変換行列を設定する */
  glMatrixMode(GL_TEXTURE);
  glLoadIdentity();

  /* テクスチャ座標の [-1,1] の範囲を [0,1] の範囲に収める */
  glTranslated(0.5, 0.5, 0.5);
  glScaled(0.5, 0.5, 0.5);

  /* 現在の透視変換行列を取り出す */
  glGetDoublev(GL_PROJECTION_MATRIX, projection);

  /* 透視変換行列をテクスチャ変換行列に設定する */
  glMultMatrixd(projection);

  /* モデルビュー変換行列に戻す */
  glMatrixMode(GL_MODELVIEW);

そしてデプスバッファを 0 でクリアしておきます。これでデプスバッファの内容は前方面の位置になります。そのあと、デプスバッファの消去値を 1(後方面の位置)に戻しておきます。

  /* デプスバッファを0でクリア */
  glClearDepth(0.0);
  glClear(GL_DEPTH_BUFFER_BIT);

  /* デプスバッファの消去値を1に戻す */
  glClearDepth(1.0);

テクスチャマッピングとテクスチャ座標、アルファテスト、それにポリゴンオフセットを有効にします。

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

  /* アルファテストを有効にする */
  glEnable(GL_ALPHA_TEST);

  /* Polygon Offset を有効にする */
  glEnable(GL_POLYGON_OFFSET_FILL);

先にデプスバッファの内容をデプステクスチャにコピーした後、デプスバッファをクリアしてから図形の描画を行います。この描画では、図形はまずデプステクスチャ(直前に保存したデプスバッファの内容)と比較され、これより奥にあるものだけがアルファテストによって切り抜かれます。切り抜かれた図形は通常のデプスバッファによって奥行き比較され、もっとも手前にあるものだけが描画されます。これを表示したいレイヤーの番号の数だけ繰り返します。

  for (i = 0; i <= layer; ++i) {

    /* デプスバッファをデプステクスチャにコピー */
    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, TEXWIDTH, TEXHEIGHT);

    /* フレームバッファとデプスバッファをクリアする */
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

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

描画が終わったら、設定を元に戻します。

  /* Polygon Offset を無効にする */
  glDisable(GL_POLYGON_OFFSET_FILL);

  /* アルファテストを無効にする */
  glDisable(GL_ALPHA_TEST);

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

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

このサンプルプログラムは、例によってマウスの左ボタンのドラッグで視点を回せます。また表示するレイヤーの切り替えは、0〜9 のキーで行うことができます。

Depth Peeling

Frame Buffer Object の使用

もとの資料では、2つのデプスバッファを切り替えることによってデプステクスチャのコピーを行わない手順が書かれていますが、デプステクスチャは参照はできても通常のデプスバッファのように値の更新はできないので、このサンプルでは glCopyTexSubImage2D() を使いました。テクスチャのコピーを避けるには、シャドウマッピングによって影付けを行う場合と同様に Frame Buffer Object を使う必要があります。