// Copyright Epic Games, Inc. All Rights Reserved. #include "BoneControllers/AnimNode_SkeletalControlBase.h" #include "Animation/AnimInstance.h" #include "Animation/AnimTrace.h" #include "Engine/SkeletalMesh.h" #include "Animation/AnimInstanceProxy.h" #include "Animation/AnimStats.h" #include "Engine/SkeletalMeshSocket.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimNode_SkeletalControlBase) #define LOCTEXT_NAMESPACE "AnimNode_SkeletalControlBase" #if UE_BUILD_SHIPPING || UE_BUILD_TEST DECLARE_LOG_CATEGORY_EXTERN(LogSkeletalControlBase, Log, Warning); #else DECLARE_LOG_CATEGORY_EXTERN(LogSkeletalControlBase, Log, All); #endif DEFINE_LOG_CATEGORY(LogSkeletalControlBase); ///////////////////////////////////////////////////// // FAnimNode_SkeletalControlBase void FAnimNode_SkeletalControlBase::Initialize_AnyThread(const FAnimationInitializeContext& Context) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Initialize_AnyThread) FAnimNode_Base::Initialize_AnyThread(Context); ComponentPose.Initialize(Context); AlphaBoolBlend.Reinitialize(); AlphaScaleBiasClamp.Reinitialize(); } void FAnimNode_SkeletalControlBase::CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) { #if WITH_EDITOR ClearValidationVisualWarnings(); #endif DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(CacheBones_AnyThread) FAnimNode_Base::CacheBones_AnyThread(Context); InitializeBoneReferences(Context.AnimInstanceProxy->GetRequiredBones()); ComponentPose.CacheBones(Context); } void FAnimNode_SkeletalControlBase::UpdateInternal(const FAnimationUpdateContext& Context) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(UpdateInternal) } void FAnimNode_SkeletalControlBase::UpdateComponentPose_AnyThread(const FAnimationUpdateContext& Context) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(UpdateComponentPose_AnyThread) ComponentPose.Update(Context); } void FAnimNode_SkeletalControlBase::Update_AnyThread(const FAnimationUpdateContext& Context) { ////////////////////////////////////////////////////////////////////////// // PERFORMANCE CRITICAL NOTE // // This function is called recursively as we traverse nodes, as such, it is critical to keep the // amount of stack space used to a minimum as many nodes can be traversed. Using too much stack // here can quickly lead to stack overflows. // // We explicitly disable inlineing for virtual calls. Normally, virtual calls are never inlined // but when PGO and LTO are enabled, the compiler can speculatively de-virtualize the calls. It does // this by comparing the v-table pointer and inlineing the call directly in here with a static jump. // As a result, it can significantly increase the amount of stack space used. ////////////////////////////////////////////////////////////////////////// DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Update_AnyThread) UE_DONT_INLINE_CALL UpdateComponentPose_AnyThread(Context); ActualAlpha = 0.f; if (IsLODEnabled(Context.AnimInstanceProxy)) { GetEvaluateGraphExposedInputs().Execute(Context); // Apply the skeletal control if it's valid switch (AlphaInputType) { case EAnimAlphaInputType::Float : ActualAlpha = AlphaScaleBias.ApplyTo(AlphaScaleBiasClamp.ApplyTo(Alpha, Context.GetDeltaTime())); break; case EAnimAlphaInputType::Bool : ActualAlpha = AlphaBoolBlend.ApplyTo(bAlphaBoolEnabled, Context.GetDeltaTime()); break; case EAnimAlphaInputType::Curve : if (UAnimInstance* AnimInstance = Cast(Context.AnimInstanceProxy->GetAnimInstanceObject())) { ActualAlpha = AlphaScaleBiasClamp.ApplyTo(AnimInstance->GetCurveValue(AlphaCurveName), Context.GetDeltaTime()); } break; }; // Make sure Alpha is clamped between 0 and 1. ActualAlpha = FMath::Clamp(ActualAlpha, 0.f, 1.f); if (FAnimWeight::IsRelevant(ActualAlpha)) { const USkeleton* Skeleton = Context.AnimInstanceProxy->GetSkeleton(); const FBoneContainer& RequiredBones = Context.AnimInstanceProxy->GetRequiredBones(); bool bIsValidToEvaluate; UE_DONT_INLINE_CALL bIsValidToEvaluate = IsValidToEvaluate(Skeleton, RequiredBones); if (bIsValidToEvaluate) { UE_DONT_INLINE_CALL UpdateInternal(Context); } } } TRACE_ANIM_NODE_VALUE(Context, TEXT("Alpha"), ActualAlpha); } bool ContainsNaN(const TArray & BoneTransforms) { for (int32 i = 0; i < BoneTransforms.Num(); ++i) { if (BoneTransforms[i].Transform.ContainsNaN()) { return true; } } return false; } void FAnimNode_SkeletalControlBase::EvaluateComponentPose_AnyThread(FComponentSpacePoseContext& Output) { DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateComponentPose_AnyThread) // Evaluate the input ComponentPose.EvaluateComponentSpace(Output); } void FAnimNode_SkeletalControlBase::EvaluateComponentSpaceInternal(FComponentSpacePoseContext& Context) { } void FAnimNode_SkeletalControlBase::EvaluateComponentSpace_AnyThread(FComponentSpacePoseContext& Output) { ////////////////////////////////////////////////////////////////////////// // PERFORMANCE CRITICAL NOTE // // This function is called recursively as we traverse nodes, as such, it is critical to keep the // amount of stack space used to a minimum as many nodes can be traversed. Using too much stack // here can quickly lead to stack overflows. // // We explicitly disable inlineing for virtual calls. Normally, virtual calls are never inlined // but when PGO and LTO are enabled, the compiler can speculatively de-virtualize the calls. It does // this by comparing the v-table pointer and inlineing the call directly in here with a static jump. // As a result, it can significantly increase the amount of stack space used. ////////////////////////////////////////////////////////////////////////// DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(EvaluateComponentSpace_AnyThread) ANIM_MT_SCOPE_CYCLE_COUNTER_VERBOSE(SkeletalControlBase, !IsInGameThread()); // Cache the incoming node IDs in a base context FAnimationBaseContext CachedContext(Output); UE_DONT_INLINE_CALL EvaluateComponentPose_AnyThread(Output); #if WITH_EDITORONLY_DATA // save current pose before applying skeletal control to compute the exact gizmo location in AnimGraphNode ForwardedPose.CopyPose(Output.Pose); #endif // #if WITH_EDITORONLY_DATA #if DO_CHECK // this is to ensure Source data does not contain NaN ensure(Output.ContainsNaN() == false); #endif // Apply the skeletal control if it's valid if (FAnimWeight::IsRelevant(ActualAlpha)) { const USkeleton* Skeleton = Output.AnimInstanceProxy->GetSkeleton(); const FBoneContainer& RequiredBones = Output.AnimInstanceProxy->GetRequiredBones(); bool bIsValidToEvaluate; UE_DONT_INLINE_CALL bIsValidToEvaluate = IsValidToEvaluate(Skeleton, RequiredBones); if (bIsValidToEvaluate) { Output.SetNodeIds(CachedContext); UE_DONT_INLINE_CALL EvaluateComponentSpaceInternal(Output); BoneTransforms.Reset(BoneTransforms.Num()); UE_DONT_INLINE_CALL EvaluateSkeletalControl_AnyThread(Output, BoneTransforms); if (BoneTransforms.Num() > 0) { const float BlendWeight = FMath::Clamp(ActualAlpha, 0.f, 1.f); Output.Pose.LocalBlendCSBoneTransforms(BoneTransforms, BlendWeight); } // we check NaN when you get out of this function in void FComponentSpacePoseLink::EvaluateComponentSpace(FComponentSpacePoseContext& Output) } } } void FAnimNode_SkeletalControlBase::AddDebugNodeData(FString& OutDebugData) { OutDebugData += FString::Printf(TEXT("Alpha: %.1f%%"), ActualAlpha*100.f); } void FAnimNode_SkeletalControlBase::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray& OutBoneTransforms) { } void FAnimNode_SkeletalControlBase::SetAlpha(float InAlpha) { Alpha = InAlpha; ActualAlpha = InAlpha; } float FAnimNode_SkeletalControlBase::GetAlpha() const { return ActualAlpha; } void FAnimNode_SkeletalControlBase::InitializeAndValidateBoneRef(FBoneReference& BoneRef, const FBoneContainer& RequiredBones) { if (BoneRef.BoneName != NAME_None) { BoneRef.Initialize(RequiredBones); } if (!BoneRef.HasValidSetup()) { const FText ErrorText = FText::Format(LOCTEXT("SkeletalControlBoneError", "Referenced Bone {0} does not exist on SkeletalMesh {1}."), FText::AsCultureInvariant(BoneRef.BoneName.ToString()), FText::AsCultureInvariant(GetNameSafe(RequiredBones.GetSkeletalMeshAsset()))); #if WITH_EDITOR AddValidationVisualWarning(ErrorText); #endif // WITH_EDITOR // If the user specified a simulation root that is not used by the skelmesh, issue a warning UE_LOG(LogSkeletalControlBase, Log, TEXT("%s"), *ErrorText.ToString()); } } #if WITH_EDITOR void FAnimNode_SkeletalControlBase::AddBoneRefMissingVisualWarning(const FString& BoneName, const FString& SkeletalMeshName) { const FText ErrorText = FText::Format(LOCTEXT("SkeletalControlBoneVisualWarning", "Simulation Base Bone {0} does not exist on SkeletalMesh {1}."), FText::FromString(BoneName), FText::FromString(SkeletalMeshName)); AddValidationVisualWarning(ErrorText); } void FAnimNode_SkeletalControlBase::AddValidationVisualWarning(FText ValidationVisualWarning) { #if WITH_EDITORONLY_DATA if (ValidationVisualWarningMessage.IsEmpty()) { ValidationVisualWarningMessage = ValidationVisualWarning; } else { ValidationVisualWarningMessage = FText::Format(FText::FromString(TEXT("{0}\n{1}")), ValidationVisualWarningMessage, ValidationVisualWarning); } #endif } void FAnimNode_SkeletalControlBase::ClearValidationVisualWarnings() { ValidationVisualWarningMessage = FText::GetEmpty(); } bool FAnimNode_SkeletalControlBase::HasValidationVisualWarnings() const { return ValidationVisualWarningMessage.IsEmpty() == false; } FText FAnimNode_SkeletalControlBase::GetValidationVisualWarningMessage() const { return ValidationVisualWarningMessage; } #endif // WITH_EDITOR #undef LOCTEXT_NAMESPACE