Init repo

This commit is contained in:
Eragon-Brisingr
2026-06-30 15:36:02 +08:00
commit a861b4680b
32 changed files with 4167 additions and 0 deletions

86
.gitignore vendored Normal file
View File

@@ -0,0 +1,86 @@
# Visual Studio 2015 user specific files
.vs/
# Rider
.idea/
# Visual Studio 2015 database file
*.VC.db
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.out
*.app
*.ipa
# These project files can be generated by the engine
*.xcodeproj
*.xcworkspace
*.sln
*.suo
*.opensdf
*.sdf
*.VC.db
*.VC.opendb
# Precompiled Assets
SourceArt/**/*.png
SourceArt/**/*.tga
# Binary Files
Binaries/
Plugins/*/Binaries/*
# Builds
Build/*
# Whitelist PakBlacklist-<BuildConfiguration>.txt files
!Build/*/
Build/*/**
!Build/*/PakBlacklist*.txt
# Don't ignore icon files in Build
!Build/**/*.ico
# Built data for maps
*_BuiltData.uasset
# Configuration files generated by the Editor
Saved/*
# Compiled source files for the engine to use
Intermediate/*
Plugins/*/Intermediate/*
# Cache files for the editor to use
DerivedDataCache/*
# python
*.pyc
# temp file
enc_temp_folder/*
*.TMP

View File

@@ -0,0 +1,114 @@
// Copyright FlecsProj. All Rights Reserved.
// Shared struct definitions for ShaderLab-generated Custom HLSL nodes.
// Included by every generated UMaterialExpressionCustom node via IncludeFilePaths.
#pragma once
// Per-pixel/vertex inputs are read through `UE::NodeName(...)` intrinsics (resolved by the graph
// builder to real material expression nodes), not through a context struct.
// Surface description filled by the Surface(...) body. Defaults mirror the
// UMaterialExpressionSubstrateSlabBSDF pin defaults so that fields the body does not
// touch keep Substrate's native behavior.
struct FShaderLabSurface
{
float3 DiffuseAlbedo;
float3 F0;
float3 F90;
float Roughness;
float Anisotropy;
float3 Normal;
float3 Tangent;
float3 SSSMFP;
float SSSMFPScale;
float SSSPhaseAnisotropy;
float3 EmissiveColor;
float SecondRoughness;
float SecondRoughnessWeight;
float FuzzRoughness;
float FuzzAmount;
float3 FuzzColor;
float GlintValue;
float2 GlintUV;
float Opacity;
float OpacityMask;
};
// Output for the PostProcess(...) entry (Domain = PostProcess). Color feeds the material EmissiveColor.
struct FShaderLabPostProcess
{
float3 Color;
float Opacity;
};
// Output for the UI(...) entry (Domain = UI). Color feeds EmissiveColor, Opacity the material Opacity.
struct FShaderLabUI
{
float3 Color;
float Opacity;
};
// Vertex-stage outputs filled by the optional Vertex(...) body.
struct FShaderLabVertex
{
float3 WorldPositionOffset;
float Displacement;
float2 CustomizedUV0;
float2 CustomizedUV1;
float2 CustomizedUV2;
float2 CustomizedUV3;
};
FShaderLabSurface ShaderLabDefaultSurface()
{
FShaderLabSurface S;
S.DiffuseAlbedo = float3(0.18, 0.18, 0.18);
S.F0 = float3(0.04, 0.04, 0.04);
S.F90 = float3(1, 1, 1);
S.Roughness = 0.5;
S.Anisotropy = 0;
S.Normal = float3(0, 0, 1);
S.Tangent = float3(1, 0, 0);
S.SSSMFP = float3(0, 0, 0);
S.SSSMFPScale = 1;
S.SSSPhaseAnisotropy = 1;
S.EmissiveColor = float3(0, 0, 0);
S.SecondRoughness = 0.5;
S.SecondRoughnessWeight = 0;
S.FuzzRoughness = 0.5;
S.FuzzAmount = 0;
S.FuzzColor = float3(0, 0, 0);
S.GlintValue = 1;
S.GlintUV = float2(0, 0);
S.Opacity = 1;
S.OpacityMask = 1;
return S;
}
FShaderLabPostProcess ShaderLabDefaultPostProcess()
{
FShaderLabPostProcess P;
P.Color = float3(0, 0, 0);
P.Opacity = 1;
return P;
}
FShaderLabUI ShaderLabDefaultUI()
{
FShaderLabUI U;
U.Color = float3(0, 0, 0);
U.Opacity = 1;
return U;
}
FShaderLabVertex ShaderLabDefaultVertex()
{
FShaderLabVertex V;
V.WorldPositionOffset = float3(0, 0, 0);
V.Displacement = 0;
V.CustomizedUV0 = float2(0, 0);
V.CustomizedUV1 = float2(0, 0);
V.CustomizedUV2 = float2(0, 0);
V.CustomizedUV3 = float2(0, 0);
return V;
}

View File

@@ -0,0 +1,37 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabDiscovery.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
TArray<FString> FShaderLabDiscovery::GetSearchRoots()
{
// `.usl` files live in `Shaders` directories (project + plugins) — the same place .ush library
// files go and which is mapped to a virtual shader path (so `Includes { "/Project/..." }` resolves).
TArray<FString> Roots;
Roots.Add(FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders")));
for (const TSharedRef<IPlugin>& Plugin : IPluginManager::Get().GetEnabledPlugins())
{
const FString Candidate = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders"));
Roots.AddUnique(Candidate);
}
return Roots;
}
TArray<FString> FShaderLabDiscovery::FindShaderLabFiles()
{
TArray<FString> Files;
for (const FString& Root : GetSearchRoots())
{
if (IFileManager::Get().DirectoryExists(*Root))
{
TArray<FString> Found;
IFileManager::Get().FindFilesRecursive(Found, *Root, TEXT("*.usl"), true, false, false);
Files.Append(Found);
}
}
return Files;
}

View File

@@ -0,0 +1,27 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabMaterialInstanceConstant.h"
#include "Materials/Material.h"
#include "ShaderLabMaterialRegistry.h"
#include "UObject/Package.h"
bool UShaderLabMaterialInstanceConstant::HasOverridenBaseProperties() const
{
// Force a self-contained permutation only at the ROOT — where our immediate parent is a ShaderLab
// in-memory /Script base material (which has no serialized shader map). For child instances (parent
// is another instance) fall through to stock behavior so they DEFER to and SHARE the root's shader
// map — this is what bounds the shader-map count (no combinatorial explosion across variants).
//
// The condition compares the parent's package against the ShaderLab /Script package POINTER (not a
// string name): a non-instance parent in that exact package is one of our bases.
if (const UMaterial* BaseMaterial = Cast<UMaterial>(Parent))
{
if (BaseMaterial->GetOutermost() == FShaderLabMaterialRegistry::Get().GetPackage())
{
return true;
}
}
return Super::HasOverridenBaseProperties();
}

View File

@@ -0,0 +1,111 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabMaterialRegistry.h"
#include "Materials/Material.h"
#include "Misc/Paths.h"
#include "ShaderLabModel.h"
#include "Serialization/AsyncLoadingEvents.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabRegistry, Log, All);
namespace
{
const TCHAR* GShaderLabPackageName = TEXT("/Script/UShaderLab");
}
FShaderLabMaterialRegistry& FShaderLabMaterialRegistry::Get()
{
static FShaderLabMaterialRegistry Instance;
return Instance;
}
UPackage* FShaderLabMaterialRegistry::GetPackage()
{
if (!Package)
{
// Mirror the engine's native-package convention (and AngelScript literal assets):
// a compiled-in /Script package whose objects resolve via FindObject, never disk.
Package = NewObject<UPackage>(nullptr, GShaderLabPackageName, RF_Public | RF_Standalone | RF_MarkAsRootSet);
Package->SetPackageFlags(PKG_CompiledIn);
Package->AddToRoot();
}
return Package;
}
FString FShaderLabMaterialRegistry::MakeObjectName(const FString& ShaderName)
{
FString Out;
Out.Reserve(ShaderName.Len());
for (const TCHAR C : ShaderName)
{
Out.AppendChar((FChar::IsAlnum(C) || C == TEXT('_')) ? C : TEXT('_'));
}
if (Out.IsEmpty())
{
Out = TEXT("Unnamed");
}
return Out;
}
FString FShaderLabMaterialRegistry::MakeObjectPath(const FString& ShaderName)
{
return FString::Printf(TEXT("%s.%s"), GShaderLabPackageName, *MakeObjectName(ShaderName));
}
UMaterial* FShaderLabMaterialRegistry::FindMaterial(const FString& ShaderName) const
{
if (!Package)
{
return nullptr;
}
return FindObject<UMaterial>(Package, *MakeObjectName(ShaderName));
}
UMaterial* FShaderLabMaterialRegistry::RegisterFromModel(const FShaderLabModel& Model)
{
UPackage* Pkg = GetPackage();
const FString ObjName = MakeObjectName(Model.ShaderName);
// Contract: a material's identity is its file name. Two different source files that sanitize to the
// same object name are a hard error (would otherwise silently clobber each other / break MIC refs).
if (const FShaderLabModel* Existing = RegisteredModels.Find(ObjName))
{
if (!FPaths::IsSamePath(Existing->SourceFilePath, Model.SourceFilePath))
{
UE_LOG(LogShaderLabRegistry, Error,
TEXT("ShaderLab: duplicate material name '%s' from '%s' and '%s' — rename one file."),
*ObjName, *Existing->SourceFilePath, *Model.SourceFilePath);
return nullptr;
}
}
UMaterial* Material = FindObject<UMaterial>(Pkg, *ObjName);
const bool bNewlyCreated = (Material == nullptr);
if (bNewlyCreated)
{
// RF_MarkAsRootSet keeps the material alive across GC. Without it the object is collected
// (RF_Standalone alone is not a GC root in a cooked runtime), which removes its entry from
// the IoStore script-object table (RemoveUnreachableScriptObjects) and makes cooked Material
// Instances that reference it resolve their Parent to null -> fall back to the default material.
Material = NewObject<UMaterial>(Pkg, FName(*ObjName), RF_Public | RF_Standalone | RF_MarkAsRootSet);
}
RegisteredModels.FindOrAdd(ObjName) = Model;
// Register with the loading system so imports to "/Script/UShaderLab.<ObjName>" from other
// packages resolve to this in-memory object even if requested mid async-load.
NotifyRegistrationEvent(
FName(GShaderLabPackageName),
FName(*ObjName),
ENotifyRegistrationType::NRT_NoExportObject,
ENotifyRegistrationPhase::NRP_Finished,
nullptr,
/*bDynamic*/ false,
Material);
BuildMaterialDelegate.Broadcast(*Material, Model);
return Material;
}

