[UE5]指定したモデルを超高解像度透過pngで出力する実装
結論::解像度はPCの性能とご相談を!!
(zenn上だと透過pngにならないかも)

クレジット
以下は今回画像に使用しているモデル関係のクレジットです。
©MyMe.VR様
さばさばショップ様
やりたいこと
指定したアセット、今回の場合はvrmモデルの透過立ち絵pngがほしい!!
しかし通常のスクリーンショットでやると
①拡大すると粗が目立つ
②透過作業がめんどくさい
この2つを自作することで解決します。
前提
UE5.6.1
C++必須
ある程度のPC(後述しますが当然解像度を高くするほどPC性能は必要です)
参考までに手元のパソコンの性能です。
ここまでの高性能でなくても全然動きます。
AMD Ryzen 7 7800X3D 8-Core Processor
RAM 32.0 GB
NVIDIA GeForce RTX 5070 Ti 16GB
①RenderTargetの作成
RenderTarget(描画ターゲット)を2つ作ります。
ClearColorのAは0.0、SizeX,Yは出力したい解像度を指定してください。
いったん1920*1080くらいでいいと思います。
やってみてクラッシュなどが発生したら下げる、いけそうなら上げる。

これを2枚作ってください。中身は同じで問題ありません。
②C++本体(一部省略)
(身内向けに作ったので命名適当です)
.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Components/SceneCaptureComponent2D.h"
#include "VrmPngRecorder.generated.h"
UCLASS()
class プロジェクト名_API AVrmPngRecorder : public AActor
{
GENERATED_BODY()
public:
AVrmPngRecorder();
/** 録画開始->BPから呼ぶ想定 */
UFUNCTION(BlueprintCallable, Category = "VRM|Recorder")
void StartRecord(AActor* TargetActor, float DurationSeconds, const FString& OutputDir, int32 FPS);
/** 録画停止 */
UFUNCTION(BlueprintCallable, Category = "VRM|Recorder")
void StopRecord();
protected:
virtual void BeginPlay() override;
private:
void TickCapture();
/** 出力先フォルダを確実に作る */
static void EnsureDir(const FString& Dir);
/** ShowOnlyにターゲットをセット */
void SetupShowOnly(AActor* TargetActor);
/** SceneCapture設定 */
void SetupCaptures();
private:
UPROPERTY(VisibleAnywhere, Category = "VRM|Recorder")
USceneComponent* Root = nullptr;
UPROPERTY(VisibleAnywhere, Category = "VRM|Recorder")
USceneCaptureComponent2D* CaptureColor = nullptr;
UPROPERTY(VisibleAnywhere, Category = "VRM|Recorder")
USceneCaptureComponent2D* CaptureMask = nullptr;
public:
/** これをRT_Colorに指定 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
UTextureRenderTarget2D* RT_Color = nullptr;
/** これをRT_Maskに指定 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
UTextureRenderTarget2D* RT_Mask = nullptr;
/** 出力解像度(RTもこのサイズにリサイズする) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
int32 Width = 1024;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
int32 Height = 1024;
/** true推奨->RTが線形(HDR)の場合に、png用にsRGB変換する */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
bool bGammaCorrectColor = true;
/** ここを設定すると、このActorのTransformを追従させる(カメラ追従) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "VRM|Recorder")
AActor* FollowCameraActor = nullptr;
private:
UPROPERTY()
AActor* OnlyActor = nullptr;
FTimerHandle TimerHandle;
bool bRecording = false;
float EndTime = 0.0f;
int32 FrameIndex = 0;
int32 TargetFPS = 30;
FString OutputDirectory;
};
#include "VrmPngRecorder.h"
#include "TimerManager.h"
#include "Engine/World.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFileManager.h"
#include "Async/Async.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Modules/ModuleManager.h"
void AVrmPngRecorder::EnsureDir(const FString& Dir)
{
IFileManager::Get().MakeDirectory(*Dir, true);
}
AVrmPngRecorder::AVrmPngRecorder()
{
PrimaryActorTick.bCanEverTick = false;
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
RootComponent = Root;
CaptureColor = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("CaptureColor"));
CaptureColor->SetupAttachment(RootComponent);
CaptureMask = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("CaptureMask"));
CaptureMask->SetupAttachment(RootComponent);
// Timerで手動キャプチャ
CaptureColor->bCaptureEveryFrame = false;
CaptureColor->bCaptureOnMovement = false;
CaptureMask->bCaptureEveryFrame = false;
CaptureMask->bCaptureOnMovement = false;
// ShowOnlyでターゲットだけ描く
CaptureColor->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
CaptureMask->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
// TAA等の状態保持->境界のギザギザ軽減に効く
CaptureColor->bAlwaysPersistRenderingState = true;
CaptureMask->bAlwaysPersistRenderingState = true;
}
void AVrmPngRecorder::BeginPlay()
{
Super::BeginPlay();
}
void AVrmPngRecorder::SetupShowOnly(AActor* TargetActor)
{
CaptureColor->ShowOnlyActors.Empty();
CaptureMask->ShowOnlyActors.Empty();
// Actor単位でOK(子コンポーネントも基本入る)
CaptureColor->ShowOnlyActors.Add(TargetActor);
CaptureMask->ShowOnlyActors.Add(TargetActor);
}
void AVrmPngRecorder::SetupCaptures()
{
// RT割当
CaptureColor->TextureTarget = RT_Color;
CaptureMask->TextureTarget = RT_Mask;
// エディタ表示に近い最終見た目を狙う
CaptureColor->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
// AにInvOpacityが入る
// → 後で A = 1 - InvOpacity にして不透明度に戻す
CaptureMask->CaptureSource = ESceneCaptureSource::SCS_SceneColorHDR;
// 背景透過用:RTは(0,0,0,0)推奨
if (RT_Color)
{
RT_Color->ClearColor = FLinearColor(0, 0, 0, 0);
}
if (RT_Mask)
{
RT_Mask->ClearColor = FLinearColor(0, 0, 0, 0);
}
}
void AVrmPngRecorder::StartRecord(AActor* TargetActor, float DurationSeconds, const FString& OutputDir, int32 FPS)
{
if (!TargetActor || !RT_Color || !RT_Mask)
{
UE_LOG(LogTemp, Warning, TEXT("StartRecord failed: TargetActor or RTs are null."));
return;
}
OnlyActor = TargetActor;
TargetFPS = FMath::Max(1, FPS);
// 出力先:相対ならSaved/配下に落とす
OutputDirectory = OutputDir;
if (OutputDirectory.IsEmpty())
{
OutputDirectory = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("VrmFrames"));
}
else if (FPaths::IsRelative(OutputDirectory))
{
OutputDirectory = FPaths::Combine(FPaths::ProjectSavedDir(), OutputDirectory);
}
EnsureDir(OutputDirectory);
// RTサイズを揃える
RT_Color->ResizeTarget(Width, Height);
RT_Mask->ResizeTarget(Width, Height);
SetupCaptures();
SetupShowOnly(TargetActor);
FrameIndex = 0;
EndTime = GetWorld()->GetTimeSeconds() + FMath::Max(0.01f, DurationSeconds);
bRecording = true;
const float Interval = 1.0f / (float)TargetFPS;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AVrmPngRecorder::TickCapture, Interval, true, 0.0f);
}
void AVrmPngRecorder::StopRecord()
{
if (!bRecording) return;
bRecording = false;
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}
void AVrmPngRecorder::TickCapture()
{
if (!bRecording) return;
const float Now = GetWorld()->GetTimeSeconds();
if (Now >= EndTime)
{
StopRecord();
return;
}
// 撮影用カメラアクタに追従
if (FollowCameraActor)
{
SetActorTransform(FollowCameraActor->GetActorTransform());
}
// キャプチャ
CaptureColor->CaptureScene();
CaptureMask->CaptureScene();
// Readback
FTextureRenderTargetResource* ResColor = RT_Color->GameThread_GetRenderTargetResource();
FTextureRenderTargetResource* ResMask = RT_Mask->GameThread_GetRenderTargetResource();
if (!ResColor || !ResMask) return;
TArray<FColor> ColorPixels;
TArray<FColor> MaskPixels;
ColorPixels.SetNumUninitialized(Width * Height);
MaskPixels.SetNumUninitialized(Width * Height);
// 超重要:RT_Colorが線形(HDR/16f)なら、png用にsRGBへ
FReadSurfaceDataFlags ColorFlags(RCM_UNorm);
ColorFlags.SetLinearToGamma(bGammaCorrectColor);
// Maskはガンマ不要(Alphaだけ使う)
FReadSurfaceDataFlags MaskFlags(RCM_UNorm);
MaskFlags.SetLinearToGamma(false);
ResColor->ReadPixels(ColorPixels, ColorFlags);
ResMask->ReadPixels(MaskPixels, MaskFlags);
// 合成:RGB=Color、A=1-InvOpacity
TArray<uint8> BGRA;
BGRA.SetNumUninitialized(Width * Height * 4);
for (int32 i = 0; i < Width * Height; ++i)
{
const FColor& C = ColorPixels[i];
const FColor& M = MaskPixels[i];
const uint8 Opacity = 255 - M.A; // InvOpacity->Opacity
const int32 o = i * 4;
BGRA[o + 0] = C.B;
BGRA[o + 1] = C.G;
BGRA[o + 2] = C.R;
BGRA[o + 3] = Opacity;
}
const FString FileName = FString::Printf(TEXT("frame_%06d.png"), FrameIndex++);
const FString FullPath = FPaths::Combine(OutputDirectory, FileName);
// png圧縮は別スレッドへ
TArray<uint8> BGRA_Copy = MoveTemp(BGRA);
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,
[BGRA_Copy = MoveTemp(BGRA_Copy), FullPath, W = Width, H = Height]()
{
IImageWrapperModule& ImageWrapperModule =
FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> Png = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
if (Png.IsValid() && Png->SetRaw(BGRA_Copy.GetData(), BGRA_Copy.Num(), W, H, ERGBFormat::BGRA, 8))
{
const TArray64<uint8>& Compressed = Png->GetCompressed(3);
FFileHelper::SaveArrayToFile(Compressed, *FullPath);
}
});
}
Levelに配置して撮影する
今作ったC++クラスをそのままLevelに配置。
RecorderのRTを前作ったやつに設定。
WidgetやHeightは先ほどのRTと同じ値にするのを推奨。
異なる場合ほぼこちらを優先します。が乱れる原因なので同じにした方がいいと思います。
GammaはTrue推奨。
FollowCameraActorは撮影用のCameraActor(適当でいいです)を指定。
適当に入力用のPlayerを作成してさっきのクラスをGet->関数を呼んでください。

TargetはActorであればなんでも入ります。
これが透過pngになります。
Dirは保存先です。画像の書き方だとSavedの配下に作られます。
DurationSecondsは秒数。FPSは1秒当たりのフレーム数です。
ここで指定した秒数指定したFPSで連続で撮影します。
つまりコマ送りになります。
1で1なら1枚だけです。3で30なら90枚です(90枚だと重すぎる)

解説
①SceneCaptureを2つ用意する
こいつが必要です。
bCaptureEveryFrame = false;
これがないと勝手に撮っちゃうので絶対に必要です。
②StartRecordで撮影設定
まずOutputDirが相対パスならSaved/配下に入れる作りにしてます。
安全処理です。
RTのサイズをWidth/Heightに強制的に揃えます。
RT_Color->ResizeTarget(Width, Height);
RT_Mask ->ResizeTarget(Width, Height);
RTアセットの元サイズよりWidth/Heightが優先です。
取得に変えてもいいと思います。
③見た目と透明度を別々のCaptureSourceで撮る
ptureColor->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
CaptureMask ->CaptureSource = ESceneCaptureSource::SCS_SceneColorHDR; // A=InvOpacity
ここが透過pngの要です。
Color(FinalColorLDR)は最終的に画面に出る色に近い状態です。
Mask(SceneColorHDR)はAlphaチャンネルにInvOpacityを持つものです。
透明なら大きく不透明なら小さいです。
④ShowOnlyで指定したActorだけを描く
CaptureColor->PrimitiveRenderMode = PRM_UseShowOnlyList;
CaptureMask ->PrimitiveRenderMode = PRM_UseShowOnlyList;
CaptureColor->ShowOnlyActors.Add(TargetActor);
CaptureMask ->ShowOnlyActors.Add(TargetActor);
背景などを移さないためにShowOnlyリストを使います(神機能)
指定したものだけ描画する神機能です。この機能を作った人は偉大すぎます。
ありがとうございます。
⑤TimerでXfps一定間隔でTickCaptureする部分
const float Interval = 1.0f / (float)TargetFPS;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AVrmPngRecorder::TickCapture, Interval, true);
⑥2枚(RGBとA)を撮る
CaptureColor->CaptureScene();
CaptureMask ->CaptureScene();
ResColor->ReadPixels(ColorPixels, Flags);
ResMask ->ReadPixels(MaskPixels, Flags);
⑦透過の合成
const uint8 A = 255 - M.A;
M.AはInvOpacity(前述の通り)なので透過を復元して詰めなおします。
BGRA[o+0] = C.B;
BGRA[o+1] = C.G;
BGRA[o+2] = C.R;
BGRA[o+3] = A;
⑧pngとして圧縮保存
Png->SetRaw(BGRA_Copy.GetData(), BGRA_Copy.Num(), W, H, ERGBFormat::BGRA, 8);
const TArray64<uint8>& Compressed = Png->GetCompressed(3);
FFileHelper::SaveArrayToFile(Compressed, *FullPath);
おしまい
以上が超解像度スクショのやり方です。
ただ前述の通り、どこまで高解像度で連続出力できるかはお手元のPC次第です。
Discussion