OpenGLのDrawCallについて

OpenGL4.4のDrawCallについて、あたらめて勉強してみたいと思います。
加えて、DrawCallに深く関連する、頂点データの指定方法と、VertexShaderで参照可能なシステム変数についても見てみたいと思います。

頂点データ配列の指定について

頂点データ配列は、DrawCallの呼び出しによって、起動されるVertexShaderの入力として使用されるデータの配列です。たとえば、頂点の位置や法線、色、UVなどがこれに相当します。これらの、OpenGLにおける指定方法を見てみたいと思います。

VertexAttributeとVertexAttributeBindingについて

VertexAttribute(以下VA)は、VertexShaderの入力となるin変数と対応付けされるデータのフォーマットを指定するものとなります。具体的なVertexAttributeの例として、頂点の位置や法線、色、UVなどが挙げられると思います。
一方で、VertexAttributeBinding(以下VAB)は、どのVAが、どの頂点データ配列(VertexBufferObject。以下VBO)を使用するかを対応付けるものになります。たとえば、頂点と法線がインターリーブで配置された配列があれば、この配列を、頂点と法線のVAにBindingします。
このように、頂点フォーマットの指定に相当するVAと、VBOの指定に相当するVABを設定するAPIが用意されています。

VertexBindingDivisorについて

VertexBindingDivisorは、端的にいえば、インスタンス描画で用いる、インスタンスごとの頂点データ配列を指定する方法です。
APIは、

GLvoid glVertexBindingDivisor(GLuint bindingindex, GLuint divisor);

となっており、VABのインデックスとdivisorの値を指定します。divisorの値に0を指定した場合は、通常の頂点データ配列と同じように、頂点ごとに頂点データを読み進めます。
divisorの値に0以外の値を指定した場合は、指定した数のインスタンスごとに、頂点データを読み進めます。たとえば、1を指定すれば、インスタンスごとに頂点データが指定できるようになります。

VertexAttributeObjectについて

VertexAttributeObject(以下VAO)には、上記のVAおよびVABに関する設定の全てが保存されるオブジェクトとなります。
VAOを作成することで、VAおよびVABに関する煩雑な設定を、VAOのBindという1つのAPI呼び出しで置き換えることが出来ます。
殆どの描画では、同一のオブジェクトに対する、VAおよびVABに関する設定が、描画フレームごとに変化することはありません。従って、VAOを用いてオブジェクト化しておくと、後の描画処理がシンプルに記述できます。
ただし、頂点データに関連する一部の設定がVAOに格納されないので注意が必要です。具体的には以下の項目が格納されません。

  • GL_ARRAY_BUFFERと GL_DRAW_INDIRECT_BUFFER(後述)のBinding情報
  • ストリップを用いた描画で用いる、Primitive Restartに関する設定

Primitive Restartを使用する場合は、

glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);

を呼び出すことで、インデックスバッファ(GL_ELEMENT_ARRAY_BUFFER)で用いる変数型の最大値が、RestartIndexとして扱われるようになります。こちらを呼び出し、以後はPrimitive Restartに関する設定を変更しないような利用方法が望ましいです。

具体的な例

下記の例では、3つのVBOに、(position, normal), (uv0, uv1), (color)と格納されているデータを、VABとVAに設定している例です。
(color)に関しては、描画インスタンスごとの値となるように設定します。さらにこれ等の設定をVAOに保存しています。

GLuint buffer0, buffer1, buffer2, buffer3;
-- create and initialize buffers --

// create and bind VAO
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

// Set up formats and relative offsets within the interleaved data.
// position float3, unnormalized, offset 0 byte
glVertexAttribFormat(0, 3, GL_FLOAT, GL_FALSE, 0);
// normal float3, unnormalized, offset 12 byte
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 12);
// uv0 float2, unnormalized, offset 0 byte
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, 0);
// uv1 float2, unnormalized, offset 8 byte
glVertexAttribFormat(3, 2, GL_FLOAT, GL_FALSE, 8);
// color float4, unnormalized, offset 0 byte
glVertexAttribFormat(4, 4, GL_FLOAT, GL_FALSE, 0);

