diff --git a/Shaders/Private/ShaderLabCommon.ush b/Shaders/Private/ShaderLabCommon.ush index 0e04966..036886a 100644 --- a/Shaders/Private/ShaderLabCommon.ush +++ b/Shaders/Private/ShaderLabCommon.ush @@ -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. diff --git a/Shaders/Private/ShaderLabDSL.ush b/Shaders/Private/ShaderLabDSL.ush index c13ca64..437a0a5 100644 --- a/Shaders/Private/ShaderLabDSL.ush +++ b/Shaders/Private/ShaderLabDSL.ush @@ -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 ; }` — 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. diff --git a/Shaders/Private/ShaderLabUEFunctions.ush b/Shaders/Private/ShaderLabUEFunctions.ush index 0042066..93ee784 100644 --- a/Shaders/Private/ShaderLabUEFunctions.ush +++ b/Shaders/Private/ShaderLabUEFunctions.ush @@ -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 diff --git a/Shaders/Private/UEFunctions/Noise.ush b/Shaders/Private/UEFunctions/Noise.ush index 8f860f9..42805b9 100644 --- a/Shaders/Private/UEFunctions/Noise.ush +++ b/Shaders/Private/UEFunctions/Noise.ush @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // UE_ noise functions — forward to the engine's MaterialExpressionNoise / MaterialExpressionVectorNoise. // diff --git a/Shaders/Private/UEFunctions/Rotation.ush b/Shaders/Private/UEFunctions/Rotation.ush index 43ead6d..0ec0961 100644 --- a/Shaders/Private/UEFunctions/Rotation.ush +++ b/Shaders/Private/UEFunctions/Rotation.ush @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // UE_ rotation helpers — forward to the engine's material HLSL. diff --git a/Shaders/Private/UEFunctions/_EngineStubs.ush b/Shaders/Private/UEFunctions/_EngineStubs.ush index 4d78956..091fcbf 100644 --- a/Shaders/Private/UEFunctions/_EngineStubs.ush +++ b/Shaders/Private/UEFunctions/_EngineStubs.ush @@ -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 diff --git a/Shaders/ShaderLab.ush b/Shaders/ShaderLab.ush index aba9fb0..4390682 100644 --- a/Shaders/ShaderLab.ush +++ b/Shaders/ShaderLab.ush @@ -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 diff --git a/Source/UShaderLab/Private/ShaderLabDiscovery.cpp b/Source/UShaderLab/Private/ShaderLabDiscovery.cpp index 55a0bb8..1daf71e 100644 --- a/Source/UShaderLab/Private/ShaderLabDiscovery.cpp +++ b/Source/UShaderLab/Private/ShaderLabDiscovery.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabDiscovery.h" diff --git a/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp b/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp index 114615b..8bff602 100644 --- a/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp +++ b/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp @@ -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(Current)->GetAssetUserData()) + { + return UserData->ShaderLabPath; + } + const UMaterialInstance* Instance = Cast(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 diff --git a/Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp b/Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp index 9ebc870..5deeb77 100644 --- a/Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp +++ b/Source/UShaderLab/Private/ShaderLabMaterialRegistry.cpp @@ -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/, /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& 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 "/". + 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(); + if (!UserData) + { + UserData = NewObject(Material); + Material->AddAssetUserData(UserData); + } + UserData->ShaderLabPath = MakeVirtualShaderPath(Model.SourceFilePath); + } + // Register with the loading system so imports to "/Script/UShaderLab." 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 diff --git a/Source/UShaderLab/Private/ShaderLabModel.cpp b/Source/UShaderLab/Private/ShaderLabModel.cpp index 271e024..fd505ff 100644 --- a/Source/UShaderLab/Private/ShaderLabModel.cpp +++ b/Source/UShaderLab/Private/ShaderLabModel.cpp @@ -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( diff --git a/Source/UShaderLab/Private/ShaderLabModule.cpp b/Source/UShaderLab/Private/ShaderLabModule.cpp index c0c5849..d26e414 100644 --- a/Source/UShaderLab/Private/ShaderLabModule.cpp +++ b/Source/UShaderLab/Private/ShaderLabModule.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabModule.h" diff --git a/Source/UShaderLab/Private/ShaderLabParser.cpp b/Source/UShaderLab/Private/ShaderLabParser.cpp index 462f8a9..ff5def5 100644 --- a/Source/UShaderLab/Private/ShaderLabParser.cpp +++ b/Source/UShaderLab/Private/ShaderLabParser.cpp @@ -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 () { return ; } + * 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; + } } } diff --git a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp index fde1ade..25af586 100644 --- a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp +++ b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabRuntimeBuilder.h" diff --git a/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp b/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp index af87491..fe38313 100644 --- a/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp +++ b/Source/UShaderLab/Private/ShaderLabSettingsApplier.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabSettingsApplier.h" diff --git a/Source/UShaderLab/Private/ShaderLabSubsystem.cpp b/Source/UShaderLab/Private/ShaderLabSubsystem.cpp index c73b19e..2b133e2 100644 --- a/Source/UShaderLab/Private/ShaderLabSubsystem.cpp +++ b/Source/UShaderLab/Private/ShaderLabSubsystem.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabSubsystem.h" diff --git a/Source/UShaderLab/Public/ShaderLabDiscovery.h b/Source/UShaderLab/Public/ShaderLabDiscovery.h index ca20695..cbfbad8 100644 --- a/Source/UShaderLab/Public/ShaderLabDiscovery.h +++ b/Source/UShaderLab/Public/ShaderLabDiscovery.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/Public/ShaderLabMaterialAssetUserData.h b/Source/UShaderLab/Public/ShaderLabMaterialAssetUserData.h new file mode 100644 index 0000000..6995afd --- /dev/null +++ b/Source/UShaderLab/Public/ShaderLabMaterialAssetUserData.h @@ -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; +}; diff --git a/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h b/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h index 86d8414..c28d0c6 100644 --- a/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h +++ b/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h @@ -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); }; diff --git a/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h b/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h index 29be3d2..d557e42 100644 --- a/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h +++ b/Source/UShaderLab/Public/ShaderLabMaterialRegistry.h @@ -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; diff --git a/Source/UShaderLab/Public/ShaderLabModel.h b/Source/UShaderLab/Public/ShaderLabModel.h index 12382d2..766d84f 100644 --- a/Source/UShaderLab/Public/ShaderLabModel.h +++ b/Source/UShaderLab/Public/ShaderLabModel.h @@ -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 (){ return ; }` 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 Interpolators; + + // Free top-level HLSL captured verbatim (helpers/structs/globals/#defines), in source order. + TArray 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; }; diff --git a/Source/UShaderLab/Public/ShaderLabModule.h b/Source/UShaderLab/Public/ShaderLabModule.h index 84d76a3..a157bdf 100644 --- a/Source/UShaderLab/Public/ShaderLabModule.h +++ b/Source/UShaderLab/Public/ShaderLabModule.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/Public/ShaderLabParser.h b/Source/UShaderLab/Public/ShaderLabParser.h index 8ae3eca..c30e951 100644 --- a/Source/UShaderLab/Public/ShaderLabParser.h +++ b/Source/UShaderLab/Public/ShaderLabParser.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h b/Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h index 54864ac..f581459 100644 --- a/Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h +++ b/Source/UShaderLab/Public/ShaderLabRuntimeBuilder.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/Public/ShaderLabSettingsApplier.h b/Source/UShaderLab/Public/ShaderLabSettingsApplier.h index 0dc71b7..c6d252d 100644 --- a/Source/UShaderLab/Public/ShaderLabSettingsApplier.h +++ b/Source/UShaderLab/Public/ShaderLabSettingsApplier.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/Public/ShaderLabSubsystem.h b/Source/UShaderLab/Public/ShaderLabSubsystem.h index ad7fbb8..a0e9175 100644 --- a/Source/UShaderLab/Public/ShaderLabSubsystem.h +++ b/Source/UShaderLab/Public/ShaderLabSubsystem.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLab/UShaderLab.Build.cs b/Source/UShaderLab/UShaderLab.Build.cs index 7b43237..e2e638c 100644 --- a/Source/UShaderLab/UShaderLab.Build.cs +++ b/Source/UShaderLab/UShaderLab.Build.cs @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. using UnrealBuildTool; diff --git a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp b/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp index 908c200..52a0883 100644 --- a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp +++ b/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "MaterialExpressionShaderLabParameterAnchor.h" diff --git a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h b/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h index af5d666..d0f37d7 100644 --- a/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h +++ b/Source/UShaderLabEditor/Private/MaterialExpressionShaderLabParameterAnchor.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp index 4ea5661..f3a991d 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp @@ -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/.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); diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp index 8cdd363..0cd29d2 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp @@ -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& OutWires, + const TMap& InterpByName, + TSet& UsedInterps, TArray& 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& 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& 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& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, bool bAllowMaterialOutputs, int32& IoY, TArray& OutErrors) { UMaterialExpressionSubstrateSlabBSDF* Slab = NewExpr(Material, IoY, 0); @@ -492,7 +672,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray 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& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, int32& IoY, TArray& OutErrors) { UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); @@ -584,7 +765,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray 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& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, int32& IoY, TArray& OutErrors) { UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); @@ -657,7 +839,7 @@ namespace ShaderLabGraph FString Body = Value.Body; TArray 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& PropertyNodes, int32& IoY, TArray& OutErrors) + { + UMaterialExpressionCustom* Custom = NewExpr(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 Wires; + const TMap EmptyInterp; + TSet 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 ;`, so it is the Custom function's body directly. + Custom->Code = WrapBodyWithLineMapping(Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath)); + Custom->RebuildOutputs(); + + UMaterialExpressionVertexInterpolator* VI = NewExpr(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& ValueByName, - const TMap& PropertyNodes, int32& IoY, TArray& OutErrors) + const TMap& PropertyNodes, + const TMap& InterpByName, const FShaderLabModel& Model, + TSet& UsedInterps, int32& IoY, TArray& 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& SlabByName, const TMap& ValueByName, - const TMap& PropertyNodes, int32& IoY, TArray& OutErrors) + const TMap& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, + int32& IoY, TArray& 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(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(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(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(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 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 InterpByName; + TSet 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 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 EmptyInterp; + TSet 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 0/1` for the current permutation ahead of every @@ -1242,3 +1543,18 @@ void FShaderLabGraphBuilder::BuildPoisonInto(UMaterial& Material, const TArray Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab")); + if (!Plugin.IsValid()) + { + return FString(); + } + return FPaths::Combine(Plugin->GetBaseDir(), TEXT("Intermediate"), TEXT("ShaderLabGen")); +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp b/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp index dd0ab17..7a6047f 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp @@ -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 diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp index 9daa800..38b35ac 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabIntrinsicRegistry.h" diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h index 6c720e1..6366ffe 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h @@ -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 diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp index 64b09f1..b232e8e 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // Atmosphere / sky-light intrinsics. diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp index 8ddfbf6..f3a1e35 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp @@ -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). diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp index 6f89072..b2261c3 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // Per-instance intrinsics. diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp index 0914bc4..a180bb8 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // Mesh / surface attribute intrinsics (vertex normals/tangents, colors, UVs). diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp index 4d0d810..0fba461 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // Object / primitive / bounds intrinsics. diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp index 9abf08c..ec916ae 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp @@ -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. diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp index 80e05ed..cdf20a5 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // Time intrinsics. diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp index bb3ce32..114cee0 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. // // View / camera / screen intrinsics. diff --git a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp index 28a5b88..8e0ae02 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #include "ShaderLabMaterialInstanceFactory.h" diff --git a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.h b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.h index 9ebfdef..495b285 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.h +++ b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp new file mode 100644 index 0000000..8b9eab3 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp @@ -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/, /Engine). Empty if unresolved. */ + static FString ResolveVirtualToDisk(const FString& VirtualPath) + { + int32 BestKeyLen = -1; + FString BestDisk; + for (const TPair& 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 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 OnExtendToolbar(const TSharedRef Commands, const TArray EditedObjects) + { + TSharedRef Extender = MakeShared(); + + UShaderLabMaterialInstanceConstant* MIC = nullptr; + for (UObject* Object : EditedObjects) + { + if (UShaderLabMaterialInstanceConstant* Found = Cast(Object)) + { + MIC = Found; + break; + } + } + if (!MIC || UShaderLabMaterialInstanceConstant::GetShaderLabPath(MIC).IsEmpty()) + { + return Extender; + } + + TWeakObjectPtr 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("MaterialEditor"); + TArray& Delegates = MaterialEditor.GetToolBarExtensibilityManager()->GetExtenderDelegates(); + Delegates.Add(FAssetEditorExtender::CreateStatic(&OnExtendToolbar)); + GExtenderDelegateHandle = Delegates.Last().GetHandle(); + } + + void Unregister() + { + if (IMaterialEditorModule* MaterialEditor = FModuleManager::GetModulePtr("MaterialEditor")) + { + MaterialEditor->GetToolBarExtensibilityManager()->GetExtenderDelegates().RemoveAll( + [](const FAssetEditorExtender& Delegate) { return Delegate.GetHandle() == GExtenderDelegateHandle; }); + } + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.h b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.h new file mode 100644 index 0000000..9f93d91 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.h @@ -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(); +} diff --git a/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h b/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h index 79bacc7..75a07bb 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h +++ b/Source/UShaderLabEditor/Public/ShaderLabGraphBuilder.h @@ -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& 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(); }; diff --git a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h index e4f9029..8785752 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h +++ b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. #pragma once diff --git a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs index e0cba8b..2edd699 100644 --- a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs +++ b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs @@ -1,4 +1,4 @@ -// Copyright FlecsProj. All Rights Reserved. +// Copyright UShaderLab. All Rights Reserved. using UnrealBuildTool; diff --git a/UShaderLab.uplugin b/UShaderLab.uplugin index e23770c..d98bc08 100644 --- a/UShaderLab.uplugin +++ b/UShaderLab.uplugin @@ -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": "",