#include "resources/model.hpp" #include "errors/errors.hpp" #include "graphics/opengl/texture.hpp" #include "graphics/opengl/texture_array.hpp" #include "logging/engine_logger.hpp" #include "maths/transform.hpp" #include "resources/image.hpp" #include "resources/material.hpp" #include "resources/mesh.hpp" #include "resources/primitive.hpp" #include "utils/assert.hpp" #include "fastgltf/core.hpp" #include "fastgltf/glm_element_traits.hpp" #include "fastgltf/tools.hpp" #include "fastgltf/types.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace kuiper; resource::mesh parse_mesh_from_root_node(const fastgltf::Asset& asset, const fastgltf::Node& node); std::vector parse_materials(const fastgltf::Asset& asset); std::expected resource::model::from_gltf_path(const std::filesystem::path& gltf_path) { fastgltf::Parser parser {}; return model::from_gltf_path(gltf_path, parser); } std::expected resource::model::from_gltf_path(const std::filesystem::path& gltf_path, fastgltf::Parser& gltf_parser) { if (gltf_path.empty() || !std::filesystem::is_regular_file(gltf_path) || !std::filesystem::exists(gltf_path)) return {}; auto logger = engine_logger::get(); auto data = fastgltf::GltfDataBuffer::FromPath(gltf_path); if (data.error() != fastgltf::Error::None) { logger.error("Failed to load glTF asset from: {}", gltf_path.filename().c_str()); return std::unexpected(error::failed_to_load); } const fastgltf::Options fastgltf_opts = fastgltf::Options::None | fastgltf::Options::LoadExternalBuffers | fastgltf::Options::LoadExternalImages | fastgltf::Options::GenerateMeshIndices; auto asset = gltf_parser.loadGltf(data.get(), gltf_path.parent_path(), fastgltf_opts); if (auto error = asset.error(); error != fastgltf::Error::None) { logger.error("Failed to load glTF asset from: {}", gltf_path.filename().c_str()); return std::unexpected(error::failed_to_load); } // Parse the glTF scene description if (asset->scenes.size() == 0) { logger.error("glTF asset \"{}\" has no scenes", gltf_path.filename().c_str()); logger.error("DEBUG: asset created with \"{}\"", asset->assetInfo.has_value() ? asset->assetInfo->generator : "[unknown]"); return std::unexpected(error::resource_invalid); } else if (asset->scenes.size() > 1) { logger.warn("glTF asset \"{}\" has multiple scenes. Defaulting to scene 0 ...", gltf_path.filename().c_str()); logger.warn("DEBUG: asset created with \"{}\"", asset->assetInfo.has_value() ? asset->assetInfo->generator : "[unknown]"); } class model out_model {}; // Collect all materials present in the model const auto materials = parse_materials(asset.get()); // Iterate through the default scene node heirarchy const std::size_t scene_idx = asset->defaultScene.has_value() ? asset->defaultScene.value() : 0; const auto& scene = asset->scenes[scene_idx]; for (const std::size_t node_idx : scene.nodeIndices) { out_model.push_mesh(parse_mesh_from_root_node(asset.get(), asset->nodes[node_idx])); } for (const auto& mat : materials) { out_model.push_material(mat); } return out_model; } resource::mesh parse_mesh_from_root_node(const fastgltf::Asset& asset, const fastgltf::Node& node) { // Node (mesh) transform // The glTF specification notes that node transforms are relative to their parent node's transform glm::mat4 out_matrix {}; if (std::holds_alternative(node.transform)) { // Node contains a transformation matrix const auto& node_trs_matrix = std::get(node.transform); out_matrix = glm::make_mat4x4(node_trs_matrix.data()); } else if (std::holds_alternative(node.transform)) { // Node contains a TRS transform, convert to matrix const auto& node_trs = std::get(node.transform); glm::vec3 pos {node_trs.translation.x(), node_trs.translation.y(), node_trs.translation.z()}; glm::quat rot {node_trs.rotation.w(), node_trs.rotation.x(), node_trs.rotation.y(), node_trs.rotation.z()}; glm::vec3 scale {node_trs.scale.x(), node_trs.scale.y(), node_trs.scale.z()}; static constexpr glm::mat4 identity_mat = glm::mat4(1.0f); const glm::mat4 translation = glm::translate(identity_mat, pos); const glm::mat4 rotation = glm::mat4_cast(rot); const glm::mat4 scaling = glm::scale(identity_mat, scale); out_matrix = translation * rotation * scaling; } // Construct the output node (mesh) resource::mesh ret_val {out_matrix}; // Populate mesh with primitives if (node.meshIndex.has_value()) { const auto mesh_idx = node.meshIndex.value(); const fastgltf::Mesh& m = asset.meshes[mesh_idx]; // TODO: Split primitive parsing into separate function for (const fastgltf::Primitive& p : m.primitives) { KUIPER_ASSERT(p.type == fastgltf::PrimitiveType::Triangles); resource::primitive out_prim {}; out_prim.type = primitive_type::triangles; // asserted above // Vertex position auto pos_attr = p.findAttribute("POSITION"); const auto& pos_accessor = asset.accessors[pos_attr->accessorIndex]; out_prim.vertices.resize(pos_accessor.count); fastgltf::iterateAccessorWithIndex( asset, pos_accessor, [&out_prim](const glm::vec3& pos, std::size_t idx) { out_prim.vertices[idx].position = pos; }); // Vertex normals auto norm_attr = p.findAttribute("NORMAL"); const auto& norm_accessor = asset.accessors[norm_attr->accessorIndex]; if (norm_accessor.normalized) { fastgltf::iterateAccessorWithIndex( asset, norm_accessor, [&out_prim](const glm::vec3& norm, std::size_t idx) { out_prim.vertices[idx].normal = norm; }); } else { fastgltf::iterateAccessorWithIndex( asset, norm_accessor, [&out_prim](const glm::vec3& norm, std::size_t idx) { out_prim.vertices[idx].normal = glm::normalize(norm); }); } // Vertex tangents auto tan_attr = p.findAttribute("TANGENT"); const auto& tan_accessor = asset.accessors[tan_attr->accessorIndex]; fastgltf::iterateAccessorWithIndex( asset, tan_accessor, [&out_prim](const glm::vec4& tan, std::size_t idx) { out_prim.vertices[idx].tangent = tan; }); // Indices const auto& idx_accessor = asset.accessors[p.indicesAccessor.value()]; out_prim.indices.resize(idx_accessor.count); fastgltf::copyFromAccessor(asset, idx_accessor, out_prim.indices.data()); // Materials out_prim.material_idx = p.materialIndex.value(); // Texture coordinates const std::size_t tex_uv_idx = asset.materials[p.materialIndex.value()].pbrData.baseColorTexture->texCoordIndex; auto tex_uv_attr = p.findAttribute(std::format("TEXCOORD_{}", tex_uv_idx)); const auto& tex_uv_accessor = asset.accessors[tex_uv_attr->accessorIndex]; fastgltf::iterateAccessorWithIndex( asset, tex_uv_accessor, [&out_prim](const glm::vec2& uv, std::size_t idx) { out_prim.vertices[idx].uv = uv; }); // Add parsed primitive to mesh ret_val.add_primitive(out_prim); } } if (!node.children.empty()) { for (const auto child_idx : node.children) { const auto submesh = parse_mesh_from_root_node(asset, asset.nodes[child_idx]); ret_val.add_submesh(submesh); } } return ret_val; } std::expected load_material_image(const fastgltf::Asset& asset, const fastgltf::TextureInfo& tex_info) { const std::size_t tex_idx = tex_info.textureIndex; const std::size_t img_idx = asset.textures[tex_idx].imageIndex.value(); const fastgltf::Image& img = asset.images[img_idx]; if (std::holds_alternative(img.data)) { const auto& path = std::get(img.data); return resource::image::from_path(path.uri.path()); } else if (std::holds_alternative(img.data)) { const auto& arr = std::get(img.data); return resource::image::from_memory((const std::uint8_t*) arr.bytes.data(), arr.bytes.size_bytes()); } else if (std::holds_alternative(img.data)) { const auto& view = std::get(img.data); const auto& buffer_view = asset.bufferViews[view.bufferViewIndex]; const auto& buffer = asset.buffers[buffer_view.bufferIndex]; if (std::holds_alternative(buffer.data)) { const auto& arr = std::get(buffer.data); return resource::image::from_memory((const std::uint8_t*) arr.bytes.data() + buffer_view.byteOffset, arr.bytes.size_bytes()); } } return std::unexpected(resource::image_error {"unknown error"}); } std::vector parse_materials(const fastgltf::Asset& asset) { auto logger = engine_logger::get(); std::vector ret_vec(asset.materials.size()); for (std::uint32_t i = 0; i < asset.materials.size(); ++i) { const auto& gltf_mat = asset.materials[i]; auto& ret_mat = ret_vec[i]; ret_mat.base_colour_factor = glm::make_vec4(gltf_mat.pbrData.baseColorFactor.data()); ret_mat.metallic_factor = gltf_mat.pbrData.metallicFactor; ret_mat.roughness_factor = gltf_mat.pbrData.roughnessFactor; // Texture parameters GLint min_filter = GL_LINEAR_MIPMAP_NEAREST; GLint mag_filter = GL_LINEAR; GLint wrap_s = GL_REPEAT; GLint wrap_t = GL_REPEAT; // Load each of the texture images we're interested in // 0: Albedo, 1: Normal, 2: Metallic-Roughness std::array imgs {}; // Base colour if (gltf_mat.pbrData.baseColorTexture.has_value()) { { const auto& tex = asset.textures[gltf_mat.pbrData.baseColorTexture->textureIndex]; if (tex.samplerIndex.has_value()) { const fastgltf::Sampler& sampler = asset.samplers[tex.samplerIndex.value()]; if (sampler.minFilter.has_value()) min_filter = std::to_underlying(sampler.minFilter.value()); if (sampler.magFilter.has_value()) mag_filter = std::to_underlying(sampler.magFilter.value()); wrap_s = std::to_underlying(sampler.wrapS); wrap_t = std::to_underlying(sampler.wrapT); } } auto base_colour_img = load_material_image(asset, gltf_mat.pbrData.baseColorTexture.value()); if (base_colour_img.has_value()) { imgs[0] = std::move(base_colour_img.value()); } else { logger.warn("Failed to load material's base colour texture image: {}", base_colour_img.error().message); imgs[0] = resource::image::make_blank(64, 64, 4); } } else { logger.warn("Material does not have a base colour texture"); imgs[0] = resource::image::make_blank(64, 64, 4); } // Normal map if (gltf_mat.normalTexture.has_value()) { auto normal_img = load_material_image(asset, gltf_mat.normalTexture.value()); if (normal_img.has_value()) { imgs[1] = std::move(normal_img.value()); } else { logger.warn("Failed to load material's normal texture image: {}", normal_img.error().message); imgs[1] = resource::image::make_blank(64, 64, 4); } } else { logger.warn("Material does not have a normal map texture"); imgs[1] = resource::image::make_blank(64, 64, 4); } if (gltf_mat.pbrData.metallicRoughnessTexture.has_value()) { auto met_rough_img = load_material_image(asset, gltf_mat.pbrData.metallicRoughnessTexture.value()); if (met_rough_img.has_value()) { imgs[2] = std::move(met_rough_img.value()); } else { logger.warn("Failed to load material's metallic-roughness texture image: {}", met_rough_img.error().message); imgs[2] = resource::image::make_blank(64, 64, 4); } } else { logger.warn("Material does not have a metallic-roughness texture"); imgs[2] = resource::image::make_blank(64, 64, 4); } // Texture 2D arrays require that each layer has the same width & height // This is used to scale all textures in this material to the same size later on std::uint32_t max_width = 0; std::uint32_t max_height = 0; for (const auto& img : imgs) { KUIPER_ASSERT(img.width() == img.height()); if (img.width() > max_width) max_width = img.width(); if (img.height() > max_height) max_height = img.height(); } for (auto& img : imgs) { if (img.width() < max_width || img.height() < img.height()) img.resize(max_width, max_height); } ret_mat.textures = gl::texture_array::make(GL_TEXTURE_2D_ARRAY); ret_mat.textures->set_storage(GL_RGBA8, max_width, max_height, 3); ret_mat.textures->upload( 0, 0, 0, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[0].pixel_data().data()); ret_mat.textures->upload( 0, 0, 1, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[1].pixel_data().data()); ret_mat.textures->upload( 0, 0, 2, max_width, max_height, 1, GL_RGBA, GL_UNSIGNED_BYTE, imgs[2].pixel_data().data()); ret_mat.textures->set_param(GL_TEXTURE_MIN_FILTER, min_filter); ret_mat.textures->set_param(GL_TEXTURE_MAG_FILTER, mag_filter); ret_mat.textures->set_param(GL_TEXTURE_WRAP_S, wrap_s); ret_mat.textures->set_param(GL_TEXTURE_WRAP_T, wrap_t); ret_mat.textures->gen_mipmaps(); } return ret_vec; }