mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Init repo
This commit is contained in:
86
.gitignore
vendored
Normal file
86
.gitignore
vendored
Normal 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
|
||||
114
Shaders/Private/ShaderLabCommon.ush
Normal file
114
Shaders/Private/ShaderLabCommon.ush
Normal 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;
|
||||
}
|
||||
37
Source/UShaderLab/Private/ShaderLabDiscovery.cpp
Normal file
37
Source/UShaderLab/Private/ShaderLabDiscovery.cpp
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
111
Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp
Normal file
111
Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp
Normal 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;
|
||||
}
|
||||
14
Source/UShaderLab/Private/ShaderLabModel.cpp
Normal file
14
Source/UShaderLab/Private/ShaderLabModel.cpp
Normal 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; });
|
||||
}
|
||||
34
Source/UShaderLab/Private/ShaderLabModule.cpp
Normal file
34
Source/UShaderLab/Private/ShaderLabModule.cpp
Normal 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)
|
||||
1070
Source/UShaderLab/Private/ShaderLabParser.cpp
Normal file
1070
Source/UShaderLab/Private/ShaderLabParser.cpp
Normal file
File diff suppressed because it is too large
Load Diff
45
Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp
Normal file
45
Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
141
Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp
Normal file
141
Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp
Normal 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;
|
||||
}
|
||||
|
||||
81
Source/UShaderLab/Private/ShaderLabSubsystem.cpp
Normal file
81
Source/UShaderLab/Private/ShaderLabSubsystem.cpp
Normal 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);
|
||||
}
|
||||
15
Source/UShaderLab/Public/ShaderLabDiscovery.h
Normal file
15
Source/UShaderLab/Public/ShaderLabDiscovery.h
Normal 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();
|
||||
};
|
||||
34
Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h
Normal file
34
Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h
Normal 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
|
||||
};
|
||||
57
Source/UShaderLab/Public/ShaderLabMaterialRegistry.h
Normal file
57
Source/UShaderLab/Public/ShaderLabMaterialRegistry.h
Normal 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;
|
||||
};
|
||||
196
Source/UShaderLab/Public/ShaderLabModel.h
Normal file
196
Source/UShaderLab/Public/ShaderLabModel.h
Normal 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;
|
||||
};
|
||||
13
Source/UShaderLab/Public/ShaderLabModule.h
Normal file
13
Source/UShaderLab/Public/ShaderLabModule.h
Normal 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;
|
||||
};
|
||||
41
Source/UShaderLab/Public/ShaderLabParser.h
Normal file
41
Source/UShaderLab/Public/ShaderLabParser.h
Normal 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);
|
||||
};
|
||||
21
Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h
Normal file
21
Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h
Normal 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);
|
||||
};
|
||||
32
Source/UShaderLab/Public/ShaderLabSettingsApplier.h
Normal file
32
Source/UShaderLab/Public/ShaderLabSettingsApplier.h
Normal 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);
|
||||
};
|
||||
34
Source/UShaderLab/Public/ShaderLabSubsystem.h
Normal file
34
Source/UShaderLab/Public/ShaderLabSubsystem.h
Normal 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;
|
||||
};
|
||||
33
Source/UShaderLab/UShaderLab.Build.cs
Normal file
33
Source/UShaderLab/UShaderLab.Build.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
168
Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp
Normal file
168
Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp
Normal 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)
|
||||
1111
Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp
Normal file
1111
Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
189
Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp
Normal file
189
Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
28
Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h
Normal file
28
Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h
Normal 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);
|
||||
};
|
||||
63
Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h
Normal file
63
Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h
Normal 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;
|
||||
};
|
||||
36
Source/UShaderLabEditor/UShaderLabEditor.Build.cs
Normal file
36
Source/UShaderLabEditor/UShaderLabEditor.Build.cs
Normal 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
28
UShaderLab.uplugin
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user