Support SL_INTERPOLATOR and local function

This commit is contained in:
Eragon-Brisingr
2026-07-02 15:14:13 +08:00
parent 0d7db515ae
commit c27af6af49
50 changed files with 938 additions and 73 deletions

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabGraphBuilder.h"
@@ -18,12 +18,17 @@
#include "Materials/MaterialExpressionSubstrate.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionVertexInterpolator.h"
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "ShaderLabIntrinsicRegistry.h"
#include "ShaderLabRuntimeBuilder.h"
#include "UObject/Class.h"
#include "ShaderLabSettingsApplier.h"
#include "UObject/UObjectGlobals.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/FileHelper.h"
#include "ShaderCore.h"
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
@@ -273,6 +278,8 @@ namespace ShaderLabGraph
int32 BodyLine,
const FString& SrcPath,
TArray<FIntrinsicWire>& OutWires,
const TMap<FName, UMaterialExpression*>& InterpByName,
TSet<FName>& UsedInterps,
TArray<FString>& OutErrors)
{
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
@@ -327,6 +334,50 @@ namespace ShaderLabGraph
const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1));
const int32 CallLen = (m + 1) - i;
// UE_Interpolator(Name): read a Vertex Interpolator's value in the pixel shader. Not a
// registry intrinsic — its arg is an interpolator name (not a numeric/enum literal), so
// handle it before the registry lookup and arg-literal validation below.
if (Name == TEXT("Interpolator"))
{
const FString InterpNameStr = ArgsRaw.TrimStartAndEnd();
FName InputName; // None on error -> substituted blank (error already recorded)
if (Stage == EShaderLabIntrinsicFrequency::VertexOnly)
{
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' is pixel-only and cannot be used in a Vertex or Interpolator body"));
bOk = false;
}
else if (InterpNameStr.IsEmpty())
{
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' requires an interpolator name argument"));
bOk = false;
}
else
{
const FString Key = FString(TEXT("Interpolator|")) + InterpNameStr;
if (const FName* Existing = InputByKey.Find(Key))
{
InputName = *Existing;
}
else if (UMaterialExpression* const* Node = InterpByName.Find(FName(*InterpNameStr)))
{
InputName = FName(*(FString(TEXT("SLI_Interp_")) + InterpNameStr));
InputByKey.Add(Key, InputName);
OutWires.Add(FIntrinsicWire{ InputName, *Node, 0 });
UsedInterps.Add(FName(*InterpNameStr));
}
else
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_Interpolator' references unknown interpolator '%s'"), *InterpNameStr));
bOk = false;
}
}
FString Replacement = InputName.IsNone() ? FString() : InputName.ToString();
while (Replacement.Len() < CallLen) { Replacement.AppendChar(TEXT(' ')); }
Result += Replacement;
i = m + 1;
continue;
}
// 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.
@@ -440,13 +491,141 @@ namespace ShaderLabGraph
bool bIsTexture = false;
};
/** Add the shared struct + function-library includes plus any user `Includes { }` paths. */
/** Map an SL_INTERPOLATOR return type to a Custom-node output type (parser guarantees float1..4). */
static ECustomMaterialOutputType InterpolatorOutputType(const FString& ReturnType)
{
if (ReturnType == TEXT("float2")) { return CMOT_Float2; }
if (ReturnType == TEXT("float3")) { return CMOT_Float3; }
if (ReturnType == TEXT("float4")) { return CMOT_Float4; }
return CMOT_Float1; // "float"
}
/** File-name-safe stem from the shader name (identity), matching the registry's illegal->'_' rule. */
static FString SanitizeShaderFileStem(const FString& Name)
{
FString Out;
for (const TCHAR C : Name)
{
Out.AppendChar((FChar::IsAlnum(C) || C == TEXT('_') || C == TEXT('-')) ? C : TEXT('_'));
}
return Out.IsEmpty() ? FString(TEXT("Unnamed")) : Out;
}
/** Virtual `#include` path for a shader's generated local-code header (empty if it has no local code). */
static FString LocalCodeVirtualPath(const FShaderLabModel& Model)
{
if (Model.LocalCode.Num() == 0)
{
return FString();
}
return FString::Printf(TEXT("%s/%s.gen.ush"),
FShaderLabGraphBuilder::GetGeneratedVirtualRoot(), *SanitizeShaderFileStem(Model.ShaderName));
}
/**
* Guard: reject UE_ node intrinsics / UE_Interpolator inside free local code. Local functions are pure
* HLSL emitted at file scope — they can't reach material-graph nodes or per-primitive parameters, so a
* `UE_Time()` there would fail with an obscure "undefined symbol". Report it clearly, mapped to the .usl.
* (HLSL library helpers like UE_Noise are NOT in the registry and remain allowed.)
*/
static bool CheckLocalCodeIntrinsics(const FShaderLabModel& Model, TArray<FString>& OutErrors)
{
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
bool bOk = true;
for (const FShaderLabLocalCode& LC : Model.LocalCode)
{
const FString& B = LC.Text;
const int32 Len = B.Len();
int32 i = 0;
while (i < Len)
{
const bool bBoundary = (i == 0) || !IsIdent(B[i - 1]);
if (bBoundary && i + 3 <= Len && B[i] == TEXT('U') && B[i + 1] == TEXT('E') && B[i + 2] == TEXT('_'))
{
int32 j = i + 3;
while (j < Len && IsIdent(B[j])) { ++j; }
const FString Name = B.Mid(i + 3, j - (i + 3));
int32 k = j;
while (k < Len && FChar::IsWhitespace(B[k])) { ++k; }
if (!Name.IsEmpty() && k < Len && B[k] == TEXT('(')
&& (Name == TEXT("Interpolator") || Registry.Find(FName(*Name))))
{
int32 Line = LC.Line;
for (int32 p = 0; p < i; ++p) { if (B[p] == TEXT('\n')) { ++Line; } }
OutErrors.Add(FString::Printf(
TEXT("%s(%d,1): error: UE_%s is a material-graph intrinsic and cannot be used inside a local function (pass its value in as a parameter)"),
*SrcPath, FMath::Max(Line, 1), *Name));
bOk = false;
}
i = j;
continue;
}
++i;
}
}
return bOk;
}
/**
* Write the shader's free top-level HLSL (LocalCode) to its generated `.gen.ush` on disk, at file
* scope (in source order, each chunk `#line`-mapped back to the .usl). Every generated Custom node
* #includes this file, so helpers/structs/globals are defined-before-use for all pixel/vertex bodies
* regardless of the translator's per-node compile order. No-op when the shader has no local code.
*/
static bool WriteLocalCodeInclude(const FShaderLabModel& Model, TArray<FString>& OutErrors)
{
if (Model.LocalCode.Num() == 0)
{
return true;
}
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
FString Content;
Content += FString::Printf(TEXT("// Generated by ShaderLab from %s.usl - do not edit.\n"), *Model.ShaderName);
Content += TEXT("#pragma once\n");
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_COMMON_INCLUDE);
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_FUNCTIONS_INCLUDE);
for (const FShaderLabLocalCode& LC : Model.LocalCode)
{
Content += FString::Printf(TEXT("#line %d \"%s\"\n"), FMath::Max(LC.Line, 1), *SrcPath);
Content += LC.Text;
Content += TEXT("\n");
}
const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir();
if (GenDir.IsEmpty())
{
OutErrors.Add(TEXT("ShaderLab: cannot locate the plugin directory for the generated local-code include"));
return false;
}
IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true);
const FString DiskPath = FPaths::Combine(GenDir, SanitizeShaderFileStem(Model.ShaderName) + TEXT(".gen.ush"));
// Shader source must be UTF-8 (no BOM) for the shader preprocessor, not the default UTF-16.
if (!FFileHelper::SaveStringToFile(Content, *DiskPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutErrors.Add(FString::Printf(TEXT("ShaderLab: failed to write generated local-code include '%s'"), *DiskPath));
return false;
}
// Drop any cached copy so the shader preprocessor re-reads the freshly written file (hot reload / re-cook).
FlushShaderFileCache();
return true;
}
/** Add the shared struct + function-library includes, the generated local-code header (if any), and
* 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);
// User-authored local functions/structs/globals live in a generated header at file scope.
const FString LocalInclude = LocalCodeVirtualPath(Model);
if (!LocalInclude.IsEmpty())
{
Custom.IncludeFilePaths.AddUnique(LocalInclude);
}
for (const FString& Include : Model.Includes)
{
if (!Include.IsEmpty())
@@ -465,6 +644,7 @@ namespace ShaderLabGraph
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
@@ -492,7 +672,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return nullptr;
}
@@ -575,6 +755,7 @@ namespace ShaderLabGraph
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,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
@@ -584,7 +765,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return false;
}
@@ -648,6 +829,7 @@ namespace ShaderLabGraph
static UMaterialExpressionCustom* BuildValueNode(
UMaterial& Material, const FShaderLabValue& Value, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
@@ -657,7 +839,7 @@ namespace ShaderLabGraph
FString Body = Value.Body;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return nullptr;
}
@@ -691,11 +873,68 @@ namespace ShaderLabGraph
return Custom;
}
/** Connect a topology mix factor (literal / Value block / Scalar property) to an operator scalar pin. */
/**
* Build a Vertex Interpolator: a Custom node computing the SL_INTERPOLATOR body at vertex frequency,
* feeding a UMaterialExpressionVertexInterpolator. Returns the interpolator node (its output 0 is the
* interpolated value, readable from pixel bodies via UE_Interpolator). nullptr on error.
*/
static UMaterialExpression* BuildInterpolatorNode(
UMaterial& Material, const FShaderLabInterpolator& Interp, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -600);
Custom->Description = FString::Printf(TEXT("ShaderLab Interpolator %s"), *Interp.Name.ToString());
Custom->OutputType = InterpolatorOutputType(Interp.ReturnType);
AddIncludes(*Custom, Model);
// Vertex frequency: pixel-only intrinsics (incl. UE_Interpolator) are rejected. No interpolator map.
FString Body = Interp.Body;
TArray<FIntrinsicWire> Wires;
const TMap<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> IgnoredUsed;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, EmptyInterp, IgnoredUsed, 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(Interp.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 contains `return <expr>;`, so it is the Custom function's body directly.
Custom->Code = WrapBodyWithLineMapping(Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath));
Custom->RebuildOutputs();
UMaterialExpressionVertexInterpolator* VI = NewExpr<UMaterialExpressionVertexInterpolator>(Material, IoY, -450);
VI->Input.Connect(0, Custom);
return VI;
}
/** Connect a topology mix factor (literal / Value block / Scalar property / scalar Interpolator) to a 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)
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, const FShaderLabModel& Model,
TSet<FName>& UsedInterps, int32& IoY, TArray<FString>& OutErrors)
{
if (Factor.Kind == FShaderLabFactor::EKind::Literal)
{
@@ -717,8 +956,21 @@ namespace ShaderLabGraph
return true;
}
}
if (UMaterialExpression* const* InterpNode = InterpByName.Find(Factor.Name))
{
const FShaderLabInterpolator* Interp = Model.FindInterpolator(Factor.Name);
if (Interp && Interp->ReturnType == TEXT("float"))
{
Target.Connect(0, *InterpNode);
UsedInterps.Add(Factor.Name);
return true;
}
OutErrors.Add(FString::Printf(
TEXT("Topology factor '%s' is a Vertex Interpolator but not scalar (float); only float interpolators can be a mix factor"), *Factor.Name.ToString()));
return false;
}
OutErrors.Add(FString::Printf(
TEXT("Topology factor '%s' is neither a Value block nor a Scalar property"), *Factor.Name.ToString()));
TEXT("Topology factor '%s' is neither a Value block, a Scalar property, nor a scalar Interpolator"), *Factor.Name.ToString()));
return false;
}
@@ -727,7 +979,9 @@ namespace ShaderLabGraph
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)
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
if (!Model.Topology.IsValidIndex(Index))
{
@@ -746,9 +1000,9 @@ namespace ShaderLabGraph
return nullptr;
}
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors);
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors);
UMaterialExpression* ChildB = (Node.ChildB != INDEX_NONE)
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors)
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors)
: nullptr;
if (!ChildA || (Node.ChildB != INDEX_NONE && !ChildB))
{
@@ -762,14 +1016,14 @@ namespace ShaderLabGraph
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;
return ConnectFactor(Material, N->Thickness, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, 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;
return ConnectFactor(Material, N->Mix, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::Add:
{
@@ -782,14 +1036,14 @@ namespace ShaderLabGraph
{
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;
return ConnectFactor(Material, N->Weight, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, 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;
return ConnectFactor(Material, N->SelectValue, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
default:
OutErrors.Add(TEXT("Unhandled topology operator"));
@@ -824,6 +1078,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
return false;
}
// Emit the shader's free top-level HLSL (local functions/structs/globals) to a generated header that
// every Custom node #includes at file scope (defined-before-use for all pixel/vertex bodies). No-op
// when the shader declares none. Guard against graph intrinsics in local code first (clear .usl error).
if (!CheckLocalCodeIntrinsics(Model, OutErrors) || !WriteLocalCodeInclude(Model, OutErrors))
{
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
@@ -843,6 +1105,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
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; } }
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { if (ReferencesToken(Interp.Body, NameStr)) { return true; } }
if (Model.bHasVertex && ReferencesToken(Model.VertexBody, NameStr)) { return true; }
for (const FShaderLabTopoNode& Node : Model.Topology)
{
@@ -863,6 +1126,12 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// engine changes, and nothing wired onto the user's body nodes.
TArray<UMaterialExpression*> AnchorInputs;
// Vertex Interpolators: VS-frequency values interpolated to the pixel shader. Built up-front so pixel
// bodies can reference them via UE_Interpolator(Name); UsedInterps tracks which get consumed (contract:
// a declared-but-unused interpolator is an error, mirroring the no-dead-slabs rule).
TMap<FName, UMaterialExpression*> InterpByName;
TSet<FName> UsedInterps;
for (const FShaderLabProperty& Prop : Model.Properties)
{
const FString NameStr = Prop.Name.ToString();
@@ -961,12 +1230,28 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// 1b) Build a Vertex Interpolator node per declared interpolator (Custom @ vertex freq -> VertexInterpolator).
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
{
if (InterpByName.Contains(Interp.Name))
{
OutErrors.Add(FString::Printf(TEXT("Duplicate interpolator name '%s'"), *Interp.Name.ToString()));
return false;
}
UMaterialExpression* Node = BuildInterpolatorNode(Material, Interp, Model, PropertyNodes, ParamY, OutErrors);
if (!Node)
{
return false;
}
InterpByName.Add(Interp.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))
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors))
{
return false;
}
@@ -974,7 +1259,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
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))
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors))
{
return false;
}
@@ -985,7 +1270,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// allowed as material-level outputs.
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
Model, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
if (!Slab)
{
return false;
@@ -1006,7 +1291,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
Model, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
if (!Slab)
{
return false;
@@ -1023,7 +1308,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
return false;
}
UMaterialExpressionCustom* ValueNode = BuildValueNode(
Material, ValueDecl, Model, PropertyNodes, ParamY, OutErrors);
Material, ValueDecl, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors);
if (!ValueNode)
{
return false;
@@ -1047,7 +1332,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
UMaterialExpression* Root = BuildTopologyNode(
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, ParamY, OutErrors);
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors);
if (!Root)
{
return false;
@@ -1098,7 +1383,10 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// Intrinsics (Stage = vertex).
FString VertexBody = Model.VertexBody;
TArray<FIntrinsicWire> VtxIntrinsicWires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, OutErrors))
// Vertex stage: UE_Interpolator is pixel-only, so pass an empty interpolator map (rejected there).
const TMap<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> IgnoredUsedInterps;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, EmptyInterp, IgnoredUsedInterps, OutErrors))
{
return false;
}
@@ -1174,6 +1462,19 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// Contract: every declared interpolator must be consumed (read via UE_Interpolator, or used as a
// topology mix factor). A dead interpolator would silently waste a scarce interpolant slot.
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
{
if (!UsedInterps.Contains(Interp.Name))
{
OutErrors.Add(FString::Printf(
TEXT("Interpolator '%s' is declared but never used (read it with UE_Interpolator(%s) or use it as a topology factor)"),
*Interp.Name.ToString(), *Interp.Name.ToString()));
return false;
}
}
// 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
@@ -1242,3 +1543,18 @@ void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray<F
Material.UpdateCachedExpressionData();
}
const TCHAR* FShaderLabGraphBuilder::GetGeneratedVirtualRoot()
{
return TEXT("/UShaderLabGen");
}
FString FShaderLabGraphBuilder::GetGeneratedShaderDir()
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"));
if (!Plugin.IsValid())
{
return FString();
}
return FPaths::Combine(Plugin->GetBaseDir(), TEXT("Intermediate"), TEXT("ShaderLabGen"));
}