OpenGL ES 3.0 / OpenGL 4.3 VertexArrayObject と VertexAttribBinding

OpenGL API のオブジェクト操作は、必ずライブラリ内の global な変数を
経由して行います。
例えば Buffer Size を調べる場合も、現在の context のステートに
オブジェクトを bind してから対応するメソッドを呼び出します。

// GL2
glBindBuffer( GL_ARRAY_BUFFER, vertex_object );
GLint	param[1];
glGetBufferParameteriv( GL_ARRAY_BUFFER, GL_BUFFER_SIZE, param );
glBindBuffer( GL_ARRAY_BUFFER, 0 );

オブジェクトが導入される前の API と互換性を保つことができる反面、
どのオブジェクトを対象としているのかコードを見ただけではわからなく
なっています。

どのオブジェクトが必要なのか、どこまで影響を与えるのかは、それぞれ
命令仕様をマニュアル等で確認する必要があります。

例えば OpenGL ES 2.0 の描画のコードは下記の通りです。

// GL2 -- (1)
glUseProgram( program );
glUniform4fv( location, 4, matrix );
glBindBuffer( GL_ARRAY_BUFFER, vertex_object );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 24, (void*)12 );
glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );
glDrawElements( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0 );

もし仮に D3D API のような書き方ができるとしたらこんな↓感じになるでしょうか。

// GL2 -- 擬似コード (2)
program->SetConstant( location, 4, matrix );
device_context->SetProgram( program );
device_context->SetIndexBuffer( index_object );
device_context->SetVertexAttrib( 0, 3, GL_FLOAT, GL_FALSE, 24,  0, vertex_object);
device_context->SetVertexAttrib( 1, 3, GL_FLOAT, GL_FALSE, 24, 12, vertex_object);
device_context->DrawIndexed( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0 );

「擬似コード (2)」を見ると device_context に SetVertexBuffer() 命令が無く、
SetVertexAttrib() の方に vertex_object を渡していることがわかるかと思います。

実際に (1) の glDrawElements() は glBindBuffer( GL_ARRAY_BUFFER ) を
見ておらず、直前に glBindBuffer( GL_ARRAY_BUFFER, 0 ) があっても動作します。

glBindBuffer( GL_ARRAY_BUFFER ) を必要とするのは glVertexAttribPointer()
の方で、各 Vertex Attribute が頂点バッファの情報を保持しています。
Attribute それぞれが異なる Vertex Buffer を参照している可能性があるからです。

やはり (1) のコードを見ただけではこのような内部構造がわからないので、
OpenGL 命令の仕様はきちんと確認した方が良いようです。

● Vertex Array Object

頂点のバインドは複雑で情報も多いので、OpenGL 3.x 以降や OpenGL ES 3.0 では
Vertex Array Object (VAO) が導入されています。

// GL3 -- Vertex Array Object の作成 (3)
GLuint input_layout= 0;
glGenVertexArrays( 1, &input_layout );

glBindVertexArray( input_layout );
glBindBuffer( GL_ARRAY_BUFFER, vertex_object );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 24, (void*)12 );
glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );
glBindVertexArray( 0 );

VAO は頂点 Attribute の情報を保存し、描画時に利用することができます。

// GL3 -- VAO を使った Rendering (4)
glUseProgram( program );
glUniform4fv( location, 4, matrix );
glBindVertexArray( input_layout );
glDrawElements( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0 );

OpenGL 3.x (core profile) 以降は VAO が必須で、glBindVertexArray() が無いと
glVertexAttribPointer() がエラーになります。

つまり glVertexAttribPointer() は GL2 までは device_context のメソッドでしたが、
GL3 からは Vertex Array Object のメソッドに相当します。
擬似コードで表現してみます。

// GL3 -- VAO 作成 擬似コード (5)
IInputLayout* input_layout= device->CreateInputLayout( 2 );
input_layout->SetIndexBuffer( index_object );
input_layout->SetVertexAttrib( 0, 3, GL_FLOAT, GL_FALSE, 24,  0, vertex_object );
input_layout->SetVertexAttrib( 1, 3, GL_FLOAT, GL_FALSE, 24, 12, vertex_object );
// GL3 -- VAO を使った Rendering 擬似コード (6)
program->SetConstant( location, 4, matrix );
device_context->SetProgram( program );
device_context->SetInputLayout( input_layout );
device_context->DrawIndexed( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0 );

