Files
UnrealEngine/Engine/Source/Runtime/AnimGraphRuntime/Private/AnimNodes/AnimNode_PoseDriver.cpp
2025-05-18 13:04:45 +08:00

507 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNodes/AnimNode_PoseDriver.h"
#include "AnimationRuntime.h"
#include "Animation/AnimCurveUtils.h"
#include "Serialization/CustomVersion.h"
#include "Animation/AnimInstanceProxy.h"
#include "RBF/RBFSolver.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AnimNode_PoseDriver)
FAnimNode_PoseDriver::FAnimNode_PoseDriver()
: DriveSource(EPoseDriverSource::Rotation)
, DriveOutput(EPoseDriverOutput::DrivePoses)
, LODThreshold(INDEX_NONE)
{
RBFParams.DistanceMethod = ERBFDistanceMethod::SwingAngle;
#if WITH_EDITORONLY_DATA
SoloTargetIndex = INDEX_NONE;
bSoloDrivenOnly = false;
RadialScaling_DEPRECATED = 0.25f;
Type_DEPRECATED = EPoseDriverType::SwingOnly;
TwistAxis_DEPRECATED = BA_X;
#endif
}
void FAnimNode_PoseDriver::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread)
FAnimNode_PoseHandler::Initialize_AnyThread(Context);
SourcePose.Initialize(Context);
}
void FAnimNode_PoseDriver::RebuildPoseList(const FBoneContainer& InBoneContainer, const UPoseAsset* InPoseAsset)
{
// Cache UIDs for driving curves
PoseExtractContext.PoseCurves.Reset();
const USkeleton* Skeleton = InPoseAsset->GetSkeleton();
if (Skeleton)
{
const TArray<FName>& PoseNames = InPoseAsset->GetPoseFNames();
for (FPoseDriverTarget& PoseTarget : PoseTargets)
{
const int32 PoseIndex = InPoseAsset->GetPoseIndexByName(PoseTarget.DrivenName);
if (PoseIndex != INDEX_NONE)
{
// we keep pose index as that is the fastest way to search when extracting pose asset
PoseTarget.PoseCurveIndex = PoseExtractContext.PoseCurves.Add(FPoseCurve(PoseIndex, PoseNames[PoseIndex], 0.f));
}
else
{
PoseTarget.PoseCurveIndex = INDEX_NONE;
}
}
}
}
void FAnimNode_PoseDriver::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(CacheBones_AnyThread)
FAnimNode_PoseHandler::CacheBones_AnyThread(Context);
// Init pose input
SourcePose.CacheBones(Context);
const FBoneContainer& BoneContainer = Context.AnimInstanceProxy->GetRequiredBones();
// Init bone refs
for (FBoneReference& SourceBoneRef : SourceBones)
{
SourceBoneRef.Initialize(BoneContainer);
}
for (FBoneReference& OnlyDriveBoneRef : OnlyDriveBones)
{
OnlyDriveBoneRef.Initialize(BoneContainer);
}
EvalSpaceBone.Initialize(BoneContainer);
// Don't want to modify SourceBones, set weight to zero (if weight array is allocated)
for (FBoneReference& SourceBoneRef : SourceBones)
{
const FCompactPoseBoneIndex SourceCompactIndex = SourceBoneRef.GetCompactPoseIndex(BoneContainer);
if (BoneBlendWeights.IsValidIndex(SourceCompactIndex.GetInt()))
{
BoneBlendWeights[SourceCompactIndex.GetInt()] = 0.f;
}
}
// Check if there are valid OnlyDriveBones, since there might be a None entry.
bHasOnlyDriveBones = false;
for (const FBoneReference& BoneRef : OnlyDriveBones)
{
if (!BoneRef.BoneName.IsNone())
{
bHasOnlyDriveBones = true;
break;
}
}
// If we are filtering for only specific bones, set blend weight to zero for unwanted bones, and remember which bones to filter
BonesToFilter.Reset();
if (bHasOnlyDriveBones && CurrentPoseAsset.IsValid())
{
// Super call above should init BoneBlendWeights to compact pose size if CurrentPoseAsset is valid
check(BoneBlendWeights.Num() == BoneContainer.GetBoneIndicesArray().Num());
const TArray<FName> TrackNames = CurrentPoseAsset.Get()->GetTrackNames();
for (const auto& TrackName : TrackNames)
{
// Check if bone in OnlyDriveBones
if (!IsBoneDriven(TrackName))
{
int32 MeshBoneIndex = BoneContainer.GetPoseBoneIndexForBoneName(TrackName);
FCompactPoseBoneIndex CompactBoneIndex = BoneContainer.MakeCompactPoseIndex(FMeshPoseBoneIndex(MeshBoneIndex));
if (CompactBoneIndex != INDEX_NONE)
{
BoneBlendWeights[CompactBoneIndex.GetInt()] = 0.f; // Set blend weight for non-additive
BonesToFilter.Add(CompactBoneIndex); // Remember bones to filter out for additive
}
}
}
}
PoseExtractContext.BonesRequired.SetNumZeroed(BoneBlendWeights.Num());
for (int32 BoneIndex = 0; BoneIndex < BoneBlendWeights.Num(); BoneIndex++)
{
PoseExtractContext.BonesRequired[BoneIndex] = BoneBlendWeights[BoneIndex] > SMALL_NUMBER;
}
}
void FAnimNode_PoseDriver::UpdateAssetPlayer(const FAnimationUpdateContext& Context)
{
FAnimNode_PoseHandler::UpdateAssetPlayer(Context);
SourcePose.Update(Context);
}
void FAnimNode_PoseDriver::GatherDebugData(FNodeDebugData& DebugData)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(GatherDebugData)
FAnimNode_PoseHandler::GatherDebugData(DebugData);
SourcePose.GatherDebugData(DebugData.BranchFlow(1.f));
}
float FAnimNode_PoseDriver::GetRadiusForTarget(const FRBFTarget& Target) const
{
return FRBFSolver::GetRadiusForTarget(Target, RBFParams);
}
bool FAnimNode_PoseDriver::IsBoneDriven(FName BoneName) const
{
for (const FBoneReference& BoneRef : OnlyDriveBones)
{
if (BoneRef.BoneName == BoneName)
{
return true;
}
}
return false;
}
void FAnimNode_PoseDriver::GetRBFTargets(TArray<FRBFTarget>& OutTargets, const FBoneContainer* BoneContainer) const
{
OutTargets.Reset();
OutTargets.AddZeroed(PoseTargets.Num());
// Create entry for each target
for (int32 TargetIdx = 0; TargetIdx < PoseTargets.Num(); TargetIdx++)
{
FRBFTarget& RBFTarget = OutTargets[TargetIdx];
const FPoseDriverTarget& PoseTarget = PoseTargets[TargetIdx];
// We want to make sure we always have the right number of Values in our RBFTarget.
// If bone entries are missing, we fill with zeroes
for (int32 SourceIdx = 0; SourceIdx < SourceBones.Num(); SourceIdx++)
{
if (PoseTarget.BoneTransforms.IsValidIndex(SourceIdx))
{
const FPoseDriverTransform& BoneTransform = PoseTarget.BoneTransforms[SourceIdx];
// Get Ref Transform
FTransform RefBoneTransform = FTransform::Identity;
if (bEvalFromRefPose && BoneContainer)
{
const FBoneReference& BoneReference = SourceBones[SourceIdx];
if (BoneReference.HasValidSetup())
{
const FCompactPoseBoneIndex CompactPoseIndex = BoneReference.CachedCompactPoseIndex;
if (CompactPoseIndex < BoneContainer->GetCompactPoseNumBones())
{
RefBoneTransform = BoneContainer->GetRefPoseTransform(CompactPoseIndex);
}
}
}
// Target Translation
if (DriveSource == EPoseDriverSource::Translation)
{
// Make translation relative to its Ref
if (bEvalFromRefPose)
{
RBFTarget.AddFromVector(RefBoneTransform.Inverse().TransformPosition(BoneTransform.TargetTranslation));
}
else
{
RBFTarget.AddFromVector(BoneTransform.TargetTranslation);
}
}
// Target Rotation
else
{
// Make rotation relative to its Ref
if (bEvalFromRefPose)
{
const FQuat TargetRotation = BoneTransform.TargetRotation.Quaternion();
RBFTarget.AddFromRotator(RefBoneTransform.Inverse().TransformRotation(TargetRotation).Rotator());
}
else
{
RBFTarget.AddFromRotator(BoneTransform.TargetRotation);
}
}
}
else
{
RBFTarget.AddFromVector(FVector::ZeroVector);
}
}
RBFTarget.ScaleFactor = PoseTarget.TargetScale;
RBFTarget.bApplyCustomCurve = PoseTarget.bApplyCustomCurve;
RBFTarget.CustomCurve = PoseTarget.CustomCurve;
RBFTarget.DistanceMethod = PoseTarget.DistanceMethod;
RBFTarget.FunctionType = PoseTarget.FunctionType;
}
}
void FAnimNode_PoseDriver::Evaluate_AnyThread(FPoseContext& Output)
{
DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)
QUICK_SCOPE_CYCLE_COUNTER(STAT_PoseDriver_Eval);
if (!IsLODEnabled(Output.AnimInstanceProxy))
{
SourcePose.Evaluate(Output);
return;
}
FPoseContext SourceData(Output);
SourcePose.Evaluate(SourceData);
// Udpate DrivenIDs if needed
if (bCachedDrivenIDsAreDirty)
{
if (CurrentPoseAsset.IsValid())
{
RebuildPoseList(Output.AnimInstanceProxy->GetRequiredBones(), CurrentPoseAsset.Get());
}
}
// Get the index of the source bone
const FBoneContainer& BoneContainer = SourceData.Pose.GetBoneContainer();
RBFInput.Values.Reset();
SourceBoneTMs.Reset();
bool bFoundAnyBone = false;
for (const FBoneReference& SourceBoneRef : SourceBones)
{
FTransform SourceBoneTM = FTransform::Identity;
const FCompactPoseBoneIndex SourceCompactIndex = SourceBoneRef.GetCompactPoseIndex(BoneContainer);
if (SourceCompactIndex.GetInt() != INDEX_NONE)
{
// If evaluating in alternative bone space, have to build component space pose
if (EvalSpaceBone.IsValidToEvaluate(BoneContainer))
{
FCSPose<FCompactPose> CSPose;
CSPose.InitPose(SourceData.Pose);
const FCompactPoseBoneIndex EvalSpaceCompactIndex = EvalSpaceBone.GetCompactPoseIndex(BoneContainer);
FTransform EvalSpaceCompSpace = CSPose.GetComponentSpaceTransform(EvalSpaceCompactIndex);
FTransform SourceBoneCompSpace = CSPose.GetComponentSpaceTransform(SourceCompactIndex);
SourceBoneTM = SourceBoneCompSpace.GetRelativeTransform(EvalSpaceCompSpace);
}
// If just evaluating in local space, just grab from local space pose
else
{
// Relative to Ref Pose
if (bEvalFromRefPose && SourceCompactIndex.GetInt() < BoneContainer.GetCompactPoseNumBones())
{
SourceBoneTM = SourceData.Pose[SourceCompactIndex].GetRelativeTransform(BoneContainer.GetRefPoseTransform(SourceCompactIndex));
}
else
{
SourceBoneTM = SourceData.Pose[SourceCompactIndex];
}
}
bFoundAnyBone = true;
}
// Build RBFInput entry
if (DriveSource == EPoseDriverSource::Translation)
{
RBFInput.AddFromVector(SourceBoneTM.GetTranslation());
}
else
{
RBFInput.AddFromRotator(SourceBoneTM.Rotator());
}
// Record this so we can use it for drawing in edit mode
SourceBoneTMs.Add(SourceBoneTM);
}
// Do nothing if bone is no bones are found/all LOD-ed out
if (!bFoundAnyBone)
{
Output = SourceData;
return;
}
RBFParams.TargetDimensions = SourceBones.Num() * 3;
OutputWeights.Reset();
// Use SoloTarget, no need to Solve
#if WITH_EDITORONLY_DATA
if (PoseTargets.IsValidIndex(SoloTargetIndex))
{
OutputWeights.Add(FRBFOutputWeight(SoloTargetIndex, 1.0f));
}
else
#endif
// Solve Weights
{
// Get target array as RBF types
GetRBFTargets(RBFTargets, &BoneContainer);
if (!SolverData.IsValid() || !FRBFSolver::IsSolverDataValid(*SolverData, RBFParams, RBFTargets))
{
SolverData = FRBFSolver::InitSolver(RBFParams, RBFTargets);
}
// Run RBF solver
FRBFSolver::Solve(*SolverData, RBFParams, RBFTargets, RBFInput, OutputWeights);
}
// Track if we have filled Output with valid pose
bool bHaveValidPose = false;
// Process active targets (if any)
if (OutputWeights.Num() > 0)
{
// Drive Poses from PoseAsset
if (DriveOutput == EPoseDriverOutput::DrivePoses)
{
const UPoseAsset* CachedPoseAsset = CurrentPoseAsset.Get();
// Check if PoseAsset is assignedand and compatible
if (CachedPoseAsset && CachedPoseAsset->GetSkeleton() != nullptr)
{
// clear the value before setting it.
for (int32 PoseIndex = 0; PoseIndex < PoseExtractContext.PoseCurves.Num(); ++PoseIndex)
{
PoseExtractContext.PoseCurves[PoseIndex].Value = 0.f;
}
// Then fill in weight for any driven poses
for (const FRBFOutputWeight& Weight : OutputWeights)
{
const FPoseDriverTarget& PoseTarget = PoseTargets[Weight.TargetIndex];
const int32 PoseIndex = PoseTarget.PoseCurveIndex;
if (PoseIndex != INDEX_NONE)
{
PoseExtractContext.PoseCurves[PoseIndex].Value = Weight.TargetWeight;
}
}
FPoseContext CurrentPose(Output);
FAnimationPoseData CurrentAnimationPoseData(CurrentPose);
// Evaluate PoseAsset
if (CachedPoseAsset->GetAnimationPose(CurrentAnimationPoseData, PoseExtractContext))
{
// If Additive, Set Source and OnlyDrive Bones to Zero and Accumulate Pose
if (CurrentPoseAsset->IsValidAdditive())
{
const FTransform AdditiveIdentity(FQuat::Identity, FVector::ZeroVector, FVector::ZeroVector);
// Don't want to modify SourceBones, set additive offset to zero (not identity transform, as need zero scale)
for (const FBoneReference& SourceBoneRef : SourceBones)
{
const FCompactPoseBoneIndex SourceCompactIndex = SourceBoneRef.GetCompactPoseIndex(BoneContainer);
CurrentPose.Pose[SourceCompactIndex] = AdditiveIdentity;
}
// If filtering for specific bones, filter out bones using BonesToFilter array
if (bHasOnlyDriveBones)
{
for (FCompactPoseBoneIndex BoneIndex : BonesToFilter)
{
CurrentPose.Pose[BoneIndex] = AdditiveIdentity;
}
}
// Start by copying input
Output = SourceData;
FAnimationPoseData AccumulatedAnimationPoseData(Output); // out
FAnimationRuntime::AccumulateAdditivePose(AccumulatedAnimationPoseData, CurrentAnimationPoseData, 1.f, EAdditiveAnimationType::AAT_LocalSpaceBase);
}
// If Non-Additive, Blend between Source and Current filtering with OnlyDrive Bones
else
{
const FAnimationPoseData SourceAnimationPoseData(SourceData);
FAnimationPoseData BlendedAnimationPoseData(Output); // out
FAnimationRuntime::BlendTwoPosesTogetherPerBone(SourceAnimationPoseData, CurrentAnimationPoseData, BoneBlendWeights, BlendedAnimationPoseData);
}
bHaveValidPose = true;
}
}
}
// Drive curves (morphs, materials etc)
else if (DriveOutput == EPoseDriverOutput::DriveCurves)
{
// Start by copying input
Output = SourceData;
// Then set curves based on target weights
FBlendedCurve DrivenCurves;
for (const FRBFOutputWeight& Weight : OutputWeights)
{
FPoseDriverTarget& PoseTarget = PoseTargets[Weight.TargetIndex];
if (PoseTarget.DrivenName != NAME_None)
{
DrivenCurves.Add(PoseTarget.DrivenName, Weight.TargetWeight);
}
}
// Merge driven curves into output
UE::Anim::FNamedValueArrayUtils::Union(Output.Curve, DrivenCurves, [](UE::Anim::FCurveElement& InOutElement, const UE::Anim::FCurveElement& InElement0, UE::Anim::ENamedValueUnionFlags InFlags)
{
if(EnumHasAnyFlags(InFlags, UE::Anim::ENamedValueUnionFlags::ValidArg1))
{
InOutElement.Value = InElement0.Value;
InOutElement.Flags |= InElement0.Flags;
}
});
bHaveValidPose = true;
}
}
// No valid pose, just pass through
if (!bHaveValidPose)
{
Output = SourceData;
}
#if WITH_EDITORONLY_DATA
else if (!bSoloDrivenOnly && PoseTargets.IsValidIndex(SoloTargetIndex))
{
SourceBoneTMs.Reset();
const FPoseDriverTarget& PoseTarget = PoseTargets[SoloTargetIndex];
for (int32 SourceIdx = 0; SourceIdx < SourceBones.Num(); SourceIdx++)
{
const FBoneReference& SourceBoneRef = SourceBones[SourceIdx];
const FCompactPoseBoneIndex SourceCompactIndex = SourceBoneRef.GetCompactPoseIndex(BoneContainer);
if (PoseTarget.BoneTransforms.IsValidIndex(SourceIdx) && SourceCompactIndex.GetInt() != INDEX_NONE)
{
FTransform& TargetTransform = Output.Pose[SourceCompactIndex];
const FPoseDriverTransform& SourceTransform = PoseTarget.BoneTransforms[SourceIdx];
if (DriveSource == EPoseDriverSource::Translation)
{
TargetTransform.SetTranslation(SourceTransform.TargetTranslation);
}
else
{
TargetTransform.SetRotation(SourceTransform.TargetRotation.Quaternion());
}
SourceBoneTMs.Add(TargetTransform);
}
}
}
#endif
}