Community Tutorial: Creating a Runtime Editable Texture in C++

Learn how to create a Texture in C++ that can be modified at runtime to change the value of pixels. For this tutorial we use this texture in a material that can be painted on by the player!

https://dev.epicgames.com/community/learning/tutorials/ow9v/unreal-engine-creating-a-runtime-editable-texture-in-c

2 Likes

This solved several problems for me. Thanks for creating this.

There is a bug that I’m guessing just got copy and pasted though. For all the linear color conversions, the B channel isn’t multiplied by 255. You might want to FLinearColor ToFColor for the conversion instead, since it does some stuff under the hood to account for different CPU architectures. You could also probably just optimize it all out and just have TextureData be an array of FColor instead of uint8. I’m still investigating that part though.

Again, thanks for creating this. Super useful.

1 Like

I’m glad it was helpful to someone!

I don’t remember if there was a reason I didn’t use ToFColor or if I just didn’t think about it, but thanks for the tip! I’ll update the tutorial, when I get a chance, to fix that.

As for storing the texture data as an FColor array I don’t believe that would work here, or at least not very well. I believe the render command needs the texture data as a uint8 array so it would require creating a uint8 array from the FColor array before calling the rendering command. Though I’m not 100% sure how the render command works and the documentation for it is almost non-existent…

I’m still in the process of testing this and I’m not suggesting you change the tutorial to use this, but I wanted to include it for anyone else that might come across this and find it useful. All FColor stores is the RGBA as a single 32 bit integer, so you should be able to cast from FColor* to uint8* and it should work fine.

	RegionData->SrcData = reinterpret_cast<uint8*>(TextureData);

The upside of this is that it simplifies the code and you now have easier access to all the FColor methods to use on your underlying data. That being said, I reiterate that I haven’t fully tested it yet. While I don’t see any reason that it should cause problems, use it at your own risk.

1 Like

OK, so I couldn’t get this to work for my use case. I tested your code exactly as you described and aside from that one bug, it worked exactly as described. However, I was trying to use the image for a Slate brush and kept getting garbage out. After further research, I came across a function that basically replaces your entire UpdateTexture function with one line of code. You also no longer need to add anything to the Build.cs file.

For my use case, I put it in a UObject rather than an ActorComponent. I also used unique_ptrs for better memory management and changed the TextureData to be an FColor array rather than uint8. I’m including my code below. Hope it helps.

O_DynamicImage_CPP_01.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "O_DynamicImage_CPP_01.generated.h"

/**
 * 
 */
UCLASS(BlueprintType, Blueprintable, Category = "AAA_DynamicTexture")
class YOURPROJECT_API UO_DynamicImage_CPP_01 : public UObject
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UO_DynamicImage_CPP_01();

	/// Fill Entire Texture with a specified color.
	UFUNCTION(BlueprintCallable, Category = "AAA_DynamicTexture")
	void FillTexture(FLinearColor Color);
 
	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture")
	void SetPixelColor(int32 X, int32 Y, FLinearColor Color);
 
	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture")
	void DrawRectangle(int32 StartX, int32 StartY, int32 Width, int32 Height, FLinearColor Color);
 
	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture")
	void DrawCircle(int32 StartX, int32 StartY, int32 Size, FLinearColor Color, bool Center = true);
 
	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture")
	void DrawFromTexture(int32 StartX, int32 StartY, UTexture2D* Texture, FLinearColor Filter = FLinearColor::White);

 	// Initialize the Dynamic Texture
	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture", meta=(ToolTip="If Height and Width are 0, the texture will be initialized with the default values."))
 	void InitializeTexture(int32 Width = 0, int32 Height = 0, FLinearColor InitialColor = FLinearColor::Black);

	//Update Texture Object from Texture Data
	void UpdateTexture(bool bFreeData = false);

	UFUNCTION(BlueprintCallable, Category="AAA_DynamicTexture")
	UTexture2D* GetTexture() const { return DynamicTexture; }
	
protected:
	UPROPERTY(EditDefaultsOnly)
	int32 TextureWidth = 512;
 
	UPROPERTY(EditDefaultsOnly)
	int32 TextureHeight = 512;

	void InitializeTexture_internal(FLinearColor InitialColor);
 
	// Array that contains the Texture Data
	std::unique_ptr<FColor[]>TextureData;
	
	// Total Count of Pixels in Texture
	uint32 TextureTotalPixels;
 
	// Texture Object
	UPROPERTY()
	UTexture2D* DynamicTexture;
 
	// Update Region Struct
	std::unique_ptr<FUpdateTextureRegion2D> TextureRegion;
 
	void SetPixelValue(int32 X, int32 Y, FColor Color);

	void SetPixelValue_Unsafe(int32 X, int32 Y, FColor& Color);
};

O_DynamicImage_CPP_01.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "O_DynamicImage_CPP_01.h"

UO_DynamicImage_CPP_01::UO_DynamicImage_CPP_01()
{
	InitializeTexture_internal(FLinearColor::White);
}

void UO_DynamicImage_CPP_01::FillTexture(FLinearColor Color)
{
	FColor FillColor = Color.ToFColor(true);
	for(int32 i = 0; i < TextureWidth; i++)
	{
		for(int32 j = 0; j < TextureHeight; j++)
		{
			SetPixelValue_Unsafe(i, j, FillColor);
		}
	}
}