View File

@@ -0,0 +1,14 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabModel.h"
const FShaderLabProperty* FShaderLabModel::FindProperty(FName InName) const
{
return Properties.FindByPredicate([InName](const FShaderLabProperty& P) { return P.Name == InName; });
}
bool FShaderLabModel::HasStaticSwitches() const
{
return Properties.ContainsByPredicate(
[](const FShaderLabProperty& P) { return P.Type == EShaderLabPropertyType::StaticBool; });
}

View File

@@ -0,0 +1,34 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabModule.h"
#include "Modules/ModuleManager.h"
#if !WITH_EDITOR
#include "Materials/Material.h"
#include "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabRuntimeBuilder.h"
#endif
DEFINE_LOG_CATEGORY_STATIC(LogShaderLab, Log, All);
void FShaderLabModule::StartupModule()
{
#if !WITH_EDITOR
// In cooked builds there is no editor graph builder; apply material settings when the registry
// builds a base shell. The shell carries no shader map — instances carry their own.
FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddLambda(
[](UMaterial& Material, const FShaderLabModel& Model)
{
FShaderLabRuntimeBuilder::ApplySettings(Material, Model);
});
#endif
UE_LOG(LogShaderLab, Log, TEXT("ShaderLab runtime module started."));
}
void FShaderLabModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FShaderLabModule, UShaderLab)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabRuntimeBuilder.h"
#include "Engine/EngineTypes.h"
#include "MaterialDomain.h"
#include "Materials/Material.h"
#include "ShaderLabModel.h"
#include "ShaderLabSettingsApplier.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabRuntime, Log, All);
void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderLabModel& Model)
{
switch (Model.Settings.Domain)
{
case EShaderLabDomain::PostProcess: Material.MaterialDomain = MD_PostProcess; break;
case EShaderLabDomain::UI: Material.MaterialDomain = MD_UI; break;
case EShaderLabDomain::Decal: Material.MaterialDomain = MD_DeferredDecal; break;
case EShaderLabDomain::Surface:
default: Material.MaterialDomain = MD_Surface; break;
}
switch (Model.Settings.BlendMode)
{
case EShaderLabBlendMode::Masked: Material.BlendMode = BLEND_Masked; break;
case EShaderLabBlendMode::Translucent: Material.BlendMode = BLEND_Translucent; break;
case EShaderLabBlendMode::Additive: Material.BlendMode = BLEND_Additive; break;
case EShaderLabBlendMode::Modulate: Material.BlendMode = BLEND_Modulate; break;
case EShaderLabBlendMode::Opaque:
default: Material.BlendMode = BLEND_Opaque; break;
}
Material.TwoSided = Model.Settings.bTwoSided ? 1 : 0;
// Reflected long-tail settings. A failure here means a malformed .usl shipped in the build, so
// log it loudly. (No usage flags: the base shell is a never-rendered template; usage lives on
// the instances.)
TArray<FString> Errors;
FShaderLabSettingsApplier::ApplyReflectedSettings(Material, Model.RawSettings, Errors);
for (const FString& Error : Errors)
{
UE_LOG(LogShaderLabRuntime, Error, TEXT("ShaderLab runtime settings '%s': %s"), *Model.ShaderName, *Error);
}
}

View File

