«フレームバッファオブジェクトの使い方あげいん 最新 ビルボード»

床井研究室

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

■ 2010年12月08日 [OpenGL][GLSL] マルチプルレンダーターゲット

2023年10月30日 08:30更新

遅延レンダリング

学生さんに向けたフレームバッファオブジェクトの説明の続きです. どうも口で説明するより文章に書いた方が理解してもらえそうな気がするので, 目を "ひんむいて" よく読んでね. 君がやろうとしているように, レンダリング結果の画像を事後処理でコネコネしたければ, レンダリング結果を画面に出力せずに一旦どっかにとっておいて, それを使ってもう一度レンダリングする必要があります. そういうテクニックを遅延レンダリング (deferred rendering) といいます. んで, レンダリング結果を CPU を介さずに GPU 側にとっておく機能がフレームバッファオブジェクトです.

しかし通常のレンダリングでは, フレームバッファのカラーバッファにカラーデータ, すなわちレンダリングされた画像だけが出力されます. これを事後処理でコネコネしようと思っても, これだけでは情報が不足することがあります. レンダリングの途中には様々な中間結果が得られるので, 事後処理の時にもこれらを活用できると, できることが結構ひろがりんぐなのです.

ということで, フレームバッファオブジェクトに複数のバッファを組み込んで, レンダリング結果の出力先 (render target) を複数使えるようにする機能が, マルチプルレンダーターゲット (Multiple Render Targets, MRT) です.

プログラマブルシェーダを使ってティーポットを描く

前回と同様に, 単にティーポットを描くプログラムを作ります. ただし, 今回はプログラマブルシェーダを使います. 陰影計算は OpenGL の固定機能を真似て作っていますが, 手を抜いています.

プログラムの初期化時に関数 loadShader() を使ってシェーダのソースプログラムを読み込みます. loadShader() で使っている関数は別に用意することにします. あと, 陰影付けをプログラマブルシェーダで行うので, glEnable(GL_LIGHTING); なんてものはどうでもよくなります.

...
 
/*
** シェーダオブジェクト
*/
#include "glsl.h"
static GLuint pass1;
 
/*
** シェーダプログラムの読み込み
*/
static GLuint loadShader(const char *vert, const char *frag)
{
  ...
}
 
