// Copyright 2025 https://yuewu.dev/en All Rights Reserved. #include "Nodes/GMS_AnimNode_OrientationWarping.h" #include "Animation/AnimInstanceProxy.h" #include "Animation/AnimNodeFunctionRef.h" #include "Animation/AnimRootMotionProvider.h" #include "BoneControllers/AnimNode_OffsetRootBone.h" #include "HAL/IConsoleManager.h" #include "Animation/AnimTrace.h" #include "Logging/LogVerbosity.h" #include "VisualLogger/VisualLogger.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_AnimNode_OrientationWarping) DECLARE_CYCLE_STAT(TEXT("OrientationWarping Eval"), STAT_OrientationWarping_Eval, STATGROUP_Anim); #if ENABLE_ANIM_DEBUG static TAutoConsoleVariable CVarAnimNodeOrientationWarpingDebug(TEXT("a.AnimNode.GenericOrientationWarping.Debug"), 0, TEXT("Turn on visualization debugging for Orientation Warping.")); static TAutoConsoleVariable CVarAnimNodeOrientationWarpingVerbose(TEXT("a.AnimNode.GenericOrientationWarping.Verbose"), 0, TEXT("Turn on verbose graph debugging for Orientation Warping")); static TAutoConsoleVariable CVarAnimNodeOrientationWarpingEnable(TEXT("a.AnimNode.GenericOrientationWarping.Enable"), 1, TEXT("Toggle Orientation Warping")); #endif namespace UE::Anim { static inline FVector GetAxisVector(const EAxis::Type& InAxis) { switch (InAxis) { case EAxis::X: return FVector::ForwardVector; case EAxis::Y: return FVector::RightVector; default: return FVector::UpVector; }; } static inline bool IsInvalidWarpingAngleDegrees(float Angle, float Tolerance) { Angle = FRotator::NormalizeAxis(Angle); return FMath::IsNearlyZero(Angle, Tolerance) || FMath::IsNearlyEqual(FMath::Abs(Angle), 180.f, Tolerance); } static float SignedAngleRadBetweenNormals(const FVector& From, const FVector& To, const FVector& Axis) { const float FromDotTo = FVector::DotProduct(From, To); const float Angle = FMath::Acos(FromDotTo); const FVector Cross = FVector::CrossProduct(From, To); const float Dot = FVector::DotProduct(Cross, Axis); return Dot >= 0 ? Angle : -Angle; } } void FGMS_AnimNode_OrientationWarping::GatherDebugData(FNodeDebugData& DebugData) { FString DebugLine = DebugData.GetNodeName(this); #if ENABLE_ANIM_DEBUG if (CVarAnimNodeOrientationWarpingVerbose.GetValueOnAnyThread() == 1) { if (Mode == EWarpingEvaluationMode::Manual) { DebugLine += TEXT("\n - Evaluation Mode: (Manual)"); DebugLine += FString::Printf(TEXT("\n - Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); } else { DebugLine += TEXT("\n - Evaluation Mode: (Graph)"); DebugLine += FString::Printf(TEXT("\n - Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); // Locomotion angle is already in degrees. DebugLine += FString::Printf(TEXT("\n - Locomotion Angle: (%.3fd)"), LocomotionAngle); DebugLine += FString::Printf(TEXT("\n - Locomotion Delta Angle Threshold: (%.3fd)"), LocomotionAngleDeltaThreshold); #if WITH_EDITORONLY_DATA DebugLine += FString::Printf(TEXT("\n - Root Motion Delta Attribute Found: %s)"), (bFoundRootMotionAttribute) ? TEXT("true") : TEXT("false")); #endif } if (const UEnum* TypeEnum = FindObject(nullptr, TEXT("/Script/CoreUObject.EAxis"))) { DebugLine += FString::Printf(TEXT("\n - Rotation Axis: (%s)"), *(TypeEnum->GetNameStringByIndex(static_cast(RotationAxis)))); } DebugLine += FString::Printf(TEXT("\n - Rotation Interpolation Speed: (%.3fd)"), RotationInterpSpeed); } else #endif { const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); DebugLine += FString::Printf(TEXT("(Orientation Angle: %.3fd)"), ActualOrientationAngleDegrees); } DebugData.AddDebugItem(DebugLine); ComponentPose.GatherDebugData(DebugData); } void FGMS_AnimNode_OrientationWarping::Initialize_AnyThread(const FAnimationInitializeContext& Context) { FAnimNode_SkeletalControlBase::Initialize_AnyThread(Context); Reset(Context); //早一点拿到ExternalBoneReference. if (IsLODEnabled(Context.AnimInstanceProxy)) { GetEvaluateGraphExposedInputs().Execute(Context); } } void FGMS_AnimNode_OrientationWarping::UpdateInternal(const FAnimationUpdateContext& Context) { FAnimNode_SkeletalControlBase::UpdateInternal(Context); // If we just became relevant and haven't been initialized yet, then reset. if (!bIsFirstUpdate && UpdateCounter.HasEverBeenUpdated() && !UpdateCounter.WasSynchronizedCounter(Context.AnimInstanceProxy->GetUpdateCounter())) { Reset(Context); } UpdateCounter.SynchronizeWith(Context.AnimInstanceProxy->GetUpdateCounter()); BlendWeight = Context.GetFinalBlendWeight(); // if (WarpingSpace == EOrientationWarpingSpace::RootBoneTransform) // { // if (UE::AnimationWarping::FRootOffsetProvider* RootOffsetProvider = Context.GetMessage()) // { // WarpingSpaceTransform = RootOffsetProvider->GetRootTransform(); // } // else // { // WarpingSpaceTransform = Context.AnimInstanceProxy->GetComponentTransform(); // } // } if (WarpingSpace == EOrientationWarpingSpace::ComponentTransform) { WarpingSpaceTransform = Context.AnimInstanceProxy->GetComponentTransform(); } } void FGMS_AnimNode_OrientationWarping::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray& OutBoneTransforms) { SCOPE_CYCLE_COUNTER(STAT_OrientationWarping_Eval); check(OutBoneTransforms.Num() == 0); float TargetOrientationAngleRad; const float DeltaSeconds = Output.AnimInstanceProxy->GetDeltaSeconds(); const float MaxAngleCorrectionRad = FMath::DegreesToRadians(MaxCorrectionDegrees); const FVector RotationAxisVector = UE::Anim::GetAxisVector(RotationAxis); FVector LocomotionForward = FVector::ZeroVector; bool bGraphDrivenWarping = false; const UE::Anim::IAnimRootMotionProvider* RootMotionProvider = UE::Anim::IAnimRootMotionProvider::Get(); if (Mode == EWarpingEvaluationMode::Graph) { bGraphDrivenWarping = !!RootMotionProvider; ensureMsgf(bGraphDrivenWarping, TEXT("Graph driven Orientation Warping expected a valid root motion delta provider interface.")); } #if WITH_EDITORONLY_DATA bFoundRootMotionAttribute = false; #endif #if ENABLE_ANIM_DEBUG FTransform RootMotionTransformDelta = FTransform::Identity; float RootMotionDeltaAngleRad = 0.0; const float PreviousOrientationAngleRad = ActualOrientationAngleRad; #endif // We will likely need to revisit LocomotionAngle participating as an input to orientation warping. // Without velocity information from the motion model (such as the capsule), LocomotionAngle isn't enough // information in isolation for all cases when deciding to warp. // // For example imagine that the motion model has stopped moving with zero velocity due to a // transition into a strafing stop. During that transition we may play an animation with non-zero // velocity for an arbitrary number of frames. In this scenario the concept of direction is meaningless // since we cannot orient the animation to match a zero velocity and consequently a zero direction, // since that would break the pose. For those frames, we would incorrectly over-orient the strafe. // // The solution may be instead to pass velocity with the actor base rotation, allowing us to retain // speed information about the motion. It may also allow us to do more complex orienting behavior // when multiple degrees of freedom can be considered. if (WarpingSpace == EOrientationWarpingSpace::ComponentTransform) { WarpingSpaceTransform = Output.AnimInstanceProxy->GetComponentTransform(); } if (bGraphDrivenWarping) { #if !ENABLE_ANIM_DEBUG FTransform RootMotionTransformDelta = FTransform::Identity; #endif bGraphDrivenWarping = RootMotionProvider->ExtractRootMotion(Output.CustomAttributes, RootMotionTransformDelta); // Graph driven orientation warping will modify the incoming root motion to orient towards the intended locomotion angle if (bGraphDrivenWarping) { #if WITH_EDITORONLY_DATA // Graph driven Orientation Warping expects a root motion delta to be present in the attribute stream. bFoundRootMotionAttribute = true; #endif // In UE, forward is defined as +x; consequently this is also true when sampling an actor's velocity. Historically the skeletal // mesh component forward will not match the actor, requiring us to correct the rotation before sampling the LocomotionForward. // In order to make orientation warping 'pure' in the future we will need to provide more context about the intent of // the actor vs the intent of the animation in their respective spaces. Specifically, we will need some form the following information: // // 1. Actor Forward // 2. Actor Velocity // 3. Skeletal Mesh Relative Rotation if (LocomotionDirection.SquaredLength() > UE_SMALL_NUMBER) { // if we have a LocomotionDirection vector, transform into root bone local space LocomotionForward = WarpingSpaceTransform.InverseTransformVector(LocomotionDirection); LocomotionForward.Normalize(); } else { LocomotionAngle = FRotator::NormalizeAxis(LocomotionAngle); // UE-184297 Avoid storing LocomotionAngle in radians in case haven't updated the pinned input, to avoid a DegToRad(RadianValue) const float LocomotionAngleRadians = FMath::DegreesToRadians(LocomotionAngle); const FQuat LocomotionRotation = FQuat(RotationAxisVector, LocomotionAngleRadians); const FTransform SkeletalMeshRelativeTransform = Output.AnimInstanceProxy->GetComponentRelativeTransform(); const FQuat SkeletalMeshRelativeRotation = SkeletalMeshRelativeTransform.GetRotation(); LocomotionForward = SkeletalMeshRelativeRotation.UnrotateVector(LocomotionRotation.GetForwardVector()).GetSafeNormal(); } // Flatten locomotion direction, along the rotation axis. LocomotionForward = (LocomotionForward - RotationAxisVector.Dot(LocomotionForward) * RotationAxisVector).GetSafeNormal(); // @todo: Graph mode using a "manual value" makes no sense. Restructure logic to address this in the future. if (bUseManualRootMotionVelocity) { RootMotionTransformDelta.SetTranslation(ManualRootMotionVelocity * DeltaSeconds); } FVector RootMotionDeltaTranslation = RootMotionTransformDelta.GetTranslation(); // Flatten root motion translation, along the rotation axis. RootMotionDeltaTranslation = RootMotionDeltaTranslation - RotationAxisVector.Dot(RootMotionDeltaTranslation) * RotationAxisVector; const float RootMotionDeltaSpeed = RootMotionDeltaTranslation.Size() / DeltaSeconds; if (RootMotionDeltaSpeed < MinRootMotionSpeedThreshold) { // If we're under the threshold, snap orientation angle to 0, and let interpolation handle the delta TargetOrientationAngleRad = 0.0f; } else { const FVector PreviousRootMotionDeltaDirection = RootMotionDeltaDirection; // Hold previous direction if we can't calculate it from current move delta, because the root is no longer moving RootMotionDeltaDirection = RootMotionDeltaTranslation.GetSafeNormal(UE_SMALL_NUMBER, PreviousRootMotionDeltaDirection); TargetOrientationAngleRad = UE::Anim::SignedAngleRadBetweenNormals(RootMotionDeltaDirection, LocomotionForward, RotationAxisVector); // Motion Matching may return an animation that deviates a lot from the movement direction (e.g movement direction going bwd and motion matching could return the fwd animation for a few frames) // When that happens, since we use the delta between root motion and movement direction, we would be over-rotating the lower body and breaking the pose during those frames // So, when that happens we use the inverse of the root motion direction to calculate our target rotation. // This feels a bit 'hacky' but its the only option I've found so far to mitigate the problem if (LocomotionAngleDeltaThreshold > 0.f) { if (FMath::Abs(FMath::RadiansToDegrees(TargetOrientationAngleRad)) > LocomotionAngleDeltaThreshold) { TargetOrientationAngleRad = FMath::UnwindRadians(TargetOrientationAngleRad + FMath::DegreesToRadians(180.0f)); RootMotionDeltaDirection = -RootMotionDeltaDirection; } } // Don't compensate interpolation by the root motion angle delta if the previous angle isn't valid. if (bCounterCompenstateInterpolationByRootMotion && !PreviousRootMotionDeltaDirection.IsNearlyZero(UE_SMALL_NUMBER)) { #if !ENABLE_ANIM_DEBUG float RootMotionDeltaAngleRad; #endif // Counter the interpolated orientation angle by the root motion direction angle delta. // This prevents our interpolation from fighting the natural root motion that's flowing through the graph. RootMotionDeltaAngleRad = UE::Anim::SignedAngleRadBetweenNormals(RootMotionDeltaDirection, PreviousRootMotionDeltaDirection, RotationAxisVector); // Root motion may have large deltas i.e. bad blends or sudden direction changes like pivots. // If there's an instantaneous pop in root motion direction, this is likely a pivot. const float MaxRootMotionDeltaToCompensateRad = FMath::DegreesToRadians(MaxRootMotionDeltaToCompensateDegrees); if (FMath::Abs(RootMotionDeltaAngleRad) < MaxRootMotionDeltaToCompensateRad) { ActualOrientationAngleRad = FMath::UnwindRadians(ActualOrientationAngleRad + RootMotionDeltaAngleRad); } } // Rotate the root motion delta fully by the warped angle const FVector WarpedRootMotionTranslationDelta = FQuat(RotationAxisVector, TargetOrientationAngleRad).RotateVector(RootMotionDeltaTranslation); RootMotionTransformDelta.SetTranslation(WarpedRootMotionTranslationDelta); } // Forward the side effects of orientation warping on the root motion contribution for this sub-graph const bool bRootMotionOverridden = RootMotionProvider->OverrideRootMotion(RootMotionTransformDelta, Output.CustomAttributes); ensureMsgf(bRootMotionOverridden, TEXT("Graph driven Orientation Warping expected a root motion delta to be present in the attribute stream prior to warping/overriding it.")); } else { // Early exit on missing root motion delta attribute return; } } else { // Manual orientation warping will take the angle directly TargetOrientationAngleRad = FRotator::NormalizeAxis(OrientationAngle); TargetOrientationAngleRad = FMath::DegreesToRadians(TargetOrientationAngleRad); } // Optionally interpolate the effective orientation towards the target orientation angle // When the orientation warping node becomes relevant, the input pose orientation may not be aligned with the desired orientation. // Instead of interpolating this difference, snap to the desired orientation if it's our first update to minimize corrections over-time. if ((RotationInterpSpeed > 0.f) && !bIsFirstUpdate) { const float SmoothOrientationAngleRad = FMath::FInterpTo(ActualOrientationAngleRad, TargetOrientationAngleRad, DeltaSeconds, RotationInterpSpeed); // Limit our interpolation rate to prevent pops. // @TODO: Use better, more physically accurate interpolation here. ActualOrientationAngleRad = FMath::Clamp(SmoothOrientationAngleRad, ActualOrientationAngleRad - MaxAngleCorrectionRad, ActualOrientationAngleRad + MaxAngleCorrectionRad); } else { ActualOrientationAngleRad = TargetOrientationAngleRad; } ActualOrientationAngleRad = FMath::Clamp(ActualOrientationAngleRad, -MaxAngleCorrectionRad, MaxAngleCorrectionRad); // Allow the alpha value of the node to affect the final rotation ActualOrientationAngleRad *= ActualAlpha; if (bScaleByGlobalBlendWeight) { ActualOrientationAngleRad *= BlendWeight; } #if ENABLE_ANIM_DEBUG bool bDebugging = false; #if WITH_EDITORONLY_DATA bDebugging = bDebugging || bEnableDebugDraw; #else constexpr float DebugDrawScale = 1.f; #endif const int32 DebugIndex = CVarAnimNodeOrientationWarpingDebug.GetValueOnAnyThread(); bDebugging = bDebugging || (DebugIndex > 0); if (bDebugging) { const FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform(); const FVector ActorForwardDirection = Output.AnimInstanceProxy->GetActorTransform().GetRotation().GetForwardVector(); FVector DebugArrowOffset = FVector::ZAxisVector * DebugDrawScale; // Draw debug shapes { const FVector ForwardDirection = bGraphDrivenWarping ? ComponentTransform.GetRotation().RotateVector(LocomotionForward) : ActorForwardDirection; Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + ForwardDirection * 100.f * DebugDrawScale, 40.f * DebugDrawScale, FColor::Red, false, 0.f, 2.f * DebugDrawScale); const FVector RotationDirection = bGraphDrivenWarping ? ComponentTransform.GetRotation().RotateVector(RootMotionDeltaDirection) : ActorForwardDirection.RotateAngleAxis(OrientationAngle, RotationAxisVector); DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + RotationDirection * 100.f * DebugDrawScale, 40.f * DebugDrawScale, FColor::Blue, false, 0.f, 2.f * DebugDrawScale); const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); const FVector WarpedRotationDirection = bGraphDrivenWarping ? RotationDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector) : ActorForwardDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector); DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; Output.AnimInstanceProxy->AnimDrawDebugDirectionalArrow( ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + WarpedRotationDirection * 100.f * DebugDrawScale, 40.f * DebugDrawScale, FColor::Green, false, 0.f, 2.f * DebugDrawScale); } // Draw text on mesh in world space { TStringBuilder<1024> DebugLine; const float PreviousOrientationAngleDegrees = FMath::RadiansToDegrees(PreviousOrientationAngleRad); const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); const float TargetOrientationAngleDegrees = FMath::RadiansToDegrees(TargetOrientationAngleRad); if (Mode == EWarpingEvaluationMode::Manual) { DebugLine.Appendf(TEXT("\n - Previous Orientation Angle: (%.3fd)"), PreviousOrientationAngleDegrees); DebugLine.Appendf(TEXT("\n - Orientation Angle: (%.3fd)"), ActualOrientationAngleDegrees); DebugLine.Appendf(TEXT("\n - Target Orientation Angle: (%.3fd)"), TargetOrientationAngleRad); } else { if (RotationInterpSpeed > 0.0f) { DebugLine.Appendf(TEXT("\n - Previous Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(PreviousOrientationAngleRad)); DebugLine.Appendf(TEXT("\n - Root Motion Frame Delta Angle: (%.3fd)"), FMath::RadiansToDegrees(RootMotionDeltaAngleRad)); } DebugLine.Appendf(TEXT("\n - Actual Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(ActualOrientationAngleRad)); DebugLine.Appendf(TEXT("\n - Target Orientation Angle: (%.3fd)"), FMath::RadiansToDegrees(TargetOrientationAngleRad)); // Locomotion angle is already in degrees. DebugLine.Appendf(TEXT("\n - Locomotion Angle: (%.3fd)"), LocomotionAngle); DebugLine.Appendf(TEXT("\n - Root Motion Delta: %s)"), *RootMotionTransformDelta.GetTranslation().ToString()); DebugLine.Appendf(TEXT("\n - Root Motion Speed: %.3fd)"), RootMotionTransformDelta.GetTranslation().Size() / DeltaSeconds); } Output.AnimInstanceProxy->AnimDrawDebugInWorldMessage(DebugLine.ToString(), FVector::UpVector * 50.0f, FColor::Yellow, 1.f /*TextScale*/); } } #endif #if ANIM_TRACE_ENABLED { const float PreviousOrientationAngleDegrees = FMath::RadiansToDegrees(PreviousOrientationAngleRad); const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); const float TargetOrientationAngleDegrees = FMath::RadiansToDegrees(TargetOrientationAngleRad); TRACE_ANIM_NODE_VALUE(Output, TEXT("Previous OrientationAngle Degrees"), PreviousOrientationAngleDegrees); TRACE_ANIM_NODE_VALUE(Output, TEXT("Actual Orientation Angle Degrees"), ActualOrientationAngleDegrees); TRACE_ANIM_NODE_VALUE(Output, TEXT("Target Orientation Angle Degrees"), TargetOrientationAngleDegrees); if (Mode == EWarpingEvaluationMode::Graph) { const float RootMotionDeltaAngleDegrees = FMath::RadiansToDegrees(RootMotionDeltaAngleRad); TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Delta Angle Degrees"), RootMotionDeltaAngleDegrees); TRACE_ANIM_NODE_VALUE(Output, TEXT("Locomotion Angle"), LocomotionAngle); TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Translation Delta"), RootMotionTransformDelta.GetTranslation()); const float RootMotionSpeed = RootMotionTransformDelta.GetTranslation().Size() / DeltaSeconds; TRACE_ANIM_NODE_VALUE(Output, TEXT("Root Motion Speed"), RootMotionSpeed); } } #endif #if ENABLE_VISUAL_LOG if (FVisualLogger::IsRecording()) { const FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform(); const FVector ActorForwardDirection = Output.AnimInstanceProxy->GetActorTransform().GetRotation().GetForwardVector(); FVector DebugArrowOffset = FVector::ZAxisVector * DebugDrawScale; // Draw debug shapes { const FVector ForwardDirection = bGraphDrivenWarping ? ComponentTransform.GetRotation().RotateVector(LocomotionForward) : ActorForwardDirection; UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + ForwardDirection * 100.f * DebugDrawScale, FColor::Red, TEXT("")); const FVector RotationDirection = bGraphDrivenWarping ? ComponentTransform.GetRotation().RotateVector(RootMotionDeltaDirection) : ActorForwardDirection.RotateAngleAxis(OrientationAngle, RotationAxisVector); DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + RotationDirection * 100.f * DebugDrawScale, FColor::Blue, TEXT("")); const float ActualOrientationAngleDegrees = FMath::RadiansToDegrees(ActualOrientationAngleRad); const FVector WarpedRotationDirection = bGraphDrivenWarping ? RotationDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector) : ActorForwardDirection.RotateAngleAxis(ActualOrientationAngleDegrees, RotationAxisVector); DebugArrowOffset += FVector::ZAxisVector * DebugDrawScale; UE_VLOG_ARROW(Output.AnimInstanceProxy->GetAnimInstanceObject(), "OrientationWarping", Display, ComponentTransform.GetLocation() + DebugArrowOffset, ComponentTransform.GetLocation() + DebugArrowOffset + WarpedRotationDirection * 100.f * DebugDrawScale, FColor::Green, TEXT("")); } } #endif const float RootOffset = FMath::UnwindRadians(ActualOrientationAngleRad * DistributedBoneOrientationAlpha); // Rotate Root Bone first, as that cheaply rotates the whole pose with one transformation. if (!FMath::IsNearlyZero(RootOffset, KINDA_SMALL_NUMBER)) { const FQuat RootRotation = FQuat(RotationAxisVector, RootOffset); const FCompactPoseBoneIndex RootBoneIndex(0); FTransform RootBoneTransform(Output.Pose.GetComponentSpaceTransform(RootBoneIndex)); RootBoneTransform.SetRotation(RootRotation * RootBoneTransform.GetRotation()); RootBoneTransform.NormalizeRotation(); Output.Pose.SetComponentSpaceTransform(RootBoneIndex, RootBoneTransform); } const int32 NumSpineBones = SpineBoneDataArray.Num(); const bool bSpineOrientationAlpha = !FMath::IsNearlyZero(DistributedBoneOrientationAlpha, KINDA_SMALL_NUMBER); const bool bUpdateSpineBones = (NumSpineBones > 0) && bSpineOrientationAlpha; if (bUpdateSpineBones) { // Spine bones counter rotate body orientation evenly across all bones. for (int32 ArrayIndex = 0; ArrayIndex < NumSpineBones; ArrayIndex++) { const FOrientationWarpingSpineBoneData& BoneData = SpineBoneDataArray[ArrayIndex]; const FQuat SpineBoneCounterRotation = FQuat(RotationAxisVector, -ActualOrientationAngleRad * DistributedBoneOrientationAlpha * BoneData.Weight); check(BoneData.Weight > 0.f); FTransform SpineBoneTransform(Output.Pose.GetComponentSpaceTransform(BoneData.BoneIndex)); SpineBoneTransform.SetRotation((SpineBoneCounterRotation * SpineBoneTransform.GetRotation())); SpineBoneTransform.NormalizeRotation(); Output.Pose.SetComponentSpaceTransform(BoneData.BoneIndex, SpineBoneTransform); } } const float IKFootRootOrientationAlpha = 1.f - DistributedBoneOrientationAlpha; const bool bUpdateIKFootRoot = (IKFootData.IKFootRootBoneIndex != FCompactPoseBoneIndex(INDEX_NONE)) && !FMath::IsNearlyZero(IKFootRootOrientationAlpha, KINDA_SMALL_NUMBER); // Rotate IK Foot Root if (bUpdateIKFootRoot) { const FQuat BoneRotation = FQuat(RotationAxisVector, ActualOrientationAngleRad * IKFootRootOrientationAlpha); FTransform IKFootRootTransform(Output.Pose.GetComponentSpaceTransform(IKFootData.IKFootRootBoneIndex)); IKFootRootTransform.SetRotation(BoneRotation * IKFootRootTransform.GetRotation()); IKFootRootTransform.NormalizeRotation(); Output.Pose.SetComponentSpaceTransform(IKFootData.IKFootRootBoneIndex, IKFootRootTransform); // IK Feet // These match the root orientation, so don't rotate them. Just preserve root rotation. // We need to update their translation though, since we rotated their parent (the IK Foot Root bone). const int32 NumIKFootBones = IKFootData.IKFootBoneIndexArray.Num(); const bool bUpdateIKFootBones = bUpdateIKFootRoot && (NumIKFootBones > 0); if (bUpdateIKFootBones) { const FQuat IKFootRotation = FQuat(RotationAxisVector, -ActualOrientationAngleRad * IKFootRootOrientationAlpha); for (int32 ArrayIndex = 0; ArrayIndex < NumIKFootBones; ArrayIndex++) { const FCompactPoseBoneIndex& IKFootBoneIndex = IKFootData.IKFootBoneIndexArray[ArrayIndex]; FTransform IKFootBoneTransform(Output.Pose.GetComponentSpaceTransform(IKFootBoneIndex)); IKFootBoneTransform.SetRotation(IKFootRotation * IKFootBoneTransform.GetRotation()); IKFootBoneTransform.NormalizeRotation(); Output.Pose.SetComponentSpaceTransform(IKFootBoneIndex, IKFootBoneTransform); } } } OutBoneTransforms.Sort(FCompareBoneTransformIndex()); bIsFirstUpdate = false; } bool FGMS_AnimNode_OrientationWarping::IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) { #if ENABLE_ANIM_DEBUG if (CVarAnimNodeOrientationWarpingEnable.GetValueOnAnyThread() == 0) { return false; } #endif if (RotationAxis == EAxis::None) { return false; } if (Mode == EWarpingEvaluationMode::Manual && UE::Anim::IsInvalidWarpingAngleDegrees(OrientationAngle, KINDA_SMALL_NUMBER)) { return false; } if (SpineBoneDataArray.IsEmpty()) { return false; } else { for (const auto& Spine : SpineBoneDataArray) { if (Spine.BoneIndex == INDEX_NONE) { return false; } } } if (IKFootData.IKFootRootBoneIndex == INDEX_NONE) { return false; } if (IKFootData.IKFootBoneIndexArray.IsEmpty()) { return false; } else { for (const auto& IKFootBoneIndex : IKFootData.IKFootBoneIndexArray) { if (IKFootBoneIndex == INDEX_NONE) { return false; } } } return true; } void FGMS_AnimNode_OrientationWarping::InitializeBoneReferences(const FBoneContainer& RequiredBones) { ExternalBoneReference.IKFootRootBone.Initialize(RequiredBones); IKFootData.IKFootRootBoneIndex = ExternalBoneReference.IKFootRootBone.GetCompactPoseIndex(RequiredBones); IKFootData.IKFootBoneIndexArray.Reset(); for (auto& Bone : ExternalBoneReference.IKFootBones) { Bone.Initialize(RequiredBones); IKFootData.IKFootBoneIndexArray.Add(Bone.GetCompactPoseIndex(RequiredBones)); } SpineBoneDataArray.Reset(); for (auto& Bone : ExternalBoneReference.SpineBones) { Bone.Initialize(RequiredBones); SpineBoneDataArray.Add(FOrientationWarpingSpineBoneData(Bone.GetCompactPoseIndex(RequiredBones))); } if (SpineBoneDataArray.Num() > 0) { // Sort bones indices so we can transform parent before child SpineBoneDataArray.Sort(FOrientationWarpingSpineBoneData::FCompareBoneIndex()); // Assign Weights. TArray> IndicesToUpdate; for (int32 Index = SpineBoneDataArray.Num() - 1; Index >= 0; Index--) { // If this bone's weight hasn't been updated, scan its parents. // If parents have weight, we add it to 'ExistingWeight'. // split (1.f - 'ExistingWeight') between all members of the chain that have no weight yet. if (SpineBoneDataArray[Index].Weight == 0.f) { IndicesToUpdate.Reset(SpineBoneDataArray.Num()); float ExistingWeight = 0.f; IndicesToUpdate.Add(Index); const FCompactPoseBoneIndex CompactBoneIndex = SpineBoneDataArray[Index].BoneIndex; for (int32 ParentIndex = Index - 1; ParentIndex >= 0; ParentIndex--) { if (RequiredBones.BoneIsChildOf(CompactBoneIndex, SpineBoneDataArray[ParentIndex].BoneIndex)) { if (SpineBoneDataArray[ParentIndex].Weight > 0.f) { ExistingWeight += SpineBoneDataArray[ParentIndex].Weight; } else { IndicesToUpdate.Add(ParentIndex); } } } check(IndicesToUpdate.Num() > 0); const float WeightToShare = 1.f - ExistingWeight; const float IndividualWeight = WeightToShare / float(IndicesToUpdate.Num()); for (int32 UpdateListIndex = 0; UpdateListIndex < IndicesToUpdate.Num(); UpdateListIndex++) { SpineBoneDataArray[IndicesToUpdate[UpdateListIndex]].Weight = IndividualWeight; } } } } } void FGMS_AnimNode_OrientationWarping::Reset(const FAnimationBaseContext& Context) { bIsFirstUpdate = true; RootMotionDeltaDirection = FVector::ZeroVector; ManualRootMotionVelocity = FVector::ZeroVector; ActualOrientationAngleRad = 0.f; }