// Bind the vertex buffers to VAB slots.
// set buffer0 to VAB slot: 0, offset 0  byte, stride 24 byte
glBindVertexBuffer(0, buffer0, 0, 24);
// set buffer1 to VAB slot: 1, offset 0 byte, stride 16 byte
glBindVertexBuffer(1, buffer1, 0, 16);
// set buffer2 to VAB slot: 2, offset 0 byte, stride 12 byte
glBindVertexBuffer(2, buffer2, 0, 12);

// Set up attrib->binding mapping
glVertexAttribBinding(0, 0); // set VA slot:0 to VAB slot:0
glVertexAttribBinding(1, 0); // set VA slot:1 to VAB slot:0
glVertexAttribBinding(2, 1); // set VA slot:2 to VAB slot:1
glVertexAttribBinding(3, 1); // set VA slot:3 to VAB slot:1
glVertexAttribBinding(4, 2); // set VA slot:4 to VAB slot:2

// Set divisor to per-instance array
// set color array as per-instance data.
glVertexBindingDivisor(2, 1);

// Set a buffer as index array buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer3);

glBindVertexArray(0);

DrawCallについて

次に、DrawCallの呼び出し手順についても見ていきたいと思います。
DrawCallの種類は多岐にわたりますので、主だったものを順を追って確認してみたいと思います。

通常の描画

GLvoid glDrawElements(GLenum mode,
                      GLsizei count,
                      GLenum type,
                      const GLvoid * indices);

頂点バッファとインデックスバッファを用いたプリミティブの描画。

  • mode
    プリミティブの種類(POINT,LINE,TRIANGLE)
  • count
    indices配列の長さ
  • type
    インデックス配列の値の種類(UBYTE, USHORT, UINT)
  • indices
    null (インデックスバッファ(GL_ELEMENT_ARRAY_BUFFER)がBindされている場合)

頂点インデックスを使用したDrawCallの基本形です。
当然ですが、DrawCallの呼び出す前に、VAO,VBO等が正しくBindされている必要があります。

インスタンス描画

GLvoid glDrawElementsInstanced(GLenum mode,
                               GLsizei count,
                               GLenum type,
                               const GLvoid * indices,
                               GLsizei primcount);
  • primcount
    描画するインスタンスの数

glDrawElementsのインスタンス描画版です。primcountに指定した数だけインスタンス描画を行います。
VertexShader内で、gl_InstanceIDシステム変数(後述)を参照することで、描画しているインスタンスのインデックスを確認することができます。
また、divisorを設定した頂点配列を用いることで、インスタンスごとの頂点属性データを指定できます。

配列オフセット付き描画

GLvoid glDrawElementsBaseVertex(GLenum mode,
                                GLsizei count,
                                GLenum type,
                                GLvoid *indices,
                                GLint basevertex);

GLvoid glDrawElementsInstancedBaseInstance(GLenum mode,
                                           GLsizei count,
                                           GLenum type,
                                           const GLvoid *indices,
                                           GLsizei instancecount,
                                           GLuint baseinstance);

GLvoid glDrawElementsInstancedBaseVertex(GLenum mode,
                                         GLsizei count,
                                         GLenum type,
                                         const GLvoid *indices,
                                         GLsizei instancecount,
                                         GLint basevertex);

GLvoid glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode,
                                                     GLsizei count,
                                                     GLenum type,
                                                     const GLvoid *indices,
                                                     GLsizei instancecount,
                                                     GLint basevertex,
                                                     GLuint baseinstance);
  • basevertex
    頂点インデックスに対して適用するオフセット値
  • baseinstance
    divisorを設定した頂点配列にアクセスするインデックスに対して適用するオフセット値