/*
** 初期化
*/
static void init(void)
{
#if defined(WIN32)
  // GLEW の初期化
  GLenum err = glewInit();
  if (err != GLEW_OK) {
    fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
    exit(1);
  }
#endif
  
  // シェーダプログラムの作成
  pass1 = loadShader("pass1.vert", "pass1.frag");
  
  ...

図形の描画の際にシェーダオブジェクト pass1 を指定して, プログラマブルシェーダを使って処理するようにします. 描画が終わったら, 次の段階 (事後処理) のためにプログラマブルシェーダの指定を解除して, 固定機能シェーダに戻しておきます.

...
 
/*
** 画面表示
*/
static void display(void)
{
  // 透視変換行列の設定
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(30.0, (GLdouble)width / (GLdouble)height, 1.0, 10.0);
  
  // モデルビュー変換行列の設定
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
  
  // ビューポートの設定
  glViewport(0, 0, FBOWIDTH, FBOHEIGHT);
  
  // 隠面消去を有効にする
  glEnable(GL_DEPTH_TEST);
  
  // フレームバッファオブジェクトを結合する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
  
  // pass1 シェーダを有効にする
  glUseProgram(pass1);
  
  // カラーバッファとデプスバッファをクリア
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  // シーンの描画
  glutSolidTeapot(1.0);
  glFlush();
  
  // シェーダを無効にする
  glUseProgram(0);
  
  // フレームバッファオブジェクトの結合を解除する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  
  ...

バーテックスシェーダは次のようになります. 陰影付けの手を抜いているので, 座標値の w 要素が 1 でないと正しく陰影付けできません. 光源も有限位置にないといけません (その割に減衰なし). また, 計算した陰影は通常 gl_FrontColor / gl_BackColor に代入してフラグメントシェーダに送りますが, ここでは陰影計算の要素ごとに分けて varying 変数に代入します.

#version 120
//
// pass1.vert
//
 
varying vec4 ambient;                                                 // 環境光の反射光 A
varying vec4 diffuse;                                                 // 拡散反射光 D
varying vec4 specular;                                                // 鏡面反射光 S
 
void main(void)
{
  vec3 position = vec3(gl_ModelViewMatrix * gl_Vertex);
  vec3 normal = normalize(gl_NormalMatrix * gl_Normal);
  
  vec3 light = normalize(gl_LightSource[0].position.xyz - position);  // その点から光源に向かう光線ベクトル L
  vec3 view = -normalize(position);                                   // その点から視点に向かう視線ベクトル V
  vec3 halfway = normalize(light + view);                             // 中間ベクトル H
  
  float nl = dot(normal, light);
  float nh = pow(dot(normal, halfway), gl_FrontMaterial.shininess);
  
  // 環境光
  ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
  
  // 拡散反射光
  diffuse = gl_FrontMaterial.diffuse * vec4(vec3(nl), 1.0) * gl_LightSource[0].diffuse;
  
  // 鏡面反射光
  specular = gl_FrontMaterial.specular * vec4(vec3(nh), 1.0) * gl_LightSource[0].specular;
  
  gl_Position = ftransform();                                         // 頂点位置の座標変換
}

フラグメントシェーダでは, varying 変数の値を合計して, 画素の色を決定します.

#version 120
//
// pass1.frag
//
 
varying vec4 ambient;                                                 // 環境光の反射光 A
varying vec4 diffuse;                                                 // 拡散反射光 D
varying vec4 specular;                                                // 鏡面反射光 S
 
void main(void)
{
  gl_FragColor = diffuse + specular + ambient;
}

これで前回と同様ティーポットが描かれると思います.

フレームバッファオブジェクトにレンダーターゲットを追加する

ここからが本題です. それまで使っていたカラーバッファには拡散反射光を格納するものとし, 新たに鏡面反射光と環境光の反射光を格納するレンダーターゲットを追加します. そのために, まずレンダーターゲットに使うテクスチャを用意します.

...
 
static int width, height;   // スクリーンの幅と高さ
 
#define FBOWIDTH  512       // フレームバッファオブジェクトの幅
#define FBOHEIGHT 512       // フレームバッファオブジェクトの高さ
 
static GLuint fb;           // フレームバッファオブジェクト
static GLuint cb;           // カラーバッファ用のテクスチャ
static GLuint rb;           // デプスバッファ用のレンダーバッファ
 
static GLuint sb;           // 鏡面反射光
static GLuint ab;           // 環境光の反射光
 
...
 
/*
** 初期化
*/
static void init(void)
{
#if defined(WIN32)
  // GLEW の初期化
  GLenum err = glewInit();
  if (err != GLEW_OK) {
    fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
    exit(1);
  }
#endif
  
  // シェーダプログラムの作成
  pass1 = loadShader("pass1.vert", "pass1.frag");
  
  // カラーバッファ用のテクスチャを用意する
  glGenTextures(1, &cb);
  glBindTexture(GL_TEXTURE_2D, cb);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FBOWIDTH, FBOHEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);
  
  // 鏡面反射光を格納するテクスチャを用意する
  glGenTextures(1, &sb);
  glBindTexture(GL_TEXTURE_2D, sb);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FBOWIDTH, FBOHEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);
  
  // 環境光の反射光を格納するテクスチャを用意する
  glGenTextures(1, &ab);
  glBindTexture(GL_TEXTURE_2D, ab);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FBOWIDTH, FBOHEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glBindTexture(GL_TEXTURE_2D, 0);
  
  // デプスバッファ用のレンダーバッファを用意する
  glGenRenderbuffersEXT(1, &rb);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, rb);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, FBOWIDTH, FBOHEIGHT);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);

これらをフレームバッファオブジェクトに追加します. もともとカラーバッファに使っていたテクスチャは GL_COLOR_ATTACHMENT0_EXT というバッファに割り当てていました. 残りのテクスチャはそれぞれ GL_COLOR_ATTACHMENT1_EXT と GL_COLOR_ATTACHMENT2_EXT に割り当てます.

  // フレームバッファオブジェクトを作成する
  glGenFramebuffersEXT(1, &fb);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
  
  // フレームバッファオブジェクトにカラーバッファとしてテクスチャを結合する
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, cb, 0);
  
  // フレームバッファオブジェクトに鏡面反射光の格納先のテクスチャを結合する
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, sb, 0);
  
  // フレームバッファオブジェクトに環境光の反射光の格納先のテクスチャを結合する
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT2_EXT, GL_TEXTURE_2D, ab, 0);
  
  // フレームバッファオブジェクトにデプスバッファとしてレンダーバッファを結合する
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, rb);
  
  // フレームバッファオブジェクトの結合を解除する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

glClearColor() で設定する背景色は, glClear() によって各カラーバッファを塗りつぶす値になります. glClear() によって, すべてのカラーバッファが同時に塗りつぶされます. 個々のバッファを個別に塗りつぶすには glClearBuffer*() を使います. なお, 事後処理の際にテクスチャの画素が背景かどうか判別できるように, 背景色のアルファ値を 0 にしておくと便利です. このほか, glEnable(GL_LIGHT0); は意味がないので削除します.

  // 背景色
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
}

あと, glDrawBuffers() に渡すバッファ名の配列 bufs を作っておきます. これにフレームバッファオブジェクトに組み込まれているバッファのうち, レンダーターゲットに使うもののバッファ名を列挙しておきます. この中に GL_FRONT や GL_BACK など, 画面表示を行うためのバッファを含めることもできます.

/*
** レンダーターゲットのリスト
*/
static const GLenum bufs[] = {
  GL_COLOR_ATTACHMENT0_EXT, //   カラーバッファ (拡散反射光)
  GL_COLOR_ATTACHMENT1_EXT, //   鏡面反射光
  GL_COLOR_ATTACHMENT2_EXT, //   環境光の反射光
};

そして描画時に glDrawBuffers() を使ってレンダーターゲットをしていすれば, 指定したバッファに結合されたテクスチャにレンダリング結果が入ります. 描画が終わったら glDrawBuffer() で GL_FRONT (ダブルバッファリングをしている場合は GL_BACK) を指定して, レンダーターゲットを一応もとに戻しておきます.

/*
** 画面表示
*/
static void display(void)
{
  // 透視変換行列の設定
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(30.0, (GLdouble)width / (GLdouble)height, 1.0, 10.0);
  
  // モデルビュー変換行列の設定
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
  
  // ビューポートの設定
  glViewport(0, 0, FBOWIDTH, FBOHEIGHT);
  
  // 隠面消去を有効にする
  glEnable(GL_DEPTH_TEST);
  
  // フレームバッファオブジェクトを結合する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
  
  // レンダーターゲットを指定する
  glDrawBuffers(sizeof bufs / sizeof bufs[0], bufs);
  
  // pass1 シェーダを有効にする
  glUseProgram(pass1);
  
  // カラーバッファとデプスバッファをクリア
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  // シーンの描画
  glutSolidTeapot(1.0);
  glFlush();
  
  // pass1 シェーダを無効にする
  glUseProgram(0);
  
  // フレームバッファオブジェクトの結合を解除する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  
  // レンダーターゲットを元に戻す
  glDrawBuffer(GL_FRONT);
  
  ...

フラグメントシェーダでは, varying 変数の値を gl_FragColor ではなく配列変数 gl_FragData に格納します. 添え字 (0 〜 2) は glDrawBuffers() で指定したバッファ名の配列 bufs の要素に対応します.

#version 120
//
// pass1.frag
//
 
varying vec4 ambient;                                                 // 環境光の反射光 A
varying vec4 diffuse;                                                 // 拡散反射光 D
varying vec4 specular;                                                // 鏡面反射光 S
 
void main(void)
{
  gl_FragData[0] = diffuse;
  gl_FragData[1] = specular;
  gl_FragData[2] = ambient;
}

ちなみに gl_FragData[0] は gl_FragColor と等価です. また, このプログラムでは光源や材質を全然指定していないので, 実は specular や ambient は 0 になってます. したがって, この状態でプログラムを実行しても, 結果は最初と全然変わりません.

レンダーターゲットのテクスチャの参照

レンダーターゲットに使ったテクスチャの値を参照するには, 普通にテクスチャマッピングを行います. マルチプルレンダーターゲットでは複数のテクスチャを使いますから, これらを同時に参照するためにマルチテクスチャの機能を使います.

まず, 表示領域全体を覆うポリゴンの描画にもプログラマブルシェーダを使うようにします. シェーダオブジェクトの名前を保持する変数 pass2 と, そこで使用する uniform 変数の場所を保持する変数 diffuse, specular, ambient を宣言しておきます.

...
 
/*
** シェーダオブジェクト
*/
#include "glsl.h"
static GLuint pass1, pass2;
static GLint diffuse, specular, ambient;
 
/*
** シェーダプログラムの読み込み
*/
static GLuint loadShader(const char *vert, const char *frag)
{
  ...
}
 
/*
** 初期化
*/
static void init(void)
{
#if defined(WIN32)
  // GLEW の初期化
  GLenum err = glewInit();
  if (err != GLEW_OK) {
    fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
    exit(1);
  }
#endif
  
  // シェーダプログラムの作成
  pass1 = loadShader("pass1.vert", "pass1.frag");
  pass2 = loadShader("pass2.vert", "pass2.frag");
  
  // pass2 シェーダの uniform 変数の場所を得る
  diffuse = glGetUniformLocation(pass2, "diffuse");
  specular = glGetUniformLocation(pass2, "specular");
  ambient = glGetUniformLocation(pass2, "ambient");
  
  ...

このバーテックスシェーダは次のような内容にします. クリッピング空間に直接ポリゴンを描くので, モデルビュー変換や透視変換を行う必要はありません. また, このポリゴンいっぱいにテクスチャを貼り付けるので, ここで頂点の位置からテクスチャ座標を生成してしまいます. クリッピング空間の座標値は [-1, 1] の範囲にあるので, これをテクスチャ空間の座標値 [0, 1] に変換するために, 0.5 倍して 0.5 を足します. これを varying 変数 texcoord に代入して, フラグメントシェーダに渡します.

#version 120
//
// pass2.vert
//
 
varying vec2 texcoord;
 
void main(void)
{
  texcoord = (gl_Position = gl_Vertex).xy * 0.5 + 0.5;
}

フラグメントシェーダではテクスチャから値を取り出して, それをもとに画素の色を決定します. ここでは単に足しているだけです.

#version 120
//
// pass2.frag
//
 
uniform sampler2D diffuse;              // 拡散反射光のテクスチャユニット
uniform sampler2D specular;             // 鏡面反射光のテクスチャユニット
uniform sampler2D ambient;              // 環境光のテクスチャユニット
varying vec2 texcoord;                  // テクスチャ座標
 
void main(void)
{
  vec4 d = texture2D(diffuse, texcoord);
  vec4 s = texture2D(specular, texcoord);
  vec4 a = texture2D(ambient, texcoord);
  
  gl_FragColor = d + s + a;
}

前述のとおりプログラマブルシェーダを使う場合は陰影付けをオン・オフする意味がなくなるので, glDisable(GL_LIGHTING); は削除します. モデルビュー変換行列や透視変換行列を単位行列に戻す必要もないので, これらも削除します. また, ここで固定機能シェーダに戻す必要もないので, glUseProgram(0); も削除します.

...
 
/*
** 画面表示
*/
static void display(void)
{
  ...
  
  // シーンの描画
  glutSolidTeapot(1.0);
  glFlush();
  
  // レンダーターゲットを元に戻す
  glDrawBuffer(GL_FRONT);
  
  // フレームバッファオブジェクトの結合を解除する
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  
  // ビューポートはウィンドウのサイズに合わせる
  glViewport(0, 0, width, height);
  
  // 隠面消去処理は行わない
  glDisable(GL_DEPTH_TEST);

プログラマブルシェーダを使う場合は, テクスチャマッピングの有効・無効の切り替えも意味を持ちません. 代わりに, ここでテクスチャユニットにテクスチャオブジェクトを割り当てておきます.

  // テクスチャマッピングを有効にする
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, cb);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, sb);
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, ab);

