mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Fix SceneTexture reference data declare.
This commit is contained in:
@@ -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*)
|
||||
|
||||
27
Shaders/Private/UEFunctions/DistanceField.ush
Normal file
27
Shaders/Private/UEFunctions/DistanceField.ush
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,4 +64,5 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
|
||||
RegisterParticleIntrinsics(*this);
|
||||
RegisterDecalIntrinsics(*this);
|
||||
RegisterAtmosphereIntrinsics(*this);
|
||||
RegisterDistanceFieldIntrinsics(*this);
|
||||
}
|
||||
|
||||
@@ -44,3 +44,4 @@ void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry);
|
||||
void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry);
|
||||
void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry);
|
||||
void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry);
|
||||
void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user