Material Expression with Multiple Outputs in C++

I’m trying to write in c++ a custom material expression that will output 3 results in 3 different output pins. In the attached image you can see the idea I’m trying to follow: basically convert a material function to a material expression in order to don’t see what is inside of it.
So far I managed to create the node with all the pins I need and in the .cpp file I already have the function and the 3 different results. I’m stuck because I can’t find a solution to distribute the 3 results in the 3 output pins.
Is there anyone who can help me?
Thank you!

This is the example code for the node in the attached image (.h and .cpp files):

“CustomNodeMultiOutput.h”


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Materials/MaterialExpression.h"
#include "UObject/ObjectMacros.h"
#include "MaterialExpressionIO.h"
#include "CustomNodeMultiOutput.generated.h"

UCLASS(MinimalAPI)
class UMaterialExpressionCustomNodeMultiOutput : public UMaterialExpression
{
GENERATED_UCLASS_BODY()

UPROPERTY()
TArray<FString> ParamNames;

UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Random"))
FExpressionInput Input1;

UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Tiling"))
FExpressionInput Input2;

UPROPERTY(meta = (RequiredInput = "false", ToolTip = "Divisions"))
FExpressionInput Input3;

UPROPERTY(EditAnywhere, Category = MaterialExpressionEndless_UVs, meta = (OverridingInputProperty = "Random"))
float Input1Deafult;

UPROPERTY(EditAnywhere, Category = MaterialExpressionEndless_UVs, meta = (OverridingInputProperty = "Tiling"))
float Input2Deafult;

UPROPERTY(EditAnywhere, Category = MaterialExpressionEndless_UVs, meta = (OverridingInputProperty = "Divisions"))
int32 Input3Deafult;

//~ Begin UMaterialExpression Interface
#if WITH_EDITOR
virtual const TArray<FExpressionInput*> GetInputs() override;
virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
virtual void GetCaption(TArray<FString>& OutCaptions) const override;
virtual TArray<FExpressionOutput>& GetOutputs() override;
virtual FText GetKeywords() const override { return FText::FromString(TEXT("SOA")); }
#endif
//~ End UMaterialExpression Interface
};

“CustomNodeMultiOutput.cpp”


// Fill out your copyright notice in the Description page of Project Settings.


#include "CustomNodeMultiOutput.h"
#include "CoreMinimal.h"
#include "MaterialExpressionIO.h"
#include "Materials/MaterialExpression.h"
#include "MaterialCompiler.h"
#include "Materials/MaterialExpressionVectorNoise.h"

#if WITH_EDITOR
#include "MaterialGraph/MaterialGraphNode_Comment.h"
#include "MaterialGraph/MaterialGraphNode.h"
#endif //WITH_EDITOR

#define LOCTEXT_NAMESPACE "MaterialExpression"

UMaterialExpressionCustomNodeMultiOutput::UMaterialExpressionCustomNodeMultiOutput(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    // Structure to hold one-time initialization
    struct FConstructorStatics
    {
        FText NAME_Utility;
        FText NAME_SOA;
        FConstructorStatics()
            : NAME_Utility(LOCTEXT("Utility", "Utility"))
            , NAME_SOA(LOCTEXT("SOA", "SOA"))
        {
        }
    };
    static FConstructorStatics ConstructorStatics;

    bShowOutputNameOnPin = true;
    bHidePreviewWindow = true;

    // Parameters Default values
    Input1Deafult = 1.0f;
    Input2Deafult = 4.0f;
    Input3Deafult = 5.0f;

    ParamNames.Add(TEXT("Result 1"));
    ParamNames.Add(TEXT("Result 2"));
    ParamNames.Add(TEXT("Result 3"));

#if WITH_EDITORONLY_DATA
    MenuCategories.Add(ConstructorStatics.NAME_Utility);
    MenuCategories.Add(ConstructorStatics.NAME_SOA);

    Outputs.Reset();
    Outputs.Add(FExpressionOutput(TEXT(""), 1, 1, 1, 1, 0));
    Outputs.Add(FExpressionOutput(TEXT(""), 1, 1, 1, 1, 0));
    Outputs.Add(FExpressionOutput(TEXT(""), 1, 1, 1, 1, 0));

#endif
}


TArray<FExpressionOutput>& UMaterialExpressionCustomNodeMultiOutput::GetOutputs()
{
    Outputs[0].OutputName = *(ParamNames[0]);
    Outputs[1].OutputName = *(ParamNames[1]);
    Outputs[2].OutputName = *(ParamNames[2]);
    return Outputs;
}

#if WITH_EDITOR
int32 UMaterialExpressionCustomNodeMultiOutput::Compile(class FMaterialCompiler* Compiler, int32 OutputIndex)
{
    // if the input is hooked up, use it, otherwise use the internal constant
    float Input1Value = Input1.GetTracedInput().Expression ? Input1.Compile(Compiler) : Compiler->Constant(Input1Deafult);
    // if the input is hooked up, use it, otherwise use the internal constant
    float Input2Value = Input2.GetTracedInput().Expression ? Input2.Compile(Compiler) : Compiler->Constant(Input2Deafult);
    // if the input is hooked up, use it, otherwise use the internal constant
    int32 Input3Value = Input3.GetTracedInput().Expression ? Input3.Compile(Compiler) : Compiler->Constant(Input3Deafult);

    //////////////////////////////////////////////
    // FUNCTION example
    //////////////////////////////////////////////

    int32 value1 = Compiler->Mul(Input1Value, Input2Value);
    int32 Result1 = Compiler->Add(value1, Input2Value);                // Output 1

    int32 Result2 = Compiler->AppendVector(value1, Input2Value);    // Output 2

    int32 Result3 = Compiler->Div(Result2, Input3Value);            // Output 3

    return 0; //right now I don't know how to return the 3 results and pass them to the 3 output pins
}

const TArray<FExpressionInput*> UMaterialExpressionCustomNodeMultiOutput::GetInputs()
{
    TArray<FExpressionInput*> OutInputs;

    int32 InputIndex = 0;
    while (FExpressionInput * Ptr = GetInput(InputIndex++))
    {
        OutInputs.Add(Ptr);
    }

    return OutInputs;
}

// Material Expression Node Name
void UMaterialExpressionCustomNodeMultiOutput::GetCaption(TArray<FString>& OutCaptions) const
{
    OutCaptions.Add(TEXT("Custom MultiOutput"));
}
#endif // WITH_EDITOR

Same question here, do you have any solution now? :joy:

Hey! I think that this way may not work properly, for things related to shaders it’s better to create Unreal Shader Files (.usf) otherwise, it won’t work correctly. I haven’t tried though.