床井研究室

頂点配列オブジェクトと頂点バッファオブジェクト

OpenGL 3.2 の Core Profile 以降では、OpenGL の図形描画は頂点配列オブジェクト (Vertex Array Object, VAO) を介して行わないといけません。VAO は任意の頂点バッファオブジェクト (Vertex Buffer Object, VBO) の組み合わせを管理できるので、異なる構造のオブジェクト (形状データ) でも VAO を指定するだけで図形の描画が開始できるので楽ちんなのですが、その辺の話はここには書いたもののこのブログに書いてないせいか、どうも今いる学生さんに伝わってない気がするので、改めて説明します。早く前回の放射照度マッピングの続きを書きたいんですけど他にも書かんならんもんがいっぱいあって、ちょっとくたびれてます。

デプスマップとかポイントクラウドとか関数とか

深度センサで取得したデプスマップやレンジファインダで取得した点群から、それが表す形状を、陰影をつけて表示することを考えます。これには Kinect Fusion1 や Dynamic Fusion2 をはじめ、既に優れた方法がいくつも提案されていますが、ここでは安直にメッシュを使って表示することにします。

頂点配列オブジェクトを使って図形を描画する

たとえば配列 position に、以下のように横 16 個、縦 12 個の点の座標値が格納されているとします。これを頂点バッファオブジェクトに格納して、これを頂点とする図形を描画します。

...
 
// メッシュの列数と行数
const auto slices(16), stacks(12);
 
...
 
