diff --git a/.gitattributes b/.gitattributes index 06b76c6c8..ecd9a7a29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,5 @@ *.cpp diff=cpp *.h diff=cpp + +*.gltf binary diff --git a/doc/lua_api.md b/doc/lua_api.md index 6cb578810..0596c1e2f 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -294,7 +294,7 @@ depends on by supplying a file with an equal name. Only a subset of model file format features is supported: Simple textured meshes (with multiple textures), optionally with normals. -The .x and .b3d formats additionally support skeletal animation. +The .x, .b3d and .gltf formats additionally support (a single) animation. #### glTF @@ -307,7 +307,10 @@ due to their space savings. This means that many glTF features are not supported *yet*, including: -* Animation +* Animations + * Only a single animation is supported, + use frame ranges within this animation. + * Only integer frames are supported. * Cameras * Materials * Only base color textures are supported diff --git a/games/devtest/mods/gltf/LICENSE.md b/games/devtest/mods/gltf/LICENSE.md index b0ae5fef5..6c3828a4a 100644 --- a/games/devtest/mods/gltf/LICENSE.md +++ b/games/devtest/mods/gltf/LICENSE.md @@ -1,4 +1,4 @@ -glTF test model (and corresponding texture) licenses: +The glTF test models (and corresponding textures) in this mod are all licensed freely: * Spider (`gltf_spider.gltf`, `gltf_spider.png`): * By [archfan7411](https://github.com/archfan7411) diff --git a/games/devtest/mods/gltf/init.lua b/games/devtest/mods/gltf/init.lua index 294da9145..1a17ac05f 100644 --- a/games/devtest/mods/gltf/init.lua +++ b/games/devtest/mods/gltf/init.lua @@ -27,8 +27,34 @@ do }, }) end + register_entity("snow_man", {"gltf_snow_man.png"}) register_entity("spider", {"gltf_spider.png"}) + +minetest.register_entity("gltf:spider_animated", { + initial_properties = { + visual = "mesh", + mesh = "gltf_spider_animated.gltf", + textures = {"gltf_spider.png"}, + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 140}, 1) + end +}) + +minetest.register_entity("gltf:simple_skin", { + initial_properties = { + visual = "mesh", + visual_size = vector.new(5, 5, 5), + mesh = "gltf_simple_skin.gltf", + textures = {}, + backface_culling = false + }, + on_activate = function(self) + self.object:set_animation({x = 0, y = 5.5}, 1) + end +}) + -- Note: Model has an animation, but we can use it as a static test nevertheless -- The claws rendering incorrectly from one side is expected behavior: -- They use an unsupported double-sided material. diff --git a/games/devtest/mods/gltf/models/gltf_simple_skin.gltf b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf new file mode 100644 index 000000000..3d6c24a6c Binary files /dev/null and b/games/devtest/mods/gltf/models/gltf_simple_skin.gltf differ diff --git a/games/devtest/mods/gltf/models/gltf_spider_animated.gltf b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf new file mode 100644 index 000000000..79221b0c7 Binary files /dev/null and b/games/devtest/mods/gltf/models/gltf_spider_animated.gltf differ diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp index ab04fae8e..54d207e5f 100644 --- a/irr/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -6,9 +6,9 @@ #include "SMaterialLayer.h" #include "coreutil.h" #include "CSkinnedMesh.h" -#include "ISkinnedMesh.h" -#include "irrTypes.h" +#include "IAnimatedMesh.h" #include "IReadFile.h" +#include "irrTypes.h" #include "matrix4.h" #include "path.h" #include "quaternion.h" @@ -23,9 +23,11 @@ #include #include #include +#include #include #include #include +#include namespace irr { @@ -51,6 +53,28 @@ core::vector3df convertHandedness(const core::vector3df &p) return core::vector3df(p.X, p.Y, -p.Z); } +template <> +core::quaternion convertHandedness(const core::quaternion &q) +{ + return core::quaternion(q.X, q.Y, -q.Z, q.W); +} + +template <> +core::matrix4 convertHandedness(const core::matrix4 &mat) +{ + // Base transformation between left & right handed coordinate systems. + static const core::matrix4 invertZ = core::matrix4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, -1, 0, + 0, 0, 0, 1); + // Convert from left-handed to right-handed, + // then apply mat, + // then convert from right-handed to left-handed. + // Both conversions just invert Z. + return invertZ * mat * invertZ; +} + namespace scene { using SelfType = CGLTFMeshFileLoader; @@ -196,6 +220,8 @@ ACCESSOR_PRIMITIVE(u16, UNSIGNED_SHORT) ACCESSOR_PRIMITIVE(u32, UNSIGNED_INT) ACCESSOR_TYPES(core::vector3df, VEC3, FLOAT) +ACCESSOR_TYPES(core::quaternion, VEC4, FLOAT) +ACCESSOR_TYPES(core::matrix4, MAT4, FLOAT) template T SelfType::Accessor::get(std::size_t i) const @@ -340,7 +366,7 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) auto *mesh = new CSkinnedMesh(); MeshExtractor parser(std::move(model.value()), mesh); try { - parser.loadNodes(); + parser.load(); } catch (std::runtime_error &e) { os::Printer::log("glTF loader", e.what(), ELL_ERROR); mesh->drop(); @@ -397,61 +423,134 @@ static video::E_TEXTURE_CLAMP convertTextureWrap(const Wrap wrap) { } } -/** - * 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 SelfType::MeshExtractor::loadMesh( - const std::size_t meshIdx, - ISkinnedMesh::SJoint *parent) const +void SelfType::MeshExtractor::addPrimitive( + const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) { - for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { - const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); - auto vertices = getVertices(primitive); - if (!vertices.has_value()) - continue; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" + auto vertices = getVertices(primitive); + if (!vertices.has_value()) + return; // "When positions are not specified, client implementations SHOULD skip primitive’s rendering" - // Excludes the max value for consistency. - if (vertices->size() >= std::numeric_limits::max()) - throw std::runtime_error("too many vertices"); + const auto n_vertices = vertices->size(); - // Apply the global transform along the parent chain. - transformVertices(*vertices, parent->GlobalMatrix); + // Excludes the max value for consistency. + if (n_vertices >= std::numeric_limits::max()) + throw std::runtime_error("too many vertices"); - auto maybeIndices = getIndices(primitive); - std::vector indices; - if (maybeIndices.has_value()) { - indices = std::move(*maybeIndices); - checkIndices(indices, vertices->size()); - } else { - // Non-indexed geometry - indices = generateIndices(vertices->size()); - } + // Apply the global transform along the parent chain. + transformVertices(*vertices, parent->GlobalMatrix); - m_irr_model->addMeshBuffer( - new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); - auto *meshbuf = m_irr_model->getMeshBuffer(m_irr_model->getMeshBufferCount() - 1); - auto &irr_mat = meshbuf->getMaterial(); + auto maybeIndices = getIndices(primitive); + std::vector indices; + if (maybeIndices.has_value()) { + indices = std::move(*maybeIndices); + checkIndices(indices, vertices->size()); + } else { + // Non-indexed geometry + indices = generateIndices(vertices->size()); + } - if (primitive.material.has_value()) { - const auto &material = m_gltf_model.materials->at(*primitive.material); - if (material.pbrMetallicRoughness.has_value()) { - const auto &texture = material.pbrMetallicRoughness->baseColorTexture; - if (texture.has_value()) { - const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; - m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); - const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler; - if (samplerIdx.has_value()) { - auto &sampler = m_gltf_model.samplers->at(*samplerIdx); - auto &layer = irr_mat.TextureLayers[0]; - layer.TextureWrapU = convertTextureWrap(sampler.wrapS); - layer.TextureWrapV = convertTextureWrap(sampler.wrapT); - } + m_irr_model->addMeshBuffer( + new SSkinMeshBuffer(std::move(*vertices), std::move(indices))); + const auto meshbufNr = m_irr_model->getMeshBufferCount() - 1; + auto *meshbuf = m_irr_model->getMeshBuffer(meshbufNr); + + if (primitive.material.has_value()) { + const auto &material = m_gltf_model.materials->at(*primitive.material); + if (material.pbrMetallicRoughness.has_value()) { + const auto &texture = material.pbrMetallicRoughness->baseColorTexture; + if (texture.has_value()) { + m_irr_model->setTextureSlot(meshbufNr, static_cast(texture->index)); + const auto samplerIdx = m_gltf_model.textures->at(texture->index).sampler; + if (samplerIdx.has_value()) { + auto &sampler = m_gltf_model.samplers->at(*samplerIdx); + auto &layer = meshbuf->getMaterial().TextureLayers[0]; + layer.TextureWrapU = convertTextureWrap(sampler.wrapS); + layer.TextureWrapV = convertTextureWrap(sampler.wrapT); } } } } + + if (!skinIdx.has_value()) { + // No skin => all vertices belong entirely to their parent + for (std::size_t v = 0; v < n_vertices; ++v) { + auto *weight = m_irr_model->addWeight(parent); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = 1.0f; + } + return; + } + + const auto &skin = m_gltf_model.skins->at(*skinIdx); + + const auto &attrs = primitive.attributes; + const auto &joints = attrs.joints; + if (!joints.has_value()) + return; + + const auto &weights = attrs.weights; + for (std::size_t set = 0; set < joints->size(); ++set) { + const auto jointAccessor = ([&]() -> ArrayAccessorVariant<4, u8, u16> { + const auto idx = joints->at(set); + const auto &acc = m_gltf_model.accessors->at(idx); + + switch (acc.componentType) { + case tiniergltf::Accessor::ComponentType::UNSIGNED_BYTE: + return Accessor>::make(m_gltf_model, idx); + case tiniergltf::Accessor::ComponentType::UNSIGNED_SHORT: + return Accessor>::make(m_gltf_model, idx); + default: + throw std::runtime_error("invalid component type"); + } + })(); + + const auto weightAccessor = createNormalizedValuesAccessor<4>(m_gltf_model, weights->at(set)); + + for (std::size_t v = 0; v < n_vertices; ++v) { + std::array jointIdxs; + if (std::holds_alternative>>(jointAccessor)) { + const auto jointIdxsU8 = std::get>>(jointAccessor).get(v); + jointIdxs = {jointIdxsU8[0], jointIdxsU8[1], jointIdxsU8[2], jointIdxsU8[3]}; + } else if (std::holds_alternative>>(jointAccessor)) { + jointIdxs = std::get>>(jointAccessor).get(v); + } + std::array strengths = getNormalizedValues(weightAccessor, v); + + // 4 joints per set + for (std::size_t in_set = 0; in_set < 4; ++in_set) { + u16 jointIdx = jointIdxs[in_set]; + f32 strength = strengths[in_set]; + if (strength == 0) + continue; + + CSkinnedMesh::SWeight *weight = m_irr_model->addWeight(m_loaded_nodes.at(skin.joints.at(jointIdx))); + weight->buffer_id = meshbufNr; + weight->vertex_id = v; + weight->strength = strength; + } + } + } +} + +/** + * 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 SelfType::MeshExtractor::deferAddMesh( + const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent) +{ + m_mesh_loaders.emplace_back([=] { + for (std::size_t pi = 0; pi < getPrimitiveCount(meshIdx); ++pi) { + const auto &primitive = m_gltf_model.meshes->at(meshIdx).primitives.at(pi); + addPrimitive(primitive, skinIdx, parent); + } + }); } // Base transformation between left & right handed coordinate systems. @@ -464,51 +563,75 @@ static const core::matrix4 leftToRight = core::matrix4( ); static const core::matrix4 rightToLeft = leftToRight; -static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m) +static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, CSkinnedMesh::SJoint *joint) { // Note: Under the hood, this casts these doubles to floats. - return core::matrix4( + core::matrix4 mat = convertHandedness(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]); + m[12], m[13], m[14], m[15])); + + // Decompose the matrix into translation, scale, and rotation. + joint->Animatedposition = mat.getTranslation(); + + auto scale = mat.getScale(); + joint->Animatedscale = scale; + core::matrix4 inverseScale; + inverseScale.setScale(core::vector3df( + scale.X == 0 ? 0 : 1 / scale.X, + scale.Y == 0 ? 0 : 1 / scale.Y, + scale.Z == 0 ? 0 : 1 / scale.Z)); + + core::matrix4 axisNormalizedMat = inverseScale * mat; + joint->Animatedrotation = axisNormalizedMat.getRotationDegrees(); + // Invert the rotation because it is applied using `getMatrix_transposed`, + // which again inverts. + joint->Animatedrotation.makeInverse(); + + return mat; } -static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs) +static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, CSkinnedMesh::SJoint *joint) { 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(); + joint->Animatedposition = convertHandedness(core::vector3df(trans[0], trans[1], trans[2])); + transMat.setTranslation(joint->Animatedposition); + core::matrix4 rotMat; + joint->Animatedrotation = convertHandedness(core::quaternion(rot[0], rot[1], rot[2], rot[3])); + core::quaternion(joint->Animatedrotation).getMatrix_transposed(rotMat); + joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]); core::matrix4 scaleMat; - scaleMat.setScale(core::vector3df(scale[0], scale[1], scale[2])); + scaleMat.setScale(joint->Animatedscale); return transMat * rotMat * scaleMat; } -static core::matrix4 loadTransform(std::optional> transform) { +static core::matrix4 loadTransform(std::optional> transform, + CSkinnedMesh::SJoint *joint) { if (!transform.has_value()) { return core::matrix4(); } - core::matrix4 mat = std::visit([](const auto &t) { return loadTransform(t); }, *transform); - return rightToLeft * mat * leftToRight; + return std::visit([joint](const auto &t) { return loadTransform(t, joint); }, *transform); } void SelfType::MeshExtractor::loadNode( const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parent) const + CSkinnedMesh::SJoint *parent) { const auto &node = m_gltf_model.nodes->at(nodeIdx); auto *joint = m_irr_model->addJoint(parent); - const core::matrix4 transform = loadTransform(node.transform); + const core::matrix4 transform = loadTransform(node.transform, joint); joint->LocalMatrix = transform; joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix; if (node.name.has_value()) { joint->Name = node.name->c_str(); } + m_loaded_nodes[nodeIdx] = joint; if (node.mesh.has_value()) { - loadMesh(*node.mesh, joint); + deferAddMesh(*node.mesh, node.skin, joint); } if (node.children.has_value()) { for (const auto &child : *node.children) { @@ -517,8 +640,10 @@ void SelfType::MeshExtractor::loadNode( } } -void SelfType::MeshExtractor::loadNodes() const +void SelfType::MeshExtractor::loadNodes() { + m_loaded_nodes = std::vector(m_gltf_model.nodes->size()); + std::vector isChild(m_gltf_model.nodes->size()); for (const auto &node : *m_gltf_model.nodes) { if (!node.children.has_value()) @@ -536,6 +661,92 @@ void SelfType::MeshExtractor::loadNodes() const } } +void SelfType::MeshExtractor::loadSkins() +{ + if (!m_gltf_model.skins.has_value()) + return; + + for (const auto &skin : *m_gltf_model.skins) { + if (!skin.inverseBindMatrices.has_value()) + continue; + const auto accessor = Accessor::make(m_gltf_model, *skin.inverseBindMatrices); + if (accessor.getCount() < skin.joints.size()) + throw std::runtime_error("accessor contains too few matrices"); + for (std::size_t i = 0; i < skin.joints.size(); ++i) { + m_loaded_nodes.at(skin.joints[i])->GlobalInversedMatrix = convertHandedness(accessor.get(i)); + } + } +} + +void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx) +{ + const auto &anim = m_gltf_model.animations->at(animIdx); + for (const auto &channel : anim.channels) { + + const auto &sampler = anim.samplers.at(channel.sampler); + if (sampler.interpolation != tiniergltf::AnimationSampler::Interpolation::LINEAR) + throw std::runtime_error("unsupported interpolation"); + + const auto inputAccessor = Accessor::make(m_gltf_model, sampler.input); + const auto n_frames = inputAccessor.getCount(); + + if (!channel.target.node.has_value()) + throw std::runtime_error("no animated node"); + + const auto &joint = m_loaded_nodes.at(*channel.target.node); + switch (channel.target.path) { + case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addPositionKey(joint); + key->frame = inputAccessor.get(i); + key->position = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::ROTATION: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addRotationKey(joint); + key->frame = inputAccessor.get(i); + key->rotation = convertHandedness(outputAccessor.get(i)); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::SCALE: { + const auto outputAccessor = Accessor::make(m_gltf_model, sampler.output); + for (std::size_t i = 0; i < n_frames; ++i) { + auto *key = m_irr_model->addScaleKey(joint); + key->frame = inputAccessor.get(i); + key->scale = outputAccessor.get(i); + } + break; + } + case tiniergltf::AnimationChannelTarget::Path::WEIGHTS: + throw std::runtime_error("no support for morph animations"); + } + } +} + +void SelfType::MeshExtractor::load() +{ + loadNodes(); + for (const auto &load_mesh : m_mesh_loaders) { + load_mesh(); + } + loadSkins(); + // Load the first animation, if there is one. + if (m_gltf_model.animations.has_value()) { + if (m_gltf_model.animations->size() > 1) { + os::Printer::log("glTF loader", + "multiple animations are not supported", ELL_WARNING); + } + loadAnimation(0); + m_irr_model->setAnimationSpeed(1); + } + m_irr_model->finalize(); +} + /** * Extracts GLTF mesh indices. */ @@ -722,4 +933,3 @@ std::optional SelfType::tryParseGLTF(io::IReadFile* file) } // namespace scene } // namespace irr - diff --git a/irr/src/CGLTFMeshFileLoader.h b/irr/src/CGLTFMeshFileLoader.h index da306769e..7674fd46a 100644 --- a/irr/src/CGLTFMeshFileLoader.h +++ b/irr/src/CGLTFMeshFileLoader.h @@ -10,9 +10,11 @@ #include "path.h" #include "S3DVertex.h" -#include +#include "tiniergltf.hpp" +#include #include +#include #include namespace irr @@ -26,9 +28,9 @@ class CGLTFMeshFileLoader : public IMeshLoader public: CGLTFMeshFileLoader() noexcept {}; - bool isALoadableFileExtension(const io::path& filename) const override; + bool isALoadableFileExtension(const io::path &filename) const override; - IAnimatedMesh* createMesh(io::IReadFile* file) override; + IAnimatedMesh *createMesh(io::IReadFile *file) override; private: template @@ -94,7 +96,8 @@ class CGLTFMeshFileLoader : public IMeshLoader const NormalizedValuesAccessor &accessor, const std::size_t i); - class MeshExtractor { + class MeshExtractor + { public: MeshExtractor(tiniergltf::GlTF &&model, CSkinnedMesh *mesh) noexcept @@ -114,12 +117,15 @@ class CGLTFMeshFileLoader : public IMeshLoader std::size_t getPrimitiveCount(const std::size_t meshIdx) const; - void loadNodes() const; + void load(); private: const tiniergltf::GlTF m_gltf_model; CSkinnedMesh *m_irr_model; + std::vector> m_mesh_loaders; + std::vector m_loaded_nodes; + void copyPositions(const std::size_t accessorIdx, std::vector& vertices) const; @@ -129,16 +135,24 @@ class CGLTFMeshFileLoader : public IMeshLoader void copyTCoords(const std::size_t accessorIdx, std::vector& vertices) const; - void loadMesh( - std::size_t meshIdx, - ISkinnedMesh::SJoint *parentJoint) const; + void addPrimitive(const tiniergltf::MeshPrimitive &primitive, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parent); - void loadNode( - const std::size_t nodeIdx, - ISkinnedMesh::SJoint *parentJoint) const; + void deferAddMesh(const std::size_t meshIdx, + const std::optional skinIdx, + CSkinnedMesh::SJoint *parentJoint); + + void loadNode(const std::size_t nodeIdx, CSkinnedMesh::SJoint *parentJoint); + + void loadNodes(); + + void loadSkins(); + + void loadAnimation(const std::size_t animIdx); }; - std::optional tryParseGLTF(io::IReadFile* file); + std::optional tryParseGLTF(io::IReadFile *file); }; } // namespace scene diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp index 99cb4b1b5..674f3c0dd 100644 --- a/src/unittest/test_irr_gltf_mesh_loader.cpp +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -8,6 +8,7 @@ #include "irr_v2d.h" #include "irr_ptr.h" +#include "ISkinnedMesh.h" #include #include "catch.h" @@ -371,7 +372,91 @@ SECTION("simple sparse accessor") CHECK(vertices[i].Pos == expectedPositions[i]); } +// https://github.com/KhronosGroup/glTF-Sample-Models/tree/main/2.0/SimpleSkin +SECTION("simple skin") +{ + using ISkinnedMesh = irr::scene::ISkinnedMesh; + const auto mesh = loadMesh(model_stem + "simple_skin.gltf"); + REQUIRE(mesh != nullptr); + auto csm = dynamic_cast(mesh); + const auto joints = csm->getAllJoints(); + REQUIRE(joints.size() == 3); + + const auto findJoint = [&](const std::function &predicate) { + for (std::size_t i = 0; i < joints.size(); ++i) { + if (predicate(joints[i])) { + return joints[i]; + } + } + throw std::runtime_error("joint not found"); + }; + + // Check the node hierarchy + const auto parent = findJoint([](auto joint) { + return !joint->Children.empty(); + }); + REQUIRE(parent->Children.size() == 1); + const auto child = parent->Children[0]; + REQUIRE(child != parent); + + SECTION("transformations are correct") + { + CHECK(parent->Animatedposition == v3f(0, 0, 0)); + CHECK(parent->Animatedrotation == irr::core::quaternion()); + CHECK(parent->Animatedscale == v3f(1, 1, 1)); + CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); + const v3f childTranslation(0, 1, 0); + CHECK(child->Animatedposition == childTranslation); + CHECK(child->Animatedrotation == irr::core::quaternion()); + CHECK(child->Animatedscale == v3f(1, 1, 1)); + irr::core::matrix4 inverseBindMatrix; + inverseBindMatrix.setInverseTranslation(childTranslation); + CHECK(child->GlobalInversedMatrix == inverseBindMatrix); + } + + SECTION("weights are correct") + { + const auto weights = [&](const ISkinnedMesh::SJoint *joint) { + std::unordered_map weights; + for (std::size_t i = 0; i < joint->Weights.size(); ++i) { + const auto weight = joint->Weights[i]; + REQUIRE(weight.buffer_id == 0); + weights[weight.vertex_id] = weight.strength; + } + return weights; + }; + const auto parentWeights = weights(parent); + const auto childWeights = weights(child); + + const auto checkWeights = [&](irr::u32 index, irr::f32 parentWeight, irr::f32 childWeight) { + const auto getWeight = [](auto weights, auto index) { + const auto it = weights.find(index); + return it == weights.end() ? 0.0f : it->second; + }; + CHECK(getWeight(parentWeights, index) == parentWeight); + CHECK(getWeight(childWeights, index) == childWeight); + }; + checkWeights(0, 1.00, 0.00); + checkWeights(1, 1.00, 0.00); + checkWeights(2, 0.75, 0.25); + checkWeights(3, 0.75, 0.25); + checkWeights(4, 0.50, 0.50); + checkWeights(5, 0.50, 0.50); + checkWeights(6, 0.25, 0.75); + checkWeights(7, 0.25, 0.75); + checkWeights(8, 0.00, 1.00); + checkWeights(9, 0.00, 1.00); + } + + SECTION("there should be a third node not involved in skinning") + { + const auto other = findJoint([&](auto joint) { + return joint != child && joint != parent; + }); + CHECK(other->Weights.empty()); + } +} + driver->closeDevice(); driver->drop(); - }