Create vector graphics with AGG in UE4

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

In the editor I got a folder with a material, a material instance and a static mesh called plane (it’s just a copy of the default plane).

assets.jpg

The material looks like this:

Then you make a material instance off that and put that in the planes material slot.

You don’t drag the plane into the editor, you drag the class (that just happens to use the plane) into the level.

And if you’re wondering why anyone would want to generate a texture of a crooked green square, I also used it to do this: