본문 바로가기
Technical Art/Editor Tool

Unreal Runtime Performance Evidence Capture Deck 구현기

by jchan46 2026. 5. 31.

구름 렌더링 작업 중 반복 캡처와 성능 확인을 자동화하기

렌더링/FX 작업 중 반복 캡처와 성능 확인을 빠르게 처리하기 위한 런타임 캡처 패널


1. 작업 배경

RDG 기반 구름 렌더링을 만들면서 생각보다 많이 반복한 일이 있었다.

화면을 맞춘다.
Screen Percentage를 바꾼다.
스크린샷을 찍는다.
stat unit을 켠다.
profilegpu를 실행한다.
캡처 파일이 어떤 조건에서 찍혔는지 따로 기억하거나 메모한다.

처음에는 별일 아닌 것처럼 보였지만, 구름 렌더링처럼 파라미터와 해상도 조건에 따라 결과가 크게 달라지는 작업에서는 이 과정이 계속 발목을 잡았다.

특히 Screen Percentage 비교를 할 때는 더 그렇다.

SP50, SP75, SP100, SP150을 각각 찍어놓고도, 나중에 파일만 보면 어느 장면이 어떤 조건이었는지 헷갈린다.
카메라 위치가 같았는지, TSR 상태였는지, RHI는 무엇이었는지, 같은 Preset이었는지 확인하려면 별도 기록이 필요했다.

그래서 단순히 “스크린샷 찍기 편한 버튼”이 아니라, 렌더링 결과를 증거 자료로 남기기 위한 캡처 시스템을 만들었다.

이번 작업의 이름은 다음과 같이 정리했다.

Performance Evidence Capture Deck
 

목표는 간단하다.

렌더링/FX 결과를 캡처할 때
이미지만 저장하지 않고,
그 이미지가 어떤 렌더링 조건에서 찍혔는지 함께 기록한다.
 

2. 문제 정의

이번 작업에서 해결하려는 문제는 크게 세 가지였다.

1. 매번 스크린샷을 수동으로 찍는 과정이 번거롭다.
2. stat unit, stat gpu, profilegpu 명령을 매번 직접 입력하기 귀찮다.
3. 캡처한 이미지가 어떤 조건에서 찍힌 것인지 추적하기 어렵다.
 

특히 세 번째가 중요했다.

렌더링 작업에서 이미지는 결과지만, 그 결과가 나온 조건이 빠지면 비교 자료로 쓰기 어렵다.

예를 들어 같은 구름 이미지라도 다음 조건이 다르면 의미가 달라진다.

Screen Percentage
Viewport Resolution
Anti-Aliasing / Upscaler
RHI
Preset
Camera Transform
Capture Type
 

그래서 이번 툴은 캡처 이미지를 저장하는 것과 동시에, JSON 메타데이터를 함께 저장하도록 구성했다.


3. 해결 레이어 판단

이 기능은 Blueprint만으로도 어느 정도 만들 수 있다.

UMG 버튼을 만들고, Execute Console Command로 stat unit, profilegpu, r.ScreenPercentage를 실행하면 된다.

하지만 캡처 파일명 생성, JSON 저장, UI 숨김/복구, Clean Capture와 GPU Capture 분리까지 들어가면 Blueprint만으로는 구조가 쉽게 지저분해진다.

그래서 역할을 분리했다.

UMG Widget
- 버튼 UI
- Capture Target 선택
- Preset 선택
- Screen Percentage 선택
- Status 표시

PlayerController
- 위젯 생성
- Home 키 토글
- Capture Component 참조 전달

C++ ActorComponent
- 캡처 실행
- UI 숨김 / 복구
- 파일명 생성
- Screenshot 요청
- JSON Metadata 저장
- stat / profilegpu 명령 실행
 

이렇게 나누면 UI는 바뀌어도 캡처 로직은 유지된다.
그리고 실제 파일 저장, 메타데이터 기록, 캡처 흐름은 C++에서 한 곳에 모을 수 있다.


4. 최종 UI 구성

런타임에서 사용하는 패널 구성은 다음과 같다.

PERFORMANCE EVIDENCE CAPTURE

