Support all BSDF

This commit is contained in:
Eragon-Brisingr
2026-07-03 11:56:30 +08:00
parent 2ed659d4d2
commit e76eb856f5
10 changed files with 1604 additions and 77 deletions

View File

@@ -11,11 +11,23 @@
#include "Engine/Texture2D.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionCollectionParameter.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Materials/MaterialParameterCollection.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionStaticBoolParameter.h"
#include "Materials/MaterialExpressionStaticSwitch.h"
#include "Materials/MaterialExpressionQualitySwitch.h"
#include "Materials/MaterialExpressionFeatureLevelSwitch.h"
#include "Materials/MaterialExpressionShadingPathSwitch.h"
#include "SceneTypes.h"
#include "RHIDefinitions.h"
#include "Materials/MaterialExpressionSubstrate.h"
#include "Materials/MaterialExpressionSingleLayerWaterMaterialOutput.h"
#include "Materials/MaterialExpressionSceneTexture.h"
#include "Engine/SubsurfaceProfile.h"
#include "Engine/SpecularProfile.h"
#include "Engine/ToonProfile.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionVertexInterpolator.h"
@@ -30,6 +42,7 @@
#include "Interfaces/IPluginManager.h"
#include "Misc/FileHelper.h"
#include "ShaderCore.h"
#include "SceneTypes.h"
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
@@ -206,6 +219,8 @@ namespace ShaderLabGraph
case EShaderLabDomain::PostProcess: return MD_PostProcess;
case EShaderLabDomain::UI: return MD_UI;
case EShaderLabDomain::Decal: return MD_DeferredDecal;
case EShaderLabDomain::Volume: return MD_Volume;
case EShaderLabDomain::LightFunction: return MD_LightFunction;
case EShaderLabDomain::Surface:
default: return MD_Surface;
}
@@ -219,6 +234,8 @@ namespace ShaderLabGraph
case EShaderLabBlendMode::Translucent: return BLEND_Translucent;
case EShaderLabBlendMode::Additive: return BLEND_Additive;
case EShaderLabBlendMode::Modulate: return BLEND_Modulate;
case EShaderLabBlendMode::AlphaComposite: return BLEND_AlphaComposite;
case EShaderLabBlendMode::AlphaHoldout: return BLEND_AlphaHoldout;
case EShaderLabBlendMode::Opaque:
default: return BLEND_Opaque;
}
@@ -235,6 +252,225 @@ namespace ShaderLabGraph
return Expr;
}
/** A trivial Custom node that emits `#define <Name> <Value>` (used for the before-attributes define leak). */
static UMaterialExpressionCustom* MakeDefineNode(UMaterial& Material, int32& IoY, const TCHAR* Name, const TCHAR* Value)
{
UMaterialExpressionCustom* D = NewExpr<UMaterialExpressionCustom>(Material, IoY, -1300);
D->Description = TEXT("ShaderLab Define");
D->OutputType = CMOT_Float1;
D->Code = TEXT("return 0;");
FCustomDefine DD;
DD.DefineName = Name;
DD.DefineValue = Value;
D->AdditionalDefines.Add(DD);
return D;
}
// --- Per-BSDF descriptor -----------------------------------------------------------------------
// Each Substrate BSDF is described by {output struct, default fn, field->pin table, node factory,
// pin resolver}. The generic BuildBsdf() works off this so all BSDFs share one code path. The engine
// BSDF node classes are distinct C++ types with no common named-pin base, so each descriptor supplies
// its own MakeNode (news the concrete node) and GetPin (casts + returns the FExpressionInput*).
struct FBsdfDesc
{
EShaderLabBsdfType Type;
const TCHAR* CustomDesc; // Custom-node Description (kept "ShaderLab Surface" for Slab: tests key on it)
const TCHAR* StructName; // e.g. "FShaderLabSurface"
const TCHAR* DefaultFn; // e.g. "ShaderLabDefaultSurface"
const FSlabFieldDef* Fields;
int32 NumFields;
UMaterialExpression* (*MakeNode)(UMaterial&, int32& IoY);
FExpressionInput* (*GetPin)(UMaterialExpression*, const FString& Field);
};
static UMaterialExpression* MakeSlabNode(UMaterial& M, int32& IoY)
{
return NewExpr<UMaterialExpressionSubstrateSlabBSDF>(M, IoY, 0);
}
static FExpressionInput* GetSlabPinGeneric(UMaterialExpression* Node, const FString& Field)
{
UMaterialExpressionSubstrateSlabBSDF* Slab = Cast<UMaterialExpressionSubstrateSlabBSDF>(Node);
return Slab ? GetSlabPin(Slab, Field) : nullptr;
}
// --- Unlit ---
static const FSlabFieldDef GUnlitFields[] = {
{ TEXT("EmissiveColor"), CMOT_Float3 }, { TEXT("TransmittanceColor"), CMOT_Float3 }, { TEXT("Normal"), CMOT_Float3 },
};
static UMaterialExpression* MakeUnlitNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateUnlitBSDF>(M, IoY, 0); }
static FExpressionInput* GetUnlitPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateUnlitBSDF* B = Cast<UMaterialExpressionSubstrateUnlitBSDF>(N); if (!B) return nullptr;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
if (F == TEXT("TransmittanceColor")) return &B->TransmittanceColor;
if (F == TEXT("Normal")) return &B->Normal;
return nullptr;
}
// --- Hair ---
static const FSlabFieldDef GHairFields[] = {
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Scatter"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Backlit"), CMOT_Float3 }, { TEXT("Tangent"), CMOT_Float3 },
{ TEXT("EmissiveColor"), CMOT_Float3 },
};
static UMaterialExpression* MakeHairNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateHairBSDF>(M, IoY, 0); }
static FExpressionInput* GetHairPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateHairBSDF* B = Cast<UMaterialExpressionSubstrateHairBSDF>(N); if (!B) return nullptr;
if (F == TEXT("BaseColor")) return &B->BaseColor;
if (F == TEXT("Scatter")) return &B->Scatter;
if (F == TEXT("Specular")) return &B->Specular;
if (F == TEXT("Roughness")) return &B->Roughness;
if (F == TEXT("Backlit")) return &B->Backlit;
if (F == TEXT("Tangent")) return &B->Tangent;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
return nullptr;
}
// --- Eye ---
static const FSlabFieldDef GEyeFields[] = {
{ TEXT("DiffuseColor"), CMOT_Float3 }, { TEXT("Roughness"), CMOT_Float1 }, { TEXT("CorneaNormal"), CMOT_Float3 },
{ TEXT("IrisNormal"), CMOT_Float3 }, { TEXT("IrisPlaneNormal"), CMOT_Float3 }, { TEXT("IrisMask"), CMOT_Float1 },
{ TEXT("IrisDistance"), CMOT_Float1 }, { TEXT("EmissiveColor"), CMOT_Float3 },
};
static UMaterialExpression* MakeEyeNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateEyeBSDF>(M, IoY, 0); }
static FExpressionInput* GetEyePin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateEyeBSDF* B = Cast<UMaterialExpressionSubstrateEyeBSDF>(N); if (!B) return nullptr;
if (F == TEXT("DiffuseColor")) return &B->DiffuseColor;
if (F == TEXT("Roughness")) return &B->Roughness;
if (F == TEXT("CorneaNormal")) return &B->CorneaNormal;
if (F == TEXT("IrisNormal")) return &B->IrisNormal;
if (F == TEXT("IrisPlaneNormal")) return &B->IrisPlaneNormal;
if (F == TEXT("IrisMask")) return &B->IrisMask;
if (F == TEXT("IrisDistance")) return &B->IrisDistance;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
return nullptr;
}
// --- Single Layer Water ---
static const FSlabFieldDef GWaterFields[] = {
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Metallic"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
{ TEXT("TopMaterialOpacity"), CMOT_Float3 }, { TEXT("WaterAlbedo"), CMOT_Float3 }, { TEXT("WaterExtinction"), CMOT_Float3 },
{ TEXT("WaterPhaseG"), CMOT_Float1 }, { TEXT("ColorScaleBehindWater"), CMOT_Float3 },
};
static UMaterialExpression* MakeWaterNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateSingleLayerWaterBSDF>(M, IoY, 0); }
static FExpressionInput* GetWaterPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateSingleLayerWaterBSDF* B = Cast<UMaterialExpressionSubstrateSingleLayerWaterBSDF>(N); if (!B) return nullptr;
if (F == TEXT("BaseColor")) return &B->BaseColor;
if (F == TEXT("Metallic")) return &B->Metallic;
if (F == TEXT("Specular")) return &B->Specular;
if (F == TEXT("Roughness")) return &B->Roughness;
if (F == TEXT("Normal")) return &B->Normal;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
if (F == TEXT("TopMaterialOpacity")) return &B->TopMaterialOpacity;
if (F == TEXT("WaterAlbedo")) return &B->WaterAlbedo;
if (F == TEXT("WaterExtinction")) return &B->WaterExtinction;
if (F == TEXT("WaterPhaseG")) return &B->WaterPhaseG;
if (F == TEXT("ColorScaleBehindWater")) return &B->ColorScaleBehindWater;
return nullptr;
}
// --- Volumetric-Fog-Cloud ---
static const FSlabFieldDef GVolumeFields[] = {
{ TEXT("Albedo"), CMOT_Float3 }, { TEXT("Extinction"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
{ TEXT("AmbientOcclusion"), CMOT_Float1 },
};
static UMaterialExpression* MakeVolumeNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateVolumetricFogCloudBSDF>(M, IoY, 0); }
static FExpressionInput* GetVolumePin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateVolumetricFogCloudBSDF* B = Cast<UMaterialExpressionSubstrateVolumetricFogCloudBSDF>(N); if (!B) return nullptr;
if (F == TEXT("Albedo")) return &B->Albedo;
if (F == TEXT("Extinction")) return &B->Extinction;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
if (F == TEXT("AmbientOcclusion")) return &B->AmbientOcclusion;
return nullptr;
}
// --- Simple Clear Coat ---
static const FSlabFieldDef GClearCoatFields[] = {
{ TEXT("DiffuseAlbedo"), CMOT_Float3 }, { TEXT("F0"), CMOT_Float3 }, { TEXT("Roughness"), CMOT_Float1 },
{ TEXT("ClearCoatCoverage"), CMOT_Float1 }, { TEXT("ClearCoatRoughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 },
{ TEXT("EmissiveColor"), CMOT_Float3 }, { TEXT("BottomNormal"), CMOT_Float3 },
};
static UMaterialExpression* MakeClearCoatNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateSimpleClearCoatBSDF>(M, IoY, 0); }
static FExpressionInput* GetClearCoatPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateSimpleClearCoatBSDF* B = Cast<UMaterialExpressionSubstrateSimpleClearCoatBSDF>(N); if (!B) return nullptr;
if (F == TEXT("DiffuseAlbedo")) return &B->DiffuseAlbedo;
if (F == TEXT("F0")) return &B->F0;
if (F == TEXT("Roughness")) return &B->Roughness;
if (F == TEXT("ClearCoatCoverage")) return &B->ClearCoatCoverage;
if (F == TEXT("ClearCoatRoughness")) return &B->ClearCoatRoughness;
if (F == TEXT("Normal")) return &B->Normal;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
if (F == TEXT("BottomNormal")) return &B->BottomNormal;
return nullptr;
}
// --- Toon (experimental) ---
static const FSlabFieldDef GToonFields[] = {
{ TEXT("BaseColor"), CMOT_Float3 }, { TEXT("Metallic"), CMOT_Float1 }, { TEXT("Specular"), CMOT_Float1 },
{ TEXT("Roughness"), CMOT_Float1 }, { TEXT("Normal"), CMOT_Float3 }, { TEXT("EmissiveColor"), CMOT_Float3 },
{ TEXT("PatternUVs"), CMOT_Float2 }, { TEXT("Anisotropy"), CMOT_Float1 }, { TEXT("Tangent"), CMOT_Float3 },
};
static UMaterialExpression* MakeToonNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateToonBSDF>(M, IoY, 0); }
static FExpressionInput* GetToonPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateToonBSDF* B = Cast<UMaterialExpressionSubstrateToonBSDF>(N); if (!B) return nullptr;
if (F == TEXT("BaseColor")) return &B->BaseColor;
if (F == TEXT("Metallic")) return &B->Metallic;
if (F == TEXT("Specular")) return &B->Specular;
if (F == TEXT("Roughness")) return &B->Roughness;
if (F == TEXT("Normal")) return &B->Normal;
if (F == TEXT("EmissiveColor")) return &B->EmissiveColor;
if (F == TEXT("PatternUVs")) return &B->PatternUVs;
if (F == TEXT("Anisotropy")) return &B->Anisotropy;
if (F == TEXT("Tangent")) return &B->Tangent;
return nullptr;
}
// --- Light Function ---
static const FSlabFieldDef GLightFunctionFields[] = {
{ TEXT("Color"), CMOT_Float3 },
};
static UMaterialExpression* MakeLightFunctionNode(UMaterial& M, int32& IoY) { return NewExpr<UMaterialExpressionSubstrateLightFunction>(M, IoY, 0); }
static FExpressionInput* GetLightFunctionPin(UMaterialExpression* N, const FString& F)
{
UMaterialExpressionSubstrateLightFunction* B = Cast<UMaterialExpressionSubstrateLightFunction>(N); if (!B) return nullptr;
if (F == TEXT("Color")) return &B->Color;
return nullptr;
}
static const FBsdfDesc GBsdfDescs[] = {
{ EShaderLabBsdfType::Slab, TEXT("ShaderLab Surface"), TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"),
GSlabFields, UE_ARRAY_COUNT(GSlabFields), &MakeSlabNode, &GetSlabPinGeneric },
{ EShaderLabBsdfType::Unlit, TEXT("ShaderLab Unlit"), TEXT("FShaderLabUnlit"), TEXT("ShaderLabDefaultUnlit"),
GUnlitFields, UE_ARRAY_COUNT(GUnlitFields), &MakeUnlitNode, &GetUnlitPin },
{ EShaderLabBsdfType::Hair, TEXT("ShaderLab Hair"), TEXT("FShaderLabHair"), TEXT("ShaderLabDefaultHair"),
GHairFields, UE_ARRAY_COUNT(GHairFields), &MakeHairNode, &GetHairPin },
{ EShaderLabBsdfType::Eye, TEXT("ShaderLab Eye"), TEXT("FShaderLabEye"), TEXT("ShaderLabDefaultEye"),
GEyeFields, UE_ARRAY_COUNT(GEyeFields), &MakeEyeNode, &GetEyePin },
{ EShaderLabBsdfType::Water, TEXT("ShaderLab Water"), TEXT("FShaderLabWater"), TEXT("ShaderLabDefaultWater"),
GWaterFields, UE_ARRAY_COUNT(GWaterFields), &MakeWaterNode, &GetWaterPin },
{ EShaderLabBsdfType::Volume, TEXT("ShaderLab Volume"), TEXT("FShaderLabVolume"), TEXT("ShaderLabDefaultVolume"),
GVolumeFields, UE_ARRAY_COUNT(GVolumeFields), &MakeVolumeNode, &GetVolumePin },
{ EShaderLabBsdfType::ClearCoat, TEXT("ShaderLab ClearCoat"), TEXT("FShaderLabClearCoat"), TEXT("ShaderLabDefaultClearCoat"),
GClearCoatFields, UE_ARRAY_COUNT(GClearCoatFields), &MakeClearCoatNode, &GetClearCoatPin },
{ EShaderLabBsdfType::Toon, TEXT("ShaderLab Toon"), TEXT("FShaderLabToon"), TEXT("ShaderLabDefaultToon"),
GToonFields, UE_ARRAY_COUNT(GToonFields), &MakeToonNode, &GetToonPin },
{ EShaderLabBsdfType::LightFunction, TEXT("ShaderLab LightFunction"), TEXT("FShaderLabLightFunction"), TEXT("ShaderLabDefaultLightFunction"),
GLightFunctionFields, UE_ARRAY_COUNT(GLightFunctionFields), &MakeLightFunctionNode, &GetLightFunctionPin },
};
static const FBsdfDesc* FindBsdfDesc(EShaderLabBsdfType Type)
{
for (const FBsdfDesc& D : GBsdfDescs) { if (D.Type == Type) { return &D; } }
return nullptr;
}
/** One intrinsic call resolved to a Custom-node input wired from an engine expression node. */
struct FIntrinsicWire
{
@@ -631,6 +867,18 @@ namespace ShaderLabGraph
OutParams.Add(FString::Printf(TEXT("TextureCube %s"), *Name));
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
break;
case EShaderLabPropertyType::Texture2DArray:
OutParams.Add(FString::Printf(TEXT("Texture2DArray %s"), *Name));
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
break;
case EShaderLabPropertyType::Texture3D:
OutParams.Add(FString::Printf(TEXT("Texture3D %s"), *Name));
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
break;
case EShaderLabPropertyType::TextureCubeArray:
OutParams.Add(FString::Printf(TEXT("TextureCubeArray %s"), *Name));
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
break;
default: break; // StaticBool never becomes a parameter (reaches bodies via the global #define).
}
}
@@ -640,7 +888,7 @@ namespace ShaderLabGraph
{
const FString Name = Prop.Name.ToString();
OutArgs.Add(Name);
if (Prop.Type == EShaderLabPropertyType::Texture2D || Prop.Type == EShaderLabPropertyType::TextureCube)
if (ShaderLabIsTextureType(Prop.Type))
{
OutArgs.Add(Name + TEXT("Sampler"));
}
@@ -1079,7 +1327,8 @@ namespace ShaderLabGraph
* Iterating the (deduped) promoted property set once means a property that is both never doubles up.
*/
static void WirePropInputs(UMaterialExpressionCustom& Custom, const FShaderLabResolvedProgram& Program,
const TMap<FName, FParamNode>& PropertyNodes, const FString& InBody, const TSet<FName>& ReqProps)
const TMap<FName, FParamNode>& PropertyNodes, const FString& InBody, const TSet<FName>& ReqProps,
const TArray<FShaderLabCollectionParam>& Collections)
{
for (const FShaderLabProperty& Prop : Program.Properties)
{
@@ -1100,6 +1349,22 @@ namespace ShaderLabGraph
Custom.Inputs.Add(In);
}
}
// Material Parameter Collection reads: same wiring as a property (deterministic declaration order).
for (const FShaderLabCollectionParam& C : Collections)
{
if (!ReferencesToken(InBody, C.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(C.Name);
if (Node && Node->Expr)
{
FCustomInput In;
In.InputName = C.Name;
In.Input.Connect(0, Node->Expr);
Custom.Inputs.Add(In);
}
}
}
/** Map an SL_INTERPOLATOR return type to a Custom-node output type (parser guarantees float1..4). */
@@ -1319,12 +1584,68 @@ namespace ShaderLabGraph
}
}
/** Map a SubSurfaceType token to the engine enum. */
static bool ParseSubSurfaceType(const FString& Token, EMaterialSubSurfaceType& Out)
{
if (Token == TEXT("None")) { Out = MSS_None; return true; }
if (Token == TEXT("Wrap")) { Out = MSS_Wrap; return true; }
if (Token == TEXT("TwoSidedWrap")) { Out = MSS_TwoSidedWrap; return true; }
if (Token == TEXT("Diffusion")) { Out = MSS_Diffusion; return true; }
if (Token == TEXT("SimpleVolume")) { Out = MSS_SimpleVolume; return true; }
return false;
}
/**
* Emit the Custom node for a pixel-stage body that writes FShaderLabSurface fields and wire it into
* a fresh Substrate Slab BSDF. Returns the slab (nullptr only on error). When bAllowMaterialOutputs,
* S.Opacity / S.OpacityMask are wired to the material-level pins (single-Surface sugar path only).
* Apply non-pin BSDF modifiers (asset profiles + SubSurfaceType) onto the concrete node. Only modifiers
* applicable to the node type are consumed; others are ignored. Returns false (with an error) on a bad
* asset path or enum token.
*/
static UMaterialExpressionSubstrateSlabBSDF* BuildSlab(
template <typename TProfile>
static bool LoadProfileAsset(const FString& Path, const TCHAR* What, TObjectPtr<TProfile>& OutPtr, TArray<FString>& OutErrors)
{
if (Path.IsEmpty()) { return true; }
TProfile* P = LoadObject<TProfile>(nullptr, *Path);
if (!P) { OutErrors.Add(FString::Printf(TEXT("%s: cannot load '%s'"), What, *Path)); return false; }
OutPtr = P;
return true;
}
static bool ApplyBsdfModifiers(UMaterialExpression* Bsdf, const FShaderLabBsdfModifiers& M, TArray<FString>& OutErrors)
{
if (UMaterialExpressionSubstrateSlabBSDF* Slab = Cast<UMaterialExpressionSubstrateSlabBSDF>(Bsdf))
{
if (!LoadProfileAsset(M.SubsurfaceProfilePath, TEXT("SubsurfaceProfile"), Slab->SubsurfaceProfile, OutErrors)) { return false; }
if (!LoadProfileAsset(M.SpecularProfilePath, TEXT("SpecularProfile"), Slab->SpecularProfile, OutErrors)) { return false; }
if (!M.SubSurfaceType.IsEmpty())
{
EMaterialSubSurfaceType T;
if (!ParseSubSurfaceType(M.SubSurfaceType, T))
{
OutErrors.Add(FString::Printf(TEXT("Unknown SubSurfaceType '%s' (use None/Wrap/TwoSidedWrap/Diffusion/SimpleVolume)"), *M.SubSurfaceType));
return false;
}
Slab->SubSurfaceType = T;
}
}
else if (UMaterialExpressionSubstrateEyeBSDF* Eye = Cast<UMaterialExpressionSubstrateEyeBSDF>(Bsdf))
{
if (!LoadProfileAsset(M.SubsurfaceProfilePath, TEXT("SubsurfaceProfile"), Eye->SubsurfaceProfile, OutErrors)) { return false; }
}
else if (UMaterialExpressionSubstrateToonBSDF* Toon = Cast<UMaterialExpressionSubstrateToonBSDF>(Bsdf))
{
if (!LoadProfileAsset(M.ToonProfilePath, TEXT("ToonProfile"), Toon->ToonProfile, OutErrors)) { return false; }
}
return true;
}
/**
* Emit the Custom node for a pixel-stage body that writes the given BSDF's output-struct fields and wire
* it into a fresh Substrate BSDF node (chosen by Desc). Returns the BSDF node (nullptr only on error).
* When bAllowMaterialOutputs, S.Opacity / S.OpacityMask are wired to the material-level pins (single-entry
* sugar path only). Generic across all Substrate BSDFs via the FBsdfDesc field->pin table.
*/
static UMaterialExpression* BuildBsdf(
const FBsdfDesc& Desc, const FShaderLabBsdfModifiers& Modifiers,
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit,
@@ -1332,11 +1653,25 @@ namespace ShaderLabGraph
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
UMaterialExpression* Bsdf = Desc.MakeNode(Material, IoY);
if (!ApplyBsdfModifiers(Bsdf, Modifiers, OutErrors))
{
return nullptr;
}
// Single Layer Water additionally requires a companion SingleLayerWaterMaterialOutput node — the
// lighting / ray-tracing shaders call GetSingleLayerWaterMaterialOutputN, which only exists when that
// node is in the graph. Add it up-front (so even an empty water body compiles); wire it below.
UMaterialExpressionSingleLayerWaterMaterialOutput* WaterOut = nullptr;
if (Desc.Type == EShaderLabBsdfType::Water)
{
WaterOut = NewExpr<UMaterialExpressionSingleLayerWaterMaterialOutput>(Material, IoY, -600);
}
TArray<const FSlabFieldDef*> UsedSlab;
for (const FSlabFieldDef& F : GSlabFields)
for (int32 i = 0; i < Desc.NumFields; ++i)
{
const FSlabFieldDef& F = Desc.Fields[i];
if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field))
{
UsedSlab.Add(&F);
@@ -1344,14 +1679,16 @@ namespace ShaderLabGraph
}
const bool bUsesOpacity = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
const bool bUsesOpacityMask = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".OpacityMask"));
const bool bUsesRefraction = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".Refraction"));
const bool bUsesPDO = bAllowMaterialOutputs && ReferencesToken(InBody, OutParamName + TEXT(".PixelDepthOffset"));
if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask)
if (UsedSlab.Num() == 0 && !bUsesOpacity && !bUsesOpacityMask && !bUsesRefraction && !bUsesPDO)
{
return Slab; // Empty body: a default Substrate slab.
return Bsdf; // Empty body: a default Substrate BSDF.
}
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab Surface");
Custom->Description = Desc.CustomDesc;
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
@@ -1369,11 +1706,11 @@ namespace ShaderLabGraph
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps);
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections);
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
FString Code = FString::Printf(TEXT("FShaderLabSurface %s = ShaderLabDefaultSurface();\n{\n%s}\n"),
*OutParamName, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
FString Code = FString::Printf(TEXT("%s %s = %s();\n{\n%s}\n"),
Desc.StructName, *OutParamName, Desc.DefaultFn, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
int32 OutputIndex = 1; // index 0 is the (unused) main return
TArray<TPair<const FSlabFieldDef*, int32>> SlabOutputs;
@@ -1403,20 +1740,54 @@ namespace ShaderLabGraph
Code += FString::Printf(TEXT("SLO_OpacityMask = %s.OpacityMask;\n"), *OutParamName);
OpacityMaskOutIdx = OutputIndex++;
}
int32 RefractionOutIdx = INDEX_NONE;
int32 PDOOutIdx = INDEX_NONE;
if (bUsesRefraction)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_Refraction"); Out.OutputType = CMOT_Float1;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_Refraction = %s.Refraction;\n"), *OutParamName);
RefractionOutIdx = OutputIndex++;
}
if (bUsesPDO)
{
FCustomOutput Out; Out.OutputName = TEXT("SLO_PixelDepthOffset"); Out.OutputType = CMOT_Float1;
Custom->AdditionalOutputs.Add(Out);
Code += FString::Printf(TEXT("SLO_PixelDepthOffset = %s.PixelDepthOffset;\n"), *OutParamName);
PDOOutIdx = OutputIndex++;
}
Code += TEXT("return 0.0f;\n");
Custom->Code = Code;
Custom->RebuildOutputs();
for (const TPair<const FSlabFieldDef*, int32>& Pair : SlabOutputs)
{
if (FExpressionInput* Pin = GetSlabPin(Slab, Pair.Key->Field))
if (FExpressionInput* Pin = Desc.GetPin(Bsdf, Pair.Key->Field))
{
Pin->Connect(Pair.Value, Custom);
}
}
if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); }
if (OpacityMaskOutIdx != INDEX_NONE) { EditorOnly.OpacityMask.Connect(OpacityMaskOutIdx, Custom); }
return Slab;
if (RefractionOutIdx != INDEX_NONE) { EditorOnly.Refraction.Connect(RefractionOutIdx, Custom); }
if (PDOOutIdx != INDEX_NONE) { EditorOnly.PixelDepthOffset.Connect(PDOOutIdx, Custom); }
// Wire the water volume fields into the companion output node (by matching Custom output field name).
if (WaterOut)
{
auto ConnectWaterOut = [&](FExpressionInput& Pin, const TCHAR* FieldName)
{
for (const TPair<const FSlabFieldDef*, int32>& P : SlabOutputs)
{
if (FCString::Strcmp(P.Key->Field, FieldName) == 0) { Pin.Connect(P.Value, Custom); return; }
}
};
ConnectWaterOut(WaterOut->ScatteringCoefficients, TEXT("WaterAlbedo"));
ConnectWaterOut(WaterOut->AbsorptionCoefficients, TEXT("WaterExtinction"));
ConnectWaterOut(WaterOut->PhaseG, TEXT("WaterPhaseG"));
ConnectWaterOut(WaterOut->ColorScaleBehindWater, TEXT("ColorScaleBehindWater"));
}
return Bsdf;
}
/**
@@ -1450,7 +1821,7 @@ namespace ShaderLabGraph
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps);
WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections);
const bool bUsesColor = ReferencesToken(InBody, OutParamName + TEXT(".Color"));
const bool bUsesOpacity = ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
@@ -1512,7 +1883,7 @@ namespace ShaderLabGraph
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
WirePropInputs(*Custom, Program, PropertyNodes, Value.Body, ReqProps);
WirePropInputs(*Custom, Program, PropertyNodes, Value.Body, ReqProps, Model.Collections);
// The body itself contains `return <scalar>;`, so it is the Custom function's body directly.
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
@@ -1553,7 +1924,7 @@ namespace ShaderLabGraph
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
WirePropInputs(*Custom, Program, PropertyNodes, Interp.Body, ReqProps);
WirePropInputs(*Custom, Program, PropertyNodes, Interp.Body, ReqProps, Model.Collections);
// The body contains `return <expr>;`, so it is the Custom function's body directly.
Custom->Code = WrapBodyWithLineMapping(Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath));
@@ -1613,7 +1984,7 @@ namespace ShaderLabGraph
/** Recursively build the Substrate expression for topology node `Index`. Returns nullptr on error. */
static UMaterialExpression* BuildTopologyNode(
UMaterial& Material, int32 Index, const FShaderLabModel& Model,
const TMap<FName, UMaterialExpressionSubstrateSlabBSDF*>& SlabByName,
const TMap<FName, UMaterialExpression*>& SlabByName,
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
@@ -1628,7 +1999,7 @@ namespace ShaderLabGraph
if (Node.Op == EShaderLabOp::SlabRef)
{
if (UMaterialExpressionSubstrateSlabBSDF* const* Found = SlabByName.Find(Node.SlabRef))
if (UMaterialExpression* const* Found = SlabByName.Find(Node.SlabRef))
{
return *Found;
}
@@ -1862,6 +2233,50 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
TMap<FName, UMaterialExpression*> InterpByName;
TSet<FName> UsedInterps;
// CustomPrimitiveData contract: author-specified slot indices must fit the fixed float budget and must
// not overlap. Scalar occupies 1 float, Vector 4 (starting at PrimitiveDataIndex). Validated across all
// declared CPD properties (parser already guarantees Scalar/Vector-only + a non-negative index).
{
constexpr int32 NumFloats = FCustomPrimitiveData::NumCustomPrimitiveDataFloats;
int32 SlotOwner[NumFloats];
for (int32 i = 0; i < NumFloats; ++i) { SlotOwner[i] = INDEX_NONE; }
bool bCpdError = false;
for (int32 PropIdx = 0; PropIdx < Program.Properties.Num(); ++PropIdx)
{
const FShaderLabProperty& Prop = Program.Properties[PropIdx];
if (!Prop.bUseCustomPrimitiveData)
{
continue;
}
const int32 Size = (Prop.Type == EShaderLabPropertyType::Scalar) ? 1 : 4;
const int32 Start = Prop.PrimitiveDataIndex;
if (Start + Size > NumFloats)
{
OutErrors.Add(FString::Printf(
TEXT("Property '%s': CustomPrimitiveData index %d + %d float(s) exceeds the %d-float budget"),
*Prop.Name.ToString(), Start, Size, NumFloats));
bCpdError = true;
continue;
}
for (int32 s = Start; s < Start + Size; ++s)
{
if (SlotOwner[s] != INDEX_NONE)
{
OutErrors.Add(FString::Printf(
TEXT("Property '%s': CustomPrimitiveData slot %d overlaps property '%s'"),
*Prop.Name.ToString(), s, *Program.Properties[SlotOwner[s]].Name.ToString()));
bCpdError = true;
break;
}
SlotOwner[s] = PropIdx;
}
}
if (bCpdError)
{
return false; // Contract-style hard failure (surfaced through the standard error path).
}
}
for (const FShaderLabProperty& Prop : Program.Properties)
{
const FString NameStr = Prop.Name.ToString();
@@ -1918,10 +2333,18 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
E->DefaultValue = Prop.ScalarDefault;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
if (Prop.bHasRange)
if (Prop.bHasClampMin)
{
E->SliderMin = Prop.RangeMin;
E->SliderMax = Prop.RangeMax;
E->SliderMin = Prop.ClampMin;
}
if (Prop.bHasClampMax)
{
E->SliderMax = Prop.ClampMax;
}
if (Prop.bUseCustomPrimitiveData)
{
E->bUseCustomPrimitiveData = true;
E->PrimitiveDataIndex = static_cast<uint8>(Prop.PrimitiveDataIndex);
}
Node.Expr = E;
break;
@@ -1934,6 +2357,11 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
E->DefaultValue = Prop.VectorDefault;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
if (Prop.bUseCustomPrimitiveData)
{
E->bUseCustomPrimitiveData = true;
E->PrimitiveDataIndex = static_cast<uint8>(Prop.PrimitiveDataIndex);
}
Node.Expr = E;
break;
}
@@ -1951,6 +2379,22 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
Node.bIsTexture = true;
break;
}
case EShaderLabPropertyType::Texture2DArray:
case EShaderLabPropertyType::Texture3D:
case EShaderLabPropertyType::TextureCubeArray:
{
// Array / volume texture object parameter. The engine assigns a matching-dimension default when
// Texture is left null; the artist binds a real asset on the Material Instance. (Built-in white/
// black/grey/normal default tokens only exist for Texture2D, so they are not applied here.)
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
E->ParameterName = Prop.Name;
E->Group = FName(*Prop.Group);
E->SortPriority = Prop.SortPriority;
E->SamplerType = SAMPLERTYPE_Color;
Node.Expr = E;
Node.bIsTexture = true;
break;
}
default:
break;
}
@@ -1960,6 +2404,41 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// 1a2) Material Parameter Collection reads (SL_COLLECTION): a CollectionParameter node per referenced
// declaration, registered in PropertyNodes so WirePropInputs wires it into bodies exactly like a property.
for (const FShaderLabCollectionParam& C : Model.Collections)
{
const FString NameStr = C.Name.ToString();
bool bRef = Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr);
for (const FShaderLabSlab& Slab : Model.Slabs) { bRef |= ReferencesToken(Slab.Body, NameStr); }
for (const FShaderLabValue& Value : Model.Values) { bRef |= ReferencesToken(Value.Body, NameStr); }
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { bRef |= ReferencesToken(Interp.Body, NameStr); }
if (Model.bHasVertex) { bRef |= ReferencesToken(Model.VertexBody, NameStr); }
if (!bRef)
{
continue; // Declared but unreferenced: skip (keeps the graph minimal/deterministic).
}
UMaterialParameterCollection* Coll = LoadObject<UMaterialParameterCollection>(nullptr, *C.CollectionPath);
if (!Coll)
{
OutErrors.Add(FString::Printf(TEXT("SL_COLLECTION '%s': cannot load Material Parameter Collection '%s'"), *NameStr, *C.CollectionPath));
return false;
}
const FGuid ParamId = Coll->GetParameterId(C.ParameterName);
if (!ParamId.IsValid())
{
OutErrors.Add(FString::Printf(TEXT("SL_COLLECTION '%s': parameter '%s' not found in '%s'"), *NameStr, *C.ParameterName.ToString(), *C.CollectionPath));
return false;
}
UMaterialExpressionCollectionParameter* E = NewExpr<UMaterialExpressionCollectionParameter>(Material, ParamY, -1000);
E->Collection = Coll;
E->ParameterName = C.ParameterName;
E->ParameterId = ParamId;
FParamNode Node;
Node.Expr = E;
PropertyNodes.Add(C.Name, Node);
}
// 1b) Build a Vertex Interpolator node per declared interpolator (Custom @ vertex freq -> VertexInterpolator).
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
{
@@ -1998,7 +2477,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
{
// Single-Surface sugar: one slab straight to FrontMaterial, with S.Opacity/S.OpacityMask
// allowed as material-level outputs.
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
UMaterialExpression* Slab = BuildBsdf(
*FindBsdfDesc(EShaderLabBsdfType::Slab), Model.SurfaceModifiers,
Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine,
Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
if (!Slab)
@@ -2009,9 +2489,10 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
else
{
// Multi-slab: each named Slab -> its own slab node; Value blocks -> scalar Custom nodes; the
// FrontMaterial topology tree mixes them; Opacity/OpacityMask come from named Value blocks.
TMap<FName, UMaterialExpressionSubstrateSlabBSDF*> SlabByName;
// Multi-BSDF: each named block -> its own Substrate BSDF node (type from FShaderLabSlab.BsdfType);
// Value blocks -> scalar Custom nodes; the FrontMaterial topology tree mixes them; Opacity/OpacityMask
// come from named Value blocks.
TMap<FName, UMaterialExpression*> SlabByName;
for (const FShaderLabSlab& SlabDecl : Model.Slabs)
{
if (SlabByName.Contains(SlabDecl.Name))
@@ -2019,7 +2500,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
OutErrors.Add(FString::Printf(TEXT("Duplicate Slab name '%s'"), *SlabDecl.Name.ToString()));
return false;
}
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
const FBsdfDesc* Desc = FindBsdfDesc(SlabDecl.BsdfType);
if (!Desc)
{
OutErrors.Add(FString::Printf(TEXT("Slab '%s' has an unsupported BSDF type"), *SlabDecl.Name.ToString()));
return false;
}
UMaterialExpression* Slab = BuildBsdf(
*Desc, SlabDecl.Modifiers,
Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine,
Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
if (!Slab)
@@ -2046,6 +2534,13 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
ValueByName.Add(ValueDecl.Name, ValueNode);
}
// Composition legality: validate operator/BSDF-type combinations up-front (mirrors the engine's Substrate
// allow-lists) so an illegal combo errors with a .usl line instead of a generic engine node-name error.
if (!Model.ValidateTopology(OutErrors))
{
return false;
}
// Every declared Slab must be reachable from FrontMaterial (contract: no dead slabs).
TSet<FName> ReferencedSlabs;
for (const FShaderLabTopoNode& Node : Model.Topology)
@@ -2084,6 +2579,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
};
if (!ConnectMaterialOutput(EditorOnly->Opacity, Model.OpacityValueName, TEXT("Opacity"))) { return false; }
if (!ConnectMaterialOutput(EditorOnly->OpacityMask, Model.OpacityMaskValueName, TEXT("OpacityMask"))) { return false; }
if (!ConnectMaterialOutput(EditorOnly->Refraction, Model.RefractionValueName, TEXT("Refraction"))) { return false; }
if (!ConnectMaterialOutput(EditorOnly->PixelDepthOffset, Model.PixelDepthOffsetValueName, TEXT("PixelDepthOffset"))) { return false; }
}
// 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry
@@ -2128,7 +2625,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
VCustom->Inputs.Add(In);
}
WirePropInputs(*VCustom, Program, PropertyNodes, Model.VertexBody, VtxReqProps);
WirePropInputs(*VCustom, Program, PropertyNodes, Model.VertexBody, VtxReqProps, Model.Collections);
const FString VSrcPath = MakeLineDirectivePath(Model.SourceFilePath);
Code += FString::Printf(TEXT("FShaderLabVertex %s = ShaderLabDefaultVertex();\n{\n%s}\n"),
@@ -2186,6 +2683,88 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// Read-only permutation macros (SHADERLAB_QUALITY / _FEATURELEVEL / _SHADINGPATH). When a body references
// one, wire the matching engine switch (Quality/FeatureLevel/ShadingPath) with per-branch `#define` Custom
// nodes into the ParameterAnchor. The switch compiles ONLY the branch for the current permutation (verified
// for the real, non-validating compile), so exactly one ordered value leaks ahead of every body `#if`.
{
auto AnyBodyUses = [&](const TCHAR* Token) -> bool
{
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, Token)) { return true; }
for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, Token)) { return true; } }
for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, Token)) { return true; } }
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { if (ReferencesToken(Interp.Body, Token)) { return true; } }
if (Model.bHasVertex && ReferencesToken(Model.VertexBody, Token)) { return true; }
return false;
};
if (AnyBodyUses(TEXT("SHADERLAB_QUALITY")))
{
UMaterialExpressionQualitySwitch* Sw = NewExpr<UMaterialExpressionQualitySwitch>(Material, ParamY, -1150);
// Ordered values: Low<Medium<High<Epic. Engine enum order is Low,High,Medium,Epic.
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("2")));
Sw->Inputs[EMaterialQualityLevel::Low].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("0")));
Sw->Inputs[EMaterialQualityLevel::Medium].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("1")));
Sw->Inputs[EMaterialQualityLevel::High].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("2")));
Sw->Inputs[EMaterialQualityLevel::Epic].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_QUALITY"), TEXT("3")));
AnchorInputs.Add(Sw);
}
if (AnyBodyUses(TEXT("SHADERLAB_FEATURELEVEL")))
{
UMaterialExpressionFeatureLevelSwitch* Sw = NewExpr<UMaterialExpressionFeatureLevelSwitch>(Material, ParamY, -1150);
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("1")));
Sw->Inputs[ERHIFeatureLevel::ES3_1].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("0")));
Sw->Inputs[ERHIFeatureLevel::SM5].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("1")));
Sw->Inputs[ERHIFeatureLevel::SM6].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_FEATURELEVEL"), TEXT("2")));
AnchorInputs.Add(Sw);
}
if (AnyBodyUses(TEXT("SHADERLAB_SHADINGPATH")))
{
UMaterialExpressionShadingPathSwitch* Sw = NewExpr<UMaterialExpressionShadingPathSwitch>(Material, ParamY, -1150);
Sw->Default.Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("0")));
Sw->Inputs[ERHIShadingPath::Deferred].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("0")));
Sw->Inputs[ERHIShadingPath::Forward].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("1")));
Sw->Inputs[ERHIShadingPath::Mobile].Connect(0, MakeDefineNode(Material, ParamY, TEXT("SHADERLAB_SHADINGPATH"), TEXT("2")));
AnchorInputs.Add(Sw);
}
}
// Scene-texture reads: the UE_SceneColor/SceneDepth/CustomDepth/CustomStencil/SceneTexture helpers call the
// engine's SceneTextureLookup in HLSL. That only works when the material sets bNeedsSceneTextures, which the
// engine derives from a UMaterialExpressionSceneTexture node being present + compiled. We add one hidden node
// wired into the ParameterAnchor (compiled before attributes, so its Compile always runs and flips the flag).
{
auto BodyUsesScene = [](const FString& Body) -> bool
{
return ReferencesToken(Body, TEXT("UE_SceneColor")) || ReferencesToken(Body, TEXT("UE_SceneDepth"))
|| ReferencesToken(Body, TEXT("UE_CustomDepth")) || ReferencesToken(Body, TEXT("UE_CustomStencil"))
|| ReferencesToken(Body, TEXT("UE_SceneTexture"));
};
bool bUsesScene = Model.bHasSurface && BodyUsesScene(Model.SurfaceBody);
for (const FShaderLabSlab& Slab : Model.Slabs) { bUsesScene |= BodyUsesScene(Slab.Body); }
for (const FShaderLabValue& Value : Model.Values) { bUsesScene |= BodyUsesScene(Value.Body); }
if (bUsesScene)
{
const EShaderLabDomain Dom = Model.Settings.Domain;
const bool bNonOpaqueSurface = (Dom == EShaderLabDomain::Surface)
&& Model.Settings.BlendMode != EShaderLabBlendMode::Opaque
&& Model.Settings.BlendMode != EShaderLabBlendMode::Masked;
const bool bDomainOk = (Dom == EShaderLabDomain::PostProcess) || (Dom == EShaderLabDomain::Decal) || bNonOpaqueSurface;
if (!bDomainOk)
{
OutErrors.Add(TEXT("Scene-texture reads (UE_SceneColor/UE_SceneDepth/UE_SceneTexture/...) require a PostProcess or Decal material, or a translucent (non-opaque) Surface material."));
return false;
}
// The hidden node flips bNeedsSceneTextures. In PostProcess use PostProcessInput0; elsewhere use
// SceneDepth — the only scene-texture family the engine permits outside PostProcess/Decal (scene
// color via SceneTexture is disallowed there, so we don't request it).
UMaterialExpressionSceneTexture* Hidden = NewExpr<UMaterialExpressionSceneTexture>(Material, ParamY, -1300);
Hidden->SceneTextureId = (Dom == EShaderLabDomain::PostProcess) ? PPI_PostProcessInput0 : PPI_SceneDepth;
AnchorInputs.Add(Hidden);
}
}
// Funnel every static-switch selector into the ParameterAnchor. The anchor is a CustomOutput compiled
// BEFORE the material attributes (ShouldCompileBeforeAttributes), so compiling it compiles each
// selector — emitting the selected `#define <Name> 0/1` for the current permutation ahead of every