SceneCaptureComponent2D colors/quality do not match main camera

Ok, so I’ve made significant progress!

For those looking in future:
Make sure you only have one camera active, the one you want to render (I removed the player camera).
Remove motion blur (Project Settings and Post Process volume, just to be safe).

I re-implemented some code from Unreal Engine - you could instead modify it, but I wanted to avoid building the engine from source. If modifying the engine, change FViewport to inherit public FRenderResource (it’s protected by default), and make FViewport have SizeX and SizeY as public. If you do that, you can use the built in BeginInitResource/BeginReleaseResource, and set the size properties directly instead of using the setter SetSize.

HDDummyViewport.h:

#pragma once

#include "CoreMinimal.h"
#include "DummyViewport.h"
#include "UObject/Object.h"

/**
 * Re-implementation of Unreal Dummy Viewport
 */
class FHDDummyViewport : public FDummyViewport
{
public:
	FHDDummyViewport(FViewportClient* InViewportClient) : FDummyViewport(InViewportClient) {};
	virtual ~FHDDummyViewport() {};

	// Expose protected values with a setter
	void SetSize(uint32 x, uint32 y)
	{
		SizeX = x;
		SizeY = y;
	}

	// Expose relevant parts of FRenderResource
	void InitOverriden(FRHICommandListBase& RHICmdList) {InitResource(RHICmdList);}
	void ReleaseOverriden() {ReleaseResource();}
};

ScreenshotUtils.h:

#pragma once

#include "CoreMinimal.h"
#include "MessagePackDefinitions.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "ScreenshotUtils.generated.h"

/**
 * 
 */
UCLASS(Blueprintable)
class DESIGNLIFE_UNREAL_API UScreenshotUtils : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category="MessagePack", meta = (WorldContext="WorldContextObject"))
	static TArray<uint8> TakeScreenshot(FHD_DataPacket data, UObject* WorldContextObject, int warmupFrames = 10);
};

ScreenshotUtils.cpp:

#include "ScreenshotUtils.h"

#include "CanvasTypes.h"
#include "HDDummyViewport.h"
#include "RenderResource.h"
#include "UnrealClient.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"

static void HighResScreenshotBeginFrame(FHDDummyViewport* DummyViewport)
{
	GFrameCounter++;
	ENQUEUE_RENDER_COMMAND(BeginFrameCommand)(
		[DummyViewport, CurrentFrameCounter = GFrameCounter](FRHICommandListImmediate& RHICmdList)
	{
		GFrameCounterRenderThread = CurrentFrameCounter;
		GFrameNumberRenderThread++;
		GPU_STATS_BEGINFRAME(RHICmdList);
		FCoreDelegates::OnBeginFrameRT.Broadcast();
		if (DummyViewport)
		{
			DummyViewport->BeginRenderFrame(RHICmdList);
		}
	});
}

static void HighResScreenshotEndFrame(FHDDummyViewport* DummyViewport)
{
	ENQUEUE_RENDER_COMMAND(EndFrameCommand)(
		[DummyViewport](FRHICommandListImmediate& RHICmdList)
	{
		if (DummyViewport)
		{
			DummyViewport->EndRenderFrame(RHICmdList, false, false);
		}
		FCoreDelegates::OnEndFrameRT.Broadcast();
		RHICmdList.EndFrame();
		GPU_STATS_ENDFRAME(RHICmdList);
	});
}

void BeginInitResourceCustom(FHDDummyViewport* Resource, FRenderCommandPipe* RenderCommandPipe = nullptr)
{
	ENQUEUE_RENDER_COMMAND(InitCommand)(RenderCommandPipe,
		[Resource](FRHICommandListBase& RHICmdList)
		{
			Resource->InitOverriden(RHICmdList);
		});
}

void BeginReleaseResourceCustom(FHDDummyViewport* Resource, FRenderCommandPipe* RenderCommandPipe = nullptr)
{
	ENQUEUE_RENDER_COMMAND(ReleaseCommand)(RenderCommandPipe,
		[Resource]
		{
			Resource->ReleaseOverriden();
		});
}

TArray<uint8> UScreenshotUtils::TakeScreenshot(FHD_DataPacket data, UObject* WorldContextObject, int warmupFrames)
{
	// Get the viewport
	UGameViewportClient* ViewportClient = WorldContextObject->GetWorld()->GetGameViewport();
	FHDDummyViewport* DummyViewport = new FHDDummyViewport(ViewportClient);

	// Set the size to the desired output
	DummyViewport->SetSize(data.screenSize.x, data.screenSize.y);
	DummyViewport->SetupHDR(EDisplayColorGamut::sRGB_D65, EDisplayOutputFormat::SDR_sRGB, false);

	// Due to protected base class (FRenderResource), cannot call directly!
	// Reimplemented from BeginInitResource
	BeginInitResourceCustom(DummyViewport);

	// Tick to ensure the camera position is up-to-date
	WorldContextObject->GetWorld()->Tick(LEVELTICK_All, 0.01f);

	// Finish the current render frame
	HighResScreenshotEndFrame(nullptr);
	FlushRenderingCommands();
	
	// Perform run-up.
	int FrameDelay = warmupFrames;
	while (FrameDelay)
	{
		HighResScreenshotBeginFrame(DummyViewport);

		// Render!
		FCanvas Canvas(DummyViewport, nullptr, ViewportClient->GetWorld(), ViewportClient->GetWorld()->GetFeatureLevel());
		{
			ViewportClient->Draw(DummyViewport, &Canvas);
		}
		Canvas.Flush_GameThread();
		
		HighResScreenshotEndFrame(DummyViewport);
		FlushRenderingCommands();

		--FrameDelay;
	}

	// Read image pixels
	TArray<FColor> Bitmap;
	DummyViewport->ReadPixels(Bitmap, FReadSurfaceDataFlags());

	// Remove alpha channel
	for (auto& Color : Bitmap)
		Color.A = 255;
	
	FImageView Image(Bitmap.GetData(), data.screenSize.x, data.screenSize.y);

	// Convert image format
	static FName ImageWrapperName("ImageWrapper");
	IImageWrapperModule* ImageWrapperModule = &FModuleManager::LoadModuleChecked<IImageWrapperModule>(ImageWrapperName);
	if (ImageWrapperModule == nullptr)
		return TArray<uint8>();

	// Encode image
	EImageFormat format = EImageFormat::PNG;
	TArray64<uint8> CompressedData;
	ImageWrapperModule->CompressImage(CompressedData,format,Image,100);

	// Clean up
	BeginReleaseResourceCustom(DummyViewport);

	// Clean up
	FlushRenderingCommands();
	delete DummyViewport;
	HighResScreenshotBeginFrame(nullptr);

	// Convert to blueprintable array (NOTE: This limits the max size)
	TArray<uint8> ReturnData;
	ReturnData.SetNumUninitialized(CompressedData.Num());
	for (int i = 0; i < CompressedData.Num(); ++i)
	{
		ReturnData[i] = CompressedData[i];
	}
	
	return ReturnData;
}

Note that my desired output here is a byte array representing the PNG, and that the input is a struct with assorted pieces of information.

1 Like