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.