mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
Merge output data for reduce duplicate calculate
This commit is contained in:
@@ -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),其余槽位直接透传原始顶点 texcoord;ShaderLab 会把「写了 UV<i> 但 N ≤ i」变成建图错误而非静默失效。示例见 `Shaders/Examples/CustomizedUV.usl`。
|
||||
|
||||
`SL_INTERPOLATOR` 用于在顶点阶段计算一个值,并在像素阶段读取。
|
||||
|
||||
```hlsl
|
||||
|
||||
47
README.md
47
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<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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user