SceneCaptureComponent2D colors/quality do not match main camera

Hi!

Pretty new to Unreal, have over a decade of Unity experience.

Bit of background for context:
My goal: To take a HTTP request containing a set of properties, and then return a screenshot (think like a render server for single frames). The client is a Unity WebGL build, sending a compressed MessagePack data packet to the server, and expecting a PNG web response.
I have a working version in Unity, but Lumen is just so much nicer than Enlighten when using the restriction of realtime lighting only (I cannot bake lighting as the shape/size/windows/light can all be adjusted by users).

So, to the problem.
My blueprint for reference (doing 20 renders for now to account for multi-frame post FX e.g. TAA):

This works, but produced an image as follows:

Note the lack of GI and the green hue.
I then decided to investigate the SceneCaptureComponent2D directly, and worked out that by default SceneCaptureComponents have a post processing weight of 1 - changing that to zero (now using a live version in the scene instead of the blueprint for speed of testing):

Better, but what I want is for it to look like how it looks in the game view:

Issues with this image should be apparent, but listing here:

  • Green hue on underside of ceiling lights instead of warmer hue (in general, the scene look green instead of warm)
  • artifacts on far wall
  • strange reflection artifacts on table
  • Missing a lot of shadow details

Render target settings:


Scene capture settings:

I initially tried the HD Screenshot tool, but given that I need a specific width/height/FoV, that seemed more complicated to achieve the correct output.

Oh, happy to do this in C++ if that’s going to give me better results! It’s been a decade since I’ve done C++, but I did mange to get a few blueprint nodes set up so fairly confident I can get there if C++ is going to be the answer.

Not sure the scene capture would ever be the same quality.

BTW, there’s a ton of parameters to the high res screenshot command. Seems like there’s got to be a way to get the pixel size you want:

Taking Screenshots | Unreal Engine 4.27 Documentation | Epic Developer Community

My understanding based on the documentation is that it takes a screenshot, and is thus bound to the screen/window resolution. I know you can specify a larger image, but that’s for upsampling, or you can specify a clip rect as a portion of the viewport.

The thing is, for my use case, I need to have the position of objects and camera perfectly aligned to the Unity WebGL view, as well as the exact width and height in pixels. I suppose there might be a mathematical way to calculate based on the target size a clip rect that works? But I’m worried that the FOV will cause issues.

I guess (thinking out loud) I could find out whether the width or height needs to be clipped (it should never be both) and adjust the FOV based on that.

Will look into that tomorrow unless someone else has some ideas!

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