void UO_DynamicImage_CPP_01::SetPixelColor(int32 X, int32 Y, FLinearColor Color)
{
	SetPixelValue(X, Y, FColor(Color.ToFColor(false)));
}

void UO_DynamicImage_CPP_01::DrawRectangle(int32 StartX, int32 StartY, int32 Width, int32 Height, FLinearColor Color)
{
	for (int32 y = 0; y < Height; y++)
	{
		for (int32 x = 0; x < Width; x++) {
			SetPixelColor(StartX + x, StartY + y, Color);
		}
	}
}

void UO_DynamicImage_CPP_01::DrawCircle(int32 StartX, int32 StartY, int32 Size, FLinearColor Color, bool Center)
{
	float radius = Size / 2;
	int32 offset = FMath::Floor(radius * Center);
 
	for (int32 y = 0; y < Size; y++)
	{
		for (int32 x = 0; x < Size; x++) {
			FVector pos = FVector(x - radius, y - radius, 0);
			if (pos.Size2D() <= radius) {
				SetPixelColor((StartX - offset) + x, (StartY - offset) + y, Color);
			}
 
		}
	}
}

void UO_DynamicImage_CPP_01::DrawFromTexture(int32 StartX, int32 StartY, UTexture2D* Texture, FLinearColor Filter)
{
	if (!Texture) {
		return;
	}
 
	int32 width = Texture->GetSizeX();
	int32 height = Texture->GetSizeY();
	uint32 texDataSize = width * height * 4;
	uint8* texData = new uint8[texDataSize];
 
	FTexture2DMipMap& readMip = Texture->GetPlatformData()->Mips[0];
	readMip.BulkData.GetCopy((void**)&texData);
 
 
	for (int32 y = 0; y < height; y++)
	{
		for (int32 x = 0; x < width; x++) {
			uint32 start = ((y * width) + x) * 4;
 
			SetPixelValue(StartX + x, StartY + y, FColor((texData[start + 2] * Filter.R), texData[start + 1] * Filter.G, texData[start] * Filter.B, texData[start + 3] * Filter.A));
		}
	}
 
	FMemory::Free(texData);
}

void UO_DynamicImage_CPP_01::InitializeTexture(int32 Width, int32 Height, FLinearColor InitialColor)
{
	if(Width > 0)
	{
		TextureWidth = Width;
	}
	if(Height > 0)
	{
		TextureHeight = Height;
	}
	InitializeTexture_internal(InitialColor);
}

void UO_DynamicImage_CPP_01::InitializeTexture_internal(FLinearColor InitialColor)
{
	// Get Total Pixels in Texture
	TextureTotalPixels = TextureWidth * TextureHeight;
 
	// Initialize Texture Data Array
	TextureData = std::make_unique<FColor[]>(TextureTotalPixels);

	if(DynamicTexture != nullptr)
	{
		DynamicTexture->RemoveFromRoot();
	}
 
	// Create Dynamic Texture Object
	DynamicTexture = UTexture2D::CreateTransient(TextureWidth, TextureHeight);
	DynamicTexture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
	DynamicTexture->SRGB = 1;
	DynamicTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
	DynamicTexture->Filter = TextureFilter::TF_Nearest;
	DynamicTexture->AddToRoot();
	DynamicTexture->UpdateResource();
 
	//Create Update Region Struct Instance
	TextureRegion = std::make_unique<FUpdateTextureRegion2D> (0, 0, 0, 0, TextureWidth, TextureHeight);
 
	FillTexture(InitialColor);
	UpdateTexture();
}

void UO_DynamicImage_CPP_01::UpdateTexture(bool bFreeData)
{
	if (DynamicTexture == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("Dynamic Texture tried to Update Texture but it hasn't been initialized!"));
		return;
	}
	int32 SrcPitch = TextureWidth * 4;
	DynamicTexture->UpdateTextureRegions(0, 1, TextureRegion.get(), SrcPitch, 4, reinterpret_cast<uint8*>(TextureData.get()));
}


void UO_DynamicImage_CPP_01::SetPixelValue(int32 X, int32 Y, FColor Color)
{
	// If Pixel is outside of Texture return
	if (X < 0 || Y < 0 || X >= TextureWidth || Y >= TextureHeight) {
		return;
	}
 
	SetPixelValue_Unsafe(X, Y, Color);
}

void UO_DynamicImage_CPP_01::SetPixelValue_Unsafe(int32 X, int32 Y, FColor& Color)
{
	// Get the Start of the Pixel Data 
	// Set Pixel Value by Offsetting from the Start of the Pixel Data
	TextureData[((Y * TextureWidth) + X)] = Color;
}
2 Likes

There is a bug at line 71. Took me couple hours to find it because i though it was my end lol.

Great!!, Thanks, that’s what I was looking for!

I have one more question. I would like to import 32bit BGRA textures from disk. Have you tried using such a function?

Thanks for sharing.
I am new to UE c++ and I am looking for a way to show my changed texture data buffer.

It’s not like openGL that you modify&sned buffer to shader to render texture.

I can compile your code but my BluePrint cannot find the functions you defined.
I searched online that you have to add “static” for function to be exposed in BP.

Can you help me and how do you call the NODEs in you UE blueprint?

Thank you for making this tutorial. It is very important to learn if you wan to use OpenCV.
Cheers,
b