OpenGLのVBOの頂点データを更新する


はじめに

Qiitaに「macOSでOpenGLプログラミング(目次)」っていうコンテンツがありまして、ちょいとやってみています。
グラデーションのかかった2D三角形を描画するところから入門できます。
今どき(といってもMetalやVulkanまでは行かないらへん)のOpenGLの入門として、
VBOやIBOを使ってVAO経由で2D描画をするところから学べます。

普段cocos2d-xとか他のゲームエンジンとかで開発していると、
描画周りの知識はふわっとしたところまでしか入ってこないんです。
仕事上でもGUIコンポーネントの表示くらいはやりますが、
描画周りっていうほど低レイヤはたいして見たことないのです。
3Dとかもっと見たことないです。

さて「macOSでOpenGLプログラミング(目次)」の話しに戻りますが、
今ちょうど「2-6. 頂点データにGLKitの構造体を使う」まで写経したところなのですが。
2-7. 頂点データをゲーム実行中に移動させる(未投稿) 」がアップされていない。
これすごく知りたい。

座標決め打ちの長方形を描いたはいいけど、 座標を動かしてそれを表示する方法が知りたい。
これがわかれば、Flappy Birdくらいは作れそう。

記事が無いなら作ればいいじゃない。(ナントカネット)

はい、そんなわけで調べました。

wgld.org | WebGL: VBOを逐次更新しながら描画する
これとか。WebGLの記事ですけど、OpenGLでも概念は同じだと思う。
なるほどね。VBOをglBufferSubData()で更新すると。

1. VBOに指定しているdataをメンバー変数化する

では、 2-6のコードの状態から、「上下キーを押したら写真が上下に移動する」ことを目標 に実装していきます。
初学者なので、正しくないことを書いていたらごめんなさい。

今までは一度決めた頂点データは更新することがなかったので、
コンストラクタの中で頂点データ配列(data変数)を作っていましたが、
その頂点データを更新できるようにするために、
Gameクラスのメンバー変数として持たせるようにします。

Game.hpp
#include <GLKit/GLKMath.h>
#include <vector>

class Game {
    // ... 省略
    struct VertexData
    {
        GLKVector3  pos;
        GLKVector4  color;
        GLKVector2  uv;
    };
    //! 頂点データ
    std::vector<VertexData> data;
    //! y座標の差分
    float dy;
    //! y座標のdirtyフラグ
    bool bDirtyDY;
}

VetexData の構造体の定義をhpp側に持ってきました。
data は以前はGame()コンストラクタのメソッド内で定義していたものです。
dy はキーを押したときにどのくらいy座標を動かすかどうかを決めた値を格納する場所です。
bDirtyDY はキーが押されたとき(=値を変更するべきとき)にだけtrueにします。
いわゆる「Dirty Flag Pattern」です。

Game.cpp
Game::Game()
{
    // 省略

    // VBOの生成
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData) * data.size(), data.data(), GL_DYNAMIC_DRAW);

    // 省略

     t = 0.5f;
     dy = 0.0f;
     bDirtyDY = false;
}

まずは std::vector<VertexData> data; を削除します。
これは先程メンバ変数に移動しました。
dybDirtyDY を初期化しておきます。
1点新しい変更として、VBOの glBufferData() の第4引数を
GL_STATIC_DRAW から GL_DYNAMIC_DRAW に変更しました。
この値は、どのくらいの頻度で使われるか?どのくらいの頻度で更新されうるか?といったヒントをOpenGLに伝えるものです。
STATICの場合は基本的には1度しか更新されないものとして扱います。
DYNAMICに変更することで「何度も更新する予定の値ですよ」とOpenGLに伝えます。
OpenGLはこのヒント情報を元に、いい感じにBuffer Objectを取り扱うそうです。
詳しくはglBufferData - OpenGL 4 Reference Pagesなどを参照してください。

2. キー入力に応じて、頂点データを書き換える

Game.cpp
void Game::Render()
{
    // 省略
    if (Input::GetKey(KeyCode::UpArrow)) {
        dy = 0.25f * Time::deltaTime;
        bDirtyDY = true;
    }
    if (Input::GetKey(KeyCode::DownArrow)) {
        dy = -0.25f * Time::deltaTime;
        bDirtyDY = true;
    }

    if (t < 0.0f) {
        t = 0.0f;
    } else if (t > 1.0f) {
        t = 1.0f;
    }

    if (bDirtyDY)
    {
        bDirtyDY = false;
        for (VertexData& datum : data)
        {
            datum.pos.y  += dy;
        }
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(VertexData) * data.size(), data.data());
        dy = 0.f;
    }
}

新しいキー入力判定として、上下キーを検知します。
押されているときだけ、上下どちらかの方向にdeltaTimeに比例した座標分だけ移動させます。
移動させることが確定した段階でdirty flagをtrueにします。

dirty flagがtrueのときだけVBOの更新を行います。
data のすべての頂点に対し、y座標を更新します。

そしてVBOを実際に更新するときに用いるのが glBufferSubData() です。
glBufferData() はバッファの生成や初期化を行う関数でしたが、
glBufferSubData() は更新に特化しているため、
glBufferData() で頂点データを更新するよりも効率よく更新することが可能です。
第1引数の GL_ARRAY_BUFFER はVBOの生成時と同じです。
第2引数はオフセット値で一部のデータだけ書き換えるときに用います。
今回はすべて置き換えるのでオフセットは使わず、0を指定します。
第3・第4引数はVBOの生成時と同じデータサイズとその先頭ポインタです。

なお、今回はdirty flagを用いて glBufferSubData() の呼び出し回数を必要最低限にしましたが、
私の手元のMacBook Airではフラグがあってもなくても大してパフォーマンスは変わりませんでした。
ただ、もっと複雑なプログラムを組むようになってくると「呼ぶ必要のない処理は行わない」ことの積み重ねが、パフォーマンスに大きく影響していきます。
日頃から「呼ぶ必要のない処理は行わない」ことは意識しておいたほうが良いでしょう。

3. まとめ

glBufferData() で作ったVBOは glBufferSubData() で更新することが可能です。
更新予定のVBOであることをOpenGLに伝えるために、
VBOのバッファオブジェクト生成時の使用用途を GL_DYNAMIC_DRAW に変更します。

macOSでOpenGLプログラミング(目次)」の執筆者 @sazameki さんに感謝を。