«ゴムシミュレータ(1) 最新 Screen Space Motion Blur »

床井研究室

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

■ 2011年12月07日 [OpenGL][ゼミ] ゴムシミュレータ(2)

2012年04月29日 12:01更新

就職活動

ご存じのとおり, 12月1日から2013年卒の就職活動が始まりました. 大学生の就職活動の現状に関してはいろいろな議論があるものの, 当事者としては現状を甘んじて受け入れざるを得ないのが実情です. 入ってくる情報はネガティブなものばかりで, すべてを真に受けているとこちらもネガティブな方向に向いて行きそうです. 何かこう, 景気のいい話はないもんでしょうかね. ただ, 現状の至る所に様々な「ミスマッチ」があるのではないかという印象があります. そして, その中のいくつかは, ちゃんと裏を取った「リサーチ」の不足が原因になっているように思えます. そのあたり, 大学もいろいろ努力や工夫を積み重ねていますが, 私自身も学生さんが向いている方向に分け入って, ちゃんとリサーチしなければいけません. ので, 情報ください.

共有メモリ

以前, シェーダによる計算は他のプロセッサの状況に関知しないけれども, 他のプロセッサとデータを共有する手段としてテクスチャを共有メモリとして参照できるというようなことを書きました.計算結果を共有メモリに書きこむことは並列処理の足を思いっきり引っ張ることになりますが, Transform Feedback を使えば, プロセッサが個別に出力した結果を一つのバッファ (メモリ) に集めることができます. これをテクスチャにコピーできれば, バーテックスシェーダによる1ステップの計算結果を, 次のステップでプロセッサ間で共有することができます. このために glCopyBufferSubData() が使えます. また, このコピー先のテクスチャメモリに格納されるデータは画像ではなく数値計算の結果なので, 格納先として テクスチャバッファオブジェクト を使います. テクスチャバッファオブジェクトの使い方については, 2011-11-04 - tuedaの日記さまを参考にさせていただきました. お世話になっております. ありがとうございました.

なお, glCopyBufferSubData() は OpenGL 3.1 で導入されたので, 今回のチュートリアルは OpenGL 3.2 ベースにしています. 3.3 にしなかったのは Mac OS 縛りを意識してのことですが, Mac OS X の GLUT は互換モードしか使えないので, いずれにしろ Mac では動きません.

テクスチャバッファオブジェクト

もともとの点のデータに, シェーダから共有メモリとして参照するテクスチャバッファオブジェクトを追加します. これには頂点バッファオブジェクト同様「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」で定義している GgBuffer<T> が使えます. また, シェーダでこのテクスチャオブジェクトを参照するときに使うテクスチャの名前 (番号) を用意しておきます.

// 点データ
static class PointBuffer
{
  // 頂点バッファオブジェクト
  GgBuffer<GLfloat[4]> position;
  
  // データの参照用に使うテクスチャバッファオブジェクト
  GgBuffer<GLfloat[4]> table;
  
  // テクスチャバッファオブジェクトを参照するテクスチャ
  GLuint tex;

そして, コンストラクタでこのテクスチャバッファオブジェクトにメモリを割り当てておきます. デストラクタでテクスチャ tex を削除していませんが, この実体はテクスチャバッファオブジェクトなので, テクスチャとして削除する必要はないようです (削除しようとしたら落ちました).

public:
  
  // デストラクタ
  ~PointBuffer(void) {}
  
