RATH was using tinyobjloader[1] for loading meshes, which took .obj files and took the position, normals, and texCoord attributes for drawing. However, I also wanted my meshes to have animation capabilities, which could be configured through Blender's keyframes for example. Because of this, I decided to replace tinyobjloader with the cgltf library[2],
reworking R_Model around primitives and the model loader as well.
Reworking R_Model
A glTF mesh consists of a bunch of primitives, and each primitive has its own attributes, textures, etc. (all defined by an external binary data file interpreted by accessors). I moved it to an R_Primitive, which is just an index range plus the texture URI it uses.
The R_ModelCreateInfo changed to only being the asset, file path, and its pipeline, and rCreateModel loads the file and builds the vertex and index buffers off the back of it.
struct R_Primitive {
u32 indexOffset;
u32 indexCount;
std::string textureUri;
R_Material* material = nullptr;
};
// The new model create info
struct R_ModelCreateInfo {
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
std::string modelPath;
};
bool Rath::R_Model::rCreateModel(Device& _device, Buffer& _buffer,
const R_ModelCreateInfo& info, R_Model* _model) {
// Needs a value model to fill information about
if (_model == nullptr) return false;
_model->device = &_device;
_model->buffer = &_buffer;
_model->modelPath = info.modelPath;
_model->pipeline = info.pipeline;
_model->pipelineLayout = info.pipelineLayout;
_model->loadModel();
_model->createVertexBuffer();
_model->createIndexBuffer();
return true;
}
The loader
Loading makes use of two cgltf functions to parse the JSON and then load the buffers it references to obtain the binary data. Each node recurses through handleNodes, which reads the primitive attributes with cgltf_accessor_read_float, moves the node's world transform into the vertices, and gets
each primitive's texture URI for the material to use later.
void Rath::R_Model::loadModel() {
cgltf_options options = {};
cgltf_data* data = NULL;
if (cgltf_parse_file(&options, modelPath.c_str(), &data) != cgltf_result_success) {
throw std::runtime_error("Failed to parse gltf");
};
if (cgltf_load_buffers(&options, data, modelPath.c_str()) != cgltf_result_success) {
throw std::runtime_error("Failed to load gltf buffers");
}
for (cgltf_size i = 0; i < data->scene->nodes_count; i++) {
handleNodes(data->scene->nodes[i]);
}
cgltf_free(data);
}
The important parts of handleNodes:
void Rath::R_Model::handleNodes(cgltf_node* node) {
if (node->mesh) {
f32 m[16];
cgltf_node_transform_world(node, m);
enginemath::Mat4 worldTransform(/* rows built from m[16] */);
for (size i = 0; i < node->mesh->primitives_count; i++) {
Vertex vertex{};
R_Primitive primitive{};
cgltf_primitive* prim = &node->mesh->primitives[i];
// posAccessor, normalAccessor, texCoordAccessor resolved from prim->attributes
// ...
cgltf_size n = posAccessor->count;
u32 vertexOffset = static_cast<u32>(vertices.size());
// loop for every vertex
for (size s = 0; s < n; s++) {
cgltf_accessor_read_float(posAccessor, s, vertex.pos.elements, 3);
if (normalAccessor) cgltf_accessor_read_float(normalAccessor, s, vertex.normal.elements, 3);
if (texCoordAccessor) cgltf_accessor_read_float(texCoordAccessor, s, vertex.texCoord.data, 2);
// bake the node's world transform into the vertex
enginemath::Vec4 p = worldTransform * enginemath::Vec4::toVec4Pos(vertex.pos);
vertex.pos = { p.x, p.y, p.z };
enginemath::Vec4 norm = worldTransform * enginemath::Vec4::toVec4Dir(vertex.normal);
vertex.normal = { norm.x, norm.y, norm.z };
vertices.push_back(vertex);
}
// a primitive is an index range plus the texture it was authored with
primitive.indexCount = prim->indices->count;
primitive.indexOffset = (u32)indices.size();
if (prim->material && prim->material->has_pbr_metallic_roughness) {
cgltf_texture* tex = prim->material->pbr_metallic_roughness.base_color_texture.texture;
if (tex && tex->image && tex->image->uri) {
primitive.textureUri = tex->image->uri;
}
}
primitives.push_back(primitive);
// indices, offset so each primitive points at where it was pushed
for (cgltf_size index = 0; index < prim->indices->count; index++) {
cgltf_size idx = cgltf_accessor_read_index(prim->indices, index);
indices.push_back(vertexOffset + static_cast<u32>(idx));
}
}
}
// recurse over the rest of the scene graph
for (size i = 0; i < node->children_count; i++) {
handleNodes(node->children[i]);
}
}
As seen below, with the working glTF model loader and small refactors, I am able to load any glTF model I put into RATH (seen with the dragon model). For next steps, I want to refactor/clean up my older code, especially my boilerplate for Vulkan because the structure feels messy to me.