Take screenshot using render target

I am trying to recreate the behaviour of the ‘shot’ or ‘highresshot’ console commands in order to take a screenshot of the viewport. I was using these commands previously, however I found that they weren’t working in the packaged version of the game (I tried using both the console command blueprint node and the equivalent in CPP, neither worked). Regardless, I never really liked using console commands to achieve this.

In my level map, I have two light sources: a directional light and a sky light. I also have a GlobalPostProcessVolume. I have added a SceneCapture2D actor to the map, keeping all settings to default except for changing Capture Source and preventing it from capturing every frame.

I have the following blueprint logic:

This produces the following screenshot:

However, this differs from what is shown in the viewport. If I use the ‘shot’ command, I get this:

Firstly, while the camera location and rotation seem fine the FOV is slightly too low. More importantly, the light rendering seems completely different - the screenshot generated from the blueprint seems to have no self-casting shadows, and no reflected light cast on the walls.

I’ve tried every combination of SceneCapture2D capture source, and Render Target format, and this combination I’ve shown gives the “best” result.

What am I doing wrong here? Is it best to achieve this in a different way? (I am not tied down to this method by any means)

Barked up this tree before. With good results.

The settings on the scene capture was all that needed to be fixed up at the time to get nice shots.

Now, With Lumen and all the new BS, I doubt you can get good results. Unless you turn all of the things off and setup up the right light in the scens.

However they may have parameters you can toggle added to the scene capture to try and better match up the scene…

scene capture has it’s own post process settings. you gotta enable the features you need in there.

I’ve copied over the post process settings from my global post process volume in the blueprint. I’ve also since this post gone through each setting one by one and verifying they match up and I still couldn’t ever get it to look the same.

Path tracer is another option I’ve looked at however it only works on certain graphics cards which isn’t ideal.

I suspect there is a bug in UE 5.5 when it comes to screenshots in shipping builds, as these functions work in UE 5.4 regardless of build:

void UMyBlueprintFunctionLibrary::ConsoleCommandScreenshot(const FString FilePath, APlayerController* PlayerController)
{
	PlayerController->ConsoleCommand(FString::Printf(TEXT("HighResShot 1 filename=\"%s\""), *FilePath));
}


void UMyBlueprintFunctionLibrary::FScreenshotRequestScreenshot(const FString FilePath)
{
	if (!GEngine || !GEngine->GameViewport) return;

	FViewport* Viewport = GEngine->GameViewport->Viewport;
	if (!Viewport) return;

	FIntPoint Size = Viewport->GetSizeXY();

	GetHighResScreenshotConfig().SetResolution(Size.X, Size.Y);
	GetHighResScreenshotConfig().FilenameOverride = FilePath;

	FScreenshotRequest::RequestScreenshot(false);
}

I’ve also tried reading pixel data from the viewport directly, but this also doesn’t work in shipping builds for both UE 5.5 and UE 5.4:

void UMyBlueprintFunctionLibrary::ViewportReadPixelsScreenshot(const FString FilePath)
{
	if (!GEngine || !GEngine->GameViewport) return;

	// Access the game viewport
	FViewport* Viewport = GEngine->GameViewport->Viewport;

	if (!Viewport) return;

	// Capture the screenshot
	TArray<FColor> Bitmap;
	bool bReadPixels = Viewport->ReadPixels(Bitmap);
	if (!bReadPixels)
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to read pixels from the viewport."));
		return;
	}

	for (FColor& Pixel : Bitmap)
	{
		// Set each pixel to be fully opaque
		Pixel.A = 255;
	}

	FIntPoint Size = Viewport->GetSizeXY();

	// Compress the Bitmap to a PNG format
	FImageView Image(Bitmap.GetData(), Size.X, Size.Y);
	bool bSaved = FImageUtils::SaveImageByExtension(*FilePath, Image);

	if (bSaved)
	{
		UE_LOG(LogTemp, Log, TEXT("Screenshot saved to: %s"), *FilePath);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to save screenshot to: %s"), *FilePath);
	}
}