OpenGL ES 3.0 では OpenGL ES 2.0 互換性のために VAO 無しでも
glVertexAttribPointer() が使えるようになっています。
bind == 0 時にデフォルトの VAO が割り当てられていると考えられます。
OpenGL 3.x でも、RADEON など一部 GPU では default VAO が有効なものが
あるようです。

Vertex Array Object は Direct3D の InputLayout や VertexDeclaration に
相当しますが、上記のように vertex_object や index_object が含まれている点で
異なっています。
D3D では頂点のバインド情報とバッファの割り当ては別の命令でした。
OpenGL でも 4.3 で D3D 同様の仕組みが導入されているようです。
(後述: Vertex Attrib Binding)

● GL_ELEMENT_ARRAY_BUFFER の扱い

VAO の index_object に対する挙動は vertex_object の場合とは異なっています。

glDrawElements() は glBindBuffer( GL_ARRAY_BUFFER ) を参照しないし
VAO も glBindBuffer( GL_ARRAY_BUFFER ) に影響を与えません。

ですが、glDrawElements() は glBindBuffer( GL_ELEMENT_ARRAY_BUFFER ) を
参照し、VAO は glBindBuffer( GL_ELEMENT_ARRAY_BUFFER ) を置き換えます。

よって直前で glBindBuffer( GL_ELEMENT_ARRAY_BUFFER ) に 0 が
書き込まれると glDrawElements() は失敗します。

GLuint  input_layout;
GLuint  vertex_object1;
GLuint  index_object1;

// ** Bind: Vertex=[0]  Index=[0]

glBindVertexArray( input_layout );
glBindBuffer( GL_ARRAY_BUFFER, vertex_object1 );

// ↓ ここでの bind は VAO input_layout に対して行う
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object1 );

// ** Bind: Vertex=[vertex_object1]  Index=[index_object1]

glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 12, 0 );
glEnableVertexAttribArray( 0 );

glBindVertexArray( 0 );

// ↑VAO の bind を解除したので index_object1 のバインドも消える↓

// ** Bind: Vertex=[vertex_object1]  Index=[0]

// VAO の変更によって GL_ELEMENT_ARRAY_BUFFER (index) の bind は
// 元に戻るが、GL_ARRAY_BUFFER (vertex) には影響がでない。

同じ glBindBuffer() 命令でも動作が異なっていることがわかるかと思います。

GL_ELEMENT_ARRAY_BUFFER のバインド情報
 ・VAO が 0 の時は Default の Bind 情報が保持される。
 ・VAO が割り当てられている場合は VAO に格納する。

例えば下記 (7) のようなコードがあった場合、(A) は描画やオブジェクトに影響を
与えませんが (B) は VAO (input_layout) の内容を置き換えています。

// GL3 -- (7)
glUseProgram( program );
glUniform4fv( location, 4, matrix );
glBindVertexArray( input_layout );

glBindBuffer( GL_ARRAY_BUFFER, vertex_object );        // -- (A)
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object ); // -- (B)

glDrawElements( GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0 );

glBindVertexArray( 0 );

● Vertex Attribute Binding

(7) にあるように、描画時の (A) glBindBuffer( GL_ARRAY_BUFFER ) は直接
影響を与えません。

もし描画に使用する頂点バッファを置き換えたい場合は、Attribute 毎に
glVertexAttribPointer() を呼び直すことになります。
glVertexAttribPointer() に渡す TYPE などの情報が必要になりますし、
同じバッファを見ている Attribute の数だけ命令を実行しなければなりません。

OpenGL 4.3 では Vertex Buffer と Vertex Attribute を分けて管理できる
仕組みが追加されています。

glVertexAttribBinding( attrib_index, bind_index );
glBindVertexBuffer( bind_index, buffer_object, offset, stride );
glVertexAttribFormat( attrib_index, size, type, normalized, offset );

各 Attribute の頂点フォーマットは、これまでの
glVertexAttribPointer() 同様に glVertexAttribFormat() で定義します。

