diff --git a/Shaders/Private/ShaderLabCommon.ush b/Shaders/Private/ShaderLabCommon.ush index 89f9e79..0e04966 100644 --- a/Shaders/Private/ShaderLabCommon.ush +++ b/Shaders/Private/ShaderLabCommon.ush @@ -4,7 +4,7 @@ #pragma once -// Per-pixel/vertex inputs are read through `UE::NodeName(...)` intrinsics (resolved by the graph +// Per-pixel/vertex inputs are read through `UE_NodeName(...)` intrinsics (resolved by the graph // builder to real material expression nodes), not through a context struct. // Surface description filled by the Surface(...) body. Defaults mirror the diff --git a/Shaders/ShaderLabDSL.ush b/Shaders/Private/ShaderLabDSL.ush similarity index 96% rename from Shaders/ShaderLabDSL.ush rename to Shaders/Private/ShaderLabDSL.ush index c71ee55..c13ca64 100644 --- a/Shaders/ShaderLabDSL.ush +++ b/Shaders/Private/ShaderLabDSL.ush @@ -45,7 +45,7 @@ // SL_SETTINGS : Domain, BlendMode, TwoSided, OpacityMaskClipValue, // Domain = Surface | PostProcess | UI | Decal // BlendMode = Opaque | Masked | Translucent | Additive | Modulate -// SL_PROPERTY : DisplayName, Group, SortPriority, Range (Scalar only), DefaultTexture (textures only) +// SL_PROPERTY : DisplayName, Category, SortPriority, Range (Scalar only), DefaultTexture (textures only) // // Topology operators (used inside SL_FRONTMATERIAL): // VerticalLayer(Top, Base, Thickness) | HorizontalMix(Background, Foreground, Mix) diff --git a/Shaders/Private/ShaderLabUEFunctions.ush b/Shaders/Private/ShaderLabUEFunctions.ush new file mode 100644 index 0000000..edf6d07 --- /dev/null +++ b/Shaders/Private/ShaderLabUEFunctions.ush @@ -0,0 +1,10 @@ +// Copyright FlecsProj. 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 +// forwarders compile) and pulled in by the ShaderLab.ush authoring umbrella (so they complete in the IDE). + +#pragma once + +#include "/Plugin/ShaderLab/Private/UEFunctions/Noise.ush" +#include "/Plugin/ShaderLab/Private/UEFunctions/Transform.ush" diff --git a/Shaders/Private/ShaderLabUENode.ush b/Shaders/Private/ShaderLabUENode.ush new file mode 100644 index 0000000..c5b2480 --- /dev/null +++ b/Shaders/Private/ShaderLabUENode.ush @@ -0,0 +1,99 @@ +// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand. +// UE_ node-intrinsic stubs + their enums, for IDE completion only (never compiled). +#pragma once + +enum EMaterialExpressionBoundsType +{ + MEILB_InstanceLocal, + MEILB_ObjectLocal, + MEILB_PreSkinnedLocal, +}; +enum EMaterialExposedViewProperty +{ + MEVP_BufferSize, + MEVP_FieldOfView, + MEVP_TanHalfFieldOfView, + MEVP_ViewSize, + MEVP_WorldSpaceViewPosition, + MEVP_WorldSpaceCameraPosition, + MEVP_ViewportOffset, + MEVP_TemporalSampleCount, + MEVP_TemporalSampleIndex, + MEVP_TemporalSampleOffset, + MEVP_RuntimeVirtualTextureOutputLevel, + MEVP_RuntimeVirtualTextureOutputDerivative, + MEVP_PreExposure, + MEVP_RuntimeVirtualTextureMaxLevel, + MEVP_ResolutionFraction, + MEVP_PostVolumeUserFlags, + MEVP_FirstPersonFieldOfView, + MEVP_FirstPersonTanHalfFieldOfView, + MEVP_FirstPersonScale, + MEVP_NearPlane, +}; + +float3 UE_ActorPositionWS() { return (float3)0; } +float3 UE_AtmosphericLightColor() { return (float3)0; } +float3 UE_AtmosphericLightVector() { return (float3)0; } +// Space is an EMaterialExpressionBoundsType token (e.g. MEILB_ObjectLocal). +float3 UE_Bounds(EMaterialExpressionBoundsType Space) { return (float3)0; } +float3 UE_CameraPositionWS() { return (float3)0; } +float3 UE_CameraVectorWS() { return (float3)0; } +float3 UE_DecalColor() { return (float3)0; } +float2 UE_DecalDerivativeDDX() { return (float2)0; } +float2 UE_DecalDerivativeDDY() { return (float2)0; } +float UE_DecalLifetimeOpacity() { return (float)0; } +float UE_DeltaTime() { return (float)0; } +float UE_DistanceCullFade() { return (float)0; } +float UE_EyeAdaptation() { return (float)0; } +float UE_IsFirstPerson() { return (float)0; } +float UE_IsOrthographic() { return (float)0; } +float2 UE_LightmapUVs() { return (float2)0; } +float3 UE_LightVector() { return (float3)0; } +float3 UE_LocalPosition() { return (float3)0; } +float3 UE_MainDirectionalLightDirection() { return (float3)0; } +float3 UE_MainDirectionalLightIlluminance() { return (float3)0; } +float3 UE_ObjectBounds() { return (float3)0; } +float3 UE_ObjectLocalBounds() { return (float3)0; } +float3 UE_ObjectOrientation() { return (float3)0; } +float3 UE_ObjectPositionWS() { return (float3)0; } +float UE_ObjectRadius() { return (float)0; } +float4 UE_ParticleColor() { return (float4)0; } +float3 UE_ParticleDirection() { return (float3)0; } +float2 UE_ParticleMacroUV() { return (float2)0; } +float UE_ParticleMotionBlurFade() { return (float)0; } +float3 UE_ParticlePositionWS() { return (float3)0; } +float UE_ParticleRadius() { return (float)0; } +float UE_ParticleRandom() { return (float)0; } +float UE_ParticleRelativeTime() { return (float)0; } +float2 UE_ParticleSize() { return (float2)0; } +float UE_ParticleSpeed() { return (float)0; } +float UE_ParticleSpriteRotation() { return (float)0; } +float UE_ParticleSubUVBlend() { return (float)0; } +float2 UE_ParticleSubUVTexCoord0() { return (float2)0; } +float2 UE_ParticleSubUVTexCoord1() { return (float2)0; } +float UE_PerInstanceCustomData(int DataIndex, float ConstDefaultValue = 0) { return (float)0; } +float UE_PerInstanceFadeAmount() { return (float)0; } +float UE_PerInstanceRandom() { return (float)0; } +float UE_PixelDepth() { return (float)0; } +float3 UE_PixelNormalWS() { return (float3)0; } +float UE_PrecomputedAOMask() { return (float)0; } +float3 UE_PreSkinnedLocalBounds() { return (float3)0; } +float3 UE_PreSkinnedNormal() { return (float3)0; } +float3 UE_PreSkinnedPosition() { return (float3)0; } +float3 UE_ReflectionVectorWS() { return (float3)0; } +float2 UE_SceneTexelSize() { return (float2)0; } +float2 UE_ScreenPosition() { return (float2)0; } +float3 UE_SkyAtmosphereLightDirection(int LightIndex = 0) { return (float3)0; } +float UE_SphericalParticleOpacity() { return (float)0; } +// UV channel `Index`, optionally tiled. +float2 UE_TextureCoordinate(int Index, float UTiling = 1, float VTiling = 1) { return (float2)0; } +float UE_Time() { return (float)0; } +float UE_TwoSidedSign() { return (float)0; } +float4 UE_VertexColor() { return (float4)0; } +float3 UE_VertexNormalWS() { return (float3)0; } +float3 UE_VertexTangentWS() { return (float3)0; } +// Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView). +float4 UE_ViewProperty(EMaterialExposedViewProperty Property) { return (float4)0; } +float2 UE_ViewSize() { return (float2)0; } +float3 UE_WorldPosition() { return (float3)0; } diff --git a/Shaders/Private/UEFunctions/Noise.ush b/Shaders/Private/UEFunctions/Noise.ush new file mode 100644 index 0000000..8f860f9 --- /dev/null +++ b/Shaders/Private/UEFunctions/Noise.ush @@ -0,0 +1,73 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// UE_ noise functions — forward to the engine's MaterialExpressionNoise / MaterialExpressionVectorNoise. +// +// Enum scheme B: in the editor (SHADERLAB_IDE, defined only by shader-validator) the function selectors +// are real HLSL `enum`s for best completion/semantics; at compile they fall back to `int` + `#define` +// (portable — the engine's own shaders avoid `enum` for cross-compiler safety). The engine functions we +// forward to are invisible to the standalone IDE parser, so we also stub them under SHADERLAB_IDE; the +// real shader compiler never defines SHADERLAB_IDE, so it uses the true engine functions instead. + +#pragma once + +#ifdef SHADERLAB_IDE + enum ENoiseFunction + { + NOISEFUNCTION_SimplexTex, + NOISEFUNCTION_GradientTex, + NOISEFUNCTION_GradientTex3D, + NOISEFUNCTION_GradientALU, + NOISEFUNCTION_ValueALU, + NOISEFUNCTION_VoronoiALU, + }; + enum EVectorNoiseFunction + { + VNF_CellnoiseALU, + VNF_VectorALU, + VNF_GradientALU, + VNF_CurlALU, + VNF_VoronoiALU, + }; + float MaterialExpressionNoise(float3 Position, float Scale, int Quality, int Function, bool bTurbulence, uint Levels, float OutputMin, float OutputMax, float LevelScale, float FilterWidth, bool bTiling, float RepeatSize) { return 0; } + float4 MaterialExpressionVectorNoise(float3 Position, int Quality, int Function, bool bTiling, float TileSize) { return (float4)0; } +#else + #define ENoiseFunction int + #define EVectorNoiseFunction int + #define NOISEFUNCTION_SimplexTex 0 + #define NOISEFUNCTION_GradientTex 1 + #define NOISEFUNCTION_GradientTex3D 2 + #define NOISEFUNCTION_GradientALU 3 + #define NOISEFUNCTION_ValueALU 4 + #define NOISEFUNCTION_VoronoiALU 5 + #define VNF_CellnoiseALU 0 + #define VNF_VectorALU 1 + #define VNF_GradientALU 2 + #define VNF_CurlALU 3 + #define VNF_VoronoiALU 4 +#endif + +// Full-control scalar noise — forwards directly to the engine implementation. +float UE_NoiseAdvanced(float3 Position, float Scale, ENoiseFunction Function, int Quality, bool bTurbulence, + uint Levels, float OutputMin, float OutputMax, float LevelScale, float FilterWidth, bool bTiling, float RepeatSize) +{ + return MaterialExpressionNoise(Position, Scale, Quality, Function, bTurbulence, Levels, + OutputMin, OutputMax, LevelScale, FilterWidth, bTiling, RepeatSize); +} + +// Simple scalar noise with the material Noise node's defaults (Simplex, turbulent, 6 octaves, [-1,1]). +float UE_Noise(float3 Position, float Scale) +{ + return UE_NoiseAdvanced(Position, Scale, NOISEFUNCTION_SimplexTex, 1, true, 6, -1.0, 1.0, 2.0, 0.0, false, 512.0); +} + +// Full-control vector noise — forwards directly to the engine implementation. +float4 UE_VectorNoiseAdvanced(float3 Position, EVectorNoiseFunction Function, int Quality, bool bTiling, float TileSize) +{ + return MaterialExpressionVectorNoise(Position, Quality, Function, bTiling, TileSize); +} + +// Simple vector noise with the material VectorNoise node's defaults (Cellnoise). +float4 UE_VectorNoise(float3 Position) +{ + return UE_VectorNoiseAdvanced(Position, VNF_CellnoiseALU, 1, false, 300.0); +} diff --git a/Shaders/Private/UEFunctions/Transform.ush b/Shaders/Private/UEFunctions/Transform.ush new file mode 100644 index 0000000..6bf049f --- /dev/null +++ b/Shaders/Private/UEFunctions/Transform.ush @@ -0,0 +1,17 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// UE_ transform functions — forward to the engine's material HLSL. The engine function is invisible to +// the standalone IDE parser, so it is stubbed under SHADERLAB_IDE (defined only by shader-validator); +// the real shader compiler uses the true engine function. + +#pragma once + +#ifdef SHADERLAB_IDE + float3 RotateAboutAxis(float4 NormalizedRotationAxisAndAngle, float3 PositionOnAxis, float3 Position) { return (float3)0; } +#endif + +// Rotate `Position` about the axis `NormalizedAxis` through `PivotPoint` by `Angle` radians. +float3 UE_RotateAboutAxis(float3 NormalizedAxis, float3 PivotPoint, float Angle, float3 Position) +{ + return RotateAboutAxis(float4(NormalizedAxis, Angle), PivotPoint, Position); +} diff --git a/Shaders/ShaderLab.ush b/Shaders/ShaderLab.ush index 1fd4b06..aba9fb0 100644 --- a/Shaders/ShaderLab.ush +++ b/Shaders/ShaderLab.ush @@ -1,44 +1,16 @@ -// 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. +// Copyright FlecsProj. 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 +// bodies reach the shader compiler). It just pulls in the pieces the IDE needs to resolve a .usl: +// - ShaderLabDSL : the SL_* annotation macros +// - ShaderLabCommon : the FShaderLabSurface/etc. structs +// - ShaderLabUENode : the UE_ node-intrinsic stubs + their enums (generated by ShaderLab.IDE.Prepare) +// - ShaderLabUEFunctions : the UE_ HLSL library functions (also auto-included at compile) + #pragma once -#include "/Plugin/ShaderLab/ShaderLabDSL.ush" +#include "/Plugin/ShaderLab/Private/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; } -} +#include "/Plugin/ShaderLab/Private/ShaderLabUENode.ush" +#include "/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush" diff --git a/Source/UShaderLab/Private/ShaderLabParser.cpp b/Source/UShaderLab/Private/ShaderLabParser.cpp index 9674b07..462f8a9 100644 --- a/Source/UShaderLab/Private/ShaderLabParser.cpp +++ b/Source/UShaderLab/Private/ShaderLabParser.cpp @@ -9,10 +9,10 @@ // /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. +// `SL_FRONTMATERIAL/OPACITY/OPACITY_MASK` — into an FShaderLabModel. +// Only entry-function bodies reach the shader compiler (the graph builder consumes them and rewrites +// UE_ calls into expression-node inputs); the authoring-shim `#include`, the macros and the property +// globals exist purely for the editor and are dropped here. namespace ShaderLabParser_Private { @@ -273,7 +273,10 @@ namespace ShaderLabParser_Private } } - /** Read verbatim up to a top-level ';' (respecting parens); does not consume the ';'. */ + /** + * Read verbatim up to a top-level ';' (paren depth 0), skipping over string/char literals and + * comments so a ';' inside them does not terminate early. Does not consume the ';'. + */ bool ReadUntilSemicolon(FString& Out) { FString T; @@ -281,6 +284,32 @@ namespace ShaderLabParser_Private while (!IsEnd()) { const TCHAR C = Peek(); + if (C == TEXT('"') || C == TEXT('\'')) + { + const TCHAR Quote = C; + T.AppendChar(Advance()); + while (!IsEnd() && Peek() != Quote) + { + const TCHAR S = Advance(); + T.AppendChar(S); + if (S == TEXT('\\') && !IsEnd()) { T.AppendChar(Advance()); } + } + if (!IsEnd()) { T.AppendChar(Advance()); } + continue; + } + if (C == TEXT('/') && Peek(1) == TEXT('/')) + { + while (!IsEnd() && Peek() != TEXT('\n')) { T.AppendChar(Advance()); } + continue; + } + if (C == TEXT('/') && Peek(1) == TEXT('*')) + { + T.AppendChar(Advance()); + T.AppendChar(Advance()); + while (!IsEnd() && !(Peek() == TEXT('*') && Peek(1) == TEXT('/'))) { T.AppendChar(Advance()); } + if (!IsEnd()) { T.AppendChar(Advance()); T.AppendChar(Advance()); } + continue; + } if (C == TEXT(';') && Paren == 0) { break; @@ -450,7 +479,9 @@ namespace ShaderLabParser_Private const FString Leaf = FPaths::GetCleanFilename(IncludePath); return Leaf == TEXT("ShaderLab.ush") || Leaf == TEXT("ShaderLabDSL.ush") - || Leaf == TEXT("ShaderLabCommon.ush"); + || Leaf == TEXT("ShaderLabCommon.ush") + || Leaf == TEXT("ShaderLabUENode.ush") + || Leaf == TEXT("ShaderLabUEFunctions.ush"); } } @@ -540,8 +571,8 @@ namespace } } - /** Apply one `Key = Value` SL_PROPERTY specifier. */ - void ApplyPropertySpecifier(FScanner& S, const FString& ArgRaw, FShaderLabProperty& Prop) + /** Apply one `Key = Value` SL_PROPERTY specifier. Diagnostics are reported at (ErrLine, ErrCol). */ + void ApplyPropertySpecifier(FScanner& S, const FString& ArgRaw, FShaderLabProperty& Prop, int32 ErrLine, int32 ErrCol) { const FString Arg = ArgRaw.TrimStartAndEnd(); if (Arg.IsEmpty()) @@ -552,7 +583,7 @@ namespace if (!Arg.Split(TEXT("="), &Key, &Value)) { S.Error(FString::Printf(TEXT("Property '%s' has malformed specifier '%s' (expected Key = Value)"), - *Prop.Name.ToString(), *Arg)); + *Prop.Name.ToString(), *Arg), ErrLine, ErrCol); return; } Key = Key.TrimStartAndEnd(); @@ -562,7 +593,7 @@ namespace { Prop.DisplayName = Value.TrimQuotes(); } - else if (Key == TEXT("Group")) + else if (Key == TEXT("Category")) { Prop.Group = Value.TrimQuotes(); } @@ -576,11 +607,17 @@ namespace } else if (Key == TEXT("Range")) { - const TArray Comps = SplitTopLevel( - Value.Replace(TEXT("("), TEXT("")).Replace(TEXT(")"), TEXT("")), TEXT(',')); + // Trim exactly the one wrapping (...) then split — preserves paren-awareness of the interior. + if (!Value.StartsWith(TEXT("(")) || !Value.EndsWith(TEXT(")"))) + { + S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString()), ErrLine, ErrCol); + return; + } + const FString Inner = Value.Mid(1, Value.Len() - 2); + const TArray Comps = SplitTopLevel(Inner, TEXT(',')); if (Comps.Num() != 2) { - S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString())); + S.Error(FString::Printf(TEXT("Property '%s' Range expects (min,max)"), *Prop.Name.ToString()), ErrLine, ErrCol); return; } Prop.bHasRange = true; @@ -589,13 +626,16 @@ namespace } else { - S.Error(FString::Printf(TEXT("Property '%s' has unknown specifier '%s'"), *Prop.Name.ToString(), *Key)); + S.Error(FString::Printf(TEXT("Property '%s' has unknown specifier '%s'"), *Prop.Name.ToString(), *Key), ErrLine, ErrCol); } } void ParseProperty(FScanner& S, FShaderLabModel& Model) { - // SL_PROPERTY( ) + // SL_PROPERTY( ) — record the '(' position so specifier errors point at the macro. + S.SkipTrivia(); + const int32 SpecLine = S.Line; + const int32 SpecCol = S.Column; FString SpecInner; if (!S.ReadBalanced(TEXT('('), TEXT(')'), SpecInner)) { @@ -604,6 +644,8 @@ namespace FShaderLabProperty Prop; Prop.SortPriority = Model.Properties.Num(); + int32 NameLine = SpecLine; + int32 NameCol = SpecCol; // The HLSL declaration following the macro determines name + type + default. S.SkipTrivia(); @@ -623,13 +665,20 @@ namespace S.Error(TEXT("StaticBool `#define` is missing a name")); return; } - // Remainder of the line is the value (1/0/true/false). + NameLine = S.Line; + NameCol = S.Column; + // Remainder of the line is the value (1/0/true/false), minus any trailing comment. FString ValueLine; while (!S.IsEnd() && S.Peek() != TEXT('\n')) { ValueLine.AppendChar(S.Advance()); } - ValueLine = ValueLine.TrimStartAndEnd(); + int32 Cut = ValueLine.Len(); + const int32 LineComment = ValueLine.Find(TEXT("//")); + const int32 BlockComment = ValueLine.Find(TEXT("/*")); + if (LineComment != INDEX_NONE) { Cut = FMath::Min(Cut, LineComment); } + if (BlockComment != INDEX_NONE) { Cut = FMath::Min(Cut, BlockComment); } + ValueLine = ValueLine.Left(Cut).TrimStartAndEnd(); Prop.Name = FName(*Name); Prop.Type = EShaderLabPropertyType::StaticBool; if (!ParseBool(ValueLine, Prop.bStaticBoolDefault)) @@ -657,6 +706,8 @@ namespace S.Error(FString::Printf(TEXT("Property of type '%s' is missing a name"), *TypeTok)); return; } + NameLine = S.Line; + NameCol = S.Column; Prop.Name = FName(*Name); const bool bIsTexture = (Prop.Type == EShaderLabPropertyType::Texture2D || Prop.Type == EShaderLabPropertyType::TextureCube); @@ -717,7 +768,7 @@ namespace // Specifiers (override SortPriority/DisplayName/etc.). for (const FString& Arg : SplitTopLevel(SpecInner, TEXT(','))) { - ApplyPropertySpecifier(S, Arg, Prop); + ApplyPropertySpecifier(S, Arg, Prop, SpecLine, SpecCol); if (S.bFailed) { return; @@ -729,7 +780,7 @@ namespace } if (Model.FindProperty(Prop.Name) != nullptr) { - S.Error(FString::Printf(TEXT("Duplicate property name '%s'"), *Prop.Name.ToString())); + S.Error(FString::Printf(TEXT("Duplicate property name '%s'"), *Prop.Name.ToString()), NameLine, NameCol); return; } Model.Properties.Add(MoveTemp(Prop)); @@ -802,6 +853,8 @@ namespace { return; } + S.SkipTrivia(); + const int32 DeclLine = S.Line, DeclCol = S.Column; FString Name, SigInner, Body; int32 BodyLine = 0; if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine)) @@ -811,11 +864,17 @@ namespace TArray Params; if (!ParseEntryParams(SigInner, Params) || Params.Num() < 1) { - S.Error(FString::Printf(TEXT("Slab '%s' must take an (inout FShaderLabSurface) parameter"), *Name)); + S.Error(FString::Printf(TEXT("Slab '%s' must take an (inout FShaderLabSurface) parameter"), *Name), DeclLine, DeclCol); + return; + } + const FName SlabName(*Name); + if (Model.Slabs.ContainsByPredicate([&SlabName](const FShaderLabSlab& E) { return E.Name == SlabName; })) + { + S.Error(FString::Printf(TEXT("Duplicate slab name '%s'"), *Name), DeclLine, DeclCol); return; } FShaderLabSlab Slab; - Slab.Name = FName(*Name); + Slab.Name = SlabName; Slab.OutParamName = Params.Last().Name; Slab.Body = MoveTemp(Body); Slab.BodyLine = BodyLine; @@ -829,14 +888,22 @@ namespace { return; } + S.SkipTrivia(); + const int32 DeclLine = S.Line, DeclCol = S.Column; FString Name, SigInner, Body; int32 BodyLine = 0; if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine)) { return; } + const FName ValueName(*Name); + if (Model.Values.ContainsByPredicate([&ValueName](const FShaderLabValue& E) { return E.Name == ValueName; })) + { + S.Error(FString::Printf(TEXT("Duplicate value name '%s'"), *Name), DeclLine, DeclCol); + return; + } FShaderLabValue Value; - Value.Name = FName(*Name); + Value.Name = ValueName; Value.Body = MoveTemp(Body); Value.BodyLine = BodyLine; Model.Values.Add(MoveTemp(Value)); @@ -927,24 +994,30 @@ namespace Node.bHasFactor = true; if (!ParseTopoFactor(S, Node.Factor)) { return INDEX_NONE; } } + S.SkipTrivia(); + if (S.Peek() == TEXT(',')) + { + S.Error(FString::Printf(TEXT("Topology operator '%s' has too many arguments"), *Name)); + return INDEX_NONE; + } if (!S.Expect(TEXT(')'), TEXT("to close topology operator"))) { return INDEX_NONE; } return Model.Topology.Add(Node); } void ParseFrontMaterial(FScanner& S, FShaderLabModel& Model) { - // SL_FRONTMATERIAL( ) - FString Inner; - if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner)) + // SL_FRONTMATERIAL( ) — parsed in place on the main scanner so topology + // diagnostics report the real .usl line/column. + if (!S.Expect(TEXT('('), TEXT("after SL_FRONTMATERIAL"))) { return; } - FScanner Sub(Inner, S.Errors); - Model.TopologyRoot = ParseTopoExpr(Sub, Model); - if (Sub.bFailed) + Model.TopologyRoot = ParseTopoExpr(S, Model); + if (S.bFailed) { - S.bFailed = true; + return; } + S.Expect(TEXT(')'), TEXT("to close SL_FRONTMATERIAL")); } /** SL_OPACITY(Name) / SL_OPACITY_MASK(Name): material-level outputs referencing a Value block. */ diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp index 8cef48b..15f0f56 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp @@ -21,10 +21,12 @@ #include "MaterialExpressionShaderLabParameterAnchor.h" #include "ShaderLabIntrinsicRegistry.h" #include "ShaderLabRuntimeBuilder.h" +#include "UObject/Class.h" #include "ShaderLabSettingsApplier.h" #include "UObject/UObjectGlobals.h" #define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush") +#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush") namespace ShaderLabGraph { @@ -238,16 +240,38 @@ namespace ShaderLabGraph } /** - * Scan a body for `UE::Name(args)` intrinsic calls, create the backing expression node for each + * Scan a body for `UE_Name(args)` intrinsic calls, create the backing expression node for each * unique (name,args), collect the resulting Custom-node inputs, and rewrite the body so each call * becomes its input variable — space-padded to the original call's length so line/column layout is * preserved (keeps `#line` compile-error mapping accurate). Returns false (and fills OutErrors) on * an unknown intrinsic, a stage/usage violation, or a bad argument. */ + /** True if `Arg` is a numeric literal (the only non-enum arg an intrinsic's const config accepts). */ + static bool IsNumericLiteral(const FString& Arg) + { + const FString T = Arg.TrimStartAndEnd(); + if (T.IsEmpty()) + { + return false; + } + bool bAnyDigit = false; + for (int32 i = 0; i < T.Len(); ++i) + { + const TCHAR C = T[i]; + if (FChar::IsDigit(C)) { bAnyDigit = true; } + else if (C == TEXT('.') || C == TEXT('+') || C == TEXT('-') + || C == TEXT('e') || C == TEXT('E') || C == TEXT('f') || C == TEXT('F')) { /* allowed */ } + else { return false; } + } + return bAnyDigit; + } + static bool EmitIntrinsics( UMaterial& Material, EShaderLabIntrinsicFrequency Stage, FString& InOutBody, + int32 BodyLine, + const FString& SrcPath, TArray& OutWires, TArray& OutErrors) { @@ -262,16 +286,30 @@ namespace ShaderLabGraph auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); }; + // Format a `path.usl(line,col): error: ` prefix from a body offset, so intrinsic diagnostics map + // to the real .usl location and flow through the same clickable/cook-failing path as compile errors. + auto Loc = [&Body, BodyLine, &SrcPath](int32 Offset) -> FString + { + int32 Line = BodyLine; + int32 Col = 1; + for (int32 p = 0; p < Offset && p < Body.Len(); ++p) + { + if (Body[p] == TEXT('\n')) { ++Line; Col = 1; } + else { ++Col; } + } + return FString::Printf(TEXT("%s(%d,%d): error: "), *SrcPath, Line, Col); + }; + int32 i = 0; while (i < Len) { const bool bBoundary = (i == 0) || !IsIdent(Body[i - 1]); - if (bBoundary && i + 4 <= Len && - Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT(':') && Body[i + 3] == TEXT(':')) + if (bBoundary && i + 3 <= Len && + Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT('_')) { - int32 j = i + 4; + int32 j = i + 3; while (j < Len && IsIdent(Body[j])) { ++j; } - const FString Name = Body.Mid(i + 4, j - (i + 4)); + const FString Name = Body.Mid(i + 3, j - (i + 3)); int32 k = j; while (k < Len && FChar::IsWhitespace(Body[k])) { ++k; } if (!Name.IsEmpty() && k < Len && Body[k] == TEXT('(')) @@ -288,6 +326,20 @@ namespace ShaderLabGraph { const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1)); const int32 CallLen = (m + 1) - i; + + // 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. + // Copy just the `UE_Name` identifier and keep scanning from the '(' — so any + // intrinsic nested in the arguments (e.g. UE_Noise(UE_WorldPosition(), ...)) still + // gets rewritten. + if (!Registry.Find(FName(*Name))) + { + Result += Body.Mid(i, j - i); + i = j; + continue; + } + const FString ArgSig = MakeArgSig(ArgsRaw); const FString Key = Name + TEXT("|") + ArgSig; @@ -299,29 +351,59 @@ namespace ShaderLabGraph else { const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name)); - if (!Desc) + if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly) { - OutErrors.Add(FString::Printf(TEXT("Unknown intrinsic 'UE::%s'"), *Name)); - bOk = false; - } - else if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly) - { - OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s' is pixel-only and cannot be used in a Vertex body"), *Name)); + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is pixel-only and cannot be used in a Vertex body"), *Name)); bOk = false; } else if (Desc->Frequency == EShaderLabIntrinsicFrequency::VertexOnly && Stage == EShaderLabIntrinsicFrequency::PixelOnly) { - OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s' is vertex-only and cannot be used in a pixel body"), *Name)); + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is vertex-only and cannot be used in a pixel body"), *Name)); bOk = false; } else { - // Usage is per-instance, so intrinsics emit their node with no base-level usage gate. - FString MakeError; - UMaterialExpression* Expr = Desc->MakeNode(Material, SplitArgs(ArgsRaw), MakeError); - if (!Expr) + // Config args are baked into the node at graph-build time, so they must be + // compile-time constants (numeric literals, or the enum's token names) — a + // variable can't configure a node field. Reject non-literals instead of + // silently coercing them (e.g. Atoi("myVar") -> 0). + const TArray CallArgs = SplitArgs(ArgsRaw); + bool bArgsOk = true; + if (CallArgs.Num() > Desc->Params.Num()) { - OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s': %s"), *Name, + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' takes at most %d argument(s), got %d"), + *Name, Desc->Params.Num(), CallArgs.Num())); + bArgsOk = false; + } + for (int32 a = 0; bArgsOk && a < CallArgs.Num(); ++a) + { + const FShaderLabIntrinsicParam& P = Desc->Params[a]; + if (P.Enum) + { + if (P.Enum->GetValueByNameString(CallArgs[a]) == INDEX_NONE) + { + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a %s token, got '%s'"), + *Name, *P.Name, *P.Enum->GetName(), *CallArgs[a])); + bArgsOk = false; + } + } + else if (!IsNumericLiteral(CallArgs[a])) + { + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a compile-time constant, got '%s'"), + *Name, *P.Name, *CallArgs[a])); + bArgsOk = false; + } + } + + FString MakeError; + UMaterialExpression* Expr = bArgsOk ? Desc->MakeNode(Material, CallArgs, MakeError) : nullptr; + if (!bArgsOk) + { + bOk = false; + } + else if (!Expr) + { + OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s': %s"), *Name, MakeError.IsEmpty() ? TEXT("failed to create node") : *MakeError)); bOk = false; } @@ -358,10 +440,13 @@ namespace ShaderLabGraph bool bIsTexture = false; }; - /** Add the shared struct include plus any user `Includes { }` paths to a generated Custom node. */ + /** Add the shared struct + function-library includes plus 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); for (const FString& Include : Model.Includes) { if (!Include.IsEmpty()) @@ -407,7 +492,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray Wires; - if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors)) + if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors)) { return nullptr; } @@ -499,7 +584,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray Wires; - if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors)) + if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors)) { return false; } @@ -572,7 +657,7 @@ namespace ShaderLabGraph FString Body = Value.Body; TArray Wires; - if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors)) + if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors)) { return nullptr; } @@ -739,7 +824,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode return false; } - // Per-pixel context is read via UE::* intrinsics, so the Surface entry takes just the output + // 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 ? &Model.SurfaceParams.Last() : nullptr; @@ -986,7 +1071,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode if (!ConnectMaterialOutput(EditorOnly->OpacityMask, Model.OpacityMaskValueName, TEXT("OpacityMask"))) { return false; } } - // 3) Optional Vertex stage. Per-pixel/vertex context is read via UE::* intrinsics, so the entry + // 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry // takes just the output struct: `Vertex(inout FShaderLabVertex V)`. if (Model.bHasVertex && Model.VertexParams.Num() >= 1) { @@ -1013,7 +1098,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode // Intrinsics (Stage = vertex). FString VertexBody = Model.VertexBody; TArray VtxIntrinsicWires; - if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, VtxIntrinsicWires, OutErrors)) + if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, OutErrors)) { return false; } diff --git a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp b/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp index 353fad0..07b883c 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIDEPrepare.cpp @@ -2,19 +2,20 @@ // // `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. /Shaders/ShaderLab.ush — umbrella header: DSL macros + structs + `namespace UE` -// intrinsic stubs generated from FShaderLabIntrinsicRegistry. -// 2. /shadertoolsconfig.json — HLSL Tools virtual-directory mappings, dumped from -// AllShaderSourceDirectoryMappings() (the same table the -// runtime registers). Machine-specific → gitignored. -// 3. /.vscode/settings.json — merges *.usl/*.usf/*.ush -> hlsl into files.associations -// (non-destructive: existing keys are preserved). +// from Rider's UE shader support or VSCode + the shader-validator extension. It writes: +// 1. /Shaders/Private/ShaderLabUENode.ush — the UE_ node-intrinsic stubs (+ their enums) +// generated from FShaderLabIntrinsicRegistry. (The static +// ShaderLab.ush umbrella includes it; it is IDE-only.) +// 2. /.vscode/settings.json — merges (non-destructively) files.associations +// (*.usl/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping +// (virtual shader roots -> disk, from AllShaderSourceDirectoryMappings). +// pathRemapping is machine-specific; the command regenerates it. #include "CoreMinimal.h" #include "Dom/JsonObject.h" #include "HAL/IConsoleManager.h" +#include "UObject/Class.h" #include "Interfaces/IPluginManager.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" @@ -104,8 +105,12 @@ namespace ShaderLabIDEPrepare_Private return Out; } - /** Build the `namespace UE { ... }` block of stub declarations from the registry (sorted, deterministic). */ - static FString GenerateIntrinsicNamespace() + /** + * Build the `UE_Name(...)` stub declarations from the registry (sorted, deterministic). They are + * free functions (not a `namespace UE`) because shader-validator's HLSL parser has no namespace + * support; the intrinsics are written `UE_Name(...)` in .usl and matched by that prefix. + */ + static FString GenerateIntrinsicStubs() { TArray Descs; FShaderLabIntrinsicRegistry::Get().ForEach([&Descs](const FShaderLabIntrinsicDesc& D) { Descs.Add(D); }); @@ -115,27 +120,61 @@ namespace ShaderLabIDEPrepare_Private }); FString Body; - Body += TEXT("namespace UE\n{\n"); + + // Emit each reflected enum used by an intrinsic arg once, as a real HLSL enum, so the param is + // typed and its tokens complete (`UE_ViewProperty(MEVP_FieldOfView)`). The graph builder resolves + // the token by name, so the enum's numeric values are irrelevant. + { + TSet EmittedEnums; + FString Enums; + for (const FShaderLabIntrinsicDesc& D : Descs) + { + for (const FShaderLabIntrinsicParam& P : D.Params) + { + if (!P.Enum || EmittedEnums.Contains(P.Enum->GetName())) + { + continue; + } + EmittedEnums.Add(P.Enum->GetName()); + Enums += FString::Printf(TEXT("enum %s\n{\n"), *P.Enum->GetName()); + for (int32 Index = 0; Index < P.Enum->NumEnums(); ++Index) + { + const FString Token = P.Enum->GetNameStringByIndex(Index); + if (!Token.IsEmpty() && !Token.EndsWith(TEXT("_MAX"))) + { + Enums += FString::Printf(TEXT("\t%s,\n"), *Token); + } + } + Enums += TEXT("};\n"); + } + } + if (!Enums.IsEmpty()) + { + Body += Enums; + Body += TEXT("\n"); + } + } + for (const FShaderLabIntrinsicDesc& D : Descs) { if (!D.Doc.IsEmpty()) { - Body += FString::Printf(TEXT("\t// %s\n"), *D.Doc); + Body += FString::Printf(TEXT("// %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); + const FString ParamType = P.Enum ? P.Enum->GetName() : P.Type; + Params += FString::Printf(TEXT("%s %s"), *ParamType, *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"), + // (the graph builder rewrites UE_ calls into material-expression inputs before compile). + Body += FString::Printf(TEXT("%s UE_%s(%s) { return (%s)0; }\n"), *D.ReturnType, *D.Name.ToString(), *Params, *D.ReturnType); } - Body += TEXT("}\n"); return Body; } @@ -158,41 +197,41 @@ namespace ShaderLabIDEPrepare_Private return true; } - static void GenerateShaderLabHeader(const FString& PluginShaderDir) + // Generates Private/ShaderLabUENode.ush: the UE_ node-intrinsic stubs (+ their enums), from the + // registry. Included by the static ShaderLab.ush umbrella; editor-only (the parser strips the shim). + static void GenerateUENodeHeader(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("// UE_ node-intrinsic stubs + their enums, for IDE completion only (never compiled).\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); + H += GenerateIntrinsicStubs(); + WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("Private"), TEXT("ShaderLabUENode.ush")), H); } - static void GenerateShaderToolsConfig(const FString& ProjectDir) + static FString SerializeObject(const TSharedRef& Obj) { - const TMap& Mappings = AllShaderSourceDirectoryMappings(); - - const TSharedRef Root = MakeShared(); - Root->SetBoolField(TEXT("root"), true); - const TSharedRef Virt = MakeShared(); - for (const TPair& 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> Writer = TJsonWriterFactory<>::Create(&Out); - FJsonSerializer::Serialize(Root, Writer); - WriteIfChanged(FPaths::Combine(ProjectDir, TEXT("shadertoolsconfig.json")), Out); + FJsonSerializer::Serialize(Obj, Writer); + return Out; } + // Virtual shader roots -> absolute disk paths, for shader-validator.pathRemapping. + static TSharedRef BuildPathRemapping() + { + const TSharedRef Remap = MakeShared(); + for (const TPair& Pair : AllShaderSourceDirectoryMappings()) + { + // Engine mappings come back relative to the engine binaries dir; the extension needs + // absolute paths, so normalize. + Remap->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value)); + } + return Remap; + } + + // Merge files.associations (*.usl/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping into + // .vscode/settings.json, preserving all other keys. Rewrites only when something actually changed. static void MergeVSCodeSettings(const FString& ProjectDir) { const FString SettingsPath = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("settings.json")); @@ -205,7 +244,7 @@ namespace ShaderLabIDEPrepare_Private 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."), + TEXT(" could not parse %s — add files.associations and shader-validator.pathRemapping manually."), *SettingsPath); return; // Never clobber an unparseable user file. } @@ -215,51 +254,53 @@ namespace ShaderLabIDEPrepare_Private Settings = MakeShared(); } - // 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* AssocPtr = nullptr; - TSharedPtr Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr) - ? *AssocPtr : MakeShared(); bool bChanged = false; + + // files.associations: union in the ShaderLab extensions. + const TSharedPtr* AssocPtr = nullptr; + const TSharedPtr Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr) + ? *AssocPtr : MakeShared(); for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.usf"), TEXT("*.ush") }) { - FString Existing2; - if (!Assoc->TryGetStringField(Ext, Existing2) || Existing2 != TEXT("hlsl")) + FString Value; + if (!Assoc->TryGetStringField(Ext, Value) || Value != TEXT("hlsl")) { Assoc->SetStringField(Ext, TEXT("hlsl")); bChanged = true; } } + Settings->SetObjectField(TEXT("files.associations"), Assoc); + + // shader-validator.pathRemapping: replace with the current (machine-specific) mapping table. + const TSharedRef Remap = BuildPathRemapping(); + const TSharedPtr* ExistingRemapPtr = nullptr; + const bool bHasRemap = Settings->TryGetObjectField(TEXT("shader-validator.pathRemapping"), ExistingRemapPtr); + if (!bHasRemap || SerializeObject(ExistingRemapPtr->ToSharedRef()) != SerializeObject(Remap)) + { + Settings->SetObjectField(TEXT("shader-validator.pathRemapping"), Remap); + bChanged = true; + } + + // shader-validator.defines: SHADERLAB_IDE gates the editor-only engine-function stubs in + // ShaderLabFunctions.ush (so its UE_ forwarders resolve in the IDE without dragging in engine + // headers). The real shader compiler never sees this define. + const TSharedPtr* DefinesPtr = nullptr; + const TSharedPtr Defines = Settings->TryGetObjectField(TEXT("shader-validator.defines"), DefinesPtr) + ? *DefinesPtr : MakeShared(); + FString DefineValue; + if (!Defines->TryGetStringField(TEXT("SHADERLAB_IDE"), DefineValue) || DefineValue != TEXT("1")) + { + Defines->SetStringField(TEXT("SHADERLAB_IDE"), TEXT("1")); + Settings->SetObjectField(TEXT("shader-validator.defines"), Defines); + bChanged = true; + } + if (!bChanged) { UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath); return; } - Settings->SetObjectField(TEXT("files.associations"), Assoc); - - FString Out; - const TSharedRef> 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); + WriteIfChanged(SettingsPath, SerializeObject(Settings.ToSharedRef())); } static void Run(const TArray& /*Args*/) @@ -271,10 +312,8 @@ namespace ShaderLabIDEPrepare_Private const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare:")); - GenerateShaderLabHeader(PluginShaderDir); - GenerateShaderToolsConfig(ProjectDir); + GenerateUENodeHeader(PluginShaderDir); MergeVSCodeSettings(ProjectDir); - EnsureGitIgnore(ProjectDir); UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare: done.")); } } diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp index d1530e4..9daa800 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsicRegistry.cpp @@ -2,50 +2,23 @@ #include "ShaderLabIntrinsicRegistry.h" -#include "Materials/Material.h" -#include "UObject/UObjectGlobals.h" +#include "ShaderLabIntrinsics.h" -// Backing expression node types for the v1 builtin intrinsics. -#include "Materials/MaterialExpressionActorPositionWS.h" -#include "Materials/MaterialExpressionCameraPositionWS.h" -#include "Materials/MaterialExpressionCameraVectorWS.h" -#include "Materials/MaterialExpressionDeltaTime.h" -#include "Materials/MaterialExpressionLightVector.h" -#include "Materials/MaterialExpressionLightmapUVs.h" -#include "Materials/MaterialExpressionLocalPosition.h" -#include "Materials/MaterialExpressionObjectBounds.h" -#include "Materials/MaterialExpressionObjectLocalBounds.h" -#include "Materials/MaterialExpressionObjectOrientation.h" -#include "Materials/MaterialExpressionObjectPositionWS.h" -#include "Materials/MaterialExpressionObjectRadius.h" -#include "Materials/MaterialExpressionParticleColor.h" -#include "Materials/MaterialExpressionPerInstanceCustomData.h" -#include "Materials/MaterialExpressionPerInstanceFadeAmount.h" -#include "Materials/MaterialExpressionPerInstanceRandom.h" -#include "Materials/MaterialExpressionPixelDepth.h" -#include "Materials/MaterialExpressionPixelNormalWS.h" -#include "Materials/MaterialExpressionPreSkinnedLocalBounds.h" -#include "Materials/MaterialExpressionPreSkinnedNormal.h" -#include "Materials/MaterialExpressionPreSkinnedPosition.h" -#include "Materials/MaterialExpressionScreenPosition.h" -#include "Materials/MaterialExpressionTextureCoordinate.h" -#include "Materials/MaterialExpressionTime.h" -#include "Materials/MaterialExpressionTwoSidedSign.h" -#include "Materials/MaterialExpressionVertexColor.h" -#include "Materials/MaterialExpressionVertexNormalWS.h" -#include "Materials/MaterialExpressionVertexTangentWS.h" -#include "Materials/MaterialExpressionViewProperty.h" -#include "Materials/MaterialExpressionWorldPosition.h" - -namespace ShaderLabIntrinsic_Private +void ShaderLabIntrinsic_Private::AddSimple( + FShaderLabIntrinsicRegistry& Registry, + const TCHAR* Name, + EShaderLabIntrinsicFrequency Frequency, + const TCHAR* ReturnType, + TFunction Make, + int32 OutputIndex) { - template - UMaterialExpression* MakeSimple(UMaterial& Material) - { - T* Expr = NewObject(&Material); - Material.GetExpressionCollection().AddExpression(Expr); - return Expr; - } + FShaderLabIntrinsicDesc Desc; + Desc.Name = FName(Name); + Desc.Frequency = Frequency; + Desc.ReturnType = ReturnType; + Desc.OutputIndex = OutputIndex; + Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray&, FString&) { return Make(M); }; + Registry.Register(MoveTemp(Desc)); } FShaderLabIntrinsicRegistry& FShaderLabIntrinsicRegistry::Get() @@ -83,130 +56,12 @@ void FShaderLabIntrinsicRegistry::ForEach(TFunctionRef Make) - { - FShaderLabIntrinsicDesc Desc; - Desc.Name = FName(Name); - Desc.Frequency = Freq; - Desc.ReturnType = ReturnType; - Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray&, FString&) { return Make(M); }; - Register(MoveTemp(Desc)); - }; - - // --- A. Mesh / surface attributes --- - AddSimple(TEXT("VertexNormalWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("VertexTangentWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("VertexColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("TwoSidedSign"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("LightmapUVs"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(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& Args, FString&) -> UMaterialExpression* - { - UMaterialExpressionTextureCoordinate* E = NewObject(&M); - if (Args.Num() >= 1) { E->CoordinateIndex = FCString::Atoi(*Args[0]); } - if (Args.Num() >= 2) { E->UTiling = FCString::Atof(*Args[1]); } - if (Args.Num() >= 3) { E->VTiling = FCString::Atof(*Args[2]); } - M.GetExpressionCollection().AddExpression(E); - return E; - }; - Register(MoveTemp(Desc)); - } - - // --- B. Positions / spaces --- - AddSimple(TEXT("WorldPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("LocalPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ActorPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("CameraPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("CameraVectorWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("LightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); - - // --- C. Object / bounds (ObjectLocalBounds & PreSkinnedLocalBounds are multi-output; output 0) --- - AddSimple(TEXT("ObjectRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ObjectBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("ObjectOrientation"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); - - // --- D. Instancing --- - AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(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& Args, FString&) -> UMaterialExpression* - { - UMaterialExpressionPerInstanceCustomData* E = NewObject(&M); - if (Args.Num() >= 1) { E->DataIndex = static_cast(FCString::Atoi(*Args[0])); } - if (Args.Num() >= 2) { E->ConstDefaultValue = FCString::Atof(*Args[1]); } - M.GetExpressionCollection().AddExpression(E); - return E; - }; - Register(MoveTemp(Desc)); - } - - // --- Time --- - AddSimple(TEXT("Time"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); - AddSimple(TEXT("DeltaTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(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& Args, FString& OutError) -> UMaterialExpression* - { - UMaterialExpressionViewProperty* E = NewObject(&M); - if (Args.Num() >= 1) - { - UEnum* Enum = StaticEnum(); - check(Enum); - const int64 Value = Enum->GetValueByNameString(Args[0]); - if (Value == INDEX_NONE) - { - OutError = FString::Printf(TEXT("unknown view property '%s'"), *Args[0]); - return nullptr; - } - E->Property = static_cast(Value); - } - M.GetExpressionCollection().AddExpression(E); - return E; - }; - Register(MoveTemp(Desc)); - } - - // ParticleColor — only meaningful on particle vertex factories; the instance enables the matching - // usage, so there is no base-level usage gate here. - { - FShaderLabIntrinsicDesc Desc; - Desc.Name = FName(TEXT("ParticleColor")); - Desc.ReturnType = TEXT("float4"); - Desc.MakeNode = [](UMaterial& M, const TArray&, FString&) { return MakeSimple(M); }; - Register(MoveTemp(Desc)); - } + RegisterMeshIntrinsics(*this); + RegisterViewIntrinsics(*this); + RegisterObjectIntrinsics(*this); + RegisterInstancingIntrinsics(*this); + RegisterTimeIntrinsics(*this); + RegisterParticleIntrinsics(*this); + RegisterDecalIntrinsics(*this); + RegisterAtmosphereIntrinsics(*this); } diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h new file mode 100644 index 0000000..6c720e1 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics.h @@ -0,0 +1,46 @@ +// Copyright FlecsProj. 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 +// separate .cpp files for maintainability; each defines one RegisterXxxIntrinsics(registry) that the +// registry's RegisterBuiltins() calls. + +#pragma once + +#include "CoreMinimal.h" +#include "Materials/Material.h" +#include "ShaderLabIntrinsicRegistry.h" + +namespace ShaderLabIntrinsic_Private +{ + /** NewObject a no-config expression node and add it to the material's collection. */ + template + UMaterialExpression* MakeSimple(UMaterial& Material) + { + T* Expr = NewObject(&Material); + Material.GetExpressionCollection().AddExpression(Expr); + return Expr; + } + + /** + * Register a no-arg intrinsic that maps to a source node. `OutputIndex` selects which output pin to + * connect (non-zero for one facet of a multi-output node, e.g. a directional light's Direction). + */ + void AddSimple( + FShaderLabIntrinsicRegistry& Registry, + const TCHAR* Name, + EShaderLabIntrinsicFrequency Frequency, + const TCHAR* ReturnType, + TFunction Make, + int32 OutputIndex = 0); +} + +// Per-category registration (defined in ShaderLabIntrinsics_.cpp). +void RegisterMeshIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterViewIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterObjectIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterInstancingIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry); +void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry); diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp new file mode 100644 index 0000000..64b09f1 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Atmosphere.cpp @@ -0,0 +1,34 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Atmosphere / sky-light intrinsics. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionAtmosphericLightColor.h" +#include "Materials/MaterialExpressionAtmosphericLightVector.h" +#include "Materials/MaterialExpressionSkyAtmosphereLightDirection.h" + +void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("AtmosphericLightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("AtmosphericLightColor"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + + // SkyAtmosphereLightDirection(LightIndex) — const int selects the atmosphere light. + { + FShaderLabIntrinsicDesc Desc; + Desc.Name = FName(TEXT("SkyAtmosphereLightDirection")); + Desc.ReturnType = TEXT("float3"); + Desc.Params = { { TEXT("int"), TEXT("LightIndex"), TEXT("0") } }; + Desc.MakeNode = [](UMaterial& M, const TArray& Args, FString&) -> UMaterialExpression* + { + UMaterialExpressionSkyAtmosphereLightDirection* E = NewObject(&M); + if (Args.Num() >= 1) { E->LightIndex = FCString::Atoi(*Args[0]); } + M.GetExpressionCollection().AddExpression(E); + return E; + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp new file mode 100644 index 0000000..8ddfbf6 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Decal.cpp @@ -0,0 +1,23 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Decal intrinsics (meaningful in the Decal domain; the instance/domain gates usage). + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionDecalColor.h" +#include "Materials/MaterialExpressionDecalDerivative.h" +#include "Materials/MaterialExpressionDecalLifetimeOpacity.h" + +void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + // DecalColor's default output pin is RGB (float3). + AddSimple(Registry, TEXT("DecalColor"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("DecalLifetimeOpacity"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + + // DecalDerivative is multi-output (DDX, DDY) — one intrinsic per output pin. + AddSimple(Registry, TEXT("DecalDerivativeDDX"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }, 0); + AddSimple(Registry, TEXT("DecalDerivativeDDY"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }, 1); +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp new file mode 100644 index 0000000..6f89072 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Instancing.cpp @@ -0,0 +1,36 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Per-instance intrinsics. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionPerInstanceCustomData.h" +#include "Materials/MaterialExpressionPerInstanceFadeAmount.h" +#include "Materials/MaterialExpressionPerInstanceRandom.h" + +void RegisterInstancingIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("PerInstanceRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PerInstanceFadeAmount"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + + // PerInstanceCustomData(DataIndex[, ConstDefaultValue]) — const args + optional input pin 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& Args, FString&) -> UMaterialExpression* + { + UMaterialExpressionPerInstanceCustomData* E = NewObject(&M); + if (Args.Num() >= 1) { E->DataIndex = static_cast(FCString::Atoi(*Args[0])); } + if (Args.Num() >= 2) { E->ConstDefaultValue = FCString::Atof(*Args[1]); } + M.GetExpressionCollection().AddExpression(E); + return E; + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp new file mode 100644 index 0000000..0914bc4 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Mesh.cpp @@ -0,0 +1,51 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Mesh / surface attribute intrinsics (vertex normals/tangents, colors, UVs). + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionLightmapUVs.h" +#include "Materials/MaterialExpressionPixelNormalWS.h" +#include "Materials/MaterialExpressionPreSkinnedNormal.h" +#include "Materials/MaterialExpressionPreSkinnedPosition.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionTwoSidedSign.h" +#include "Materials/MaterialExpressionVertexColor.h" +#include "Materials/MaterialExpressionVertexNormalWS.h" +#include "Materials/MaterialExpressionVertexTangentWS.h" + +void RegisterMeshIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("VertexNormalWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("VertexTangentWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PixelNormalWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("VertexColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("TwoSidedSign"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("LightmapUVs"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PreSkinnedNormal"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PreSkinnedPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + + // TextureCoordinate(Index[, UTiling, VTiling]) — const args configure the node. + { + 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& Args, FString&) -> UMaterialExpression* + { + UMaterialExpressionTextureCoordinate* E = NewObject(&M); + if (Args.Num() >= 1) { E->CoordinateIndex = FCString::Atoi(*Args[0]); } + if (Args.Num() >= 2) { E->UTiling = FCString::Atof(*Args[1]); } + if (Args.Num() >= 3) { E->VTiling = FCString::Atof(*Args[2]); } + M.GetExpressionCollection().AddExpression(E); + return E; + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp new file mode 100644 index 0000000..4d0d810 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Object.cpp @@ -0,0 +1,69 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Object / primitive / bounds intrinsics. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionActorPositionWS.h" +#include "Materials/MaterialExpressionBounds.h" +#include "Materials/MaterialExpressionDistanceCullFade.h" +#include "Materials/MaterialExpressionLocalPosition.h" +#include "Materials/MaterialExpressionObjectBounds.h" +#include "Materials/MaterialExpressionObjectLocalBounds.h" +#include "Materials/MaterialExpressionObjectOrientation.h" +#include "Materials/MaterialExpressionObjectPositionWS.h" +#include "Materials/MaterialExpressionObjectRadius.h" +#include "Materials/MaterialExpressionPrecomputedAOMask.h" +#include "Materials/MaterialExpressionPreSkinnedLocalBounds.h" + +void RegisterObjectIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("LocalPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ActorPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ObjectPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ObjectRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ObjectBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + // Multi-output nodes: connect output 0 (matches existing behavior). + AddSimple(Registry, TEXT("ObjectLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ObjectOrientation"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PreSkinnedLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("DistanceCullFade"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PrecomputedAOMask"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + + // Bounds(Space) — unified bounds node; Space is an EMaterialExpressionBoundsType token (MEILB_*). + { + FShaderLabIntrinsicDesc Desc; + Desc.Name = FName(TEXT("Bounds")); + Desc.ReturnType = TEXT("float3"); + Desc.Doc = TEXT("Space is an EMaterialExpressionBoundsType token (e.g. MEILB_ObjectLocal)."); + + FShaderLabIntrinsicParam SpaceParam; + SpaceParam.Name = TEXT("Space"); + SpaceParam.Enum = StaticEnum(); + check(SpaceParam.Enum); + Desc.Params = { MoveTemp(SpaceParam) }; + + Desc.MakeNode = [](UMaterial& M, const TArray& Args, FString& OutError) -> UMaterialExpression* + { + UMaterialExpressionBounds* E = NewObject(&M); + if (Args.Num() >= 1) + { + UEnum* Enum = StaticEnum(); + check(Enum); + const int64 Value = Enum->GetValueByNameString(Args[0]); + if (Value == INDEX_NONE) + { + OutError = FString::Printf(TEXT("unknown bounds space '%s'"), *Args[0]); + return nullptr; + } + E->Type = static_cast(Value); + } + M.GetExpressionCollection().AddExpression(E); + return E; + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp new file mode 100644 index 0000000..9abf08c --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Particle.cpp @@ -0,0 +1,44 @@ +// Copyright FlecsProj. 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. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionParticleColor.h" +#include "Materials/MaterialExpressionParticleDirection.h" +#include "Materials/MaterialExpressionParticleMacroUV.h" +#include "Materials/MaterialExpressionParticleMotionBlurFade.h" +#include "Materials/MaterialExpressionParticlePositionWS.h" +#include "Materials/MaterialExpressionParticleRadius.h" +#include "Materials/MaterialExpressionParticleRandom.h" +#include "Materials/MaterialExpressionParticleRelativeTime.h" +#include "Materials/MaterialExpressionParticleSize.h" +#include "Materials/MaterialExpressionParticleSpeed.h" +#include "Materials/MaterialExpressionParticleSpriteRotation.h" +#include "Materials/MaterialExpressionParticleSubUVProperties.h" +#include "Materials/MaterialExpressionSphericalParticleOpacity.h" + +void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("ParticleColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticlePositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleDirection"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleSpeed"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleRelativeTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleMotionBlurFade"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleSpriteRotation"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ParticleMacroUV"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("SphericalParticleOpacity"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + + // ParticleSubUVProperties is multi-output (TexCoord0, TexCoord1, Blend) — one intrinsic per pin. + AddSimple(Registry, TEXT("ParticleSubUVTexCoord0"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }, 0); + AddSimple(Registry, TEXT("ParticleSubUVTexCoord1"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }, 1); + AddSimple(Registry, TEXT("ParticleSubUVBlend"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }, 2); +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp new file mode 100644 index 0000000..80e05ed --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_Time.cpp @@ -0,0 +1,17 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// Time intrinsics. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionDeltaTime.h" +#include "Materials/MaterialExpressionTime.h" + +void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("Time"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("DeltaTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); +} diff --git a/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp new file mode 100644 index 0000000..bb3ce32 --- /dev/null +++ b/Source/UShaderLabEditor/Private/ShaderLabIntrinsics_View.cpp @@ -0,0 +1,79 @@ +// Copyright FlecsProj. All Rights Reserved. +// +// View / camera / screen intrinsics. + +#include "ShaderLabIntrinsics.h" + +#include "Materials/MaterialExpressionCameraPositionWS.h" +#include "Materials/MaterialExpressionCameraVectorWS.h" +#include "Materials/MaterialExpressionEyeAdaptation.h" +#include "Materials/MaterialExpressionIsFirstPerson.h" +#include "Materials/MaterialExpressionIsOrthographic.h" +#include "Materials/MaterialExpressionLightVector.h" +#include "Materials/MaterialExpressionMainDirectionalLight.h" +#include "Materials/MaterialExpressionPixelDepth.h" +#include "Materials/MaterialExpressionReflectionVectorWS.h" +#include "Materials/MaterialExpressionSceneTexelSize.h" +#include "Materials/MaterialExpressionScreenPosition.h" +#include "Materials/MaterialExpressionViewProperty.h" +#include "Materials/MaterialExpressionViewSize.h" +#include "Materials/MaterialExpressionWorldPosition.h" + +void RegisterViewIntrinsics(FShaderLabIntrinsicRegistry& Registry) +{ + using namespace ShaderLabIntrinsic_Private; + using EFreq = EShaderLabIntrinsicFrequency; + + AddSimple(Registry, TEXT("WorldPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("CameraPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("CameraVectorWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("LightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("PixelDepth"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ScreenPosition"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ReflectionVectorWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("ViewSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("SceneTexelSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("IsOrthographic"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("EyeAdaptation"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + AddSimple(Registry, TEXT("IsFirstPerson"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple(M); }); + + // MainDirectionalLight is multi-output (Illuminance, Direction) — one intrinsic per output pin. + AddSimple(Registry, TEXT("MainDirectionalLightIlluminance"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }, 0); + AddSimple(Registry, TEXT("MainDirectionalLightDirection"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple(M); }, 1); + + // ViewProperty(Property) — Property is an EMaterialExposedViewProperty token. + { + FShaderLabIntrinsicDesc Desc; + Desc.Name = FName(TEXT("ViewProperty")); + Desc.ReturnType = TEXT("float4"); + Desc.Doc = TEXT("Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView)."); + + FShaderLabIntrinsicParam PropertyParam; + PropertyParam.Name = TEXT("Property"); + // Safe: UShaderLabEditor depends on Engine, whose reflected enums register before any editor + // code runs, and this executes lazily at first registry use (build/command/test time). + PropertyParam.Enum = StaticEnum(); + check(PropertyParam.Enum); + Desc.Params = { MoveTemp(PropertyParam) }; + + Desc.MakeNode = [](UMaterial& M, const TArray& Args, FString& OutError) -> UMaterialExpression* + { + UMaterialExpressionViewProperty* E = NewObject(&M); + if (Args.Num() >= 1) + { + UEnum* Enum = StaticEnum(); + check(Enum); + const int64 Value = Enum->GetValueByNameString(Args[0]); + if (Value == INDEX_NONE) + { + OutError = FString::Printf(TEXT("unknown view property '%s'"), *Args[0]); + return nullptr; + } + E->Property = static_cast(Value); + } + M.GetExpressionCollection().AddExpression(E); + return E; + }; + Registry.Register(MoveTemp(Desc)); + } +} diff --git a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h index 0f4222b..e4f9029 100644 --- a/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h +++ b/Source/UShaderLabEditor/Public/ShaderLabIntrinsicRegistry.h @@ -6,6 +6,7 @@ class UMaterial; class UMaterialExpression; +class UEnum; /** Shader stage an intrinsic is valid in (used to reject e.g. PixelDepth inside a Vertex body). */ enum class EShaderLabIntrinsicFrequency : uint8 @@ -24,16 +25,23 @@ struct FShaderLabIntrinsicParam FString Name; /** Default-value literal for the stub (e.g. "1"); empty means the parameter is required. */ FString DefaultLiteral; + /** + * When non-null, this parameter is a reflected enum passed by token name (e.g. + * `UE_ViewProperty(MEVP_FieldOfView)`). The stub generator emits the enum from it and types the + * parameter as it; the arg validator accepts only its token names. Points at a process-permanent + * native `UEnum` (`StaticEnum()`), so a raw pointer is safe. `Type` is ignored when this is set. + */ + const UEnum* Enum = nullptr; }; /** - * Describes one `UE::NodeName(...)` intrinsic: how to build the backing material expression node, + * 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. + * can emit `UE_Name(...)` HLSL stub declarations from this single source of truth, giving the IDE + * member/signature completion for `UE_` calls without any drift from the real registration. */ struct FShaderLabIntrinsicDesc { @@ -56,7 +64,7 @@ struct FShaderLabIntrinsicDesc /** * 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)`). + * are the literal arguments parsed from the call (e.g. {"0","2.0"} for `UE_TextureCoordinate(0,2.0)`). * Return nullptr and set `OutError` on a bad argument. Contract: this only runs in the editor/cook * (graphs are editor-only); the cooked runtime never builds a graph. */ @@ -79,7 +87,7 @@ 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). */ + /** Visit every registered intrinsic (used by ShaderLab.IDE.Prepare to generate the UE_ stub header). */ void ForEach(TFunctionRef Fn) const; private: