«Vine Linux 6 への移行 最新 いい加減な視差画像生成»

床井研究室

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

■ 2011年11月21日 [OpenGL][GLSL] Transform Feedback と Vertex Texture Fetch

2011年11月29日 17:37更新

大学祭

「昨日一昨日はうちの大学祭でして」って去年書いて, もう一年経ったかと思うと月日の流れの速さに慄然としてしまいますが, この間にブログを8個しか書いてないという事実にも気付いて再び恐れおののいている今日この頃であります. この間, 何の成果も出してないというのは立場上非常にまずいということは重々自覚しておりますが, 決して怠けていたわけではなく, ただひたすらにそれなりに忙しかったという言い訳だけは申し添えさえて頂きたいと存じます.

さて, その大学祭ではありますが, あいにく初日の天候が悪く, 和歌山ラーメンも食べられずに (雨のため出店できなかったらしい) 部屋で悶々とコードをいじっておりましたところ, ありがたいことに何人かの卒業生が顔を見せに来てくださいました. ぐだぐだとした長話に付き合っていただき, 本当にありがとうございました. とてもうれしかったんですけど先生ヅラした話ばかりしてごめんよう.

今回やること

今回はフレームバッファオブジェクト (Frame Buffer Object (FBO)) を使って作成したテクスチャ (Renndering To Texture (RTT)) をバーテックスシェーダで参照 (Vertex Texture Fetch (VTF)) して, Transform Feedback (TFB) の計算に使うということをやってみます. 嫌がらせみたいな3文字アクロニムのオンパレードですね.

ストリーム・プロセッシング

GPU (おおここにも3文字アクロニム) を使った計算は, 頂点属性 (attribute) というデータセットに対して一斉にプロセッサ (shader processor, sp) を起動して, 各プロセッサは他のプロセッサが何をしているのか全く関知せずに (あーでも GLSL の Version 4 以降には barrier() とかいう組み込み関数があるな) ひたすら手元のデータをもとに計算をして, 結果をやっぱり手元のメモリに出力するという流れになります. この出力先となる手元のメモリを Transform Feedback Buffer にすることで, 計算結果を次のステップの入力データセットとして使うことができます. こういう計算モデルをストリーム・プロセッシングとかストリーミング・プロセッシングとかいうそうです (この辺詳しくないので間違ってたらごめん).

んで, この計算モデルの性能のキモは, やっぱり「周りを見ない」というところにありまして, これが, たとえば出力先が共有メモリだったりすると, いきなり性能が落ちてしまいます. グラフィックスの場合はバーテックスシェーダの先にラスタライザが控えていて, 最終的な共有メモリであるフレームバッファの面倒をこれが見てくれますけど, 「レイキャスティング」と「レイキャスティングふたたび」の結果を見る限り, 使い方によってはラスタライザもボトルネックになるんじゃないかと思います (最近はこの部分も並列化されつつあります - けど, 今のところ並列度はあまり高くないみたいですね).

まあ, そんなこんなですけど, やっぱりこういう計算モデルでもプロセッサ間で共通のパラメータを持てた方が便利です. 共有メモリであっても読み出しに限ればキャッシュが効くので, 足を引っ張ることはあまりありません.

Vertex Texture Fetch

というわけで, このような共有メモリとしてテクスチャを使うことができます. もともとテクスチャはラスタライザステージの後, すなわちフラグメントシェーダでしか使えなかったんですけど, 現在はバーテックスシェーダ等でも参照できるようになっています. この機能を Vertex Texture Fetch といいます. というような話は実はどうでもよくて, 今回は単に学生さんが「Transform Feedback は自分の目的には使えない」みたいなことを言ったので, 「こういう使い方もできるんだけど」という感じで解説を書きます. これはもっと早く書かないと今週末あたりのイベントに間に合わなかったんですけど, 結構時間がかかるうえに今ほかのことで滅茶苦茶忙しいので, 本当にどうしようかと頭を抱えているってことだけは理解してください. こういう学生さん向けの解説資料をあと二・三個書かんといかんと思ってんですけど, ほんまどうしよう.

Transform Feedback

『OpenGL の勉強をする』という言い方には違和感がある」とか言ってる割に, 今 Transform Feedback の使い方を「勉強している」ところなのですが, その時に作ったサンプルプログラムを, 「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」を使って書き直しました. あと glBufferData() の usage を GL_STATIC_DRAW にしてましたけど, twitter で「GL_DYNAMIC_COPY じゃないのか」という指摘があったので, そこも変更しました (動作は変わんないんですけど). このプログラムをベースに解説します. なお, この「オレオレ補助ライブラリ」 は来年の講義の宿題のベースにしようと今練っているところです.

点を落下させる

このプログラムは点を上から落下させるだけの単純なものです.

点の落下

図形を追加する

この中に図形を追加します. うちでは千年一日のごとく使っている「三角形分割された Alias OBJ 形式のファイル」を使います. プロジェクトに以下のファイルを追加します.

そして以下の内容を main.cpp の最初の部分に追加します. 解説をこの辺りから始めると終わらないので, GgObj クラスだとか GgSimpleShader クラスだとかの詳細は省きます. あと, ここで static なポインタ変数を使うのはダサイと思いますけど (atexit() とか使ってるし) 私は static おじさんだからいいんです.

...
 
// トラックボール処理
static GgTrackball *trackball = 0;
 
// 三角形だけからなる Alias OBJ ファイル
static GgObj *obj = 0;
 
// OBJ ファイル用の単純なシェーダ
#include "GgSimpleShader.h"
static GgSimpleShader *objShader = 0;
 
