Import .HDR Image at Runtime

Are there examples anywhere of creating a UTextureCube from an .hdr file at runtime?

The UTexture::Source member is editor only and cannot be used in shipping builds. I’ve got as far as using the FHDRLoadHelper and FDDSLoadHelper classes to get the raw data, but my attempts at populating a dynamically allocated UTextureCube have failed. Typically with a crash down in FTextureCubeResource::GetData as I’ve clearly set things up wrong.

I’ve been able to successfully create UTexture2Ds in a similar fashion leveraging the ImageWrapperModule.

Here is my non-working attempt…

UTextureCube* CreateTransientTextureCube(int32 InSizeX, int32 InSizeY, EPixelFormat InFormat, const FName InName = NAME_None)
{
	LLM_SCOPE(ELLMTag::Textures);

	UTextureCube* NewTexture = NULL;
	if (InSizeX > 0 && InSizeY > 0 &&
		(InSizeX % GPixelFormats[InFormat].BlockSizeX) == 0 &&
		(InSizeY % GPixelFormats[InFormat].BlockSizeY) == 0)
	{
		NewTexture = NewObject<UTextureCube>(
			GetTransientPackage(),
			InName,
			RF_Transient
			);

		NewTexture->PlatformData = new FTexturePlatformData();
		NewTexture->PlatformData->SizeX = InSizeX;
		NewTexture->PlatformData->SizeY = InSizeY;
		NewTexture->PlatformData->PixelFormat = InFormat;

		// Allocate first mipmap.
		int32 NumBlocksX = InSizeX / GPixelFormats[InFormat].BlockSizeX;
		int32 NumBlocksY = InSizeY / GPixelFormats[InFormat].BlockSizeY;
		FTexture2DMipMap* Mip = new FTexture2DMipMap();
		NewTexture->PlatformData->Mips.Add(Mip);
		Mip->SizeX = InSizeX;
		Mip->SizeY = InSizeY;
		Mip->BulkData.Lock(LOCK_READ_WRITE);
		Mip->BulkData.Realloc(NumBlocksX * NumBlocksY * GPixelFormats[InFormat].BlockBytes);
		Mip->BulkData.Unlock();
	}
	else
	{
		UE_LOG(LogTexture, Warning, TEXT("Invalid parameters specified for CreateTransientTextureCube()"));
	}
	return NewTexture;
}

UTextureCube* LoadHDR(FString FileName)
{
	TArray<uint8> Data;
	if (!FFileHelper::LoadFileToArray(Data, *FileName))
	{
		return nullptr;
	}

	FHDRLoadHelper HDRLoadHelper(Data.GetData(), Data.GetAllocatedSize());
	if (HDRLoadHelper.IsValid())
	{
		TArray<uint8> DDSFile;
		HDRLoadHelper.ExtractDDSInRGBE(DDSFile);
		FDDSLoadHelper HDRDDSLoadHelper(DDSFile.GetData(), DDSFile.Num());

		//if (HDRDDSLoadHelper.IsValidCubemapTexture())
		if (HDRDDSLoadHelper.IsValid2DTexture())
		{
			int32 Width = HDRDDSLoadHelper.DDSHeader->dwWidth;
			int32 Height = HDRDDSLoadHelper.DDSHeader->dwHeight;
			EPixelFormat PixelFormat = HDRDDSLoadHelper.ComputePixelFormat();

			if (UTextureCube* NewTexture = CreateTransientTextureCube(Width, Height, PixelFormat))
			{
				uint8* MipData = static_cast<uint8*>(NewTexture->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_WRITE));

				// Bulk data was already allocated for the correct size when we called CreateTransient above
				FMemory::Memcpy(MipData, HDRDDSLoadHelper.GetDDSDataPointer(), NewTexture->PlatformData->Mips[0].BulkData.GetBulkDataSize());

				NewTexture->PlatformData->Mips[0].BulkData.Unlock();

				NewTexture->UpdateResource();
				return NewTexture;
			}
		}
	}

	return nullptr;
}

Any help would be greatly appreciated.
-Randy

1 Like

Have you find a solution?

Unfortunately not. :frowning:

I just failed at FFileHelper::LoadFileToArray for hdr image

I used imagewrapper to get image data:

// Create ImageWrapper
	IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
	TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::HDR);
	
	/* Load texturecube */
	IsValid = false;
	UTextureCube* LoadedT2D = NULL;


	//Load From File, shall check if unit8 is useable
	TArray<uint8> RawFileData;
	//TArray64<uint8> RawFileData;

	
	if (!FFileHelper::LoadFileToArray(RawFileData, *FullFilePath))
	{
		UE_LOG(LogTemp, Warning, TEXT("Load file failed"));
		return NULL;
	}
	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	UE_LOG(LogTemp, Warning, TEXT("Load file succeed"));
	//Create T2D!
	// Check if ImageWrapper is valid and set the compressed data
	UE_LOG(LogTemp, Warning, TEXT("Ready get raw data"));
	if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()))
	{
		TArray<uint8> UncompressedBGRA;
		//TArray64<uint8> UncompressedBGRA;

		UE_LOG(LogTemp, Warning, TEXT("Getting raw data..."));
		// ERGBFormat::BGRA may have good performance
		// This if shall be test
		// The format and/or the bit depth is not supported by the HdrImageWrapper. Only the BGRE format and a bitdepth of 8 is supported

		// for test
		bool bRawdata = false;
		 //if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
		if (ImageWrapper->GetRaw(ERGBFormat::BGRE, 8, UncompressedBGRA))
		{
			 bRawdata = true;
			// Format for HDR:PF_ASTC_8x8_HDR,shall be check if this format is useable
			UE_LOG(LogTemp, Warning, TEXT("Ready create TextureCube..."));
			LoadedT2D = UTextureCube::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_ASTC_8x8_HDR);
			UE_LOG(LogTemp, Warning, TEXT("Create TextureCube"));
			//Valid?
			if (!LoadedT2D || !LoadedT2D->GetPlatformData())
			{
				UE_LOG(LogTemp, Warning, TEXT("Create texturecube failed"));
				return NULL;
			}
			//~~~~~~~~~~~~~~



			//Copy!
			void* TextureData = LoadedT2D->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
			FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num());
			// this will case crash
			LoadedT2D->GetPlatformData()->Mips[0].BulkData.Unlock();

			//Update!
			LoadedT2D->UpdateResource();
		}
		
	}

	
	return LoadedT2D;
	
	//return LoadTexture2D(FullFilePath,ImageWrapper,IsValid,Width,Height);

But the color and position of imported texturecube is not right …

I was able to come up with the following function which seems to be working

#include "UObject/UObjectGlobals.h" 
#include "Misc/FileHelper.h" 
#include "IImageWrapperModule.h" 
#include "Modules/ModuleManager.h" 
#include "Formats/HdrImageWrapper.h" 
#include "IImageWrapper.h" 
#include "Engine/TextureCube.h" 
#include "Factories/Factory.h" 
#include "Misc/SecureHash.h" 
#include "EditorFramework/AssetImportData.h" 
#include "UObject/ObjectMacros.h" 


UTextureCube* CreateTextureCube( UObject* InParent, FName Name, EObjectFlags Flags )
{
	return Cast<UTextureCube>(NewObject<UObject>(InParent, UTextureCube::StaticClass(), Name, Flags));	
}

// Filename should be absolute path 
UTextureCube* ImportHDRFile(FString Filename) {
    FString PackageName = "/Game/198L";
    FString Name = "198L";
    UPackage* Pkg = CreatePackage( *PackageName);

    EObjectFlags Flags =  RF_Public | RF_Standalone | RF_Transactional;



    TArray<uint8> Data;
    if (!FFileHelper::LoadFileToArray(Data, *Filename))
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to load file '%s' to array"), *Filename);
    }
    Data.Add(0);
    const uint8* Buffer = &Data[0];
    const uint8* BufferEnd = Buffer + Data.Num() - 1;
    const int32 Length = BufferEnd - Buffer;
    UTextureCube* TextureCube = nullptr; 

    IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
	if (ImageWrapperModule.DetectImageFormat(Buffer, Length) == EImageFormat::HDR)
	{
		FHdrImageWrapper HdrImageWrapper;
		if (HdrImageWrapper.SetCompressedFromView(TArrayView64<const uint8>(Buffer, Length)))
		{
			TArray64<uint8> UnCompressedData;
			if (HdrImageWrapper.GetRaw(ERGBFormat::BGRE, 8, UnCompressedData))
			{
				// create the cube texture
				TextureCube = CreateTextureCube(Pkg, FName(*Name), Flags);
				if ( TextureCube )
				{
					TextureCube->Source.Init(
						HdrImageWrapper.GetWidth(),
						HdrImageWrapper.GetHeight(),
						/*NumSlices=*/ 1,
						/*NumMips=*/ 1,
						TSF_BGRE8,
						UnCompressedData.GetData()
						);
					// the loader can suggest a compression setting
					TextureCube->CompressionSettings = TC_HDR;
					TextureCube->SRGB = false;
                }
                FMD5Hash FileHash = FMD5Hash::HashFile(*Filename);
                TextureCube->AssetImportData->Update(Filename, FileHash.IsValid() ? &FileHash : nullptr);

                TextureCube->PostEditChange();
            }
        }
    }
    return TextureCube;
}

Most of the code was borrowed from EditorFactories.cpp file in UE 5.0.3 release.

This function can even be called from blueprint function library to return a TextureCube.

1 Like

Worth pointing out this is editor code. And won’t work when packaged. So it doesn’t exactly work at runtime.

But it did work for me when running in editor.

Unfortunately, this still only works in PIE mode, not packaged

You can try my plugin @Randy_Croucher1 @Andaharoo @Interestingname
I’ve developed RuntimeImageLoader specifically to handle runtime needs. It can load basically any popular image format as well as .HDR images (cubemaps):

Feel free to share your feedback.