Merge output data for reduce duplicate calculate

This commit is contained in:
Eragon-Brisingr
2026-07-05 13:23:08 +08:00
parent a6fbdf10f8
commit 59af77e8f6
8 changed files with 515 additions and 138 deletions

View File

@@ -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<i>` **必须**配 `SL_SETTINGS(NumCustomizedUVs = N)``N > i`——引擎只分配前 `NumCustomizedUVs` 组(默认 0其余槽位直接透传原始顶点 texcoordShaderLab 会把「写了 UV<i> 但 N ≤ i」变成建图错误而非静默失效。示例见 `Shaders/Examples/CustomizedUV.usl`
`SL_INTERPOLATOR` 用于在顶点阶段计算一个值,并在像素阶段读取。
```hlsl

View File

@@ -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<i>` **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

View File

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

View File

@@ -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 <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
// 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.<field> 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 <vertex HLSL>; }`
// — a value computed per-vertex and interpolated to the pixel shader (backed by a UMaterialExpressionVertexInterpolator).

View File

@@ -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 <Name>([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.<name> 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<FShaderLabEntryParam> 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)

View File

@@ -26,6 +26,7 @@ namespace ShaderLabSettings_Private
FName(TEXT("bIsThinSurface")),
FName(TEXT("DitheredLODTransition")),
FName(TEXT("RefractionMethod")),
FName(TEXT("NumCustomizedUVs")), // caps how many CustomizedUV<i> vertex outputs the material compiles
};
return Names;
}

View File

@@ -412,11 +412,19 @@ struct USHADERLAB_API FShaderLabModel
TArray<FShaderLabValue> Values;
TArray<FShaderLabTopoNode> 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 <Name>(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<FShaderLabEntryParam> 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 <Name>(inout FShaderLabRVTOutput O){...}`
// block that fills the channels written into an RVT. Additive alongside the pixel entry (the material still

View File

@@ -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<FString, FString>& 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<FString>& 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_<Field> 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<TPair<FString, int32>>& WrittenOutputs, int32 BodyLine,
UMaterialExpression*& RootBsdf, int32& IoY, TArray<FString>& OutErrors)
{
for (const TPair<FString, int32>& 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<UMaterialExpressionSubstrateWeight>(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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
bool bAllowMaterialOutputs, const TCHAR* StructNameOverride, const TCHAR* DefaultFnOverride,
int32& IoY, TArray<FString>& 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<const FMatOutFieldDef*> 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<TPair<const FSlabFieldDef*, int32>> SlabOutputs;
@@ -1914,37 +2050,16 @@ namespace ShaderLabGraph
SlabOutputs.Add(TPair<const FSlabFieldDef*, int32>(F, OutputIndex));
++OutputIndex;
}
int32 OpacityOutIdx = INDEX_NONE;
int32 OpacityMaskOutIdx = INDEX_NONE;
if (bUsesOpacity)
TArray<TPair<FString, int32>> 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<FString, int32>(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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples, UMaterialExpression*& RootBsdf, int32& IoY, TArray<FString>& OutErrors)
{
const FString& OutParamName = Model.MaterialOutputParamName;
const FString& InBody = Model.MaterialOutputBody;
TArray<const FMatOutFieldDef*> 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<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab Material Output");
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
TSet<FName> 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<TPair<FString, int32>> 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<FString, int32>(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<i> 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<FString, FString>& 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<UMaterialExpressionCustom>(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<FName, FFunctionCtx>& Pair : Emit.Ctx)