From 036b40a9d8152a42ab82823d6e2be4a1c20baf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:11:37 +0200 Subject: [PATCH] Fix gltf static mesh loading issues (#14) * Support u8 / u32 indices * Skip primitives without vertices * Add support for non-indexed geometry & skipping primitives * Fix possible memory leak on error * Use SSkinnedMesh * Check indices * Properly mirror node hierarchy * Update .gitignore * Reorder includes * Add some throws for logic errors * Fix non-indexed geometry winding order, add unit test * Address code review comments * Add matrix transform unit test --- .gitignore | 12 + include/SSkinMeshBuffer.h | 26 +- source/Irrlicht/CGLTFMeshFileLoader.cpp | 310 ++++++++++++------ source/Irrlicht/CGLTFMeshFileLoader.h | 38 ++- .../assets/blender_cube_matrix_transform.gltf | 106 ++++++ .../assets/triangle_without_indices.gltf | 54 +++ .../tests/testCGLTFMeshFileLoader.cpp | 33 +- 7 files changed, 466 insertions(+), 113 deletions(-) create mode 100644 source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf create mode 100644 source/Irrlicht/tests/assets/triangle_without_indices.gltf diff --git a/.gitignore b/.gitignore index 8bced59d..e39046fc 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,15 @@ scripts/glext.h # vscode cmake plugin build/* + +# vscode clangd plugin +.cache +compile_commands.json + +# build dependencies +_deps + +# autogenerated test-related files +**/CTestTestfile.cmake +Testing +DartConfiguration.tcl diff --git a/include/SSkinMeshBuffer.h b/include/SSkinMeshBuffer.h index 5ced6057..f5993226 100644 --- a/include/SSkinMeshBuffer.h +++ b/include/SSkinMeshBuffer.h @@ -301,7 +301,31 @@ struct SSkinMeshBuffer : public IMeshBuffer } //! append the vertices and indices to the current buffer - void append(const void *const vertices, u32 numVertices, const u16 *const indices, u32 numIndices) override {} + void append(const void* const vertices, u32 numVertices, const u16* const indices, u32 numIndices) override { + if (vertices == getVertices()) + throw std::logic_error("can't append own vertices"); + + if (VertexType != video::EVT_STANDARD) + throw std::logic_error("invalid vertex type"); + + const u32 prevVertexCount = getVertexCount(); + + Vertices_Standard.reallocate(prevVertexCount + numVertices); + for (u32 i=0; i < numVertices; ++i) { + Vertices_Standard.push_back(static_cast(vertices)[i]); + BoundingBox.addInternalPoint(static_cast(vertices)[i].Pos); + } + + Indices.reallocate(getIndexCount() + numIndices); + for (u32 i=0; i < numIndices; ++i) { + Indices.push_back(indices[i] + prevVertexCount); + } + } + + //! NOT IMPLEMENTED YET: append the meshbuffer to the current buffer + void append(const IMeshBuffer* const other) override { + throw std::logic_error("not implemented yet"); + } //! get the current hardware mapping hint for vertex buffers E_HARDWARE_MAPPING getHardwareMappingHint_Vertex() const override diff --git a/source/Irrlicht/CGLTFMeshFileLoader.cpp b/source/Irrlicht/CGLTFMeshFileLoader.cpp index 2d19cadb..81e11775 100644 --- a/source/Irrlicht/CGLTFMeshFileLoader.cpp +++ b/source/Irrlicht/CGLTFMeshFileLoader.cpp @@ -1,23 +1,25 @@ #include "CGLTFMeshFileLoader.h" -#include "CMeshBuffer.h" + #include "coreutil.h" -#include "IAnimatedMesh.h" -#include "ILogger.h" -#include "IReadFile.h" +#include "CSkinnedMesh.h" +#include "ISkinnedMesh.h" #include "irrTypes.h" -#include "os.h" +#include "IReadFile.h" +#include "matrix4.h" #include "path.h" #include "S3DVertex.h" -#include "SAnimatedMesh.h" -#include "SColor.h" -#include "SMesh.h" +#include "quaternion.h" #include "vector3d.h" + +#include "tiniergltf.hpp" + +#include #include #include +#include #include #include #include -#include #include #include #include @@ -96,72 +98,214 @@ IAnimatedMesh* CGLTFMeshFileLoader::createMesh(io::IReadFile* file) return nullptr; } - MeshExtractor parser(std::move(model.value())); - SMesh* baseMesh(new SMesh {}); - loadPrimitives(parser, baseMesh); - - SAnimatedMesh* animatedMesh(new SAnimatedMesh {}); - animatedMesh->addMesh(baseMesh); - baseMesh->drop(); - - return animatedMesh; + ISkinnedMesh *mesh = new CSkinnedMesh(); + MeshExtractor parser(std::move(model.value()), mesh); + try { + parser.loadNodes(); + } catch (std::runtime_error &e) { + mesh->drop(); + return nullptr; + } + return mesh; } +static void transformVertices(std::vector &vertices, const core::matrix4 &transform) +{ + for (auto &vertex : vertices) { + // Apply scaling, rotation and rotation (in that order) to the position. + transform.transformVect(vertex.Pos); + // For the normal, we do not want to apply the translation. + // TODO note that this also applies scaling; the Irrlicht method is misnamed. + transform.rotateVect(vertex.Normal); + // Renormalize (length might have been affected by scaling). + vertex.Normal.normalize(); + } +} + +static void checkIndices(const std::vector &indices, const std::size_t nVerts) +{ + for (u16 index : indices) { + if (index >= nVerts) + throw std::runtime_error("index out of bounds"); + } +} + +static std::vector generateIndices(const std::size_t nVerts) +{ + std::vector indices(nVerts); + for (std::size_t i = 0; i < nVerts; i += 3) { + // Reverse winding order per triangle + indices[i] = i + 2; + indices[i + 1] = i + 1; + indices[i + 2] = i; + } + return indices; +} /** * Load up the rawest form of the model. The vertex positions and indices. * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes * If material is undefined, then a default material MUST be used. */ -void CGLTFMeshFileLoader::loadPrimitives( - const MeshExtractor& parser, - SMesh* mesh) +void CGLTFMeshFileLoader::MeshExtractor::loadMesh( + const std::size_t meshIdx, + ISkinnedMesh::SJoint *parent) const { - for (std::size_t i = 0; i < parser.getMeshCount(); ++i) { - for (std::size_t j = 0; j < parser.getPrimitiveCount(i); ++j) { - auto indices = parser.getIndices(i, j); - auto vertices = parser.getVertices(i, j); + for (std::size_t j = 0; j < getPrimitiveCount(meshIdx); ++j) { + auto vertices = getVertices(meshIdx, j); + if (!vertices.has_value()) + continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" - SMeshBuffer* meshbuf(new SMeshBuffer {}); - meshbuf->append(vertices.data(), vertices.size(), - indices.data(), indices.size()); - mesh->addMeshBuffer(meshbuf); - meshbuf->drop(); + // Excludes the max value for consistency. + if (vertices->size() >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); + + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); + + auto maybeIndices = getIndices(meshIdx, j); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } + + auto *meshbuf = m_irr_model->addMeshBuffer(); + meshbuf->append(vertices->data(), vertices->size(), + indices.data(), indices.size()); + } +} + +// Base transformation between left & right handed coordinate systems. +// This just inverts the Z axis. +static core::matrix4 leftToRight = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1 +); +static core::matrix4 rightToLeft = leftToRight; + +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) +{ + // Note: Under the hood, this casts these doubles to floats. + return core::matrix4( + m[0], m[1], m[2], m[3], + m[4], m[5], m[6], m[7], + m[8], m[9], m[10], m[11], + m[12], m[13], m[14], m[15]); +} + +static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) +{ + const auto &trans = trs.translation; + const auto &rot = trs.rotation; + const auto &scale = trs.scale; + core::matrix4 transMat; + transMat.setTranslation(core::vector3df(trans[0], trans[1], trans[2])); + core::matrix4 rotMat = core::quaternion(rot[0], rot[1], rot[2], rot[3]).getMatrix(); + core::matrix4 scaleMat; + scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); + return transMat * rotMat * scaleMat; +} + +static core::matrix4 loadTransform(std::optional> transform) { + if (!transform.has_value()) { + return core::matrix4(); + } + core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); + return rightToLeft * mat * leftToRight; +} + +void CGLTFMeshFileLoader::MeshExtractor::loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parent) const +{ + const auto &node = m_gltf_model.nodes->at(nodeIdx); + auto *joint = m_irr_model->addJoint(parent); + const core::matrix4 transform = loadTransform(node.transform); + joint->LocalMatrix = transform; + joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; + if (node.name.has_value()) { + joint->Name = node.name->c_str(); + } + if (node.mesh.has_value()) { + loadMesh(*node.mesh, joint); + } + if (node.children.has_value()) { + for (const auto &child : *node.children) { + loadNode(child, joint); } } } -CGLTFMeshFileLoader::MeshExtractor::MeshExtractor( - const tiniergltf::GlTF& model) noexcept - : m_model(model) -{ -} - -CGLTFMeshFileLoader::MeshExtractor::MeshExtractor( - const tiniergltf::GlTF&& model) noexcept - : m_model(model) +void CGLTFMeshFileLoader::MeshExtractor::loadNodes() const { + std::vector isChild(m_gltf_model.nodes->size()); + for (const auto &node : *m_gltf_model.nodes) { + if (!node.children.has_value()) + continue; + for (const auto &child : *node.children) { + isChild[child] = true; + } + } + // Load all nodes that aren't children. + // Children will be loaded by their parent nodes. + for (std::size_t i = 0; i < m_gltf_model.nodes->size(); ++i) { + if (!isChild[i]) { + loadNode(i, nullptr); + } + } } /** * Extracts GLTF mesh indices into the irrlicht model. */ -std::vector CGLTFMeshFileLoader::MeshExtractor::getIndices( +std::optional> CGLTFMeshFileLoader::MeshExtractor::getIndices( const std::size_t meshIdx, const std::size_t primitiveIdx) const { - // FIXME this need not exist. What do we do if it doesn't? const auto accessorIdx = getIndicesAccessorIdx(meshIdx, primitiveIdx); + if (!accessorIdx.has_value()) + return std::nullopt; // non-indexed geometry + const auto &accessor = m_gltf_model.accessors->at(accessorIdx.value()); const auto& buf = getBuffer(accessorIdx.value()); - // FIXME check accessor type (which could also be u8 or u32). std::vector indices{}; const auto count = getElemCount(accessorIdx.value()); for (std::size_t i = 0; i < count; ++i) { - std::size_t elemIdx = count - i - 1; - indices.push_back(readPrimitive( - BufferOffset(buf, elemIdx * sizeof(u16)))); + std::size_t elemIdx = count - i - 1; // reverse index order + u16 index; + // Note: glTF forbids the max value for each component type. + switch (accessor.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: { + index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u8))); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + break; + } + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: { + index = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u16))); + if (index == std::numeric_limits::max()) + throw std::runtime_error("invalid index"); + break; + } + case tiniergltf::Accessor::ComponentType::UNSIGNED_INT: { + u32 indexWide = readPrimitive(BufferOffset(buf, elemIdx * sizeof(u32))); + // Use >= here for consistency. + if (indexWide >= std::numeric_limits::max()) + throw std::runtime_error("index too large (>= 65536)"); + index = static_cast(indexWide); + break; + } + default: + throw std::runtime_error("invalid index component type"); + } + indices.push_back(index); } return indices; @@ -170,15 +314,20 @@ std::vector CGLTFMeshFileLoader::MeshExtractor::getIndices( /** * Create a vector of video::S3DVertex (model data) from a mesh & primitive index. */ -std::vector CGLTFMeshFileLoader::MeshExtractor::getVertices( +std::optional> CGLTFMeshFileLoader::MeshExtractor::getVertices( const std::size_t meshIdx, const std::size_t primitiveIdx) const { const auto positionAccessorIdx = getPositionAccessorIdx( meshIdx, primitiveIdx); + if (!positionAccessorIdx.has_value()) { + // "When positions are not specified, client implementations SHOULD skip primitive's rendering" + return std::nullopt; + } + std::vector vertices{}; - vertices.resize(getElemCount(positionAccessorIdx)); - copyPositions(positionAccessorIdx, vertices); + vertices.resize(getElemCount(*positionAccessorIdx)); + copyPositions(*positionAccessorIdx, vertices); const auto normalAccessorIdx = getNormalAccessorIdx( meshIdx, primitiveIdx); @@ -200,7 +349,7 @@ std::vector CGLTFMeshFileLoader::MeshExtractor::getVertices( */ std::size_t CGLTFMeshFileLoader::MeshExtractor::getMeshCount() const { - return m_model.meshes->size(); + return m_gltf_model.meshes->size(); } /** @@ -209,7 +358,7 @@ std::size_t CGLTFMeshFileLoader::MeshExtractor::getMeshCount() const std::size_t CGLTFMeshFileLoader::MeshExtractor::getPrimitiveCount( const std::size_t meshIdx) const { - return m_model.meshes->at(meshIdx).primitives.size(); + return m_gltf_model.meshes->at(meshIdx).primitives.size(); } /** @@ -246,6 +395,7 @@ core::vector2df CGLTFMeshFileLoader::MeshExtractor::readVec2DF( /** * Read a vector3df from a buffer at an offset. + * Also does right-to-left-handed coordinate system conversion (inverts Z axis). * @return vec3 core::Vector3df */ core::vector3df CGLTFMeshFileLoader::MeshExtractor::readVec3DF( @@ -253,9 +403,9 @@ core::vector3df CGLTFMeshFileLoader::MeshExtractor::readVec3DF( const core::vector3df scale = {1.0f,1.0f,1.0f}) { return core::vector3df( - scale.X * readPrimitive(readFrom), - scale.Y * readPrimitive(BufferOffset(readFrom, sizeof(float))), - -scale.Z * readPrimitive(BufferOffset(readFrom, 2 * + readPrimitive(readFrom), + readPrimitive(BufferOffset(readFrom, sizeof(float))), + -readPrimitive(BufferOffset(readFrom, 2 * sizeof(float)))); } @@ -273,8 +423,7 @@ void CGLTFMeshFileLoader::MeshExtractor::copyPositions( const auto byteStride = getByteStride(accessorIdx); for (std::size_t i = 0; i < count; i++) { - const auto v = readVec3DF(BufferOffset(buffer, - (byteStride * i)), getScale()); + const auto v = readVec3DF(BufferOffset(buffer, byteStride * i)); vertices[i].Pos = v; } } @@ -316,29 +465,6 @@ void CGLTFMeshFileLoader::MeshExtractor::copyTCoords( } } -/** - * Gets the scale of a model's node via a reference Vector3df. - * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node - * Type: number[3] (tinygltf: vector) - * Required: NO - * @returns: core::vector2df -*/ -core::vector3df CGLTFMeshFileLoader::MeshExtractor::getScale() const -{ - core::vector3df buffer{1.0f,1.0f,1.0f}; - // FIXME this just checks the first node - const auto &node = m_model.nodes->at(0); - // FIXME this does not take the matrix into account - // (fix: properly map glTF -> Irrlicht node hierarchy) - if (std::holds_alternative(node.transform)) { - const auto &trs = std::get(node.transform); - buffer.X = static_cast(trs.scale[0]); - buffer.Y = static_cast(trs.scale[1]); - buffer.Z = static_cast(trs.scale[2]); - } - return buffer; -} - /** * The number of elements referenced by this accessor, not to be confused with the number of bytes or number of components. * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_accessor_count @@ -348,7 +474,7 @@ core::vector3df CGLTFMeshFileLoader::MeshExtractor::getScale() const std::size_t CGLTFMeshFileLoader::MeshExtractor::getElemCount( const std::size_t accessorIdx) const { - return m_model.accessors->at(accessorIdx).count; + return m_gltf_model.accessors->at(accessorIdx).count; } /** @@ -361,9 +487,9 @@ std::size_t CGLTFMeshFileLoader::MeshExtractor::getElemCount( std::size_t CGLTFMeshFileLoader::MeshExtractor::getByteStride( const std::size_t accessorIdx) const { - const auto& accessor = m_model.accessors->at(accessorIdx); + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); // FIXME this does not work with sparse / zero-initialized accessors - const auto& view = m_model.bufferViews->at(accessor.bufferView.value()); + const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); return view.byteStride.value_or(accessor.elementSize()); } @@ -377,7 +503,7 @@ std::size_t CGLTFMeshFileLoader::MeshExtractor::getByteStride( bool CGLTFMeshFileLoader::MeshExtractor::isAccessorNormalized( const std::size_t accessorIdx) const { - const auto& accessor = m_model.accessors->at(accessorIdx); + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); return accessor.normalized; } @@ -388,10 +514,10 @@ bool CGLTFMeshFileLoader::MeshExtractor::isAccessorNormalized( CGLTFMeshFileLoader::BufferOffset CGLTFMeshFileLoader::MeshExtractor::getBuffer( const std::size_t accessorIdx) const { - const auto& accessor = m_model.accessors->at(accessorIdx); + const auto& accessor = m_gltf_model.accessors->at(accessorIdx); // FIXME this does not work with sparse / zero-initialized accessors - const auto& view = m_model.bufferViews->at(accessor.bufferView.value()); - const auto& buffer = m_model.buffers->at(view.buffer); + const auto& view = m_gltf_model.bufferViews->at(accessor.bufferView.value()); + const auto& buffer = m_gltf_model.buffers->at(view.buffer); return BufferOffset(buffer.data, view.byteOffset); } @@ -408,23 +534,19 @@ std::optional CGLTFMeshFileLoader::MeshExtractor::getIndicesAccesso const std::size_t meshIdx, const std::size_t primitiveIdx) const { - return m_model.meshes->at(meshIdx).primitives[primitiveIdx].indices; + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].indices; } /** * The index of the accessor that contains the POSITIONs. * Documentation: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview * Type: VEC3 (Float) - * ! Required: YES (Appears so, needs another pair of eyes to research.) - * Second pair of eyes says: "When positions are not specified, client implementations SHOULD skip primitive’s rendering" */ -std::size_t CGLTFMeshFileLoader::MeshExtractor::getPositionAccessorIdx( +std::optional CGLTFMeshFileLoader::MeshExtractor::getPositionAccessorIdx( const std::size_t meshIdx, const std::size_t primitiveIdx) const { - // FIXME position-less primitives should be skipped. - return m_model.meshes->at(meshIdx).primitives[primitiveIdx] - .attributes.position.value(); + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.position; } /** @@ -437,7 +559,7 @@ std::optional CGLTFMeshFileLoader::MeshExtractor::getNormalAccessor const std::size_t meshIdx, const std::size_t primitiveIdx) const { - return m_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.normal; + return m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.normal; } /** @@ -450,7 +572,7 @@ std::optional CGLTFMeshFileLoader::MeshExtractor::getTCoordAccessor const std::size_t meshIdx, const std::size_t primitiveIdx) const { - const auto& texcoords = m_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.texcoord; + const auto& texcoords = m_gltf_model.meshes->at(meshIdx).primitives[primitiveIdx].attributes.texcoord; if (!texcoords.has_value()) return std::nullopt; return texcoords->at(0); diff --git a/source/Irrlicht/CGLTFMeshFileLoader.h b/source/Irrlicht/CGLTFMeshFileLoader.h index 5fd49e4e..aabf1d24 100644 --- a/source/Irrlicht/CGLTFMeshFileLoader.h +++ b/source/Irrlicht/CGLTFMeshFileLoader.h @@ -1,13 +1,12 @@ #ifndef __C_GLTF_MESH_FILE_LOADER_INCLUDED__ #define __C_GLTF_MESH_FILE_LOADER_INCLUDED__ -#include "IAnimatedMesh.h" +#include "ISkinnedMesh.h" #include "IMeshLoader.h" #include "IReadFile.h" #include "irrTypes.h" #include "path.h" #include "S3DVertex.h" -#include "SMesh.h" #include "vector2d.h" #include "vector3d.h" @@ -52,26 +51,33 @@ private: public: using vertex_t = video::S3DVertex; - MeshExtractor(const tiniergltf::GlTF& model) noexcept; + MeshExtractor(const tiniergltf::GlTF &model, + ISkinnedMesh *mesh) noexcept + : m_gltf_model(model), m_irr_model(mesh) {}; - MeshExtractor(const tiniergltf::GlTF&& model) noexcept; + MeshExtractor(const tiniergltf::GlTF &&model, + ISkinnedMesh *mesh) noexcept + : m_gltf_model(model), m_irr_model(mesh) {}; /* Gets indices for the given mesh/primitive. * * Values are return in Irrlicht winding order. */ - std::vector getIndices(const std::size_t meshIdx, + std::optional> getIndices(const std::size_t meshIdx, const std::size_t primitiveIdx) const; - std::vector getVertices(std::size_t meshIdx, + std::optional> getVertices(std::size_t meshIdx, const std::size_t primitiveIdx) const; std::size_t getMeshCount() const; std::size_t getPrimitiveCount(const std::size_t meshIdx) const; + void loadNodes() const; + private: - tiniergltf::GlTF m_model; + const tiniergltf::GlTF m_gltf_model; + ISkinnedMesh *m_irr_model; template static T readPrimitive(const BufferOffset& readFrom); @@ -96,12 +102,6 @@ private: void copyTCoords(const std::size_t accessorIdx, std::vector& vertices) const; - /* Get the scale factor from the glTF mesh information. - * - * Returns vec3(1.0, 1.0, 1.0) if no scale factor is present. - */ - core::vector3df getScale() const; - std::size_t getElemCount(const std::size_t accessorIdx) const; std::size_t getByteStride(const std::size_t accessorIdx) const; @@ -113,7 +113,7 @@ private: std::optional getIndicesAccessorIdx(const std::size_t meshIdx, const std::size_t primitiveIdx) const; - std::size_t getPositionAccessorIdx(const std::size_t meshIdx, + std::optional getPositionAccessorIdx(const std::size_t meshIdx, const std::size_t primitiveIdx) const; /* Get the accessor id of the normals of a primitive. @@ -125,9 +125,15 @@ private: */ std::optional getTCoordAccessorIdx(const std::size_t meshIdx, const std::size_t primitiveIdx) const; - }; + + void loadMesh( + std::size_t meshIdx, + ISkinnedMesh::SJoint *parentJoint) const; - void loadPrimitives(const MeshExtractor& parser, SMesh* mesh); + void loadNode( + const std::size_t nodeIdx, + ISkinnedMesh::SJoint *parentJoint) const; + }; std::optional tryParseGLTF(io::IReadFile* file); }; diff --git a/source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf b/source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf new file mode 100644 index 00000000..aefd46fa --- /dev/null +++ b/source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf @@ -0,0 +1,106 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.7.33", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube", + "matrix" : [ + 1, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 3, 0, + 4, 5, 6, 1 + ] + } + ], + "meshes" : [ + { + "name" : "Cube.004", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + } + ], + "buffers" : [ + { + "byteLength" : 840, + "uri" : "data:application/octet-stream;base64,AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAADAPgAAgD8AAAA+AACAPgAAwD4AAAAAAAAgPwAAgD8AACA/AAAAAAAAYD8AAIA+AADAPgAAQD8AAAA+AAAAPwAAwD4AAEA/AAAgPwAAQD8AACA/AABAPwAAYD8AAAA/AADAPgAAgD4AAMA+AACAPgAAwD4AAIA+AAAgPwAAgD4AACA/AACAPgAAID8AAIA+AADAPgAAAD8AAMA+AAAAPwAAwD4AAAA/AAAgPwAAAD8AACA/AAAAPwAAID8AAAA/AAADAAkAAAAJAAYACAAKABUACAAVABMAFAAXABEAFAARAA4ADQAPAAQADQAEAAIABwASAAwABwAMAAEAFgALAAUAFgAFABAA" + } + ] +} diff --git a/source/Irrlicht/tests/assets/triangle_without_indices.gltf b/source/Irrlicht/tests/assets/triangle_without_indices.gltf new file mode 100644 index 00000000..0f798342 --- /dev/null +++ b/source/Irrlicht/tests/assets/triangle_without_indices.gltf @@ -0,0 +1,54 @@ +{ + "scene" : 0, + "scenes" : [ + { + "nodes" : [ 0 ] + } + ], + + "nodes" : [ + { + "mesh" : 0 + } + ], + + "meshes" : [ + { + "primitives" : [ { + "attributes" : { + "POSITION" : 0 + } + } ] + } + ], + + "buffers" : [ + { + "uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA", + "byteLength" : 36 + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteOffset" : 0, + "byteLength" : 36, + "target" : 34962 + } + ], + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 3, + "type" : "VEC3", + "max" : [ 1.0, 1.0, 0.0 ], + "min" : [ 0.0, 0.0, 0.0 ] + } + ], + + "asset" : { + "version" : "2.0" + } +} \ No newline at end of file diff --git a/source/Irrlicht/tests/testCGLTFMeshFileLoader.cpp b/source/Irrlicht/tests/testCGLTFMeshFileLoader.cpp index 888043b2..eb012906 100644 --- a/source/Irrlicht/tests/testCGLTFMeshFileLoader.cpp +++ b/source/Irrlicht/tests/testCGLTFMeshFileLoader.cpp @@ -1,4 +1,5 @@ #include "CReadFile.h" +#include "vector3d.h" #include @@ -54,8 +55,10 @@ TEST_CASE("load empty gltf file") { TEST_CASE("minimal triangle") { auto path = GENERATE( - "source/Irrlicht/tests/assets/minimal_triangle.gltf", - "source/Irrlicht/tests/assets/triangle_with_vertex_stride.gltf"); + "source/Irrlicht/tests/assets/minimal_triangle.gltf", + "source/Irrlicht/tests/assets/triangle_with_vertex_stride.gltf", + // Test non-indexed geometry. + "source/Irrlicht/tests/assets/triangle_without_indices.gltf"); INFO(path); ScopedMesh sm(path); REQUIRE(sm.getMesh() != nullptr); @@ -164,6 +167,32 @@ TEST_CASE("blender cube scaled") { } } +TEST_CASE("blender cube matrix transform") { + ScopedMesh sm("source/Irrlicht/tests/assets/blender_cube_matrix_transform.gltf"); + REQUIRE(sm.getMesh() != nullptr); + REQUIRE(sm.getMesh()->getMeshBufferCount() == 1); + + SECTION("Transformation is correct") { + REQUIRE(sm.getMesh()->getMeshBuffer(0)->getVertexCount() == 24); + const auto* vertices = reinterpret_cast( + sm.getMesh()->getMeshBuffer(0)->getVertices()); + const auto checkVertex = [&](const std::size_t i, irr::core::vector3df vec) { + // The transform scales by (1, 2, 3) and translates by (4, 5, 6). + CHECK(vertices[i].Pos == vec * irr::core::vector3df{1, 2, 3} + // The -6 is due to the coordinate system conversion. + + irr::core::vector3df{4, 5, -6}); + }; + checkVertex(0, irr::core::vector3df{-1, -1, -1}); + checkVertex(3, irr::core::vector3df{-1, 1, -1}); + checkVertex(6, irr::core::vector3df{-1, -1, 1}); + checkVertex(9, irr::core::vector3df{-1, 1, 1}); + checkVertex(12, irr::core::vector3df{1, -1, -1}); + checkVertex(15, irr::core::vector3df{1, 1, -1}); + checkVertex(18, irr::core::vector3df{1, -1, 1}); + checkVertex(21, irr::core::vector3df{1, 1, 1}); + } +} + TEST_CASE("snow man") { ScopedMesh sm("source/Irrlicht/tests/assets/snow_man.gltf"); REQUIRE(sm.getMesh() != nullptr);