// 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 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 LevelSequenceActor) { ALevelSequenceActor* Actor = LevelSequenceActor.Get(); if (!Actor) { return nullptr; } ULevelSequence* LevelSequence = Actor->GetSequence(); if (!LevelSequence) { return nullptr; } return LevelSequence->GetMovieScene(); } UMovieSceneCinematicShotTrack* GetCinematicShotTrack(TWeakObjectPtr LevelSequenceActor) { UMovieScene* MovieScene = GetMovieScene(LevelSequenceActor); if (!MovieScene) { return nullptr; } return MovieScene->FindTrack(); } UMovieSceneMotionVectorSimulationSystem* FindMotionVectorSimulation(ALevelSequenceActor* LevelSequenceActor) { return static_cast(LevelSequenceActor->GetSequencePlayer())->GetEvaluationTemplate().GetEntitySystemLinker()->FindSystem(); } 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(this, MovieSceneCaptureUIName); #endif } #if WITH_EDITORONLY_DATA void UAutomatedLevelSequenceCapture::AddFormatMappings(TMap& 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(CachedState.CameraComponent.Get()->GetOuter()); if (OuterActor) { OutFormatMappings.Add(TEXT("camera"), OuterActor->GetActorLabel()); } } } void UAutomatedLevelSequenceCapture::Initialize(TSharedPtr InViewport, int32 PIEInstance) { TimeController = MakeShared(); 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( LevelSequenceAsset.TryLoad() ); if( Asset != nullptr ) { for( auto It = TActorIterator( InViewport->GetClient()->GetWorld() ); It; ++It ) { if( It->GetSequence() == Asset ) { // Found it! Actor = *It; this->LevelSequenceActor = Actor; break; } } } } } if (!Actor) { ULevelSequence* Asset = Cast(LevelSequenceAsset.TryLoad()); if (Asset) { // Spawn a new actor Actor = InViewport->GetClient()->GetWorld()->SpawnActor(); 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(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(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::Empty())); if (ShotMovieScene) { TRange 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(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 ExtendedCachedPlaybackRange = UE::MovieScene::ExpandRange(CachedPlaybackRange, HandleFramesResolutionSpace); TRange TotalRange = TRange::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 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()); 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 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 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(this, "BurnInOptions"); } TSharedPtr OptionsContainer = Object.TryGetField(TEXT("BurnInOptions")); if (OptionsContainer.IsValid()) { FJsonObjectConverter::JsonAttributesToUStruct(OptionsContainer->AsObject()->Values, BurnInOptions->GetClass(), BurnInOptions, 0, 0); } BurnInOptions->ResetSettings(); if (BurnInOptions->Settings) { TSharedPtr 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(); 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 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(); 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 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