From 59af77e8f6f64ea0d00deb1849345f88d88d11c2 Mon Sep 17 00:00:00 2001 From: Eragon-Brisingr <450614754@qq.com> Date: Sun, 5 Jul 2026 13:23:08 +0800 Subject: [PATCH] Merge output data for reduce duplicate calculate --- Docs/README_CN.md | 33 +- README.md | 47 ++- Shaders/Private/ShaderLabCommon.ush | 110 +++++- Shaders/Private/ShaderLabDSL.ush | 14 +- Source/UShaderLab/Private/ShaderLabParser.cpp | 81 ++-- .../Private/ShaderLabSettingsApplier.cpp | 1 + Source/UShaderLab/Public/ShaderLabModel.h | 18 +- .../Private/ShaderLabGraphBuilder.cpp | 349 ++++++++++++++---- 8 files changed, 515 insertions(+), 138 deletions(-) diff --git a/Docs/README_CN.md b/Docs/README_CN.md index 10e7eb3..2dd0e05 100644 --- a/Docs/README_CN.md +++ b/Docs/README_CN.md @@ -181,17 +181,12 @@ void Surface(inout FShaderLabSurface S) } ``` -常用字段: +`FShaderLabSurface` = 18 个 SlabBSDF 着色引脚 **加上** 材质级(整材质)输出字段,所以简单材质可以在一个 body 里全部写完。常用字段: -- `DiffuseAlbedo` -- `F0` -- `Roughness` -- `Normal` -- `EmissiveColor` -- `Opacity` -- `OpacityMask` -- `Refraction` -- `PixelDepthOffset` +- 着色(SlabBSDF 引脚):`DiffuseAlbedo`、`F0`、`F90`、`Roughness`、`Anisotropy`、`Normal`、`Tangent`、`EmissiveColor`、`SSSMFP`、`SecondRoughness`、`Fuzz*`、`Glint*` … +- 材质级:`Opacity`、`OpacityMask`、`Refraction`、`PixelDepthOffset`、`AmbientOcclusion`、`SurfaceThickness` + +材质级字段仅在匹配的 `Domain`/`BlendMode` 下生效(建图期按引擎 `IsPropertyActive` 做契约校验,不匹配则映射 `.usl` 行报错):`OpacityMask` 需 `Masked`;`Refraction` 需半透明混合 + `RefractionMethod = RM_IndexOfRefraction`;`SurfaceThickness` 需 `bIsThinSurface = true`。**`Opacity` 是覆盖率**:Substrate 下引擎的 `MP_Opacity` 仅对 `AlphaComposite` 激活,因此半透明/贴花的覆盖率由建图器**自动在整材质外包一层 Substrate Weight 节点**实现(你只需写 `S.Opacity`)。 如果需要在 body 里使用依赖材质上下文的变换函数,可以显式声明 `Parameters`: @@ -231,21 +226,21 @@ void Unlit(inout FShaderLabUnlit U) ## 多层材质 -多个 `SL_SLAB` 可以通过 `SL_FRONTMATERIAL` 组合成一个 Substrate 材质。 +多个 `SL_SLAB` 可以通过 `SL_FRONTMATERIAL` 组合成一个 Substrate 材质。Slab 是纯 BSDF 层,参数为 `inout FShaderLabSlab`(只有 18 个 SlabBSDF 引脚,没有 Opacity 等材质级字段)。整材质输出写在单独的 `SL_MATERIAL` 块里(见下)。 ```hlsl SL_PROPERTY(Category = "Layer", ClampMin = 0, ClampMax = 1) float Mix = 0.5; SL_SLAB() -void Base(inout FShaderLabSurface S) +void Base(inout FShaderLabSlab S) { S.DiffuseAlbedo = float3(0.1, 0.1, 0.1); S.Roughness = 0.8; } SL_SLAB() -void Coat(inout FShaderLabSurface S) +void Coat(inout FShaderLabSlab S) { S.DiffuseAlbedo = float3(0.8, 0.2, 0.1); S.Roughness = 0.2; @@ -274,6 +269,16 @@ float EdgeMask() SL_FRONTMATERIAL(HorizontalMix(Base, Coat, EdgeMask)) ``` +多层材质的整材质输出(单入口 `SL_SURFACE` 里是 `S.*`)写在 `SL_MATERIAL` 块里,填 `FShaderLabMaterialOutput`——一个 Custom 节点输出全部字段(共享计算)。它与 `SL_SURFACE` 互斥。 + +```hlsl +SL_MATERIAL() +void Material(inout FShaderLabMaterialOutput O) +{ + O.OpacityMask = Texture2DSample(MaskTex, MaskTexSampler, UE_TextureCoordinate(0)).a; +} +``` + ## 顶点与插值 `SL_VERTEX` 用于输出顶点阶段数据,比如世界位置偏移。 @@ -289,6 +294,8 @@ void Vertex(inout FShaderLabVertex V) } ``` +`SL_VERTEX` 还能在顶点频率计算 UV,写入 `V.CustomizedUV0..7`(插值到像素,用 `UE_TextureCoordinate(i)` 读回)。写 `V.CustomizedUV` **必须**配 `SL_SETTINGS(NumCustomizedUVs = N)` 且 `N > i`——引擎只分配前 `NumCustomizedUVs` 组(默认 0),其余槽位直接透传原始顶点 texcoord;ShaderLab 会把「写了 UV 但 N ≤ i」变成建图错误而非静默失效。示例见 `Shaders/Examples/CustomizedUV.usl`。 + `SL_INTERPOLATOR` 用于在顶点阶段计算一个值,并在像素阶段读取。 ```hlsl diff --git a/README.md b/README.md index f557bf8..d1e29b9 100644 --- a/README.md +++ b/README.md @@ -181,17 +181,18 @@ void Surface(inout FShaderLabSurface S) } ``` -Common fields: +`FShaderLabSurface` = the 18 SlabBSDF shading pins **plus** the material-level (whole-material) outputs, so a +simple material writes everything in one block. Common fields: -- `DiffuseAlbedo` -- `F0` -- `Roughness` -- `Normal` -- `EmissiveColor` -- `Opacity` -- `OpacityMask` -- `Refraction` -- `PixelDepthOffset` +- Shading (SlabBSDF pins): `DiffuseAlbedo`, `F0`, `F90`, `Roughness`, `Anisotropy`, `Normal`, `Tangent`, + `EmissiveColor`, `SSSMFP`, `SecondRoughness`, `Fuzz*`, `Glint*`, … +- Material-level: `Opacity`, `OpacityMask`, `Refraction`, `PixelDepthOffset`, `AmbientOcclusion`, `SurfaceThickness` + +The material-level fields are only active for the matching `Domain`/`BlendMode` (validated at build with a +`.usl`-line error, mirroring the engine): `OpacityMask` needs `Masked`; `Refraction` needs a translucent blend + +`RefractionMethod = RM_IndexOfRefraction`; `SurfaceThickness` needs `bIsThinSurface = true`. **`Opacity` is +coverage**: under Substrate the engine's `MP_Opacity` is only active for `AlphaComposite`, so translucent/decal +coverage is applied automatically by wrapping the material in a Substrate Weight node (you just write `S.Opacity`). If a body needs transform helpers that depend on the material context, declare `Parameters` explicitly: @@ -231,21 +232,23 @@ Common entries: ## Layered Materials -Multiple `SL_SLAB` blocks can be combined into one Substrate material with `SL_FRONTMATERIAL`. +Multiple `SL_SLAB` blocks can be combined into one Substrate material with `SL_FRONTMATERIAL`. A slab is a +pure BSDF layer, so it takes `inout FShaderLabSlab` (the 18 SlabBSDF pins only — no material-level fields like +Opacity). Whole-material outputs go in an `SL_MATERIAL` block (see below). ```hlsl SL_PROPERTY(Category = "Layer", ClampMin = 0, ClampMax = 1) float Mix = 0.5; SL_SLAB() -void Base(inout FShaderLabSurface S) +void Base(inout FShaderLabSlab S) { S.DiffuseAlbedo = float3(0.1, 0.1, 0.1); S.Roughness = 0.8; } SL_SLAB() -void Coat(inout FShaderLabSurface S) +void Coat(inout FShaderLabSlab S) { S.DiffuseAlbedo = float3(0.8, 0.2, 0.1); S.Roughness = 0.2; @@ -274,6 +277,18 @@ float EdgeMask() SL_FRONTMATERIAL(HorizontalMix(Base, Coat, EdgeMask)) ``` +For a layered material, the whole-material outputs (which a single `SL_SURFACE` would carry as `S.*`) go in an +`SL_MATERIAL` block filling `FShaderLabMaterialOutput` — one Custom node feeds all of them (shared computation). +It is mutually exclusive with `SL_SURFACE`. + +```hlsl +SL_MATERIAL() +void Material(inout FShaderLabMaterialOutput O) +{ + O.OpacityMask = Texture2DSample(MaskTex, MaskTexSampler, UE_TextureCoordinate(0)).a; +} +``` + ## Vertex And Interpolator `SL_VERTEX` outputs vertex-stage data, such as world position offset. @@ -289,6 +304,12 @@ void Vertex(inout FShaderLabVertex V) } ``` +`SL_VERTEX` can also compute UVs at vertex frequency into `V.CustomizedUV0..7` (interpolated to the pixel shader, +read back with `UE_TextureCoordinate(i)`). Writing `V.CustomizedUV` **requires** `SL_SETTINGS(NumCustomizedUVs = N)` +with `N > i` — the engine only allocates the first `NumCustomizedUVs` slots (default 0) and otherwise passes the +raw vertex texcoord through. ShaderLab turns a missing/too-small `NumCustomizedUVs` into a build error rather than a +silent no-op. See `Shaders/Examples/CustomizedUV.usl`. + `SL_INTERPOLATOR` computes a value in the vertex stage and reads it in the pixel stage. ```hlsl diff --git a/Shaders/Private/ShaderLabCommon.ush b/Shaders/Private/ShaderLabCommon.ush index 7e0e02f..abd96c8 100644 --- a/Shaders/Private/ShaderLabCommon.ush +++ b/Shaders/Private/ShaderLabCommon.ush @@ -41,9 +41,56 @@ #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. +// A single Substrate Slab BSDF layer, filled by an SL_SLAB() body in a multi-slab shader. These 18 fields +// mirror the pins of UMaterialExpressionSubstrateSlabBSDF one-for-one (name/order/type). A slab is only a +// BSDF layer — material-level outputs (Opacity/Refraction/...) are NOT slab concerns: set them once per +// material via an SL_MATERIAL() block (FShaderLabMaterialOutput). Defaults mirror the node's pin defaults. +struct FShaderLabSlab +{ + float3 DiffuseAlbedo; + float3 F0; + float3 F90; + float Roughness; + float Anisotropy; + float3 Normal; + float3 Tangent; + float3 SSSMFP; + float SSSMFPScale; + float SSSPhaseAnisotropy; + float3 EmissiveColor; + float SecondRoughness; + float SecondRoughnessWeight; + float FuzzRoughness; + float FuzzAmount; + float3 FuzzColor; + float GlintValue; + float2 GlintUV; +}; + +// Material-level (whole-material) pixel-stage outputs, filled by an SL_MATERIAL() block in a multi-slab +// shader. Each field maps to a main-material-node pin (NOT a slab pin); which are active depends on the +// material Domain/BlendMode (validated by the graph builder mirroring the engine's IsPropertyActive): +// Opacity — translucency coverage (routed through a Substrate Weight over the material) +// OpacityMask — Masked blend cutout +// Refraction — scalar IOR (RefractionMethod = IndexOfRefraction); 1.0 = no bending +// PixelDepthOffset — world-unit depth push toward camera +// AmbientOcclusion — ambient occlusion (Lit) +// SurfaceThickness — thin-surface thickness in cm (only when the material is a thin surface) +struct FShaderLabMaterialOutput +{ + float Opacity; + float OpacityMask; + float Refraction; + float PixelDepthOffset; + float AmbientOcclusion; + float SurfaceThickness; +}; + +// Simple single-entry surface, filled by the SL_SURFACE(...) body. It is the one-stop path: the 18 Slab +// shading fields (== FShaderLabSlab) PLUS the material-level outputs (== FShaderLabMaterialOutput), so a +// simple opaque/masked/translucent material can be written in one block. Multi-slab shaders instead split +// these across SL_SLAB (FShaderLabSlab) + SL_MATERIAL (FShaderLabMaterialOutput). Slab-field defaults +// mirror the SubstrateSlabBSDF pin defaults; material-output defaults mirror the main-node pin defaults. struct FShaderLabSurface { float3 DiffuseAlbedo; @@ -64,11 +111,13 @@ struct FShaderLabSurface float3 FuzzColor; float GlintValue; float2 GlintUV; + // Material-level outputs (single-Surface sugar). See FShaderLabMaterialOutput for pin semantics. 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 + float Refraction; + float PixelDepthOffset; + float AmbientOcclusion; + float SurfaceThickness; }; // --- Additional Substrate BSDF output structs. Each mirrors the pins of its engine BSDF node; the graph @@ -283,10 +332,17 @@ struct FShaderLabVertex { float3 WorldPositionOffset; float Displacement; + // Customized UVs 0-7 (mirrors the engine's 8 MP_CustomizedUVs slots). A written slot i requires + // SL_SETTINGS(NumCustomizedUVs = N) with N > i, otherwise the graph builder errors (writing a slot the + // material never allocates would silently pass through the raw vertex texcoord). float2 CustomizedUV0; float2 CustomizedUV1; float2 CustomizedUV2; float2 CustomizedUV3; + float2 CustomizedUV4; + float2 CustomizedUV5; + float2 CustomizedUV6; + float2 CustomizedUV7; }; FShaderLabSurface ShaderLabDefaultSurface() @@ -314,9 +370,47 @@ FShaderLabSurface ShaderLabDefaultSurface() S.OpacityMask = 1; S.Refraction = 1.0; S.PixelDepthOffset = 0.0; + S.AmbientOcclusion = 1; + S.SurfaceThickness = 0.01; // SUBSTRATE_LAYER_DEFAULT_THICKNESS_CM return S; } +FShaderLabSlab ShaderLabDefaultSlab() +{ + FShaderLabSlab S; + S.DiffuseAlbedo = float3(0.18, 0.18, 0.18); + S.F0 = float3(0.04, 0.04, 0.04); + S.F90 = float3(1, 1, 1); + S.Roughness = 0.5; + S.Anisotropy = 0; + S.Normal = float3(0, 0, 1); + S.Tangent = float3(1, 0, 0); + S.SSSMFP = float3(0, 0, 0); + S.SSSMFPScale = 1; + S.SSSPhaseAnisotropy = 1; + S.EmissiveColor = float3(0, 0, 0); + S.SecondRoughness = 0.5; + S.SecondRoughnessWeight = 0; + S.FuzzRoughness = 0.5; + S.FuzzAmount = 0; + S.FuzzColor = float3(0, 0, 0); + S.GlintValue = 1; + S.GlintUV = float2(0, 0); + return S; +} + +FShaderLabMaterialOutput ShaderLabDefaultMaterialOutput() +{ + FShaderLabMaterialOutput O; + O.Opacity = 1; + O.OpacityMask = 1; + O.Refraction = 1.0; + O.PixelDepthOffset = 0.0; + O.AmbientOcclusion = 1; + O.SurfaceThickness = 0.01; // SUBSTRATE_LAYER_DEFAULT_THICKNESS_CM + return O; +} + FShaderLabPostProcess ShaderLabDefaultPostProcess() { FShaderLabPostProcess P; @@ -342,6 +436,10 @@ FShaderLabVertex ShaderLabDefaultVertex() V.CustomizedUV1 = float2(0, 0); V.CustomizedUV2 = float2(0, 0); V.CustomizedUV3 = float2(0, 0); + V.CustomizedUV4 = float2(0, 0); + V.CustomizedUV5 = float2(0, 0); + V.CustomizedUV6 = float2(0, 0); + V.CustomizedUV7 = float2(0, 0); return V; } diff --git a/Shaders/Private/ShaderLabDSL.ush b/Shaders/Private/ShaderLabDSL.ush index 4bb0c3f..0a0d2eb 100644 --- a/Shaders/Private/ShaderLabDSL.ush +++ b/Shaders/Private/ShaderLabDSL.ush @@ -88,13 +88,17 @@ #define SL_LIGHTFUNCTION(...) // void Name(inout FShaderLabLightFunction L) — Domain = LightFunction // Multi-slab building blocks. -#define SL_SLAB(...) // precedes `void Name([FMaterialPixelParameters Parameters,] inout FShaderLabSurface S) { ... }` +#define SL_SLAB(...) // precedes `void Name([FMaterialPixelParameters Parameters,] inout FShaderLabSlab S) { ... }` #define SL_VALUE(...) // precedes `float Name([FMaterialPixelParameters Parameters]) { return ; }` #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 + +// Material-level (whole-material) pixel-stage outputs for a multi-slab shader. Precedes a +// `void Name([FMaterialPixelParameters Parameters,] inout FShaderLabMaterialOutput O) { ... }`. Writing the +// fields it needs (O.Opacity / O.OpacityMask / O.Refraction / O.PixelDepthOffset / O.AmbientOcclusion / +// O.SurfaceThickness) is the multi-slab counterpart of setting S. in a single SL_SURFACE: one Custom +// node feeds all of them (shared computation). At most one per shader; not allowed together with SL_SURFACE +// (a single Surface already carries these fields). +#define SL_MATERIAL(...) // precedes `void Name([FMaterialPixelParameters Parameters,] inout FShaderLabMaterialOutput O) { ... }` // Vertex Interpolator: precedes `floatN Name([FMaterialVertexParameters Parameters]) { return ; }` // — a value computed per-vertex and interpolated to the pixel shader (backed by a UMaterialExpressionVertexInterpolator). diff --git a/Source/UShaderLab/Private/ShaderLabParser.cpp b/Source/UShaderLab/Private/ShaderLabParser.cpp index de31b53..181a6e3 100644 --- a/Source/UShaderLab/Private/ShaderLabParser.cpp +++ b/Source/UShaderLab/Private/ShaderLabParser.cpp @@ -1878,21 +1878,52 @@ namespace S.Expect(TEXT(')'), TEXT("to close SL_FRONTMATERIAL")); } - /** SL_OPACITY(Name) / SL_OPACITY_MASK(Name): material-level outputs referencing a Value block. */ - void ParseMaterialOutput(FScanner& S, FName& OutValueName, const TCHAR* What) + /** + * SL_MATERIAL() void ([FMaterialPixelParameters Parameters,] inout FShaderLabMaterialOutput O){...}: + * the whole-material pixel-stage output block for a multi-slab shader. Parsed like a pixel entry; the fields + * O. the body writes feed the main-material-node pins from one Custom node. Contract: at most one, and + * not allowed together with a single SL_SURFACE (enforced here + at end-of-parse). + */ + void ParseMaterialBlock(FScanner& S, FShaderLabModel& Model) { - FString Inner; - if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner)) + FString Ignored; + if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_MATERIAL() { return; } - const FString Name = Inner.TrimStartAndEnd(); - if (Name.IsEmpty()) + S.SkipTrivia(); + const int32 DeclLine = S.Line, DeclCol = S.Column; + FString Name, SigInner, Body; + int32 BodyLine = 0; + if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine)) { - S.Error(FString::Printf(TEXT("%s expects a Value block name"), What)); return; } - OutValueName = FName(*Name); + TArray Params; + if (!ParseEntryParams(SigInner, Params) || Params.Num() < 1) + { + S.Error(TEXT("SL_MATERIAL must take an (inout FShaderLabMaterialOutput) parameter"), DeclLine, DeclCol); + return; + } + if (!ValidateBodyParameters(S, Params, Body, TEXT("FMaterialPixelParameters"), /*bRequireStruct*/ true, TEXT("SL_MATERIAL"), DeclLine, DeclCol)) + { + return; + } + if (Params.Last().Type != TEXT("FShaderLabMaterialOutput")) + { + S.Error(FString::Printf(TEXT("SL_MATERIAL must take an (inout FShaderLabMaterialOutput), got '%s'"), *Params.Last().Type), DeclLine, DeclCol); + return; + } + if (Model.bHasMaterialOutput) + { + S.Error(TEXT("A shader may declare only one SL_MATERIAL block"), DeclLine, DeclCol); + return; + } + Model.bHasMaterialOutput = true; + Model.MaterialOutputParams = MoveTemp(Params); + Model.MaterialOutputParamName = Model.MaterialOutputParams.Last().Name; + Model.MaterialOutputBody = MoveTemp(Body); + Model.MaterialOutputBodyLine = BodyLine; } } @@ -2042,7 +2073,7 @@ bool FShaderLabParser::Parse( else if (Token == TEXT("SL_SLAB")) { if (RejectInLibrary(TEXT("SL_SLAB"))) { return false; } - ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Slab, TEXT("SL_SLAB"), TEXT("FShaderLabSurface")); + ParseBsdfBlock(S, OutModel, EShaderLabBsdfType::Slab, TEXT("SL_SLAB"), TEXT("FShaderLabSlab")); } else if (Token == TEXT("SL_UNLIT")) { @@ -2099,25 +2130,10 @@ bool FShaderLabParser::Parse( if (RejectInLibrary(TEXT("SL_FRONTMATERIAL"))) { return false; } ParseFrontMaterial(S, OutModel); } - else if (Token == TEXT("SL_OPACITY")) + else if (Token == TEXT("SL_MATERIAL")) { - if (RejectInLibrary(TEXT("SL_OPACITY"))) { return false; } - ParseMaterialOutput(S, OutModel.OpacityValueName, TEXT("SL_OPACITY")); - } - else if (Token == TEXT("SL_OPACITY_MASK")) - { - 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")); + if (RejectInLibrary(TEXT("SL_MATERIAL"))) { return false; } + ParseMaterialBlock(S, OutModel); } else { @@ -2174,13 +2190,12 @@ bool FShaderLabParser::Parse( 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())) + // The SL_MATERIAL block is the multi-Slab counterpart of the single-Surface material-level fields. A single + // SL_SURFACE already carries O.* as S.* on its own struct, so having both is redundant and ambiguous — reject + // with guidance to use the S.* fields instead. + if (OutModel.bHasSurface && OutModel.bHasMaterialOutput) { - 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.")); + S.Error(TEXT("SL_MATERIAL is for multi-Slab shaders (with SL_FRONTMATERIAL). In a single SL_SURFACE, set S.Opacity / S.OpacityMask / S.Refraction / S.PixelDepthOffset / S.AmbientOcclusion / S.SurfaceThickness instead.")); return false; } if (bHasMultiSlab) diff --git a/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp b/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp index 8b0b892..5842c8b 100644 --- a/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp +++ b/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp @@ -26,6 +26,7 @@ namespace ShaderLabSettings_Private FName(TEXT("bIsThinSurface")), FName(TEXT("DitheredLODTransition")), FName(TEXT("RefractionMethod")), + FName(TEXT("NumCustomizedUVs")), // caps how many CustomizedUV vertex outputs the material compiles }; return Names; } diff --git a/Source/UShaderLab/Public/ShaderLabModel.h b/Source/UShaderLab/Public/ShaderLabModel.h index 6a4b2a6..0a52722 100644 --- a/Source/UShaderLab/Public/ShaderLabModel.h +++ b/Source/UShaderLab/Public/ShaderLabModel.h @@ -412,11 +412,19 @@ struct USHADERLAB_API FShaderLabModel TArray Values; TArray Topology; int32 TopologyRoot = INDEX_NONE; - /** Optional material-level outputs (multi-slab): names of Value blocks feeding Opacity/OpacityMask/Refraction/PDO. */ - FName OpacityValueName; - FName OpacityMaskValueName; - FName RefractionValueName; - FName PixelDepthOffsetValueName; + + /** + * Optional material-level output block (multi-slab): `SL_MATERIAL() void (inout FShaderLabMaterialOutput O){...}`. + * The whole-material pixel-stage outputs (Opacity/OpacityMask/Refraction/PixelDepthOffset/AmbientOcclusion/ + * SurfaceThickness) written here feed the main-material-node pins from a single Custom node (shared computation). + * Mutually exclusive with the single-entry Surface (which carries these fields inline); at most one per shader. + */ + bool bHasMaterialOutput = false; + TArray MaterialOutputParams; + FString MaterialOutputBody; + int32 MaterialOutputBodyLine = 0; + /** Name of the inout FShaderLabMaterialOutput parameter (e.g. "O"). */ + FString MaterialOutputParamName; // Runtime Virtual Texture write (optional): a `SL_RVTOUTPUT() void (inout FShaderLabRVTOutput O){...}` // block that fills the channels written into an RVT. Additive alongside the pixel entry (the material still diff --git a/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp index a7b84c0..23a8f21 100644 --- a/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp @@ -119,6 +119,10 @@ namespace ShaderLabGraph { TEXT("CustomizedUV1"), CMOT_Float2 }, { TEXT("CustomizedUV2"), CMOT_Float2 }, { TEXT("CustomizedUV3"), CMOT_Float2 }, + { TEXT("CustomizedUV4"), CMOT_Float2 }, + { TEXT("CustomizedUV5"), CMOT_Float2 }, + { TEXT("CustomizedUV6"), CMOT_Float2 }, + { TEXT("CustomizedUV7"), CMOT_Float2 }, }; /** Absolute, forward-slashed path for use inside an HLSL `#line N "path"` directive. */ @@ -535,7 +539,7 @@ namespace ShaderLabGraph } static const FBsdfDesc GBsdfDescs[] = { - { EShaderLabBsdfType::Slab, TEXT("ShaderLab Surface"), TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"), + { EShaderLabBsdfType::Slab, TEXT("ShaderLab Surface"), TEXT("FShaderLabSlab"), TEXT("ShaderLabDefaultSlab"), GSlabFields, UE_ARRAY_COUNT(GSlabFields), &MakeSlabNode, &GetSlabPinGeneric }, { EShaderLabBsdfType::Unlit, TEXT("ShaderLab Unlit"), TEXT("FShaderLabUnlit"), TEXT("ShaderLabDefaultUnlit"), GUnlitFields, UE_ARRAY_COUNT(GUnlitFields), &MakeUnlitNode, &GetUnlitPin }, @@ -1827,11 +1831,131 @@ namespace ShaderLabGraph return true; } + // --- Material-level (whole-material) pixel-stage outputs (FShaderLabMaterialOutput / the S.* material fields + // of FShaderLabSurface). Shared by the single-entry Surface path and the multi-slab SL_MATERIAL block. Each + // field maps to a main-material-node pin, EXCEPT Opacity which is coverage: under Substrate MP_Opacity is only + // active for AlphaComposite (Material.cpp IsPropertyActive), so translucent/Decal coverage is applied by + // wrapping the FrontMaterial root in a SubstrateWeight node (the correct Substrate coverage mechanism). --- + struct FMatOutFieldDef { const TCHAR* Field; ECustomMaterialOutputType OutType; }; + static const FMatOutFieldDef GMatOutFields[] = { + { TEXT("Opacity"), CMOT_Float1 }, + { TEXT("OpacityMask"), CMOT_Float1 }, + { TEXT("Refraction"), CMOT_Float1 }, + { TEXT("PixelDepthOffset"), CMOT_Float1 }, + { TEXT("AmbientOcclusion"), CMOT_Float1 }, + { TEXT("SurfaceThickness"), CMOT_Float1 }, + }; + + /** Non-Opacity material-output field -> its main-node pin. Opacity returns nullptr (routed via Weight). */ + static FExpressionInput* GetMaterialOutputPin(UMaterialEditorOnlyData& E, const FString& F) + { + if (F == TEXT("OpacityMask")) return &E.OpacityMask; + if (F == TEXT("Refraction")) return &E.Refraction; + if (F == TEXT("PixelDepthOffset")) return &E.PixelDepthOffset; + if (F == TEXT("AmbientOcclusion")) return &E.AmbientOcclusion; + if (F == TEXT("SurfaceThickness")) return &E.SurfaceThickness; + return nullptr; // Opacity + } + + static bool RawSettingIsTrue(const FShaderLabModel& M, const TCHAR* Key) + { + for (const TPair& P : M.RawSettings) + { + if (P.Key == Key) { const FString V = P.Value.TrimStartAndEnd(); return V == TEXT("true") || V == TEXT("1"); } + } + return false; + } + + /** + * Contract: is a material-level output active for this material's Domain/BlendMode? Mirrors the engine's + * Substrate IsPropertyActive (Material.cpp). Writing an inactive output would be silently ignored, so we + * reject it with a .usl-line error instead. Validated for Surface/Decal domains (the only ones carrying + * these fields). Returns true when active. + */ + static bool ValidateMaterialOutputActive(const FShaderLabModel& M, const FString& Field, int32 Line, TArray& OutErrors) + { + const EShaderLabDomain Domain = M.Settings.Domain; + const EShaderLabBlendMode Blend = M.Settings.BlendMode; + const bool bTranslucentFamily = Blend != EShaderLabBlendMode::Opaque && Blend != EShaderLabBlendMode::Masked; + auto Fail = [&](const FString& Why) { OutErrors.Add(FString::Printf(TEXT("%s(%d): %s"), *M.SourceFilePath, Line, *Why)); return false; }; + + if (Field == TEXT("Opacity")) + { + // Coverage: meaningful for translucent-family blends (via Weight) or Decal domain. AlphaComposite uses MP_Opacity. + if (Domain != EShaderLabDomain::Decal && !bTranslucentFamily) + { + return Fail(TEXT("O.Opacity (coverage) requires a translucent BlendMode (Translucent/Additive/Modulate/AlphaComposite/AlphaHoldout) or Domain = Decal")); + } + } + else if (Field == TEXT("OpacityMask")) + { + if (Blend != EShaderLabBlendMode::Masked) + { + return Fail(TEXT("O.OpacityMask requires BlendMode = Masked")); + } + } + else if (Field == TEXT("Refraction")) + { + if (!bTranslucentFamily || Blend == EShaderLabBlendMode::Modulate || Blend == EShaderLabBlendMode::AlphaHoldout) + { + return Fail(TEXT("O.Refraction requires a translucent BlendMode (Translucent/Additive/AlphaComposite) with RefractionMethod = RM_IndexOfRefraction")); + } + } + else if (Field == TEXT("SurfaceThickness")) + { + if (!RawSettingIsTrue(M, TEXT("bIsThinSurface"))) + { + return Fail(TEXT("O.SurfaceThickness requires SL_SETTINGS(bIsThinSurface = true)")); + } + } + // PixelDepthOffset / AmbientOcclusion: active broadly (Lit / depth) — accepted as-is. + return true; + } + + /** + * Wire material-level outputs (already emitted as SLO_ additional outputs of `Custom`) into the graph: + * each non-Opacity field -> its main-node pin; Opacity -> either MP_Opacity (AlphaComposite) or a SubstrateWeight + * wrapping the FrontMaterial root (translucent/Decal coverage). Validates each field against the blend/domain. + * `RootBsdf` is updated in place when wrapped. Returns false on a contract violation. + */ + static bool WireMaterialOutputs( + UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FShaderLabModel& Model, + UMaterialExpressionCustom* Custom, const TArray>& WrittenOutputs, int32 BodyLine, + UMaterialExpression*& RootBsdf, int32& IoY, TArray& OutErrors) + { + for (const TPair& Out : WrittenOutputs) + { + if (!ValidateMaterialOutputActive(Model, Out.Key, BodyLine, OutErrors)) { return false; } + + if (Out.Key == TEXT("Opacity")) + { + if (Model.Settings.BlendMode == EShaderLabBlendMode::AlphaComposite) + { + EditorOnly.Opacity.Connect(Out.Value, Custom); // engine uses MP_Opacity as the alpha-composite alpha override + } + else + { + // Coverage via a Substrate Weight over the whole material (MP_Opacity is inactive here under Substrate). + UMaterialExpressionSubstrateWeight* W = NewExpr(Material, IoY, -150); + W->A.Connect(0, RootBsdf); + W->Weight.Connect(Out.Value, Custom); + RootBsdf = W; + } + } + else if (FExpressionInput* Pin = GetMaterialOutputPin(EditorOnly, Out.Key)) + { + Pin->Connect(Out.Value, Custom); + } + } + 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. + * it into a fresh Substrate BSDF node (chosen by Desc). Returns the FrontMaterial root (nullptr only on error). + * When bAllowMaterialOutputs (single-entry SL_SURFACE sugar), the FShaderLabSurface material-level fields are + * also emitted and wired (StructName/DefaultFn then name the full-surface struct). Generic across all Substrate + * BSDFs via the FBsdfDesc field->pin table. */ static UMaterialExpression* BuildBsdf( const FBsdfDesc& Desc, const FShaderLabBsdfModifiers& Modifiers, @@ -1841,8 +1965,12 @@ namespace ShaderLabGraph const TMap& PropertyNodes, const TMap& InterpByName, TSet& UsedInterps, const FSampleNodes& Samples, - bool bAllowMaterialOutputs, int32& IoY, TArray& OutErrors) + bool bAllowMaterialOutputs, const TCHAR* StructNameOverride, const TCHAR* DefaultFnOverride, + int32& IoY, TArray& OutErrors) { + const TCHAR* StructName = StructNameOverride ? StructNameOverride : Desc.StructName; + const TCHAR* DefaultFn = DefaultFnOverride ? DefaultFnOverride : Desc.DefaultFn; + UMaterialExpression* Bsdf = Desc.MakeNode(Material, IoY); if (!ApplyBsdfModifiers(Bsdf, Modifiers, OutErrors)) { @@ -1867,12 +1995,20 @@ namespace ShaderLabGraph UsedSlab.Add(&F); } } - 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")); + // Single-entry SL_SURFACE also carries the material-level (whole-material) fields inline. + TArray UsedMatOut; + if (bAllowMaterialOutputs) + { + for (const FMatOutFieldDef& F : GMatOutFields) + { + if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field)) + { + UsedMatOut.Add(&F); + } + } + } - if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask && !bUsesRefraction && !bUsesPDO) + if (UsedSlab.Num() == 0 && UsedMatOut.Num() == 0) { return Bsdf; // Empty body: a default Substrate BSDF. } @@ -1900,7 +2036,7 @@ namespace ShaderLabGraph const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath); FString Code = FString::Printf(TEXT("%s %s = %s();\n{\n%s}\n"), - Desc.StructName, *OutParamName, Desc.DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath)); + StructName, *OutParamName, DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath)); int32 OutputIndex = 1; // index 0 is the (unused) main return TArray> SlabOutputs; @@ -1914,37 +2050,16 @@ namespace ShaderLabGraph SlabOutputs.Add(TPair(F, OutputIndex)); ++OutputIndex; } - int32 OpacityOutIdx = INDEX_NONE; - int32 OpacityMaskOutIdx = INDEX_NONE; - if (bUsesOpacity) + TArray> MatOutputs; + for (const FMatOutFieldDef* F : UsedMatOut) { - FCustomOutput Out; Out.OutputName = TEXT("SLO_Opacity"); Out.OutputType = CMOT_Float1; + FCustomOutput Out; + Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field)); + Out.OutputType = F->OutType; Custom->AdditionalOutputs.Add(Out); - Code += FString::Printf(TEXT("SLO_Opacity = %s.Opacity;\n"), *OutParamName); - OpacityOutIdx = OutputIndex++; - } - if (bUsesOpacityMask) - { - FCustomOutput Out; Out.OutputName = TEXT("SLO_OpacityMask"); Out.OutputType = CMOT_Float1; - Custom->AdditionalOutputs.Add(Out); - 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 += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *OutParamName, F->Field); + MatOutputs.Add(TPair(F->Field, OutputIndex)); + ++OutputIndex; } Code += TEXT("return 0.0f;\n"); Custom->Code = Code; @@ -1957,10 +2072,12 @@ namespace ShaderLabGraph Pin->Connect(Pair.Value, Custom); } } - if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); } - if (OpacityMaskOutIdx != INDEX_NONE) { EditorOnly.OpacityMask.Connect(OpacityMaskOutIdx, Custom); } - if (RefractionOutIdx != INDEX_NONE) { EditorOnly.Refraction.Connect(RefractionOutIdx, Custom); } - if (PDOOutIdx != INDEX_NONE) { EditorOnly.PixelDepthOffset.Connect(PDOOutIdx, Custom); } + // Material-level outputs (single-entry sugar): wire to main-node pins / coverage-Weight. Root may be wrapped. + UMaterialExpression* Root = Bsdf; + if (!WireMaterialOutputs(Material, EditorOnly, Model, Custom, MatOutputs, BodyLine, Root, IoY, OutErrors)) + { + return nullptr; + } // Wire the water volume fields into the companion output node (by matching Custom output field name). if (WaterOut) @@ -1977,7 +2094,7 @@ namespace ShaderLabGraph ConnectWaterOut(WaterOut->PhaseG, TEXT("WaterPhaseG")); ConnectWaterOut(WaterOut->ColorScaleBehindWater, TEXT("ColorScaleBehindWater")); } - return Bsdf; + return Root; } /** @@ -2128,6 +2245,79 @@ namespace ShaderLabGraph return true; } + /** + * Build the multi-slab material-level output block (SL_MATERIAL). Mirrors BuildBsdf's material-output half: + * one Custom node runs the body filling an FShaderLabMaterialOutput; the written fields become AdditionalOutputs + * wired to main-node pins (or, for Opacity, a SubstrateWeight over the FrontMaterial root). `RootBsdf` is + * updated in place when the coverage-Weight wraps it. Empty body = nothing to wire. + */ + static bool BuildMaterialOutputBlock( + UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FShaderLabModel& Model, + const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, + const TMap& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, UMaterialExpression*& RootBsdf, int32& IoY, TArray& OutErrors) + { + const FString& OutParamName = Model.MaterialOutputParamName; + const FString& InBody = Model.MaterialOutputBody; + + TArray Used; + for (const FMatOutFieldDef& F : GMatOutFields) + { + if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field)) + { + Used.Add(&F); + } + } + if (Used.Num() == 0) + { + return true; // Empty SL_MATERIAL body: leave all main-node pins at their defaults. + } + + UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); + Custom->Description = TEXT("ShaderLab Material Output"); + Custom->OutputType = CMOT_Float1; + AddIncludes(*Custom, Model); + + FString Body = InBody; + TArray Wires; + TSet ReqProps; + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Model.MaterialOutputBodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors)) + { + return false; + } + for (const FIntrinsicWire& Wire : Wires) + { + FCustomInput In; + In.InputName = Wire.InputName; + In.Input.Connect(Wire.OutputIndex, Wire.Expr); + Custom->Inputs.Add(In); + } + WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections); + + const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath); + FString Code = FString::Printf(TEXT("FShaderLabMaterialOutput %s = ShaderLabDefaultMaterialOutput();\n{\n%s}\n"), + *OutParamName, *WrapBodyWithLineMapping(Body, Model.MaterialOutputBodyLine, SrcPath)); + + int32 OutputIndex = 1; + TArray> MatOutputs; + for (const FMatOutFieldDef* F : Used) + { + FCustomOutput Out; + Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field)); + Out.OutputType = F->OutType; + Custom->AdditionalOutputs.Add(Out); + Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *OutParamName, F->Field); + MatOutputs.Add(TPair(F->Field, OutputIndex)); + ++OutputIndex; + } + Code += TEXT("return 0.0f;\n"); + Custom->Code = Code; + Custom->RebuildOutputs(); + + return WireMaterialOutputs(Material, EditorOnly, Model, Custom, MatOutputs, Model.MaterialOutputBodyLine, RootBsdf, IoY, OutErrors); + } + /** Build a Custom node whose return value is the scalar Value-block body. Output 0 is the scalar. */ static UMaterialExpressionCustom* BuildValueNode( UMaterial& Material, const FShaderLabValue& Value, const FShaderLabModel& Model, @@ -2431,6 +2621,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode for (const FShaderLabValue& Value : Model.Values) { Collect(Value.Body); } for (const FShaderLabInterpolator& Interp : Model.Interpolators) { Collect(Interp.Body); } if (Model.bHasVertex) { Collect(Model.VertexBody); } + if (Model.bHasMaterialOutput) { Collect(Model.MaterialOutputBody); } + if (Model.bHasRVTOutput) { Collect(Model.RVTOutputBody); } for (const FName& Fn : DirectlyCalled) { if (!ComputeFunctionCtx(Fn, Emit, OutErrors)) @@ -2477,6 +2669,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, NameStr)) { return true; } } for (const FShaderLabInterpolator& Interp : Model.Interpolators) { if (ReferencesToken(Interp.Body, NameStr)) { return true; } } if (Model.bHasVertex && ReferencesToken(Model.VertexBody, NameStr)) { return true; } + if (Model.bHasMaterialOutput && ReferencesToken(Model.MaterialOutputBody, NameStr)) { return true; } + if (Model.bHasRVTOutput && ReferencesToken(Model.RVTOutputBody, NameStr)) { return true; } for (const FShaderLabTopoNode& Node : Model.Topology) { if (Node.bHasFactor && Node.Factor.Kind == FShaderLabFactor::EKind::Named && Node.Factor.Name == PropName) @@ -2732,6 +2926,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode 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 (Model.bHasMaterialOutput) { bRef |= ReferencesToken(Model.MaterialOutputBody, NameStr); } + if (Model.bHasRVTOutput) { bRef |= ReferencesToken(Model.RVTOutputBody, NameStr); } if (!bRef) { continue; // Declared but unreferenced: skip (keeps the graph minimal/deterministic). @@ -2785,6 +2981,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, NameStr)) { return true; } } for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, NameStr)) { return true; } } if (Model.bHasRVTOutput && ReferencesToken(Model.RVTOutputBody, NameStr)) { return true; } + if (Model.bHasMaterialOutput && ReferencesToken(Model.MaterialOutputBody, NameStr)) { return true; } return false; }; @@ -2904,12 +3101,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode } else if (Model.bHasSurface) { - // Single-Surface sugar: one slab straight to FrontMaterial, with S.Opacity/S.OpacityMask - // allowed as material-level outputs. + // Single-Surface sugar: one slab straight to FrontMaterial, with the FShaderLabSurface material-level + // fields (S.Opacity/OpacityMask/Refraction/PixelDepthOffset/AmbientOcclusion/SurfaceThickness) allowed + // inline. The full-surface struct name overrides the Slab desc's (which names the bare FShaderLabSlab). UMaterialExpression* Slab = BuildBsdf( *FindBsdfDesc(EShaderLabBsdfType::Slab), Model.SurfaceModifiers, Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, - Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors); + Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, + /*bAllowMaterialOutputs*/ true, TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"), ParamY, OutErrors); if (!Slab) { return false; @@ -2938,7 +3137,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode UMaterialExpression* Slab = BuildBsdf( *Desc, SlabDecl.Modifiers, Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine, - Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors); + Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, + /*bAllowMaterialOutputs*/ false, /*StructNameOverride*/ nullptr, /*DefaultFnOverride*/ nullptr, ParamY, OutErrors); if (!Slab) { return false; @@ -2991,25 +3191,19 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode { return false; } - EditorOnly->FrontMaterial.Connect(0, WrapBsdfForDecal(Material, Model, Root, ParamY)); - // Material-level Opacity / OpacityMask from named Value blocks. - auto ConnectMaterialOutput = [&](FExpressionInput& Pin, FName ValueName, const TCHAR* What) -> bool + // Material-level outputs (whole-material) come from an optional SL_MATERIAL block: one Custom node feeds + // the main-node pins (Opacity routed as a coverage-Weight over the root). Wire before FrontMaterial so a + // coverage-Weight can wrap the root. + if (Model.bHasMaterialOutput) { - if (ValueName.IsNone()) { return true; } - UMaterialExpressionCustom* const* ValueNode = ValueByName.Find(ValueName); - if (!ValueNode) + if (!BuildMaterialOutputBlock(Material, *EditorOnly, Model, Program, Emit, PropertyNodes, + InterpByName, UsedInterps, Samples, Root, ParamY, OutErrors)) { - OutErrors.Add(FString::Printf(TEXT("%s references unknown Value '%s'"), What, *ValueName.ToString())); return false; } - Pin.Connect(0, *ValueNode); - return true; - }; - 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; } + } + EditorOnly->FrontMaterial.Connect(0, WrapBsdfForDecal(Material, Model, Root, ParamY)); } // 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry @@ -3027,6 +3221,31 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode } } + // Contract: a written V.CustomizedUV requires SL_SETTINGS(NumCustomizedUVs = N) with N > i — the + // engine only allocates/compiles the first NumCustomizedUVs slots (UMaterial::NumCustomizedUVs, default 0) + // and silently passes the raw vertex texcoord through for the rest. Fail loudly instead of that silent no-op. + { + int32 NumCustomizedUVs = 0; + for (const TPair& P : Model.RawSettings) + { + if (P.Key == TEXT("NumCustomizedUVs")) { NumCustomizedUVs = FCString::Atoi(*P.Value.TrimStartAndEnd()); } + } + for (const FVertexFieldDef* F : UsedVtx) + { + const FString Field(F->Field); + if (Field.StartsWith(TEXT("CustomizedUV"))) + { + const int32 UvIndex = FCString::Atoi(*Field.Mid(12)); + if (UvIndex >= NumCustomizedUVs) + { + OutErrors.Add(FString::Printf(TEXT("%s(%d): V.CustomizedUV%d requires SL_SETTINGS(NumCustomizedUVs = %d) or higher (currently %d)"), + *Model.SourceFilePath, Model.VertexBodyLine, UvIndex, UvIndex + 1, NumCustomizedUVs)); + return false; + } + } + } + } + if (UsedVtx.Num() > 0) { UMaterialExpressionCustom* VCustom = NewExpr(Material, ParamY, -300); @@ -3136,6 +3355,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode 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; } + if (Model.bHasMaterialOutput && ReferencesToken(Model.MaterialOutputBody, Token)) { return true; } + if (Model.bHasRVTOutput && ReferencesToken(Model.RVTOutputBody, Token)) { return true; } return false; }; @@ -3186,6 +3407,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode for (const FShaderLabValue& Value : Model.Values) { Bodies.Add({ &Value.Body, Value.BodyLine }); } for (const FShaderLabInterpolator& Interp : Model.Interpolators) { Bodies.Add({ &Interp.Body, Interp.BodyLine }); } if (Model.bHasVertex) { Bodies.Add({ &Model.VertexBody, Model.VertexBodyLine }); } + if (Model.bHasMaterialOutput) { Bodies.Add({ &Model.MaterialOutputBody, Model.MaterialOutputBodyLine }); } + if (Model.bHasRVTOutput) { Bodies.Add({ &Model.RVTOutputBody, Model.RVTOutputBodyLine }); } if (Emit.HasLibraries()) { for (const TPair& Pair : Emit.Ctx)