@@ -0,0 +1,141 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabSettingsApplier.h"
#include "Materials/Material.h"
#include "Materials/MaterialInterface.h"
#include "UObject/Class.h"
#include "UObject/EnumProperty.h"
#include "UObject/UnrealType.h"
namespace ShaderLabSettings_Private
{
// UMaterial properties a `.usl` Settings block may set by reflection. Names only — adding a
// setting is a one-line edit here. Every entry must be a non-editor-only property so the cooked
// runtime shell can apply the identical value.
const TSet<FName>& AllowedSettingNames()
{
static const TSet<FName> Names = {
FName(TEXT("OpacityMaskClipValue")),
FName(TEXT("TranslucencyLightingMode")),
FName(TEXT("TranslucencyPass")),
FName(TEXT("bCastDynamicShadowAsMasked")),
FName(TEXT("bEnableResponsiveAA")),
FName(TEXT("bScreenSpaceReflections")),
FName(TEXT("bContactShadows")),
FName(TEXT("bIsThinSurface")),
FName(TEXT("DitheredLODTransition")),
};
return Names;
}
bool ParseBool(const FString& In, bool& Out)
{
const FString V = In.TrimStartAndEnd();
if (V == TEXT("true") || V == TEXT("1")) { Out = true; return true; }
if (V == TEXT("false") || V == TEXT("0")) { Out = false; return true; }
return false;
}
// Coerce `Value` to `Prop`'s type and write it into `Material`. Returns false with a reason on
// type mismatch / unresolved enum token.
bool CoerceAndSet(UMaterial& Material, FProperty* Prop, const FString& Value, FString& OutReason)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(&Material);
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
{
bool bVal = false;
if (!ParseBool(Value, bVal))
{
OutReason = TEXT("expected true/false");
return false;
}
BoolProp->SetPropertyValue_InContainer(&Material, bVal);
return true;
}
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
{
FloatProp->SetPropertyValue(ValuePtr, FCString::Atof(*Value));
return true;
}
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
{
DoubleProp->SetPropertyValue(ValuePtr, FCString::Atod(*Value));
return true;
}
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
{
IntProp->SetPropertyValue(ValuePtr, FCString::Atoi(*Value));
return true;
}
// TEnumAsByte<EFoo> reflects as FByteProperty with ->Enum set; resolve the bare token by name.
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
{
if (UEnum* Enum = ByteProp->Enum)
{
const int64 EnumVal = Enum->GetValueByNameString(Value);
if (EnumVal == INDEX_NONE)
{
OutReason = FString::Printf(TEXT("unknown enum value '%s' for %s"), *Value, *Enum->GetName());
return false;
}
ByteProp->SetPropertyValue(ValuePtr, static_cast<uint8>(EnumVal));
return true;
}
ByteProp->SetPropertyValue(ValuePtr, static_cast<uint8>(FCString::Atoi(*Value)));
return true;
}
// enum class reflects as FEnumProperty.
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
{
UEnum* Enum = EnumProp->GetEnum();
const int64 EnumVal = Enum ? Enum->GetValueByNameString(Value) : INDEX_NONE;
if (EnumVal == INDEX_NONE)
{
OutReason = FString::Printf(TEXT("unknown enum value '%s'"), *Value);
return false;
}
EnumProp->GetUnderlyingProperty()->SetIntPropertyValue(ValuePtr, EnumVal);
return true;
}
OutReason = FString::Printf(TEXT("unsupported property type '%s'"), *Prop->GetClass()->GetName());
return false;
}
}
bool FShaderLabSettingsApplier::ApplyReflectedSettings(
UMaterial& Material,
const TArray<TPair<FString, FString>>& RawSettings,
TArray<FString>& OutErrors)
{
using namespace ShaderLabSettings_Private;
bool bOk = true;
for (const TPair<FString, FString>& Pair : RawSettings)
{
const FName Key(*Pair.Key);
if (!AllowedSettingNames().Contains(Key))
{
OutErrors.Add(FString::Printf(TEXT("Setting '%s' is not in the ShaderLab allowlist"), *Pair.Key));
bOk = false;
continue;
}
FProperty* Prop = UMaterial::StaticClass()->FindPropertyByName(Key);
if (!Prop)
{
OutErrors.Add(FString::Printf(TEXT("UMaterial has no property '%s'"), *Pair.Key));
bOk = false;
continue;
}
FString Reason;
if (!CoerceAndSet(Material, Prop, Pair.Value, Reason))
{
OutErrors.Add(FString::Printf(TEXT("Setting '%s': %s"), *Pair.Key, *Reason));
bOk = false;
}
}
return bOk;
}

View File

@@ -0,0 +1,81 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabSubsystem.h"
#include "Misc/CoreDelegates.h"
#include "Misc/FileHelper.h"
#include "ShaderLabDiscovery.h"
#include "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabParser.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabSubsystem, Log, All);
void UShaderLabSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Defer the scan until the engine (and material/shader systems) are fully up.
PostEngineInitHandle = FCoreDelegates::GetOnPostEngineInit().AddWeakLambda(this, [this]()
{
DiscoverAndRegisterAll();
});
}
void UShaderLabSubsystem::Deinitialize()
{
if (PostEngineInitHandle.IsValid())
{
FCoreDelegates::GetOnPostEngineInit().Remove(PostEngineInitHandle);
PostEngineInitHandle.Reset();
}
Super::Deinitialize();
}
void UShaderLabSubsystem::DiscoverAndRegisterAll()
{
if (bScanned)
{
return;
}
bScanned = true;
// Both editor and cooked runtime rebuild from the SAME .usl sources — there is no serialized
// side data. The .usl files are staged into packaged builds (a wildcard RuntimeDependency in
// ShaderLab.Build.cs) and the parser/discovery are runtime-safe, so a cooked build re-parses
// them exactly as the editor does. In the editor the bound builder compiles a graph; in a cooked
// build it only applies settings to a never-rendered base shell (the instances carry the shaders).
const TArray<FString> Files = FShaderLabDiscovery::FindShaderLabFiles();
int32 NumOk = 0;
for (const FString& File : Files)
{
if (RebuildFromFile(File))
{
++NumOk;
}
}
UE_LOG(LogShaderLabSubsystem, Log, TEXT("ShaderLab: registered %d/%d shader file(s)."), NumOk, Files.Num());
}
UMaterial* UShaderLabSubsystem::RebuildFromFile(const FString& FilePath)
{
FString Source;
if (!FFileHelper::LoadFileToString(Source, *FilePath))
{
UE_LOG(LogShaderLabSubsystem, Warning, TEXT("ShaderLab: failed to read '%s'"), *FilePath);
return nullptr;
}
FShaderLabModel Model;
TArray<FShaderLabParseError> Errors;
if (!FShaderLabParser::Parse(Source, FilePath, Model, Errors))
{
for (const FShaderLabParseError& E : Errors)
{
UE_LOG(LogShaderLabSubsystem, Error, TEXT("ShaderLab parse error in %s %s"), *FilePath, *E.ToString());
}
return nullptr;
}
return FShaderLabMaterialRegistry::Get().RegisterFromModel(Model);
}

View File

@@ -0,0 +1,15 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
/** Locates `.usl` source files under the project and enabled plugins. */
struct USHADERLAB_API FShaderLabDiscovery
{
/** Directories scanned for `*.usl` files: <Project>/ShaderLab and <Plugin>/ShaderLab. */
static TArray<FString> GetSearchRoots();
/** Absolute paths of every discovered `.usl` file. */
static TArray<FString> FindShaderLabFiles();
};

View File

@@ -0,0 +1,34 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Materials/MaterialInstanceConstant.h"
#include "ShaderLabMaterialInstanceConstant.generated.h"
/**
* A Material Instance Constant parented (directly or transitively) to a ShaderLab in-memory
* /Script base material.
*
* ShaderLab base materials live as pure in-memory /Script objects with NO serialized shader map.
* To render an instance of one without serializing the base's shader map (and the engine changes
* that would require), this subclass forces a *self-contained* static permutation — i.e. flips
* UMaterialInstance::bHasStaticPermutationResource to true so the instance compiles + cooks its OWN
* shader map through the standard pipeline — but ONLY when its immediate parent is one of those
* /Script base materials (a "root" instance).
*
* When the parent is another material instance (a "child"), it falls through to stock behavior so
* the child DEFERS to and SHARES the root's shader map. That keeps the shader-map count bounded:
* many color/parameter variants off one root share a single compiled map instead of each compiling
* its own (no combinatorial explosion). See ShaderLabMICShaderMapSpikeTest for the proof.
*/
UCLASS()
class USHADERLAB_API UShaderLabMaterialInstanceConstant : public UMaterialInstanceConstant
{
GENERATED_BODY()
public:
//~ UMaterialInstance interface
virtual bool HasOverridenBaseProperties() const override;
//~ End UMaterialInstance interface
};

View File