int main()
{
  ...
  
  // 頂点数
  const auto vertices(slices * stacks);
  
  // 頂点位置
  GLfloat position[stacks][slices][3];
  
  //
  // ここで position に座標値を設定する
  //
  
  ...

この配列 position の内容を glGenBuffers() で生成した頂点バッファオブジェクト positionBuffer に転送し、それを頂点配列オブジェクト vao に組み込みます。まず glGenVertexArrays() で頂点配列オブジェクト vao を生成し、それを glBindVertexArray() で結合した状態で、glBindBuffer() で頂点位置を格納する頂点バッファオブジェクト positionBufferGL_ARRAY_BUFFER に結合します。ここではその後に glBufferData() によりバッファオブジェクトに使う (GPU 上の) メモリの確保と position に格納されたデータの転送を行っています。

  // 頂点配列オブジェクト
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  
  // 頂点位置を格納する頂点バッファオブジェクト
  GLuint positionBuffer;
  glGenBuffers(1, &positionBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
  
  // この頂点バッファオブジェクトのメモリを確保してデータを転送する
  glBufferData(GL_ARRAY_BUFFER, sizeof position, position, GL_STATIC_DRAW);

また、この頂点バッファオブジェクト positionBuffer に、頂点属性配列のインデックスの 0 番を指定します。これは glVertexAttribPointer() を使います。これによりこの頂点バッファオブジェクトの内容 (この場合は座標値) はバーテックスシェーダにおいてインデックスに 0 番を指定 (location = 0) した attribute 変数で取り出すことができます。そのあと glEnableVertexAttribArray() で 0 番の頂点属性配列を有効にします。最後に glBindVertexArray(0) を実行して、この頂点配列オブジェクトの結合を解除します。

  // この頂点バッファオブジェクトを 0 番の attribute 変数から取り出す
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // この頂点配列オブジェクトの結合を解除する
  glBindVertexArray(0);

そして、この点の描画に用いるシェーダを用意します。シェーダのソースプログラムの point.vert と point.frag の内容はあとで説明します。このシェーダのプログラムオブジェクト point から、シェーダ内の座標変換の変換行列を格納する uniform 変数 mc の場所を mcLoc に取り出しておきます。

  // メッシュ描画用のシェーダ
  const auto point(ggLoadShader("point.vert", "point.frag"));
  const auto mcLoc(glGetUniformLocation(point, "mc"));

以上の処理により、配列 position に格納されている座標値を頂点位置とする図形を、次のようにして描画することができます。まず、描画に使うシェーダのプログラムオブジェクト point を指定し、その uniform 変数 mc にモデルビュー変換行列と投影変換行列の積を設定します。その後、頂点配列オブジェクト vao を指定して、glDrawArrays() により図形を描画します。ここでは図形要素として GL_POINTS を指定しているので、点群が描かれます。

  ...
  
  // ウィンドウが開いている間くり返し描画する
  while (!window.shouldClose())
  {
    // 画面消去
    window.clear();
    
    // シェーダの指定
    glUseProgram(point);
    glUniformMatrix4fv(mcLoc, 1, GL_FALSE, (window.getMp() * window.getMv()).get());
    
    // 描画
    glBindVertexArray(vao);
    glDrawArrays(GL_POINTS, 0, vertices);
    
    // バッファを入れ替える
    window.swapBuffers();
  }

整列した点群を描く

そこで試しに下図のような整列した点群を用意します。左上の数字は頂点の番号です。

整列した点群

下の例は縦 stacks 個、横 slices 個の整列した点群のデータを作ります。点群は高さを 1 とし、slices / stacks の幅で xy 平面上 (z = 0) に整列して配置されます。

  // 頂点数
  const auto vertices(slices * stacks);
  
  // 頂点位置
  GLfloat position[stacks][slices][3];
  for (auto j = 0; j < stacks; ++j)
  {
    for (auto i = 0; i < slices; ++i)
    {
      const auto x((GLfloat(i) / GLfloat(slices - 1) - 0.5f) * GLfloat(slices) / GLfloat(stacks));
      const auto y((GLfloat(j) / GLfloat(stacks - 1) - 0.5f));
      
      position[j][i][0] = x;
      position[j][i][1] = y;
      position[j][i][2] = 0.0f;
    }
  }

これを描画すると、こんな具合になります。これは slices = 16stacks = 12 の場合です。

整列した点群の表示

点を動かす

描画時に頂点バッファオブジェクトの内容を更新することにより、この点を動かすことができます。描画時に頂点バッファオブジェクトの内容を書き換えるので、glBufferData() では配列変数 position と同じサイズのメモリの確保のみを行います。position の内容の転送を行わないので、glBufferData() の第 3 引数は nullptr にします。また、この頂点バッファオブジェクトの内容は頻繁に書き換えるので、第 4 引数の GL_STATIC_DRAWGL_DYNAMIC_DRAW に変更します。

  // 頂点配列オブジェクト
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  
  // 頂点位置を格納する頂点バッファオブジェクト
  GLuint positionBuffer;
  glGenBuffers(1, &positionBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
  
  // この頂点バッファオブジェクトのメモリを確保する
  glBufferData(GL_ARRAY_BUFFER, sizeof position, nullptr, GL_DYNAMIC_DRAW);
  
  // この頂点バッファオブジェクトを 0 番の attribute 変数から取り出す
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // この頂点配列オブジェクトの結合を解除する
  glBindVertexArray(0);

一方、この図形を描画する直前に、頂点位置を格納する頂点バッファオブジェクト positionBuffer の内容を glBufferSubData() で更新します。ここでは z 値だけを点群の中心からの距離 $r$、時刻 $t$ に対して $\sin(r - 2\pi t) / (r + \pi)$ だけずらします。

  ...
  
  // ウィンドウが開いている間くり返し描画する
  while (!window.shouldClose())
  {
    // 画面消去
    window.clear();
    
    // シェーダの指定
    glUseProgram(point);
    glUniformMatrix4fv(mcLoc, 1, GL_FALSE, (window.getMp() * window.getMv()).get());
    
    // 頂点位置を格納する頂点バッファオブジェクトに頂点座標値を設定する
    static auto frame(0);
    const auto cycle(100);
    const auto t(float(frame) / float(cycle));
    const auto pi(3.14159265f);
    for (auto j = 0; j < stacks; ++j)
    {
      for (auto i = 0; i < slices; ++i)
      {
        const auto x((GLfloat(i) / GLfloat(slices - 1) - 0.5f) * GLfloat(slices) / GLfloat(stacks));
        const auto y((GLfloat(j) / GLfloat(stacks - 1) - 0.5f));
        const auto r(hypot(x, y) * 6.0f * pi);
        
        position[j][i][0] = x;
        position[j][i][1] = y;
        position[j][i][2] = sin(r - 2.0f * pi * t) / (r + pi);
      }
    }
    glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof position, position);
    if (++frame >= cycle) frame = 0;
    
    // 描画
    glBindVertexArray(vao);
    glDrawArrays(GL_POINTS, 0, vertices);
    
    // バッファを入れ替える
    window.swapBuffers();
  }

こんな風になります。時間があったら GIF アニメ作ります。

点群のアニメーション

面を張る

点群がこのように整列していれば、それに面を張ることは容易です。例えば、下図のように点を結んで三角形を描きます。

三角形で面を張る

ただし、OpenGL でこのようなメッシュを描くには、以下の二通りの方法があります。あ、もしかしたら方法はほかにもあるかもしれません。

`GL_TRIANGLE_STRIP` と `GL_TRIANGLES`

この二つのうち、おそらく GL_TRIANGLE_STRIP を用いたほうが描画に用いられる頂点数が少ないため効率が良いと考えられます。実際 CG 用のモデラーの中には、このような形で出力するポリゴンデータを最適化する機能を持つものもあります。しかし、個人的にめんどくさいので、ここではすべての三角形をバラバラに描画する GL_TRIANGLES を用います。

このようなメッシュを GL_TRIANGLES で描画する場合、メッシュの一つのマス目を二つの三角形で描画することになります。つまり $k$ 番目の頂点一つ当たり、$k$, $k + 1$, $k + \text{slices}$ を頂点とする三角形と $k + 1$, $k + \text{slices} + 1$, $k + \text{slices}$ を頂点とする三角形の計 6 個の頂点を描くことになります。めちゃめちゃ増えてしまいます。でもほかの方法を考えるのがめんどくさいので、これでいいんです。

二つの三角形のインデックス

とは言っても、頂点座標値を増やすのもなんだかシャクですし、同じ頂点に対して異なる頂点属性を与えてしまうと、この後に行う (予定の) 頂点の法線ベクトルの取り扱いもめんどくさくなってしまいます。そこで、この頂点座標値などの頂点属性はそのままにしておいて、インデックスすなわち頂点の番号を使って描画する頂点を指定する方法を採ります。

そのためにインデックスを格納する頂点バッファオブジェクト indexBuffer を準備し、これも頂点配列オブジェクト vao に組み込みます。この頂点バッファオブジェクトは GL_ELEMENT_ARRAY_BUFFER に結合します。描画する頂点数は全部で $(\text{slices} - 1) \times (\text{stacks} - 1) \times 2 \times 3$ 個あります。これを定数 indexes とします。なお、頂点数の定数 vertices は不要になります。

  // 頂点配列オブジェクト
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  
  // 頂点位置を格納する頂点バッファオブジェクト
  GLuint positionBuffer;
  glGenBuffers(1, &positionBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
  
  // この頂点バッファオブジェクトのメモリを確保する
  glBufferData(GL_ARRAY_BUFFER, sizeof position, nullptr, GL_DYNAMIC_DRAW);
  
  // この頂点バッファオブジェクトを 0 番の attribute 変数から取り出す
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // インデックスバッファオブジェクト
  GLuint indexBuffer;
  glGenBuffers(1, &indexBuffer);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
  
  // このインデックスバッファオブジェクトのメモリを確保する
  const auto indexes((slices - 1) * (stacks - 1) * 2 * 3);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexes * sizeof (GLuint), nullptr, GL_STATIC_DRAW);

この頂点バッファオブジェクトに頂点番号を設定します。別に配列変数を用意するのが煩わしいので、ここでは glMapBuffer() を使ってバッファオブジェクトを CPU のメインメモリにマップし、そこに直接頂点番号を代入します。最後に glUnmapBuffer() でマップを解除します。

  // このインデックスバッファオブジェクトにインデックスを格納する
  auto index(static_cast<GLuint *>(glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY)));
  for (auto j = 0; j < stacks - 1; ++j)
  {
    for (auto i = 0; i < slices - 1; ++i)
    {
      const auto k(slices * j + i);
      index[0] = k;
      index[1] = index[5] = k + 1;
      index[2] = index[4] = k + slices;
      index[3] = k + slices + 1;
      index += 6;
    }
  }
  glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
  
  // この頂点配列オブジェクトの結合を解除する
  glBindVertexArray(0);

そして glDrawArrays() の代わりに glDrawElements() を使って描画します。

    // 描画
    glBindVertexArray(vao);
    glDrawElements(GL_TRIANGLES, indexes, GL_UNSIGNED_INT, 0);

これでなんか白いものがうにょうにょ動いているのがわかると思います。

疑似カラー処理

このままでは図形の色が白一色なので形がわかりません。そこでシェーダ側で簡単に色を付けてみたいと思います。頂点バッファオブジェクト positionBuffer の個々の頂点の座標値はバーテックスシェーダ point.vert で location = 0 を指定した attribute 変数 pv で取り出すことができます。そこで、その z 成分 pv.z の値をもとに適当に色を作り、それを頂点色として varying 変数 vc に代入してラスタライザに送ります。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
 
// 変換行列
uniform mat4 mc;                                    // クリッピング座標系への変換行列
 
// 頂点属性
layout (location = 0) in vec4 pv;                   // 頂点位置
 
// 頂点色
out vec3 vc;
 
void main(void)
{
  // 疑似カラー処理
  float z = pv.z * 6.0 + 2.0;
  vc = clamp(vec3(z - 2.0, 2.0 - abs(z - 2.0), 2.0 - z), 0.0, 1.0);
  
  gl_Position = mc * pv;
}

フラグメントシェーダ point.frag ではラスタライザで補間された頂点色の補間値が入っている varying 変数の vc を vec4 型にキャストしてフレームバッファに出力します。

#version 150 core
#extension GL_ARB_explicit_attrib_location : enable
 
// フレームバッファに出力するデータ
layout (location = 0) out vec4 fc;                  // フラグメントの色
 
// 頂点色の補間値
in vec3 vc;
 
void main(void)
{
  fc = vec4(vc, 1.0);
}

下図は slices = 160stacks = 120 にしています。$640 \times 480$ とかにすると Windows (Visual Studio) の場合はスタックの上限に引っかかって落ちます。その場合はスタックのサイズを変更するか、配列変数 positionnormal の宣言に static を付けてください。

面を張って疑似カラー処理をした結果

(まだ続きます)

  1. Newcombe, Richard A., et al. “KinectFusion: Real-time dense surface mapping and tracking.” Mixed and augmented reality (ISMAR), 2011 10th IEEE international symposium on. IEEE, 2011. 

  2. Newcombe, Richard A., Dieter Fox, and Steven M. Seitz. “DynamicFusion: Reconstruction and Tracking of Non-rigid Scenes in Real-Time.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2015.