Download

My observations and tricks with the Material editor.

So, i’ve been working on following a unity tutorial on procedural generating terrain (here) and working on the shader part for textures on it, i had to fudge a lot with the custom nodes to get it to work.

First off, there’s many ways of doing things, this is what i’ve done following the tutorial mentioned above.

All scalar values are floats, all vectors (2,3,4) are also floats, in shader code to allow for multi compile, you would use MaterialFloat or the equivalent vector versions MaterialFloat2, MaterialFloat3, MaterialFloat4. You can use int/uint and their multi value counterpart with no issues as, i believe, that all shader languages have support for them.

You can view how the shader code will look like under “Windows > Shader Code” in the menu bar at the top of the material editor. There’s various version, the HLSL is one that is the “standard” from which the others will derive from.

Custom nodes can be annoying to use BUT there’s ways to use them effectively.
Custom nodes are shader functions, ergo, there’s limitations to what you can do.
Except that you can break out of that shader function by having this in it:


  return 0.0;
}

Using this, you can have custom functions and structures along with defines and constants, though your millage will vary as sometimes the compiler will complain about some of it.
The one thing you MUST do if you are breaking out of the function is to not end another function with a “}”;

Example:


  return 0.0;
}
MaterialFloat4 triplanar(MaterialFloat3 worldPos, MaterialFloat scale, MaterialFloat3 blendAxes, Texture2DArray textures, SamplerState sampler, MaterialFloat3 uvi){
    MaterialFloat3 scaledWorldPos = worldPos / scale;
    MaterialFloat3 scaledX = MaterialFloat3(scaledWorldPos.yz, 1) * uvi;
    MaterialFloat3 scaledY = MaterialFloat3(scaledWorldPos.xz, 1) * uvi;
    MaterialFloat3 scaledZ = MaterialFloat3(scaledWorldPos.xy, 1) * uvi;
    MaterialFloat4 xProjection = ProcessMaterialColorTextureLookup(Texture2DArraySample(textures, sampler, scaledX)) * blendAxes.x;
    MaterialFloat4 yProjection = ProcessMaterialColorTextureLookup(Texture2DArraySample(textures, sampler, scaledY)) * blendAxes.y;
    MaterialFloat4 zProjection = ProcessMaterialColorTextureLookup(Texture2DArraySample(textures, sampler, scaledZ)) * blendAxes.z;
    return xProjection + yProjection + zProjection;
}
MaterialFloat inverseLerp(MaterialFloat a, MaterialFloat b, MaterialFloat value){
  return saturate((value-a)/(b-a));

Best way of doing this is to only have one custom node dedicated to functions to be accessed in other custom nodes.

Now, to use these functions, you have to connect the node to the node that you want to use. This ensures that the functions are added to the top of the custom node list when the shader is compiled.
Just have that function node not accept any input and label it appropriately.

Texture2DArrays are available in 4.25. To enable usage, you have to use the command:


r.AllowTexture2DArrayCreation

I dont know how the support is, but i have not experienced any issues.
Do note, Texture2DArrays require that ALL images be the SAME size and the SAME pixel format.

Texture Objects and Texture Samplers can use Texture2DArrays or regular Texture2Ds, they do not care. Depending on what you have inserted in them will determine what they output, in the case of a Texture Object, it will output a Texture2DArray or a Texture2D which then can be used in a custom node or other nodes.

If you are using a Texture Object in a custom node, it will pass in a sampler state for that texture object for the custom node to use with the input’s name and “Sampler” at the end of it.
So if you have an input named “TextureOBJ” it will have an accompanying sampler state named “TextureOBJSampler” for usage with sampling functions.
For sampling within the custom node, while its fine to use the hlsl code to sample, it can be useful for multi compile to use:

For regular Texture2Ds:


ProcessMaterialColorTextureLookup(Texture2DSample(<texture>, <sampler>, <uvs>))

For regular Texture2DArrays:


ProcessMaterialColorTextureLookup(Texture2DArraySample(<texture>, <sampler>, <uvs>))

To pass arrays to a shader is simple yet also annoying. There is no native support for arrays in UE shaders. So pack them in a texture.
Here’s what I have to be able to easily create these textures:
H:


#pragma once
#include "CoreMinimal.h"
#include "Engine\Texture2DArray.h"
class API TextureGenerator {
public:
    static UTexture2D* CreateTextureFromBGRA(TArray<FColor> data, int width, int height);
    static UTexture2D* CreateRGBA32BitTexture(TArray<FLinearColor> data, int width, int height);

    static UTexture2D* TextureFromColorMap(TArray<FColor> colorMap, int width, int height){
        return TextureGenerator::CreateTextureFromBGRA(colorMap, width, height);
    }
    static UTexture2D* TextureFromColorMapSqr(TArray<FColor> colorMap, int size){
        return TextureGenerator::CreateTextureFromBGRA(colorMap, size, size);
    }
    static UTexture2D* TextureFromColorMap(TArray<FLinearColor> colorMap, int width, int height){
        return TextureGenerator::CreateRGBA32BitTexture(colorMap, width, height);
    }
    static UTexture2D* TextureFromColorMapSqr(TArray<FLinearColor> colorMap, int size){
        return TextureGenerator::CreateRGBA32BitTexture(colorMap, size, size);
    }
    static UTexture2D* TextureFromHeightMap(TArray<TArray<float>> heightmap);
    static UTexture2D* UpdateTextureFromHeightMap(TArray<TArray<float>> heightmap, UTexture2D* texture = nullptr);
    static UTexture2D* UpdateTextureFromColorMap(TArray<FColor> colorMap, int width, int height, UTexture2D* texture = nullptr);
    static UTexture2D* UpdateTextureFromColorMapSqr(TArray<FColor> colorMap, int size, UTexture2D* texture = nullptr){
        return UpdateTextureFromColorMap(colorMap, size, size, texture);
    }
    static UTexture2D* UpdateTextureFromColorMap(TArray<FLinearColor> colorMap, int width, int height, UTexture2D* texture = nullptr);
    static UTexture2D* UpdateTextureFromColorMapSqr(TArray<FLinearColor> colorMap, int size, UTexture2D* texture = nullptr){
        return UpdateTextureFromColorMap(colorMap, size, size, texture);
    }

    static UTexture2D* CreateTextureFrom32BitFloat(TArray<float> data, int width, int height);
    static UTexture2D* UpdateTextureFrom32BitFloat(TArray<float> data, int width, int height, UTexture2D* texture);
    static UTexture2D* CreateTextureFrom32BitFloat(TArray<TArray<float>> data);
    static UTexture2D* UpdateTextureFrom32BitFloat(TArray<TArray<float>> data, UTexture2D* texture);
    static UTexture2DArray* CreateTexture2DArray(TArray<UTexture2D*> data, int width, int height, int depth, EPixelFormat format = PF_B8G8R8A8);
    static UTexture2DArray* UpdateTexture2DArray(TArray<UTexture2D*> data, int width, int height, int depth, EPixelFormat format = PF_B8G8R8A8, UTexture2DArray* texture = nullptr);
private:
    static UTexture2DArray* CreateTransientArray(int32 InSizeX, int32 InSizeY, int32 InSizeZ, EPixelFormat InFormat = PF_B8G8R8A8, const FName InName = NAME_None);
};

CPP:


#include "TextureGenerator.h"
#include "ImageUtils.h"
UTexture2D* TextureGenerator::CreateTextureFromBGRA(TArray<FColor> data, int width, int height) {
    /*FCreateTexture2DParameters params;
    params.bDeferCompression = true;
    params.bSRGB = false;
    params.bUseAlpha = false;
    params.TextureGroup = TextureGroup::TEXTUREGROUP_Pixels2D;
    return FImageUtils::CreateTexture2D(
        width, height, //size
        data, //TArray<FColor>
        nullptr, //UObject* outer
        NAME_None, //Name
        RF_Transient, //Flags
        params //FCreateTexture2DParameters
    );*/
    UTexture2D* Texture;
    Texture = UTexture2D::CreateTransient(width, height, PF_B8G8R8A8);
    if(!Texture)
    {
        return nullptr;
    }
    #if WITH_EDITORONLY_DATA
    Texture->MipGenSettings = TMGS_NoMipmaps;
    #endif
    Texture->NeverStream = true;
    Texture->SRGB = 0;
    FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, data.GetData(), width * height * GPixelFormats[PF_B8G8R8A8].BlockBytes);
    Mip.BulkData.Unlock();
    Texture->UpdateResource();
    return Texture;
}
UTexture2D* TextureGenerator::CreateRGBA32BitTexture(TArray<FLinearColor> data, int width, int height) {
    UTexture2D* Texture;
    Texture = UTexture2D::CreateTransient(width, height, PF_R32G32B32A32_UINT);
    if(!Texture)
    {
        return nullptr;
    }
    #if WITH_EDITORONLY_DATA
    Texture->MipGenSettings = TMGS_NoMipmaps;
    #endif
    Texture->NeverStream = true;
    Texture->SRGB = 0;
    FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, data.GetData(), width * height * GPixelFormats[PF_R32G32B32A32_UINT].BlockBytes);
    Mip.BulkData.Unlock();
    Texture->UpdateResource();
    return Texture;
}
UTexture2D* TextureGenerator::TextureFromHeightMap(TArray<TArray<float>> heightmap){
    int width = heightmap[0].Num();
    int height = heightmap.Num();
    TArray<FColor> colorMap;
    colorMap.SetNumUninitialized(width * height, false);
    for (int y = 0; y < height; y++){
        for (int x = 0; x < width; x++){
            uint8 col = uint8(heightmap[y][x] * 255);
            colorMap[y * width + x] = FColor(col,col,col,255);
        }
    }
    return TextureFromColorMap(colorMap, width, height);
}
UTexture2D* TextureGenerator::UpdateTextureFromHeightMap(TArray<TArray<float>> heightmap, UTexture2D* texture){
    int width = heightmap[0].Num();
    int height = heightmap.Num();
    TArray<FColor> colorMap;
    colorMap.SetNumUninitialized(width * height, false);
    for (int y = 0; y < height; y++){
        for (int x = 0; x < width; x++){
            uint8 col = uint8(heightmap[y][x] * 255);
            colorMap[y * width + x] = FColor(col,col,col,255);
        }
    }
    if (texture == nullptr)
        return TextureFromColorMap(colorMap, width, height);
    return UpdateTextureFromColorMap(colorMap, width, height, texture);
}
UTexture2D* TextureGenerator::UpdateTextureFromColorMap(TArray<FColor> colorMap, int width, int height, UTexture2D* texture){
    if (texture == nullptr)
        return CreateTextureFromBGRA(colorMap, width, height);
    FTexture2DMipMap& Mip = texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, colorMap.GetData(), width * height * GPixelFormats[PF_B8G8R8A8].BlockBytes);
    Mip.BulkData.Unlock();
    texture->UpdateResource();
    return texture;
}
UTexture2D* TextureGenerator::UpdateTextureFromColorMap(TArray<FLinearColor> colorMap, int width, int height, UTexture2D* texture){
    if (texture == nullptr)
        return CreateRGBA32BitTexture(colorMap, width, height);
    FTexture2DMipMap& Mip = texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, colorMap.GetData(), width * height * GPixelFormats[PF_R32G32B32A32_UINT].BlockBytes);
    Mip.BulkData.Unlock();
    texture->UpdateResource();
    return texture;
}
UTexture2D* TextureGenerator::CreateTextureFrom32BitFloat(TArray<float> data, int width, int height) {
    UTexture2D* Texture;
    Texture = UTexture2D::CreateTransient(width, height, PF_R32_FLOAT);
    if(!Texture)
    {
        return nullptr;
    }
    #if WITH_EDITORONLY_DATA
    Texture->MipGenSettings = TMGS_NoMipmaps;
    #endif
    Texture->NeverStream = true;
    Texture->SRGB = 0;

    Texture->LODGroup = TextureGroup::TEXTUREGROUP_Pixels2D;
    FTexture2DMipMap& Mip = Texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, data.GetData(), width * height * GPixelFormats[PF_R32_FLOAT].BlockBytes);
    Mip.BulkData.Unlock();
    Texture->UpdateResource();
    return Texture;
}
UTexture2D* TextureGenerator::UpdateTextureFrom32BitFloat(TArray<float> data, int width, int height, UTexture2D* texture){
    if (texture == nullptr)
        return CreateTextureFrom32BitFloat(data, width, height);
    FTexture2DMipMap& Mip = texture->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(Data, data.GetData(), width * height * GPixelFormats[PF_R32_FLOAT].BlockBytes);
    Mip.BulkData.Unlock();
    texture->UpdateResource();
    return texture;
}
UTexture2D* TextureGenerator::CreateTextureFrom32BitFloat(TArray<TArray<float>> data){
    int width = data[0].Num();
    int height = data.Num();
    TArray<float> dataArray;
    dataArray.SetNumUninitialized(width * height, false);
    for (int y = 0; y < height; y++)
        for (int x = 0; x < width; x++)
            dataArray[y * width + x] = data[y][x];
    return CreateTextureFrom32BitFloat(dataArray, width, height);
}
UTexture2D* TextureGenerator::UpdateTextureFrom32BitFloat(TArray<TArray<float>> data, UTexture2D* texture){
    int width = data[0].Num();
    int height = data.Num();
    TArray<float> dataArray;
    dataArray.SetNumUninitialized(width * height, false);
    for (int y = 0; y < height; y++)
        for (int x = 0; x < width; x++)
            dataArray[y * width + x] = data[y][x];
    if (texture == nullptr)
        return CreateTextureFrom32BitFloat(dataArray, width, height);
    return UpdateTextureFrom32BitFloat(dataArray, width, height, texture);
}
UTexture2DArray* TextureGenerator::CreateTexture2DArray(TArray<UTexture2D*> data, int width, int height, int depth, EPixelFormat format){
    UTexture2DArray* textureArray = TextureGenerator::CreateTransientArray(width, height, depth, format);
    if (!textureArray) return nullptr;
#if WITH_EDITORONLY_DATA
    textureArray->MipGenSettings = TMGS_NoMipmaps;
#endif
    textureArray->NeverStream = true;
    textureArray->SRGB = 0;
#if WITH_EDITORONLY_DATA
    textureArray->SourceTextures = data;
#else
    //copy the textures?
    FTexture2DMipMap& Mip = textureArray->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    /*TArray<void*> inData;
    for (int i=0; i<data.Num();i++){
        inData.Add(data->PlatformData->Mips[0]);
    }*/
    FMemory::Memcpy(Data, data.GetData(), width * height * depth * GPixelFormats[format].BlockBytes);
    //FMemory::Memcpy(Data, inData.GetData(), width * height * depth * GPixelFormats[format].BlockBytes);
    Mip.BulkData.Unlock();
#endif
    textureArray->UpdateResource();
    return textureArray;
}
UTexture2DArray* TextureGenerator::UpdateTexture2DArray(TArray<UTexture2D*> data, int width, int height, int depth, EPixelFormat format, UTexture2DArray* textureArray){
    if (textureArray == nullptr)
        return CreateTexture2DArray(data, width, height, depth, format);
#if WITH_EDITORONLY_DATA
    textureArray->SourceTextures = data;
#else
    //copy the textures?
    FTexture2DMipMap& Mip = textureArray->PlatformData->Mips[0];
    void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);
    /*TArray<void*> inData;
    for (int i=0; i<data.Num();i++){
        inData.Add(data->PlatformData->Mips[0]);
    }*/
    FMemory::Memcpy(Data, data.GetData(), width * height * depth * GPixelFormats[format].BlockBytes);
    //FMemory::Memcpy(Data, inData.GetData(), width * height * depth * GPixelFormats[format].BlockBytes);
    Mip.BulkData.Unlock();
