3DCG

Python3で「OpenGL」を学ぶ 「ModernGL」Buffer Format

概要

Buffer Formatとは、頂点バッファオブジェクト(VBO)内のデータのレイアウトを記述した短い文字列を指す。

VBOは多くの場合、Cのような構造体の均質な配列を含んでいる。バッファフォーマットは、配列の各要素がどのように見えるかを記述する。例えば、高精度の2次元頂点位置の配列を含むバッファのフォーマットは “2f8” となる。

バッファ形式は、Context.vertex_array() のコンストラクタで、内容 arg の 2番目の要素として使用される。以下のかんたんな使用例を参照。

構文

バッファ形式は次のようになる。

[count]type[size] [[count]type[size]…] [/usage]

ここでcountはオプションの整数。(デフォルトは1)


type はデータ型を示す1文字。

  • f : float
  • i : int
  • u : unsigned int
  • x : padding

size は、型を格納するために使用するオプションのバイト数。フォーマットは、スペースで区切られた複数の [count]type[size] トリプルを含むことができる(単一インターリーブ配列の例を参照)。

/usageはオプション。スペースを先頭にして、スラッシュの後に 1文字を続けて、バッファ内の連続した値をどのようにシェーダに渡すかを示すもの。

  • /v : 頂点ごとに指定する。バッファからの連続した値が各頂点に渡される。これは、使用法を省略した場合のデフォルトの動作。
  • /i : インスタンスごとに指定する。バッファからの連続した値が各インスタンスに渡される。
  • /r : レンダーごとに指定する.。バッファの最初の値が各インスタンスの各頂点に渡される。

複数のVBOをVAOに渡す場合、最初のものは /v でなければならない、例はこちらで示されている。

タイプとサイズの有効な組み合わせは以下の通り。

 size
type1248
fUnsigned byte (normalized)Half floatFloatDouble
iByteShortInt
uUnsigned byteUnsigned shortUnsigned int
x1 byte2 bytes4 bytes8 bytes

エントリー f1 には、2つの変わった性質がある。

  1. 型は f (float の場合) 、符号なしバイトを含むバッファを定義している。このサイズの float のみの場合、値は正規化される。すなわち、バッファ内の 0 から 255 までの符号なしバイトは、頂点シェーダに到達するまでに 0.0 から 1.0 までの float値に変換される。これは、色を符号なしバイトとして渡すためのもの。
  2. 3f1 のフォーマットを持つ 3つの符号なしバイトは、vec3 アトリビュートに割り当てられる可能性がある。しかし、ModernGL v6.0 からは、代わりに vec4 アトリビュートに渡すことができる。これは、3バイトの RGB 値のバッファをアルファチャネルも含むアトリビュートに渡すためのもの。

i 型と u 型にはサイズ 8 の型はない。

