1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00

Fix and clean up skeletal animation (#15722)

* Fix attachments lagging behind their parents (#14818)
* Fix animation blending (#14817)
* Bring back cool guy as another .x smoke test
* Add .x mesh loader unittest
* Do bounding box & matrix calculation at proper point in time
* Remove obsolete `SAnimatedMesh`
This commit is contained in:
Lars Müller 2025-06-01 23:21:35 +02:00 committed by GitHub
parent 0bb87eb1ff
commit fde6384a09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 856 additions and 1388 deletions

View file

@ -315,6 +315,9 @@ due to their space savings.
Bone weights should be normalized, e.g. using ["normalize all" in Blender](https://docs.blender.org/manual/en/4.2/grease_pencil/modes/weight_paint/weights_menu.html#normalize-all).
Note that nodes using matrix transforms must not be animated.
This also extends to bone overrides, which must not be applied to them.
You can use the [Khronos glTF validator](https://github.com/KhronosGroup/glTF-Validator)
to check whether a model is a valid glTF file.

View file

@ -13,3 +13,9 @@ Jordach (CC BY-SA 3.0):
Zeg9 (CC BY-SA 3.0):
testentities_lava_flan.x
testentities_lava_flan.png
"Cool Guy":
hecks (refer to irr/LICENSE):
testentities_cool_guy.x
testentities_cool_guy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

View file

@ -102,6 +102,19 @@ core.register_entity("testentities:lava_flan", {
end,
})
core.register_entity("testentities:cool_guy", {
initial_properties = {
visual = "mesh",
mesh = "testentities_cool_guy.x",
textures = {
"testentities_cool_guy.png"
},
},
on_activate = function(self)
self.object:set_animation({x = 0, y = 29}, 30, 0, true)
end,
})
-- Advanced visual tests
-- An entity for testing animated and yaw-modulated sprites

View file

@ -13,8 +13,8 @@ namespace scene
//! Interface for an animated mesh.
/** There are already simple implementations of this interface available so
you don't have to implement this interface on your own if you need to:
You might want to use irr::scene::SAnimatedMesh, irr::scene::SMesh,
irr::scene::SMeshBuffer etc. */
You might want to use irr::scene::SMesh, irr::scene::SMeshBuffer etc.
*/
class IAnimatedMesh : public IMesh
{
public:
@ -34,22 +34,8 @@ public:
scene node the mesh is instantiated in.*/
virtual void setAnimationSpeed(f32 fps) = 0;
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number, >= 0, <= getMaxFrameNumber()
Linear interpolation is used if this is between two frames.
\return Returns the animated mesh for the given frame */
virtual IMesh *getMesh(f32 frame) = 0;
//! Returns the type of the animated mesh.
/** In most cases it is not necessary to use this method.
This is useful for making a safe downcast. For example,
if getMeshType() returns EAMT_MD2 it's safe to cast the
IAnimatedMesh to IAnimatedMeshMD2.
\returns Type of the mesh. */
E_ANIMATED_MESH_TYPE getMeshType() const override
{
return EAMT_UNKNOWN;
}
//! Returns the type of the animated mesh. Useful for safe downcasts.
E_ANIMATED_MESH_TYPE getMeshType() const = 0;
};
} // end namespace scene

View file

@ -12,35 +12,8 @@ namespace irr
{
namespace scene
{
enum E_JOINT_UPDATE_ON_RENDER
{
//! do nothing
EJUOR_NONE = 0,
//! get joints positions from the mesh (for attached nodes, etc)
EJUOR_READ,
//! control joint positions in the mesh (eg. ragdolls, or set the animation from animateJoints() )
EJUOR_CONTROL
};
class IAnimatedMeshSceneNode;
//! Callback interface for catching events of ended animations.
/** Implement this interface and use
IAnimatedMeshSceneNode::setAnimationEndCallback to be able to
be notified if an animation playback has ended.
**/
class IAnimationEndCallBack : public virtual IReferenceCounted
{
public:
//! Will be called when the animation playback has ended.
/** See IAnimatedMeshSceneNode::setAnimationEndCallback for
more information.
\param node: Node of which the animation has ended. */
virtual void OnAnimationEnd(IAnimatedMeshSceneNode *node) = 0;
};
//! Scene node capable of displaying an animated mesh.
class IAnimatedMeshSceneNode : public ISceneNode
{
@ -120,11 +93,10 @@ public:
/** When true the animations are played looped */
virtual bool getLoopMode() const = 0;
//! Sets a callback interface which will be called if an animation playback has ended.
/** Set this to 0 to disable the callback again.
Please note that this will only be called when in non looped
mode, see IAnimatedMeshSceneNode::setLoopMode(). */
virtual void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) = 0;
//! Will be called right after the joints have been animated,
//! but before the transforms have been propagated recursively to children.
virtual void setOnAnimateCallback(
const std::function<void(f32 dtime)> &cb) = 0;
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style.
/** In this way it is possible to change the materials a mesh
@ -139,20 +111,15 @@ public:
virtual void setMesh(IAnimatedMesh *mesh) = 0;
//! Returns the current mesh
virtual IAnimatedMesh *getMesh(void) = 0;
//! Set how the joints should be updated on render
virtual void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) = 0;
virtual IAnimatedMesh *getMesh() = 0;
//! Sets the transition time in seconds
/** Note: This needs to enable joints, and setJointmode set to
EJUOR_CONTROL. You must call animateJoints(), or the mesh will
not animate. */
/** Note: You must call animateJoints(), or the mesh will not animate. */
virtual void setTransitionTime(f32 Time) = 0;
//! animates the joints in the mesh based on the current frame.
/** Also takes in to account transitions. */
virtual void animateJoints(bool CalculateAbsolutePositions = true) = 0;
virtual void animateJoints() = 0;
//! render mesh ignoring its transformation.
/** Culling is unaffected. */

View file

@ -11,85 +11,41 @@ namespace irr
namespace scene
{
//! Enumeration for different bone animation modes
enum E_BONE_ANIMATION_MODE
{
//! The bone is usually animated, unless it's parent is not animated
EBAM_AUTOMATIC = 0,
//! The bone is animated by the skin, if it's parent is not animated then animation will resume from this bone onward
EBAM_ANIMATED,
//! The bone is not animated by the skin
EBAM_UNANIMATED,
//! Not an animation mode, just here to count the available modes
EBAM_COUNT
};
enum E_BONE_SKINNING_SPACE
{
//! local skinning, standard
EBSS_LOCAL = 0,
//! global skinning
EBSS_GLOBAL,
EBSS_COUNT
};
//! Names for bone animation modes
const c8 *const BoneAnimationModeNames[] = {
"automatic",
"animated",
"unanimated",
0,
};
//! Interface for bones used for skeletal animation.
/** Used with SkinnedMesh and IAnimatedMeshSceneNode. */
class IBoneSceneNode : public ISceneNode
{
public:
IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id = -1) :
ISceneNode(parent, mgr, id), positionHint(-1), scaleHint(-1), rotationHint(-1) {}
IBoneSceneNode(ISceneNode *parent, ISceneManager *mgr,
s32 id = -1, u32 boneIndex = 0,
const std::optional<std::string> &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;
}
//! Sets the animation mode of the bone.
/** \return True if successful. (Unused) */
virtual bool setAnimationMode(E_BONE_ANIMATION_MODE mode) = 0;
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override
{
return Box;
}
//! Gets the current animation mode of the bone
virtual E_BONE_ANIMATION_MODE getAnimationMode() const = 0;
const u32 BoneIndex;
//! Get the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override = 0;
//! Returns the relative transformation of the scene node.
// virtual core::matrix4 getRelativeTransformation() const = 0;
//! The animation method.
void OnAnimate(u32 timeMs) override = 0;
// Bogus box; bone scene nodes are not rendered anyways.
static constexpr core::aabbox3d<f32> Box = {{0, 0, 0}};
//! The render method.
/** Does nothing as bones are not visible. */
void render() override {}
//! How the relative transformation of the bone is used
virtual void setSkinningSpace(E_BONE_SKINNING_SPACE space) = 0;
//! How the relative transformation of the bone is used
virtual E_BONE_SKINNING_SPACE getSkinningSpace() const = 0;
//! Updates the absolute position based on the relative and the parents position
virtual void updateAbsolutePositionOfAllChildren() = 0;
s32 positionHint;
s32 scaleHint;
s32 rotationHint;
};
} // end namespace scene

View file

@ -20,38 +20,6 @@ enum E_ANIMATED_MESH_TYPE
//! Unknown animated mesh type.
EAMT_UNKNOWN = 0,
//! Quake 2 MD2 model file
EAMT_MD2,
//! Quake 3 MD3 model file
EAMT_MD3,
//! Maya .obj static model
EAMT_OBJ,
//! Quake 3 .bsp static Map
EAMT_BSP,
//! 3D Studio .3ds file
EAMT_3DS,
//! My3D Mesh, the file format by Zhuck Dimitry
EAMT_MY3D,
//! Pulsar LMTools .lmts file. This Irrlicht loader was written by Jonas Petersen
EAMT_LMTS,
//! Cartography Shop .csm file. This loader was created by Saurav Mohapatra.
EAMT_CSM,
//! .oct file for Paul Nette's FSRad or from Murphy McCauley's Blender .oct exporter.
/** The oct file format contains 3D geometry and lightmaps and
can be loaded directly by Irrlicht */
EAMT_OCT,
//! Halflife MDL model file
EAMT_MDL_HALFLIFE,
//! generic skinned mesh
EAMT_SKINNED,
@ -119,9 +87,7 @@ public:
virtual void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) = 0;
//! Returns the type of the meshes.
/** This is useful for making a safe downcast. For example,
if getMeshType() returns EAMT_MD2 it's safe to cast the
IMesh to IAnimatedMeshMD2.
/** This is useful for making a safe downcast.
Note: It's no longer just about animated meshes, that name has just historical reasons.
\returns Type of the mesh */
virtual E_ANIMATED_MESH_TYPE getMeshType() const

View file

@ -66,26 +66,6 @@ public:
IReferenceCounted::drop() for more information. */
virtual SMesh *createMeshCopy(IMesh *mesh) const = 0;
//! Get amount of polygons in mesh.
/** \param mesh Input mesh
\return Number of polygons in mesh. */
virtual s32 getPolyCount(IMesh *mesh) const = 0;
//! Get amount of polygons in mesh.
/** \param mesh Input mesh
\return Number of polygons in mesh. */
virtual s32 getPolyCount(IAnimatedMesh *mesh) const = 0;
//! Create a new AnimatedMesh and adds the mesh to it
/** \param mesh Input mesh
\param type The type of the animated mesh to create.
\return Newly created animated mesh with mesh as its only
content. When you don't need the animated mesh anymore, you
should call IAnimatedMesh::drop(). See
IReferenceCounted::drop() for more information. */
virtual IAnimatedMesh *createAnimatedMesh(IMesh *mesh,
scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) const = 0;
//! Apply a manipulator on the Meshbuffer
/** \param func A functor defining the mesh manipulation.
\param buffer The Meshbuffer to apply the manipulator to.

View file

@ -32,7 +32,7 @@ public:
//! Get the currently defined mesh for display.
/** \return Pointer to mesh which is displayed by this node. */
virtual IMesh *getMesh(void) = 0;
virtual IMesh *getMesh() = 0;
//! Sets if the scene node should not copy the materials of the mesh but use them directly.
/** In this way it is possible to change the materials of a mesh

View file

@ -94,16 +94,12 @@ public:
\param timeMs Current time in milliseconds. */
virtual void OnAnimate(u32 timeMs)
{
if (IsVisible) {
// update absolute position
if (!IsVisible && Children.empty())
return;
updateAbsolutePosition();
// perform the post render process on all children
ISceneNodeList::iterator it = Children.begin();
for (; it != Children.end(); ++it)
(*it)->OnAnimate(timeMs);
}
for (auto *child : Children)
child->OnAnimate(timeMs);
}
//! Renders the node.

View file

@ -1,167 +0,0 @@
// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h
#pragma once
#include <vector>
#include "IAnimatedMesh.h"
#include "IMesh.h"
#include "aabbox3d.h"
namespace irr
{
namespace scene
{
//! Simple implementation of the IAnimatedMesh interface.
struct SAnimatedMesh final : public IAnimatedMesh
{
//! constructor
SAnimatedMesh(scene::IMesh *mesh = 0, scene::E_ANIMATED_MESH_TYPE type = scene::EAMT_UNKNOWN) :
IAnimatedMesh(), FramesPerSecond(25.f), Type(type)
{
addMesh(mesh);
recalculateBoundingBox();
}
//! destructor
virtual ~SAnimatedMesh()
{
// drop meshes
for (auto *mesh : Meshes)
mesh->drop();
}
f32 getMaxFrameNumber() const override
{
return static_cast<f32>(Meshes.size() - 1);
}
//! Gets the default animation speed of the animated mesh.
/** \return Amount of frames per second. If the amount is 0, it is a static, non animated mesh. */
f32 getAnimationSpeed() const override
{
return FramesPerSecond;
}
//! Gets the frame count of the animated mesh.
/** \param fps Frames per second to play the animation with. If the amount is 0, it is not animated.
The actual speed is set in the scene node the mesh is instantiated in.*/
void setAnimationSpeed(f32 fps) override
{
FramesPerSecond = fps;
}
//! Returns the IMesh interface for a frame.
/** \param frame: Frame number as zero based index.
\return The animated mesh based for the given frame */
IMesh *getMesh(f32 frame) override
{
if (Meshes.empty())
return nullptr;
return Meshes[static_cast<s32>(frame)];
}
//! adds a Mesh
void addMesh(IMesh *mesh)
{
if (mesh) {
mesh->grab();
Meshes.push_back(mesh);
}
}
//! Returns an axis aligned bounding box of the mesh.
/** \return A bounding box of this mesh is returned. */
const core::aabbox3d<f32> &getBoundingBox() const override
{
return Box;
}
//! set user axis aligned bounding box
void setBoundingBox(const core::aabbox3df &box) override
{
Box = box;
}
//! Recalculates the bounding box.
void recalculateBoundingBox()
{
Box.reset(0, 0, 0);
if (Meshes.empty())
return;
Box = Meshes[0]->getBoundingBox();
for (u32 i = 1; i < Meshes.size(); ++i)
Box.addInternalBox(Meshes[i]->getBoundingBox());
}
//! Returns the type of the animated mesh.
E_ANIMATED_MESH_TYPE getMeshType() const override
{
return Type;
}
//! returns amount of mesh buffers.
u32 getMeshBufferCount() const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBufferCount();
}
//! returns pointer to a mesh buffer
IMeshBuffer *getMeshBuffer(u32 nr) const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBuffer(nr);
}
//! Returns pointer to a mesh buffer which fits a material
/** \param material: material to search for
\return Returns the pointer to the mesh buffer or
NULL if there is no such mesh buffer. */
IMeshBuffer *getMeshBuffer(const video::SMaterial &material) const override
{
if (Meshes.empty())
return 0;
return Meshes[0]->getMeshBuffer(material);
}
//! set the hardware mapping hint, for driver
void setHardwareMappingHint(E_HARDWARE_MAPPING newMappingHint, E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override
{
for (u32 i = 0; i < Meshes.size(); ++i)
Meshes[i]->setHardwareMappingHint(newMappingHint, buffer);
}
//! flags the meshbuffer as changed, reloads hardware buffers
void setDirty(E_BUFFER_TYPE buffer = EBT_VERTEX_AND_INDEX) override
{
for (u32 i = 0; i < Meshes.size(); ++i)
Meshes[i]->setDirty(buffer);
}
//! All meshes defining the animated mesh
std::vector<IMesh *> Meshes;
//! The bounding box of this mesh
core::aabbox3d<f32> Box{{0.0f, 0.0f, 0.0f}};
//! Default animation speed of this mesh.
f32 FramesPerSecond;
//! The type of the mesh.
E_ANIMATED_MESH_TYPE Type;
};
} // end namespace scene
} // end namespace irr

View file

@ -5,7 +5,7 @@
#pragma once
#include <vector>
#include "IMesh.h"
#include "IAnimatedMesh.h"
#include "IMeshBuffer.h"
#include "aabbox3d.h"
@ -14,7 +14,7 @@ namespace irr
namespace scene
{
//! Simple implementation of the IMesh interface.
struct SMesh final : public IMesh
struct SMesh final : public IAnimatedMesh
{
//! constructor
SMesh() {}
@ -134,6 +134,15 @@ struct SMesh final : public IMesh
//! The bounding box of this mesh
core::aabbox3d<f32> BoundingBox{{0, 0, 0}};
// Implement animated mesh interface as a static mesh.
// Slightly hacky: Eventually should be consolidated with SSkinnedMesh,
// with all the animation-related parts behind an optional.
virtual f32 getMaxFrameNumber() const override { return 0.0f; }
virtual f32 getAnimationSpeed() const override { return 0.0f; }
virtual void setAnimationSpeed(f32 fps) override {}
E_ANIMATED_MESH_TYPE getMeshType() const override { return EAMT_STATIC; }
};
} // end namespace scene

View file

@ -8,11 +8,18 @@
#include "ISceneManager.h"
#include "SMeshBuffer.h"
#include "SSkinMeshBuffer.h"
#include "aabbox3d.h"
#include "irrMath.h"
#include "irrTypes.h"
#include "matrix4.h"
#include "quaternion.h"
#include "vector3d.h"
#include "Transform.h"
#include <optional>
#include <string>
#include <variant>
#include <vector>
namespace irr
{
@ -37,9 +44,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;
@ -64,14 +70,12 @@ 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;
//! Turns the given array of local matrices into an array of global matrices
//! by multiplying with respective parent matrices.
void calculateGlobalMatrices(std::vector<core::matrix4> &matrices) const;
//! Animates joints based on frame input
void animateMesh(f32 frame);
//! 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<core::matrix4> &animated_transforms);
//! returns amount of mesh buffers.
u32 getMeshBufferCount() const override;
@ -89,14 +93,15 @@ public:
void setTextureSlot(u32 meshbufNr, u32 textureSlot);
//! returns an axis aligned bounding box
//! Returns bounding box of the mesh *in static pose*.
const core::aabbox3d<f32> &getBoundingBox() const override {
return BoundingBox;
// TODO ideally we shouldn't be forced to implement this
return StaticPoseBox;
}
//! set user axis aligned bounding box
//! Set bounding box of the mesh *in static pose*.
void setBoundingBox(const core::aabbox3df &box) override {
BoundingBox = box;
StaticPoseBox = box;
}
//! set the hardware mapping hint, for driver
@ -140,28 +145,15 @@ 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();
//! Moves the mesh into static position.
void resetAnimation();
void updateBoundingBox();
//! Recovers the joints from the mesh
void recoverJointsFromMesh(std::vector<IBoneSceneNode *> &jointChildSceneNodes);
//! Transfers the joint data to the mesh
void transferJointsToMesh(const std::vector<IBoneSceneNode *> &jointChildSceneNodes);
//! Creates an array of joints from this mesh as children of node
void addJoints(std::vector<IBoneSceneNode *> &jointChildSceneNodes,
IAnimatedMeshSceneNode *node,
ISceneManager *smgr);
std::vector<IBoneSceneNode *> addJoints(
IAnimatedMeshSceneNode *node, ISceneManager *smgr);
//! A vertex weight
struct SWeight
@ -236,7 +228,7 @@ public:
static core::quaternion interpolateValue(core::quaternion from, core::quaternion to, f32 time) {
core::quaternion result;
result.slerp(from, to, time, 0.001f);
result.slerp(from, to, time);
return result;
}
@ -288,15 +280,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() {
@ -309,16 +300,34 @@ public:
//! Joints
struct SJoint
{
SJoint() : GlobalSkinningSpace(false) {}
SJoint() {}
//! The name of this joint
std::optional<std::string> Name;
//! Local matrix of this joint
core::matrix4 LocalMatrix;
//! Local transformation to be set by loaders. Mutated by animation.
using VariantTransform = std::variant<core::Transform, core::matrix4>;
VariantTransform transform{core::Transform{}};
VariantTransform animate(f32 frame) const {
if (keys.empty())
return transform;
if (std::holds_alternative<core::matrix4>(transform)) {
// .x lets animations override matrix transforms entirely,
// which is what we implement here.
// .gltf does not allow animation of nodes using matrix transforms.
// Note that a decomposition into a TRS transform need not exist!
core::Transform trs;
keys.updateTransform(frame, trs);
return {trs};
}
auto trs = std::get<core::Transform>(transform);
keys.updateTransform(frame, trs);
return {trs};
}
//! List of child joints
std::vector<SJoint *> Children;
//! List of attached meshes
std::vector<u32> AttachedMeshes;
@ -329,42 +338,49 @@ public:
//! Skin weights
std::vector<SWeight> Weights;
//! Bounding box of all affected vertices, in local space
core::aabbox3df LocalBoundingBox{{0, 0, 0}};
//! 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;
//! These should be set by loaders.
core::vector3df Animatedposition;
core::vector3df Animatedscale;
core::quaternion Animatedrotation;
// The .x and .gltf formats pre-calculate this
std::optional<core::matrix4> GlobalInversedMatrix;
private:
//! Internal members used by SkinnedMesh
friend class SkinnedMesh;
bool GlobalSkinningSpace;
void setParent(SJoint *parent) {
ParentJointID = parent ? parent->JointID : std::optional<u16>{};
}
u16 JointID; // TODO refactor away: pointers -> IDs (problem: .x loader abuses SJoint)
std::optional<u16> ParentJointID;
};
//! Animates joints based on frame input
std::vector<SJoint::VariantTransform> animateMesh(f32 frame);
//! Calculates a bounding box given an animation in the form of global joint transforms.
core::aabbox3df calculateBoundingBox(
const std::vector<core::matrix4> &global_transforms);
void recalculateBaseBoundingBoxes();
const std::vector<SJoint *> &getAllJoints() const {
return AllJoints;
}
protected:
void checkForAnimation();
bool checkForAnimation() const;
void topoSortJoints();
void prepareForSkinning();
void calculateStaticBoundingBox();
void calculateJointBoundingBoxes();
void calculateBufferBoundingBoxes();
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,
@ -376,25 +392,25 @@ protected:
//! Mapping from meshbuffer number to bindable texture slot
std::vector<u32> TextureSlots;
//! Joints, topologically sorted (parents come before their children).
std::vector<SJoint *> AllJoints;
std::vector<SJoint *> RootJoints;
// bool can't be used here because std::vector<bool>
// doesn't allow taking a reference to individual elements.
std::vector<std::vector<char>> Vertices_Moved;
core::aabbox3d<f32> BoundingBox{{0, 0, 0}};
//! Bounding box of just the static parts of the mesh
core::aabbox3df StaticPartsBox{{0, 0, 0}};
//! Bounding box of the mesh in static pose
core::aabbox3df StaticPoseBox{{0, 0, 0}};
f32 EndFrame;
f32 FramesPerSecond;
f32 LastAnimatedFrame;
bool SkinnedLastFrame;
bool HasAnimation;
bool PreparedForSkinning;
bool AnimateNormals;
bool HardwareSkinning;
SourceFormat SrcFormat;
};

42
irr/include/Transform.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include "irrMath.h"
#include <matrix4.h>
#include <vector3d.h>
#include <quaternion.h>
namespace irr
{
namespace core
{
struct Transform {
vector3df translation;
quaternion rotation;
vector3df scale{1};
Transform interpolate(Transform to, f32 time) const
{
core::quaternion interpolated_rotation;
interpolated_rotation.slerp(rotation, to.rotation, time);
return {
to.translation.getInterpolated(translation, time),
interpolated_rotation,
to.scale.getInterpolated(scale, time),
};
}
matrix4 buildMatrix() const
{
matrix4 T;
T.setTranslation(translation);
matrix4 R;
rotation.getMatrix_transposed(R);
matrix4 S;
S.setScale(scale);
return T * R * S;
}
};
} // end namespace core
} // end namespace irr

View file

@ -180,7 +180,7 @@ public:
linear interpolation.
*/
quaternion &slerp(quaternion q1, quaternion q2,
f32 time, f32 threshold = .05f);
f32 time, f32 threshold = .001f);
//! Set this quaternion to represent a rotation from angle and axis.
/** Axis must be unit length.

View file

@ -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 "S3DVertex.h"
#include "Transform.h"
#include "irrTypes.h"
#include "matrix4.h"
#include "os.h"
#include "SkinnedMesh.h"
#include "IDummyTransformationSceneNode.h"
@ -17,6 +21,9 @@
#include "IFileSystem.h"
#include "quaternion.h"
#include <algorithm>
#include <cstddef>
#include <optional>
#include <cassert>
namespace irr
{
@ -30,13 +37,13 @@ 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),
JointMode(EJUOR_NONE), JointsUsed(false),
JointsUsed(false),
Looping(true), ReadOnlyMaterials(false), RenderFromIdentity(false),
LoopCallBack(0), PassCount(0)
PassCount(0)
{
setMesh(mesh);
}
@ -44,8 +51,6 @@ CAnimatedMeshSceneNode::CAnimatedMeshSceneNode(IAnimatedMesh *mesh,
//! destructor
CAnimatedMeshSceneNode::~CAnimatedMeshSceneNode()
{
if (LoopCallBack)
LoopCallBack->drop();
if (Mesh)
Mesh->drop();
}
@ -87,8 +92,7 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame)
CurrentFrameNr = StartFrame + fmodf(CurrentFrameNr - StartFrame, EndFrame - StartFrame);
} else // backwards...
{
} else { // backwards...
if (CurrentFrameNr < StartFrame)
CurrentFrameNr = EndFrame - fmodf(EndFrame - CurrentFrameNr, EndFrame - StartFrame);
}
@ -97,18 +101,9 @@ void CAnimatedMeshSceneNode::buildFrameNr(u32 timeMs)
CurrentFrameNr += timeMs * FramesPerSecond;
if (FramesPerSecond > 0.f) { // forwards...
if (CurrentFrameNr > EndFrame) {
CurrentFrameNr = EndFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
} else // backwards...
{
if (CurrentFrameNr < StartFrame) {
CurrentFrameNr = StartFrame;
if (LoopCallBack)
LoopCallBack->OnAnimationEnd(this);
}
CurrentFrameNr = std::min(CurrentFrameNr, EndFrame);
} else { // backwards...
CurrentFrameNr = std::max(CurrentFrameNr, StartFrame);
}
}
}
@ -156,39 +151,19 @@ void CAnimatedMeshSceneNode::OnRegisterSceneNode()
IMesh *CAnimatedMeshSceneNode::getMeshForCurrentFrame()
{
if (Mesh->getMeshType() != EAMT_SKINNED) {
return Mesh->getMesh(getFrameNr());
} else {
return 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 *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
auto *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
if (JointMode == EJUOR_CONTROL) // write to mesh
skinnedMesh->transferJointsToMesh(JointChildSceneNodes);
else
skinnedMesh->animateMesh(getFrameNr());
// Update the skinned mesh for the current joint transforms.
skinnedMesh->skinMesh();
if (JointMode == EJUOR_READ) { // read from mesh
skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes);
//---slow---
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n)
if (JointChildSceneNodes[n]->getParent() == this) {
JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option
}
}
if (JointMode == EJUOR_CONTROL) {
// For meshes other than EJUOR_CONTROL, this is done by calling animateMesh()
skinnedMesh->updateBoundingBox();
}
// Matrices have already been calculated in OnAnimate
skinnedMesh->skinMesh(PerJoint.GlobalMatrices);
return skinnedMesh;
}
}
//! OnAnimate() is called just before rendering the whole scene.
void CAnimatedMeshSceneNode::OnAnimate(u32 timeMs)
@ -201,7 +176,28 @@ void CAnimatedMeshSceneNode::OnAnimate(u32 timeMs)
buildFrameNr(timeMs - LastTimeMs);
LastTimeMs = timeMs;
// This needs to be done on animate, which is called recursively *before*
// anything is rendered so that the transformations of children are up to date
animateJoints();
// Copy old transforms *before* bone overrides have been applied.
// TODO if there are no bone overrides or no animation blending, this is unnecessary.
copyOldTransforms();
if (OnAnimateCallback)
OnAnimateCallback(timeMs / 1000.0f);
IAnimatedMeshSceneNode::OnAnimate(timeMs);
if (auto *skinnedMesh = dynamic_cast<SkinnedMesh*>(Mesh)) {
for (u16 i = 0; i < PerJoint.SceneNodes.size(); ++i)
PerJoint.GlobalMatrices[i] = PerJoint.SceneNodes[i]->getRelativeTransformation();
assert(PerJoint.GlobalMatrices.size() == skinnedMesh->getJointCount());
skinnedMesh->calculateGlobalMatrices(PerJoint.GlobalMatrices);
Box = skinnedMesh->calculateBoundingBox(PerJoint.GlobalMatrices);
} else {
Box = Mesh->getBoundingBox();
}
}
//! renders the node.
@ -218,15 +214,7 @@ void CAnimatedMeshSceneNode::render()
++PassCount;
scene::IMesh *m = getMeshForCurrentFrame();
if (m) {
Box = m->getBoundingBox();
} else {
#ifdef _DEBUG
os::Printer::log("Animated Mesh returned no mesh to render.", ELL_WARNING);
#endif
return;
}
assert(m);
driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
@ -294,11 +282,12 @@ void CAnimatedMeshSceneNode::render()
if (DebugDataVisible & scene::EDS_SKELETON) {
if (Mesh->getMeshType() == EAMT_SKINNED) {
// draw skeleton
for (auto *joint : ((SkinnedMesh *)Mesh)->getAllJoints()) {
for (const auto *childJoint : joint->Children) {
driver->draw3DLine(joint->GlobalAnimatedMatrix.getTranslation(),
childJoint->GlobalAnimatedMatrix.getTranslation(),
const auto &joints = (static_cast<SkinnedMesh *>(Mesh))->getAllJoints();
for (u16 i = 0; i < PerJoint.GlobalMatrices.size(); ++i) {
const auto translation = PerJoint.GlobalMatrices[i].getTranslation();
if (auto pjid = joints[i]->ParentJointID) {
const auto parent_translation = PerJoint.GlobalMatrices[*pjid].getTranslation();
driver->draw3DLine(parent_translation, translation,
video::SColor(255, 51, 66, 255));
}
}
@ -407,12 +396,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(const c8 *jointName)
return 0;
}
if (JointChildSceneNodes.size() <= *number) {
if (PerJoint.SceneNodes.size() <= *number) {
os::Printer::log("Joint was found in mesh, but is not loaded into node", jointName, ELL_WARNING);
return 0;
}
return JointChildSceneNodes[*number];
return PerJoint.SceneNodes[*number];
}
//! Returns a pointer to a child node, which has the same transformation as
@ -426,12 +415,12 @@ IBoneSceneNode *CAnimatedMeshSceneNode::getJointNode(u32 jointID)
checkJoints();
if (JointChildSceneNodes.size() <= jointID) {
if (PerJoint.SceneNodes.size() <= jointID) {
os::Printer::log("Joint not loaded into node", ELL_WARNING);
return 0;
}
return JointChildSceneNodes[jointID];
return PerJoint.SceneNodes[jointID];
}
//! Gets joint count.
@ -452,9 +441,9 @@ bool CAnimatedMeshSceneNode::removeChild(ISceneNode *child)
{
if (ISceneNode::removeChild(child)) {
if (JointsUsed) { // stop weird bugs caused while changing parents as the joints are being created
for (u32 i = 0; i < JointChildSceneNodes.size(); ++i) {
if (JointChildSceneNodes[i] == child) {
JointChildSceneNodes[i] = 0; // remove link to child
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (PerJoint.SceneNodes[i] == child) {
PerJoint.SceneNodes[i] = 0; // remove link to child
break;
}
}
@ -478,22 +467,6 @@ bool CAnimatedMeshSceneNode::getLoopMode() const
return Looping;
}
//! Sets a callback interface which will be called if an animation
//! playback has ended. Set this to 0 to disable the callback again.
void CAnimatedMeshSceneNode::setAnimationEndCallback(IAnimationEndCallBack *callback)
{
if (callback == LoopCallBack)
return;
if (LoopCallBack)
LoopCallBack->drop();
LoopCallBack = callback;
if (LoopCallBack)
LoopCallBack->grab();
}
//! Sets if the scene node should not copy the materials of the mesh but use them in a read only style.
void CAnimatedMeshSceneNode::setReadOnlyMaterials(bool readonly)
{
@ -525,19 +498,16 @@ void CAnimatedMeshSceneNode::setMesh(IAnimatedMesh *mesh)
// get materials and bounding box
Box = Mesh->getBoundingBox();
IMesh *m = Mesh->getMesh(0);
if (m) {
Materials.clear();
Materials.reallocate(m->getMeshBufferCount());
Materials.reallocate(Mesh->getMeshBufferCount());
for (u32 i = 0; i < m->getMeshBufferCount(); ++i) {
IMeshBuffer *mb = m->getMeshBuffer(i);
for (u32 i = 0; i < Mesh->getMeshBufferCount(); ++i) {
IMeshBuffer *mb = Mesh->getMeshBuffer(i);
if (mb)
Materials.push_back(mb->getMaterial());
else
Materials.push_back(video::SMaterial());
}
}
// clean up joint nodes
if (JointsUsed) {
@ -556,14 +526,7 @@ void CAnimatedMeshSceneNode::updateAbsolutePosition()
IAnimatedMeshSceneNode::updateAbsolutePosition();
}
//! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set)
void CAnimatedMeshSceneNode::setJointMode(E_JOINT_UPDATE_ON_RENDER mode)
{
checkJoints();
JointMode = mode;
}
//! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2)
//! Sets the transition time in seconds (note: This needs to enable joints)
//! you must call animateJoints(), or the mesh will not animate
void CAnimatedMeshSceneNode::setTransitionTime(f32 time)
{
@ -571,10 +534,6 @@ void CAnimatedMeshSceneNode::setTransitionTime(f32 time)
if (TransitionTime == ttime)
return;
TransitionTime = ttime;
if (ttime != 0)
setJointMode(EJUOR_CONTROL);
else
setJointMode(EJUOR_NONE);
}
//! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected)
@ -583,120 +542,104 @@ void CAnimatedMeshSceneNode::setRenderFromIdentity(bool enable)
RenderFromIdentity = enable;
}
//! updates the joint positions of this mesh
void CAnimatedMeshSceneNode::animateJoints(bool CalculateAbsolutePositions)
void CAnimatedMeshSceneNode::addJoints()
{
if (Mesh && Mesh->getMeshType() == EAMT_SKINNED) {
const auto &joints = static_cast<SkinnedMesh*>(Mesh)->getAllJoints();
PerJoint.setN(joints.size());
PerJoint.SceneNodes.clear();
PerJoint.SceneNodes.reserve(joints.size());
for (size_t i = 0; i < joints.size(); ++i) {
const auto *joint = joints[i];
ISceneNode *parent = this;
if (joint->ParentJointID)
parent = PerJoint.SceneNodes.at(*joint->ParentJointID); // exists because of topo. order
assert(parent);
const auto *matrix = std::get_if<core::matrix4>(&joint->transform);
PerJoint.SceneNodes.push_back(new CBoneSceneNode(
parent, SceneManager, 0, i, joint->Name,
matrix ? core::Transform{} : std::get<core::Transform>(joint->transform),
matrix ? *matrix : std::optional<core::matrix4>{}));
}
}
void CAnimatedMeshSceneNode::updateJointSceneNodes(
const std::vector<SkinnedMesh::SJoint::VariantTransform> &transforms)
{
for (size_t i = 0; i < transforms.size(); ++i) {
const auto &transform = transforms[i];
auto *node = static_cast<CBoneSceneNode*>(PerJoint.SceneNodes[i]);
if (const auto *trs = std::get_if<core::Transform>(&transform)) {
node->setTransform(*trs);
// .x lets animations override matrix transforms entirely.
node->Matrix = std::nullopt;
} else {
node->Matrix = std::get<core::matrix4>(transform);
}
}
}
//! updates the joint positions of this mesh
void CAnimatedMeshSceneNode::animateJoints()
{
if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED)
return;
checkJoints();
const f32 frame = getFrameNr(); // old?
SkinnedMesh *skinnedMesh = static_cast<SkinnedMesh *>(Mesh);
skinnedMesh->animateMesh(frame);
skinnedMesh->recoverJointsFromMesh(JointChildSceneNodes);
if (!skinnedMesh->isStatic())
updateJointSceneNodes(skinnedMesh->animateMesh(getFrameNr()));
//-----------------------------------------
// Transition
//-----------------------------------------
if (Transiting != 0.f) {
// Init additional matrices
if (PretransitingSave.size() < JointChildSceneNodes.size()) {
for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n)
PretransitingSave.push_back(core::matrix4());
}
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) {
//------Position------
JointChildSceneNodes[n]->setPosition(
core::lerp(
PretransitingSave[n].getTranslation(),
JointChildSceneNodes[n]->getPosition(),
TransitingBlend));
//------Rotation------
// Code is slow, needs to be fixed up
const core::quaternion RotationStart(PretransitingSave[n].getRotationRadians());
const core::quaternion RotationEnd(JointChildSceneNodes[n]->getRotation() * core::DEGTORAD);
core::quaternion QRotation;
QRotation.slerp(RotationStart, RotationEnd, TransitingBlend);
core::vector3df tmpVector;
QRotation.toEuler(tmpVector);
tmpVector *= core::RADTODEG; // convert from radians back to degrees
JointChildSceneNodes[n]->setRotation(tmpVector);
//------Scale------
// JointChildSceneNodes[n]->setScale(
// core::lerp(
// PretransitingSave[n].getScale(),
// JointChildSceneNodes[n]->getScale(),
// TransitingBlend));
}
}
if (CalculateAbsolutePositions) {
//---slow---
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n) {
if (JointChildSceneNodes[n]->getParent() == this) {
JointChildSceneNodes[n]->updateAbsolutePositionOfAllChildren(); // temp, should be an option
}
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (PerJoint.PreTransSaves[i]) {
PerJoint.SceneNodes[i]->setTransform(PerJoint.PreTransSaves[i]->interpolate(
PerJoint.SceneNodes[i]->getTransform(), TransitingBlend));
}
}
}
}
/*!
*/
void CAnimatedMeshSceneNode::checkJoints()
{
if (!Mesh || Mesh->getMeshType() != EAMT_SKINNED)
return;
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);
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i)
removeChild(PerJoint.SceneNodes[i]);
addJoints();
JointsUsed = true;
JointMode = EJUOR_READ;
}
}
/*!
*/
void CAnimatedMeshSceneNode::copyOldTransforms()
{
for (u32 i = 0; i < PerJoint.SceneNodes.size(); ++i) {
if (!PerJoint.SceneNodes[i]->Matrix) {
PerJoint.PreTransSaves[i] = PerJoint.SceneNodes[i]->getTransform();
} else {
PerJoint.PreTransSaves[i] = std::nullopt;
}
}
}
void CAnimatedMeshSceneNode::beginTransition()
{
if (!JointsUsed)
return;
if (TransitionTime != 0) {
// Check the array is big enough
if (PretransitingSave.size() < JointChildSceneNodes.size()) {
for (u32 n = PretransitingSave.size(); n < JointChildSceneNodes.size(); ++n)
PretransitingSave.push_back(core::matrix4());
}
// Copy the position of joints
for (u32 n = 0; n < JointChildSceneNodes.size(); ++n)
PretransitingSave[n] = JointChildSceneNodes[n]->getRelativeTransformation();
Transiting = core::reciprocal((f32)TransitionTime);
}
TransitingBlend = 0.f;
}
/*!
*/
ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager *newManager)
{
if (!newParent)
@ -722,19 +665,15 @@ ISceneNode *CAnimatedMeshSceneNode::clone(ISceneNode *newParent, ISceneManager *
newNode->EndFrame = EndFrame;
newNode->FramesPerSecond = FramesPerSecond;
newNode->CurrentFrameNr = CurrentFrameNr;
newNode->JointMode = JointMode;
newNode->JointsUsed = JointsUsed;
newNode->TransitionTime = TransitionTime;
newNode->Transiting = Transiting;
newNode->TransitingBlend = TransitingBlend;
newNode->Looping = Looping;
newNode->ReadOnlyMaterials = ReadOnlyMaterials;
newNode->LoopCallBack = LoopCallBack;
if (newNode->LoopCallBack)
newNode->LoopCallBack->grab();
newNode->PassCount = PassCount;
newNode->JointChildSceneNodes = JointChildSceneNodes;
newNode->PretransitingSave = PretransitingSave;
newNode->PerJoint.SceneNodes = PerJoint.SceneNodes;
newNode->PerJoint.PreTransSaves = PerJoint.PreTransSaves;
newNode->RenderFromIdentity = RenderFromIdentity;
return newNode;

View file

@ -4,9 +4,12 @@
#pragma once
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "IAnimatedMesh.h"
#include "SkinnedMesh.h"
#include "Transform.h"
#include "matrix4.h"
namespace irr
@ -54,9 +57,11 @@ public:
//! returns the current loop mode
bool getLoopMode() const override;
//! Sets a callback interface which will be called if an animation
//! playback has ended. Set this to 0 to disable the callback again.
void setAnimationEndCallback(IAnimationEndCallBack *callback = 0) override;
void setOnAnimateCallback(
const std::function<void(f32 dtime)> &cb) override
{
OnAnimateCallback = cb;
}
//! sets the speed with which the animation is played
//! NOTE: setMesh will also change this value and set it to the default speed of the mesh
@ -117,15 +122,16 @@ public:
//! updates the absolute position based on the relative and the parents position
void updateAbsolutePosition() override;
//! Set the joint update mode (0-unused, 1-get joints only, 2-set joints only, 3-move and set)
void setJointMode(E_JOINT_UPDATE_ON_RENDER mode) override;
//! Sets the transition time in seconds (note: This needs to enable joints, and setJointmode maybe set to 2)
//! Sets the transition time in seconds (note: This needs to enable joints)
//! you must call animateJoints(), or the mesh will not animate
void setTransitionTime(f32 Time) override;
void updateJointSceneNodes(const std::vector<SkinnedMesh::SJoint::VariantTransform> &transforms);
//! updates the joint positions of this mesh
void animateJoints(bool CalculateAbsolutePositions = true) override;
void animateJoints() override;
void addJoints();
//! render mesh ignoring its transformation. Used with ragdolls. (culling is unaffected)
void setRenderFromIdentity(bool On) override;
@ -142,6 +148,7 @@ private:
void buildFrameNr(u32 timeMs);
void checkJoints();
void copyOldTransforms();
void beginTransition();
core::array<video::SMaterial> Materials;
@ -158,19 +165,30 @@ private:
f32 Transiting; // is mesh transiting (plus cache of TransitionTime)
f32 TransitingBlend; // 0-1, calculated on buildFrameNr
// 0-unused, 1-get joints only, 2-set joints only, 3-move and set
E_JOINT_UPDATE_ON_RENDER JointMode;
bool JointsUsed;
bool Looping;
bool ReadOnlyMaterials;
bool RenderFromIdentity;
IAnimationEndCallBack *LoopCallBack;
s32 PassCount;
std::function<void(f32)> OnAnimateCallback;
std::vector<IBoneSceneNode *> JointChildSceneNodes;
core::array<core::matrix4> PretransitingSave;
struct PerJointData {
std::vector<CBoneSceneNode *> SceneNodes;
std::vector<core::matrix4> GlobalMatrices;
std::vector<std::optional<core::Transform>> PreTransSaves;
void setN(u16 n) {
SceneNodes.clear();
SceneNodes.resize(n);
GlobalMatrices.clear();
GlobalMatrices.resize(n);
PreTransSaves.clear();
PreTransSaves.resize(n);
}
};
PerJointData PerJoint;
};
} // end namespace scene

