mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Change usl grammar same like as hlsl
This commit is contained in:
44
Shaders/ShaderLab.ush
Normal file
44
Shaders/ShaderLab.ush
Normal file
@@ -0,0 +1,44 @@
|
||||
// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.
|
||||
// Authoring umbrella header: include this from every .usl for IDE completion.
|
||||
// At real shader-compile time this header is NOT included (the parser strips it);
|
||||
// the UE:: stubs below are for the editor only.
|
||||
#pragma once
|
||||
|
||||
#include "/Plugin/ShaderLab/ShaderLabDSL.ush"
|
||||
#include "/Plugin/ShaderLab/Private/ShaderLabCommon.ush"
|
||||
|
||||
namespace UE
|
||||
{
|
||||
float3 ActorPositionWS() { return (float3)0; }
|
||||
float3 CameraPositionWS() { return (float3)0; }
|
||||
float3 CameraVectorWS() { return (float3)0; }
|
||||
float DeltaTime() { return (float)0; }
|
||||
float2 LightmapUVs() { return (float2)0; }
|
||||
float3 LightVector() { return (float3)0; }
|
||||
float3 LocalPosition() { return (float3)0; }
|
||||
float3 ObjectBounds() { return (float3)0; }
|
||||
float3 ObjectLocalBounds() { return (float3)0; }
|
||||
float3 ObjectOrientation() { return (float3)0; }
|
||||
float3 ObjectPositionWS() { return (float3)0; }
|
||||
float ObjectRadius() { return (float)0; }
|
||||
float4 ParticleColor() { return (float4)0; }
|
||||
float PerInstanceCustomData(int DataIndex, float ConstDefaultValue = 0) { return (float)0; }
|
||||
float PerInstanceFadeAmount() { return (float)0; }
|
||||
float PerInstanceRandom() { return (float)0; }
|
||||
float PixelDepth() { return (float)0; }
|
||||
float3 PixelNormalWS() { return (float3)0; }
|
||||
float3 PreSkinnedLocalBounds() { return (float3)0; }
|
||||
float3 PreSkinnedNormal() { return (float3)0; }
|
||||
float3 PreSkinnedPosition() { return (float3)0; }
|
||||
float2 ScreenPosition() { return (float2)0; }
|
||||
// UV channel `Index`, optionally tiled.
|
||||
float2 TextureCoordinate(int Index, float UTiling = 1, float VTiling = 1) { return (float2)0; }
|
||||
float Time() { return (float)0; }
|
||||
float TwoSidedSign() { return (float)0; }
|
||||
float4 VertexColor() { return (float4)0; }
|
||||
float3 VertexNormalWS() { return (float3)0; }
|
||||
float3 VertexTangentWS() { return (float3)0; }
|
||||
// Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView).
|
||||
float4 ViewProperty(int Property) { return (float4)0; }
|
||||
float3 WorldPosition() { return (float3)0; }
|
||||
}
|
||||
54
Shaders/ShaderLabDSL.ush
Normal file
54
Shaders/ShaderLabDSL.ush
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright FlecsProj. 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
|
||||
// HLSL Tools / Rider), so the file stays valid HLSL and gets free completion; the ShaderLab C++
|
||||
// parser reads these macro invocations + the HLSL declaration that follows to rebuild its model.
|
||||
//
|
||||
// Included (transitively, via ShaderLab.ush) by every `.usl`. This file is static and checked in;
|
||||
// the sibling ShaderLab.ush is generated by the `ShaderLab.IDE.Prepare` console command.
|
||||
|
||||
#pragma once
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Annotation macros. Two-line UFUNCTION style: the macro sits on its own line
|
||||
// directly above the HLSL declaration it annotates.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Material settings. Standalone (no following declaration).
|
||||
// SL_SETTINGS(Domain = Surface, BlendMode = Opaque, TwoSided = false, OpacityMaskClipValue = 0.4)
|
||||
#define SL_SETTINGS(...)
|
||||
|
||||
// Material parameter. Precedes the HLSL declaration whose type selects the DSL type:
|
||||
// float -> Scalar | float3 -> Color | float4 -> Vector |
|
||||
// Texture2D/TextureCube -> texture | "#define X v" -> StaticBool.
|
||||
// Scalar/Color/Vector defaults come from the HLSL initializer; texture default from DefaultTexture.
|
||||
#define SL_PROPERTY(...)
|
||||
|
||||
// Pixel/vertex entry points. Precede a `void Name(inout F... X) { ... }` (Vertex takes FShaderLabVertex).
|
||||
#define SL_SURFACE(...)
|
||||
#define SL_POSTPROCESS(...)
|
||||
#define SL_UI(...)
|
||||
#define SL_VERTEX(...)
|
||||
|
||||
// Multi-slab building blocks.
|
||||
#define SL_SLAB(...) // precedes `void Name(inout FShaderLabSurface S) { ... }`
|
||||
#define SL_VALUE(...) // precedes `float Name() { return <float>; }`
|
||||
#define SL_FRONTMATERIAL(...) // standalone: SL_FRONTMATERIAL(VerticalLayer(Coat, Metal, Thickness))
|
||||
#define SL_OPACITY(...) // standalone: SL_OPACITY(SomeValueName)
|
||||
#define SL_OPACITY_MASK(...) // standalone: SL_OPACITY_MASK(SomeValueName)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Specifier vocabulary (canonical, PascalCase — aligned with UE conventions).
|
||||
// These are the keys accepted inside the macros above.
|
||||
//
|
||||
// SL_SETTINGS : Domain, BlendMode, TwoSided, OpacityMaskClipValue, <reflection-allowlist keys...>
|
||||
// Domain = Surface | PostProcess | UI | Decal
|
||||
// BlendMode = Opaque | Masked | Translucent | Additive | Modulate
|
||||
// SL_PROPERTY : DisplayName, Group, SortPriority, Range (Scalar only), DefaultTexture (textures only)
|
||||
//
|
||||
// Topology operators (used inside SL_FRONTMATERIAL):
|
||||
// VerticalLayer(Top, Base, Thickness) | HorizontalMix(Background, Foreground, Mix)
|
||||
// Add(A, B) | Weight(A, Weight) | Select(A, B, Threshold)
|
||||
// A mix factor is a float literal, a Scalar property name, or an SL_VALUE block name.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -5,6 +5,15 @@
|
||||
#include "Misc/Paths.h"
|
||||
#include "ShaderLabModel.h"
|
||||
|
||||
// A `.usl` is plain HLSL annotated with UFUNCTION/UPROPERTY-style `SL_*` macros (defined in
|
||||
// /Plugin/ShaderLab/ShaderLabDSL.ush as empty expansions). This parser scans the top-level
|
||||
// constructs — `#include`s, `SL_SETTINGS`, `SL_PROPERTY` + a following HLSL declaration, the
|
||||
// `SL_SURFACE/POSTPROCESS/UI/VERTEX/SLAB/VALUE` markers + their functions, and the standalone
|
||||
// `SL_FRONTMATERIAL/OPACITY/OPACITY_MASK` — and rebuilds the same FShaderLabModel the old DSL did.
|
||||
// Everything not annotated (the authoring-shim `#include`, the macros, the property globals' editor
|
||||
// presence) never reaches the shader compiler: the graph builder only consumes entry-function bodies
|
||||
// and rewrites UE:: calls into expression-node inputs.
|
||||
|
||||
namespace ShaderLabParser_Private
|
||||
{
|
||||
/** Character scanner with line/column tracking and comment-aware trivia skipping. */
|
||||
@@ -254,60 +263,36 @@ namespace ShaderLabParser_Private
|
||||
Error(FString::Printf(TEXT("Unterminated block opened with '%c'"), Open), OpenLine, OpenCol);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove `//` line and block comments, preserving quoted strings. Used on the structured blocks
|
||||
* (Settings/Usage/Properties) before splitting them into statements — HLSL bodies keep their
|
||||
* comments verbatim and must NOT pass through here.
|
||||
*/
|
||||
static FString StripComments(const FString& In)
|
||||
{
|
||||
FString Out;
|
||||
Out.Reserve(In.Len());
|
||||
const int32 N = In.Len();
|
||||
int32 i = 0;
|
||||
while (i < N)
|
||||
/** Consume the remainder of the current physical line (used to skip non-DSL preprocessor lines). */
|
||||
void SkipToEndOfLine()
|
||||
{
|
||||
const TCHAR C = In[i];
|
||||
if (C == TEXT('"') || C == TEXT('\''))
|
||||
while (!IsEnd() && Peek() != TEXT('\n'))
|
||||
{
|
||||
const TCHAR Quote = C;
|
||||
Out.AppendChar(C);
|
||||
++i;
|
||||
while (i < N)
|
||||
{
|
||||
if (In[i] == TEXT('\\') && i + 1 < N)
|
||||
{
|
||||
Out.AppendChar(In[i]);
|
||||
Out.AppendChar(In[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
Out.AppendChar(In[i]);
|
||||
const bool bClose = (In[i] == Quote);
|
||||
++i;
|
||||
if (bClose) { break; }
|
||||
}
|
||||
continue;
|
||||
Advance();
|
||||
}
|
||||
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('/'))
|
||||
{
|
||||
while (i < N && In[i] != TEXT('\n')) { ++i; }
|
||||
continue;
|
||||
}
|
||||
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('*'))
|
||||
{
|
||||
i += 2;
|
||||
while (i + 1 < N && !(In[i] == TEXT('*') && In[i + 1] == TEXT('/'))) { ++i; }
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
Out.AppendChar(C);
|
||||
++i;
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
/** Read verbatim up to a top-level ';' (respecting parens); does not consume the ';'. */
|
||||
bool ReadUntilSemicolon(FString& Out)
|
||||
{
|
||||
FString T;
|
||||
int32 Paren = 0;
|
||||
while (!IsEnd())
|
||||
{
|
||||
const TCHAR C = Peek();
|
||||
if (C == TEXT(';') && Paren == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (C == TEXT('(')) { ++Paren; }
|
||||
else if (C == TEXT(')')) { --Paren; }
|
||||
T.AppendChar(Advance());
|
||||
}
|
||||
Out = T.TrimStartAndEnd();
|
||||
return !IsEnd();
|
||||
}
|
||||
};
|
||||
|
||||
/** Split `Inner` on top-level occurrences of `Delim`, respecting (), [] and quotes. */
|
||||
static TArray<FString> SplitTopLevel(const FString& Inner, TCHAR Delim)
|
||||
@@ -364,17 +349,6 @@ namespace ShaderLabParser_Private
|
||||
return Parts;
|
||||
}
|
||||
|
||||
static bool ParsePropertyType(const FString& Token, EShaderLabPropertyType& Out)
|
||||
{
|
||||
if (Token == TEXT("Scalar")) { Out = EShaderLabPropertyType::Scalar; return true; }
|
||||
if (Token == TEXT("Color")) { Out = EShaderLabPropertyType::Color; return true; }
|
||||
if (Token == TEXT("Vector")) { Out = EShaderLabPropertyType::Vector; return true; }
|
||||
if (Token == TEXT("Texture2D")) { Out = EShaderLabPropertyType::Texture2D; return true; }
|
||||
if (Token == TEXT("TextureCube")) { Out = EShaderLabPropertyType::TextureCube; return true; }
|
||||
if (Token == TEXT("StaticBool")) { Out = EShaderLabPropertyType::StaticBool; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ParseDomain(const FString& Token, EShaderLabDomain& Out)
|
||||
{
|
||||
if (Token == TEXT("Surface")) { Out = EShaderLabDomain::Surface; return true; }
|
||||
@@ -396,30 +370,43 @@ namespace ShaderLabParser_Private
|
||||
|
||||
static bool ParseBool(const FString& Token, bool& Out)
|
||||
{
|
||||
if (Token == TEXT("true")) { Out = true; return true; }
|
||||
if (Token == TEXT("false")) { Out = false; return true; }
|
||||
if (Token == TEXT("true") || Token == TEXT("1")) { Out = true; return true; }
|
||||
if (Token == TEXT("false") || Token == TEXT("0")) { Out = false; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Parse a parenthesized float list "(r, g, b[, a])" into a color (alpha defaults to 1). */
|
||||
static bool ParseColorLiteral(const FString& InRaw, FLinearColor& Out)
|
||||
/** Map an HLSL declaration type token to a DSL property type. */
|
||||
static bool InferPropertyType(const FString& HlslType, EShaderLabPropertyType& Out)
|
||||
{
|
||||
if (HlslType == TEXT("float")) { Out = EShaderLabPropertyType::Scalar; return true; }
|
||||
if (HlslType == TEXT("float3")) { Out = EShaderLabPropertyType::Color; return true; }
|
||||
if (HlslType == TEXT("float4")) { Out = EShaderLabPropertyType::Vector; return true; }
|
||||
if (HlslType == TEXT("Texture2D")) { Out = EShaderLabPropertyType::Texture2D; return true; }
|
||||
if (HlslType == TEXT("TextureCube")) { Out = EShaderLabPropertyType::TextureCube; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Parse a `floatN(a, b, c[, d])` initializer (or a bare parenthesized list) into a color. */
|
||||
static bool ParseVectorInitializer(const FString& InRaw, FLinearColor& Out)
|
||||
{
|
||||
FString Raw = InRaw.TrimStartAndEnd();
|
||||
if (!Raw.StartsWith(TEXT("(")) || !Raw.EndsWith(TEXT(")")))
|
||||
const int32 Open = Raw.Find(TEXT("("));
|
||||
const int32 Close = Raw.Find(TEXT(")"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
||||
if (Open == INDEX_NONE || Close == INDEX_NONE || Close <= Open)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Raw = Raw.Mid(1, Raw.Len() - 2);
|
||||
const TArray<FString> Comps = SplitTopLevel(Raw, TEXT(','));
|
||||
if (Comps.Num() < 3 || Comps.Num() > 4)
|
||||
const FString ListInner = Raw.Mid(Open + 1, Close - Open - 1);
|
||||
const TArray<FString> Comps = SplitTopLevel(ListInner, TEXT(','));
|
||||
if (Comps.Num() < 1 || Comps.Num() > 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Out = FLinearColor(
|
||||
FCString::Atof(*Comps[0]),
|
||||
FCString::Atof(*Comps[1]),
|
||||
FCString::Atof(*Comps[2]),
|
||||
Comps.Num() == 4 ? FCString::Atof(*Comps[3]) : 1.0f);
|
||||
Comps.Num() > 0 ? FCString::Atof(*Comps[0]) : 0.0f,
|
||||
Comps.Num() > 1 ? FCString::Atof(*Comps[1]) : 0.0f,
|
||||
Comps.Num() > 2 ? FCString::Atof(*Comps[2]) : 0.0f,
|
||||
Comps.Num() > 3 ? FCString::Atof(*Comps[3]) : 1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -456,22 +443,55 @@ namespace ShaderLabParser_Private
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True if `IncludePath` names a ShaderLab authoring-shim header (excluded from the compiled includes). */
|
||||
static bool IsAuthoringShimInclude(const FString& IncludePath)
|
||||
{
|
||||
const FString Leaf = FPaths::GetCleanFilename(IncludePath);
|
||||
return Leaf == TEXT("ShaderLab.ush")
|
||||
|| Leaf == TEXT("ShaderLabDSL.ush")
|
||||
|| Leaf == TEXT("ShaderLabCommon.ush");
|
||||
}
|
||||
}
|
||||
|
||||
using namespace ShaderLabParser_Private;
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Read `<retType> <name> ( <sig> ) { <body> }` after a marker macro. Returns false on error. */
|
||||
bool ParseAnnotatedFunction(FScanner& S, FString& OutName, FString& OutSigInner, FString& OutBody, int32& OutBodyLine)
|
||||
{
|
||||
FString RetType;
|
||||
if (!S.ReadIdentifier(RetType))
|
||||
{
|
||||
S.Error(TEXT("Expected a function declaration after the ShaderLab marker"));
|
||||
return false;
|
||||
}
|
||||
if (!S.ReadIdentifier(OutName))
|
||||
{
|
||||
S.Error(TEXT("Expected a function name"));
|
||||
return false;
|
||||
}
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), OutSigInner))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), OutBody, &OutBodyLine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParseSettings(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FShaderLabSettings& Out = Model.Settings;
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Inner))
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Settings are simple `Key = Value;` pairs; tolerate trailing items.
|
||||
for (const FString& StmtRaw : SplitTopLevel(StripComments(Inner), TEXT(';')))
|
||||
for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(',')))
|
||||
{
|
||||
const FString Stmt = StmtRaw.TrimStartAndEnd();
|
||||
if (Stmt.IsEmpty())
|
||||
@@ -520,220 +540,271 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void ParseOnePropertyDecl(FScanner& S, const FString& Stmt, FShaderLabModel& Model)
|
||||
/** Apply one `Key = Value` SL_PROPERTY specifier. */
|
||||
void ApplyPropertySpecifier(FScanner& S, const FString& ArgRaw, FShaderLabProperty& Prop)
|
||||
{
|
||||
// Form: Type Name ( "Disp", "Group" [, Range(a,b)] ) = default
|
||||
FString TypeName;
|
||||
int32 Cursor = 0;
|
||||
const FString Trimmed = Stmt.TrimStartAndEnd();
|
||||
|
||||
// Read type token.
|
||||
while (Cursor < Trimmed.Len() && !FChar::IsWhitespace(Trimmed[Cursor]))
|
||||
const FString Arg = ArgRaw.TrimStartAndEnd();
|
||||
if (Arg.IsEmpty())
|
||||
{
|
||||
++Cursor;
|
||||
}
|
||||
TypeName = Trimmed.Left(Cursor).TrimStartAndEnd();
|
||||
|
||||
EShaderLabPropertyType Type;
|
||||
if (!ParsePropertyType(TypeName, Type))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Unknown property type '%s'"), *TypeName));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read name token up to '('.
|
||||
const int32 ParenOpen = Trimmed.Find(TEXT("("), ESearchCase::CaseSensitive, ESearchDir::FromStart, Cursor);
|
||||
if (ParenOpen == INDEX_NONE)
|
||||
FString Key, Value;
|
||||
if (!Arg.Split(TEXT("="), &Key, &Value))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' missing '(' metadata block"), *TypeName));
|
||||
return;
|
||||
}
|
||||
const FString NameToken = Trimmed.Mid(Cursor, ParenOpen - Cursor).TrimStartAndEnd();
|
||||
if (NameToken.IsEmpty())
|
||||
{
|
||||
S.Error(TEXT("Property is missing a name"));
|
||||
S.Error(FString::Printf(TEXT("Property '%s' has malformed specifier '%s' (expected Key = Value)"),
|
||||
*Prop.Name.ToString(), *Arg));
|
||||
return;
|
||||
}
|
||||
Key = Key.TrimStartAndEnd();
|
||||
Value = Value.TrimStartAndEnd();
|
||||
|
||||
// Find matching ')' for the metadata block (no nested parens except Range(...)).
|
||||
int32 Depth = 0;
|
||||
int32 ParenClose = INDEX_NONE;
|
||||
for (int32 i = ParenOpen; i < Trimmed.Len(); ++i)
|
||||
if (Key == TEXT("DisplayName"))
|
||||
{
|
||||
if (Trimmed[i] == TEXT('(')) { ++Depth; }
|
||||
else if (Trimmed[i] == TEXT(')')) { --Depth; if (Depth == 0) { ParenClose = i; break; } }
|
||||
Prop.DisplayName = Value.TrimQuotes();
|
||||
}
|
||||
if (ParenClose == INDEX_NONE)
|
||||
else if (Key == TEXT("Group"))
|
||||
{
|
||||
Prop.Group = Value.TrimQuotes();
|
||||
}
|
||||
else if (Key == TEXT("SortPriority"))
|
||||
{
|
||||
Prop.SortPriority = FCString::Atoi(*Value);
|
||||
}
|
||||
else if (Key == TEXT("DefaultTexture"))
|
||||
{
|
||||
Prop.TextureDefault = Value.TrimQuotes();
|
||||
}
|
||||
else if (Key == TEXT("Range"))
|
||||
{
|
||||
const TArray<FString> Comps = SplitTopLevel(
|
||||
Value.Replace(TEXT("("), TEXT("")).Replace(TEXT(")"), TEXT("")), TEXT(','));
|
||||
if (Comps.Num() != 2)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString()));
|
||||
return;
|
||||
}
|
||||
Prop.bHasRange = true;
|
||||
Prop.RangeMin = FCString::Atof(*Comps[0]);
|
||||
Prop.RangeMax = FCString::Atof(*Comps[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' has unknown specifier '%s'"), *Prop.Name.ToString(), *Key));
|
||||
}
|
||||
}
|
||||
|
||||
void ParseProperty(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
// SL_PROPERTY( <specifiers> )
|
||||
FString SpecInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SpecInner))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' has an unterminated '(' block"), *NameToken));
|
||||
return;
|
||||
}
|
||||
|
||||
FShaderLabProperty Prop;
|
||||
Prop.Name = FName(*NameToken);
|
||||
Prop.Type = Type;
|
||||
Prop.SortPriority = Model.Properties.Num();
|
||||
|
||||
const FString MetaInner = Trimmed.Mid(ParenOpen + 1, ParenClose - ParenOpen - 1);
|
||||
const TArray<FString> MetaArgs = SplitTopLevel(MetaInner, TEXT(','));
|
||||
int32 StringArgIndex = 0;
|
||||
for (const FString& ArgRaw : MetaArgs)
|
||||
// The HLSL declaration following the macro determines name + type + default.
|
||||
S.SkipTrivia();
|
||||
if (S.Peek() == TEXT('#'))
|
||||
{
|
||||
const FString Arg = ArgRaw.TrimStartAndEnd();
|
||||
if (Arg.IsEmpty())
|
||||
// `#define NAME VALUE` => StaticBool.
|
||||
S.Advance(); // '#'
|
||||
FString Directive;
|
||||
if (!S.ReadIdentifier(Directive) || Directive != TEXT("define"))
|
||||
{
|
||||
continue;
|
||||
S.Error(TEXT("SL_PROPERTY on a preprocessor line expects `#define Name 0|1`"));
|
||||
return;
|
||||
}
|
||||
if (Arg.StartsWith(TEXT("\"")))
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
const FString Unquoted = Arg.TrimQuotes();
|
||||
if (StringArgIndex == 0) { Prop.DisplayName = Unquoted; }
|
||||
else if (StringArgIndex == 1) { Prop.Group = Unquoted; }
|
||||
++StringArgIndex;
|
||||
S.Error(TEXT("StaticBool `#define` is missing a name"));
|
||||
return;
|
||||
}
|
||||
else if (Arg.StartsWith(TEXT("Range")))
|
||||
// Remainder of the line is the value (1/0/true/false).
|
||||
FString ValueLine;
|
||||
while (!S.IsEnd() && S.Peek() != TEXT('\n'))
|
||||
{
|
||||
FString RangeInner;
|
||||
const int32 RO = Arg.Find(TEXT("("));
|
||||
const int32 RC = Arg.Find(TEXT(")"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
||||
if (RO == INDEX_NONE || RC == INDEX_NONE || RC <= RO)
|
||||
ValueLine.AppendChar(S.Advance());
|
||||
}
|
||||
ValueLine = ValueLine.TrimStartAndEnd();
|
||||
Prop.Name = FName(*Name);
|
||||
Prop.Type = EShaderLabPropertyType::StaticBool;
|
||||
if (!ParseBool(ValueLine, Prop.bStaticBoolDefault))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("StaticBool '%s' default must be 0/1 or true/false"), *Name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FString TypeTok;
|
||||
if (!S.ReadIdentifier(TypeTok))
|
||||
{
|
||||
S.Error(TEXT("SL_PROPERTY must be followed by an HLSL declaration"));
|
||||
return;
|
||||
}
|
||||
if (!InferPropertyType(TypeTok, Prop.Type))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Unsupported property type '%s' (use float/float3/float4/Texture2D/TextureCube or `#define` for StaticBool)"), *TypeTok));
|
||||
return;
|
||||
}
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property of type '%s' is missing a name"), *TypeTok));
|
||||
return;
|
||||
}
|
||||
Prop.Name = FName(*Name);
|
||||
|
||||
const bool bIsTexture = (Prop.Type == EShaderLabPropertyType::Texture2D || Prop.Type == EShaderLabPropertyType::TextureCube);
|
||||
if (bIsTexture)
|
||||
{
|
||||
// `Texture2D Name;` (no initializer) optionally followed by `SamplerState NameSampler;`.
|
||||
if (!S.Expect(TEXT(';'), TEXT("after a texture property")))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' has malformed Range(...)"), *NameToken));
|
||||
return;
|
||||
}
|
||||
RangeInner = Arg.Mid(RO + 1, RC - RO - 1);
|
||||
const TArray<FString> RangeComps = SplitTopLevel(RangeInner, TEXT(','));
|
||||
if (RangeComps.Num() != 2)
|
||||
S.SkipTrivia();
|
||||
const TCHAR* Save = S.Ptr;
|
||||
const int32 SaveLine = S.Line, SaveCol = S.Column;
|
||||
FString Maybe;
|
||||
if (S.ReadIdentifier(Maybe) && Maybe == TEXT("SamplerState"))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *NameToken));
|
||||
return;
|
||||
FString SamplerName;
|
||||
S.ReadIdentifier(SamplerName);
|
||||
S.Expect(TEXT(';'), TEXT("after a SamplerState declaration"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a sampler — rewind so the main loop sees this construct.
|
||||
S.Ptr = Save; S.Line = SaveLine; S.Column = SaveCol;
|
||||
}
|
||||
Prop.bHasRange = true;
|
||||
Prop.RangeMin = FCString::Atof(*RangeComps[0]);
|
||||
Prop.RangeMax = FCString::Atof(*RangeComps[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' has unknown metadata '%s'"), *NameToken, *Arg));
|
||||
return;
|
||||
// Scalar/Color/Vector: read the `= <initializer>;` for the default value.
|
||||
if (S.Match(TEXT('=')))
|
||||
{
|
||||
FString Init;
|
||||
S.ReadUntilSemicolon(Init);
|
||||
switch (Prop.Type)
|
||||
{
|
||||
case EShaderLabPropertyType::Scalar:
|
||||
Prop.ScalarDefault = FCString::Atof(*Init);
|
||||
break;
|
||||
case EShaderLabPropertyType::Color:
|
||||
case EShaderLabPropertyType::Vector:
|
||||
if (!ParseVectorInitializer(Init, Prop.VectorDefault))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' default must be floatN(...)"), *Name));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!S.Expect(TEXT(';'), TEXT("after a property declaration")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Prop.DisplayName.IsEmpty())
|
||||
{
|
||||
Prop.DisplayName = NameToken;
|
||||
}
|
||||
|
||||
// Default value after '='.
|
||||
const FString AfterParen = Trimmed.Mid(ParenClose + 1).TrimStartAndEnd();
|
||||
if (!AfterParen.StartsWith(TEXT("=")))
|
||||
// Specifiers (override SortPriority/DisplayName/etc.).
|
||||
for (const FString& Arg : SplitTopLevel(SpecInner, TEXT(',')))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' is missing '= default'"), *NameToken));
|
||||
return;
|
||||
}
|
||||
const FString DefaultRaw = AfterParen.Mid(1).TrimStartAndEnd();
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case EShaderLabPropertyType::Scalar:
|
||||
Prop.ScalarDefault = FCString::Atof(*DefaultRaw);
|
||||
break;
|
||||
case EShaderLabPropertyType::Color:
|
||||
case EShaderLabPropertyType::Vector:
|
||||
if (!ParseColorLiteral(DefaultRaw, Prop.VectorDefault))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' default must be (r,g,b[,a])"), *NameToken));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case EShaderLabPropertyType::Texture2D:
|
||||
case EShaderLabPropertyType::TextureCube:
|
||||
if (!DefaultRaw.StartsWith(TEXT("\"")))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' default must be a quoted texture token"), *NameToken));
|
||||
return;
|
||||
}
|
||||
Prop.TextureDefault = DefaultRaw.TrimQuotes();
|
||||
break;
|
||||
case EShaderLabPropertyType::StaticBool:
|
||||
if (!ParseBool(DefaultRaw, Prop.bStaticBoolDefault))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Property '%s' default must be true/false"), *NameToken));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Prop.SortPriority = Model.Properties.Num();
|
||||
if (Model.FindProperty(Prop.Name) != nullptr)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Duplicate property name '%s'"), *NameToken));
|
||||
return;
|
||||
}
|
||||
Model.Properties.Add(MoveTemp(Prop));
|
||||
}
|
||||
|
||||
void ParseProperties(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Inner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const FString& StmtRaw : SplitTopLevel(StripComments(Inner), TEXT(';')))
|
||||
{
|
||||
const FString Stmt = StmtRaw.TrimStartAndEnd();
|
||||
if (Stmt.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ParseOnePropertyDecl(S, Stmt, Model);
|
||||
ApplyPropertySpecifier(S, Arg, Prop);
|
||||
if (S.bFailed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ParseIncludes(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Inner))
|
||||
if (Prop.DisplayName.IsEmpty())
|
||||
{
|
||||
Prop.DisplayName = Prop.Name.ToString();
|
||||
}
|
||||
if (Model.FindProperty(Prop.Name) != nullptr)
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Duplicate property name '%s'"), *Prop.Name.ToString()));
|
||||
return;
|
||||
}
|
||||
// Accept whitespace/comma/semicolon separated quoted strings.
|
||||
FScanner Sub(Inner, S.Errors);
|
||||
while (true)
|
||||
Model.Properties.Add(MoveTemp(Prop));
|
||||
}
|
||||
|
||||
void ParseInclude(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
// At a '#include' directive; '#' already consumed, 'include' identifier already read.
|
||||
FString Path;
|
||||
if (!S.ReadString(Path))
|
||||
{
|
||||
// Tolerate angle-bracket includes by skipping the line; ShaderLab uses quoted virtual paths.
|
||||
S.SkipToEndOfLine();
|
||||
return;
|
||||
}
|
||||
if (!IsAuthoringShimInclude(Path))
|
||||
{
|
||||
Sub.SkipTrivia();
|
||||
if (Sub.IsEnd())
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (Sub.Peek() == TEXT(',') || Sub.Peek() == TEXT(';'))
|
||||
{
|
||||
Sub.Advance();
|
||||
continue;
|
||||
}
|
||||
FString Path;
|
||||
if (!Sub.ReadString(Path))
|
||||
{
|
||||
S.Error(TEXT("Includes block expects quoted paths"));
|
||||
return;
|
||||
}
|
||||
Model.Includes.Add(Path);
|
||||
}
|
||||
}
|
||||
|
||||
void ParseSlab(FScanner& S, FShaderLabModel& Model)
|
||||
void ParseEntry(FScanner& S, FShaderLabModel& Model, const FString& Marker)
|
||||
{
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
// Marker macro takes no args: SL_SURFACE()/SL_POSTPROCESS()/SL_UI()/SL_VERTEX().
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored))
|
||||
{
|
||||
S.Error(TEXT("Slab must be followed by a name"));
|
||||
return;
|
||||
}
|
||||
FString SigInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SigInner))
|
||||
FString Name, SigInner, Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FShaderLabEntryParam> Params;
|
||||
if (!ParseEntryParams(SigInner, Params))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s has a malformed parameter signature"), *Marker));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Marker == TEXT("SL_VERTEX"))
|
||||
{
|
||||
Model.VertexParams = MoveTemp(Params);
|
||||
Model.VertexBody = MoveTemp(Body);
|
||||
Model.VertexBodyLine = BodyLine;
|
||||
Model.bHasVertex = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Model.bHasSurface)
|
||||
{
|
||||
S.Error(TEXT("A shader may declare only one pixel entry (SL_SURFACE/SL_POSTPROCESS/SL_UI)"));
|
||||
return;
|
||||
}
|
||||
Model.SurfaceParams = MoveTemp(Params);
|
||||
Model.SurfaceBody = MoveTemp(Body);
|
||||
Model.SurfaceBodyLine = BodyLine;
|
||||
Model.bHasSurface = true;
|
||||
Model.SurfaceEntry = (Marker == TEXT("SL_POSTPROCESS")) ? EShaderLabEntry::PostProcess
|
||||
: (Marker == TEXT("SL_UI")) ? EShaderLabEntry::UI
|
||||
: EShaderLabEntry::Surface;
|
||||
}
|
||||
|
||||
void ParseSlab(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_SLAB()
|
||||
{
|
||||
return;
|
||||
}
|
||||
FString Name, SigInner, Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -743,12 +814,6 @@ namespace
|
||||
S.Error(FString::Printf(TEXT("Slab '%s' must take an (inout FShaderLabSurface) parameter"), *Name));
|
||||
return;
|
||||
}
|
||||
FString Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Body, &BodyLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FShaderLabSlab Slab;
|
||||
Slab.Name = FName(*Name);
|
||||
Slab.OutParamName = Params.Last().Name;
|
||||
@@ -759,20 +824,14 @@ namespace
|
||||
|
||||
void ParseValue(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
{
|
||||
S.Error(TEXT("Value must be followed by a name"));
|
||||
return;
|
||||
}
|
||||
FString SigInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SigInner)) // signature is empty: Value Name() { ... }
|
||||
FString Ignored;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Ignored)) // SL_VALUE()
|
||||
{
|
||||
return;
|
||||
}
|
||||
FString Body;
|
||||
FString Name, SigInner, Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Body, &BodyLine))
|
||||
if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -874,33 +933,35 @@ namespace
|
||||
|
||||
void ParseFrontMaterial(FScanner& S, FShaderLabModel& Model)
|
||||
{
|
||||
if (!S.Expect(TEXT('='), TEXT("after FrontMaterial")))
|
||||
// SL_FRONTMATERIAL( <topology expression> )
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Model.TopologyRoot = ParseTopoExpr(S, Model);
|
||||
if (S.bFailed)
|
||||
FScanner Sub(Inner, S.Errors);
|
||||
Model.TopologyRoot = ParseTopoExpr(Sub, Model);
|
||||
if (Sub.bFailed)
|
||||
{
|
||||
return;
|
||||
S.bFailed = true;
|
||||
}
|
||||
S.Expect(TEXT(';'), TEXT("after FrontMaterial expression"));
|
||||
}
|
||||
|
||||
/** Parse `Opacity = <ValueName>;` / `OpacityMask = <ValueName>;` (material-level outputs). */
|
||||
/** SL_OPACITY(Name) / SL_OPACITY_MASK(Name): material-level outputs referencing a Value block. */
|
||||
void ParseMaterialOutput(FScanner& S, FName& OutValueName, const TCHAR* What)
|
||||
{
|
||||
if (!S.Expect(TEXT('='), What))
|
||||
FString Inner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FString Name;
|
||||
if (!S.ReadIdentifier(Name))
|
||||
const FString Name = Inner.TrimStartAndEnd();
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s expects a Value block name"), What));
|
||||
return;
|
||||
}
|
||||
OutValueName = FName(*Name);
|
||||
S.Expect(TEXT(';'), What);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -913,7 +974,7 @@ bool FShaderLabParser::Parse(
|
||||
OutModel = FShaderLabModel();
|
||||
OutModel.SourceFilePath = SourceName;
|
||||
// Identity + display name come from the file name (e.g. "Basic.usl" -> "Basic"); the file IS the
|
||||
// shader, so there is no top-level `shader "Name" { }` wrapper — sections are parsed flat to EOF.
|
||||
// shader. Annotated-HLSL constructs are parsed flat to EOF in any order.
|
||||
OutModel.ShaderName = FPaths::GetBaseFilename(SourceName);
|
||||
|
||||
FScanner S(Source, OutErrors);
|
||||
@@ -923,94 +984,68 @@ bool FShaderLabParser::Parse(
|
||||
S.SkipTrivia();
|
||||
if (S.IsEnd())
|
||||
{
|
||||
break; // End of file: all sections parsed.
|
||||
break; // End of file: all constructs parsed.
|
||||
}
|
||||
|
||||
FString Section;
|
||||
if (!S.ReadIdentifier(Section))
|
||||
// Top-level preprocessor lines: `#include "..."` feeds Model.Includes; everything else
|
||||
// (`#pragma`, stray `#define`, etc.) is non-DSL and skipped.
|
||||
if (S.Peek() == TEXT('#'))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Expected a section keyword, found '%c'"), S.Peek()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Section == TEXT("Settings"))
|
||||
{
|
||||
ParseSettings(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("Properties"))
|
||||
{
|
||||
ParseProperties(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("Includes"))
|
||||
{
|
||||
ParseIncludes(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("Slab"))
|
||||
{
|
||||
ParseSlab(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("Value"))
|
||||
{
|
||||
ParseValue(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("FrontMaterial"))
|
||||
{
|
||||
ParseFrontMaterial(S, OutModel);
|
||||
}
|
||||
else if (Section == TEXT("Opacity"))
|
||||
{
|
||||
ParseMaterialOutput(S, OutModel.OpacityValueName, TEXT("Opacity"));
|
||||
}
|
||||
else if (Section == TEXT("OpacityMask"))
|
||||
{
|
||||
ParseMaterialOutput(S, OutModel.OpacityMaskValueName, TEXT("OpacityMask"));
|
||||
}
|
||||
else if (Section == TEXT("Surface") || Section == TEXT("PostProcess") || Section == TEXT("UI") || Section == TEXT("Vertex"))
|
||||
{
|
||||
const bool bIsSurface = (Section != TEXT("Vertex"));
|
||||
FString SigInner;
|
||||
if (!S.ReadBalanced(TEXT('('), TEXT(')'), SigInner))
|
||||
S.Advance(); // '#'
|
||||
FString Directive;
|
||||
if (S.ReadIdentifier(Directive) && Directive == TEXT("include"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TArray<FShaderLabEntryParam> Params;
|
||||
if (!ParseEntryParams(SigInner, Params))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("%s has a malformed parameter signature"), *Section));
|
||||
return false;
|
||||
}
|
||||
FString Body;
|
||||
int32 BodyLine = 0;
|
||||
if (!S.ReadBalanced(TEXT('{'), TEXT('}'), Body, &BodyLine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (bIsSurface)
|
||||
{
|
||||
if (OutModel.bHasSurface)
|
||||
{
|
||||
S.Error(TEXT("A shader may declare only one pixel entry (Surface/PostProcess/UI)"));
|
||||
return false;
|
||||
}
|
||||
OutModel.SurfaceParams = MoveTemp(Params);
|
||||
OutModel.SurfaceBody = MoveTemp(Body);
|
||||
OutModel.SurfaceBodyLine = BodyLine;
|
||||
OutModel.bHasSurface = true;
|
||||
OutModel.SurfaceEntry = (Section == TEXT("PostProcess")) ? EShaderLabEntry::PostProcess
|
||||
: (Section == TEXT("UI")) ? EShaderLabEntry::UI
|
||||
: EShaderLabEntry::Surface;
|
||||
ParseInclude(S, OutModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutModel.VertexParams = MoveTemp(Params);
|
||||
OutModel.VertexBody = MoveTemp(Body);
|
||||
OutModel.VertexBodyLine = BodyLine;
|
||||
OutModel.bHasVertex = true;
|
||||
S.SkipToEndOfLine();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Token;
|
||||
if (!S.ReadIdentifier(Token))
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Expected a ShaderLab macro or #include, found '%c'"), S.Peek()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Token == TEXT("SL_SETTINGS"))
|
||||
{
|
||||
ParseSettings(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_PROPERTY"))
|
||||
{
|
||||
ParseProperty(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_SURFACE") || Token == TEXT("SL_POSTPROCESS") || Token == TEXT("SL_UI") || Token == TEXT("SL_VERTEX"))
|
||||
{
|
||||
ParseEntry(S, OutModel, Token);
|
||||
}
|
||||
else if (Token == TEXT("SL_SLAB"))
|
||||
{
|
||||
ParseSlab(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_VALUE"))
|
||||
{
|
||||
ParseValue(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_FRONTMATERIAL"))
|
||||
{
|
||||
ParseFrontMaterial(S, OutModel);
|
||||
}
|
||||
else if (Token == TEXT("SL_OPACITY"))
|
||||
{
|
||||
ParseMaterialOutput(S, OutModel.OpacityValueName, TEXT("SL_OPACITY"));
|
||||
}
|
||||
else if (Token == TEXT("SL_OPACITY_MASK"))
|
||||
{
|
||||
ParseMaterialOutput(S, OutModel.OpacityMaskValueName, TEXT("SL_OPACITY_MASK"));
|
||||
}
|
||||
else
|
||||
{
|
||||
S.Error(FString::Printf(TEXT("Unknown section '%s'"), *Section));
|
||||
S.Error(FString::Printf(TEXT("Unexpected token '%s' (expected a SL_ macro or #include)"), *Token));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1024,24 +1059,24 @@ bool FShaderLabParser::Parse(
|
||||
const bool bHasMultiSlab = OutModel.Slabs.Num() > 0 || OutModel.TopologyRoot != INDEX_NONE;
|
||||
if (OutModel.bHasSurface && bHasMultiSlab)
|
||||
{
|
||||
S.Error(TEXT("Use either a single Surface(...) or named Slab blocks with FrontMaterial, not both"));
|
||||
S.Error(TEXT("Use either a single SL_SURFACE(...) or SL_SLAB blocks with SL_FRONTMATERIAL, not both"));
|
||||
return false;
|
||||
}
|
||||
if (!OutModel.bHasSurface && !bHasMultiSlab)
|
||||
{
|
||||
S.Error(TEXT("Shader must declare a Surface(...) entry point, or Slab blocks with a FrontMaterial expression"));
|
||||
S.Error(TEXT("Shader must declare a SL_SURFACE(...) entry point, or SL_SLAB blocks with a SL_FRONTMATERIAL expression"));
|
||||
return false;
|
||||
}
|
||||
if (bHasMultiSlab)
|
||||
{
|
||||
if (OutModel.Slabs.Num() == 0)
|
||||
{
|
||||
S.Error(TEXT("FrontMaterial requires at least one Slab block"));
|
||||
S.Error(TEXT("SL_FRONTMATERIAL requires at least one SL_SLAB block"));
|
||||
return false;
|
||||
}
|
||||
if (OutModel.TopologyRoot == INDEX_NONE)
|
||||
{
|
||||
S.Error(TEXT("Slab blocks require a 'FrontMaterial = ...;' topology expression"));
|
||||
S.Error(TEXT("SL_SLAB blocks require a SL_FRONTMATERIAL(...) topology expression"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1051,18 +1086,18 @@ bool FShaderLabParser::Parse(
|
||||
const EShaderLabDomain Domain = OutModel.Settings.Domain;
|
||||
if (Domain == EShaderLabDomain::PostProcess && !(OutModel.bHasSurface && OutModel.SurfaceEntry == EShaderLabEntry::PostProcess))
|
||||
{
|
||||
S.Error(TEXT("Domain = PostProcess requires a PostProcess(inout FShaderLabPostProcess) entry"));
|
||||
S.Error(TEXT("Domain = PostProcess requires a SL_POSTPROCESS(inout FShaderLabPostProcess) entry"));
|
||||
return false;
|
||||
}
|
||||
if (Domain == EShaderLabDomain::UI && !(OutModel.bHasSurface && OutModel.SurfaceEntry == EShaderLabEntry::UI))
|
||||
{
|
||||
S.Error(TEXT("Domain = UI requires a UI(inout FShaderLabUI) entry"));
|
||||
S.Error(TEXT("Domain = UI requires a SL_UI(inout FShaderLabUI) entry"));
|
||||
return false;
|
||||
}
|
||||
if ((Domain == EShaderLabDomain::Surface || Domain == EShaderLabDomain::Decal) && OutModel.bHasSurface
|
||||
&& OutModel.SurfaceEntry != EShaderLabEntry::Surface)
|
||||
{
|
||||
S.Error(TEXT("Surface/Decal domains require a Surface entry (or Slab blocks), not PostProcess/UI"));
|
||||
S.Error(TEXT("Surface/Decal domains require a SL_SURFACE entry (or SL_SLAB blocks), not PostProcess/UI"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
285
Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp
Normal file
285
Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright FlecsProj. 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
|
||||
// from Rider's UE shader support or VSCode + HLSL Tools. It writes, all from a single source of truth:
|
||||
// 1. <Plugin>/Shaders/ShaderLab.ush — umbrella header: DSL macros + structs + `namespace UE`
|
||||
// intrinsic stubs generated from FShaderLabIntrinsicRegistry.
|
||||
// 2. <Project>/shadertoolsconfig.json — HLSL Tools virtual-directory mappings, dumped from
|
||||
// AllShaderSourceDirectoryMappings() (the same table the
|
||||
// runtime registers). Machine-specific → gitignored.
|
||||
// 3. <Project>/.vscode/settings.json — merges *.usl/*.usf/*.ush -> hlsl into files.associations
|
||||
// (non-destructive: existing keys are preserved).
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "ShaderCore.h"
|
||||
#include "ShaderLabIntrinsicRegistry.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabIDE, Log, All);
|
||||
|
||||
namespace ShaderLabIDEPrepare_Private
|
||||
{
|
||||
// Strip line and block comments (string-aware) so JSONC settings.json parses as JSON.
|
||||
static FString StripJsonComments(const FString& In)
|
||||
{
|
||||
FString Out;
|
||||
Out.Reserve(In.Len());
|
||||
const int32 N = In.Len();
|
||||
for (int32 i = 0; i < N;)
|
||||
{
|
||||
const TCHAR C = In[i];
|
||||
if (C == TEXT('"'))
|
||||
{
|
||||
Out.AppendChar(C);
|
||||
++i;
|
||||
while (i < N)
|
||||
{
|
||||
const TCHAR D = In[i];
|
||||
Out.AppendChar(D);
|
||||
++i;
|
||||
if (D == TEXT('\\') && i < N) { Out.AppendChar(In[i]); ++i; }
|
||||
else if (D == TEXT('"')) { break; }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('/'))
|
||||
{
|
||||
while (i < N && In[i] != TEXT('\n')) { ++i; }
|
||||
continue;
|
||||
}
|
||||
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('*'))
|
||||
{
|
||||
i += 2;
|
||||
while (i + 1 < N && !(In[i] == TEXT('*') && In[i + 1] == TEXT('/'))) { ++i; }
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
Out.AppendChar(C);
|
||||
++i;
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
// Remove trailing commas before `}`/`]` (string-aware) so VSCode-style JSONC parses as strict JSON.
|
||||
static FString StripTrailingCommas(const FString& In)
|
||||
{
|
||||
FString Out;
|
||||
Out.Reserve(In.Len());
|
||||
const int32 N = In.Len();
|
||||
for (int32 i = 0; i < N; ++i)
|
||||
{
|
||||
const TCHAR C = In[i];
|
||||
if (C == TEXT('"'))
|
||||
{
|
||||
Out.AppendChar(C);
|
||||
for (++i; i < N; ++i)
|
||||
{
|
||||
const TCHAR D = In[i];
|
||||
Out.AppendChar(D);
|
||||
if (D == TEXT('\\') && i + 1 < N) { Out.AppendChar(In[++i]); }
|
||||
else if (D == TEXT('"')) { break; }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (C == TEXT(','))
|
||||
{
|
||||
int32 j = i + 1;
|
||||
while (j < N && FChar::IsWhitespace(In[j])) { ++j; }
|
||||
if (j < N && (In[j] == TEXT('}') || In[j] == TEXT(']')))
|
||||
{
|
||||
continue; // drop the trailing comma
|
||||
}
|
||||
}
|
||||
Out.AppendChar(C);
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
/** Build the `namespace UE { ... }` block of stub declarations from the registry (sorted, deterministic). */
|
||||
static FString GenerateIntrinsicNamespace()
|
||||
{
|
||||
TArray<FShaderLabIntrinsicDesc> Descs;
|
||||
FShaderLabIntrinsicRegistry::Get().ForEach([&Descs](const FShaderLabIntrinsicDesc& D) { Descs.Add(D); });
|
||||
Descs.Sort([](const FShaderLabIntrinsicDesc& A, const FShaderLabIntrinsicDesc& B)
|
||||
{
|
||||
return A.Name.LexicalLess(B.Name);
|
||||
});
|
||||
|
||||
FString Body;
|
||||
Body += TEXT("namespace UE\n{\n");
|
||||
for (const FShaderLabIntrinsicDesc& D : Descs)
|
||||
{
|
||||
if (!D.Doc.IsEmpty())
|
||||
{
|
||||
Body += FString::Printf(TEXT("\t// %s\n"), *D.Doc);
|
||||
}
|
||||
FString Params;
|
||||
for (int32 i = 0; i < D.Params.Num(); ++i)
|
||||
{
|
||||
const FShaderLabIntrinsicParam& P = D.Params[i];
|
||||
if (i > 0) { Params += TEXT(", "); }
|
||||
Params += FString::Printf(TEXT("%s %s"), *P.Type, *P.Name);
|
||||
if (!P.DefaultLiteral.IsEmpty()) { Params += FString::Printf(TEXT(" = %s"), *P.DefaultLiteral); }
|
||||
}
|
||||
// Stub body returns a zero of the return type; this is authoring-only and never compiled
|
||||
// (the graph builder rewrites UE:: calls into material-expression inputs before compile).
|
||||
Body += FString::Printf(TEXT("\t%s %s(%s) { return (%s)0; }\n"),
|
||||
*D.ReturnType, *D.Name.ToString(), *Params, *D.ReturnType);
|
||||
}
|
||||
Body += TEXT("}\n");
|
||||
return Body;
|
||||
}
|
||||
|
||||
static bool WriteIfChanged(const FString& Path, const FString& Content)
|
||||
{
|
||||
FString Existing;
|
||||
if (FFileHelper::LoadFileToString(Existing, *Path) && Existing.Equals(Content, ESearchCase::CaseSensitive))
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *Path);
|
||||
return true;
|
||||
}
|
||||
// UTF-8 (no BOM) so HLSL Tools / Rider and JSON parsers read the generated files reliably
|
||||
// (FFileHelper defaults to UTF-16, which not every shader/JSON tool handles).
|
||||
if (!FFileHelper::SaveStringToFile(Content, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Error, TEXT(" FAILED to write: %s"), *Path);
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT(" wrote: %s"), *Path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void GenerateShaderLabHeader(const FString& PluginShaderDir)
|
||||
{
|
||||
FString H;
|
||||
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
|
||||
H += TEXT("// Authoring umbrella header: include this from every .usl for IDE completion.\n");
|
||||
H += TEXT("// At real shader-compile time this header is NOT included (the parser strips it);\n");
|
||||
H += TEXT("// the UE:: stubs below are for the editor only.\n");
|
||||
H += TEXT("#pragma once\n\n");
|
||||
H += TEXT("#include \"/Plugin/ShaderLab/ShaderLabDSL.ush\"\n");
|
||||
H += TEXT("#include \"/Plugin/ShaderLab/Private/ShaderLabCommon.ush\"\n\n");
|
||||
H += GenerateIntrinsicNamespace();
|
||||
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("ShaderLab.ush")), H);
|
||||
}
|
||||
|
||||
static void GenerateShaderToolsConfig(const FString& ProjectDir)
|
||||
{
|
||||
const TMap<FString, FString>& Mappings = AllShaderSourceDirectoryMappings();
|
||||
|
||||
const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
||||
Root->SetBoolField(TEXT("root"), true);
|
||||
const TSharedRef<FJsonObject> Virt = MakeShared<FJsonObject>();
|
||||
for (const TPair<FString, FString>& Pair : Mappings)
|
||||
{
|
||||
// Engine mappings come back relative to the engine binaries dir; HLSL Tools needs
|
||||
// absolute (or config-relative) paths, so normalize to full paths.
|
||||
Virt->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value));
|
||||
}
|
||||
Root->SetObjectField(TEXT("hlsl.virtualDirectoryMappings"), Virt);
|
||||
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Root, Writer);
|
||||
WriteIfChanged(FPaths::Combine(ProjectDir, TEXT("shadertoolsconfig.json")), Out);
|
||||
}
|
||||
|
||||
static void MergeVSCodeSettings(const FString& ProjectDir)
|
||||
{
|
||||
const FString SettingsPath = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("settings.json"));
|
||||
|
||||
TSharedPtr<FJsonObject> Settings;
|
||||
FString Existing;
|
||||
if (FFileHelper::LoadFileToString(Existing, *SettingsPath))
|
||||
{
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(StripTrailingCommas(StripJsonComments(Existing)));
|
||||
if (!FJsonSerializer::Deserialize(Reader, Settings) || !Settings.IsValid())
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Warning,
|
||||
TEXT(" could not parse %s — add files.associations for *.usl/*.usf/*.ush -> hlsl manually."),
|
||||
*SettingsPath);
|
||||
return; // Never clobber an unparseable user file.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings = MakeShared<FJsonObject>();
|
||||
}
|
||||
|
||||
// Union into files.associations, preserving any existing entries. Only rewrite the file when
|
||||
// we actually add a mapping, so a hand-maintained settings.json (comments, formatting) is left
|
||||
// untouched once the associations are present.
|
||||
const TSharedPtr<FJsonObject>* AssocPtr = nullptr;
|
||||
TSharedPtr<FJsonObject> Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr)
|
||||
? *AssocPtr : MakeShared<FJsonObject>();
|
||||
bool bChanged = false;
|
||||
for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.usf"), TEXT("*.ush") })
|
||||
{
|
||||
FString Existing2;
|
||||
if (!Assoc->TryGetStringField(Ext, Existing2) || Existing2 != TEXT("hlsl"))
|
||||
{
|
||||
Assoc->SetStringField(Ext, TEXT("hlsl"));
|
||||
bChanged = true;
|
||||
}
|
||||
}
|
||||
if (!bChanged)
|
||||
{
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath);
|
||||
return;
|
||||
}
|
||||
Settings->SetObjectField(TEXT("files.associations"), Assoc);
|
||||
|
||||
FString Out;
|
||||
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
||||
FJsonSerializer::Serialize(Settings.ToSharedRef(), Writer);
|
||||
WriteIfChanged(SettingsPath, Out);
|
||||
}
|
||||
|
||||
static void EnsureGitIgnore(const FString& ProjectDir)
|
||||
{
|
||||
const FString Path = FPaths::Combine(ProjectDir, TEXT(".gitignore"));
|
||||
FString GI;
|
||||
FFileHelper::LoadFileToString(GI, *Path); // missing file -> empty, fine
|
||||
if (GI.Contains(TEXT("shadertoolsconfig.json")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!GI.IsEmpty() && !GI.EndsWith(TEXT("\n")))
|
||||
{
|
||||
GI += TEXT("\n");
|
||||
}
|
||||
GI += TEXT("# Machine-specific HLSL Tools config (generated by ShaderLab.IDE.Prepare).\n");
|
||||
GI += TEXT("shadertoolsconfig.json\n");
|
||||
WriteIfChanged(Path, GI);
|
||||
}
|
||||
|
||||
static void Run(const TArray<FString>& /*Args*/)
|
||||
{
|
||||
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"));
|
||||
check(Plugin.IsValid()); // The command lives in this plugin's editor module; it must be findable.
|
||||
|
||||
const FString PluginShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders"));
|
||||
const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
||||
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare:"));
|
||||
GenerateShaderLabHeader(PluginShaderDir);
|
||||
GenerateShaderToolsConfig(ProjectDir);
|
||||
MergeVSCodeSettings(ProjectDir);
|
||||
EnsureGitIgnore(ProjectDir);
|
||||
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare: done."));
|
||||
}
|
||||
}
|
||||
|
||||
static FAutoConsoleCommand GShaderLabIDEPrepareCommand(
|
||||
TEXT("ShaderLab.IDE.Prepare"),
|
||||
TEXT("Generate IDE completion assets for .usl: <Plugin>/Shaders/ShaderLab.ush, shadertoolsconfig.json, .vscode/settings.json."),
|
||||
FConsoleCommandWithArgsDelegate::CreateStatic(&ShaderLabIDEPrepare_Private::Run));
|
||||
@@ -63,6 +63,8 @@ void FShaderLabIntrinsicRegistry::Register(FShaderLabIntrinsicDesc Desc)
|
||||
{
|
||||
check(!Desc.Name.IsNone());
|
||||
check(Desc.MakeNode);
|
||||
// Contract: every intrinsic is IDE-describable so ShaderLab.IDE.Prepare can always emit a stub.
|
||||
check(!Desc.ReturnType.IsEmpty());
|
||||
Descs.Add(Desc.Name, MoveTemp(Desc));
|
||||
}
|
||||
|
||||
@@ -71,35 +73,49 @@ const FShaderLabIntrinsicDesc* FShaderLabIntrinsicRegistry::Find(FName Name) con
|
||||
return Descs.Find(Name);
|
||||
}
|
||||
|
||||
void FShaderLabIntrinsicRegistry::ForEach(TFunctionRef<void(const FShaderLabIntrinsicDesc&)> Fn) const
|
||||
{
|
||||
for (const TPair<FName, FShaderLabIntrinsicDesc>& Pair : Descs)
|
||||
{
|
||||
Fn(Pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
void FShaderLabIntrinsicRegistry::RegisterBuiltins()
|
||||
{
|
||||
using namespace ShaderLabIntrinsic_Private;
|
||||
using EFreq = EShaderLabIntrinsicFrequency;
|
||||
|
||||
// Helper for the common no-arg case.
|
||||
auto AddSimple = [this](const TCHAR* Name, EFreq Freq, TFunction<UMaterialExpression*(UMaterial&)> Make)
|
||||
// Helper for the common no-arg case (ReturnType drives the generated authoring stub).
|
||||
auto AddSimple = [this](const TCHAR* Name, EFreq Freq, const TCHAR* ReturnType, TFunction<UMaterialExpression*(UMaterial&)> Make)
|
||||
{
|
||||
FShaderLabIntrinsicDesc Desc;
|
||||
Desc.Name = FName(Name);
|
||||
Desc.Frequency = Freq;
|
||||
Desc.ReturnType = ReturnType;
|
||||
Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray<FString>&, FString&) { return Make(M); };
|
||||
Register(MoveTemp(Desc));
|
||||
};
|
||||
|
||||
// --- A. Mesh / surface attributes ---
|
||||
AddSimple(TEXT("VertexNormalWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
|
||||
AddSimple(TEXT("VertexTangentWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
|
||||
AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
|
||||
AddSimple(TEXT("VertexColor"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
|
||||
AddSimple(TEXT("TwoSidedSign"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
|
||||
AddSimple(TEXT("LightmapUVs"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
|
||||
AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
|
||||
AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
|
||||
AddSimple(TEXT("VertexNormalWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
|
||||
AddSimple(TEXT("VertexTangentWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
|
||||
AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
|
||||
AddSimple(TEXT("VertexColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
|
||||
AddSimple(TEXT("TwoSidedSign"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
|
||||
AddSimple(TEXT("LightmapUVs"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
|
||||
AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
|
||||
AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
|
||||
|
||||
// TextureCoordinate(Index[, UTiling, VTiling]) — const args configure the node (mechanism: scalar/int args).
|
||||
{
|
||||
FShaderLabIntrinsicDesc Desc;
|
||||
Desc.Name = FName(TEXT("TextureCoordinate"));
|
||||
Desc.ReturnType = TEXT("float2");
|
||||
Desc.Params = { { TEXT("int"), TEXT("Index"), TEXT("") },
|
||||
{ TEXT("float"), TEXT("UTiling"), TEXT("1") },
|
||||
{ TEXT("float"), TEXT("VTiling"), TEXT("1") } };
|
||||
Desc.Doc = TEXT("UV channel `Index`, optionally tiled.");
|
||||
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
|
||||
{
|
||||
UMaterialExpressionTextureCoordinate* E = NewObject<UMaterialExpressionTextureCoordinate>(&M);
|
||||
@@ -113,31 +129,34 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
|
||||
}
|
||||
|
||||
// --- B. Positions / spaces ---
|
||||
AddSimple(TEXT("WorldPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
|
||||
AddSimple(TEXT("LocalPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
|
||||
AddSimple(TEXT("ActorPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
|
||||
AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
|
||||
AddSimple(TEXT("CameraPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
|
||||
AddSimple(TEXT("CameraVectorWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
|
||||
AddSimple(TEXT("LightVector"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
|
||||
AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
|
||||
AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
|
||||
AddSimple(TEXT("WorldPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
|
||||
AddSimple(TEXT("LocalPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
|
||||
AddSimple(TEXT("ActorPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
|
||||
AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
|
||||
AddSimple(TEXT("CameraPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
|
||||
AddSimple(TEXT("CameraVectorWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
|
||||
AddSimple(TEXT("LightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
|
||||
AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
|
||||
AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
|
||||
|
||||
// --- C. Object / bounds (ObjectLocalBounds & PreSkinnedLocalBounds are multi-output; output 0) ---
|
||||
AddSimple(TEXT("ObjectRadius"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
|
||||
AddSimple(TEXT("ObjectBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
|
||||
AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
|
||||
AddSimple(TEXT("ObjectOrientation"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
|
||||
AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
|
||||
AddSimple(TEXT("ObjectRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
|
||||
AddSimple(TEXT("ObjectBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
|
||||
AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
|
||||
AddSimple(TEXT("ObjectOrientation"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
|
||||
AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
|
||||
|
||||
// --- D. Instancing ---
|
||||
AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
|
||||
AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
|
||||
AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
|
||||
AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
|
||||
|
||||
// PerInstanceCustomData(DataIndex[, ConstDefaultValue]) — mechanism: const args + optional input pin left at default.
|
||||
{
|
||||
FShaderLabIntrinsicDesc Desc;
|
||||
Desc.Name = FName(TEXT("PerInstanceCustomData"));
|
||||
Desc.ReturnType = TEXT("float");
|
||||
Desc.Params = { { TEXT("int"), TEXT("DataIndex"), TEXT("") },
|
||||
{ TEXT("float"), TEXT("ConstDefaultValue"), TEXT("0") } };
|
||||
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
|
||||
{
|
||||
UMaterialExpressionPerInstanceCustomData* E = NewObject<UMaterialExpressionPerInstanceCustomData>(&M);
|
||||
@@ -150,13 +169,16 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
|
||||
}
|
||||
|
||||
// --- Time ---
|
||||
AddSimple(TEXT("Time"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
|
||||
AddSimple(TEXT("DeltaTime"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
|
||||
AddSimple(TEXT("Time"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
|
||||
AddSimple(TEXT("DeltaTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
|
||||
|
||||
// ViewProperty(Property) — representative of the enum-by-name const-arg mechanism.
|
||||
{
|
||||
FShaderLabIntrinsicDesc Desc;
|
||||
Desc.Name = FName(TEXT("ViewProperty"));
|
||||
Desc.ReturnType = TEXT("float4");
|
||||
Desc.Params = { { TEXT("int"), TEXT("Property"), TEXT("") } };
|
||||
Desc.Doc = TEXT("Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView).");
|
||||
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString& OutError) -> UMaterialExpression*
|
||||
{
|
||||
UMaterialExpressionViewProperty* E = NewObject<UMaterialExpressionViewProperty>(&M);
|
||||
@@ -183,6 +205,7 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
|
||||
{
|
||||
FShaderLabIntrinsicDesc Desc;
|
||||
Desc.Name = FName(TEXT("ParticleColor"));
|
||||
Desc.ReturnType = TEXT("float4");
|
||||
Desc.MakeNode = [](UMaterial& M, const TArray<FString>&, FString&) { return MakeSimple<UMaterialExpressionParticleColor>(M); };
|
||||
Register(MoveTemp(Desc));
|
||||
}
|
||||
|
||||
@@ -15,10 +15,25 @@ enum class EShaderLabIntrinsicFrequency : uint8
|
||||
VertexOnly,
|
||||
};
|
||||
|
||||
/** One formal parameter of an intrinsic, used only to generate the IDE authoring stub signature. */
|
||||
struct FShaderLabIntrinsicParam
|
||||
{
|
||||
/** HLSL type of the parameter, e.g. "int", "float". */
|
||||
FString Type;
|
||||
/** Parameter name shown in completion, e.g. "Index". */
|
||||
FString Name;
|
||||
/** Default-value literal for the stub (e.g. "1"); empty means the parameter is required. */
|
||||
FString DefaultLiteral;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes one `UE::NodeName(...)` intrinsic: how to build the backing material expression node,
|
||||
* which of its outputs to read, and what it requires of the material. The Custom-node input type is
|
||||
* derived by the engine from the connected node's real output type, so no output type is declared here.
|
||||
*
|
||||
* The ReturnType/Params/Doc fields carry no runtime meaning — they exist so `ShaderLab.IDE.Prepare`
|
||||
* can emit a `namespace UE { ... }` HLSL stub header from this single source of truth, giving the IDE
|
||||
* member/signature completion for `UE::` calls without any drift from the real registration.
|
||||
*/
|
||||
struct FShaderLabIntrinsicDesc
|
||||
{
|
||||
@@ -30,6 +45,15 @@ struct FShaderLabIntrinsicDesc
|
||||
/** Stage restriction; misuse is a build error. */
|
||||
EShaderLabIntrinsicFrequency Frequency = EShaderLabIntrinsicFrequency::Any;
|
||||
|
||||
/** HLSL return type for the authoring stub, e.g. "float2"/"float3"/"float". Required (see Register). */
|
||||
FString ReturnType;
|
||||
|
||||
/** Formal parameters for the authoring stub (empty for the common no-arg intrinsics). */
|
||||
TArray<FShaderLabIntrinsicParam> Params;
|
||||
|
||||
/** Optional one-line doc surfaced as a comment above the stub. */
|
||||
FString Doc;
|
||||
|
||||
/**
|
||||
* Create the backing expression node and add it to `Material`'s expression collection. `ConstArgs`
|
||||
* are the literal arguments parsed from the call (e.g. {"0","2.0"} for `UE::TextureCoordinate(0,2.0)`).
|
||||
@@ -55,6 +79,9 @@ public:
|
||||
/** Lookup by name; nullptr if unknown. */
|
||||
const FShaderLabIntrinsicDesc* Find(FName Name) const;
|
||||
|
||||
/** Visit every registered intrinsic (used by ShaderLab.IDE.Prepare to generate the UE:: stub header). */
|
||||
void ForEach(TFunctionRef<void(const FShaderLabIntrinsicDesc&)> Fn) const;
|
||||
|
||||
private:
|
||||
FShaderLabIntrinsicRegistry();
|
||||
void RegisterBuiltins();
|
||||
|
||||
@@ -22,6 +22,7 @@ public class UShaderLabEditor : ModuleRules
|
||||
"UShaderLab",
|
||||
"RenderCore",
|
||||
"RHI",
|
||||
"Json",
|
||||
"UnrealEd",
|
||||
"AssetTools",
|
||||
"MaterialEditor",
|
||||
|
||||
Reference in New Issue
Block a user