このバッファフォーマットの構文は、ModernGL に特有のもの。以下の使用例にあるように、このフォーマットは struct.pack に渡されるフォーマット文字列に似ていることがあり、これは別の構文。(ドキュメント

バッファフォーマットは、広範囲の頂点アトリビュートフォーマットを表すことができる。バッファフォーマットを使用して表現できない特殊なアトリビュートフォーマットの稀なケースのために、VertexArray.bind()メソッドがあり、基礎となるOpenGLバインディングコールを手動で設定する。(これは一般的には推奨されない。)

バッファフォーマットの例

“2f” はカウントが 2 で、型は f (float) 。したがって、頂点シェーダの vec2 アトリビュートに渡される 2つのfloatを記述している。floatのサイズは指定されていないため、デフォルトは4バイト。バッファの使用法は指定されていないため、デフォルトは /v (vertex) で、配列内の連続したfloatの各ペアは、レンダーコール中に連続した頂点に渡される。

“3i2/i “は3つのi(整数)を意味する。各整数のサイズは2バイトであり、つまり、それらはショートであり、ivec3属性に渡される。最後の /i は、バッファ内の連続した値が、インスタンスのレンダリング呼び出し中に連続したインスタンスに渡されることを意味する。つまり、特定のインスタンス内の全ての頂点に同じ値が渡される。

インターリーブされた値を保持するバッファは、スペースで区切られた複数のカウント型サイズのトリプルで表現される。したがって、以下のようになる。

“2f 3u x /v “の意味:

  • 2f: 2つのfloatで、vec2属性に渡され、その後、
  • 3u: 3つの符号なしバイトで、uvec3に渡され、その後、
  • x: アラインメントのための1バイトのパディング

/vは、バッファ内の連続した要素がレンダリング中に連続した頂点に渡されることを示す。/vは省略可能。

かんたんな使い方の例

1つの三角形を形成する2次元の頂点位置を含むVBOを考える。

# a 2D triangle (ie. three (x, y) vertices)
verts = [
     0.0, 0.9,
    -0.5, 0.0,
     0.5, 0.0,
]

# pack all six values into a binary array of C-like floats
verts_buffer = struct.pack("6f", *verts)

# put the array into a VBO
vbo = ctx.buffer(verts_buffer)

# use the VBO in a VAO
vao = ctx.vertex_array(
    shader_program,
    [
        (vbo, "2f", "in_vert"), # <---- the "2f" is the buffer format
    ]
    index_buffer_object
)

(vbo, “2f”, “in_vert”) は、2つのfloatからなる値の配列を含んでいることを示す。これらの値は、頂点シェーダで以下のように宣言された in_vert アトリビュートに渡される。

in vec2 in_vert;

2f フォーマットではサイズコンポーネントが省略されているため、floatはデフォルトでそれぞれ4バイトになる。このフォーマットでは、バッファからの連続した (x, y) 行がレンダーコール中に連続した頂点に渡されるように、後続の /usage コンポーネントも省略されている。

シングルインターリーブ配列の例

バッファ配列は、複数の値をインターリーブした要素を含む場合がある。例えばバッファ配列の各要素には、2D頂点の位置(xy)が float で、RGB 色が符号なしの int で、アライメントのための 1 バイトのパディングが含まれているとする。

positioncolorpadding
xyrgb
floatfloatunsigned byteunsigned byteunsigned bytebyte

このようなバッファは、どのようにコントラクトしても、VAO を使用して VAO に渡される。

vao = ctx.vertex_array(
    shader_program,
    [
        (vbo, "2f 3f1 x", "in_vert", "in_color")
    ]
    index_buffer_object
)

フォーマットは2f(xyのfloat)で始まり、シェーダのin_vertアトリビュートに渡され、次のように宣言される。

in vec2 in_vert;

次にスペースの後にあるのは 3f1で、これはRGBの符号なしバイトで、f1によって浮動小数点に正規化される。これらのfloatはシェーダの in_colorアトリビュートに渡される。

in vec3 in_color;

最後に、フォーマットは x で終わる。これは 1 バイトのパディングで、シェーダのアトリビュート名を必要としません。

/usage が異なる複数の配列の例

/usage 部分の説明のために、12個のキューブをインスタンスレンダリングでレンダリングすることを考える。ここでは、以下のように使用する。

  • vbo_verts_normals には、1つのキューブ内の頂点の頂点 (3つのfaloat) と法線 (3つのfloat) が含まれる
  • vbo_offset_orientation には、各キューブの位置と方向付けに使用されるオフセット (3つのfloat) とオリエンテーション (9つのfloat・matrices) が含まれる
  • vbo_colors には、色 (3つのfloat) が含まれます。この例では、バッファ内の色は 1色のみで、各キューブの各頂点に使用される

シェーダは、上記のすべての値をアトリビュートとして使用する。

インスタンスレンダリングの呼び出しに備えて、上記の VBO を 1つの VAO にバインドする。

vao = ctx.vertex_array(
    shader_program,
    [
        (vbo_verts_normals,      "3f 3f /v", "in_vert", "in_norm"),
        (vbo_offset_orientation, "3f 9f /i", "in_offset", "in_orientation"),
        (vbo_colors,             "3f /r",    "in_color"),
    ]
    index_buffer_object
)

つまり、/v を使った頂点と法線は、インスタンス内の各頂点に渡される。これは、VAO の最初の VBO は /v を使用しなければならないというルールを満たしている。これらは頂点属性に次のように渡される。

in vec3 in_vert;
in vec3 in_norm;

オフセットと方向は、インスタンス内の各頂点に同じ値を渡すが、バッファ内の次の値を次のインスタンスの頂点に渡す。

in vec3 in_offset;
in mat3 in_orientation;

単一の色をすべてのインスタンスの各頂点に渡す。もし色を /v または /i で格納していた場合、同じ色の値を重複して vbo_colors に格納しなければならない。すべてのキューブを 1つの色でレンダリングするには、このような重複は不要。r を使用すると、バッファに必要なのは 1つの色だけで、それはすべてのインスタンスのすべての頂点に渡され、すべてのレンダリング呼び出しのために使用される。

in vec3 in_color;

別のアプローチとしては、色は一定のため、ユニフォームとして渡すという方法もあります。しかし、アトリビュートとして色を渡す方が柔軟性があります。これにより、異なるバッファにバインドされた同じシェーダプログラムを再利用して、インスタンスごと、または頂点ごとに異なるカラーデータを渡すことができる。

参考にした記事はこちら