@@ -0,0 +1,57 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "ShaderLabModel.h"
class UPackage;
class UMaterial;
/**
* Broadcast when a base material shell has been created/located and needs its contents filled:
* in the editor the graph builder rebuilds the expression graph; at cooked runtime the shell
* builder applies settings + attaches the precompiled shader map.
*/
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnShaderLabBuildMaterial, UMaterial& /*Material*/, const FShaderLabModel& /*Model*/);
/**
* Owns the in-memory base materials. They live in a compiled-in package named "/Script/UShaderLab"
* so that saved assets (MICs) can hard-reference them by object path and have that import resolve
* via FindObject in editor, cook AND cooked runtime without any .uasset on disk — the same trick
* the engine uses for all native /Script packages. Materials must be created before any referencing
* asset is resolved (cook start / early runtime).
*/
class USHADERLAB_API FShaderLabMaterialRegistry
{
public:
static FShaderLabMaterialRegistry& Get();
/** The compiled-in "/Script/UShaderLab" package (created on first use, kept rooted). */
UPackage* GetPackage();
/** Deterministic, UObject-safe object name for a shader name like "ShaderLab/RustyMetal". */
static FString MakeObjectName(const FString& ShaderName);
/** Full object path, e.g. "/Script/UShaderLab.ShaderLab_RustyMetal". */
static FString MakeObjectPath(const FString& ShaderName);
/** Find an already-created base material by shader name (nullptr if absent). */
UMaterial* FindMaterial(const FString& ShaderName) const;
/**
* Find-or-create the base material for `Model`, register it for path resolution, and
* broadcast OnBuildMaterial so a bound builder fills it in. Idempotent per shader name.
*/
UMaterial* RegisterFromModel(const FShaderLabModel& Model);
FOnShaderLabBuildMaterial& OnBuildMaterial() { return BuildMaterialDelegate; }
/** Models registered so far, keyed by object name (used by the instance factory picker and the example/validation commandlets). */
const TMap<FString, FShaderLabModel>& GetRegisteredModels() const { return RegisteredModels; }
private:
UPackage* Package = nullptr;
FOnShaderLabBuildMaterial BuildMaterialDelegate;
TMap<FString, FShaderLabModel> RegisteredModels;
};

View File

@@ -0,0 +1,196 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
/**
* Pure-data model parsed from a `.usl` DSL file.
*
* This layer has NO Engine/GPU/UObject dependency on purpose: it is the single source of truth
* shared by both the editor graph builder (which turns it into a UMaterialExpression graph) and the
* cooked-runtime shell builder (which applies the settings to a never-rendered base shell). The same
* .usl is re-parsed at runtime from the packaged build. Keeping it pure also makes it trivially
* unit-testable without an editor or shader compiler.
*/
enum class EShaderLabPropertyType : uint8
{
Scalar,
Color,
Vector,
Texture2D,
TextureCube,
StaticBool,
};
enum class EShaderLabDomain : uint8
{
Surface,
PostProcess,
UI,
Decal,
};
/** Which single pixel-entry a shader declared (when bHasSurface). Selects the output struct + wiring. */
enum class EShaderLabEntry : uint8
{
Surface, // FShaderLabSurface -> Substrate slab
PostProcess, // FShaderLabPostProcess -> EmissiveColor/Opacity
UI, // FShaderLabUI -> EmissiveColor/Opacity
};
enum class EShaderLabBlendMode : uint8
{
Opaque,
Masked,
Translucent,
Additive,
Modulate,
};
/** A single declared shader property -> becomes a standard UE material parameter. */
struct USHADERLAB_API FShaderLabProperty
{
FName Name;
EShaderLabPropertyType Type = EShaderLabPropertyType::Scalar;
FString DisplayName;
FString Group;
int32 SortPriority = 0;
/** Range(min,max) meta — only meaningful for Scalar. */
bool bHasRange = false;
float RangeMin = 0.0f;
float RangeMax = 1.0f;
// Defaults (only the field matching Type is meaningful).
float ScalarDefault = 0.0f;
FLinearColor VectorDefault = FLinearColor::Black;
/** Built-in texture token ("white"/"black"/"normal"/"grey") or an asset path. */
FString TextureDefault;
bool bStaticBoolDefault = false;
};
/** A parameter in a `Surface(...)`/`Vertex(...)` entry-point signature. */
struct USHADERLAB_API FShaderLabEntryParam
{
FString Type;
FString Name;
bool bInout = false;
};
/** A named `Slab <Name>(inout FShaderLabSurface S){...}` block in a multi-slab shader. */
struct USHADERLAB_API FShaderLabSlab
{
FName Name;
/** Name of the inout output-struct parameter (e.g. "S"). */
FString OutParamName;
FString Body;
int32 BodyLine = 0;
};
/** A named `Value <Name>(){ return <float HLSL>; }` block, used as a topology mix factor. */
struct USHADERLAB_API FShaderLabValue
{
FName Name;
FString Body;
int32 BodyLine = 0;
};
/** Substrate topology operators (short DSL aliases mapping to engine Substrate expression nodes). */
enum class EShaderLabOp : uint8
{
SlabRef, // leaf: references a named Slab
VerticalLayer, // VerticalLayer(Top, Base, Thickness)
HorizontalMix, // HorizontalMix(Background, Foreground, Mix)
Add, // Add(A, B)
Weight, // Weight(A, Weight)
Select, // Select(A, B, Threshold)
};
/**
* A scalar argument to a topology operator. Either a float literal or a named reference; the named
* case is resolved by the graph builder to a Value block (preferred) or a Scalar property, error if
* neither exists (the parser cannot classify it because Value blocks may be declared later).
*/
struct USHADERLAB_API FShaderLabFactor
{
enum class EKind : uint8 { Literal, Named };
EKind Kind = EKind::Literal;
float Literal = 0.0f;
FName Name; // value-block or scalar-property name (when Kind == Named)
};
/** One node of the FrontMaterial topology tree. Children index into FShaderLabModel::Topology. */
struct USHADERLAB_API FShaderLabTopoNode
{
EShaderLabOp Op = EShaderLabOp::SlabRef;
FName SlabRef; // for SlabRef
int32 ChildA = INDEX_NONE; // first material input
int32 ChildB = INDEX_NONE; // second material input (binary ops)
bool bHasFactor = false;
FShaderLabFactor Factor; // for VerticalLayer/HorizontalMix/Weight/Select
};
struct USHADERLAB_API FShaderLabSettings
{
EShaderLabDomain Domain = EShaderLabDomain::Surface;
EShaderLabBlendMode BlendMode = EShaderLabBlendMode::Opaque;
bool bTwoSided = false;
};
struct USHADERLAB_API FShaderLabModel
{
/** Display/identity name = source file base name (e.g. "Basic" from "Basic.usl"). Drives the object name. */
FString ShaderName;
/** Source file this model was parsed from (for diagnostics / hashing context). */
FString SourceFilePath;
FShaderLabSettings Settings;
/**
* Long-tail material settings applied via reflection onto UMaterial by name (the typed
* Domain/BlendMode/TwoSided above stay first-class because the graph builder branches on them).
* Stored as raw key/value strings to keep this model Core-only; the allowlist check and value
* coercion happen in FShaderLabSettingsApplier at build/runtime, where errors are surfaced.
*/
TArray<TPair<FString, FString>> RawSettings;
TArray<FShaderLabProperty> Properties;
TArray<FString> Includes;
// Pixel stage. Either a single `Surface(...)` (sugar: one anonymous slab straight to FrontMaterial),
// OR one-or-more named `Slab` blocks + a `FrontMaterial = <topology>` expression. The two are
// mutually exclusive (enforced by the parser).
TArray<FShaderLabEntryParam> SurfaceParams;
FString SurfaceBody;
/** 1-based source line of the first character of SurfaceBody (for #line mapping of compile errors). */
int32 SurfaceBodyLine = 0;
/** True when a single pixel-entry (`Surface`/`PostProcess`/`UI`) was used (vs named Slabs + FrontMaterial). */
bool bHasSurface = false;
/** Which single pixel-entry was declared (valid when bHasSurface). */
EShaderLabEntry SurfaceEntry = EShaderLabEntry::Surface;
// Multi-slab topology (used when !bHasSurface).
TArray<FShaderLabSlab> Slabs;
TArray<FShaderLabValue> Values;
TArray<FShaderLabTopoNode> Topology;
int32 TopologyRoot = INDEX_NONE;
/** Optional material-level outputs (multi-slab): names of Value blocks feeding Opacity/OpacityMask. */
FName OpacityValueName;
FName OpacityMaskValueName;
// Vertex stage (optional).
bool bHasVertex = false;
TArray<FShaderLabEntryParam> VertexParams;
FString VertexBody;
int32 VertexBodyLine = 0;
/** Find a property by name (nullptr if absent). */
const FShaderLabProperty* FindProperty(FName InName) const;
/** True if any property is a StaticBool (drives shader-map permutation count). */
bool HasStaticSwitches() const;
};

