Files
2025-05-18 13:04:45 +08:00

499 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNextAlignment.h"
#include "AnimNextAnimGraphSettings.h"
#include <AHEasing/easing.h>
#include "Animation/AnimRootMotionProvider.h"
#include "Animation/AnimSequence.h"
#include "Engine/World.h"
#include "EvaluationNotifies/AnimNotifyState_Alignment.h"
#include "EvaluationVM/EvaluationVM.h"
#include "EvaluationVM/KeyframeState.h"
#include "VisualLogger/VisualLogger.h"
void FEvaluationNotify_AlignmentInstance::Start()
{
const UNotifyState_AlignmentBase* AlignmentNotify = CastChecked<UNotifyState_AlignmentBase>(AnimNotify);
AlignBone = AlignmentNotify->AlignBone;
bFirstFrame = true;
}
namespace AnimNextAlignment
{
constexpr float FrameTime = 1.0f/30.0f;
float SampleCurve(float SampleTime, float StartTime, float EndTime, const TArray<float>& CurveData)
{
if (SampleTime <= StartTime)
{
return 0.0f;
}
if (SampleTime >= EndTime)
{
return 1.0;
}
// do nearest neighbor sampling for simplicity and performance
const int StartFrame = FMath::Clamp(round(StartTime / FrameTime), 0, CurveData.Num()-1);
const int EndFrame = FMath::Clamp(round(EndTime / FrameTime), 0, CurveData.Num()-1);
const int SampleFrame = FMath::Clamp(round(SampleTime / FrameTime), 0, CurveData.Num()-1);
return (CurveData[SampleFrame] - CurveData[StartFrame])/(CurveData[EndFrame] - CurveData[StartFrame]);
}
void GetTransformForFrame(float Frame, const TArray<FTransform>& Trajectory, FTransform& OutTransform)
{
int32 LowerFrame = FMath::Clamp(floor(Frame), 0, Trajectory.Num() -1);
int32 UpperFrame = FMath::Min(Trajectory.Num() -1 , LowerFrame + 1);
float Alpha = Frame - LowerFrame;
OutTransform = Trajectory[LowerFrame];
OutTransform.BlendWith(Trajectory[UpperFrame], Alpha);
}
}
float FEvaluationNotify_AlignmentInstance::GetWeight(float Time, const FAlignmentWarpCurve& WarpCurve) const
{
float Weight;
if (WarpCurve.CurveType == EAlignmentWeightCurveType::FromRootMotionTranslation && !AnimTrajectoryData.TranslationCurve.IsEmpty())
{
const float Duration = (EndTime - StartTime);
const float StartCurveSampleTime = FMath::Max(WarpCurve.StartRatio * Duration, ActualStartTime) - ActualStartTime;
return AnimNextAlignment::SampleCurve(Time - ActualStartTime, StartCurveSampleTime, WarpCurve.EndRatio * Duration - ActualStartTime, AnimTrajectoryData.TranslationCurve);
}
else if (WarpCurve.CurveType == EAlignmentWeightCurveType::FromRootMotionRotation && !AnimTrajectoryData.RotationCurve.IsEmpty())
{
const float Duration = (EndTime - StartTime);
const float StartCurveSampleTime = FMath::Max(WarpCurve.StartRatio * Duration, ActualStartTime) - ActualStartTime;
return AnimNextAlignment::SampleCurve(Time - ActualStartTime, StartCurveSampleTime, WarpCurve.EndRatio * Duration - ActualStartTime, AnimTrajectoryData.RotationCurve);
}
else
{
const float StartTimeWithRatio = FMath::Max(FMath::Lerp(StartTime, EndTime, WarpCurve.StartRatio), ActualStartTime);
const float EndTimeWithRatio = FMath::Lerp(StartTime, EndTime, WarpCurve.EndRatio);
const float CurrentRelativeTime = FMath::Clamp((Time-StartTimeWithRatio)/(EndTimeWithRatio-StartTimeWithRatio), 0,1);
switch(WarpCurve.CurveType)
{
case EAlignmentWeightCurveType::EaseIn:
Weight = CubicEaseIn(CurrentRelativeTime);
break;
case EAlignmentWeightCurveType::EaseOut:
Weight = CubicEaseOut(CurrentRelativeTime);
break;
case EAlignmentWeightCurveType::EaseInOut:
Weight = CubicEaseInOut(CurrentRelativeTime);
break;
case EAlignmentWeightCurveType::Instant:
Weight = 1.f;
break;
case EAlignmentWeightCurveType::DoNotWarp:
Weight = 0.f;
break;
default:
Weight = CurrentRelativeTime;
}
}
return Weight;
}
void FEvaluationNotify_AlignToGroundInstance::End(UE::AnimNext::FEvaluationNotifiesTrait::FInstanceData& TraitInstanceData)
{
FEvaluationNotify_AlignmentInstance::End(TraitInstanceData);
const UNotifyState_AlignToGround* AlignToGroundNotify = CastChecked<UNotifyState_AlignToGround>(AnimNotify);
TraitInstanceData.DataInterface->SetVariable(AlignToGroundNotify->PlaybackRateOutputVariable, 1.0);
}
bool FEvaluationNotify_AlignToGroundInstance::GetTargetTransform(UE::AnimNext::FEvaluationNotifiesTrait::FInstanceData& TraitInstanceData, FTransform& Transform)
{
const UNotifyState_AlignToGround* AlignToGroundNotify = CastChecked<UNotifyState_AlignToGround>(AnimNotify);
const FCollisionShape CollisionShape = FCollisionShape::MakeSphere(AlignToGroundNotify->TraceRadius);
FTransform StartTransform = TraitInstanceData.RootBoneTransform;
if (AlignToGroundNotify->bForceStartTransformUpright)
{
FRotator Rotation = StartTransform.GetRotation().Rotator();
Rotation.Roll = 0;
Rotation.Pitch = 0;
StartTransform.SetRotation(Rotation.Quaternion());
}
const float PredictionDelta = FMath::Max(EndTime - CurrentTime, 0);
FTransform PredictedRootMotion = TraitInstanceData.OnExtractRootMotionAttribute.Execute(CurrentTime, PredictionDelta, false);
FTransform EndTransform = PredictedRootMotion * StartTransform;
const FVector TraceDirectionWS = FVector::UpVector;
const FVector TraceStart = EndTransform.GetLocation() + (TraceDirectionWS * AlignToGroundNotify->TraceStartOffset);
const FVector TraceEnd = EndTransform.GetLocation() + (AlignToGroundNotify->TraceEndOffset * TraceDirectionWS);
FCollisionQueryParams QueryParams;
// Ignore self and all attached components
// QueryParams.AddIgnoredActor(Context.OwningActor);
const ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(AlignToGroundNotify->TraceChannel);
if (const UObject* HostObject = TraitInstanceData.HostObject)
{
UWorld* World = HostObject->GetWorld();
check(World);
FHitResult HitResult;
const bool bHit = World->SweepSingleByChannel(
HitResult, TraceStart, TraceEnd, FQuat::Identity, CollisionChannel, CollisionShape, QueryParams);
UE_VLOG_CAPSULE(TraitInstanceData.HostObject, "AlignToGround", Display,
TraceEnd,
(AlignToGroundNotify->TraceStartOffset - AlignToGroundNotify->TraceEndOffset) * 0.5f,
AlignToGroundNotify->TraceRadius,
FQuat::Identity,
FColor::Green,
TEXT(""));
if (bHit)
{
Transform = EndTransform;
Transform.SetLocation(HitResult.ImpactPoint);
static const FBox UnitBox(FVector(-10, -10, -10), FVector(10, 10, 10));
UE_VLOG_OBOX(TraitInstanceData.HostObject, "AlignToGround", Display, UnitBox, Transform.ToMatrixWithScale(), FColor::Red, TEXT(""));
if (!AlignToGroundNotify->PlaybackRateOutputVariable.IsNone())
{
float AnimatedFallDistance = StartTransform.GetTranslation().Z - EndTransform.GetTranslation().Z;
if (AnimatedFallDistance > UE_KINDA_SMALL_NUMBER && PredictionDelta > UE_KINDA_SMALL_NUMBER)
{
// Math computes acceleration due to gravity as animated,
// and then uses that to determine a modified playback rate for different actual fall heights (such that at the animated height the modifier will be 1.0)
// - assuming starting from 0 vertical velocity (which is typically where you would want to start this notify, as you start falling)
float AnimatedFallingAcceleration = 2.0f * AnimatedFallDistance / (PredictionDelta * PredictionDelta);
float ActualFallDistance = StartTransform.GetTranslation().Z - HitResult.ImpactPoint.Z;
float ModifiedTime = sqrtf(2.0f * ActualFallDistance/AnimatedFallingAcceleration);
float PlaybackRate = FMath::Clamp(PredictionDelta / ModifiedTime, AlignToGroundNotify->MinPlaybackRateModifier, AlignToGroundNotify->MaxPlaybackRateModifier);
TraitInstanceData.DataInterface->SetVariable(AlignToGroundNotify->PlaybackRateOutputVariable, (double)PlaybackRate);
}
}
return true;
}
}
return false;
}
FTransform GetModelSpaceTransform(const UE::AnimNext::FLODPoseStack& Pose, FBoneIndexType SkeletonBoneIndex)
{
const TArrayView<const FBoneIndexType> SkeletonToPoseIndexMap = Pose.GetSkeletonBoneIndexToLODBoneIndexMap();
const FBoneIndexType PoseIndex = SkeletonToPoseIndexMap[SkeletonBoneIndex];
FTransform BoneTransform = Pose.LocalTransforms[PoseIndex];
FBoneIndexType ParentSkeletonIndex = Pose.GetSkeletonAsset()->GetReferenceSkeleton().GetParentIndex(SkeletonBoneIndex);
while(ParentSkeletonIndex != (FBoneIndexType)-1)
{
const FBoneIndexType ParentPoseIndex = SkeletonToPoseIndexMap[ParentSkeletonIndex];
BoneTransform = BoneTransform * Pose.LocalTransforms[ParentPoseIndex];
ParentSkeletonIndex = Pose.GetSkeletonAsset()->GetReferenceSkeleton().GetParentIndex(ParentSkeletonIndex);
}
return BoneTransform;
}
bool FEvaluationNotify_AlignmentInstance::GetTargetTransform(UE::AnimNext::FEvaluationNotifiesTrait::FInstanceData& TraitInstanceData, FTransform& Transform)
{
const UNotifyState_Alignment* AlignmentNotify = CastChecked<UNotifyState_Alignment>(AnimNotify);
return TraitInstanceData.DataInterface->GetVariable(AlignmentNotify->TransformName, Transform) == EPropertyBagResult::Success;
}
void FEvaluationNotify_AlignmentInstance::Update(UE::AnimNext::FEvaluationNotifiesTrait::FInstanceData& TraitInstanceData, UE::AnimNext::FEvaluationVM& VM)
{
if (bDisabled)
{
return;
}
UNotifyState_AlignmentBase* AlignmentNotify = CastChecked<UNotifyState_AlignmentBase>(AnimNotify);
if (const TUniquePtr<UE::AnimNext::FKeyframeState>* Keyframe = VM.PeekValue<TUniquePtr<UE::AnimNext::FKeyframeState>>(UE::AnimNext::KEYFRAME_STACK_NAME, 0))
{
if (bFirstFrame)
{
bFirstFrame = false;
if (AlignmentNotify->Disable != NAME_None)
{
TraitInstanceData.DataInterface->GetVariable(AlignmentNotify->Disable, bDisabled);
if (bDisabled)
{
return;
}
}
ActualStartTime = CurrentTime;
const UE::AnimNext::FLODPoseStack& Pose = Keyframe->Get()->Pose;
AlignBone.Initialize(Pose.GetSkeletonAsset());
if (GetTargetTransform(TraitInstanceData, TargetTransform))
{
static const FBox UnitBox(FVector(-10, -10, -10), FVector(10, 10, 10));
UE_VLOG_OBOX(TraitInstanceData.HostObject, "Alignment", Display, UnitBox, TargetTransform.ToMatrixWithScale(), FColor::Blue, TEXT(""));
// get alignment transform
const float PredictionDelta = EndTime - CurrentTime;
if (AlignBone.HasValidSetup())
{
// get alignment bone relative to predicted root motion end point, and remove that as an offset to the alignment target
// Alignment bone is expected to be at the alignment end point at the beginning of the Notify window (we don't have a good way to predict the component space transform of it in the future)
FTransform PredictedRootMotion = TraitInstanceData.OnExtractRootMotionAttribute.Execute(CurrentTime, PredictionDelta, false);
FTransform AlignBoneTransform = GetModelSpaceTransform(Pose, AlignBone.BoneIndex);
TargetTransform = AlignBoneTransform.Inverse() * PredictedRootMotion * TargetTransform;
}
TargetTransform = AlignmentNotify->AlignOffset * TargetTransform;
if (AlignmentNotify->bForceTargetTransformUpright)
{
FRotator Rotation = TargetTransform.GetRotation().Rotator();
Rotation.Roll = 0;
Rotation.Pitch = 0;
TargetTransform.SetRotation(Rotation.Quaternion());
}
StartingRootTransform = TraitInstanceData.RootBoneTransform;
// extract root motion trajectory todo: this should be cached and reused
int NumFrames = 1 + (EndTime - CurrentTime) / AnimNextAlignment::FrameTime;
if (NumFrames <= 0)
{
return;
}
AnimTrajectoryData.Trajectory.SetNum(NumFrames);
AnimTrajectoryData.TranslationCurve.SetNum(NumFrames);
AnimTrajectoryData.RotationCurve.SetNum(NumFrames);
float SteeringAngleThreshold = FMath::DegreesToRadians(AlignmentNotify->SteeringSettings.AngleThreshold);
FTransform PredictedTransform;
float PredictionTime = CurrentTime;
for(int i=0;i<NumFrames; i++)
{
FTransform RootMotionThisFrame = TraitInstanceData.OnExtractRootMotionAttribute.Execute(PredictionTime, AnimNextAlignment::FrameTime, false);
PredictedTransform = RootMotionThisFrame * PredictedTransform;
AnimTrajectoryData.Trajectory[i] = PredictedTransform;
PredictionTime += AnimNextAlignment::FrameTime;
AnimTrajectoryData.TranslationCurve[i] = RootMotionThisFrame.GetTranslation().Length();
if (i>0)
{
AnimTrajectoryData.TranslationCurve[i] += AnimTrajectoryData.TranslationCurve[i-1];
}
AnimTrajectoryData.RotationCurve[i] = fabs(RootMotionThisFrame.GetRotation().GetAngle());
if (i>0)
{
AnimTrajectoryData.RotationCurve[i] += AnimTrajectoryData.RotationCurve[i-1];
}
}
// normalize curves;
if (AnimTrajectoryData.TranslationCurve.Last() > UE_SMALL_NUMBER)
{
if (AnimTrajectoryData.TranslationCurve.Last() < UE_SMALL_NUMBER)
{
AnimTrajectoryData.TranslationCurve.Reset();
}
else
{
for(float& Value : AnimTrajectoryData.TranslationCurve)
{
Value /= AnimTrajectoryData.TranslationCurve.Last();
}
}
}
if (AnimTrajectoryData.RotationCurve.Last() > UE_SMALL_NUMBER)
{
if (AnimTrajectoryData.RotationCurve.Last() < UE_SMALL_NUMBER)
{
AnimTrajectoryData.RotationCurve.Reset();
}
else
{
for(float& Value : AnimTrajectoryData.RotationCurve)
{
Value /= AnimTrajectoryData.RotationCurve.Last();
}
}
}
FVector UnWarpedPreviousPosition;
FVector WarpedPreviousPosition;
if (AlignmentNotify->bEnableSteering && AlignmentNotify->SteeringSettings.bEnableSmoothing)
{
FilteredSteeringTarget = FQuat::Identity;
TargetSmoothingState.Reset();
FilteredSteeringTarget = UKismetMathLibrary::QuaternionSpringInterp(FilteredSteeringTarget, FQuat::Identity, TargetSmoothingState,
AlignmentNotify->SteeringSettings.SmoothStiffness, AlignmentNotify->SteeringSettings.SmoothDamping, TraitInstanceData.DeltaTime, 1, 0, true);
}
WarpedTrajectory.SetNum(AnimTrajectoryData.Trajectory.Num());
FVector MovementDirection = TraitInstanceData.RootBoneTransform.GetRotation().GetRightVector(); // todo, get this from translation over first few frames or based on some setting?
FTransform InverseLastFrame = AnimTrajectoryData.Trajectory.Last().Inverse();
// Translation warping + steering
for(int i=0;i<NumFrames; i++)
{
FTransform TransformFromRoot = AnimTrajectoryData.Trajectory[i] * TraitInstanceData.RootBoneTransform;
FTransform TransformFromTarget = AnimTrajectoryData.Trajectory[i] * InverseLastFrame * TargetTransform;
FVector OldPosition = TransformFromRoot.GetTranslation();
FVector UnWarpedDelta = OldPosition - UnWarpedPreviousPosition;
UnWarpedPreviousPosition = OldPosition;
FVector NewPosition = OldPosition;
if (AlignmentNotify->bSeparateTranslationCurves)
{
float InMovementDirectionWeight = GetWeight(ActualStartTime + AnimNextAlignment::FrameTime*i, AlignmentNotify->TranslationWarpingCurve_InMovementDirection);
float OutOfMovementDirectionWeight = GetWeight(ActualStartTime + AnimNextAlignment::FrameTime*i, AlignmentNotify->TranslationWarpingCurve_OutOfMovementDirection);
FVector Delta = TransformFromTarget.GetTranslation() - OldPosition;
FVector InMovementDirectionDelta = MovementDirection * Delta.Dot(MovementDirection);
FVector OutOfMovementDirectionDelta = Delta - InMovementDirectionDelta;
NewPosition = OldPosition + InMovementDirectionDelta * InMovementDirectionWeight + OutOfMovementDirectionDelta * OutOfMovementDirectionWeight;
}
else
{
float Weight = GetWeight(ActualStartTime + AnimNextAlignment::FrameTime*i, AlignmentNotify->TranslationWarpingCurve);
NewPosition = FMath::Lerp(OldPosition, TransformFromTarget.GetTranslation(), Weight);
}
FVector WarpedDelta = NewPosition - WarpedPreviousPosition;
WarpedPreviousPosition = NewPosition;
WarpedTrajectory[i].SetTranslation(NewPosition);
WarpedTrajectory[i].SetRotation(TransformFromRoot.GetRotation());
if (i > 0 && AlignmentNotify->bEnableSteering)
{
FQuat OldRotation = TransformFromRoot.GetRotation();
FQuat DirectionChange = FQuat::FindBetweenVectors(UnWarpedDelta, WarpedDelta);
if (AlignmentNotify->SteeringSettings.bEnableSmoothing)
{
if (DirectionChange.GetAngle() < SteeringAngleThreshold)
{
FilteredSteeringTarget = UKismetMathLibrary::QuaternionSpringInterp(FilteredSteeringTarget, DirectionChange, TargetSmoothingState,
AlignmentNotify->SteeringSettings.SmoothStiffness, AlignmentNotify->SteeringSettings.SmoothDamping, TraitInstanceData.DeltaTime, 1, 0, true);
}
DirectionChange = FilteredSteeringTarget;
WarpedTrajectory[i].SetRotation(OldRotation*DirectionChange);
}
else
{
if (DirectionChange.GetAngle() < SteeringAngleThreshold)
{
WarpedTrajectory[i].SetRotation(OldRotation*DirectionChange);
}
}
}
}
// Rotation warping
for(int i=0;i<NumFrames; i++)
{
float Weight = GetWeight(ActualStartTime + AnimNextAlignment::FrameTime*i, AlignmentNotify->RotationWarpingCurve);
FQuat OldRotation = WarpedTrajectory[i].GetRotation();
FTransform TransformFromTarget = AnimTrajectoryData.Trajectory[i] * InverseLastFrame * TargetTransform;
WarpedTrajectory[i].SetRotation(FQuat::Slerp(OldRotation, TransformFromTarget.GetRotation(), Weight));
}
}
}
const UE::Anim::IAnimRootMotionProvider* RootMotionProvider = UE::Anim::IAnimRootMotionProvider::Get();
ensureMsgf(RootMotionProvider, TEXT("Alignment expected a valid root motion delta provider interface."));
if (RootMotionProvider && !WarpedTrajectory.IsEmpty())
{
float Frame = (CurrentTime - ActualStartTime) / AnimNextAlignment::FrameTime;
FTransform WorldTransform;
AnimNextAlignment::GetTransformForFrame(Frame, WarpedTrajectory, WorldTransform);
if(AlignmentNotify->UpdateMode == EAlignmentUpdateMode::World)
{
RootMotionProvider->OverrideRootMotion(WorldTransform.GetRelativeTransform(TraitInstanceData.RootBoneTransform), Keyframe->Get()->Attributes);
}
else // relative mode
{
FTransform PrevTransform;
AnimNextAlignment::GetTransformForFrame(PreviousFrame, WarpedTrajectory, PrevTransform);
RootMotionProvider->OverrideRootMotion(WorldTransform.GetRelativeTransform(PrevTransform), Keyframe->Get()->Attributes);
PreviousFrame = Frame;
}
// unwarped trajectory relative to starting transform
FTransform PreviousTransform = AnimTrajectoryData.Trajectory[0] * StartingRootTransform;
for(int i=1;i<AnimTrajectoryData.Trajectory.Num(); i++)
{
FTransform TransformFromRoot = AnimTrajectoryData.Trajectory[i] * StartingRootTransform;
UE_VLOG_SEGMENT(TraitInstanceData.HostObject, "Alignment", Display, PreviousTransform.GetLocation(), TransformFromRoot.GetLocation(), i%2 == 0 ? FColor::Yellow : FColor::Red, TEXT(""));
PreviousTransform = TransformFromRoot;
}
// unwarped trajectory relative to target transform
FTransform InverseLastFrame = AnimTrajectoryData.Trajectory.Last().Inverse();
PreviousTransform = AnimTrajectoryData.Trajectory[0] * InverseLastFrame * TargetTransform;
for(int i=1;i<AnimTrajectoryData.Trajectory.Num(); i++)
{
FTransform TransformFromTarget = AnimTrajectoryData.Trajectory[i] * InverseLastFrame * TargetTransform;
UE_VLOG_SEGMENT(TraitInstanceData.HostObject, "Alignment", Display, PreviousTransform.GetLocation(), TransformFromTarget.GetLocation(), i%2 == 0 ? FColor::Yellow : FColor::Red, TEXT(""));
PreviousTransform = TransformFromTarget;
}
// the warped trajectory
for(int i=1; i<WarpedTrajectory.Num(); i++)
{
UE_VLOG_SEGMENT(TraitInstanceData.HostObject, "Alignment", Display, WarpedTrajectory[i-1].GetLocation(), WarpedTrajectory[i].GetLocation(), i%2 == 0 ? FColor::Green : FColor::Blue, TEXT(""));
UE_VLOG_SEGMENT(TraitInstanceData.HostObject, "Alignment", Display, WarpedTrajectory[i].GetLocation(), WarpedTrajectory[i].GetLocation() + WarpedTrajectory[i].GetRotation().GetUpVector() * 10, i%2 == 0 ? FColor::Yellow : FColor::Red, TEXT(""));
}
// a dot representing our current position on the trajectory
UE_VLOG_SPHERE(TraitInstanceData.HostObject, "Alignment", Display, WorldTransform.GetLocation(), 1, FColor::Red, TEXT(""));
}
}
}