Fix animation retargetting to properly handle different skeleton nodes orientation

This commit is contained in:
Wojtek Figat
2024-11-22 12:29:05 +01:00
parent f8f4edfa76
commit 74993dcf9e
4 changed files with 98 additions and 36 deletions

View File

@@ -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<AnimGraphContext*> 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;

View File

@@ -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();

View File

@@ -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>((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>((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount);
for (int32 i = 0; i < nodesCount; i++)
mappingData.NodesMapping[i] = -1;
if (const auto* sourceAnim = Cast<Animation>(source))
{
const auto& channels = sourceAnim->Data.Channels;

View File

@@ -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.
/// </summary>
/// <param name="source">The source asset (animation or other skinned model) to get mapping to its skeleton.</param>
/// <param name="autoRetarget">Enables automatic skeleton retargeting based on nodes names. Can be disabled to query existing skeleton mapping or return null if not defined.</param>
/// <returns>The skeleton mapping for the source asset into this skeleton.</returns>
SkeletonMapping GetSkeletonMapping(Asset* source);
SkeletonMapping GetSkeletonMapping(Asset* source, bool autoRetarget = true);
/// <summary>
/// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.