UE4 – Save a procedurally generated texture as a new asset

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);

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
uint8* TextureData = (uint8*) Mip->BulkData.Realloc(TextureWidth * TextureHeight * 4);
FMemory::Memcpy(TextureData, Pixels, sizeof(uint8) * TextureHeight * TextureWidth * 4);

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:


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

Leave a Reply