diff --git a/irr/include/IBoneSceneNode.h b/irr/include/IBoneSceneNode.h index bd8efaa89..d169b8d30 100644 --- a/irr/include/IBoneSceneNode.h +++ b/irr/include/IBoneSceneNode.h @@ -16,17 +16,32 @@ namespace scene class IBoneSceneNode : public ISceneNode { public: - IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1) : - ISceneNode(parent, mgr, id) {} + IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, + s32 id = -1, u32 boneIndex = 0, + const std::optional &boneName = std::nullopt) + : + ISceneNode(parent, mgr, id), + BoneIndex(boneIndex) + { + setName(boneName); + } - //! Get the index of the bone - virtual u32 getBoneIndex() const = 0; + //! Returns the index of the bone + u32 getBoneIndex() const + { + return BoneIndex; + } - //! Get the axis aligned bounding box of this node - const core::aabbox3d &getBoundingBox() const override = 0; + //! returns the axis aligned bounding box of this node + const core::aabbox3d &getBoundingBox() const override + { + return Box; + } - //! Returns the relative transformation of the scene node. - // virtual core::matrix4 getRelativeTransformation() const = 0; + const u32 BoneIndex; + + // Bogus box; bone scene nodes are not rendered anyways. + static constexpr core::aabbox3d Box = {{0, 0, 0}}; //! The render method. /** Does nothing as bones are not visible. */ diff --git a/irr/include/SkinnedMesh.h b/irr/include/SkinnedMesh.h index 5c125999a..98cefc858 100644 --- a/irr/include/SkinnedMesh.h +++ b/irr/include/SkinnedMesh.h @@ -8,14 +8,17 @@ #include "ISceneManager.h" #include "SMeshBuffer.h" #include "SSkinMeshBuffer.h" +#include "aabbox3d.h" #include "irrMath.h" #include "matrix4.h" #include "quaternion.h" #include "vector3d.h" +#include "Transform.h" #include #include #include +#include namespace irr { @@ -40,9 +43,8 @@ public: //! constructor SkinnedMesh(SourceFormat src_format) : EndFrame(0.f), FramesPerSecond(25.f), - LastAnimatedFrame(-1), SkinnedLastFrame(false), HasAnimation(false), PreparedForSkinning(false), - AnimateNormals(true), HardwareSkinning(false), + AnimateNormals(true), SrcFormat(src_format) { SkinningBuffers = &LocalBuffers; @@ -67,14 +69,16 @@ public: The actual speed is set in the scene node the mesh is instantiated in.*/ void setAnimationSpeed(f32 fps) override; - //! returns the animated mesh for the given frame - IMesh *getMesh(f32) override; + //! **Must not be called**. + //! TODO refactor Irrlicht so that we need not implement this. + IMesh *getMesh(f32) override { assert(false); }; - //! Animates joints based on frame input - void animateMesh(f32 frame); + //! Turns the given array of local matrices into an array of global matrices + //! by multiplying with respective parent matrices. + void calculateGlobalMatrices(std::vector &matrices); - //! Performs a software skin on this mesh based of joint positions - void skinMesh(); + //! Performs a software skin on this mesh based on the given joint matrices + void skinMesh(const std::vector &animated_transforms); //! returns amount of mesh buffers. u32 getMeshBufferCount() const override; @@ -143,10 +147,6 @@ public: return !HasAnimation; } - //! Allows to enable hardware skinning. - /* This feature is not implemented in Irrlicht yet */ - bool setHardwareSkinning(bool on); - //! Refreshes vertex data cached in joints such as positions and normals void refreshJointCache(); @@ -155,16 +155,9 @@ public: void updateBoundingBox(); - //! Recovers the joints from the mesh - void recoverJointsFromMesh(std::vector &jointChildSceneNodes); - - //! Transfers the joint data to the mesh - void transferJointsToMesh(const std::vector &jointChildSceneNodes); - //! Creates an array of joints from this mesh as children of node - void addJoints(std::vector &jointChildSceneNodes, - IAnimatedMeshSceneNode *node, - ISceneManager *smgr); + std::vector addJoints( + IAnimatedMeshSceneNode *node, ISceneManager *smgr); //! A vertex weight struct SWeight @@ -291,15 +284,14 @@ public: }); } - void updateTransform(f32 frame, - core::vector3df &t, core::quaternion &r, core::vector3df &s) const + void updateTransform(f32 frame, core::Transform &transform) const { if (auto pos = position.get(frame)) - t = *pos; + transform.translation = *pos; if (auto rot = rotation.get(frame)) - r = *rot; + transform.rotation = *rot; if (auto scl = scale.get(frame)) - s = *scl; + transform.scale = *scl; } void cleanup() { @@ -317,55 +309,24 @@ public: //! The name of this joint std::optional Name; - struct Transform { - core::vector3df translation; - core::quaternion rotation; - core::vector3df scale{1}; - - core::matrix4 buildMatrix() const { - core::matrix4 T; - T.setTranslation(translation); - core::matrix4 R; - rotation.getMatrix_transposed(R); - core::matrix4 S; - S.setScale(scale); - return T * R * S; - } - }; - //! Local transformation to be set by loaders. Mutated by animation. //! If a matrix is used, this joint **must not** be animated, //! because then the unique decomposition into translation, rotation and scale need not exist! - std::variant transform = Transform{}; + using VariantTransform = std::variant; + VariantTransform transform{core::Transform{}}; - Transform &getAnimatableTransform() { - if (std::holds_alternative(transform)) - return std::get(transform); - const auto &mat = std::get(transform); - Transform trs; - trs.translation = mat.getTranslation(); - trs.scale = mat.getScale(); - trs.rotation = core::quaternion( - mat.getRotationDegrees(trs.scale) * core::DEGTORAD); - transform = trs; - // TODO raise a warning if the recomposed matrix does not equal the decomposed. - return std::get(transform); - } - - void animate(f32 frame) { + VariantTransform animate(f32 frame) const { if (keys.empty()) - return; - auto &transform = getAnimatableTransform(); - keys.updateTransform(frame, - transform.translation, - transform.rotation, - transform.scale); - } + return transform; - core::matrix4 buildLocalMatrix() const { - if (std::holds_alternative(transform)) - return std::get(transform); - return std::get(transform).buildMatrix(); + if (std::holds_alternative(transform)) { + // TODO raise a warning: Attempt to animate a static joint. + return transform; + } + + auto trs = std::get(transform); + keys.updateTransform(frame, trs); + return {trs}; } //! List of child joints @@ -382,33 +343,35 @@ public: //! Unnecessary for loaders, will be overwritten on finalize core::matrix4 GlobalMatrix; // loaders may still choose to set this (temporarily) to calculate absolute vertex data. - core::matrix4 GlobalAnimatedMatrix; - core::matrix4 LocalAnimatedMatrix; // The .x and .gltf formats pre-calculate this std::optional GlobalInversedMatrix; - private: - //! Internal members used by SkinnedMesh - friend class SkinnedMesh; + + // TODO friends? + u16 JointID; // TODO refactor away: pointers -> IDs + std::optional ParentJointID; }; + //! Animates joints based on frame input + std::vector animateMesh(f32 frame); + + //! TODO + core::aabbox3df calculateBoundingBox( + const std::vector &transforms); + const std::vector &getAllJoints() const { return AllJoints; } protected: - void checkForAnimation(); + bool checkForAnimation() const; + + void topoSortJoints(); + + void prepareForSkinning(); void normalizeWeights(); - void buildAllLocalAnimatedMatrices(); - - void buildAllGlobalAnimatedMatrices(SJoint *Joint = 0, SJoint *ParentJoint = 0); - - void calculateGlobalMatrices(SJoint *Joint, SJoint *ParentJoint); - - void skinJoint(SJoint *Joint, SJoint *ParentJoint); - void calculateTangents(core::vector3df &normal, core::vector3df &tangent, core::vector3df &binormal, const core::vector3df &vt1, const core::vector3df &vt2, const core::vector3df &vt3, @@ -420,8 +383,8 @@ protected: //! Mapping from meshbuffer number to bindable texture slot std::vector TextureSlots; + //! Joints, topologically sorted (parents come before their children). std::vector AllJoints; - std::vector RootJoints; // bool can't be used here because std::vector // doesn't allow taking a reference to individual elements. @@ -432,15 +395,17 @@ protected: f32 EndFrame; f32 FramesPerSecond; - f32 LastAnimatedFrame; - bool SkinnedLastFrame; - bool HasAnimation; bool PreparedForSkinning; bool AnimateNormals; +<<<<<<< HEAD bool HardwareSkinning; SourceFormat SrcFormat; +||||||| parent of 44f1cd940 (progress) + bool HardwareSkinning; +======= +>>>>>>> 44f1cd940 (progress) }; // Interface for mesh loaders diff --git a/irr/include/Transform.h b/irr/include/Transform.h new file mode 100644 index 000000000..dbc8294e6 --- /dev/null +++ b/irr/include/Transform.h @@ -0,0 +1,44 @@ +#pragma once + +#include "irrMath.h" +#include +#include +#include +#include + +namespace irr +{ +namespace core +{ + +struct Transform { + vector3df translation; + quaternion rotation; + vector3df scale{1}; + + // Tries to decompose the matrix, if there is one. + static Transform decompose(const core::matrix4 &mat) + { + auto scale = mat.getScale(); + return { + mat.getTranslation(), + quaternion(mat.getRotationDegrees(scale) * DEGTORAD), + scale, + }; + } + + matrix4 buildMatrix() const + { + matrix4 T; + T.setTranslation(translation); + matrix4 R; + // TODO this is sussy. probably shouldn't be doing this. + rotation.getMatrix_transposed(R); + matrix4 S; + S.setScale(scale); + return T * R * S; + } +}; + +} // end namespace core +} // end namespace irr \ No newline at end of file diff --git a/irr/src/CAnimatedMeshSceneNode.cpp b/irr/src/CAnimatedMeshSceneNode.cpp index c8eb0eabd..326781a36 100644 --- a/irr/src/CAnimatedMeshSceneNode.cpp +++ b/irr/src/CAnimatedMeshSceneNode.cpp @@ -3,9 +3,13 @@ // For conditions of distribution and use, see copyright notice in irrlicht.h #include "CAnimatedMeshSceneNode.h" +#include "CBoneSceneNode.h" #include "IVideoDriver.h" #include "ISceneManager.h" +#include "MatrixBoneSceneNode.h" #include "S3DVertex.h" +#include "Transform.h" +#include "matrix4.h" #include "os.h" #include "SkinnedMesh.h" #include "IDummyTransformationSceneNode.h" @@ -17,6 +21,7 @@ #include "IFileSystem.h" #include "quaternion.h" #include +#include namespace irr { @@ -30,7 +35,7 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh, const core::vector3df &rotation, const core::vector3df &scale) : IAnimatedMeshSceneNode(parent, mgr, id, position, rotation, scale), - Mesh(0), + Mesh(nullptr), StartFrame(0), EndFrame(0), FramesPerSecond(0.025f), CurrentFrameNr(0.f), LastTimeMs(0), TransitionTime(0), Transiting(0.f), TransitingBlend(0.f), @@ -143,25 +148,27 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode() IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame() { - if (Mesh->getMeshType() != EAMT_SKINNED) { + if (Mesh->getMeshType() != EAMT_SKINNED) return Mesh->getMesh(getFrameNr()); - } else { - // As multiple scene nodes may be sharing the same skinned mesh, we have to - // re-animate it every frame to ensure that this node gets the mesh that it needs. - SkinnedMesh *skinnedMesh = static_cast(Mesh); + // As multiple scene nodes may be sharing the same skinned mesh, we have to + // re-animate it every frame to ensure that this node gets the mesh that it needs. - skinnedMesh->transferJointsToMesh(JointChildSceneNodes); + auto *skinnedMesh = static_cast(Mesh); - // Update the skinned mesh for the current joint transforms. - skinnedMesh->skinMesh(); + std::vector matrices; + matrices.reserve(JointChildSceneNodes.size()); + for (auto *node : JointChildSceneNodes) + matrices.push_back(node->getRelativeTransformation()); + skinnedMesh->calculateGlobalMatrices(matrices); - skinnedMesh->updateBoundingBox(); + skinnedMesh->skinMesh(matrices); - Box = skinnedMesh->getBoundingBox(); + skinnedMesh->updateBoundingBox(); - return skinnedMesh; - } + Box = skinnedMesh->getBoundingBox(); + + return skinnedMesh; } //! OnAnimate() is called just before rendering the whole scene. @@ -268,13 +275,13 @@ void CAnimatedMeshSceneNode::render() if (Mesh->getMeshType() == EAMT_SKINNED) { // draw skeleton - for (auto *joint : ((SkinnedMesh *)Mesh)->getAllJoints()) { + /*for (auto *joint : ((SkinnedMesh *)Mesh)->getAllJoints()) { for (const auto *childJoint : joint->Children) { driver->draw3DLine(joint->GlobalAnimatedMatrix.getTranslation(), childJoint->GlobalAnimatedMatrix.getTranslation(), video::SColor(255, 51, 66, 255)); } - } + }*/ } } @@ -526,6 +533,42 @@ void CAnimatedMeshSceneNode::setRenderFromIdentity(bool enable) RenderFromIdentity = enable; } +void CAnimatedMeshSceneNode::addJoints() +{ + const auto &joints = static_cast(Mesh)->getAllJoints(); + JointChildSceneNodes.clear(); + JointChildSceneNodes.reserve(joints.size()); + for (size_t i = 0; i < joints.size(); ++i) { + const auto *joint = joints[i]; + ISceneNode *parent = this; + if (joint->ParentJointID) + parent = JointChildSceneNodes.at(*joint->ParentJointID); // exists because of topo. order + assert(parent); + if (const auto *matrix = std::get_if(&joint->transform)) { + JointChildSceneNodes.push_back(new MatrixBoneSceneNode( + parent, SceneManager, 0, i, joint->Name, *matrix)); + } else { + JointChildSceneNodes.push_back(new CBoneSceneNode( + parent, SceneManager, 0, i, joint->Name, + std::get(joint->transform))); + } + } +} + +void CAnimatedMeshSceneNode::updateJointSceneNodes( + const std::vector &transforms) +{ + for (size_t i = 0; i < transforms.size(); ++i) { + const auto &transform = transforms[i]; + IBoneSceneNode *node = JointChildSceneNodes[i]; + if (const auto *trs = std::get_if(&transform)) { + dynamic_cast(node)->setTransform(*trs); + } else { + assert(dynamic_cast(node)); + } + } +} + //! updates the joint positions of this mesh void CAnimatedMeshSceneNode::animateJoints() { @@ -536,8 +579,7 @@ void CAnimatedMeshSceneNode::animateJoints() SkinnedMesh *skinnedMesh = static_cast(Mesh); - skinnedMesh->animateMesh(getFrameNr()); - skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes); + updateJointSceneNodes(skinnedMesh->animateMesh(getFrameNr())); //----------------------------------------- // Transition @@ -593,11 +635,7 @@ void CAnimatedMeshSceneNode::checkJoints() if (!JointsUsed) { for (u32 i = 0; i < JointChildSceneNodes.size(); ++i) removeChild(JointChildSceneNodes[i]); - JointChildSceneNodes.clear(); - - // Create joints for SkinnedMesh - ((SkinnedMesh *)Mesh)->addJoints(JointChildSceneNodes, this, SceneManager); - ((SkinnedMesh *)Mesh)->recoverJointsFromMesh(JointChildSceneNodes); + addJoints(); JointsUsed = true; } diff --git a/irr/src/CAnimatedMeshSceneNode.h b/irr/src/CAnimatedMeshSceneNode.h index 6acc5e241..73c258546 100644 --- a/irr/src/CAnimatedMeshSceneNode.h +++ b/irr/src/CAnimatedMeshSceneNode.h @@ -7,6 +7,7 @@ #include "IAnimatedMeshSceneNode.h" #include "IAnimatedMesh.h" +#include "SkinnedMesh.h" #include "matrix4.h" namespace irr @@ -123,9 +124,13 @@ public: //! you must call animateJoints(), or the mesh will not animate void setTransitionTime(f32 Time) override; + void updateJointSceneNodes(const std::vector &transforms); + //! updates the joint positions of this mesh void animateJoints() override; + void addJoints(); + //! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected) void setRenderFromIdentity(bool On) override; diff --git a/irr/src/CB3DMeshFileLoader.cpp b/irr/src/CB3DMeshFileLoader.cpp index f3cd2f179..6e3e14791 100644 --- a/irr/src/CB3DMeshFileLoader.cpp +++ b/irr/src/CB3DMeshFileLoader.cpp @@ -143,7 +143,7 @@ bool CB3DMeshFileLoader::readChunkNODE(SkinnedMesh::SJoint *inJoint) os::Printer::log(logStr.c_str(), joint->Name.value_or("").c_str(), ELL_DEBUG); #endif - SkinnedMesh::SJoint::Transform transform; + core::Transform transform; { f32 t[3], s[3], r[4]; @@ -159,9 +159,9 @@ bool CB3DMeshFileLoader::readChunkNODE(SkinnedMesh::SJoint *inJoint) } if (inJoint) - joint->GlobalMatrix = inJoint->GlobalMatrix * joint->buildLocalMatrix(); + joint->GlobalMatrix = inJoint->GlobalMatrix * transform.buildMatrix(); else - joint->GlobalMatrix = joint->buildLocalMatrix(); + joint->GlobalMatrix = transform.buildMatrix(); while (B3dStack.getLast().startposition + B3dStack.getLast().length > B3DFile->getPos()) // this chunk repeats { diff --git a/irr/src/CBoneSceneNode.h b/irr/src/CBoneSceneNode.h index 6dd4e2440..1da8fca8d 100644 --- a/irr/src/CBoneSceneNode.h +++ b/irr/src/CBoneSceneNode.h @@ -7,6 +7,7 @@ // Used with SkinnedMesh and IAnimatedMeshSceneNode, for boned meshes #include "IBoneSceneNode.h" +#include "Transform.h" #include @@ -21,29 +22,26 @@ public: //! constructor CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1, u32 boneIndex = 0, - const std::optional &boneName = std::nullopt) : - IBoneSceneNode(parent, mgr, id), - BoneIndex(boneIndex) + const std::optional &boneName = std::nullopt, + const core::Transform &transform = {}) : + IBoneSceneNode(parent, mgr, id, boneIndex, boneName) { - setName(boneName); + setTransform(transform); } - //! Returns the index of the bone - u32 getBoneIndex() const override + void setTransform(const core::Transform &transform) { - return BoneIndex; + setPosition(transform.translation); + { + core::vector3df euler; + auto rot = transform.rotation; + // Invert to be consistent with setRotationDegrees + rot.makeInverse(); + rot.toEuler(euler); + setRotation(euler * core::RADTODEG); + } + setScale(transform.scale); } - - //! returns the axis aligned bounding box of this node - const core::aabbox3d &getBoundingBox() const override - { - return Box; - } - - const u32 BoneIndex; - - // Bogus box; bone scene nodes are not rendered anyways. - static constexpr core::aabbox3d Box = {{0, 0, 0}}; }; } // end namespace scene diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp index 794b5af94..e5cef108e 100644 --- a/irr/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -551,7 +551,7 @@ static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, SkinnedMesh const auto &t = trs.translation; const auto &r = trs.rotation; const auto &s = trs.scale; - SkinnedMesh::SJoint::Transform transform{ + core::Transform transform{ convertHandedness(core::vector3df(t[0], t[1], t[2])), convertHandedness(core::quaternion(r[0], r[1], r[2], r[3])), core::vector3df(s[0], s[1], s[2]), diff --git a/irr/src/CXMeshFileLoader.cpp b/irr/src/CXMeshFileLoader.cpp index 295e14feb..eeab4cfb9 100644 --- a/irr/src/CXMeshFileLoader.cpp +++ b/irr/src/CXMeshFileLoader.cpp @@ -4,6 +4,7 @@ #include "CXMeshFileLoader.h" #include "SkinnedMesh.h" +#include "Transform.h" #include "os.h" #include "fast_atof.h" @@ -552,9 +553,16 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent) if (!parseDataObjectFrame(joint)) return false; } else if (objectName == "FrameTransformMatrix") { - joint->transform = core::matrix4(); - if (!parseDataObjectTransformationMatrix(std::get(joint->transform))) + core::matrix4 matrix; + if (!parseDataObjectTransformationMatrix(matrix)) return false; + auto transform = core::Transform::decompose(matrix); + // Try to decompose. If the recomposed matrix equals the old one with a liberal tolerance, use that. + if (transform.buildMatrix().equals(matrix, 1e-5)) { + joint->transform = transform; + } else { + joint->transform = matrix; + } } else if (objectName == "Mesh") { /* frame.Meshes.push_back(SXMesh()); diff --git a/irr/src/MatrixBoneSceneNode.h b/irr/src/MatrixBoneSceneNode.h new file mode 100644 index 000000000..c56fb02ae --- /dev/null +++ b/irr/src/MatrixBoneSceneNode.h @@ -0,0 +1,56 @@ +// Copyright (C) 2025 Joe Mama +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "IBoneSceneNode.h" +#include "matrix4.h" +#include "vector3d.h" +#include + +#include + +// We must represent some transforms differently: +// Bones can have static non-TRS matrix transforms, +// for example shearing (can not be decomposed at all) +// or negatively scaling an axis (can not be decomposed uniquely). +// Hence well-defined animation is not possible for such nodes +// (and in fact glTF even guarantees that they will never be animated). + +namespace irr +{ +namespace scene +{ + +class MatrixBoneSceneNode : public IBoneSceneNode +{ +public: + //! constructor + MatrixBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, + s32 id = -1, u32 boneIndex = 0, + const std::optional &boneName = std::nullopt, + const core::matrix4 &matrix = core::IdentityMatrix) : + IBoneSceneNode(parent, mgr, id, boneIndex, boneName), + matrix(matrix) + {} + + const core::matrix4 matrix; + + // Matrix nodes should not be fake decomposed. + const core::vector3df &getPosition() const override { assert(false); } + const core::vector3df &getRotation() const override { assert(false); } + const core::vector3df &getScale() const override { assert(false); } + + // This node should be static. + void setPosition(const core::vector3df &pos) override { assert(false); } + void setRotation(const core::vector3df &euler_deg) override { assert(false); } + void setScale(const core::vector3df &scale) override { assert(false); } + + core::matrix4 getRelativeTransformation() const override + { + return matrix; + } +}; + +} // end namespace scene +} // end namespace irr diff --git a/irr/src/SkinnedMesh.cpp b/irr/src/SkinnedMesh.cpp index 5340884cb..940517802 100644 --- a/irr/src/SkinnedMesh.cpp +++ b/irr/src/SkinnedMesh.cpp @@ -6,11 +6,15 @@ #include "IBoneSceneNode.h" #include "CBoneSceneNode.h" #include "IAnimatedMeshSceneNode.h" +#include "MatrixBoneSceneNode.h" #include "SSkinMeshBuffer.h" +#include "Transform.h" #include "irrMath.h" #include "matrix4.h" #include "os.h" #include "vector3d.h" +#include +#include #include #include #include @@ -52,125 +56,55 @@ void SkinnedMesh::setAnimationSpeed(f32 fps) FramesPerSecond = fps; } -//! returns the animated mesh based -IMesh *SkinnedMesh::getMesh(f32 frame) +// Keyframe Animation + + +using VariantTransform = SkinnedMesh::SJoint::VariantTransform; +std::vector SkinnedMesh::animateMesh(f32 frame) { - // animate(frame,startFrameLoop, endFrameLoop); - if (frame == -1) - return this; - - animateMesh(frame); - skinMesh(); - return this; -} - -//-------------------------------------------------------------------------- -// Keyframe Animation -//-------------------------------------------------------------------------- - -//! Animates joints based on frame input -void SkinnedMesh::animateMesh(f32 frame) -{ - if (!HasAnimation || LastAnimatedFrame == frame) - return; - - LastAnimatedFrame = frame; - SkinnedLastFrame = false; - + assert(HasAnimation); + std::vector result; + result.reserve(AllJoints.size()); for (auto *joint : AllJoints) - joint->animate(frame); - - // Note: - // LocalAnimatedMatrix needs to be built at some point, but this function may be called lots of times for - // one render (to play two animations at the same time) LocalAnimatedMatrix only needs to be built once. - // a call to buildAllLocalAnimatedMatrices is needed before skinning the mesh, and before the user gets the joints to move - - //---------------- - // Temp! - buildAllLocalAnimatedMatrices(); - //----------------- - - updateBoundingBox(); + result.push_back(joint->animate(frame)); + return result; } -void SkinnedMesh::buildAllLocalAnimatedMatrices() +// Software Skinning + +void SkinnedMesh::skinMesh(const std::vector &global_matrices) { - for (auto *joint : AllJoints) { - // Could be faster: - joint->LocalAnimatedMatrix = joint->buildLocalMatrix(); - } - SkinnedLastFrame = false; -} - -void SkinnedMesh::buildAllGlobalAnimatedMatrices(SJoint *joint, SJoint *parentJoint) -{ - if (!joint) { - for (auto *rootJoint : RootJoints) - buildAllGlobalAnimatedMatrices(rootJoint, 0); - return; - } else { - // Find global matrix... - if (!parentJoint) - joint->GlobalAnimatedMatrix = joint->LocalAnimatedMatrix; - else - joint->GlobalAnimatedMatrix = parentJoint->GlobalAnimatedMatrix * joint->LocalAnimatedMatrix; - } - - for (auto *childJoint : joint->Children) - buildAllGlobalAnimatedMatrices(childJoint, joint); -} - -//-------------------------------------------------------------------------- -// Software Skinning -//-------------------------------------------------------------------------- - -//! Preforms a software skin on this mesh based of joint positions -void SkinnedMesh::skinMesh() -{ - if (!HasAnimation || SkinnedLastFrame) + if (!HasAnimation) return; - //---------------- - // This is marked as "Temp!". A shiny dubloon to whomever can tell me why. - buildAllGlobalAnimatedMatrices(); - //----------------- - - SkinnedLastFrame = true; - if (!HardwareSkinning) { - // rigid animation - for (auto *joint : AllJoints) { - for (u32 attachedMeshIdx : joint->AttachedMeshes) { - SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; - Buffer->Transformation = joint->GlobalAnimatedMatrix; - } + // rigid animation + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + for (u32 attachedMeshIdx : joint->AttachedMeshes) { + SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; + Buffer->Transformation = global_matrices[i]; } - - // clear skinning helper array - for (std::vector &buf : Vertices_Moved) - std::fill(buf.begin(), buf.end(), false); - - // skin starting with the root joints - for (auto *rootJoint : RootJoints) - skinJoint(rootJoint, 0); - - for (auto *buffer : *SkinningBuffers) - buffer->setDirty(EBT_VERTEX); } - updateBoundingBox(); -} -void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) -{ - if (joint->Weights.size()) { - // Find this joints pull on vertices... + // clear skinning helper array + for (std::vector &buf : Vertices_Moved) + std::fill(buf.begin(), buf.end(), false); + + // skin starting with the root joints + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + if (joint->Weights.empty()) + continue; + + // Find this joints pull on vertices // Note: It is assumed that the global inversed matrix has been calculated at this point. - core::matrix4 jointVertexPull = joint->GlobalAnimatedMatrix * joint->GlobalInversedMatrix.value(); + core::matrix4 jointVertexPull = global_matrices[i] * joint->GlobalInversedMatrix.value(); core::vector3df thisVertexMove, thisNormalMove; auto &buffersUsed = *SkinningBuffers; - // Skin Vertices Positions and Normals... + // Skin Vertices, Positions and Normals for (const auto &weight : joint->Weights) { // Pull this vertex... jointVertexPull.transformVect(thisVertexMove, weight.StaticPos); @@ -202,9 +136,8 @@ void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint) } } - // Skin all children - for (auto *childJoint : joint->Children) - skinJoint(childJoint, joint); + for (auto *buffer : *SkinningBuffers) + buffer->setDirty(EBT_VERTEX); } //! Gets joint count. @@ -256,7 +189,7 @@ IMeshBuffer *SkinnedMesh::getMeshBuffer(const video::SMaterial &material) const if (LocalBuffers[i]->getMaterial() == material) return LocalBuffers[i]; } - return 0; + return nullptr; } u32 SkinnedMesh::getTextureSlot(u32 meshbufNr) const @@ -283,29 +216,6 @@ void SkinnedMesh::setDirty(E_BUFFER_TYPE buffer) LocalBuffers[i]->setDirty(buffer); } -//! (This feature is not implemented in irrlicht yet) -bool SkinnedMesh::setHardwareSkinning(bool on) -{ - if (HardwareSkinning != on) { - if (on) { - - // set mesh to static pose... - for (auto *joint : AllJoints) { - for (const auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; - LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos = weight.StaticPos; - LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal; - LocalBuffers[buffer_id]->boundingBoxNeedsRecalculated(); - } - } - } - - HardwareSkinning = on; - } - return HardwareSkinning; -} - void SkinnedMesh::refreshJointCache() { // copy cache from the mesh... @@ -330,114 +240,125 @@ void SkinnedMesh::resetAnimation() LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal = weight.StaticNormal; } } - SkinnedLastFrame = false; - LastAnimatedFrame = -1; } -void SkinnedMesh::calculateGlobalMatrices(SJoint *joint, SJoint *parentJoint) +//! Turns the given array of local matrices into an array of global matrices +//! by multiplying with respective parent matrices. +void SkinnedMesh::calculateGlobalMatrices(std::vector &matrices) { - if (!joint && parentJoint) // bit of protection from endless loops - return; - - // Go through the root bones - if (!joint) { - for (auto *rootJoint : RootJoints) - calculateGlobalMatrices(rootJoint, nullptr); - return; + // Note that the joints are topologically sorted. + for (u16 i = 0; i < AllJoints.size(); ++i) { + if (auto parent_id = AllJoints[i]->ParentJointID) { + matrices[i] = matrices[*parent_id] * matrices[i]; + } } - - const auto local_matrix = joint->buildLocalMatrix(); - if (!parentJoint) - joint->GlobalMatrix = local_matrix; - else - joint->GlobalMatrix = parentJoint->GlobalMatrix * local_matrix; - - joint->LocalAnimatedMatrix = local_matrix; - joint->GlobalAnimatedMatrix = joint->GlobalMatrix; - - if (!joint->GlobalInversedMatrix.has_value()) { // might be pre calculated - joint->GlobalInversedMatrix = joint->GlobalMatrix; - joint->GlobalInversedMatrix->makeInverse(); // slow - } - - for (auto *childJoint : joint->Children) - calculateGlobalMatrices(childJoint, joint); - SkinnedLastFrame = false; } -void SkinnedMesh::checkForAnimation() +bool SkinnedMesh::checkForAnimation() const { - // Check for animation... - HasAnimation = false; for (auto *joint : AllJoints) { if (!joint->keys.empty()) { - HasAnimation = true; - break; + return true; } } - // meshes with weights, are still counted as animated for ragdolls, etc - if (!HasAnimation) { - for (auto *joint : AllJoints) { - if (joint->Weights.size()) { - HasAnimation = true; - break; + // meshes with weights are animatable + for (auto *joint : AllJoints) { + if (!joint->Weights.empty()) { + return true; + } + } + + return false; +} + +void SkinnedMesh::prepareForSkinning() +{ + HasAnimation = checkForAnimation(); + if (!HasAnimation || PreparedForSkinning) + return; + + PreparedForSkinning = true; + + EndFrame = 0.0f; + for (const auto *joint : AllJoints) { + EndFrame = std::max(EndFrame, joint->keys.getEndFrame()); + } + + for (auto *joint : AllJoints) { + for (auto &weight : joint->Weights) { + const u16 buffer_id = weight.buffer_id; + const u32 vertex_id = weight.vertex_id; + + // check for invalid ids + if (buffer_id >= LocalBuffers.size()) { + os::Printer::log("Skinned Mesh: Weight buffer id too large", ELL_WARNING); + weight.buffer_id = weight.vertex_id = 0; + } else if (vertex_id >= LocalBuffers[buffer_id]->getVertexCount()) { + os::Printer::log("Skinned Mesh: Weight vertex id too large", ELL_WARNING); + weight.buffer_id = weight.vertex_id = 0; } } } - if (HasAnimation) { - EndFrame = 0.0f; - for (const auto *joint : AllJoints) { - EndFrame = std::max(EndFrame, joint->keys.getEndFrame()); + for (u32 i = 0; i < Vertices_Moved.size(); ++i) + for (u32 j = 0; j < Vertices_Moved[i].size(); ++j) + Vertices_Moved[i][j] = false; + + // For skinning: cache weight values for speed + for (auto *joint : AllJoints) { + for (auto &weight : joint->Weights) { + const u16 buffer_id = weight.buffer_id; + const u32 vertex_id = weight.vertex_id; + + weight.Moved = &Vertices_Moved[buffer_id][vertex_id]; + weight.StaticPos = LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos; + weight.StaticNormal = LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal; + + // weight._Pos=&Buffers[buffer_id]->getVertex(vertex_id)->Pos; } } - if (HasAnimation && !PreparedForSkinning) { - PreparedForSkinning = true; + normalizeWeights(); - // check for bugs: - for (auto *joint : AllJoints) { - for (auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; - - // check for invalid ids - if (buffer_id >= LocalBuffers.size()) { - os::Printer::log("Skinned Mesh: Weight buffer id too large", ELL_WARNING); - weight.buffer_id = weight.vertex_id = 0; - } else if (vertex_id >= LocalBuffers[buffer_id]->getVertexCount()) { - os::Printer::log("Skinned Mesh: Weight vertex id too large", ELL_WARNING); - weight.buffer_id = weight.vertex_id = 0; - } - } - } - - // An array used in skinning - - for (u32 i = 0; i < Vertices_Moved.size(); ++i) - for (u32 j = 0; j < Vertices_Moved[i].size(); ++j) - Vertices_Moved[i][j] = false; - - // For skinning: cache weight values for speed - - for (auto *joint : AllJoints) { - for (auto &weight : joint->Weights) { - const u16 buffer_id = weight.buffer_id; - const u32 vertex_id = weight.vertex_id; - - weight.Moved = &Vertices_Moved[buffer_id][vertex_id]; - weight.StaticPos = LocalBuffers[buffer_id]->getVertex(vertex_id)->Pos; - weight.StaticNormal = LocalBuffers[buffer_id]->getVertex(vertex_id)->Normal; - - // weight._Pos=&Buffers[buffer_id]->getVertex(vertex_id)->Pos; - } - } - - // normalize weights - normalizeWeights(); + for (auto *joint : AllJoints) { + joint->keys.cleanup(); } - SkinnedLastFrame = false; +} + +void SkinnedMesh::topoSortJoints() +{ + size_t n = AllJoints.size(); + + std::vector permutation; // new id -> old id + + std::vector> children(AllJoints.size()); + for (u16 i = 0; i < n; ++i) { + if (auto parentId = AllJoints[i]->ParentJointID) + children[*parentId].push_back(i); + else + permutation.push_back(i); + } + + // Levelorder + for (u16 i = 0; i < n; ++i) { + permutation.insert(permutation.end(), + children[i].begin(), children[i].end()); + } + + // old id -> new id + std::vector inverse_permutation(n); + for (u16 i = 0; i < n; ++i) + inverse_permutation[permutation[i]] = i; + + std::vector joints(n); + for (u16 i = 0; i < n; ++i) { + joints[i] = AllJoints[permutation[i]]; + joints[i]->JointID = i; + if (auto parentId = joints[i]->ParentJointID) + joints[i]->ParentJointID = inverse_permutation[*parentId]; + } + AllJoints = joints; } //! called by loader after populating with mesh and bone data @@ -445,60 +366,40 @@ SkinnedMesh *SkinnedMeshBuilder::finalize() { os::Printer::log("Skinned Mesh - finalize", ELL_DEBUG); - // Make sure we recalc the next frame - LastAnimatedFrame = -1; - SkinnedLastFrame = false; + topoSortJoints(); - // calculate bounding box + // Calculate bounding box for (auto *buffer : LocalBuffers) { buffer->recalculateBoundingBox(); } - if (AllJoints.size() || RootJoints.size()) { - // populate AllJoints or RootJoints, depending on which is empty - if (RootJoints.empty()) { - - for (auto *joint : AllJoints) { - - bool foundParent = false; - for (const auto *parentJoint : AllJoints) { - for (const auto *childJoint : parentJoint->Children) { - if (childJoint == joint) - foundParent = true; - } - } - - if (!foundParent) - RootJoints.push_back(joint); - } - } else { - AllJoints = RootJoints; - } - } - - // Set array sizes... - + // Set array sizes for (u32 i = 0; i < LocalBuffers.size(); ++i) { Vertices_Moved.emplace_back(LocalBuffers[i]->getVertexCount()); } - checkForAnimation(); + prepareForSkinning(); - if (HasAnimation) { - for (auto *joint : AllJoints) { - joint->keys.cleanup(); - } - } - - // Needed for animation and skinning... - - calculateGlobalMatrices(0, 0); - - // rigid animation for non animated meshes + std::vector matrices; + matrices.reserve(AllJoints.size()); + // TODO populate with local matrices for (auto *joint : AllJoints) { + if (const auto *matrix = std::get_if(&joint->transform)) + matrices.push_back(*matrix); + else + matrices.push_back(std::get(joint->transform).buildMatrix()); + } + calculateGlobalMatrices(matrices); + + + for (size_t i = 0; i < AllJoints.size(); ++i) { + auto *joint = AllJoints[i]; + joint->GlobalInversedMatrix = matrices[i]; + joint->GlobalInversedMatrix->makeInverse(); + // rigid animation for non animated meshes for (u32 attachedMeshIdx : joint->AttachedMeshes) { SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx]; - Buffer->Transformation = joint->GlobalAnimatedMatrix; + Buffer->Transformation = matrices[i]; } } @@ -554,14 +455,11 @@ void SkinnedMeshBuilder::addMeshBuffer(SSkinMeshBuffer *meshbuf) SkinnedMesh::SJoint *SkinnedMeshBuilder::addJoint(SJoint *parent) { SJoint *joint = new SJoint; + if (parent) + joint->ParentJointID = parent->JointID; + joint->JointID = AllJoints.size(); AllJoints.push_back(joint); - if (!parent) { - // Add root joints to array in finalize() - } else { - // Set parent (Be careful of the mesh loader also setting the parent) - parent->Children.push_back(joint); - } return joint; } @@ -631,88 +529,6 @@ void SkinnedMesh::normalizeWeights() } } -void SkinnedMesh::recoverJointsFromMesh(std::vector &jointChildSceneNodes) -{ - for (u32 i = 0; i < AllJoints.size(); ++i) { - IBoneSceneNode *node = jointChildSceneNodes[i]; - SJoint *joint = AllJoints[i]; - if (std::holds_alternative(joint->transform)) { - auto transform = std::get(joint->transform); - node->setPosition(transform.translation); - { - core::vector3df euler; - auto rot = transform.rotation; - // Invert to be consistent with setRotationDegrees - rot.makeInverse(); - rot.toEuler(euler); - node->setRotation(euler * core::RADTODEG); - } - node->setScale(transform.scale); - } else { - node->setPosition(joint->LocalAnimatedMatrix.getTranslation()); - node->setRotation(joint->LocalAnimatedMatrix.getRotationDegrees()); - node->setScale(joint->LocalAnimatedMatrix.getScale()); - } - - // node->updateAbsolutePosition(); // WTF - } -} - -void SkinnedMesh::transferJointsToMesh(const std::vector &jointChildSceneNodes) -{ - for (u32 i = 0; i < AllJoints.size(); ++i) { - const IBoneSceneNode *const node = jointChildSceneNodes[i]; - SJoint *joint = AllJoints[i]; - - if (std::holds_alternative(joint->transform)) { - joint->LocalAnimatedMatrix = core::matrix4(); - joint->LocalAnimatedMatrix.setRotationDegrees(node->getRotation()); - joint->LocalAnimatedMatrix.setTranslation(node->getPosition()); - joint->LocalAnimatedMatrix *= core::matrix4().setScale(node->getScale()); - } - } - // Make sure we recalc the next frame - LastAnimatedFrame = -1; - SkinnedLastFrame = false; -} - -void SkinnedMesh::addJoints(std::vector &jointChildSceneNodes, - IAnimatedMeshSceneNode *node, ISceneManager *smgr) -{ - // Create new joints - for (u32 i = 0; i < AllJoints.size(); ++i) { - jointChildSceneNodes.push_back(new CBoneSceneNode(0, smgr, 0, i, AllJoints[i]->Name)); - } - - // Match up parents - for (u32 i = 0; i < jointChildSceneNodes.size(); ++i) { - const SJoint *const joint = AllJoints[i]; // should be fine - - s32 parentID = -1; - - for (u32 j = 0; (parentID == -1) && (j < AllJoints.size()); ++j) { - if (i != j) { - const SJoint *const parentTest = AllJoints[j]; - for (u32 n = 0; n < parentTest->Children.size(); ++n) { - if (parentTest->Children[n] == joint) { - parentID = j; - break; - } - } - } - } - - IBoneSceneNode *bone = jointChildSceneNodes[i]; - if (parentID != -1) - bone->setParent(jointChildSceneNodes[parentID]); - else - bone->setParent(node); - - bone->drop(); - } - SkinnedLastFrame = false; -} - void SkinnedMesh::convertMeshToTangents() { // now calculate tangents diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 13648aa87..f056035f5 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -178,15 +178,6 @@ static void setBillboardTextureMatrix(scene::IBillboardSceneNode *bill, matrix.setTextureScale(txs, tys); } -// Evaluate transform chain recursively; irrlicht does not do this for us -static void updatePositionRecursive(scene::ISceneNode *node) -{ - scene::ISceneNode *parent = node->getParent(); - if (parent) - updatePositionRecursive(parent); - node->updateAbsolutePosition(); -} - static bool logOnce(const std::ostringstream &from, std::ostream &log_to) { thread_local std::vector logged; diff --git a/src/unittest/test_irr_gltf_mesh_loader.cpp b/src/unittest/test_irr_gltf_mesh_loader.cpp index 391a9fd29..bc49b8896 100644 --- a/src/unittest/test_irr_gltf_mesh_loader.cpp +++ b/src/unittest/test_irr_gltf_mesh_loader.cpp @@ -414,14 +414,14 @@ SECTION("simple skin") SECTION("transformations are correct") { { - const auto &transform = std::get(parent->transform); + const auto &transform = std::get(parent->transform); CHECK(transform.translation == v3f(0, 0, 0)); CHECK(transform.rotation == irr::core::quaternion()); CHECK(transform.scale == v3f(1, 1, 1)); CHECK(parent->GlobalInversedMatrix == irr::core::matrix4()); } { - const auto &transform = std::get(child->transform); + const auto &transform = std::get(child->transform); const v3f translation(0, 1, 0); CHECK(transform.translation == translation); CHECK(transform.rotation == irr::core::quaternion());