Kazimieras Mikelis' Game Blog

Taking Runtime Screenshots in Unreal Engine

Learn to take in-game screenshots and convert them to Texture 2Ds in Unreal Engine with C++.

Mikelis' Game Devlog

In today's refreshingly simple Devlog entry, we will be looking at taking screenshots in Unreal Engine 4. We will be taking them through C++ and storing them as a pixel color array in a container class. If we would like to take screenshots as files, many plugins can already help us out. But what if we want to measure the average color on the screen, save our screenshots in a save game object, build social features around screenshot sharing, or just have a convenient way to take a screenshot to memory? That's what we will be diving into in this Devlog post.

Design

Let's first consider how we would accomplish this task. Unreal Engine has built-in screenshot functionality, but it's not exposing its results to code and is kind of aimed at the Editor users, so that's no good for us. We could use one of the plugins above to save to a file, too, but then we'd have to load data from a file, and there are all sorts of pitfalls and slowdowns here, especially for platform-agnostic development. Another option would be to create a camera in our scene on demand, align perfectly with our player's camera, and use it to take a scene capture. A-ha! That captures the view to memory by design, so it's a viable option. Let's use that.

View Capture Actor

While creating the Actor to capture what a player sees, we might need to create a new object to store the screenshot itself after capture or expose some C++ functions as Blueprint function nodes. If you would like to brush up on memory management in the engine or exposing functions to Blueprints, check out the previous weeks' articles on Devlog before we get going. With this knowledge in our possession, let's dive straight into it.

Setting Up

Let’s create a new C++ Actor class based on AActor. If you are using the C++ Class Wizard, you will need to select Actor from the parent class list. For this Devlog entry, I will use the class name "ViewCapture". I will also remove a few overridden Actor function definitions and declarations to keep our class tidy and straightforward:

// ViewCapture.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ViewCapture.generated.h"
UCLASS()
class SANDBOX_API AViewCapture : public AActor
{
    GENERATED_BODY()

public:    
    // Sets default values for this actor's properties
    AViewCapture();
};
// ViewCapture.cpp
#include "ViewCapture.h"
// Sets default values
AViewCapture::AViewCapture()
{
    PrimaryActorTick.bCanEverTick = false;
}

Implementing Functions

We will probably need at least two functions in our actor class β€” one to align and take on the parameters of the player's camera and another function to capture the view and give it to us in a container class object. To accomplish the task of capturing the view, we will need to use a USceneCaptureComponent2D and a UTextureRenderTarget2D. So let's implement all of those in our class now –

// ViewCapture.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "ViewCapture.generated.h"
// An enumeration for execution pins in Blueprint functions
UENUM(BlueprintType)
enum EViewCaptureOutcomes
{
    Failure,
    Success
};
UCLASS()
class SANDBOX_API AViewCapture : public AActor
{
    GENERATED_BODY()

public:

    // Sets default values for this actor's properties
    AViewCapture(const FObjectInitializer& ObjectInitializer);
    UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="View Capture", ToolTip = "Align the camera of this View Capture actor with the player's camera."))
    void SetCameraToPlayerView(TEnumAsByte<EViewCaptureOutcomes> & Outcome);
    bool SetCameraToPlayerView();
    UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="View Capture", ToolTip = "Capture the player's view.\n\nResolution - a power of 2 resolution for the view capture, like 512"))
    void CapturePlayersView(TEnumAsByte<EViewCaptureOutcomes> & Outcome, int32 Resolution, TArray<FColor> & ColorData);
    bool CapturePlayersView(int32 Resolution, TArray<FColor> & ColorData);
    // The pointer to our "Camera" USceneCaptureComponent2D. 
    UPROPERTY(EditAnywhere, Transient)
    class USceneCaptureComponent2D * Camera; 
};
// ViewCapture.cpp
#include "ViewCapture.h"
// Sets default values
AViewCapture::AViewCapture(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false;
    // Create our SceneCaptureComponent2D and make it the root component of this Actor.
    Camera = ObjectInitializer.CreateDefaultSubobject<USceneCaptureComponent2D>(this, TEXT("Camera"));
    SetRootComponent(Camera);
    // Make sure we don't capture every frame for performance, and because our render target will be made to be GC'd.
    Camera->bCaptureEveryFrame = false;
}
void AViewCapture::SetCameraToPlayerView(TEnumAsByte<EViewCaptureOutcomes>& Outcome)
{
}
bool AViewCapture::SetCameraToPlayerView()
{
    return false;
}
void AViewCapture::CapturePlayersView(TEnumAsByte<EViewCaptureOutcomes>& Outcome, int32 Resolution,
    TArray<FColor>& ColorData)
{
}
bool AViewCapture::CapturePlayersView(int32 Resolution, TArray<FColor>& ColorData)
{
    return false;
}

