Reading the pixels from a UTexture2D is not particularly difficult, indeed this post on Unreal AnswerHub resume almost perfectly how to do it. However, there are some points missing and one could go in the case where a call to RawImageData->Lock(LOCK_READ_ONLY) will return nullptr. When it happens, it prevents us from reading the pixels, and moreover, potentially causes a huge crash if the situation wasn’t anticipated.
We will review quickly how to read the pixels from a UTexture2D, and how to avoid the situation where nullptr is return when locking the image.
Reading the pixels.
As stated in the AnswerHub’s post, reading the pixels of a UTexture2D can be separated in the following steps:
- Accessing the mipmap 0 of the texture (i the one with the same resolution as the original texture)
- Accessing the BulkData of this mipmap
- Locking it to access to the pixels
- Unlocking it
In terms of code, it can be written like this:
const FColor* FormatedImageData = static_cast<const FColor*>( MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
for(int32 X = 0; X < MyTexture2D->SizeX; X++)
{
for (int32 Y = 0; Y < MyTexture2D->SizeY; Y++)
{
FColor PixelColor = FormatedImageData[Y * MyTexture2D->SizeX + X];
// Do the job with the pixel
}
}
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
In this code sample, we can note two interresting points.
First, the result of LockReadOnly(), which returns an array to the bytes of the texture is casted to an array of FColor. It could also be casted to an array of uint8, if so, FormatedImageData[0] would represent the red channel of the first pixel, FormatedImageData[1] would the green channel of the first pixel and so on. By casting in FColor*, we have FormatedImageData[0] containing all the color channels of the first pixel.
Second, the returned array is a 1D array representing a 2D texture, we have to convert our (X, Y) 2D coordinates into 1D coordinates. This is made by using the following formula: Y * MyTexture2D->SizeX + X which is used to access to the pixel (X, Y) in the FormatedImageData array.
Preventing LockReadOnly from returning nullptr.
When calling LockReadOnly we may get a nullptr in return if the texture is not in a specific format.
Indeed, and as far as I know, the texture must have the following parameters:
- CompressionSettings set to VectorDisplacementmap
- MipGenSettings to NoMipmaps
- SRGB to false
In a general case, we will assume that these conditions are not met. So, what we will do is, saving the current texture settings, applying the settings above, reading the pixels, restoring the previous settings.
Here’s the code sample to save and apply the new settings:
TextureCompressionSettings OldCompressionSettings = MyTexture2D->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = MyTexture2D->MipGenSettings;
bool OldSRGB = MyTexture2D->SRGB;
MyTexture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
MyTexture2D->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
MyTexture2D->SRGB = false;
MyTexture2D->UpdateResource();
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();
Conclusion.
When we integrate all the code samples detailed in this article, we get the following code:
TextureCompressionSettings OldCompressionSettings = MyTexture2D->CompressionSettings; TextureMipGenSettings OldMipGenSettings = MyTexture2D->MipGenSettings; bool OldSRGB = MyTexture2D->SRGB;
MyTexture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
MyTexture2D->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
MyTexture2D->SRGB = false;
MyTexture2D->UpdateResource();
const FColor* FormatedImageData = static_cast<const FColor*>( MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
for(int32 X = 0; X < MyTexture2D->SizeX; X++)
{
for (int32 Y = 0; Y < MyTexture2D->SizeY; Y++)
{
FColor PixelColor = FormatedImageData[Y * MyTexture2D->SizeX + X];
}
}
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
Texture->CompressionSettings = OldCompressionSettings;
Texture->MipGenSettings = OldMipGenSettings;
Texture->SRGB = OldSRGB;
Texture->UpdateResource();
This code have been tested and we hope it will be working for you. However, there may have some case not managed, and eventually you may find another mandatory setting when reading the pixels (Unreal documentation is not really exhaustive about it). If so, feel free to contact us so we can improve this article.
Thank you so much for this. I was going crazy trying to figure out why my code was only working for certain textures, even with DXT decompression code in place.
I did this and in UnrealEngine Editor it works fine, but when i do the build for Android or Windows, it doesnt work,
Do you know any solution?
Thanks.
Just remove ‘MipGenSettings’ references and it should work.
your are the best this post very helpful
I did this for a new blueprint callable function i’m building:
UFUNCTION(BluePrintPure, Category = « General »)
static TArray GetPixels(UTexture2D* Texture);
Inside the function, i put your code. The problem was inside the for loops, when trying to access the SizeX and SizeY parameters from Texture, it threw a compilation error.
I replaced it like this: Texture->PlatformData->Mips[0].SizeX
and Texture->PlatformData->Mips[0].SizeY
and now it’s compiling.
But i didn’t finish testing if it really works or not. Could you give me some hint on why it didn’t work using the code you provided?
Thank you very much for this tutorial i think i’m getting really close to what i need!
thanks for the post!
what exactly is happening when LockReadOnly is called? Is memory copied from GPU? does one need to free it later or is it taken care of? is there a way to access GPU memory (using CUDA)?
thanks,
This throws error in a packaged game within the Forloop, when X = 0 and Y = 570, while trying to read from respective value of the FormattedImageData. The error goes as below –
Attempt to sync load bulk data with EDL enabled (LoadDataIntoMemory). This is not desireable.
Hi, how would you read/sample pixel values UTextureCube in same way?
This works well in PIE but LockReadOnly() returns NULL on deployment build (launch command) regardless texture settings:
MyTexture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
MyTexture2D->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
MyTexture2D->SRGB = false;
MyTexture2D->UpdateResource();
tested deployment build for iOS and Windows
The reason your LockReadOnly returned null is that the static cast failed. Only a pointer of BGRA8 can be casted to FColor* while other formats like DXT1 will fail. You should use FLinearColor* or FFloat16Color* instead for DXT1/3 textures.
man, static cast works at compile time. dont mislead readers
Works like a charm and outputs the actual colors of the texture.
Going to use this for texture based level editing.
Lots of plugin possibilities with this functionality.
As opposed to the commentators that said they had errors with a packaged game:
I just tried a test package (Development) on an Android device and it worked.
Should add that I’m not using the functionality to set the mipgensettings, compression and srgb but simply set it on the Texture2D.
Hi, I try to read the vertices of a Landscape (original size 8128×8129). So my texture should be this:
UTexture2D* MyTexture2D = landscape->LandscapeComponents[0]->GetHeightmap();
But if I use your method, the size of the MyTexture2D is always 512×512 instead of the original size of the landscape 8129×8129.
Can you help?
Hi! How does that work if you want to access the vertices of a landscape with size 8129×8129?
If I use the heightmap of a component:
UTexture2D* MyTexture2D = landscape->LandscapeComponents[0]->GetHeightmap();
and then use your algorithm:
TextureCompressionSettings OldCompressionSettings = MyTexture2D->CompressionSettings;
TextureMipGenSettings OldMipGenSettings = MyTexture2D->MipGenSettings;
bool OldSRGB = MyTexture2D->SRGB;
MyTexture2D->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
MyTexture2D->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
MyTexture2D->SRGB = false;
MyTexture2D->UpdateResource();
const FColor* FormatedImageData = static_cast(MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
int size_x = MyTexture2D->GetSizeX(); // *************** HERE I GET 512 **********************************************
UE_LOG(LogTemp, Warning, TEXT(“size: %i”), size_x);
for (int32 X = 0; X GetSizeX(); X++)
{
for (int32 Y = 0; Y GetSizeY(); Y++)
{
FColor PixelColor = FormatedImageData[Y * MyTexture2D->GetSizeX() + X];
}
}
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
MyTexture2D->CompressionSettings = OldCompressionSettings;
MyTexture2D->MipGenSettings = OldMipGenSettings;
MyTexture2D->SRGB = OldSRGB;
MyTexture2D->UpdateResource();
The size of the MyTexture2D is 512×512 instead of 8129×8129.
Any idea??? Thank you!
Working fine in editor, but I also have a problem when trying to package the project for windows. The line:
const FColor* FormatedImageData = static_cast( MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
Is causing the following error when building the projet:
error C2440: ‘static_cast’: cannot convert from ‘const ElementType *’ to ‘const FColor *’
with [ ElementType=uint8 ]
I am able to get around the build error by splitting array ptr recovering and FColor construction:
const uint8* ImageData = static_cast(MyTexture2D->PlatformData->Mips[0].BulkData.LockReadOnly());
int32 Id = 4 * (PixelYId * MyTexture2D->GetSizeX() + PixelXId); // 4 times because of RGBA
FColor PixelColor = FColor(ImageData[Id], ImageData[Id + 1], ImageData[Id + 2], ImageData[Id + 3]);
MyTexture2D->PlatformData->Mips[0].BulkData.Unlock();
Which once again appears to work fine in editor. But when I try to launch the .exe, it results in fatal error (No more details 🙁 ). Any ideas on how to resolve this?
Thank you!
The static_cast should be a reinterpret_cast. At least on clang, this fails to compile otherwise.
Another vote for: this doesn’t work in a build. Shame, just finished a gamejam but now can’t package it in a build.
First had to change the static cast to reinterpret cast to get it to build.
Then the cast fails in a build (tried debug, development and shipping).
FTexture2DMipMap* TileMipMap = &TileMapTexture->PlatformData->Mips[0];
FByteBulkData* TileRawImageData = &TileMipMap->BulkData;
FColor* TileImageData = reinterpret_cast(TileRawImageData->Lock(LOCK_READ_ONLY));
this fixed it for me (not sure if its the lock method, or breaking it down into steps, but I am too tired to now debug further 😀
Can it work on Texture OR MediaTexture? How to access PlatformData from them? Thanks for any reply?