diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h
index d2d6eba96..08cbf23b0 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.h
+++ b/Source/Engine/Content/Assets/SkinnedModel.h
@@ -64,7 +64,6 @@ public:
///
/// Gets the amount of loaded model LODs.
///
- /// Loaded LODs count
API_PROPERTY() FORCE_INLINE int32 GetLoadedLODs() const
{
return _loadedLODs;
@@ -93,7 +92,6 @@ public:
///
/// Gets index of the highest resident LOD (may be equal to LODs.Count if no LOD has been uploaded). Note: LOD=0 is the highest (top quality)
///
- /// LOD index
FORCE_INLINE int32 HighestResidentLODIndex() const
{
return GetLODsCount() - _loadedLODs;
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index a1a9d6db6..a62aad592 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -44,7 +44,8 @@ void AnimatedModel::UpdateAnimation()
|| !IsActiveInHierarchy()
|| SkinnedModel == nullptr
|| !SkinnedModel->IsLoaded()
- || _lastUpdateFrame == Engine::FrameCount)
+ || _lastUpdateFrame == Engine::FrameCount
+ || _masterPose)
return;
_lastUpdateFrame = Engine::FrameCount;
@@ -183,6 +184,17 @@ int32 AnimatedModel::FindClosestNode(const Vector3& location, bool worldSpace) c
return result;
}
+void AnimatedModel::SetMasterPoseModel(AnimatedModel* masterPose)
+{
+ if (masterPose == _masterPose)
+ return;
+ if (_masterPose)
+ _masterPose->AnimationUpdated.Unbind(this);
+ _masterPose = masterPose;
+ if (_masterPose)
+ _masterPose->AnimationUpdated.Bind(this);
+}
+
#define CHECK_ANIM_GRAPH_PARAM_ACCESS() \
if (!AnimationGraph) \
{ \
@@ -378,6 +390,7 @@ void AnimatedModel::BeginPlay(SceneBeginData* data)
void AnimatedModel::EndPlay()
{
AnimationManager::RemoveFromUpdate(this);
+ SetMasterPoseModel(nullptr);
// Base
ModelInstanceActor::EndPlay();
@@ -443,11 +456,21 @@ void AnimatedModel::UpdateBounds()
void AnimatedModel::OnAnimationUpdated()
{
ANIM_GRAPH_PROFILE_EVENT("OnAnimationUpdated");
+ auto& skeleton = SkinnedModel->Skeleton;
+
+ // Copy pose from the master
+ if (_masterPose && _masterPose->SkinnedModel->Skeleton.Nodes.Count() == skeleton.Nodes.Count())
+ {
+ ANIM_GRAPH_PROFILE_EVENT("Copy Master Pose");
+ const auto& masterInstance = _masterPose->GraphInstance;
+ GraphInstance.NodesPose = masterInstance.NodesPose;
+ GraphInstance.RootTransform = masterInstance.RootTransform;
+ GraphInstance.RootMotion = masterInstance.RootMotion;
+ }
// Calculate the final bones transformations and update skinning
{
ANIM_GRAPH_PROFILE_EVENT("Final Pose");
- auto& skeleton = SkinnedModel->Skeleton;
UpdateBones.Resize(skeleton.Bones.Count(), false);
for (int32 boneIndex = 0; boneIndex < skeleton.Bones.Count(); boneIndex++)
{
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index 6317e280c..df218b897 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -65,6 +65,7 @@ private:
float _lastMinDstSqr;
uint64 _lastUpdateFrame;
BlendShapesInstance _blendShapes;
+ ScriptingObjectReference _masterPose;
public:
@@ -224,6 +225,12 @@ public:
/// The zero-based index of the found node. Returns -1 if skeleton is missing.
API_FUNCTION() int32 FindClosestNode(const Vector3& location, bool worldSpace = false) const;
+ ///
+ /// Sets the master pose model that will be used to copy the skeleton nodes animation. Useful for modular characters.
+ ///
+ /// The master pose actor to use.
+ API_FUNCTION() void SetMasterPoseModel(AnimatedModel* masterPose);
+
public:
///