Status: Ready

Target
[ RDGSmoke / SkinnedDecal / NiagaraFX / OutlinePass ]

Preset
[ Low / Medium / High / Debug ]

ScreenPercentage
[ SP50 ] [ SP75 ] [ SP100 ] [ SP150 ]

[ Clean Capture ]
[ GPU Capture ]
[ Perf Snapshot ]
[ Render Stats ]
[ Clear ]
 

각 버튼의 역할은 분리했다.

Clean Capture
- PNG 저장
- JSON Metadata 저장
- profilegpu 실행 없음
- stat overlay 실행 없음

GPU Capture
- PNG 저장
- JSON Metadata 저장
- stat unit 실행
- profilegpu 실행

Perf Snapshot
- stat unit
- stat gpu

Render Stats
- stat scenerendering
- stat rhi
- stat initviews

Clear
- stat none
 

처음에는 하나의 Capture 버튼으로 충분할 것 같았지만, 실제로 써보니 Clean Capture와 GPU Capture는 목적이 달랐다.

Clean Capture는 결과 이미지와 조건 기록용이다.
GPU Capture는 성능 검증용이다.

이 둘을 분리해두면 나중에 자료를 정리할 때 훨씬 명확하다.


5. C++ Capture Settings 구조

먼저 캡처에 필요한 설정을 구조체로 정리했다.

 
UENUM(BlueprintType)
enum class EPerformanceCaptureType : uint8
{
    Clean       UMETA(DisplayName="Clean Screenshot"),
    GPU         UMETA(DisplayName="Screenshot + GPU Capture"),
    RenderStats UMETA(DisplayName="Screenshot + Render Stats"),
    FXStats     UMETA(DisplayName="Screenshot + FX Stats")
};

USTRUCT(BlueprintType)
struct FPerformanceCaptureSettings
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    FString CaptureTarget = TEXT("RDGSmoke");

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    FString PresetName = TEXT("High");

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    EPerformanceCaptureType CaptureType = EPerformanceCaptureType::GPU;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    float ManualScreenPercentage = 100.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool bTakeScreenshot = true;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool bWriteMetadataFile = true;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool bHideRegisteredWidget = true;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool bShowUIInScreenshot = false;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Timing")
    float DelayAfterHide = 0.15f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Timing")
    float DelayAfterScreenshot = 0.10f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Timing")
    float DelayAfterStatUnit = 0.05f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Timing")
    float DelayBeforeRestoreUI = 0.50f;
};
 

Blueprint에서는 이 구조체를 만들어 C++ Component의 CaptureEvidence() 함수에 넘긴다.

이 구조체를 둔 이유는 간단하다.

버튼마다 직접 문자열과 옵션을 흩뿌리는 대신,
캡처 한 번에 필요한 조건을 하나의 Settings로 묶기 위해서다.
 

Clean Capture든 GPU Capture든 같은 함수로 처리하되, CaptureType만 다르게 넘긴다.


6. C++ Component 기본 구조

캡처 시스템은 ActorComponent로 만들었다.

 
UCLASS(ClassGroup=(Debug), meta=(BlueprintSpawnableComponent))
class PROJECTCOMMIT_API UPerformanceEvidenceCaptureComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UPerformanceEvidenceCaptureComponent();

    UFUNCTION(BlueprintCallable, Category="Performance Evidence Capture")
    void CaptureEvidence(const FPerformanceCaptureSettings& Settings);

    UFUNCTION(BlueprintCallable, Category="Performance Evidence Capture")
    void ClearStats();

    UFUNCTION(BlueprintCallable, Category="Performance Evidence Capture")
    void SetWidgetToHide(UUserWidget* InWidget);

    UFUNCTION(BlueprintCallable, Category="Performance Evidence Capture")
    FString GetLastScreenshotPath() const { return LastScreenshotPath; }

    UFUNCTION(BlueprintCallable, Category="Performance Evidence Capture")
    FString GetLastMetadataPath() const { return LastMetadataPath; }