View file

@ -143,31 +143,25 @@ bool CB3DMeshFileLoader::readChunkNODE(SkinnedMesh::SJoint *inJoint)
os::Printer::log(logStr.c_str(), joint->Name.value_or("").c_str(), ELL_DEBUG);
#endif
f32 position[3], scale[3], rotation[4];
core::Transform transform;
{
f32 t[3], s[3], r[4];
readFloats(position, 3);
readFloats(scale, 3);
readFloats(rotation, 4);
readFloats(t, 3);
readFloats(s, 3);
readFloats(r, 4);
joint->Animatedposition = core::vector3df(position[0], position[1], position[2]);
joint->Animatedscale = core::vector3df(scale[0], scale[1], scale[2]);
joint->Animatedrotation = core::quaternion(rotation[1], rotation[2], rotation[3], rotation[0]);
// Build LocalMatrix:
core::matrix4 positionMatrix;
positionMatrix.setTranslation(joint->Animatedposition);
core::matrix4 scaleMatrix;
scaleMatrix.setScale(joint->Animatedscale);
core::matrix4 rotationMatrix;
joint->Animatedrotation.getMatrix_transposed(rotationMatrix);
joint->LocalMatrix = positionMatrix * rotationMatrix * scaleMatrix;
joint->transform = transform = {
{t[0], t[1], t[2]},
{r[1], r[2], r[3], r[0]},
{s[0], s[1], s[2]},
};
}
if (inJoint)
joint->GlobalMatrix = inJoint->GlobalMatrix * joint->LocalMatrix;
joint->GlobalMatrix = inJoint->GlobalMatrix * transform.buildMatrix();
else
joint->GlobalMatrix = joint->LocalMatrix;
joint->GlobalMatrix = transform.buildMatrix();
while (B3dStack.getLast().startposition + B3dStack.getLast().length > B3DFile->getPos()) // this chunk repeats
{

View file

@ -1,86 +0,0 @@
// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h
#include "CBoneSceneNode.h"
#include <optional>
namespace irr
{
namespace scene
{
//! constructor
CBoneSceneNode::CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr, s32 id,
u32 boneIndex, const std::optional<std::string> &boneName) :
IBoneSceneNode(parent, mgr, id),
BoneIndex(boneIndex),
AnimationMode(EBAM_AUTOMATIC), SkinningSpace(EBSS_LOCAL)
{
setName(boneName);
}
//! Returns the index of the bone
u32 CBoneSceneNode::getBoneIndex() const
{
return BoneIndex;
}
//! Sets the animation mode of the bone. Returns true if successful.
bool CBoneSceneNode::setAnimationMode(E_BONE_ANIMATION_MODE mode)
{
AnimationMode = mode;
return true;
}
//! Gets the current animation mode of the bone
E_BONE_ANIMATION_MODE CBoneSceneNode::getAnimationMode() const
{
return AnimationMode;
}
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &CBoneSceneNode::getBoundingBox() const
{
return Box;
}
/*
//! Returns the relative transformation of the scene node.
core::matrix4 CBoneSceneNode::getRelativeTransformation() const
{
return core::matrix4(); // RelativeTransformation;
}
*/
void CBoneSceneNode::OnAnimate(u32 timeMs)
{
if (IsVisible) {
// update absolute position
// updateAbsolutePosition();
// perform the post render process on all children
ISceneNodeList::iterator it = Children.begin();
for (; it != Children.end(); ++it)
(*it)->OnAnimate(timeMs);
}
}
void CBoneSceneNode::helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node)
{
Node->updateAbsolutePosition();
ISceneNodeList::const_iterator it = Node->getChildren().begin();
for (; it != Node->getChildren().end(); ++it) {
helper_updateAbsolutePositionOfAllChildren((*it));
}
}
void CBoneSceneNode::updateAbsolutePositionOfAllChildren()
{
helper_updateAbsolutePositionOfAllChildren(this);
}
} // namespace scene
} // namespace irr

