mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
Support SL_INTERPOLATOR and local function
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// UE_ noise functions — forward to the engine's MaterialExpressionNoise / MaterialExpressionVectorNoise.
|
||||
//
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// UE_ rotation helpers — forward to the engine's material HLSL.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabDiscovery.h"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabModule.h"
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabRuntimeBuilder.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabSettingsApplier.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabSubsystem.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
25
Source/UShaderLab/Public/ShaderLabMaterialAssetUserData.h
Normal file
25
Source/UShaderLab/Public/ShaderLabMaterialAssetUserData.h
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "MaterialExpressionShaderLabParameterAnchor.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabIntrinsicRegistry.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// Atmosphere / sky-light intrinsics.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// Per-instance intrinsics.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// Mesh / surface attribute intrinsics (vertex normals/tangents, colors, UVs).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// Object / primitive / bounds intrinsics.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// Time intrinsics.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
//
|
||||
// View / camera / screen intrinsics.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "ShaderLabMaterialInstanceFactory.h"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
133
Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp
Normal file
133
Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.cpp
Normal 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
|
||||
14
Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.h
Normal file
14
Source/UShaderLabEditor/Private/ShaderLabVSCodeButton.h
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright FlecsProj. All Rights Reserved.
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
Reference in New Issue
Block a user