private:
    UPROPERTY()
    TObjectPtr<UUserWidget> WidgetToHide;

    FPerformanceCaptureSettings ActiveSettings;

    FTimerHandle TimerHandle_RequestScreenshot;
    FTimerHandle TimerHandle_AfterScreenshot;
    FTimerHandle TimerHandle_AfterStatUnit;
    FTimerHandle TimerHandle_Finish;

    FString LastBaseFileName;
    FString LastScreenshotPath;
    FString LastMetadataPath;

    ESlateVisibility CachedWidgetVisibility = ESlateVisibility::Visible;
    bool bIsCapturing = false;

private:
    void Step_RequestScreenshot();
    void Step_RunCommands();
    void Step_RunProfileGPU();
    void Step_FinishCapture();

    FString BuildBaseFileName() const;
    FString BuildCaptureDirectory() const;
    FString BuildMetadataText() const;

    FString GetCleanMapName() const;
    FString GetViewportResolutionString() const;
    FString GetAntiAliasingName() const;
    FString GetRHINameSafe() const;

    float GetCVarFloatSafe(const TCHAR* Name, float Fallback) const;
    int32 GetCVarIntSafe(const TCHAR* Name, int32 Fallback) const;

    FString SanitizeToken(const FString& In) const;
    void ExecuteConsoleCommandSafe(const FString& Command) const;
    APlayerController* ResolvePlayerController() const;
};
 

캡처를 한 번의 긴 함수로 처리하지 않고, Timer 기반 단계로 나눈 이유가 있다.

UI 숨김
→ 약간 대기
→ Screenshot 요청
→ Metadata 저장
→ stat/profilegpu 실행
→ UI 복구
 

이 순서가 한 프레임 안에서 바로 처리되면, UI가 스크린샷에 섞이거나 캡처 타이밍이 어긋날 수 있다.
그래서 짧은 Delay를 두고 순차적으로 처리했다.


7. CaptureEvidence 실행 흐름

핵심 함수는 CaptureEvidence()다.

 
void UPerformanceEvidenceCaptureComponent::CaptureEvidence(const FPerformanceCaptureSettings& Settings)
{
#if UE_BUILD_SHIPPING
    return;
#else
    if (bIsCapturing)
    {
        UE_LOG(LogTemp, Warning, TEXT("[PerfEvidence] Capture already running."));
        return;
    }

    UWorld* World = GetWorld();
    if (!World)
    {
        UE_LOG(LogTemp, Warning, TEXT("[PerfEvidence] No valid world."));
        return;
    }

    bIsCapturing = true;
    ActiveSettings = Settings;

    const FString CaptureDir = BuildCaptureDirectory();
    IFileManager::Get().MakeDirectory(*CaptureDir, true);

    LastBaseFileName = BuildBaseFileName();
    LastScreenshotPath = FPaths::Combine(CaptureDir, LastBaseFileName + TEXT(".png"));
    LastMetadataPath = FPaths::Combine(CaptureDir, LastBaseFileName + TEXT(".json"));

    if (ActiveSettings.bHideRegisteredWidget && WidgetToHide)
    {
        CachedWidgetVisibility = WidgetToHide->GetVisibility();
        WidgetToHide->SetVisibility(ESlateVisibility::Hidden);
    }

    UE_LOG(LogTemp, Display, TEXT("[PerfEvidence] Capture Start: %s"), *LastBaseFileName);

    World->GetTimerManager().SetTimer(
        TimerHandle_RequestScreenshot,
        this,
        &UPerformanceEvidenceCaptureComponent::Step_RequestScreenshot,
        FMath::Max(0.0f, ActiveSettings.DelayAfterHide),
        false
    );
#endif
}
 

여기서 중요한 부분은 세 가지다.

1. 캡처 중복 실행 방지
2. UI 숨김 후 Delay
3. 파일명과 저장 경로를 C++에서 생성
 

스크린샷은 프로젝트의 Saved/PerformanceCaptures 폴더에 저장한다.

 
FString UPerformanceEvidenceCaptureComponent::BuildCaptureDirectory() const
{
    return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PerformanceCaptures"));
}
 

8. Screenshot + Metadata 저장

