Tuesday, March 29, 2016

OpenGLでDirectX風に頂点レイアウトを書く方法

以前の記事で、OpenGLの頂点レイアウトやInput Layout Qualifiersの事を書きましたが、もうちょっといい方法を思いついたので書きます。以下のようにDirectX11のように頂点レイアウトを構造体の配列として書きます。

struct InputElement {
const char* name;
ShaderFormat format;
int offset;
int inputSlot;
bool perInstance;
};
static InputElement elements[] = {
{"POSITION", SF_R32G32B32_FLOAT, 0, 0, false},
{"COLOR", SF_R8G8B8A8_UNORM, 12, 0, false},
{"TEXCOORD", SF_R32G32_FLOAT, 16, 0, false},
};
GLuint CreateProgramWithElements()
{
GLuint program = glCreateProgram();
for (int i = 0; i < numElements; i++) {
glBindAttribLocation(program, i, elements[i].name);
}
return program;
}
void CreatePrograms()
{
GLuint a = CreateProgramWithElements();
GLuint b = CreateProgramWithElements();
...
この方法のキモは、elementsの配列の並び順にglBindAttribLocationで0から順に番号を割り当てることです。この例では、"POSITION"が0番、"COLOR"が1番、"TEXCOORD"が2番です。例えば以下のような頂点シェーダのための定義になります。inのところだけ見てください。glBindAttribLocationを使うので当然Input Layout Qualifiersは不要です。

#version 310 es
precision highp float;
in vec3 POSITION;
in vec2 TEXCOORD;
in vec4 COLOR;
out vec2 texcoord;
out vec4 color;
layout (binding = 0) uniform matUbo {
layout (row_major) mat4 matProj;
};
void main() {
gl_Position = vec4(POSITION.xyz, 1) * matProj;
texcoord = TEXCOORD;
color = COLOR;
}
この過程を同じ頂点レイアウトを使用する複数のシェーダーに適用することで、シェーダが変わっても同じVAO(Vertex Array Object)を使用できます。言い換えると、シェーダーからglGetAttribLocationで"POSITION"は何番か、と問い合わせる方法でVAOを生成してしまうとそのシェーダ専用のVAOしか作れなくなってしまう為、InputElementという仲介役を立てたと言えます。

以上を踏まえたVAOを生成する実装の実例を紹介します。長いですがやってることはInputElementの配列を解釈しながらVAOを作っているだけです。こうして作ったVAOは同じInputElement配列を指定して作った頂点シェーダならどれにでもバインドできるようになります。

void afSetVertexAttributes(const InputElement elements[], int numElements, int numBuffers, GLuint const vertexBufferIds[], const int strides[])
{
for (int i = 0; i < numElements; i++) {
const InputElement& d = elements[i];
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferIds[d.inputSlot]);
GLenum r = glGetError();
if (r != GL_NO_ERROR) {
aflog("glBindBuffer error! i=%d inputSlot=%d vbo=%d\n", i, d.inputSlot, vertexBufferIds[d.inputSlot].x);
}
afHandleGLError(glEnableVertexAttribArray(i));
switch (d.format) {
case SF_R32_FLOAT:
case SF_R32G32_FLOAT:
case SF_R32G32B32_FLOAT:
case SF_R32G32B32A32_FLOAT:
glVertexAttribPointer(i, d.format - SF_R32_FLOAT + 1, GL_FLOAT, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UNORM:
case SF_R8G8_UNORM:
case SF_R8G8B8_UNORM:
case SF_R8G8B8A8_UNORM:
glVertexAttribPointer(i, d.format - SF_R8_UNORM + 1, GL_UNSIGNED_BYTE, GL_TRUE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UINT_TO_FLOAT:
case SF_R8G8_UINT_TO_FLOAT:
case SF_R8G8B8_UINT_TO_FLOAT:
case SF_R8G8B8A8_UINT_TO_FLOAT:
glVertexAttribPointer(i, d.format - SF_R8_UINT_TO_FLOAT + 1, GL_UNSIGNED_BYTE, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R8_UINT:
case SF_R8G8_UINT:
case SF_R8G8B8_UINT:
case SF_R8G8B8A8_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R8_UINT + 1, GL_UNSIGNED_BYTE, strides[d.inputSlot], (void*)d.offset);
#endif
break;
case SF_R16_UINT_TO_FLOAT:
case SF_R16G16_UINT_TO_FLOAT:
case SF_R16G16B16_UINT_TO_FLOAT:
case SF_R16G16B16A16_UINT_TO_FLOAT:
glVertexAttribPointer(i, d.format - SF_R16_UINT_TO_FLOAT + 1, GL_UNSIGNED_SHORT, GL_FALSE, strides[d.inputSlot], (void*)d.offset);
break;
case SF_R16_UINT:
case SF_R16G16_UINT:
case SF_R16G16B16_UINT:
case SF_R16G16B16A16_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R16_UINT + 1, GL_UNSIGNED_SHORT, strides[d.inputSlot], (void*)d.offset);
#endif
break;
case SF_R32_UINT:
case SF_R32G32_UINT:
case SF_R32G32B32_UINT:
case SF_R32G32B32A32_UINT:
#ifdef AF_GLES31
glVertexAttribIPointer(i, d.format - SF_R32_UINT + 1, GL_UNSIGNED_INT, strides[d.inputSlot], (void*)d.offset);
#endif
break;
default:
assert(0);
break;
}
#ifdef AF_GLES31
if (d.perInstance) {
glVertexAttribDivisor(i, 1);
}
#endif
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
GLuint afCreateVAO(const InputElement elements[], int numElements, int numBuffers, GLuint const vertexBufferIds[], const int strides[], GLuint ibo)
{
VAOID vao;
glGenVertexArrays(1, &vao.x);
glBindVertexArray(vao);
afSetVertexAttributes(elements, numElements, numBuffers, vertexBufferIds, strides);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBindVertexArray(0);
return vao;
}
view raw vao_maker.cpp hosted with ❤ by GitHub
afCreateVAOは、InputElement構造体の配列、インデックスバッファ、頂点バッファの配列(それぞれ順にInputElement構造体のslotに対応、OpenGLにslotという概念は無いが、DirectX風に定義)、strideの配列を受け取ってVAOを作ります。

ほぼ同じ引数を受け取るafSetVertexAttributesは、頂点バッファを現在のVAOにバインドする部分です。VAOの無いOpenGL ES 2.0でもそのまま使えるコードです。

こうすることでInput Layout Qualifiersを使う必要もなくなり、glGetAttribLocationも必要なくなり、OpenGL ES 2.0、OpenGL ES 3.0双方で同じコードが動き、DirectXとのコードの共通化にも役立ちます。