mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
Support all BSDF
This commit is contained in:
@@ -7,6 +7,40 @@
|
||||
// Per-pixel/vertex inputs are read through `UE_NodeName(...)` intrinsics (resolved by the graph
|
||||
// builder to real material expression nodes), not through a context struct.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read-only project/engine-driven permutation macros. The graph builder leaks SHADERLAB_QUALITY /
|
||||
// SHADERLAB_FEATURELEVEL / SHADERLAB_SHADINGPATH (via a before-attributes QualitySwitch/FeatureLevelSwitch/
|
||||
// ShadingPathSwitch, same mechanism as static switches) so a body can `#if` on them to adapt per permutation.
|
||||
// These are NOT author-settable — the engine picks the permutation. Compare against the ordered constants
|
||||
// below (ordered low->high so `<=`/`>=` are meaningful).
|
||||
// ---------------------------------------------------------------------------
|
||||
#define QUALITY_LOW 0
|
||||
#define QUALITY_MEDIUM 1
|
||||
#define QUALITY_HIGH 2
|
||||
#define QUALITY_EPIC 3
|
||||
|
||||
#define FEATURELEVEL_ES31 0
|
||||
#define FEATURELEVEL_SM5 1
|
||||
#define FEATURELEVEL_SM6 2
|
||||
|
||||
#define SHADINGPATH_DEFERRED 0
|
||||
#define SHADINGPATH_FORWARD 1
|
||||
#define SHADINGPATH_MOBILE 2
|
||||
|
||||
#ifdef SHADERLAB_IDE
|
||||
// IDE only: give the leaked macros a default so `#if SHADERLAB_QUALITY ...` completes/checks. At real
|
||||
// compile these come from the graph's before-attributes switch nodes (SHADERLAB_IDE is never defined then).
|
||||
#ifndef SHADERLAB_QUALITY
|
||||
#define SHADERLAB_QUALITY QUALITY_HIGH
|
||||
#endif
|
||||
#ifndef SHADERLAB_FEATURELEVEL
|
||||
#define SHADERLAB_FEATURELEVEL FEATURELEVEL_SM6
|
||||
#endif
|
||||
#ifndef SHADERLAB_SHADINGPATH
|
||||
#define SHADERLAB_SHADINGPATH SHADINGPATH_DEFERRED
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Surface description filled by the Surface(...) body. Defaults mirror the
|
||||
// UMaterialExpressionSubstrateSlabBSDF pin defaults so that fields the body does not
|
||||
// touch keep Substrate's native behavior.
|
||||
@@ -32,8 +66,204 @@ struct FShaderLabSurface
|
||||
float2 GlintUV;
|
||||
float Opacity;
|
||||
float OpacityMask;
|
||||
// Material-level outputs (single-Surface sugar): wired to the main node's Refraction / PixelDepthOffset pins.
|
||||
float Refraction; // scalar IOR (RefractionMethod = IndexOfRefraction); 1.0 = no bending
|
||||
float PixelDepthOffset; // world-unit depth push toward camera
|
||||
};
|
||||
|
||||
// --- Additional Substrate BSDF output structs. Each mirrors the pins of its engine BSDF node; the graph
|
||||
// builder wires only the fields a body writes, leaving the rest at the node's native default. ---
|
||||
|
||||
// Unlit BSDF (SL_UNLIT): pure emissive / transmittance, no lighting.
|
||||
struct FShaderLabUnlit
|
||||
{
|
||||
float3 EmissiveColor;
|
||||
float3 TransmittanceColor;
|
||||
float3 Normal;
|
||||
};
|
||||
|
||||
// Hair BSDF (SL_HAIR): hair-fiber shading.
|
||||
struct FShaderLabHair
|
||||
{
|
||||
float3 BaseColor;
|
||||
float Scatter;
|
||||
float Specular;
|
||||
float Roughness;
|
||||
float3 Backlit;
|
||||
float3 Tangent;
|
||||
float3 EmissiveColor;
|
||||
};
|
||||
|
||||
// Eye BSDF (SL_EYE): cornea / iris / sclera shading.
|
||||
struct FShaderLabEye
|
||||
{
|
||||
float3 DiffuseColor;
|
||||
float Roughness;
|
||||
float3 CorneaNormal;
|
||||
float3 IrisNormal;
|
||||
float3 IrisPlaneNormal;
|
||||
float IrisMask;
|
||||
float IrisDistance;
|
||||
float3 EmissiveColor;
|
||||
};
|
||||
|
||||
// Single Layer Water BSDF (SL_WATER): water surface + underwater scattering.
|
||||
struct FShaderLabWater
|
||||
{
|
||||
float3 BaseColor;
|
||||
float Metallic;
|
||||
float Specular;
|
||||
float Roughness;
|
||||
float3 Normal;
|
||||
float3 EmissiveColor;
|
||||
float3 TopMaterialOpacity;
|
||||
float3 WaterAlbedo;
|
||||
float3 WaterExtinction;
|
||||
float WaterPhaseG;
|
||||
float3 ColorScaleBehindWater;
|
||||
};
|
||||
|
||||
// Volumetric-Fog-Cloud BSDF (SL_VOLUME, Domain = Volume): participating media.
|
||||
struct FShaderLabVolume
|
||||
{
|
||||
float3 Albedo;
|
||||
float3 Extinction;
|
||||
float3 EmissiveColor;
|
||||
float AmbientOcclusion;
|
||||
};
|
||||
|
||||
// Simple Clear Coat BSDF (SL_CLEARCOAT): bottom layer + clear coat top.
|
||||
struct FShaderLabClearCoat
|
||||
{
|
||||
float3 DiffuseAlbedo;
|
||||
float3 F0;
|
||||
float Roughness;
|
||||
float ClearCoatCoverage;
|
||||
float ClearCoatRoughness;
|
||||
float3 Normal;
|
||||
float3 EmissiveColor;
|
||||
float3 BottomNormal;
|
||||
};
|
||||
|
||||
// Toon BSDF (SL_TOON, experimental): stylized shading.
|
||||
struct FShaderLabToon
|
||||
{
|
||||
float3 BaseColor;
|
||||
float Metallic;
|
||||
float Specular;
|
||||
float Roughness;
|
||||
float3 Normal;
|
||||
float3 EmissiveColor;
|
||||
float2 PatternUVs;
|
||||
float Anisotropy;
|
||||
float3 Tangent;
|
||||
};
|
||||
|
||||
// Light Function (SL_LIGHTFUNCTION, Domain = LightFunction): per-light modulation color.
|
||||
struct FShaderLabLightFunction
|
||||
{
|
||||
float3 Color;
|
||||
};
|
||||
|
||||
FShaderLabUnlit ShaderLabDefaultUnlit()
|
||||
{
|
||||
FShaderLabUnlit U;
|
||||
U.EmissiveColor = float3(0, 0, 0);
|
||||
U.TransmittanceColor = float3(1, 1, 1);
|
||||
U.Normal = float3(0, 0, 1);
|
||||
return U;
|
||||
}
|
||||
|
||||
FShaderLabHair ShaderLabDefaultHair()
|
||||
{
|
||||
FShaderLabHair H;
|
||||
H.BaseColor = float3(0, 0, 0);
|
||||
H.Scatter = 0;
|
||||
H.Specular = 0.5;
|
||||
H.Roughness = 0.5;
|
||||
H.Backlit = float3(0, 0, 0);
|
||||
H.Tangent = float3(1, 0, 0);
|
||||
H.EmissiveColor = float3(0, 0, 0);
|
||||
return H;
|
||||
}
|
||||
|
||||
FShaderLabEye ShaderLabDefaultEye()
|
||||
{
|
||||
FShaderLabEye E;
|
||||
E.DiffuseColor = float3(0, 0, 0);
|
||||
E.Roughness = 0.5;
|
||||
E.CorneaNormal = float3(0, 0, 1);
|
||||
E.IrisNormal = float3(0, 0, 1);
|
||||
E.IrisPlaneNormal = float3(0, 0, 1);
|
||||
E.IrisMask = 0;
|
||||
E.IrisDistance = 0;
|
||||
E.EmissiveColor = float3(0, 0, 0);
|
||||
return E;
|
||||
}
|
||||
|
||||
FShaderLabWater ShaderLabDefaultWater()
|
||||
{
|
||||
FShaderLabWater W;
|
||||
W.BaseColor = float3(0, 0, 0);
|
||||
W.Metallic = 0;
|
||||
W.Specular = 0.5;
|
||||
W.Roughness = 0.5;
|
||||
W.Normal = float3(0, 0, 1);
|
||||
W.EmissiveColor = float3(0, 0, 0);
|
||||
W.TopMaterialOpacity = float3(0, 0, 0);
|
||||
W.WaterAlbedo = float3(0, 0, 0);
|
||||
W.WaterExtinction = float3(0, 0, 0);
|
||||
W.WaterPhaseG = 0;
|
||||
W.ColorScaleBehindWater = float3(1, 1, 1);
|
||||
return W;
|
||||
}
|
||||
|
||||
FShaderLabVolume ShaderLabDefaultVolume()
|
||||
{
|
||||
FShaderLabVolume V;
|
||||
V.Albedo = float3(0, 0, 0);
|
||||
V.Extinction = float3(0, 0, 0);
|
||||
V.EmissiveColor = float3(0, 0, 0);
|
||||
V.AmbientOcclusion = 1;
|
||||
return V;
|
||||
}
|
||||
|
||||
FShaderLabClearCoat ShaderLabDefaultClearCoat()
|
||||
{
|
||||
FShaderLabClearCoat C;
|
||||
C.DiffuseAlbedo = float3(0.18, 0.18, 0.18);
|
||||
C.F0 = float3(0.04, 0.04, 0.04);
|
||||
C.Roughness = 0.5;
|
||||
C.ClearCoatCoverage = 0.5;
|
||||
C.ClearCoatRoughness = 0.5;
|
||||
C.Normal = float3(0, 0, 1);
|
||||
C.EmissiveColor = float3(0, 0, 0);
|
||||
C.BottomNormal = float3(0, 0, 1);
|
||||
return C;
|
||||
}
|
||||
|
||||
FShaderLabToon ShaderLabDefaultToon()
|
||||
{
|
||||
FShaderLabToon T;
|
||||
T.BaseColor = float3(0.18, 0.18, 0.18);
|
||||
T.Metallic = 0;
|
||||
T.Specular = 0.5;
|
||||
T.Roughness = 0.5;
|
||||
T.Normal = float3(0, 0, 1);
|
||||
T.EmissiveColor = float3(0, 0, 0);
|
||||
T.PatternUVs = float2(0, 0);
|
||||
T.Anisotropy = 0;
|
||||
T.Tangent = float3(1, 0, 0);
|
||||
return T;
|
||||
}
|
||||
|
||||
FShaderLabLightFunction ShaderLabDefaultLightFunction()
|
||||
{
|
||||
FShaderLabLightFunction L;
|
||||
L.Color = float3(1, 1, 1);
|
||||
return L;
|
||||
}
|
||||
|
||||
// Output for the PostProcess(...) entry (Domain = PostProcess). Color feeds the material EmissiveColor.
|
||||
struct FShaderLabPostProcess
|
||||
{
|
||||
@@ -82,6 +312,8 @@ FShaderLabSurface ShaderLabDefaultSurface()
|
||||
S.GlintUV = float2(0, 0);
|
||||
S.Opacity = 1;
|
||||
S.OpacityMask = 1;
|
||||
S.Refraction = 1.0;
|
||||
S.PixelDepthOffset = 0.0;
|
||||
return S;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
// #include "/Project/Lib/DetailLib.uslfunc"
|
||||
#define SL_IMPORT(...)
|
||||
|
||||
// Material Parameter Collection read. Precedes a `floatN <Name>;` declaration; reads a global MPC value
|
||||
// (not a per-instance knob). Path is the collection asset; optional Parameter overrides the in-collection name.
|
||||
// SL_COLLECTION(Path = "/Game/MPC/MPC_Weather", Parameter = "WindVector")
|
||||
// float3 WindVector;
|
||||
#define SL_COLLECTION(...)
|
||||
|
||||
// Library function (`.uslfunc` files only). Precedes a normal HLSL function; it may use UE_ intrinsics,
|
||||
// reference the library's own properties, and call other library functions.
|
||||
// SL_FUNCTION()
|
||||
@@ -39,20 +45,40 @@
|
||||
#define SL_FUNCTION(...)
|
||||
|
||||
// Pixel/vertex entry points. Precede a `void Name(inout F... X) { ... }` (Vertex takes FShaderLabVertex).
|
||||
// Optional material context: to call the UE_Transform* helpers (which need the material `Parameters`), add
|
||||
// a leading `FMaterialPixelParameters Parameters` (pixel entries) or `FMaterialVertexParameters Parameters`
|
||||
// (SL_VERTEX) BEFORE the output-struct parameter, named literally `Parameters`, e.g.
|
||||
// SL_SURFACE() void Surface(FMaterialPixelParameters Parameters, inout FShaderLabSurface S) { ... }
|
||||
#define SL_SURFACE(...)
|
||||
#define SL_POSTPROCESS(...)
|
||||
#define SL_UI(...)
|
||||
#define SL_VERTEX(...)
|
||||
|
||||
// Additional Substrate BSDF entries. Each precedes a `void Name(inout F<Bsdf> X) { ... }` whose output struct
|
||||
// is the matching FShaderLab<Bsdf> (see ShaderLabCommon.ush). Used either as the sole entry (whole material =
|
||||
// that BSDF) or as a named block referenced by SL_FRONTMATERIAL. Composition legality is enforced by the
|
||||
// builder (mirroring Substrate's operator allow-lists). SL_VOLUME requires Domain = Volume; SL_LIGHTFUNCTION
|
||||
// requires Domain = LightFunction; the rest are Surface-domain BSDFs.
|
||||
#define SL_UNLIT(...) // void Name(inout FShaderLabUnlit U)
|
||||
#define SL_HAIR(...) // void Name(inout FShaderLabHair H)
|
||||
#define SL_EYE(...) // void Name(inout FShaderLabEye E)
|
||||
#define SL_WATER(...) // void Name(inout FShaderLabWater W)
|
||||
#define SL_CLEARCOAT(...) // void Name(inout FShaderLabClearCoat C)
|
||||
#define SL_TOON(...) // void Name(inout FShaderLabToon T)
|
||||
#define SL_VOLUME(...) // void Name(inout FShaderLabVolume V) — Domain = Volume
|
||||
#define SL_LIGHTFUNCTION(...) // void Name(inout FShaderLabLightFunction L) — Domain = LightFunction
|
||||
|
||||
// Multi-slab building blocks.
|
||||
#define SL_SLAB(...) // precedes `void Name(inout FShaderLabSurface S) { ... }`
|
||||
#define SL_VALUE(...) // precedes `float Name() { return <float>; }`
|
||||
#define SL_SLAB(...) // precedes `void Name([FMaterialPixelParameters Parameters,] inout FShaderLabSurface S) { ... }`
|
||||
#define SL_VALUE(...) // precedes `float Name([FMaterialPixelParameters Parameters]) { return <float>; }`
|
||||
#define SL_FRONTMATERIAL(...) // standalone: SL_FRONTMATERIAL(VerticalLayer(Coat, Metal, Thickness))
|
||||
#define SL_OPACITY(...) // standalone: SL_OPACITY(SomeValueName)
|
||||
#define SL_OPACITY_MASK(...) // standalone: SL_OPACITY_MASK(SomeValueName)
|
||||
#define SL_REFRACTION(...) // standalone: SL_REFRACTION(SomeValueName) — feeds the material Refraction pin
|
||||
#define SL_PIXEL_DEPTH_OFFSET(...)// standalone: SL_PIXEL_DEPTH_OFFSET(SomeValueName) — feeds the material PixelDepthOffset pin
|
||||
|
||||
// Vertex Interpolator: precedes `floatN Name() { return <vertex-frequency HLSL>; }` — a value computed
|
||||
// per-vertex and interpolated to the pixel shader (backed by a UMaterialExpressionVertexInterpolator).
|
||||
// Vertex Interpolator: precedes `floatN Name([FMaterialVertexParameters Parameters]) { return <vertex HLSL>; }`
|
||||
// — a value computed per-vertex and interpolated to the pixel shader (backed by a UMaterialExpressionVertexInterpolator).
|
||||
// Read it in a pixel body with `UE_Interpolator(Name)`, or use a scalar one as a topology mix factor.
|
||||
#define SL_INTERPOLATOR(...)
|
||||
|
||||
@@ -70,7 +96,10 @@
|
||||
// SL_SETTINGS : Domain, BlendMode, TwoSided, OpacityMaskClipValue, <reflection-allowlist keys...>
|
||||
// Domain = Surface | PostProcess | UI | Decal
|
||||
// BlendMode = Opaque | Masked | Translucent | Additive | Modulate
|
||||
// SL_PROPERTY : DisplayName, Category, SortPriority, Range (Scalar only), DefaultTexture (textures only)
|
||||
// SL_PROPERTY : Category, SortPriority, DefaultTexture (textures only),
|
||||
// ClampMin / ClampMax (Scalar slider bounds; each independent),
|
||||
// CustomPrimitiveData = <index> (Scalar/Vector only; per-instance via Custom Primitive Data;
|
||||
// mutually exclusive with ClampMin/ClampMax)
|
||||
//
|
||||
// Topology operators (used inside SL_FRONTMATERIAL):
|
||||
// VerticalLayer(Top, Base, Thickness) | HorizontalMix(Background, Foreground, Mix)
|
||||
|
||||
@@ -8,5 +8,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/Transform.ush" // generated (UE_Transform*VectorTo*)
|
||||
#include "/Plugin/ShaderLab/Private/UEFunctions/TransformPosition.ush" // generated (UE_Transform*PositionTo*)
|
||||
|
||||
57
Shaders/Private/UEFunctions/SceneTexture.ush
Normal file
57
Shaders/Private/UEFunctions/SceneTexture.ush
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// UE_ scene-texture reads — forward to the engine's SceneTextureLookup (MaterialTemplate.ush). Because
|
||||
// these take a dynamic UV they are emitted as HLSL helpers (like Noise), NOT wired intrinsics. The graph
|
||||
// builder places a hidden UMaterialExpressionSceneTexture node (wired to the ParameterAnchor) whenever a
|
||||
// body uses one of these, so the engine sets bNeedsSceneTextures and binds the scene-texture SRVs.
|
||||
//
|
||||
// Enum scheme B (see Noise.ush): under SHADERLAB_IDE the id is a real HLSL `enum` for completion/semantics;
|
||||
// at real compile it degrades to `int` + `#define`s aliasing the engine's PPI_* ids (defined in the material
|
||||
// environment), so no magic numbers are hard-coded here.
|
||||
//
|
||||
// Domain rules (enforced by the builder, mirroring the engine): SceneDepth/CustomDepth/CustomStencil work in
|
||||
// a translucent Surface, Decal, or PostProcess material; the full GBuffer / PostProcessInput ids require the
|
||||
// PostProcess (or Decal) domain. (Scene-color-behind-translucency uses a different engine path and is not
|
||||
// exposed here yet — read it as PostProcessInput0 in a PostProcess material instead.)
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef SHADERLAB_IDE
|
||||
enum ESLSceneTexture
|
||||
{
|
||||
SL_ST_SceneColor,
|
||||
SL_ST_SceneDepth,
|
||||
SL_ST_CustomDepth,
|
||||
SL_ST_CustomStencil,
|
||||
SL_ST_WorldNormal,
|
||||
SL_ST_PostProcessInput0,
|
||||
SL_ST_PostProcessInput1,
|
||||
SL_ST_PostProcessInput2,
|
||||
SL_ST_PostProcessInput3,
|
||||
SL_ST_PostProcessInput4,
|
||||
};
|
||||
float4 SceneTextureLookup(float2 UV, int SceneTextureIndex, bool bFiltered) { return (float4)0; }
|
||||
#else
|
||||
#define ESLSceneTexture int
|
||||
#define SL_ST_SceneColor PPI_SceneColor
|
||||
#define SL_ST_SceneDepth PPI_SceneDepth
|
||||
#define SL_ST_CustomDepth PPI_CustomDepth
|
||||
#define SL_ST_CustomStencil PPI_CustomStencil
|
||||
#define SL_ST_WorldNormal PPI_WorldNormal
|
||||
#define SL_ST_PostProcessInput0 PPI_PostProcessInput0
|
||||
#define SL_ST_PostProcessInput1 PPI_PostProcessInput1
|
||||
#define SL_ST_PostProcessInput2 PPI_PostProcessInput2
|
||||
#define SL_ST_PostProcessInput3 PPI_PostProcessInput3
|
||||
#define SL_ST_PostProcessInput4 PPI_PostProcessInput4
|
||||
#endif
|
||||
|
||||
// Full-control lookup: read any scene-texture id at UV (id is an ESLSceneTexture token).
|
||||
float4 UE_SceneTexture(ESLSceneTexture Id, float2 UV)
|
||||
{
|
||||
return SceneTextureLookup(UV, Id, false);
|
||||
}
|
||||
|
||||
// Convenience wrappers for the common reads.
|
||||
float UE_SceneDepth(float2 UV) { return SceneTextureLookup(UV, SL_ST_SceneDepth, false).r; }
|
||||
float UE_CustomDepth(float2 UV) { return SceneTextureLookup(UV, SL_ST_CustomDepth, false).r; }
|
||||
float UE_CustomStencil(float2 UV) { return SceneTextureLookup(UV, SL_ST_CustomStencil, false).r; }
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "ShaderLabModel.h"
|
||||
|
||||
#include "Templates/Function.h"
|
||||
|
||||
const FShaderLabProperty* FShaderLabModel::FindProperty(FName InName) const
|
||||
{
|
||||
return Properties.FindByPredicate([InName](const FShaderLabProperty& P) { return P.Name == InName; });
|
||||
@@ -22,3 +24,114 @@ bool FShaderLabModel::HasStaticSwitches() const
|
||||
return Properties.ContainsByPredicate(
|
||||
[](const FShaderLabProperty& P) { return P.Type == EShaderLabPropertyType::StaticBool; });
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// One-hot flag per BSDF type; a subtree's flowing-type set is the OR of its leaves.
|
||||
uint32 BsdfTypeFlag(EShaderLabBsdfType T) { return 1u << static_cast<uint32>(T); }
|
||||
|
||||
const TCHAR* BsdfTypeName(EShaderLabBsdfType T)
|
||||
{
|
||||
switch (T)
|
||||
{
|
||||
case EShaderLabBsdfType::Slab: return TEXT("Slab");
|
||||
case EShaderLabBsdfType::Unlit: return TEXT("Unlit");
|
||||
case EShaderLabBsdfType::Hair: return TEXT("Hair");
|
||||
case EShaderLabBsdfType::Eye: return TEXT("Eye");
|
||||
case EShaderLabBsdfType::Water: return TEXT("Water");
|
||||
case EShaderLabBsdfType::Volume: return TEXT("Volume");
|
||||
case EShaderLabBsdfType::ClearCoat: return TEXT("ClearCoat");
|
||||
case EShaderLabBsdfType::Toon: return TEXT("Toon");
|
||||
case EShaderLabBsdfType::LightFunction: return TEXT("LightFunction");
|
||||
default: return TEXT("?");
|
||||
}
|
||||
}
|
||||
|
||||
const TCHAR* OpName(EShaderLabOp Op)
|
||||
{
|
||||
switch (Op)
|
||||
{
|
||||
case EShaderLabOp::VerticalLayer: return TEXT("VerticalLayer");
|
||||
case EShaderLabOp::HorizontalMix: return TEXT("HorizontalMix");
|
||||
case EShaderLabOp::Add: return TEXT("Add");
|
||||
case EShaderLabOp::Weight: return TEXT("Weight");
|
||||
case EShaderLabOp::Select: return TEXT("Select");
|
||||
default: return TEXT("SlabRef");
|
||||
}
|
||||
}
|
||||
|
||||
// Allowed flowing-type mask per operator (mirrors SubstrateTranslatorCommon.cpp allow-lists).
|
||||
uint32 AllowedMask(EShaderLabOp Op)
|
||||
{
|
||||
const uint32 Slab = BsdfTypeFlag(EShaderLabBsdfType::Slab);
|
||||
const uint32 Unlit = BsdfTypeFlag(EShaderLabBsdfType::Unlit);
|
||||
const uint32 Toon = BsdfTypeFlag(EShaderLabBsdfType::Toon);
|
||||
const uint32 Hair = BsdfTypeFlag(EShaderLabBsdfType::Hair);
|
||||
const uint32 Eye = BsdfTypeFlag(EShaderLabBsdfType::Eye);
|
||||
switch (Op)
|
||||
{
|
||||
case EShaderLabOp::VerticalLayer:
|
||||
case EShaderLabOp::HorizontalMix:
|
||||
case EShaderLabOp::Add: return Slab;
|
||||
case EShaderLabOp::Weight: return Unlit | Slab | Toon;
|
||||
case EShaderLabOp::Select: return Slab | Hair | Eye | Toon;
|
||||
default: return 0xFFFFFFFFu; // SlabRef: leaf, unconstrained here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FShaderLabModel::ValidateTopology(TArray<FString>& OutErrors) const
|
||||
{
|
||||
if (TopologyRoot == INDEX_NONE || Topology.Num() == 0)
|
||||
{
|
||||
return true; // Single-Surface sugar / non-Substrate entries: nothing to validate.
|
||||
}
|
||||
|
||||
auto SlabTypeByName = [this](FName SlabName, EShaderLabBsdfType& OutType) -> bool
|
||||
{
|
||||
for (const FShaderLabSlab& S : Slabs)
|
||||
{
|
||||
if (S.Name == SlabName) { OutType = S.BsdfType; return true; }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
bool bOk = true;
|
||||
// Recursively compute a subtree's flowing-type mask and validate each operator against its allow-list.
|
||||
TFunction<uint32(int32)> Visit = [&](int32 Index) -> uint32
|
||||
{
|
||||
if (!Topology.IsValidIndex(Index)) { return 0; }
|
||||
const FShaderLabTopoNode& Node = Topology[Index];
|
||||
if (Node.Op == EShaderLabOp::SlabRef)
|
||||
{
|
||||
EShaderLabBsdfType T;
|
||||
return SlabTypeByName(Node.SlabRef, T) ? BsdfTypeFlag(T) : 0;
|
||||
}
|
||||
uint32 Mask = 0;
|
||||
if (Node.ChildA != INDEX_NONE) { Mask |= Visit(Node.ChildA); }
|
||||
if (Node.ChildB != INDEX_NONE) { Mask |= Visit(Node.ChildB); }
|
||||
|
||||
const uint32 Allowed = AllowedMask(Node.Op);
|
||||
const uint32 Illegal = Mask & ~Allowed;
|
||||
if (Illegal != 0)
|
||||
{
|
||||
// List the offending flowing types.
|
||||
FString Types;
|
||||
for (uint32 i = 0; i <= static_cast<uint32>(EShaderLabBsdfType::LightFunction); ++i)
|
||||
{
|
||||
if (Illegal & (1u << i))
|
||||
{
|
||||
if (!Types.IsEmpty()) { Types += TEXT(", "); }
|
||||
Types += BsdfTypeName(static_cast<EShaderLabBsdfType>(i));
|
||||
}
|
||||
}
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("%s(%d): Substrate error: operator '%s' cannot combine BSDF type(s) {%s} (only Slab layers/mixes/adds; Weight allows Unlit/Slab/Toon; Select allows Slab/Hair/Eye/Toon)."),
|
||||
*SourceFilePath, Node.SourceLine, OpName(Node.Op), *Types));
|
||||
bOk = false;
|
||||
}
|
||||
return Mask;
|
||||
};
|
||||
Visit(TopologyRoot);
|
||||
return bOk;
|
||||
}
|
||||
|
||||
@@ -384,6 +384,8 @@ namespace ShaderLabParser_Private
|
||||
if (Token == TEXT("PostProcess")) { Out = EShaderLabDomain::PostProcess; return true; }
|
||||
if (Token == TEXT("UI")) { Out = EShaderLabDomain::UI; return true; }
|
||||
if (Token == TEXT("Decal")) { Out = EShaderLabDomain::Decal; return true; }
|
||||
if (Token == TEXT("Volume")) { Out = EShaderLabDomain::Volume; return true; }
|
||||
if (Token == TEXT("LightFunction")) { Out = EShaderLabDomain::LightFunction; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -394,6 +396,8 @@ namespace ShaderLabParser_Private
|
||||
if (Token == TEXT("Translucent")) { Out = EShaderLabBlendMode::Translucent; return true; }
|
||||
if (Token == TEXT("Additive")) { Out = EShaderLabBlendMode::Additive; return true; }
|
||||
if (Token == TEXT("Modulate")) { Out = EShaderLabBlendMode::Modulate; return true; }
|
||||
if (Token == TEXT("AlphaComposite")) { Out = EShaderLabBlendMode::AlphaComposite; return true; }
|
||||
if (Token == TEXT("AlphaHoldout")) { Out = EShaderLabBlendMode::AlphaHoldout; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -404,6 +408,23 @@ namespace ShaderLabParser_Private
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True if Token is a non-empty run of ASCII digits (a non-negative integer literal). */
|
||||
static bool IsNonNegativeInteger(const FString& Token)
|
||||
{
|
||||
if (Token.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const TCHAR C : Token)
|
||||
{
|
||||
if (!FChar::IsDigit(C))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Map an HLSL declaration type token to a DSL property type. */
|
||||
static bool InferPropertyType(const FString& HlslType, EShaderLabPropertyType& Out)
|
||||
{
|
||||
@@ -412,6 +433,9 @@ namespace ShaderLabParser_Private
|
||||
if (HlslType == TEXT("float4")) { Out = EShaderLabPropertyType::Vector; return true; }
|
||||
if (HlslType == TEXT("Texture2D")) { Out = EShaderLabPropertyType::Texture2D; return true; }
|
||||
if (HlslType == TEXT("TextureCube")) { Out = EShaderLabPropertyType::TextureCube; return true; }
|
||||
if (HlslType == TEXT("Texture2DArray")) { Out = EShaderLabPropertyType::Texture2DArray; return true; }
|
||||
if (HlslType == TEXT("Texture3D")) { Out = EShaderLabPropertyType::Texture3D; return true; }
|
||||
if (HlslType == TEXT("TextureCubeArray")) { Out = EShaderLabPropertyType::TextureCubeArray; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -514,6 +538,96 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True if `Body` references `Ident` as a whole identifier token (not as a substring of a longer word). */
|
||||
bool BodyReferencesIdentifier(const FString& Body, const TCHAR* Ident)
|
||||
{
|
||||
const FString Needle(Ident);
|
||||
const int32 NeedleLen = Needle.Len();
|
||||
auto IsIdentChar = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
||||
int32 From = 0;
|
||||
for (;;)
|
||||
{
|
||||
const int32 At = Body.Find(Needle, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
|
||||
if (At == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const bool bLeftOk = (At == 0) || !IsIdentChar(Body[At - 1]);
|
||||
const int32 EndIdx = At + NeedleLen;
|
||||
const bool bRightOk = (EndIdx >= Body.Len()) || !IsIdentChar(Body[EndIdx]);
|
||||
if (bLeftOk && bRightOk)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
From = At + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the optional material `Parameters` context parameter on a body-bearing construct, and check
|
||||
* the body's use of it. Contract:
|
||||
* - When bRequireStruct, the output-struct parameter (its type starts with "FShaderLab") must be the
|
||||
* LAST parameter, so the graph builder's `Last()` selects it as the output struct.
|
||||
* - Any non-struct parameter must be a single `Parameters` of type `ParamsType` (the frequency-correct
|
||||
* FMaterial{Pixel,Vertex}Parameters), named literally `Parameters` — matching the ambient variable UE
|
||||
* injects into the generated Custom node (its declared signature is dropped at real compile).
|
||||
* - If the body references the `Parameters` identifier it MUST be declared, so the IDE (which sees the
|
||||
* declaration) and the real compile (which uses the ambient `Parameters`) agree.
|
||||
* Reports the first violation at (ErrLine, ErrCol) and returns false.
|
||||
*/
|
||||
bool ValidateBodyParameters(FScanner& S, const TArray<FShaderLabEntryParam>& Params, const FString& Body,
|
||||
const TCHAR* ParamsType, bool bRequireStruct, const TCHAR* What, int32 ErrLine, int32 ErrCol)
|
||||
{
|
||||
bool bHasParameters = false;
|
||||
for (int32 i = 0; i < Params.Num(); ++i)
|
||||
{
|
||||
const FShaderLabEntryParam& P = Params[i];
|
||||
const bool bIsStruct = P.Type.StartsWith(TEXT("FShaderLab"));
|
||||
if (bIsStruct)
|
||||
{
|
||||
if (!bRequireStruct)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s takes no output-struct parameter"), What), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
if (i != Params.Num() - 1)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s: the output-struct parameter must be the last parameter (put `Parameters` before it)"), What), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A non-struct parameter is only allowed as the optional material context.
|
||||
if (P.Name != TEXT("Parameters"))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s: unexpected parameter '%s' (only an optional `Parameters` context parameter is allowed)"), What, *P.Name), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
if (P.Type != ParamsType)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s: `Parameters` must be of type %s here"), What, ParamsType), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
if (bHasParameters)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s: duplicate `Parameters` parameter"), What), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
bHasParameters = true;
|
||||
}
|
||||
if (bRequireStruct && (Params.Num() == 0 || !Params.Last().Type.StartsWith(TEXT("FShaderLab"))))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s must take an (inout FShaderLab...) output parameter as its last parameter"), What), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
if (!bHasParameters && BodyReferencesIdentifier(Body, TEXT("Parameters")))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s uses `Parameters` but does not declare it; add a `%s Parameters` parameter to the signature"), What, ParamsType), ErrLine, ErrCol);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParseSettings(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FShaderLabSettings& Out = Model.Settings;
|
||||
@@ -589,11 +703,7 @@ namespace
|
||||
Key = Key.TrimStartAndEnd();
|
||||
Value = Value.TrimStartAndEnd();
|
||||
|
||||
if (Key == TEXT("DisplayName"))
|
||||
{
|
||||
Prop.DisplayName = Value.TrimQuotes();
|
||||
}
|
||||
else if (Key == TEXT("Category"))
|
||||
if (Key == TEXT("Category"))
|
||||
{
|
||||
Prop.Group = Value.TrimQuotes();
|
||||
}
|
||||
@@ -605,24 +715,28 @@ namespace
|
||||
{
|
||||
Prop.TextureDefault = Value.TrimQuotes();
|
||||
}
|
||||
else if (Key == TEXT("Range"))
|
||||
else if (Key == TEXT("ClampMin"))
|
||||
{
|
||||
// Trim exactly the one wrapping (...) then split — preserves paren-awareness of the interior.
|
||||
if (!Value.StartsWith(TEXT("(")) || !Value.EndsWith(TEXT(")")))
|
||||
// UE UPROPERTY-style slider lower bound (maps to ScalarParameter SliderMin).
|
||||
Prop.bHasClampMin = true;
|
||||
Prop.ClampMin = FCString::Atof(*Value);
|
||||
}
|
||||
else if (Key == TEXT("ClampMax"))
|
||||
{
|
||||
// UE UPROPERTY-style slider upper bound (maps to ScalarParameter SliderMax).
|
||||
Prop.bHasClampMax = true;
|
||||
Prop.ClampMax = FCString::Atof(*Value);
|
||||
}
|
||||
else if (Key == TEXT("CustomPrimitiveData"))
|
||||
{
|
||||
// Value is the mandatory, author-specified float slot index (no auto-assignment).
|
||||
if (!IsNonNegativeInteger(Value))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString()), ErrLine, ErrCol);
|
||||
S.Error(FString::Printf(TEXT("Property '%s' CustomPrimitiveData expects a non-negative integer index"), *Prop.Name.ToString()), ErrLine, ErrCol);
|
||||
return;
|
||||
}
|
||||
const FString Inner = Value.Mid(1, Value.Len() - 2);
|
||||
const TArray<FString> Comps = SplitTopLevel(Inner, TEXT(','));
|
||||
if (Comps.Num() != 2)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString()), ErrLine, ErrCol);
|
||||
return;
|
||||
}
|
||||
Prop.bHasRange = true;
|
||||
Prop.RangeMin = FCString::Atof(*Comps[0]);
|
||||
Prop.RangeMax = FCString::Atof(*Comps[1]);
|
||||
Prop.bUseCustomPrimitiveData = true;
|
||||
Prop.PrimitiveDataIndex = FCString::Atoi(*Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -697,7 +811,7 @@ namespace
|
||||
}
|
||||
if (!InferPropertyType(TypeTok, Prop.Type))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Unsupported property type '%s' (use float/float3/float4/Texture2D/TextureCube or `#define` for StaticBool)"), *TypeTok));
|
||||
S.Error(FString::Printf(TEXT("Unsupported property type '%s' (use float/float3/float4/Texture2D/TextureCube/Texture2DArray/Texture3D/TextureCubeArray or `#define` for StaticBool)"), *TypeTok));
|
||||
return;
|
||||
}
|
||||
FString Name;
|
||||
@@ -710,7 +824,7 @@ namespace
|
||||
NameCol = S.Column;
|
||||
Prop.Name = FName(*Name);
|
||||
|
||||
const bool bIsTexture = (Prop.Type == EShaderLabPropertyType::Texture2D || Prop.Type == EShaderLabPropertyType::TextureCube);
|
||||
const bool bIsTexture = ShaderLabIsTextureType(Prop.Type);
|
||||
if (bIsTexture)
|
||||
{
|
||||
// `Texture2D Name;` (no initializer) optionally followed by `SamplerState NameSampler;`.
|
||||
@@ -774,9 +888,20 @@ namespace
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Prop.DisplayName.IsEmpty())
|
||||
// CustomPrimitiveData contract: only Scalar/Vector support it in UE, and it is mutually exclusive
|
||||
// with the slider clamps (the MIC editor disables the slider under bUseCustomPrimitiveData).
|
||||
if (Prop.bUseCustomPrimitiveData)
|
||||
{
|
||||
Prop.DisplayName = Prop.Name.ToString();
|
||||
if (Prop.Type != EShaderLabPropertyType::Scalar && Prop.Type != EShaderLabPropertyType::Vector)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s': CustomPrimitiveData is only valid on Scalar (float) or Vector (float4) properties"), *Prop.Name.ToString()), NameLine, NameCol);
|
||||
return;
|
||||
}
|
||||
if (Prop.bHasClampMin || Prop.bHasClampMax)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s': ClampMin/ClampMax cannot be combined with CustomPrimitiveData"), *Prop.Name.ToString()), NameLine, NameCol);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Model.FindProperty(Prop.Name) != nullptr)
|
||||
{
|
||||
@@ -912,14 +1037,139 @@ namespace
|
||||
Model.Functions.Add(MoveTemp(Fn));
|
||||
}
|
||||
|
||||
void ParseEntry(FScanner& S, FShaderLabModel& Model, const FString& Marker)
|
||||
/**
|
||||
* Parse BSDF node modifiers from an SL_SURFACE/SL_SLAB(...) specifier list. Empty list is fine. Each modifier
|
||||
* is validated for applicability to BsdfType (contract-style: a modifier the target BSDF can't consume is a
|
||||
* hard error, not a silent no-op): SubsurfaceProfile -> Slab/Eye; SpecularProfile/SubSurfaceType -> Slab;
|
||||
* ToonProfile -> Toon.
|
||||
*/
|
||||
bool ParseBsdfModifiers(FScanner& S, const FString& Inner, EShaderLabBsdfType BsdfType, FShaderLabBsdfModifiers& Out, const TCHAR* What)
|
||||
{
|
||||
// Marker macro takes no args: SL_SURFACE()/SL_POSTPROCESS()/SL_UI()/SL_VERTEX().
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored))
|
||||
const bool bIsSlab = (BsdfType == EShaderLabBsdfType::Slab);
|
||||
const bool bIsEye = (BsdfType == EShaderLabBsdfType::Eye);
|
||||
const bool bIsToon = (BsdfType == EShaderLabBsdfType::Toon);
|
||||
for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(',')))
|
||||
{
|
||||
const FString Stmt = StmtRaw.TrimStartAndEnd();
|
||||
if (Stmt.IsEmpty()) { continue; }
|
||||
FString Key, Value;
|
||||
if (!Stmt.Split(TEXT("="), &Key, &Value))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s has malformed specifier '%s' (expected Key = Value)"), What, *Stmt));
|
||||
return false;
|
||||
}
|
||||
Key = Key.TrimStartAndEnd();
|
||||
Value = Value.TrimStartAndEnd().TrimQuotes();
|
||||
if (Key == TEXT("SubsurfaceProfile"))
|
||||
{
|
||||
if (!bIsSlab && !bIsEye) { S.Error(FString::Printf(TEXT("%s: SubsurfaceProfile only applies to SL_SURFACE/SL_SLAB or SL_EYE"), What)); return false; }
|
||||
Out.SubsurfaceProfilePath = Value;
|
||||
}
|
||||
else if (Key == TEXT("SpecularProfile"))
|
||||
{
|
||||
if (!bIsSlab) { S.Error(FString::Printf(TEXT("%s: SpecularProfile only applies to SL_SURFACE/SL_SLAB"), What)); return false; }
|
||||
Out.SpecularProfilePath = Value;
|
||||
}
|
||||
else if (Key == TEXT("SubSurfaceType"))
|
||||
{
|
||||
if (!bIsSlab) { S.Error(FString::Printf(TEXT("%s: SubSurfaceType only applies to SL_SURFACE/SL_SLAB"), What)); return false; }
|
||||
Out.SubSurfaceType = Value;
|
||||
}
|
||||
else if (Key == TEXT("ToonProfile"))
|
||||
{
|
||||
if (!bIsToon) { S.Error(FString::Printf(TEXT("%s: ToonProfile only applies to SL_TOON"), What)); return false; }
|
||||
Out.ToonProfilePath = Value;
|
||||
}
|
||||
else { S.Error(FString::Printf(TEXT("%s has unknown specifier '%s' (use SubsurfaceProfile/SpecularProfile/ToonProfile/SubSurfaceType)"), What, *Key)); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** SL_COLLECTION(Path="...", [Parameter="..."]) floatN <Name>; — a Material Parameter Collection read. */
|
||||
void ParseCollection(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FShaderLabCollectionParam C;
|
||||
for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(',')))
|
||||
{
|
||||
const FString Stmt = StmtRaw.TrimStartAndEnd();
|
||||
if (Stmt.IsEmpty()) { continue; }
|
||||
FString Key, Value;
|
||||
if (!Stmt.Split(TEXT("="), &Key, &Value))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Malformed SL_COLLECTION specifier '%s' (expected Key = Value)"), *Stmt));
|
||||
return;
|
||||
}
|
||||
Key = Key.TrimStartAndEnd();
|
||||
Value = Value.TrimStartAndEnd().TrimQuotes();
|
||||
if (Key == TEXT("Path")) { C.CollectionPath = Value; }
|
||||
else if (Key == TEXT("Parameter")) { C.ParameterName = FName(*Value); }
|
||||
else { S.Error(FString::Printf(TEXT("SL_COLLECTION has unknown specifier '%s' (use Path / Parameter)"), *Key)); return; }
|
||||
}
|
||||
S.SkipTrivia();
|
||||
const int32 DeclLine = S.Line, DeclCol = S.Column;
|
||||
FString TypeTok;
|
||||
if (!S.ReadIdentifier(TypeTok))
|
||||
{
|
||||
S.Error(TEXT("SL_COLLECTION must be followed by a `float/float3/float4 <Name>;` declaration"), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
if (!InferPropertyType(TypeTok, C.Type)
|
||||
|| !(C.Type == EShaderLabPropertyType::Scalar || C.Type == EShaderLabPropertyType::Color || C.Type == EShaderLabPropertyType::Vector))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("SL_COLLECTION type must be float/float3/float4, got '%s'"), *TypeTok), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
S.Error(TEXT("SL_COLLECTION declaration is missing a name"), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
if (!S.Expect(TEXT(';'), TEXT("after an SL_COLLECTION declaration")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (C.CollectionPath.IsEmpty())
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("SL_COLLECTION '%s' requires Path = \"/Game/...\""), *Name), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
C.Name = FName(*Name);
|
||||
C.Line = DeclLine;
|
||||
if (C.ParameterName.IsNone()) { C.ParameterName = C.Name; }
|
||||
if (Model.Collections.ContainsByPredicate([&C](const FShaderLabCollectionParam& E) { return E.Name == C.Name; }))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Duplicate SL_COLLECTION name '%s'"), *Name), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
Model.Collections.Add(MoveTemp(C));
|
||||
}
|
||||
|
||||
void ParseEntry(FScanner& S, FShaderLabModel& Model, const FString& Marker)
|
||||
{
|
||||
// SL_SURFACE(...) accepts BSDF modifier specifiers; the other entries take no args.
|
||||
FString SpecInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SpecInner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Marker != TEXT("SL_SURFACE") && !SpecInner.TrimStartAndEnd().IsEmpty())
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s takes no specifiers (only SL_SURFACE/SL_SLAB accept BSDF modifiers)"), *Marker));
|
||||
return;
|
||||
}
|
||||
FShaderLabBsdfModifiers EntryMods;
|
||||
if (Marker == TEXT("SL_SURFACE") && !ParseBsdfModifiers(S, SpecInner, EShaderLabBsdfType::Slab, EntryMods, *Marker))
|
||||
{
|
||||
return;
|
||||
}
|
||||
S.SkipTrivia();
|
||||
const int32 DeclLine = S.Line, DeclCol = S.Column;
|
||||
FString Name, SigInner, Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine))
|
||||
@@ -933,6 +1183,15 @@ namespace
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertex/Interpolator run at vertex frequency; Surface/PostProcess/UI at pixel frequency. An optional
|
||||
// explicit `Parameters` context parameter must match that frequency (see ValidateBodyParameters).
|
||||
const bool bVertexEntry = (Marker == TEXT("SL_VERTEX"));
|
||||
const TCHAR* ParamsType = bVertexEntry ? TEXT("FMaterialVertexParameters") : TEXT("FMaterialPixelParameters");
|
||||
if (!ValidateBodyParameters(S, Params, Body, ParamsType, /*bRequireStruct*/ true, *Marker, DeclLine, DeclCol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Marker == TEXT("SL_VERTEX"))
|
||||
{
|
||||
Model.VertexParams = MoveTemp(Params);
|
||||
@@ -954,12 +1213,26 @@ namespace
|
||||
Model.SurfaceEntry = (Marker == TEXT("SL_POSTPROCESS")) ? EShaderLabEntry::PostProcess
|
||||
: (Marker == TEXT("SL_UI")) ? EShaderLabEntry::UI
|
||||
: EShaderLabEntry::Surface;
|
||||
if (Model.SurfaceEntry == EShaderLabEntry::Surface)
|
||||
{
|
||||
Model.SurfaceModifiers = MoveTemp(EntryMods);
|
||||
}
|
||||
}
|
||||
|
||||
void ParseSlab(FScanner& S, FShaderLabModel& Model)
|
||||
/**
|
||||
* Parse a named BSDF block: `SL_SLAB()`/`SL_HAIR()`/... `void <Name>(inout F<Bsdf> X){...}`. BsdfType comes
|
||||
* from the marker; ExpectedStruct is the required output-struct type name (e.g. "FShaderLabHair"). Blocks go
|
||||
* into Model.Slabs; a single dedicated (non-Slab) block with no SL_FRONTMATERIAL is auto-rooted at end-of-parse.
|
||||
*/
|
||||
void ParseBsdfBlock(FScanner& S, FShaderLabModel& Model, EShaderLabBsdfType BsdfType, const TCHAR* Marker, const TCHAR* ExpectedStruct)
|
||||
{
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_SLAB()
|
||||
FString SpecInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SpecInner)) // marker(...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
FShaderLabBsdfModifiers BlockMods;
|
||||
if (!ParseBsdfModifiers(S, SpecInner, BsdfType, BlockMods, Marker))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -974,7 +1247,17 @@ namespace
|
||||
TArray<FShaderLabEntryParam> Params;
|
||||
if (!ParseEntryParams(SigInner, Params) || Params.Num() < 1)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Slab '%s' must take an (inout FShaderLabSurface) parameter"), *Name), DeclLine, DeclCol);
|
||||
S.Error(FString::Printf(TEXT("%s '%s' must take an (inout %s) parameter"), Marker, *Name, ExpectedStruct), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
if (!ValidateBodyParameters(S, Params, Body, TEXT("FMaterialPixelParameters"), /*bRequireStruct*/ true,
|
||||
*FString::Printf(TEXT("%s '%s'"), Marker, *Name), DeclLine, DeclCol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Params.Last().Type != ExpectedStruct)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s '%s' must take an (inout %s), got '%s'"), Marker, *Name, ExpectedStruct, *Params.Last().Type), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
const FName SlabName(*Name);
|
||||
@@ -985,9 +1268,11 @@ namespace
|
||||
}
|
||||
FShaderLabSlab Slab;
|
||||
Slab.Name = SlabName;
|
||||
Slab.BsdfType = BsdfType;
|
||||
Slab.OutParamName = Params.Last().Name;
|
||||
Slab.Body = MoveTemp(Body);
|
||||
Slab.BodyLine = BodyLine;
|
||||
Slab.Modifiers = MoveTemp(BlockMods);
|
||||
Model.Slabs.Add(MoveTemp(Slab));
|
||||
}
|
||||
|
||||
@@ -1006,6 +1291,17 @@ namespace
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FShaderLabEntryParam> Params;
|
||||
if (!ParseEntryParams(SigInner, Params))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Value '%s' has a malformed parameter signature"), *Name), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
if (!ValidateBodyParameters(S, Params, Body, TEXT("FMaterialPixelParameters"), /*bRequireStruct*/ false,
|
||||
*FString::Printf(TEXT("Value '%s'"), *Name), DeclLine, DeclCol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const FName ValueName(*Name);
|
||||
if (Model.Values.ContainsByPredicate([&ValueName](const FShaderLabValue& E) { return E.Name == ValueName; }))
|
||||
{
|
||||
@@ -1066,6 +1362,17 @@ namespace
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FShaderLabEntryParam> Params;
|
||||
if (!ParseEntryParams(SigInner, Params))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Interpolator '%s' has a malformed parameter signature"), *Name), DeclLine, DeclCol);
|
||||
return;
|
||||
}
|
||||
if (!ValidateBodyParameters(S, Params, Body, TEXT("FMaterialVertexParameters"), /*bRequireStruct*/ false,
|
||||
*FString::Printf(TEXT("Interpolator '%s'"), *Name), DeclLine, DeclCol))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const FName IName(*Name);
|
||||
if (Model.Interpolators.ContainsByPredicate([&IName](const FShaderLabInterpolator& E) { return E.Name == IName; }))
|
||||
{
|
||||
@@ -1221,6 +1528,8 @@ namespace
|
||||
/** Recursively parse a FrontMaterial topology expression; returns the node index (INDEX_NONE on error). */
|
||||
int32 ParseTopoExpr(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
S.SkipTrivia();
|
||||
const int32 NodeLine = S.Line; // source line of this operator/leaf token, for composition-error mapping
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
@@ -1234,6 +1543,7 @@ namespace
|
||||
FShaderLabTopoNode Leaf;
|
||||
Leaf.Op = EShaderLabOp::SlabRef;
|
||||
Leaf.SlabRef = FName(*Name);
|
||||
Leaf.SourceLine = NodeLine;
|
||||
return Model.Topology.Add(Leaf);
|
||||
}
|
||||
|
||||
@@ -1257,6 +1567,7 @@ namespace
|
||||
}
|
||||
FShaderLabTopoNode Node;
|
||||
Node.Op = Op;
|
||||
Node.SourceLine = NodeLine;
|
||||
Node.ChildA = ParseTopoExpr(S, Model);
|
||||
if (S.bFailed) { return INDEX_NONE; }
|
||||
if (NumExprChildren >= 2)
|
||||
@@ -1433,6 +1744,11 @@ bool FShaderLabParser::Parse(
|
||||
{
|
||||
ParseProperty(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_COLLECTION"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_COLLECTION"))) { return false; }
|
||||
ParseCollection(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_SURFACE") || Token == TEXT("SL_POSTPROCESS") || Token == TEXT("SL_UI") || Token == TEXT("SL_VERTEX"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("A pixel/vertex entry"))) { return false; }
|
||||
@@ -1441,7 +1757,47 @@ bool FShaderLabParser::Parse(
|
||||
else if (Token == TEXT("SL_SLAB"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_SLAB"))) { return false; }
|
||||
ParseSlab(S, OutModel);
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Slab, TEXT("SL_SLAB"), TEXT("FShaderLabSurface"));
|
||||
}
|
||||
else if (Token == TEXT("SL_UNLIT"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_UNLIT"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Unlit, TEXT("SL_UNLIT"), TEXT("FShaderLabUnlit"));
|
||||
}
|
||||
else if (Token == TEXT("SL_HAIR"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_HAIR"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Hair, TEXT("SL_HAIR"), TEXT("FShaderLabHair"));
|
||||
}
|
||||
else if (Token == TEXT("SL_EYE"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_EYE"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Eye, TEXT("SL_EYE"), TEXT("FShaderLabEye"));
|
||||
}
|
||||
else if (Token == TEXT("SL_WATER"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_WATER"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Water, TEXT("SL_WATER"), TEXT("FShaderLabWater"));
|
||||
}
|
||||
else if (Token == TEXT("SL_CLEARCOAT"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_CLEARCOAT"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::ClearCoat, TEXT("SL_CLEARCOAT"), TEXT("FShaderLabClearCoat"));
|
||||
}
|
||||
else if (Token == TEXT("SL_TOON"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_TOON"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Toon, TEXT("SL_TOON"), TEXT("FShaderLabToon"));
|
||||
}
|
||||
else if (Token == TEXT("SL_VOLUME"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_VOLUME"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Volume, TEXT("SL_VOLUME"), TEXT("FShaderLabVolume"));
|
||||
}
|
||||
else if (Token == TEXT("SL_LIGHTFUNCTION"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_LIGHTFUNCTION"))) { return false; }
|
||||
ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::LightFunction, TEXT("SL_LIGHTFUNCTION"), TEXT("FShaderLabLightFunction"));
|
||||
}
|
||||
else if (Token == TEXT("SL_VALUE"))
|
||||
{
|
||||
@@ -1468,6 +1824,16 @@ bool FShaderLabParser::Parse(
|
||||
if (RejectInLibrary(TEXT("SL_OPACITY_MASK"))) { return false; }
|
||||
ParseMaterialOutput(S, OutModel.OpacityMaskValueName, TEXT("SL_OPACITY_MASK"));
|
||||
}
|
||||
else if (Token == TEXT("SL_REFRACTION"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_REFRACTION"))) { return false; }
|
||||
ParseMaterialOutput(S, OutModel.RefractionValueName, TEXT("SL_REFRACTION"));
|
||||
}
|
||||
else if (Token == TEXT("SL_PIXEL_DEPTH_OFFSET"))
|
||||
{
|
||||
if (RejectInLibrary(TEXT("SL_PIXEL_DEPTH_OFFSET"))) { return false; }
|
||||
ParseMaterialOutput(S, OutModel.PixelDepthOffsetValueName, TEXT("SL_PIXEL_DEPTH_OFFSET"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a ShaderLab macro: a free top-level HLSL declaration (helper function / struct /
|
||||
@@ -1499,6 +1865,17 @@ bool FShaderLabParser::Parse(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Names must be unique across SL_PROPERTY and SL_COLLECTION — both resolve to the same in-body identifier
|
||||
// and are wired through the same PropertyNodes map, so a collision would overwrite one and double-wire.
|
||||
for (const FShaderLabCollectionParam& C : OutModel.Collections)
|
||||
{
|
||||
if (OutModel.FindProperty(C.Name))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("'%s' is declared as both an SL_PROPERTY and an SL_COLLECTION"), *C.Name.ToString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Pixel stage: exactly one of { single Surface } or { named Slabs + FrontMaterial }.
|
||||
const bool bHasMultiSlab = OutModel.Slabs.Num() > 0 || OutModel.TopologyRoot != INDEX_NONE;
|
||||
if (OutModel.bHasSurface && bHasMultiSlab)
|
||||
@@ -1511,17 +1888,41 @@ bool FShaderLabParser::Parse(
|
||||
S.Error(TEXT("Shader must declare a SL_SURFACE(...) entry point, or SL_SLAB blocks with a SL_FRONTMATERIAL expression"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Material-level output markers only apply to the multi-Slab path (they name a Value block). In a single
|
||||
// pixel entry they would be silently dropped — reject with guidance to use the S.* fields instead.
|
||||
if (OutModel.bHasSurface
|
||||
&& (!OutModel.OpacityValueName.IsNone() || !OutModel.OpacityMaskValueName.IsNone()
|
||||
|| !OutModel.RefractionValueName.IsNone() || !OutModel.PixelDepthOffsetValueName.IsNone()))
|
||||
{
|
||||
S.Error(TEXT("SL_OPACITY / SL_OPACITY_MASK / SL_REFRACTION / SL_PIXEL_DEPTH_OFFSET are for multi-Slab shaders (with SL_FRONTMATERIAL). In a single SL_SURFACE, set S.Opacity / S.OpacityMask / S.Refraction / S.PixelDepthOffset instead."));
|
||||
return false;
|
||||
}
|
||||
if (bHasMultiSlab)
|
||||
{
|
||||
if (OutModel.Slabs.Num() == 0)
|
||||
{
|
||||
S.Error(TEXT("SL_FRONTMATERIAL requires at least one SL_SLAB block"));
|
||||
S.Error(TEXT("SL_FRONTMATERIAL requires at least one BSDF block"));
|
||||
return false;
|
||||
}
|
||||
if (OutModel.TopologyRoot == INDEX_NONE)
|
||||
{
|
||||
S.Error(TEXT("SL_SLAB blocks require a SL_FRONTMATERIAL(...) topology expression"));
|
||||
return false;
|
||||
// A single dedicated BSDF block (SL_UNLIT/HAIR/EYE/WATER/CLEARCOAT/TOON/VOLUME/LIGHTFUNCTION) is
|
||||
// the whole material: auto-root it. A lone SL_SLAB (BsdfType == Slab) still requires SL_FRONTMATERIAL,
|
||||
// as do multiple blocks (which need a topology expression to combine them).
|
||||
if (OutModel.Slabs.Num() == 1 && OutModel.Slabs[0].BsdfType != EShaderLabBsdfType::Slab)
|
||||
{
|
||||
FShaderLabTopoNode Leaf;
|
||||
Leaf.Op = EShaderLabOp::SlabRef;
|
||||
Leaf.SlabRef = OutModel.Slabs[0].Name;
|
||||
Leaf.SourceLine = OutModel.Slabs[0].BodyLine;
|
||||
OutModel.TopologyRoot = OutModel.Topology.Add(Leaf);
|
||||
}
|
||||
else
|
||||
{
|
||||
S.Error(TEXT("SL_SLAB blocks require a SL_FRONTMATERIAL(...) topology expression"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1545,5 +1946,21 @@ bool FShaderLabParser::Parse(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Volume / LightFunction domains use their dedicated BSDF entry as the (sole) root block.
|
||||
auto HasSoleBsdf = [&OutModel](EShaderLabBsdfType Type) -> bool
|
||||
{
|
||||
return !OutModel.bHasSurface && OutModel.Slabs.Num() == 1 && OutModel.Slabs[0].BsdfType == Type;
|
||||
};
|
||||
if (Domain == EShaderLabDomain::Volume && !HasSoleBsdf(EShaderLabBsdfType::Volume))
|
||||
{
|
||||
S.Error(TEXT("Domain = Volume requires a single SL_VOLUME(inout FShaderLabVolume) entry"));
|
||||
return false;
|
||||
}
|
||||
if (Domain == EShaderLabDomain::LightFunction && !HasSoleBsdf(EShaderLabBsdfType::LightFunction))
|
||||
{
|
||||
S.Error(TEXT("Domain = LightFunction requires a single SL_LIGHTFUNCTION(inout FShaderLabLightFunction) entry"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderL
|
||||
case EShaderLabDomain::PostProcess: Material.MaterialDomain = MD_PostProcess; break;
|
||||
case EShaderLabDomain::UI: Material.MaterialDomain = MD_UI; break;
|
||||
case EShaderLabDomain::Decal: Material.MaterialDomain = MD_DeferredDecal; break;
|
||||
case EShaderLabDomain::Volume: Material.MaterialDomain = MD_Volume; break;
|
||||
case EShaderLabDomain::LightFunction: Material.MaterialDomain = MD_LightFunction; break;
|
||||
case EShaderLabDomain::Surface:
|
||||
default: Material.MaterialDomain = MD_Surface; break;
|
||||
}
|
||||
@@ -27,6 +29,8 @@ void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderL
|
||||
case EShaderLabBlendMode::Translucent: Material.BlendMode = BLEND_Translucent; break;
|
||||
case EShaderLabBlendMode::Additive: Material.BlendMode = BLEND_Additive; break;
|
||||
case EShaderLabBlendMode::Modulate: Material.BlendMode = BLEND_Modulate; break;
|
||||
case EShaderLabBlendMode::AlphaComposite: Material.BlendMode = BLEND_AlphaComposite; break;
|
||||
case EShaderLabBlendMode::AlphaHoldout: Material.BlendMode = BLEND_AlphaHoldout; break;
|
||||
case EShaderLabBlendMode::Opaque:
|
||||
default: Material.BlendMode = BLEND_Opaque; break;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace ShaderLabSettings_Private
|
||||
FName(TEXT("bContactShadows")),
|
||||
FName(TEXT("bIsThinSurface")),
|
||||
FName(TEXT("DitheredLODTransition")),
|
||||
FName(TEXT("RefractionMethod")),
|
||||
};
|
||||
return Names;
|
||||
}
|
||||
|
||||
@@ -21,15 +21,28 @@ enum class EShaderLabPropertyType : uint8
|
||||
Vector,
|
||||
Texture2D,
|
||||
TextureCube,
|
||||
Texture2DArray,
|
||||
Texture3D, // volume texture
|
||||
TextureCubeArray,
|
||||
StaticBool,
|
||||
};
|
||||
|
||||
/** True for any texture-object property type (2D/Cube/2DArray/3D/CubeArray). */
|
||||
inline bool ShaderLabIsTextureType(EShaderLabPropertyType T)
|
||||
{
|
||||
return T == EShaderLabPropertyType::Texture2D || T == EShaderLabPropertyType::TextureCube
|
||||
|| T == EShaderLabPropertyType::Texture2DArray || T == EShaderLabPropertyType::Texture3D
|
||||
|| T == EShaderLabPropertyType::TextureCubeArray;
|
||||
}
|
||||
|
||||
enum class EShaderLabDomain : uint8
|
||||
{
|
||||
Surface,
|
||||
PostProcess,
|
||||
UI,
|
||||
Decal,
|
||||
Volume, // MD_Volume — volumetric fog/cloud materials; requires an SL_VOLUME entry
|
||||
LightFunction, // MD_LightFunction — light gobo/pattern; requires an SL_LIGHTFUNCTION entry
|
||||
};
|
||||
|
||||
/** Which single pixel-entry a shader declared (when bHasSurface). Selects the output struct + wiring. */
|
||||
@@ -40,6 +53,26 @@ enum class EShaderLabEntry : uint8
|
||||
UI, // FShaderLabUI -> EmissiveColor/Opacity
|
||||
};
|
||||
|
||||
/**
|
||||
* Which Substrate BSDF a named block (`FShaderLabSlab`) or single-entry sugar builds. Slab is the classic
|
||||
* composable path (SL_SURFACE / SL_SLAB); the others are dedicated BSDFs (SL_UNLIT/HAIR/EYE/WATER/CLEARCOAT/
|
||||
* TOON/VOLUME/LIGHTFUNCTION). Each maps to a concrete UMaterialExpressionSubstrate*BSDF node + output struct.
|
||||
* The graph builder keys a per-BSDF descriptor table on this. Composition legality (which types may feed which
|
||||
* topology operator) is validated by FShaderLabModel::ValidateTopology mirroring the engine allow-lists.
|
||||
*/
|
||||
enum class EShaderLabBsdfType : uint8
|
||||
{
|
||||
Slab, // FShaderLabSurface -> UMaterialExpressionSubstrateSlabBSDF
|
||||
Unlit, // FShaderLabUnlit -> UMaterialExpressionSubstrateUnlitBSDF
|
||||
Hair, // FShaderLabHair -> UMaterialExpressionSubstrateHairBSDF
|
||||
Eye, // FShaderLabEye -> UMaterialExpressionSubstrateEyeBSDF
|
||||
Water, // FShaderLabWater -> UMaterialExpressionSubstrateSingleLayerWaterBSDF
|
||||
Volume, // FShaderLabVolume -> UMaterialExpressionSubstrateVolumetricFogCloudBSDF
|
||||
ClearCoat, // FShaderLabClearCoat -> UMaterialExpressionSubstrateSimpleClearCoatBSDF
|
||||
Toon, // FShaderLabToon -> UMaterialExpressionSubstrateToonBSDF
|
||||
LightFunction, // FShaderLabLightFunction -> UMaterialExpressionSubstrateLightFunction
|
||||
};
|
||||
|
||||
enum class EShaderLabBlendMode : uint8
|
||||
{
|
||||
Opaque,
|
||||
@@ -47,6 +80,8 @@ enum class EShaderLabBlendMode : uint8
|
||||
Translucent,
|
||||
Additive,
|
||||
Modulate,
|
||||
AlphaComposite, // BLEND_AlphaComposite — premultiplied alpha
|
||||
AlphaHoldout, // BLEND_AlphaHoldout
|
||||
};
|
||||
|
||||
/** A single declared shader property -> becomes a standard UE material parameter. */
|
||||
@@ -55,14 +90,23 @@ struct USHADERLAB_API FShaderLabProperty
|
||||
FName Name;
|
||||
EShaderLabPropertyType Type = EShaderLabPropertyType::Scalar;
|
||||
|
||||
FString DisplayName;
|
||||
FString Group;
|
||||
int32 SortPriority = 0;
|
||||
|
||||
/** Range(min,max) meta — only meaningful for Scalar. */
|
||||
bool bHasRange = false;
|
||||
float RangeMin = 0.0f;
|
||||
float RangeMax = 1.0f;
|
||||
/** ClampMin/ClampMax slider bounds (UE UPROPERTY style) — only meaningful for Scalar. Each is
|
||||
* independent; a set field maps to the ScalarParameter's SliderMin/SliderMax respectively. */
|
||||
bool bHasClampMin = false;
|
||||
float ClampMin = 0.0f;
|
||||
bool bHasClampMax = false;
|
||||
float ClampMax = 1.0f;
|
||||
|
||||
/**
|
||||
* bUseCustomPrimitiveData: read this parameter from the component's Custom Primitive Data instead of a
|
||||
* material uniform (per-instance data without a Material Instance). Only Scalar/Vector support it in UE.
|
||||
* PrimitiveDataIndex is the mandatory, author-specified float slot (Scalar uses 1 float, Vector uses 4).
|
||||
*/
|
||||
bool bUseCustomPrimitiveData = false;
|
||||
int32 PrimitiveDataIndex = INDEX_NONE;
|
||||
|
||||
// Defaults (only the field matching Type is meaningful).
|
||||
float ScalarDefault = 0.0f;
|
||||
@@ -72,6 +116,20 @@ struct USHADERLAB_API FShaderLabProperty
|
||||
bool bStaticBoolDefault = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* A `SL_COLLECTION(Path="...", [Parameter="..."]) floatN <Name>;` declaration: a read of a Material Parameter
|
||||
* Collection (MPC) value. Unlike SL_PROPERTY it is NOT a per-instance knob — it reads a global collection
|
||||
* value (UMaterialExpressionCollectionParameter). Type (Scalar/Color/Vector) is inferred from the HLSL type.
|
||||
*/
|
||||
struct USHADERLAB_API FShaderLabCollectionParam
|
||||
{
|
||||
FName Name; // in-HLSL identifier the body references
|
||||
FString CollectionPath; // asset path of the UMaterialParameterCollection
|
||||
FName ParameterName; // parameter name within the collection (defaults to Name)
|
||||
EShaderLabPropertyType Type = EShaderLabPropertyType::Scalar; // Scalar / Color / Vector
|
||||
int32 Line = 0;
|
||||
};
|
||||
|
||||
/** A parameter in a `Surface(...)`/`Vertex(...)` entry-point signature. */
|
||||
struct USHADERLAB_API FShaderLabEntryParam
|
||||
{
|
||||
@@ -80,14 +138,34 @@ struct USHADERLAB_API FShaderLabEntryParam
|
||||
bool bInout = false;
|
||||
};
|
||||
|
||||
/** A named `Slab <Name>(inout FShaderLabSurface S){...}` block in a multi-slab shader. */
|
||||
/**
|
||||
* Non-pin BSDF node modifiers set via marker specifiers (SL_SURFACE/SL_SLAB(...), etc.): asset references and
|
||||
* enum selectors that are node properties, not connectable float pins. Empty/None means "leave the node default".
|
||||
* SubsurfaceProfile/SpecularProfile apply to Slab (SubsurfaceProfile also to Eye); ToonProfile applies to Toon;
|
||||
* SubSurfaceType (Slab) is one of None/Wrap/TwoSidedWrap/Diffusion/SimpleVolume.
|
||||
*/
|
||||
struct USHADERLAB_API FShaderLabBsdfModifiers
|
||||
{
|
||||
FString SubsurfaceProfilePath;
|
||||
FString SpecularProfilePath;
|
||||
FString ToonProfilePath;
|
||||
FString SubSurfaceType; // enum token; empty = leave default
|
||||
};
|
||||
|
||||
/**
|
||||
* A named BSDF block in a multi-slab / multi-BSDF shader, e.g. `SL_SLAB() void <Name>(inout FShaderLabSurface S){...}`
|
||||
* or `SL_HAIR() void <Name>(inout FShaderLabHair H){...}`. BsdfType (set by the marker) selects the engine node
|
||||
* and output struct; the graph builder keys its per-BSDF descriptor table on it.
|
||||
*/
|
||||
struct USHADERLAB_API FShaderLabSlab
|
||||
{
|
||||
FName Name;
|
||||
EShaderLabBsdfType BsdfType = EShaderLabBsdfType::Slab;
|
||||
/** Name of the inout output-struct parameter (e.g. "S"). */
|
||||
FString OutParamName;
|
||||
FString Body;
|
||||
int32 BodyLine = 0;
|
||||
FShaderLabBsdfModifiers Modifiers;
|
||||
};
|
||||
|
||||
/** A named `Value <Name>(){ return <float HLSL>; }` block, used as a topology mix factor. */
|
||||
@@ -190,6 +268,8 @@ struct USHADERLAB_API FShaderLabTopoNode
|
||||
int32 ChildB = INDEX_NONE; // second material input (binary ops)
|
||||
bool bHasFactor = false;
|
||||
FShaderLabFactor Factor; // for VerticalLayer/HorizontalMix/Weight/Select
|
||||
/** 1-based source line of this operator/leaf token inside SL_FRONTMATERIAL, for composition-error mapping. */
|
||||
int32 SourceLine = 0;
|
||||
};
|
||||
|
||||
struct USHADERLAB_API FShaderLabSettings
|
||||
@@ -227,6 +307,8 @@ struct USHADERLAB_API FShaderLabModel
|
||||
TArray<TPair<FString, FString>> RawSettings;
|
||||
|
||||
TArray<FShaderLabProperty> Properties;
|
||||
/** Material Parameter Collection reads (SL_COLLECTION). Global values, not per-instance knobs. */
|
||||
TArray<FShaderLabCollectionParam> Collections;
|
||||
TArray<FString> Includes;
|
||||
|
||||
/**
|
||||
@@ -258,15 +340,19 @@ struct USHADERLAB_API FShaderLabModel
|
||||
bool bHasSurface = false;
|
||||
/** Which single pixel-entry was declared (valid when bHasSurface). */
|
||||
EShaderLabEntry SurfaceEntry = EShaderLabEntry::Surface;
|
||||
/** BSDF node modifiers from SL_SURFACE(...) specifiers (only meaningful for the Surface entry). */
|
||||
FShaderLabBsdfModifiers SurfaceModifiers;
|
||||
|
||||
// Multi-slab topology (used when !bHasSurface).
|
||||
TArray<FShaderLabSlab> Slabs;
|
||||
TArray<FShaderLabValue> Values;
|
||||
TArray<FShaderLabTopoNode> Topology;
|
||||
int32 TopologyRoot = INDEX_NONE;
|
||||
/** Optional material-level outputs (multi-slab): names of Value blocks feeding Opacity/OpacityMask. */
|
||||
/** Optional material-level outputs (multi-slab): names of Value blocks feeding Opacity/OpacityMask/Refraction/PDO. */
|
||||
FName OpacityValueName;
|
||||
FName OpacityMaskValueName;
|
||||
FName RefractionValueName;
|
||||
FName PixelDepthOffsetValueName;
|
||||
|
||||
// Vertex stage (optional).
|
||||
bool bHasVertex = false;
|
||||
@@ -291,4 +377,12 @@ struct USHADERLAB_API FShaderLabModel
|
||||
|
||||
/** True if any property is a StaticBool (drives shader-map permutation count). */
|
||||
bool HasStaticSwitches() const;
|
||||
|
||||
/**
|
||||
* Validate the FrontMaterial topology against the Substrate operator BSDF-type allow-lists (mirrors the
|
||||
* engine's SubstrateTranslatorCommon allow-lists): VerticalLayer/HorizontalMix/Add accept only Slab; Weight
|
||||
* accepts Unlit/Slab/Toon; Select accepts Slab/Hair/Eye/Toon. On an illegal combination, appends an error
|
||||
* mapped to the offending operator's .usl line. Returns true when the topology is legal (or empty).
|
||||
*/
|
||||
bool ValidateTopology(TArray<FString>& OutErrors) const;
|
||||
};
|
||||
|
||||
@@ -11,11 +11,23 @@
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialExpressionConstant.h"
|
||||
#include "Materials/MaterialExpressionCollectionParameter.h"
|
||||
#include "Materials/MaterialExpressionCustom.h"
|
||||
#include "Materials/MaterialParameterCollection.h"
|
||||
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||
#include "Materials/MaterialExpressionStaticBoolParameter.h"
|
||||
#include "Materials/MaterialExpressionStaticSwitch.h"
|
||||
#include "Materials/MaterialExpressionQualitySwitch.h"
|
||||
#include "Materials/MaterialExpressionFeatureLevelSwitch.h"
|
||||
#include "Materials/MaterialExpressionShadingPathSwitch.h"
|
||||
#include "SceneTypes.h"
|
||||
#include "RHIDefinitions.h"
|
||||
#include "Materials/MaterialExpressionSubstrate.h"
|
||||
#include "Materials/MaterialExpressionSingleLayerWaterMaterialOutput.h"
|
||||
#include "Materials/MaterialExpressionSceneTexture.h"
|
||||
#include "Engine/SubsurfaceProfile.h"
|
||||
#include "Engine/SpecularProfile.h"
|
||||
#include "Engine/ToonProfile.h"
|
||||
#include "Materials/MaterialExpressionTextureObjectParameter.h"
|
||||
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||
#include "Materials/MaterialExpressionVertexInterpolator.h"
|
||||
@@ -30,6 +42,7 @@
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "ShaderCore.h"
|
||||
#include "SceneTypes.h"
|
||||
|
||||
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
|
||||
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
|
||||
@@ -206,6 +219,8 @@ namespace ShaderLabGraph
|
||||
case EShaderLabDomain::PostProcess: return MD_PostProcess;
|
||||
case EShaderLabDomain::UI: return MD_UI;
|
||||
case EShaderLabDomain::Decal: return MD_DeferredDecal;
|
||||
case EShaderLabDomain::Volume: return MD_Volume;
|
||||
case EShaderLabDomain::LightFunction: return MD_LightFunction;
|
||||
case EShaderLabDomain::Surface:
|
||||
default: return MD_Surface;
|
||||
}
|
||||
@@ -219,6 +234,8 @@ namespace ShaderLabGraph
|
||||
case EShaderLabBlendMode::Translucent: return BLEND_Translucent;
|
||||
case EShaderLabBlendMode::Additive: return BLEND_Additive;
|
||||
case EShaderLabBlendMode::Modulate: return BLEND_Modulate;
|
||||
case EShaderLabBlendMode::AlphaComposite: return BLEND_AlphaComposite;
|
||||
case EShaderLabBlendMode::AlphaHoldout: return BLEND_AlphaHoldout;
|
||||
case EShaderLabBlendMode::Opaque:
|
||||
default: return BLEND_Opaque;
|
||||
}
|
||||
@@ -235,6 +252,225 @@ namespace ShaderLabGraph
|
||||
return Expr;
|
||||
}
|
||||
|
||||
/** A trivial Custom node that emits `#define <Name> <Value>` (used for the before-attributes define leak). */
|
||||
static UMaterialExpressionCustom* MakeDefineNode(UMaterial& Material, int32& IoY, const TCHAR* Name, const TCHAR* Value)
|
||||
{
|
||||
UMaterialExpressionCustom* D = NewExpr<UMaterialExpressionCustom>(Material, IoY, -1300);
|
||||
D->Description = TEXT("ShaderLab Define");
|
||||
D->OutputType = CMOT_Float1;
|
||||
D->Code = TEXT("return 0;");
|
||||
FCustomDefine DD;
|
||||
DD.DefineName = Name;
|
||||
DD.DefineValue = Value;
|
||||
D->AdditionalDefines.Add(DD);
|
||||
return D;
|
||||
}
|
||||
|
||||
// --- Per-BSDF descriptor -----------------------------------------------------------------------
|
||||
// Each Substrate BSDF is described by {output struct, default fn, field->pin table, node factory,
|
||||
// pin resolver}. The generic BuildBsdf() works off this so all BSDFs share one code path. The engine
|
||||
// BSDF node classes are distinct C++ types with no common named-pin base, so each descriptor supplies
|
||||
// its own MakeNode (news the concrete node) and GetPin (casts + returns the FExpressionInput*).
|
||||
struct FBsdfDesc
|
||||
{
|
||||
EShaderLabBsdfType Type;
|
||||
const TCHAR* CustomDesc; // Custom-node Description (kept "ShaderLab Surface" for Slab: tests key on it)
|
||||
const TCHAR* StructName; // e.g. "FShaderLabSurface"
|
||||
const TCHAR* DefaultFn; // e.g. "ShaderLabDefaultSurface"
|
||||
const FSlabFieldDef* Fields;
|
||||
int32 NumFields;
|
||||
UMaterialExpression* (*MakeNode)(UMaterial&, int32& IoY);
|
||||
FExpressionInput* (*GetPin)(UMaterialExpression*, const FString& Field);
|
||||
};
|
||||
|
||||
static UMaterialExpression* MakeSlabNode(UMaterial& M, int32& IoY)
|
||||
{
|
||||
return NewExpr<UMaterialExpressionSubstrateSlabBSDF>(M, IoY, 0);
|
||||
}
|
||||
static FExpressionInput* GetSlabPinGeneric(UMaterialExpression* Node, const FString& Field)
|
||||
{
|
||||
UMaterialExpressionSubstrateSlabBSDF* Slab = Cast<UMaterialExpressionSubstrateSlabBSDF>(Node);
|
||||
return Slab ? GetSlabPin(Slab, Field) : nullptr;
|
||||
}
|
||||
|
||||
// --- Unlit ---
|
||||
static const FSlabFieldDef GUnlitFields[] = {
|
||||
{ TEXT("EmissiveColor"), CMOT_Float3 }, { TEXT("TransmittanceColor"), CMOT_Float3 }, { TEXT("Normal"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeUnlitNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateUnlitBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetUnlitPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateUnlitBSDF* B = Cast<UMaterialExpressionSubstrateUnlitBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
if (F == TEXT("TransmittanceColor")) return &B->TransmittanceColor;
|
||||
if (F == TEXT("Normal")) return &B->Normal;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Hair ---
|
||||
static const FSlabFieldDef GHairFields[] = {
|
||||
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Scatter"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
|
||||
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Backlit"), CMOT_Float3 }, { TEXT("Tangent"), CMOT_Float3 },
|
||||
{ TEXT("EmissiveColor"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeHairNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateHairBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetHairPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateHairBSDF* B = Cast<UMaterialExpressionSubstrateHairBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("BaseColor")) return &B->BaseColor;
|
||||
if (F == TEXT("Scatter")) return &B->Scatter;
|
||||
if (F == TEXT("Specular")) return &B->Specular;
|
||||
if (F == TEXT("Roughness")) return &B->Roughness;
|
||||
if (F == TEXT("Backlit")) return &B->Backlit;
|
||||
if (F == TEXT("Tangent")) return &B->Tangent;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Eye ---
|
||||
static const FSlabFieldDef GEyeFields[] = {
|
||||
{ TEXT("DiffuseColor"), CMOT_Float3 }, { TEXT("Roughness"), CMOT_Float1 }, { TEXT("CorneaNormal"), CMOT_Float3 },
|
||||
{ TEXT("IrisNormal"), CMOT_Float3 }, { TEXT("IrisPlaneNormal"), CMOT_Float3 }, { TEXT("IrisMask"), CMOT_Float1 },
|
||||
{ TEXT("IrisDistance"), CMOT_Float1 }, { TEXT("EmissiveColor"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeEyeNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateEyeBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetEyePin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateEyeBSDF* B = Cast<UMaterialExpressionSubstrateEyeBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("DiffuseColor")) return &B->DiffuseColor;
|
||||
if (F == TEXT("Roughness")) return &B->Roughness;
|
||||
if (F == TEXT("CorneaNormal")) return &B->CorneaNormal;
|
||||
if (F == TEXT("IrisNormal")) return &B->IrisNormal;
|
||||
if (F == TEXT("IrisPlaneNormal")) return &B->IrisPlaneNormal;
|
||||
if (F == TEXT("IrisMask")) return &B->IrisMask;
|
||||
if (F == TEXT("IrisDistance")) return &B->IrisDistance;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Single Layer Water ---
|
||||
static const FSlabFieldDef GWaterFields[] = {
|
||||
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Metallic"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
|
||||
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
|
||||
{ TEXT("TopMaterialOpacity"), CMOT_Float3 }, { TEXT("WaterAlbedo"), CMOT_Float3 }, { TEXT("WaterExtinction"), CMOT_Float3 },
|
||||
{ TEXT("WaterPhaseG"), CMOT_Float1 }, { TEXT("ColorScaleBehindWater"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeWaterNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateSingleLayerWaterBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetWaterPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateSingleLayerWaterBSDF* B = Cast<UMaterialExpressionSubstrateSingleLayerWaterBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("BaseColor")) return &B->BaseColor;
|
||||
if (F == TEXT("Metallic")) return &B->Metallic;
|
||||
if (F == TEXT("Specular")) return &B->Specular;
|
||||
if (F == TEXT("Roughness")) return &B->Roughness;
|
||||
if (F == TEXT("Normal")) return &B->Normal;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
if (F == TEXT("TopMaterialOpacity")) return &B->TopMaterialOpacity;
|
||||
if (F == TEXT("WaterAlbedo")) return &B->WaterAlbedo;
|
||||
if (F == TEXT("WaterExtinction")) return &B->WaterExtinction;
|
||||
if (F == TEXT("WaterPhaseG")) return &B->WaterPhaseG;
|
||||
if (F == TEXT("ColorScaleBehindWater")) return &B->ColorScaleBehindWater;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Volumetric-Fog-Cloud ---
|
||||
static const FSlabFieldDef GVolumeFields[] = {
|
||||
{ TEXT("Albedo"), CMOT_Float3 }, { TEXT("Extinction"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
|
||||
{ TEXT("AmbientOcclusion"), CMOT_Float1 },
|
||||
};
|
||||
static UMaterialExpression* MakeVolumeNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateVolumetricFogCloudBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetVolumePin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateVolumetricFogCloudBSDF* B = Cast<UMaterialExpressionSubstrateVolumetricFogCloudBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("Albedo")) return &B->Albedo;
|
||||
if (F == TEXT("Extinction")) return &B->Extinction;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
if (F == TEXT("AmbientOcclusion")) return &B->AmbientOcclusion;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Simple Clear Coat ---
|
||||
static const FSlabFieldDef GClearCoatFields[] = {
|
||||
{ TEXT("DiffuseAlbedo"), CMOT_Float3 }, { TEXT("F0"), CMOT_Float3 }, { TEXT("Roughness"), CMOT_Float1 },
|
||||
{ TEXT("ClearCoatCoverage"), CMOT_Float1 }, { TEXT("ClearCoatRoughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 },
|
||||
{ TEXT("EmissiveColor"), CMOT_Float3 }, { TEXT("BottomNormal"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeClearCoatNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateSimpleClearCoatBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetClearCoatPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateSimpleClearCoatBSDF* B = Cast<UMaterialExpressionSubstrateSimpleClearCoatBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("DiffuseAlbedo")) return &B->DiffuseAlbedo;
|
||||
if (F == TEXT("F0")) return &B->F0;
|
||||
if (F == TEXT("Roughness")) return &B->Roughness;
|
||||
if (F == TEXT("ClearCoatCoverage")) return &B->ClearCoatCoverage;
|
||||
if (F == TEXT("ClearCoatRoughness")) return &B->ClearCoatRoughness;
|
||||
if (F == TEXT("Normal")) return &B->Normal;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
if (F == TEXT("BottomNormal")) return &B->BottomNormal;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Toon (experimental) ---
|
||||
static const FSlabFieldDef GToonFields[] = {
|
||||
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Metallic"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
|
||||
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
|
||||
{ TEXT("PatternUVs"), CMOT_Float2 }, { TEXT("Anisotropy"), CMOT_Float1 }, { TEXT("Tangent"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeToonNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateToonBSDF>(M, IoY, 0); }
|
||||
static FExpressionInput* GetToonPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateToonBSDF* B = Cast<UMaterialExpressionSubstrateToonBSDF>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("BaseColor")) return &B->BaseColor;
|
||||
if (F == TEXT("Metallic")) return &B->Metallic;
|
||||
if (F == TEXT("Specular")) return &B->Specular;
|
||||
if (F == TEXT("Roughness")) return &B->Roughness;
|
||||
if (F == TEXT("Normal")) return &B->Normal;
|
||||
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
|
||||
if (F == TEXT("PatternUVs")) return &B->PatternUVs;
|
||||
if (F == TEXT("Anisotropy")) return &B->Anisotropy;
|
||||
if (F == TEXT("Tangent")) return &B->Tangent;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Light Function ---
|
||||
static const FSlabFieldDef GLightFunctionFields[] = {
|
||||
{ TEXT("Color"), CMOT_Float3 },
|
||||
};
|
||||
static UMaterialExpression* MakeLightFunctionNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateLightFunction>(M, IoY, 0); }
|
||||
static FExpressionInput* GetLightFunctionPin(UMaterialExpression* N, const FString& F)
|
||||
{
|
||||
UMaterialExpressionSubstrateLightFunction* B = Cast<UMaterialExpressionSubstrateLightFunction>(N); if (!B) return nullptr;
|
||||
if (F == TEXT("Color")) return &B->Color;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const FBsdfDesc GBsdfDescs[] = {
|
||||
{ EShaderLabBsdfType::Slab, TEXT("ShaderLab Surface"), TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"),
|
||||
GSlabFields, UE_ARRAY_COUNT(GSlabFields), &MakeSlabNode, &GetSlabPinGeneric },
|
||||
{ EShaderLabBsdfType::Unlit, TEXT("ShaderLab Unlit"), TEXT("FShaderLabUnlit"), TEXT("ShaderLabDefaultUnlit"),
|
||||
GUnlitFields, UE_ARRAY_COUNT(GUnlitFields), &MakeUnlitNode, &GetUnlitPin },
|
||||
{ EShaderLabBsdfType::Hair, TEXT("ShaderLab Hair"), TEXT("FShaderLabHair"), TEXT("ShaderLabDefaultHair"),
|
||||
GHairFields, UE_ARRAY_COUNT(GHairFields), &MakeHairNode, &GetHairPin },
|
||||
{ EShaderLabBsdfType::Eye, TEXT("ShaderLab Eye"), TEXT("FShaderLabEye"), TEXT("ShaderLabDefaultEye"),
|
||||
GEyeFields, UE_ARRAY_COUNT(GEyeFields), &MakeEyeNode, &GetEyePin },
|
||||
{ EShaderLabBsdfType::Water, TEXT("ShaderLab Water"), TEXT("FShaderLabWater"), TEXT("ShaderLabDefaultWater"),
|
||||
GWaterFields, UE_ARRAY_COUNT(GWaterFields), &MakeWaterNode, &GetWaterPin },
|
||||
{ EShaderLabBsdfType::Volume, TEXT("ShaderLab Volume"), TEXT("FShaderLabVolume"), TEXT("ShaderLabDefaultVolume"),
|
||||
GVolumeFields, UE_ARRAY_COUNT(GVolumeFields), &MakeVolumeNode, &GetVolumePin },
|
||||
{ EShaderLabBsdfType::ClearCoat, TEXT("ShaderLab ClearCoat"), TEXT("FShaderLabClearCoat"), TEXT("ShaderLabDefaultClearCoat"),
|
||||
GClearCoatFields, UE_ARRAY_COUNT(GClearCoatFields), &MakeClearCoatNode, &GetClearCoatPin },
|
||||
{ EShaderLabBsdfType::Toon, TEXT("ShaderLab Toon"), TEXT("FShaderLabToon"), TEXT("ShaderLabDefaultToon"),
|
||||
GToonFields, UE_ARRAY_COUNT(GToonFields), &MakeToonNode, &GetToonPin },
|
||||
{ EShaderLabBsdfType::LightFunction, TEXT("ShaderLab LightFunction"), TEXT("FShaderLabLightFunction"), TEXT("ShaderLabDefaultLightFunction"),
|
||||
GLightFunctionFields, UE_ARRAY_COUNT(GLightFunctionFields), &MakeLightFunctionNode, &GetLightFunctionPin },
|
||||
};
|
||||
|
||||
static const FBsdfDesc* FindBsdfDesc(EShaderLabBsdfType Type)
|
||||
{
|
||||
for (const FBsdfDesc& D : GBsdfDescs) { if (D.Type == Type) { return &D; } }
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/** One intrinsic call resolved to a Custom-node input wired from an engine expression node. */
|
||||
struct FIntrinsicWire
|
||||
{
|
||||
@@ -631,6 +867,18 @@ namespace ShaderLabGraph
|
||||
OutParams.Add(FString::Printf(TEXT("TextureCube %s"), *Name));
|
||||
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
|
||||
break;
|
||||
case EShaderLabPropertyType::Texture2DArray:
|
||||
OutParams.Add(FString::Printf(TEXT("Texture2DArray %s"), *Name));
|
||||
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
|
||||
break;
|
||||
case EShaderLabPropertyType::Texture3D:
|
||||
OutParams.Add(FString::Printf(TEXT("Texture3D %s"), *Name));
|
||||
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
|
||||
break;
|
||||
case EShaderLabPropertyType::TextureCubeArray:
|
||||
OutParams.Add(FString::Printf(TEXT("TextureCubeArray %s"), *Name));
|
||||
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
|
||||
break;
|
||||
default: break; // StaticBool never becomes a parameter (reaches bodies via the global #define).
|
||||
}
|
||||
}
|
||||
@@ -640,7 +888,7 @@ namespace ShaderLabGraph
|
||||
{
|
||||
const FString Name = Prop.Name.ToString();
|
||||
OutArgs.Add(Name);
|
||||
if (Prop.Type == EShaderLabPropertyType::Texture2D || Prop.Type == EShaderLabPropertyType::TextureCube)
|
||||
if (ShaderLabIsTextureType(Prop.Type))
|
||||
{
|
||||
OutArgs.Add(Name + TEXT("Sampler"));
|
||||
}
|
||||
@@ -1079,7 +1327,8 @@ namespace ShaderLabGraph
|
||||
* Iterating the (deduped) promoted property set once means a property that is both never doubles up.
|
||||
*/
|
||||
static void WirePropInputs(UMaterialExpressionCustom& Custom, const FShaderLabResolvedProgram& Program,
|
||||
const TMap<FName, FParamNode>& PropertyNodes, const FString& InBody, const TSet<FName>& ReqProps)
|
||||
const TMap<FName, FParamNode>& PropertyNodes, const FString& InBody, const TSet<FName>& ReqProps,
|
||||
const TArray<FShaderLabCollectionParam>& Collections)
|
||||
{
|
||||
for (const FShaderLabProperty& Prop : Program.Properties)
|
||||
{
|
||||
@@ -1100,6 +1349,22 @@ namespace ShaderLabGraph
|
||||
Custom.Inputs.Add(In);
|
||||
}
|
||||
}
|
||||
// Material Parameter Collection reads: same wiring as a property (deterministic declaration order).
|
||||
for (const FShaderLabCollectionParam& C : Collections)
|
||||
{
|
||||
if (!ReferencesToken(InBody, C.Name.ToString()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FParamNode* Node = PropertyNodes.Find(C.Name);
|
||||
if (Node && Node->Expr)
|
||||
{
|
||||
FCustomInput In;
|
||||
In.InputName = C.Name;
|
||||
In.Input.Connect(0, Node->Expr);
|
||||
Custom.Inputs.Add(In);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Map an SL_INTERPOLATOR return type to a Custom-node output type (parser guarantees float1..4). */
|
||||
@@ -1319,12 +1584,68 @@ namespace ShaderLabGraph
|
||||
}
|
||||
}
|
||||
|
||||
/** Map a SubSurfaceType token to the engine enum. */
|
||||
static bool ParseSubSurfaceType(const FString& Token, EMaterialSubSurfaceType& Out)
|
||||
{
|
||||
if (Token == TEXT("None")) { Out = MSS_None; return true; }
|
||||
if (Token == TEXT("Wrap")) { Out = MSS_Wrap; return true; }
|
||||
if (Token == TEXT("TwoSidedWrap")) { Out = MSS_TwoSidedWrap; return true; }
|
||||
if (Token == TEXT("Diffusion")) { Out = MSS_Diffusion; return true; }
|
||||
if (Token == TEXT("SimpleVolume")) { Out = MSS_SimpleVolume; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the Custom node for a pixel-stage body that writes FShaderLabSurface fields and wire it into
|
||||
* a fresh Substrate Slab BSDF. Returns the slab (nullptr only on error). When bAllowMaterialOutputs,
|
||||
* S.Opacity / S.OpacityMask are wired to the material-level pins (single-Surface sugar path only).
|
||||
* Apply non-pin BSDF modifiers (asset profiles + SubSurfaceType) onto the concrete node. Only modifiers
|
||||
* applicable to the node type are consumed; others are ignored. Returns false (with an error) on a bad
|
||||
* asset path or enum token.
|
||||
*/
|
||||
static UMaterialExpressionSubstrateSlabBSDF* BuildSlab(
|
||||
template <typename TProfile>
|
||||
static bool LoadProfileAsset(const FString& Path, const TCHAR* What, TObjectPtr<TProfile>& OutPtr, TArray<FString>& OutErrors)
|
||||
{
|
||||
if (Path.IsEmpty()) { return true; }
|
||||
TProfile* P = LoadObject<TProfile>(nullptr, *Path);
|
||||
if (!P) { OutErrors.Add(FString::Printf(TEXT("%s: cannot load '%s'"), What, *Path)); return false; }
|
||||
OutPtr = P;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ApplyBsdfModifiers(UMaterialExpression* Bsdf, const FShaderLabBsdfModifiers& M, TArray<FString>& OutErrors)
|
||||
{
|
||||
if (UMaterialExpressionSubstrateSlabBSDF* Slab = Cast<UMaterialExpressionSubstrateSlabBSDF>(Bsdf))
|
||||
{
|
||||
if (!LoadProfileAsset(M.SubsurfaceProfilePath, TEXT("SubsurfaceProfile"), Slab->SubsurfaceProfile, OutErrors)) { return false; }
|
||||
if (!LoadProfileAsset(M.SpecularProfilePath, TEXT("SpecularProfile"), Slab->SpecularProfile, OutErrors)) { return false; }
|
||||
if (!M.SubSurfaceType.IsEmpty())
|
||||
{
|
||||
EMaterialSubSurfaceType T;
|
||||
if (!ParseSubSurfaceType(M.SubSurfaceType, T))
|
||||
{
|
||||
OutErrors.Add(FString::Printf(TEXT("Unknown SubSurfaceType '%s' (use None/Wrap/TwoSidedWrap/Diffusion/SimpleVolume)"), *M.SubSurfaceType));
|
||||
return false;
|
||||
}
|
||||
Slab->SubSurfaceType = T;
|
||||
}
|
||||
}
|
||||
else if (UMaterialExpressionSubstrateEyeBSDF* Eye = Cast<UMaterialExpressionSubstrateEyeBSDF>(Bsdf))
|
||||
{
|
||||
if (!LoadProfileAsset(M.SubsurfaceProfilePath, TEXT("SubsurfaceProfile"), Eye->SubsurfaceProfile, OutErrors)) { return false; }
|
||||
}
|
||||
else if (UMaterialExpressionSubstrateToonBSDF* Toon = Cast<UMaterialExpressionSubstrateToonBSDF>(Bsdf))
|
||||
{
|
||||
if (!LoadProfileAsset(M.ToonProfilePath, TEXT("ToonProfile"), Toon->ToonProfile, OutErrors)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the Custom node for a pixel-stage body that writes the given BSDF's output-struct fields and wire
|
||||
* it into a fresh Substrate BSDF node (chosen by Desc). Returns the BSDF node (nullptr only on error).
|
||||
* When bAllowMaterialOutputs, S.Opacity / S.OpacityMask are wired to the material-level pins (single-entry
|
||||
* sugar path only). Generic across all Substrate BSDFs via the FBsdfDesc field->pin table.
|
||||
*/
|
||||
static UMaterialExpression* BuildBsdf(
|
||||
const FBsdfDesc& Desc, const FShaderLabBsdfModifiers& Modifiers,
|
||||
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
|
||||
const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
|
||||
const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit,
|
||||
@@ -1332,11 +1653,25 @@ namespace ShaderLabGraph
|
||||
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
|
||||
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
|
||||
{
|
||||
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
|
||||
UMaterialExpression* Bsdf = Desc.MakeNode(Material, IoY);
|
||||
if (!ApplyBsdfModifiers(Bsdf, Modifiers, OutErrors))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Single Layer Water additionally requires a companion SingleLayerWaterMaterialOutput node — the
|
||||
// lighting / ray-tracing shaders call GetSingleLayerWaterMaterialOutputN, which only exists when that
|
||||
// node is in the graph. Add it up-front (so even an empty water body compiles); wire it below.
|
||||
UMaterialExpressionSingleLayerWaterMaterialOutput* WaterOut = nullptr;
|
||||
if (Desc.Type == EShaderLabBsdfType::Water)
|
||||
{
|
||||
WaterOut = NewExpr<UMaterialExpressionSingleLayerWaterMaterialOutput>(Material, IoY, -600);
|
||||
}
|
||||
|
||||
TArray<const FSlabFieldDef*> UsedSlab;
|
||||
for (const FSlabFieldDef& F : GSlabFields)
|
||||
for (int32 i = 0; i < Desc.NumFields; ++i)
|
||||
{
|
||||
const FSlabFieldDef& F = Desc.Fields[i];
|
||||
if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field))
|
||||
{
|
||||
UsedSlab.Add(&F);
|
||||
@@ -1344,14 +1679,16 @@ namespace ShaderLabGraph
|
||||
}
|
||||
const bool bUsesOpacity = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
|
||||
const bool bUsesOpacityMask = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".OpacityMask"));
|
||||
const bool bUsesRefraction = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".Refraction"));
|
||||
const bool bUsesPDO = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".PixelDepthOffset"));
|
||||
|
||||
if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask)
|
||||
if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask && !bUsesRefraction && !bUsesPDO)
|
||||
{
|
||||
return Slab; // Empty body: a default Substrate slab.
|
||||
return Bsdf; // Empty body: a default Substrate BSDF.
|
||||
}
|
||||
|
||||
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
||||
Custom->Description = TEXT("ShaderLab Surface");
|
||||
Custom->Description = Desc.CustomDesc;
|
||||
Custom->OutputType = CMOT_Float1;
|
||||
AddIncludes(*Custom, Model);
|
||||
|
||||
@@ -1369,11 +1706,11 @@ namespace ShaderLabGraph
|
||||
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
||||
Custom->Inputs.Add(In);
|
||||
}
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps);
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections);
|
||||
|
||||
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
||||
FString Code = FString::Printf(TEXT("FShaderLabSurface %s = ShaderLabDefaultSurface();\n{\n%s}\n"),
|
||||
*OutParamName, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
|
||||
FString Code = FString::Printf(TEXT("%s %s = %s();\n{\n%s}\n"),
|
||||
Desc.StructName, *OutParamName, Desc.DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
|
||||
|
||||
int32 OutputIndex = 1; // index 0 is the (unused) main return
|
||||
TArray<TPair<const FSlabFieldDef*, int32>> SlabOutputs;
|
||||
@@ -1403,20 +1740,54 @@ namespace ShaderLabGraph
|
||||
Code += FString::Printf(TEXT("SLO_OpacityMask = %s.OpacityMask;\n"), *OutParamName);
|
||||
OpacityMaskOutIdx = OutputIndex++;
|
||||
}
|
||||
int32 RefractionOutIdx = INDEX_NONE;
|
||||
int32 PDOOutIdx = INDEX_NONE;
|
||||
if (bUsesRefraction)
|
||||
{
|
||||
FCustomOutput Out; Out.OutputName = TEXT("SLO_Refraction"); Out.OutputType = CMOT_Float1;
|
||||
Custom->AdditionalOutputs.Add(Out);
|
||||
Code += FString::Printf(TEXT("SLO_Refraction = %s.Refraction;\n"), *OutParamName);
|
||||
RefractionOutIdx = OutputIndex++;
|
||||
}
|
||||
if (bUsesPDO)
|
||||
{
|
||||
FCustomOutput Out; Out.OutputName = TEXT("SLO_PixelDepthOffset"); Out.OutputType = CMOT_Float1;
|
||||
Custom->AdditionalOutputs.Add(Out);
|
||||
Code += FString::Printf(TEXT("SLO_PixelDepthOffset = %s.PixelDepthOffset;\n"), *OutParamName);
|
||||
PDOOutIdx = OutputIndex++;
|
||||
}
|
||||
Code += TEXT("return 0.0f;\n");
|
||||
Custom->Code = Code;
|
||||
Custom->RebuildOutputs();
|
||||
|
||||
for (const TPair<const FSlabFieldDef*, int32>& Pair : SlabOutputs)
|
||||
{
|
||||
if (FExpressionInput* Pin = GetSlabPin(Slab, Pair.Key->Field))
|
||||
if (FExpressionInput* Pin = Desc.GetPin(Bsdf, Pair.Key->Field))
|
||||
{
|
||||
Pin->Connect(Pair.Value, Custom);
|
||||
}
|
||||
}
|
||||
if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); }
|
||||
if (OpacityMaskOutIdx != INDEX_NONE) { EditorOnly.OpacityMask.Connect(OpacityMaskOutIdx, Custom); }
|
||||
return Slab;
|
||||
if (RefractionOutIdx != INDEX_NONE) { EditorOnly.Refraction.Connect(RefractionOutIdx, Custom); }
|
||||
if (PDOOutIdx != INDEX_NONE) { EditorOnly.PixelDepthOffset.Connect(PDOOutIdx, Custom); }
|
||||
|
||||
// Wire the water volume fields into the companion output node (by matching Custom output field name).
|
||||
if (WaterOut)
|
||||
{
|
||||
auto ConnectWaterOut = [&](FExpressionInput& Pin, const TCHAR* FieldName)
|
||||
{
|
||||
for (const TPair<const FSlabFieldDef*, int32>& P : SlabOutputs)
|
||||
{
|
||||
if (FCString::Strcmp(P.Key->Field, FieldName) == 0) { Pin.Connect(P.Value, Custom); return; }
|
||||
}
|
||||
};
|
||||
ConnectWaterOut(WaterOut->ScatteringCoefficients, TEXT("WaterAlbedo"));
|
||||
ConnectWaterOut(WaterOut->AbsorptionCoefficients, TEXT("WaterExtinction"));
|
||||
ConnectWaterOut(WaterOut->PhaseG, TEXT("WaterPhaseG"));
|
||||
ConnectWaterOut(WaterOut->ColorScaleBehindWater, TEXT("ColorScaleBehindWater"));
|
||||
}
|
||||
return Bsdf;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1450,7 +1821,7 @@ namespace ShaderLabGraph
|
||||
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
||||
Custom->Inputs.Add(In);
|
||||
}
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps);
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections);
|
||||
|
||||
const bool bUsesColor = ReferencesToken(InBody, OutParamName + TEXT(".Color"));
|
||||
const bool bUsesOpacity = ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
|
||||
@@ -1512,7 +1883,7 @@ namespace ShaderLabGraph
|
||||
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
||||
Custom->Inputs.Add(In);
|
||||
}
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, Value.Body, ReqProps);
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, Value.Body, ReqProps, Model.Collections);
|
||||
|
||||
// The body itself contains `return <scalar>;`, so it is the Custom function's body directly.
|
||||
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
||||
@@ -1553,7 +1924,7 @@ namespace ShaderLabGraph
|
||||
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
||||
Custom->Inputs.Add(In);
|
||||
}
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, Interp.Body, ReqProps);
|
||||
WirePropInputs(*Custom, Program, PropertyNodes, Interp.Body, ReqProps, Model.Collections);
|
||||
|
||||
// The body contains `return <expr>;`, so it is the Custom function's body directly.
|
||||
Custom->Code = WrapBodyWithLineMapping(Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath));
|
||||
@@ -1613,7 +1984,7 @@ namespace ShaderLabGraph
|
||||
/** Recursively build the Substrate expression for topology node `Index`. Returns nullptr on error. */
|
||||
static UMaterialExpression* BuildTopologyNode(
|
||||
UMaterial& Material, int32 Index, const FShaderLabModel& Model,
|
||||
const TMap<FName, UMaterialExpressionSubstrateSlabBSDF*>& SlabByName,
|
||||
const TMap<FName, UMaterialExpression*>& SlabByName,
|
||||
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
|
||||
const TMap<FName, FParamNode>& PropertyNodes,
|
||||
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
|
||||
@@ -1628,7 +1999,7 @@ namespace ShaderLabGraph
|
||||
|
||||
if (Node.Op == EShaderLabOp::SlabRef)
|
||||
{
|
||||
if (UMaterialExpressionSubstrateSlabBSDF* const* Found = SlabByName.Find(Node.SlabRef))
|
||||
if (UMaterialExpression* const* Found = SlabByName.Find(Node.SlabRef))
|
||||
{
|
||||
return *Found;
|
||||
}
|
||||
@@ -1862,6 +2233,50 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
TMap<FName, UMaterialExpression*> InterpByName;
|
||||
TSet<FName> UsedInterps;
|
||||
|
||||
// CustomPrimitiveData contract: author-specified slot indices must fit the fixed float budget and must
|
||||
// not overlap. Scalar occupies 1 float, Vector 4 (starting at PrimitiveDataIndex). Validated across all
|
||||
// declared CPD properties (parser already guarantees Scalar/Vector-only + a non-negative index).
|
||||
{
|
||||
constexpr int32 NumFloats = FCustomPrimitiveData::NumCustomPrimitiveDataFloats;
|
||||
int32 SlotOwner[NumFloats];
|
||||
for (int32 i = 0; i < NumFloats; ++i) { SlotOwner[i] = INDEX_NONE; }
|
||||
bool bCpdError = false;
|
||||
for (int32 PropIdx = 0; PropIdx < Program.Properties.Num(); ++PropIdx)
|
||||
{
|
||||
const FShaderLabProperty& Prop = Program.Properties[PropIdx];
|
||||
if (!Prop.bUseCustomPrimitiveData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int32 Size = (Prop.Type == EShaderLabPropertyType::Scalar) ? 1 : 4;
|
||||
const int32 Start = Prop.PrimitiveDataIndex;
|
||||
if (Start + Size > NumFloats)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("Property '%s': CustomPrimitiveData index %d + %d float(s) exceeds the %d-float budget"),
|
||||
*Prop.Name.ToString(), Start, Size, NumFloats));
|
||||
bCpdError = true;
|
||||
continue;
|
||||
}
|
||||
for (int32 s = Start; s < Start + Size; ++s)
|
||||
{
|
||||
if (SlotOwner[s] != INDEX_NONE)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(
|
||||
TEXT("Property '%s': CustomPrimitiveData slot %d overlaps property '%s'"),
|
||||
*Prop.Name.ToString(), s, *Program.Properties[SlotOwner[s]].Name.ToString()));
|
||||
bCpdError = true;
|
||||
break;
|
||||
}
|
||||
SlotOwner[s] = PropIdx;
|
||||
}
|
||||
}
|
||||
if (bCpdError)
|
||||
{
|
||||
return false; // Contract-style hard failure (surfaced through the standard error path).
|
||||
}
|
||||
}
|
||||
|
||||
for (const FShaderLabProperty& Prop : Program.Properties)
|
||||
{
|
||||
const FString NameStr = Prop.Name.ToString();
|
||||
@@ -1918,10 +2333,18 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
E->DefaultValue = Prop.ScalarDefault;
|
||||
E->Group = FName(*Prop.Group);
|
||||
E->SortPriority = Prop.SortPriority;
|
||||
if (Prop.bHasRange)
|
||||
if (Prop.bHasClampMin)
|
||||
{
|
||||
E->SliderMin = Prop.RangeMin;
|
||||
E->SliderMax = Prop.RangeMax;
|
||||
E->SliderMin = Prop.ClampMin;
|
||||
}
|
||||
if (Prop.bHasClampMax)
|
||||
{
|
||||
E->SliderMax = Prop.ClampMax;
|
||||
}
|
||||
if (Prop.bUseCustomPrimitiveData)
|
||||
{
|
||||
E->bUseCustomPrimitiveData = true;
|
||||
E->PrimitiveDataIndex = static_cast<uint8>(Prop.PrimitiveDataIndex);
|
||||
}
|
||||
Node.Expr = E;
|
||||
break;
|
||||
@@ -1934,6 +2357,11 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
E->DefaultValue = Prop.VectorDefault;
|
||||
E->Group = FName(*Prop.Group);
|
||||
E->SortPriority = Prop.SortPriority;
|
||||
if (Prop.bUseCustomPrimitiveData)
|
||||
{
|
||||
E->bUseCustomPrimitiveData = true;
|
||||
E->PrimitiveDataIndex = static_cast<uint8>(Prop.PrimitiveDataIndex);
|
||||
}
|
||||
Node.Expr = E;
|
||||
break;
|
||||
}
|
||||
@@ -1951,6 +2379,22 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
Node.bIsTexture = true;
|
||||
break;
|
||||
}
|
||||
case EShaderLabPropertyType::Texture2DArray:
|
||||
case EShaderLabPropertyType::Texture3D:
|
||||
case EShaderLabPropertyType::TextureCubeArray:
|
||||
{
|
||||
// Array / volume texture object parameter. The engine assigns a matching-dimension default when
|
||||
// Texture is left null; the artist binds a real asset on the Material Instance. (Built-in white/
|
||||
// black/grey/normal default tokens only exist for Texture2D, so they are not applied here.)
|
||||
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
|
||||
E->ParameterName = Prop.Name;
|
||||
E->Group = FName(*Prop.Group);
|
||||
E->SortPriority = Prop.SortPriority;
|
||||
E->SamplerType = SAMPLERTYPE_Color;
|
||||
Node.Expr = E;
|
||||
Node.bIsTexture = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -1960,6 +2404,41 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
}
|
||||
}
|
||||
|
||||
// 1a2) Material Parameter Collection reads (SL_COLLECTION): a CollectionParameter node per referenced
|
||||
// declaration, registered in PropertyNodes so WirePropInputs wires it into bodies exactly like a property.
|
||||
for (const FShaderLabCollectionParam& C : Model.Collections)
|
||||
{
|
||||
const FString NameStr = C.Name.ToString();
|
||||
bool bRef = Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr);
|
||||
for (const FShaderLabSlab& Slab : Model.Slabs) { bRef |= ReferencesToken(Slab.Body, NameStr); }
|
||||
for (const FShaderLabValue& Value : Model.Values) { bRef |= ReferencesToken(Value.Body, NameStr); }
|
||||
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { bRef |= ReferencesToken(Interp.Body, NameStr); }
|
||||
if (Model.bHasVertex) { bRef |= ReferencesToken(Model.VertexBody, NameStr); }
|
||||
if (!bRef)
|
||||
{
|
||||
continue; // Declared but unreferenced: skip (keeps the graph minimal/deterministic).
|
||||
}
|
||||
UMaterialParameterCollection* Coll = LoadObject<UMaterialParameterCollection>(nullptr, *C.CollectionPath);
|
||||
if (!Coll)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(TEXT("SL_COLLECTION '%s': cannot load Material Parameter Collection '%s'"), *NameStr, *C.CollectionPath));
|
||||
return false;
|
||||
}
|
||||
const FGuid ParamId = Coll->GetParameterId(C.ParameterName);
|
||||
if (!ParamId.IsValid())
|
||||
{
|
||||
OutErrors.Add(FString::Printf(TEXT("SL_COLLECTION '%s': parameter '%s' not found in '%s'"), *NameStr, *C.ParameterName.ToString(), *C.CollectionPath));
|
||||
return false;
|
||||
}
|
||||
UMaterialExpressionCollectionParameter* E = NewExpr<UMaterialExpressionCollectionParameter>(Material, ParamY, -1000);
|
||||
E->Collection = Coll;
|
||||
E->ParameterName = C.ParameterName;
|
||||
E->ParameterId = ParamId;
|
||||
FParamNode Node;
|
||||
Node.Expr = E;
|
||||
PropertyNodes.Add(C.Name, Node);
|
||||
}
|
||||
|
||||
// 1b) Build a Vertex Interpolator node per declared interpolator (Custom @ vertex freq -> VertexInterpolator).
|
||||
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
|
||||
{
|
||||
@@ -1998,7 +2477,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
{
|
||||
// Single-Surface sugar: one slab straight to FrontMaterial, with S.Opacity/S.OpacityMask
|
||||
// allowed as material-level outputs.
|
||||
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
|
||||
UMaterialExpression* Slab = BuildBsdf(
|
||||
*FindBsdfDesc(EShaderLabBsdfType::Slab), Model.SurfaceModifiers,
|
||||
Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine,
|
||||
Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
|
||||
if (!Slab)
|
||||
@@ -2009,9 +2489,10 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multi-slab: each named Slab -> its own slab node; Value blocks -> scalar Custom nodes; the
|
||||
// FrontMaterial topology tree mixes them; Opacity/OpacityMask come from named Value blocks.
|
||||
TMap<FName, UMaterialExpressionSubstrateSlabBSDF*> SlabByName;
|
||||
// Multi-BSDF: each named block -> its own Substrate BSDF node (type from FShaderLabSlab.BsdfType);
|
||||
// Value blocks -> scalar Custom nodes; the FrontMaterial topology tree mixes them; Opacity/OpacityMask
|
||||
// come from named Value blocks.
|
||||
TMap<FName, UMaterialExpression*> SlabByName;
|
||||
for (const FShaderLabSlab& SlabDecl : Model.Slabs)
|
||||
{
|
||||
if (SlabByName.Contains(SlabDecl.Name))
|
||||
@@ -2019,7 +2500,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
OutErrors.Add(FString::Printf(TEXT("Duplicate Slab name '%s'"), *SlabDecl.Name.ToString()));
|
||||
return false;
|
||||
}
|
||||
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
|
||||
const FBsdfDesc* Desc = FindBsdfDesc(SlabDecl.BsdfType);
|
||||
if (!Desc)
|
||||
{
|
||||
OutErrors.Add(FString::Printf(TEXT("Slab '%s' has an unsupported BSDF type"), *SlabDecl.Name.ToString()));
|
||||
return false;
|
||||
}
|
||||
UMaterialExpression* Slab = BuildBsdf(
|
||||
*Desc, SlabDecl.Modifiers,
|
||||
Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine,
|
||||
Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
|
||||
if (!Slab)
|
||||
@@ -2046,6 +2534,13 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
ValueByName.Add(ValueDecl.Name, ValueNode);
|
||||
}
|
||||
|
||||
// Composition legality: validate operator/BSDF-type combinations up-front (mirrors the engine's Substrate
|
||||
// allow-lists) so an illegal combo errors with a .usl line instead of a generic engine node-name error.
|
||||
if (!Model.ValidateTopology(OutErrors))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Every declared Slab must be reachable from FrontMaterial (contract: no dead slabs).
|
||||
TSet<FName> ReferencedSlabs;
|
||||
for (const FShaderLabTopoNode& Node : Model.Topology)
|
||||
@@ -2084,6 +2579,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
};
|
||||
if (!ConnectMaterialOutput(EditorOnly->Opacity, Model.OpacityValueName, TEXT("Opacity"))) { return false; }
|
||||
if (!ConnectMaterialOutput(EditorOnly->OpacityMask, Model.OpacityMaskValueName, TEXT("OpacityMask"))) { return false; }
|
||||
if (!ConnectMaterialOutput(EditorOnly->Refraction, Model.RefractionValueName, TEXT("Refraction"))) { return false; }
|
||||
if (!ConnectMaterialOutput(EditorOnly->PixelDepthOffset, Model.PixelDepthOffsetValueName, TEXT("PixelDepthOffset"))) { return false; }
|
||||
}
|
||||
|
||||
// 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry
|
||||
@@ -2128,7 +2625,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
||||
VCustom->Inputs.Add(In);
|
||||
}
|
||||
WirePropInputs(*VCustom, Program, PropertyNodes, Model.VertexBody, VtxReqProps);
|
||||
WirePropInputs(*VCustom, Program, PropertyNodes, Model.VertexBody, VtxReqProps, Model.Collections);
|
||||
|
||||
const FString VSrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
||||
Code += FString::Printf(TEXT("FShaderLabVertex %s = ShaderLabDefaultVertex();\n{\n%s}\n"),
|
||||
@@ -2186,6 +2683,88 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only permutation macros (SHADERLAB_QUALITY / _FEATURELEVEL / _SHADINGPATH). When a body references
|
||||
// one, wire the matching engine switch (Quality/FeatureLevel/ShadingPath) with per-branch `#define` Custom
|
||||
// nodes into the ParameterAnchor. The switch compiles ONLY the branch for the current permutation (verified
|
||||
// for the real, non-validating compile), so exactly one ordered value leaks ahead of every body `#if`.
|
||||
{
|
||||
auto AnyBodyUses = [&](const TCHAR* Token) -> bool
|
||||
{
|
||||
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, Token)) { return true; }
|
||||
for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, Token)) { return true; } }
|
||||
for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, Token)) { return true; } }
|
||||
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { if (ReferencesToken(Interp.Body, Token)) { return true; } }
|
||||
if (Model.bHasVertex && ReferencesToken(Model.VertexBody, Token)) { return true; }
|
||||
return false;
|
||||
};
|
||||
|
||||
if (AnyBodyUses(TEXT("SHADERLAB_QUALITY")))
|
||||
{
|
||||
UMaterialExpressionQualitySwitch* Sw = NewExpr<UMaterialExpressionQualitySwitch>(Material, ParamY, -1150);
|
||||
// Ordered values: Low<Medium<High<Epic. Engine enum order is Low,High,Medium,Epic.
|
||||
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("2")));
|
||||
Sw->Inputs[EMaterialQualityLevel::Low].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("0")));
|
||||
Sw->Inputs[EMaterialQualityLevel::Medium].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("1")));
|
||||
Sw->Inputs[EMaterialQualityLevel::High].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("2")));
|
||||
Sw->Inputs[EMaterialQualityLevel::Epic].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("3")));
|
||||
AnchorInputs.Add(Sw);
|
||||
}
|
||||
if (AnyBodyUses(TEXT("SHADERLAB_FEATURELEVEL")))
|
||||
{
|
||||
UMaterialExpressionFeatureLevelSwitch* Sw = NewExpr<UMaterialExpressionFeatureLevelSwitch>(Material, ParamY, -1150);
|
||||
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("1")));
|
||||
Sw->Inputs[ERHIFeatureLevel::ES3_1].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("0")));
|
||||
Sw->Inputs[ERHIFeatureLevel::SM5].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("1")));
|
||||
Sw->Inputs[ERHIFeatureLevel::SM6].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("2")));
|
||||
AnchorInputs.Add(Sw);
|
||||
}
|
||||
if (AnyBodyUses(TEXT("SHADERLAB_SHADINGPATH")))
|
||||
{
|
||||
UMaterialExpressionShadingPathSwitch* Sw = NewExpr<UMaterialExpressionShadingPathSwitch>(Material, ParamY, -1150);
|
||||
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("0")));
|
||||
Sw->Inputs[ERHIShadingPath::Deferred].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("0")));
|
||||
Sw->Inputs[ERHIShadingPath::Forward].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("1")));
|
||||
Sw->Inputs[ERHIShadingPath::Mobile].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("2")));
|
||||
AnchorInputs.Add(Sw);
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
{
|
||||
auto BodyUsesScene = [](const FString& Body) -> bool
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Funnel every static-switch selector into the ParameterAnchor. The anchor is a CustomOutput compiled
|
||||
// BEFORE the material attributes (ShouldCompileBeforeAttributes), so compiling it compiles each
|
||||
// selector — emitting the selected `#define <Name> 0/1` for the current permutation ahead of every
|
||||
|
||||
Reference in New Issue
Block a user