View File

@@ -0,0 +1,13 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
class FShaderLabModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,41 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
struct FShaderLabModel;
/** A single parse diagnostic with 1-based source location. */
struct USHADERLAB_API FShaderLabParseError
{
int32 Line = 0;
int32 Column = 0;
FString Message;
FString ToString() const
{
return FString::Printf(TEXT("(%d:%d) %s"), Line, Column, *Message);
}
};
/**
* Recursive-descent parser for the `.usl` DSL.
*
* Input is untrusted text, so the parser collects diagnostics and returns false on failure
* rather than asserting (contract-style `check` is reserved for framework invariants, not
* external input validation).
*/
class USHADERLAB_API FShaderLabParser
{
public:
/**
* Parse `Source` into `OutModel`. `SourceName` is recorded into the model and used in
* diagnostics. Returns true on success; on failure `OutErrors` is non-empty.
*/
static bool Parse(
const FString& Source,
const FString& SourceName,
FShaderLabModel& OutModel,
TArray<FShaderLabParseError>& OutErrors);
};

View File

@@ -0,0 +1,21 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
struct FShaderLabModel;
/**
* Runtime (cooked) counterpart to the editor graph builder. There is no expression graph at runtime,
* so this only applies the material settings (domain / blend mode / two-sided + reflected settings)
* onto the base shell. The shell carries no shader map and is never rendered directly — it exists so
* that cooked UShaderLabMaterialInstanceConstant instances (which carry their own shaders) resolve
* their Parent and read parameter/setting metadata.
*/
struct USHADERLAB_API FShaderLabRuntimeBuilder
{
/** Apply the material settings (domain / blend / two-sided) + the reflected long-tail settings onto the shell. */
static void ApplySettings(UMaterial& Material, const FShaderLabModel& Model);
};

View File

@@ -0,0 +1,32 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
/**
* Applies the long-tail (non-typed) ShaderLab settings onto a UMaterial, shared by the editor graph
* builder and the cooked runtime shell builder so the same flags drive shader compilation
* everywhere.
*
* Reflected settings: arbitrary `Key = Value` pairs resolved against an allowlist of UMaterial
* properties and coerced by reflection (float/int/bool-bitfield/enum-by-name).
*
* (Material usage is intentionally NOT a ShaderLab concern: base materials are templates with no
* usage flags. Usage is established per-instance on the UShaderLabMaterialInstanceConstant — the
* editor auto-sets it on mesh assignment, or the author ticks it in the instance's Usage Flag
* Overrides — so only the vertex-factory permutations actually used get compiled.)
*
* Contract: an unknown/disallowed key or an unresolvable value is collected into OutErrors (a hard
* build failure / cook abort) — never silently skipped.
*/
struct USHADERLAB_API FShaderLabSettingsApplier
{
/** Apply reflected settings. Returns false (and appends to OutErrors) on any rejected entry. */
static bool ApplyReflectedSettings(
UMaterial& Material,
const TArray<TPair<FString, FString>>& RawSettings,
TArray<FString>& OutErrors);
};

View File

@@ -0,0 +1,34 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/EngineSubsystem.h"
#include "ShaderLabSubsystem.generated.h"
class UMaterial;
/**
* Discovers `.usl` files at startup and registers an in-memory base material for each.
* Runs the scan on post-engine-init so the material system is ready. Also drives hot-reload
* rebuilds of a single file in the editor.
*/
UCLASS()
class USHADERLAB_API UShaderLabSubsystem : public UEngineSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/** Scan every search root and (re)register all discovered shaders. */
void DiscoverAndRegisterAll();
/** Parse a single `.usl` file and (re)register its material. Returns null on parse error. */
UMaterial* RebuildFromFile(const FString& FilePath);
private:
bool bScanned = false;
FDelegateHandle PostEngineInitHandle;
};

View File

@@ -0,0 +1,33 @@
// Copyright FlecsProj. All Rights Reserved.
using UnrealBuildTool;
public class UShaderLab : ModuleRules
{
public UShaderLab(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
bUseUnity = false;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"RenderCore",
"RHI",
"Projects",
});
// Stage the .usl sources into packaged builds so the cooked runtime can rebuild base-material
// shells by re-parsing them (no serialized side data, no cook-time hook). A wildcard works here
// because .usl are source files that exist at build time (when RuntimeDependencies are globbed),
// unlike a cook-generated file. "..." matches recursively, including the top level.
RuntimeDependencies.Add("$(ProjectDir)/Shaders/.../*.usl", StagedFileType.UFS);
}
}

View File

@@ -0,0 +1,57 @@
// Copyright FlecsProj. All Rights Reserved.
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "MaterialCompiler.h"
UMaterialExpressionShaderLabParameterAnchor::UMaterialExpressionShaderLabParameterAnchor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
#if WITH_EDITOR
TArrayView<FExpressionInput*> UMaterialExpressionShaderLabParameterAnchor::GetInputsView()
{
CachedInputs.Reset(Inputs.Num());
for (FExpressionInput& Input : Inputs)
{
CachedInputs.Add(&Input);
}
return CachedInputs;
}
FExpressionInput* UMaterialExpressionShaderLabParameterAnchor::GetInput(int32 InputIndex)
{
return Inputs.IsValidIndex(InputIndex) ? &Inputs[InputIndex] : nullptr;
}
FName UMaterialExpressionShaderLabParameterAnchor::GetInputName(int32 InputIndex) const
{
return FName(*FString::Printf(TEXT("Param%d"), InputIndex));
}
int32 UMaterialExpressionShaderLabParameterAnchor::Compile(FMaterialCompiler* Compiler, int32 OutputIndex)
{
// Compiled in the before-attributes pass. Compiling each input forces its static-switch selector
// (and the selected `#define`-emitting Custom node) to compile here — emitting `#define <Switch> 0/1`
// for the current permutation ahead of every body's `#if`. The returned value is unused.
int32 Last = INDEX_NONE;
for (FExpressionInput& Input : Inputs)
{
if (Input.GetTracedInput().Expression)
{
const int32 Code = Input.Compile(Compiler);
if (Code != INDEX_NONE)
{
Last = Code;
}
}
}
return Last != INDEX_NONE ? Last : Compiler->Constant(0.0f);
}
void UMaterialExpressionShaderLabParameterAnchor::GetCaption(TArray<FString>& OutCaptions) const
{
OutCaptions.Add(TEXT("ShaderLab Static-Switch Defines / Parameter Anchor"));
}
#endif // WITH_EDITOR

View File

