Files
UnrealEngine/Engine/Source/Runtime/MovieSceneCapture/Private/UserDefinedCaptureProtocol.cpp
2025-05-18 13:04:45 +08:00

474 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Protocols/UserDefinedCaptureProtocol.h"
#include "ImageWriteQueue.h"
#include "ImageWriteBlueprintLibrary.h"
#include "Modules/ModuleManager.h"
#include "Async/Async.h"
#include "Engine/Texture.h"
#include "UnrealClient.h"
#include "MovieSceneCaptureModule.h"
#include "MovieSceneCaptureSettings.h"
#include "Slate/SceneViewport.h"
#include "ImagePixelData.h"
#include "Engine/Engine.h"
#include "Logging/MessageLog.h"
#include "ViewportClient.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(UserDefinedCaptureProtocol)
#define LOCTEXT_NAMESPACE "UserDefinedImageCaptureProtocol"
struct FCaptureProtocolFrameData : IFramePayload
{
FFrameMetrics Metrics;
FCaptureProtocolFrameData(const FFrameMetrics& InMetrics)
: Metrics(InMetrics)
{}
};
/** Callable utility struct that calls a handler with the specified parameters on the game thread */
struct FCallPixelHandler_GameThread
{
static void Dispatch(const FCapturedPixelsID& InStreamID, const FFrameMetrics& InFrameMetrics, const FCapturedPixels& InPixels, TWeakObjectPtr<UUserDefinedCaptureProtocol> InWeakProtocol)
{
FCallPixelHandler_GameThread Functor;
Functor.Pixels = InPixels;
Functor.FrameMetrics = InFrameMetrics;
Functor.StreamID = InStreamID;
Functor.WeakProtocol = InWeakProtocol;
if (!IsInGameThread())
{
AsyncTask(ENamedThreads::GameThread, MoveTemp(Functor));
}
else
{
Functor();
}
}
void operator()()
{
check(IsInGameThread());
UUserDefinedCaptureProtocol* Protocol = WeakProtocol.Get();
if (Protocol)
{
Protocol->OnPixelsReceivedImpl(Pixels, StreamID, FrameMetrics);
}
}
private:
/** The captured pixels themselves */
FCapturedPixels Pixels;
/** The ID of the stream that these pixels represent */
FCapturedPixelsID StreamID;
/** Metrics for the frame from which the pixel data is derived */
FFrameMetrics FrameMetrics;
/** Weak pointer back to the protocol. Only used to invoke OnBufferReady. */
TWeakObjectPtr<UUserDefinedCaptureProtocol> WeakProtocol;
};
FString FCapturedPixelsID::ToString() const
{
FString Name;
for (const TTuple<FName, FName>& Pair : Identifiers)
{
if (Name.Len() > 0)
{
Name += TEXT(",");
}
Name += Pair.Key.ToString();
if (Pair.Value != NAME_None)
{
Name += TEXT(":");
Name += Pair.Value.ToString();
}
}
return Name.Len() > 0 ? Name : TEXT("<none>");
}
UUserDefinedCaptureProtocol::UUserDefinedCaptureProtocol(const FObjectInitializer& ObjInit)
: Super(ObjInit)
, World(nullptr)
, NumOutstandingOperations(0)
{
CurrentStreamID = nullptr;
}
void UUserDefinedCaptureProtocol::PreTickImpl()
{
OnPreTick();
}
void UUserDefinedCaptureProtocol::TickImpl()
{
OnTick();
// Process any frames that have been captured from the frame grabber
if (FinalPixelsFrameGrabber.IsValid())
{
TArray<FCapturedFrameData> CapturedFrames = FinalPixelsFrameGrabber->GetCapturedFrames();
for (FCapturedFrameData& Frame : CapturedFrames)
{
// Steal the frame and make it shareable
FCapturedPixels CapturedPixels { MakeShared<TImagePixelData<FColor>, ESPMode::ThreadSafe>( Frame.BufferSize, TArray64<FColor>(MoveTemp(Frame.ColorBuffer) ) ) };
FFrameMetrics CapturedMetrics = static_cast<FCaptureProtocolFrameData*>(Frame.Payload.Get())->Metrics;
// Call the handler
OnPixelsReceivedImpl(CapturedPixels, FinalPixelsID, CapturedMetrics);
}
}
}
bool UUserDefinedCaptureProtocol::SetupImpl()
{
if (FViewportClient* Client = InitSettings->SceneViewport->GetClient())
{
World = Client->GetWorld();
}
else
{
World = nullptr;
}
int32 PreviousPlayInEditorID = UE::GetPlayInEditorID();
if (World)
{
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (World == Context.World())
{
UE::SetPlayInEditorID(Context.PIEInstance);
}
}
}
// Preemptively create the frame grabber for final pixels, but do not start capturing final pixels until instructed
FinalPixelsFrameGrabber.Reset(new FFrameGrabber(InitSettings->SceneViewport.ToSharedRef(), InitSettings->DesiredSize, PF_B8G8R8A8, 3));
const bool bSuccess = OnSetup();
UE::SetPlayInEditorID(PreviousPlayInEditorID);
return bSuccess;
}
void UUserDefinedCaptureProtocol::WarmUpImpl()
{
OnWarmUp();
}
bool UUserDefinedCaptureProtocol::StartCaptureImpl()
{
OnStartCapture();
return true;
}
void UUserDefinedCaptureProtocol::StartCapturingFinalPixels(const FCapturedPixelsID& StreamID)
{
if (FinalPixelsFrameGrabber.IsValid() && GetState() == EMovieSceneCaptureProtocolState::Capturing && !FinalPixelsFrameGrabber->IsCapturingFrames())
{
FinalPixelsID = StreamID;
FinalPixelsFrameGrabber->StartCapturingFrames();
}
}
void UUserDefinedCaptureProtocol::StopCapturingFinalPixels()
{
if (FinalPixelsFrameGrabber.IsValid() && GetState() == EMovieSceneCaptureProtocolState::Capturing && FinalPixelsFrameGrabber->IsCapturingFrames())
{
FinalPixelsFrameGrabber->StopCapturingFrames();
}
}
void UUserDefinedCaptureProtocol::BeginFinalizeImpl()
{
StopCapturingFinalPixels();
OnBeginFinalize();
}
bool UUserDefinedCaptureProtocol::HasFinishedProcessingImpl() const
{
if (NumOutstandingOperations != 0)
{
return false;
}
// If the frame grabber is still processing, we still have work to do
if (FinalPixelsFrameGrabber.IsValid() && FinalPixelsFrameGrabber->HasOutstandingFrames())
{
return false;
}
return OnCanFinalize();
}
void UUserDefinedCaptureProtocol::FinalizeImpl()
{
if (FinalPixelsFrameGrabber.IsValid())
{
FinalPixelsFrameGrabber->Shutdown();
FinalPixelsFrameGrabber.Reset();
}
OnFinalize();
}
void UUserDefinedCaptureProtocol::CaptureFrameImpl(const FFrameMetrics& InFrameMetrics)
{
CachedFrameMetrics = InFrameMetrics;
if (FinalPixelsFrameGrabber.IsValid() && FinalPixelsFrameGrabber->IsCapturingFrames())
{
ReportOutstandingWork(1);
FinalPixelsFrameGrabber->CaptureThisFrame(MakeShared<FCaptureProtocolFrameData, ESPMode::ThreadSafe>(CachedFrameMetrics));
}
OnCaptureFrame();
}
void UUserDefinedCaptureProtocol::ResolveBuffer(UTexture* Buffer, const FCapturedPixelsID& StreamID)
{
if (!IsCapturing())
{
FFrame::KismetExecutionMessage(TEXT("Capture protocol is not currently capturing frames."), ELogVerbosity::Error);
return;
}
TWeakObjectPtr<UUserDefinedCaptureProtocol> WeakProtocol = MakeWeakObjectPtr(this);
FFrameMetrics FrameMetrics = CachedFrameMetrics;
// Capture the current state by-value into the lambda so it can be correctly processed by the thread that resolves the pixels
auto OnPixelsReady = [StreamID, FrameMetrics, WeakProtocol](TUniquePtr<FImagePixelData>&& PixelData)
{
FCapturedPixels CapturedPixels = { MakeShareable(PixelData.Release()) };
FCallPixelHandler_GameThread::Dispatch(StreamID, FrameMetrics, CapturedPixels, WeakProtocol);
};
// Resolve the texture data
if (UImageWriteBlueprintLibrary::ResolvePixelData(Buffer, OnPixelsReady))
{
ReportOutstandingWork(1);
}
}
void UUserDefinedCaptureProtocol::OnPixelsReceivedImpl(const FCapturedPixels& Pixels, const FCapturedPixelsID& StreamID, FFrameMetrics FrameMetrics)
{
--NumOutstandingOperations;
if (Pixels.ImageData->IsDataWellFormed())
{
OnPixelsReceived(Pixels, StreamID, FrameMetrics);
}
}
FString UUserDefinedCaptureProtocol::GenerateFilename(const FFrameMetrics& InFrameMetrics) const
{
if (!CaptureHost)
{
FFrame::KismetExecutionMessage(TEXT("Capture protocol is not currently set up to generate filenames."), ELogVerbosity::Error);
return FString();
}
FString Filename = Super::GenerateFilenameImpl(InFrameMetrics, TEXT(""));
EnsureFileWritableImpl(Filename);
return Filename;
}
void UUserDefinedCaptureProtocol::AddFormatMappingsImpl(TMap<FString, FStringFormatArg>& FormatMappings) const
{
if (CurrentStreamID)
{
for (const TTuple<FName, FName>& Pair : CurrentStreamID->Identifiers)
{
if (Pair.Value == NAME_None)
{
FormatMappings.Add(Pair.Key.ToString(), FString());
}
else
{
FormatMappings.Add(Pair.Key.ToString(), Pair.Value.ToString());
}
}
}
}
void UUserDefinedCaptureProtocol::ReportOutstandingWork(int32 NumNewOperations)
{
NumOutstandingOperations += NumNewOperations;
}
UUserDefinedImageCaptureProtocol::UUserDefinedImageCaptureProtocol(const FObjectInitializer& ObjInit)
: Super(ObjInit)
, Format(EDesiredImageFormat::EXR)
, bEnableCompression(false)
, CompressionQuality(100)
{}
void UUserDefinedImageCaptureProtocol::OnReleaseConfigImpl(FMovieSceneCaptureSettings& InSettings)
{
// Remove .{frame} if it exists
InSettings.OutputFormat = InSettings.OutputFormat.Replace(TEXT(".{frame}"), TEXT(""));
}
void UUserDefinedImageCaptureProtocol::OnLoadConfigImpl(FMovieSceneCaptureSettings& InSettings)
{
FString OutputFormat = InSettings.OutputFormat;
// Ensure the format string tries to always export a uniquely named frame so the file doesn't overwrite itself if the user doesn't add it.
bool bHasFrameFormat = OutputFormat.Contains(TEXT("{frame}")) || OutputFormat.Contains(TEXT("{shot_frame}"));
if (!bHasFrameFormat)
{
OutputFormat.Append(TEXT(".{frame}"));
InSettings.OutputFormat = OutputFormat;
UE_LOG(LogMovieSceneCapture, Display, TEXT("Automatically appended .{frame} to the format string as specified format string did not provide a way to differentiate between frames via {frame} or {shot_frame}!"));
}
}
FString UUserDefinedImageCaptureProtocol::GenerateFilename(const FFrameMetrics& InFrameMetrics) const
{
if (!CaptureHost)
{
FFrame::KismetExecutionMessage(TEXT("Capture protocol is not currently set up to generate filenames."), ELogVerbosity::Error);
return FString();
}
const TCHAR* Extension = TEXT("");
switch (Format)
{
case EDesiredImageFormat::BMP: Extension = TEXT(".bmp"); break;
case EDesiredImageFormat::PNG: Extension = TEXT(".png"); break;
case EDesiredImageFormat::JPG: Extension = TEXT(".jpg"); break;
case EDesiredImageFormat::EXR:
default:
Extension = TEXT(".exr");
break;
}
FString Filename = GenerateFilenameImpl(InFrameMetrics, Extension);
EnsureFileWritableImpl(Filename);
return Filename;
}
FString UUserDefinedImageCaptureProtocol::GenerateFilenameForCurrentFrame()
{
return GenerateFilename(CachedFrameMetrics);
}
FString UUserDefinedImageCaptureProtocol::GenerateFilenameForBuffer(UTexture* Buffer, const FCapturedPixelsID& StreamID)
{
if (!CaptureHost)
{
FFrame::KismetExecutionMessage(TEXT("Capture protocol is not currently set up to generate filenames."), ELogVerbosity::Error);
return FString();
}
const TCHAR* Extension = TEXT(".ext");
switch (Format)
{
case EDesiredImageFormat::EXR: Extension = TEXT(".exr"); break;
case EDesiredImageFormat::BMP: Extension = TEXT(".bmp"); break;
case EDesiredImageFormat::PNG: Extension = TEXT(".png"); break;
case EDesiredImageFormat::JPG: Extension = TEXT(".jpg"); break;
default: break;
}
CurrentStreamID = &StreamID;
FString Filename = GenerateFilenameImpl(CachedFrameMetrics, Extension);
EnsureFileWritableImpl(Filename);
CurrentStreamID = nullptr;
return Filename;
}
void UUserDefinedImageCaptureProtocol::WriteImageToDisk(const FCapturedPixels& PixelData, const FCapturedPixelsID& StreamID, const FFrameMetrics& FrameMetrics, bool bCopyImageData)
{
if (!PixelData.ImageData.IsValid())
{
return;
}
else if (PixelData.ImageData->GetBitDepth() != 8)
{
if (Format == EDesiredImageFormat::BMP)
{
FMessageLog("PIE")
.Warning(FText::Format(LOCTEXT("InvalidBMPExport", "Unable to write the specified render target (stream '{0}' is {1}bit) as BMP. BMPs must be supplied 8bit render targets."),
FText::FromString(StreamID.ToString()), FText::AsNumber(PixelData.ImageData->GetBitDepth())));
return;
}
else if (Format == EDesiredImageFormat::JPG)
{
FMessageLog("PIE")
.Warning(FText::Format(LOCTEXT("InvalidJPGExport", "Unable to write the specified render target (stream '{0}' is {1}bit) as JPG. JPGs must be supplied 8bit render targets."),
FText::FromString(StreamID.ToString()), FText::AsNumber(PixelData.ImageData->GetBitDepth())));
return;
}
}
TUniquePtr<FImageWriteTask> ImageTask = MakeUnique<FImageWriteTask>();
// Cache the buffer ID so we generate the correct filename
CurrentStreamID = &StreamID;
ImageTask->PixelData = bCopyImageData ? PixelData.ImageData->CopyImageData() : PixelData.ImageData->MoveImageDataToNew();
ImageTask->Filename = GenerateFilename(FrameMetrics);
ImageTask->Format = ImageFormatFromDesired(Format);
ImageTask->bOverwriteFile = false;
CurrentStreamID = nullptr;
// If the pixels are FColors, and this is the final pixels buffer, and we're writing PNG, always write out full alpha
if (PixelData.ImageData->GetType() == EImagePixelType::Color && ImageTask->Format == EImageFormat::PNG && StreamID.Identifiers.OrderIndependentCompareEqual(FinalPixelsID.Identifiers))
{
ImageTask->AddPreProcessorToSetAlphaOpaque();
}
if (Format == EDesiredImageFormat::EXR)
{
ImageTask->CompressionQuality = bEnableCompression ? (int32)EImageCompressionQuality::Default : (int32)EImageCompressionQuality::Uncompressed;
}
else
{
ImageTask->CompressionQuality = bEnableCompression ? CompressionQuality : 100;
}
{
// Set a callback that is called on the main thread when this file has been written
TWeakObjectPtr<UUserDefinedImageCaptureProtocol> WeakThis = this;
ImageTask->OnCompleted = [WeakThis](bool)
{
UUserDefinedImageCaptureProtocol* This = WeakThis.Get();
if (This)
{
This->OnFileWritten();
}
};
}
IImageWriteQueue& ImageWriteQueue = FModuleManager::Get().LoadModuleChecked<IImageWriteQueueModule>("ImageWriteQueue").GetWriteQueue();
TFuture<bool> DispatchedTask = ImageWriteQueue.Enqueue(MoveTemp(ImageTask));
if (DispatchedTask.IsValid())
{
// If we actually dispatched the task, increment the number of outstanding operations
ReportOutstandingWork(1);
}
}
void UUserDefinedImageCaptureProtocol::OnFileWritten()
{
--NumOutstandingOperations;
}
#undef LOCTEXT_NAMESPACE