void UBlendSpaceBase::TickAssetPlayer(FAnimTickRecord& Instance, struct FAnimNotifyQueue& NotifyQueue, FAnimAssetTickContext& Context) const { const float DeltaTime = Context.GetDeltaTime(); float MoveDelta = Instance.PlayRateMultiplier * DeltaTime; // this happens even if MoveDelta == 0.f. This still should happen if it is being interpolated // since we allow setting position of blendspace, we can't ignore MoveDelta == 0.f // also now we don't have to worry about not following if DeltaTime = 0.f { // first filter input using blend filter const FVector BlendSpacePosition(Instance.BlendSpace.BlendSpacePositionX, Instance.BlendSpace.BlendSpacePositionY, 0.f); const FVector BlendInput = FilterInput(Instance.BlendSpace.BlendFilter, BlendSpacePosition, DeltaTime); EBlendSpaceAxis AxisToScale = GetAxisToScale(); if (AxisToScale != BSA_None) { float FilterMultiplier = 1.f; // first use multiplier using new blendinput // new filtered input is going to be used for sampling animation // so we'll need to change playrate if you'd like to not slide foot if ( !BlendSpacePosition.Equals(BlendInput) ) { // apply speed change if you want, if (AxisToScale == BSA_X) { if (BlendInput.X != 0.f) { FilterMultiplier = BlendSpacePosition.X / BlendInput.X; } } else if (AxisToScale == BSA_Y) { if (BlendInput.Y != 0.f) { FilterMultiplier = BlendSpacePosition.Y / BlendInput.Y; } } } // now find if clamped input is different // if different, then apply scale to fit in FVector ClampedInput = ClampBlendInput(BlendInput); if ( !ClampedInput.Equals(BlendInput) ) { // apply speed change if you want, if (AxisToScale == BSA_X) { if (ClampedInput.X != 0.f) { FilterMultiplier *= BlendInput.X / ClampedInput.X; } } else if (AxisToScale == BSA_Y) { if (ClampedInput.Y != 0.f) { FilterMultiplier *= BlendInput.Y / ClampedInput.Y; } } } MoveDelta *= FilterMultiplier; UE_LOG(LogAnimation, Log, TEXT("BlendSpace(%s) - BlendInput(%s) : FilteredBlendInput(%s), FilterMultiplier(%0.2f)"), *GetName(), *BlendSpacePosition.ToString(), *BlendInput.ToString(), FilterMultiplier ); } check(Instance.BlendSpace.BlendSampleDataCache); // For Target weight interpolation, we'll need to save old data, and interpolate to new data TArray<FBlendSampleData>& OldSampleDataList = FBlendSpaceScratchData::Get().OldSampleDataList; TArray<FBlendSampleData>& NewSampleDataList = FBlendSpaceScratchData::Get().NewSampleDataList; check(!OldSampleDataList.Num() && !NewSampleDataList.Num()); // this must be called non-recursively OldSampleDataList.Append(*Instance.BlendSpace.BlendSampleDataCache); // get sample data based on new input // consolidate all samples and sort them, so that we can handle from biggest weight to smallest Instance.BlendSpace.BlendSampleDataCache->Reset(); // new sample data that will be used for evaluation TArray<FBlendSampleData> & SampleDataList = *Instance.BlendSpace.BlendSampleDataCache; // get sample data from blendspace if (GetSamplesFromBlendInput(BlendInput, NewSampleDataList)) { float NewAnimLength=0.f; float PreInterpAnimLength = 0.f; // if target weight interpolation is set if (TargetWeightInterpolationSpeedPerSec > 0.f) { UE_LOG(LogAnimation, Verbose, TEXT("Target Weight Interpolation: Target Samples ")); // recalculate AnimLength based on weight of target animations - this is used for scaling animation later (change speed) PreInterpAnimLength = GetAnimationLengthFromSampleData(NewSampleDataList); UE_LOG(LogAnimation, Verbose, TEXT("BlendSpace(%s) - BlendInput(%s) : PreAnimLength(%0.5f) "), *GetName(), *BlendInput.ToString(), PreInterpAnimLength); // target weight interpolation if (InterpolateWeightOfSampleData(DeltaTime, OldSampleDataList, NewSampleDataList, SampleDataList)) { // now I need to normalize FBlendSampleData::NormalizeDataWeight(SampleDataList); } else { // if interpolation failed, just copy new sample data tto sample data SampleDataList = NewSampleDataList; } // recalculate AnimLength based on weight of animations UE_LOG(LogAnimation, Verbose, TEXT("Target Weight Interpolation: Interp Samples ")); } else { // when there is no target weight interpolation, just copy new to target SampleDataList.Append(NewSampleDataList); } bool bCanDoMarkerSync = (SampleIndexWithMarkers != INDEX_NONE) && (Context.IsSingleAnimationContext() || (Instance.bCanUseMarkerSync && Context.CanUseMarkerPosition())); if (bCanDoMarkerSync) { //Copy previous frame marker data to current frame for (FBlendSampleData& PrevBlendSampleItem : OldSampleDataList) { for (FBlendSampleData& CurrentBlendSampleItem : SampleDataList) { // it only can have one animation in the sample, make sure to copy Time if (PrevBlendSampleItem.Animation && PrevBlendSampleItem.Animation == CurrentBlendSampleItem.Animation) { CurrentBlendSampleItem.Time = PrevBlendSampleItem.Time; CurrentBlendSampleItem.PreviousTime = PrevBlendSampleItem.PreviousTime; CurrentBlendSampleItem.MarkerTickRecord = PrevBlendSampleItem.MarkerTickRecord; } } } } NewAnimLength = GetAnimationLengthFromSampleData(SampleDataList); if (PreInterpAnimLength > 0.f && NewAnimLength > 0.f) { MoveDelta *= PreInterpAnimLength / NewAnimLength; } float& NormalizedCurrentTime = *(Instance.TimeAccumulator); const float NormalizedPreviousTime = NormalizedCurrentTime; // @note for sync group vs non sync group // in blendspace, it will still sync even if only one node in sync group // so you're never non-sync group unless you have situation where some markers are relevant to one sync group but not all the time // here we save NormalizedCurrentTime as Highest weighted samples' position in sync group // if you're not in sync group, NormalizedCurrentTime is based on normalized length by sample weights // if you move between sync to non sync within blendspace, you're going to see pop because we'll have to jump // for now, our rule is to keep normalized time as highest weighted sample position within its own length // also MoveDelta doesn't work if you're in sync group. It will move according to sync group position // @todo consider using MoveDelta when this is leader, but that can be scary because it's not matching with DeltaTime any more. // if you have interpolation delay, that value can be applied, but the output might be unpredictable. // // to fix this better in the future, we should use marker sync position from last tick // but that still doesn't fix if you just join sync group, you're going to see pop since your animation doesn't fix if (Context.IsLeader()) { // advance current time - blend spaces hold normalized time as when dealing with changing anim length it would be possible to go backwards UE_LOG(LogAnimation, Verbose, TEXT("BlendSpace(%s) - BlendInput(%s) : AnimLength(%0.5f) "), *GetName(), *BlendInput.ToString(), NewAnimLength); const int32 HighestMarkerSyncWeightIndex = bCanDoMarkerSync ? GetHighestWeightMarkerSyncSample(SampleDataList, SampleData) : -1; if (HighestMarkerSyncWeightIndex == -1) { bCanDoMarkerSync = false; } if (bCanDoMarkerSync) { FBlendSampleData& SampleDataItem = SampleDataList[HighestMarkerSyncWeightIndex]; const FBlendSample& Sample = SampleData[SampleDataItem.SampleDataIndex]; bool bResetMarkerDataOnFollowers = false; if (!Instance.MarkerTickRecord->IsValid()) { SampleDataItem.MarkerTickRecord.Reset(); bResetMarkerDataOnFollowers = true; } else if (!SampleDataItem.MarkerTickRecord.IsValid() && Context.MarkerTickContext.GetMarkerSyncStartPosition().IsValid()) { Sample.Animation->GetMarkerIndicesForPosition(Context.MarkerTickContext.GetMarkerSyncStartPosition(), true, SampleDataItem.MarkerTickRecord.PreviousMarker, SampleDataItem.MarkerTickRecord.NextMarker, SampleDataItem.Time); } const float NewDeltaTime = Context.GetDeltaTime() * Instance.PlayRateMultiplier; if (!FMath::IsNearlyZero(NewDeltaTime)) { Context.SetLeaderDelta(NewDeltaTime); Sample.Animation->TickByMarkerAsLeader(SampleDataItem.MarkerTickRecord, Context.MarkerTickContext, SampleDataItem.Time, SampleDataItem.PreviousTime, NewDeltaTime, true); check(Context.MarkerTickContext.IsMarkerSyncStartValid()); TickFollowerSamples(SampleDataList, HighestMarkerSyncWeightIndex, Context, bResetMarkerDataOnFollowers); } NormalizedCurrentTime = SampleDataItem.Time / Sample.Animation->SequenceLength; *Instance.MarkerTickRecord = SampleDataItem.MarkerTickRecord; } else { // Advance time using current/new anim length float CurrentTime = NormalizedCurrentTime * NewAnimLength; FAnimationRuntime::AdvanceTime(Instance.bLooping, MoveDelta, /*inout*/ CurrentTime, NewAnimLength); NormalizedCurrentTime = NewAnimLength ? (CurrentTime / NewAnimLength) : 0.0f; UE_LOG(LogAnimMarkerSync, Log, TEXT("Leader (%s) (normal advance) - PreviousTime (%0.2f), CurrentTime (%0.2f), MoveDelta (%0.2f) "), *GetName(), NormalizedPreviousTime, NormalizedCurrentTime, MoveDelta); } Context.SetAnimationPositionRatio(NormalizedCurrentTime); } else { if(!Context.MarkerTickContext.IsMarkerSyncStartValid()) { bCanDoMarkerSync = false; } if (bCanDoMarkerSync) { const int32 HighestWeightIndex = GetHighestWeightSample(SampleDataList); FBlendSampleData& SampleDataItem = SampleDataList[HighestWeightIndex]; const FBlendSample& Sample = SampleData[SampleDataItem.SampleDataIndex]; if (Context.GetDeltaTime() != 0.f) { if(!Instance.MarkerTickRecord->IsValid()) { SampleDataItem.Time = NormalizedCurrentTime * Sample.Animation->SequenceLength; } TickFollowerSamples(SampleDataList, -1, Context, false); } *Instance.MarkerTickRecord = SampleDataItem.MarkerTickRecord; NormalizedCurrentTime = SampleDataItem.Time / Sample.Animation->SequenceLength; } else { NormalizedCurrentTime = Context.GetAnimationPositionRatio(); UE_LOG(LogAnimMarkerSync, Log, TEXT("Leader (%s) (normal advance) - PreviousTime (%0.2f), CurrentTime (%0.2f), MoveDelta (%0.2f) "), *GetName(), NormalizedPreviousTime, NormalizedCurrentTime, MoveDelta); } } // generate notifies and sets time { TArray<const FAnimNotifyEvent*> Notifies; const float ClampedNormalizedPreviousTime = FMath::Clamp<float>(NormalizedPreviousTime, 0.f, 1.f); const float ClampedNormalizedCurrentTime = FMath::Clamp<float>(NormalizedCurrentTime, 0.f, 1.f); const bool bGenerateNotifies = Context.ShouldGenerateNotifies() && (NormalizedCurrentTime != NormalizedPreviousTime) && NotifyTriggerMode != ENotifyTriggerMode::None; // Get the index of the highest weight, assuming that the first is the highest until we find otherwise const bool bTriggerNotifyHighestWeightedAnim = NotifyTriggerMode == ENotifyTriggerMode::HighestWeightedAnimation && SampleDataList.Num() > 0; const int32 HighestWeightIndex = (bGenerateNotifies && bTriggerNotifyHighestWeightedAnim) ? GetHighestWeightSample(SampleDataList) : -1; for (int32 I = 0; I < SampleDataList.Num(); ++I) { FBlendSampleData& SampleEntry = SampleDataList[I]; const int32 SampleDataIndex = SampleEntry.SampleDataIndex; // Skip SamplesPoints that has no relevant weight if( SampleData.IsValidIndex(SampleDataIndex) && (SampleEntry.TotalWeight > ZERO_ANIMWEIGHT_THRESH) ) { const FBlendSample& Sample = SampleData[SampleDataIndex]; if( Sample.Animation ) { float PrevSampleDataTime; float& CurrentSampleDataTime = SampleEntry.Time; if (!bCanDoMarkerSync || Sample.Animation->AuthoredSyncMarkers.Num() == 0) //Have already updated time if we are doing marker sync { const float SampleNormalizedPreviousTime = Sample.Animation->RateScale >= 0.f ? ClampedNormalizedPreviousTime : 1.f - ClampedNormalizedPreviousTime; const float SampleNormalizedCurrentTime = Sample.Animation->RateScale >= 0.f ? ClampedNormalizedCurrentTime : 1.f - ClampedNormalizedCurrentTime; PrevSampleDataTime = SampleNormalizedPreviousTime * Sample.Animation->SequenceLength; CurrentSampleDataTime = SampleNormalizedCurrentTime * Sample.Animation->SequenceLength; } else { PrevSampleDataTime = SampleEntry.PreviousTime; } // Figure out delta time float DeltaTimePosition = CurrentSampleDataTime - PrevSampleDataTime; const float SampleMoveDelta = MoveDelta * Sample.Animation->RateScale; // if we went against play rate, then loop around. if ((SampleMoveDelta * DeltaTimePosition) < 0.f) { DeltaTimePosition += FMath::Sign<float>(SampleMoveDelta) * Sample.Animation->SequenceLength; } if( bGenerateNotifies && (!bTriggerNotifyHighestWeightedAnim || (I == HighestWeightIndex))) { // Harvest and record notifies Sample.Animation->GetAnimNotifies(PrevSampleDataTime, DeltaTimePosition, Instance.bLooping, Notifies); } if (Context.RootMotionMode == ERootMotionMode::RootMotionFromEverything && Sample.Animation->bEnableRootMotion) { Context.RootMotionMovementParams.AccumulateWithBlend(Sample.Animation->ExtractRootMotion(PrevSampleDataTime, DeltaTimePosition, Instance.bLooping), SampleEntry.GetWeight()); } UE_LOG(LogAnimation, Verbose, TEXT("%d. Blending animation(%s) with %f weight at time %0.2f"), I+1, *Sample.Animation->GetName(), SampleEntry.GetWeight(), CurrentSampleDataTime); } } } if (bGenerateNotifies && Notifies.Num() > 0) { NotifyQueue.AddAnimNotifies(Notifies, Instance.EffectiveBlendWeight); } } } OldSampleDataList.Reset(); NewSampleDataList.Reset(); } }
void UBlendSpaceBase::TickAssetPlayerInstance(const FAnimTickRecord& Instance, class UAnimInstance* InstanceOwner, FAnimAssetTickContext& Context) const { const float DeltaTime = Context.GetDeltaTime(); float MoveDelta = Instance.PlayRateMultiplier * DeltaTime; // this happens even if MoveDelta == 0.f. This still should happen if it is being interpolated // since we allow setting position of blendspace, we can't ignore MoveDelta == 0.f // also now we don't have to worry about not following if DeltaTime = 0.f { // first filter input using blend filter const FVector BlendInput = FilterInput(Instance.BlendFilter, Instance.BlendSpacePosition, DeltaTime); EBlendSpaceAxis AxisToScale = GetAxisToScale(); if (AxisToScale != BSA_None) { float FilterMultiplier = 1.f; // first use multiplier using new blendinput // new filtered input is going to be used for sampling animation // so we'll need to change playrate if you'd like to not slide foot if ( !Instance.BlendSpacePosition.Equals(BlendInput) ) { // apply speed change if you want, if (AxisToScale == BSA_X) { if (BlendInput.X != 0.f) { FilterMultiplier = Instance.BlendSpacePosition.X / BlendInput.X; } } else if (AxisToScale == BSA_Y) { if (BlendInput.Y != 0.f) { FilterMultiplier = Instance.BlendSpacePosition.Y / BlendInput.Y; } } } // now find if clamped input is different // if different, then apply scale to fit in FVector ClampedInput = ClampBlendInput(BlendInput); if ( !ClampedInput.Equals(BlendInput) ) { // apply speed change if you want, if (AxisToScale == BSA_X) { if (ClampedInput.X != 0.f) { FilterMultiplier *= BlendInput.X / ClampedInput.X; } } else if (AxisToScale == BSA_Y) { if (ClampedInput.Y != 0.f) { FilterMultiplier *= BlendInput.Y / ClampedInput.Y; } } } MoveDelta *= FilterMultiplier; UE_LOG(LogAnimation, Log, TEXT("BlendSpace(%s) - BlendInput(%s) : FilteredBlendInput(%s), FilterMultiplier(%0.2f)"), *GetName(), *Instance.BlendSpacePosition.ToString(), *BlendInput.ToString(), FilterMultiplier ); } check(Instance.BlendSampleDataCache); // For Target weight interpolation, we'll need to save old data, and interpolate to new data static TArray<FBlendSampleData> OldSampleDataList; static TArray<FBlendSampleData> NewSampleDataList; check(IsInGameThread() && !OldSampleDataList.Num() && !NewSampleDataList.Num()); // this must be called non-recursively on the game thread OldSampleDataList.Append(*Instance.BlendSampleDataCache); // get sample data based on new input // consolidate all samples and sort them, so that we can handle from biggest weight to smallest Instance.BlendSampleDataCache->Reset(); // new sample data that will be used for evaluation TArray<FBlendSampleData> & SampleDataList = *Instance.BlendSampleDataCache; // get sample data from blendspace if (GetSamplesFromBlendInput(BlendInput, NewSampleDataList)) { float NewAnimLength=0; // if target weight interpolation is set if (TargetWeightInterpolationSpeedPerSec > 0.f) { UE_LOG(LogAnimation, Verbose, TEXT("Target Weight Interpolation: Target Samples ")); // recalculate AnimLength based on weight of target animations - this is used for scaling animation later (change speed) float PreInterpAnimLength = GetAnimationLengthFromSampleData(NewSampleDataList); UE_LOG(LogAnimation, Verbose, TEXT("BlendSpace(%s) - BlendInput(%s) : PreAnimLength(%0.5f) "), *GetName(), *BlendInput.ToString(), PreInterpAnimLength); // target weight interpolation if (InterpolateWeightOfSampleData(DeltaTime, OldSampleDataList, NewSampleDataList, SampleDataList)) { // now I need to normalize NormalizeSampleDataWeight(SampleDataList); } else { // if interpolation failed, just copy new sample data tto sample data SampleDataList = NewSampleDataList; } // recalculate AnimLength based on weight of animations UE_LOG(LogAnimation, Verbose, TEXT("Target Weight Interpolation: Interp Samples ")); NewAnimLength = GetAnimationLengthFromSampleData(SampleDataList); // now scale the animation if (NewAnimLength > 0.f) { MoveDelta *= PreInterpAnimLength/NewAnimLength; } } else { // when there is no target weight interpolation, just copy new to target SampleDataList.Append(NewSampleDataList); NewAnimLength = GetAnimationLengthFromSampleData(SampleDataList); } float& NormalizedCurrentTime = *(Instance.TimeAccumulator); const float NormalizedPreviousTime = NormalizedCurrentTime; if (Context.IsLeader()) { // advance current time - blend spaces hold normalized time as when dealing with changing anim length it would be possible to go backwards UE_LOG(LogAnimation, Verbose, TEXT("BlendSpace(%s) - BlendInput(%s) : AnimLength(%0.5f) "), *GetName(), *BlendInput.ToString(), NewAnimLength); // Advance time using current/new anim length float CurrentTime = NormalizedCurrentTime * NewAnimLength; FAnimationRuntime::AdvanceTime(Instance.bLooping, MoveDelta, /*inout*/ CurrentTime, NewAnimLength); NormalizedCurrentTime = NewAnimLength ? (CurrentTime / NewAnimLength) : 0.0f; Context.SetSyncPoint(NormalizedCurrentTime); } else { NormalizedCurrentTime = Context.GetSyncPoint(); } // generate notifies and sets time { TArray<const FAnimNotifyEvent*> Notifies; // now calculate time for each samples const float ClampedNormalizedPreviousTime = FMath::Clamp<float>(NormalizedPreviousTime, 0.f, 1.f); const float ClampedNormalizedCurrentTime = FMath::Clamp<float>(NormalizedCurrentTime, 0.f, 1.f); const bool bGenerateNotifies = Context.ShouldGenerateNotifies() && (NormalizedCurrentTime != NormalizedPreviousTime) && NotifyTriggerMode != ENotifyTriggerMode::None; int32 HighestWeightIndex = 0; // Get the index of the highest weight, assuming that the first is the highest until we find otherwise bool bTriggerNotifyHighestWeightedAnim = NotifyTriggerMode == ENotifyTriggerMode::HighestWeightedAnimation && SampleDataList.Num() > 0; if(bGenerateNotifies && bTriggerNotifyHighestWeightedAnim) { float HighestWeight = SampleDataList[HighestWeightIndex].GetWeight(); for(int32 I = 1 ; I < SampleDataList.Num(); I++) { if(SampleDataList[I].GetWeight() > HighestWeight) { HighestWeightIndex = I; HighestWeight = SampleDataList[I].GetWeight(); } } } for (int32 I = 0; I < SampleDataList.Num(); ++I) { FBlendSampleData& SampleEntry = SampleDataList[I]; const int32 SampleDataIndex = SampleEntry.SampleDataIndex; // Skip SamplesPoints that has no relevant weight if( SampleData.IsValidIndex(SampleDataIndex) && (SampleEntry.TotalWeight > ZERO_ANIMWEIGHT_THRESH) ) { const FBlendSample& Sample = SampleData[SampleDataIndex]; if( Sample.Animation ) { const float SampleNormalizedPreviousTime = Sample.Animation->RateScale >= 0.f ? ClampedNormalizedPreviousTime : 1.f - ClampedNormalizedPreviousTime; const float SampleNormalizedCurrentTime = Sample.Animation->RateScale >= 0.f ? ClampedNormalizedCurrentTime : 1.f - ClampedNormalizedCurrentTime; const float PrevSampleDataTime = SampleNormalizedPreviousTime * Sample.Animation->SequenceLength; float& CurrentSampleDataTime = SampleEntry.Time; CurrentSampleDataTime = SampleNormalizedCurrentTime * Sample.Animation->SequenceLength; // Figure out delta time float DeltaTimePosition = CurrentSampleDataTime - PrevSampleDataTime; const float SampleMoveDelta = MoveDelta * Sample.Animation->RateScale; // if we went against play rate, then loop around. if ((SampleMoveDelta * DeltaTimePosition) < 0.f) { DeltaTimePosition += FMath::Sign<float>(SampleMoveDelta) * Sample.Animation->SequenceLength; } if( bGenerateNotifies && (!bTriggerNotifyHighestWeightedAnim || (I == HighestWeightIndex))) { // Harvest and record notifies Sample.Animation->GetAnimNotifies(PrevSampleDataTime, DeltaTimePosition, Instance.bLooping, Notifies); } if (Context.RootMotionMode == ERootMotionMode::RootMotionFromEverything && Sample.Animation->bEnableRootMotion) { Context.RootMotionMovementParams.AccumulateWithBlend(Sample.Animation->ExtractRootMotion(PrevSampleDataTime, DeltaTimePosition, Instance.bLooping), SampleEntry.GetWeight()); } UE_LOG(LogAnimation, Verbose, TEXT("%d. Blending animation(%s) with %f weight at time %0.2f"), I+1, *Sample.Animation->GetName(), SampleEntry.GetWeight(), CurrentSampleDataTime); } } } if (bGenerateNotifies && Notifies.Num() > 0) { InstanceOwner->AddAnimNotifies(Notifies, Instance.EffectiveBlendWeight); } } } OldSampleDataList.Reset(); NewSampleDataList.Reset(); } }