Support SL_INTERPOLATOR and local function

This commit is contained in:
Eragon-Brisingr
2026-07-02 15:14:13 +08:00
parent 0d7db515ae
commit c27af6af49
50 changed files with 938 additions and 73 deletions

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
// Shared struct definitions for ShaderLab-generated Custom HLSL nodes.
// Included by every generated UMaterialExpressionCustom node via IncludeFilePaths.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// ShaderLab DSL vocabulary — the UFUNCTION/UPROPERTY-style annotation macros that mark up an
// otherwise-plain-HLSL `.usl` file. Every macro expands to NOTHING for the preprocessor (and for
@@ -38,6 +38,18 @@
#define SL_OPACITY(...) // standalone: SL_OPACITY(SomeValueName)
#define SL_OPACITY_MASK(...) // standalone: SL_OPACITY_MASK(SomeValueName)
// Vertex Interpolator: precedes `floatN Name() { return <vertex-frequency HLSL>; }` — a value computed
// per-vertex and interpolated to the pixel shader (backed by a UMaterialExpressionVertexInterpolator).
// Read it in a pixel body with `UE_Interpolator(Name)`, or use a scalar one as a topology mix factor.
#define SL_INTERPOLATOR(...)
// IDE-only: `UE_Interpolator(Name)` reads the interpolator's value. In the editor it expands to a direct
// call of the (real HLSL) interpolator function so completion/type-checking work; at real compile this
// authoring shim is stripped by the parser and the graph builder rewrites the call into a node input.
#ifndef UE_Interpolator
#define UE_Interpolator(Name) (Name())
#endif
// ---------------------------------------------------------------------------
// Specifier vocabulary (canonical, PascalCase — aligned with UE conventions).
// These are the keys accepted inside the macros above.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Aggregator for the ShaderLab `UE_` HLSL library functions. This header only includes the per-category
// implementation files under UEFunctions/. It is auto-included into every generated Custom node (so the

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// UE_ noise functions — forward to the engine's MaterialExpressionNoise / MaterialExpressionVectorNoise.
//

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// UE_ rotation helpers — forward to the engine's material HLSL.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Editor-only (SHADERLAB_IDE) stubs for the engine material HLSL symbols that the generated transform
// libraries (Transform.ush / TransformPosition.ush) call. shader-validator can't see the engine's

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// ShaderLab authoring umbrella — `#include "/Plugin/ShaderLab/ShaderLab.ush"` at the top of every .usl
// for IDE completion. This header is NOT compiled: the ShaderLab parser strips it (only entry-function

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabDiscovery.h"

View File

@@ -1,11 +1,15 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabMaterialInstanceConstant.h"
#include "Materials/Material.h"
#include "ShaderLabMaterialAssetUserData.h"
#include "ShaderLabMaterialRegistry.h"
#include "UObject/AssetRegistryTagsContext.h"
#include "UObject/Package.h"
const FName UShaderLabMaterialInstanceConstant::ShaderLabPathTagName(TEXT("ShaderLabPath"));
bool UShaderLabMaterialInstanceConstant::HasOverridenBaseProperties() const
{
// Force a self-contained permutation only at the ROOT — where our immediate parent is a ShaderLab
@@ -25,3 +29,34 @@ bool UShaderLabMaterialInstanceConstant::HasOverridenBaseProperties() const
return Super::HasOverridenBaseProperties();
}
FString UShaderLabMaterialInstanceConstant::GetShaderLabPath(const UMaterialInterface* Material)
{
// Walk parent -> ... -> ShaderLab base material; the base carries the source .usl virtual path.
const UMaterialInterface* Current = Material;
while (Current)
{
if (const UShaderLabMaterialAssetUserData* UserData =
const_cast<UMaterialInterface*>(Current)->GetAssetUserData<UShaderLabMaterialAssetUserData>())
{
return UserData->ShaderLabPath;
}
const UMaterialInstance* Instance = Cast<UMaterialInstance>(Current);
Current = Instance ? ToRawPtr(Instance->Parent) : nullptr;
}
return FString();
}
#if WITH_EDITOR
void UShaderLabMaterialInstanceConstant::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
{
Super::GetAssetRegistryTags(Context);
// Export the source .usl virtual path so users can filter ShaderLab instances in the Content Browser.
const FString Path = GetShaderLabPath(this);
if (!Path.IsEmpty())
{
Context.AddTag(FAssetRegistryTag(ShaderLabPathTagName, Path, FAssetRegistryTag::TT_Alphabetical));
}
}
#endif

View File

