[4.20] Volume Texture not being persistent between engine launches (+ bad thumbnail on it)

So, I use my own way of loading a RAW file into a volume texture.

I take an existing texture, delete all it’s mips, add a new single mip and modify PlatformData so that when I call UpdateResource(), the RHI just takes my bulk data with parameters and creates the resource. I don’t get any errors and I can use my new Volume Texture in a material and it works.

There are two things that happen now and I don’t like them.

a) It’s not persistent between launches. Even if I explicitly press the “Save” button, the next time I fire up my editor, the texture is a 0x0x0, 0 byte texture. From the code and editor I can see that the way we’re expected to make Volume Textures is to import them from Pseudo-Volume slices, but I need to directly load RAW data.

b) The thumbnail in the editor is still showing the previous dimensions. You can see that in the screenshot (but that is just something I noticed and isn’t bothering me that much, unless it has a connection to why it’s not getting properly saved).

I’m hoping I’m just doing something wrong and this isn’t actually a bug…

You can pretty much skip the first half of the following code, as that is handling the opening of the file and checking sizes.

/** Loads a RAW 3D volume (G8 format) into the provided Volume Texture asset. Will output error log messages and return if unsuccessful */
void URaymarchBlueprintLibrary::LoadRawVolumeIntoVolumeTexture(
	const UObject* WorldContextObject,
	FString textureName, int xDim, int yDim, int zDim, UVolumeTexture* inTexture, UVolumeTexture*& outTexture) {

	FIntVector Size(xDim, yDim, zDim);
	const int TotalSize = xDim * yDim * yDim;

	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

	FString RelativePath = FPaths::ProjectContentDir();
	FString FullPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativePath) + textureName;

	IFileHandle* FileHandle = PlatformFile.OpenRead(*FullPath);

	if (!FileHandle) {
		// Or not.
		MY_LOG("File could not be opened.");
		return;
	}
	MY_LOG("RAW file was opened!")

	if (FileHandle->Size() < TotalSize) {
		MY_LOG("File is smaller than expected, cannot read volume.")
		return;
	} else if (FileHandle->Size() > TotalSize) {
		MY_LOG("File is larger than expected, check your dimensions and pixel format (nonfatal, but the texture will probably be screwed up)")
	}

	// Set volume texture parameters. 
	inTexture->MipGenSettings = TMGS_NoMipmaps;
	inTexture->NeverStream = false;
	inTexture->CompressionNone = true;
	inTexture->SRGB = false;

	// Set PlatformData parameters (create PlatformData if it doesn't exist)
	if (!inTexture->PlatformData) {
		inTexture->PlatformData = new FTexturePlatformData();
	}
	inTexture->PlatformData->PixelFormat = PF_G8;
	inTexture->PlatformData->SizeX = xDim;
	inTexture->PlatformData->SizeY = yDim;
	inTexture->PlatformData->NumSlices = zDim;

	FTexture2DMipMap* mip = new FTexture2DMipMap();
	mip->SizeX = xDim;
	mip->SizeY = yDim;
	mip->SizeZ = zDim;
	mip->BulkData.Lock(LOCK_READ_WRITE);

	uint8* ByteArray = (uint8*)mip->BulkData.Realloc(TotalSize);
	FileHandle->Read(ByteArray, TotalSize);

	mip->BulkData.Unlock();

	// Close the RAW file.
	delete FileHandle;

	// Let the whole world know we were successful.
	MY_LOG("File was successfully read!");

	// If the texture already has MIPs in it, destroy and free them (Empty() calls destructors and frees space).
	if (inTexture->PlatformData->Mips.Num() != 0) {
		inTexture->PlatformData->Mips.Empty();
	}

	// Add the new MIP.
	inTexture->PlatformData->Mips.Add(mip);

	inTexture->UpdateResource();
	return;
}

After this code is run on a “TEST2” Volume Texture asset, the editor looks like this (you can see the still-bad thumbnail)

So, anybody have an idea of what I’m doing wrong in the code / should I load the texture in a different way for it to save?

Here’s a version that works as desired :slight_smile:

If you’re gonna play around with this, keep in mind I’m using G8 format, so my pixels are one byte each.

#define MY_LOG(x) if(GEngine){GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Yellow, TEXT(x));}
void URaymarchBlueprintLibrary::LoadRawVolumeIntoVolumeTextureAsset(const UObject* WorldContextObject, FString FileName, int xDim, int yDim, int zDim, FString TextureName)
    {
    	FString PackageName = TEXT("/Game/GeneratedTextures/");
    	PackageName += TextureName;
    	UPackage* Package = CreatePackage(NULL, *PackageName);
    	Package->FullyLoad();
    
    	FIntVector Size(xDim, yDim, zDim);
    	const int TotalSize = xDim * yDim * zDim;
    
    	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
    
    	FString RelativePath = FPaths::ProjectContentDir();
    	FString FullPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*RelativePath) + FileName;
    
    	IFileHandle* FileHandle = PlatformFile.OpenRead(*FullPath);
    
    	if (!FileHandle) {
    		// Or not.
    		MY_LOG("File could not be opened.");
    		return;
    	}
    	MY_LOG("RAW file was opened!")
    
    	if (FileHandle->Size() < TotalSize) {
    		MY_LOG("File is smaller than expected, cannot read volume.")
    			delete FileHandle;
    			return;
    	}
    	else if (FileHandle->Size() > TotalSize) {
    		MY_LOG("File is larger than expected, check your dimensions and pixel format (nonfatal, but the texture will probably be screwed up)")
    	}
    
    
    	UVolumeTexture* NewTexture = NewObject<UVolumeTexture>((UObject*)Package, FName(*TextureName), RF_Public | RF_Standalone | RF_MarkAsRootSet);
    
    	NewTexture->AddToRoot();				// This line prevents garbage collection of the texture
    	NewTexture->PlatformData = new FTexturePlatformData();	// Then we initialize the PlatformData
    	NewTexture->PlatformData->SizeX = xDim;
    	NewTexture->PlatformData->SizeY = yDim;
    	NewTexture->PlatformData->NumSlices = zDim;
    	NewTexture->PlatformData->PixelFormat = EPixelFormat::PF_G8;
    
    	NewTexture->MipGenSettings = TMGS_LeaveExistingMips;
    	NewTexture->SRGB = false;
    
    	FTexture2DMipMap* mip = new FTexture2DMipMap();
    	mip->SizeX = xDim;
    	mip->SizeY = yDim;
    	mip->SizeZ = zDim;
    
    	mip->BulkData.Lock(LOCK_READ_WRITE);
    
    	uint8* ByteArray = (uint8*)mip->BulkData.Realloc(TotalSize);
    	FileHandle->Read(ByteArray, TotalSize);
    
    	mip->BulkData.Unlock();
    
    	// Close the RAW file.
    	delete FileHandle;
    
    	// Let the whole world know we were successful.
    	MY_LOG("File was successfully read!");
    
    	// Add the new MIP.
    	NewTexture->PlatformData->Mips.Add(mip);
    
    	NewTexture->Source.Init(xDim, yDim, zDim, 1, ETextureSourceFormat::TSF_G8, ByteArray);
    	NewTexture->UpdateResource();
    	Package->MarkPackageDirty();
    	FAssetRegistryModule::AssetCreated(NewTexture);
    
    	FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    	bool bSaved = UPackage::SavePackage(Package, NewTexture, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName, GError, nullptr, true, true, SAVE_NoError);
    
    	return;
    }

If you want the volume texture not to be compressed when built from the source data, set the NewTexture->CompressionNone to true.