Good practice: you might have noticed that I have provided two overloads for two functions β€” SetCameraToPlayerView and CapturePlayersView. One of these overloads is intended for C++ use, and the other one is more convenient for Blueprint function use. In this case, the implementation differences are minor, but having separate functions for Blueprint and C++ is an excellent habit to develop so that our functions can take advantage of both programming languages and be intuitive to developers on both. For example, we don't want to be dealing with the TEnumAsByte type if we have booleans in C++, but we want to convey that this function might fail as a Blueprint function through execution pins (more about this in last week's post). We must only be careful not to duplicate our code, so one of the functions will have to be a wrapper for the other.

Some more experienced programmers out there might be asking why aren't we marking our "C++" overloads as inline / FORCEINLINE. We want them to be accessible from other C++ classes, even if there is a theoretical speed penalty when using Blueprint functions.

I like to wrap my C++ functions in Blueprint functions like this:

// Excerpt from ViewCapture.cpp
bool AViewCapture::SetCameraToPlayerView()
{
    return false;
}
bool AViewCapture::CapturePlayersView(int32 Resolution, TArray<FColor>& ColorData)
{
    return false;
}
// Function wrappers for Blueprint functions.
void AViewCapture::SetCameraToPlayerView(TEnumAsByte<EViewCaptureOutcomes>& Outcome)
    { Outcome = SetCameraToPlayerView() ? Success : Failure; }
void AViewCapture::CapturePlayersView(TEnumAsByte<EViewCaptureOutcomes>& Outcome, const int32 Resolution,
    TArray<FColor>& ColorData)
    { Outcome = CapturePlayersView(Resolution, ColorData) ? Success : Failure; }

With these semantics out of the way, let's look to finishing up our function definitions.

// Excerpt from ViewCapture.cpp
bool AViewCapture::SetCameraToPlayerView()
{
    APlayerCameraManager * PlayerCamera = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0);

    const FVector CameraLocation = PlayerCamera->GetCameraLocation();
    const FRotator CameraRotation = PlayerCamera->GetCameraRotation();

    SetActorLocationAndRotation(CameraLocation, CameraRotation);
    Camera->SetWorldLocationAndRotation(CameraLocation, CameraRotation);

    Camera->FOVAngle = PlayerCamera->GetFOVAngle();

    return true;
}

Good practice: as you see, we're using assertions in this code and returning false on failure. If the code always crashes before returning false, why do we even return a boolean with this function? Well, checkf is an assertion that only runs in the development environment. It's a great habit to use many assertions in your code like that because it will help catch bugs before deploying and releasing our games. Upon release, these assertions won't be evaluated, so as a last resort fallback, we will return false and fire the Failure execution pin in our Blueprint function.

Let's define our scene capture function now.

// Excerpt from ViewCapture.cpp
bool AViewCapture::CapturePlayersView(int32 Resolution, TArray<FColor>& ColorData)
{
    // Make the resolution a power of two.
    Resolution = FGenericPlatformMath::Pow(2,FGenericPlatformMath::FloorLog2(FGenericPlatformMath::Max(Resolution, 1) * 2 - 1));

    // Move our actor and its camera component to player's camera.
    if(!SetCameraToPlayerView()) return false;

    // Create a temporary object that we will let die in GC in a moment after this scope ends.
    UTextureRenderTarget2D * TextureRenderTarget = NewObject<UTextureRenderTarget2D>();
    TextureRenderTarget->InitCustomFormat(Resolution,Resolution,PF_B8G8R8A8,false);
    // Take the capture. Camera is an instance of a USceneCaptureComponent2D.
    Camera->TextureTarget = TextureRenderTarget;
    Camera->CaptureScene();
    // Output the capture to a pixel array.
    ColorData.Empty();
    ColorData.Reserve(Resolution * Resolution);
    TextureRenderTarget->GameThread_GetRenderTargetResource()->ReadPixels(ColorData);
    ColorData.Shrink();

    return true;
}