View file

@ -7,6 +7,8 @@
// Used with SkinnedMesh and IAnimatedMeshSceneNode, for boned meshes
#include "IBoneSceneNode.h"
#include "Transform.h"
#include "matrix4.h"
#include <optional>
@ -21,49 +23,48 @@ public:
//! constructor
CBoneSceneNode(ISceneNode *parent, ISceneManager *mgr,
s32 id = -1, u32 boneIndex = 0,
const std::optional<std::string> &boneName = std::nullopt);
//! Returns the index of the bone
u32 getBoneIndex() const override;
//! Sets the animation mode of the bone. Returns true if successful.
bool setAnimationMode(E_BONE_ANIMATION_MODE mode) override;
//! Gets the current animation mode of the bone
E_BONE_ANIMATION_MODE getAnimationMode() const override;
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &getBoundingBox() const override;
/*
//! Returns the relative transformation of the scene node.
//core::matrix4 getRelativeTransformation() const override;
*/
void OnAnimate(u32 timeMs) override;
void updateAbsolutePositionOfAllChildren() override;
//! How the relative transformation of the bone is used
void setSkinningSpace(E_BONE_SKINNING_SPACE space) override
const std::optional<std::string> &boneName = std::nullopt,
const core::Transform &transform = {},
const std::optional<core::matrix4> &matrix = std::nullopt) :
IBoneSceneNode(parent, mgr, id, boneIndex, boneName),
Matrix(matrix)
{
SkinningSpace = space;
setTransform(transform);
}
E_BONE_SKINNING_SPACE getSkinningSpace() const override
void setTransform(const core::Transform &transform)
{
return SkinningSpace;
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);
}
private:
void helper_updateAbsolutePositionOfAllChildren(ISceneNode *Node);
core::Transform getTransform() const
{
return {
getPosition(),
core::quaternion(getRotation() * core::DEGTORAD).makeInverse(),
getScale()
};
}
u32 BoneIndex;
core::matrix4 getRelativeTransformation() const override
{
if (Matrix)
return *Matrix;
return IBoneSceneNode::getRelativeTransformation();
}
core::aabbox3d<f32> Box{-1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
E_BONE_ANIMATION_MODE AnimationMode;
E_BONE_SKINNING_SPACE SkinningSpace;
//! Some file formats alternatively let bones specify a transformation matrix.
//! If this is set, it overrides the TRS properties.
std::optional<core::matrix4> Matrix;
};
} // end namespace scene