@@ -0,0 +1,63 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "MaterialValueType.h"
#include "Materials/MaterialExpressionCustomOutput.h"
#include "MaterialExpressionShaderLabParameterAnchor.generated.h"
/**
* Internal ShaderLab node with two jobs, both keeping workaround machinery off the user's body nodes:
*
* 1. Anchors parameters so the Material Instance editor lists them. StaticBool parameters are consumed
* only inside HLSL `#if`, so they would otherwise be unconnected; the instance editor's visibility
* walk only reaches parameters connected to a material output or a CustomOutput. This is a
* CustomOutput, so it is always traversed; each anchored parameter reaches one of its inputs (via
* its static-switch selector).
*
* 2. Injects per-permutation static-switch `#define`s with ZERO engine modifications. Each input is a
* StaticSwitch over two `#define <Switch> 1` / `#define <Switch> 0` Custom nodes. This node returns
* ShouldCompileBeforeAttributes()==true, so FHLSLMaterialTranslator compiles it BEFORE the material
* attributes (FrontMaterial / body). Its Compile() compiles each input, so the selected `#define`
* (only the taken StaticSwitch branch is compiled) is emitted ahead of every body's `#if`. The
* `#define` leaks forward (preprocessor is global) and the body's `#if <Switch>` sees the current
* permutation's value — including a Material Instance's static override.
*
* It only ever lives in the editor graph (never serialized into a cooked package).
*/
UCLASS(MinimalAPI, collapsecategories, hidecategories = Object)
class UMaterialExpressionShaderLabParameterAnchor : public UMaterialExpressionCustomOutput
{
GENERATED_UCLASS_BODY()
/** One input per static-switch selector (a StaticSwitch over two `#define`-emitting Custom nodes). */
UPROPERTY()
TArray<FExpressionInput> Inputs;
//~ Begin UMaterialExpressionCustomOutput Interface
// One output so the translator actually compiles us (a zero-output CustomOutput is skipped). The
// output value is unused; compiling us is purely to emit our inputs' `#define`s before attributes.
virtual int32 GetNumOutputs() const override { return 1; }
virtual FString GetFunctionName() const override { return TEXT("ShaderLabParameterAnchor"); }
#if WITH_EDITOR
virtual bool NeedsCustomOutputDefines() override { return false; } // don't emit NUM_MATERIAL_OUTPUTS_*
virtual bool ShouldCompileBeforeAttributes() override { return true; } // emit #defines ahead of body #if
#endif
//~ End UMaterialExpressionCustomOutput Interface
#if WITH_EDITOR
//~ Begin UMaterialExpression Interface
virtual TArrayView<FExpressionInput*> GetInputsView() override;
virtual FExpressionInput* GetInput(int32 InputIndex) override;
virtual FName GetInputName(int32 InputIndex) const override;
virtual EMaterialValueType GetInputValueType(int32 InputIndex) override { return MCT_Float1; }
virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
virtual void GetCaption(TArray<FString>& OutCaptions) const override;
//~ End UMaterialExpression Interface
#endif
private:
/** Scratch pointer view rebuilt by GetInputsView(). */
TArray<FExpressionInput*> CachedInputs;
};

View File