これ等のAPIは、いずれもglDrawElements()とglDrawElementsInstanced()に、頂点データ配列、もしくはdivisorを指定したインスタンスごとのデータ配列にアクセスする際のオフセット値を指定できるようにしたものです。
特に、インスタンス描画に対しては、baseinstanceの値とdivisorの値によって、次のように配列のインデックスが計算されます。

((current instance No) / divisor) + baseinstance

GPU側のデータを用いた描画

GLvoid glDrawElementsIndirect(GLenum mode,
                              GLenum type,
                              const GLvoid *indirect);
  • mode
    プリミティブの種類(POINT,LINE,TRIANGLE)
  • type
    インデックス配列の値の種類(UBYTE, USHORT, UINT)
  • indirect
    DrawElementsIndirectCommand構造体へのポインタ(ただし必ずnullを指定)

DrawElementsIndirectCommand構造体は以下の内容となっています。

typedef struct {
  GLuint  count;
  GLuint  primCount;
  GLuint  firstIndex;
  GLuint  baseVertex;
  GLuint  baseInstance;
} DrawElementsIndirectCommand;

glDrawElementsInstancedBaseVertexBaseInstance()の呼び出しを、構造体を通して行えるようにしたものです。一見すると、Client側のメモリに配置した構造体に対応しているように見えますが、indirectポインタはnullである必要があります。
実際の描画に用いる構造体は、GL_DRAW_INDIRECT_BUFFERという、描画命令バッファに格納されている必要があり、かつ、DrawCallの呼び出し時にBindされている必要があります。

GPU側のデータを用いた複数の描画

GLvoid glMultiDrawElementsIndirect(GLenum mode,
                                   GLenum type,
                                   const GLvoid *indirect,
                                   GLsizei drawcount,
                                   GLsizei stride);
  • drawcount
    DrawElementsIndirectCommand構造体配列の長さ
  • stride
    DrawElementsIndirectCommand構造体配列のサイズ(ポインタのインクリメント量)

glDrawElementsIndirect()を複数の構造体に対応させたものです。
構造体1つの描画ごとに、VertexShaderで参照できる、gl_DrawIDARBシステム変数(後述)がインクリメントされます。この変数を参照することで、VertexShader内で何番目の構造体による描画なのかを知ることが出来ます。

頂点シェーダーで参照できるシステム変数について

GLSL4.4では頂点シェーダーで以下の変数が参照可能です。

in int gl_VertexID;
in int gl_InstanceID;

加えて、ARB_shader_draw_parameters拡張を使用することで、以下のシステム変数が使用可能になります。

#extension GL_ARB_shader_draw_parameters : required

in int gl_DrawIDARB;
in int gl_BaseVertexARB;
in int gl_BaseInstanceARB;

それぞれが格納する値は以下の様になっています。

  • gl_VertexID
    頂点のインデックスが格納される。basevertexを用いて頂点インデックスに対してオフセットを適用した場合は、そのオフセットが加算された値になる。
  • gl_InstanceID
    インスタンスのインデックスが格納される。baseinstanceを用いてインスタンスのインデックスに対してオフセットを適用した場合でも、オフセットが加算されていない値になる。
  • gl_DrawIDARB
    MultiDraw系のDrawCallで描画された際に、描画された構造体のインデックスが格納される。
  • gl_BaseVertexARB
    basevertexを用いて頂点インデックスに対してオフセットを適用した場合に、そのオフセット値が格納される。
  • gl_BaseInstanceARB
    baseinstanceを用いてインスタンスのインデックスに対してオフセットを適用した場合に、そのオフセット値が格納される。

gl_VertexIDと、gl_InstanceIDが格納する値に一貫性が無いので注意が必要です。
なんとなくですが、basevertexはGPU上でインデックスをオフセットして、baseinstanceはドライバがポインタをオフセットしているためだと思われます。
gl_DrawIDARBは、MultiDraw系のDrawCallを使用する場合はほぼ必須の変数になると思われます。

最後に

当初は、DrawCallの説明だけ記事を書くつもりではなかったのですが、長くなったので、記事にしました。