mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
3579 lines
151 KiB
C++
3579 lines
151 KiB
C++
// Copyright UShaderLab. All Rights Reserved.
|
|
|
|
#include "ShaderLabGraphBuilder.h"
|
|
|
|
#include "ShaderLabModel.h"
|
|
|
|
#include "MaterialDomain.h"
|
|
#include "Engine/EngineTypes.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Engine/Texture.h"
|
|
#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/MaterialExpressionTextureSampleParameter2D.h"
|
|
#include "Materials/MaterialExpressionTextureCoordinate.h"
|
|
#include "Materials/MaterialExpressionRuntimeVirtualTextureSampleParameter.h"
|
|
#include "Materials/MaterialExpressionRuntimeVirtualTextureOutput.h"
|
|
#include "VT/RuntimeVirtualTextureEnum.h"
|
|
#include "VT/RuntimeVirtualTexture.h"
|
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
#include "Materials/MaterialExpressionVertexInterpolator.h"
|
|
#include "MaterialExpressionShaderLabParameterAnchor.h"
|
|
#include "ShaderLabIntrinsicRegistry.h"
|
|
#include "ShaderLabAnchorCapabilityRegistry.h"
|
|
#include "ShaderLabImportResolver.h"
|
|
#include "UObject/Class.h"
|
|
#include "ShaderLabSettingsApplier.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#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")
|
|
|
|
namespace ShaderLabGraph
|
|
{
|
|
// --- Surface fields that map to Substrate Slab pins (in deterministic order). ---
|
|
struct FSlabFieldDef
|
|
{
|
|
const TCHAR* Field;
|
|
ECustomMaterialOutputType OutType;
|
|
};
|
|
|
|
static const FSlabFieldDef GSlabFields[] = {
|
|
{ TEXT("DiffuseAlbedo"), CMOT_Float3 },
|
|
{ TEXT("F0"), CMOT_Float3 },
|
|
{ TEXT("F90"), CMOT_Float3 },
|
|
{ TEXT("Roughness"), CMOT_Float1 },
|
|
{ TEXT("Anisotropy"), CMOT_Float1 },
|
|
{ TEXT("Normal"), CMOT_Float3 },
|
|
{ TEXT("Tangent"), CMOT_Float3 },
|
|
{ TEXT("SSSMFP"), CMOT_Float3 },
|
|
{ TEXT("SSSMFPScale"), CMOT_Float1 },
|
|
{ TEXT("SSSPhaseAnisotropy"), CMOT_Float1 },
|
|
{ TEXT("EmissiveColor"), CMOT_Float3 },
|
|
{ TEXT("SecondRoughness"), CMOT_Float1 },
|
|
{ TEXT("SecondRoughnessWeight"), CMOT_Float1 },
|
|
{ TEXT("FuzzRoughness"), CMOT_Float1 },
|
|
{ TEXT("FuzzAmount"), CMOT_Float1 },
|
|
{ TEXT("FuzzColor"), CMOT_Float3 },
|
|
{ TEXT("GlintValue"), CMOT_Float1 },
|
|
{ TEXT("GlintUV"), CMOT_Float2 },
|
|
};
|
|
|
|
static FExpressionInput* GetSlabPin(UMaterialExpressionSubstrateSlabBSDF* Slab, const FString& Field)
|
|
{
|
|
if (Field == TEXT("DiffuseAlbedo")) return &Slab->DiffuseAlbedo;
|
|
if (Field == TEXT("F0")) return &Slab->F0;
|
|
if (Field == TEXT("F90")) return &Slab->F90;
|
|
if (Field == TEXT("Roughness")) return &Slab->Roughness;
|
|
if (Field == TEXT("Anisotropy")) return &Slab->Anisotropy;
|
|
if (Field == TEXT("Normal")) return &Slab->Normal;
|
|
if (Field == TEXT("Tangent")) return &Slab->Tangent;
|
|
if (Field == TEXT("SSSMFP")) return &Slab->SSSMFP;
|
|
if (Field == TEXT("SSSMFPScale")) return &Slab->SSSMFPScale;
|
|
if (Field == TEXT("SSSPhaseAnisotropy")) return &Slab->SSSPhaseAnisotropy;
|
|
if (Field == TEXT("EmissiveColor")) return &Slab->EmissiveColor;
|
|
if (Field == TEXT("SecondRoughness")) return &Slab->SecondRoughness;
|
|
if (Field == TEXT("SecondRoughnessWeight")) return &Slab->SecondRoughnessWeight;
|
|
if (Field == TEXT("FuzzRoughness")) return &Slab->FuzzRoughness;
|
|
if (Field == TEXT("FuzzAmount")) return &Slab->FuzzAmount;
|
|
if (Field == TEXT("FuzzColor")) return &Slab->FuzzColor;
|
|
if (Field == TEXT("GlintValue")) return &Slab->GlintValue;
|
|
if (Field == TEXT("GlintUV")) return &Slab->GlintUV;
|
|
return nullptr;
|
|
}
|
|
|
|
// --- Vertex-stage output fields. ---
|
|
struct FVertexFieldDef
|
|
{
|
|
const TCHAR* Field;
|
|
ECustomMaterialOutputType OutType;
|
|
};
|
|
static const FVertexFieldDef GVertexFields[] = {
|
|
{ TEXT("WorldPositionOffset"), CMOT_Float3 },
|
|
{ TEXT("Displacement"), CMOT_Float1 },
|
|
{ TEXT("CustomizedUV0"), CMOT_Float2 },
|
|
{ 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. */
|
|
static FString MakeLineDirectivePath(const FString& SourceFilePath)
|
|
{
|
|
FString Full = FPaths::ConvertRelativePathToFull(SourceFilePath);
|
|
Full.ReplaceInline(TEXT("\\"), TEXT("/"));
|
|
return Full;
|
|
}
|
|
|
|
// Virtual-shader-path -> disk resolution is now the shared FShaderLabGraphBuilder::ResolveVirtualShaderFile
|
|
// (defined below, exposed in the header) so the editor module (hot-reload loader) and the VSCode button
|
|
// reuse the same longest-prefix-match logic instead of each hand-rolling it.
|
|
|
|
/**
|
|
* Wrap a user HLSL body so shader-compiler errors map back to the .usl source: a `#line`
|
|
* directive sets the file+line to the body's origin, and a trailing directive points past it to a
|
|
* sentinel so errors in our generated epilogue are not mis-attributed to the user's file.
|
|
*/
|
|
static FString WrapBodyWithLineMapping(const FString& Body, int32 BodyLine, const FString& SrcPath)
|
|
{
|
|
// BodyLine is the source line of the char right after '{' (usually the newline ending that
|
|
// line); the body's real content starts on the next line. Empirically the compiler reports
|
|
// content one line high relative to `#line BodyLine`, so map with BodyLine-1.
|
|
const int32 MappedLine = FMath::Max(BodyLine - 1, 1);
|
|
return FString::Printf(TEXT("#line %d \"%s\"\n%s\n#line 1 \"ShaderLabGenerated.ush\"\n"),
|
|
MappedLine, *SrcPath, *Body);
|
|
}
|
|
|
|
/** True if `Token` appears in `Body` delimited by non-identifier characters. */
|
|
static bool ReferencesToken(const FString& Body, const FString& Token)
|
|
{
|
|
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
|
int32 From = 0;
|
|
while (true)
|
|
{
|
|
const int32 Idx = Body.Find(Token, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
|
|
if (Idx == INDEX_NONE)
|
|
{
|
|
return false;
|
|
}
|
|
const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' ');
|
|
const int32 AfterIdx = Idx + Token.Len();
|
|
const TCHAR After = (AfterIdx < Body.Len()) ? Body[AfterIdx] : TEXT(' ');
|
|
if (!IsIdent(Before) && !IsIdent(After))
|
|
{
|
|
return true;
|
|
}
|
|
From = Idx + Token.Len();
|
|
}
|
|
}
|
|
|
|
static UTexture* ResolveDefaultTexture(const FString& Token, bool& bOutIsNormal)
|
|
{
|
|
bOutIsNormal = (Token == TEXT("normal"));
|
|
const TCHAR* Path = nullptr;
|
|
if (Token == TEXT("white")) { Path = TEXT("/Engine/EngineResources/WhiteSquareTexture.WhiteSquareTexture"); }
|
|
else if (Token == TEXT("black")) { Path = TEXT("/Engine/EngineResources/Black.Black"); }
|
|
else if (Token == TEXT("grey") || Token == TEXT("gray")) { Path = TEXT("/Engine/EngineResources/GreyTexture.GreyTexture"); }
|
|
else if (Token == TEXT("normal")) { Path = TEXT("/Engine/EngineMaterials/DefaultNormal.DefaultNormal"); }
|
|
|
|
UTexture* Tex = nullptr;
|
|
if (Path)
|
|
{
|
|
Tex = LoadObject<UTexture>(nullptr, Path);
|
|
}
|
|
else if (!Token.IsEmpty())
|
|
{
|
|
Tex = LoadObject<UTexture>(nullptr, *Token);
|
|
}
|
|
if (!Tex)
|
|
{
|
|
Tex = LoadObject<UTexture>(nullptr, TEXT("/Engine/EngineResources/WhiteSquareTexture.WhiteSquareTexture"));
|
|
}
|
|
return Tex;
|
|
}
|
|
|
|
/**
|
|
* Map a non-virtual SamplerType token (SL_PROPERTY(SamplerType=...)) to EMaterialSamplerType. Returns
|
|
* false for an unknown or Virtual* token (Virtual belongs to SL_VTSAMPLE, not a plain texture property).
|
|
*/
|
|
static bool MapSamplerType(const FString& Token, EMaterialSamplerType& Out)
|
|
{
|
|
if (Token == TEXT("Color")) { Out = SAMPLERTYPE_Color; return true; }
|
|
if (Token == TEXT("LinearColor")) { Out = SAMPLERTYPE_LinearColor; return true; }
|
|
if (Token == TEXT("Grayscale")) { Out = SAMPLERTYPE_Grayscale; return true; }
|
|
if (Token == TEXT("LinearGrayscale")) { Out = SAMPLERTYPE_LinearGrayscale; return true; }
|
|
if (Token == TEXT("Alpha")) { Out = SAMPLERTYPE_Alpha; return true; }
|
|
if (Token == TEXT("Normal")) { Out = SAMPLERTYPE_Normal; return true; }
|
|
if (Token == TEXT("Masks")) { Out = SAMPLERTYPE_Masks; return true; }
|
|
if (Token == TEXT("DistanceFieldFont")) { Out = SAMPLERTYPE_DistanceFieldFont; return true; }
|
|
if (Token == TEXT("Data")) { Out = SAMPLERTYPE_Data; return true; }
|
|
return false;
|
|
}
|
|
|
|
/** Map a Virtual* SamplerType token (SL_VTSAMPLE(SamplerType=...)) to EMaterialSamplerType. */
|
|
static bool MapVirtualSamplerType(const FString& Token, EMaterialSamplerType& Out)
|
|
{
|
|
if (Token == TEXT("VirtualColor")) { Out = SAMPLERTYPE_VirtualColor; return true; }
|
|
if (Token == TEXT("VirtualGrayscale")) { Out = SAMPLERTYPE_VirtualGrayscale; return true; }
|
|
if (Token == TEXT("VirtualAlpha")) { Out = SAMPLERTYPE_VirtualAlpha; return true; }
|
|
if (Token == TEXT("VirtualNormal")) { Out = SAMPLERTYPE_VirtualNormal; return true; }
|
|
if (Token == TEXT("VirtualMasks")) { Out = SAMPLERTYPE_VirtualMasks; return true; }
|
|
if (Token == TEXT("VirtualLinearColor")) { Out = SAMPLERTYPE_VirtualLinearColor; return true; }
|
|
if (Token == TEXT("VirtualLinearGrayscale")) { Out = SAMPLERTYPE_VirtualLinearGrayscale; return true; }
|
|
return false;
|
|
}
|
|
|
|
/** Map an SL_RVTSAMPLE MaterialType token to ERuntimeVirtualTextureMaterialType. */
|
|
static bool MapRVTMaterialType(const FString& Token, ERuntimeVirtualTextureMaterialType& Out)
|
|
{
|
|
if (Token == TEXT("BaseColor")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor; return true; }
|
|
if (Token == TEXT("Mask4")) { Out = ERuntimeVirtualTextureMaterialType::Mask4; return true; }
|
|
if (Token == TEXT("BaseColor_Normal_Roughness")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Roughness; return true; }
|
|
if (Token == TEXT("BaseColor_Normal_Specular")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular; return true; }
|
|
if (Token == TEXT("BaseColor_Normal_Specular_YCoCg")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_YCoCg; return true; }
|
|
if (Token == TEXT("BaseColor_Normal_Specular_Mask_YCoCg")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_Mask_YCoCg; return true; }
|
|
if (Token == TEXT("WorldHeight")) { Out = ERuntimeVirtualTextureMaterialType::WorldHeight; return true; }
|
|
if (Token == TEXT("Displacement")) { Out = ERuntimeVirtualTextureMaterialType::Displacement; return true; }
|
|
return false;
|
|
}
|
|
|
|
// FShaderLabRVT member -> RuntimeVirtualTextureSample output pin index + Custom-input HLSL type.
|
|
// Pin order mirrors UMaterialExpressionRuntimeVirtualTextureSample::InitOutputs (MaterialExpressions.cpp).
|
|
struct FRVTMemberDef { const TCHAR* Member; int32 PinIndex; ECustomMaterialOutputType OutType; };
|
|
static const FRVTMemberDef GRVTMembers[] = {
|
|
{ TEXT("BaseColor"), 0, CMOT_Float3 },
|
|
{ TEXT("Specular"), 1, CMOT_Float1 },
|
|
{ TEXT("Roughness"), 2, CMOT_Float1 },
|
|
{ TEXT("Normal"), 3, CMOT_Float3 },
|
|
{ TEXT("WorldHeight"), 4, CMOT_Float1 },
|
|
{ TEXT("Mask"), 5, CMOT_Float1 },
|
|
{ TEXT("Displacement"), 6, CMOT_Float1 },
|
|
{ TEXT("Mask4"), 7, CMOT_Float4 },
|
|
};
|
|
|
|
// FShaderLabRVTOutput field -> RuntimeVirtualTextureOutput input pin + Custom-output HLSL type.
|
|
struct FRVTOutFieldDef { const TCHAR* Field; ECustomMaterialOutputType OutType; };
|
|
static const FRVTOutFieldDef GRVTOutFields[] = {
|
|
{ TEXT("BaseColor"), CMOT_Float3 },
|
|
{ TEXT("Specular"), CMOT_Float1 },
|
|
{ TEXT("Roughness"), CMOT_Float1 },
|
|
{ TEXT("Normal"), CMOT_Float3 },
|
|
{ TEXT("WorldHeight"), CMOT_Float1 },
|
|
{ TEXT("Opacity"), CMOT_Float1 },
|
|
{ TEXT("Mask"), CMOT_Float1 },
|
|
{ TEXT("Displacement"), CMOT_Float1 },
|
|
{ TEXT("Mask4"), CMOT_Float4 },
|
|
};
|
|
static FExpressionInput* GetRVTOutputPin(UMaterialExpressionRuntimeVirtualTextureOutput* N, const FString& F)
|
|
{
|
|
if (F == TEXT("BaseColor")) return &N->BaseColor;
|
|
if (F == TEXT("Specular")) return &N->Specular;
|
|
if (F == TEXT("Roughness")) return &N->Roughness;
|
|
if (F == TEXT("Normal")) return &N->Normal;
|
|
if (F == TEXT("WorldHeight")) return &N->WorldHeight;
|
|
if (F == TEXT("Opacity")) return &N->Opacity;
|
|
if (F == TEXT("Mask")) return &N->Mask;
|
|
if (F == TEXT("Displacement")) return &N->Displacement;
|
|
if (F == TEXT("Mask4")) return &N->Mask4;
|
|
return nullptr;
|
|
}
|
|
|
|
static EMaterialDomain MapDomain(EShaderLabDomain D)
|
|
{
|
|
switch (D)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
static T* NewExpr(UMaterial& Material, int32& IoY, int32 Column);
|
|
|
|
// A Substrate material in the Decal domain must route its BSDF through a SubstrateConvertToDecal node — that
|
|
// node flags the material with the SSM_Decal shading model, without which the engine's Substrate sanitization
|
|
// (Material.cpp) silently resets MaterialDomain back to MD_Surface (→ DecalComponent then rejects it with
|
|
// "Decal Material must use Deferred Decal Material Domain"). This mirrors exactly what the material editor
|
|
// inserts when you pick the Deferred Decal domain. Returns the node to connect to FrontMaterial (the wrapper
|
|
// for Decal, otherwise the BSDF unchanged).
|
|
static UMaterialExpression* WrapBsdfForDecal(UMaterial& Material, const FShaderLabModel& Model, UMaterialExpression* Bsdf, int32& IoY)
|
|
{
|
|
if (Model.Settings.Domain != EShaderLabDomain::Decal)
|
|
{
|
|
return Bsdf;
|
|
}
|
|
UMaterialExpressionSubstrateConvertToDecal* Node = NewExpr<UMaterialExpressionSubstrateConvertToDecal>(Material, IoY, 300);
|
|
Node->DecalMaterial.Connect(0, Bsdf);
|
|
return Node;
|
|
}
|
|
|
|
static EBlendMode MapBlend(EShaderLabBlendMode B)
|
|
{
|
|
switch (B)
|
|
{
|
|
case EShaderLabBlendMode::Masked: return BLEND_Masked;
|
|
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;
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
static T* NewExpr(UMaterial& Material, int32& IoY, int32 Column)
|
|
{
|
|
T* Expr = NewObject<T>(&Material);
|
|
Material.GetExpressionCollection().AddExpression(Expr);
|
|
Expr->MaterialExpressionEditorX = Column;
|
|
Expr->MaterialExpressionEditorY = IoY;
|
|
IoY += 120;
|
|
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("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 },
|
|
{ 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
|
|
{
|
|
FName InputName;
|
|
UMaterialExpression* Expr = nullptr;
|
|
int32 OutputIndex = 0;
|
|
};
|
|
|
|
/**
|
|
* Pre-sampled VT/RVT read nodes available to a (pixel) body, keyed by the DSL name. Because a virtual
|
|
* texture cannot be sampled inside an opaque Custom node, these real sample nodes are built up-front and
|
|
* their outputs wired into the body as inputs by PrepareBody: a VT `<Name>` becomes a float4 input; an
|
|
* RVT `<Name>.<Member>` is rewritten to a per-member input carrying the matching output pin. Null maps
|
|
* (vertex/interpolator bodies) mean "no VT/RVT here" — a reference there is rejected as pixel-only.
|
|
*/
|
|
struct FSampleNodes
|
|
{
|
|
const TMap<FName, UMaterialExpression*>* VT = nullptr; // name -> UMaterialExpressionTextureSampleParameter2D (Virtual*)
|
|
const TMap<FName, UMaterialExpression*>* RVT = nullptr; // name -> UMaterialExpressionRuntimeVirtualTextureSampleParameter
|
|
bool HasAny() const { return (VT && VT->Num() > 0) || (RVT && RVT->Num() > 0); }
|
|
};
|
|
|
|
/** Split a call's argument text into trimmed, top-level comma-separated literals. */
|
|
static TArray<FString> SplitArgs(const FString& ArgsRaw)
|
|
{
|
|
TArray<FString> Out;
|
|
if (ArgsRaw.TrimStartAndEnd().IsEmpty())
|
|
{
|
|
return Out;
|
|
}
|
|
ArgsRaw.ParseIntoArray(Out, TEXT(","), /*CullEmpty*/ false);
|
|
for (FString& A : Out)
|
|
{
|
|
A.TrimStartAndEndInline();
|
|
}
|
|
return Out;
|
|
}
|
|
|
|
/** A stable, identifier-safe suffix encoding a call's literal args (e.g. "0, 2.0" -> "0_2_0"). */
|
|
static FString MakeArgSig(const FString& ArgsRaw)
|
|
{
|
|
FString Sig;
|
|
for (const TCHAR C : ArgsRaw)
|
|
{
|
|
if (FChar::IsAlnum(C)) { Sig.AppendChar(C); }
|
|
else if (!FChar::IsWhitespace(C)) { Sig.AppendChar(TEXT('_')); }
|
|
}
|
|
return Sig;
|
|
}
|
|
|
|
/**
|
|
* Scan a body for `UE_Name(args)` intrinsic calls, create the backing expression node for each
|
|
* unique (name,args), collect the resulting Custom-node inputs, and rewrite the body so each call
|
|
* becomes its input variable — space-padded to the original call's length so line/column layout is
|
|
* preserved (keeps `#line` compile-error mapping accurate). Returns false (and fills OutErrors) on
|
|
* an unknown intrinsic, a stage/usage violation, or a bad argument.
|
|
*/
|
|
/** True if `Arg` is a numeric literal (the only non-enum arg an intrinsic's const config accepts). */
|
|
static bool IsNumericLiteral(const FString& Arg)
|
|
{
|
|
const FString T = Arg.TrimStartAndEnd();
|
|
if (T.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
bool bAnyDigit = false;
|
|
for (int32 i = 0; i < T.Len(); ++i)
|
|
{
|
|
const TCHAR C = T[i];
|
|
if (FChar::IsDigit(C)) { bAnyDigit = true; }
|
|
else if (C == TEXT('.') || C == TEXT('+') || C == TEXT('-')
|
|
|| C == TEXT('e') || C == TEXT('E') || C == TEXT('f') || C == TEXT('F')) { /* allowed */ }
|
|
else { return false; }
|
|
}
|
|
return bAnyDigit;
|
|
}
|
|
|
|
static bool EmitIntrinsics(
|
|
UMaterial& Material,
|
|
EShaderLabIntrinsicFrequency Stage,
|
|
FString& InOutBody,
|
|
int32 BodyLine,
|
|
const FString& SrcPath,
|
|
TArray<FIntrinsicWire>& OutWires,
|
|
const TMap<FName, UMaterialExpression*>& InterpByName,
|
|
TSet<FName>& UsedInterps,
|
|
TArray<FString>& OutErrors)
|
|
{
|
|
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
|
|
const FString& Body = InOutBody;
|
|
const int32 Len = Body.Len();
|
|
|
|
FString Result;
|
|
Result.Reserve(Len);
|
|
TMap<FString, FName> InputByKey; // (Name + argsig) -> already-created input name (dedup)
|
|
bool bOk = true;
|
|
|
|
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
|
|
|
// Format a `path.usl(line,col): error: ` prefix from a body offset, so intrinsic diagnostics map
|
|
// to the real .usl location and flow through the same clickable/cook-failing path as compile errors.
|
|
auto Loc = [&Body, BodyLine, &SrcPath](int32 Offset) -> FString
|
|
{
|
|
int32 Line = BodyLine;
|
|
int32 Col = 1;
|
|
for (int32 p = 0; p < Offset && p < Body.Len(); ++p)
|
|
{
|
|
if (Body[p] == TEXT('\n')) { ++Line; Col = 1; }
|
|
else { ++Col; }
|
|
}
|
|
return FString::Printf(TEXT("%s(%d,%d): error: "), *SrcPath, Line, Col);
|
|
};
|
|
|
|
int32 i = 0;
|
|
while (i < Len)
|
|
{
|
|
const bool bBoundary = (i == 0) || !IsIdent(Body[i - 1]);
|
|
if (bBoundary && i + 3 <= Len &&
|
|
Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT('_'))
|
|
{
|
|
int32 j = i + 3;
|
|
while (j < Len && IsIdent(Body[j])) { ++j; }
|
|
const FString Name = Body.Mid(i + 3, j - (i + 3));
|
|
int32 k = j;
|
|
while (k < Len && FChar::IsWhitespace(Body[k])) { ++k; }
|
|
if (!Name.IsEmpty() && k < Len && Body[k] == TEXT('('))
|
|
{
|
|
// Read balanced (...) for the argument list.
|
|
int32 Depth = 0;
|
|
int32 m = k;
|
|
for (; m < Len; ++m)
|
|
{
|
|
if (Body[m] == TEXT('(')) { ++Depth; }
|
|
else if (Body[m] == TEXT(')')) { if (--Depth == 0) { break; } }
|
|
}
|
|
if (m < Len)
|
|
{
|
|
const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1));
|
|
const int32 CallLen = (m + 1) - i;
|
|
|
|
// UE_Interpolator(Name): read a Vertex Interpolator's value in the pixel shader. Not a
|
|
// registry intrinsic — its arg is an interpolator name (not a numeric/enum literal), so
|
|
// handle it before the registry lookup and arg-literal validation below.
|
|
if (Name == TEXT("Interpolator"))
|
|
{
|
|
const FString InterpNameStr = ArgsRaw.TrimStartAndEnd();
|
|
FName InputName; // None on error -> substituted blank (error already recorded)
|
|
if (Stage == EShaderLabIntrinsicFrequency::VertexOnly)
|
|
{
|
|
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' is pixel-only and cannot be used in a Vertex or Interpolator body"));
|
|
bOk = false;
|
|
}
|
|
else if (InterpNameStr.IsEmpty())
|
|
{
|
|
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' requires an interpolator name argument"));
|
|
bOk = false;
|
|
}
|
|
else
|
|
{
|
|
const FString Key = FString(TEXT("Interpolator|")) + InterpNameStr;
|
|
if (const FName* Existing = InputByKey.Find(Key))
|
|
{
|
|
InputName = *Existing;
|
|
}
|
|
else if (UMaterialExpression* const* Node = InterpByName.Find(FName(*InterpNameStr)))
|
|
{
|
|
InputName = FName(*(FString(TEXT("SLI_Interp_")) + InterpNameStr));
|
|
InputByKey.Add(Key, InputName);
|
|
OutWires.Add(FIntrinsicWire{ InputName, *Node, 0 });
|
|
UsedInterps.Add(FName(*InterpNameStr));
|
|
}
|
|
else
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_Interpolator' references unknown interpolator '%s'"), *InterpNameStr));
|
|
bOk = false;
|
|
}
|
|
}
|
|
FString Replacement = InputName.IsNone() ? FString() : InputName.ToString();
|
|
while (Replacement.Len() < CallLen) { Replacement.AppendChar(TEXT(' ')); }
|
|
Result += Replacement;
|
|
i = m + 1;
|
|
continue;
|
|
}
|
|
|
|
// Only registered node-intrinsics are rewritten into material-expression inputs.
|
|
// Any other `UE_X(...)` is left for the shader compiler: it's an HLSL library
|
|
// function (e.g. UE_Noise from ShaderLabFunctions.ush, auto-included) or a typo.
|
|
// Copy just the `UE_Name` identifier and keep scanning from the '(' — so any
|
|
// intrinsic nested in the arguments (e.g. UE_Noise(UE_WorldPosition(), ...)) still
|
|
// gets rewritten.
|
|
//
|
|
// Overloaded intrinsics (bAllowRawHelperWithArgs) also fall through here when the call
|
|
// carries arguments: the nullary form is the wired intrinsic, but `UE_Name(<args>)` is a
|
|
// same-named raw HLSL helper — leave it for the shader compiler (its side-effect binding
|
|
// is handled by an anchor capability).
|
|
const FShaderLabIntrinsicDesc* Found = Registry.Find(FName(*Name));
|
|
const bool bRawHelperOverload = Found && Found->bAllowRawHelperWithArgs
|
|
&& SplitArgs(ArgsRaw).Num() > Found->Params.Num();
|
|
if (!Found || bRawHelperOverload)
|
|
{
|
|
Result += Body.Mid(i, j - i);
|
|
i = j;
|
|
continue;
|
|
}
|
|
|
|
const FString ArgSig = MakeArgSig(ArgsRaw);
|
|
const FString Key = Name + TEXT("|") + ArgSig;
|
|
|
|
FName InputName;
|
|
if (const FName* Existing = InputByKey.Find(Key))
|
|
{
|
|
InputName = *Existing;
|
|
}
|
|
else
|
|
{
|
|
const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name));
|
|
if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is pixel-only and cannot be used in a Vertex body"), *Name));
|
|
bOk = false;
|
|
}
|
|
else if (Desc->Frequency == EShaderLabIntrinsicFrequency::VertexOnly && Stage == EShaderLabIntrinsicFrequency::PixelOnly)
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is vertex-only and cannot be used in a pixel body"), *Name));
|
|
bOk = false;
|
|
}
|
|
else
|
|
{
|
|
// Config args are baked into the node at graph-build time, so they must be
|
|
// compile-time constants (numeric literals, or the enum's token names) — a
|
|
// variable can't configure a node field. Reject non-literals instead of
|
|
// silently coercing them (e.g. Atoi("myVar") -> 0).
|
|
const TArray<FString> CallArgs = SplitArgs(ArgsRaw);
|
|
bool bArgsOk = true;
|
|
if (CallArgs.Num() > Desc->Params.Num())
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' takes at most %d argument(s), got %d"),
|
|
*Name, Desc->Params.Num(), CallArgs.Num()));
|
|
bArgsOk = false;
|
|
}
|
|
for (int32 a = 0; bArgsOk && a < CallArgs.Num(); ++a)
|
|
{
|
|
const FShaderLabIntrinsicParam& P = Desc->Params[a];
|
|
if (P.Enum)
|
|
{
|
|
if (P.Enum->GetValueByNameString(CallArgs[a]) == INDEX_NONE)
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a %s token, got '%s'"),
|
|
*Name, *P.Name, *P.Enum->GetName(), *CallArgs[a]));
|
|
bArgsOk = false;
|
|
}
|
|
}
|
|
else if (!IsNumericLiteral(CallArgs[a]))
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a compile-time constant, got '%s'"),
|
|
*Name, *P.Name, *CallArgs[a]));
|
|
bArgsOk = false;
|
|
}
|
|
}
|
|
|
|
FString MakeError;
|
|
UMaterialExpression* Expr = bArgsOk ? Desc->MakeNode(Material, CallArgs, MakeError) : nullptr;
|
|
if (!bArgsOk)
|
|
{
|
|
bOk = false;
|
|
}
|
|
else if (!Expr)
|
|
{
|
|
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s': %s"), *Name,
|
|
MakeError.IsEmpty() ? TEXT("failed to create node") : *MakeError));
|
|
bOk = false;
|
|
}
|
|
else
|
|
{
|
|
InputName = FName(*(FString(TEXT("SLI_")) + Name + (ArgSig.IsEmpty() ? TEXT("") : (FString(TEXT("_")) + ArgSig))));
|
|
InputByKey.Add(Key, InputName);
|
|
OutWires.Add(FIntrinsicWire{ InputName, Expr, Desc->OutputIndex });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Substitute the call with its input variable, space-padded to keep columns stable.
|
|
FString Replacement = InputName.IsNone() ? FString() : InputName.ToString();
|
|
while (Replacement.Len() < CallLen) { Replacement.AppendChar(TEXT(' ')); }
|
|
Result += Replacement;
|
|
i = m + 1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
Result.AppendChar(Body[i]);
|
|
++i;
|
|
}
|
|
|
|
InOutBody = MoveTemp(Result);
|
|
return bOk;
|
|
}
|
|
|
|
// =====================================================================================================
|
|
// Function-library emission (`.uslfunc` imports).
|
|
//
|
|
// A library function is reusable HLSL that may use `UE_` intrinsics, its library's promoted properties,
|
|
// and other library functions. Custom nodes are opaque HLSL blocks — a node-local value can't be wired
|
|
// out to another node — so we can't turn a library function into a separate graph node. Instead each
|
|
// function is emitted to the shader's generated `.gen.ush`, REWRITTEN to take the properties/intrinsics
|
|
// it (transitively) needs as trailing parameters, and every call site (in a body or in another function)
|
|
// is rewritten to pass those. The consuming Custom node wires the matching parameter/intrinsic nodes as
|
|
// node-local inputs so they are available to pass in. Everything downstream works in the promoted-name
|
|
// space produced by the resolver.
|
|
// =====================================================================================================
|
|
|
|
/** One `UE_Name(args)` intrinsic use pulled out of a function body. */
|
|
struct FIntrinsicUse
|
|
{
|
|
FString Name;
|
|
FString ArgsRaw;
|
|
FString ArgSig; // identifier-safe signature of ArgsRaw (matches EmitIntrinsics' naming)
|
|
};
|
|
|
|
/** The Custom-node input / function-parameter variable name for an intrinsic use. */
|
|
static FString MakeIntrinsicVar(const FString& Name, const FString& ArgSig)
|
|
{
|
|
return FString(TEXT("SLI_")) + Name + (ArgSig.IsEmpty() ? TEXT("") : (FString(TEXT("_")) + ArgSig));
|
|
}
|
|
|
|
/** Transitive context a library function needs: promoted property names + intrinsic uses (stable order). */
|
|
struct FFunctionCtx
|
|
{
|
|
TArray<FName> ReqProps;
|
|
TArray<FIntrinsicUse> ReqIntr;
|
|
int32 State = 0; // 0 = not computed, 1 = computing (recursion guard), 2 = done
|
|
};
|
|
|
|
/** Resolved program + per-function context, computed once per BuildInto. */
|
|
struct FLibraryEmit
|
|
{
|
|
const FShaderLabResolvedProgram* Program = nullptr;
|
|
TMap<FName, FFunctionCtx> Ctx;
|
|
bool HasLibraries() const { return Program && Program->Libraries.Num() > 0; }
|
|
};
|
|
|
|
/**
|
|
* Create the backing expression node for an intrinsic use (registry lookup + stage/arg validation).
|
|
* Mirrors EmitIntrinsics' registry path; used to wire intrinsics that a called library function needs
|
|
* but that do not appear literally in the calling body. Returns false + OutError on any violation.
|
|
*/
|
|
static bool CreateIntrinsicNode(UMaterial& Material, const FString& Name, const FString& ArgsRaw,
|
|
EShaderLabIntrinsicFrequency Stage, UMaterialExpression*& OutExpr, int32& OutOutputIndex, FString& OutError)
|
|
{
|
|
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
|
|
const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name));
|
|
if (!Desc)
|
|
{
|
|
OutError = FString::Printf(TEXT("unknown intrinsic 'UE_%s'"), *Name);
|
|
return false;
|
|
}
|
|
if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s' is pixel-only and cannot be used in a Vertex body"), *Name);
|
|
return false;
|
|
}
|
|
if (Desc->Frequency == EShaderLabIntrinsicFrequency::VertexOnly && Stage == EShaderLabIntrinsicFrequency::PixelOnly)
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s' is vertex-only and cannot be used in a pixel body"), *Name);
|
|
return false;
|
|
}
|
|
const TArray<FString> CallArgs = SplitArgs(ArgsRaw);
|
|
if (CallArgs.Num() > Desc->Params.Num())
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s' takes at most %d argument(s), got %d"), *Name, Desc->Params.Num(), CallArgs.Num());
|
|
return false;
|
|
}
|
|
for (int32 a = 0; a < CallArgs.Num(); ++a)
|
|
{
|
|
const FShaderLabIntrinsicParam& P = Desc->Params[a];
|
|
if (P.Enum)
|
|
{
|
|
if (P.Enum->GetValueByNameString(CallArgs[a]) == INDEX_NONE)
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a %s token, got '%s'"), *Name, *P.Name, *P.Enum->GetName(), *CallArgs[a]);
|
|
return false;
|
|
}
|
|
}
|
|
else if (!IsNumericLiteral(CallArgs[a]))
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a compile-time constant, got '%s'"), *Name, *P.Name, *CallArgs[a]);
|
|
return false;
|
|
}
|
|
}
|
|
FString MakeError;
|
|
UMaterialExpression* Expr = Desc->MakeNode(Material, CallArgs, MakeError);
|
|
if (!Expr)
|
|
{
|
|
OutError = FString::Printf(TEXT("intrinsic 'UE_%s': %s"), *Name, MakeError.IsEmpty() ? TEXT("failed to create node") : *MakeError);
|
|
return false;
|
|
}
|
|
OutExpr = Expr;
|
|
OutOutputIndex = Desc->OutputIndex;
|
|
return true;
|
|
}
|
|
|
|
/** The HLSL parameter type token for a promoted property (textures also emit a paired sampler). */
|
|
static void AppendPropParam(const FShaderLabProperty& Prop, TArray<FString>& OutParams)
|
|
{
|
|
const FString Name = Prop.Name.ToString();
|
|
switch (Prop.Type)
|
|
{
|
|
case EShaderLabPropertyType::Scalar: OutParams.Add(FString::Printf(TEXT("float %s"), *Name)); break;
|
|
case EShaderLabPropertyType::Color: OutParams.Add(FString::Printf(TEXT("float3 %s"), *Name)); break;
|
|
case EShaderLabPropertyType::Vector: OutParams.Add(FString::Printf(TEXT("float4 %s"), *Name)); break;
|
|
case EShaderLabPropertyType::Texture2D:
|
|
OutParams.Add(FString::Printf(TEXT("Texture2D %s"), *Name));
|
|
OutParams.Add(FString::Printf(TEXT("SamplerState %sSampler"), *Name));
|
|
break;
|
|
case EShaderLabPropertyType::TextureCube:
|
|
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).
|
|
}
|
|
}
|
|
|
|
/** The call-site argument(s) for a promoted property (textures pass texture + sampler). */
|
|
static void AppendPropArg(const FShaderLabProperty& Prop, TArray<FString>& OutArgs)
|
|
{
|
|
const FString Name = Prop.Name.ToString();
|
|
OutArgs.Add(Name);
|
|
if (ShaderLabIsTextureType(Prop.Type))
|
|
{
|
|
OutArgs.Add(Name + TEXT("Sampler"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build the trailing parameter-declaration list (bArgs=false) or call-argument list (bArgs=true) for a
|
|
* function context. Iterated in the same stable order for both so params and args line up positionally.
|
|
*/
|
|
static FString BuildTrailing(const FFunctionCtx& Ctx, const FShaderLabResolvedProgram& Program, bool bArgs)
|
|
{
|
|
TArray<FString> Parts;
|
|
for (const FName& PropName : Ctx.ReqProps)
|
|
{
|
|
const FShaderLabProperty* Prop = Program.Properties.FindByPredicate(
|
|
[PropName](const FShaderLabProperty& P) { return P.Name == PropName; });
|
|
if (!Prop)
|
|
{
|
|
continue; // Defensive: resolver guarantees membership; skip if somehow absent.
|
|
}
|
|
if (bArgs) { AppendPropArg(*Prop, Parts); }
|
|
else { AppendPropParam(*Prop, Parts); }
|
|
}
|
|
for (const FIntrinsicUse& Use : Ctx.ReqIntr)
|
|
{
|
|
const FString Var = MakeIntrinsicVar(Use.Name, Use.ArgSig);
|
|
if (bArgs)
|
|
{
|
|
Parts.Add(Var);
|
|
}
|
|
else
|
|
{
|
|
const FShaderLabIntrinsicDesc* Desc = FShaderLabIntrinsicRegistry::Get().Find(FName(*Use.Name));
|
|
const FString Type = (Desc && !Desc->ReturnType.IsEmpty()) ? Desc->ReturnType : FString(TEXT("float"));
|
|
Parts.Add(FString::Printf(TEXT("%s %s"), *Type, *Var));
|
|
}
|
|
}
|
|
return FString::Join(Parts, TEXT(", "));
|
|
}
|
|
|
|
/** True at an identifier start position (previous char is not part of an identifier). */
|
|
static bool IsIdentBoundary(const FString& B, int32 i)
|
|
{
|
|
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
|
return (i == 0 || !IsIdent(B[i - 1])) && (IsIdent(B[i]) || B[i] == TEXT('_'));
|
|
}
|
|
|
|
/** Index just past an identifier starting at i (assumes IsIdentBoundary(B,i)). */
|
|
static int32 IdentEnd(const FString& B, int32 i)
|
|
{
|
|
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
|
int32 j = i;
|
|
while (j < B.Len() && IsIdent(B[j])) { ++j; }
|
|
return j;
|
|
}
|
|
|
|
/** First non-whitespace index at/after i (B.Len() if none). */
|
|
static int32 SkipSpace(const FString& B, int32 i)
|
|
{
|
|
while (i < B.Len() && FChar::IsWhitespace(B[i])) { ++i; }
|
|
return i;
|
|
}
|
|
|
|
/** Scan a function body for its DIRECT promoted-property refs, intrinsic uses, and callee names. */
|
|
static bool ScanDirectRefs(const FString& Body, const FShaderLabResolvedLibrary& OwnerLib,
|
|
const FShaderLabResolvedProgram& Program, const FString& SrcPath, int32 BodyLine,
|
|
TSet<FName>& OutProps, TArray<FIntrinsicUse>& OutIntr, TSet<FName>& OutCallees, TArray<FString>& OutErrors)
|
|
{
|
|
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
|
|
|
|
// Promoted properties visible in this library are found by whole-word reference.
|
|
for (const TPair<FName, FName>& Pair : OwnerLib.PropRewrite)
|
|
{
|
|
if (Program.PropertyOwner.Contains(Pair.Value) && ReferencesToken(Body, Pair.Key.ToString()))
|
|
{
|
|
OutProps.Add(Pair.Value);
|
|
}
|
|
}
|
|
|
|
const int32 Len = Body.Len();
|
|
int32 i = 0;
|
|
bool bOk = true;
|
|
while (i < Len)
|
|
{
|
|
const TCHAR C = Body[i];
|
|
// Skip comments and string/char literals so tokens inside them are not treated as code.
|
|
if (C == TEXT('/') && i + 1 < Len && Body[i + 1] == TEXT('/')) { while (i < Len && Body[i] != TEXT('\n')) { ++i; } continue; }
|
|
if (C == TEXT('/') && i + 1 < Len && Body[i + 1] == TEXT('*')) { i += 2; while (i + 1 < Len && !(Body[i] == TEXT('*') && Body[i + 1] == TEXT('/'))) { ++i; } i += 2; continue; }
|
|
if (C == TEXT('"') || C == TEXT('\'')) { const TCHAR Q = C; ++i; while (i < Len && Body[i] != Q) { if (Body[i] == TEXT('\\')) { ++i; } ++i; } ++i; continue; }
|
|
if (!IsIdentBoundary(Body, i)) { ++i; continue; }
|
|
|
|
const int32 e = IdentEnd(Body, i);
|
|
const FString Ident = Body.Mid(i, e - i);
|
|
const int32 paren = SkipSpace(Body, e);
|
|
const bool bCall = (paren < Len && Body[paren] == TEXT('('));
|
|
|
|
if (bCall && Ident.StartsWith(TEXT("UE_")))
|
|
{
|
|
const FString Name = Ident.Mid(3);
|
|
if (Name == TEXT("Interpolator"))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("%s(%d,1): error: UE_Interpolator cannot be used inside a library function"), *SrcPath, FMath::Max(BodyLine, 1)));
|
|
bOk = false;
|
|
i = e;
|
|
continue;
|
|
}
|
|
if (const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name)))
|
|
{
|
|
// Registry intrinsic: read its balanced (...) and record the use (args are literals).
|
|
int32 depth = 0, m = paren;
|
|
for (; m < Len; ++m) { if (Body[m] == TEXT('(')) { ++depth; } else if (Body[m] == TEXT(')')) { if (--depth == 0) { break; } } }
|
|
const FString ArgsRaw = (m < Len) ? Body.Mid(paren + 1, m - (paren + 1)) : FString();
|
|
// Overloaded intrinsic called with args: it's the same-named raw HLSL helper, not the wired
|
|
// intrinsic — leave it and keep scanning its arguments (like an unknown UE_ helper below).
|
|
if (Desc->bAllowRawHelperWithArgs && SplitArgs(ArgsRaw).Num() > Desc->Params.Num())
|
|
{
|
|
i = e;
|
|
continue;
|
|
}
|
|
const FString ArgSig = MakeArgSig(ArgsRaw);
|
|
if (!OutIntr.ContainsByPredicate([&](const FIntrinsicUse& U) { return U.Name == Name && U.ArgSig == ArgSig; }))
|
|
{
|
|
OutIntr.Add(FIntrinsicUse{ Name, ArgsRaw, ArgSig });
|
|
}
|
|
i = (m < Len) ? m + 1 : e;
|
|
continue;
|
|
}
|
|
// Unknown UE_ (an HLSL library helper like UE_Noise): leave it; keep scanning its args.
|
|
i = e;
|
|
continue;
|
|
}
|
|
|
|
if (bCall && Program.FunctionOwnerLibrary.Contains(FName(*Ident)))
|
|
{
|
|
OutCallees.Add(FName(*Ident));
|
|
}
|
|
i = e;
|
|
}
|
|
return bOk;
|
|
}
|
|
|
|
/** Compute (memoized) the transitive context of a function; detects recursion (illegal in HLSL). */
|
|
static bool ComputeFunctionCtx(const FName FuncName, FLibraryEmit& Emit, TArray<FString>& OutErrors)
|
|
{
|
|
FFunctionCtx& Ctx = Emit.Ctx.FindOrAdd(FuncName);
|
|
if (Ctx.State == 2) { return true; }
|
|
if (Ctx.State == 1)
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("ShaderLab: recursive library function call involving '%s' (not allowed)"), *FuncName.ToString()));
|
|
return false;
|
|
}
|
|
Ctx.State = 1;
|
|
|
|
int32 LibIdx = INDEX_NONE;
|
|
const FShaderLabFunction* Fn = Emit.Program->FindFunction(FuncName, LibIdx);
|
|
check(Fn); // caller only asks for known functions
|
|
const FShaderLabResolvedLibrary& Lib = Emit.Program->Libraries[LibIdx];
|
|
const FString SrcPath = MakeLineDirectivePath(Lib.Model.SourceFilePath);
|
|
|
|
TSet<FName> DirectProps, DirectCallees;
|
|
TArray<FIntrinsicUse> DirectIntr;
|
|
if (!ScanDirectRefs(Fn->Body, Lib, *Emit.Program, SrcPath, Fn->BodyLine, DirectProps, DirectIntr, DirectCallees, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TSet<FName> Props = DirectProps;
|
|
TArray<FIntrinsicUse> Intr = DirectIntr;
|
|
for (const FName& Callee : DirectCallees)
|
|
{
|
|
if (!ComputeFunctionCtx(Callee, Emit, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
// Re-find after potential rehash of the map from recursive FindOrAdd.
|
|
const FFunctionCtx& CalleeCtx = Emit.Ctx[Callee];
|
|
for (const FName& P : CalleeCtx.ReqProps) { Props.Add(P); }
|
|
for (const FIntrinsicUse& U : CalleeCtx.ReqIntr)
|
|
{
|
|
if (!Intr.ContainsByPredicate([&](const FIntrinsicUse& X) { return X.Name == U.Name && X.ArgSig == U.ArgSig; }))
|
|
{
|
|
Intr.Add(U);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stable deterministic order (params and args must agree across all emission sites).
|
|
TArray<FName> SortedProps = Props.Array();
|
|
SortedProps.Sort([](const FName& A, const FName& B) { return A.LexicalLess(B); });
|
|
Intr.Sort([](const FIntrinsicUse& A, const FIntrinsicUse& B)
|
|
{
|
|
return (A.Name + TEXT("|") + A.ArgSig) < (B.Name + TEXT("|") + B.ArgSig);
|
|
});
|
|
|
|
FFunctionCtx& Store = Emit.Ctx.FindOrAdd(FuncName);
|
|
Store.ReqProps = MoveTemp(SortedProps);
|
|
Store.ReqIntr = MoveTemp(Intr);
|
|
Store.State = 2;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Append each library function call's trailing context arguments (the callee's required promoted
|
|
* properties + intrinsic variables) just before its closing paren. Used on both rewritten library
|
|
* function bodies and consuming shader bodies. Comments and string literals are skipped.
|
|
*/
|
|
static FString InjectCalleeArgs(const FString& P1, const FLibraryEmit& Emit)
|
|
{
|
|
if (!Emit.HasLibraries())
|
|
{
|
|
return P1;
|
|
}
|
|
FString Out;
|
|
Out.Reserve(P1.Len());
|
|
const int32 L2 = P1.Len();
|
|
struct FParenInfo { FString Trailing; bool bHasArgs = false; };
|
|
TArray<FParenInfo> Stack;
|
|
int32 k = 0;
|
|
while (k < L2)
|
|
{
|
|
const TCHAR C = P1[k];
|
|
if (C == TEXT('/') && k + 1 < L2 && P1[k + 1] == TEXT('/')) { const int32 s = k; while (k < L2 && P1[k] != TEXT('\n')) { ++k; } Out += P1.Mid(s, k - s); continue; }
|
|
if (C == TEXT('/') && k + 1 < L2 && P1[k + 1] == TEXT('*')) { const int32 s = k; k += 2; while (k + 1 < L2 && !(P1[k] == TEXT('*') && P1[k + 1] == TEXT('/'))) { ++k; } k = FMath::Min(k + 2, L2); Out += P1.Mid(s, k - s); continue; }
|
|
if (C == TEXT('"') || C == TEXT('\'')) { const int32 s = k; const TCHAR Q = C; ++k; while (k < L2 && P1[k] != Q) { if (P1[k] == TEXT('\\')) { ++k; } ++k; } ++k; Out += P1.Mid(s, FMath::Min(k, L2) - s); if (Stack.Num()) { Stack.Last().bHasArgs = true; } continue; }
|
|
|
|
if (IsIdentBoundary(P1, k))
|
|
{
|
|
const int32 e = IdentEnd(P1, k);
|
|
const FString Ident = P1.Mid(k, e - k);
|
|
const int32 paren = SkipSpace(P1, e);
|
|
if (paren < L2 && P1[paren] == TEXT('(') && Emit.Program->FunctionOwnerLibrary.Contains(FName(*Ident)))
|
|
{
|
|
Out += P1.Mid(k, (paren + 1) - k); // identifier + spaces + '('
|
|
FParenInfo Info;
|
|
Info.Trailing = BuildTrailing(Emit.Ctx[FName(*Ident)], *Emit.Program, /*bArgs*/ true);
|
|
Stack.Add(Info);
|
|
if (Stack.Num() > 1) { Stack[Stack.Num() - 2].bHasArgs = true; }
|
|
k = paren + 1;
|
|
continue;
|
|
}
|
|
if (Stack.Num()) { Stack.Last().bHasArgs = true; }
|
|
Out += Ident;
|
|
k = e;
|
|
continue;
|
|
}
|
|
|
|
if (C == TEXT('('))
|
|
{
|
|
Stack.Add(FParenInfo());
|
|
if (Stack.Num() > 1) { Stack[Stack.Num() - 2].bHasArgs = true; }
|
|
Out.AppendChar(C);
|
|
++k;
|
|
continue;
|
|
}
|
|
if (C == TEXT(')'))
|
|
{
|
|
FParenInfo Info = Stack.Num() ? Stack.Pop() : FParenInfo();
|
|
if (!Info.Trailing.IsEmpty())
|
|
{
|
|
Out += Info.bHasArgs ? (FString(TEXT(", ")) + Info.Trailing) : Info.Trailing;
|
|
}
|
|
Out.AppendChar(C);
|
|
++k;
|
|
continue;
|
|
}
|
|
if (!FChar::IsWhitespace(C) && Stack.Num()) { Stack.Last().bHasArgs = true; }
|
|
Out.AppendChar(C);
|
|
++k;
|
|
}
|
|
return Out;
|
|
}
|
|
|
|
/**
|
|
* Rewrite a library function body: promoted-property refs and registry-intrinsic calls are renamed
|
|
* (to their promoted / SLI variable names), then each call to another library function gets the callee's
|
|
* trailing context arguments appended. Comments and string literals are skipped.
|
|
*/
|
|
static FString RewriteFunctionBody(const FString& Body, const FShaderLabResolvedLibrary& OwnerLib,
|
|
const FLibraryEmit& Emit)
|
|
{
|
|
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
|
|
const int32 Len = Body.Len();
|
|
|
|
// Pass 1: rename properties (bare -> promoted) and registry intrinsics (UE_X(...) -> SLI var).
|
|
FString P1;
|
|
P1.Reserve(Len);
|
|
int32 i = 0;
|
|
while (i < Len)
|
|
{
|
|
const TCHAR C = Body[i];
|
|
if (C == TEXT('/') && i + 1 < Len && Body[i + 1] == TEXT('/')) { const int32 s = i; while (i < Len && Body[i] != TEXT('\n')) { ++i; } P1 += Body.Mid(s, i - s); continue; }
|
|
if (C == TEXT('/') && i + 1 < Len && Body[i + 1] == TEXT('*')) { const int32 s = i; i += 2; while (i + 1 < Len && !(Body[i] == TEXT('*') && Body[i + 1] == TEXT('/'))) { ++i; } i = FMath::Min(i + 2, Len); P1 += Body.Mid(s, i - s); continue; }
|
|
if (C == TEXT('"') || C == TEXT('\'')) { const int32 s = i; const TCHAR Q = C; ++i; while (i < Len && Body[i] != Q) { if (Body[i] == TEXT('\\')) { ++i; } ++i; } ++i; P1 += Body.Mid(s, FMath::Min(i, Len) - s); continue; }
|
|
if (!IsIdentBoundary(Body, i)) { P1.AppendChar(C); ++i; continue; }
|
|
|
|
const int32 e = IdentEnd(Body, i);
|
|
const FString Ident = Body.Mid(i, e - i);
|
|
const int32 paren = SkipSpace(Body, e);
|
|
const bool bCall = (paren < Len && Body[paren] == TEXT('('));
|
|
|
|
if (bCall && Ident.StartsWith(TEXT("UE_")))
|
|
{
|
|
const FString Name = Ident.Mid(3);
|
|
if (const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name)))
|
|
{
|
|
int32 depth = 0, m = paren;
|
|
for (; m < Len; ++m) { if (Body[m] == TEXT('(')) { ++depth; } else if (Body[m] == TEXT(')')) { if (--depth == 0) { break; } } }
|
|
const FString ArgsRaw = (m < Len) ? Body.Mid(paren + 1, m - (paren + 1)) : FString();
|
|
// Overloaded intrinsic called with args -> the same-named raw HLSL helper form: emit the
|
|
// identifier verbatim (do NOT rewrite to an SLI_ input) and keep scanning its arguments.
|
|
if (Desc->bAllowRawHelperWithArgs && SplitArgs(ArgsRaw).Num() > Desc->Params.Num())
|
|
{
|
|
P1 += Ident;
|
|
i = e;
|
|
continue;
|
|
}
|
|
P1 += MakeIntrinsicVar(Name, MakeArgSig(ArgsRaw));
|
|
i = (m < Len) ? m + 1 : e;
|
|
continue;
|
|
}
|
|
// Unknown UE_ helper: keep the identifier, keep scanning its args.
|
|
P1 += Ident;
|
|
i = e;
|
|
continue;
|
|
}
|
|
|
|
// Don't rewrite a member access (`s.Contrast`, `p->Contrast`): only a bare identifier is the
|
|
// promoted property. Member fields that happen to share a property's name must be left alone.
|
|
const bool bMemberAccess = (i > 0 && Body[i - 1] == TEXT('.'))
|
|
|| (i > 1 && Body[i - 1] == TEXT('>') && Body[i - 2] == TEXT('-'));
|
|
if (!bMemberAccess)
|
|
{
|
|
if (const FName* Promoted = OwnerLib.PropRewrite.Find(FName(*Ident)))
|
|
{
|
|
P1 += Promoted->ToString();
|
|
i = e;
|
|
continue;
|
|
}
|
|
}
|
|
P1 += Ident;
|
|
i = e;
|
|
}
|
|
|
|
// Pass 2: append trailing context args to each call of a library function.
|
|
return InjectCalleeArgs(P1, Emit);
|
|
}
|
|
|
|
/** Collect the names of library functions called (syntactically `Name(`) in a body. */
|
|
static void CollectCalledFunctions(const FString& Body, const FShaderLabResolvedProgram& Program, TSet<FName>& Out)
|
|
{
|
|
const int32 Len = Body.Len();
|
|
for (int32 i = 0; i < Len; )
|
|
{
|
|
if (!IsIdentBoundary(Body, i)) { ++i; continue; }
|
|
const int32 e = IdentEnd(Body, i);
|
|
const int32 paren = SkipSpace(Body, e);
|
|
if (paren < Len && Body[paren] == TEXT('('))
|
|
{
|
|
const FName Ident(*Body.Mid(i, e - i));
|
|
if (Program.FunctionOwnerLibrary.Contains(Ident)) { Out.Add(Ident); }
|
|
}
|
|
i = e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare a consuming body (Surface/Slab/Value/Interpolator/Vertex): rewrite calls to library functions
|
|
* (appending their context args), emit the body's own UE_ intrinsics, and additionally create+wire the
|
|
* intrinsics that called functions need (stage-validated). Fills OutWires (intrinsic inputs) and
|
|
* OutReqProps (promoted property names the called functions need, which the caller also wires).
|
|
*/
|
|
static bool PrepareBody(
|
|
UMaterial& Material, EShaderLabIntrinsicFrequency Stage,
|
|
FString& InOutBody, int32 BodyLine, const FString& SrcPath,
|
|
const FLibraryEmit& Emit,
|
|
TArray<FIntrinsicWire>& OutWires, TSet<FName>& OutReqProps,
|
|
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
|
|
const FSampleNodes& Samples,
|
|
TArray<FString>& OutErrors)
|
|
{
|
|
// Which library functions does this body call? (Scan the original body — function names are not
|
|
// touched by intrinsic substitution, so the result is order-independent.)
|
|
TSet<FName> Called;
|
|
if (Emit.HasLibraries())
|
|
{
|
|
CollectCalledFunctions(InOutBody, *Emit.Program, Called);
|
|
}
|
|
|
|
// Pre-sampled VT / RVT reads (pixel-only): wire each referenced sample node's output into the body.
|
|
// Done before EmitIntrinsics so the RVT member rewrite doesn't disturb intrinsic column padding of
|
|
// UE_ substitutions (RVT rewrite only touches `<Name>.<Member>`, never `UE_...`).
|
|
if (Samples.VT || Samples.RVT)
|
|
{
|
|
if (Stage != EShaderLabIntrinsicFrequency::PixelOnly)
|
|
{
|
|
// Reject VT/RVT reads outside a pixel body (vertex/interpolator). Only flag when actually used.
|
|
auto FirstUsed = [&](const TMap<FName, UMaterialExpression*>* Map, const TCHAR* What) -> bool
|
|
{
|
|
if (!Map) { return false; }
|
|
for (const TPair<FName, UMaterialExpression*>& Pair : *Map)
|
|
{
|
|
if (ReferencesToken(InOutBody, Pair.Key.ToString()))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("%s(%d,1): error: %s read '%s' is only allowed in a pixel body"),
|
|
*SrcPath, FMath::Max(BodyLine - 1, 1), What, *Pair.Key.ToString()));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
bool bMisuse = FirstUsed(Samples.VT, TEXT("Virtual texture"));
|
|
bMisuse |= FirstUsed(Samples.RVT, TEXT("Runtime virtual texture"));
|
|
if (bMisuse) { return false; }
|
|
}
|
|
else
|
|
{
|
|
// Streaming VT: `<Name>` used verbatim as a float4 -> wire the sample node's RGBA output (pin 5).
|
|
if (Samples.VT)
|
|
{
|
|
for (const TPair<FName, UMaterialExpression*>& Pair : *Samples.VT)
|
|
{
|
|
if (ReferencesToken(InOutBody, Pair.Key.ToString()))
|
|
{
|
|
OutWires.Add(FIntrinsicWire{ Pair.Key, Pair.Value, /*RGBA*/ 5 });
|
|
}
|
|
}
|
|
}
|
|
// RVT: rewrite `<Name>.<Member>` -> `SLRVT_<Name>_<Member>` and wire the matching output pin.
|
|
if (Samples.RVT)
|
|
{
|
|
for (const TPair<FName, UMaterialExpression*>& Pair : *Samples.RVT)
|
|
{
|
|
const FString NameStr = Pair.Key.ToString();
|
|
for (const FRVTMemberDef& M : GRVTMembers)
|
|
{
|
|
const FString Access = NameStr + TEXT(".") + M.Member;
|
|
if (!ReferencesToken(InOutBody, Access))
|
|
{
|
|
continue;
|
|
}
|
|
const FString InputVar = FString(TEXT("SLRVT_")) + NameStr + TEXT("_") + M.Member;
|
|
InOutBody.ReplaceInline(*Access, *InputVar, ESearchCase::CaseSensitive);
|
|
OutWires.Add(FIntrinsicWire{ FName(*InputVar), Pair.Value, M.PinIndex });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The body's own UE_ intrinsics FIRST: EmitIntrinsics space-pads its substitutions so compile-error
|
|
// columns map accurately, and it reports offsets against THIS body — so it must run before callee-arg
|
|
// injection (which inserts text mid-line and would shift those columns).
|
|
if (!EmitIntrinsics(Material, Stage, InOutBody, BodyLine, SrcPath, OutWires, InterpByName, UsedInterps, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Now rewrite each library-function call to pass the context args the callee needs. Safe to run
|
|
// after EmitIntrinsics: it only touches `Name(` for library functions, and appends already-final
|
|
// promoted/SLI identifiers.
|
|
if (Called.Num() > 0)
|
|
{
|
|
InOutBody = InjectCalleeArgs(InOutBody, Emit);
|
|
}
|
|
|
|
// Add intrinsics/props required by called functions (deduped against what the body already wired).
|
|
bool bOk = true;
|
|
for (const FName& Callee : Called)
|
|
{
|
|
const FFunctionCtx& Ctx = Emit.Ctx[Callee];
|
|
for (const FName& P : Ctx.ReqProps) { OutReqProps.Add(P); }
|
|
|
|
// Map a validation failure back to the library that actually contains the offending intrinsic,
|
|
// not this calling body (the intrinsic literal lives in the callee's source, not here).
|
|
int32 CalleeLib = INDEX_NONE;
|
|
Emit.Program->FindFunction(Callee, CalleeLib);
|
|
const FString CalleeSrc = (CalleeLib != INDEX_NONE)
|
|
? MakeLineDirectivePath(Emit.Program->Libraries[CalleeLib].Model.SourceFilePath) : SrcPath;
|
|
|
|
for (const FIntrinsicUse& Use : Ctx.ReqIntr)
|
|
{
|
|
const FName InputName(*MakeIntrinsicVar(Use.Name, Use.ArgSig));
|
|
if (OutWires.ContainsByPredicate([&](const FIntrinsicWire& W) { return W.InputName == InputName; }))
|
|
{
|
|
continue;
|
|
}
|
|
UMaterialExpression* Expr = nullptr;
|
|
int32 OutIdx = 0;
|
|
FString Err;
|
|
if (!CreateIntrinsicNode(Material, Use.Name, Use.ArgsRaw, Stage, Expr, OutIdx, Err))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("%s(1,1): error: %s (in library function '%s', reached from %s)"),
|
|
*CalleeSrc, *Err, *Callee.ToString(), *FPaths::GetCleanFilename(SrcPath)));
|
|
bOk = false;
|
|
continue;
|
|
}
|
|
OutWires.Add(FIntrinsicWire{ InputName, Expr, OutIdx });
|
|
}
|
|
}
|
|
return bOk;
|
|
}
|
|
|
|
/** A created property parameter node (shared across all slabs/values that reference it). */
|
|
struct FParamNode
|
|
{
|
|
UMaterialExpression* Expr = nullptr;
|
|
bool bIsTexture = false;
|
|
};
|
|
|
|
/**
|
|
* Wire the promoted-property parameter nodes a body needs onto its Custom node: every non-StaticBool
|
|
* property that is either referenced literally in the body OR required by a called library function.
|
|
* 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 TArray<FShaderLabCollectionParam>& Collections)
|
|
{
|
|
for (const FShaderLabProperty& Prop : Program.Properties)
|
|
{
|
|
if (Prop.Type == EShaderLabPropertyType::StaticBool)
|
|
{
|
|
continue;
|
|
}
|
|
if (!ReqProps.Contains(Prop.Name) && !ReferencesToken(InBody, Prop.Name.ToString()))
|
|
{
|
|
continue;
|
|
}
|
|
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
|
|
if (Node && Node->Expr)
|
|
{
|
|
FCustomInput In;
|
|
In.InputName = Prop.Name;
|
|
In.Input.Connect(0, Node->Expr);
|
|
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). */
|
|
static ECustomMaterialOutputType InterpolatorOutputType(const FString& ReturnType)
|
|
{
|
|
if (ReturnType == TEXT("float2")) { return CMOT_Float2; }
|
|
if (ReturnType == TEXT("float3")) { return CMOT_Float3; }
|
|
if (ReturnType == TEXT("float4")) { return CMOT_Float4; }
|
|
return CMOT_Float1; // "float"
|
|
}
|
|
|
|
/** File-name-safe stem from the shader name (identity), matching the registry's illegal->'_' rule. */
|
|
static FString SanitizeShaderFileStem(const FString& Name)
|
|
{
|
|
FString Out;
|
|
for (const TCHAR C : Name)
|
|
{
|
|
Out.AppendChar((FChar::IsAlnum(C) || C == TEXT('_') || C == TEXT('-')) ? C : TEXT('_'));
|
|
}
|
|
return Out.IsEmpty() ? FString(TEXT("Unnamed")) : Out;
|
|
}
|
|
|
|
/**
|
|
* Virtual `#include` path for a shader's generated header (free local code + emitted library functions),
|
|
* or empty when the shader has neither local code nor `.uslfunc` imports. Gated on Model alone so the
|
|
* node-include side (AddIncludes) and the file-writing side (WriteLocalCodeInclude) always agree.
|
|
*/
|
|
static FString LocalCodeVirtualPath(const FShaderLabModel& Model)
|
|
{
|
|
if (Model.LocalCode.Num() == 0 && Model.Imports.Num() == 0)
|
|
{
|
|
return FString();
|
|
}
|
|
return FString::Printf(TEXT("%s/%s.gen.ush"),
|
|
FShaderLabGraphBuilder::GetGeneratedVirtualRoot(), *SanitizeShaderFileStem(Model.ShaderName));
|
|
}
|
|
|
|
/**
|
|
* Guard: reject UE_ node intrinsics / UE_Interpolator inside free local code. Local functions are pure
|
|
* HLSL emitted at file scope — they can't reach material-graph nodes or per-primitive parameters, so a
|
|
* `UE_Time()` there would fail with an obscure "undefined symbol". Report it clearly, mapped to the .usl.
|
|
* (HLSL library helpers like UE_Noise are NOT in the registry and remain allowed.)
|
|
*/
|
|
static bool CheckLocalCodeIntrinsics(const FShaderLabModel& Model, TArray<FString>& OutErrors)
|
|
{
|
|
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
|
|
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
|
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
|
|
bool bOk = true;
|
|
for (const FShaderLabLocalCode& LC : Model.LocalCode)
|
|
{
|
|
const FString& B = LC.Text;
|
|
const int32 Len = B.Len();
|
|
int32 i = 0;
|
|
while (i < Len)
|
|
{
|
|
const bool bBoundary = (i == 0) || !IsIdent(B[i - 1]);
|
|
if (bBoundary && i + 3 <= Len && B[i] == TEXT('U') && B[i + 1] == TEXT('E') && B[i + 2] == TEXT('_'))
|
|
{
|
|
int32 j = i + 3;
|
|
while (j < Len && IsIdent(B[j])) { ++j; }
|
|
const FString Name = B.Mid(i + 3, j - (i + 3));
|
|
int32 k = j;
|
|
while (k < Len && FChar::IsWhitespace(B[k])) { ++k; }
|
|
if (!Name.IsEmpty() && k < Len && B[k] == TEXT('(')
|
|
&& (Name == TEXT("Interpolator") || Registry.Find(FName(*Name))))
|
|
{
|
|
int32 Line = LC.Line;
|
|
for (int32 p = 0; p < i; ++p) { if (B[p] == TEXT('\n')) { ++Line; } }
|
|
OutErrors.Add(FString::Printf(
|
|
TEXT("%s(%d,1): error: UE_%s is a material-graph intrinsic and cannot be used inside a local function (pass its value in as a parameter)"),
|
|
*SrcPath, FMath::Max(Line, 1), *Name));
|
|
bOk = false;
|
|
}
|
|
i = j;
|
|
continue;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
return bOk;
|
|
}
|
|
|
|
/**
|
|
* Write the shader's free top-level HLSL (LocalCode) to its generated `.gen.ush` on disk, at file
|
|
* scope (in source order, each chunk `#line`-mapped back to the .usl). Every generated Custom node
|
|
* #includes this file, so helpers/structs/globals are defined-before-use for all pixel/vertex bodies
|
|
* regardless of the translator's per-node compile order. No-op when the shader has no local code.
|
|
*/
|
|
/** Join an author signature with the trailing context params/args (handles the empty-signature case). */
|
|
static FString JoinSignature(const FString& SignatureInner, const FString& Trailing)
|
|
{
|
|
const FString Sig = SignatureInner.TrimStartAndEnd();
|
|
if (Trailing.IsEmpty()) { return Sig; }
|
|
if (Sig.IsEmpty()) { return Trailing; }
|
|
return Sig + TEXT(", ") + Trailing;
|
|
}
|
|
|
|
static bool WriteLocalCodeInclude(const FShaderLabModel& Model, const FLibraryEmit& Emit, TArray<FString>& OutErrors)
|
|
{
|
|
if (Model.LocalCode.Num() == 0 && !Emit.HasLibraries())
|
|
{
|
|
return true;
|
|
}
|
|
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
|
FString Content;
|
|
Content += FString::Printf(TEXT("// Generated by ShaderLab from %s - do not edit.\n"), *Model.ShaderName);
|
|
Content += TEXT("#pragma once\n");
|
|
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_COMMON_INCLUDE);
|
|
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_FUNCTIONS_INCLUDE);
|
|
|
|
// Imported library dependencies: real `.ush` includes, then library free HLSL (in dependency order),
|
|
// so library functions below see their own helpers/structs/globals.
|
|
if (Emit.HasLibraries())
|
|
{
|
|
TSet<FString> SeenIncludes;
|
|
for (const FShaderLabResolvedLibrary& Lib : Emit.Program->Libraries)
|
|
{
|
|
for (const FString& Inc : Lib.Model.Includes)
|
|
{
|
|
if (!Inc.IsEmpty() && !SeenIncludes.Contains(Inc))
|
|
{
|
|
SeenIncludes.Add(Inc);
|
|
Content += FString::Printf(TEXT("#include \"%s\"\n"), *Inc);
|
|
}
|
|
}
|
|
}
|
|
for (const FShaderLabResolvedLibrary& Lib : Emit.Program->Libraries)
|
|
{
|
|
const FString LibSrc = MakeLineDirectivePath(Lib.Model.SourceFilePath);
|
|
for (const FShaderLabLocalCode& LC : Lib.Model.LocalCode)
|
|
{
|
|
Content += FString::Printf(TEXT("#line %d \"%s\"\n"), FMath::Max(LC.Line, 1), *LibSrc);
|
|
Content += LC.Text;
|
|
Content += TEXT("\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const FShaderLabLocalCode& LC : Model.LocalCode)
|
|
{
|
|
Content += FString::Printf(TEXT("#line %d \"%s\"\n"), FMath::Max(LC.Line, 1), *SrcPath);
|
|
Content += LC.Text;
|
|
Content += TEXT("\n");
|
|
}
|
|
|
|
// Library functions actually reached from the shader's bodies (Emit.Ctx holds exactly those). Emit
|
|
// prototypes for all first so intra-/cross-library calls resolve regardless of declaration order,
|
|
// then the rewritten definitions in dependency order.
|
|
if (Emit.HasLibraries())
|
|
{
|
|
Content += TEXT("#line 1 \"ShaderLabGenerated.ush\"\n");
|
|
for (const FShaderLabResolvedLibrary& Lib : Emit.Program->Libraries)
|
|
{
|
|
for (const FShaderLabFunction& Fn : Lib.Model.Functions)
|
|
{
|
|
const FFunctionCtx* Ctx = Emit.Ctx.Find(Fn.Name);
|
|
if (!Ctx || Ctx->State != 2) { continue; }
|
|
const FString Sig = JoinSignature(Fn.SignatureInner, BuildTrailing(*Ctx, *Emit.Program, /*bArgs*/ false));
|
|
Content += FString::Printf(TEXT("%s %s(%s);\n"), *Fn.ReturnType, *Fn.Name.ToString(), *Sig);
|
|
}
|
|
}
|
|
for (const FShaderLabResolvedLibrary& Lib : Emit.Program->Libraries)
|
|
{
|
|
const FString LibSrc = MakeLineDirectivePath(Lib.Model.SourceFilePath);
|
|
for (const FShaderLabFunction& Fn : Lib.Model.Functions)
|
|
{
|
|
const FFunctionCtx* Ctx = Emit.Ctx.Find(Fn.Name);
|
|
if (!Ctx || Ctx->State != 2) { continue; }
|
|
const FString Sig = JoinSignature(Fn.SignatureInner, BuildTrailing(*Ctx, *Emit.Program, /*bArgs*/ false));
|
|
const FString RewrittenBody = RewriteFunctionBody(Fn.Body, Lib, Emit);
|
|
Content += FString::Printf(TEXT("%s %s(%s)\n{\n%s}\n"), *Fn.ReturnType, *Fn.Name.ToString(), *Sig,
|
|
*WrapBodyWithLineMapping(RewrittenBody, Fn.BodyLine, LibSrc));
|
|
}
|
|
}
|
|
}
|
|
|
|
const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir();
|
|
if (GenDir.IsEmpty())
|
|
{
|
|
OutErrors.Add(TEXT("ShaderLab: cannot locate the plugin directory for the generated local-code include"));
|
|
return false;
|
|
}
|
|
IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true);
|
|
const FString DiskPath = FPaths::Combine(GenDir, SanitizeShaderFileStem(Model.ShaderName) + TEXT(".gen.ush"));
|
|
// Shader source must be UTF-8 (no BOM) for the shader preprocessor, not the default UTF-16.
|
|
if (!FFileHelper::SaveStringToFile(Content, *DiskPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("ShaderLab: failed to write generated local-code include '%s'"), *DiskPath));
|
|
return false;
|
|
}
|
|
// Drop any cached copy so the shader preprocessor re-reads the freshly written file (hot reload / re-cook).
|
|
FlushShaderFileCache();
|
|
return true;
|
|
}
|
|
|
|
/** Add the shared struct + function-library includes, the generated local-code header (if any), and
|
|
* any user `Includes { }` paths. */
|
|
static void AddIncludes(UMaterialExpressionCustom& Custom, const FShaderLabModel& Model)
|
|
{
|
|
Custom.IncludeFilePaths.Add(SHADERLAB_COMMON_INCLUDE);
|
|
// The function library provides the UE_ HLSL helpers (UE_Noise, UE_RotateAboutAxis, ...) that
|
|
// are left verbatim in the body (not rewritten into nodes).
|
|
Custom.IncludeFilePaths.Add(SHADERLAB_FUNCTIONS_INCLUDE);
|
|
// User-authored local functions/structs/globals live in a generated header at file scope.
|
|
const FString LocalInclude = LocalCodeVirtualPath(Model);
|
|
if (!LocalInclude.IsEmpty())
|
|
{
|
|
Custom.IncludeFilePaths.AddUnique(LocalInclude);
|
|
}
|
|
for (const FString& Include : Model.Includes)
|
|
{
|
|
if (!Include.IsEmpty())
|
|
{
|
|
Custom.IncludeFilePaths.AddUnique(Include);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// --- 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 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,
|
|
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
|
|
const FString& InBody, int32 BodyLine, 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,
|
|
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))
|
|
{
|
|
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 (int32 i = 0; i < Desc.NumFields; ++i)
|
|
{
|
|
const FSlabFieldDef& F = Desc.Fields[i];
|
|
if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field))
|
|
{
|
|
UsedSlab.Add(&F);
|
|
}
|
|
}
|
|
// 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 && UsedMatOut.Num() == 0)
|
|
{
|
|
return Bsdf; // Empty body: a default Substrate BSDF.
|
|
}
|
|
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
|
Custom->Description = Desc.CustomDesc;
|
|
Custom->OutputType = CMOT_Float1;
|
|
AddIncludes(*Custom, Model);
|
|
|
|
FString Body = InBody;
|
|
TArray<FIntrinsicWire> Wires;
|
|
TSet<FName> ReqProps;
|
|
if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors))
|
|
{
|
|
return nullptr;
|
|
}
|
|
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<TPair<const FSlabFieldDef*, int32>> SlabOutputs;
|
|
for (const FSlabFieldDef* F : UsedSlab)
|
|
{
|
|
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);
|
|
SlabOutputs.Add(TPair<const FSlabFieldDef*, int32>(F, OutputIndex));
|
|
++OutputIndex;
|
|
}
|
|
TArray<TPair<FString, int32>> 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<FString, int32>(F->Field, OutputIndex));
|
|
++OutputIndex;
|
|
}
|
|
Code += TEXT("return 0.0f;\n");
|
|
Custom->Code = Code;
|
|
Custom->RebuildOutputs();
|
|
|
|
for (const TPair<const FSlabFieldDef*, int32>& Pair : SlabOutputs)
|
|
{
|
|
if (FExpressionInput* Pin = Desc.GetPin(Bsdf, Pair.Key->Field))
|
|
{
|
|
Pin->Connect(Pair.Value, 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)
|
|
{
|
|
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 Root;
|
|
}
|
|
|
|
/**
|
|
* Build the Custom node for a PostProcess/UI entry (Domain = PostProcess/UI). The output struct has
|
|
* Color (-> material EmissiveColor) and Opacity (-> material Opacity); there is no Substrate slab.
|
|
*/
|
|
static bool BuildEmissiveEntry(
|
|
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const TCHAR* StructName, const TCHAR* DefaultFn,
|
|
const FString& OutParamName, const FString& InBody, int32 BodyLine, 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,
|
|
int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
|
Custom->Description = TEXT("ShaderLab Emissive Entry");
|
|
Custom->OutputType = CMOT_Float1;
|
|
AddIncludes(*Custom, Model);
|
|
|
|
FString Body = InBody;
|
|
TArray<FIntrinsicWire> Wires;
|
|
TSet<FName> 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 bool bUsesColor = ReferencesToken(InBody, OutParamName + TEXT(".Color"));
|
|
const bool bUsesOpacity = ReferencesToken(InBody, OutParamName + TEXT(".Opacity"));
|
|
|
|
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;
|
|
int32 ColorOutIdx = INDEX_NONE;
|
|
int32 OpacityOutIdx = INDEX_NONE;
|
|
if (bUsesColor)
|
|
{
|
|
FCustomOutput Out; Out.OutputName = TEXT("SLO_Color"); Out.OutputType = CMOT_Float3;
|
|
Custom->AdditionalOutputs.Add(Out);
|
|
Code += FString::Printf(TEXT("SLO_Color = %s.Color;\n"), *OutParamName);
|
|
ColorOutIdx = OutputIndex++;
|
|
}
|
|
if (bUsesOpacity)
|
|
{
|
|
FCustomOutput Out; Out.OutputName = TEXT("SLO_Opacity"); Out.OutputType = CMOT_Float1;
|
|
Custom->AdditionalOutputs.Add(Out);
|
|
Code += FString::Printf(TEXT("SLO_Opacity = %s.Opacity;\n"), *OutParamName);
|
|
OpacityOutIdx = OutputIndex++;
|
|
}
|
|
Code += TEXT("return 0.0f;\n");
|
|
Custom->Code = Code;
|
|
Custom->RebuildOutputs();
|
|
|
|
if (ColorOutIdx != INDEX_NONE) { EditorOnly.EmissiveColor.Connect(ColorOutIdx, Custom); }
|
|
if (OpacityOutIdx != INDEX_NONE) { EditorOnly.Opacity.Connect(OpacityOutIdx, Custom); }
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Build the Runtime Virtual Texture WRITE output (SL_RVTOUTPUT). Mirrors BuildEmissiveEntry: a Custom node
|
|
* runs the body filling an FShaderLabRVTOutput, its written fields become AdditionalOutputs, and each is
|
|
* wired to the matching pin of a UMaterialExpressionRuntimeVirtualTextureOutput (a custom output whose mere
|
|
* presence makes the material write into an RVT when a mesh renders into one). Unwritten fields keep the
|
|
* node's own defaults.
|
|
*/
|
|
static bool BuildRVTOutput(
|
|
UMaterial& Material, const FString& OutParamName, const FString& InBody, int32 BodyLine,
|
|
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, int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
UMaterialExpressionRuntimeVirtualTextureOutput* RVTOut =
|
|
NewExpr<UMaterialExpressionRuntimeVirtualTextureOutput>(Material, IoY, -600);
|
|
|
|
TArray<const FRVTOutFieldDef*> Used;
|
|
for (const FRVTOutFieldDef& F : GRVTOutFields)
|
|
{
|
|
if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field))
|
|
{
|
|
Used.Add(&F);
|
|
}
|
|
}
|
|
if (Used.Num() == 0)
|
|
{
|
|
return true; // Empty body: the RVT output node stays at its pin defaults.
|
|
}
|
|
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
|
Custom->Description = TEXT("ShaderLab RVT Output");
|
|
Custom->OutputType = CMOT_Float1;
|
|
AddIncludes(*Custom, Model);
|
|
|
|
FString Body = InBody;
|
|
TArray<FIntrinsicWire> Wires;
|
|
TSet<FName> 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("FShaderLabRVTOutput %s = ShaderLabDefaultRVTOutput();\n{\n%s}\n"),
|
|
*OutParamName, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath));
|
|
|
|
int32 OutputIndex = 1;
|
|
TArray<TPair<const FRVTOutFieldDef*, int32>> Outs;
|
|
for (const FRVTOutFieldDef* 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);
|
|
Outs.Add(TPair<const FRVTOutFieldDef*, int32>(F, OutputIndex));
|
|
++OutputIndex;
|
|
}
|
|
Code += TEXT("return 0.0f;\n");
|
|
Custom->Code = Code;
|
|
Custom->RebuildOutputs();
|
|
|
|
for (const TPair<const FRVTOutFieldDef*, int32>& Pair : Outs)
|
|
{
|
|
if (FExpressionInput* Pin = GetRVTOutputPin(RVTOut, Pair.Key->Field))
|
|
{
|
|
Pin->Connect(Pair.Value, Custom);
|
|
}
|
|
}
|
|
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,
|
|
const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit,
|
|
const TMap<FName, FParamNode>& PropertyNodes,
|
|
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
|
|
const FSampleNodes& Samples,
|
|
int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
|
Custom->Description = FString::Printf(TEXT("ShaderLab Value %s"), *Value.Name.ToString());
|
|
Custom->OutputType = (Value.ReturnType == TEXT("float2")) ? CMOT_Float2 : CMOT_Float1;
|
|
AddIncludes(*Custom, Model);
|
|
|
|
FString Body = Value.Body;
|
|
TArray<FIntrinsicWire> Wires;
|
|
TSet<FName> ReqProps;
|
|
if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors))
|
|
{
|
|
return nullptr;
|
|
}
|
|
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, 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);
|
|
Custom->Code = WrapBodyWithLineMapping(Body, Value.BodyLine, SrcPath);
|
|
Custom->RebuildOutputs();
|
|
return Custom;
|
|
}
|
|
|
|
/**
|
|
* Build a Vertex Interpolator: a Custom node computing the SL_INTERPOLATOR body at vertex frequency,
|
|
* feeding a UMaterialExpressionVertexInterpolator. Returns the interpolator node (its output 0 is the
|
|
* interpolated value, readable from pixel bodies via UE_Interpolator). nullptr on error.
|
|
*/
|
|
static UMaterialExpression* BuildInterpolatorNode(
|
|
UMaterial& Material, const FShaderLabInterpolator& Interp, const FShaderLabModel& Model,
|
|
const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit,
|
|
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -600);
|
|
Custom->Description = FString::Printf(TEXT("ShaderLab Interpolator %s"), *Interp.Name.ToString());
|
|
Custom->OutputType = InterpolatorOutputType(Interp.ReturnType);
|
|
AddIncludes(*Custom, Model);
|
|
|
|
// Vertex frequency: pixel-only intrinsics (incl. UE_Interpolator) are rejected. No interpolator map.
|
|
FString Body = Interp.Body;
|
|
TArray<FIntrinsicWire> Wires;
|
|
TSet<FName> ReqProps;
|
|
const TMap<FName, UMaterialExpression*> EmptyInterp;
|
|
TSet<FName> IgnoredUsed;
|
|
const FSampleNodes NoSamples; // VT/RVT reads are pixel-only; not available in a vertex-frequency interpolator body.
|
|
if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, EmptyInterp, IgnoredUsed, NoSamples, OutErrors))
|
|
{
|
|
return nullptr;
|
|
}
|
|
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, 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));
|
|
Custom->RebuildOutputs();
|
|
|
|
UMaterialExpressionVertexInterpolator* VI = NewExpr<UMaterialExpressionVertexInterpolator>(Material, IoY, -450);
|
|
VI->Input.Connect(0, Custom);
|
|
return VI;
|
|
}
|
|
|
|
/** Connect a topology mix factor (literal / Value block / Scalar property / scalar Interpolator) to a scalar pin. */
|
|
static bool ConnectFactor(
|
|
UMaterial& Material, FExpressionInput& Target, const FShaderLabFactor& Factor,
|
|
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
|
|
const TMap<FName, FParamNode>& PropertyNodes,
|
|
const TMap<FName, UMaterialExpression*>& InterpByName, const FShaderLabModel& Model,
|
|
TSet<FName>& UsedInterps, int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
if (Factor.Kind == FShaderLabFactor::EKind::Literal)
|
|
{
|
|
UMaterialExpressionConstant* Const = NewExpr<UMaterialExpressionConstant>(Material, IoY, -300);
|
|
Const->R = Factor.Literal;
|
|
Target.Connect(0, Const);
|
|
return true;
|
|
}
|
|
if (UMaterialExpressionCustom* const* ValueNode = ValueByName.Find(Factor.Name))
|
|
{
|
|
Target.Connect(0, *ValueNode);
|
|
return true;
|
|
}
|
|
if (const FParamNode* Node = PropertyNodes.Find(Factor.Name))
|
|
{
|
|
if (Node->Expr && !Node->bIsTexture)
|
|
{
|
|
Target.Connect(0, Node->Expr);
|
|
return true;
|
|
}
|
|
}
|
|
if (UMaterialExpression* const* InterpNode = InterpByName.Find(Factor.Name))
|
|
{
|
|
const FShaderLabInterpolator* Interp = Model.FindInterpolator(Factor.Name);
|
|
if (Interp && Interp->ReturnType == TEXT("float"))
|
|
{
|
|
Target.Connect(0, *InterpNode);
|
|
UsedInterps.Add(Factor.Name);
|
|
return true;
|
|
}
|
|
OutErrors.Add(FString::Printf(
|
|
TEXT("Topology factor '%s' is a Vertex Interpolator but not scalar (float); only float interpolators can be a mix factor"), *Factor.Name.ToString()));
|
|
return false;
|
|
}
|
|
OutErrors.Add(FString::Printf(
|
|
TEXT("Topology factor '%s' is neither a Value block, a Scalar property, nor a scalar Interpolator"), *Factor.Name.ToString()));
|
|
return false;
|
|
}
|
|
|
|
/** 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, UMaterialExpression*>& SlabByName,
|
|
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
|
|
const TMap<FName, FParamNode>& PropertyNodes,
|
|
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
|
|
int32& IoY, TArray<FString>& OutErrors)
|
|
{
|
|
if (!Model.Topology.IsValidIndex(Index))
|
|
{
|
|
OutErrors.Add(TEXT("Invalid topology node index"));
|
|
return nullptr;
|
|
}
|
|
const FShaderLabTopoNode& Node = Model.Topology[Index];
|
|
|
|
if (Node.Op == EShaderLabOp::SlabRef)
|
|
{
|
|
if (UMaterialExpression* const* Found = SlabByName.Find(Node.SlabRef))
|
|
{
|
|
return *Found;
|
|
}
|
|
OutErrors.Add(FString::Printf(TEXT("FrontMaterial references unknown Slab '%s'"), *Node.SlabRef.ToString()));
|
|
return nullptr;
|
|
}
|
|
|
|
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors);
|
|
UMaterialExpression* ChildB = (Node.ChildB != INDEX_NONE)
|
|
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors)
|
|
: nullptr;
|
|
if (!ChildA || (Node.ChildB != INDEX_NONE && !ChildB))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
switch (Node.Op)
|
|
{
|
|
case EShaderLabOp::VerticalLayer:
|
|
{
|
|
UMaterialExpressionSubstrateVerticalLayering* N = NewExpr<UMaterialExpressionSubstrateVerticalLayering>(Material, IoY, -150);
|
|
N->Top.Connect(0, ChildA);
|
|
N->Base.Connect(0, ChildB);
|
|
return ConnectFactor(Material, N->Thickness, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
|
|
}
|
|
case EShaderLabOp::HorizontalMix:
|
|
{
|
|
UMaterialExpressionSubstrateHorizontalMixing* N = NewExpr<UMaterialExpressionSubstrateHorizontalMixing>(Material, IoY, -150);
|
|
N->Background.Connect(0, ChildA);
|
|
N->Foreground.Connect(0, ChildB);
|
|
return ConnectFactor(Material, N->Mix, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
|
|
}
|
|
case EShaderLabOp::Add:
|
|
{
|
|
UMaterialExpressionSubstrateAdd* N = NewExpr<UMaterialExpressionSubstrateAdd>(Material, IoY, -150);
|
|
N->A.Connect(0, ChildA);
|
|
N->B.Connect(0, ChildB);
|
|
return N;
|
|
}
|
|
case EShaderLabOp::Weight:
|
|
{
|
|
UMaterialExpressionSubstrateWeight* N = NewExpr<UMaterialExpressionSubstrateWeight>(Material, IoY, -150);
|
|
N->A.Connect(0, ChildA);
|
|
return ConnectFactor(Material, N->Weight, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
|
|
}
|
|
case EShaderLabOp::Select:
|
|
{
|
|
UMaterialExpressionSubstrateSelect* N = NewExpr<UMaterialExpressionSubstrateSelect>(Material, IoY, -150);
|
|
N->A.Connect(0, ChildA);
|
|
N->B.Connect(0, ChildB);
|
|
return ConnectFactor(Material, N->SelectValue, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
|
|
}
|
|
default:
|
|
OutErrors.Add(TEXT("Unhandled topology operator"));
|
|
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)
|
|
{
|
|
FString Best, BestDir;
|
|
for (const TPair<FString, FString>& Pair : AllShaderSourceDirectoryMappings())
|
|
{
|
|
if ((VirtualPath.StartsWith(Pair.Key + TEXT("/")) || VirtualPath == Pair.Key) && Pair.Key.Len() > Best.Len())
|
|
{
|
|
Best = Pair.Key;
|
|
BestDir = Pair.Value;
|
|
}
|
|
}
|
|
if (Best.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
FString Rest = VirtualPath.Mid(Best.Len());
|
|
Rest.RemoveFromStart(TEXT("/"));
|
|
OutDiskPath = FPaths::Combine(BestDir, Rest);
|
|
return true;
|
|
}
|
|
|
|
bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors)
|
|
{
|
|
using namespace ShaderLabGraph;
|
|
|
|
// Reset to a clean graph + apply material settings.
|
|
Material.AssignExpressionCollection(FMaterialExpressionCollection());
|
|
Material.MaterialDomain = MapDomain(Model.Settings.Domain);
|
|
Material.BlendMode = MapBlend(Model.Settings.BlendMode);
|
|
Material.TwoSided = Model.Settings.bTwoSided ? 1 : 0;
|
|
Material.bUseMaterialAttributes = false;
|
|
|
|
// Reflected long-tail settings (no usage: the base is a template; usage is set per-instance).
|
|
// Identical to the runtime shell (same model -> same FShaderLabSettingsApplier). Bad settings are
|
|
// a hard build failure (contract style).
|
|
if (!FShaderLabSettingsApplier::ApplyReflectedSettings(Material, Model.RawSettings, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
UMaterialEditorOnlyData* EditorOnly = Material.GetEditorOnlyData();
|
|
if (!EditorOnly)
|
|
{
|
|
OutErrors.Add(TEXT("Material has no editor-only data"));
|
|
return false;
|
|
}
|
|
|
|
// Resolve `.uslfunc` imports (transitively): promote their properties onto this material and gather the
|
|
// functions to emit. With no imports this is just the shader's own properties. Then compute the context
|
|
// (properties + intrinsics) each reachable library function needs, so emission and call sites agree.
|
|
FShaderLabResolvedProgram Program;
|
|
FLibraryEmit Emit;
|
|
Emit.Program = &Program;
|
|
if (Model.Imports.Num() > 0)
|
|
{
|
|
FShaderLabImportResolver::FSourceLoader Loader =
|
|
[](const FString& VirtualPath, FString& OutSource, FString& OutDiskPath, FString& OutError) -> bool
|
|
{
|
|
if (!ResolveVirtualShaderFile(VirtualPath, OutDiskPath))
|
|
{
|
|
OutError = TEXT("no shader-source directory maps this virtual path");
|
|
return false;
|
|
}
|
|
if (!FFileHelper::LoadFileToString(OutSource, *OutDiskPath))
|
|
{
|
|
OutError = FString::Printf(TEXT("cannot read '%s'"), *OutDiskPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
if (!FShaderLabImportResolver::Resolve(Model, Loader, Program, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
// Library free HLSL (non-function) is pure like shader local code — reject graph intrinsics in it.
|
|
for (const FShaderLabResolvedLibrary& Lib : Program.Libraries)
|
|
{
|
|
if (!CheckLocalCodeIntrinsics(Lib.Model, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Program.Properties = Model.Properties;
|
|
}
|
|
|
|
// Compute the transitive context of every library function reachable from the shader's bodies.
|
|
if (Emit.HasLibraries())
|
|
{
|
|
TSet<FName> DirectlyCalled;
|
|
auto Collect = [&](const FString& Body) { CollectCalledFunctions(Body, Program, DirectlyCalled); };
|
|
if (Model.bHasSurface) { Collect(Model.SurfaceBody); }
|
|
for (const FShaderLabSlab& Slab : Model.Slabs) { Collect(Slab.Body); }
|
|
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))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Promoted properties required (transitively) by any reachable library function — these get a parameter
|
|
// node even though they never appear literally in a shader body.
|
|
TSet<FName> GloballyUsedReqProps;
|
|
for (const TPair<FName, FFunctionCtx>& Pair : Emit.Ctx)
|
|
{
|
|
for (const FName& P : Pair.Value.ReqProps) { GloballyUsedReqProps.Add(P); }
|
|
}
|
|
|
|
// Emit the shader's free top-level HLSL (local functions/structs/globals) AND the rewritten library
|
|
// functions to a generated header that every Custom node #includes at file scope (defined-before-use
|
|
// for all pixel/vertex bodies). Guard against graph intrinsics in the shader's local code first.
|
|
if (!CheckLocalCodeIntrinsics(Model, OutErrors) || !WriteLocalCodeInclude(Model, Emit, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Per-pixel context is read via UE_* intrinsics, so the Surface entry takes just the output
|
|
// struct: `Surface(inout FShaderLabSurface S)`. For multi-slab there is no Surface param.
|
|
const FShaderLabEntryParam* SurfaceOutParam = Model.bHasSurface && Model.SurfaceParams.Num() > 0
|
|
? &Model.SurfaceParams.Last() : nullptr;
|
|
if (Model.bHasSurface && !SurfaceOutParam)
|
|
{
|
|
OutErrors.Add(TEXT("Surface(...) must take an (inout FShaderLabSurface) parameter"));
|
|
return false;
|
|
}
|
|
|
|
int32 ParamY = -400;
|
|
|
|
// True if a property is referenced by any body (Surface / Slabs / Values / Vertex) OR used directly
|
|
// as a topology mix factor (e.g. `VerticalLayer(Coat, Base, Thickness)` with Thickness a Scalar).
|
|
auto IsPropertyReferenced = [&Model](const FName PropName, const FString& NameStr) -> bool
|
|
{
|
|
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr)) { return true; }
|
|
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; } }
|
|
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)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// A promoted property gets a node if a body references it directly OR a reachable library function needs
|
|
// it. StaticBool additionally counts references inside reachable library function bodies (its promoted
|
|
// name equals its bare name, and it reaches those bodies through the global `#define`).
|
|
auto IsPropUsed = [&](const FShaderLabProperty& Prop) -> bool
|
|
{
|
|
const FString NameStr = Prop.Name.ToString();
|
|
if (IsPropertyReferenced(Prop.Name, NameStr))
|
|
{
|
|
return true;
|
|
}
|
|
if (Prop.Type == EShaderLabPropertyType::StaticBool)
|
|
{
|
|
for (const TPair<FName, FFunctionCtx>& Pair : Emit.Ctx)
|
|
{
|
|
int32 LibIdx = INDEX_NONE;
|
|
const FShaderLabFunction* Fn = Program.FindFunction(Pair.Key, LibIdx);
|
|
if (Fn && ReferencesToken(Fn->Body, NameStr))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return GloballyUsedReqProps.Contains(Prop.Name);
|
|
};
|
|
|
|
// 1) Create a parameter node per property referenced by any stage.
|
|
TMap<FName, FParamNode> PropertyNodes;
|
|
// Static-switch selectors funneled into the ParameterAnchor: each is a StaticSwitch over two
|
|
// `#define <Name> 1` / `#define <Name> 0` Custom nodes, driven by the switch parameter. The anchor
|
|
// is compiled before the material attributes and compiles these, so the selected per-permutation
|
|
// `#define` is emitted ahead of every body's `#if` — per-permutation static switches with zero
|
|
// engine changes, and nothing wired onto the user's body nodes.
|
|
TArray<UMaterialExpression*> AnchorInputs;
|
|
|
|
// Vertex Interpolators: VS-frequency values interpolated to the pixel shader. Built up-front so pixel
|
|
// bodies can reference them via UE_Interpolator(Name); UsedInterps tracks which get consumed (contract:
|
|
// a declared-but-unused interpolator is an error, mirroring the no-dead-slabs rule).
|
|
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();
|
|
|
|
if (Prop.Type == EShaderLabPropertyType::StaticBool)
|
|
{
|
|
if (IsPropUsed(Prop))
|
|
{
|
|
// Real static-switch parameter so Material Instances can override it (shown in the MIC editor).
|
|
// Reached for visibility via the selector below (which the anchor connects).
|
|
UMaterialExpressionStaticBoolParameter* E = NewExpr<UMaterialExpressionStaticBoolParameter>(Material, ParamY, -1000);
|
|
E->ParameterName = Prop.Name;
|
|
E->DefaultValue = Prop.bStaticBoolDefault ? 1 : 0;
|
|
E->Group = FName(*Prop.Group);
|
|
E->SortPriority = Prop.SortPriority;
|
|
|
|
// Two trivial Custom nodes emit `#define Name 1` / `#define Name 0`; a StaticSwitch driven by
|
|
// the parameter selects one. The translator compiles ONLY the selected branch, so exactly one
|
|
// `#define` is produced per shader permutation (incl. the MIC's static override).
|
|
auto MakeDefiner = [&](bool bValue) -> UMaterialExpressionCustom*
|
|
{
|
|
UMaterialExpressionCustom* D = NewExpr<UMaterialExpressionCustom>(Material, ParamY, -1300);
|
|
D->Description = TEXT("ShaderLab StaticSwitch Define");
|
|
D->OutputType = CMOT_Float1;
|
|
D->Code = TEXT("return 0;");
|
|
FCustomDefine DD;
|
|
DD.DefineName = NameStr;
|
|
DD.DefineValue = bValue ? TEXT("1") : TEXT("0");
|
|
D->AdditionalDefines.Add(DD);
|
|
return D;
|
|
};
|
|
UMaterialExpressionStaticSwitch* Selector = NewExpr<UMaterialExpressionStaticSwitch>(Material, ParamY, -1150);
|
|
Selector->A.Connect(0, MakeDefiner(true)); // selected when the switch is TRUE
|
|
Selector->B.Connect(0, MakeDefiner(false)); // selected when FALSE
|
|
Selector->Value.Connect(0, E);
|
|
Selector->DefaultValue = Prop.bStaticBoolDefault;
|
|
AnchorInputs.Add(Selector);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!IsPropUsed(Prop))
|
|
{
|
|
continue; // Unused value property: skip (keeps the graph minimal and deterministic).
|
|
}
|
|
|
|
FParamNode Node;
|
|
switch (Prop.Type)
|
|
{
|
|
case EShaderLabPropertyType::Scalar:
|
|
{
|
|
UMaterialExpressionScalarParameter* E = NewExpr<UMaterialExpressionScalarParameter>(Material, ParamY, -1000);
|
|
E->ParameterName = Prop.Name;
|
|
E->DefaultValue = Prop.ScalarDefault;
|
|
E->Group = FName(*Prop.Group);
|
|
E->SortPriority = Prop.SortPriority;
|
|
if (Prop.bHasClampMin)
|
|
{
|
|
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;
|
|
}
|
|
case EShaderLabPropertyType::Color:
|
|
case EShaderLabPropertyType::Vector:
|
|
{
|
|
UMaterialExpressionVectorParameter* E = NewExpr<UMaterialExpressionVectorParameter>(Material, ParamY, -1000);
|
|
E->ParameterName = Prop.Name;
|
|
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;
|
|
}
|
|
case EShaderLabPropertyType::Texture2D:
|
|
case EShaderLabPropertyType::TextureCube:
|
|
{
|
|
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
|
|
E->ParameterName = Prop.Name;
|
|
E->Group = FName(*Prop.Group);
|
|
E->SortPriority = Prop.SortPriority;
|
|
bool bIsNormal = false;
|
|
E->Texture = ResolveDefaultTexture(Prop.TextureDefault, bIsNormal);
|
|
// Explicit SamplerType wins; otherwise infer (normal default token -> Normal, else Color).
|
|
EMaterialSamplerType Sampler = bIsNormal ? SAMPLERTYPE_Normal : SAMPLERTYPE_Color;
|
|
if (!Prop.SamplerType.IsEmpty())
|
|
{
|
|
const bool bMapped = MapSamplerType(Prop.SamplerType, Sampler);
|
|
checkf(bMapped, TEXT("ShaderLab property '%s': invalid SamplerType '%s' (parser should have rejected)."),
|
|
*Prop.Name.ToString(), *Prop.SamplerType);
|
|
}
|
|
E->SamplerType = Sampler;
|
|
Node.Expr = E;
|
|
Node.bIsTexture = true;
|
|
break;
|
|
}
|
|
case EShaderLabPropertyType::Texture2DArray:
|
|
case EShaderLabPropertyType::Texture3D:
|
|
case EShaderLabPropertyType::TextureCubeArray:
|
|
{
|
|
// Array / volume texture object parameter. Built-in white/black/grey/normal default tokens are
|
|
// Texture2D-only and must NOT be applied here, but an explicit asset-path default (e.g.
|
|
// "/Engine/EngineResources/DefaultVolumeTexture") of the matching dimension is honored. Left null,
|
|
// the engine assigns a matching-dimension default and the artist binds a real asset on the instance.
|
|
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
|
|
E->ParameterName = Prop.Name;
|
|
E->Group = FName(*Prop.Group);
|
|
E->SortPriority = Prop.SortPriority;
|
|
EMaterialSamplerType Sampler = SAMPLERTYPE_Color;
|
|
if (!Prop.SamplerType.IsEmpty())
|
|
{
|
|
const bool bMapped = MapSamplerType(Prop.SamplerType, Sampler);
|
|
checkf(bMapped, TEXT("ShaderLab property '%s': invalid SamplerType '%s' (parser should have rejected)."),
|
|
*Prop.Name.ToString(), *Prop.SamplerType);
|
|
}
|
|
E->SamplerType = Sampler;
|
|
if (Prop.TextureDefault.StartsWith(TEXT("/")))
|
|
{
|
|
UTexture* DefaultTex = LoadObject<UTexture>(nullptr, *Prop.TextureDefault);
|
|
checkf(DefaultTex, TEXT("ShaderLab property '%s': array/volume DefaultTexture '%s' failed to load."),
|
|
*Prop.Name.ToString(), *Prop.TextureDefault);
|
|
E->Texture = DefaultTex;
|
|
}
|
|
Node.Expr = E;
|
|
Node.bIsTexture = true;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
if (Node.Expr)
|
|
{
|
|
PropertyNodes.Add(Prop.Name, Node);
|
|
}
|
|
}
|
|
|
|
// 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 (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).
|
|
}
|
|
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)
|
|
{
|
|
if (InterpByName.Contains(Interp.Name))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("Duplicate interpolator name '%s'"), *Interp.Name.ToString()));
|
|
return false;
|
|
}
|
|
UMaterialExpression* Node = BuildInterpolatorNode(Material, Interp, Model, Program, Emit, PropertyNodes, ParamY, OutErrors);
|
|
if (!Node)
|
|
{
|
|
return false;
|
|
}
|
|
InterpByName.Add(Interp.Name, Node);
|
|
}
|
|
|
|
// 1c) Pre-sampled VT / RVT read nodes (SL_VTSAMPLE / SL_RVTSAMPLE). A virtual texture cannot be sampled
|
|
// inside a Custom node, so we build a real sample node here (sampling at the author-chosen UV) and let
|
|
// PrepareBody wire its output(s) into the pixel bodies. Only build a node when a pixel body references it.
|
|
TMap<FName, UMaterialExpression*> VTNodeByName;
|
|
TMap<FName, UMaterialExpression*> RVTNodeByName;
|
|
{
|
|
auto ReferencedInPixelBodies = [&Model](const FString& NameStr) -> bool
|
|
{
|
|
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr)) { return true; }
|
|
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;
|
|
};
|
|
|
|
// Build the coordinate expression feeding a sample node's Coordinates pin. Returns true on success;
|
|
// OutCoord is null for UV=World (leave Coordinates unconnected — the node derives UV from world pos).
|
|
auto BuildUVNode = [&](const FShaderLabUV& UV, const TCHAR* What, FName Owner, UMaterialExpression*& OutCoord) -> bool
|
|
{
|
|
OutCoord = nullptr;
|
|
switch (UV.Kind)
|
|
{
|
|
case FShaderLabUV::EKind::TexCoord:
|
|
{
|
|
UMaterialExpressionTextureCoordinate* TC = NewExpr<UMaterialExpressionTextureCoordinate>(Material, ParamY, -800);
|
|
TC->CoordinateIndex = UV.TexCoordIndex;
|
|
OutCoord = TC;
|
|
return true;
|
|
}
|
|
case FShaderLabUV::EKind::World:
|
|
return true; // Coordinates unconnected.
|
|
case FShaderLabUV::EKind::ValueBlock:
|
|
{
|
|
const FShaderLabValue* Block = Model.Values.FindByPredicate(
|
|
[&](const FShaderLabValue& V) { return V.Name == UV.ValueBlockName; });
|
|
if (!Block)
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("%s '%s': UV references unknown Value block '%s'"),
|
|
What, *Owner.ToString(), *UV.ValueBlockName.ToString()));
|
|
return false;
|
|
}
|
|
if (Block->ReturnType != TEXT("float2"))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("%s '%s': UV Value block '%s' must return float2 (got '%s')"),
|
|
What, *Owner.ToString(), *UV.ValueBlockName.ToString(), *Block->ReturnType));
|
|
return false;
|
|
}
|
|
const FSampleNodes NoSamples; // A UV block is itself a coordinate source; it must not sample VT/RVT.
|
|
UMaterialExpressionCustom* UVNode = BuildValueNode(
|
|
Material, *Block, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, NoSamples, ParamY, OutErrors);
|
|
if (!UVNode) { return false; }
|
|
OutCoord = UVNode;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
for (const FShaderLabVTSample& VT : Model.VTSamples)
|
|
{
|
|
if (!ReferencedInPixelBodies(VT.Name.ToString())) { continue; }
|
|
EMaterialSamplerType Sampler = SAMPLERTYPE_VirtualColor;
|
|
const FString Token = VT.SamplerType.IsEmpty() ? FString(TEXT("VirtualColor")) : VT.SamplerType;
|
|
const bool bMapped = MapVirtualSamplerType(Token, Sampler);
|
|
checkf(bMapped, TEXT("SL_VTSAMPLE '%s': invalid virtual SamplerType '%s' (parser should have rejected)."),
|
|
*VT.Name.ToString(), *Token);
|
|
|
|
UMaterialExpressionTextureSampleParameter2D* S = NewExpr<UMaterialExpressionTextureSampleParameter2D>(Material, ParamY, -1000);
|
|
S->ParameterName = VT.Name;
|
|
S->SamplerType = Sampler;
|
|
if (!VT.DefaultTexture.IsEmpty())
|
|
{
|
|
// Graceful: a streaming VT default is often a project asset created later; if absent, leave null
|
|
// (the artist binds it on the MIC). Unlike array/volume defaults, this is not a hard contract.
|
|
S->Texture = LoadObject<UTexture>(nullptr, *VT.DefaultTexture);
|
|
}
|
|
UMaterialExpression* Coord = nullptr;
|
|
if (!BuildUVNode(VT.UV, TEXT("SL_VTSAMPLE"), VT.Name, Coord)) { return false; }
|
|
if (Coord) { S->Coordinates.Connect(0, Coord); }
|
|
VTNodeByName.Add(VT.Name, S);
|
|
}
|
|
|
|
for (const FShaderLabRVTSample& RVT : Model.RVTSamples)
|
|
{
|
|
if (!ReferencedInPixelBodies(RVT.Name.ToString())) { continue; }
|
|
ERuntimeVirtualTextureMaterialType MatType = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Roughness;
|
|
const bool bMapped = MapRVTMaterialType(RVT.MaterialType, MatType);
|
|
checkf(bMapped, TEXT("SL_RVTSAMPLE '%s': invalid MaterialType '%s' (parser should have rejected)."),
|
|
*RVT.Name.ToString(), *RVT.MaterialType);
|
|
|
|
UMaterialExpressionRuntimeVirtualTextureSampleParameter* S = NewExpr<UMaterialExpressionRuntimeVirtualTextureSampleParameter>(Material, ParamY, -1000);
|
|
S->ParameterName = RVT.Name;
|
|
S->MaterialType = MatType;
|
|
if (!RVT.VirtualTexture.IsEmpty())
|
|
{
|
|
// Graceful: the RVT asset is a project asset (created with the level); if absent, leave null
|
|
// (the sample compiles to constants; the artist binds the RVT on the MIC).
|
|
S->VirtualTexture = LoadObject<URuntimeVirtualTexture>(nullptr, *RVT.VirtualTexture);
|
|
}
|
|
// Outputs (BaseColor..Mask4, all 8 pins) are populated by the node constructor's InitOutputs();
|
|
// they exist regardless of MaterialType, so member->pin indices in GRVTMembers are stable.
|
|
UMaterialExpression* Coord = nullptr;
|
|
if (!BuildUVNode(RVT.UV, TEXT("SL_RVTSAMPLE"), RVT.Name, Coord)) { return false; }
|
|
if (Coord) { S->Coordinates.Connect(0, Coord); }
|
|
RVTNodeByName.Add(RVT.Name, S);
|
|
}
|
|
}
|
|
FSampleNodes Samples;
|
|
Samples.VT = &VTNodeByName;
|
|
Samples.RVT = &RVTNodeByName;
|
|
|
|
// 2) Build the pixel stage and connect it to FrontMaterial (what makes it a Substrate material).
|
|
if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::PostProcess)
|
|
{
|
|
// PostProcess domain: Color -> EmissiveColor, Opacity -> Opacity (no Substrate slab).
|
|
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabPostProcess"), TEXT("ShaderLabDefaultPostProcess"),
|
|
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::UI)
|
|
{
|
|
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabUI"), TEXT("ShaderLabDefaultUI"),
|
|
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors))
|
|
{
|
|
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
|
|
// 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, TEXT("FShaderLabSurface"), TEXT("ShaderLabDefaultSurface"), ParamY, OutErrors);
|
|
if (!Slab)
|
|
{
|
|
return false;
|
|
}
|
|
EditorOnly->FrontMaterial.Connect(0, WrapBsdfForDecal(Material, Model, Slab, ParamY));
|
|
}
|
|
else
|
|
{
|
|
// 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))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("Duplicate Slab name '%s'"), *SlabDecl.Name.ToString()));
|
|
return false;
|
|
}
|
|
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, Samples,
|
|
/*bAllowMaterialOutputs*/ false, /*StructNameOverride*/ nullptr, /*DefaultFnOverride*/ nullptr, ParamY, OutErrors);
|
|
if (!Slab)
|
|
{
|
|
return false;
|
|
}
|
|
SlabByName.Add(SlabDecl.Name, Slab);
|
|
}
|
|
|
|
TMap<FName, UMaterialExpressionCustom*> ValueByName;
|
|
for (const FShaderLabValue& ValueDecl : Model.Values)
|
|
{
|
|
if (ValueByName.Contains(ValueDecl.Name))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("Duplicate Value name '%s'"), *ValueDecl.Name.ToString()));
|
|
return false;
|
|
}
|
|
UMaterialExpressionCustom* ValueNode = BuildValueNode(
|
|
Material, ValueDecl, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors);
|
|
if (!ValueNode)
|
|
{
|
|
return false;
|
|
}
|
|
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)
|
|
{
|
|
if (Node.Op == EShaderLabOp::SlabRef) { ReferencedSlabs.Add(Node.SlabRef); }
|
|
}
|
|
for (const FShaderLabSlab& SlabDecl : Model.Slabs)
|
|
{
|
|
if (!ReferencedSlabs.Contains(SlabDecl.Name))
|
|
{
|
|
OutErrors.Add(FString::Printf(TEXT("Slab '%s' is declared but never used in FrontMaterial"), *SlabDecl.Name.ToString()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
UMaterialExpression* Root = BuildTopologyNode(
|
|
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors);
|
|
if (!Root)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// 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 (!BuildMaterialOutputBlock(Material, *EditorOnly, Model, Program, Emit, PropertyNodes,
|
|
InterpByName, UsedInterps, Samples, Root, ParamY, OutErrors))
|
|
{
|
|
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
|
|
// takes just the output struct: `Vertex(inout FShaderLabVertex V)`.
|
|
if (Model.bHasVertex && Model.VertexParams.Num() >= 1)
|
|
{
|
|
const FShaderLabEntryParam& VtxOut = Model.VertexParams.Last();
|
|
|
|
TArray<const FVertexFieldDef*> UsedVtx;
|
|
for (const FVertexFieldDef& F : GVertexFields)
|
|
{
|
|
if (ReferencesToken(Model.VertexBody, VtxOut.Name + TEXT(".") + F.Field))
|
|
{
|
|
UsedVtx.Add(&F);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
VCustom->Description = TEXT("ShaderLab Vertex");
|
|
VCustom->OutputType = CMOT_Float1;
|
|
AddIncludes(*VCustom, Model);
|
|
|
|
FString Code;
|
|
|
|
// Intrinsics (Stage = vertex) + library-function context.
|
|
FString VertexBody = Model.VertexBody;
|
|
TArray<FIntrinsicWire> VtxIntrinsicWires;
|
|
TSet<FName> VtxReqProps;
|
|
// Vertex stage: UE_Interpolator is pixel-only, so pass an empty interpolator map (rejected there).
|
|
const TMap<FName, UMaterialExpression*> EmptyInterp;
|
|
TSet<FName> IgnoredUsedInterps;
|
|
const FSampleNodes NoSamples; // VT/RVT reads are pixel-only; not available in a vertex body.
|
|
if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, VtxIntrinsicWires, VtxReqProps, EmptyInterp, IgnoredUsedInterps, NoSamples, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
for (const FIntrinsicWire& Wire : VtxIntrinsicWires)
|
|
{
|
|
FCustomInput In;
|
|
In.InputName = Wire.InputName;
|
|
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
|
|
VCustom->Inputs.Add(In);
|
|
}
|
|
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"),
|
|
*VtxOut.Name, *WrapBodyWithLineMapping(VertexBody, Model.VertexBodyLine, VSrcPath));
|
|
|
|
int32 VOutputIndex = 1;
|
|
TArray<TPair<FString, int32>> VtxOutputs;
|
|
for (const FVertexFieldDef* F : UsedVtx)
|
|
{
|
|
FCustomOutput Out;
|
|
Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field));
|
|
Out.OutputType = F->OutType;
|
|
VCustom->AdditionalOutputs.Add(Out);
|
|
Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *VtxOut.Name, F->Field);
|
|
VtxOutputs.Add(TPair<FString, int32>(F->Field, VOutputIndex));
|
|
++VOutputIndex;
|
|
}
|
|
Code += TEXT("return 0.0f;\n");
|
|
|
|
VCustom->Code = Code;
|
|
VCustom->RebuildOutputs();
|
|
|
|
for (const TPair<FString, int32>& Pair : VtxOutputs)
|
|
{
|
|
if (Pair.Key == TEXT("WorldPositionOffset"))
|
|
{
|
|
EditorOnly->WorldPositionOffset.Connect(Pair.Value, VCustom);
|
|
}
|
|
else if (Pair.Key == TEXT("Displacement"))
|
|
{
|
|
EditorOnly->Displacement.Connect(Pair.Value, VCustom);
|
|
}
|
|
else if (Pair.Key.StartsWith(TEXT("CustomizedUV")))
|
|
{
|
|
const int32 UvIndex = FCString::Atoi(*Pair.Key.Mid(12));
|
|
if (UvIndex >= 0 && UvIndex < 8)
|
|
{
|
|
EditorOnly->CustomizedUVs[UvIndex].Connect(Pair.Value, VCustom);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3b) Optional Runtime Virtual Texture WRITE (SL_RVTOUTPUT): an additive custom output alongside the
|
|
// pixel entry. Uses the same VT/RVT sample context as the pixel bodies (a write body may also read).
|
|
if (Model.bHasRVTOutput)
|
|
{
|
|
if (!BuildRVTOutput(Material, Model.RVTOutputParamName, Model.RVTOutputBody, Model.RVTOutputBodyLine,
|
|
Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Contract: every declared interpolator must be consumed (read via UE_Interpolator, or used as a
|
|
// topology mix factor). A dead interpolator would silently waste a scarce interpolant slot.
|
|
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
|
|
{
|
|
if (!UsedInterps.Contains(Interp.Name))
|
|
{
|
|
OutErrors.Add(FString::Printf(
|
|
TEXT("Interpolator '%s' is declared but never used (read it with UE_Interpolator(%s) or use it as a topology factor)"),
|
|
*Interp.Name.ToString(), *Interp.Name.ToString()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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; }
|
|
if (Model.bHasMaterialOutput && ReferencesToken(Model.MaterialOutputBody, Token)) { return true; }
|
|
if (Model.bHasRVTOutput && ReferencesToken(Model.RVTOutputBody, 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);
|
|
}
|
|
}
|
|
|
|
// Anchor side-effect capabilities: raw-HLSL body helpers (SceneTexture reads, global distance field
|
|
// reads, ...) that call an engine HLSL function directly from a Custom node. The helper compiles as bare
|
|
// HLSL, but the runtime resource binding it needs is only established as a side effect of compiling a
|
|
// matching UMaterialExpression (UseSceneTextureId / bUsesGlobalDistanceField / ...). Because the Custom
|
|
// body bypasses that node, we synthesize hidden expression(s) wired into the before-attributes
|
|
// ParameterAnchor purely for the side effect. See FShaderLabAnchorCapabilityRegistry.
|
|
{
|
|
// Bodies to scan: the shader's own pixel/vertex bodies AND every reachable `.uslfunc` library-function
|
|
// body (a library function is inlined into the consuming Custom node, so it can equally reference a raw
|
|
// helper and must contribute to binding). Emit.Ctx holds exactly the reachable library functions.
|
|
TArray<FShaderLabScannedBody> Bodies;
|
|
if (Model.bHasSurface) { Bodies.Add({ &Model.SurfaceBody, Model.SurfaceBodyLine }); }
|
|
for (const FShaderLabSlab& Slab : Model.Slabs) { Bodies.Add({ &Slab.Body, Slab.BodyLine }); }
|
|
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)
|
|
{
|
|
int32 LibIdx = INDEX_NONE;
|
|
if (const FShaderLabFunction* Fn = Program.FindFunction(Pair.Key, LibIdx))
|
|
{
|
|
Bodies.Add({ &Fn->Body, Fn->BodyLine });
|
|
}
|
|
}
|
|
}
|
|
|
|
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
|
|
bool bCapabilityFailed = false;
|
|
FShaderLabAnchorCapabilityRegistry::Get().ForEach(
|
|
[&](const FShaderLabAnchorCapability& Cap)
|
|
{
|
|
if (bCapabilityFailed) { return; }
|
|
|
|
bool bActive = false;
|
|
for (const FShaderLabScannedBody& B : Bodies)
|
|
{
|
|
for (const FString& Token : Cap.TriggerTokens)
|
|
{
|
|
if (ReferencesToken(*B.Text, Token)) { bActive = true; break; }
|
|
}
|
|
if (bActive) { break; }
|
|
}
|
|
if (!bActive) { return; }
|
|
|
|
FShaderLabAnchorCapabilityContext CapCtx{ Material, Model, Bodies, SrcPath,
|
|
[&Material, &ParamY](UClass* Class) -> UMaterialExpression*
|
|
{
|
|
UMaterialExpression* Expr = NewObject<UMaterialExpression>(&Material, Class);
|
|
Material.GetExpressionCollection().AddExpression(Expr);
|
|
Expr->MaterialExpressionEditorX = -1300;
|
|
Expr->MaterialExpressionEditorY = ParamY;
|
|
ParamY += 120;
|
|
return Expr;
|
|
} };
|
|
|
|
TArray<UMaterialExpression*> CapNodes;
|
|
if (!Cap.Emit(CapCtx, CapNodes, OutErrors))
|
|
{
|
|
bCapabilityFailed = true;
|
|
return;
|
|
}
|
|
AnchorInputs.Append(CapNodes);
|
|
});
|
|
if (bCapabilityFailed)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// body's `#if`. This gives per-permutation static switches with zero engine changes, and keeps the
|
|
// machinery off the user's body nodes. The anchor also makes the switch parameters visible in the
|
|
// Material Instance editor (reached via selector -> Value -> parameter).
|
|
if (AnchorInputs.Num() > 0)
|
|
{
|
|
UMaterialExpressionShaderLabParameterAnchor* Anchor =
|
|
NewExpr<UMaterialExpressionShaderLabParameterAnchor>(Material, ParamY, -1300);
|
|
Anchor->Inputs.SetNum(AnchorInputs.Num());
|
|
for (int32 Index = 0; Index < AnchorInputs.Num(); ++Index)
|
|
{
|
|
Anchor->Inputs[Index].Connect(0, AnchorInputs[Index]);
|
|
}
|
|
}
|
|
|
|
Material.UpdateCachedExpressionData();
|
|
return true;
|
|
}
|
|
|
|
void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath)
|
|
{
|
|
using namespace ShaderLabGraph;
|
|
|
|
// A minimal, structurally-valid Substrate material: one Custom node -> Slab.DiffuseAlbedo -> FrontMaterial,
|
|
// so the Custom code is reached by translation. The code `#error`s with the diagnostics; that aborts the
|
|
// shader preprocessor, so the material fails to compile with our messages — surfacing everywhere real
|
|
// shader errors do (MIC editor red text, FMaterialResource::GetCompileErrors, cook). DiffuseAlbedo (a core
|
|
// BSDF pin) is used deliberately: EmissiveColor gets dead-stripped from the compiled permutation, which
|
|
// would drop the Custom function (and its #error) before the preprocessor ever sees it.
|
|
Material.AssignExpressionCollection(FMaterialExpressionCollection());
|
|
Material.MaterialDomain = MD_Surface;
|
|
Material.BlendMode = BLEND_Opaque;
|
|
Material.TwoSided = 0;
|
|
Material.bUseMaterialAttributes = false;
|
|
|
|
UMaterialEditorOnlyData* EditorOnly = Material.GetEditorOnlyData();
|
|
check(EditorOnly); // A UMaterial always has editor-only data in the editor (contract).
|
|
|
|
// `#error` halts at the first hit, so merge every diagnostic into one directive (newlines stripped to
|
|
// keep it a single preprocessor line). The `#line` makes the compiler error click through to the .usl;
|
|
// each merged message still carries its own precise `(line,col)` as text.
|
|
FString Combined;
|
|
for (int32 Index = 0; Index < Diagnostics.Num(); ++Index)
|
|
{
|
|
if (Index > 0) { Combined += TEXT(" ; "); }
|
|
Combined += Diagnostics[Index];
|
|
}
|
|
Combined.ReplaceInline(TEXT("\r"), TEXT(""));
|
|
Combined.ReplaceInline(TEXT("\n"), TEXT(" "));
|
|
if (Combined.IsEmpty()) { Combined = TEXT("ShaderLab: shader failed to build"); }
|
|
|
|
int32 IoY = 0;
|
|
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
|
|
Custom->Description = TEXT("ShaderLab Error");
|
|
Custom->OutputType = CMOT_Float3;
|
|
Custom->Code = FString::Printf(
|
|
TEXT("#line 1 \"%s\"\n#error ShaderLab: %s\n#line 1 \"ShaderLabGenerated.ush\"\nreturn float3(1,0,1);\n"),
|
|
*SrcPath, *Combined);
|
|
Custom->RebuildOutputs();
|
|
|
|
if (Substrate::IsSubstrateEnabled())
|
|
{
|
|
// Substrate: Custom -> Slab.DiffuseAlbedo -> FrontMaterial (DiffuseAlbedo is a core BSDF pin, always
|
|
// translated, so the #error is reached).
|
|
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(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();
|
|
}
|
|
|
|
const TCHAR* FShaderLabGraphBuilder::GetGeneratedVirtualRoot()
|
|
{
|
|
return TEXT("/UShaderLabGen");
|
|
}
|
|
|
|
FString FShaderLabGraphBuilder::GetGeneratedShaderDir()
|
|
{
|
|
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"));
|
|
if (!Plugin.IsValid())
|
|
{
|
|
return FString();
|
|
}
|
|
return FPaths::Combine(Plugin->GetBaseDir(), TEXT("Intermediate"), TEXT("ShaderLabGen"));
|
|
}
|