  // コンストラクタ
  //    n: 頂点数, pos: 頂点の位置
  PointBuffer(unsigned int n)
  {
    // 頂点バッファオブジェクトのメモリを確保する
    position.load(GL_ARRAY_BUFFER, n, 0, GL_DYNAMIC_COPY);
    
    // 作成した頂点バッファオブジェクトに初期値を設定する
    GLfloat (*p)[4] = (GLfloat (*)[4])glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
    for (unsigned int i = 0; i < n; ++i)
    {
      GLfloat t = (GLfloat)i / (GLfloat)(pointn - 1);
      
      p[i][0] = point0[0] * (1.0f - t) + point1[0] * t;
      p[i][1] = point0[1] * (1.0f - t) + point1[1] * t;
      p[i][2] = point0[2] * (1.0f - t) + point1[2] * t;
      p[i][3] = 1.0f;
    }
    glUnmapBuffer(GL_ARRAY_BUFFER);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    // データの参照用に使うテクスチャバッファオブジェクトのメモリを確保する
    table.load(GL_TEXTURE_BUFFER, n, 0, GL_DYNAMIC_COPY);

このテクスチャバッファオブジェクトを参照するテクスチャを作成します. glTexImage2D() などの代わりに glTexBuffer() を使って, 画像データの代わりにテクスチャバッファオブジェクトの名前 table.buf() を指定します. テクスチャバッファオブジェクトの internalFormat は頂点バッファオブジェクトと同じ内容を格納するので GL_RGBA32F にします.

    // テクスチャバッファオブジェクトを参照するテクスチャを作成する
    glActiveTexture(GL_TEXTURE0);
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_BUFFER, tex);
    glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, table.buf());
    glBindTexture(GL_TEXTURE_BUFFER, 0);
  }
  
  ...

そして今回のキモの部分です. この PointBuffer クラスに描画せずに計算だけを行う calculate() というメソッドを追加します. 引数の loop は計算の繰り返し回数です.

  ...
  
  // 計算の実行
  void calculate(unsigned int loop = 1)
  {
    for (unsigned int i = 0; i < loop; ++i)
    {

まず, 現在の頂点バッファオブジェクトの内容を, glCopyBufferSubData() を使ってテクスチャバッファオブジェクトにコピーします.

      // 頂点バッファオブジェクトとテクスチャバッファオブジェクトを結合する
      glBindBuffer(GL_ARRAY_BUFFER, position.buf());
      glBindBuffer(GL_TEXTURE_BUFFER, table.buf());
      
      // 頂点バッファオブジェクトからテクスチャオブジェクトにコピーする
      glCopyBufferSubData(GL_ARRAY_BUFFER, GL_TEXTURE_BUFFER, 0, 0, sizeof (GLfloat[4]) * position.num());
      
      // 頂点バッファオブジェクトとテクスチャバッファオブジェクトを解放する
      glBindBuffer(GL_TEXTURE_BUFFER, 0);
      glBindBuffer(GL_ARRAY_BUFFER, 0);

コピー元の頂点バッファオブジェクトを Transform Feedback のターゲットに指定し, Transform Feedback を有効にします.

      // 頂点バッファオブジェクトを position のターゲットとして指定する
      glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, position.buf());
      
      // Transform Feedback 開始
      glBeginTransformFeedback(GL_POINTS);

また, シェーダで参照するテクスチャバッファオブジェクトを保持するテクスチャを結合しておきます.

      // テクスチャバッファオブジェクトのテクスチャを結合する
      glBindTexture(GL_TEXTURE_BUFFER, tex);

そして glEnable(GL_RASTERIZER_DISCARD) によりラスタライザを止めて描画を実行します. ラスタライザを止めると図形の描画は行わず, バーテックシェーダによる計算が終了した時点で即座に処理が終了します.

      // ラスタライザを止めて計算を実行する
      glEnable(GL_RASTERIZER_DISCARD);
      glDrawArrays(GL_POINTS, 0, position.num());
      glDisable(GL_RASTERIZER_DISCARD);

Transform Feedback を終了し, テクスチャオブジェクトを保持するテクスチャの結合を解除します.

      // Transform Feedback 終了
      glEndTransformFeedback();
      
      // テクスチャバッファオブジェクトのテクスチャを解除する
      glBindTexture(GL_TEXTURE_BUFFER, 0);
    }
  }
  
} *pointBuffer = 0;                           // 頂点バッファオブジェクト