Good practice: When dealing with unusually large container class objects, it's an excellent idea to preallocate a memory block for them. Depending on the particular container class, adding an element to the container might cause the entire container object to be copied from one block of free memory to another, longer block so that the container fits. Some container classes, like vectors in the STL, are intelligent about this and guesstimate how much memory the container will need. But some elementary container classes only get as much memory as they need to store the newly added element. When storing hundreds of thousands of pixels in a container, hundreds of memory copy operations could occur. That's why we ColorData.Reserve() an appropriately-sized block of memory free memory in advance.

And that's it. It is now up to you what you will do with the color data from a player's view capture. But for inspiration, we can explore a use case below.

Use Case: Converting RGB data to a Texture

In the following example, we are using the output from CapturePlayersView function to create a new 8-bit UTexture2D for use in anything from shading/texturing to the user interface:

TArray<FColor> RawPixels; 
if(!CapturePlayersView(512, RawPixels) || !RawPixels.Num()) return;
// Create a new texture, get a reference to its first mipmap, and calculate the required data size in bytes.
UTexture2D * Texture = UTexture2D::CreateTransient(Size, Size,PF_B8G8R8A8);
FTexture2DMipMap & Mip = Texture->PlatformData->Mips[0];
const int32 BufferSize = PixelData.Num() * 4;
// Create a data buffer where we will build our memory block for a new mip.
TArray<uint8> DataBuffer;
DataBuffer.Reserve(BufferSize);
for(int32 i = 0, Max = PixelData.Num(); i < Max; i++)
    DataBuffer.Append({ PixelData[i].B, PixelData[i].G, PixelData[i].R, 0xff });
// Memcpy operation.
void* DataBufferData = DataBuffer.GetData();
void* Data = Mip.BulkData.Lock( LOCK_READ_WRITE );
Mip.BulkData.Realloc(BufferSize);
FMemory::Memcpy( Data, DataBufferData, BufferSize);
Mip.BulkData.Unlock();
// Let the engine process new data.
Texture->UpdateResource();

Be warned, though β€” as soon as the variable Photograph is unset after the scope concludes, the texture will be Garbage-Collected. If you would like to keep the texture around, ensure it's being pointed to from a valid object. There's a whole science about this, and you can read the high-level overview of Unreal's Garbage Collection in this an earlier post. Still, the most common way of keeping an object around is decorating its pointer with the UPROPERTY() macro in a class declaration.

Here's an in-game example of storing RGB values in a save game object and displaying screenshots as UI textures in a list of saves:

Mikelis' Game Devlog

A working example of saving screenshots to a save game slot.

In the next week's blog, we will go more in-depth about saving arbitrary data like 8-bit RGB arrays in Unreal Engine's save game objects. :)

Further Learning

If you would like to explore practical examples of handling screenshots and related textures in Unreal Engine, I highly recommend reading the screenshot function definitions in Rama's Victory Blueprint Plugin's function library. Thanks for this great resource, Rama.

Conclusion

In this Devlog post, I have discussed capturing the player's view or taking a screenshot and storing it in memory as an array of pixel RGB data. We may use this to save screenshots to a save game object or use them for a vast gamut of interesting purposes in gameplay. With the technical bit out of the way, it is now up to you to get creative! I hope this write-up was a useful resource for taking screenshots and good practices in the C++ code for Unreal Engine.

Source Code

The full source code for the ViewCapture actor is below:

// ViewCapture.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "ViewCapture.generated.h"
// An enumeration for execution pins in Blueprint functions
UENUM(BlueprintType)
enum EViewCaptureOutcomes
{
    Failure,
    Success
};
UCLASS()
class SANDBOX_API AViewCapture : public AActor
{
    GENERATED_BODY()

public:

    // Sets default values for this actor's properties
    AViewCapture(const FObjectInitializer& ObjectInitializer);
    UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="View Capture", ToolTip = "Align the camera of this View Capture actor with the player's camera."))
    void SetCameraToPlayerView(TEnumAsByte<EViewCaptureOutcomes> & Outcome);
    bool SetCameraToPlayerView();
    UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "Outcome", Category="View Capture", ToolTip = "Capture the player's view.\n\nResolution - a power of 2 resolution for the view capture, like 512"))
    void CapturePlayersView(TEnumAsByte<EViewCaptureOutcomes> & Outcome, int32 Resolution, TArray<FColor> & ColorData);
    bool CapturePlayersView(int32 Resolution, TArray<FColor> & ColorData);
    // The pointer to our "Camera" USceneCaptureComponent2D. 
    UPROPERTY(EditAnywhere, Transient)
    class USceneCaptureComponent2D * Camera; 
};
// ViewCapture.cpp
#include "ViewCapture.h"
#include "Camera/CameraComponent.h"
#include "Kismet/GameplayStatics.h"
// Sets default values
AViewCapture::AViewCapture(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false;
    // Create our SceneCaptureComponent2D and make it the root component of this Actor.
    Camera = ObjectInitializer.CreateDefaultSubobject<USceneCaptureComponent2D>(this, TEXT("Camera"));
    SetRootComponent(Camera);
    // Make sure we don't capture every frame for performance, and because our render target will be made to be GC'd.
    Camera->bCaptureEveryFrame = false;
    // Set the right format to not deal with alpha issues
    Camera->CaptureSource = ESceneCaptureSource::SCS_FinalColorHDR;
}
bool AViewCapture::SetCameraToPlayerView()
{
    APlayerCameraManager * PlayerCamera = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0);

    const FVector CameraLocation = PlayerCamera->GetCameraLocation();
    const FRotator CameraRotation = PlayerCamera->GetCameraRotation();

    SetActorLocationAndRotation(CameraLocation, CameraRotation);
    Camera->SetWorldLocationAndRotation(CameraLocation, CameraRotation);

    Camera->FOVAngle = PlayerCamera->GetFOVAngle();

    return true;
}
bool AViewCapture::CapturePlayersView(int32 Resolution, TArray<FColor>& ColorData)
{
    // Make the resolution a power of two.
    Resolution = FGenericPlatformMath::Pow(2,FGenericPlatformMath::FloorLog2(FGenericPlatformMath::Max(Resolution, 1) * 2 - 1));

    // Move our actor and its camera component to player's camera.
    if(!SetCameraToPlayerView()) return false;

    // Create a temporary object that we will let die in GC in a moment after this scope ends.
    UTextureRenderTarget2D * TextureRenderTarget = NewObject<UTextureRenderTarget2D>();
    TextureRenderTarget->InitCustomFormat(Resolution,Resolution,PF_B8G8R8A8,false);
    // Take the capture.
    Camera->TextureTarget = TextureRenderTarget;
    Camera->CaptureScene();
    // Output the capture to a pixel array.
    ColorData.Empty();
    ColorData.Reserve(Resolution * Resolution);
    TextureRenderTarget->GameThread_GetRenderTargetResource()->ReadPixels(ColorData);
    ColorData.Shrink();

    return true;
}
// Function wrappers for Blueprint functions.
void AViewCapture::SetCameraToPlayerView(TEnumAsByte<EViewCaptureOutcomes>& Outcome)
    { Outcome = SetCameraToPlayerView() ? Success : Failure; }
void AViewCapture::CapturePlayersView(TEnumAsByte<EViewCaptureOutcomes>& Outcome, const int32 Resolution,
    TArray<FColor>& ColorData)
    { Outcome = CapturePlayersView(Resolution, ColorData) ? Success : Failure; }

#Engineering #Unreal Engine #Cpp #Game Development #Screenshot

- 4 toasts