스크린샷 요청과 메타데이터 저장은 같은 단계에서 처리한다.

 
void UPerformanceEvidenceCaptureComponent::Step_RequestScreenshot()
{
#if !UE_BUILD_SHIPPING
    if (ActiveSettings.bTakeScreenshot)
    {
        FScreenshotRequest::RequestScreenshot(
            LastScreenshotPath,
            ActiveSettings.bShowUIInScreenshot,
            false
        );

        UE_LOG(LogTemp, Display, TEXT("[PerfEvidence] Screenshot Requested: %s"), *LastScreenshotPath);
    }

    if (ActiveSettings.bWriteMetadataFile)
    {
        const FString Metadata = BuildMetadataText();
        FFileHelper::SaveStringToFile(Metadata, *LastMetadataPath);
        UE_LOG(LogTemp, Display, TEXT("[PerfEvidence] Metadata Saved: %s"), *LastMetadataPath);
    }

    if (UWorld* World = GetWorld())
    {
        World->GetTimerManager().SetTimer(
            TimerHandle_AfterScreenshot,
            this,
            &UPerformanceEvidenceCaptureComponent::Step_RunCommands,
            FMath::Max(0.0f, ActiveSettings.DelayAfterScreenshot),
            false
        );
    }
#endif
}
 

이 단계에서는 FScreenshotRequest::RequestScreenshot()를 사용해 파일명을 직접 지정한다.
Blueprint의 단순 Shot 명령보다 파일명 관리가 명확하다.


9. Capture Type에 따른 명령 실행

캡처 타입에 따라 이후 실행 명령을 분리했다.

 
void UPerformanceEvidenceCaptureComponent::Step_RunCommands()
{
#if !UE_BUILD_SHIPPING
    switch (ActiveSettings.CaptureType)
    {
    case EPerformanceCaptureType::Clean:
        break;

    case EPerformanceCaptureType::GPU:
        ExecuteConsoleCommandSafe(TEXT("stat unit"));

        if (UWorld* World = GetWorld())
        {
            World->GetTimerManager().SetTimer(
                TimerHandle_AfterStatUnit,
                this,
                &UPerformanceEvidenceCaptureComponent::Step_RunProfileGPU,
                FMath::Max(0.0f, ActiveSettings.DelayAfterStatUnit),
                false
            );
            return;
        }
        break;

    case EPerformanceCaptureType::RenderStats:
        ExecuteConsoleCommandSafe(TEXT("stat unit"));
        ExecuteConsoleCommandSafe(TEXT("stat scenerendering"));
        ExecuteConsoleCommandSafe(TEXT("stat rhi"));
        ExecuteConsoleCommandSafe(TEXT("stat initviews"));
        break;

    case EPerformanceCaptureType::FXStats:
        ExecuteConsoleCommandSafe(TEXT("stat unit"));
        ExecuteConsoleCommandSafe(TEXT("stat niagara"));
        ExecuteConsoleCommandSafe(TEXT("stat particles"));
        break;
    }

    Step_FinishCapture();
#endif
}
 

GPU Capture일 때만 profilegpu를 실행한다.

 
void UPerformanceEvidenceCaptureComponent::Step_RunProfileGPU()
{
#if !UE_BUILD_SHIPPING
    ExecuteConsoleCommandSafe(TEXT("profilegpu"));

    if (UWorld* World = GetWorld())
    {
        World->GetTimerManager().SetTimer(
            TimerHandle_Finish,
            this,
            &UPerformanceEvidenceCaptureComponent::Step_FinishCapture,
            FMath::Max(0.0f, ActiveSettings.DelayBeforeRestoreUI),
            false
        );
    }
#endif
}
 

마지막에는 UI를 복구한다.

 
void UPerformanceEvidenceCaptureComponent::Step_FinishCapture()
{
#if !UE_BUILD_SHIPPING
    if (ActiveSettings.bHideRegisteredWidget && WidgetToHide)
    {
        WidgetToHide->SetVisibility(CachedWidgetVisibility);
    }

    bIsCapturing = false;

    UE_LOG(LogTemp, Display, TEXT("[PerfEvidence] Capture Finished."));
#endif
}
 

10. 파일명 생성 규칙

