Hey Everyone,
Here’s a little thing I needed for a project of mine you may or may not have heard of, just got it going as a simple clean test (with a little help from the mighty Phyronnaz of voxel plugin fame).
Anyway, it’s super handy if you need to be able to make vector graphics inside UE4 so I figured I’d share the code (it was a total pain in the ask to figure out).
Here’s what it does (told you it was simple):
and here’s the code (you have to put AGG 2.4 into a third party folder in your project root)
dtTest01.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "../../ThirdParty/agg-2.4/include/agg.h"
#include <vector>
#include "Engine/Texture2D.h"
#include "Engine/StaticMesh.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "ConstructorHelpers.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "dtTest01.generated.h"
UCLASS()
class DTEX_API AdtTest01 : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AdtTest01();
// update class instances in the editor if changes are made to their properties
virtual void OnConstruction(const FTransform& Transform) override;
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// Called after the actors components have been initialized
virtual void PostInitializeComponents() override;
// Called every frame
virtual void Tick(float DeltaSeconds) override;
// Make the dynamic texture
void MakeTexture();
// test creation of an AGG texture
void TestAGG();
// Magic method to draw texture really quickly
void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData);
// components
UPROPERTY(VisibleAnywhere)
USceneComponent* root;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* testPlane;
UPROPERTY(VisibleAnywhere, Transient)
UMaterialInstanceDynamic* dtMaterialInstanceDynamic;
UPROPERTY(VisibleAnywhere, Transient)
UTexture2D* dtTexture;
private:
// dTex res
int dtWidth;
int dtHeight;
int dtBytesPerPixel;
// dTex buffer
uint8 *dtBuffer;
int dtBufferSize;
int dtBufferSizeSqrt;
// update texture region magic thingy
FUpdateTextureRegion2D* dtUpdateTextureRegion;
// Quick random function for testing (NEEDS TO BE MADE PSEUDO RANDOM)
double rnd();
};
dtTest01.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "dtTest01.h"
///////////////////
// //
// Constructor //
// //
///////////////////
AdtTest01::AdtTest01()
{
//---------------- setup the actor components ----------------//
// 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;
// give this actor a transformable root
root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
RootComponent = root;
// add a plane to preview the test results for the cairo test texture
testPlane = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Test Plane"));
testPlane->SetupAttachment(root);
// get a test mesh from the content browser to use in the staticmeshcomponent
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshPlane(TEXT("StaticMesh'/Game/Core/Systems/dTex/plane.plane'"));
if (StaticMeshPlane.Object) testPlane->SetStaticMesh(StaticMeshPlane.Object);
}
////////////////////////
// //
// OnConstruction() //
// //
////////////////////////
// regenerate instance (in editor) when necessary
void AdtTest01::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
MakeTexture();
TestAGG();
}
///////////////////
// //
// BeginPlay() //
// //
///////////////////
// Called when the game starts or when spawned
void AdtTest01::BeginPlay()
{
Super::BeginPlay();
}
//////////////////////////////////
// //
// PostInitializeComponents() //
// //
//////////////////////////////////
// Called when the components have been initialized
void AdtTest01::PostInitializeComponents()
{
Super::PostInitializeComponents();
MakeTexture();
TestAGG();
}
//////////////
// //
// Tick() //
// //
//////////////
// Called every frame
void AdtTest01::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TestAGG();
}
/////////////////////
// //
// MakeTexture() //
// //
/////////////////////
void AdtTest01::MakeTexture()
{
// dynamic texture properties (hard wired here for now)
dtBytesPerPixel = 4;
dtWidth = 2048; // OR textureToReadFrom->GetSizeX();
dtHeight = 2048; // OR textureToReadFrom->GetSizeY();
// create buffers to collate pixel data into
dtBufferSize = dtWidth * dtHeight * dtBytesPerPixel;
dtBufferSizeSqrt = dtWidth * dtBytesPerPixel;
dtBuffer = new uint8[dtBufferSize]; // this is the data that we Memcpy into the dynamic texture
// Create dynamic material
dtMaterialInstanceDynamic = testPlane->CreateAndSetMaterialInstanceDynamic(0);
// create dynamic texture
dtTexture = UTexture2D::CreateTransient(dtWidth, dtHeight);
dtTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
dtTexture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap;
dtTexture->SRGB = 0;
dtTexture->AddToRoot(); // Guarantee no garbage collection by adding it as a root reference
dtTexture->UpdateResource(); // Update the texture with new variable values.
// plug the dynamic texture into the dynamic material
dtMaterialInstanceDynamic->SetTextureParameterValue(FName("DynamicTextureParam"), dtTexture);
// Create a new texture region with the width and height of our dynamic texture
dtUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, dtWidth, dtHeight);
}
/////////////////
// //
// TestAGG() //
// //
/////////////////
void AdtTest01::TestAGG()
{
// color buffer
agg::int8u* cBuffer = new agg::int8u[dtWidth * dtHeight * 3];
memset(cBuffer, 255, dtWidth * dtHeight * 3);
agg::rendering_buffer agg_rbuf(cBuffer, dtWidth, dtHeight, dtWidth * 3);
agg::pixfmt_rgb24 agg_pixf(agg_rbuf);
// alpha buffer
agg::int8u* amask_buf = new agg::int8u[dtWidth * dtHeight];
memset(amask_buf, 255, dtWidth * dtHeight);
agg::rendering_buffer amask_rbuf(amask_buf, dtWidth, dtHeight, dtWidth);
agg::amask_no_clip_gray8 amask(amask_rbuf);
// alpha mask adaptor
agg::pixfmt_amask_adaptor<agg::pixfmt_rgb24, agg::amask_no_clip_gray8> pixf_amask(agg_pixf, amask);
agg::renderer_base<agg::pixfmt_rgb24> agg_rbase(agg_pixf);
agg::rasterizer_scanline_aa<> agg_ras;
agg::scanline_u8 agg_sl;
///////////////////////////////
// Test basic shape creation //
///////////////////////////////
// set default fill color (B, G, R - why when it says rgba I don't know)
agg_rbase.clear(agg::rgba(1, 1, 1, 1));
// clear out any vectors in agg_ras from the last texture request
agg_ras.reset();
// put shape points in a vector to aid transforming
std::vector<FVector2D> shape_raw;
shape_raw.push_back(FVector2D(4, 4));
shape_raw.push_back(FVector2D(60, 4));
shape_raw.push_back(FVector2D(60, 60));
shape_raw.push_back(FVector2D(4, 60));
// prepare a transform
agg::trans_affine m;
m.rotate(10.0 * 3.14159265 / 180.0);
m.scale(20 + rnd() * 10, 20 + rnd() * 10);
m.translate(50, 50);
// apply the transform to each shape point
for (int i = 0; i < shape_raw.size(); i++) {
double temp_x(0.0);
double temp_y(0.0);
temp_x = static_cast<double>(shape_raw*.X);
temp_y = static_cast<double>(shape_raw*.Y);
m.transform(&temp_x, &temp_y);
shape_raw*.X = static_cast<float>(temp_x);
shape_raw*.Y = static_cast<float>(temp_y);
}
// iterate through shape vector
agg_ras.add_vertex(shape_raw[0].X, shape_raw[0].Y, 1); // 1 = start new shape
for (int j = 1; j<shape_raw.size(); j++) agg_ras.add_vertex(shape_raw[j].X, shape_raw[j].Y, 2); // 2 = continue current shape
agg_ras.add_vertex(0, 0, 79); // 79 = close current shape (so x,y don't matter)
// rasterize the vector shapes - agg_rbase feeds into agg_pixf, which feeds into agg_rbuff, which (now) feeds into cBuffer - hopefully the alpha-mask adaptor picks up what it needs to from this automatically
agg::render_scanlines_aa_solid(agg_ras, agg_sl, agg_rbase, agg::rgba(0, 1, 0, 1));
// but now we need to copy the split rgb/a data from agg buffers to mixed rgba UE4 buffer
int pixelAmount = dtBufferSize / dtBytesPerPixel;
for (int i = 0; i < pixelAmount; ++i)
{
int iBlue = i * 4 + 0;
int iGreen = i * 4 + 1;
int iRed = i * 4 + 2;
int iAlpha = i * 4 + 3;
dtBuffer[iBlue] = cBuffer[i * 3 + 0];
dtBuffer[iGreen] = cBuffer[i * 3 + 1];
dtBuffer[iRed] = cBuffer[i * 3 + 2];
dtBuffer[iAlpha] = amask_buf*;
}
delete] amask_buf;
delete] cBuffer;
UpdateTextureRegions(dtTexture, 0, 1, dtUpdateTextureRegion, dtBufferSizeSqrt, (uint32)4, dtBuffer, false);
dtMaterialInstanceDynamic->SetTextureParameterValue("DynamicTextureParam", dtTexture);
}
//////////////////////////////
// //
// UpdateTextureRegions() //
// //
//////////////////////////////
void AdtTest01::UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
{
if (Texture && Texture->Resource)
{
struct FUpdateTextureRegionsData
{
FTexture2DResource* Texture2DResource;
int32 MipIndex;
uint32 NumRegions;
FUpdateTextureRegion2D* Regions;
uint32 SrcPitch;
uint32 SrcBpp;
uint8* SrcData;
};
FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;
RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
RegionData->MipIndex = MipIndex;
RegionData->NumRegions = NumRegions;
RegionData->Regions = Regions;
RegionData->SrcPitch = SrcPitch;
RegionData->SrcBpp = SrcBpp;
RegionData->SrcData = SrcData;
ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
UpdateTextureRegionsData,
FUpdateTextureRegionsData*, RegionData, RegionData,
bool, bFreeData, bFreeData,
{
for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
{
int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
if (RegionData->MipIndex >= CurrentFirstMip)
{
RHIUpdateTexture2D(
RegionData->Texture2DResource->GetTexture2DRHI(),
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;
});
}
}
/////////////
// //
// rnd() //
// //
/////////////
// Quick random function for testing
double AdtTest01::rnd() { return double(rand()) / RAND_MAX; }