diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 18013b169..a4424e922 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -7,7 +7,7 @@ #include "Engine/Graphics/Models/SkeletonData.h" #include "Engine/Scripting/Scripting.h" -extern void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 i); +extern void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes); ThreadLocal AnimGraphExecutor::Context; @@ -338,31 +338,25 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) if (_graph.BaseModel != data.NodesSkeleton) { ANIM_GRAPH_PROFILE_EVENT("Retarget"); - - // Init nodes for the target skeleton auto& targetSkeleton = data.NodesSkeleton->Skeleton; retargetNodes = *animResult; retargetNodes.Nodes.Resize(targetSkeleton.Nodes.Count()); Transform* targetNodes = retargetNodes.Nodes.Get(); - for (int32 i = 0; i < retargetNodes.Nodes.Count(); i++) - targetNodes[i] = targetSkeleton.Nodes[i].LocalTransform; - // Use skeleton mapping + // Attempt to retarget output pose for the target skeleton const SkinnedModel::SkeletonMapping mapping = data.NodesSkeleton->GetSkeletonMapping(_graph.BaseModel); if (mapping.NodesMapping.IsValid()) { + // Use skeleton mapping const auto& sourceSkeleton = _graph.BaseModel->Skeleton; Transform* sourceNodes = animResult->Nodes.Get(); + RetargetSkeletonPose(sourceSkeleton, targetSkeleton, mapping, sourceNodes, targetNodes); + } + else + { + // Use T-pose as a fallback for (int32 i = 0; i < retargetNodes.Nodes.Count(); i++) - { - const int32 nodeToNode = mapping.NodesMapping[i]; - if (nodeToNode != -1) - { - Transform node = sourceNodes[nodeToNode]; - RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i); - targetNodes[i] = node; - } - } + targetNodes[i] = targetSkeleton.Nodes[i].LocalTransform; } animResult = &retargetNodes; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index a0c90b854..bee1f4db4 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -109,24 +109,86 @@ namespace nodes->RootMotion.Orientation.Normalize(); } } + + Matrix ComputeWorldMatrixRecursive(const SkeletonData& skeleton, int32 index, Matrix localMatrix) + { + const auto& node = skeleton.Nodes[index]; + index = node.ParentIndex; + while (index != -1) + { + const auto& parent = skeleton.Nodes[index]; + localMatrix *= parent.LocalTransform.GetWorld(); + index = parent.ParentIndex; + } + return localMatrix; + } + + Matrix ComputeInverseParentMatrixRecursive(const SkeletonData& skeleton, int32 index) + { + Matrix inverseParentMatrix = Matrix::Identity; + const auto& node = skeleton.Nodes[index]; + if (node.ParentIndex != -1) + { + inverseParentMatrix = ComputeWorldMatrixRecursive(skeleton, index, inverseParentMatrix); + inverseParentMatrix = Matrix::Invert(inverseParentMatrix); + } + return inverseParentMatrix; + } } -void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, Transform& node, int32 i) +void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 targetIndex) { - const int32 nodeToNode = mapping.NodesMapping[i]; - if (nodeToNode == -1) + // sourceSkeleton - skeleton of Anim Graph (Base Locomotion pack) + // targetSkeleton - visual mesh skeleton (City Characters pack) + // target - anim graph input/output transformation of that node + const auto& targetNode = targetSkeleton.Nodes[targetIndex]; + const int32 sourceIndex = sourceMapping.NodesMapping[targetIndex]; + if (sourceIndex == -1) + { + // Use T-pose + node = targetNode.LocalTransform; return; + } + const auto& sourceNode = sourceSkeleton.Nodes[sourceIndex]; - // Map source skeleton node to the target skeleton (use ref pose difference) - const auto& sourceNode = sourceSkeleton.Nodes[nodeToNode]; - const auto& targetNode = targetSkeleton.Nodes[i]; - Transform value = node; - const Transform sourceToTarget = targetNode.LocalTransform - sourceNode.LocalTransform; - value.Translation += sourceToTarget.Translation; - value.Scale *= sourceToTarget.Scale; - value.Orientation = sourceToTarget.Orientation * value.Orientation; // TODO: find out why this doesn't match referenced animation when played on that skeleton originally - value.Orientation.Normalize(); - node = value; + // [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/] + + // Calculate T-Pose of source node, target node and target parent node + Matrix bindMatrix = ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, sourceNode.LocalTransform.GetWorld()); + Matrix inverseBindMatrix = Matrix::Invert(bindMatrix); + Matrix targetMatrix = ComputeWorldMatrixRecursive(targetSkeleton, targetIndex, targetNode.LocalTransform.GetWorld()); + Matrix inverseParentMatrix = ComputeInverseParentMatrixRecursive(targetSkeleton, targetIndex); + + // Target node animation is world-space difference of the animated source node inside the target's parent node world-space + Matrix localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, node.GetWorld()); + localMatrix = targetMatrix * localMatrix * inverseParentMatrix; + + // Extract local node transformation + localMatrix.Decompose(node); +} + +void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes) +{ + // TODO: cache source and target skeletons world-space poses for faster retargeting (use some pooled memory) + ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == mapping.NodesMapping.Length()); + for (int32 targetIndex = 0; targetIndex < targetSkeleton.Nodes.Count(); targetIndex++) + { + auto& targetNode = targetSkeleton.Nodes.Get()[targetIndex]; + const int32 sourceIndex = mapping.NodesMapping.Get()[targetIndex]; + Transform node; + if (sourceIndex == -1) + { + // Use T-pose + node = targetNode.LocalTransform; + } + else + { + // Retarget + node = sourceNodes[sourceIndex]; + RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, targetIndex); + } + targetNodes[targetIndex] = node; + } } AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node) @@ -1867,8 +1929,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu } // Check for transition interruption else if (EnumHasAnyFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::InterruptionRuleRechecking) && - EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) && - bucket.ActiveTransition->RuleGraph) + EnumHasNoneFlags(bucket.ActiveTransition->Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule) && + bucket.ActiveTransition->RuleGraph) { // Execute transition rule auto rootNode = bucket.ActiveTransition->RuleGraph->GetRootNode(); diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index d70c9fb35..80131b03b 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -155,7 +155,7 @@ void SkinnedModel::GetLODData(int32 lodIndex, BytesContainer& data) const GetChunkData(chunkIndex, data); } -SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source) +SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bool autoRetarget) { SkeletonMapping mapping; mapping.TargetSkeleton = this; @@ -168,10 +168,6 @@ SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source) PROFILE_CPU(); // Initialize the mapping - const int32 nodesCount = Skeleton.Nodes.Count(); - mappingData.NodesMapping = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount); - for (int32 i = 0; i < nodesCount; i++) - mappingData.NodesMapping[i] = -1; SkeletonRetarget* retarget = nullptr; const Guid sourceId = source->GetID(); for (auto& e : _skeletonRetargets) @@ -182,6 +178,15 @@ SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source) break; } } + if (!retarget && !autoRetarget) + { + // Skip automatic retarget + return mapping; + } + const int32 nodesCount = Skeleton.Nodes.Count(); + mappingData.NodesMapping = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount); + for (int32 i = 0; i < nodesCount; i++) + mappingData.NodesMapping[i] = -1; if (const auto* sourceAnim = Cast(source)) { const auto& channels = sourceAnim->Data.Channels; diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h index 1461844a1..cf778027d 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.h +++ b/Source/Engine/Content/Assets/SkinnedModel.h @@ -177,8 +177,9 @@ public: /// Gets the skeleton mapping for a given asset (animation or other skinned model). Uses identity mapping or manually created retargeting setup. /// /// The source asset (animation or other skinned model) to get mapping to its skeleton. + /// Enables automatic skeleton retargeting based on nodes names. Can be disabled to query existing skeleton mapping or return null if not defined. /// The skeleton mapping for the source asset into this skeleton. - SkeletonMapping GetSkeletonMapping(Asset* source); + SkeletonMapping GetSkeletonMapping(Asset* source, bool autoRetarget = true); /// /// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.