mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Support SL_INTERPOLATOR and local function
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user