ここで注目してほしいことは, この処理ではバーテックスシェーダの入力となる頂点属性を保持する頂点バッファオブジェクトや, そのデータを受け取る attribute 変数を指定していないという点です. この描画 (計算) に使うシェーダプログラムは, 処理すべき入力データを attribute 変数からではなく, テクスチャバッファオブジェクトから得ることになります.

シェーダプログラム

計算に使うシェーダのクラスも, 「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」で定義している GgShader を派生して作ります. ラスタライザを起動しなければフラグメントシェーダは起動されませんから, このシェーダはバーテックスシェーダのみになります (ジオメトリシェーダはオプション). また attribute 変数も使いません.

static class ForceShader
  : public GgShader
{
  // uniform 変数の場所
  GLint tableLoc;

バーテックスシェーダによる計算の結果を出力する varying 変数は position とします.

public:
  
  // デストラクタ
  ~ForceShader(void) {}
  
  // コンストラクタ
  ForceShader(
    const char *vert = "force.vert",
    const char *position = "position",
    const char *table = "table"
    )
  {
    const char *varyings[] = { position };
    
    load(vert, 0, 0, 0, 0, sizeof varyings / sizeof varyings[0], varyings);
    tableLoc = glGetUniformLocation(getProgram(), table);
  }
  
  // プログラムオブジェクトを使用する
  void use(void) const
  {
      GgShader::use();
      glUniform1i(tableLoc, 0);
  }
  
} *forceShader = 0;

このシェーダのクラスのインスタンスをプログラムの起動時に生成し, 終了時に削除します.

// 後始末
static void cleanup(void)
{
  // 頂点バッファオブジェクトを削除する
  delete pointBuffer;
  
  // プログラムオブジェクトを削除する
  delete pointShader;
  
  // 力の計算用のシェーダを削除する
  delete forceShader;
}
 
// 初期化処理
static void init(void)
{
  // ゲームグラフィックス特論の都合にもとづく初期化
  ggInit();
  
  // 頂点バッファオブジェクトを作成する
  pointBuffer = new PointBuffer(pointn);
  
  // プログラムオブジェクト(シェーダ)を作成する
  pointShader = new PointShader();
  
  // 力の計算用のシェーダを作成する
  forceShader = new ForceShader();
  
  // OpenGL の初期設定
  glClearColor(1.0, 1.0, 1.0, 1.0);
  
  // 後始末
  atexit(cleanup);
}

このバーテックスシェーダのソースプログラムは, 例えば次のようにします. テクスチャバッファオブジェクトから取り出したデータをそのまま出力用の varying 変数 position に出力しています. テクスチャバッファオブジェクトの内容が頂点バッファオブジェクトの内容そのままなので, こういうことができます. テクスチャをサンプリングするのではなくテクスチャバッファオブジェクトの (メモリ) 要素を取り出すので, GLSL の組み込み関数 texelFetch() を使います. gl_VertexID はバーテックスシェーダにおいて処理中の頂点属性の番号が格納されている, int 型の組み込み変数です.

#version 150
 
uniform samplerBuffer table;
out vec4 position;
 
void main(void)
{
  vec4 p0 = texelFetch(table, gl_VertexID);
  
  position = p0;
}

最後に, 図形を描画した後に, その時の頂点バッファオブジェクトの内容を使って計算を実行するようにします. 計算用のシェーダを指定してから, 計算するメソッドを呼び出します.

// 画面に図形を描画する
static void display(void)
{
  // 画面クリア
  glClear(GL_COLOR_BUFFER_BIT);
  
  // 点の描画
  pointShader->use();
  pointShader->setColor(0.0f, 0.0f, 0.0f);
  pointBuffer->draw(pointShader->getPositionLoc(), GL_LINE_STRIP);
  pointShader->setColor(1.0f, 0.0f, 0.0f);
  glPointSize(pointSize);
  pointBuffer->draw(pointShader->getPositionLoc(), GL_POINTS);
  
  // 計算の実行
  forceShader->use();
  pointBuffer->calculate(10);
  
  // ダブルバッファリング
  glutSwapBuffers();
}

