Fix SceneTexture reference data declare.

This commit is contained in:
Eragon-Brisingr
2026-07-03 18:45:27 +08:00
parent 530708cd29
commit 9d901f30bf
9 changed files with 624 additions and 32 deletions

View File

@@ -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*)

View File

@@ -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

View File

@@ -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("<unknown>");
}
/**
* 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<ESceneTextureId>& OutIds, TArray<FString>& 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("<expression>") : *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<UMaterialExpression*>& OutNodes, TArray<FString>& 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<ESceneTextureId> 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(<literal SL_ST_* token>, 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<UMaterialExpressionSceneTexture>(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<UMaterialExpression*>& OutNodes, TArray<FString>& /*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<void(const FShaderLabAnchorCapability&)> 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));
}
}

View File

@@ -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(<args>)` 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<FShaderLabScannedBody> 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<FName, FFunctionCtx>& 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<UMaterialExpressionSceneTexture>(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<UMaterialExpression>(&Material, Class);
Material.GetExpressionCollection().AddExpression(Expr);
Expr->MaterialExpressionEditorX = -1300;
Expr->MaterialExpressionEditorY = ParamY;
ParamY += 120;
return Expr;
} };
TArray<UMaterialExpression*> CapNodes;
if (!Cap.Emit(CapCtx, CapNodes, OutErrors))
{
bCapabilityFailed = true;
return;
}
AnchorInputs.Append(CapNodes);
});
if (bCapabilityFailed)
{
return false;
}
}

View File

@@ -64,4 +64,5 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
RegisterParticleIntrinsics(*this);
RegisterDecalIntrinsics(*this);
RegisterAtmosphereIntrinsics(*this);
RegisterDistanceFieldIntrinsics(*this);
}

View File

@@ -44,3 +44,4 @@ void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry);

View File

@@ -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>&, FString&)
{
return MakeSimple<UMaterialExpressionDistanceToNearestSurface>(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>&, FString&)
{
return MakeSimple<UMaterialExpressionDistanceFieldGradient>(M);
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -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<FShaderLabScannedBody>& 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<UMaterialExpression*(UClass* /*Class*/)> 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<FString> 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<bool(const FShaderLabAnchorCapabilityContext& /*Ctx*/,
TArray<UMaterialExpression*>& /*OutNodes*/, TArray<FString>& /*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<void(const FShaderLabAnchorCapability&)> Fn) const;
private:
FShaderLabAnchorCapabilityRegistry();
void RegisterBuiltins();
TArray<FShaderLabAnchorCapability> Capabilities;
};

View File

@@ -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(<args>)` 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;