언리얼에는 Static Material과 Dynamic Material이 존재합니다.
Static을 사용할 경우 내부의 Paramater를 조정할 수 없습니다(최적화를 위해서). 그렇기 때문에 해당 포스팅에서 다루려는 동적인 Texture 변환을 위해서는 Dynamic Material을 사용해야합니다.
이는 Texture 또한 마찬가지이고, 여기서 다루려는 방법은 캔버스에 그림을 그리는 것과 같은 방식으로 동작합니다. 따라서 포스팅에서는 Dynamic Texture를 사용할 것입니다.
(반대로 이미 준비된 텍스쳐를 돌아가면서 사용할 것이라면 Static Texture를 사용하면 됩니다.)

위 블루프린트는 같은 프로젝트를 진행하는 팀원이 작성한 방법인데, 몇 가지 살펴봐야할 부분이 있어서 첨부하였습니다.
'Create Render Target'을 Export할 것이고, 이를 Load해서 활용할 예정이기 때문에 여기서 사용한 Width, Height, Format을 맞춰야합니다.

위 블루프린트는 BP_Character의 'Render Target'을 Export하는 부분입니다. Character에 BP_Character의 Material을 넣어주는 부분은 구현한 방법에서는 불필요합니다. 다만 팀원이 작업한 부분이기에 지우지 않고 남겨두었습니다.
이제 본격적으로 관련 코드를 살펴보겠습니다.
void UKismetRenderingLibrary::ExportRenderTarget(UObject* WorldContextObject, UTextureRenderTarget2D* TextureRenderTarget, const FString& FilePath, const FString& FileName)
{
FString TotalFileName = FPaths::Combine(*FilePath, *FileName);
FText PathError;
FPaths::ValidatePath(TotalFileName, &PathError);
if (!TextureRenderTarget)
{
FMessageLog("Blueprint").Warning(FText::Format(LOCTEXT("ExportRenderTarget_InvalidTextureRenderTarget", "ExportRenderTarget[{0}]: TextureRenderTarget must be non-null."), FText::FromString(GetPathNameSafe(WorldContextObject))));
}
else if (!TextureRenderTarget->GetResource())
{
FMessageLog("Blueprint").Warning(FText::Format(LOCTEXT("ExportRenderTarget_ReleasedTextureRenderTarget", "ExportRenderTarget[{0}]: render target has been released."), FText::FromString(GetPathNameSafe(WorldContextObject))));
}
else if (!PathError.IsEmpty())
{
FMessageLog("Blueprint").Warning(FText::Format(LOCTEXT("ExportRenderTarget_InvalidFilePath", "ExportRenderTarget[{0}]: Invalid file path provided: '{1}'"), FText::FromString(GetPathNameSafe(WorldContextObject)), PathError));
}
else if (FileName.IsEmpty())
{
FMessageLog("Blueprint").Warning(FText::Format(LOCTEXT("ExportRenderTarget_InvalidFileName", "ExportRenderTarget[{0}]: FileName must be non-empty."), FText::FromString(GetPathNameSafe(WorldContextObject))));
}
else
{
FArchive* Ar = IFileManager::Get().CreateFileWriter(*TotalFileName);
if (Ar)
{
FBufferArchive Buffer;
bool bSuccess = false;
if (TextureRenderTarget->RenderTargetFormat == RTF_RGBA16f)
{
// Note == is case insensitive
if (FPaths::GetExtension(TotalFileName) == TEXT("HDR"))
{
bSuccess = FImageUtils::ExportRenderTarget2DAsHDR(TextureRenderTarget, Buffer);
}
else
{
bSuccess = FImageUtils::ExportRenderTarget2DAsEXR(TextureRenderTarget, Buffer);
}
}
else
{
bSuccess = FImageUtils::ExportRenderTarget2DAsPNG(TextureRenderTarget, Buffer);
}
if (bSuccess)
{
Ar->Serialize(const_cast<uint8*>(Buffer.GetData()), Buffer.Num());
}
delete Ar;
}
else
{
FMessageLog("Blueprint").Warning(LOCTEXT("ExportRenderTarget_FileWriterFailedToCreate", "ExportRenderTarget: FileWrite failed to create."));
}
}
}
블루프린트에서 사용한 Render Target을 Export할 때, 사용된 코드입니다.
첫번째 블루프린트 이미지를 확인해보면 Format이 'RTF_RGBA16f'인 것을 확인할 수 있습니다. 또한 확장자를 HDR로 설정하지 않았으므로 'ExportRenderTarget2DAsEXR' 메소드를 실행하게 됩니다.
중요한 점은 원한다면 RenderTarget의 Format을 변경해서 HDR, EXR, PNG 중, 원하는 방식으로 Export 할 수 있다는 것입니다.
Export형식을 안다는건 굉장히 중요하므로 마지막으로 강조하고 넘어가겠습니다.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "RHIFwd.h"
#include "DynamicTextureComponent.generated.h"
UCLASS()
class UDynamicTextureComponent : public UActorComponent
{
GENERATED_BODY()
private:
uint8* TextureData;
UPROPERTY()
UTexture2D* DynamicTexture;
FUpdateTextureRegion2D* TextureRegion;
public:
UTexture2D* GetDynamicTexture() const { return DynamicTexture; }
void InitializeTexture();
void FillTexture(FLinearColor Color);
void UpdateTexture(UTexture2D* inTexture, bool bFreeData = false);
bool LoadTextureFromFile(const FString& FilePath);
~UDynamicTextureComponent();
UPROPERTY(EditDefaultsOnly)
int32 TextureWidth = 1024;
UPROPERTY(EditDefaultsOnly)
int32 TextureHeight = 1024;
};
글이 너무 길어질 수 있으므로 'InitializeTexture', 'LoadTextureFromFile' 두 가지 메소드를 다룰 예정입니다.
void UDynamicTextureComponent::InitializeTexture()
{
uint32 TotalPixels = TextureWidth * TextureHeight;
uint32 TextureDataSize = TotalPixels * 4 * sizeof(float);
TextureData = new uint8[TextureDataSize];
DynamicTexture = UTexture2D::CreateTransient(TextureWidth, TextureHeight, PF_A32B32G32R32F);
if (!DynamicTexture)
{
return;
}
DynamicTexture->CompressionSettings = TC_HDR;
DynamicTexture->SRGB = 0;
DynamicTexture->Filter = TF_Nearest;
DynamicTexture->AddToRoot();
DynamicTexture->UpdateResource();
TextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureWidth, TextureHeight);
if (!TextureRegion)
{
return;
}
FillTexture(FLinearColor::Red);
FlushRenderingCommands();
UpdateTexture(DynamicTexture);
}
'CreateTransient'메소드를 사용해서 텍스쳐를 생성하는데, PF_A32B32G32R32F 형식을 사용하여 생성하였습니다.
커다란 프로젝트를 진행하는게 아니라서 Texture 크기에 대한 부담이 없어 각 RGBA에 'full float'의 크기를 할당하도록 설정했습니다.
'FUpdateTextureRegion2D'는 단순하게 동적으로 갱신할 Texture 영역에 대한 범위를 의미합니다.
왼쪽 위인 (0, 0)으로 시작하여 오른쪽 아래인 (TextureWidth, TextureHeight)까지 모든 범위를 다루게 됩니다. 최적화를 다루게 된다면 유심히 봐야할 수 있으나, 당장 확인할 필요는 없습니다.
초기화 단계이다 보니, 주로 필요한 객체를 생성하는데 초점이 맞춰진 메소드입니다.
bool UDynamicTextureComponent::LoadTextureFromFile(const FString& FilePath)
{
TArray<uint8> FileData;
if (!FFileHelper::LoadFileToArray(FileData, *FilePath))
{
FNetLogger::EditerLog(FColor::Red, TEXT("Failed to load file: %s"), *FilePath);
return false;
}
IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::EXR);
if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(FileData.GetData(), FileData.Num()))
{
TArray<uint8> RawData;
if (ImageWrapper->GetRaw(ERGBFormat::RGBAF, 32, RawData))
{
int32 ImageWidth = ImageWrapper->GetWidth();
int32 ImageHeight = ImageWrapper->GetHeight();
if (ImageWidth != TextureWidth || ImageHeight != TextureHeight)
{
FNetLogger::EditerLog(FColor::Red, TEXT("Texture size mismatch: %s"), *FilePath);
return false;
}
const uint32 TextureTotalPixels = TextureWidth * TextureHeight;
FMemory::Memcpy(TextureData, RawData.GetData(), TextureTotalPixels * 4 * 4);
UpdateTexture(DynamicTexture);
return true;
}
}
FNetLogger::EditerLog(FColor::Red, TEXT("Failed 2 load image: %s"), *FilePath);
return false;
}
파일을 로드하는 형식은 이전에 언급한 것과 같이 'EXR'형식으로 설정하여 가져왔습니다. 이전에 형식이 중요하다고 강조한 이유가 Export한 형식을 알지 못한다면 Load할 때, 문제가 생기기 때문입니다.
이후에 'GetRaw' 메소드를 호출하여 'RGBAF' 형식의 32bit 형식으로 가져오는데, 초기화 단계에서 설정한 Texture코드와 맞춰서 설정한 것입니다.
Memcpy 부분에서 'TextureTotalPixels * 4 * 4'로 Count를 설정해준 이유는 4byte(32bit)를 RGBA 4개로 각 픽셀이 가지고 있기 때문에 그렇게 설정했습니다.