Files
UShaderLab/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp
2026-07-01 19:47:47 +08:00

1245 lines
46 KiB
C++

// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabGraphBuilder.h"
#include "ShaderLabModel.h"
#include "MaterialDomain.h"
#include "Engine/EngineTypes.h"
#include "Misc/Paths.h"
#include "Engine/Texture.h"
#include "Engine/Texture2D.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionStaticBoolParameter.h"
#include "Materials/MaterialExpressionStaticSwitch.h"
#include "Materials/MaterialExpressionSubstrate.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "ShaderLabIntrinsicRegistry.h"
#include "ShaderLabRuntimeBuilder.h"
#include "UObject/Class.h"
#include "ShaderLabSettingsApplier.h"
#include "UObject/UObjectGlobals.h"
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
namespace ShaderLabGraph
{
// --- Surface fields that map to Substrate Slab pins (in deterministic order). ---
struct FSlabFieldDef
{
const TCHAR* Field;
ECustomMaterialOutputType OutType;
};
static const FSlabFieldDef GSlabFields[] = {
{ TEXT("DiffuseAlbedo"), CMOT_Float3 },
{ TEXT("F0"), CMOT_Float3 },
{ TEXT("F90"), CMOT_Float3 },
{ TEXT("Roughness"), CMOT_Float1 },
{ TEXT("Anisotropy"), CMOT_Float1 },
{ TEXT("Normal"), CMOT_Float3 },
{ TEXT("Tangent"), CMOT_Float3 },
{ TEXT("SSSMFP"), CMOT_Float3 },
{ TEXT("SSSMFPScale"), CMOT_Float1 },
{ TEXT("SSSPhaseAnisotropy"), CMOT_Float1 },
{ TEXT("EmissiveColor"), CMOT_Float3 },
{ TEXT("SecondRoughness"), CMOT_Float1 },
{ TEXT("SecondRoughnessWeight"), CMOT_Float1 },
{ TEXT("FuzzRoughness"), CMOT_Float1 },
{ TEXT("FuzzAmount"), CMOT_Float1 },
{ TEXT("FuzzColor"), CMOT_Float3 },
{ TEXT("GlintValue"), CMOT_Float1 },
{ TEXT("GlintUV"), CMOT_Float2 },
};
static FExpressionInput* GetSlabPin(UMaterialExpressionSubstrateSlabBSDF* Slab, const FString& Field)
{
if (Field == TEXT("DiffuseAlbedo")) return &Slab->DiffuseAlbedo;
if (Field == TEXT("F0")) return &Slab->F0;
if (Field == TEXT("F90")) return &Slab->F90;
if (Field == TEXT("Roughness")) return &Slab->Roughness;
if (Field == TEXT("Anisotropy")) return &Slab->Anisotropy;
if (Field == TEXT("Normal")) return &Slab->Normal;
if (Field == TEXT("Tangent")) return &Slab->Tangent;
if (Field == TEXT("SSSMFP")) return &Slab->SSSMFP;
if (Field == TEXT("SSSMFPScale")) return &Slab->SSSMFPScale;
if (Field == TEXT("SSSPhaseAnisotropy")) return &Slab->SSSPhaseAnisotropy;
if (Field == TEXT("EmissiveColor")) return &Slab->EmissiveColor;
if (Field == TEXT("SecondRoughness")) return &Slab->SecondRoughness;
if (Field == TEXT("SecondRoughnessWeight")) return &Slab->SecondRoughnessWeight;
if (Field == TEXT("FuzzRoughness")) return &Slab->FuzzRoughness;
if (Field == TEXT("FuzzAmount")) return &Slab->FuzzAmount;
if (Field == TEXT("FuzzColor")) return &Slab->FuzzColor;
if (Field == TEXT("GlintValue")) return &Slab->GlintValue;
if (Field == TEXT("GlintUV")) return &Slab->GlintUV;
return nullptr;
}
// --- Vertex-stage output fields. ---
struct FVertexFieldDef
{
const TCHAR* Field;
ECustomMaterialOutputType OutType;
};
static const FVertexFieldDef GVertexFields[] = {
{ TEXT("WorldPositionOffset"), CMOT_Float3 },
{ TEXT("Displacement"), CMOT_Float1 },
{ TEXT("CustomizedUV0"), CMOT_Float2 },
{ TEXT("CustomizedUV1"), CMOT_Float2 },
{ TEXT("CustomizedUV2"), CMOT_Float2 },
{ TEXT("CustomizedUV3"), CMOT_Float2 },
};
/** Absolute, forward-slashed path for use inside an HLSL `#line N "path"` directive. */
static FString MakeLineDirectivePath(const FString& SourceFilePath)
{
FString Full = FPaths::ConvertRelativePathToFull(SourceFilePath);
Full.ReplaceInline(TEXT("\\"), TEXT("/"));
return Full;
}
/**
* Wrap a user HLSL body so shader-compiler errors map back to the .usl source: a `#line`
* directive sets the file+line to the body's origin, and a trailing directive points past it to a
* sentinel so errors in our generated epilogue are not mis-attributed to the user's file.
*/
static FString WrapBodyWithLineMapping(const FString& Body, int32 BodyLine, const FString& SrcPath)
{
// BodyLine is the source line of the char right after '{' (usually the newline ending that
// line); the body's real content starts on the next line. Empirically the compiler reports
// content one line high relative to `#line BodyLine`, so map with BodyLine-1.
const int32 MappedLine = FMath::Max(BodyLine - 1, 1);
return FString::Printf(TEXT("#line %d \"%s\"\n%s\n#line 1 \"ShaderLabGenerated.ush\"\n"),
MappedLine, *SrcPath, *Body);
}
/** True if `Token` appears in `Body` delimited by non-identifier characters. */
static bool ReferencesToken(const FString& Body, const FString& Token)
{
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
int32 From = 0;
while (true)
{
const int32 Idx = Body.Find(Token, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
if (Idx == INDEX_NONE)
{
return false;
}
const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' ');
const int32 AfterIdx = Idx + Token.Len();
const TCHAR After = (AfterIdx < Body.Len()) ? Body[AfterIdx] : TEXT(' ');
if (!IsIdent(Before) && !IsIdent(After))
{
return true;
}
From = Idx + Token.Len();
}
}
static UTexture* ResolveDefaultTexture(const FString& Token, bool& bOutIsNormal)
{
bOutIsNormal = (Token == TEXT("normal"));
const TCHAR* Path = nullptr;
if (Token == TEXT("white")) { Path = TEXT("/Engine/EngineResources/WhiteSquareTexture.WhiteSquareTexture"); }
else if (Token == TEXT("black")) { Path = TEXT("/Engine/EngineResources/Black.Black"); }
else if (Token == TEXT("grey") || Token == TEXT("gray")) { Path = TEXT("/Engine/EngineResources/GreyTexture.GreyTexture"); }
else if (Token == TEXT("normal")) { Path = TEXT("/Engine/EngineMaterials/DefaultNormal.DefaultNormal"); }
UTexture* Tex = nullptr;
if (Path)
{
Tex = LoadObject<UTexture>(nullptr, Path);
}
else if (!Token.IsEmpty())
{
Tex = LoadObject<UTexture>(nullptr, *Token);
}
if (!Tex)
{
Tex = LoadObject<UTexture>(nullptr, TEXT("/Engine/EngineResources/WhiteSquareTexture.WhiteSquareTexture"));
}
return Tex;
}
static EMaterialDomain MapDomain(EShaderLabDomain D)
{
switch (D)
{
case EShaderLabDomain::PostProcess: return MD_PostProcess;
case EShaderLabDomain::UI: return MD_UI;
case EShaderLabDomain::Decal: return MD_DeferredDecal;
case EShaderLabDomain::Surface:
default: return MD_Surface;
}
}
static EBlendMode MapBlend(EShaderLabBlendMode B)
{
switch (B)
{
case EShaderLabBlendMode::Masked: return BLEND_Masked;
case EShaderLabBlendMode::Translucent: return BLEND_Translucent;
case EShaderLabBlendMode::Additive: return BLEND_Additive;
case EShaderLabBlendMode::Modulate: return BLEND_Modulate;
case EShaderLabBlendMode::Opaque:
default: return BLEND_Opaque;
}
}
template <typename T>
static T* NewExpr(UMaterial& Material, int32& IoY, int32 Column)
{
T* Expr = NewObject<T>(&Material);
Material.GetExpressionCollection().AddExpression(Expr);
Expr->MaterialExpressionEditorX = Column;
Expr->MaterialExpressionEditorY = IoY;
IoY += 120;
return Expr;
}
/** One intrinsic call resolved to a Custom-node input wired from an engine expression node. */
struct FIntrinsicWire
{
FName InputName;
UMaterialExpression* Expr = nullptr;
int32 OutputIndex = 0;
};
/** Split a call's argument text into trimmed, top-level comma-separated literals. */
static TArray<FString> SplitArgs(const FString& ArgsRaw)
{
TArray<FString> Out;
if (ArgsRaw.TrimStartAndEnd().IsEmpty())
{
return Out;
}
ArgsRaw.ParseIntoArray(Out, TEXT(","), /*CullEmpty*/ false);
for (FString& A : Out)
{
A.TrimStartAndEndInline();
}
return Out;
}
/** A stable, identifier-safe suffix encoding a call's literal args (e.g. "0, 2.0" -> "0_2_0"). */
static FString MakeArgSig(const FString& ArgsRaw)
{
FString Sig;
for (const TCHAR C : ArgsRaw)
{
if (FChar::IsAlnum(C)) { Sig.AppendChar(C); }
else if (!FChar::IsWhitespace(C)) { Sig.AppendChar(TEXT('_')); }
}
return Sig;
}
/**
* Scan a body for `UE_Name(args)` intrinsic calls, create the backing expression node for each
* unique (name,args), collect the resulting Custom-node inputs, and rewrite the body so each call
* becomes its input variable — space-padded to the original call's length so line/column layout is
* preserved (keeps `#line` compile-error mapping accurate). Returns false (and fills OutErrors) on
* an unknown intrinsic, a stage/usage violation, or a bad argument.
*/
/** True if `Arg` is a numeric literal (the only non-enum arg an intrinsic's const config accepts). */
static bool IsNumericLiteral(const FString& Arg)
{
const FString T = Arg.TrimStartAndEnd();
if (T.IsEmpty())
{
return false;
}
bool bAnyDigit = false;
for (int32 i = 0; i < T.Len(); ++i)
{
const TCHAR C = T[i];
if (FChar::IsDigit(C)) { bAnyDigit = true; }
else if (C == TEXT('.') || C == TEXT('+') || C == TEXT('-')
|| C == TEXT('e') || C == TEXT('E') || C == TEXT('f') || C == TEXT('F')) { /* allowed */ }
else { return false; }
}
return bAnyDigit;
}
static bool EmitIntrinsics(
UMaterial& Material,
EShaderLabIntrinsicFrequency Stage,
FString& InOutBody,
int32 BodyLine,
const FString& SrcPath,
TArray<FIntrinsicWire>& OutWires,
TArray<FString>& OutErrors)
{
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
const FString& Body = InOutBody;
const int32 Len = Body.Len();
FString Result;
Result.Reserve(Len);
TMap<FString, FName> InputByKey; // (Name + argsig) -> already-created input name (dedup)
bool bOk = true;
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
// Format a `path.usl(line,col): error: ` prefix from a body offset, so intrinsic diagnostics map
// to the real .usl location and flow through the same clickable/cook-failing path as compile errors.
auto Loc = [&Body, BodyLine, &SrcPath](int32 Offset) -> FString
{
int32 Line = BodyLine;
int32 Col = 1;
for (int32 p = 0; p < Offset && p < Body.Len(); ++p)
{
if (Body[p] == TEXT('\n')) { ++Line; Col = 1; }
else { ++Col; }
}
return FString::Printf(TEXT("%s(%d,%d): error: "), *SrcPath, Line, Col);
};
int32 i = 0;
while (i < Len)
{
const bool bBoundary = (i == 0) || !IsIdent(Body[i - 1]);
if (bBoundary && i + 3 <= Len &&
Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT('_'))
{
int32 j = i + 3;
while (j < Len && IsIdent(Body[j])) { ++j; }
const FString Name = Body.Mid(i + 3, j - (i + 3));
int32 k = j;
while (k < Len && FChar::IsWhitespace(Body[k])) { ++k; }
if (!Name.IsEmpty() && k < Len && Body[k] == TEXT('('))
{
// Read balanced (...) for the argument list.
int32 Depth = 0;
int32 m = k;
for (; m < Len; ++m)
{
if (Body[m] == TEXT('(')) { ++Depth; }
else if (Body[m] == TEXT(')')) { if (--Depth == 0) { break; } }
}
if (m < Len)
{
const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1));
const int32 CallLen = (m + 1) - i;
// Only registered node-intrinsics are rewritten into material-expression inputs.
// Any other `UE_X(...)` is left for the shader compiler: it's an HLSL library
// function (e.g. UE_Noise from ShaderLabFunctions.ush, auto-included) or a typo.
// Copy just the `UE_Name` identifier and keep scanning from the '(' — so any
// intrinsic nested in the arguments (e.g. UE_Noise(UE_WorldPosition(), ...)) still
// gets rewritten.
if (!Registry.Find(FName(*Name)))
{
Result += Body.Mid(i, j - i);
i = j;
continue;
}
const FString ArgSig = MakeArgSig(ArgsRaw);
const FString Key = Name + TEXT("|") + ArgSig;
FName InputName;
if (const FName* Existing = InputByKey.Find(Key))
{
InputName = *Existing;
}
else
{
const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name));
if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is pixel-only and cannot be used in a Vertex body"), *Name));
bOk = false;
}
else if (Desc->Frequency == EShaderLabIntrinsicFrequency::VertexOnly && Stage == EShaderLabIntrinsicFrequency::PixelOnly)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is vertex-only and cannot be used in a pixel body"), *Name));
bOk = false;
}
else
{
// Config args are baked into the node at graph-build time, so they must be
// compile-time constants (numeric literals, or the enum's token names) — a
// variable can't configure a node field. Reject non-literals instead of
// silently coercing them (e.g. Atoi("myVar") -> 0).
const TArray<FString> CallArgs = SplitArgs(ArgsRaw);
bool bArgsOk = true;
if (CallArgs.Num() > Desc->Params.Num())
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' takes at most %d argument(s), got %d"),
*Name, Desc->Params.Num(), CallArgs.Num()));
bArgsOk = false;
}
for (int32 a = 0; bArgsOk && a < CallArgs.Num(); ++a)
{
const FShaderLabIntrinsicParam& P = Desc->Params[a];
if (P.Enum)
{
if (P.Enum->GetValueByNameString(CallArgs[a]) == INDEX_NONE)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a %s token, got '%s'"),
*Name, *P.Name, *P.Enum->GetName(), *CallArgs[a]));
bArgsOk = false;
}
}
else if (!IsNumericLiteral(CallArgs[a]))
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a compile-time constant, got '%s'"),
*Name, *P.Name, *CallArgs[a]));
bArgsOk = false;
}
}
FString MakeError;
UMaterialExpression* Expr = bArgsOk ? Desc->MakeNode(Material, CallArgs, MakeError) : nullptr;
if (!bArgsOk)
{
bOk = false;
}
else if (!Expr)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s': %s"), *Name,
MakeError.IsEmpty() ? TEXT("failed to create node") : *MakeError));
bOk = false;
}
else
{
InputName = FName(*(FString(TEXT("SLI_")) + Name + (ArgSig.IsEmpty() ? TEXT("") : (FString(TEXT("_")) + ArgSig))));
InputByKey.Add(Key, InputName);
OutWires.Add(FIntrinsicWire{ InputName, Expr, Desc->OutputIndex });
}
}
}
// Substitute the call with its input variable, space-padded to keep columns stable.
FString Replacement = InputName.IsNone() ? FString() : InputName.ToString();
while (Replacement.Len() < CallLen) { Replacement.AppendChar(TEXT(' ')); }
Result += Replacement;
i = m + 1;
continue;
}
}
}
Result.AppendChar(Body[i]);
++i;
}
InOutBody = MoveTemp(Result);
return bOk;
}
/** A created property parameter node (shared across all slabs/values that reference it). */
struct FParamNode
{
UMaterialExpression* Expr = nullptr;
bool bIsTexture = false;
};
/** Add the shared struct + function-library includes plus any user `Includes { }` paths. */
static void AddIncludes(UMaterialExpressionCustom& Custom, const FShaderLabModel& Model)
{
Custom.IncludeFilePaths.Add(SHADERLAB_COMMON_INCLUDE);
// The function library provides the UE_ HLSL helpers (UE_Noise, UE_RotateAboutAxis, ...) that
// are left verbatim in the body (not rewritten into nodes).
Custom.IncludeFilePaths.Add(SHADERLAB_FUNCTIONS_INCLUDE);
for (const FString& Include : Model.Includes)
{
if (!Include.IsEmpty())
{
Custom.IncludeFilePaths.AddUnique(Include);
}
}
}
/**
* Emit the Custom node for a pixel-stage body that writes FShaderLabSurface fields and wire it into
* a fresh Substrate Slab BSDF. Returns the slab (nullptr only on error). When bAllowMaterialOutputs,
* S.Opacity / S.OpacityMask are wired to the material-level pins (single-Surface sugar path only).
*/
static UMaterialExpressionSubstrateSlabBSDF* BuildSlab(
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
TArray<const FSlabFieldDef*> UsedSlab;
for (const FSlabFieldDef& F : GSlabFields)
{
if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field))
{
UsedSlab.Add(&F);
}
}
const bool bUsesOpacity = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
const bool bUsesOpacityMask = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".OpacityMask"));
if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask)
{
return Slab; // Empty body: a default Substrate slab.
}
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab Surface");
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return nullptr;
}
for (const FIntrinsicWire& Wire : Wires)
{
FCustomInput In;
In.InputName = Wire.InputName;
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
for (const FShaderLabProperty& Prop : Model.Properties)
{
if (Prop.Type == EShaderLabPropertyType::StaticBool || !ReferencesToken(InBody, Prop.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
if (Node && Node->Expr)
{
FCustomInput In;
In.InputName = Prop.Name;
In.Input.Connect(0, Node->Expr);
Custom->Inputs.Add(In);
}
}
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
FString Code = FString::Printf(TEXT("FShaderLabSurface %s = ShaderLabDefaultSurface();\n{\n%s}\n"),
*OutParamName, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
int32 OutputIndex = 1; // index 0 is the (unused) main return
TArray<TPair<const FSlabFieldDef*, int32>> SlabOutputs;
for (const FSlabFieldDef* F : UsedSlab)
{
FCustomOutput Out;
Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field));
Out.OutputType = F->OutType;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *OutParamName, F->Field);
SlabOutputs.Add(TPair<const FSlabFieldDef*, int32>(F, OutputIndex));
++OutputIndex;
}
int32 OpacityOutIdx = INDEX_NONE;
int32 OpacityMaskOutIdx = INDEX_NONE;
if (bUsesOpacity)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_Opacity"); Out.OutputType = CMOT_Float1;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_Opacity = %s.Opacity;\n"), *OutParamName);
OpacityOutIdx = OutputIndex++;
}
if (bUsesOpacityMask)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_OpacityMask"); Out.OutputType = CMOT_Float1;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_OpacityMask = %s.OpacityMask;\n"), *OutParamName);
OpacityMaskOutIdx = OutputIndex++;
}
Code += TEXT("return 0.0f;\n");
Custom->Code = Code;
Custom->RebuildOutputs();
for (const TPair<const FSlabFieldDef*, int32>& Pair : SlabOutputs)
{
if (FExpressionInput* Pin = GetSlabPin(Slab, Pair.Key->Field))
{
Pin->Connect(Pair.Value, Custom);
}
}
if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); }
if (OpacityMaskOutIdx != INDEX_NONE) { EditorOnly.OpacityMask.Connect(OpacityMaskOutIdx, Custom); }
return Slab;
}
/**
* Build the Custom node for a PostProcess/UI entry (Domain = PostProcess/UI). The output struct has
* Color (-> material EmissiveColor) and Opacity (-> material Opacity); there is no Substrate slab.
*/
static bool BuildEmissiveEntry(
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const TCHAR* StructName, const TCHAR* DefaultFn,
const FString& OutParamName, const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab Emissive Entry");
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return false;
}
for (const FIntrinsicWire& Wire : Wires)
{
FCustomInput In;
In.InputName = Wire.InputName;
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
for (const FShaderLabProperty& Prop : Model.Properties)
{
if (Prop.Type == EShaderLabPropertyType::StaticBool || !ReferencesToken(InBody, Prop.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
if (Node && Node->Expr)
{
FCustomInput In;
In.InputName = Prop.Name;
In.Input.Connect(0, Node->Expr);
Custom->Inputs.Add(In);
}
}
const bool bUsesColor = ReferencesToken(InBody, OutParamName + TEXT(".Color"));
const bool bUsesOpacity = ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
FString Code = FString::Printf(TEXT("%s %s = %s();\n{\n%s}\n"),
StructName, *OutParamName, DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
int32 OutputIndex = 1;
int32 ColorOutIdx = INDEX_NONE;
int32 OpacityOutIdx = INDEX_NONE;
if (bUsesColor)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_Color"); Out.OutputType = CMOT_Float3;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_Color = %s.Color;\n"), *OutParamName);
ColorOutIdx = OutputIndex++;
}
if (bUsesOpacity)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_Opacity"); Out.OutputType = CMOT_Float1;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_Opacity = %s.Opacity;\n"), *OutParamName);
OpacityOutIdx = OutputIndex++;
}
Code += TEXT("return 0.0f;\n");
Custom->Code = Code;
Custom->RebuildOutputs();
if (ColorOutIdx != INDEX_NONE) { EditorOnly.EmissiveColor.Connect(ColorOutIdx, Custom); }
if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); }
return true;
}
/** Build a Custom node whose return value is the scalar Value-block body. Output 0 is the scalar. */
static UMaterialExpressionCustom* BuildValueNode(
UMaterial& Material, const FShaderLabValue& Value, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = FString::Printf(TEXT("ShaderLab Value %s"), *Value.Name.ToString());
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
FString Body = Value.Body;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return nullptr;
}
for (const FIntrinsicWire& Wire : Wires)
{
FCustomInput In;
In.InputName = Wire.InputName;
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
for (const FShaderLabProperty& Prop : Model.Properties)
{
if (Prop.Type == EShaderLabPropertyType::StaticBool || !ReferencesToken(Value.Body, Prop.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
if (Node && Node->Expr)
{
FCustomInput In;
In.InputName = Prop.Name;
In.Input.Connect(0, Node->Expr);
Custom->Inputs.Add(In);
}
}
// The body itself contains `return <scalar>;`, so it is the Custom function's body directly.
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
Custom->Code = WrapBodyWithLineMapping(Body, Value.BodyLine, SrcPath);
Custom->RebuildOutputs();
return Custom;
}
/** Connect a topology mix factor (literal / Value block / Scalar property) to an operator scalar pin. */
static bool ConnectFactor(
UMaterial& Material, FExpressionInput& Target, const FShaderLabFactor& Factor,
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
{
if (Factor.Kind == FShaderLabFactor::EKind::Literal)
{
UMaterialExpressionConstant* Const = NewExpr<UMaterialExpressionConstant>(Material, IoY, -300);
Const->R = Factor.Literal;
Target.Connect(0, Const);
return true;
}
if (UMaterialExpressionCustom* const* ValueNode = ValueByName.Find(Factor.Name))
{
Target.Connect(0, *ValueNode);
return true;
}
if (const FParamNode* Node = PropertyNodes.Find(Factor.Name))
{
if (Node->Expr && !Node->bIsTexture)
{
Target.Connect(0, Node->Expr);
return true;
}
}
OutErrors.Add(FString::Printf(
TEXT("Topology factor '%s' is neither a Value block nor a Scalar property"), *Factor.Name.ToString()));
return false;
}
/** Recursively build the Substrate expression for topology node `Index`. Returns nullptr on error. */
static UMaterialExpression* BuildTopologyNode(
UMaterial& Material, int32 Index, const FShaderLabModel& Model,
const TMap<FName, UMaterialExpressionSubstrateSlabBSDF*>& SlabByName,
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
{
if (!Model.Topology.IsValidIndex(Index))
{
OutErrors.Add(TEXT("Invalid topology node index"));
return nullptr;
}
const FShaderLabTopoNode& Node = Model.Topology[Index];
if (Node.Op == EShaderLabOp::SlabRef)
{
if (UMaterialExpressionSubstrateSlabBSDF* const* Found = SlabByName.Find(Node.SlabRef))
{
return *Found;
}
OutErrors.Add(FString::Printf(TEXT("FrontMaterial references unknown Slab '%s'"), *Node.SlabRef.ToString()));
return nullptr;
}
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors);
UMaterialExpression* ChildB = (Node.ChildB != INDEX_NONE)
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors)
: nullptr;
if (!ChildA || (Node.ChildB != INDEX_NONE && !ChildB))
{
return nullptr;
}
switch (Node.Op)
{
case EShaderLabOp::VerticalLayer:
{
UMaterialExpressionSubstrateVerticalLayering* N = NewExpr<UMaterialExpressionSubstrateVerticalLayering>(Material, IoY, -150);
N->Top.Connect(0, ChildA);
N->Base.Connect(0, ChildB);
return ConnectFactor(Material, N->Thickness, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::HorizontalMix:
{
UMaterialExpressionSubstrateHorizontalMixing* N = NewExpr<UMaterialExpressionSubstrateHorizontalMixing>(Material, IoY, -150);
N->Background.Connect(0, ChildA);
N->Foreground.Connect(0, ChildB);
return ConnectFactor(Material, N->Mix, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::Add:
{
UMaterialExpressionSubstrateAdd* N = NewExpr<UMaterialExpressionSubstrateAdd>(Material, IoY, -150);
N->A.Connect(0, ChildA);
N->B.Connect(0, ChildB);
return N;
}
case EShaderLabOp::Weight:
{
UMaterialExpressionSubstrateWeight* N = NewExpr<UMaterialExpressionSubstrateWeight>(Material, IoY, -150);
N->A.Connect(0, ChildA);
return ConnectFactor(Material, N->Weight, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::Select:
{
UMaterialExpressionSubstrateSelect* N = NewExpr<UMaterialExpressionSubstrateSelect>(Material, IoY, -150);
N->A.Connect(0, ChildA);
N->B.Connect(0, ChildB);
return ConnectFactor(Material, N->SelectValue, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
}
default:
OutErrors.Add(TEXT("Unhandled topology operator"));
return nullptr;
}
}
}
bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors)
{
using namespace ShaderLabGraph;
// Reset to a clean graph + apply material settings.
Material.AssignExpressionCollection(FMaterialExpressionCollection());
Material.MaterialDomain = MapDomain(Model.Settings.Domain);
Material.BlendMode = MapBlend(Model.Settings.BlendMode);
Material.TwoSided = Model.Settings.bTwoSided ? 1 : 0;
Material.bUseMaterialAttributes = false;
// Reflected long-tail settings (no usage: the base is a template; usage is set per-instance).
// Identical to the runtime shell (same model -> same FShaderLabSettingsApplier). Bad settings are
// a hard build failure (contract style).
if (!FShaderLabSettingsApplier::ApplyReflectedSettings(Material, Model.RawSettings, OutErrors))
{
return false;
}
UMaterialEditorOnlyData* EditorOnly = Material.GetEditorOnlyData();
if (!EditorOnly)
{
OutErrors.Add(TEXT("Material has no editor-only data"));
return false;
}
// Per-pixel context is read via UE_* intrinsics, so the Surface entry takes just the output
// struct: `Surface(inout FShaderLabSurface S)`. For multi-slab there is no Surface param.
const FShaderLabEntryParam* SurfaceOutParam = Model.bHasSurface && Model.SurfaceParams.Num() > 0
? &Model.SurfaceParams.Last() : nullptr;
if (Model.bHasSurface && !SurfaceOutParam)
{
OutErrors.Add(TEXT("Surface(...) must take an (inout FShaderLabSurface) parameter"));
return false;
}
int32 ParamY = -400;
// True if a property is referenced by any body (Surface / Slabs / Values / Vertex) OR used directly
// as a topology mix factor (e.g. `VerticalLayer(Coat, Base, Thickness)` with Thickness a Scalar).
auto IsPropertyReferenced = [&Model](const FName PropName, const FString& NameStr) -> bool
{
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr)) { return true; }
for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, NameStr)) { return true; } }
for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, NameStr)) { return true; } }
if (Model.bHasVertex && ReferencesToken(Model.VertexBody, NameStr)) { return true; }
for (const FShaderLabTopoNode& Node : Model.Topology)
{
if (Node.bHasFactor && Node.Factor.Kind == FShaderLabFactor::EKind::Named && Node.Factor.Name == PropName)
{
return true;
}
}
return false;
};
// 1) Create a parameter node per property referenced by any stage.
TMap<FName, FParamNode> PropertyNodes;
// Static-switch selectors funneled into the ParameterAnchor: each is a StaticSwitch over two
// `#define <Name> 1` / `#define <Name> 0` Custom nodes, driven by the switch parameter. The anchor
// is compiled before the material attributes and compiles these, so the selected per-permutation
// `#define` is emitted ahead of every body's `#if` — per-permutation static switches with zero
// engine changes, and nothing wired onto the user's body nodes.
TArray<UMaterialExpression*> AnchorInputs;
for (const FShaderLabProperty& Prop : Model.Properties)
{
const FString NameStr = Prop.Name.ToString();
if (Prop.Type == EShaderLabPropertyType::StaticBool)
{
if (IsPropertyReferenced(Prop.Name, NameStr))
{
// Real static-switch parameter so Material Instances can override it (shown in the MIC editor).
// Reached for visibility via the selector below (which the anchor connects).
UMaterialExpressionStaticBoolParameter* E = NewExpr<UMaterialExpressionStaticBoolParameter>(Material, ParamY, -1000);
E->ParameterName = Prop.Name;
E->DefaultValue = Prop.bStaticBoolDefault ? 1 : 0;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
// Two trivial Custom nodes emit `#define Name 1` / `#define Name 0`; a StaticSwitch driven by
// the parameter selects one. The translator compiles ONLY the selected branch, so exactly one
// `#define` is produced per shader permutation (incl. the MIC's static override).
auto MakeDefiner = [&](bool bValue) -> UMaterialExpressionCustom*
{
UMaterialExpressionCustom* D = NewExpr<UMaterialExpressionCustom>(Material, ParamY, -1300);
D->Description = TEXT("ShaderLab StaticSwitch Define");
D->OutputType = CMOT_Float1;
D->Code = TEXT("return 0;");
FCustomDefine DD;
DD.DefineName = NameStr;
DD.DefineValue = bValue ? TEXT("1") : TEXT("0");
D->AdditionalDefines.Add(DD);
return D;
};
UMaterialExpressionStaticSwitch* Selector = NewExpr<UMaterialExpressionStaticSwitch>(Material, ParamY, -1150);
Selector->A.Connect(0, MakeDefiner(true)); // selected when the switch is TRUE
Selector->B.Connect(0, MakeDefiner(false)); // selected when FALSE
Selector->Value.Connect(0, E);
Selector->DefaultValue = Prop.bStaticBoolDefault;
AnchorInputs.Add(Selector);
}
continue;
}
if (!IsPropertyReferenced(Prop.Name, NameStr))
{
continue; // Unused value property: skip (keeps the graph minimal and deterministic).
}
FParamNode Node;
switch (Prop.Type)
{
case EShaderLabPropertyType::Scalar:
{
UMaterialExpressionScalarParameter* E = NewExpr<UMaterialExpressionScalarParameter>(Material, ParamY, -1000);
E->ParameterName = Prop.Name;
E->DefaultValue = Prop.ScalarDefault;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
if (Prop.bHasRange)
{
E->SliderMin = Prop.RangeMin;
E->SliderMax = Prop.RangeMax;
}
Node.Expr = E;
break;
}
case EShaderLabPropertyType::Color:
case EShaderLabPropertyType::Vector:
{
UMaterialExpressionVectorParameter* E = NewExpr<UMaterialExpressionVectorParameter>(Material, ParamY, -1000);
E->ParameterName = Prop.Name;
E->DefaultValue = Prop.VectorDefault;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
Node.Expr = E;
break;
}
case EShaderLabPropertyType::Texture2D:
case EShaderLabPropertyType::TextureCube:
{
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
E->ParameterName = Prop.Name;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
bool bIsNormal = false;
E->Texture = ResolveDefaultTexture(Prop.TextureDefault, bIsNormal);
E->SamplerType = bIsNormal ? SAMPLERTYPE_Normal : SAMPLERTYPE_Color;
Node.Expr = E;
Node.bIsTexture = true;
break;
}
default:
break;
}
if (Node.Expr)
{
PropertyNodes.Add(Prop.Name, Node);
}
}
// 2) Build the pixel stage and connect it to FrontMaterial (what makes it a Substrate material).
if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::PostProcess)
{
// PostProcess domain: Color -> EmissiveColor, Opacity -> Opacity (no Substrate slab).
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabPostProcess"), TEXT("ShaderLabDefaultPostProcess"),
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, ParamY, OutErrors))
{
return false;
}
}
else if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::UI)
{
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabUI"), TEXT("ShaderLabDefaultUI"),
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, ParamY, OutErrors))
{
return false;
}
}
else if (Model.bHasSurface)
{
// Single-Surface sugar: one slab straight to FrontMaterial, with S.Opacity/S.OpacityMask
// allowed as material-level outputs.
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
if (!Slab)
{
return false;
}
EditorOnly->FrontMaterial.Connect(0, Slab);
}
else
{
// Multi-slab: each named Slab -> its own slab node; Value blocks -> scalar Custom nodes; the
// FrontMaterial topology tree mixes them; Opacity/OpacityMask come from named Value blocks.
TMap<FName, UMaterialExpressionSubstrateSlabBSDF*> SlabByName;
for (const FShaderLabSlab& SlabDecl : Model.Slabs)
{
if (SlabByName.Contains(SlabDecl.Name))
{
OutErrors.Add(FString::Printf(TEXT("Duplicate Slab name '%s'"), *SlabDecl.Name.ToString()));
return false;
}
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
if (!Slab)
{
return false;
}
SlabByName.Add(SlabDecl.Name, Slab);
}
TMap<FName, UMaterialExpressionCustom*> ValueByName;
for (const FShaderLabValue& ValueDecl : Model.Values)
{
if (ValueByName.Contains(ValueDecl.Name))
{
OutErrors.Add(FString::Printf(TEXT("Duplicate Value name '%s'"), *ValueDecl.Name.ToString()));
return false;
}
UMaterialExpressionCustom* ValueNode = BuildValueNode(
Material, ValueDecl, Model, PropertyNodes, ParamY, OutErrors);
if (!ValueNode)
{
return false;
}
ValueByName.Add(ValueDecl.Name, ValueNode);
}
// Every declared Slab must be reachable from FrontMaterial (contract: no dead slabs).
TSet<FName> ReferencedSlabs;
for (const FShaderLabTopoNode& Node : Model.Topology)
{
if (Node.Op == EShaderLabOp::SlabRef) { ReferencedSlabs.Add(Node.SlabRef); }
}
for (const FShaderLabSlab& SlabDecl : Model.Slabs)
{
if (!ReferencedSlabs.Contains(SlabDecl.Name))
{
OutErrors.Add(FString::Printf(TEXT("Slab '%s' is declared but never used in FrontMaterial"), *SlabDecl.Name.ToString()));
return false;
}
}
UMaterialExpression* Root = BuildTopologyNode(
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, ParamY, OutErrors);
if (!Root)
{
return false;
}
EditorOnly->FrontMaterial.Connect(0, Root);
// Material-level Opacity / OpacityMask from named Value blocks.
auto ConnectMaterialOutput = [&](FExpressionInput& Pin, FName ValueName, const TCHAR* What) -> bool
{
if (ValueName.IsNone()) { return true; }
UMaterialExpressionCustom* const* ValueNode = ValueByName.Find(ValueName);
if (!ValueNode)
{
OutErrors.Add(FString::Printf(TEXT("%s references unknown Value '%s'"), What, *ValueName.ToString()));
return false;
}
Pin.Connect(0, *ValueNode);
return true;
};
if (!ConnectMaterialOutput(EditorOnly->Opacity, Model.OpacityValueName, TEXT("Opacity"))) { return false; }
if (!ConnectMaterialOutput(EditorOnly->OpacityMask, Model.OpacityMaskValueName, TEXT("OpacityMask"))) { return false; }
}
// 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry
// takes just the output struct: `Vertex(inout FShaderLabVertex V)`.
if (Model.bHasVertex && Model.VertexParams.Num() >= 1)
{
const FShaderLabEntryParam& VtxOut = Model.VertexParams.Last();
TArray<const FVertexFieldDef*> UsedVtx;
for (const FVertexFieldDef& F : GVertexFields)
{
if (ReferencesToken(Model.VertexBody, VtxOut.Name + TEXT(".") + F.Field))
{
UsedVtx.Add(&F);
}
}
if (UsedVtx.Num() > 0)
{
UMaterialExpressionCustom* VCustom = NewExpr<UMaterialExpressionCustom>(Material, ParamY, -300);
VCustom->Description = TEXT("ShaderLab Vertex");
VCustom->OutputType = CMOT_Float1;
AddIncludes(*VCustom, Model);
FString Code;
// Intrinsics (Stage = vertex).
FString VertexBody = Model.VertexBody;
TArray<FIntrinsicWire> VtxIntrinsicWires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, OutErrors))
{
return false;
}
for (const FIntrinsicWire& Wire : VtxIntrinsicWires)
{
FCustomInput In;
In.InputName = Wire.InputName;
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
VCustom->Inputs.Add(In);
}
for (const FShaderLabProperty& Prop : Model.Properties)
{
if (Prop.Type == EShaderLabPropertyType::StaticBool)
{
continue;
}
if (!ReferencesToken(Model.VertexBody, Prop.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
if (!Node || !Node->Expr)
{
continue;
}
FCustomInput In;
In.InputName = Prop.Name;
In.Input.Connect(0, Node->Expr);
VCustom->Inputs.Add(In);
}
const FString VSrcPath = MakeLineDirectivePath(Model.SourceFilePath);
Code += FString::Printf(TEXT("FShaderLabVertex %s = ShaderLabDefaultVertex();\n{\n%s}\n"),
*VtxOut.Name, *WrapBodyWithLineMapping(VertexBody, Model.VertexBodyLine, VSrcPath));
int32 VOutputIndex = 1;
TArray<TPair<FString, int32>> VtxOutputs;
for (const FVertexFieldDef* F : UsedVtx)
{
FCustomOutput Out;
Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field));
Out.OutputType = F->OutType;
VCustom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *VtxOut.Name, F->Field);
VtxOutputs.Add(TPair<FString, int32>(F->Field, VOutputIndex));
++VOutputIndex;
}
Code += TEXT("return 0.0f;\n");
VCustom->Code = Code;
VCustom->RebuildOutputs();
for (const TPair<FString, int32>& Pair : VtxOutputs)
{
if (Pair.Key == TEXT("WorldPositionOffset"))
{
EditorOnly->WorldPositionOffset.Connect(Pair.Value, VCustom);
}
else if (Pair.Key == TEXT("Displacement"))
{
EditorOnly->Displacement.Connect(Pair.Value, VCustom);
}
else if (Pair.Key.StartsWith(TEXT("CustomizedUV")))
{
const int32 UvIndex = FCString::Atoi(*Pair.Key.Mid(12));
if (UvIndex >= 0 && UvIndex < 8)
{
EditorOnly->CustomizedUVs[UvIndex].Connect(Pair.Value, VCustom);
}
}
}
}
}
// Funnel every static-switch selector into the ParameterAnchor. The anchor is a CustomOutput compiled
// BEFORE the material attributes (ShouldCompileBeforeAttributes), so compiling it compiles each
// selector — emitting the selected `#define <Name> 0/1` for the current permutation ahead of every
// body's `#if`. This gives per-permutation static switches with zero engine changes, and keeps the
// machinery off the user's body nodes. The anchor also makes the switch parameters visible in the
// Material Instance editor (reached via selector -> Value -> parameter).
if (AnchorInputs.Num() > 0)
{
UMaterialExpressionShaderLabParameterAnchor* Anchor =
NewExpr<UMaterialExpressionShaderLabParameterAnchor>(Material, ParamY, -1300);
Anchor->Inputs.SetNum(AnchorInputs.Num());
for (int32 Index = 0; Index < AnchorInputs.Num(); ++Index)
{
Anchor->Inputs[Index].Connect(0, AnchorInputs[Index]);
}
}
Material.UpdateCachedExpressionData();
return true;
}
void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath)
{
using namespace ShaderLabGraph;
// A minimal, structurally-valid Substrate material: one Custom node -> Slab.DiffuseAlbedo -> FrontMaterial,
// so the Custom code is reached by translation. The code `#error`s with the diagnostics; that aborts the
// shader preprocessor, so the material fails to compile with our messages — surfacing everywhere real
// shader errors do (MIC editor red text, FMaterialResource::GetCompileErrors, cook). DiffuseAlbedo (a core
// BSDF pin) is used deliberately: EmissiveColor gets dead-stripped from the compiled permutation, which
// would drop the Custom function (and its #error) before the preprocessor ever sees it.
Material.AssignExpressionCollection(FMaterialExpressionCollection());
Material.MaterialDomain = MD_Surface;
Material.BlendMode = BLEND_Opaque;
Material.TwoSided = 0;
Material.bUseMaterialAttributes = false;
UMaterialEditorOnlyData* EditorOnly = Material.GetEditorOnlyData();
check(EditorOnly); // A UMaterial always has editor-only data in the editor (contract).
// `#error` halts at the first hit, so merge every diagnostic into one directive (newlines stripped to
// keep it a single preprocessor line). The `#line` makes the compiler error click through to the .usl;
// each merged message still carries its own precise `(line,col)` as text.
FString Combined;
for (int32 Index = 0; Index < Diagnostics.Num(); ++Index)
{
if (Index > 0) { Combined += TEXT(" ; "); }
Combined += Diagnostics[Index];
}
Combined.ReplaceInline(TEXT("\r"), TEXT(""));
Combined.ReplaceInline(TEXT("\n"), TEXT(" "));
if (Combined.IsEmpty()) { Combined = TEXT("ShaderLab: shader failed to build"); }
int32 IoY = 0;
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab Error");
Custom->OutputType = CMOT_Float3;
Custom->Code = FString::Printf(
TEXT("#line 1 \"%s\"\n#error ShaderLab: %s\n#line 1 \"ShaderLabGenerated.ush\"\nreturn float3(1,0,1);\n"),
*SrcPath, *Combined);
Custom->RebuildOutputs();
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
Slab->DiffuseAlbedo.Connect(0, Custom);
EditorOnly->FrontMaterial.Connect(0, Slab);
Material.UpdateCachedExpressionData();
}