파일명에는 너무 많은 정보를 넣지 않았다.
대신 핵심 조건만 짧게 넣었다.

PCap_DateTime_Map_Target_Preset_SP_AA_Resolution_Type.png
 

실제 예시는 다음과 같다.

PCap_20260531_140457_NewMap_RDGSmoke_High_SP50_TSR_1615x880_GPU.png
 

파일명 생성 코드는 다음과 같다.

 
FString UPerformanceEvidenceCaptureComponent::BuildBaseFileName() const
{
    const FDateTime Now = FDateTime::Now();

    const FString DateToken = Now.ToString(TEXT("%Y%m%d_%H%M%S"));
    const FString MapToken = SanitizeToken(GetCleanMapName());
    const FString TargetToken = SanitizeToken(ActiveSettings.CaptureTarget);
    const FString PresetToken = SanitizeToken(ActiveSettings.PresetName);

    const float RawSP = GetCVarFloatSafe(TEXT("r.ScreenPercentage"), -1.0f);
    const float EffectiveSP = RawSP > 0.0f ? RawSP : ActiveSettings.ManualScreenPercentage;

    const FString SPToken = EffectiveSP > 0.0f
        ? FString::Printf(TEXT("SP%.0f"), EffectiveSP)
        : TEXT("SPAuto");

    const FString AAToken = SanitizeToken(GetAntiAliasingName());
    const FString ResToken = SanitizeToken(GetViewportResolutionString());

    FString TypeToken = TEXT("Clean");

    switch (ActiveSettings.CaptureType)
    {
    case EPerformanceCaptureType::Clean:
        TypeToken = TEXT("Clean");
        break;
    case EPerformanceCaptureType::GPU:
        TypeToken = TEXT("GPU");
        break;
    case EPerformanceCaptureType::RenderStats:
        TypeToken = TEXT("RenderStats");
        break;
    case EPerformanceCaptureType::FXStats:
        TypeToken = TEXT("FXStats");
        break;
    }

    return FString::Printf(
        TEXT("PCap_%s_%s_%s_%s_%s_%s_%s_%s"),
        *DateToken,
        *MapToken,
        *TargetToken,
        *PresetToken,
        *SPToken,
        *AAToken,
        *ResToken,
        *TypeToken
    );
}
 

파일명에는 핵심 정보만 넣고, 상세 조건은 JSON에 저장한다.
이렇게 해야 파일명이 지나치게 길어지지 않는다.


11. Metadata JSON 저장

