Procedural Materials - Epic Wiki
# Procedural Materials
From Epic Wiki
Jump to: navigation, search
# Goal of this article
With this article you will learn how to make and use of dynamic materials to create procedurally generated texture that during runtime is writable and readable in C++. This article will be catered to C++ programmers, and currently there is no support for blueprints.
# Contents
# Setup
# What you will need
- A texture that with the following properties: (Editor)
- No compression (B8G8R8A8) making it humanly readable and writable.
- No mipmaps
- No sRGB applied
- A material with a Texture Sample 2D parameter (Editor)
- A function for enqueueing tasks on the render thread. (C++)
- Create the dynamic texture and link it with the dynamic material (C++)
- Manipulate the texture (C++)
# Texture Setup
Image
First you will need to import a texture with your desired dimensions. In its' settings change the following:
- Mip Gen Settings -> NoMipmaps
- sRGB -> false
- Compression Settings -> TC Vector Displacementmap
# Motivation
Unreal4 auto-compresses the textures when imported. They usually default to DXT5 which is not appropriate for us to edit. It skews the data and we're unable to get predictable results and values from the texture. By setting the Compression Settings to TC Vector Displacementmap we are guaranteed no compression.
We are setting the Gamma (sRGB) to false because it also skews the data. Gamma is useful for expanding the color palette in the region we humans are able to detect change. But since we're going to edit the texture it will obscure the pixel values which will make it harder for us to establish a proper prediction and estimation when working with the texture.
By setting the Mip Gen Settings to NoMipmaps we tell the engine that we are not going to be needing any mipmaps of our texture. This is to prevent bugs when the engine tries to downscale the texture (which we will dynamically write to and read from).
# Material Setup
- Create a Texture Sample Parameter 2D node in the material
- Change the Sampler Type to Linear Color
- Change the Parameter Name to DynamicTextureParam (or whatever name you set in SetTextureParameterValue())
# Motivation
The material setup can be as advanced as you wish, all you're going to need to keep in mind is that the texture from the dynamic parameter will be just that; dynamic. The picture illustrates how a simple setup may look (Use the colors of the dynamic texture and sample them with TexCoords). Something to keep in mind: If you don't set the Sampler Type to Linear Color you will encounter an error prompting you to change it accordingly. Later you will write to this parameter through C++.
# Enqueue Render Task
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; }); } }
# Explanation
This function is a straight copy-paste from here, which is where you can read some more about it if you wish to know the ins- and outs of how it works and what it does.
# Dynamic Texture & Dynamic Material
It does not matter where you do this; as long as it is only done once. A good start-off point would be to have this code in PostInitializeComponents().
And the setup is complete. We now have three things:
- An array of uint8's which acts as a copy of our static texture
- A dynamic texture which is read/write enabled
- A dynamic material which is read/write enabled
# Manipulating the texture
This is the final piece of the puzzle, which also happens to be the easiest. All you need to do now is to manipulate the array of mDynamicColors and set the individual color values. Worth noting; the entire array exists of BGRA-values, which means that every fourth index is the color B for the current pixel. Here's a small example:
# Implemenations
Simple Dynamic Texture Implementation (Public Domain)
# Complete example
This code is a snippet from our own game. It should be pretty straight forward what it does. Please note that this is copyrighted.
The .h file
pragma once
include "GameFramework/Actor.h"
include "WLavaActor.generated.h"
/**
* */
UCLASS() class SPELLSWORN_API AWLavaActor : public AStaticMeshActor { GENERATED_UCLASS_BODY()
virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; virtual void PostInitializeComponents() override; virtual void Tick(float DeltaTime) override; void PropagateLava(); int GetLavaValue(const FVector& aWorldPosition) const;
void SetMeltRateDivisor(float aDivisor) { mMeltRateDivisor = aDivisor; } float GetDynamicMeltRate() const { return mMeltRate + mMeltRate * mMeltRateDivisor * .2f; }
int32 GetRowSize() const { return mRowSize; } int32 GetCellSize() const { return mCellSize; } UFUNCTION() void BeginPropagateLava(); UFUNCTION() void EndPropagateLava(); void UpdateLava();
UFUNCTION() void ResetColors();
private: FTimerHandle mTimerHandle_PropagateLava;
UPROPERTY(EditAnywhere, Category = "TextureToArray") class UWTextureToArray* LavaPixelArray; TArray
UPROPERTY(EditAnywhere, Category = "WLavaActor") float mMeltRate; UPROPERTY(EditAnywhere, Category = "WLavaActor") uint8 mMeltWhenNeighborIsBelow; UPROPERTY(EditAnywhere, Category = "WLavaActor") float mMeltValue;
int32 mCellSize; int32 mRowSize; int32 mArraySize; TArray
TArray<class UMaterialInstanceDynamic*> mDynamicMaterials; UPROPERTY() UTexture2D *mDynamicTexture; FUpdateTextureRegion2D* mUpdateTextureRegion; uint8* mDynamicColors; float* mDynamicColorsFloat; uint32 mDataSize; uint32 mDataSqrtSize;
UPROPERTY(ReplicatedUsing=OnRep_CurrentMeltTime) float mCurrentMeltTime; float mPreviousMeltTime; float mMeltRateDivisor;
UFUNCTION() void OnRep_CurrentMeltTime();
UPROPERTY(Replicated) bool bPropagating; float mPropagateTime;
};
The .cpp file
include "Spellsworn.h"
include "WLavaActor.h"
include "WGameState.h"
include "WGameMode.h"
include "StaticMeshResources.h"
include "Misc/TextureToArray/WTextureToArray.h"
define RED 2
define GREEN 1
define BLUE 0
define ALPHA 3
define ALPHA_CHECK 200
void 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; }); } }
AWLavaActor::AWLavaActor(const class FObjectInitializer& PCIP) : Super(PCIP) { PrimaryActorTick.bCanEverTick = true; mMeltRate = 0.05f; mMeltWhenNeighborIsBelow = 127; mMeltRateDivisor = 0.0f; SetReplicates(true); bPropagating = false; mDynamicColors = nullptr; mDynamicColorsFloat = nullptr; mUpdateTextureRegion = nullptr; }
void AWLavaActor::GetLifetimeReplicatedProps(TArray
DOREPLIFETIME(AWLavaActor, mCurrentMeltTime); DOREPLIFETIME(AWLavaActor, bPropagating); }
void AWLavaActor::BeginPlay() { Super::BeginPlay();
UWorld* world = GetWorld(); if (world) { AWGameState* gameState = world->GetGameState
void AWLavaActor::OnRep_CurrentMeltTime() { if (mCurrentMeltTime - mPreviousMeltTime > 2.f) { //GetWorld()->GetTimerManager().ClearTimer(mTimerHandle_PropagateLava); //GetWorld()->GetTimerManager().SetTimer(mTimerHandle_PropagateLava, this, &AWLavaActor::PropagateLava, GetDynamicMeltRate(), false);
int timesToPropagate = (mCurrentMeltTime - mPreviousMeltTime) / mMeltRate; for (int i = 0; i < timesToPropagate; ++i) { PropagateLava(); } } }
void AWLavaActor::EndPlay(const EEndPlayReason::Type EndPlayReason) { //PRINT("Destroying lava actor"); UWorld* world = GetWorld(); if (world) { AWGameState* gameState = world->GetGameState
void AWLavaActor::PostInitializeComponents() { Super::PostInitializeComponents(); if (mDynamicColors) delete[] mDynamicColors; if (mDynamicColorsFloat) delete[] mDynamicColorsFloat; if (mUpdateTextureRegion) delete mUpdateTextureRegion; if (!LavaPixelArray) return;
ExportedLavaValues = LavaPixelArray->GetArray(); int32 w, h; w = FMath::Sqrt(ExportedLavaValues->Num() / 4); h = w;
if (!IsRunningDedicatedServer()) { mDynamicMaterials.Empty(); mDynamicMaterials.Add(GetStaticMeshComponent()->CreateAndSetMaterialInstanceDynamic(0)); mDynamicTexture = UTexture2D::CreateTransient(w, h); mDynamicTexture->CompressionSettings = TextureCompressionSettings::TC_VectorDisplacementmap; mDynamicTexture->SRGB = 0; mDynamicTexture->AddToRoot(); mDynamicTexture->UpdateResource();
mUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, w, h);
mDynamicMaterials[0]->SetTextureParameterValue("DynamicTextureParam", mDynamicTexture); } mDataSize = w * h * 4; mDataSqrtSize = w * 4; mArraySize = w * h; mRowSize = w; mDynamicColors = new uint8[mDataSize]; mDynamicColorsFloat = new float[mArraySize];
ResetColors(); }
void AWLavaActor::BeginPropagateLava() { bPropagating = true; mPropagateTime = 0; mCurrentMeltTime = mPreviousMeltTime = 0.0f; //GetWorld()->GetTimerManager().ClearTimer(mTimerHandle_PropagateLava); //GetWorld()->GetTimerManager().SetTimer(mTimerHandle_PropagateLava, this, &AWLavaActor::PropagateLava, GetDynamicMeltRate(), false); ResetColors(); }
void AWLavaActor::EndPropagateLava() { bPropagating = false; mPropagateTime = 0; mCurrentMeltTime = mPreviousMeltTime = 0.0f; //GetWorld()->GetTimerManager().ClearTimer(mTimerHandle_PropagateLava); ResetColors(); }
void AWLavaActor::ResetColors() { mArrayOfIndexes.Empty(); FMemory::Memcpy(mDynamicColors, ExportedLavaValues->GetData(), mDataSize);
for (int i = 0; i < mArraySize; ++i) { mDynamicColorsFloat[i] = static_cast
UpdateLava(); }
int AWLavaActor::GetLavaValue(const FVector& aWorldPosition) const { if (!mDynamicColors) return 0; FVector origin; FVector bounds; GetActorBounds(false, origin, bounds); FVector corner = origin - bounds; FVector relativePosition = aWorldPosition - corner; float cellSize = (bounds.X*2) / FMath::Max
return value; }
void AWLavaActor::PropagateLava() { mPreviousMeltTime = mCurrentMeltTime; mCurrentMeltTime += mMeltRate;
//GetWorld()->GetTimerManager().ClearTimer(mTimerHandle_PropagateLava); //GetWorld()->GetTimerManager().SetTimer(mTimerHandle_PropagateLava, this, &AWLavaActor::PropagateLava, GetDynamicMeltRate(), false);
struct Neighbor { Neighbor(int indx, float v) :index(indx), val(v){} int index; float val; }; for (int32 i = 0; i < mArrayOfIndexes.Num(); ++i) { int index = mArrayOfIndexes[i]; int colorIndex = index * 4; uint8 colorValue = mDynamicColors[colorIndex + RED]; if (colorValue <= mDynamicColors[colorIndex + BLUE] || mDynamicColors[colorIndex + ALPHA] < ALPHA_CHECK) { mArrayOfIndexes.RemoveAt(i--); continue; }
int x = index % mRowSize; int y = FMath::FloorToInt(index / mRowSize); Neighbor neighbors[12] = { Neighbor((mRowSize * (y - 1) + (x - 0)) * 4, 1.00f), // Below Neighbor((mRowSize * (y - 1) + (x - 1)) * 4, 0.75f), // Below Left Neighbor((mRowSize * (y - 0) + (x - 1)) * 4, 1.00f), // Left Neighbor((mRowSize * (y + 1) + (x - 1)) * 4, 0.75f), // Above Left Neighbor((mRowSize * (y + 1) + (x + 0)) * 4, 1.00f), // Above Neighbor((mRowSize * (y + 1) + (x + 1)) * 4, 0.75f), // Above Right Neighbor((mRowSize * (y - 0) + (x + 1)) * 4, 1.00f), // Right Neighbor((mRowSize * (y - 1) + (x - 0)) * 4, 0.75f), // Below right Neighbor((mRowSize * (y - 2) + (x - 0)) * 4, 0.50f), // Below * 2 Neighbor((mRowSize * (y - 0) + (x - 2)) * 4, 0.50f), // Left * 2 Neighbor((mRowSize * (y + 2) + (x - 0)) * 4, 0.50f), // Above * 2 Neighbor((mRowSize * (y + 0) + (x + 2)) * 4, 0.50f), // Right * 2 };
for (uint8 n = 0; n < 12; ++n) { if (n >= 0 && n < mDataSize) { colorValue = mDynamicColors[colorIndex + RED]; if (colorValue <= 0) break;
int rVal = mDynamicColors[neighbors[n].index + RED]; if (rVal < mMeltWhenNeighborIsBelow) { float propVal = mMeltValue * neighbors[n].val/* * (mDynamicColorsFloat[index] / 255.f)*/; mDynamicColorsFloat[index] -= propVal; mDynamicColorsFloat[index] = FMath::Max
UpdateLava(); }
void AWLavaActor::Tick(float DeltaTime) { Super::Tick(DeltaTime);
if(bPropagating) { mPropagateTime += DeltaTime; float dynamicMeltRate = GetDynamicMeltRate(); while(mPropagateTime >= dynamicMeltRate) { mPropagateTime -= dynamicMeltRate; PropagateLava(); } } }
void AWLavaActor::UpdateLava() { if (!IsRunningDedicatedServer()) { UpdateTextureRegions(mDynamicTexture, 0, 1, mUpdateTextureRegion, mDataSqrtSize, (uint32)4, mDynamicColors, false); mDynamicMaterials[0]->SetTextureParameterValue("DynamicTextureParam", mDynamicTexture); } }
Retrieved from "https://wiki.unrealengine.com/index.php?title=Procedural_Materials&oldid=379"