View file

@ -539,34 +539,25 @@ static core::matrix4 loadTransform(const tiniergltf::Node::Matrix &m, SkinnedMes
mat[i] = static_cast<f32>(m[i]);
mat = convertHandedness(mat);
// Decompose the matrix into translation, scale, and rotation.
joint->Animatedposition = mat.getTranslation();
auto scale = mat.getScale();
joint->Animatedscale = scale;
joint->Animatedrotation = mat.getRotationRadians(scale);
// Invert the rotation because it is applied using `getMatrix_transposed`,
// which again inverts.
joint->Animatedrotation.makeInverse();
// Note: "When a node is targeted for animation [...],
// only TRS properties MAY be present; matrix MUST NOT be present."
// Thus we MUST NOT do any decomposition, which in general need not exist.
joint->transform = mat;
return mat;
}
static core::matrix4 loadTransform(const tiniergltf::Node::TRS &trs, SkinnedMesh::SJoint *joint)
{
const auto &trans = trs.translation;
const auto &rot = trs.rotation;
const auto &scale = trs.scale;
core::matrix4 transMat;
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(joint->Animatedscale);
return transMat * rotMat * scaleMat;
const auto &t = trs.translation;
const auto &r = trs.rotation;
const auto &s = trs.scale;
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]),
};
joint->transform = transform;
return transform.buildMatrix();
}
static core::matrix4 loadTransform(std::optional<std::variant<tiniergltf::Node::Matrix, tiniergltf::Node::TRS>> transform,
@ -584,8 +575,7 @@ void SelfType::MeshExtractor::loadNode(
const auto &node = m_gltf_model.nodes->at(nodeIdx);
auto *joint = m_irr_model->addJoint(parent);
const core::matrix4 transform = loadTransform(node.transform, joint);
joint->LocalMatrix = transform;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * joint->LocalMatrix : joint->LocalMatrix;
joint->GlobalMatrix = parent ? parent->GlobalMatrix * transform : transform;
if (node.name.has_value()) {
joint->Name = node.name->c_str();
}
@ -642,7 +632,6 @@ 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);
bool interpolate = ([&]() {
@ -663,6 +652,11 @@ void SelfType::MeshExtractor::loadAnimation(const std::size_t animIdx)
throw std::runtime_error("no animated node");
auto *joint = m_loaded_nodes.at(*channel.target.node);
if (std::holds_alternative<core::matrix4>(joint->transform)) {
warn("nodes using matrix transforms must not be animated");
continue;
}
switch (channel.target.path) {
case tiniergltf::AnimationChannelTarget::Path::TRANSLATION: {
const auto outputAccessor = Accessor<core::vector3df>::make(m_gltf_model, sampler.output);

View file

@ -320,7 +320,6 @@ set(IRRMESHLOADER
add_library(IRRMESHOBJ OBJECT
SkinnedMesh.cpp
CBoneSceneNode.cpp
CMeshSceneNode.cpp
CAnimatedMeshSceneNode.cpp
${IRRMESHLOADER}

View file

@ -35,7 +35,7 @@ void CMeshCache::removeMesh(const IMesh *const mesh)
if (!mesh)
return;
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) {
if (Meshes[i].Mesh == mesh) {
Meshes[i].Mesh->drop();
Meshes.erase(i);
return;
@ -53,7 +53,7 @@ u32 CMeshCache::getMeshCount() const
s32 CMeshCache::getMeshIndex(const IMesh *const mesh) const
{
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh))
if (Meshes[i].Mesh == mesh)
return (s32)i;
}
@ -93,7 +93,7 @@ const io::SNamedPath &CMeshCache::getMeshName(const IMesh *const mesh) const
return emptyNamedPath;
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh))
if (Meshes[i].Mesh == mesh)
return Meshes[i].NamedPath;
}
@ -115,7 +115,7 @@ bool CMeshCache::renameMesh(u32 index, const io::path &name)
bool CMeshCache::renameMesh(const IMesh *const mesh, const io::path &name)
{
for (u32 i = 0; i < Meshes.size(); ++i) {
if (Meshes[i].Mesh == mesh || (Meshes[i].Mesh && Meshes[i].Mesh->getMesh(0) == mesh)) {
if (Meshes[i].Mesh == mesh) {
Meshes[i].NamedPath.setPath(name);
Meshes.sort();
return true;

View file

@ -6,7 +6,6 @@
#include "SkinnedMesh.h"
#include "SMesh.h"
#include "CMeshBuffer.h"
#include "SAnimatedMesh.h"
#include "os.h"
#include <cassert>
@ -178,34 +177,5 @@ SMesh *CMeshManipulator::createMeshCopy(scene::IMesh *mesh) const
return clone;
}
//! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IMesh *mesh) const
{
if (!mesh)
return 0;
s32 trianglecount = 0;
for (u32 g = 0; g < mesh->getMeshBufferCount(); ++g)
trianglecount += mesh->getMeshBuffer(g)->getIndexCount() / 3;
return trianglecount;
}
//! Returns amount of polygons in mesh.
s32 CMeshManipulator::getPolyCount(scene::IAnimatedMesh *mesh) const
{
if (mesh && mesh->getMaxFrameNumber() != 0)
return getPolyCount(mesh->getMesh(0));
return 0;
}
//! create a new AnimatedMesh and adds the mesh to it
IAnimatedMesh *CMeshManipulator::createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const
{
return new SAnimatedMesh(mesh, type);
}
} // end namespace scene
} // end namespace irr

View file

@ -31,15 +31,6 @@ public:
//! Clones a static IMesh into a modifiable SMesh.
SMesh *createMeshCopy(scene::IMesh *mesh) const override;
//! Returns amount of polygons in mesh.
s32 getPolyCount(scene::IMesh *mesh) const override;
//! Returns amount of polygons in mesh.
s32 getPolyCount(scene::IAnimatedMesh *mesh) const override;
//! create a new AnimatedMesh and adds the mesh to it
IAnimatedMesh *createAnimatedMesh(scene::IMesh *mesh, scene::E_ANIMATED_MESH_TYPE type) const override;
};
} // end namespace scene

View file

@ -1238,7 +1238,7 @@ void CNullDriver::addOcclusionQuery(scene::ISceneNode *node, const scene::IMesh
else if (node->getType() == scene::ESNT_MESH)
mesh = static_cast<scene::IMeshSceneNode *>(node)->getMesh();
else
mesh = static_cast<scene::IAnimatedMeshSceneNode *>(node)->getMesh()->getMesh(0);
mesh = static_cast<scene::IAnimatedMeshSceneNode *>(node)->getMesh();
if (!mesh)
return;
}

View file

@ -7,7 +7,6 @@
#include "IVideoDriver.h"
#include "SMesh.h"
#include "SMeshBuffer.h"
#include "SAnimatedMesh.h"
#include "IReadFile.h"
#include "fast_atof.h"
#include "coreutil.h"
@ -272,23 +271,19 @@ IAnimatedMesh *COBJMeshFileLoader::createMesh(io::IReadFile *file)
}
}
// Create the Animated mesh if there's anything in the mesh
SAnimatedMesh *animMesh = 0;
if (0 != mesh->getMeshBufferCount()) {
mesh->recalculateBoundingBox();
animMesh = new SAnimatedMesh();
animMesh->Type = EAMT_OBJ;
animMesh->addMesh(mesh);
animMesh->recalculateBoundingBox();
}
// Clean up the allocate obj file contents
delete[] buf;
// more cleaning up
cleanUp();
mesh->drop();
return animMesh;
// Nothing in the mesh
if (mesh->getMeshBufferCount() == 0) {
mesh->drop();
return nullptr;
}
mesh->recalculateBoundingBox();
return mesh;
}
//! Read RGB color