JSON에는 캡처 당시의 조건을 기록한다.

 
FString UPerformanceEvidenceCaptureComponent::BuildMetadataText() const
{
    const float ScreenPercentage = GetCVarFloatSafe(TEXT("r.ScreenPercentage"), -1.0f);

    const float EffectiveScreenPercentage =
        ScreenPercentage > 0.0f
            ? ScreenPercentage
            : ActiveSettings.ManualScreenPercentage;

    const FString ScreenPercentageSource =
        ScreenPercentage > 0.0f
            ? TEXT("CVar")
            : TEXT("Manual");

    const float SecondarySP = GetCVarFloatSafe(TEXT("r.SecondaryScreenPercentage.GameViewport"), -1.0f);
    const int32 DynamicResMode = GetCVarIntSafe(TEXT("r.DynamicRes.OperationMode"), -1);
    const int32 AAMethod = GetCVarIntSafe(TEXT("r.AntiAliasingMethod"), -1);

    FString Text;

    Text += TEXT("{\n");
    Text += FString::Printf(TEXT("  \"CaptureFile\": \"%s.png\",\n"), *LastBaseFileName);
    Text += FString::Printf(TEXT("  \"CaptureTime\": \"%s\",\n"), *FDateTime::Now().ToString(TEXT("%Y-%m-%d %H:%M:%S")));
    Text += FString::Printf(TEXT("  \"MapName\": \"%s\",\n"), *GetCleanMapName());
    Text += FString::Printf(TEXT("  \"CaptureTarget\": \"%s\",\n"), *ActiveSettings.CaptureTarget);
    Text += FString::Printf(TEXT("  \"PresetName\": \"%s\",\n"), *ActiveSettings.PresetName);
    Text += FString::Printf(TEXT("  \"CaptureType\": \"%s\",\n"), *SanitizeToken(StaticEnum<EPerformanceCaptureType>()->GetNameStringByValue((int64)ActiveSettings.CaptureType)));
    Text += FString::Printf(TEXT("  \"ViewportResolution\": \"%s\",\n"), *GetViewportResolutionString());
    Text += FString::Printf(TEXT("  \"ScreenPercentageCVar\": %.2f,\n"), ScreenPercentage);
    Text += FString::Printf(TEXT("  \"ManualScreenPercentage\": %.2f,\n"), ActiveSettings.ManualScreenPercentage);
    Text += FString::Printf(TEXT("  \"EffectiveScreenPercentage\": %.2f,\n"), EffectiveScreenPercentage);
    Text += FString::Printf(TEXT("  \"ScreenPercentageSource\": \"%s\",\n"), *ScreenPercentageSource);
    Text += FString::Printf(TEXT("  \"SecondaryScreenPercentageGameViewport\": %.2f,\n"), SecondarySP);
    Text += FString::Printf(TEXT("  \"DynamicResolutionMode\": %d,\n"), DynamicResMode);
    Text += FString::Printf(TEXT("  \"AntiAliasingMethodRaw\": %d,\n"), AAMethod);
    Text += FString::Printf(TEXT("  \"AntiAliasingName\": \"%s\",\n"), *GetAntiAliasingName());
    Text += FString::Printf(TEXT("  \"RHI\": \"%s\",\n"), *GetRHINameSafe());

    if (const APlayerController* PC = ResolvePlayerController())
    {
        if (const AActor* ViewTarget = PC->GetViewTarget())
        {
            Text += FString::Printf(TEXT("  \"CameraLocation\": \"%s\",\n"), *ViewTarget->GetActorLocation().ToString());
            Text += FString::Printf(TEXT("  \"CameraRotation\": \"%s\"\n"), *ViewTarget->GetActorRotation().ToString());
        }
        else
        {
            Text += TEXT("  \"CameraLocation\": \"None\",\n");
            Text += TEXT("  \"CameraRotation\": \"None\"\n");
        }
    }
    else
    {
        Text += TEXT("  \"CameraLocation\": \"None\",\n");
        Text += TEXT("  \"CameraRotation\": \"None\"\n");
    }

    Text += TEXT("}\n");

    return Text;
}
 

EffectiveScreenPercentage와 ScreenPercentageSource를 따로 둔 이유는 Unreal에서 r.ScreenPercentage가 자동값이나 0으로 처리되는 경우가 있기 때문이다.

실제 비교 기준으로 사용할 값을 별도로 기록해두면, 나중에 캡처 조건을 해석할 때 혼동이 줄어든다.


12. Screen Percentage 버튼

Screen Percentage는 UI 버튼으로 제어했다.

SP50
SP75
SP100
SP150
 

버튼은 Blueprint에서 ApplyScreenPercentage() 함수를 호출한다.

Btn_SP50  → ApplyScreenPercentage(50)
Btn_SP75  → ApplyScreenPercentage(75)
Btn_SP100 → ApplyScreenPercentage(100)
Btn_SP150 → ApplyScreenPercentage(150)
 

Blueprint 함수 내부는 다음 흐름이다.

Set CurrentScreenPercentage
→ Execute Console Command "r.ScreenPercentage {Value}"
→ Status Text 업데이트
 

중요한 점은 콘솔 명령만 실행하는 것이 아니라, 내부 변수 CurrentScreenPercentage도 함께 갱신한다는 점이다.

그래야 이후 캡처할 때 파일명과 JSON에도 같은 값이 반영된다.


13. UMG에서 Capture Settings 생성

UMG에서는 버튼 클릭 시 FPerformanceCaptureSettings를 생성해 C++ Component에 전달한다.

CurrentCaptureTarget      → CaptureTarget
CurrentPresetName         → PresetName
CurrentScreenPercentage   → ManualScreenPercentage

Clean Capture와 GPU Capture는 같은 함수를 사용하지만 Capture Type만 다르다.

