// Copyright 2025 RLoris #include "FileHelperScreenshotAction.h" #include "Async/Async.h" #include "Camera/CameraActor.h" #include "Camera/CameraComponent.h" #include "Components/SceneCaptureComponent2D.h" #include "Engine/TextureRenderTarget2D.h" #include "Engine/Engine.h" #include "Framework/Application/SlateApplication.h" #include "HAL/PlatformFileManager.h" #include "IImageWrapper.h" #include "ImageUtils.h" #include "ImageWriteQueue.h" #include "ImageWriteTask.h" #include "Kismet/GameplayStatics.h" #include "Misc/Paths.h" #include "Misc/FileHelper.h" #include "Modules/ModuleManager.h" #include "TextureResource.h" #include "UnrealClient.h" #include "Widgets/SViewport.h" UFileHelperScreenshotAction* UFileHelperScreenshotAction::TakeScreenshot(const FFileHelperScreenshotActionOptions& InOptions) { UFileHelperScreenshotAction* Node = NewObject(); Node->Options = InOptions; Node->Options.Filename = FPaths::GetBaseFilename(Node->Options.Filename, /** Remove path */true); Node->bActive = false; return Node; } UTexture2D* UFileHelperScreenshotAction::LoadScreenshot(const FString& InFilePath) { return FImageUtils::ImportFileAsTexture2D(InFilePath); } void UFileHelperScreenshotAction::Activate() { if (bActive) { FFrame::KismetExecutionMessage(TEXT("ScreenshotUtility is already running"), ELogVerbosity::Warning); OnTaskFailed(); return; } Reset(); FText ErrorFilename; if (!FFileHelper::IsFilenameValidForSaving(Options.Filename, ErrorFilename)) { FFrame::KismetExecutionMessage(TEXT("Filename is not valid"), ELogVerbosity::Warning); OnTaskFailed(); return; } bActive = true; ScreenshotTexture = nullptr; const FString FinalFilename = (Options.bPrefixTimestamp ? (FDateTime::Now().ToString(TEXT("%Y_%m_%d__%H_%M_%S__"))) : "") + Options.Filename; FilePath = FPaths::Combine(Options.DirectoryPath, FinalFilename); if (!Options.CustomCameraActor) { if (Options.bShowUI) { CreateViewportScreenshot(); } else { CreatePlayerPOVScreenshot(); } } else { CreateCustomCameraScreenshot(); } } void UFileHelperScreenshotAction::OnTaskCompleted() { IPlatformFile& FileManager = FPlatformFileManager::Get().GetPlatformFile(); if (!ScreenshotTexture && FileManager.FileExists(*FilePath)) { ScreenshotTexture = FImageUtils::ImportFileAsTexture2D(FilePath); } if (ScreenshotTexture) { Completed.Broadcast(ScreenshotTexture, FilePath); } else { OnTaskFailed(); } Reset(); } void UFileHelperScreenshotAction::OnTaskFailed() { Reset(); Failed.Broadcast(ScreenshotTexture, FilePath); } void UFileHelperScreenshotAction::CreateCustomCameraScreenshot() { const ACameraActor* Camera = Options.CustomCameraActor; if (!Camera) { OnTaskFailed(); return; } UWorld* World = Camera->GetWorld(); if (!World) { OnTaskFailed(); return; } const UCameraComponent* CameraComponent = Camera->GetCameraComponent(); if (!CameraComponent) { OnTaskFailed(); return; } const FViewport* GameViewport = World->GetGameViewport()->Viewport; const FIntRect ViewRect(0, 0, GameViewport->GetSizeXY().X, GameViewport->GetSizeXY().Y); const FVector CameraLocation = Camera->GetActorLocation(); const FRotator CameraRotation = Camera->GetActorRotation(); const float CameraFOV = CameraComponent->FieldOfView; const ECameraProjectionMode::Type CameraProjectionMode = CameraComponent->ProjectionMode; const float CameraOrthoWidth = CameraComponent->OrthoWidth; CreateRenderTargetScreenshot(GWorld, CameraFOV, CameraProjectionMode, CameraOrthoWidth, CameraLocation, CameraRotation, ViewRect.Width(), ViewRect.Height()); } void UFileHelperScreenshotAction::CreateViewportScreenshot() { if (!GEngine || !GEngine->GameViewport || !GEngine->GameViewport->Viewport) { OnTaskFailed(); return; } FViewport* Viewport = GEngine->GameViewport->Viewport; // Get viewport size const FIntPoint ViewportSize = Viewport->GetSizeXY(); const int32 Width = ViewportSize.X; const int32 Height = ViewportSize.Y; TWeakObjectPtr ThisWeak(this); auto ThenTask = [ThisWeak](const TFuture& InFuture) { bool bResult = InFuture.Get(); AsyncTask(ENamedThreads::Type::GameThread, [ThisWeak, bResult]() { UFileHelperScreenshotAction* This = ThisWeak.Get(); if (!This) { return; } if (!bResult) { This->OnTaskFailed(); return; } UTexture2D* Texture = FImageUtils::ImportFileAsTexture2D(This->FilePath); if (!Texture) { This->OnTaskFailed(); return; } This->ScreenshotTexture = Texture; This->OnTaskCompleted(); }); }; if (Options.bWithHDR && Viewport->IsHDRViewport()) { TArray OutPixels; OutPixels.Reserve(Width * Height); if (!Viewport->ReadLinearColorPixels(OutPixels, FReadSurfaceDataFlags(), FIntRect(0, 0, Width, Height)) || OutPixels.IsEmpty()) { OnTaskFailed(); return; } OutPixels.Shrink(); // If UI is enabled, capture it separately and blend it const TSharedPtr ViewportWidget = GEngine->GameViewport->GetGameViewportWidget(); if (Options.bShowUI && ViewportWidget.IsValid()) { FIntVector OutSize; if (!FSlateApplication::Get().TakeHDRScreenshot(ViewportWidget.ToSharedRef(), OutPixels, OutSize)) { OnTaskFailed(); return; } } FilePath += TEXT(".exr"); WriteLinearColorBufferToDiskAsync(MoveTemp(OutPixels), Width, Height) .Then(ThenTask); } else { TArray OutPixels; OutPixels.Reserve(Width * Height); if (!Viewport->ReadPixels(OutPixels, FReadSurfaceDataFlags(), FIntRect(0, 0, Width, Height)) || OutPixels.IsEmpty()) { OnTaskFailed(); return; } OutPixels.Shrink(); // If UI is enabled, capture it separately and blend it const TSharedPtr ViewportWidget = GEngine->GameViewport->GetGameViewportWidget(); if (Options.bShowUI && ViewportWidget.IsValid()) { FIntVector OutSize; if (!FSlateApplication::Get().TakeScreenshot(ViewportWidget.ToSharedRef(), OutPixels, OutSize)) { OnTaskFailed(); return; } } for (FColor& Color : OutPixels) { Color.A = 255; } FilePath += TEXT(".png"); WriteColorBufferToDiskAsync(MoveTemp(OutPixels), Width, Height) .Then(ThenTask); } } void UFileHelperScreenshotAction::CreatePlayerPOVScreenshot() { if (!GEngine || !GEngine->GameViewport || !GEngine->GameViewport->Viewport) { OnTaskFailed(); return; } const APlayerController* PlayerController = UGameplayStatics::GetPlayerController(GWorld, 0); if (!PlayerController || !PlayerController->PlayerCameraManager) { OnTaskFailed(); return; } FVector2D ViewportSize; GEngine->GameViewport->GetViewportSize(ViewportSize); FVector CameraLocation; FRotator CameraRotation; PlayerController->GetPlayerViewPoint(CameraLocation, CameraRotation); const float FOV = PlayerController->PlayerCameraManager->GetFOVAngle(); constexpr ECameraProjectionMode::Type ProjectionMode = ECameraProjectionMode::Perspective; CreateRenderTargetScreenshot(GWorld, FOV, ProjectionMode, 0.f, CameraLocation, CameraRotation, ViewportSize.X, ViewportSize.Y); } void UFileHelperScreenshotAction::CreateRenderTargetScreenshot(UWorld* InWorld, float InFOV, ECameraProjectionMode::Type InProjectionMode, float InOrthoWidth, const FVector& InLocation, const FRotator& InRotation, int32 InWidth, int32 InHeight) { USceneCaptureComponent2D* SceneComponent = NewObject(this, TEXT("SceneComponent")); SceneComponent->RegisterComponentWithWorld(InWorld); SceneComponent->bCaptureEveryFrame = false; SceneComponent->bCaptureOnMovement = false; SceneComponent->bAlwaysPersistRenderingState = true; SceneComponent->CaptureSource = ESceneCaptureSource::SCS_FinalColorHDR; SceneComponent->FOVAngle = InFOV; SceneComponent->ProjectionType = InProjectionMode; SceneComponent->OrthoWidth = InOrthoWidth; SceneComponent->SetWorldLocationAndRotation(InLocation, InRotation); UTextureRenderTarget2D* TextureRenderTarget = NewObject(); TextureRenderTarget->InitCustomFormat(InWidth,InHeight,PF_B8G8R8A8,false); TextureRenderTarget->UpdateResourceImmediate(); SceneComponent->TextureTarget = TextureRenderTarget; SceneComponent->CaptureScene(); SceneComponent->UnregisterComponent(); SceneComponent->MarkAsGarbage(); FTextureRenderTargetResource* RenderTargetResource = TextureRenderTarget->GameThread_GetRenderTargetResource(); if (!RenderTargetResource) { OnTaskFailed(); return; } TWeakObjectPtr ThisWeak(this); auto ThenTask = [ThisWeak](const TFuture& InFuture) { bool bResult = InFuture.Get(); AsyncTask(ENamedThreads::Type::GameThread, [ThisWeak, bResult]() { UFileHelperScreenshotAction* This = ThisWeak.Get(); if (!This) { return; } if (!bResult) { This->OnTaskFailed(); return; } UTexture2D* Texture = FImageUtils::ImportFileAsTexture2D(This->FilePath); if (!Texture) { This->OnTaskFailed(); return; } This->ScreenshotTexture = Texture; This->OnTaskCompleted(); }); }; if (Options.bWithHDR && RenderTargetResource->GetSceneHDREnabled()) { TArray OutPixels; OutPixels.Reserve(InWidth * InHeight); if (!RenderTargetResource->ReadLinearColorPixels(OutPixels) || OutPixels.IsEmpty()) { OnTaskFailed(); return; } OutPixels.Shrink(); FilePath += TEXT(".exr"); WriteLinearColorBufferToDiskAsync(MoveTemp(OutPixels), InWidth, InHeight) .Then(ThenTask); } else { TArray OutPixels; OutPixels.Reserve(InWidth * InHeight); if (!RenderTargetResource->ReadPixels(OutPixels) || OutPixels.IsEmpty()) { OnTaskFailed(); return; } OutPixels.Shrink(); for (FColor& Color : OutPixels) { Color.A = 255; } FilePath += TEXT(".png"); WriteColorBufferToDiskAsync(MoveTemp(OutPixels), InWidth, InHeight) .Then(ThenTask); } TextureRenderTarget->MarkAsGarbage(); } bool UFileHelperScreenshotAction::WriteColorBufferToDisk(const TArray& InBuffer, int32 InWidth, int32 InHeight, UTexture2D*& OutTexture) const { if (InBuffer.IsEmpty()) { return false; } TArray OutImage; FImageUtils::ThumbnailCompressImageArray(InWidth, InHeight, InBuffer, OutImage); if (OutImage.IsEmpty()) { return false; } if (!FFileHelper::SaveArrayToFile(OutImage, *FilePath)) { return false; } OutTexture = FImageUtils::ImportBufferAsTexture2D(OutImage); return IsValid(OutTexture); } TFuture UFileHelperScreenshotAction::WriteLinearColorBufferToDiskAsync(TArray&& InBuffer, int32 InWidth, int32 InHeight) { IImageWriteQueueModule& ImageWriteQueueModule = FModuleManager::LoadModuleChecked("ImageWriteQueue"); TUniquePtr ImageTask = MakeUnique(); ImageTask->Format = EImageFormat::EXR; ImageTask->PixelData = MakeUnique>(FIntPoint(InWidth, InHeight), TArray64(InBuffer)); ImageTask->Filename = FilePath; ImageTask->bOverwriteFile = true; ImageTask->CompressionQuality = 100; return ImageWriteQueueModule.GetWriteQueue().Enqueue(MoveTemp(ImageTask)); } TFuture UFileHelperScreenshotAction::WriteColorBufferToDiskAsync(TArray&& InBuffer, int32 InWidth, int32 InHeight) { IImageWriteQueueModule& ImageWriteQueueModule = FModuleManager::LoadModuleChecked("ImageWriteQueue"); TUniquePtr ImageTask = MakeUnique(); ImageTask->Format = EImageFormat::PNG; ImageTask->PixelData = MakeUnique>(FIntPoint(InWidth, InHeight), TArray64(InBuffer)); ImageTask->Filename = FilePath; ImageTask->bOverwriteFile = true; ImageTask->CompressionQuality = 100; return ImageWriteQueueModule.GetWriteQueue().Enqueue(MoveTemp(ImageTask)); } void UFileHelperScreenshotAction::ConvertLinearColorToColorBuffer(const TArray& InSourceBuffer, TArray& OutDestBuffer) { OutDestBuffer.Empty(InSourceBuffer.Num()); for (const FLinearColor& LinearColor : InSourceBuffer) { OutDestBuffer.Emplace(LinearColor.ToFColor(false)); // Apply gamma correction } } void UFileHelperScreenshotAction::Reset() { ScreenshotTexture = nullptr; bActive = false; FilePath.Empty(); }