diff --git a/Shaders/Private/ShaderLabUEFunctions.ush b/Shaders/Private/ShaderLabUEFunctions.ush index 74a7e21..33185a5 100644 --- a/Shaders/Private/ShaderLabUEFunctions.ush +++ b/Shaders/Private/ShaderLabUEFunctions.ush @@ -9,5 +9,6 @@ #include "/Plugin/ShaderLab/Private/UEFunctions/Noise.ush" #include "/Plugin/ShaderLab/Private/UEFunctions/Rotation.ush" #include "/Plugin/ShaderLab/Private/UEFunctions/SceneTexture.ush" +#include "/Plugin/ShaderLab/Private/UEFunctions/DistanceField.ush" #include "/Plugin/ShaderLab/Private/UEFunctions/Transform.ush" // generated (UE_Transform*VectorTo*) #include "/Plugin/ShaderLab/Private/UEFunctions/TransformPosition.ush" // generated (UE_Transform*PositionTo*) diff --git a/Shaders/Private/UEFunctions/DistanceField.ush b/Shaders/Private/UEFunctions/DistanceField.ush new file mode 100644 index 0000000..23a645a --- /dev/null +++ b/Shaders/Private/UEFunctions/DistanceField.ush @@ -0,0 +1,27 @@ +// Copyright UShaderLab. All Rights Reserved. +// +// UE_ global-distance-field reads at an arbitrary, body-computed world position — forward to the engine's +// GetDistanceToNearestSurfaceGlobal / GetDistanceFieldGradientGlobal. Because these take a DYNAMIC position +// they are emitted as HLSL helpers (like Noise / SceneTexture), NOT wired intrinsics: a body-computed value +// cannot be fed into a material-expression node from inside the opaque Custom node. +// +// The position argument is an ABSOLUTE world position (e.g. UE_WorldPosition()); MakeDFVector3(pos, 0) packs +// it as a large-world-coordinate value and the engine overload converts to translated-world internally. +// +// Global-distance-field binding: the engine only prepares the global distance field for a view when the +// material's relevance flags bUsesGlobalDistanceField, which is a side effect of compiling a +// UMaterialExpressionDistanceToNearestSurface. The raw HLSL call bypasses that node, so the graph builder's +// "GlobalDistanceField" anchor capability adds a hidden one when this parameterized form is used (see +// FShaderLabAnchorCapabilityRegistry). The NULLARY form UE_DistanceToNearestSurface() / UE_DistanceFieldGradient() +// is instead a wired intrinsic whose real node both supplies the value (current pixel) and sets the flag. + +#pragma once + +#ifdef SHADERLAB_IDE + // IDE stubs: the parameterized overloads (the nullary forms are generated into ShaderLabUENode.ush). + float UE_DistanceToNearestSurface(float3 AbsoluteWorldPosition) { return 0; } + float3 UE_DistanceFieldGradient(float3 AbsoluteWorldPosition) { return (float3)0; } +#else + float UE_DistanceToNearestSurface(float3 AbsoluteWorldPosition) { return GetDistanceToNearestSurfaceGlobal(MakeDFVector3(AbsoluteWorldPosition, 0)); } + float3 UE_DistanceFieldGradient(float3 AbsoluteWorldPosition) { return GetDistanceFieldGradientGlobal(MakeDFVector3(AbsoluteWorldPosition, 0)); } +#endif diff --git a/Source/UShaderLabEditor/Private/ShaderLabAnchorCapabilityRegistry.cpp b/Source/UShaderLabEditor/Private/ShaderLabAnchorCapabilityRegistry.cpp new file mode 100644 index 0000000..79d200e --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabAnchorCapabilityRegistry.cpp @@ -0,0 +1,352 @@ +// Copyright UShaderLab. All Rights Reserved. + +#include "ShaderLabAnchorCapabilityRegistry.h" + +#include "ShaderLabModel.h" +#include "Materials/Material.h" +#include "Materials/MaterialExpressionSceneTexture.h" +#include "Materials/MaterialExpressionDistanceToNearestSurface.h" +#include "MaterialSceneTextureId.h" + +namespace ShaderLabAnchorCapability +{ + static bool IsIdentChar(TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); } + + /** True if `Token` appears in `Body` delimited by non-identifier characters (same rule as the builder). */ + static bool ReferencesToken(const FString& Body, const TCHAR* Token) + { + const int32 TokenLen = FCString::Strlen(Token); + 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 + TokenLen; + const TCHAR After = (AfterIdx < Body.Len()) ? Body[AfterIdx] : TEXT(' '); + if (!IsIdentChar(Before) && !IsIdentChar(After)) { return true; } + From = Idx + TokenLen; + } + } + + /** 1-based line of character offset `Index` within `Body`, given the body starts on `BodyLine`. */ + static int32 LineOf(const FString& Body, int32 Index, int32 BodyLine) + { + int32 Line = FMath::Max(BodyLine, 1); + for (int32 i = 0; i < Index && i < Body.Len(); ++i) + { + if (Body[i] == TEXT('\n')) { ++Line; } + } + return Line; + } + + // --- SceneTexture ------------------------------------------------------------------------------- + + struct FSceneTextureIdToken + { + const TCHAR* Token; // SL_ST_* DSL token + ESceneTextureId Id; // engine id it aliases (mirrors UEFunctions/SceneTexture.ush) + const TCHAR* Display; // human name for error messages + }; + + static const FSceneTextureIdToken GSceneTextureIds[] = + { + { TEXT("SL_ST_SceneColor"), PPI_SceneColor, TEXT("SceneColor") }, + { TEXT("SL_ST_SceneDepth"), PPI_SceneDepth, TEXT("SceneDepth") }, + { TEXT("SL_ST_CustomDepth"), PPI_CustomDepth, TEXT("CustomDepth") }, + { TEXT("SL_ST_CustomStencil"), PPI_CustomStencil, TEXT("CustomStencil") }, + { TEXT("SL_ST_WorldNormal"), PPI_WorldNormal, TEXT("WorldNormal") }, + { TEXT("SL_ST_PostProcessInput0"), PPI_PostProcessInput0, TEXT("PostProcessInput0") }, + { TEXT("SL_ST_PostProcessInput1"), PPI_PostProcessInput1, TEXT("PostProcessInput1") }, + { TEXT("SL_ST_PostProcessInput2"), PPI_PostProcessInput2, TEXT("PostProcessInput2") }, + { TEXT("SL_ST_PostProcessInput3"), PPI_PostProcessInput3, TEXT("PostProcessInput3") }, + { TEXT("SL_ST_PostProcessInput4"), PPI_PostProcessInput4, TEXT("PostProcessInput4") }, + }; + + static const FSceneTextureIdToken* FindIdToken(const FString& Token) + { + for (const FSceneTextureIdToken& E : GSceneTextureIds) + { + if (Token.Equals(E.Token, ESearchCase::CaseSensitive)) { return &E; } + } + return nullptr; + } + + static const TCHAR* DisplayNameOf(ESceneTextureId Id) + { + for (const FSceneTextureIdToken& E : GSceneTextureIds) + { + if (E.Id == Id) { return E.Display; } + } + return TEXT(""); + } + + /** + * Scan `Body` for `UE_SceneTexture(` call sites, extract each first argument, and require it to be a + * literal SL_ST_* token (contract). Recognized ids are added to OutIds; a non-literal / unknown first + * argument appends a `.usl`-mapped error and returns false. + */ + static bool ExtractSceneTextureCallIds(const FString& Body, int32 BodyLine, const FString& SourcePath, + TSet& OutIds, TArray& OutErrors) + { + const TCHAR* Fn = TEXT("UE_SceneTexture"); + const int32 FnLen = FCString::Strlen(Fn); + bool bOk = true; + int32 From = 0; + while (true) + { + const int32 Idx = Body.Find(Fn, ESearchCase::CaseSensitive, ESearchDir::FromStart, From); + if (Idx == INDEX_NONE) { break; } + From = Idx + FnLen; + + // Delimited match only (skip UE_SceneTextureFoo and the like). + const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' '); + if (IsIdentChar(Before)) { continue; } + + // Expect '(' after optional whitespace. + int32 P = Idx + FnLen; + while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; } + if (P >= Body.Len() || Body[P] != TEXT('(')) { continue; } // not a call (e.g. a #define alias) + ++P; + while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; } + + // Read a bare identifier for the first argument. + const int32 ArgStart = P; + while (P < Body.Len() && IsIdentChar(Body[P])) { ++P; } + const FString Arg = Body.Mid(ArgStart, P - ArgStart); + while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; } + const bool bArgIsLoneIdent = !Arg.IsEmpty() && P < Body.Len() && Body[P] == TEXT(','); + + const FSceneTextureIdToken* IdTok = bArgIsLoneIdent ? FindIdToken(Arg) : nullptr; + if (!IdTok) + { + const int32 Line = LineOf(Body, Idx, BodyLine); + OutErrors.Add(FString::Printf( + TEXT("%s(%d): Scene-texture read: UE_SceneTexture id must be a literal SL_ST_* token (got '%s'). A computed id cannot be bound and would return black at runtime."), + *SourcePath, Line, Arg.IsEmpty() ? TEXT("") : *Arg)); + bOk = false; + continue; + } + OutIds.Add(IdTok->Id); + } + return bOk; + } + + /** Per-id domain legality, mirroring the engine's UseSceneTextureId rules (HLSLMaterialTranslator). */ + static bool IsIdLegalInDomain(ESceneTextureId Id, EShaderLabDomain Domain, bool bNonOpaqueSurface, FString& OutReason) + { + const bool bPostProcess = (Domain == EShaderLabDomain::PostProcess); + const bool bDecal = (Domain == EShaderLabDomain::Decal); + + switch (Id) + { + case PPI_SceneColor: + if (!(Domain == EShaderLabDomain::Surface && bNonOpaqueSurface)) + { + OutReason = TEXT("SceneColor can only be read in a translucent Surface material (PostProcess should read PostProcessInput0)"); + return false; + } + return true; + + case PPI_PostProcessInput0: + case PPI_PostProcessInput1: + case PPI_PostProcessInput2: + case PPI_PostProcessInput3: + case PPI_PostProcessInput4: + if (!bPostProcess) + { + OutReason = TEXT("PostProcessInput* can only be read in a PostProcess material"); + return false; + } + return true; + + case PPI_WorldNormal: + if (!(bPostProcess || bDecal)) + { + OutReason = TEXT("WorldNormal can only be read in a PostProcess or Decal material"); + return false; + } + return true; + + case PPI_SceneDepth: + case PPI_CustomDepth: + case PPI_CustomStencil: + if (!(bPostProcess || bDecal || bNonOpaqueSurface)) + { + OutReason = TEXT("SceneDepth/CustomDepth/CustomStencil require a PostProcess, Decal, or translucent Surface material"); + return false; + } + return true; + + default: + OutReason = TEXT("this scene-texture id is not supported by ShaderLab"); + return false; + } + } + + static bool EmitSceneTexture(const FShaderLabAnchorCapabilityContext& Ctx, + TArray& OutNodes, TArray& OutErrors) + { + const EShaderLabDomain Domain = Ctx.Model.Settings.Domain; + const bool bNonOpaqueSurface = (Domain == EShaderLabDomain::Surface) + && Ctx.Model.Settings.BlendMode != EShaderLabBlendMode::Opaque + && Ctx.Model.Settings.BlendMode != EShaderLabBlendMode::Masked; + + TSet Ids; + bool bOk = true; + for (const FShaderLabScannedBody& B : Ctx.Bodies) + { + check(B.Text); + // Convenience wrappers with fixed ids. + if (ReferencesToken(*B.Text, TEXT("UE_SceneDepth"))) { Ids.Add(PPI_SceneDepth); } + if (ReferencesToken(*B.Text, TEXT("UE_CustomDepth"))) { Ids.Add(PPI_CustomDepth); } + if (ReferencesToken(*B.Text, TEXT("UE_CustomStencil"))) { Ids.Add(PPI_CustomStencil); } + // General form: UE_SceneTexture(, uv). + bOk &= ExtractSceneTextureCallIds(*B.Text, B.SourceLine, Ctx.SourcePath, Ids, OutErrors); + } + if (!bOk) + { + return false; + } + if (Ids.Num() == 0) + { + // A trigger token matched but resolved to no concrete read (e.g. the token appears only in a + // comment, or as a bare mention with no call). Nothing to bind — emit nothing. + return true; + } + + // Per-id domain validation (friendly, .usl-level; the engine's UseSceneTextureId also enforces this). + for (const ESceneTextureId Id : Ids) + { + FString Reason; + if (!IsIdLegalInDomain(Id, Domain, bNonOpaqueSurface, Reason)) + { + OutErrors.Add(FString::Printf(TEXT("Scene-texture read '%s': %s."), DisplayNameOf(Id), *Reason)); + bOk = false; + } + } + if (!bOk) + { + return false; + } + + // One hidden node per distinct id. Compiling it (before attributes, via the anchor) runs + // UseSceneTextureId(id) -> sets the UsedSceneTextures bit so that id's SRV is bound at runtime. The + // node's own fetch result is unused (the real read is the raw HLSL SceneTextureLookup in the body) and + // is optimized out; only the binding side effect matters. + for (const ESceneTextureId Id : Ids) + { + UMaterialExpressionSceneTexture* Node = + CastChecked(Ctx.MakeNode(UMaterialExpressionSceneTexture::StaticClass())); + Node->SceneTextureId = Id; + OutNodes.Add(Node); + } + return true; + } + + // --- Global Distance Field ---------------------------------------------------------------------- + + /** True if `Body` has a call `Fn(...)` with a non-empty argument list — the parameterized helper form. */ + static bool HasCallWithArgs(const FString& Body, const TCHAR* Fn) + { + const int32 FnLen = FCString::Strlen(Fn); + int32 From = 0; + while (true) + { + const int32 Idx = Body.Find(Fn, ESearchCase::CaseSensitive, ESearchDir::FromStart, From); + if (Idx == INDEX_NONE) { return false; } + From = Idx + FnLen; + const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' '); + if (IsIdentChar(Before)) { continue; } // delimited match only + int32 P = Idx + FnLen; + while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; } + if (P >= Body.Len() || Body[P] != TEXT('(')) { continue; } + ++P; + while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; } + if (P < Body.Len() && Body[P] != TEXT(')')) { return true; } // non-empty arg list + } + } + + static bool EmitGlobalDistanceField(const FShaderLabAnchorCapabilityContext& Ctx, + TArray& OutNodes, TArray& /*OutErrors*/) + { + // Only the parameterized helper form (UE_DistanceToNearestSurface(worldPos) / UE_DistanceFieldGradient( + // worldPos)) needs us: the nullary form is a wired intrinsic whose real node already sets the flag. So + // fire only when a call carries an argument; a nullary-only shader adds no redundant hidden node. + bool bParameterized = false; + for (const FShaderLabScannedBody& B : Ctx.Bodies) + { + check(B.Text); + if (HasCallWithArgs(*B.Text, TEXT("UE_DistanceToNearestSurface")) + || HasCallWithArgs(*B.Text, TEXT("UE_DistanceFieldGradient"))) + { + bParameterized = true; + break; + } + } + if (!bParameterized) + { + return true; // nullary-only: nothing to do. + } + + // A single hidden node suffices: its Compile sets MaterialCompilationOutput.bUsesGlobalDistanceField, + // which drives the material relevance the renderer uses to prepare the global distance field for the + // view. The node's optional Position input is left unconnected (the real, body-computed position flows + // through the raw HLSL helper). Platform support is enforced by the engine (ErrorUnlessPlatformSupports). + OutNodes.Add(Ctx.MakeNode(UMaterialExpressionDistanceToNearestSurface::StaticClass())); + return true; + } +} + +FShaderLabAnchorCapabilityRegistry& FShaderLabAnchorCapabilityRegistry::Get() +{ + static FShaderLabAnchorCapabilityRegistry Instance; + return Instance; +} + +FShaderLabAnchorCapabilityRegistry::FShaderLabAnchorCapabilityRegistry() +{ + RegisterBuiltins(); +} + +void FShaderLabAnchorCapabilityRegistry::Register(FShaderLabAnchorCapability Capability) +{ + check(!Capability.Id.IsNone()); + check(Capability.TriggerTokens.Num() > 0); + check(Capability.Emit); + for (FShaderLabAnchorCapability& Existing : Capabilities) + { + if (Existing.Id == Capability.Id) + { + Existing = MoveTemp(Capability); // Override by Id. + return; + } + } + Capabilities.Add(MoveTemp(Capability)); +} + +void FShaderLabAnchorCapabilityRegistry::ForEach(TFunctionRef Fn) const +{ + for (const FShaderLabAnchorCapability& Cap : Capabilities) + { + Fn(Cap); + } +} + +void FShaderLabAnchorCapabilityRegistry::RegisterBuiltins() +{ + { + FShaderLabAnchorCapability Cap; + Cap.Id = TEXT("SceneTexture"); + Cap.TriggerTokens = { TEXT("UE_SceneTexture"), TEXT("UE_SceneDepth"), TEXT("UE_CustomDepth"), TEXT("UE_CustomStencil") }; + Cap.Emit = &ShaderLabAnchorCapability::EmitSceneTexture; + Register(MoveTemp(Cap)); + } + { + FShaderLabAnchorCapability Cap; + Cap.Id = TEXT("GlobalDistanceField"); + Cap.TriggerTokens = { TEXT("UE_DistanceToNearestSurface"), TEXT("UE_DistanceFieldGradient") }; + Cap.Emit = &ShaderLabAnchorCapability::EmitGlobalDistanceField; + Register(MoveTemp(Cap)); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp index 8750032..b358f3a 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp @@ -33,6 +33,7 @@ #include "Materials/MaterialExpressionVertexInterpolator.h" #include "MaterialExpressionShaderLabParameterAnchor.h" #include "ShaderLabIntrinsicRegistry.h" +#include "ShaderLabAnchorCapabilityRegistry.h" #include "ShaderLabImportResolver.h" #include "ShaderLabRuntimeBuilder.h" #include "UObject/Class.h" @@ -667,7 +668,15 @@ namespace ShaderLabGraph // Copy just the `UE_Name` identifier and keep scanning from the '(' — so any // intrinsic nested in the arguments (e.g. UE_Noise(UE_WorldPosition(), ...)) still // gets rewritten. - if (!Registry.Find(FName(*Name))) + // + // Overloaded intrinsics (bAllowRawHelperWithArgs) also fall through here when the call + // carries arguments: the nullary form is the wired intrinsic, but `UE_Name()` is a + // same-named raw HLSL helper — leave it for the shader compiler (its side-effect binding + // is handled by an anchor capability). + const FShaderLabIntrinsicDesc* Found = Registry.Find(FName(*Name)); + const bool bRawHelperOverload = Found && Found->bAllowRawHelperWithArgs + && SplitArgs(ArgsRaw).Num() > Found->Params.Num(); + if (!Found || bRawHelperOverload) { Result += Body.Mid(i, j - i); i = j; @@ -1015,12 +1024,19 @@ namespace ShaderLabGraph i = e; continue; } - if (Registry.Find(FName(*Name))) + if (const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name))) { // Registry intrinsic: read its balanced (...) and record the use (args are literals). int32 depth = 0, m = paren; for (; m < Len; ++m) { if (Body[m] == TEXT('(')) { ++depth; } else if (Body[m] == TEXT(')')) { if (--depth == 0) { break; } } } const FString ArgsRaw = (m < Len) ? Body.Mid(paren + 1, m - (paren + 1)) : FString(); + // Overloaded intrinsic called with args: it's the same-named raw HLSL helper, not the wired + // intrinsic — leave it and keep scanning its arguments (like an unknown UE_ helper below). + if (Desc->bAllowRawHelperWithArgs && SplitArgs(ArgsRaw).Num() > Desc->Params.Num()) + { + i = e; + continue; + } const FString ArgSig = MakeArgSig(ArgsRaw); if (!OutIntr.ContainsByPredicate([&](const FIntrinsicUse& U) { return U.Name == Name && U.ArgSig == ArgSig; })) { @@ -1205,11 +1221,19 @@ namespace ShaderLabGraph if (bCall && Ident.StartsWith(TEXT("UE_"))) { const FString Name = Ident.Mid(3); - if (Registry.Find(FName(*Name))) + if (const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name))) { int32 depth = 0, m = paren; for (; m < Len; ++m) { if (Body[m] == TEXT('(')) { ++depth; } else if (Body[m] == TEXT(')')) { if (--depth == 0) { break; } } } const FString ArgsRaw = (m < Len) ? Body.Mid(paren + 1, m - (paren + 1)) : FString(); + // Overloaded intrinsic called with args -> the same-named raw HLSL helper form: emit the + // identifier verbatim (do NOT rewrite to an SLI_ input) and keep scanning its arguments. + if (Desc->bAllowRawHelperWithArgs && SplitArgs(ArgsRaw).Num() > Desc->Params.Num()) + { + P1 += Ident; + i = e; + continue; + } P1 += MakeIntrinsicVar(Name, MakeArgSig(ArgsRaw)); i = (m < Len) ? m + 1 : e; continue; @@ -2757,39 +2781,74 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode } } - // Scene-texture reads: the UE_SceneColor/SceneDepth/CustomDepth/CustomStencil/SceneTexture helpers call the - // engine's SceneTextureLookup in HLSL. That only works when the material sets bNeedsSceneTextures, which the - // engine derives from a UMaterialExpressionSceneTexture node being present + compiled. We add one hidden node - // wired into the ParameterAnchor (compiled before attributes, so its Compile always runs and flips the flag). + // Anchor side-effect capabilities: raw-HLSL body helpers (SceneTexture reads, global distance field + // reads, ...) that call an engine HLSL function directly from a Custom node. The helper compiles as bare + // HLSL, but the runtime resource binding it needs is only established as a side effect of compiling a + // matching UMaterialExpression (UseSceneTextureId / bUsesGlobalDistanceField / ...). Because the Custom + // body bypasses that node, we synthesize hidden expression(s) wired into the before-attributes + // ParameterAnchor purely for the side effect. See FShaderLabAnchorCapabilityRegistry. { - auto BodyUsesScene = [](const FString& Body) -> bool + // Bodies to scan: the shader's own pixel/vertex bodies AND every reachable `.uslfunc` library-function + // body (a library function is inlined into the consuming Custom node, so it can equally reference a raw + // helper and must contribute to binding). Emit.Ctx holds exactly the reachable library functions. + TArray Bodies; + if (Model.bHasSurface) { Bodies.Add({ &Model.SurfaceBody, Model.SurfaceBodyLine }); } + for (const FShaderLabSlab& Slab : Model.Slabs) { Bodies.Add({ &Slab.Body, Slab.BodyLine }); } + for (const FShaderLabValue& Value : Model.Values) { Bodies.Add({ &Value.Body, Value.BodyLine }); } + for (const FShaderLabInterpolator& Interp : Model.Interpolators) { Bodies.Add({ &Interp.Body, Interp.BodyLine }); } + if (Model.bHasVertex) { Bodies.Add({ &Model.VertexBody, Model.VertexBodyLine }); } + if (Emit.HasLibraries()) { - return ReferencesToken(Body, TEXT("UE_SceneColor")) || ReferencesToken(Body, TEXT("UE_SceneDepth")) - || ReferencesToken(Body, TEXT("UE_CustomDepth")) || ReferencesToken(Body, TEXT("UE_CustomStencil")) - || ReferencesToken(Body, TEXT("UE_SceneTexture")); - }; - bool bUsesScene = Model.bHasSurface && BodyUsesScene(Model.SurfaceBody); - for (const FShaderLabSlab& Slab : Model.Slabs) { bUsesScene |= BodyUsesScene(Slab.Body); } - for (const FShaderLabValue& Value : Model.Values) { bUsesScene |= BodyUsesScene(Value.Body); } - - if (bUsesScene) - { - const EShaderLabDomain Dom = Model.Settings.Domain; - const bool bNonOpaqueSurface = (Dom == EShaderLabDomain::Surface) - && Model.Settings.BlendMode != EShaderLabBlendMode::Opaque - && Model.Settings.BlendMode != EShaderLabBlendMode::Masked; - const bool bDomainOk = (Dom == EShaderLabDomain::PostProcess) || (Dom == EShaderLabDomain::Decal) || bNonOpaqueSurface; - if (!bDomainOk) + for (const TPair& Pair : Emit.Ctx) { - OutErrors.Add(TEXT("Scene-texture reads (UE_SceneColor/UE_SceneDepth/UE_SceneTexture/...) require a PostProcess or Decal material, or a translucent (non-opaque) Surface material.")); - return false; + int32 LibIdx = INDEX_NONE; + if (const FShaderLabFunction* Fn = Program.FindFunction(Pair.Key, LibIdx)) + { + Bodies.Add({ &Fn->Body, Fn->BodyLine }); + } } - // The hidden node flips bNeedsSceneTextures. In PostProcess use PostProcessInput0; elsewhere use - // SceneDepth — the only scene-texture family the engine permits outside PostProcess/Decal (scene - // color via SceneTexture is disallowed there, so we don't request it). - UMaterialExpressionSceneTexture* Hidden = NewExpr(Material, ParamY, -1300); - Hidden->SceneTextureId = (Dom == EShaderLabDomain::PostProcess) ? PPI_PostProcessInput0 : PPI_SceneDepth; - AnchorInputs.Add(Hidden); + } + + const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath); + bool bCapabilityFailed = false; + FShaderLabAnchorCapabilityRegistry::Get().ForEach( + [&](const FShaderLabAnchorCapability& Cap) + { + if (bCapabilityFailed) { return; } + + bool bActive = false; + for (const FShaderLabScannedBody& B : Bodies) + { + for (const FString& Token : Cap.TriggerTokens) + { + if (ReferencesToken(*B.Text, Token)) { bActive = true; break; } + } + if (bActive) { break; } + } + if (!bActive) { return; } + + FShaderLabAnchorCapabilityContext CapCtx{ Material, Model, Bodies, SrcPath, + [&Material, &ParamY](UClass* Class) -> UMaterialExpression* + { + UMaterialExpression* Expr = NewObject(&Material, Class); + Material.GetExpressionCollection().AddExpression(Expr); + Expr->MaterialExpressionEditorX = -1300; + Expr->MaterialExpressionEditorY = ParamY; + ParamY += 120; + return Expr; + } }; + + TArray CapNodes; + if (!Cap.Emit(CapCtx, CapNodes, OutErrors)) + { + bCapabilityFailed = true; + return; + } + AnchorInputs.Append(CapNodes); + }); + if (bCapabilityFailed) + { + return false; } } diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp index 38b35ac..58b1dc5 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp @@ -64,4 +64,5 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins() RegisterParticleIntrinsics(*this); RegisterDecalIntrinsics(*this); RegisterAtmosphereIntrinsics(*this); + RegisterDistanceFieldIntrinsics(*this); } diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h index 6366ffe..a17b272 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h @@ -44,3 +44,4 @@ void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry); void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry); void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry); void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry); diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_DistanceField.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_DistanceField.cpp new file mode 100644 index 0000000..6830893 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_DistanceField.cpp @@ -0,0 +1,43 @@ +// Copyright UShaderLab. All Rights Reserved. +// +// Global-distance-field intrinsics. These names are OVERLOADED (bAllowRawHelperWithArgs): the nullary form +// UE_DistanceToNearestSurface() / UE_DistanceFieldGradient() is this wired intrinsic (its real node reads the +// current pixel's world position and sets bUsesGlobalDistanceField), while UE_Name(worldPos) is a same-named +// raw HLSL helper in UEFunctions/DistanceField.ush whose binding is supplied by the "GlobalDistanceField" +// anchor capability. The parser routes a call with args to the helper and a nullary call to this node. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionDistanceToNearestSurface.h" +#include "Materials/MaterialExpressionDistanceFieldGradient.h" + +void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + { + FShaderLabIntrinsicDesc Desc; + Desc.Name = TEXT("DistanceToNearestSurface"); + Desc.Frequency = EFreq::Any; + Desc.ReturnType = TEXT("float"); + Desc.bAllowRawHelperWithArgs = true; + Desc.MakeNode = [](UMaterial& M, const TArray&, FString&) + { + return MakeSimple(M); + }; + Registry.Register(MoveTemp(Desc)); + } + { + FShaderLabIntrinsicDesc Desc; + Desc.Name = TEXT("DistanceFieldGradient"); + Desc.Frequency = EFreq::Any; + Desc.ReturnType = TEXT("float3"); + Desc.bAllowRawHelperWithArgs = true; + Desc.MakeNode = [](UMaterial& M, const TArray&, FString&) + { + return MakeSimple(M); + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h b/Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h new file mode 100644 index 0000000..9fcc852 --- /dev/null +++ b/Source/UShaderLabEditor/Public/ShaderLabAnchorCapabilityRegistry.h @@ -0,0 +1,98 @@ +// Copyright UShaderLab. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +class UMaterial; +class UMaterialExpression; +struct FShaderLabModel; + +/** + * One body of HLSL scanned for capability triggers: the verbatim text plus its 1-based `.usl` source line + * (so a capability can map a contract violation back to the exact authored line). Covers a shader's own + * pixel/vertex bodies AND every reachable `.uslfunc` library-function body, since a library function called + * from a body is inlined into the same Custom node and can equally reference a raw scene-texture helper. + */ +struct FShaderLabScannedBody +{ + /** Non-owning pointer to the body text (lives in the FShaderLabModel / resolved program). */ + const FString* Text = nullptr; + /** 1-based source line of the body's first character, for error line-mapping. */ + int32 SourceLine = 0; +}; + +/** Everything a capability needs to detect usage, validate it, and create its hidden anchor node(s). */ +struct FShaderLabAnchorCapabilityContext +{ + /** The material being built (target of MakeNode). */ + UMaterial& Material; + /** The parsed shader model (domain / blend mode live here for per-id domain validation). */ + const FShaderLabModel& Model; + /** Shader bodies + reachable library-function bodies to scan. */ + const TArray& Bodies; + /** The shader's `.usl` absolute path (`#line` directive target for mapped errors). */ + const FString& SourcePath; + /** + * Create a material expression of `Class`, add it to the material's expression collection, and lay it + * out on the canvas — the capability then sets any type-specific fields (e.g. SceneTextureId) on the + * CastChecked<> result. Mirrors the builder's internal NewExpr helper. + */ + TFunction MakeNode; +}; + +/** + * A "capability" that a raw-HLSL body helper depends on: the helper (e.g. `SceneTextureLookup`, + * `GetDistanceToNearestSurfaceGlobal`) compiles fine as bare HLSL inside a Custom node, but the runtime + * resource binding it needs is only established as a side effect of compiling a matching UMaterialExpression + * (via `UseSceneTextureId` / `bUsesGlobalDistanceField` / ...). Because the Custom body bypasses that node, + * we synthesize a hidden expression, wired into the before-attributes ParameterAnchor, purely for the + * side effect. This registry is the general mechanism; each dynamic-input helper family registers one entry. + * + * (Helpers with NO dynamic body input — e.g. EyeAdaptation — do not need this: they are exposed as ordinary + * `UE_` wired intrinsics whose real node is connected into the Custom input, so its Compile side effect fires + * naturally. This mechanism is only for helpers that take a body-computed argument, which cannot be fed into + * a node from inside the opaque Custom body.) + */ +struct FShaderLabAnchorCapability +{ + /** Identifier for diagnostics / tests (e.g. "SceneTexture", "GlobalDistanceField"). */ + FName Id; + + /** + * HLSL tokens whose presence in any scanned body activates this capability (e.g. "UE_SceneTexture", + * "UE_SceneDepth"). Matched delimited by non-identifier characters. + */ + TArray TriggerTokens; + + /** + * Emit the hidden before-attributes node(s) for this capability into OutNodes. Runs only when at least + * one TriggerToken is present. Returns false and appends `.usl`-mapped messages to OutErrors on a + * contract violation (e.g. a non-literal id) or an illegal domain — contract style, aborting the build. + */ + TFunction& /*OutNodes*/, TArray& /*OutErrors*/)> Emit; +}; + +/** + * Open registry of anchor side-effect capabilities. ShaderLab seeds its builtins (SceneTexture, Global + * Distance Field) on first use; other editor modules may register their own from their StartupModule via + * Get().Register(...). The graph builder queries it once per build. + */ +class USHADERLABEDITOR_API FShaderLabAnchorCapabilityRegistry +{ +public: + static FShaderLabAnchorCapabilityRegistry& Get(); + + /** Register (or override, by Id) a capability. */ + void Register(FShaderLabAnchorCapability Capability); + + /** Visit every registered capability. */ + void ForEach(TFunctionRef Fn) const; + +private: + FShaderLabAnchorCapabilityRegistry(); + void RegisterBuiltins(); + + TArray Capabilities; +}; diff --git a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h index 8785752..2daa7f2 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h +++ b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h @@ -53,6 +53,16 @@ struct FShaderLabIntrinsicDesc /** Stage restriction; misuse is a build error. */ EShaderLabIntrinsicFrequency Frequency = EShaderLabIntrinsicFrequency::Any; + /** + * When true, this name is overloaded: the nullary form `UE_Name()` is this wired intrinsic, while + * `UE_Name()` is a raw-HLSL helper of the same name (see UEFunctions/*.ush). A call carrying more + * arguments than `Params` declares is the helper overload — the parser leaves it verbatim in the body + * for the shader compiler instead of rejecting it. Used e.g. by UE_DistanceToNearestSurface (nullary = + * current pixel; `(worldPos)` = distance-field lookup at an arbitrary position, whose global-distance- + * field binding is supplied by an anchor capability). Default false keeps the strict const-arg contract. + */ + bool bAllowRawHelperWithArgs = false; + /** HLSL return type for the authoring stub, e.g. "float2"/"float3"/"float". Required (see Register). */ FString ReturnType;