void UMyBlueprintFunctionLibrary::ViewportReadSurfaceDataScreenshot(const FString FilePath)
{
	if (!GEngine || !GEngine->GameViewport) return;

	// Access the game viewport
	FViewport* Viewport = GEngine->GameViewport->Viewport;
	if (!Viewport) return;

	FIntPoint Size = Viewport->GetSizeXY();
	FTextureRHIRef RenderTarget = Viewport->GetRenderTargetTexture();

	if (!RenderTarget) return;

	// Enqueue the rendering command
	ENQUEUE_RENDER_COMMAND(CaptureScreenshotCommand)(
		[RenderTarget, FilePath, Size](FRHICommandListImmediate& RHICmdList)
		{
			// Read pixel data from the render target
			TArray<FColor> Bitmap;
			FReadSurfaceDataFlags ReadPixelFlags(RCM_UNorm);
			RHICmdList.ReadSurfaceData(RenderTarget, FIntRect(0, 0, Size.X, Size.Y), Bitmap, ReadPixelFlags);

			for (FColor& Pixel : Bitmap)
			{
				// Set each pixel to be fully opaque
				Pixel.A = 255;
			}

			// Compress the Bitmap to a PNG format
			FImageView Image(Bitmap.GetData(), Size.X, Size.Y);
			bool bSaved = FImageUtils::SaveImageByExtension(*FilePath, Image);

			if (bSaved)
			{
				UE_LOG(LogTemp, Log, TEXT("Screenshot saved to: %s"), *FilePath);
			}
			else
			{
				UE_LOG(LogTemp, Error, TEXT("Failed to save screenshot to: %s"), *FilePath);
			}
		}
		);
}

ViewportReadPixelsScreenshot fails in shipping builds as Viewport->ReadPixels(Bitmap) returns an array of black pixels.
ViewportReadSurfaceDataScreenshot fails in shipping builds as Viewport->GetRenderTargetTexture(); returns a nullptr. Both functions work in any other build.

At this stage I’ve tried everything I can think of and I’m either going to downgrade to UE5.4 or just go with the render target solution above and accept that it won’t look identical. Unless anyone else has any suggestions?

You can’t levarage Editor Only / Debug functions on final distributions unless you specifically include them as part of your distribution.

You can do that by changing the build settings to specifically include whatver DLL/part you need.

However - you need to error check your code and prevent executing functions that would rely on the missing components should said components not be loaded.

Probably abort the function/return null if the function you want to call doesnt exist - you know just as a general best practice that keeps crashes from happening.

Just like you did here
if (!GEngine || !GEngine->GameViewport) return;

But xor is probably worng.

You want to return if
Gengine is null, if gengine game viewport is null, and if gengine game viewport vieport is null.
So

If (Gengine==null && GEngine->GameViewport==null && GEngine->GameViewport->Viewport)
{
//any code in here is automatically accessible since it passed verification.
//IDEs wont throw fits about “possibly null” and such nonsense.
}
Else
{
//here the code was inaccessible, so you can force return if you even need the else at all.
}

Using Xand you sequentially prevent accessing invalid things too, but IDEs may fail to detect the fact your code is definitely not accessing null stuff because of the return.

You really shouldnt do this either

FViewport* Viewport = GEngine->GameViewport->Viewport;
if (!Viewport) return;

On the surface, seems like nothing is wrong. Afterall a pointer can be null.
In practice, it can (and will) lead to issues. Even if just depdending on the IDE (which here is probably visual studio, so its going to throw issy fits about it almost guaranteed).

To be honest?

Why use engine provided read/write stuff when you can just include IO headers and write your own files using native/non compromised/universally accessible dll ?

Read byte array > write from stream.
This stuff

And since the engine loads directX, you can use that directly for the image writing too.
It’s just more painful (because directx).

Assuming ofc that
FImageUtils::SaveImageByExtension(*FilePath, Image);
Is not exposed/avaliable to the final build is why im beinging DX up at all.