View file

@ -8,7 +8,6 @@
#include "CSceneManager.h"
#include "IVideoDriver.h"
#include "IFileSystem.h"
#include "SAnimatedMesh.h"
#include "CMeshCache.h"
#include "IGUIEnvironment.h"
#include "IMaterialRenderer.h"

View file

@ -4,6 +4,7 @@
#include "CXMeshFileLoader.h"
#include "SkinnedMesh.h"
#include "Transform.h"
#include "os.h"
#include "fast_atof.h"
@ -513,6 +514,7 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
if (n.has_value()) {
JointID = *n;
joint = AnimatedMesh->getAllJoints()[JointID];
joint->setParent(Parent);
}
}
@ -527,8 +529,6 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
#ifdef _XREADER_DEBUG
os::Printer::log("using joint ", name.c_str(), ELL_DEBUG);
#endif
if (Parent)
Parent->Children.push_back(joint);
}
// Now inside a frame.
@ -552,12 +552,10 @@ bool CXMeshFileLoader::parseDataObjectFrame(SkinnedMesh::SJoint *Parent)
if (!parseDataObjectFrame(joint))
return false;
} else if (objectName == "FrameTransformMatrix") {
if (!parseDataObjectTransformationMatrix(joint->LocalMatrix))
core::matrix4 matrix;
if (!parseDataObjectTransformationMatrix(matrix))
return false;
// joint->LocalAnimatedMatrix
// joint->LocalAnimatedMatrix.makeInverse();
// joint->LocalMatrix=tmp*joint->LocalAnimatedMatrix;
joint->transform = matrix;
} else if (objectName == "Mesh") {
/*
frame.Meshes.push_back(SXMesh());

View file

@ -7,7 +7,15 @@
#include "CBoneSceneNode.h"
#include "IAnimatedMeshSceneNode.h"
#include "SSkinMeshBuffer.h"
#include "Transform.h"
#include "aabbox3d.h"
#include "irrMath.h"
#include "matrix4.h"
#include "os.h"
#include "vector3d.h"
#include <cassert>
#include <cstddef>
#include <variant>
#include <vector>
#include <cassert>
@ -48,154 +56,55 @@ void SkinnedMesh::setAnimationSpeed(f32 fps)
FramesPerSecond = fps;
}
//! returns the animated mesh based
IMesh *SkinnedMesh::getMesh(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)
using VariantTransform = SkinnedMesh::SJoint::VariantTransform;
std::vector<VariantTransform> SkinnedMesh::animateMesh(f32 frame)
{
if (!HasAnimation || LastAnimatedFrame == frame)
return;
LastAnimatedFrame = frame;
SkinnedLastFrame = false;
for (auto *joint : AllJoints) {
// The joints can be animated here with no input from their
// parents, but for setAnimationMode extra checks are needed
// to their parents
joint->keys.updateTransform(frame,
joint->Animatedposition,
joint->Animatedrotation,
joint->Animatedscale);
assert(HasAnimation);
std::vector<VariantTransform> result;
result.reserve(AllJoints.size());
for (auto *joint : AllJoints)
result.push_back(joint->animate(frame));
return result;
}
// 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();
}
void SkinnedMesh::buildAllLocalAnimatedMatrices()
core::aabbox3df SkinnedMesh::calculateBoundingBox(
const std::vector<core::matrix4> &global_transforms)
{
for (auto *joint : AllJoints) {
// Could be faster:
if (!joint->keys.empty()) {
joint->GlobalSkinningSpace = false;
// IRR_TEST_BROKEN_QUATERNION_USE: TODO - switched to getMatrix_transposed instead of getMatrix for downward compatibility.
// Not tested so far if this was correct or wrong before quaternion fix!
// Note that using getMatrix_transposed inverts the rotation.
joint->Animatedrotation.getMatrix_transposed(joint->LocalAnimatedMatrix);
// --- joint->LocalAnimatedMatrix *= joint->Animatedrotation.getMatrix() ---
f32 *m1 = joint->LocalAnimatedMatrix.pointer();
core::vector3df &Pos = joint->Animatedposition;
m1[0] += Pos.X * m1[3];
m1[1] += Pos.Y * m1[3];
m1[2] += Pos.Z * m1[3];
m1[4] += Pos.X * m1[7];
m1[5] += Pos.Y * m1[7];
m1[6] += Pos.Z * m1[7];
m1[8] += Pos.X * m1[11];
m1[9] += Pos.Y * m1[11];
m1[10] += Pos.Z * m1[11];
m1[12] += Pos.X * m1[15];
m1[13] += Pos.Y * m1[15];
m1[14] += Pos.Z * m1[15];
// -----------------------------------
if (!joint->keys.scale.empty()) {
/*
core::matrix4 scaleMatrix;
scaleMatrix.setScale(joint->Animatedscale);
joint->LocalAnimatedMatrix *= scaleMatrix;
*/
// -------- joint->LocalAnimatedMatrix *= scaleMatrix -----------------
core::matrix4 &mat = joint->LocalAnimatedMatrix;
mat[0] *= joint->Animatedscale.X;
mat[1] *= joint->Animatedscale.X;
mat[2] *= joint->Animatedscale.X;
mat[3] *= joint->Animatedscale.X;
mat[4] *= joint->Animatedscale.Y;
mat[5] *= joint->Animatedscale.Y;
mat[6] *= joint->Animatedscale.Y;
mat[7] *= joint->Animatedscale.Y;
mat[8] *= joint->Animatedscale.Z;
mat[9] *= joint->Animatedscale.Z;
mat[10] *= joint->Animatedscale.Z;
mat[11] *= joint->Animatedscale.Z;
// -----------------------------------
assert(global_transforms.size() == AllJoints.size());
core::aabbox3df result = StaticPartsBox;
// skeletal animation
for (u16 i = 0; i < AllJoints.size(); ++i) {
auto box = AllJoints[i]->LocalBoundingBox;
global_transforms[i].transformBoxEx(box);
result.addInternalBox(box);
}
} else {
joint->LocalAnimatedMatrix = joint->LocalMatrix;
}
}
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->GlobalSkinningSpace)
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)
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 (u16 i = 0; i < AllJoints.size(); ++i) {
for (u32 j : AllJoints[i]->AttachedMeshes) {
auto box = (*SkinningBuffers)[j]->BoundingBox;
global_transforms[i].transformBoxEx(box);
result.addInternalBox(box);
}
}
return result;
}
// Software Skinning
void SkinnedMesh::skinMesh(const std::vector<core::matrix4> &global_matrices)
{
if (!HasAnimation)
return;
// 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 = joint->GlobalAnimatedMatrix;
Buffer->Transformation = global_matrices[i];
}
}
@ -204,27 +113,20 @@ void SkinnedMesh::skinMesh()
std::fill(buf.begin(), buf.end(), false);
// skin starting with the root joints
for (auto *rootJoint : RootJoints)
skinJoint(rootJoint, 0);
for (size_t i = 0; i < AllJoints.size(); ++i) {
auto *joint = AllJoints[i];
if (joint->Weights.empty())
continue;
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...
// 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);
@ -251,14 +153,11 @@ void SkinnedMesh::skinJoint(SJoint *joint, SJoint *parentJoint)
//*(weight._Pos) += thisVertexMove * weight.strength;
}
buffersUsed[weight.buffer_id]->boundingBoxNeedsRecalculated();
}
}
// Skin all children
for (auto *childJoint : joint->Children)
skinJoint(childJoint, joint);
for (auto *buffer : *SkinningBuffers)
buffer->setDirty(EBT_VERTEX);
}
//! Gets joint count.
@ -310,7 +209,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
@ -337,29 +236,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...
@ -384,72 +260,51 @@ 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<core::matrix4> &matrices) const
{
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];
}
}
}
if (!parentJoint)
joint->GlobalMatrix = joint->LocalMatrix;
else
joint->GlobalMatrix = parentJoint->GlobalMatrix * joint->LocalMatrix;
joint->LocalAnimatedMatrix = joint->LocalMatrix;
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) {
// meshes with weights are animatable
for (auto *joint : AllJoints) {
if (joint->Weights.size()) {
HasAnimation = true;
break;
}
if (!joint->Weights.empty()) {
return true;
}
}
if (HasAnimation) {
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());
}
}
if (HasAnimation && !PreparedForSkinning) {
PreparedForSkinning = true;
// check for bugs:
for (auto *joint : AllJoints) {
for (auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
@ -466,14 +321,11 @@ void SkinnedMesh::checkForAnimation()
}
}
// 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;
@ -482,15 +334,118 @@ void SkinnedMesh::checkForAnimation()
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();
}
}
void SkinnedMesh::calculateStaticBoundingBox()
{
std::vector<std::vector<bool>> animated(getMeshBufferCount());
for (u32 mb = 0; mb < getMeshBufferCount(); mb++)
animated[mb] = std::vector<bool>(getMeshBuffer(mb)->getVertexCount());
for (auto *joint : AllJoints) {
for (auto &weight : joint->Weights) {
const u16 buffer_id = weight.buffer_id;
const u32 vertex_id = weight.vertex_id;
animated[buffer_id][vertex_id] = true;
}
}
bool first = true;
for (u16 mb = 0; mb < getMeshBufferCount(); mb++) {
for (u32 v = 0; v < getMeshBuffer(mb)->getVertexCount(); v++) {
if (!animated[mb][v]) {
auto pos = getMeshBuffer(mb)->getVertexBuffer()->getPosition(v);
if (!first) {
StaticPartsBox.addInternalPoint(pos);
} else {
StaticPartsBox.reset(pos);
first = false;
}
}
}
}
}
void SkinnedMesh::calculateJointBoundingBoxes()
{
for (auto *joint : AllJoints) {
bool first = true;
for (auto &weight : joint->Weights) {
if (weight.strength < 1e-6)
continue;
auto pos = weight.StaticPos;
joint->GlobalInversedMatrix.value().transformVect(pos);
if (!first) {
joint->LocalBoundingBox.addInternalPoint(pos);
} else {
joint->LocalBoundingBox.reset(pos);
first = false;
}
}
}
}
void SkinnedMesh::calculateBufferBoundingBoxes()
{
for (u32 j = 0; j < LocalBuffers.size(); ++j) {
// If we use skeletal animation, this will just be a bounding box of the static pose;
// if we use rigid animation, this will correctly transform the points first.
LocalBuffers[j]->recalculateBoundingBox();
}
}
void SkinnedMesh::recalculateBaseBoundingBoxes() {
calculateStaticBoundingBox();
calculateJointBoundingBoxes();
calculateBufferBoundingBoxes();
}
void SkinnedMesh::topoSortJoints()
{
size_t n = AllJoints.size();
std::vector<u16> new_to_old_id;
std::vector<std::vector<u16>> children(n);
for (u16 i = 0; i < n; ++i) {
if (auto parentId = AllJoints[i]->ParentJointID)
children[*parentId].push_back(i);
else
new_to_old_id.push_back(i);
}
// Levelorder
for (u16 i = 0; i < n; ++i) {
new_to_old_id.insert(new_to_old_id.end(),
children[new_to_old_id[i]].begin(),
children[new_to_old_id[i]].end());
}
std::vector<u16> old_to_new_id(n);
for (u16 i = 0; i < n; ++i)
old_to_new_id[new_to_old_id[i]] = i;
std::vector<SJoint *> joints(n);
for (u16 i = 0; i < n; ++i) {
joints[i] = AllJoints[new_to_old_id[i]];
joints[i]->JointID = i;
if (auto parentId = joints[i]->ParentJointID)
joints[i]->ParentJointID = old_to_new_id[*parentId];
}
AllJoints = std::move(joints);
for (u16 i = 0; i < n; ++i) {
if (auto pjid = AllJoints[i]->ParentJointID)
assert(*pjid < i);
}
SkinnedLastFrame = false;
}
//! called by loader after populating with mesh and bone data
@ -498,98 +453,44 @@ SkinnedMesh *SkinnedMeshBuilder::finalize()
{
os::Printer::log("Skinned Mesh - finalize", ELL_DEBUG);
// Make sure we recalc the next frame
LastAnimatedFrame = -1;
SkinnedLastFrame = false;
// 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...
topoSortJoints();
// Set array sizes
for (u32 i = 0; i < LocalBuffers.size(); ++i) {
Vertices_Moved.emplace_back(LocalBuffers[i]->getVertexCount());
}
checkForAnimation();
prepareForSkinning();
if (HasAnimation) {
std::vector<core::matrix4> matrices;
matrices.reserve(AllJoints.size());
for (auto *joint : AllJoints) {
joint->keys.cleanup();
if (const auto *matrix = std::get_if<core::matrix4>(&joint->transform))
matrices.push_back(*matrix);
else
matrices.push_back(std::get<core::Transform>(joint->transform).buildMatrix());
}
calculateGlobalMatrices(matrices);
for (size_t i = 0; i < AllJoints.size(); ++i) {
auto *joint = AllJoints[i];
if (!joint->GlobalInversedMatrix) {
joint->GlobalInversedMatrix = matrices[i];
joint->GlobalInversedMatrix->makeInverse();
}
// Needed for animation and skinning...
calculateGlobalMatrices(0, 0);
// rigid animation for non animated meshes
for (auto *joint : AllJoints) {
for (u32 attachedMeshIdx : joint->AttachedMeshes) {
SSkinMeshBuffer *Buffer = (*SkinningBuffers)[attachedMeshIdx];
Buffer->Transformation = joint->GlobalAnimatedMatrix;
Buffer->Transformation = matrices[i];
}
}
// calculate bounding box
if (LocalBuffers.empty())
BoundingBox.reset(0, 0, 0);
else {
irr::core::aabbox3df bb(LocalBuffers[0]->BoundingBox);
LocalBuffers[0]->Transformation.transformBoxEx(bb);
BoundingBox.reset(bb);
for (u32 j = 1; j < LocalBuffers.size(); ++j) {
bb = LocalBuffers[j]->BoundingBox;
LocalBuffers[j]->Transformation.transformBoxEx(bb);
BoundingBox.addInternalBox(bb);
}
}
recalculateBaseBoundingBoxes();
StaticPoseBox = calculateBoundingBox(matrices);
return this;
}
void SkinnedMesh::updateBoundingBox()
{
if (!SkinningBuffers)
return;
BoundingBox.reset(0, 0, 0);
for (auto *buffer : *SkinningBuffers) {
buffer->recalculateBoundingBox();
core::aabbox3df bb = buffer->BoundingBox;
buffer->Transformation.transformBoxEx(bb);
BoundingBox.addInternalBox(bb);
}
}
scene::SSkinMeshBuffer *SkinnedMeshBuilder::addMeshBuffer()
{
scene::SSkinMeshBuffer *buffer = new scene::SSkinMeshBuffer();
@ -607,14 +508,10 @@ void SkinnedMeshBuilder::addMeshBuffer(SSkinMeshBuffer *meshbuf)
SkinnedMesh::SJoint *SkinnedMeshBuilder::addJoint(SJoint *parent)
{
SJoint *joint = new SJoint;
joint->setParent(parent);
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;
}
@ -684,73 +581,6 @@ void SkinnedMesh::normalizeWeights()
}
}
void SkinnedMesh::recoverJointsFromMesh(std::vector<IBoneSceneNode *> &jointChildSceneNodes)
{
for (u32 i = 0; i < AllJoints.size(); ++i) {
IBoneSceneNode *node = jointChildSceneNodes[i];
SJoint *joint = AllJoints[i];
node->setPosition(joint->LocalAnimatedMatrix.getTranslation());
node->setRotation(joint->LocalAnimatedMatrix.getRotationDegrees());
node->setScale(joint->LocalAnimatedMatrix.getScale());
node->updateAbsolutePosition();
}
}
void SkinnedMesh::transferJointsToMesh(const std::vector<IBoneSceneNode *> &jointChildSceneNodes)
{
for (u32 i = 0; i < AllJoints.size(); ++i) {
const IBoneSceneNode *const node = jointChildSceneNodes[i];
SJoint *joint = AllJoints[i];
joint->LocalAnimatedMatrix.setRotationDegrees(node->getRotation());
joint->LocalAnimatedMatrix.setTranslation(node->getPosition());
joint->LocalAnimatedMatrix *= core::matrix4().setScale(node->getScale());
joint->GlobalSkinningSpace = (node->getSkinningSpace() == EBSS_GLOBAL);
}
// Make sure we recalc the next frame
LastAnimatedFrame = -1;
SkinnedLastFrame = false;
}
void SkinnedMesh::addJoints(std::vector<IBoneSceneNode *> &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

View file

@ -916,12 +916,7 @@ struct Node {
std::optional<std::size_t> skin;
std::optional<std::vector<double>> weights;
Node(const Json::Value &o)
: transform(Matrix {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
})
: transform(TRS{})
{
check(o.isObject());
if (o.isMember("camera")) {

View file

@ -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<u64> logged;
@ -682,7 +673,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
m_animated_meshnode = m_smgr->addAnimatedMeshSceneNode(mesh, m_matrixnode);
m_animated_meshnode->grab();
mesh->drop(); // The scene node took hold of it
m_animated_meshnode->animateJoints(); // Needed for some animations
m_animated_meshnode->setScale(m_prop.visual_size);
// set vertex colors to ensure alpha is set
@ -693,6 +683,21 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
m_animated_meshnode->forEachMaterial([this] (auto &mat) {
mat.BackfaceCulling = m_prop.backface_culling;
});
m_animated_meshnode->setOnAnimateCallback([&](f32 dtime) {
for (auto &it : m_bone_override) {
auto* bone = m_animated_meshnode->getJointNode(it.first.c_str());
if (!bone)
continue;
BoneOverride &props = it.second;
props.dtime_passed += dtime;
bone->setPosition(props.getPosition(bone->getPosition()));
bone->setRotation(props.getRotationEulerDeg(bone->getRotation()));
bone->setScale(props.getScale(bone->getScale()));
}
});
} else
errorstream<<"GenericCAO::addToScene(): Could not load mesh "<<m_prop.mesh<<std::endl;
break;
@ -783,7 +788,6 @@ void GenericCAO::addToScene(ITextureSource *tsrc, scene::ISceneManager *smgr)
updateMarker();
updateNodePos();
updateAnimation();
updateBones(.0f);
updateAttachments();
setNodeLight(m_last_light);
updateMeshCulling();
@ -1174,18 +1178,6 @@ void GenericCAO::step(float dtime, ClientEnvironment *env)
rot_translator.val_current = m_rotation;
updateNodePos();
}
if (m_animated_meshnode) {
// Everything must be updated; the whole transform
// chain as well as the animated mesh node.
// Otherwise, bone attachments would be relative to
// a position that's one frame old.
if (m_matrixnode)
updatePositionRecursive(m_matrixnode);
m_animated_meshnode->updateAbsolutePosition();
m_animated_meshnode->animateJoints();
updateBones(dtime);
}
}
static void setMeshBufferTextureCoords(scene::IMeshBuffer *buf, const v2f *uv, u32 count)
@ -1394,44 +1386,6 @@ void GenericCAO::updateAnimationSpeed()
m_animated_meshnode->setAnimationSpeed(m_animation_speed);
}
void GenericCAO::updateBones(f32 dtime)
{
if (!m_animated_meshnode)
return;
if (m_bone_override.empty()) {
m_animated_meshnode->setJointMode(scene::EJUOR_NONE);
return;
}
m_animated_meshnode->setJointMode(scene::EJUOR_CONTROL); // To write positions to the mesh on render
for (auto &it : m_bone_override) {
std::string bone_name = it.first;
scene::IBoneSceneNode* bone = m_animated_meshnode->getJointNode(bone_name.c_str());
if (!bone)
continue;
BoneOverride &props = it.second;
props.dtime_passed += dtime;
bone->setPosition(props.getPosition(bone->getPosition()));
bone->setRotation(props.getRotationEulerDeg(bone->getRotation()));
bone->setScale(props.getScale(bone->getScale()));
}
// The following is needed for set_bone_pos to propagate to
// attached objects correctly.
// Irrlicht ought to do this, but doesn't when using EJUOR_CONTROL.
for (u32 i = 0; i < m_animated_meshnode->getJointCount(); ++i) {
auto bone = m_animated_meshnode->getJointNode(i);
// Look for the root bone.
if (bone && bone->getParent() == m_animated_meshnode) {
// Update entire skeleton.
bone->updateAbsolutePositionOfAllChildren();
break;
}
}
}
void GenericCAO::updateAttachments()
{
ClientActiveObject *parent = getParent();
@ -1747,7 +1701,6 @@ void GenericCAO::processMessage(const std::string &data)
} else {
m_bone_override[bone] = props;
}
// updateBones(); now called every step
} else if (cmd == AO_CMD_ATTACH_TO) {
u16 parent_id = readS16(is);
std::string bone = deSerializeString16(is);

View file

@ -286,8 +286,6 @@ public:
void updateAnimationSpeed();
void updateBones(f32 dtime);
void processMessage(const std::string &data) override;
bool directReportPunch(v3f dir, const ItemStack *punchitem,

View file

@ -10,10 +10,9 @@
#include <cmath>
#include <iostream>
#include <IAnimatedMesh.h>
#include <SAnimatedMesh.h>
#include <IAnimatedMeshSceneNode.h>
#include "S3DVertex.h"
#include "SMesh.h"
#include <SMesh.h>
#include "SMeshBuffer.h"
inline static void applyShadeFactor(video::SColor& color, float factor)
@ -97,11 +96,8 @@ scene::IAnimatedMesh* createCubeMesh(v3f scale)
mesh->addMeshBuffer(buf);
buf->drop();
}
scene::SAnimatedMesh *anim_mesh = new scene::SAnimatedMesh(mesh);
mesh->drop();
scaleMesh(anim_mesh, scale); // also recalculates bounding box
return anim_mesh;
scaleMesh(mesh, scale); // also recalculates bounding box
return mesh;
}
template<typename F>