@@ -1,9 +1,11 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabMaterialRegistry.h"
#include "Materials/Material.h"
#include "Misc/Paths.h"
#include "ShaderCore.h"
#include "ShaderLabMaterialAssetUserData.h"
#include "ShaderLabModel.h"
#include "Serialization/AsyncLoadingEvents.h"
#include "UObject/Package.h"
@@ -55,6 +57,40 @@ FString FShaderLabMaterialRegistry::MakeObjectPath(const FString& ShaderName)
return FString::Printf(TEXT("%s.%s"), GShaderLabPackageName, *MakeObjectName(ShaderName).ToString());
}
FString FShaderLabMaterialRegistry::MakeVirtualShaderPath(const FString& AbsoluteSourcePath)
{
FString Full = FPaths::ConvertRelativePathToFull(AbsoluteSourcePath);
Full.ReplaceInline(TEXT("\\"), TEXT("/"));
// Reverse the registered shader-source mappings (/Project, /Plugin/<AnyPlugin>, /Engine, ...) —
// generic, so any plugin that authors `.usl` under a mapped Shaders root gets a stable virtual path,
// with no hardcoded plugin name. Pick the longest matching real dir (most specific root); on a tie,
// the lexicographically smaller virtual key for determinism. The mappings are editor/cook-populated;
// at cooked runtime the map is empty and the ShaderLabPath is unused, so the absolute-path fallback is fine.
FString BestVirtual;
FString BestKey;
int32 BestRealLen = -1;
for (const TPair<FString, FString>& Mapping : AllShaderSourceDirectoryMappings())
{
FString Real = FPaths::ConvertRelativePathToFull(Mapping.Value);
Real.ReplaceInline(TEXT("\\"), TEXT("/"));
if (!Full.StartsWith(Real + TEXT("/"), ESearchCase::IgnoreCase))
{
continue;
}
const bool bBetter = (Real.Len() > BestRealLen)
|| (Real.Len() == BestRealLen && Mapping.Key < BestKey);
if (bBetter)
{
BestRealLen = Real.Len();
BestKey = Mapping.Key;
// Full.RightChop(Real.Len()) begins with '/', so the result is "<Key>/<rel>".
BestVirtual = Mapping.Key + Full.RightChop(Real.Len());
}
}
return (BestRealLen >= 0) ? BestVirtual : Full;
}
UMaterial* FShaderLabMaterialRegistry::FindMaterial(const FString& ShaderName) const
{
if (!Package)
@@ -96,6 +132,18 @@ UMaterial* FShaderLabMaterialRegistry::RegisterFromModel(const FShaderLabModel&
RegisteredModels.FindOrAdd(ObjName) = Model;
// Tag the base material with the source `.usl` virtual path (stable across machines). MICs derive
// their `ShaderLabPath` asset-registry tag from this; the editor resolves it back to disk (VSCode button).
{
UShaderLabMaterialAssetUserData* UserData = Material->GetAssetUserData<UShaderLabMaterialAssetUserData>();
if (!UserData)
{
UserData = NewObject<UShaderLabMaterialAssetUserData>(Material);
Material->AddAssetUserData(UserData);
}
UserData->ShaderLabPath = MakeVirtualShaderPath(Model.SourceFilePath);
}
// Register with the loading system so imports to "/Script/UShaderLab.<ObjName>" from other
// packages resolve to this in-memory object — required for cooked builds, where the linker
// resolves a Material Instance's parent import through this registration (without it the parent

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabModel.h"
@@ -7,6 +7,11 @@ const FShaderLabProperty* FShaderLabModel::FindProperty(FName InName) const
return Properties.FindByPredicate([InName](const FShaderLabProperty& P) { return P.Name == InName; });
}
const FShaderLabInterpolator* FShaderLabModel::FindInterpolator(FName InName) const
{
return Interpolators.FindByPredicate([InName](const FShaderLabInterpolator& I) { return I.Name == InName; });
}
bool FShaderLabModel::HasStaticSwitches() const
{
return Properties.ContainsByPredicate(

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabModule.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabParser.h"
@@ -909,6 +909,173 @@ namespace
Model.Values.Add(MoveTemp(Value));
}
/** True if `Type` is a legal Vertex Interpolator return type (mapped to a float1..4 interpolant). */
bool IsInterpolatorReturnType(const FString& Type)
{
return Type == TEXT("float") || Type == TEXT("float2") || Type == TEXT("float3") || Type == TEXT("float4");
}
/**
* SL_INTERPOLATOR() floatN <Name>() { return <vertex-frequency HLSL>; }
* A value computed at vertex frequency and interpolated to the pixel shader; read via
* `UE_Interpolator(Name)`. Mirrors ParseValue but records the (validated) float return type.
*/
void ParseInterpolator(FScanner& S, FShaderLabModel& Model)
{
FString Ignored;
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_INTERPOLATOR()
{
return;
}
S.SkipTrivia();
const int32 DeclLine = S.Line, DeclCol = S.Column;
FString RetType;
if (!S.ReadIdentifier(RetType))
{
S.Error(TEXT("Expected a function declaration after SL_INTERPOLATOR"));
return;
}
if (!IsInterpolatorReturnType(RetType))
{
S.Error(FString::Printf(TEXT("SL_INTERPOLATOR return type must be float/float2/float3/float4, got '%s'"), *RetType), DeclLine, DeclCol);
return;
}
FString Name, SigInner, Body;
int32 BodyLine = 0;
if (!S.ReadIdentifier(Name))
{
S.Error(TEXT("Expected an interpolator name"));
return;
}
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SigInner))
{
return;
}
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Body, &BodyLine))
{
return;
}
const FName IName(*Name);
if (Model.Interpolators.ContainsByPredicate([&IName](const FShaderLabInterpolator& E) { return E.Name == IName; }))
{
S.Error(FString::Printf(TEXT("Duplicate interpolator name '%s'"), *Name), DeclLine, DeclCol);
return;
}
FShaderLabInterpolator Interp;
Interp.Name = IName;
Interp.ReturnType = RetType;
Interp.Body = MoveTemp(Body);
Interp.BodyLine = BodyLine;
Model.Interpolators.Add(MoveTemp(Interp));
}
/** Capture the current physical line verbatim as a free-code chunk (non-#include preprocessor lines). */
void CaptureLocalLine(FScanner& S, FShaderLabModel& Model)
{
const int32 StartLine = S.Line;
FString Text;
// Honor backslash line-continuations so a multi-line `#define` is captured whole.
while (!S.IsEnd())
{
bool bContinued = false;
while (!S.IsEnd() && S.Peek() != TEXT('\n'))
{
const TCHAR C = S.Advance();
Text.AppendChar(C);
bContinued = (C == TEXT('\\'));
}
if (bContinued && !S.IsEnd())
{
Text.AppendChar(S.Advance()); // consume '\n' and keep going onto the continued line
continue;
}
break;
}
FShaderLabLocalCode LC;
LC.Text = Text.TrimEnd();
LC.Line = StartLine;
Model.LocalCode.Add(MoveTemp(LC));
}
/**
* Capture one free top-level HLSL declaration verbatim (function / struct / global / prototype /
* typedef) as a local-code chunk. Reads the signature up to a depth-0 '{' (then the balanced body
* plus an optional trailing ';') or a depth-0 ';'. Comment/string/char literals are skipped so a
* '{'/';' inside them does not terminate early. Returns false (with an error) if unterminated.
*/
bool CaptureLocalDeclaration(FScanner& S, FShaderLabModel& Model)
{
const int32 StartLine = S.Line;
FString Sig;
int32 Paren = 0, Bracket = 0;
bool bBrace = false, bSemicolon = false;
while (!S.IsEnd())
{
const TCHAR C = S.Peek();
if (C == TEXT('/') && S.Peek(1) == TEXT('/'))
{
while (!S.IsEnd() && S.Peek() != TEXT('\n')) { Sig.AppendChar(S.Advance()); }
continue;
}
if (C == TEXT('/') && S.Peek(1) == TEXT('*'))
{
Sig.AppendChar(S.Advance());
Sig.AppendChar(S.Advance());
while (!S.IsEnd() && !(S.Peek() == TEXT('*') && S.Peek(1) == TEXT('/'))) { Sig.AppendChar(S.Advance()); }
if (!S.IsEnd()) { Sig.AppendChar(S.Advance()); Sig.AppendChar(S.Advance()); }
continue;
}
if (C == TEXT('"') || C == TEXT('\''))
{
const TCHAR Quote = C;
Sig.AppendChar(S.Advance());
while (!S.IsEnd() && S.Peek() != Quote)
{
const TCHAR Ch = S.Advance();
Sig.AppendChar(Ch);
if (Ch == TEXT('\\') && !S.IsEnd()) { Sig.AppendChar(S.Advance()); }
}
if (!S.IsEnd()) { Sig.AppendChar(S.Advance()); }
continue;
}
if (C == TEXT('(')) { ++Paren; Sig.AppendChar(S.Advance()); continue; }
if (C == TEXT(')')) { --Paren; Sig.AppendChar(S.Advance()); continue; }
if (C == TEXT('[')) { ++Bracket; Sig.AppendChar(S.Advance()); continue; }
if (C == TEXT(']')) { --Bracket; Sig.AppendChar(S.Advance()); continue; }
if (Paren == 0 && Bracket == 0 && C == TEXT('{')) { bBrace = true; break; }
if (Paren == 0 && Bracket == 0 && C == TEXT(';')) { Sig.AppendChar(S.Advance()); bSemicolon = true; break; }
Sig.AppendChar(S.Advance());
}
FString Text = MoveTemp(Sig);
if (bBrace)
{
// Position is at the '{'. Capture the balanced body verbatim and any trailing ';'.
FString Inner;
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Inner))
{
return false;
}
Text += TEXT("{");
Text += Inner;
Text += TEXT("}");
if (S.Match(TEXT(';'))) { Text += TEXT(";"); }
}
else if (!bSemicolon)
{
S.Error(TEXT("Unterminated top-level HLSL declaration (expected '{' or ';')"), StartLine);
return false;
}
FShaderLabLocalCode LC;
LC.Text = MoveTemp(Text);
LC.Line = StartLine;
Model.LocalCode.Add(MoveTemp(LC));
return true;
}
/** Parse a scalar mix factor: a float literal, or a name (Value block / Scalar property, resolved later). */
bool ParseTopoFactor(FScanner& S, FShaderLabFactor& Out)
{
@@ -1060,10 +1227,12 @@ bool FShaderLabParser::Parse(
break; // End of file: all constructs parsed.
}
// Top-level preprocessor lines: `#include "..."` feeds Model.Includes; everything else
// (`#pragma`, stray `#define`, etc.) is non-DSL and skipped.
// Top-level preprocessor lines: `#include "..."` feeds Model.Includes; every other preprocessor
// line (`#pragma`, `#define`, `#if`, ...) is free HLSL captured verbatim into LocalCode.
if (S.Peek() == TEXT('#'))
{
const TCHAR* Save = S.Ptr;
const int32 SaveLine = S.Line, SaveCol = S.Column;
S.Advance(); // '#'
FString Directive;
if (S.ReadIdentifier(Directive) && Directive == TEXT("include"))
@@ -1072,11 +1241,15 @@ bool FShaderLabParser::Parse(
}
else
{
S.SkipToEndOfLine();
// Rewind to the '#' and capture the whole preprocessor line as local code.
S.Ptr = Save; S.Line = SaveLine; S.Column = SaveCol;
CaptureLocalLine(S, OutModel);
}
continue;
}
const TCHAR* DeclSave = S.Ptr;
const int32 DeclSaveLine = S.Line, DeclSaveCol = S.Column;
FString Token;
if (!S.ReadIdentifier(Token))
{
@@ -1104,6 +1277,10 @@ bool FShaderLabParser::Parse(
{
ParseValue(S, OutModel);
}
else if (Token == TEXT("SL_INTERPOLATOR"))
{
ParseInterpolator(S, OutModel);
}
else if (Token == TEXT("SL_FRONTMATERIAL"))
{
ParseFrontMaterial(S, OutModel);
@@ -1118,8 +1295,14 @@ bool FShaderLabParser::Parse(
}
else
{
S.Error(FString::Printf(TEXT("Unexpected token '%s' (expected a SL_ macro or #include)"), *Token));
return false;
// Not a ShaderLab macro: a free top-level HLSL declaration (helper function / struct /
// global / prototype). Rewind to its first token and capture it verbatim into LocalCode;
// the graph builder emits these at file scope in a generated .gen.ush included by every node.
S.Ptr = DeclSave; S.Line = DeclSaveLine; S.Column = DeclSaveCol;
if (!CaptureLocalDeclaration(S, OutModel))
{
return false;
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabRuntimeBuilder.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabSettingsApplier.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabSubsystem.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -0,0 +1,25 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/AssetUserData.h"
#include "ShaderLabMaterialAssetUserData.generated.h"
/**
* Asset user data attached to a ShaderLab in-memory base UMaterial, carrying the virtual shader path of
* the `.usl` it was built from (e.g. "/Project/Examples/Basic.usl"). Stored as the DSL virtual path
* (not an absolute disk path) so it is stable across machines; the editor resolves it back to disk via
* AllShaderSourceDirectoryMappings when needed. UShaderLabMaterialInstanceConstant reads this off its
* parent base material to export the `ShaderLabPath` asset registry tag.
*/
UCLASS()
class USHADERLAB_API UShaderLabMaterialAssetUserData : public UAssetUserData
{
GENERATED_BODY()
public:
/** Virtual shader path of the source `.usl` (e.g. "/Project/Examples/Basic.usl"). */
UPROPERTY()
FString ShaderLabPath;
};

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once
@@ -31,4 +31,20 @@ public:
//~ UMaterialInstance interface
virtual bool HasOverridenBaseProperties() const override;
//~ End UMaterialInstance interface
#if WITH_EDITOR
//~ UObject interface
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
//~ End UObject interface
#endif
/** Asset-registry tag key exposing the source `.usl` virtual path for Content Browser filtering. */
static const FName ShaderLabPathTagName;
/**
* Resolve the source `.usl` virtual path for a material by walking up the parent chain to the
* ShaderLab base material and reading its UShaderLabMaterialAssetUserData. Empty if not a ShaderLab
* material. Works for any instance depth (child instances inherit the root's path).
*/
static FString GetShaderLabPath(const UMaterialInterface* Material);
};

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once
@@ -36,6 +36,14 @@ public:
/** Full object path, e.g. "/Script/UShaderLab.ShaderLab_RustyMetal". */
static FString MakeObjectPath(const FString& ShaderName);
/**
* Convert an absolute `.usl` disk path to the stable DSL virtual shader path (e.g.
* "/Project/Examples/Basic.usl" or "/Plugin/ShaderLab/..."). Runtime-safe (uses the project/plugin
* `Shaders` root convention, not the editor-only shader mappings). Returns the forward-slashed
* absolute path unchanged if it is under no known root.
*/
static FString MakeVirtualShaderPath(const FString& AbsoluteSourcePath);
/** Find an already-created base material by shader name (nullptr if absent). */
UMaterial* FindMaterial(const FString& ShaderName) const;

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once
@@ -98,6 +98,32 @@ struct USHADERLAB_API FShaderLabValue
int32 BodyLine = 0;
};
/**
* A `SL_INTERPOLATOR() floatN <Name>(){ return <vertex-frequency HLSL>; }` block: a value computed at
* vertex frequency and interpolated to the pixel shader (backed by UMaterialExpressionVertexInterpolator).
* Read in a pixel body via `UE_Interpolator(Name)`, or used as a topology mix factor when scalar.
*/
struct USHADERLAB_API FShaderLabInterpolator
{
FName Name;
/** HLSL return type: one of float/float2/float3/float4 (validated by the parser). */
FString ReturnType;
FString Body;
int32 BodyLine = 0;
};
/**
* A verbatim chunk of free top-level HLSL (function / struct / global / `#define`) written directly in
* the `.usl`. Emitted into a generated `.gen.ush` at file scope (in source order) and #included by every
* generated Custom node, so it is defined-before-use for all pixel/vertex bodies regardless of the
* translator's per-node compile order. Line is the 1-based source line of the chunk's first character.
*/
struct USHADERLAB_API FShaderLabLocalCode
{
FString Text;
int32 Line = 0;
};
/** Substrate topology operators (short DSL aliases mapping to engine Substrate expression nodes). */
enum class EShaderLabOp : uint8
{
@@ -197,9 +223,18 @@ struct USHADERLAB_API FShaderLabModel
FString VertexBody;
int32 VertexBodyLine = 0;
// Vertex interpolators (optional): VS-frequency values interpolated to the pixel shader.
TArray<FShaderLabInterpolator> Interpolators;
// Free top-level HLSL captured verbatim (helpers/structs/globals/#defines), in source order.
TArray<FShaderLabLocalCode> LocalCode;
/** Find a property by name (nullptr if absent). */
const FShaderLabProperty* FindProperty(FName InName) const;
/** Find an interpolator by name (nullptr if absent). */
const FShaderLabInterpolator* FindInterpolator(FName InName) const;
/** True if any property is a StaticBool (drives shader-map permutation count). */
bool HasStaticSwitches() const;
};

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
using UnrealBuildTool;

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "MaterialExpressionShaderLabParameterAnchor.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,8 +1,9 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "DirectoryWatcherModule.h"
#include "Editor.h"
#include "Engine/Engine.h"
#include "HAL/FileManager.h"
#include "IDirectoryWatcher.h"
#include "Interfaces/IPluginManager.h"
#include "MaterialEditingLibrary.h"
@@ -16,6 +17,7 @@
#include "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabSubsystem.h"
#include "ShaderLabVSCodeButton.h"
#include "UObject/Package.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabEditor, Log, All);
@@ -108,17 +110,37 @@ public:
}
}
// Map the plugin's Intermediate/ShaderLabGen directory (a build-time artifact dir) so the graph
// builder can #include per-shader generated local-code headers (`/UShaderLabGen/<Name>.gen.ush`).
// AddShaderSourceDirectoryMapping requires the real dir to already exist, so create it first.
{
const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir();
const TCHAR* GenRoot = FShaderLabGraphBuilder::GetGeneratedVirtualRoot();
if (!GenDir.IsEmpty())
{
IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true);
if (FPaths::DirectoryExists(GenDir) && !AllShaderSourceDirectoryMappings().Contains(GenRoot))
{
AddShaderSourceDirectoryMapping(GenRoot, GenDir);
}
}
}
// Fill in / recompile base materials whenever the registry asks (startup + hot reload).
BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile);
StartWatchingSources();
// "Open .usl in VSCode" toolbar button on the Material Instance Editor for ShaderLab MICs.
ShaderLabVSCodeButton::Register();
UE_LOG(LogShaderLabEditor, Log, TEXT("ShaderLabEditor module started."));
}
virtual void ShutdownModule() override
{
StopWatchingSources();
ShaderLabVSCodeButton::Unregister();
if (BuildHandle.IsValid())
{
FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle);

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabGraphBuilder.h"
@@ -18,12 +18,17 @@
#include "Materials/MaterialExpressionSubstrate.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionVertexInterpolator.h"
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "ShaderLabIntrinsicRegistry.h"
#include "ShaderLabRuntimeBuilder.h"
#include "UObject/Class.h"
#include "ShaderLabSettingsApplier.h"
#include "UObject/UObjectGlobals.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/FileHelper.h"
#include "ShaderCore.h"
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
@@ -273,6 +278,8 @@ namespace ShaderLabGraph
int32 BodyLine,
const FString& SrcPath,
TArray<FIntrinsicWire>& OutWires,
const TMap<FName, UMaterialExpression*>& InterpByName,
TSet<FName>& UsedInterps,
TArray<FString>& OutErrors)
{
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
@@ -327,6 +334,50 @@ namespace ShaderLabGraph
const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1));
const int32 CallLen = (m + 1) - i;
// UE_Interpolator(Name): read a Vertex Interpolator's value in the pixel shader. Not a
// registry intrinsic — its arg is an interpolator name (not a numeric/enum literal), so
// handle it before the registry lookup and arg-literal validation below.
if (Name == TEXT("Interpolator"))
{
const FString InterpNameStr = ArgsRaw.TrimStartAndEnd();
FName InputName; // None on error -> substituted blank (error already recorded)
if (Stage == EShaderLabIntrinsicFrequency::VertexOnly)
{
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' is pixel-only and cannot be used in a Vertex or Interpolator body"));
bOk = false;
}
else if (InterpNameStr.IsEmpty())
{
OutErrors.Add(Loc(i) + TEXT("intrinsic 'UE_Interpolator' requires an interpolator name argument"));
bOk = false;
}
else
{
const FString Key = FString(TEXT("Interpolator|")) + InterpNameStr;
if (const FName* Existing = InputByKey.Find(Key))
{
InputName = *Existing;
}
else if (UMaterialExpression* const* Node = InterpByName.Find(FName(*InterpNameStr)))
{
InputName = FName(*(FString(TEXT("SLI_Interp_")) + InterpNameStr));
InputByKey.Add(Key, InputName);
OutWires.Add(FIntrinsicWire{ InputName, *Node, 0 });
UsedInterps.Add(FName(*InterpNameStr));
}
else
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_Interpolator' references unknown interpolator '%s'"), *InterpNameStr));
bOk = false;
}
}
FString Replacement = InputName.IsNone() ? FString() : InputName.ToString();
while (Replacement.Len() < CallLen) { Replacement.AppendChar(TEXT(' ')); }
Result += Replacement;
i = m + 1;
continue;
}
// Only registered node-intrinsics are rewritten into material-expression inputs.
// Any other `UE_X(...)` is left for the shader compiler: it's an HLSL library
// function (e.g. UE_Noise from ShaderLabFunctions.ush, auto-included) or a typo.
@@ -440,13 +491,141 @@ namespace ShaderLabGraph
bool bIsTexture = false;
};
/** Add the shared struct + function-library includes plus any user `Includes { }` paths. */
/** Map an SL_INTERPOLATOR return type to a Custom-node output type (parser guarantees float1..4). */
static ECustomMaterialOutputType InterpolatorOutputType(const FString& ReturnType)
{
if (ReturnType == TEXT("float2")) { return CMOT_Float2; }
if (ReturnType == TEXT("float3")) { return CMOT_Float3; }
if (ReturnType == TEXT("float4")) { return CMOT_Float4; }
return CMOT_Float1; // "float"
}
/** File-name-safe stem from the shader name (identity), matching the registry's illegal->'_' rule. */
static FString SanitizeShaderFileStem(const FString& Name)
{
FString Out;
for (const TCHAR C : Name)
{
Out.AppendChar((FChar::IsAlnum(C) || C == TEXT('_') || C == TEXT('-')) ? C : TEXT('_'));
}
return Out.IsEmpty() ? FString(TEXT("Unnamed")) : Out;
}
/** Virtual `#include` path for a shader's generated local-code header (empty if it has no local code). */
static FString LocalCodeVirtualPath(const FShaderLabModel& Model)
{
if (Model.LocalCode.Num() == 0)
{
return FString();
}
return FString::Printf(TEXT("%s/%s.gen.ush"),
FShaderLabGraphBuilder::GetGeneratedVirtualRoot(), *SanitizeShaderFileStem(Model.ShaderName));
}
/**
* Guard: reject UE_ node intrinsics / UE_Interpolator inside free local code. Local functions are pure
* HLSL emitted at file scope — they can't reach material-graph nodes or per-primitive parameters, so a
* `UE_Time()` there would fail with an obscure "undefined symbol". Report it clearly, mapped to the .usl.
* (HLSL library helpers like UE_Noise are NOT in the registry and remain allowed.)
*/
static bool CheckLocalCodeIntrinsics(const FShaderLabModel& Model, TArray<FString>& OutErrors)
{
const FShaderLabIntrinsicRegistry& Registry = FShaderLabIntrinsicRegistry::Get();
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
bool bOk = true;
for (const FShaderLabLocalCode& LC : Model.LocalCode)
{
const FString& B = LC.Text;
const int32 Len = B.Len();
int32 i = 0;
while (i < Len)
{
const bool bBoundary = (i == 0) || !IsIdent(B[i - 1]);
if (bBoundary && i + 3 <= Len && B[i] == TEXT('U') && B[i + 1] == TEXT('E') && B[i + 2] == TEXT('_'))
{
int32 j = i + 3;
while (j < Len && IsIdent(B[j])) { ++j; }
const FString Name = B.Mid(i + 3, j - (i + 3));
int32 k = j;
while (k < Len && FChar::IsWhitespace(B[k])) { ++k; }
if (!Name.IsEmpty() && k < Len && B[k] == TEXT('(')
&& (Name == TEXT("Interpolator") || Registry.Find(FName(*Name))))
{
int32 Line = LC.Line;
for (int32 p = 0; p < i; ++p) { if (B[p] == TEXT('\n')) { ++Line; } }
OutErrors.Add(FString::Printf(
TEXT("%s(%d,1): error: UE_%s is a material-graph intrinsic and cannot be used inside a local function (pass its value in as a parameter)"),
*SrcPath, FMath::Max(Line, 1), *Name));
bOk = false;
}
i = j;
continue;
}
++i;
}
}
return bOk;
}
/**
* Write the shader's free top-level HLSL (LocalCode) to its generated `.gen.ush` on disk, at file
* scope (in source order, each chunk `#line`-mapped back to the .usl). Every generated Custom node
* #includes this file, so helpers/structs/globals are defined-before-use for all pixel/vertex bodies
* regardless of the translator's per-node compile order. No-op when the shader has no local code.
*/
static bool WriteLocalCodeInclude(const FShaderLabModel& Model, TArray<FString>& OutErrors)
{
if (Model.LocalCode.Num() == 0)
{
return true;
}
const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath);
FString Content;
Content += FString::Printf(TEXT("// Generated by ShaderLab from %s.usl - do not edit.\n"), *Model.ShaderName);
Content += TEXT("#pragma once\n");
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_COMMON_INCLUDE);
Content += FString::Printf(TEXT("#include \"%s\"\n"), SHADERLAB_FUNCTIONS_INCLUDE);
for (const FShaderLabLocalCode& LC : Model.LocalCode)
{
Content += FString::Printf(TEXT("#line %d \"%s\"\n"), FMath::Max(LC.Line, 1), *SrcPath);
Content += LC.Text;
Content += TEXT("\n");
}
const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir();
if (GenDir.IsEmpty())
{
OutErrors.Add(TEXT("ShaderLab: cannot locate the plugin directory for the generated local-code include"));
return false;
}
IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true);
const FString DiskPath = FPaths::Combine(GenDir, SanitizeShaderFileStem(Model.ShaderName) + TEXT(".gen.ush"));
// Shader source must be UTF-8 (no BOM) for the shader preprocessor, not the default UTF-16.
if (!FFileHelper::SaveStringToFile(Content, *DiskPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutErrors.Add(FString::Printf(TEXT("ShaderLab: failed to write generated local-code include '%s'"), *DiskPath));
return false;
}
// Drop any cached copy so the shader preprocessor re-reads the freshly written file (hot reload / re-cook).
FlushShaderFileCache();
return true;
}
/** Add the shared struct + function-library includes, the generated local-code header (if any), and
* any user `Includes { }` paths. */
static void AddIncludes(UMaterialExpressionCustom& Custom, const FShaderLabModel& Model)
{
Custom.IncludeFilePaths.Add(SHADERLAB_COMMON_INCLUDE);
// The function library provides the UE_ HLSL helpers (UE_Noise, UE_RotateAboutAxis, ...) that
// are left verbatim in the body (not rewritten into nodes).
Custom.IncludeFilePaths.Add(SHADERLAB_FUNCTIONS_INCLUDE);
// User-authored local functions/structs/globals live in a generated header at file scope.
const FString LocalInclude = LocalCodeVirtualPath(Model);
if (!LocalInclude.IsEmpty())
{
Custom.IncludeFilePaths.AddUnique(LocalInclude);
}
for (const FString& Include : Model.Includes)
{
if (!Include.IsEmpty())
@@ -465,6 +644,7 @@ namespace ShaderLabGraph
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const FString& OutParamName,
const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr<UMaterialExpressionSubstrateSlabBSDF>(Material, IoY, 0);
@@ -492,7 +672,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return nullptr;
}
@@ -575,6 +755,7 @@ namespace ShaderLabGraph
UMaterial& Material, UMaterialEditorOnlyData& EditorOnly, const TCHAR* StructName, const TCHAR* DefaultFn,
const FString& OutParamName, const FString& InBody, int32 BodyLine, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
@@ -584,7 +765,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return false;
}
@@ -648,6 +829,7 @@ namespace ShaderLabGraph
static UMaterialExpressionCustom* BuildValueNode(
UMaterial& Material, const FShaderLabValue& Value, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
@@ -657,7 +839,7 @@ namespace ShaderLabGraph
FString Body = Value.Body;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, InterpByName, UsedInterps, OutErrors))
{
return nullptr;
}
@@ -691,11 +873,68 @@ namespace ShaderLabGraph
return Custom;
}
/** Connect a topology mix factor (literal / Value block / Scalar property) to an operator scalar pin. */
/**
* Build a Vertex Interpolator: a Custom node computing the SL_INTERPOLATOR body at vertex frequency,
* feeding a UMaterialExpressionVertexInterpolator. Returns the interpolator node (its output 0 is the
* interpolated value, readable from pixel bodies via UE_Interpolator). nullptr on error.
*/
static UMaterialExpression* BuildInterpolatorNode(
UMaterial& Material, const FShaderLabInterpolator& Interp, const FShaderLabModel& Model,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -600);
Custom->Description = FString::Printf(TEXT("ShaderLab Interpolator %s"), *Interp.Name.ToString());
Custom->OutputType = InterpolatorOutputType(Interp.ReturnType);
AddIncludes(*Custom, Model);
// Vertex frequency: pixel-only intrinsics (incl. UE_Interpolator) are rejected. No interpolator map.
FString Body = Interp.Body;
TArray<FIntrinsicWire> Wires;
const TMap<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> IgnoredUsed;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, EmptyInterp, IgnoredUsed, OutErrors))
{
return nullptr;
}
for (const FIntrinsicWire& Wire : Wires)
{
FCustomInput In;
In.InputName = Wire.InputName;
In.Input.Connect(Wire.OutputIndex, Wire.Expr);
Custom->Inputs.Add(In);
}
for (const FShaderLabProperty& Prop : Model.Properties)
{
if (Prop.Type == EShaderLabPropertyType::StaticBool || !ReferencesToken(Interp.Body, Prop.Name.ToString()))
{
continue;
}
const FParamNode* Node = PropertyNodes.Find(Prop.Name);
if (Node && Node->Expr)
{
FCustomInput In;
In.InputName = Prop.Name;
In.Input.Connect(0, Node->Expr);
Custom->Inputs.Add(In);
}
}
// The body contains `return <expr>;`, so it is the Custom function's body directly.
Custom->Code = WrapBodyWithLineMapping(Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath));
Custom->RebuildOutputs();
UMaterialExpressionVertexInterpolator* VI = NewExpr<UMaterialExpressionVertexInterpolator>(Material, IoY, -450);
VI->Input.Connect(0, Custom);
return VI;
}
/** Connect a topology mix factor (literal / Value block / Scalar property / scalar Interpolator) to a scalar pin. */
static bool ConnectFactor(
UMaterial& Material, FExpressionInput& Target, const FShaderLabFactor& Factor,
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, const FShaderLabModel& Model,
TSet<FName>& UsedInterps, int32& IoY, TArray<FString>& OutErrors)
{
if (Factor.Kind == FShaderLabFactor::EKind::Literal)
{
@@ -717,8 +956,21 @@ namespace ShaderLabGraph
return true;
}
}
if (UMaterialExpression* const* InterpNode = InterpByName.Find(Factor.Name))
{
const FShaderLabInterpolator* Interp = Model.FindInterpolator(Factor.Name);
if (Interp && Interp->ReturnType == TEXT("float"))
{
Target.Connect(0, *InterpNode);
UsedInterps.Add(Factor.Name);
return true;
}
OutErrors.Add(FString::Printf(
TEXT("Topology factor '%s' is a Vertex Interpolator but not scalar (float); only float interpolators can be a mix factor"), *Factor.Name.ToString()));
return false;
}
OutErrors.Add(FString::Printf(
TEXT("Topology factor '%s' is neither a Value block nor a Scalar property"), *Factor.Name.ToString()));
TEXT("Topology factor '%s' is neither a Value block, a Scalar property, nor a scalar Interpolator"), *Factor.Name.ToString()));
return false;
}
@@ -727,7 +979,9 @@ namespace ShaderLabGraph
UMaterial& Material, int32 Index, const FShaderLabModel& Model,
const TMap<FName, UMaterialExpressionSubstrateSlabBSDF*>& SlabByName,
const TMap<FName, UMaterialExpressionCustom*>& ValueByName,
const TMap<FName, FParamNode>& PropertyNodes, int32& IoY, TArray<FString>& OutErrors)
const TMap<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
int32& IoY, TArray<FString>& OutErrors)
{
if (!Model.Topology.IsValidIndex(Index))
{
@@ -746,9 +1000,9 @@ namespace ShaderLabGraph
return nullptr;
}
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors);
UMaterialExpression* ChildA = BuildTopologyNode(Material, Node.ChildA, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors);
UMaterialExpression* ChildB = (Node.ChildB != INDEX_NONE)
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, IoY, OutErrors)
? BuildTopologyNode(Material, Node.ChildB, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, IoY, OutErrors)
: nullptr;
if (!ChildA || (Node.ChildB != INDEX_NONE && !ChildB))
{
@@ -762,14 +1016,14 @@ namespace ShaderLabGraph
UMaterialExpressionSubstrateVerticalLayering* N = NewExpr<UMaterialExpressionSubstrateVerticalLayering>(Material, IoY, -150);
N->Top.Connect(0, ChildA);
N->Base.Connect(0, ChildB);
return ConnectFactor(Material, N->Thickness, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
return ConnectFactor(Material, N->Thickness, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::HorizontalMix:
{
UMaterialExpressionSubstrateHorizontalMixing* N = NewExpr<UMaterialExpressionSubstrateHorizontalMixing>(Material, IoY, -150);
N->Background.Connect(0, ChildA);
N->Foreground.Connect(0, ChildB);
return ConnectFactor(Material, N->Mix, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
return ConnectFactor(Material, N->Mix, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::Add:
{
@@ -782,14 +1036,14 @@ namespace ShaderLabGraph
{
UMaterialExpressionSubstrateWeight* N = NewExpr<UMaterialExpressionSubstrateWeight>(Material, IoY, -150);
N->A.Connect(0, ChildA);
return ConnectFactor(Material, N->Weight, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
return ConnectFactor(Material, N->Weight, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
case EShaderLabOp::Select:
{
UMaterialExpressionSubstrateSelect* N = NewExpr<UMaterialExpressionSubstrateSelect>(Material, IoY, -150);
N->A.Connect(0, ChildA);
N->B.Connect(0, ChildB);
return ConnectFactor(Material, N->SelectValue, Node.Factor, ValueByName, PropertyNodes, IoY, OutErrors) ? N : nullptr;
return ConnectFactor(Material, N->SelectValue, Node.Factor, ValueByName, PropertyNodes, InterpByName, Model, UsedInterps, IoY, OutErrors) ? N : nullptr;
}
default:
OutErrors.Add(TEXT("Unhandled topology operator"));
@@ -824,6 +1078,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
return false;
}
// Emit the shader's free top-level HLSL (local functions/structs/globals) to a generated header that
// every Custom node #includes at file scope (defined-before-use for all pixel/vertex bodies). No-op
// when the shader declares none. Guard against graph intrinsics in local code first (clear .usl error).
if (!CheckLocalCodeIntrinsics(Model, OutErrors) || !WriteLocalCodeInclude(Model, OutErrors))
{
return false;
}
// Per-pixel context is read via UE_* intrinsics, so the Surface entry takes just the output
// struct: `Surface(inout FShaderLabSurface S)`. For multi-slab there is no Surface param.
const FShaderLabEntryParam* SurfaceOutParam = Model.bHasSurface && Model.SurfaceParams.Num() > 0
@@ -843,6 +1105,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr)) { return true; }
for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, NameStr)) { return true; } }
for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, NameStr)) { return true; } }
for (const FShaderLabInterpolator& Interp : Model.Interpolators) { if (ReferencesToken(Interp.Body, NameStr)) { return true; } }
if (Model.bHasVertex && ReferencesToken(Model.VertexBody, NameStr)) { return true; }
for (const FShaderLabTopoNode& Node : Model.Topology)
{
@@ -863,6 +1126,12 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// engine changes, and nothing wired onto the user's body nodes.
TArray<UMaterialExpression*> AnchorInputs;
// Vertex Interpolators: VS-frequency values interpolated to the pixel shader. Built up-front so pixel
// bodies can reference them via UE_Interpolator(Name); UsedInterps tracks which get consumed (contract:
// a declared-but-unused interpolator is an error, mirroring the no-dead-slabs rule).
TMap<FName, UMaterialExpression*> InterpByName;
TSet<FName> UsedInterps;
for (const FShaderLabProperty& Prop : Model.Properties)
{
const FString NameStr = Prop.Name.ToString();
@@ -961,12 +1230,28 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// 1b) Build a Vertex Interpolator node per declared interpolator (Custom @ vertex freq -> VertexInterpolator).
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
{
if (InterpByName.Contains(Interp.Name))
{
OutErrors.Add(FString::Printf(TEXT("Duplicate interpolator name '%s'"), *Interp.Name.ToString()));
return false;
}
UMaterialExpression* Node = BuildInterpolatorNode(Material, Interp, Model, PropertyNodes, ParamY, OutErrors);
if (!Node)
{
return false;
}
InterpByName.Add(Interp.Name, Node);
}
// 2) Build the pixel stage and connect it to FrontMaterial (what makes it a Substrate material).
if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::PostProcess)
{
// PostProcess domain: Color -> EmissiveColor, Opacity -> Opacity (no Substrate slab).
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabPostProcess"), TEXT("ShaderLabDefaultPostProcess"),
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, ParamY, OutErrors))
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors))
{
return false;
}
@@ -974,7 +1259,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
else if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::UI)
{
if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabUI"), TEXT("ShaderLabDefaultUI"),
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, ParamY, OutErrors))
SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors))
{
return false;
}
@@ -985,7 +1270,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// allowed as material-level outputs.
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
Model, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors);
if (!Slab)
{
return false;
@@ -1006,7 +1291,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
UMaterialExpressionSubstrateSlabBSDF* Slab = BuildSlab(
Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine,
Model, PropertyNodes, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
Model, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors);
if (!Slab)
{
return false;
@@ -1023,7 +1308,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
return false;
}
UMaterialExpressionCustom* ValueNode = BuildValueNode(
Material, ValueDecl, Model, PropertyNodes, ParamY, OutErrors);
Material, ValueDecl, Model, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors);
if (!ValueNode)
{
return false;
@@ -1047,7 +1332,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
UMaterialExpression* Root = BuildTopologyNode(
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, ParamY, OutErrors);
Material, Model.TopologyRoot, Model, SlabByName, ValueByName, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors);
if (!Root)
{
return false;
@@ -1098,7 +1383,10 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// Intrinsics (Stage = vertex).
FString VertexBody = Model.VertexBody;
TArray<FIntrinsicWire> VtxIntrinsicWires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, OutErrors))
// Vertex stage: UE_Interpolator is pixel-only, so pass an empty interpolator map (rejected there).
const TMap<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> IgnoredUsedInterps;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, EmptyInterp, IgnoredUsedInterps, OutErrors))
{
return false;
}
@@ -1174,6 +1462,19 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
}
}
// Contract: every declared interpolator must be consumed (read via UE_Interpolator, or used as a
// topology mix factor). A dead interpolator would silently waste a scarce interpolant slot.
for (const FShaderLabInterpolator& Interp : Model.Interpolators)
{
if (!UsedInterps.Contains(Interp.Name))
{
OutErrors.Add(FString::Printf(
TEXT("Interpolator '%s' is declared but never used (read it with UE_Interpolator(%s) or use it as a topology factor)"),
*Interp.Name.ToString(), *Interp.Name.ToString()));
return false;
}
}
// Funnel every static-switch selector into the ParameterAnchor. The anchor is a CustomOutput compiled
// BEFORE the material attributes (ShouldCompileBeforeAttributes), so compiling it compiles each
// selector — emitting the selected `#define <Name> 0/1` for the current permutation ahead of every
@@ -1242,3 +1543,18 @@ void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray<F
Material.UpdateCachedExpressionData();
}
const TCHAR* FShaderLabGraphBuilder::GetGeneratedVirtualRoot()
{
return TEXT("/UShaderLabGen");
}
FString FShaderLabGraphBuilder::GetGeneratedShaderDir()
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"));
if (!Plugin.IsValid())
{
return FString();
}
return FPaths::Combine(Plugin->GetBaseDir(), TEXT("Intermediate"), TEXT("ShaderLabGen"));
}

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// `ShaderLab.IDE.Prepare` console command — generates the IDE authoring assets so a `.usl` file
// (which is plain HLSL plus SL_* annotation macros) gets full member/function/include completion

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabIntrinsicRegistry.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Internal shared helpers + per-category registration entry points for the builtin `UE_` intrinsics.
// Registration is split by category (mesh/view/object/instancing/time/particle/decal/atmosphere) into

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Atmosphere / sky-light intrinsics.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Decal intrinsics (meaningful in the Decal domain; the instance/domain gates usage).

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Per-instance intrinsics.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Mesh / surface attribute intrinsics (vertex normals/tangents, colors, UVs).

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Object / primitive / bounds intrinsics.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Particle / sprite intrinsics. These only produce meaningful values on particle vertex factories;
// the instance enables the matching usage, so there is no base-level usage gate here.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// Time intrinsics.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
//
// View / camera / screen intrinsics.

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabMaterialInstanceFactory.h"

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -0,0 +1,133 @@
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabVSCodeButton.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Framework/MultiBox/MultiBoxExtender.h"
#include "HAL/PlatformProcess.h"
#include "MaterialEditorModule.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "ShaderCore.h"
#include "ShaderLabMaterialInstanceConstant.h"
#include "Styling/AppStyle.h"
#include "Textures/SlateIcon.h"
#include "Toolkits/AssetEditorToolkit.h"
#define LOCTEXT_NAMESPACE "ShaderLabVSCode"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabVSCode, Log, All);
namespace ShaderLabVSCodeButton
{
static FDelegateHandle GExtenderDelegateHandle;
/** Reverse the ShaderLabPath virtual path (e.g. "/Project/Examples/Basic.usl") to a disk path via the
* registered shader mappings (generic — /Project, /Plugin/<any>, /Engine). Empty if unresolved. */
static FString ResolveVirtualToDisk(const FString& VirtualPath)
{
int32 BestKeyLen = -1;
FString BestDisk;
for (const TPair<FString, FString>& Mapping : AllShaderSourceDirectoryMappings())
{
if (VirtualPath.StartsWith(Mapping.Key + TEXT("/"), ESearchCase::IgnoreCase) && Mapping.Key.Len() > BestKeyLen)
{
BestKeyLen = Mapping.Key.Len();
BestDisk = Mapping.Value / VirtualPath.RightChop(Mapping.Key.Len() + 1);
}
}
return (BestKeyLen >= 0) ? FPaths::ConvertRelativePathToFull(BestDisk) : FString();
}
static void OpenInVSCode(TWeakObjectPtr<UShaderLabMaterialInstanceConstant> MICWeak)
{
UShaderLabMaterialInstanceConstant* MIC = MICWeak.Get();
if (!MIC)
{
return;
}
const FString VirtualPath = UShaderLabMaterialInstanceConstant::GetShaderLabPath(MIC);
if (VirtualPath.IsEmpty())
{
UE_LOG(LogShaderLabVSCode, Warning, TEXT("'%s' has no ShaderLabPath."), *MIC->GetName());
return;
}
const FString DiskPath = ResolveVirtualToDisk(VirtualPath);
if (DiskPath.IsEmpty() || !FPaths::FileExists(DiskPath))
{
UE_LOG(LogShaderLabVSCode, Warning, TEXT("Cannot resolve '%s' to an existing file (got '%s')."), *VirtualPath, *DiskPath);
return;
}
// `code` is a .cmd on Windows, so launch it through the command interpreter; it opens/reuses VSCode.
#if PLATFORM_WINDOWS
const FString Exe = TEXT("cmd.exe");
const FString Args = FString::Printf(TEXT("/c code \"%s\""), *DiskPath);
#else
const FString Exe = TEXT("code");
const FString Args = FString::Printf(TEXT("\"%s\""), *DiskPath);
#endif
FProcHandle Proc = FPlatformProcess::CreateProc(*Exe, *Args, /*bLaunchDetached*/ true,
/*bLaunchHidden*/ true, /*bLaunchReallyHidden*/ true, nullptr, 0, nullptr, nullptr);
if (Proc.IsValid())
{
FPlatformProcess::CloseProc(Proc);
}
else
{
UE_LOG(LogShaderLabVSCode, Warning, TEXT("Failed to launch VSCode ('code' on PATH?) for '%s'."), *DiskPath);
}
}
/** Context-aware extender: add the button only when a ShaderLab MIC is being edited. */
static TSharedRef<FExtender> OnExtendToolbar(const TSharedRef<FUICommandList> Commands, const TArray<UObject*> EditedObjects)
{
TSharedRef<FExtender> Extender = MakeShared<FExtender>();
UShaderLabMaterialInstanceConstant* MIC = nullptr;
for (UObject* Object : EditedObjects)
{
if (UShaderLabMaterialInstanceConstant* Found = Cast<UShaderLabMaterialInstanceConstant>(Object))
{
MIC = Found;
break;
}
}
if (!MIC || UShaderLabMaterialInstanceConstant::GetShaderLabPath(MIC).IsEmpty())
{
return Extender;
}
TWeakObjectPtr<UShaderLabMaterialInstanceConstant> MICWeak(MIC);
Extender->AddToolBarExtension("Asset", EExtensionHook::After, Commands,
FToolBarExtensionDelegate::CreateLambda([MICWeak](FToolBarBuilder& Builder)
{
Builder.AddToolBarButton(
FUIAction(FExecuteAction::CreateStatic(&OpenInVSCode, MICWeak)),
NAME_None,
LOCTEXT("OpenSourceLabel", "Open .usl"),
LOCTEXT("OpenSourceTooltip", "Open this ShaderLab material's source .usl in VSCode"),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Edit"));
}));
return Extender;
}
void Register()
{
IMaterialEditorModule& MaterialEditor = FModuleManager::LoadModuleChecked<IMaterialEditorModule>("MaterialEditor");
TArray<FAssetEditorExtender>& Delegates = MaterialEditor.GetToolBarExtensibilityManager()->GetExtenderDelegates();
Delegates.Add(FAssetEditorExtender::CreateStatic(&OnExtendToolbar));
GExtenderDelegateHandle = Delegates.Last().GetHandle();
}
void Unregister()
{
if (IMaterialEditorModule* MaterialEditor = FModuleManager::GetModulePtr<IMaterialEditorModule>("MaterialEditor"))
{
MaterialEditor->GetToolBarExtensibilityManager()->GetExtenderDelegates().RemoveAll(
[](const FAssetEditorExtender& Delegate) { return Delegate.GetHandle() == GExtenderDelegateHandle; });
}
}
}
#undef LOCTEXT_NAMESPACE

View File

@@ -0,0 +1,14 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
/**
* Registers a context-aware toolbar button in the Material Instance Editor that opens the source `.usl`
* of a UShaderLabMaterialInstanceConstant in VSCode. Implemented as an FAssetEditorExtender delegate on
* the MaterialEditor module's toolbar extensibility manager (no custom asset editor needed).
*/
namespace ShaderLabVSCodeButton
{
void Register();
void Unregister();
}

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once
@@ -35,4 +35,17 @@ public:
* at shader compile. Does NOT trigger compilation (the caller decides when).
*/
static void BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath);
/**
* Virtual shader root under which per-shader generated local-code headers live
* (e.g. "/UShaderLabGen"). The editor module maps it to GetGeneratedShaderDir() at startup.
*/
static const TCHAR* GetGeneratedVirtualRoot();
/**
* Absolute disk directory backing GetGeneratedVirtualRoot(): the plugin's
* `Intermediate/ShaderLabGen`. A build-time artifact only (never staged into a pak); the cooked
* runtime never builds graphs and skips the mapping. Empty string if the plugin can't be found.
*/
static FString GetGeneratedShaderDir();
};

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
#pragma once

View File

@@ -1,4 +1,4 @@
// Copyright FlecsProj. All Rights Reserved.
// Copyright UShaderLab. All Rights Reserved.
using UnrealBuildTool;

View File

@@ -5,7 +5,7 @@
"FriendlyName": "UShaderLab",
"Description": "Unity-ShaderLab-like text DSL that builds in-memory Substrate materials.",
"Category": "Rendering",
"CreatedBy": "FlecsProj",
"CreatedBy": "UShaderLab",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",