diff --git a/Shaders/Private/ShaderLabCommon.ush b/Shaders/Private/ShaderLabCommon.ush index abd96c8..6f07c55 100644 --- a/Shaders/Private/ShaderLabCommon.ush +++ b/Shaders/Private/ShaderLabCommon.ush @@ -41,89 +41,36 @@ #endif #endif -// 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; -}; +// --------------------------------------------------------------------------- +// SHADERLAB_SUBSTRATE — the project-level rendering scheme, derived from the engine's own SUBSTRATE_ENABLED +// shader define (set from r.Substrate at material-shader-environment build time, ShaderCompiler.cpp). This +// selects which surface-struct parameterization the SL_SURFACE body sees. It is a compile-time constant for +// the whole material (Substrate is project-wide, not per-material), so it needs no per-permutation leak. +// The C++ graph builder branches on the same source (Substrate::IsSubstrateEnabled()), keeping the two in sync. +// --------------------------------------------------------------------------- +#ifndef SHADERLAB_SUBSTRATE + #ifdef SHADERLAB_IDE + #define SHADERLAB_SUBSTRATE 1 // IDE default (Substrate authoring); override in shader-validator defines to check legacy. + #elif defined(SUBSTRATE_ENABLED) + #define SHADERLAB_SUBSTRATE SUBSTRATE_ENABLED + #else + #define SHADERLAB_SUBSTRATE 0 // Fallback if the engine define is somehow absent: bias to the legacy + // (deletable) path. In practice SUBSTRATE_ENABLED is always set for + // material shaders, so this branch is not expected to be taken. + #endif +#endif -// 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; -}; +// Mode-specific surface/BSDF structs live in split files so the legacy scheme is deletable wholesale. +#if SHADERLAB_SUBSTRATE + #include "/Plugin/ShaderLab/Private/Surface_Substrate.ush" +#else + #include "/Plugin/ShaderLab/Private/Surface_Legacy.ush" +#endif -// 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; - 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 outputs (single-Surface sugar). See FShaderLabMaterialOutput for pin semantics. - float Opacity; - float OpacityMask; - float Refraction; - float PixelDepthOffset; - float AmbientOcclusion; - float SurfaceThickness; -}; +// --- Mode-independent output structs (identical fields/semantics in both schemes). --- -// --- Additional Substrate BSDF output structs. Each mirrors the pins of its engine BSDF node; the graph -// builder wires only the fields a body writes, leaving the rest at the node's native default. --- - -// Unlit BSDF (SL_UNLIT): pure emissive / transmittance, no lighting. +// Unlit BSDF (SL_UNLIT): pure emissive / transmittance, no lighting. Under Substrate -> UnlitBSDF; +// under legacy -> MSM_Unlit (only EmissiveColor feeds MP_EmissiveColor). Struct is shared either way. struct FShaderLabUnlit { float3 EmissiveColor; @@ -131,89 +78,6 @@ struct FShaderLabUnlit float3 Normal; }; -// Hair BSDF (SL_HAIR): hair-fiber shading. -struct FShaderLabHair -{ - float3 BaseColor; - float Scatter; - float Specular; - float Roughness; - float3 Backlit; - float3 Tangent; - float3 EmissiveColor; -}; - -// Eye BSDF (SL_EYE): cornea / iris / sclera shading. -struct FShaderLabEye -{ - float3 DiffuseColor; - float Roughness; - float3 CorneaNormal; - float3 IrisNormal; - float3 IrisPlaneNormal; - float IrisMask; - float IrisDistance; - float3 EmissiveColor; -}; - -// Single Layer Water BSDF (SL_WATER): water surface + underwater scattering. -struct FShaderLabWater -{ - float3 BaseColor; - float Metallic; - float Specular; - float Roughness; - float3 Normal; - float3 EmissiveColor; - float3 TopMaterialOpacity; - float3 WaterAlbedo; - float3 WaterExtinction; - float WaterPhaseG; - float3 ColorScaleBehindWater; -}; - -// Volumetric-Fog-Cloud BSDF (SL_VOLUME, Domain = Volume): participating media. -struct FShaderLabVolume -{ - float3 Albedo; - float3 Extinction; - float3 EmissiveColor; - float AmbientOcclusion; -}; - -// Simple Clear Coat BSDF (SL_CLEARCOAT): bottom layer + clear coat top. -struct FShaderLabClearCoat -{ - float3 DiffuseAlbedo; - float3 F0; - float Roughness; - float ClearCoatCoverage; - float ClearCoatRoughness; - float3 Normal; - float3 EmissiveColor; - float3 BottomNormal; -}; - -// Toon BSDF (SL_TOON, experimental): stylized shading. -struct FShaderLabToon -{ - float3 BaseColor; - float Metallic; - float Specular; - float Roughness; - float3 Normal; - float3 EmissiveColor; - float2 PatternUVs; - float Anisotropy; - float3 Tangent; -}; - -// Light Function (SL_LIGHTFUNCTION, Domain = LightFunction): per-light modulation color. -struct FShaderLabLightFunction -{ - float3 Color; -}; - FShaderLabUnlit ShaderLabDefaultUnlit() { FShaderLabUnlit U; @@ -223,96 +87,6 @@ FShaderLabUnlit ShaderLabDefaultUnlit() return U; } -FShaderLabHair ShaderLabDefaultHair() -{ - FShaderLabHair H; - H.BaseColor = float3(0, 0, 0); - H.Scatter = 0; - H.Specular = 0.5; - H.Roughness = 0.5; - H.Backlit = float3(0, 0, 0); - H.Tangent = float3(1, 0, 0); - H.EmissiveColor = float3(0, 0, 0); - return H; -} - -FShaderLabEye ShaderLabDefaultEye() -{ - FShaderLabEye E; - E.DiffuseColor = float3(0, 0, 0); - E.Roughness = 0.5; - E.CorneaNormal = float3(0, 0, 1); - E.IrisNormal = float3(0, 0, 1); - E.IrisPlaneNormal = float3(0, 0, 1); - E.IrisMask = 0; - E.IrisDistance = 0; - E.EmissiveColor = float3(0, 0, 0); - return E; -} - -FShaderLabWater ShaderLabDefaultWater() -{ - FShaderLabWater W; - W.BaseColor = float3(0, 0, 0); - W.Metallic = 0; - W.Specular = 0.5; - W.Roughness = 0.5; - W.Normal = float3(0, 0, 1); - W.EmissiveColor = float3(0, 0, 0); - W.TopMaterialOpacity = float3(0, 0, 0); - W.WaterAlbedo = float3(0, 0, 0); - W.WaterExtinction = float3(0, 0, 0); - W.WaterPhaseG = 0; - W.ColorScaleBehindWater = float3(1, 1, 1); - return W; -} - -FShaderLabVolume ShaderLabDefaultVolume() -{ - FShaderLabVolume V; - V.Albedo = float3(0, 0, 0); - V.Extinction = float3(0, 0, 0); - V.EmissiveColor = float3(0, 0, 0); - V.AmbientOcclusion = 1; - return V; -} - -FShaderLabClearCoat ShaderLabDefaultClearCoat() -{ - FShaderLabClearCoat C; - C.DiffuseAlbedo = float3(0.18, 0.18, 0.18); - C.F0 = float3(0.04, 0.04, 0.04); - C.Roughness = 0.5; - C.ClearCoatCoverage = 0.5; - C.ClearCoatRoughness = 0.5; - C.Normal = float3(0, 0, 1); - C.EmissiveColor = float3(0, 0, 0); - C.BottomNormal = float3(0, 0, 1); - return C; -} - -FShaderLabToon ShaderLabDefaultToon() -{ - FShaderLabToon T; - T.BaseColor = float3(0.18, 0.18, 0.18); - T.Metallic = 0; - T.Specular = 0.5; - T.Roughness = 0.5; - T.Normal = float3(0, 0, 1); - T.EmissiveColor = float3(0, 0, 0); - T.PatternUVs = float2(0, 0); - T.Anisotropy = 0; - T.Tangent = float3(1, 0, 0); - return T; -} - -FShaderLabLightFunction ShaderLabDefaultLightFunction() -{ - FShaderLabLightFunction L; - L.Color = float3(1, 1, 1); - return L; -} - // Output for the PostProcess(...) entry (Domain = PostProcess). Color feeds the material EmissiveColor. struct FShaderLabPostProcess { @@ -345,72 +119,6 @@ struct FShaderLabVertex float2 CustomizedUV7; }; -FShaderLabSurface ShaderLabDefaultSurface() -{ - FShaderLabSurface 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); - S.Opacity = 1; - 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; diff --git a/Shaders/Private/Surface_Legacy.ush b/Shaders/Private/Surface_Legacy.ush new file mode 100644 index 0000000..f923ea3 --- /dev/null +++ b/Shaders/Private/Surface_Legacy.ush @@ -0,0 +1,55 @@ +// Copyright UShaderLab. All Rights Reserved. +// Legacy (non-Substrate) surface output struct. Included by ShaderLabCommon.ush only when +// SHADERLAB_SUBSTRATE == 0 (i.e. the project runs with r.Substrate=0). The Substrate counterpart is +// Surface_Substrate.ush. This whole file — plus ShaderLabSurfaceEmitter_Legacy.* and the legacy branch +// in ShaderLabGraphBuilder — is the deletable "old scheme" slice: once UE removes the non-Substrate +// path, delete these and the #else in ShaderLabCommon.ush. +// +// SL_SURFACE stays the same concept across both modes, but the shading parameterization differs: Substrate +// uses DiffuseAlbedo/F0/F90/... while legacy uses the classic Metallic/Roughness workflow (BaseColor/ +// Metallic/Specular). Common fields (Normal/Roughness/Anisotropy/Tangent/EmissiveColor and the material- +// level outputs) carry the same names in both, so a body that only touches those needs no `#if`. A body +// that touches the mode-specific shading fields guards them with `#if SHADERLAB_SUBSTRATE ... #else ...`. + +#pragma once + +// Single-entry surface, filled by the SL_SURFACE(...) body. Legacy = classic Metallic/Roughness workflow +// mapped to a MSM_DefaultLit material: shading fields feed MP_BaseColor/Metallic/Specular/Roughness/ +// Anisotropy/Normal/Tangent/EmissiveColor, and the material-level outputs feed the matching main pins. +// (Substrate-only fields — F0/F90/SSSMFP/Fuzz/Glint/SurfaceThickness — do not exist here.) +struct FShaderLabSurface +{ + float3 BaseColor; + float Metallic; + float Specular; + float Roughness; + float Anisotropy; + float3 Normal; + float3 Tangent; + float3 EmissiveColor; + // Material-level outputs. See the graph builder's activeness contract (Domain/BlendMode). + float Opacity; // translucency coverage -> MP_Opacity (legacy wires it directly) + float OpacityMask; // Masked blend cutout -> MP_OpacityMask + float Refraction; // scalar IOR -> MP_Refraction + float PixelDepthOffset; // world-unit depth push -> MP_PixelDepthOffset + float AmbientOcclusion; // -> MP_AmbientOcclusion +}; + +FShaderLabSurface ShaderLabDefaultSurface() +{ + FShaderLabSurface S; + S.BaseColor = float3(0.18, 0.18, 0.18); + S.Metallic = 0; + S.Specular = 0.5; + S.Roughness = 0.5; + S.Anisotropy = 0; + S.Normal = float3(0, 0, 1); + S.Tangent = float3(1, 0, 0); + S.EmissiveColor = float3(0, 0, 0); + S.Opacity = 1; + S.OpacityMask = 1; + S.Refraction = 1.0; + S.PixelDepthOffset = 0.0; + S.AmbientOcclusion = 1; + return S; +} diff --git a/Shaders/Private/Surface_Substrate.ush b/Shaders/Private/Surface_Substrate.ush new file mode 100644 index 0000000..895e71c --- /dev/null +++ b/Shaders/Private/Surface_Substrate.ush @@ -0,0 +1,328 @@ +// Copyright UShaderLab. All Rights Reserved. +// Substrate-mode surface/BSDF output structs. Included by ShaderLabCommon.ush only when +// SHADERLAB_SUBSTRATE != 0 (i.e. the project runs with r.Substrate=1). The legacy (non-Substrate) +// counterpart is Surface_Legacy.ush. Keeping the two schemes in separate files makes the legacy path +// deletable wholesale once UE drops the non-Substrate rendering path. + +#pragma once + +// 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; + 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 outputs (single-Surface sugar). See FShaderLabMaterialOutput for pin semantics. + float Opacity; + float OpacityMask; + float Refraction; + float PixelDepthOffset; + float AmbientOcclusion; + float SurfaceThickness; +}; + +// --- Additional Substrate BSDF output structs. Each mirrors the pins of its engine BSDF node; the graph +// builder wires only the fields a body writes, leaving the rest at the node's native default. --- + +// Hair BSDF (SL_HAIR): hair-fiber shading. +struct FShaderLabHair +{ + float3 BaseColor; + float Scatter; + float Specular; + float Roughness; + float3 Backlit; + float3 Tangent; + float3 EmissiveColor; +}; + +// Eye BSDF (SL_EYE): cornea / iris / sclera shading. +struct FShaderLabEye +{ + float3 DiffuseColor; + float Roughness; + float3 CorneaNormal; + float3 IrisNormal; + float3 IrisPlaneNormal; + float IrisMask; + float IrisDistance; + float3 EmissiveColor; +}; + +// Single Layer Water BSDF (SL_WATER): water surface + underwater scattering. +struct FShaderLabWater +{ + float3 BaseColor; + float Metallic; + float Specular; + float Roughness; + float3 Normal; + float3 EmissiveColor; + float3 TopMaterialOpacity; + float3 WaterAlbedo; + float3 WaterExtinction; + float WaterPhaseG; + float3 ColorScaleBehindWater; +}; + +// Volumetric-Fog-Cloud BSDF (SL_VOLUME, Domain = Volume): participating media. +struct FShaderLabVolume +{ + float3 Albedo; + float3 Extinction; + float3 EmissiveColor; + float AmbientOcclusion; +}; + +// Simple Clear Coat BSDF (SL_CLEARCOAT): bottom layer + clear coat top. +struct FShaderLabClearCoat +{ + float3 DiffuseAlbedo; + float3 F0; + float Roughness; + float ClearCoatCoverage; + float ClearCoatRoughness; + float3 Normal; + float3 EmissiveColor; + float3 BottomNormal; +}; + +// Toon BSDF (SL_TOON, experimental): stylized shading. +struct FShaderLabToon +{ + float3 BaseColor; + float Metallic; + float Specular; + float Roughness; + float3 Normal; + float3 EmissiveColor; + float2 PatternUVs; + float Anisotropy; + float3 Tangent; +}; + +// Light Function (SL_LIGHTFUNCTION, Domain = LightFunction): per-light modulation color. +struct FShaderLabLightFunction +{ + float3 Color; +}; + +FShaderLabHair ShaderLabDefaultHair() +{ + FShaderLabHair H; + H.BaseColor = float3(0, 0, 0); + H.Scatter = 0; + H.Specular = 0.5; + H.Roughness = 0.5; + H.Backlit = float3(0, 0, 0); + H.Tangent = float3(1, 0, 0); + H.EmissiveColor = float3(0, 0, 0); + return H; +} + +FShaderLabEye ShaderLabDefaultEye() +{ + FShaderLabEye E; + E.DiffuseColor = float3(0, 0, 0); + E.Roughness = 0.5; + E.CorneaNormal = float3(0, 0, 1); + E.IrisNormal = float3(0, 0, 1); + E.IrisPlaneNormal = float3(0, 0, 1); + E.IrisMask = 0; + E.IrisDistance = 0; + E.EmissiveColor = float3(0, 0, 0); + return E; +} + +FShaderLabWater ShaderLabDefaultWater() +{ + FShaderLabWater W; + W.BaseColor = float3(0, 0, 0); + W.Metallic = 0; + W.Specular = 0.5; + W.Roughness = 0.5; + W.Normal = float3(0, 0, 1); + W.EmissiveColor = float3(0, 0, 0); + W.TopMaterialOpacity = float3(0, 0, 0); + W.WaterAlbedo = float3(0, 0, 0); + W.WaterExtinction = float3(0, 0, 0); + W.WaterPhaseG = 0; + W.ColorScaleBehindWater = float3(1, 1, 1); + return W; +} + +FShaderLabVolume ShaderLabDefaultVolume() +{ + FShaderLabVolume V; + V.Albedo = float3(0, 0, 0); + V.Extinction = float3(0, 0, 0); + V.EmissiveColor = float3(0, 0, 0); + V.AmbientOcclusion = 1; + return V; +} + +FShaderLabClearCoat ShaderLabDefaultClearCoat() +{ + FShaderLabClearCoat C; + C.DiffuseAlbedo = float3(0.18, 0.18, 0.18); + C.F0 = float3(0.04, 0.04, 0.04); + C.Roughness = 0.5; + C.ClearCoatCoverage = 0.5; + C.ClearCoatRoughness = 0.5; + C.Normal = float3(0, 0, 1); + C.EmissiveColor = float3(0, 0, 0); + C.BottomNormal = float3(0, 0, 1); + return C; +} + +FShaderLabToon ShaderLabDefaultToon() +{ + FShaderLabToon T; + T.BaseColor = float3(0.18, 0.18, 0.18); + T.Metallic = 0; + T.Specular = 0.5; + T.Roughness = 0.5; + T.Normal = float3(0, 0, 1); + T.EmissiveColor = float3(0, 0, 0); + T.PatternUVs = float2(0, 0); + T.Anisotropy = 0; + T.Tangent = float3(1, 0, 0); + return T; +} + +FShaderLabLightFunction ShaderLabDefaultLightFunction() +{ + FShaderLabLightFunction L; + L.Color = float3(1, 1, 1); + return L; +} + +FShaderLabSurface ShaderLabDefaultSurface() +{ + FShaderLabSurface 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); + S.Opacity = 1; + 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; +} diff --git a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp index b5927dc..d33643d 100644 --- a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp +++ b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp @@ -8,6 +8,7 @@ #include "MaterialTypes.h" #include "Materials/Material.h" #include "Materials/MaterialInterface.h" +#include "RenderUtils.h" // Substrate::IsSubstrateEnabled() #include "ShaderLabModel.h" #include "ShaderLabSettingsApplier.h" @@ -44,6 +45,7 @@ namespace ShaderLabRuntime_Private static const FFieldToProperty GFieldToProperty[] = { { TEXT(".DiffuseAlbedo"), MP_DiffuseColor }, // Substrate Slab albedo/F0 parameterization { TEXT(".F0"), MP_SpecularColor }, + { TEXT(".BaseColor"), MP_BaseColor }, // legacy (non-Substrate) DefaultLit parameterization { TEXT(".Normal"), MP_Normal }, { TEXT(".Roughness"), MP_Roughness }, { TEXT(".Metallic"), MP_Metallic }, @@ -78,7 +80,8 @@ namespace ShaderLabRuntime_Private // mask for the Decal domain even if field detection missed everything (e.g. body writes via a helper). if (!bAny && Model.Settings.Domain == EShaderLabDomain::Decal) { - Data.SetPropertyConnected(MP_DiffuseColor); + // Base-color channel differs by scheme: Substrate uses MP_DiffuseColor, legacy MP_BaseColor. + Data.SetPropertyConnected(Substrate::IsSubstrateEnabled() ? MP_DiffuseColor : MP_BaseColor); Data.SetPropertyConnected(MP_Normal); } } @@ -112,6 +115,19 @@ void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderL Material.TwoSided = Model.Settings.bTwoSided ? 1 : 0; + // Legacy (non-Substrate): set the shell's shading model explicitly. A MIC with no ShadingModel override + // derives its ShadingModels from Parent->GetShadingModels() (UMaterialInstance::UpdateOverridableBaseProperties, + // MaterialInstance.cpp), so a shell left at the default MSM_DefaultLit would make an SL_UNLIT material's + // instance render lit and mismatch its baked Unlit shader map. Mirror the editor graph builder's choice + // (ShaderLabSurfaceEmitter_Legacy.inl). Under Substrate the shading model is driven by the FrontMaterial + // graph (MSM_FromMaterialExpression), so leave it untouched — matching the Substrate shell's existing behavior. + if (!Substrate::IsSubstrateEnabled() && Model.Settings.Domain == EShaderLabDomain::Surface) + { + const bool bUnlit = !Model.bHasSurface && Model.Slabs.Num() == 1 + && Model.Slabs[0].BsdfType == EShaderLabBsdfType::Unlit; + Material.SetShadingModel(bUnlit ? MSM_Unlit : MSM_DefaultLit); + } + // Reflected long-tail settings. A failure here means a malformed .usl shipped in the build, so // log it loudly. (No usage flags: the base shell is a never-rendered template; usage lives on // the instances.) diff --git a/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp index 23a8f21..86311d8 100644 --- a/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabBuilder/Private/ShaderLabGraphBuilder.cpp @@ -49,6 +49,7 @@ #include "Misc/FileHelper.h" #include "ShaderCore.h" #include "SceneTypes.h" +#include "RenderUtils.h" // Substrate::IsSubstrateEnabled() #define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush") #define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush") @@ -2521,6 +2522,10 @@ namespace ShaderLabGraph return nullptr; } } + +// Legacy (non-Substrate) pixel-stage emitter. Pulled into this TU (inside namespace ShaderLabGraph) so it +// reuses the helpers above without a shared header. Deletable wholesale with the non-Substrate scheme. +#include "ShaderLabSurfaceEmitter_Legacy.inl" } bool FShaderLabGraphBuilder::ResolveVirtualShaderFile(const FString& VirtualPath, FString& OutDiskPath) @@ -3099,6 +3104,17 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode return false; } } + else if (!Substrate::IsSubstrateEnabled()) + { + // Legacy (non-Substrate) scheme: PostProcess/UI already handled above (mode-independent). Everything + // else (SL_SURFACE -> DefaultLit, single SL_UNLIT -> Unlit) wires to the standard material pins; the + // rest is rejected with a .usl error. See ShaderLabSurfaceEmitter_Legacy.inl. + if (!EmitLegacyPixelStage(Material, *EditorOnly, Model, Program, Emit, PropertyNodes, + InterpByName, UsedInterps, Samples, SurfaceOutParam, ParamY, OutErrors)) + { + return false; + } + } else if (Model.bHasSurface) { // Single-Surface sugar: one slab straight to FrontMaterial, with the FShaderLabSurface material-level @@ -3526,9 +3542,22 @@ void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArrayRebuildOutputs(); - UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr(Material, IoY, 0); - Slab->DiffuseAlbedo.Connect(0, Custom); - EditorOnly->FrontMaterial.Connect(0, Slab); + if (Substrate::IsSubstrateEnabled()) + { + // Substrate: Custom -> Slab.DiffuseAlbedo -> FrontMaterial (DiffuseAlbedo is a core BSDF pin, always + // translated, so the #error is reached). + UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr(Material, IoY, 0); + Slab->DiffuseAlbedo.Connect(0, Custom); + EditorOnly->FrontMaterial.Connect(0, Slab); + } + else + { + // Legacy (non-Substrate): FrontMaterial is inactive, so the Slab->FrontMaterial poison would be + // dead-stripped and the #error never compiled. Wire the Custom to MP_BaseColor (a core DefaultLit pin + // that is always translated) so the #error still aborts the compile with our diagnostics. + Material.SetShadingModel(MSM_DefaultLit); + EditorOnly->BaseColor.Connect(0, Custom); + } Material.UpdateCachedExpressionData(); } diff --git a/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp b/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp index 5b7ec0d..96ddcf0 100644 --- a/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp +++ b/Source/UShaderLabBuilder/Private/ShaderLabIDEPrepare.cpp @@ -24,6 +24,7 @@ #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" +#include "RenderUtils.h" // Substrate::IsSubstrateEnabled() #include "ShaderCore.h" #include "ShaderLabIntrinsicRegistry.h" @@ -405,6 +406,20 @@ namespace ShaderLabIDEPrepare_Private bChanged = true; } + // SHADERLAB_SUBSTRATE picks the surface-struct parameterization (Substrate Slab fields vs legacy + // Metallic/Roughness) the IDE checks against. It mirrors the project's r.Substrate so authoring on + // the legacy (r.Substrate=0) branch sees the legacy FShaderLabSurface (S.BaseColor/Metallic/...) and + // does not red-underline it as "no member". The real compiler derives this from the engine's own + // SUBSTRATE_ENABLED; this only affects the editor's shader-validator. + const TCHAR* SubstrateValue = Substrate::IsSubstrateEnabled() ? TEXT("1") : TEXT("0"); + FString SubstrateDefine; + if (!Defines->TryGetStringField(TEXT("SHADERLAB_SUBSTRATE"), SubstrateDefine) || SubstrateDefine != SubstrateValue) + { + Defines->SetStringField(TEXT("SHADERLAB_SUBSTRATE"), SubstrateValue); + Settings->SetObjectField(TEXT("shader-validator.defines"), Defines); + bChanged = true; + } + if (!bChanged) { UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath); diff --git a/Source/UShaderLabBuilder/Private/ShaderLabSurfaceEmitter_Legacy.inl b/Source/UShaderLabBuilder/Private/ShaderLabSurfaceEmitter_Legacy.inl new file mode 100644 index 0000000..e79c656 --- /dev/null +++ b/Source/UShaderLabBuilder/Private/ShaderLabSurfaceEmitter_Legacy.inl @@ -0,0 +1,265 @@ +// Copyright UShaderLab. All Rights Reserved. +// +// Legacy (non-Substrate) pixel-stage emitter. This file is #included into ShaderLabGraphBuilder.cpp's +// translation unit (inside namespace ShaderLabGraph) so it can reuse the mode-independent body-emission +// helpers (PrepareBody / WirePropInputs / AddIncludes / NewExpr / WrapBodyWithLineMapping) directly, +// without extracting a shared base or duplicating them. It is a plain #include (not a compiled .cpp), so +// UBT does not build it standalone. +// +// It is the deletable "old scheme" slice on the C++ side: once UE removes the non-Substrate rendering path, +// delete this file, its #include in ShaderLabGraphBuilder.cpp, the !IsSubstrateEnabled() dispatch branch, +// and Surface_Legacy.ush. The Substrate path stays untouched. +// +// Scope (first cut): SL_SURFACE -> MSM_DefaultLit (classic Metallic/Roughness workflow) and a single +// SL_UNLIT block -> MSM_Unlit. PostProcess/UI entries are mode-independent and handled by the shared +// BuildEmissiveEntry, so they never reach here. Multi-slab topology, SL_SLAB, and the other Substrate +// BSDFs (Hair/Eye/Water/ClearCoat/Toon/Volume/LightFunction) have no legacy equivalent and are rejected +// with a .usl-mapped error (contract style, no silent fallback). + +// A legacy DefaultLit surface shading field -> its main-material-node pin. Fields mirror the legacy +// FShaderLabSurface (Surface_Legacy.ush) shading section; names differ from the Substrate parameterization. +struct FLegacyFieldDef { const TCHAR* Field; ECustomMaterialOutputType OutType; }; + +static const FLegacyFieldDef GLegacySurfaceShadingFields[] = { + { TEXT("BaseColor"), CMOT_Float3 }, + { TEXT("Metallic"), CMOT_Float1 }, + { TEXT("Specular"), CMOT_Float1 }, + { TEXT("Roughness"), CMOT_Float1 }, + { TEXT("Anisotropy"), CMOT_Float1 }, + { TEXT("Normal"), CMOT_Float3 }, + { TEXT("Tangent"), CMOT_Float3 }, + { TEXT("EmissiveColor"), CMOT_Float3 }, +}; + +// Legacy material-level outputs (the Substrate FShaderLabMaterialOutput minus SurfaceThickness, which has no +// legacy pin). Opacity here wires straight to MP_Opacity (no Substrate coverage-Weight indirection). +static const FMatOutFieldDef GLegacyMatOutFields[] = { + { TEXT("Opacity"), CMOT_Float1 }, + { TEXT("OpacityMask"), CMOT_Float1 }, + { TEXT("Refraction"), CMOT_Float1 }, + { TEXT("PixelDepthOffset"), CMOT_Float1 }, + { TEXT("AmbientOcclusion"), CMOT_Float1 }, +}; + +/** Legacy DefaultLit shading field -> its editor-only main-node pin. */ +static FExpressionInput* GetLegacySurfacePin(UMaterialEditorOnlyData& E, const FString& F) +{ + if (F == TEXT("BaseColor")) return &E.BaseColor; + if (F == TEXT("Metallic")) return &E.Metallic; + if (F == TEXT("Specular")) return &E.Specular; + if (F == TEXT("Roughness")) return &E.Roughness; + if (F == TEXT("Anisotropy")) return &E.Anisotropy; + if (F == TEXT("Normal")) return &E.Normal; + if (F == TEXT("Tangent")) return &E.Tangent; + if (F == TEXT("EmissiveColor")) return &E.EmissiveColor; + return nullptr; +} + +/** Legacy material-level output field -> its editor-only main-node pin (Opacity included, wired directly). */ +static FExpressionInput* GetLegacyMatOutPin(UMaterialEditorOnlyData& E, const FString& F) +{ + if (F == TEXT("Opacity")) return &E.Opacity; + 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; + return nullptr; +} + +/** + * Emit a legacy pixel-stage body as a single Custom node whose written struct fields are exposed as SLO_ + * additional outputs, then connect each output to the given main-node pin. Mirrors the Custom-node middle of + * BuildBsdf (shared machinery: intrinsics, property inputs, includes, #line mapping) but wires to fixed + * material pins instead of a Substrate BSDF node. `ShadingFields`/`GetShadingPin` describe the BSDF-style + * shading fields; `bAllowMatOut` additionally emits the legacy material-level outputs (Surface only). + * Returns false on a contract violation (with .usl-mapped errors). + */ +static bool BuildLegacyCustomToPins( + UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FShaderLabModel& Model, + const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, + const TMap& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, + const TCHAR* StructName, const TCHAR* DefaultFn, const FString& OutParamName, + const FString& InBody, int32 BodyLine, + const FLegacyFieldDef* ShadingFields, int32 NumShadingFields, + FExpressionInput* (*GetShadingPin)(UMaterialEditorOnlyData&, const FString&), + bool bAllowMatOut, + int32& IoY, TArray& OutErrors) +{ + TArray UsedShading; + for (int32 i = 0; i < NumShadingFields; ++i) + { + if (ReferencesToken(InBody, OutParamName + TEXT(".") + ShadingFields[i].Field)) + { + UsedShading.Add(&ShadingFields[i]); + } + } + TArray UsedMatOut; + if (bAllowMatOut) + { + for (const FMatOutFieldDef& F : GLegacyMatOutFields) + { + if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field)) + { + UsedMatOut.Add(&F); + } + } + } + + if (UsedShading.Num() == 0 && UsedMatOut.Num() == 0) + { + // A single-Surface body (bAllowMatOut) that touches OutParam. but matched no known legacy + // field is almost certainly writing a Substrate-only field (DiffuseAlbedo/F0/SSSMFP/...) that the legacy + // FShaderLabSurface does not have. Reject with a .usl-mapped error rather than silently emitting a flat + // default-gray material (the body would never be compiled, so DXC's "no member" never fires). Unlit is + // exempt: its unwired fields (Normal/TransmittanceColor) are real struct members, legal-but-ignored. + if (bAllowMatOut && InBody.Contains(OutParamName + TEXT("."))) + { + OutErrors.Add(FString::Printf( + TEXT("%s(%d): SL_SURFACE body writes no field of the legacy %s. The non-Substrate scheme supports " + "BaseColor/Metallic/Specular/Roughness/Anisotropy/Normal/Tangent/EmissiveColor + material " + "outputs (Opacity/OpacityMask/Refraction/PixelDepthOffset/AmbientOcclusion). Substrate-only " + "fields (DiffuseAlbedo/F0/SSSMFP/Fuzz/Glint/SurfaceThickness) require r.Substrate=1."), + *Model.SourceFilePath, BodyLine, StructName)); + return false; + } + // Genuinely empty body: leave all pins unconnected (the material uses its pin defaults). Still valid. + return true; + } + + UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); + Custom->Description = TEXT("ShaderLab Surface"); + Custom->OutputType = CMOT_Float1; + AddIncludes(*Custom, Model); + + FString Body = InBody; + TArray Wires; + TSet ReqProps; + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, + 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("%s %s = %s();\n{\n%s}\n"), + StructName, *OutParamName, DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath)); + + int32 OutputIndex = 1; // index 0 is the (unused) main return + TArray> ShadingOutputs; + for (const FLegacyFieldDef* F : UsedShading) + { + 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); + ShadingOutputs.Add(TPair(F, OutputIndex)); + ++OutputIndex; + } + TArray> MatOutputs; + for (const FMatOutFieldDef* F : UsedMatOut) + { + FCustomOutput Out; + Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field)); + Out.OutputType = F->OutType; + Custom->AdditionalOutputs.Add(Out); + Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *OutParamName, F->Field); + MatOutputs.Add(TPair(F->Field, OutputIndex)); + ++OutputIndex; + } + Code += TEXT("return 0.0f;\n"); + Custom->Code = Code; + Custom->RebuildOutputs(); + + for (const TPair& Pair : ShadingOutputs) + { + if (FExpressionInput* Pin = GetShadingPin(EditorOnly, Pair.Key->Field)) + { + Pin->Connect(Pair.Value, Custom); + } + } + for (const TPair& Out : MatOutputs) + { + // Same active-for-blend/domain contract as the Substrate path (reused verbatim); wiring differs + // (legacy connects Opacity straight to MP_Opacity, no coverage-Weight). + if (!ValidateMaterialOutputActive(Model, Out.Key, BodyLine, OutErrors)) { return false; } + if (FExpressionInput* Pin = GetLegacyMatOutPin(EditorOnly, Out.Key)) + { + Pin->Connect(Out.Value, Custom); + } + } + return true; +} + +/** + * Legacy scheme entry: build the Surface / single-Unlit pixel stage and wire it to the standard material + * pins + shading model. Returns false (with .usl errors) for any construct with no legacy equivalent. + * Called only when Substrate is disabled and the entry is neither PostProcess nor UI. + */ +static bool EmitLegacyPixelStage( + UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FShaderLabModel& Model, + const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, + const TMap& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, const FShaderLabEntryParam* SurfaceOutParam, + int32& IoY, TArray& OutErrors) +{ + auto Reject = [&](const FString& Why) { OutErrors.Add(FString::Printf(TEXT("%s: %s"), *Model.SourceFilePath, *Why)); return false; }; + + if (Model.bHasSurface) + { + // SL_SURFACE -> MSM_DefaultLit. (PostProcess/UI never reach here.) + check(Model.SurfaceEntry == EShaderLabEntry::Surface); + check(SurfaceOutParam); + if (!Model.SurfaceModifiers.SubsurfaceProfilePath.IsEmpty() + || !Model.SurfaceModifiers.SpecularProfilePath.IsEmpty() + || !Model.SurfaceModifiers.SubSurfaceType.IsEmpty() + || !Model.SurfaceModifiers.ToonProfilePath.IsEmpty()) + { + return Reject(TEXT("SL_SURFACE BSDF modifiers (SubsurfaceProfile/SpecularProfile/SubSurfaceType/ToonProfile) require Substrate (r.Substrate=1)")); + } + Material.SetShadingModel(MSM_DefaultLit); + return BuildLegacyCustomToPins( + Material, EditorOnly, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, + TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"), SurfaceOutParam->Name, + Model.SurfaceBody, Model.SurfaceBodyLine, + GLegacySurfaceShadingFields, UE_ARRAY_COUNT(GLegacySurfaceShadingFields), &GetLegacySurfacePin, + /*bAllowMatOut*/ true, IoY, OutErrors); + } + + // Multi-BSDF path. Legacy only supports a single SL_UNLIT block auto-rooted to a lone SlabRef. + if (Model.bHasMaterialOutput) + { + return Reject(TEXT("SL_MATERIAL (multi-slab material outputs) requires Substrate (r.Substrate=1); use SL_SURFACE in the legacy scheme")); + } + if (Model.Slabs.Num() != 1 || Model.Slabs[0].BsdfType != EShaderLabBsdfType::Unlit) + { + return Reject(TEXT("the legacy (non-Substrate) scheme supports only SL_SURFACE (DefaultLit) and a single SL_UNLIT block; multi-slab topology and the Slab/Hair/Eye/Water/ClearCoat/Toon/Volume/LightFunction BSDFs require Substrate (r.Substrate=1)")); + } + + // Single Unlit -> MSM_Unlit: only EmissiveColor is meaningful (Normal/TransmittanceColor are ignored by + // the Unlit shading model). Wire EmissiveColor -> MP_EmissiveColor. + const FShaderLabSlab& Unlit = Model.Slabs[0]; + static const FLegacyFieldDef GLegacyUnlitFields[] = { { TEXT("EmissiveColor"), CMOT_Float3 } }; + auto GetUnlitPin = [](UMaterialEditorOnlyData& E, const FString& F) -> FExpressionInput* + { + return F == TEXT("EmissiveColor") ? &E.EmissiveColor : nullptr; + }; + Material.SetShadingModel(MSM_Unlit); + return BuildLegacyCustomToPins( + Material, EditorOnly, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, + TEXT("FShaderLabUnlit"), TEXT("ShaderLabDefaultUnlit"), Unlit.OutParamName, + Unlit.Body, Unlit.BodyLine, + GLegacyUnlitFields, UE_ARRAY_COUNT(GLegacyUnlitFields), +GetUnlitPin, + /*bAllowMatOut*/ false, IoY, OutErrors); +}