ただし各 Attribute が参照するバッファの情報は別で、bind_index を用いた
間接参照となります。
bind_index が指しているのは VertexBuffer の情報が入ったテーブルです。
実際のバッファの情報はこの VertexBuffer のテーブルの方に格納されます。

◎ GL3.x/4.x
・頂点 Attribute が持つ情報
    Type, Size, Offset, NormalizeFlag, VertexBuffer, Stride


◎ GL4.3/4.4 VertexAttribBinding
・頂点 Attribute が持つ情報
    Type, Size, Offset, NormalizeFlag, BindIndex (BindVertexBuffer を参照する)

・BindVertexBuffer のテーブルが持つ情報
    VertexBuffer, Stride

bind_index を通して、Attribute 毎に個別に持っていたバッファ情報を共有する
ことができます。

bind_index の値は glGetVertexAttribiv( GL_VERTEX_ATTRIB_BINDING ) で確認
することができます。
また実装上は Vertex Attribute の Table と Vertex Buffer の Table は
同一のもので、bind_index は自分自身への 2段階間接参照であることもわかります。

// GL3 --- (8)
GLuint input_layout= 0;

glGenVertexArrays( 1, &input_layout );
glBindVertexArray( input_layout );

glBindBuffer( GL_ARRAY_BUFFER, vertex_object );
glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object );

glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0 );
glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, 24, (void*)12 );

glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );

glBindVertexArray( 0 );

(8) を VertexAttribBinding を使って置き換えると下記の通り。

// GL4.3/4.4 --- (9)
GLuint input_layout= 0;

glGenVertexArrays( 1, &input_layout );
glBindVertexArray( input_layout );

glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, index_object );

glBindVertexBuffer( 0, vertex_object, 0, 24 );
glVertexAttribBinding( 0, 0 );
glVertexAttribBinding( 1, 0 );

glVertexAttribFormat( 0, 3, GL_FLOAT, GL_FALSE,  0 );
glVertexAttribFormat( 1, 3, GL_FLOAT, GL_FALSE, 12 );

glEnableVertexAttribArray( 0 );
glEnableVertexAttribArray( 1 );

glBindVertexArray( 0 );

VertexAttribBinding を使うと、下記のように glBindVertexBuffer() 一つで
頂点が参照する Vertex Buffer を置き換えることができます。

// GL4.3/4.4 -- (10)
glBindVertexArray( input_layout );
glBindVertexBuffer( 0, vertex_object2, 0, 64 );
glBindVertexArray( 0 );

試してみると glBindVertexBuffer() は glBindBuffer( GL_ARRAY_BUFFER )
を経由しないし全く変更もしないことがわかります。
これで描画に glBindBuffer( GL_ARRAY_BUFFER ) は必要なくなりました。

関連エントリ
OpenGL ES 3.0 と Vertex Array Object

OpenGL ES 3.0 / OpenGL 4.3 VertexArrayObject と VertexAttribBinding」への2件のフィードバック

  1. 通りすがり

    解説お疲れ様です・・・おかげでVAOまわりがなんとか理解できましが。が、それにしてもOpenGLのAPIって使ってみると酷いですねぇ。D3Dは初期化が死ぬほどメンドクサイだけで、あとはすんなりという印象だったのですが、GLは全領域にわたって分かりずらいorz。バージョン間の相違だとか、ステートの影響範囲だとか設計が恐ろしく冗長ですよね。D3Dのクラスにバージョン番号をつけるという何とも言えない戦略は、それはそれで機能していたのか・・・バカにしていたのですが、似たようなインターフェイスで動作が異なるより、よっぽどマシなんですね。(そもそも、OGLがステートマシンなのって、昔SceneGraphと一緒に使っていた時の名残な気がします。ローレベルにするなら、ステートなんて捨てればいいのにとも思うのですが。クロノスの中の人は何考えてるんでしょうね)

  2. oga 投稿作成者

    ありがとうございます
    API は D3D の方がわかりやすくデバッグしやすいですね。
    OpenGL も 4.5 では GL_ARB_direct_state_access が取り込まれて
    bind 無しの API が使えるようになりました。
    ただ D3D12 や Metal など次の世代の API も登場しつつあるので、
    OpenGL ももっと根本的な改善をして欲しいところです。

コメントは停止中です。