はじめに
前回は、構造体とvectorを使って元データを用意することで、VBOとVAOの指定が分かりやすくなるということを解説しました。
これまでの描画では、VBOに用意した座標データと色情報のデータを、そのままの順番で頂点シェーダにストリームしてきました。今回はインデックス・リストを使って、頂点データの順番を制御するということを解説したいと思います。
1. インデックス・リストについて
次の図のような3Dモデルを考えてみましょう。
これは球体をX方向とY方向にそれぞれ10分割して三角形メッシュで表したものですが、このモデルの場合、各頂点が6つの三角形で共有されていることが分かります(上部と下部だけは異なる形状ですが)。この球体のモデルでは、頂点数は92個、三角形のメッシュ数は180個です。この球体を描画するためのデータをこれまでのように単純に用意した場合、座標が重複したデータを含めて、メッシュ数180個x3=540個の頂点データを用意しなければいけません。現在、座標にGLfloat値を3個、色情報にGLfloat値を4個使っていますので、float型1個あたりのデータ容量を4バイトとして、4x7x540=15,120バイト(=14.7KB)というちょっとしたサイズのメモリ消費量になります。
それに対して、各頂点に番号を割り振って、メッシュのデータをそれぞれ「頂点0, 1, 2を結んだメッシュ」「頂点2, 3, 0を結んだメッシュ」のように指定できれば、頂点データは92個だけで済みます(メモリ消費量は2.5KBです)。頂点データの個数が少ないということは、各種パイプラインで転送しなければならないデータの総量も少なくなるということですので、高速化にもつながります。
これを実現するのがインデックス・リストです。OpenGLでは、インデックス・リストもVBOと同じBuffer Objectとして作成し、同じ方法でデータをセットアップします。
2. インデックス・リストのバッファを用意する
Game.hppを編集して、VBOのハンドル変数の下に、インデックス・リストのハンドル変数を用意します。今回は「indexBuffer」という名前にしました。
class Game
{
private:
ShaderProgram *program;
GLuint vbo;
GLuint indexBuffer;
GLuint vao;
/* 以下、省略 */
};
次にGame.cppを編集して、インデックス・リストを作成するコードを書きます。
Game::Game()
{
program = new ShaderProgram("myshader.vsh", "myshader.fsh");
std::vector<VertexData> data;
data.push_back({ { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } });
data.push_back({ { 0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } });
data.push_back({ { 0.5f, 0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } });
data.push_back({ { -0.5f, 0.5f, 0.0f }, { 1.0f, 1.0f, 1.0f, 1.0f } });
std::vector<GLushort> indices;
indices.push_back(0);
indices.push_back(1);
indices.push_back(2);
indices.push_back(2);
indices.push_back(3);
indices.push_back(0);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData) * data.size(), &data[0], GL_STATIC_DRAW);
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * indices.size(), &indices[0], GL_STATIC_DRAW);
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData), ((VertexData *)0)->pos);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData), ((VertexData *)0)->color);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
今回はまずdata
変数に格納する頂点のデータとして、正方形を構成する4つの頂点座標を用意しました。前回までと同じく、頂点座標、頂点の色情報の順に並んでいます。左下、右下、右上、左上の順に座標データを格納していますので、それぞれに対応して0, 1, 2, 3とインデックスが割り振られます。
次に、これらの頂点を組み合わせるインデックス・リストの元データが、GLushort型を格納するvectorであるindices
変数です。<0, 1, 2>と<2, 3, 0>の組み合わせで2つの三角形を描くことにより、正方形が描画できます。
VBOもインデックス・リストのバッファも、格納するものは違ってもBuffer Objectであることに変わりはありませんので、インデックス・リストのバッファはglGenBuffer()
関数を使って作成します。ただし、glBindBuffer()
関数とglBufferData()
関数を呼び出す時の第1引数がGL_ELEMENT_ARRAY_BUFFER
となりますので、注意してください。glBindBuffer()
関数でバッファをバインドし、それと同時にGL_ELEMENT_ARRAY_BUFFER
の指定でインデックス・リストを格納するためのバッファとして利用することを指定したら、glBufferData()
関数でサイズとポインタを渡して、インデックス・リストのデータをGPUのメモリ上に転送します。
なお、こうして用意したインデックス・リストのバッファは、VAOから参照するように設定する必要があります。VAOをバインドした後、改めてGL_ELEMENT_ARRAY_BUFFER
とindexBuffer
を引数にしてglBindBuffer()
関数を呼び出すことで、VAOがインデックス・リストを利用した描画を行う際にindexBuffer
変数が表すバッファのデータが利用されるようになります。
VAOをバインドした後にインデックス・リストのバッファをバインドしてからインデックス・リストのデータを転送しても良いのですが、今回は解説のためにVAOからインデックス・リストのバッファを参照する設定を明示的に分けています。
Gameクラスのデストラクタには、作成したインデックス・バッファを解放するためのコードを書き加えます。
Game::~Game()
{
delete program;
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(1, &vbo);
glDeleteBuffers(1, &indexBuffer);
glDeleteVertexArrays(1, &vao);
}
3. インデックス・リストを利用した描画に変更する
Game.cppのGame::Render()関数を編集して、これまでglDrawArrays()
関数を使って描画していたのを、glDrawElements()
関数を使って描画するように書き換えます。
void Game::Render()
{
program->Use();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(vao);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (void *)0);
}
glDrawElements()
関数の第1引数はglDrawArrays()
関数と同じく、プリミティブの形状を示す「GL_TRIANGLES」などの定数ですが、第2引数はインデックス・リストに格納されたインデックスの個数になります。第3引数はインデックス情報の型です。第4引数は0をポインタとして渡します。
glDrawArrays()
関数を使って描画が行われる場合、VBOに格納された頂点データはそのままの順番でストリーミングされますが、glDrawElements()
関数が使われると、VBOに格納された頂点データはインデックス・リストのインデックスに従って並べ替えられてストリーミングされるようになります。
コンストラクタの中でVAOにインデックス・リストのバッファを関連付ける処理を行いましたので、Render()関数の中では、indexBuffer
を改めてバインドする必要はありません。
このプログラムを実行すると、インデックス・リストに格納された2つ分のプリミティブ情報に従ってVBOの頂点データが並べ替えられてストリーミングされ、次のように三角形2個で構成された正方形が画面に表示されるようになります。
ここまでのプロジェクト:MyGLGame_step2-3.zip
4. まとめ
今回は、インデックス・リストを使って、複数の頂点データを組み合わせて描画を行う方法について解説しました。
3Dモデルを格納したファイルでは、基本的にはまず頂点データのリストが格納されて、次にインデックス・リストが格納されて、それを組み合わせるようになっています。そのため、今回解説したインデックス・リストが使えるようになると、様々な3Dモデルを読み込んで表示するのも簡単にできるようになります。
次回は、ポリゴン上にテクスチャを表示する方法について解説しましょう。