/*
** 落下する点群
**
**    頂点バッファオブジェクトを使用する形状データのための基底クラス
*/
class Drops
{
  ...

そして, 初期化の際にこれらのインスタンスを作っておきます. cross.obj は四角形2枚 (三角形4枚) からなる形状データです.

...
 
/*
** 初期化
*/
static void init(void)
{
  ...
  
  // トラックボール処理
  trackball = new GgTrackball;
  
  // OBJ データ
  obj = new GgObj("cross.obj");
  
  // OBJ データ用のシェーダ
  static GgSimpleLight light;
  light.setDiffuse(1.0f, 1.0f, 1.0f);
  light.setSpecular(1.0f, 1.0f, 1.0f);
  light.setAmbient(0.2f, 0.2f, 0.2f);
  light.setPosition(3.0f, 4.0f, 5.0f);
  static GgSimpleMaterial material;
  material.setDiffuse(0.6f, 0.5f, 0.1f);
  material.setSpecular(0.4f, 0.4f, 0.4f);
  material.setAmbient(0.5f, 0.6f, 0.1f);
  material.setShininess(30.0f);
  objShader = new GgSimpleShader();
  objShader->light = &light;
  objShader->material = &material;
  
  // 点データ
  drops = new GgDrops(POINTS, GRAVITY);
  ...

作成したインスタンスはプログラム終了時に削除するようにしておきます. こんな処理は auto_ptr とか vector とかのコンテナ使えばいらんのでしょうけど. static おじさんのこだわりは害悪以外の何物でもないですね.

...
 
/*
** 後始末
*/
static void leave(void)
{
  // OBJ データの削除
  delete obj;
  
  // OBJ データの用のシェーダの削除
  delete objShader;
  
  // 点データの削除
  delete drops;
  
  ...

そしてこれを描画します.

...
  
/*
** 画面表示
*/
static void display(void)
{
  // モデルビュー変換行列
  GgMatrix modelviewMatrix = viewMatrix * trackball->get();
  
  // 画面クリア
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  // 図形の描画
  objShader->use(projectionMatrix, modelviewMatrix);
  obj->draw(objShader->getPvLoc(), objShader->getNvLoc());
  
  // 点群の描画
  dropsShader->use(projectionMatrix, modelviewMatrix, GRAVITY);
  drops->draw(dropsShader->getPositionLocation(), dropsShader->getVelocityLocation());
  
...

こんな具合になります.

図形の追加

フレームバッファオブジェクトによるテクスチャの作成

追加した図形を真上から見たときの深度値 (デプスバッファの値) と可視点の法線ベクトルをフレームバッファオブジェクトを使って取得し, テクスチャを作成します. まず, テクスチャオブジェクトを作成し, それを組み合わせてフレームバッファオブジェクトを作成します. main.cpp の最初の部分でフレームバッファオブジェクト名やテクスチャ名を格納する変数を宣言しておきます.

...
 
// トラックボール処理
static GgTrackball *trackball = 0;
 
// 三角形だけからなる Alias OBJ ファイル
static GgObj *obj = 0;
 
// OBJ ファイル用の単純なシェーダ
#include "GgSimpleShader.h"
static GgSimpleShader *objShader = 0;
 
// フレームバッファオブジェクトのサイズ
#define FBOWIDTH 512
#define FBOHEIGHT 512
 
static GLuint fb;           // フレームバッファオブジェクト
static GLuint nb;           // 法線バッファ用のテクスチャ
static GLuint db;           // デプスバッファ用のテクスチャ
 
/*
** 落下する点群
**
**    頂点バッファオブジェクトを使用する形状データのための基底クラス
*/
class Drops
{
  ...
初期化の際にテクスチャを作り, それを組み合わせてフレームバッファオブジェクトを作ります. 法線を格納するテクスチャは浮動小数点テクスチャの方がいいので internalFormat に GL_RGBA32F を指定します (GL_RGBA でも値をスケール・オフセットすれば使えます). 深度を格納するテクスチャの internalFormat は GL_DEPTH_COMPONENT にします.
...
 
/*
** 初期化
*/
static void init(void)
{
  // ゲームグラフィックス特論の都合にもとづく初期化
  ggInit();
  
  // 法線バッファ用のテクスチャを用意する
  glGenTextures(1, &nb);
  glBindTexture(GL_TEXTURE_2D, nb);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 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, &db);
  glBindTexture(GL_TEXTURE_2D, db);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, FBOWIDTH, FBOHEIGHT, 0, GL_DEPTH_COMPONENT, 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);
  
  // フレームバッファオブジェクトを生成する
  glGenFramebuffersEXT(1, &fb);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, nb, 0);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, db, 0);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  
  // 視野変換行列の設定
  viewMatrix.loadLookat(0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
  
  // トラックボール処理
  trackball = new GgTrackball;
  ...

フレームバッファオブジェクトに描画するときに使うシェーダを作成します. 「オレオレ補助プログラム」を使っている都合上, このために GgShader クラスを継承したクラスをひとつ作ります. attribute 変数は頂点の座標値 pv と頂点の法線ベクトル nv の二つだとします.

/*
** 上から見たデプスバッファを求めるシェーダ
*/
class UpShader
  : public GgShader
{
  // attribute 変数の場所
  GLint pvLoc;        // 位置
  GLint nvLoc;        // 法線
  
public:
  
  // デストラクタ
  virtual ~UpShader(void) {}
  
  // コンストラクタ
  UpShader(
    const GLchar *vert  = "upShader.vert",
    const GLchar *frag  = "upShader.frag",
    const GLchar *pv    = "pv",
    const GLchar *nv    = "nv"
    )
    : GgShader(vert, frag)
  {
    // attribute 変数の場所の取得
    pvLoc = glGetAttribLocation(name(), pv);
    nvLoc = glGetAttribLocation(name(), nv);
  }
  
  // attribute 変数 pv の場所を得る
  GLint getPvLoc(void)
  {
    return pvLoc;
  }
  
  // attribute 変数 nv の場所を得る
  GLint getNvLoc(void)
  {
    return nvLoc;
  }
  
  // シェーダプログラムの適用
  void use(void) const
  {
    // プログラムオブジェクトの指定
    glUseProgram(name());
  }
};

このバーテックスシェーダ upShader.vert では頂点の法線ベクトル nv をそのままフラグメントシェーダに送ります. また頂点の座標値は, この図形を「上」から見た時のテクスチャを作るので, xz 平面を xy 平面とし y 方向を z 方向とするように要素を入れ替えます. また透視変換は行わないので w は 1 にしておきます.

#version 120
 
// 頂点属性
attribute vec4 pv;  // ローカル座標系の頂点の位置
attribute vec3 nv;  // 頂点の法線ベクトル
 
// フラグメントシェーダに送る法線ベクトル
varying vec3 norm;
 
void main(void)
{
  norm = nv;
  gl_Position = vec4(pv.xzy, 1.0);
}

フラグメントシェーダ upShader.frag では頂点の法線ベクトルを補間したものを正規化して, 画素値とします.

#version 120
 
// バーテックスシェーダから受け取る法線ベクトル
varying vec3 norm;
 
void main(void)
{
  gl_FragColor = vec4(normalize(norm), 0.0);
}

このシェーダのインスタンスを作成します. もちろん, 終わったら消すようにします.

...
 
static UpShader *upShader = 0;
 
/*
** 後始末
*/
static void leave(void)
{
  // OBJ データの削除
  delete obj;
  
  // OBJ データの用のシェーダの削除
  delete objShader;
  
  // 上から見たデプスバッファを求めるシェーダの削除
  delete upShader;
  
  // 点データの削除
  delete drops;
  
  ...
}
 
/*
** 初期化
*/
static void init(void)
{
  // ゲームグラフィックス特論の都合にもとづく初期化
  ggInit();
  
  ...
  
  // 上から見たデプスバッファを求めるシェーダ
  upShader = new UpShader();
  
  // 視野変換行列の設定
  viewMatrix.loadLookat(0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
  
  ...

このシェーダを使ってフレームバッファオブジェクトに描画します. 隠面消去の際のデプスバッファの比較関数 glDepthFunc() を GL_GREATER にしているのは, upShader.vert で図形の y と z を入れ替えたためです. この場合, 空から見れば z が大きい方が空に近くなります. これに伴って, デプスバッファをクリアする値 glClearDepth() も最も遠いところの値 (1.0) ではなく, 最も近いところの値 (0.0) にします.

...
 
/*
** 画面表示
*/
static void display(void)
{
  // モデルビュー変換行列
  GgMatrix modelviewMatrix = viewMatrix * trackball->get();
  
  // フレームバッファオブジェクトに描画
  GLint viewport[4];                                              // ビューポート保存用
  glGetIntegerv(GL_VIEWPORT, viewport);                           // ビューポートの保存
  glViewport(0, 0, FBOWIDTH, FBOHEIGHT);                          // ビューポートを FBO のサイズに
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);                   // FBO に描画開始
  glDepthFunc(GL_GREATER);                                        // 遠くを優先にして隠面消去
  glClearDepth(0.0);                                              // なのでデプスバッファの初期値は 0
  glClear(GL_DEPTH_BUFFER_BIT);                                   // FBO のデプスバッファをクリア
  upShader->use();                                                // シェーダの使用
  obj->draw(upShader->getPvLoc(), upShader->getNvLoc());          // 図形の描画
  glDepthFunc(GL_LESS);                                           // 近くを優先にした隠面消去に戻す
  glClearDepth(1.0);                                              // なのでデプスバッファの初期値は 1
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);                    // FBO への描画終了
  glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); // ビューポートの復帰
  
  // 画面クリア
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  ...

テクスチャの参照

点を描画する際に, フレームバッファオブジェクトで作成したテクスチャを参照するようにします. シェーダにテクスチャのサンプラに使う uniform 変数を追加します. コンストラクタで uniform 変数の場所を求めておき, シェーダを使用する際にそれらにテクスチャユニット番号を設定した後, それぞれのテクスチャユニットに対してテクスチャを結合します.

/*
** 落下する点群のシェーダ
*/
class DropsShader
  : public GgShader
{
  // attribute 変数の場所
  GLint positionLocation;
  GLint velocityLocation;
  
  // uniform 変数の場所
  GLint transformMatrixLocation;
  GLint gravityLocation;
  GLint nmapLocation;
  GLint dmapLocation;
  
public:
  
  // デストラクタ
  ~DropsShader(void) {}
  
  // コンストラクタ
  DropsShader(void)
  {
    // Transform Feedback する varying 変数
    const static char *varyings[] = { "pos", "vel" };
    
    // シェーダーのソースファイルの読み込み
    load("DropsShader.vert", "DropsShader.frag", 0, 0, 0, sizeof varyings / sizeof varyings[0], varyings);
    positionLocation = glGetAttribLocation(name(), "position");
    velocityLocation = glGetAttribLocation(name(), "velocity");
    transformMatrixLocation = glGetUniformLocation(name(), "transformMatrix");
    gravityLocation = glGetUniformLocation(name(), "gravity");
    nmapLocation = glGetUniformLocation(name(), "nmap");
    dmapLocation = glGetUniformLocation(name(), "dmap");
  }
  
  // シェーダプログラムを使用する
  void use(GgMatrix &projection, GgMatrix &modelview, GLfloat gravity) const
  {
    glUseProgram(name());
    glUniformMatrix4fv(transformMatrixLocation, 1, GL_FALSE, (projection * modelview).get());
    glUniform1f(gravityLocation, gravity);
    
    // サンプラ変数にテクスチャユニット番号を渡す
    glUniform1i(nmapLocation, 0);
    glUniform1i(dmapLocation, 1);
    
    // テクスチャの結合
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, nb);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, db);
  }
  
  // attribute 変数 pos の場所を得る
  GLint getPositionLocation(void) const
  {
    return positionLocation;
  }
  
  // attribute 変数 vel の場所を得る
  GLint getVelocityLocation(void) const
  {
    return velocityLocation;
  }
};

また, このバーテックスシェーダ dropsShader.vert では, 点の現在位置とデプステクスチャを比較し, 点の方が下になれば, その位置の法線のテクスチャの値を使って跳ね返り方向を求め, それを新たな速度にします. ただし, この方法だとピッチングマシーンの実験同様うまく跳ね返らないことがある (ゴロになる…ここのベストアンサーでも指摘されてるしw) けど, もういいよね.

#version 120
//
// simple.vert
//
attribute vec3 position, velocity;
uniform mat4 transformMatrix;
uniform float gravity;
uniform sampler2D nmap, dmap;
varying vec3 pos, vel;
const float tstep = 1.0;
 
void main(void)
{
  gl_Position = transformMatrix * vec4(position, 1.0);
  vel = velocity + vec3(0.0, gravity, 0.0) * tstep;     // 速度に加速度×時間を加える
  pos = position + vel * tstep;                         // 点の位置を移動する
  if (pos.y < -1.0) {                                   // 点が範囲外に出たら
    pos.xz = fract(pos.xz * 0.5 + 0.5) * 2.0 - 1.0;
    pos.y += 2.0;                                       // 位置を反対側に戻す
    vel = vec3(0.0, 0.0, 0.0);                          // 速度を 0 にする
  }
  else {
    vec3 p = pos.xyz * 0.5 + 0.5;
    if (p.y < texture2D(dmap, p.xz).y) {
      vel = reflect(vel, texture2D(nmap, p.xz).xyz);
    }
  }
}

あ, 地面に落ちた点を空に戻すときの位置の決め方がいい加減なので, ずっと動かしていると図形の真上から点が供給されなくなります. ちゃんと初期位置を覚えておけばいいんですけど, もういいよね.

衝突判定付の点の落下

編集 «Vine Linux 6 への移行 最新 いい加減な視差画像生成»