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();
 The call to UpdateResource() applies the new settings. Then, we can read the pixels using the code presented in the first part of this article.
Once the pixels are read, and the texture is unlocked, we can reapply the old settings:
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.