#endif
    textureArray->UpdateResource();
    return textureArray;
}
UTexture2DArray* TextureGenerator::CreateTransientArray(int32 InSizeX, int32 InSizeY, int32 InSizeZ, EPixelFormat InFormat, const FName InName)
{
    UTexture2DArray* NewTexture = NULL;
    if (InSizeX > 0 && InSizeY > 0 && InSizeZ > 0 &&
        (InSizeX % GPixelFormats[InFormat].BlockSizeX) == 0 &&
        (InSizeY % GPixelFormats[InFormat].BlockSizeY) == 0 &&
        (InSizeZ % GPixelFormats[InFormat].BlockSizeZ) == 0)
    {
        NewTexture = NewObject<UTexture2DArray>(
            GetTransientPackage(),
            InName,
            RF_Transient
            );
        NewTexture->PlatformData = new FTexturePlatformData();
        NewTexture->PlatformData->SizeX = InSizeX;
        NewTexture->PlatformData->SizeY = InSizeY;
        NewTexture->PlatformData->SetNumSlices(InSizeZ);
        NewTexture->PlatformData->PixelFormat = InFormat;
        // Allocate first mipmap.
        int32 NumBlocksX = InSizeX / GPixelFormats[InFormat].BlockSizeX;
        int32 NumBlocksY = InSizeY / GPixelFormats[InFormat].BlockSizeY;
        int32 NumBlocksZ = InSizeZ / GPixelFormats[InFormat].BlockSizeZ;
        FTexture2DMipMap* Mip = new FTexture2DMipMap();
        NewTexture->PlatformData->Mips.Add(Mip);
        Mip->SizeX = InSizeX;
        Mip->SizeY = InSizeY;
        Mip->SizeZ = InSizeZ;
        Mip->BulkData.Lock(LOCK_READ_WRITE);
        Mip->BulkData.Realloc(NumBlocksX * NumBlocksY * NumBlocksZ * GPixelFormats[InFormat].BlockBytes);
        Mip->BulkData.Unlock();
    }
    else
    {
        UE_LOG(LogTexture, Warning, TEXT("Invalid parameters specified for UTexture2D::CreateTransient()"));
    }
    return NewTexture;
}

It’s not the best way of doing it but it does work. You will have to add “RenderCore” to your build.cs file as GPixelFormats array is in there.
To use the float array textures, the easiest way is through a custom node and a texture object.
Example:



Texture[uint2(index,0).r


The “r” is important as this is the only way to get a single float out of the texture without any issues as the texture’s pixel format is PF_R32_FLOAT and it’s all packed in the red channel.
You can also do multidimentional float arrays also, the same applies.
Example:



Texture[uint2(xIndex, yIndex)].r


Hopefully this helps out in people’s venture into custom shader code in Unreal Engine.

So, for those that would like to directly include external files into the custom node via the “Include Files Paths”, i found this thread: https://forums.unrealengine.com/deve…-demo-download
Though i did some tweaks so that it only will work when compiling an editor.
Header:



#pragma once
#include "CoreMinimal.h"
#if WITH_EDITOR
#include "Modules/ModuleManager.h"
class F<GAME_MODULE_NAME_HERE>Module : public FDefaultGameModuleImpl {
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
#endif

C++:


// Copyright Epic Games, Inc. All Rights Reserved.
#include "<MODULE HEADER>.h"
#if WITH_EDITOR
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
void F<GAME_MODULE_NAME_HERE>Module::StartupModule()
{
#if (ENGINE_MINOR_VERSION >= 21)    
    FString ShaderDirectory = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
    AddShaderSourceDirectoryMapping("/Project", ShaderDirectory);
#endif
}
void F<GAME_MODULE_NAME_HERE>Module::ShutdownModule()
{
}
IMPLEMENT_PRIMARY_GAME_MODULE( F<GAME_MODULE_NAME_HERE>Module, <MODULE NAME>, "<MODULE NAME>" );
#else
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, <MODULE NAME>, "<MODULE NAME>" );
#endif

To use the “Include Files Paths” sections, in the root of your project folder create “Shaders”, then when including the file in the custom node, add “/Project/” to the beginning.
This includes the file directly above the custom node function, so be sure to not reinclude it in another custom node in the same material to prevent redefinition or duplication errors.

If using hot reload, it will error on the “AddShaderSourceDirectoryMapping”, this can be safely ignored.