mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
1112 lines
40 KiB
C++
1112 lines
40 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 "ShaderLabSettingsApplier.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
|
|
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.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.
|
|
*/
|
|
static bool EmitIntrinsics(
|
|
UMaterial& Material,
|
|
EShaderLabIntrinsicFrequency Stage,
|
|
FString& InOutBody,
|
|
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('_'); };
|
|
|
|
int32 i = 0;
|
|
while (i < Len)
|
|
{
|
|
const bool bBoundary = (i == 0) || !IsIdent(Body[i - 1]);
|
|
if (bBoundary && i + 4 <= Len &&
|
|
Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT(':') && Body[i + 3] == TEXT(':'))
|
|
{
|
|
int32 j = i + 4;
|
|
while (j < Len && IsIdent(Body[j])) { ++j; }
|
|
const FString Name = Body.Mid(i + 4, j - (i + 4));
|
|
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;
|
|
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)
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("Unknown intrinsic 'UE::%s'"), *Name));
|
|
bOk = false;
|
|
}
|
|
else if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
|
|
{
|
|
OutErrors.Add(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(FString::Printf(TEXT("Intrinsic 'UE::%s' is vertex-only and cannot be used in a pixel body"), *Name));
|
|
bOk = false;
|
|
}
|
|
else
|
|
{
|
|
// Usage is per-instance, so intrinsics emit their node with no base-level usage gate.
|
|
FString MakeError;
|
|
UMaterialExpression* Expr = Desc->MakeNode(Material, SplitArgs(ArgsRaw), MakeError);
|
|
if (!Expr)
|
|
{
|
|
OutErrors.Add(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 include plus any user `Includes { }` paths to a generated Custom node. */
|
|
static void AddIncludes(UMaterialExpressionCustom& Custom, const FShaderLabModel& Model)
|
|
{
|
|
Custom.IncludeFilePaths.Add(SHADERLAB_COMMON_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, 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, 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, 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, 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;
|
|
}
|