Clean Capture
→ CaptureType = Clean

GPU Capture
→ CaptureType = GPU
 

14. 실제 캡처 결과

이번 테스트에서는 같은 카메라 기준으로 SP150, SP100, SP75, SP50 캡처를 생성했다.

RDGSmoke / High / SP150 / TSR / GPU Capture
Sp50
RDGSmoke / High / SP100 / TSR / GPU Capture
Sp75
RDGSmoke / High / SP75 / TSR / GPU Capture
Sp100


15. 저장 파일 구조

캡처 결과는 Saved/PerformanceCaptures 폴더에 저장된다.

예시 파일명은 다음과 같다.

PCap_20260531_140457_NewMap_RDGSmoke_High_SP50_TSR_1615x880_GPU.png
PCap_20260531_140457_NewMap_RDGSmoke_High_SP50_TSR_1615x880_GPU.json
 

이 구조를 사용하면 파일명만 봐도 대략적인 조건을 알 수 있다.

Map        = NewMap
Target     = RDGSmoke
Preset     = High
SP         = 50
AA         = TSR
Resolution = 1615x880
Type       = GPU
 

16. JSON 메타데이터 예시

예시 JSON은 다음과 같다.

 
{
  "CaptureFile": "PCap_20260531_140457_NewMap_RDGSmoke_High_SP50_TSR_1615x880_GPU.png",
  "CaptureTime": "2026-05-31 14:04:57",
  "MapName": "NewMap",
  "CaptureTarget": "RDGSmoke",
  "PresetName": "High",
  "CaptureType": "GPU",
  "ViewportResolution": "1615x880",
  "ScreenPercentageCVar": 50.00,
  "ManualScreenPercentage": 50.00,
  "EffectiveScreenPercentage": 50.00,
  "ScreenPercentageSource": "CVar",
  "SecondaryScreenPercentageGameViewport": 0.00,
  "DynamicResolutionMode": 0,
  "AntiAliasingMethodRaw": 4,
  "AntiAliasingName": "TSR",
  "RHI": "D3D12",
  "CameraLocation": "X=-1145.459 Y=-10303.927 Z=92.150",
  "CameraRotation": "P=0.000000 Y=53.902855 R=0.000000"
}
 

이 정보 덕분에 나중에 이미지만 남아도 어떤 조건에서 나온 결과인지 다시 확인할 수 있다.


17. 결과

이번 작업으로 다음이 가능해졌다.

반복 스크린샷 작업 자동화
Clean Capture / GPU Capture 분리
Screen Percentage별 비교 캡처
PNG + JSON 동시 저장
Capture Target / Preset 기록
Camera Transform 기록
AA / RHI / Viewport Resolution 기록
stat / profilegpu 명령 버튼화
 

작업 전에는 구름 렌더링을 조정할 때마다 같은 과정을 반복해야 했다.

이제는 런타임 패널에서 Target, Preset, Screen Percentage를 선택하고 버튼만 누르면 된다.
결과 이미지와 캡처 조건은 자동으로 같은 이름의 PNG / JSON 파일로 저장된다.


18. 한계와 다음 개선 방향

현재 버전은 1차 구현이다.

아직 다음 기능은 들어가 있지 않다.

Clean Capture 시 월드 Debug Bounds 자동 숨김
Camera Bookmark 저장/복원
SP50~SP150 자동 일괄 캡처
Capture Result Browser
GPU ms / Draw Call 자동 기록
Unreal Insights Trace 자동 시작/종료
 

특히 다음에 추가하고 싶은 기능은 Capture SP Set이다.

현재는 SP50, SP75, SP100, SP150을 수동으로 눌러가며 캡처한다.
다음 단계에서는 같은 카메라 위치에서 네 가지 SP 조건을 자동으로 연속 캡처하는 구조로 확장할 수 있다.

Save Camera View
→ Capture SP50
→ Capture SP75
→ Capture SP100
→ Capture SP150
→ Restore Camera View
 

이 기능까지 들어가면 Screen Percentage 비교용 Evidence Pack을 한 번에 생성할 수 있다.