この処理は頂点バッファオブジェクトの内容をテクスチャバッファオブジェクトにコピーして, バーテックスシェーダでテクスチャバッファオブジェクトの内容を Transform Feedback により, 頂点バッファオブジェクトに書き戻しているだけなので, 結果はこれまでと変わらないはずです.

ゴム化する

それでは, 点の位置をマウスで動かした結果をもとに, 点の位置を修正するようにシェーダプログラムを書き換えてみましょう. 点と点を結ぶ線分をゴムやバネだと考えれば, フックの法則 (Hooke's law) により, 点の移動に伴って線分が伸びた長さに比例した力が, 移動した点に伸びと反対方向にかかるものとします. もちろん, ある点が動くことにより, その点につながっている近傍の点にも向きが反対で同じ強さの力がかかります.

移動した頂点にかかる力

これをシェーダプログラムで計算します. いま, ゴムの自然長を l, ゴムのバネ定数を k とします. これらにはとりあえず適当な値を設定しておきます.

#version 150
 
uniform samplerBuffer table;
out vec4 position;
const float l = 0.1;
const float k = 0.1;

gl_VertexID にシェーダが処理対象としている点の番号が入っているので, その点に隣接する点の番号は gl_VertexID ± 1 です. これを使ってテクスチャバッファオブジェクトから隣接する点の位置を得ます.

void main(void)
{
  vec4 p0 = texelFetch(table, gl_VertexID);
  
  vec2 p1 = texelFetch(table, gl_VertexID - 1).xy;
  vec2 p2 = texelFetch(table, gl_VertexID + 1).xy;

移動する点と隣接するそれぞれの点との距離 l1, l2 を求め, それぞれの伸びの量にバネ定数 k をかけて力を求めます (伸びの方向 l1, l2 が図とは反転しています).

  vec2 l1 = p1 - p0.xy;
  vec2 l2 = p2 - p0.xy;
  vec2 f1 = (length(l1) - l) * k * normalize(l1);
  vec2 f2 = (length(l2) - l) * k * normalize(l2);

移動した点にかかる力は, これらの力の合計になります.

  vec2 f = f1 + f2;

この力を使って p0 を変更します. これにはオイラー法や, やまもんが使ってた改良オイラー法 (修正オイラー法), 話題の Verlet 積分なんかが使えると思います. いずれも頂点バッファオブジェクトを追加する必要があるので, ここでは位置に直接この力を足してしまいます. ちゃんとした積分法も後でやります.

  p0 += vec4(f, 0.0, 0.0);
  
  position = p0;
}

操作性の改善

これでとりあえずゴムひもみたいな動きをするものができますが, 動きが不安定ですし, 点を思ったように動かせない時もあります. これは, もちろん力の計算をまじめにやってないこともありますが, 両端の点の処理がいい加減なことや, マウスによって動かしている点の位置を, 力の計算で求めた位置で上書きしていることもあります. また, このプログラムではドラッグ中しか画面表示を更新しないため, マウスのボタンを離すと図形の動きが止まってしまいます.

そこで, 両端の点や, マウスで動かしている点については, 力の計算による位置の変更を行わないようにし, スペースキーによってマウスを操作していない時も画面表示を更新するようにします.

座標値は同次座標で表していますが, 実際には w の要素は使っていません. そこで力による位置の変更を行わない点の位置の識別に w の要素を使います. w が 0 の点は動かさないことにします.

// 点データ
static class PointBuffer
{
  ...
  
  // コンストラクタ
  //    n: 頂点数, pos: 頂点の位置
  PointBuffer(unsigned int n)
  {
    // 頂点バッファオブジェクトのメモリを確保する
    position.load(GL_ARRAY_BUFFER, n, 0, GL_DYNAMIC_COPY);
    
    // 作成した頂点バッファオブジェクトに初期値を設定する
    GLfloat (*p)[4] = (GLfloat (*)[4])glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
    // 最初の点
    p[0][0] = point0[0];
    p[0][1] = point0[1];
    p[0][2] = point0[2];
    p[0][3] = 0.0f;
    // 中間の点
    for (unsigned int i = 1; i < n - 1; ++i)
    {
      GLfloat t = (GLfloat)i / (GLfloat)(pointn - 1);
      
      p[i][0] = point0[0] * (1.0f - t) + point1[0] * t;
      p[i][1] = point0[1] * (1.0f - t) + point1[1] * t;
      p[i][2] = point0[2] * (1.0f - t) + point1[2] * t;
      p[i][3] = 1.0f;
    }
    // 最後の点
    p[n - 1][0] = point1[0];
    p[n - 1][1] = point1[1];
    p[n - 1][2] = point1[2];
    p[n - 1][3] = 0.0f;
    glUnmapBuffer(GL_ARRAY_BUFFER);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    ...
  }
  
  ...
  
  // バッファ中の i 番目の点の位置を (x, y) に設定する
  void move(int i, GLfloat x, GLfloat y, GLfloat value) const
  {
    glBindBuffer(GL_ARRAY_BUFFER, position.buf());
    GLfloat p[] = { x, y, 0.0, value };
    glBufferSubData(GL_ARRAY_BUFFER, sizeof (GLfloat[4]) * i, sizeof p, p);
  }
  
  ...

ドラッグ中以外も画面表示を更新するかどうかを判定するフラグ animate を用意し, 初期値を false (更新しない) にしておきます.

// アニメーション中か否か
static bool animate = false;
 
// 画面に図形を描画する
static void display(void)
{
  // 画面クリア
  glClear(GL_COLOR_BUFFER_BIT);
  
  // 点の描画
  pointShader->use();
  pointShader->setColor(0.0f, 0.0f, 0.0f);
  pointBuffer->draw(pointShader->getPositionLoc(), GL_LINE_STRIP);
  pointShader->setColor(1.0f, 0.0f, 0.0f);
  glPointSize(pointSize);
  pointBuffer->draw(pointShader->getPositionLoc(), GL_POINTS);

そして画面表示を常時更新しているときだけ力の計算を実行するようにしておきます. こうしないと, 画面は止まっているのに GPU (のファン) がぶんぶん回りだすことがあります.

  // 計算の実行
  if (animate)
  {
    forceShader->use();
    pointBuffer->calculate(10);
  }
  
  // ダブルバッファリング
  glutSwapBuffers();
}

マウスボタンを押したときは, 画面表示を常時更新しているのでなければ, アニメーションを開始します. 一方, マウスボタンを離したときには w 要素を非 0 にして点の座標値を設定し, この点に対して力の計算による位置の変更を行うようにします. 同時に画面表示の常時更新中でないときのみ, アニメーションを停止するようにします.

// マウスのボタンを押したときの処理
static void mouse(int button, int state, int x, int y)
{
  // 押されたボタンを覚えておく
  pressed = button;
  
  switch (button)
  {
  case GLUT_LEFT_BUTTON:
    if (state == GLUT_DOWN)
    {
      // 左ボタンを押したときの処理
      hit = pointBuffer->pick((GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy, pointSize / cx, pointSize / cy);
      if (!animate && hit >= 0) glutIdleFunc(idle);
    }
    else
    {
      // 左ボタンを離したときの処理
      pointBuffer->move(hit, (GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy, 1.0);
      hit = -1;
      if (!animate) glutIdleFunc(0);
    }
    break;
    
    ...

マウスのドラッグ中は, w 要素を 0 にして点の座標値を設定し, この点に対して力の計算による位置の変更を行わないようにします. これでマウスの操作が素直に点の位置に反映されるようになります.

// マウスのドラッグ中の処理
static void motion(int x, int y)
{
  switch (pressed)
  {
  case GLUT_LEFT_BUTTON:
    // 左ボタンでドラッグ中の処理
    if (hit >= 0)
    {
      pointBuffer->move(hit, (GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy, 0.0);
    }
    break;
    
    ...

最後に, スペースバーをタイプしたときに画面表示の常時更新の on/off を切り替えるようにします.

// キーボードをタイプしたときの処理
static void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case ' ':
    animate = !animate;
    if (animate)
      glutIdleFunc(idle);
    else
      glutIdleFunc(0);
    break;
    
    ...

こういう風に頂点を移動したとします.

頂点の移動

スペースバーをタイプすると力の計算を始めます.

力の計算の結果

力の計算 (画面表示の常時更新) をしながら点を移動することもできます.

画面表示を常時更新しながら点を移動する

重力の追加

シェーダプログラムに次のように下方向に重力 g を与えておくと, スペースを押した瞬間, ゴムひもがだらんと下に垂れ下がります.

#version 150
 
uniform samplerBuffer table;
out vec4 position;
const float l = 0.1;
const float k = 0.1;
const vec2 g = vec2(0.0, -0.001);
 
void main(void)
{
  vec4 p0 = texelFetch(table, gl_VertexID);
  
  if (p0.w > 0.0)
  {
    vec2 p1 = texelFetch(table, gl_VertexID - 1).xy;
    vec2 p2 = texelFetch(table, gl_VertexID + 1).xy;
    vec2 v1 = p1 - p0.xy;
    vec2 v2 = p2 - p0.xy;
    vec2 f1 = (length(v1) - l) * k * normalize(v1);
    vec2 f2 = (length(v2) - l) * k * normalize(v2);
    vec2 f = f1 + f2 + g;
    
    p0 += vec4(f, 0.0, 0.0);
  }
  
  position = p0;
}

バネ定数はもっと大きくしたほうがいいかもしれません.

懸垂線
コメント(5) [コメントを投稿する]
しろうさぎ 2011年12月24日 12:47

openGL 使える人がほしいんだけどなあ。零細企業には来てくれないですよね。

とこ 2011年12月24日 16:01

しろうさぎさま,毎度ありがとうございます.<br> そうですね,確かに大手指向はあります.しかし,これは学生さん個人の考え方というより,大学や親御さんなどの社会的な背景の影響が大きいようにも思います.そういう環境的な要因にとらわれず,我が道を進もうとする学生さんも少なくありません.<br> ミスマッチがあるとすれば,やはり学生さんと企業さんとの接点の少なさでしょうか.現在の主流である大手就活サイトの利用が,かえって機会の多様性を見えにくくしているようにも思えます.<br> ですので,学校の就職支援の部局に求人票を出すという伝統的な手段は,今でも目的の学生にアピールするのに有効な手段のはずだと考えています.しかし,今は大手就活サイトを利用した就職活動がメインストリームだと考える傾向があるので,大学に集まってくる求人票にはなかなか目が向かないということもあるかも知れません.これは非常にもったいない話です.<br> それで,学生さんを求人票のあるところに連れて行くということは,1年生の頃からやっています.求人票を見て「こういう人が求められている」ということ知ることは,自分が勉強していることの意義を確認することにもなります.そのためにも求人票という形での情報提供は有り難いと思っています.

とおりすがり 2012年02月09日 22:07

下記URLにて宿題の丸投げがあるようです。ご一報まで。<br>http://toro.2ch.net/test/read.cgi/tech/1328276597/118-

とこ 2012年02月12日 00:02

とおりすがりさま,ありがとうございます.<br><br>実はリファラを見て知ってました.でも,これは前期の課題ですので,私のところではないみたいですね.うちは Vine Linux ですし.みなさま適切な対応w をして頂いたみたいで,ニヤニヤしておりました.

とこ 2012年04月29日 11:44

tDiary のソースコードカラーリングプラグインを教えてもらもらいました!ありがとうございます!時間ができたら GLSL に対応できたらいいな♪と思います.


編集 «ゴムシミュレータ(1) 最新 Screen Space Motion Blur »