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
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.
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.
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;
}
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
The important thing with this example is that the output of the code is a “Component” instead of a regular Actor.
When I first did the tutorial I didn’t understand that concept and hence I innocently created an Actor class( prefixed by A e.g ADynamicTextureComponent : public AActorComponent
).
In the authors code it’s obviously class RUNTIME_TEXTURE_API UDynamicTextureComponent : public UActorComponent
Therefore I wasn’t able to access the functions until I copied the code exactly.
Important - There is a bug in the code which other have pointed out. In many places TextureData[start] = Color.B * 255;
isn’t multiplied by 255. So make sure you do wherever it occours.
// Set Pixel Value by Offsetting from the Start of the Pixel Data
TextureData[start] = Color.B * 255; // multiply Blue by 255
TextureData[start + 1] = Color.G * 255;
TextureData[start + 2] = Color.R * 255;
TextureData[start + 3] = Color.A * 255;
EDIT - SOLVED
Personally I am still trying to figure out how to get this to work without adding it as a component !
Write back when I do.
Just derive from actor class and use the same code
Full code in the comment below.
Cheers ,
b
EDIT - SOLVED
hey got a simple question.
Can i follow your or the authors workflow and create a simple actor class and still have this code work ?
The original author writes to a editable texture via making a component within the BP.
I want to be able to use it using a plain vanilla actor class.
Thanks and cheers,
b
CODE
//HEADER
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "DynamicTexture.generated.h"
UCLASS()
class RUNTIME_TEXTURE_API ADynamicTexture : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ADynamicTexture();
UPROPERTY(EditDefaultsOnly)
int32 TextureWidth = 512;
UPROPERTY(EditDefaultsOnly)
int32 TextureHeight = 512;
UPROPERTY(BlueprintReadWrite)
UMaterialInstanceDynamic* DynamicMaterial;
UPROPERTY(EditDefaultsOnly)
FName DynamicMaterialParamName = "DynamicTexture";
/// Fill Entire Texture with a specified color.
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void FillTexture(FLinearColor Color);
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void SetPixelColor(int32 X, int32 Y, FLinearColor Color);
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void DrawRectangle(int32 StartX, int32 StartY, int32 Width, int32 Height, FLinearColor Color);
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void DrawCircle(int32 StartX, int32 StartY, int32 Size, FLinearColor Color, bool Center = true);
UFUNCTION(BlueprintCallable, Category = "Dynamic Texture")
void DrawFromTexture(int32 StartX, int32 StartY, UTexture2D* Texture, FLinearColor Filter = FLinearColor::White);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
// Array that contains the Texture Data
uint8* TextureData;
// Total Bytes of Texture Data
uint32 TextureDataSize;
// Texture Data Sqrt Size
uint32 TextureDataSqrtSize;
// Total Count of Pixels in Texture
uint32 TextureTotalPixels;
// Texture Object
UPROPERTY()
UTexture2D* DynamicTexture;
// Update Region Struct
FUpdateTextureRegion2D* TextureRegion;
// Initialize the Dynamic Texture
void InitializeTexture();
private:
//Update Texture Object from Texture Data
void UpdateTexture(bool bFreeData = false);
void SetPixelValue(int32 X, int32 Y, FColor Color);
};
//C++
// Fill out your copyright notice in the Description page of Project Settings.
#include "DynamicTexture.h"
#include "RHICommandList.h"
#include "Rendering/Texture2DResource.h"
// Sets default values
ADynamicTexture::ADynamicTexture()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void ADynamicTexture::BeginPlay()
{
//Must be called before Super otherwise Blueprint can't modify texture at BeginPlay.
InitializeTexture();
Super::BeginPlay();
}
// Called every frame
void ADynamicTexture::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UpdateTexture();
if (DynamicMaterial)
{
DynamicMaterial->SetTextureParameterValue("DynamicTexture", DynamicTexture);
}
}
void ADynamicTexture::FillTexture(FLinearColor Color)
{
for (uint32 i = 0; i < TextureTotalPixels; i++)
{
TextureData[i * 4] = Color.B * 255;
TextureData[i * 4 + 1] = Color.G * 255;
TextureData[i * 4 + 2] = Color.R * 255;
TextureData[i * 4 + 3] = Color.A * 255;
}
}
void ADynamicTexture::SetPixelColor(int32 X, int32 Y, FLinearColor Color)
{
// If Pixel is outside of Texture return
if (X < 0 || Y < 0 || X >= TextureWidth || Y >= TextureHeight) {
return;
}
// Get the Start of the Pixel Data
uint32 start = ((Y * TextureWidth) + X) * 4;
// Set Pixel Value by Offsetting from the Start of the Pixel Data
TextureData[start] = Color.B * 255;
TextureData[start + 1] = Color.G * 255;
TextureData[start + 2] = Color.R * 255;
TextureData[start + 3] = Color.A * 255;
}
void ADynamicTexture::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 ADynamicTexture::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 ADynamicTexture::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 ADynamicTexture::InitializeTexture()
{
// Get Total Pixels in Texture
TextureTotalPixels = TextureWidth * TextureHeight;
// Get Total Bytes of Texture - Each pixel has 4 bytes for RGBA
TextureDataSize = TextureTotalPixels * 4;
TextureDataSqrtSize = TextureWidth * 4;
// Initialize Texture Data Array
TextureData = new uint8[TextureDataSize];
// Create Dynamic Texture Object
DynamicTexture = UTexture2D::CreateTransient(TextureWidth, TextureHeight);
DynamicTexture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
DynamicTexture->SRGB = 0;
DynamicTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
DynamicTexture->Filter = TextureFilter::TF_Nearest;
DynamicTexture->AddToRoot();
DynamicTexture->UpdateResource();
//Create Update Region Struct Instance
TextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, TextureWidth, TextureHeight);
FillTexture(FLinearColor::Black);
UpdateTexture();
}
void ADynamicTexture::UpdateTexture(bool bFreeData)
{
if (DynamicTexture == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Dynamic Texture tried to Update Texture but it hasn't been initialized!"));
return;
}
struct FUpdateTextureRegionsData
{
FTexture2DResource* Texture2DResource;
FRHITexture2D* TextureRHI;
int32 MipIndex;
uint32 NumRegions;
FUpdateTextureRegion2D* Regions;
uint32 SrcPitch;
uint32 SrcBpp;
uint8* SrcData;
};
FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;
UTexture2D* Texture = DynamicTexture;
RegionData->Texture2DResource = (FTexture2DResource*)Texture->GetResource();
RegionData->TextureRHI = RegionData->Texture2DResource->GetTexture2DRHI();
RegionData->MipIndex = 0;
RegionData->NumRegions = 1;
RegionData->Regions = TextureRegion;
RegionData->SrcPitch = TextureDataSqrtSize;
RegionData->SrcBpp = 4;
RegionData->SrcData = TextureData;
ENQUEUE_RENDER_COMMAND(UpdateTextureRegionsData)(
[RegionData, bFreeData, Texture](FRHICommandListImmediate& RHICmdList)
{
for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
{
int32 CurrentFirstMip = Texture->FirstResourceMemMip;
if (RegionData->TextureRHI && RegionData->MipIndex >= CurrentFirstMip)
{
RHIUpdateTexture2D(
RegionData->TextureRHI,
RegionData->MipIndex - CurrentFirstMip,
RegionData->Regions[RegionIndex],
RegionData->SrcPitch,
RegionData->SrcData
+ RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
+ RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
);
}
}
if (bFreeData) {
FMemory::Free(RegionData->Regions);
FMemory::Free(RegionData->SrcData);
}
delete RegionData;
});
}
void ADynamicTexture::SetPixelValue(int32 X, int32 Y, FColor Color)
{
// If Pixel is outside of Texture return
if (X < 0 || Y < 0 || X >= TextureWidth || Y >= TextureHeight) {
return;
}
// Get the Start of the Pixel Data
uint32 start = ((Y * TextureWidth) + X) * 4;
// Set Pixel Value by Offsetting from the Start of the Pixel Data
TextureData[start] = Color.B * 255;
TextureData[start + 1] = Color.G;
TextureData[start + 2] = Color.R;
TextureData[start + 3] = Color.A;
}
Create a Blueprint from this class and follow the Authors instructions
- Create a new Material with a TextureSampleParameter2D with the RGB output going into Base Color. Make sure the Texture Parameter has the name “DynamicTexture” or if you use a different name make sure to set the value of DynamicMaterialParamName on the component to match exactly. Apply & Save the material
- Create an Actor Blueprint and add a Plane component to it, this will be what has the dynamic material applied
- In the Construction Script for the Actor Blueprint call the function Create Dynamic Material Instance with the target being the Plane we added and select the material we created earlier
- Right-Click the Return Value and Promote to Variable. I named it M Dynamic Material
6.Set DynamicMaterial property you created in C++ by calling it in the BP and pipe in the M Dynamic Material variable you created in the step above - In the Event Graph Call **Dynamic Circle ** function
Cheers
b