Creating the UTexture2D object.
Let’s say we have generated a UTexture2D from C++ within the editor. It could be interesting to save it in order to use later. Let’s have a look on how to do this.
The first step is to create a UTexture2D object. Here, we cannot create a transient texture using UTexture2D::CreateTransient, because if we did so, we would be unable to save the texture (transient objects can’t be saved).
So, we will create a package, and then store a new texture in this package.
FString PackageName = TEXT("/Game/ProceduralTextures/");
PackageName += TextureName;
UPackage* Package = CreatePackage(NULL, *PackageName);
Package->FullyLoad();
UTexture2D* NewTexture = NewObject<UTexture2D>(Package, *TextureName, RF_Public | RF_Standalone | RF_MarkAsRootSet);
Here, we are creating a package “/Game/ProceduralTextures/” and adding a texture named TextureName in this package when calling NewObject<Texture2D>.
Filling the texture with our data.
Once we have our texture, we can initialize it and fill it with our data.
NewTexture->AddToRoot(); // This line prevents garbage collection of the texture
NewTexture->PlatformData = new FTexturePlatformData(); // Then we initialize the PlatformData
NewTexture->PlatformData->SizeX = TextureWidth;
NewTexture->PlatformData->SizeY = TextureHeight;
NewTexture->PlatformData->NumSlices = 1;
NewTexture->PlatformData->PixelFormat = EPixelFormat::PF_B8G8R8A8;
So far, it’s pretty similar to what we do when creating a transient texture: we fill the PlatformData object with the parameters of our texture (width, height, pixel format). Here we are chosing PF_B8G8R8A8 as pixel format, but we could choose any pixel format, as long as we fill the data accordingly after.
Then, it’s time to fill the texture.
uint8* Pixels = new uint8[TextureWidth * TextureHeight * 4];
for (int32 y = 0; y < TextureHeight; y++)
{
for (int32 x = 0; x < TextureWidth; x++)
{
int32 curPixelIndex = ((y * TextureWidth) + x);
Pixels[4 * curPixelIndex] = B;
Pixels[4 * curPixelIndex + 1] = G;
Pixels[4 * curPixelIndex + 2] = R;
Pixels[4 * curPixelIndex + 3] = A;
}
}
There are different ways to fill the texture. In this small code snippet, we’re just creating a pixel array and defining a color for each one. The array size is the Width x Height x Bytes per pixel. As the pixel format is BGRA, there are four bytes per pixel. We may note that the pixels are filled in the order BGRA to match the previously selected pixel format.
Then, we need to populate the texture with the data in the pixel array:
// Allocate first mipmap.
FTexture2DMipMap* Mip = new(NewTexture->PlatformData->Mips) FTexture2DMipMap();
Mip->SizeX = TextureWidth;
Mip->SizeY = TextureHeight;
// Lock the texture so it can be modified
Mip->BulkData.Lock(LOCK_READ_WRITE);
uint8* TextureData = (uint8*) Mip->BulkData.Realloc(TextureWidth * TextureHeight * 4);
FMemory::Memcpy(TextureData, Pixels, sizeof(uint8) * TextureHeight * TextureWidth * 4);
Mip->BulkData.Unlock();
At this point we have generated texture. Now, we want to save it as a new asset.
Saving the texture.
So far, we have only set data in the PlatformData. However, PlatformData is sort of transient and cannot be saved on the disk. To initialize the data in a non-transient field of the texture, we will refer to the Source field.
NewTexture->Source.Init(TextureWidth, TextureHeight, 1, 1, ETextureSourceFormat::TSF_BGRA8, Pixels);
The call to this function will initialize the Source object, setting its width, its height, its pixel format and also the data from the Pixels array.
Finally, we will call SavePackage to save the newly created asset:
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);
delete[] Pixels; // Don't forget to free the memory here
Im getting a crash on UpdateResource() command, can’t figure out why… Any ideas?
OH, my problem was the size I assigned to the texture. It was crashing in the TextureCompressorModule because its width and height were not power of 2.
Thank you so much! I had exactly the same problem and you saved me a lot of debugging.
I get same error in the TextureCompressorModule ,but I had set the width and height to be power of 2, is it rigtht?
saviour
I have been using this code for some time, thank you. Lately i have migrated to 4.22 version and I have got deprecation warning on line:
FTexture2DMipMap* Mip = new(NewTexture->PlatformData->Mips) FTexture2DMipMap();
warning C4996: ‘operator new’: Placement new on TIndirectArray has been deprecated – users should call Add() passing a pointer to an object created with new. Please update your code to the new API before upgrading to the next release, otherwise your project will no longer compile.
I cant see how to rewrite it. Any idea??
FTexture2DMipMap* Mip = new FTexture2DMipMap();
NewTexture->PlatformData->Mips.Add(Mip);
Mip->SizeX = TextureWidth;
Mip->SizeY = TextureHeight;
Great tutorial. Do you happen to know where UTexture2D.PlatformData is getting destroyed? I can seem to find this in the engine anywhere but not sure if i’m misunderstanding something as my pointer knowledge is a little rusty…
For anyone that got stuck like me trying to make a HDR texture package out of a Linear Color array – you need to first convert to FFloat16Color and use Pixel Format PF_FloatRGBA.
Heres my example code.
TArray colors;
colors.SetNum(_data.Num());
for (int32 i = 0; i PlatformData = new FTexturePlatformData();
_tex->PlatformData->SizeX = _width;
_tex->PlatformData->SizeY = _height;
_tex->PlatformData->PixelFormat = PF_FloatRGBA;
_tex->CompressionSettings = TextureCompressionSettings::TC_HDR;
_tex->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
_tex->SRGB = false;
// Allocate first mipmap.
int32 NumBlocksX = _width / GPixelFormats[PF_FloatRGBA].BlockSizeX;
int32 NumBlocksY = _height / GPixelFormats[PF_FloatRGBA].BlockSizeY;
FTexture2DMipMap* Mip = new FTexture2DMipMap();
_tex->PlatformData->Mips.Add(Mip);
Mip->SizeX = _width;
Mip->SizeY = _height;
Mip->BulkData.Lock(LOCK_READ_WRITE);
void* Data = Mip->BulkData.Realloc(NumBlocksX * NumBlocksY * GPixelFormats[PF_FloatRGBA].BlockBytes);
FMemory::Memcpy(Data, colors.GetData(), _width * _height * GPixelFormats[PF_FloatRGBA].BlockBytes);
Mip->BulkData.Unlock();
_tex->Source.Init(_width, _height, 1, 1, ETextureSourceFormat::TSF_RGBA16F, (uint8*)colors.GetData());
_tex->AddToRoot();
_tex->UpdateResource();
Sorry Copy Paste Error – It cropped out my for loop
TArray colors;
colors.SetNum(_data.Num());
for (int32 i = 0; i PlatformData = new FTexturePlatformData();
_tex->PlatformData->SizeX = _width;
_tex->PlatformData->SizeY = _height;
_tex->PlatformData->PixelFormat = PF_FloatRGBA;
_tex->CompressionSettings = TextureCompressionSettings::TC_HDR;
_tex->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
_tex->SRGB = false;
// Allocate first mipmap.
int32 NumBlocksX = _width / GPixelFormats[PF_FloatRGBA].BlockSizeX;
int32 NumBlocksY = _height / GPixelFormats[PF_FloatRGBA].BlockSizeY;
FTexture2DMipMap* Mip = new FTexture2DMipMap();
_tex->PlatformData->Mips.Add(Mip);
Mip->SizeX = _width;
Mip->SizeY = _height;
Mip->BulkData.Lock(LOCK_READ_WRITE);
void* Data = Mip->BulkData.Realloc(NumBlocksX * NumBlocksY * GPixelFormats[PF_FloatRGBA].BlockBytes);
FMemory::Memcpy(Data, colors.GetData(), _width * _height * GPixelFormats[PF_FloatRGBA].BlockBytes);
Mip->BulkData.Unlock();
_tex->Source.Init(_width, _height, 1, 1, ETextureSourceFormat::TSF_RGBA16F, (uint8*)colors.GetData());
_tex->AddToRoot();
_tex->UpdateResource();
This approach to create a texture only seems to work if the texture w x h is is in power of two. What needs to be done if it isnt?
Sorry if I’m not understanding this correctly, but how would you go about saving a texture with float color data, for more accuracy? As an example if I wanted all pixels to be RGBA(0.145, 0.543, 0.223, 1.0), how would I save this to a texture when it requires the data to be passed as a uint8* ?