View file

@ -4,7 +4,6 @@
#include "nodedef.h"
#include "SAnimatedMesh.h"
#include "itemdef.h"
#if CHECK_CLIENT_BUILD()
#include "client/mesh.h"
@ -964,13 +963,6 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc
// Note: By freshly reading, we get an unencumbered mesh.
if (scene::IMesh *src_mesh = client->getMesh(mesh)) {
bool apply_bs = false;
// For frame-animated meshes, always get the first frame,
// which holds a model for which we can eventually get the static pose.
while (auto *src_meshes = dynamic_cast<scene::SAnimatedMesh *>(src_mesh)) {
src_mesh = src_meshes->getMesh(0.0f);
src_mesh->grab();
src_meshes->drop();
}
if (auto *skinned_mesh = dynamic_cast<scene::SkinnedMesh *>(src_mesh)) {
// Compatibility: Animated meshes, as well as static gltf meshes, are not scaled by BS.
// See https://github.com/luanti-org/luanti/pull/16112#issuecomment-2881860329

View file

@ -59,6 +59,8 @@ set (UNITTEST_CLIENT_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test_eventmanager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_gameui.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_irr_gltf_mesh_loader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_irr_x_mesh_loader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_irr_matrix4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_mesh_compare.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_keycode.cpp
PARENT_SCOPE)

View file

@ -394,37 +394,41 @@ SECTION("simple skin")
const auto joints = csm->getAllJoints();
REQUIRE(joints.size() == 3);
const auto findJoint = [&](const std::function<bool(SkinnedMesh::SJoint*)> &predicate) {
for (std::size_t i = 0; i < joints.size(); ++i) {
if (predicate(joints[i])) {
return joints[i];
const auto findJoint = [&](const std::function<bool(const SkinnedMesh::SJoint*)> &predicate) {
for (const auto *joint : joints) {
if (predicate(joint)) {
return joint;
}
}
throw std::runtime_error("joint not found");
};
// Check the node hierarchy
const auto parent = findJoint([](auto joint) {
return !joint->Children.empty();
const auto child = findJoint([&](auto *joint) {
return !!joint->ParentJointID;
});
REQUIRE(parent->Children.size() == 1);
const auto child = parent->Children[0];
REQUIRE(child != parent);
const auto *parent = joints.at(*child->ParentJointID);
SECTION("transformations are correct")
{
CHECK(parent->Animatedposition == v3f(0, 0, 0));
CHECK(parent->Animatedrotation == irr::core::quaternion());
CHECK(parent->Animatedscale == v3f(1, 1, 1));
{
const auto &transform = std::get<core::Transform>(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 v3f childTranslation(0, 1, 0);
CHECK(child->Animatedposition == childTranslation);
CHECK(child->Animatedrotation == irr::core::quaternion());
CHECK(child->Animatedscale == v3f(1, 1, 1));
}
{
const auto &transform = std::get<core::Transform>(child->transform);
const v3f translation(0, 1, 0);
CHECK(transform.translation == translation);
CHECK(transform.rotation == irr::core::quaternion());
CHECK(transform.scale == v3f(1, 1, 1));
irr::core::matrix4 inverseBindMatrix;
inverseBindMatrix.setTranslation(-childTranslation);
inverseBindMatrix.setTranslation(-translation);
CHECK(child->GlobalInversedMatrix == inverseBindMatrix);
}
}
SECTION("weights are correct")
{

View file

@ -0,0 +1,111 @@
// Luanti
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "catch_amalgamated.hpp"
#include "content/subgames.h"
#include "filesys.h"
#include "irrlichttypes.h"
#include "irr_ptr.h"
#include "EDriverTypes.h"
#include "IFileSystem.h"
#include "IReadFile.h"
#include "ISceneManager.h"
#include "SkinnedMesh.h"
#include "irrlicht.h"
#include "catch.h"
#include "matrix4.h"
TEST_CASE("x") {
const auto gamespec = findSubgame("devtest");
if (!gamespec.isValid())
SKIP();
irr::SIrrlichtCreationParameters p;
p.DriverType = video::EDT_NULL;
auto *driver = irr::createDeviceEx(p);
REQUIRE(driver);
auto *smgr = driver->getSceneManager();
const auto loadMesh = [&] (const io::path& filepath) {
irr_ptr<io::IReadFile> file(driver->getFileSystem()->createAndOpenFile(filepath));
REQUIRE(file);
return smgr->getMesh(file.get());
};
const static auto model_stem = gamespec.gamemods_path +
DIR_DELIM + "testentities" + DIR_DELIM + "models" + DIR_DELIM + "testentities_";
SECTION("cool guy") {
const auto *mesh = dynamic_cast<irr::scene::SkinnedMesh*>(loadMesh(model_stem + "cool_guy.x"));
REQUIRE(mesh);
REQUIRE(mesh->getMeshBufferCount() == 1);
auto getJointId = [&](auto name) {
return mesh->getJointNumber(name).value();
};
const auto root = getJointId("Root");
const auto armature = getJointId("Armature");
const auto armature_body = getJointId("Armature_body");
const auto armature_arm_r = getJointId("Armature_arm_r");
std::vector<core::matrix4> matrices;
matrices.reserve(mesh->getJointCount());
for (auto *joint : mesh->getAllJoints()) {
if (const auto *matrix = std::get_if<core::matrix4>(&joint->transform))
matrices.push_back(*matrix);
else
matrices.push_back(std::get<core::Transform>(joint->transform).buildMatrix());
}
auto local_matrices = matrices;
mesh->calculateGlobalMatrices(matrices);
SECTION("joints are topologically sorted") {
REQUIRE(root < armature);
REQUIRE(armature < armature_body);
REQUIRE(armature_body < armature_arm_r);
}
SECTION("parents are correct") {
const auto get_parent = [&](auto id) {
return mesh->getAllJoints()[id]->ParentJointID;
};
REQUIRE(!get_parent(root));
REQUIRE(get_parent(armature).value() == root);
REQUIRE(get_parent(armature_body).value() == armature);
REQUIRE(get_parent(armature_arm_r).value() == armature_body);
}
SECTION("local matrices are correct") {
REQUIRE(local_matrices[root].equals(core::IdentityMatrix));
REQUIRE(local_matrices[armature].equals(core::IdentityMatrix));
REQUIRE(local_matrices[armature_body] == core::matrix4(
-1,0,0,0,
0,0,1,0,
0,1,0,0,
0,2.571201,0,1
));
REQUIRE(local_matrices[armature_arm_r] == core::matrix4(
-0.047733,0.997488,-0.05233,0,
0.901521,0.020464,-0.432251,0,
-0.430095,-0.067809,-0.900233,
0,-0.545315,0,1,1
));
}
SECTION("global matrices are correct") {
REQUIRE(matrices[armature_body] == local_matrices[armature_body]);
REQUIRE(matrices[armature_arm_r] ==
matrices[armature_body] * local_matrices[armature_arm_r]);
}
}
driver->closeDevice();
driver->drop();
}