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

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;
}
3 Likes