Files
UnrealEngine/Engine/Source/Editor/MovieSceneTools/Private/AutomatedLevelSequenceCapture.cpp
2025-05-18 13:04:45 +08:00

1144 lines
39 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AutomatedLevelSequenceCapture.h"
#include "MovieScene.h"
#include "Dom/JsonValue.h"
#include "Dom/JsonObject.h"
#include "Slate/SceneViewport.h"
#include "Misc/CommandLine.h"
#include "LevelSequenceActor.h"
#include "JsonObjectConverter.h"
#include "Tracks/MovieSceneCinematicShotTrack.h"
#include "MovieSceneTranslatorEDL.h"
#include "FCPXML/FCPXMLMovieSceneTranslator.h"
#include "EngineUtils.h"
#include "Sections/MovieSceneCinematicShotSection.h"
#include "TimerManager.h"
#include "GameFramework/PlayerController.h"
#include "Camera/CameraComponent.h"
#include "MovieSceneCaptureModule.h"
#include "MovieSceneTimeHelpers.h"
#include "MovieSceneToolHelpers.h"
#include "Protocols/AudioCaptureProtocol.h"
#include "ShaderCompiler.h"
#include "DistanceFieldAtlas.h"
#include "MeshCardBuild.h"
#include "MeshCardRepresentation.h"
#include "EntitySystem/MovieSceneEntitySystemLinker.h"
#include "Systems/MovieSceneMotionVectorSimulationSystem.h"
const FName UAutomatedLevelSequenceCapture::AutomatedLevelSequenceCaptureUIName = FName(TEXT("AutomatedLevelSequenceCaptureUIInstance"));
struct FMovieSceneTimeController_FrameStep : FMovieSceneTimeController
{
FMovieSceneTimeController_FrameStep()
: StartTimeOffset(0)
{}
virtual void OnTick(float DeltaSeconds, float InPlayRate) override
{}
virtual void OnStartPlaying(const FQualifiedFrameTime& InStartTime)
{
DeltaTime = FFrameTime(0);
StartTimeOffset = FFrameTime(0);
}
virtual FFrameTime OnRequestCurrentTime(const FQualifiedFrameTime& InCurrentTime, float InPlayRate) override
{
TOptional<FQualifiedFrameTime> StartTimeIfPlaying = GetPlaybackStartTime();
if (!StartTimeIfPlaying.IsSet())
{
UE_LOG(LogMovieSceneCapture, VeryVerbose, TEXT("FMovieSceneTimeController_FrameStep::OnRequestCurrentTime = Paused at frame %d subframe %f"), InCurrentTime.Time.FrameNumber.Value, InCurrentTime.Time.GetSubFrame());
return InCurrentTime.Time;
}
else
{
// Scale the delta time (should be one frame) by this frame's play rate, and add it to the current time offset
if (InPlayRate == 1.f)
{
StartTimeOffset += DeltaTime;
}
else
{
StartTimeOffset += DeltaTime * InPlayRate;
}
DeltaTime = FFrameTime(0);
FFrameTime NewTime = StartTimeIfPlaying->ConvertTo(InCurrentTime.Rate) + StartTimeOffset;
UE_LOG(LogMovieSceneCapture, VeryVerbose, TEXT("FMovieSceneTimeController_FrameStep::OnRequestCurrentTime = Playing at frame %d subframe %f"), NewTime.FrameNumber.Value, NewTime.GetSubFrame());
return NewTime;
}
}
FFrameTime DeltaTime;
FFrameTime StartTimeOffset;
};
UMovieScene* GetMovieScene(TWeakObjectPtr<ALevelSequenceActor> LevelSequenceActor)
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if (!Actor)
{
return nullptr;
}
ULevelSequence* LevelSequence = Actor->GetSequence();
if (!LevelSequence)
{
return nullptr;
}
return LevelSequence->GetMovieScene();
}
UMovieSceneCinematicShotTrack* GetCinematicShotTrack(TWeakObjectPtr<ALevelSequenceActor> LevelSequenceActor)
{
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return nullptr;
}
return MovieScene->FindTrack<UMovieSceneCinematicShotTrack>();
}
UMovieSceneMotionVectorSimulationSystem* FindMotionVectorSimulation(ALevelSequenceActor* LevelSequenceActor)
{
return static_cast<IMovieScenePlayer*>(LevelSequenceActor->GetSequencePlayer())->GetEvaluationTemplate().GetEntitySystemLinker()->FindSystem<UMovieSceneMotionVectorSimulationSystem>();
}
UAutomatedLevelSequenceCapture::UAutomatedLevelSequenceCapture(const FObjectInitializer& Init)
: Super(Init)
{
#if WITH_EDITORONLY_DATA == 0
if (!HasAnyFlags(RF_ClassDefaultObject))
{
checkf(false, TEXT("Automated level sequence captures can only be used in editor builds."));
}
#else
bUseCustomStartFrame = false;
CustomStartFrame = 0;
bUseCustomEndFrame = false;
CustomEndFrame = 1;
WarmUpFrameCount = 0;
DelayBeforeWarmUp = 0.0f;
DelayBeforeShotWarmUp = 0.0f;
DelayEveryFrame = 0.0f;
bWriteEditDecisionList = true;
bWriteFinalCutProXML = true;
RemainingWarmUpFrames = 0;
NumShots = 0;
ShotIndex = -1;
BurnInOptions = Init.CreateDefaultSubobject<ULevelSequenceBurnInOptions>(this, MovieSceneCaptureUIName);
#endif
}
#if WITH_EDITORONLY_DATA
void UAutomatedLevelSequenceCapture::AddFormatMappings(TMap<FString, FStringFormatArg>& OutFormatMappings, const FFrameMetrics& FrameMetrics) const
{
OutFormatMappings.Add(TEXT("sequence"), CachedState.RootName);
OutFormatMappings.Add(TEXT("shot"), CachedState.CurrentShotName);
OutFormatMappings.Add(TEXT("shot_frame"), FString::Printf(TEXT("%0*d"), Settings.ZeroPadFrameNumbers, CachedState.CurrentShotLocalTime.Time.FrameNumber.Value));
if (CachedState.CameraComponent.IsValid())
{
AActor* OuterActor = Cast<AActor>(CachedState.CameraComponent.Get()->GetOuter());
if (OuterActor)
{
OutFormatMappings.Add(TEXT("camera"), OuterActor->GetActorLabel());
}
}
}
void UAutomatedLevelSequenceCapture::Initialize(TSharedPtr<FSceneViewport> InViewport, int32 PIEInstance)
{
TimeController = MakeShared<FMovieSceneTimeController_FrameStep>();
Viewport = InViewport;
// Apply command-line overrides from parent class first. This needs to be called before setting up the capture strategy with the desired frame rate.
Super::Initialize(InViewport);
// Apply command-line overrides
{
FString LevelSequenceAssetPath;
if (FParse::Value(FCommandLine::Get(), TEXT("-LevelSequence="), LevelSequenceAssetPath))
{
LevelSequenceAsset.SetPath(LevelSequenceAssetPath);
}
FString ShotNameOverride;
if (FParse::Value(FCommandLine::Get(), TEXT("-Shot="), ShotNameOverride))
{
ShotName = ShotNameOverride;
}
int32 StartFrameOverride;
if( FParse::Value( FCommandLine::Get(), TEXT( "-MovieStartFrame=" ), StartFrameOverride ) )
{
bUseCustomStartFrame = true;
CustomStartFrame = StartFrameOverride;
}
int32 EndFrameOverride;
if( FParse::Value( FCommandLine::Get(), TEXT( "-MovieEndFrame=" ), EndFrameOverride ) )
{
bUseCustomEndFrame = true;
CustomEndFrame = EndFrameOverride;
}
int32 WarmUpFrameCountOverride;
if( FParse::Value( FCommandLine::Get(), TEXT( "-MovieWarmUpFrames=" ), WarmUpFrameCountOverride ) )
{
WarmUpFrameCount = WarmUpFrameCountOverride;
}
float DelayBeforeWarmUpOverride;
if( FParse::Value( FCommandLine::Get(), TEXT( "-MovieDelayBeforeWarmUp=" ), DelayBeforeWarmUpOverride ) )
{
DelayBeforeWarmUp = DelayBeforeWarmUpOverride;
}
float DelayBeforeShotWarmUpOverride;
if( FParse::Value( FCommandLine::Get(), TEXT( "-MovieDelayBeforeShotWarmUp=" ), DelayBeforeShotWarmUpOverride ) )
{
DelayBeforeShotWarmUp = DelayBeforeShotWarmUpOverride;
}
float DelayEveryFrameOverride;
if (FParse::Value(FCommandLine::Get(), TEXT("-MovieDelayEveryFrame="), DelayEveryFrameOverride))
{
DelayEveryFrame = DelayEveryFrameOverride;
}
bool bWriteEditDecisionListOverride;
if (FParse::Bool(FCommandLine::Get(), TEXT("-WriteEditDecisionList="), bWriteEditDecisionListOverride))
{
bWriteEditDecisionList = bWriteEditDecisionListOverride;
}
bool bWriteFinalCutProXMLOverride;
if (FParse::Bool(FCommandLine::Get(), TEXT("-WriteFinalCutProXML="), bWriteFinalCutProXMLOverride))
{
bWriteFinalCutProXML = bWriteFinalCutProXMLOverride;
}
}
if (Settings.bUsePathTracer)
{
float PathTracerSamplePerPixel = float(Settings.FrameRate.AsSeconds(Settings.PathTracerSamplePerPixel));
if (DelayEveryFrame != PathTracerSamplePerPixel)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("Delay every frame overridden by path tracer sample per pixel: %f"), PathTracerSamplePerPixel);
DelayEveryFrame = PathTracerSamplePerPixel;
}
}
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
// If we don't have a valid actor, attempt to find a level sequence actor in the world that references this asset
if( Actor == nullptr )
{
if( LevelSequenceAsset.IsValid() )
{
ULevelSequence* Asset = Cast<ULevelSequence>( LevelSequenceAsset.TryLoad() );
if( Asset != nullptr )
{
for( auto It = TActorIterator<ALevelSequenceActor>( InViewport->GetClient()->GetWorld() ); It; ++It )
{
if( It->GetSequence() == Asset )
{
// Found it!
Actor = *It;
this->LevelSequenceActor = Actor;
break;
}
}
}
}
}
if (!Actor)
{
ULevelSequence* Asset = Cast<ULevelSequence>(LevelSequenceAsset.TryLoad());
if (Asset)
{
// Spawn a new actor
Actor = InViewport->GetClient()->GetWorld()->SpawnActor<ALevelSequenceActor>();
Actor->SetSequence(Asset);
LevelSequenceActor = Actor;
}
else
{
//FPlatformMisc::RequestExit(FMovieSceneCaptureExitCodes::AssetNotFound);
}
}
if (Actor && !ShotName.IsEmpty())
{
UMovieScene* MovieScene = GetMovieScene(Actor);
UMovieSceneCinematicShotTrack* CinematicShotTrack = GetCinematicShotTrack(Actor);
if (CinematicShotTrack && MovieScene)
{
FFrameRate TickResolution = MovieScene->GetTickResolution();
FFrameRate DisplayRate = MovieScene->GetDisplayRate();
UMovieSceneCinematicShotSection* CinematicShotSection = nullptr;
for (UMovieSceneSection* Section : CinematicShotTrack->GetAllSections())
{
if (UMovieSceneCinematicShotSection* ShotSection = Cast<UMovieSceneCinematicShotSection>(Section))
{
if (ShotSection->GetShotDisplayName() == ShotName)
{
CinematicShotSection = ShotSection;
break;
}
}
}
if (CinematicShotSection)
{
bUseCustomStartFrame = true;
bUseCustomEndFrame = true;
FFrameNumber StartFrame = UE::MovieScene::DiscreteInclusiveLower(CinematicShotSection->GetRange());
FFrameNumber EndFrame = UE::MovieScene::DiscreteExclusiveUpper(CinematicShotSection->GetRange());
CustomStartFrame = FFrameRate::TransformTime(StartFrame, TickResolution, DisplayRate).CeilToFrame();
CustomEndFrame = FFrameRate::TransformTime(EndFrame, TickResolution, DisplayRate).CeilToFrame();
UE_LOG(LogMovieSceneCapture, Log, TEXT("Found shot '%s' to capture. Setting custom start and end frames to %d and %d."), *ShotName, CustomStartFrame.Value, CustomEndFrame.Value);
}
else
{
UE_LOG(LogMovieSceneCapture, Error, TEXT("Could not find named shot '%s' to capture"), *ShotName);
}
}
}
ExportEDL();
ExportFCPXML();
if (Actor)
{
// Ensure it doesn't loop (-1 is indefinite)
Actor->PlaybackSettings.LoopCount.Value = 0;
Actor->GetSequencePlayer()->SetTimeController(TimeController);
Actor->PlaybackSettings.bPauseAtEnd = true;
Actor->PlaybackSettings.bAutoPlay = false;
if (BurnInOptions)
{
Actor->BurnInOptions = BurnInOptions;
bool bUseBurnIn = false;
if( FParse::Bool( FCommandLine::Get(), TEXT( "-UseBurnIn=" ), bUseBurnIn ) )
{
Actor->BurnInOptions->bUseBurnIn = bUseBurnIn;
}
}
// Make sure we're not playing yet, and have a fully up to date player based on the above settings (in case AutoPlay was called from BeginPlay)
if( Actor->GetSequencePlayer() != nullptr )
{
if (Actor->GetSequencePlayer()->IsPlaying())
{
Actor->GetSequencePlayer()->Stop();
}
Actor->InitializePlayer();
}
if (InitializeShots())
{
FFrameNumber StartTime, EndTime;
SetupShot(StartTime, EndTime);
}
Actor->RefreshBurnIn();
}
else
{
UE_LOG(LogMovieSceneCapture, Error, TEXT("Could not find or create a Level Sequence Actor for this capture. Capturing will fail."));
}
CaptureState = ELevelSequenceCaptureState::Setup;
CaptureStrategy = MakeShareable(new FFixedTimeStepCaptureStrategy(Settings.GetFrameRate()));
CaptureStrategy->OnInitialize();
// Cache these off. We override them for audio passes and that overrides the settings members directly
// so we need to be able to restore them at the end
CachedWarmUpFrameCount = WarmUpFrameCount;
CachedDelayBeforeWarmUp = DelayBeforeWarmUp;
CachedDelayBeforeShotWarmUp = DelayBeforeShotWarmUp;
}
bool UAutomatedLevelSequenceCapture::InitializeShots()
{
NumShots = 0;
ShotIndex = -1;
CachedShotStates.Empty();
if (Settings.HandleFrames <= 0)
{
return false;
}
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return false;
}
UMovieSceneCinematicShotTrack* CinematicShotTrack = GetCinematicShotTrack(LevelSequenceActor);
if (!CinematicShotTrack)
{
return false;
}
NumShots = CinematicShotTrack->GetAllSections().Num();
ShotIndex = 0;
CachedPlaybackRange = MovieScene->GetPlaybackRange();
// Compute handle frames in tick resolution space since that is what the section ranges are defined in
FFrameNumber HandleFramesResolutionSpace = ConvertFrameTime(Settings.HandleFrames, Settings.GetFrameRate(), MovieScene->GetTickResolution()).FloorToFrame();
CinematicShotTrack->SortSections();
for (int32 SectionIndex = 0; SectionIndex < CinematicShotTrack->GetAllSections().Num(); ++SectionIndex)
{
UMovieSceneCinematicShotSection* ShotSection = Cast<UMovieSceneCinematicShotSection>(CinematicShotTrack->GetAllSections()[SectionIndex]);
UMovieScene* ShotMovieScene = ShotSection->GetSequence() ? ShotSection->GetSequence()->GetMovieScene() : nullptr;
if (ShotMovieScene != nullptr)
{
// Expand the inner shot section range by the handle size, multiplied by the difference between the outer and inner tick resolutions (and factoring in the time scale)
const float OuterToInnerRateDilation = (MovieScene->GetTickResolution() == ShotMovieScene->GetTickResolution()) ? 1.f : (ShotMovieScene->GetTickResolution() / MovieScene->GetTickResolution()).AsDecimal();
// coderot: this is not considering timewarping
const float OuterToInnerScale = OuterToInnerRateDilation;
CachedShotStates.Add(FCinematicShotCache(ShotSection->IsActive(), ShotSection->IsLocked(), ShotSection->GetRange(), ShotMovieScene ? ShotMovieScene->GetPlaybackRange() : TRange<FFrameNumber>::Empty()));
if (ShotMovieScene)
{
TRange<FFrameNumber> NewPlaybackRange = UE::MovieScene::ExpandRange(ShotMovieScene->GetPlaybackRange(), FFrameNumber(FMath::FloorToInt(HandleFramesResolutionSpace.Value * OuterToInnerScale)));
ShotMovieScene->SetPlaybackRange(NewPlaybackRange, false);
}
ShotSection->SetIsLocked(false);
ShotSection->SetIsActive(false);
ShotSection->SetRange(UE::MovieScene::ExpandRange(ShotSection->GetRange(), HandleFramesResolutionSpace));
}
}
return NumShots > 0;
}
void UAutomatedLevelSequenceCapture::RestoreShots()
{
if (Settings.HandleFrames <= 0)
{
return;
}
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return;
}
UMovieSceneCinematicShotTrack* CinematicShotTrack = GetCinematicShotTrack(LevelSequenceActor);
if (!CinematicShotTrack)
{
return;
}
MovieScene->SetPlaybackRange(CachedPlaybackRange, false);
for (int32 SectionIndex = 0; SectionIndex < CinematicShotTrack->GetAllSections().Num(); ++SectionIndex)
{
UMovieSceneCinematicShotSection* ShotSection = Cast<UMovieSceneCinematicShotSection>(CinematicShotTrack->GetAllSections()[SectionIndex]);
UMovieScene* ShotMovieScene = ShotSection->GetSequence() ? ShotSection->GetSequence()->GetMovieScene() : nullptr;
if (ShotMovieScene)
{
ShotMovieScene->SetPlaybackRange(CachedShotStates[SectionIndex].MovieSceneRange, false);
}
ShotSection->SetIsActive(CachedShotStates[SectionIndex].bActive);
ShotSection->SetRange(CachedShotStates[SectionIndex].ShotRange);
ShotSection->SetIsLocked(CachedShotStates[SectionIndex].bLocked);
}
}
bool UAutomatedLevelSequenceCapture::SetupShot(FFrameNumber& StartTime, FFrameNumber& EndTime)
{
if (Settings.HandleFrames <= 0)
{
return false;
}
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return false;
}
UMovieSceneCinematicShotTrack* CinematicShotTrack = GetCinematicShotTrack(LevelSequenceActor);
if (!CinematicShotTrack)
{
return false;
}
if (ShotIndex > CinematicShotTrack->GetAllSections().Num()-1)
{
return false;
}
// Only render shots that are active
for (; ShotIndex < CinematicShotTrack->GetAllSections().Num(); )
{
if (CachedShotStates[ShotIndex].bActive)
{
break;
}
++ShotIndex;
}
// Disable all shots unless it's the current one being rendered
for (int32 SectionIndex = 0; SectionIndex < CinematicShotTrack->GetAllSections().Num(); ++SectionIndex)
{
UMovieSceneSection* ShotSection = CinematicShotTrack->GetAllSections()[SectionIndex];
ShotSection->SetIsActive(SectionIndex == ShotIndex);
ShotSection->MarkAsChanged();
if (SectionIndex == ShotIndex)
{
// We intersect with the CachedPlaybackRange instead of copying the playback range from the shot to handle the case where
// the playback range intersected the middle of the shot before we started manipulating ranges. We manually expand the root
// Movie Sequence's playback range by the number of handle frames to allow handle frames to work as expected on first/last shot.
FFrameNumber HandleFramesResolutionSpace = ConvertFrameTime(Settings.HandleFrames, Settings.GetFrameRate(), MovieScene->GetTickResolution()).FloorToFrame();
TRange<FFrameNumber> ExtendedCachedPlaybackRange = UE::MovieScene::ExpandRange(CachedPlaybackRange, HandleFramesResolutionSpace);
TRange<FFrameNumber> TotalRange = TRange<FFrameNumber>::Intersection(ShotSection->GetRange(), ExtendedCachedPlaybackRange);
StartTime = TotalRange.IsEmpty() ? FFrameNumber(0) : UE::MovieScene::DiscreteInclusiveLower(TotalRange);
EndTime = TotalRange.IsEmpty() ? FFrameNumber(0) : UE::MovieScene::DiscreteExclusiveUpper(TotalRange);
MovieScene->SetPlaybackRange(StartTime, (EndTime - StartTime).Value, false);
MovieScene->MarkAsChanged();
}
}
return true;
}
void UAutomatedLevelSequenceCapture::SetupFrameRange()
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if( Actor )
{
ULevelSequence* LevelSequence = Actor->GetSequence();
if( LevelSequence != nullptr )
{
UMovieScene* MovieScene = LevelSequence->GetMovieScene();
if( MovieScene != nullptr )
{
FFrameRate SourceFrameRate = MovieScene->GetTickResolution();
TRange<FFrameNumber> SequenceRange = MovieScene->GetPlaybackRange();
FFrameNumber PlaybackStartFrame = ConvertFrameTime(UE::MovieScene::DiscreteInclusiveLower(SequenceRange), SourceFrameRate, Settings.GetFrameRate()).CeilToFrame();
FFrameNumber PlaybackEndFrame = ConvertFrameTime(UE::MovieScene::DiscreteExclusiveUpper(SequenceRange), SourceFrameRate, Settings.GetFrameRate()).CeilToFrame();
if( bUseCustomStartFrame )
{
PlaybackStartFrame = CustomStartFrame;
}
if( !Settings.bUseRelativeFrameNumbers )
{
// NOTE: The frame number will be an offset from the first frame that we start capturing on, not the frame
// that we start playback at (in the case of WarmUpFrameCount being non-zero). So we'll cache out frame
// number offset before adjusting for the warm up frames.
this->FrameNumberOffset = PlaybackStartFrame.Value;
}
if( bUseCustomEndFrame )
{
PlaybackEndFrame = CustomEndFrame;
}
// This is a fun hack... Due to the fragility of this code (which this makes more fragile admittedly...) the original audio implementation
// just ran the entire process twice, starting all the way back at the Setup loop so that everything would be re-initialized like it was
// for the video capture. Unfortunately, it looks like when we switch from a Fixed Timestep clock to the Platform clock (needed for audio
// as audio is realtime only) this allows the sequence player to get out of sync with the warmup frame counter. Audio recording doesn't start
// until the warmup time has passed, but because the sequence is playing at realtime it can start playing the part of the sequence you wanted
// the audio for before the recorder ever kicks in!
// To minimize changes, we're just going to override any warmup/delay times to zero here in the event that this is an audio pass, so that
// they should be more in sync.
if (bIsAudioCapturePass)
{
WarmUpFrameCount = 0;
DelayBeforeWarmUp = 0.f;
DelayBeforeShotWarmUp = 0.f;
}
RemainingWarmUpFrames = FMath::Max( WarmUpFrameCount, 0 );
if( RemainingWarmUpFrames > 0 )
{
// We were asked to playback additional frames before we start capturing
PlaybackStartFrame -= RemainingWarmUpFrames;
}
if (UMovieSceneMotionVectorSimulationSystem* MotionVectorSim = FindMotionVectorSimulation(Actor))
{
MotionVectorSim->PreserveSimulatedMotion(true);
}
// Override the movie scene's playback range
Actor->GetSequencePlayer()->SetFrameRate(Settings.GetFrameRate());
Actor->GetSequencePlayer()->SetFrameRange(PlaybackStartFrame.Value, (PlaybackEndFrame - PlaybackStartFrame).Value);
Actor->GetSequencePlayer()->SetPlaybackPosition(FMovieSceneSequencePlaybackParams(FFrameTime(PlaybackStartFrame), EUpdatePositionMethod::Jump));
Actor->GetSequencePlayer()->SetSnapshotOffsetFrames(WarmUpFrameCount);
}
}
}
}
void UAutomatedLevelSequenceCapture::EnableCinematicMode()
{
if (!GetSettings().bCinematicMode)
{
return;
}
// iterate through the controller list and set cinematic mode if necessary
bool bNeedsCinematicMode = !GetSettings().bAllowMovement || !GetSettings().bAllowTurning || !GetSettings().bShowPlayer || !GetSettings().bShowHUD;
if (!bNeedsCinematicMode)
{
return;
}
if (Viewport.IsValid())
{
for (FConstPlayerControllerIterator Iterator = Viewport.Pin()->GetClient()->GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PC = Iterator->Get();
if (PC && PC->IsLocalController())
{
PC->SetCinematicMode(true, !GetSettings().bShowPlayer, !GetSettings().bShowHUD, !GetSettings().bAllowMovement, !GetSettings().bAllowTurning);
}
}
}
}
void UAutomatedLevelSequenceCapture::OnTick(float DeltaSeconds)
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if (!Actor || !Actor->GetSequencePlayer())
{
return;
}
// Block till level streaming is completed. This would cause hitches under normal gameplay, but because we already run at slower-than-real-time
// it doesn't matter for movie captures. This solves situations where games have systems that pull in sublevels via level streaming that cannot
// be normally controlled via the Sequencer Level Visibility track. If all levels are already loaded, blocking will have no effect.
if (Actor->GetWorld())
{
Actor->GetWorld()->BlockTillLevelStreamingCompleted();
}
if (GShaderCompilingManager && GShaderCompilingManager->GetNumRemainingJobs() > 0)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Waiting for %d shaders to finish compiling..."), GFrameCounter, GShaderCompilingManager->GetNumRemainingJobs());
GShaderCompilingManager->FinishAllCompilation();
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Done waiting for shaders to finish."), GFrameCounter);
}
if (GDistanceFieldAsyncQueue && GDistanceFieldAsyncQueue->GetNumOutstandingTasks() > 0)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Waiting for %d Mesh Distance Fields to finish building..."), GFrameCounter, GDistanceFieldAsyncQueue->GetNumOutstandingTasks());
GDistanceFieldAsyncQueue->BlockUntilAllBuildsComplete();
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Done waiting for Mesh Distance Fields to build."), GFrameCounter);
}
if (GCardRepresentationAsyncQueue && GCardRepresentationAsyncQueue->GetNumOutstandingTasks() > 0)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Waiting for %d Mesh Cards to finish building..."), GFrameCounter, GCardRepresentationAsyncQueue->GetNumOutstandingTasks());
GCardRepresentationAsyncQueue->BlockUntilAllBuildsComplete();
UE_LOG(LogMovieSceneCapture, Log, TEXT("[%d] Done waiting for Mesh Cards to build."), GFrameCounter);
}
// Setup the automated capture
if (CaptureState == ELevelSequenceCaptureState::Setup)
{
SetupFrameRange();
EnableCinematicMode();
// Bind to the event so we know when to capture a frame
if (!bIsAudioCapturePass)
{
OnPlayerUpdatedBinding = Actor->GetSequencePlayer()->OnSequenceUpdated().AddUObject( this, &UAutomatedLevelSequenceCapture::SequenceUpdated );
}
StartWarmup();
if (DelayBeforeWarmUp + DelayBeforeShotWarmUp + DelayEveryFrame > 0)
{
CaptureState = ELevelSequenceCaptureState::DelayBeforeWarmUp;
Actor->GetWorld()->GetTimerManager().SetTimer(DelayTimer, FTimerDelegate::CreateUObject(this, &UAutomatedLevelSequenceCapture::DelayBeforeWarmupFinished), DelayBeforeWarmUp + DelayBeforeShotWarmUp + DelayEveryFrame, false);
}
else
{
DelayBeforeWarmupFinished();
}
}
// Then we'll just wait a little bit. We'll delay the specified number of seconds before capturing to allow any
// textures to stream in or post processing effects to settle.
if( CaptureState == ELevelSequenceCaptureState::DelayBeforeWarmUp )
{
// Do nothing, just hold at the current frame. This assumes that the current frame isn't changing by any other mechanisms.
}
else if( CaptureState == ELevelSequenceCaptureState::ReadyToWarmUp )
{
Actor->GetSequencePlayer()->Play();
// Start warming up
CaptureState = ELevelSequenceCaptureState::WarmingUp;
}
// Count down our warm up frames.
if( CaptureState == ELevelSequenceCaptureState::WarmingUp)
{
// The post increment is important - it ensures we capture the very first frame if there are no warm up frames,
// but correctly skip n frames if there are n warmup frames
if (RemainingWarmUpFrames-- == 0)
{
// Start capturing - this will capture the *next* update from sequencer
CaptureState = ELevelSequenceCaptureState::FinishedWarmUp;
UpdateFrameState();
StartCapture();
}
else
{
// Move onto the next frame
TimeController->DeltaTime = FFrameTime(1);
}
}
if( bCapturing && !Actor->GetSequencePlayer()->IsPlaying() && CaptureState != ELevelSequenceCaptureState::Paused )
{
++ShotIndex;
FFrameNumber StartTime, EndTime;
if (SetupShot(StartTime, EndTime))
{
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
FFrameNumber StartTimePlayRateSpace = ConvertFrameTime(StartTime, MovieScene->GetTickResolution(), Settings.GetFrameRate()).CeilToFrame();
FFrameNumber EndTimePlayRateSpace = ConvertFrameTime(EndTime, MovieScene->GetTickResolution(), Settings.GetFrameRate()).CeilToFrame();
Actor->GetSequencePlayer()->SetFrameRange(StartTimePlayRateSpace.Value, (EndTimePlayRateSpace - StartTimePlayRateSpace).Value);
Actor->GetSequencePlayer()->SetPlaybackPosition(FMovieSceneSequencePlaybackParams(FFrameTime(StartTimePlayRateSpace), EUpdatePositionMethod::Jump));
Actor->GetSequencePlayer()->Play();
// We need to re-register to the binding when we start each shot. When a shot reaches the last frame it unregisters the binding so that
// any subsequent seeking doesn't accidentally render extra frames. SetupShot doesn't get called until after the first time we finish
// rendering a shot so this doesn't register the delegate twice on the first go.
OnPlayerUpdatedBinding = Actor->GetSequencePlayer()->OnSequenceUpdated().AddUObject(this, &UAutomatedLevelSequenceCapture::SequenceUpdated);
CaptureState = ELevelSequenceCaptureState::FinishedWarmUp;
UpdateFrameState();
}
else
{
// This is called when the sequence finishes playing and we've reached the end of all shots within the sequence.
// We only render the audio pass if they have specified an audio capture protocol, so we allow this early out
// when there is no audio, or when we have finished the audio pass.
if (IsAudioPassIfNeeded() && CaptureState != ELevelSequenceCaptureState::Setup)
{
// If they don't want to render audio, or they have rendered an audio pass, we finish and finalize the data.
Actor->GetSequencePlayer()->OnSequenceUpdated().Remove( OnPlayerUpdatedBinding );
FinalizeWhenReady();
// Restore our cached variables since these are the actual one represented in the in-engine UI
WarmUpFrameCount = CachedWarmUpFrameCount;
DelayBeforeWarmUp = CachedDelayBeforeWarmUp;
DelayBeforeShotWarmUp = CachedDelayBeforeShotWarmUp;
}
else
{
// Reset us to use the platform clock for controlling the playback rate of the sequence. The audio system
// uses the platform clock for timings as well.
Actor->GetSequencePlayer()->SetTimeController(MakeShared<FMovieSceneTimeController_PlatformClock>());
CaptureState = ELevelSequenceCaptureState::Setup;
// We'll now repeat the whole process including warmups and delays. The audio capture will pause recording while we are delayed.
// This creates an audio discrepancy during the transition point (if there is shot warmup) but it allows a complex scenes to spend
// enough time loading that it doesn't cause an audio desync.
bIsAudioCapturePass = true;
bCapturing = false;
}
}
}
}
void UAutomatedLevelSequenceCapture::DelayBeforeWarmupFinished()
{
// Wait a frame to go by after we've set the fixed time step, so that the animation starts
// playback at a consistent time
CaptureState = ELevelSequenceCaptureState::ReadyToWarmUp;
}
void UAutomatedLevelSequenceCapture::PauseFinished()
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
Actor->GetSequencePlayer()->Play();
CaptureState = ELevelSequenceCaptureState::FinishedWarmUp;
if (bIsAudioCapturePass)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("WarmUp pause finished. Resuming the capture of audio."));
}
else
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("WarmUp pause finished. Resuming the capture of images."));
}
}
void UAutomatedLevelSequenceCapture::SequenceUpdated(const UMovieSceneSequencePlayer& Player, FFrameTime CurrentTime, FFrameTime PreviousTime)
{
if (bCapturing)
{
FLevelSequencePlayerSnapshot PreviousState = CachedState;
UpdateFrameState();
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if (Actor && Actor->GetSequencePlayer())
{
// If this is a new shot, set the state to shot warm up and pause on this frame until warmed up
const bool bHasMultipleShots = PreviousState.CurrentShotName != PreviousState.RootName;
const bool bNewShot = bHasMultipleShots && PreviousState.ShotID != CachedState.ShotID;
const bool bNewFrame = PreviousTime != CurrentTime;
const bool bDelayingBeforeShotWarmUp = (bNewShot && DelayBeforeShotWarmUp > 0);
const bool bDelayingEveryFrame = (bNewFrame && DelayEveryFrame > 0);
if (Actor->GetSequencePlayer()->IsPlaying() && ( bDelayingBeforeShotWarmUp || bDelayingEveryFrame ))
{
if (bIsAudioCapturePass)
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("Entering WarmUp pause, pausing audio capture."));
if (AudioCaptureProtocol)
{
AudioCaptureProtocol->WarmUp();
}
}
else
{
UE_LOG(LogMovieSceneCapture, Log, TEXT("Entering WarmUp pause, pausing image capture."));
if (ImageCaptureProtocol)
{
ImageCaptureProtocol->WarmUp();
}
}
CaptureState = ELevelSequenceCaptureState::Paused;
if (UMovieSceneMotionVectorSimulationSystem* MotionVectorSim = FindMotionVectorSimulation(Actor))
{
MotionVectorSim->PreserveSimulatedMotion(true);
}
Actor->GetWorld()->GetTimerManager().SetTimer(DelayTimer, FTimerDelegate::CreateUObject(this, &UAutomatedLevelSequenceCapture::PauseFinished), DelayBeforeShotWarmUp + DelayEveryFrame, false);
Actor->GetSequencePlayer()->Pause();
}
else if (CaptureState == ELevelSequenceCaptureState::FinishedWarmUp)
{
// If we were preserving simulated motion, now's the time to stop that since we've captured the frame that was being simulated
if (UMovieSceneMotionVectorSimulationSystem* MotionVectorSim = FindMotionVectorSimulation(Actor))
{
MotionVectorSim->PreserveSimulatedMotion(false);
}
// These are called each frame to allow the state machine inside the protocol to transition back to capturing
// after paused if needed. This is needed for things like the avi writer who spin up an avi writer per shot (if needed)
// so that we can capture the movies into individual avi files per shot due to the format text.
if (bIsAudioCapturePass)
{
if (AudioCaptureProtocol)
{
AudioCaptureProtocol->StartCapture();
}
}
else
{
if (ImageCaptureProtocol)
{
ImageCaptureProtocol->StartCapture();
}
}
bool bOnLastFrame = (CurrentTime.FrameNumber >= Actor->GetSequencePlayer()->GetStartTime().Time.FrameNumber + Actor->GetSequencePlayer()->GetFrameDuration() - 1);
bool bLastShot = NumShots == 0 ? true : ShotIndex == NumShots - 1;
CaptureThisFrame((CurrentTime - PreviousTime) / Settings.GetFrameRate());
CachedMetrics.PreviousFrame = CurrentTime.FrameNumber.Value;
// Our callback can be called multiple times for a given frame due to how Level Sequences evaluate.
// For example, frame 161 is evaluated and an image is written. This isn't considered the end of the sequence
// as technically the Level Sequence can be evaluated up to 161.9999994, so on the next Update loop it tries to
// evaluate frame 162 (due to our fixed timestep controller). This then puts it over the limit so it forces a
// reevaluation of 161 before calling Stop/Pause. This then invokes this callback a second time for frame 161
// and we end up with two instances of 161! To solve this, when we reach the last frame of each shot we stop listening
// to updates. If there's a new shot it will re-register the delegate once it is set up.
if (bOnLastFrame)
{
if (bLastShot && IsAudioPassIfNeeded())
{
FinalizeWhenReady();
}
Actor->GetSequencePlayer()->OnSequenceUpdated().Remove(OnPlayerUpdatedBinding);
}
// Move onto the next frame now that we've captured this one
TimeController->DeltaTime = FFrameTime(1);
}
}
}
}
void UAutomatedLevelSequenceCapture::UpdateFrameState()
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if (Actor && Actor->GetSequencePlayer())
{
Actor->GetSequencePlayer()->TakeFrameSnapshot(CachedState);
}
}
void UAutomatedLevelSequenceCapture::LoadFromConfig()
{
UMovieSceneCapture::LoadFromConfig();
BurnInOptions->LoadConfig();
BurnInOptions->ResetSettings();
if (BurnInOptions->Settings)
{
BurnInOptions->Settings->LoadConfig();
}
}
void UAutomatedLevelSequenceCapture::SaveToConfig()
{
FFrameNumber CurrentStartFrame = CustomStartFrame;
FFrameNumber CurrentEndFrame = CustomEndFrame;
bool bRestoreFrameOverrides = RestoreFrameOverrides();
BurnInOptions->SaveConfig();
if (BurnInOptions->Settings)
{
BurnInOptions->Settings->SaveConfig();
}
UMovieSceneCapture::SaveToConfig();
if (bRestoreFrameOverrides)
{
SetFrameOverrides(CurrentStartFrame, CurrentEndFrame);
}
}
void UAutomatedLevelSequenceCapture::Close()
{
Super::Close();
CachedState = FLevelSequencePlayerSnapshot();
RestoreShots();
}
bool UAutomatedLevelSequenceCapture::RestoreFrameOverrides()
{
bool bAnySet = CachedStartFrame.IsSet() || CachedEndFrame.IsSet() || bCachedUseCustomStartFrame.IsSet() || bCachedUseCustomEndFrame.IsSet();
if (CachedStartFrame.IsSet())
{
CustomStartFrame = CachedStartFrame.GetValue();
CachedStartFrame.Reset();
}
if (CachedEndFrame.IsSet())
{
CustomEndFrame = CachedEndFrame.GetValue();
CachedEndFrame.Reset();
}
if (bCachedUseCustomStartFrame.IsSet())
{
bUseCustomStartFrame = bCachedUseCustomStartFrame.GetValue();
bCachedUseCustomStartFrame.Reset();
}
if (bCachedUseCustomEndFrame.IsSet())
{
bUseCustomEndFrame = bCachedUseCustomEndFrame.GetValue();
bCachedUseCustomEndFrame.Reset();
}
return bAnySet;
}
void UAutomatedLevelSequenceCapture::SetFrameOverrides(FFrameNumber InStartFrame, FFrameNumber InEndFrame)
{
CachedStartFrame = CustomStartFrame;
CachedEndFrame = CustomEndFrame;
bCachedUseCustomStartFrame = bUseCustomStartFrame;
bCachedUseCustomEndFrame = bUseCustomEndFrame;
CustomStartFrame = InStartFrame;
CustomEndFrame = InEndFrame;
bUseCustomStartFrame = true;
bUseCustomEndFrame = true;
}
void UAutomatedLevelSequenceCapture::SerializeAdditionalJson(FJsonObject& Object)
{
TSharedRef<FJsonObject> OptionsContainer = MakeShareable(new FJsonObject);
if (FJsonObjectConverter::UStructToJsonObject(BurnInOptions->GetClass(), BurnInOptions, OptionsContainer, 0, 0))
{
Object.SetField(TEXT("BurnInOptions"), MakeShareable(new FJsonValueObject(OptionsContainer)));
}
if (BurnInOptions->Settings)
{
TSharedRef<FJsonObject> SettingsDataObject = MakeShareable(new FJsonObject);
if (FJsonObjectConverter::UStructToJsonObject(BurnInOptions->Settings->GetClass(), BurnInOptions->Settings, SettingsDataObject, 0, 0))
{
Object.SetField(TEXT("BurnInOptionsInitSettings"), MakeShareable(new FJsonValueObject(SettingsDataObject)));
}
}
}
void UAutomatedLevelSequenceCapture::DeserializeAdditionalJson(const FJsonObject& Object)
{
if (!BurnInOptions)
{
BurnInOptions = NewObject<ULevelSequenceBurnInOptions>(this, "BurnInOptions");
}
TSharedPtr<FJsonValue> OptionsContainer = Object.TryGetField(TEXT("BurnInOptions"));
if (OptionsContainer.IsValid())
{
FJsonObjectConverter::JsonAttributesToUStruct(OptionsContainer->AsObject()->Values, BurnInOptions->GetClass(), BurnInOptions, 0, 0);
}
BurnInOptions->ResetSettings();
if (BurnInOptions->Settings)
{
TSharedPtr<FJsonValue> SettingsDataObject = Object.TryGetField(TEXT("BurnInOptionsInitSettings"));
if (SettingsDataObject.IsValid())
{
FJsonObjectConverter::JsonAttributesToUStruct(SettingsDataObject->AsObject()->Values, BurnInOptions->Settings->GetClass(), BurnInOptions->Settings, 0, 0);
}
}
}
void UAutomatedLevelSequenceCapture::ExportEDL()
{
if (!bWriteEditDecisionList)
{
return;
}
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return;
}
UMovieSceneCinematicShotTrack* ShotTrack = MovieScene->FindTrack<UMovieSceneCinematicShotTrack>();
if (!ShotTrack)
{
return;
}
FString SaveFilename = Settings.OutputDirectory.Path / MovieScene->GetOuter()->GetName();
int32 HandleFrames = Settings.HandleFrames;
FString MovieExtension = Settings.MovieExtension;
MovieSceneTranslatorEDL::ExportEDL(MovieScene, Settings.GetFrameRate(), SaveFilename, HandleFrames, MovieExtension);
}
double UAutomatedLevelSequenceCapture::GetEstimatedCaptureDurationSeconds() const
{
ALevelSequenceActor* Actor = LevelSequenceActor.Get();
if (Actor)
{
TRange<FFrameNumber> PlaybackRange = Actor->GetSequence()->GetMovieScene()->GetPlaybackRange();
int32 MovieSceneDurationFrameCount = UE::MovieScene::DiscreteSize(PlaybackRange);
return Actor->GetSequence()->GetMovieScene()->GetTickResolution().AsSeconds(FFrameTime(FFrameNumber(MovieSceneDurationFrameCount)));
}
return 0.0;
}
void UAutomatedLevelSequenceCapture::ExportFCPXML()
{
if (!bWriteFinalCutProXML)
{
return;
}
UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor);
if (!MovieScene)
{
return;
}
UMovieSceneCinematicShotTrack* ShotTrack = MovieScene->FindTrack<UMovieSceneCinematicShotTrack>();
if (!ShotTrack)
{
return;
}
FString SaveFilename = Settings.OutputDirectory.Path / MovieScene->GetOuter()->GetName() + TEXT(".xml");
FString FilenameFormat = Settings.OutputFormat;
int32 HandleFrames = Settings.HandleFrames;
FFrameRate FrameRate = Settings.GetFrameRate();
uint32 ResX = Settings.Resolution.ResX;
uint32 ResY = Settings.Resolution.ResY;
FString MovieExtension = Settings.MovieExtension;
FFCPXMLExporter *Exporter = new FFCPXMLExporter;
TSharedRef<FMovieSceneTranslatorContext> ExportContext(new FMovieSceneTranslatorContext);
ExportContext->Init();
bool bSuccess = Exporter->Export(MovieScene, FilenameFormat, FrameRate, ResX, ResY, HandleFrames, SaveFilename, ExportContext, MovieExtension);
// Log any messages in context
MovieSceneToolHelpers::MovieSceneTranslatorLogMessages(Exporter, ExportContext, false);
delete Exporter;
}
#endif