そしてシェーダプログラムを有効にして, そこで使っている uniform 変数に値を設定します.

  // pass2 シェーダを有効にする
  glUseProgram(pass2);
  
  // uniform 変数に値 (テクスチャユニット番号) を設定する
  glUniform1i(diffuse, 0);
  glUniform1i(specular, 1);
  glUniform1i(ambient, 2);

ポリゴンを描くときに色を指定する必要はありません. またバーテックスシェーダでテクスチャ座標を生成しているので, glTexCoord*() でテクスチャ座標を指定する必要もありません.

  // 正方形を描く
  glBegin(GL_TRIANGLE_FAN);
  glVertex2d(-1.0, -1.0);
  glVertex2d( 1.0, -1.0);
  glVertex2d( 1.0,  1.0);
  glVertex2d(-1.0,  1.0);
  glEnd();

最後に固定機能シェーダに戻しておきます

  // シェーダを無効にする
  glUseProgram(0);
  
  glFlush();
}

このフラグメントシェーダ pass2.frag をいろいろいじってください.

(ああしんど)

コメント(2) [コメントを投稿する]
かし 2023年08月28日 19:03

コメント文「フレームバッファオブジェクトに拡散反射光の格納先のテクスチャを結合する」は <br>「フレームバッファオブジェクトに鏡面反射光の格納先のテクスチャを結合する」の間違いではないでしょうか。

とこ 2023年09月06日 16:18

かしさま、コメントありがとうございます。その通りです。直しました。


編集 «フレームバッファオブジェクトの使い方あげいん 最新 ビルボード»