415 lines
14 KiB
C++
415 lines
14 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Evaluation/MovieScenePlaybackManager.h"
|
|
|
|
#include "Channels/MovieSceneTimeWarpChannel.h"
|
|
#include "Misc/CoreMiscDefines.h"
|
|
#include "MovieScene.h"
|
|
#include "MovieSceneFwd.h"
|
|
#include "MovieSceneSequence.h"
|
|
|
|
FMovieScenePlaybackManager::FMovieScenePlaybackManager()
|
|
{
|
|
}
|
|
|
|
FMovieScenePlaybackManager::FMovieScenePlaybackManager(UMovieSceneSequence* InSequence)
|
|
{
|
|
Initialize(InSequence);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::Initialize(UMovieSceneSequence* InSequence)
|
|
{
|
|
if (!ensure(InSequence))
|
|
{
|
|
return;
|
|
}
|
|
|
|
UMovieScene* MovieScene = InSequence->GetMovieScene();
|
|
const EMovieSceneEvaluationType EvaluationType = MovieScene->GetEvaluationType();
|
|
|
|
DisplayRate = MovieScene->GetDisplayRate();
|
|
|
|
// Make our playback position work *only* in ticks. We will handle conversion to/from frames ourselves.
|
|
const FFrameRate TickResolution = MovieScene->GetTickResolution();
|
|
PlaybackPosition.SetTimeBase(TickResolution, TickResolution, EvaluationType);
|
|
|
|
const TRange<FFrameNumber> PlaybackRange = MovieScene->GetPlaybackRange();
|
|
SequenceStartTick = UE::MovieScene::DiscreteInclusiveLower(PlaybackRange);
|
|
SequenceEndTick = UE::MovieScene::DiscreteExclusiveUpper(PlaybackRange);
|
|
|
|
ResetPlaybackSettings();
|
|
|
|
PlaybackPosition.Reset(SequenceStartTick);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::ResetPlaybackSettings()
|
|
{
|
|
StartOffsetTicks = EndOffsetTicks = FFrameNumber(0);
|
|
NumLoopsToPlay = 1;
|
|
NumLoopsCompleted = 0;
|
|
PlayRate = 1.0;
|
|
PlayDirection = EPlayDirection::Forwards;
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::Update(float InDeltaSeconds, FContexts& OutContexts)
|
|
{
|
|
using namespace UE::MovieScene;
|
|
|
|
if (PlaybackStatus != EMovieScenePlayerStatus::Playing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the new time, advanced by InDeltaSeconds.
|
|
const FFrameTime PreviousTick = PlaybackPosition.GetCurrentPosition();
|
|
const FFrameTime DeltaTicks = PlaybackPosition.GetInputRate().AsFrameTime(
|
|
(IsPlayingForward() ? InDeltaSeconds : (-InDeltaSeconds)) * PlayRate);
|
|
const FFrameTime NextTick = PreviousTick + DeltaTicks;
|
|
|
|
FFrameTime WarpedNextTick = NextTick;
|
|
if (bTransformPlaybackTime)
|
|
{
|
|
if (TimeTransform.FindFirstWarpDomain() == ETimeWarpChannelDomain::PlayRate)
|
|
{
|
|
WarpedNextTick = TimeTransform.TransformTime(WarpedNextTick);
|
|
}
|
|
}
|
|
|
|
InternalUpdateToTick(WarpedNextTick.RoundToFrame(), OutContexts);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::UpdateTo(const FFrameTime NextTime, FContexts& OutContexts)
|
|
{
|
|
const FFrameTime NextTick = ConvertFrameTime(NextTime, DisplayRate, PlaybackPosition.GetInputRate());
|
|
InternalUpdateToTick(NextTick.RoundToFrame(), OutContexts);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::InternalUpdateToTick(const FFrameNumber NextTick, FContexts& OutContexts)
|
|
{
|
|
using namespace UE::MovieScene;
|
|
|
|
// If we are stopped, just move the playhead to the given time, without generating evaluation contexts.
|
|
if (PlaybackStatus == EMovieScenePlayerStatus::Stopped)
|
|
{
|
|
PlaybackPosition.Reset(NextTick);
|
|
return;
|
|
}
|
|
|
|
// Check we have some loop counters that make sense.
|
|
if (NumLoopsToPlay > 0 && !ensure(NumLoopsCompleted < NumLoopsToPlay))
|
|
{
|
|
PlaybackStatus = EMovieScenePlayerStatus::Stopped;
|
|
PlaybackPosition.Reset(NextTick);
|
|
}
|
|
|
|
// Gather some information about this update.
|
|
const bool bShouldJump =
|
|
(PlaybackStatus != EMovieScenePlayerStatus::Playing && PlaybackStatus != EMovieScenePlayerStatus::Scrubbing);
|
|
|
|
const FFrameNumber EffectiveStartTick = SequenceStartTick + StartOffsetTicks;
|
|
const FFrameNumber EffectiveEndTick = SequenceEndTick - EndOffsetTicks;
|
|
const FFrameNumber EffectiveDurationTicks = FMath::Max(FFrameNumber(0), EffectiveEndTick - EffectiveStartTick);
|
|
const FFrameNumber LastValidTick = GetLastValidTick();
|
|
// IMPORTANT: we assume that LastValidTick is less than the duration (current implementation is
|
|
// duration minus one tick).
|
|
|
|
const bool bIsPlayingForward = IsPlayingForward();
|
|
const FFrameNumber LoopStartTick = bIsPlayingForward ? EffectiveStartTick : LastValidTick;
|
|
const FFrameNumber LoopLastTick = bIsPlayingForward ? LastValidTick : EffectiveStartTick;
|
|
// If the start/end offsets make the duration 0, we treat each loop as one frame long.
|
|
const FFrameNumber LoopDurationTicks = FMath::Max(FFrameNumber(1), EffectiveDurationTicks);
|
|
|
|
// Figure out if we crossed the loop-end boundary.
|
|
if ((bIsPlayingForward && NextTick > LoopLastTick) ||
|
|
((!bIsPlayingForward && NextTick < LoopLastTick)))
|
|
{
|
|
// Compute how many times we crossed the loop-end boundary.
|
|
const FFrameNumber LoopRelativeTick = NextTick - LoopStartTick;
|
|
const int32 NumLoopingsOver = FMath::Abs(LoopRelativeTick.Value) / LoopDurationTicks.Value;
|
|
ensure(NumLoopingsOver > 0);
|
|
|
|
// Massage this a bit with the following rules:
|
|
//
|
|
// 1) Don't go over the number of loops to play unless we're playing indefinitely.
|
|
//
|
|
// 2) Add an extra completed loop if we reached the end (as opposed to crossing it) because
|
|
// that should count as "completing a loop".
|
|
//
|
|
const int32 NumLoopsNewlyCompleted = ((NumLoopsToPlay > 0) ?
|
|
FMath::Min(NumLoopingsOver, NumLoopsToPlay - NumLoopsCompleted) :
|
|
NumLoopingsOver);
|
|
|
|
// Play the last bit of the loop if we are looping and doing any sort of dissections.
|
|
if (DissectLooping != EMovieSceneLoopDissection::None)
|
|
{
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(LoopLastTick), PlaybackStatus)
|
|
.SetHasJumped(bShouldJump)
|
|
);
|
|
}
|
|
|
|
// See if we need to generate more update ranges for the loops. This can happen if we had a
|
|
// large delta-time, and the duration of a loop is pretty short (i.e. we could have looped
|
|
// several times in one update).
|
|
if (NumLoopingsOver > 1)
|
|
{
|
|
const int32 ExtraLoops = NumLoopingsOver - 1;
|
|
|
|
if (DissectLooping == EMovieSceneLoopDissection::DissectAll)
|
|
{
|
|
// If we dissect the looping, we add an explicit update for each loop, from start to end.
|
|
if (!bPingPongPlayback)
|
|
{
|
|
for (int32 Index = 0; Index < ExtraLoops; ++Index)
|
|
{
|
|
PlaybackPosition.Reset(LoopStartTick);
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(LoopLastTick), PlaybackStatus)
|
|
.SetHasJumped(true)
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReversePlayDirection();
|
|
|
|
for (int32 Index = 0; Index < ExtraLoops; ++Index)
|
|
{
|
|
if (IsPlayingForward())
|
|
{
|
|
PlaybackPosition.Reset(EffectiveStartTick);
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(LastValidTick), PlaybackStatus)
|
|
.SetHasJumped(true)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
PlaybackPosition.Reset(LastValidTick);
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(EffectiveStartTick), PlaybackStatus)
|
|
.SetHasJumped(true)
|
|
);
|
|
}
|
|
|
|
ReversePlayDirection();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We are not dissecting loops so don't emit extra evaluation contexts.
|
|
// If we are ping-pong'ing, we at least need to keep track of which way we are now going.
|
|
if (bPingPongPlayback && (ExtraLoops + 1) % 2 != 0)
|
|
{
|
|
ReversePlayDirection();
|
|
}
|
|
}
|
|
}
|
|
else if (bPingPongPlayback)
|
|
{
|
|
ReversePlayDirection();
|
|
}
|
|
|
|
// Complete the loops we said we completed.
|
|
NumLoopsCompleted += NumLoopsNewlyCompleted;
|
|
|
|
if (NumLoopsToPlay > 0 && NumLoopsCompleted >= NumLoopsToPlay)
|
|
{
|
|
// If we have played all the loops we needed to play, we can stop playback.
|
|
// However, if we don't dissect looping, we didn't finish the loop, so do it now.
|
|
if (DissectLooping == EMovieSceneLoopDissection::None)
|
|
{
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(LoopLastTick), PlaybackStatus)
|
|
.SetHasJumped(bShouldJump)
|
|
);
|
|
}
|
|
|
|
PlaybackStatus = EMovieScenePlayerStatus::Stopped;
|
|
}
|
|
else
|
|
{
|
|
// Start the next loop with any overplay from the update.
|
|
//
|
|
// When playing forward, we have, e.g.:
|
|
// loop = [-30, -10], next time = -5, relative time = 25, mod(25, 20) = 5
|
|
//
|
|
// When playing backwards, we have, e.g.:
|
|
// loop = [-30, -10], next time = -35, relative time = -25, mod(-25, 20) = -5
|
|
const FFrameNumber OverplayTicks = LoopRelativeTick.Value % LoopDurationTicks.Value;
|
|
|
|
// Don't use LoopStartTick/LoopLastTick here because if we are ping-pong'ing, we would
|
|
// be going the other way. Use the most recent value of PlayDirection (via IsPlayingForward())
|
|
// for the same reason.
|
|
// Also, reverse OverplayTicks when ping-pong'in since we're playing this overplay in the
|
|
// reverse direction.
|
|
const FFrameTime EffectiveOverplayTicks =
|
|
(IsPlayingForward() ? EffectiveStartTick : LastValidTick) +
|
|
((!bPingPongPlayback) ? OverplayTicks : (-OverplayTicks));
|
|
PlaybackPosition.Reset(IsPlayingForward() ? EffectiveStartTick : LastValidTick);
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(EffectiveOverplayTicks), PlaybackStatus)
|
|
.SetHasJumped(true)
|
|
);
|
|
|
|
// If the overplay leads us exactly to the last tick of the loop, let's count that
|
|
// as a completed loop... but only if that finishes playback. Otherwise, we'll wait
|
|
// for the next update to loop over in order to avoid counting that loop twice.
|
|
if (EffectiveOverplayTicks == LoopLastTick
|
|
&& NumLoopsToPlay > 0
|
|
&& NumLoopsCompleted == NumLoopsToPlay - 1)
|
|
{
|
|
++NumLoopsCompleted;
|
|
|
|
PlaybackStatus = EMovieScenePlayerStatus::Stopped;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We haven't crossed a loop-end boundary... just chug along.
|
|
OutContexts.Add(
|
|
FMovieSceneContext(PlaybackPosition.PlayTo(NextTick), PlaybackStatus)
|
|
.SetHasJumped(bShouldJump)
|
|
);
|
|
|
|
// If we were updated right up to the last tick of the loop, let's count that as a completed
|
|
// loop... but only if that finishes playback. Otherwise, we'll wait for the next update to
|
|
// loop over in order to avoid counting that loop twice.
|
|
if (NextTick == LoopLastTick
|
|
&& NumLoopsToPlay > 0
|
|
&& NumLoopsCompleted == NumLoopsToPlay - 1)
|
|
{
|
|
++NumLoopsCompleted;
|
|
|
|
PlaybackStatus = EMovieScenePlayerStatus::Stopped;
|
|
}
|
|
}
|
|
|
|
ensure(OutContexts.Num() > 0);
|
|
ensure(OutContexts.Num() == 1 || (DissectLooping != EMovieSceneLoopDissection::None));
|
|
}
|
|
|
|
FFrameNumber FMovieScenePlaybackManager::GetLastValidTick() const
|
|
{
|
|
// TODO: handle precision problems with float SubFrame, or change SubFrame to double.
|
|
return SequenceEndTick - EndOffsetTicks - 1; // minus one tick for exclusive end frame.
|
|
}
|
|
|
|
FMovieSceneContext FMovieScenePlaybackManager::UpdateAtCurrentTime() const
|
|
{
|
|
return FMovieSceneContext(PlaybackPosition.GetCurrentPositionAsRange(), PlaybackStatus);
|
|
}
|
|
|
|
FFrameTime FMovieScenePlaybackManager::GetCurrentTime() const
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
return ConvertFrameTime(PlaybackPosition.GetCurrentPosition(), TickResolution, DisplayRate);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetCurrentTime(const FFrameTime& InFrameTime)
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
const FFrameNumber CurrentTick = ConvertFrameTime(InFrameTime, DisplayRate, TickResolution).RoundToFrame();
|
|
PlaybackPosition.Reset(CurrentTick);
|
|
}
|
|
|
|
TRange<FFrameTime> FMovieScenePlaybackManager::GetEffectivePlaybackRange() const
|
|
{
|
|
const FFrameNumber StartTick = SequenceStartTick + StartOffsetTicks;
|
|
const FFrameNumber EndTick = SequenceEndTick - EndOffsetTicks;
|
|
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
|
|
const FFrameTime StartFrame = ConvertFrameTime(StartTick, TickResolution, DisplayRate);
|
|
const FFrameTime EndFrame = ConvertFrameTime(EndTick, TickResolution, DisplayRate);
|
|
|
|
return TRange<FFrameTime>(TRangeBound<FFrameTime>::Inclusive(StartFrame), TRangeBound<FFrameTime>::Exclusive(EndFrame));
|
|
}
|
|
|
|
FFrameTime FMovieScenePlaybackManager::GetEffectiveStartTime() const
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
return ConvertFrameTime(SequenceStartTick + StartOffsetTicks, TickResolution, DisplayRate);
|
|
}
|
|
|
|
FFrameTime FMovieScenePlaybackManager::GetEffectiveEndTime() const
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
return ConvertFrameTime(SequenceEndTick + EndOffsetTicks, TickResolution, DisplayRate);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetStartOffset(const FFrameTime& InStartOffset)
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
const FFrameNumber InStartOffsetTicks = ConvertFrameTime(InStartOffset, DisplayRate, TickResolution).RoundToFrame();
|
|
|
|
SetStartAndEndOffsetTicks(InStartOffsetTicks, EndOffsetTicks);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetEndOffset(const FFrameTime& InEndOffset)
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
const FFrameNumber InEndOffsetTicks = ConvertFrameTime(InEndOffset, DisplayRate, TickResolution).RoundToFrame();
|
|
|
|
SetStartAndEndOffsetTicks(StartOffsetTicks, InEndOffsetTicks);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetEndOffsetAsTime(const FFrameTime& InEndTime)
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
const FFrameNumber InEndTick = ConvertFrameTime(InEndTime, DisplayRate, TickResolution).RoundToFrame();
|
|
|
|
const FFrameNumber InEndOffsetTicks = SequenceEndTick - InEndTick;
|
|
SetStartAndEndOffsetTicks(StartOffsetTicks, InEndOffsetTicks);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetStartAndEndOffsetTicks(FFrameNumber InStartOffsetTicks, FFrameNumber InEndOffsetTicks)
|
|
{
|
|
const FFrameNumber SequenceDurationTicks = SequenceEndTick - SequenceStartTick;
|
|
|
|
StartOffsetTicks = FMath::Min(
|
|
FMath::Max(InStartOffsetTicks, FFrameNumber(0)),
|
|
SequenceDurationTicks);
|
|
|
|
EndOffsetTicks = FMath::Min(
|
|
FMath::Max(InEndOffsetTicks, FFrameNumber(0)),
|
|
SequenceDurationTicks - StartOffsetTicks);
|
|
}
|
|
|
|
FFrameTime FMovieScenePlaybackManager::GetStartOffset() const
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
return ConvertFrameTime(StartOffsetTicks, TickResolution, DisplayRate);
|
|
}
|
|
|
|
FFrameTime FMovieScenePlaybackManager::GetEndOffset() const
|
|
{
|
|
const FFrameRate TickResolution = PlaybackPosition.GetOutputRate();
|
|
return ConvertFrameTime(EndOffsetTicks, TickResolution, DisplayRate);
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetNumLoopsToPlay(int32 InNumLoopsToPlay)
|
|
{
|
|
NumLoopsToPlay = InNumLoopsToPlay;
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::ReversePlayDirection()
|
|
{
|
|
if (PlayDirection == EPlayDirection::Forwards)
|
|
{
|
|
PlayDirection = EPlayDirection::Backwards;
|
|
}
|
|
else
|
|
{
|
|
PlayDirection = EPlayDirection::Forwards;
|
|
}
|
|
}
|
|
|
|
void FMovieScenePlaybackManager::SetPingPongPlayback(bool bInPingPongPlayback)
|
|
{
|
|
bPingPongPlayback = bInPingPongPlayback;
|
|
}
|
|
|