OpenGL 4.x でのインスタンシング描画を試してみました。インスタンシング描画とはCPU(C++)からの描画呼び出しは1回で、同じモデルを複数回描画することを指します。このときモデル形状のデータは使い回して、別々の場所に描画するということをやってみます。これはインスタンシングの中でもハードウェアインスタンシングと呼ばれていて、古くはDirectX 9.0c Shader Model 3 のころに導入されたものなので知っている人も多いかと思います。
一方で OpenGL でハードウェアインスタンシングが使えるようになったのはそこから結構遅れました。こっそりと対応APIが入っていたこともありましたが、OpenGL 3.3付近でようやく標準的なものとなったようです。
インスタンシング用API
複数回の描画を行うためには、モデル形状のような回数には不変なデータと、位置指定のようなインスタンス毎で変化するデータの2種類を制御・設定する必要が出てきます。これらを設定するAPIが下記になります。
GLuint index; GLuint divisor; glVertexAttribDivisor( index, divisor );
このAPIは、頂点データの増分間隔を設定するものとなります。通常の頂点データは1つの頂点データを処理するとその分だけカウンタがインクリメントされて次の頂点データを処理するようになります。このAPIで、divisor に 1 を設定した場合、index で示される場所の頂点データは描画インスタンス毎にインクリメントされるようになります。
通常の増分に戻す場合には、divisor = 0 として設定します。またここで index は頂点入力アトリビュート、もしくは頂点入力としてのGLSL入力変数のロケーション番号となります。
glDrawArraysInstanced( GLenum mode, GLint first, GLsizei count, GLsizei primCount); glDrawElementsInstanced( GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primCount );
描画のAPIはこれらを使用します。OpenGLの拡張名としては GL_ARB_draw_instanced になります。countに頂点数やインデックス数を指定します。そして、通常の glDrawArrays,glDrawElements では存在しなかった最後の引数 primCount がインスタンス数を設定するようになっています。primCount なので、プリミティブ数かと間違えそうになりますが、インスタンス数です。注意しましょう。
サンプル
立方体のデータを1つ、それを4つほど出すためのワールド行列4つ分のバッファを用意します。どちらも頂点バッファとして準備します。準備した後、ワールド行列のバッファの方はインスタンス毎に参照データを更新したいため、glVertexAttribDivisor 関数を用いて設定を行っておきます。
// 立方体のデータ列は割愛 GLuint vbo[2]; // 0:CubeVertices, 1:WorldMatrix glGenBuffers( 2, vbo ); glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); glBufferData( GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW ); glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); glBufferData( GL_ARRAY_BUFFER, sizeof(worldMatrices), worldMatrices, GL_DYNAMIC_DRAW ); // locXX は頂点入力変数のロケーション番号が取得済みとする. glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); glVertexAttribPointer( locPosition, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), NULL ); offset += sizeof(float)*3; glVertexAttribPointer( locColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizoef(Vertex), NULL + offset ); // ... glEnableVertexAttribArray( locPosition ); glEnableVertexAttribArray( locColor ); // 続いてワールド行列用のバッファに対してセット offset = 0; glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); glVertexAttribPointer( locMat0, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), NULL + offset ); offset += sizeof(vec4); glVertexAttribPointer( locMat1, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), NULL + offset ); offset += sizeof(vec4); glVertexAttribPointer( locMat2, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), NULL + offset ); offset += sizeof(vec4); glVertexAttribPointer( locMat3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), NULL + offset ); offset += sizeof(vec4); glEnableVertexAttribArray( locMat0 ); glEnableVertexAttribArray( locMat1 ); glEnableVertexAttribArray( locMat2 ); glEnableVertexAttribArray( locMat3 ); // インスタンス毎の更新間隔のセット glVertexAttribDivisor( locMat0, 1 ); glVertexAttribDivisor( locMat1, 1 ); glVertexAttribDivisor( locMat2, 1 ); glVertexAttribDivisor( locMat3, 1 );
ポイントは頂点データとして GLSL は mat4 の行列を受け取れるように記述できるのですが、glVertexAttribPointer では GL_FLOAT の4つのデータを1単位として合計4回のセットが必要になります。このミスマッチが気になったので、自分では GLSL の頂点入力としても vec4 の4つを宣言して使用するようにしました。
#version 150 in vec4 in_position; in vec4 in_color; in vec4 in_modelmat0; in vec4 in_modelmat1; in vec4 in_modelmat2; in vec4 in_modelmat3; uniform VS_Block { mat4 pv; }; void main() { mat4 worldMat; worldMat[0]=in_modelmat0; worldMat[1]=in_modelmat1; worldMat[2]=in_modelmat2; worldMat[3]=in_modelmat3; vec4 worldPos = worldMat * in_position; gl_Position = pv * worldPos; ... }
描画そのもののコードはこんな感じになります。
// インスタンス毎ワールド行列の更新. mat4 worldMat[4]; // ここでworldMatを更新. glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); glBufferData( GL_ARRAY_BUFFER, sizeof(worldMat), worldMat, GL_DYNAMIC_DRAW ); // VAOのバインド もしくは glVertexAttribPointer でのセットコード // Uniform Buffer の更新&セット // 描画 glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, indexbuffer ); glDrawElementsInstanced( GL_TRIANGLES, indexCount, GL_UNSIGNED_SHORT, NULL, 4 );
worldMatを更新の部分で書くインスタンス毎のワールド行列を生成し、頂点バッファとして送り込んでおきます。
実行した結果このような感じになります。立方体のデータ1つで4つ描画しています。
今までの設定でどのようなことが起こっているかを図示するとこのような状況となります。
インダイレクト描画でインスタンシングを使う
DirectX 11 ではインダイレクト描画であってもインスタンシング描画をできるようなAPIになっていました。OpenGLではどうなのかを試してみたいと思います。
DirectXの DrawInstancedIndirectの関数に対応するのは、やはり glDrawArraysIndirect, glDrawElementsIndirect になるようで、Indirectのバッファの中身はどうなのかを確認してみます。
struct CommandDrawArrays { GLuint vertexCount; GLuint instanceCount; GLuint firstVertex; GLuint baseInstance; }; struct CommandDrawElements { GLuint indexCount; GLuint instanceCount; GLuint firstIndex; GLint baseVertex; GLuint baseInstance; };
このようになっており、第二メンバでインスタンス数を設定できるようになっています。この構造体の名前やメンバ名はここで命名したものが含まれています。公式のドキュメント等では別名がついています。なぜここだけ特殊なのかというと、公式の方がわかりにくい&歴史的事情でメンバ名が変わるなどがあったためです。
参考までに公式のほうでは現在このようになっています。
typedef struct { GLuint count; GLuint primCount; GLuint first; GLuint reservedMustBeZero; } DrawArraysIndirectCommand; typedef struct { GLuint count; GLuint primCount; GLuint firstIndex; GLint baseVertex; GLuint reservedMustBeZero; } DrawElementsIndirectCommand;
最後のメンバ reservedMustBeZero ですが、OpenGLの世代が進むと baseInstance を設定できるように変更になっていきます。バージョンにとらわれず使用したい場合にはゼロを指定してしまうのがよいでしょう。また、ここでprimCount となっているメンバですがこれはインスタンス数という意味です。glDrawArraysInstancedの関数と同様になっており、間違えないように注意しましょう。
この構造体に従って値をセットしてバッファからの描画をおこなえば、先ほどと同じような描画結果を得ることができました。すなわち OpenGL でも DirectX と同様に、インダイレクト描画であってもインスタンシング描画が行えるということが確認できました