@@ -0,0 +1,168 @@
// Copyright FlecsProj. All Rights Reserved.
#include "DirectoryWatcherModule.h"
#include "Engine/Engine.h"
#include "IDirectoryWatcher.h"
#include "Interfaces/IPluginManager.h"
#include "Materials/Material.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "ShaderCore.h"
#include "ShaderLabDiscovery.h"
#include "ShaderLabGraphBuilder.h"
#include "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabSubsystem.h"
#include "UObject/Package.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabEditor, Log, All);
namespace
{
/** Editor-side reaction to a base material needing build: rebuild graph + trigger compile. */
void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model)
{
TArray<FString> Errors;
if (!FShaderLabGraphBuilder::BuildInto(Material, Model, Errors))
{
for (const FString& E : Errors)
{
UE_LOG(LogShaderLabEditor, Error, TEXT("ShaderLab build '%s': %s"), *Model.ShaderName, *E);
}
return;
}
// Recompile for rendering / editor preview (async).
Material.PostEditChange();
}
}
class FShaderLabEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
// Map the plugin's Shaders/ directory so generated Custom nodes can #include
// "/Plugin/ShaderLab/Private/ShaderLabCommon.ush" at shader-compile time.
if (const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab")))
{
const FString ShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders"));
if (!AllShaderSourceDirectoryMappings().Contains(TEXT("/Plugin/ShaderLab")))
{
AddShaderSourceDirectoryMapping(TEXT("/Plugin/ShaderLab"), ShaderDir);
}
}
// Map the project's Shaders/ directory to the virtual root "/Project" so user .ush library
// files placed there (alongside .usl) can be #included from a shader's Includes { } block.
{
const FString ProjectShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
if (FPaths::DirectoryExists(ProjectShaderDir) && !AllShaderSourceDirectoryMappings().Contains(TEXT("/Project")))
{
AddShaderSourceDirectoryMapping(TEXT("/Project"), ProjectShaderDir);
}
}
// Fill in / recompile base materials whenever the registry asks (startup + hot reload).
BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile);
StartWatchingSources();
// Note: ShaderLab base materials are deliberately NOT surfaced to the Content Browser — they are
// hidden templates. Users create UShaderLabMaterialInstanceConstant instances via the factory
// (Content Browser ▸ Material ▸ ShaderLab Material Instance), which picks a base directly from
// the registry. The instances are the only assets users interact with.
//
// There is NO cook-time hook: the .usl sources are staged into packaged builds (a wildcard
// RuntimeDependency in ShaderLab.Build.cs) and the cooked runtime re-parses them directly, so
// nothing special needs to be written at cook.
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLabEditor module started."));
}
virtual void ShutdownModule() override
{
StopWatchingSources();
if (BuildHandle.IsValid())
{
FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle);
BuildHandle.Reset();
}
}
private:
void StartWatchingSources()
{
FDirectoryWatcherModule& Module =
FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
IDirectoryWatcher* Watcher = Module.Get();
if (!Watcher)
{
return;
}
for (const FString& Root : FShaderLabDiscovery::GetSearchRoots())
{
if (!FPaths::DirectoryExists(Root))
{
continue;
}
FDelegateHandle Handle;
Watcher->RegisterDirectoryChangedCallback_Handle(
Root,
IDirectoryWatcher::FDirectoryChanged::CreateStatic(&FShaderLabEditorModule::OnDirectoryChanged),
Handle,
IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges);
WatchedRoots.Add(Root, Handle);
}
}
void StopWatchingSources()
{
if (FModuleManager::Get().IsModuleLoaded(TEXT("DirectoryWatcher")))
{
FDirectoryWatcherModule& Module =
FModuleManager::GetModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
if (IDirectoryWatcher* Watcher = Module.Get())
{
for (const TPair<FString, FDelegateHandle>& Pair : WatchedRoots)
{
Watcher->UnregisterDirectoryChangedCallback_Handle(Pair.Key, Pair.Value);
}
}
}
WatchedRoots.Reset();
}
static void OnDirectoryChanged(const TArray<FFileChangeData>& Changes)
{
UShaderLabSubsystem* Subsystem = GEngine ? GEngine->GetEngineSubsystem<UShaderLabSubsystem>() : nullptr;
if (!Subsystem)
{
return;
}
TSet<FString> Rebuilt;
for (const FFileChangeData& Change : Changes)
{
if (!Change.Filename.EndsWith(TEXT(".usl")))
{
continue;
}
if (Change.Action == FFileChangeData::FCA_Removed)
{
continue; // Leave the existing in-memory material in place on delete.
}
const FString Normalized = FPaths::ConvertRelativePathToFull(Change.Filename);
if (Rebuilt.Contains(Normalized))
{
continue;
}
Rebuilt.Add(Normalized);
Subsystem->RebuildFromFile(Normalized);
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLab hot-reloaded '%s'"), *Normalized);
}
}
FDelegateHandle BuildHandle;
TMap<FString, FDelegateHandle> WatchedRoots;
};
IMPLEMENT_MODULE(FShaderLabEditorModule, UShaderLabEditor)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabIntrinsicRegistry.h"
#include "Materials/Material.h"
#include "UObject/UObjectGlobals.h"
// Backing expression node types for the v1 builtin intrinsics.
#include "Materials/MaterialExpressionActorPositionWS.h"
#include "Materials/MaterialExpressionCameraPositionWS.h"
#include "Materials/MaterialExpressionCameraVectorWS.h"
#include "Materials/MaterialExpressionDeltaTime.h"
#include "Materials/MaterialExpressionLightVector.h"
#include "Materials/MaterialExpressionLightmapUVs.h"
#include "Materials/MaterialExpressionLocalPosition.h"
#include "Materials/MaterialExpressionObjectBounds.h"
#include "Materials/MaterialExpressionObjectLocalBounds.h"
#include "Materials/MaterialExpressionObjectOrientation.h"
#include "Materials/MaterialExpressionObjectPositionWS.h"
#include "Materials/MaterialExpressionObjectRadius.h"
#include "Materials/MaterialExpressionParticleColor.h"
#include "Materials/MaterialExpressionPerInstanceCustomData.h"
#include "Materials/MaterialExpressionPerInstanceFadeAmount.h"
#include "Materials/MaterialExpressionPerInstanceRandom.h"
#include "Materials/MaterialExpressionPixelDepth.h"
#include "Materials/MaterialExpressionPixelNormalWS.h"
#include "Materials/MaterialExpressionPreSkinnedLocalBounds.h"
#include "Materials/MaterialExpressionPreSkinnedNormal.h"
#include "Materials/MaterialExpressionPreSkinnedPosition.h"
#include "Materials/MaterialExpressionScreenPosition.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionTime.h"
#include "Materials/MaterialExpressionTwoSidedSign.h"
#include "Materials/MaterialExpressionVertexColor.h"
#include "Materials/MaterialExpressionVertexNormalWS.h"
#include "Materials/MaterialExpressionVertexTangentWS.h"
#include "Materials/MaterialExpressionViewProperty.h"
#include "Materials/MaterialExpressionWorldPosition.h"
namespace ShaderLabIntrinsic_Private
{
template <typename T>
UMaterialExpression* MakeSimple(UMaterial& Material)
{
T* Expr = NewObject<T>(&Material);
Material.GetExpressionCollection().AddExpression(Expr);
return Expr;
}
}
FShaderLabIntrinsicRegistry& FShaderLabIntrinsicRegistry::Get()
{
static FShaderLabIntrinsicRegistry Instance;
return Instance;
}
FShaderLabIntrinsicRegistry::FShaderLabIntrinsicRegistry()
{
RegisterBuiltins();
}
void FShaderLabIntrinsicRegistry::Register(FShaderLabIntrinsicDesc Desc)
{
check(!Desc.Name.IsNone());
check(Desc.MakeNode);
Descs.Add(Desc.Name, MoveTemp(Desc));
}
const FShaderLabIntrinsicDesc* FShaderLabIntrinsicRegistry::Find(FName Name) const
{
return Descs.Find(Name);
}
void FShaderLabIntrinsicRegistry::RegisterBuiltins()
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
// Helper for the common no-arg case.
auto AddSimple = [this](const TCHAR* Name, EFreq Freq, TFunction<UMaterialExpression*(UMaterial&)> Make)
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(Name);
Desc.Frequency = Freq;
Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray<FString>&, FString&) { return Make(M); };
Register(MoveTemp(Desc));
};
// --- A. Mesh / surface attributes ---
AddSimple(TEXT("VertexNormalWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
AddSimple(TEXT("VertexTangentWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
AddSimple(TEXT("VertexColor"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
AddSimple(TEXT("TwoSidedSign"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
AddSimple(TEXT("LightmapUVs"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
// TextureCoordinate(Index[, UTiling, VTiling]) — const args configure the node (mechanism: scalar/int args).
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("TextureCoordinate"));
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
{
UMaterialExpressionTextureCoordinate* E = NewObject<UMaterialExpressionTextureCoordinate>(&M);
if (Args.Num() >= 1) { E->CoordinateIndex = FCString::Atoi(*Args[0]); }
if (Args.Num() >= 2) { E->UTiling = FCString::Atof(*Args[1]); }
if (Args.Num() >= 3) { E->VTiling = FCString::Atof(*Args[2]); }
M.GetExpressionCollection().AddExpression(E);
return E;
};
Register(MoveTemp(Desc));
}
// --- B. Positions / spaces ---
AddSimple(TEXT("WorldPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
AddSimple(TEXT("LocalPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
AddSimple(TEXT("ActorPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
AddSimple(TEXT("CameraPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
AddSimple(TEXT("CameraVectorWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
AddSimple(TEXT("LightVector"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
// --- C. Object / bounds (ObjectLocalBounds & PreSkinnedLocalBounds are multi-output; output 0) ---
AddSimple(TEXT("ObjectRadius"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
AddSimple(TEXT("ObjectBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
AddSimple(TEXT("ObjectOrientation"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
// --- D. Instancing ---
AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
// PerInstanceCustomData(DataIndex[, ConstDefaultValue]) — mechanism: const args + optional input pin left at default.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("PerInstanceCustomData"));
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
{
UMaterialExpressionPerInstanceCustomData* E = NewObject<UMaterialExpressionPerInstanceCustomData>(&M);
if (Args.Num() >= 1) { E->DataIndex = static_cast<uint32>(FCString::Atoi(*Args[0])); }
if (Args.Num() >= 2) { E->ConstDefaultValue = FCString::Atof(*Args[1]); }
M.GetExpressionCollection().AddExpression(E);
return E;
};
Register(MoveTemp(Desc));
}
// --- Time ---
AddSimple(TEXT("Time"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
AddSimple(TEXT("DeltaTime"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
// ViewProperty(Property) — representative of the enum-by-name const-arg mechanism.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("ViewProperty"));
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString& OutError) -> UMaterialExpression*
{
UMaterialExpressionViewProperty* E = NewObject<UMaterialExpressionViewProperty>(&M);
if (Args.Num() >= 1)
{
UEnum* Enum = StaticEnum<EMaterialExposedViewProperty>();
check(Enum);
const int64 Value = Enum->GetValueByNameString(Args[0]);
if (Value == INDEX_NONE)
{
OutError = FString::Printf(TEXT("unknown view property '%s'"), *Args[0]);
return nullptr;
}
E->Property = static_cast<EMaterialExposedViewProperty>(Value);
}
M.GetExpressionCollection().AddExpression(E);
return E;
};
Register(MoveTemp(Desc));
}
// ParticleColor — only meaningful on particle vertex factories; the instance enables the matching
// usage, so there is no base-level usage gate here.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("ParticleColor"));
Desc.MakeNode = [](UMaterial& M, const TArray<FString>&, FString&) { return MakeSimple<UMaterialExpressionParticleColor>(M); };
Register(MoveTemp(Desc));
}
}

View File

@@ -0,0 +1,151 @@
// Copyright FlecsProj. All Rights Reserved.
#include "ShaderLabMaterialInstanceFactory.h"
#include "AssetRegistry/AssetData.h"
#include "AssetThumbnail.h"
#include "AssetTypeCategories.h"
#include "Editor.h"
#include "Framework/Application/SlateApplication.h"
#include "Materials/Material.h"
#include "Misc/App.h"
#include "ShaderLabMaterialInstanceConstant.h"
#include "ShaderLabMaterialRegistry.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Layout/SUniformWrapPanel.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SWindow.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "ShaderLabMaterialInstanceFactory"
namespace
{
/** Modal picker of the discovered ShaderLab base materials, with preview thumbnails. */
UMaterial* PickShaderLabBase()
{
FShaderLabMaterialRegistry& Registry = FShaderLabMaterialRegistry::Get();
// Collect the registered in-memory base materials (sorted by name for a stable layout).
TArray<UMaterial*> Bases;
TArray<FString> Names;
Registry.GetRegisteredModels().GenerateKeyArray(Names);
Names.Sort();
for (const FString& Name : Names)
{
if (UMaterial* Base = Registry.FindMaterial(Name))
{
Bases.Add(Base);
}
}
if (Bases.IsEmpty())
{
return nullptr;
}
UMaterial* Chosen = nullptr;
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(LOCTEXT("PickBaseTitle", "Choose ShaderLab base material"))
.ClientSize(FVector2D(560, 520))
.SupportsMaximize(false)
.SupportsMinimize(false);
TSharedRef<FAssetThumbnailPool> ThumbnailPool = MakeShared<FAssetThumbnailPool>(64);
TSharedRef<SUniformWrapPanel> Grid = SNew(SUniformWrapPanel).SlotPadding(FMargin(6.f));
for (UMaterial* Base : Bases)
{
// Hold the thumbnail alive for the window's lifetime by capturing it in the button lambda.
TSharedRef<FAssetThumbnail> Thumbnail = MakeShared<FAssetThumbnail>(FAssetData(Base), 96, 96, ThumbnailPool);
Grid->AddSlot()
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.ToolTipText(FText::FromString(Base->GetName()))
.OnClicked_Lambda([&Chosen, Base, Window, Thumbnail]()
{
Chosen = Base;
Window->RequestDestroyWindow();
return FReply::Handled();
})
.Content()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)
[
SNew(SBox).WidthOverride(96.f).HeightOverride(96.f)
[
Thumbnail->MakeThumbnailWidget()
]
]
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0, 4, 0, 0)
[
SNew(STextBlock).Text(FText::FromString(Base->GetName()))
]
]
];
}
Window->SetContent(
SNew(SScrollBox)
+ SScrollBox::Slot().Padding(8.f)
[
Grid
]);
GEditor->EditorAddModalWindow(Window);
return Chosen;
}
}
UShaderLabMaterialInstanceFactory::UShaderLabMaterialInstanceFactory()
{
SupportedClass = UShaderLabMaterialInstanceConstant::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
uint32 UShaderLabMaterialInstanceFactory::GetMenuCategories() const
{
return EAssetTypeCategories::Materials;
}
FText UShaderLabMaterialInstanceFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "ShaderLab Material Instance");
}
bool UShaderLabMaterialInstanceFactory::ConfigureProperties()
{
// A commandlet/automation can set InitialParent directly and skip the (headless-unavailable) dialog.
if (InitialParent)
{
return true;
}
if (FApp::IsUnattended() || IsRunningCommandlet() || !GEditor)
{
return false; // No parent and no UI to pick one.
}
InitialParent = PickShaderLabBase();
return InitialParent != nullptr; // false = user cancelled / no bases -> abort creation.
}
UObject* UShaderLabMaterialInstanceFactory::FactoryCreateNew(
UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UShaderLabMaterialInstanceConstant* Instance =
NewObject<UShaderLabMaterialInstanceConstant>(InParent, InClass, InName, Flags);
if (InitialParent)
{
Instance->SetParentEditorOnly(InitialParent);
Instance->PostEditChange();
}
return Instance;
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,37 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "ShaderLabMaterialInstanceFactory.generated.h"
class UMaterial;
/**
* Creates a UShaderLabMaterialInstanceConstant parented to a ShaderLab in-memory /Script base
* material. Appears in the Content Browser under "Material & Textures ▸ ShaderLab Material Instance".
*
* ShaderLab base materials are hidden templates (not pickable as assets), so this factory is the
* entry point: ConfigureProperties() pops a picker of the discovered base materials (with preview
* thumbnails). A commandlet may instead set InitialParent directly and skip the dialog.
*/
UCLASS()
class UShaderLabMaterialInstanceFactory : public UFactory
{
GENERATED_BODY()
public:
UShaderLabMaterialInstanceFactory();
/** Parent base material for the created instance. Set by the picker, or directly by a commandlet. */
UPROPERTY()
TObjectPtr<UMaterial> InitialParent = nullptr;
//~ UFactory interface
virtual bool ConfigureProperties() override;
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual uint32 GetMenuCategories() const override;
virtual FText GetDisplayName() const override;
//~ End UFactory interface
};

View File

@@ -0,0 +1,28 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
struct FShaderLabModel;
/**
* Editor-only builder that turns a parsed FShaderLabModel into a UMaterial expression graph:
* property parameter nodes + a Custom HLSL node for the Surface body wired into a Substrate
* Slab BSDF (-> FrontMaterial), plus an optional Vertex Custom node for WPO/displacement/UVs.
*
* The build is fully deterministic for a given model (node creation order, input/output order,
* generated HLSL) so the cook-time and editor-time graphs — and therefore the baked uniform
* expression set — match.
*/
class USHADERLABEDITOR_API FShaderLabGraphBuilder
{
public:
/**
* Clear `Material`'s expression graph and rebuild it from `Model`, applying settings
* (domain/blend/two-sided) and updating cached expression data. Does NOT trigger shader
* compilation — callers decide when to compile. Returns false with diagnostics on failure.
*/
static bool BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors);
};

View File

@@ -0,0 +1,63 @@
// Copyright FlecsProj. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
class UMaterialExpression;
/** Shader stage an intrinsic is valid in (used to reject e.g. PixelDepth inside a Vertex body). */
enum class EShaderLabIntrinsicFrequency : uint8
{
Any,
PixelOnly,
VertexOnly,
};
/**
* Describes one `UE::NodeName(...)` intrinsic: how to build the backing material expression node,
* which of its outputs to read, and what it requires of the material. The Custom-node input type is
* derived by the engine from the connected node's real output type, so no output type is declared here.
*/
struct FShaderLabIntrinsicDesc
{
FName Name;
/** Which output pin of the created node to connect (covers multi-output nodes like ObjectLocalBounds). */
int32 OutputIndex = 0;
/** Stage restriction; misuse is a build error. */
EShaderLabIntrinsicFrequency Frequency = EShaderLabIntrinsicFrequency::Any;
/**
* Create the backing expression node and add it to `Material`'s expression collection. `ConstArgs`
* are the literal arguments parsed from the call (e.g. {"0","2.0"} for `UE::TextureCoordinate(0,2.0)`).
* Return nullptr and set `OutError` on a bad argument. Contract: this only runs in the editor/cook
* (graphs are editor-only); the cooked runtime never builds a graph.
*/
TFunction<UMaterialExpression*(UMaterial& /*Material*/, const TArray<FString>& /*ConstArgs*/, FString& /*OutError*/)> MakeNode;
};
/**
* Open registry mapping intrinsic names to descriptors. ShaderLab seeds its builtins on first use;
* other editor modules can register intrinsics for their own custom UMaterialExpression nodes from
* their StartupModule via Get().Register(...).
*/
class USHADERLABEDITOR_API FShaderLabIntrinsicRegistry
{
public:
static FShaderLabIntrinsicRegistry& Get();
/** Register (or override) an intrinsic. */
void Register(FShaderLabIntrinsicDesc Desc);
/** Lookup by name; nullptr if unknown. */
const FShaderLabIntrinsicDesc* Find(FName Name) const;
private:
FShaderLabIntrinsicRegistry();
void RegisterBuiltins();
TMap<FName, FShaderLabIntrinsicDesc> Descs;
};

View File

@@ -0,0 +1,36 @@
// Copyright FlecsProj. All Rights Reserved.
using UnrealBuildTool;
public class UShaderLabEditor : ModuleRules
{
public UShaderLabEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
bUseUnity = false;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"UShaderLab",
"RenderCore",
"RHI",
"UnrealEd",
"AssetTools",
"MaterialEditor",
"Slate",
"SlateCore",
"Projects",
"DirectoryWatcher",
"ContentBrowserData",
"TargetPlatform",
});
}
}

28
UShaderLab.uplugin Normal file
View File

@@ -0,0 +1,28 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "UShaderLab",
"Description": "Unity-ShaderLab-like text DSL that builds in-memory Substrate materials.",
"Category": "Rendering",
"CreatedBy": "FlecsProj",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": true,
"Installed": false,
"Modules": [
{
"Name": "UShaderLab",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "UShaderLabEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}