概要
今エンジン開発で使っているopenglでgltfを描画するコードの解説記事となります。
ただし、頂点と法線を読み込んで以下の画像の様に表示するだけなので、アニメーション等は扱いませんのでご容赦ください。
コード
以下のRenderComponentクラスとRenderManagerクラスのバッファ確保と描画しているコードを解説していきます。(一部記事内では分かりやすさのために、コードの順番が前後しますがご了承ください。)
記事内ではあまり触れていませんが、シェーダは以下のフォルダーにあるvertexshaderとfragmentshaderです。
gltfについて
gltfは3Dモデルのデータフォーマットで、blender等からエクスポート可能です。
どういう形式かは以前記事をかいたので、こっちをご覧ください。
何があれば描画できるか
どういった情報があれば描画出来るか考えてみます。
今回は形状を表示出来ればよいので
頂点座標
頂点のインデックス
の二点があればよさそうです。
あと、オブジェクト上に軽く色付けするために法線
もとってみます。
tinygltfで読み込む
前準備
まずはtinygltfのTinyGLTFクラスのLoadASCIIFromFile関数
でgltfファイルを読み込みます。
以下のコードをご覧ください。ここでは、./Resource/Mesh/test_model/testmodel.gltf
というパスにあるgltfファイルを読み込み、tinygltfがパースした結果をModel型のmodelという名前の変数に代入しています。このmodelの中を見ると、gltfファイルに書かれた頂点情報が取得できます。
tinygltf::Model model;
tinygltf::TinyGLTF loader;
std::string err, warn;
bool ret = loader.LoadASCIIFromFile(&model, &err, &warn, "./Resource/Mesh/test_model/testmodel.gltf");
gltfで頂点情報のあるところは上から見ていくと下の方にあるので、以下のコードで頂点情報の所まで見ていきます。ただ、シーンが複数ある場合や、ノードが複数ある場合には対応してません。。(いづれ複数あるパターンに対応出来たら追記したい)
//sceneを取得(固定で0としているが、あんまりよくないかも)
auto scene = model.scenes[0];
//シーンの参照しているのノード(本当は複数あるパターンにも対応すべき)
auto nodeindex = scene.nodes[0];
//nodeが複数ある場合は以下がfor文等になると思われる
//ノード
auto node = model.nodes[nodeindex];
//ノードに対応するメッシュ
auto mesh = model.meshes[node.mesh];
//accessorのインデックス
auto position_index = mesh.primitives[0].attributes["POSITION"];
//accessorのインデックス
auto indices_index = mesh.primitives[0].indices;
//bufferview
auto buffer_views = model.bufferViews;
頂点座標
以下のコードで出てくるShaderVector4
という構造体はこれです。
struct ShaderVector4
{
GLfloat position[4];
};
以下のコードですべての頂点の頂点座標のが入ったstd::vectorを作ります。
//POSITIONのaccessor
auto position_accessor = model.accessors[position_index];
//bufferview
auto position_buffer_view = buffer_views[position_accessor.bufferView];
//buffer
auto position_buffer = model.buffers[position_buffer_view.buffer];
//頂点座標の生データ(ここに3次元の頂点情報が詰まっている)
const float* positions = reinterpret_cast<const float*>(&position_buffer.data[position_buffer_view.byteOffset + position_accessor.byteOffset]);
//頂点座標をvertex_vectorに追加
this->vertex_vector = {};
for (unsigned long long i = 0; i < position_accessor.count; ++i)
{
//頂点座標ひとつひとつ読み込んでる
ShaderVector4 v = {positions[i * 3 + 0], positions[i * 3 + 1], positions[i * 3 + 2], 1};
this->vertex_vector.push_back(v);
}
頂点のバッファ作る
さっき作った頂点座標のstd::vectorからopenglのバッファを作ります。(シェーダに渡す部分も含んでいます。)
auto position_point_count = position_accessor.count;
//buffer参照するやつ
GLuint vertexBuffer;
//バッファ確保
glGenBuffers(1, &vertexBuffer);
//GL_ARRAY_BUFFERと紐づけ(頂点座標なので)
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
//先ほど読み込んだ頂点座標のstd::vector
auto vertex = this->vertex_vector;
//bufferにデータを書き込む
glBufferData(GL_ARRAY_BUFFER, position_point_count * sizeof(ShaderVector4), vertex.data(), GL_STATIC_DRAW);
//シェーダのpositionという変数のポインター参照を取得
auto vertexShader_PositionAttribute = glGetAttribLocation(program, "position");
//有効にする
glEnableVertexAttribArray(vertexShader_PositionAttribute);
//紐づけるバッファについて教えるやつ
glVertexAttribPointer(vertexShader_PositionAttribute, 4, GL_FLOAT, GL_FALSE, 0, 0);
インデックス
インデックスもほぼ同様に取得できる。
以下のコードでインデックスのstd::vectorを作っている。
//インデックスのaccessor
auto indices_accessor = model.accessors[indices_index];
//インデックスのbufferview
auto indices_buffer_view = buffer_views[indices_accessor.bufferView];
//インデックスのbuffer
auto indices_buffer = model.buffers[indices_buffer_view.buffer];
//インデックスの生データ
const unsigned short* indices_data_fromgltf = reinterpret_cast<const unsigned short*>(&indices_buffer.data[indices_buffer_view.byteOffset + indices_accessor.byteOffset]);
//インデックスをindex_vectorに追加
this->index_vector = {};
for (unsigned long long i = 0; i < indices_accessor.count; ++i)
{
//インデックスを詰め込んでいく
this->index_vector.push_back(indices_data_fromgltf[i]);
}
インデックスバッファ作る
そしてインデックスのバッファを以下のコードで作ります。(シェーダには渡さないので、シェーダに渡すコードは無し)
//インデックスバッファ
GLuint elementBuffer;
//バッファ作る
glGenBuffers(1, &elementBuffer);
//GL_ELEMENT_ARRAY_BUFFERと紐づける(インデックスなので)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBuffer);
//先ほど確保したインデックスのstd::vectorを取得
auto indices = this->index_vector;
//インデックスのサイズ(頂点数と同じとは限らない)
auto indeicesSize = indices.size()*sizeof(indices[0]);
//バッファに流し込む
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indeicesSize, indices.data(), GL_STATIC_DRAW);
法線作る
以下のコードで出てくるShaderVector3はこれです。
struct ShaderVector3
{
GLfloat position[3];
};
以下のコードで、法線のstd::vectorを作る。
//法線のaccessorのインデックス
auto normal_accessors_index = mesh.primitives[0].attributes["NORMAL"];
//法線のaccessor
auto normal_accessor = model.accessors[normal_accessors_index];
//法線のbufferview
auto normal_buffer_view = buffer_views[normal_accessor.bufferView];
//法線のbuffer
auto normal_buffer = model.buffers[normal_buffer_view.buffer ];
//法線の生データ
const float* normal_data_fromgltf = reinterpret_cast<const float*>(&normal_buffer.data[normal_buffer_view.byteOffset + normal_accessor.byteOffset]);
std::vector<ShaderVector3> normal_vector = {};
for (unsigned long long i = 0; i < normal_accessor.count; ++i)
{
//法線を一つ一つ読み込む
ShaderVector3 v = { normal_data_fromgltf[i * 3 + 0], normal_data_fromgltf[i * 3 + 1], normal_data_fromgltf[i * 3 + 2] };
normal_vector.push_back(v);
}
法線バッファ作る
先ほど作ったstd::vectorからopenglのバッファを作ります(シェーダに渡す部分も含みます。)
//法線バッファ
GLuint normalBuffer;
//バッファ作る
glGenBuffers(1, &normalBuffer);
//GL_ARRAY_BUFFERに紐付け
glBindBuffer(GL_ARRAY_BUFFER, normalBuffer);
//先ほど確保した法線を流し込む
glBufferData(GL_ARRAY_BUFFER, normal_vector.size() * sizeof(ShaderVector3), normal_vector.data(), GL_STATIC_DRAW);
//シェーダにあるnormalという変数の参照取得
auto vertexshader_normal_attribute = glGetAttribLocation(program, "normal");
//有効化
glEnableVertexAttribArray(vertexshader_normal_attribute);
//どんな値かとか設定するやつ
glVertexAttribPointer(vertexshader_normal_attribute, 3, GL_FLOAT, GL_FALSE, 0, 0);
描画
あとは描画するだけ。実際にコードで描画に直接関係ない部分は省略。
mvp行列の計算もあるが、基本的にはglBindVertexArrayでVertexArrayをバインドして、glDrawElementsしてるだけです。gltf使わずに三角形を手打ちで描画しているのと同じです。(つまり、gltf要素は特にないです)
void RenderManager::RenderOneGameObject(components::RenderComponent* render_component)
{
glUseProgram(this->renderComponent->GetProgram());
glBindVertexArray(this->renderComponent->GetVao());
//プロジェクション行列
glm::mat4 Projection = glm::perspective(glm::radians(45.0f), 4.0f / 3.0f, 0.1f, 400.0f);
// カメラ行列
camerax += cameradiff * 1.3;
if (camerax > 15 || camerax < -15)
{
cameradiff *= -1;
}
cameraWorldRotate += 0.01;
if(cameraWorldRotate>=360)
{
cameraWorldRotate = 0;
}
float radius = 10;
//view行列
glm::mat4 View = glm::lookAt(
glm::vec3(10*glm::cos(cameraWorldRotate), 7, radius*sin(cameraWorldRotate)),
glm::vec3(0, 0, 0),
glm::vec3(0, 1, 0)
);
auto gameobject = render_component->GetGameObject();
//座標を取得
auto position = gameobject->GetComponent<components::TransformComponent>()->GetPosition();
//モデル行列
glm::mat4 Model = glm::translate(glm::vec3(position.x, position.y, position.z));
//MVP行列
glm::mat4 MVP = Projection * View * Model;
auto vShader_mvp_pointer = glGetUniformLocation(this->renderComponent->GetProgram(), "mvp");
//シェーダにMVP行列を渡す
glUniformMatrix4fv(vShader_mvp_pointer, 1, GL_FALSE, &MVP[0][0]);
//描画
glDrawElements(GL_TRIANGLES, render_component->draw_point_count, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
シェーダ
一応シェーダも記載しておきます。
#version 430
in vec4 position;
in vec3 normal;
uniform mat4 mvp;
out vec3 vNormal;
void main(){
gl_Position=mvp*position;
vNormal=normal;
}
#version 430
out vec4 fragment;
in vec3 vNormal;
uniform float c;
void main() {
fragment = vec4(vNormal, 1.0);
}
あとがき
初めは一行ずづコード説明をかいていたのですが、見にくいなと思い一行ずづの説明はコード内にコメントで書きました。
また、記事を書く前はもう少し書くことがあるかなと思ったのですが、書いてみるとgltfから頂点情報を読み取ってopenglのバッファに流すだけなので、あまり書くことが無かったですね。。ただ、c++初学者なので変な書き方をしているかもしれません、その時はコメント等で教えていただけると有難いです。
ちなみにですが、今回のコードで改善点としては、ノードが複数あるパターンに対応していない、頂点座標等をキャストする型を決め打ちでしている点があると思うので、今後改善していきたいと思います。