diff --git a/Shaders/Private/ShaderLabCommon.ush b/Shaders/Private/ShaderLabCommon.ush index d738ea2..7e0e02f 100644 --- a/Shaders/Private/ShaderLabCommon.ush +++ b/Shaders/Private/ShaderLabCommon.ush @@ -344,3 +344,52 @@ FShaderLabVertex ShaderLabDefaultVertex() V.CustomizedUV3 = float2(0, 0); return V; } + +// Runtime Virtual Texture WRITE channels, filled by an SL_RVTOUTPUT() body. Mirrors the pins of +// UMaterialExpressionRuntimeVirtualTextureOutput; the graph builder wires each written field to the +// matching pin (unwritten fields keep the node's own default). Which fields the RVT actually stores is +// governed by the RVT asset's material type, not this struct. +struct FShaderLabRVTOutput +{ + float3 BaseColor; + float Specular; + float Roughness; + float3 Normal; + float WorldHeight; + float Opacity; + float Mask; + float Displacement; + float4 Mask4; +}; + +FShaderLabRVTOutput ShaderLabDefaultRVTOutput() +{ + FShaderLabRVTOutput O; + O.BaseColor = float3(0, 0, 0); + O.Specular = 0.5; + O.Roughness = 0.5; + O.Normal = float3(0, 0, 1); + O.WorldHeight = 0; + O.Opacity = 1; + O.Mask = 1; + O.Displacement = 0; + O.Mask4 = float4(0, 0, 0, 0); + return O; +} + +#ifdef SHADERLAB_IDE +// IDE-only: the result struct of an SL_RVTSAMPLE Runtime Virtual Texture READ. A body reads channels via +// `.BaseColor` etc.; at real compile the graph builder rewrites those member accesses into wired +// inputs carrying the RVT sample node's output pins, so this type never reaches the shader compiler. +struct FShaderLabRVT +{ + float3 BaseColor; + float3 Normal; + float Roughness; + float Specular; + float WorldHeight; + float Mask; + float Displacement; + float4 Mask4; +}; +#endif // SHADERLAB_IDE diff --git a/Shaders/Private/ShaderLabDSL.ush b/Shaders/Private/ShaderLabDSL.ush index 8d56d60..4bb0c3f 100644 --- a/Shaders/Private/ShaderLabDSL.ush +++ b/Shaders/Private/ShaderLabDSL.ush @@ -38,6 +38,25 @@ // float3 WindVector; #define SL_COLLECTION(...) +// Pre-sampled streaming Virtual Texture read. Precedes a `float4 ;` declaration; the body uses +// directly as the sampled color (a VT cannot be sampled inside a Custom node, so the graph builder samples it +// with a real node and feeds the result in). SamplerType defaults to VirtualColor. UV selects the coordinate. +// SL_VTSAMPLE(DefaultTexture = "/Game/VT/T_Albedo", SamplerType = VirtualColor, UV = TexCoord0) +// float4 VTAlbedo; +#define SL_VTSAMPLE(...) + +// Pre-sampled Runtime Virtual Texture read. Precedes an `FShaderLabRVT ;` declaration; the body reads +// channels via .BaseColor / .Normal / .Roughness / ... (see FShaderLabRVT in ShaderLabCommon.ush). +// MaterialType selects the RVT layout; UV = World derives the coordinate from world position. +// SL_RVTSAMPLE(VirtualTexture = "/Game/RVT/RVT_Terrain", MaterialType = BaseColor_Normal_Roughness, UV = World) +// FShaderLabRVT Terrain; +#define SL_RVTSAMPLE(...) + +// Runtime Virtual Texture WRITE. An additive output block (alongside the pixel entry) that fills the channels +// written into an RVT when the mesh renders into it. Precedes a `void (inout FShaderLabRVTOutput O){...}`. +// SL_RVTOUTPUT() void RVTOutput(inout FShaderLabRVTOutput O) { O.BaseColor = ...; O.Normal = ...; } +#define SL_RVTOUTPUT(...) + // Library function (`.uslfunc` files only). Precedes a normal HLSL function; it may use UE_ intrinsics, // reference the library's own properties, and call other library functions. // SL_FUNCTION() @@ -97,9 +116,14 @@ // Domain = Surface | PostProcess | UI | Decal // BlendMode = Opaque | Masked | Translucent | Additive | Modulate // SL_PROPERTY : Category, SortPriority, DefaultTexture (textures only), +// SamplerType (textures only: Color|LinearColor|Normal|Grayscale|LinearGrayscale|Masks|Alpha| +// DistanceFieldFont|Data; Virtual* is rejected — use SL_VTSAMPLE), // ClampMin / ClampMax (Scalar slider bounds; each independent), // CustomPrimitiveData = (Scalar/Vector only; per-instance via Custom Primitive Data; // mutually exclusive with ClampMin/ClampMax) +// SL_VTSAMPLE : DefaultTexture, SamplerType (Virtual* only; default VirtualColor), UV +// SL_RVTSAMPLE: VirtualTexture, MaterialType, UV +// UV (VT/RVT) : TexCoord (a texcoord set) | | World (RVT 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 index 07d44bd..3551ad1 100644 --- a/Shaders/Private/ShaderLabUEFunctions.ush +++ b/Shaders/Private/ShaderLabUEFunctions.ush @@ -6,7 +6,7 @@ #pragma once -#include "/Plugin/ShaderLab/Private/UEFunctions/Sampling.ush" // IDE-only Unreal Texture*Sample/Gather stubs. +#include "/Plugin/ShaderLab/Private/UEFunctions/TextureSample.ush" // IDE-only texture-sampling stubs #include "/Plugin/ShaderLab/Private/UEFunctions/Noise.ush" #include "/Plugin/ShaderLab/Private/UEFunctions/Rotation.ush" #include "/Plugin/ShaderLab/Private/UEFunctions/SceneTexture.ush" diff --git a/Shaders/Private/UEFunctions/Sampling.ush b/Shaders/Private/UEFunctions/Sampling.ush deleted file mode 100644 index abf9cac..0000000 --- a/Shaders/Private/UEFunctions/Sampling.ush +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright UShaderLab. All Rights Reserved. -// -// IDE-only stubs for Unreal's material texture sampling helpers from Common.ush. -// The real material template already provides these functions; this file only lets -// standalone HLSL tooling resolve calls inside .usl authoring files. - -#pragma once - -#ifdef SHADERLAB_IDE - -struct FloatDeriv2 -{ - float2 Value; - float2 Ddx; - float2 Ddy; -}; - -float4 Texture2DSample(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } -float4 Texture2DSample(Texture2D Tex, SamplerState Sampler, FloatDeriv2 UV) { return (float4)0; } -float Texture2DSample_A8(Texture2D Tex, SamplerState Sampler, float2 UV) { return 0; } -float4 Texture2DSampleLevel(Texture2D Tex, SamplerState Sampler, float2 UV, float Mip) { return (float4)0; } -float4 Texture2DSampleBias(Texture2D Tex, SamplerState Sampler, float2 UV, float MipBias) { return (float4)0; } -float4 Texture2DSampleGrad(Texture2D Tex, SamplerState Sampler, float2 UV, float2 DDX, float2 DDY) { return (float4)0; } - -float4 Texture3DSample(Texture3D Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture3DSampleLevel(Texture3D Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } -float4 Texture3DSampleBias(Texture3D Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } -float4 Texture3DSampleGrad(Texture3D Tex, SamplerState Sampler, float3 UV, float3 DDX, float3 DDY) { return (float4)0; } - -float4 TextureCubeSample(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 TextureCubeSampleLevel(TextureCube Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } -float TextureCubeSampleDepthLevel(TextureCube TexDepth, SamplerState Sampler, float3 UV, float Mip) { return 0; } -float4 TextureCubeSampleBias(TextureCube Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } -float4 TextureCubeSampleGrad(TextureCube Tex, SamplerState Sampler, float3 UV, float3 DDX, float3 DDY) { return (float4)0; } - -float4 Texture2DArraySample(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture2DArraySampleLevel(Texture2DArray Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } -float4 Texture2DArraySampleBias(Texture2DArray Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } -float4 Texture2DArraySampleGrad(Texture2DArray Tex, SamplerState Sampler, float3 UV, float2 DDX, float2 DDY) { return (float4)0; } - -float4 Texture2DGatherRed(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } -float4 Texture2DGatherGreen(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } -float4 Texture2DGatherBlue(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } -float4 Texture2DGatherAlpha(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } - -float4 TextureCubeGatherRed(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 TextureCubeGatherGreen(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 TextureCubeGatherBlue(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 TextureCubeGatherAlpha(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } - -float4 Texture2DArrayGatherRed(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture2DArrayGatherGreen(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture2DArrayGatherBlue(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture2DArrayGatherAlpha(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } - -float4 Texture2DSample_Decal(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } -float4 Texture3DSample_Decal(Texture3D Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 Texture2DArraySample_Decal(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } -float4 TextureCubeSample_Decal(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } - -float4 TextureCubeArraySample(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } -float4 TextureCubeArraySampleLevel(TextureCubeArray Tex, SamplerState Sampler, float4 UV, float Mip) { return (float4)0; } -float4 TextureCubeArraySampleBias(TextureCubeArray Tex, SamplerState Sampler, float4 UV, float MipBias) { return (float4)0; } -float4 TextureCubeArraySampleGrad(TextureCubeArray Tex, SamplerState Sampler, float4 UV, float3 DDX, float3 DDY) { return (float4)0; } -float4 TextureCubeArraySampleLevel(TextureCubeArray Tex, SamplerState Sampler, float3 UV, float ArrayIndex, float Mip) { return (float4)0; } - -float4 TextureCubeArrayGatherRed(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } -float4 TextureCubeArrayGatherGreen(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } -float4 TextureCubeArrayGatherBlue(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } -float4 TextureCubeArrayGatherAlpha(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } - -#endif // SHADERLAB_IDE diff --git a/Shaders/Private/UEFunctions/TextureSample.ush b/Shaders/Private/UEFunctions/TextureSample.ush new file mode 100644 index 0000000..78a05ed --- /dev/null +++ b/Shaders/Private/UEFunctions/TextureSample.ush @@ -0,0 +1,55 @@ +// Copyright UShaderLab. All Rights Reserved. +// +// IDE-only (SHADERLAB_IDE) stubs for the engine's material texture-sampling intrinsics +// (Texture2DSample family, in Common.ush). A `.usl` body samples textures with plain HLSL, +// e.g. `Texture2DSample(Tex, TexSampler, uv)`, which reaches the real engine symbols only inside +// the generated Custom node. shader-validator can't see the material environment, so without these +// stubs those calls show "undeclared". The real shader compiler never defines SHADERLAB_IDE, so it +// uses the engine's true functions and this whole file is empty at compile time — no redefinition. +// +// This file is auto-included into every Custom node (via ShaderLabUEFunctions.ush), hence the hard +// SHADERLAB_IDE guard is essential. + +#pragma once + +#ifdef SHADERLAB_IDE + +// Hide the engine's MaterialFloat alias from authors: under the IDE it degrades to plain float so +// completion/type-checking only ever surface `float`. (Guarded so a stray engine include can't clash.) +#ifndef MaterialFloat + #define MaterialFloat float + #define MaterialFloat2 float2 + #define MaterialFloat3 float3 + #define MaterialFloat4 float4 +#endif + +// --- Texture2D --- +float4 Texture2DSample(Texture2D Tex, SamplerState Sampler, float2 UV) { return (float4)0; } +float Texture2DSample_A8(Texture2D Tex, SamplerState Sampler, float2 UV) { return 0; } +float4 Texture2DSampleLevel(Texture2D Tex, SamplerState Sampler, float2 UV, float Mip) { return (float4)0; } +float4 Texture2DSampleBias(Texture2D Tex, SamplerState Sampler, float2 UV, float MipBias) { return (float4)0; } +float4 Texture2DSampleGrad(Texture2D Tex, SamplerState Sampler, float2 UV, float2 DDX, float2 DDY) { return (float4)0; } + +// --- Texture3D (volume) --- +float4 Texture3DSample(Texture3D Tex, SamplerState Sampler, float3 UV) { return (float4)0; } +float4 Texture3DSampleLevel(Texture3D Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } +float4 Texture3DSampleBias(Texture3D Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } +float4 Texture3DSampleGrad(Texture3D Tex, SamplerState Sampler, float3 UV, float3 DDX, float3 DDY) { return (float4)0; } + +// --- TextureCube --- +float4 TextureCubeSample(TextureCube Tex, SamplerState Sampler, float3 UV) { return (float4)0; } +float4 TextureCubeSampleLevel(TextureCube Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } +float4 TextureCubeSampleBias(TextureCube Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } +float4 TextureCubeSampleGrad(TextureCube Tex, SamplerState Sampler, float3 UV, float3 DDX, float3 DDY) { return (float4)0; } + +// --- Texture2DArray (third component = slice index) --- +float4 Texture2DArraySample(Texture2DArray Tex, SamplerState Sampler, float3 UV) { return (float4)0; } +float4 Texture2DArraySampleLevel(Texture2DArray Tex, SamplerState Sampler, float3 UV, float Mip) { return (float4)0; } +float4 Texture2DArraySampleBias(Texture2DArray Tex, SamplerState Sampler, float3 UV, float MipBias) { return (float4)0; } +float4 Texture2DArraySampleGrad(Texture2DArray Tex, SamplerState Sampler, float3 UV, float2 DDX, float2 DDY) { return (float4)0; } + +// --- TextureCubeArray (fourth component = slice index) --- +float4 TextureCubeArraySample(TextureCubeArray Tex, SamplerState Sampler, float4 UV) { return (float4)0; } +float4 TextureCubeArraySampleLevel(TextureCubeArray Tex, SamplerState Sampler, float4 UV, float Mip) { return (float4)0; } + +#endif // SHADERLAB_IDE diff --git a/Source/UShaderLab/Private/ShaderLabParser.cpp b/Source/UShaderLab/Private/ShaderLabParser.cpp index 6c3fd89..de31b53 100644 --- a/Source/UShaderLab/Private/ShaderLabParser.cpp +++ b/Source/UShaderLab/Private/ShaderLabParser.cpp @@ -514,7 +514,7 @@ using namespace ShaderLabParser_Private; namespace { /** Read ` ( ) { }` after a marker macro. Returns false on error. */ - bool ParseAnnotatedFunction(FScanner& S, FString& OutName, FString& OutSigInner, FString& OutBody, int32& OutBodyLine) + bool ParseAnnotatedFunction(FScanner& S, FString& OutName, FString& OutSigInner, FString& OutBody, int32& OutBodyLine, FString* OutRetType = nullptr) { FString RetType; if (!S.ReadIdentifier(RetType)) @@ -522,6 +522,7 @@ namespace S.Error(TEXT("Expected a function declaration after the ShaderLab marker")); return false; } + if (OutRetType) { *OutRetType = RetType; } if (!S.ReadIdentifier(OutName)) { S.Error(TEXT("Expected a function name")); @@ -738,6 +739,24 @@ namespace Prop.bUseCustomPrimitiveData = true; Prop.PrimitiveDataIndex = FCString::Atoi(*Value); } + else if (Key == TEXT("SamplerType")) + { + // Only valid on a texture property, and only a non-virtual token (Virtual* -> use SL_VTSAMPLE). + const FString Token = Value.TrimQuotes(); + static const TCHAR* const Allowed[] = { + TEXT("Color"), TEXT("LinearColor"), TEXT("Grayscale"), TEXT("LinearGrayscale"), + TEXT("Alpha"), TEXT("Normal"), TEXT("Masks"), TEXT("DistanceFieldFont"), TEXT("Data"), + }; + bool bOk = false; + for (const TCHAR* A : Allowed) { if (Token == A) { bOk = true; break; } } + if (!bOk) + { + S.Error(FString::Printf(TEXT("Property '%s' has invalid SamplerType '%s' (use Color/LinearColor/Normal/Grayscale/LinearGrayscale/Masks/Alpha/DistanceFieldFont/Data; Virtual* belongs to SL_VTSAMPLE)"), + *Prop.Name.ToString(), *Token), ErrLine, ErrCol); + return; + } + Prop.SamplerType = Token; + } else { S.Error(FString::Printf(TEXT("Property '%s' has unknown specifier '%s'"), *Prop.Name.ToString(), *Key), ErrLine, ErrCol); @@ -903,6 +922,12 @@ namespace return; } } + // SamplerType is only meaningful on a texture property. + if (!Prop.SamplerType.IsEmpty() && !ShaderLabIsTextureType(Prop.Type)) + { + S.Error(FString::Printf(TEXT("Property '%s': SamplerType is only valid on a texture property"), *Prop.Name.ToString()), NameLine, NameCol); + return; + } if (Model.FindProperty(Prop.Name) != nullptr) { S.Error(FString::Printf(TEXT("Duplicate property name '%s'"), *Prop.Name.ToString()), NameLine, NameCol); @@ -1150,6 +1175,244 @@ namespace Model.Collections.Add(MoveTemp(C)); } + /** + * Parse a pre-sample `UV = ` value: `TexCoord` (a texcoord set), `World` (RVT only — derive from + * world position), or a bare identifier naming an SL_VALUE float2 block (validated at build time, since the + * block may be declared later in the file). Returns false (and reports) on a malformed spec. + */ + bool ParseUVSpec(FScanner& S, const FString& RawValue, bool bAllowWorld, const TCHAR* What, FShaderLabUV& Out) + { + const FString V = RawValue.TrimStartAndEnd().TrimQuotes(); + if (V.StartsWith(TEXT("TexCoord"))) + { + const FString IdxStr = V.Mid(8); + if (!IsNonNegativeInteger(IdxStr)) + { + S.Error(FString::Printf(TEXT("%s: UV 'TexCoord' expects a non-negative integer, got '%s'"), What, *V)); + return false; + } + Out.Kind = FShaderLabUV::EKind::TexCoord; + Out.TexCoordIndex = FCString::Atoi(*IdxStr); + return true; + } + if (V == TEXT("World")) + { + if (!bAllowWorld) + { + S.Error(FString::Printf(TEXT("%s: UV = World is only valid for SL_RVTSAMPLE"), What)); + return false; + } + Out.Kind = FShaderLabUV::EKind::World; + return true; + } + if (V.IsEmpty()) + { + S.Error(FString::Printf(TEXT("%s: UV is empty (use TexCoord, a float2 SL_VALUE block name%s)"), What, bAllowWorld ? TEXT(", or World") : TEXT(""))); + return false; + } + Out.Kind = FShaderLabUV::EKind::ValueBlock; + Out.ValueBlockName = FName(*V); + return true; + } + + /** SL_VTSAMPLE(DefaultTexture="...", [SamplerType=Virtual*], [UV=...]) float4 ; — a streaming VT read. */ + void ParseVTSample(FScanner& S, FShaderLabModel& Model) + { + FString Inner; + if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner)) + { + return; + } + FShaderLabVTSample VT; + bool bHasUV = false; + for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(','))) + { + const FString Stmt = StmtRaw.TrimStartAndEnd(); + if (Stmt.IsEmpty()) { continue; } + FString Key, Value; + if (!Stmt.Split(TEXT("="), &Key, &Value)) + { + S.Error(FString::Printf(TEXT("Malformed SL_VTSAMPLE specifier '%s' (expected Key = Value)"), *Stmt)); + return; + } + Key = Key.TrimStartAndEnd(); + Value = Value.TrimStartAndEnd(); + if (Key == TEXT("DefaultTexture")) { VT.DefaultTexture = Value.TrimQuotes(); } + else if (Key == TEXT("SamplerType")) + { + const FString Token = Value.TrimQuotes(); + static const TCHAR* const Virtuals[] = { + TEXT("VirtualColor"), TEXT("VirtualGrayscale"), TEXT("VirtualAlpha"), TEXT("VirtualNormal"), + TEXT("VirtualMasks"), TEXT("VirtualLinearColor"), TEXT("VirtualLinearGrayscale"), + }; + bool bOk = false; + for (const TCHAR* A : Virtuals) { if (Token == A) { bOk = true; break; } } + if (!bOk) + { + S.Error(FString::Printf(TEXT("SL_VTSAMPLE SamplerType must be a Virtual* type (VirtualColor/VirtualNormal/...), got '%s'"), *Token)); + return; + } + VT.SamplerType = Token; + } + else if (Key == TEXT("UV")) { if (!ParseUVSpec(S, Value, /*bAllowWorld*/ false, TEXT("SL_VTSAMPLE"), VT.UV)) { return; } bHasUV = true; } + else { S.Error(FString::Printf(TEXT("SL_VTSAMPLE has unknown specifier '%s' (use DefaultTexture / SamplerType / UV)"), *Key)); return; } + } + if (!bHasUV) { VT.UV.Kind = FShaderLabUV::EKind::TexCoord; VT.UV.TexCoordIndex = 0; } + + S.SkipTrivia(); + const int32 DeclLine = S.Line, DeclCol = S.Column; + FString TypeTok; + if (!S.ReadIdentifier(TypeTok) || TypeTok != TEXT("float4")) + { + S.Error(TEXT("SL_VTSAMPLE must be followed by a `float4 ;` declaration"), DeclLine, DeclCol); + return; + } + FString Name; + if (!S.ReadIdentifier(Name)) + { + S.Error(TEXT("SL_VTSAMPLE declaration is missing a name"), DeclLine, DeclCol); + return; + } + if (!S.Expect(TEXT(';'), TEXT("after an SL_VTSAMPLE declaration"))) + { + return; + } + VT.Name = FName(*Name); + VT.Line = DeclLine; + if (Model.VTSamples.ContainsByPredicate([&VT](const FShaderLabVTSample& E) { return E.Name == VT.Name; })) + { + S.Error(FString::Printf(TEXT("Duplicate SL_VTSAMPLE name '%s'"), *Name), DeclLine, DeclCol); + return; + } + Model.VTSamples.Add(MoveTemp(VT)); + } + + /** SL_RVTSAMPLE(VirtualTexture="...", MaterialType=..., [UV=...]) FShaderLabRVT ; — a Runtime VT read. */ + void ParseRVTSample(FScanner& S, FShaderLabModel& Model) + { + FString Inner; + if (!S.ReadBalanced(TEXT('('), TEXT(')'), Inner)) + { + return; + } + FShaderLabRVTSample RVT; + bool bHasUV = false; + for (const FString& StmtRaw : SplitTopLevel(Inner, TEXT(','))) + { + const FString Stmt = StmtRaw.TrimStartAndEnd(); + if (Stmt.IsEmpty()) { continue; } + FString Key, Value; + if (!Stmt.Split(TEXT("="), &Key, &Value)) + { + S.Error(FString::Printf(TEXT("Malformed SL_RVTSAMPLE specifier '%s' (expected Key = Value)"), *Stmt)); + return; + } + Key = Key.TrimStartAndEnd(); + Value = Value.TrimStartAndEnd(); + if (Key == TEXT("VirtualTexture")) { RVT.VirtualTexture = Value.TrimQuotes(); } + else if (Key == TEXT("MaterialType")) + { + const FString Token = Value.TrimQuotes(); + static const TCHAR* const Types[] = { + TEXT("BaseColor"), TEXT("Mask4"), TEXT("BaseColor_Normal_Roughness"), + TEXT("BaseColor_Normal_Specular"), TEXT("BaseColor_Normal_Specular_YCoCg"), + TEXT("BaseColor_Normal_Specular_Mask_YCoCg"), TEXT("WorldHeight"), TEXT("Displacement"), + }; + bool bOk = false; + for (const TCHAR* A : Types) { if (Token == A) { bOk = true; break; } } + if (!bOk) + { + S.Error(FString::Printf(TEXT("SL_RVTSAMPLE MaterialType '%s' is not a valid ERuntimeVirtualTextureMaterialType"), *Token)); + return; + } + RVT.MaterialType = Token; + } + else if (Key == TEXT("UV")) { if (!ParseUVSpec(S, Value, /*bAllowWorld*/ true, TEXT("SL_RVTSAMPLE"), RVT.UV)) { return; } bHasUV = true; } + else { S.Error(FString::Printf(TEXT("SL_RVTSAMPLE has unknown specifier '%s' (use VirtualTexture / MaterialType / UV)"), *Key)); return; } + } + if (!bHasUV) { RVT.UV.Kind = FShaderLabUV::EKind::World; } + + S.SkipTrivia(); + const int32 DeclLine = S.Line, DeclCol = S.Column; + FString TypeTok; + if (!S.ReadIdentifier(TypeTok) || TypeTok != TEXT("FShaderLabRVT")) + { + S.Error(TEXT("SL_RVTSAMPLE must be followed by an `FShaderLabRVT ;` declaration"), DeclLine, DeclCol); + return; + } + FString Name; + if (!S.ReadIdentifier(Name)) + { + S.Error(TEXT("SL_RVTSAMPLE declaration is missing a name"), DeclLine, DeclCol); + return; + } + if (!S.Expect(TEXT(';'), TEXT("after an SL_RVTSAMPLE declaration"))) + { + return; + } + if (RVT.MaterialType.IsEmpty()) + { + S.Error(FString::Printf(TEXT("SL_RVTSAMPLE '%s' requires MaterialType = "), *Name), DeclLine, DeclCol); + return; + } + RVT.Name = FName(*Name); + RVT.Line = DeclLine; + if (Model.RVTSamples.ContainsByPredicate([&RVT](const FShaderLabRVTSample& E) { return E.Name == RVT.Name; })) + { + S.Error(FString::Printf(TEXT("Duplicate SL_RVTSAMPLE name '%s'"), *Name), DeclLine, DeclCol); + return; + } + Model.RVTSamples.Add(MoveTemp(RVT)); + } + + /** SL_RVTOUTPUT() void (inout FShaderLabRVTOutput O){...} — write channels into a Runtime VT. */ + void ParseRVTOutput(FScanner& S, FShaderLabModel& Model) + { + FString SpecInner; + if (!S.ReadBalanced(TEXT('('), TEXT(')'), SpecInner)) + { + return; + } + if (!SpecInner.TrimStartAndEnd().IsEmpty()) + { + S.Error(TEXT("SL_RVTOUTPUT takes no specifiers")); + return; + } + if (Model.bHasRVTOutput) + { + S.Error(TEXT("A shader may declare only one SL_RVTOUTPUT block")); + 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; + } + TArray Params; + if (!ParseEntryParams(SigInner, Params) || Params.Num() < 1) + { + S.Error(TEXT("SL_RVTOUTPUT must take an (inout FShaderLabRVTOutput) parameter"), DeclLine, DeclCol); + return; + } + if (!ValidateBodyParameters(S, Params, Body, TEXT("FMaterialPixelParameters"), /*bRequireStruct*/ true, + TEXT("SL_RVTOUTPUT"), DeclLine, DeclCol)) + { + return; + } + if (Params.Last().Type != TEXT("FShaderLabRVTOutput")) + { + S.Error(FString::Printf(TEXT("SL_RVTOUTPUT must take an (inout FShaderLabRVTOutput), got '%s'"), *Params.Last().Type), DeclLine, DeclCol); + return; + } + Model.bHasRVTOutput = true; + Model.RVTOutputParamName = Params.Last().Name; + Model.RVTOutputBody = MoveTemp(Body); + Model.RVTOutputBodyLine = BodyLine; + } + void ParseEntry(FScanner& S, FShaderLabModel& Model, const FString& Marker) { // SL_SURFACE(...) accepts BSDF modifier specifiers; the other entries take no args. @@ -1285,12 +1548,18 @@ namespace } S.SkipTrivia(); const int32 DeclLine = S.Line, DeclCol = S.Column; - FString Name, SigInner, Body; + FString Name, SigInner, Body, RetType; int32 BodyLine = 0; - if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine)) + if (!ParseAnnotatedFunction(S, Name, SigInner, Body, BodyLine, &RetType)) { return; } + // Scalar (float) blocks are mix factors / material outputs; a float2 block may serve as a pre-sample UV. + if (RetType != TEXT("float") && RetType != TEXT("float2")) + { + S.Error(FString::Printf(TEXT("SL_VALUE '%s' return type must be float or float2, got '%s'"), *Name, *RetType), DeclLine, DeclCol); + return; + } TArray Params; if (!ParseEntryParams(SigInner, Params)) { @@ -1310,6 +1579,7 @@ namespace } FShaderLabValue Value; Value.Name = ValueName; + Value.ReturnType = RetType; Value.Body = MoveTemp(Body); Value.BodyLine = BodyLine; Model.Values.Add(MoveTemp(Value)); @@ -1749,6 +2019,21 @@ bool FShaderLabParser::Parse( if (RejectInLibrary(TEXT("SL_COLLECTION"))) { return false; } ParseCollection(S, OutModel); } + else if (Token == TEXT("SL_VTSAMPLE")) + { + if (RejectInLibrary(TEXT("SL_VTSAMPLE"))) { return false; } + ParseVTSample(S, OutModel); + } + else if (Token == TEXT("SL_RVTSAMPLE")) + { + if (RejectInLibrary(TEXT("SL_RVTSAMPLE"))) { return false; } + ParseRVTSample(S, OutModel); + } + else if (Token == TEXT("SL_RVTOUTPUT")) + { + if (RejectInLibrary(TEXT("SL_RVTOUTPUT"))) { return false; } + ParseRVTOutput(S, OutModel); + } else if (Token == TEXT("SL_SURFACE") || Token == TEXT("SL_POSTPROCESS") || Token == TEXT("SL_UI") || Token == TEXT("SL_VERTEX")) { if (RejectInLibrary(TEXT("A pixel/vertex entry"))) { return false; } diff --git a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp index 07e53a7..b5927dc 100644 --- a/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp +++ b/Source/UShaderLab/Private/ShaderLabRuntimeBuilder.cpp @@ -4,12 +4,87 @@ #include "Engine/EngineTypes.h" #include "MaterialDomain.h" +#include "MaterialCachedData.h" +#include "MaterialTypes.h" #include "Materials/Material.h" +#include "Materials/MaterialInterface.h" #include "ShaderLabModel.h" #include "ShaderLabSettingsApplier.h" DEFINE_LOG_CATEGORY_STATIC(LogShaderLabRuntime, Log, All); +#if !WITH_EDITOR +namespace ShaderLabRuntime_Private +{ + // --- Cooked-runtime shell "connected property" mask (general mechanism; see AIDoc §2.8) --- + // + // The base material is a never-rendered, memory-only shell rebuilt at runtime from the .usl WITHOUT an + // expression graph (ApplySettings only). So its FMaterialCachedExpressionData is null and + // GetCachedExpressionData() falls back to the shared FMaterialCachedExpressionData::EmptyData -> every + // runtime query of "is material property X connected" on the shell answers false. That is normally harmless + // (instances render from their own baked shader map), but the engine's DEFERRED DECAL path is different: it + // asks the SHELL `IsPropertyConnected(MP_*)` at draw time (DecalRenderingCommon::ComputeDecalBlendDesc) to + // decide which GBuffer channels the decal writes — an empty mask yields a None descriptor and the decal is + // silently not drawn (works in-editor, where the shell has the full graph; fails cooked/-game). More domains + // could hit the same class of runtime connectivity query in future. + // + // Fix: give the shell its OWN cached data whose PropertyConnectedMask reflects what the .usl body actually + // writes, derived from the model (no graph needed). CachedExpressionData is a protected UMaterialInterface + // member (a TUniquePtr, not a UPROPERTY, so unreachable via FProperty reflection); we reach it with a tiny + // `using`-re-exposing access lens. This touches only that one data member (identical layout, no added + // members/vtable), needs zero engine changes, and leaves the shell an ordinary UMaterial. + struct FCachedExprLens : public UMaterial + { + using UMaterialInterface::CachedExpressionData; + }; + + // Output-struct member access in a body ("S.") -> the EMaterialProperty it feeds. Extensible: add a + // row when a new output field needs to be reflected into the shell's runtime connectivity. + struct FFieldToProperty { const TCHAR* Field; EMaterialProperty Property; }; + static const FFieldToProperty GFieldToProperty[] = { + { TEXT(".DiffuseAlbedo"), MP_DiffuseColor }, // Substrate Slab albedo/F0 parameterization + { TEXT(".F0"), MP_SpecularColor }, + { TEXT(".Normal"), MP_Normal }, + { TEXT(".Roughness"), MP_Roughness }, + { TEXT(".Metallic"), MP_Metallic }, + { TEXT(".Specular"), MP_Specular }, + { TEXT(".EmissiveColor"), MP_EmissiveColor }, + { TEXT(".Color"), MP_EmissiveColor }, // PostProcess / UI emissive output + { TEXT(".Opacity"), MP_Opacity }, + { TEXT(".OpacityMask"), MP_OpacityMask }, + { TEXT(".AmbientOcclusion"), MP_AmbientOcclusion }, + }; + + static void PopulateShellConnectivity(UMaterial& Shell, const FShaderLabModel& Model) + { + // A property is "connected" if any pixel body (single Surface or the named Slabs) writes its field. + auto BodyWrites = [&Model](const TCHAR* Field) -> bool + { + if (Model.SurfaceBody.Contains(Field)) { return true; } + for (const FShaderLabSlab& Slab : Model.Slabs) { if (Slab.Body.Contains(Field)) { return true; } } + return false; + }; + + FCachedExprLens& Lens = static_cast(Shell); + Lens.CachedExpressionData = MakeUnique(FMaterialCachedExpressionData::EmptyData); + FMaterialCachedExpressionData& Data = *Lens.CachedExpressionData; + + bool bAny = false; + for (const FFieldToProperty& Map : GFieldToProperty) + { + if (BodyWrites(Map.Field)) { Data.SetPropertyConnected(Map.Property); bAny = true; } + } + // Deferred decals hard-fail on an empty descriptor (whole decal dropped); guarantee a minimal non-empty + // mask for the Decal domain even if field detection missed everything (e.g. body writes via a helper). + if (!bAny && Model.Settings.Domain == EShaderLabDomain::Decal) + { + Data.SetPropertyConnected(MP_DiffuseColor); + Data.SetPropertyConnected(MP_Normal); + } + } +} +#endif // !WITH_EDITOR + void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderLabModel& Model) { switch (Model.Settings.Domain) @@ -46,4 +121,11 @@ void FShaderLabRuntimeBuilder::ApplySettings(UMaterial& Material, const FShaderL { UE_LOG(LogShaderLabRuntime, Error, TEXT("ShaderLab runtime settings '%s': %s"), *Model.ShaderName, *Error); } + +#if !WITH_EDITOR + // Reflect the body's written properties into the shell's connectivity mask so runtime queries on the shell + // (notably deferred-decal GBuffer-channel selection) are correct even though the shell has no graph. See the + // mechanism comment above / AIDoc §2.8. Editor keeps the engine's graph-derived cached data untouched. + ShaderLabRuntime_Private::PopulateShellConnectivity(Material, Model); +#endif } diff --git a/Source/UShaderLab/Public/ShaderLabModel.h b/Source/UShaderLab/Public/ShaderLabModel.h index ec7f2ae..6a4b2a6 100644 --- a/Source/UShaderLab/Public/ShaderLabModel.h +++ b/Source/UShaderLab/Public/ShaderLabModel.h @@ -114,6 +114,15 @@ struct USHADERLAB_API FShaderLabProperty /** Built-in texture token ("white"/"black"/"normal"/"grey") or an asset path. */ FString TextureDefault; bool bStaticBoolDefault = false; + + /** + * Optional SamplerType override for texture properties (SL_PROPERTY(SamplerType=...)). Empty = infer + * (normal default token -> Normal, else Color). Stored as the engine token WITHOUT the SAMPLERTYPE_ + * prefix ("Color"/"LinearColor"/"Normal"/"Grayscale"/"LinearGrayscale"/"Masks"/"Alpha"/ + * "DistanceFieldFont"/"Data"). Virtual* tokens are rejected by the parser (use SL_VTSAMPLE). The graph + * builder maps this to EMaterialSamplerType (kept as a string here to keep the model Engine-free). + */ + FString SamplerType; }; /** @@ -130,6 +139,52 @@ struct USHADERLAB_API FShaderLabCollectionParam int32 Line = 0; }; +/** + * The sampling coordinate for a pre-sampled VT/RVT read (SL_VTSAMPLE/SL_RVTSAMPLE `UV=...`). Because a + * virtual texture cannot be sampled inside an opaque Custom node, the graph builder builds a real sample + * node whose Coordinates pin is fed by this UV; the float4/struct result is then wired into the body. + * TexCoord : UE_TextureCoordinate(Index) — a UMaterialExpressionTextureCoordinate node + * ValueBlock: an SL_VALUE block returning float2 — computed per-pixel HLSL feeding Coordinates + * World : derive UV from world position (RVT only) — leaves Coordinates unconnected + */ +struct USHADERLAB_API FShaderLabUV +{ + enum class EKind : uint8 { TexCoord, ValueBlock, World }; + EKind Kind = EKind::TexCoord; + int32 TexCoordIndex = 0; // when Kind == TexCoord + FName ValueBlockName; // when Kind == ValueBlock (references an SL_VALUE float2 block) +}; + +/** + * A `SL_VTSAMPLE(DefaultTexture="...", [SamplerType=Virtual*], UV=...) float4 ;` declaration: a + * pre-sampled streaming Virtual Texture read. The graph builder builds a UMaterialExpressionTextureSample- + * Parameter2D (SamplerType = a Virtual* type) sampling at UV; its RGBA output is wired into the body as a + * plain float4 input named . The artist rebinds the streaming VT asset on the MIC (it is a parameter). + */ +struct USHADERLAB_API FShaderLabVTSample +{ + FName Name; // in-HLSL identifier the body references (as a float4) + FString DefaultTexture; // asset path of the default streaming VT (empty = engine default) + FString SamplerType; // Virtual* token WITHOUT prefix (default "VirtualColor") + FShaderLabUV UV; + int32 Line = 0; +}; + +/** + * A `SL_RVTSAMPLE(VirtualTexture="...", MaterialType=..., UV=...) FShaderLabRVT ;` declaration: a + * pre-sampled Runtime Virtual Texture read. The graph builder builds a UMaterialExpressionRuntimeVirtual- + * TextureSampleParameter (MaterialType selects the layout/output pins) sampling at UV; each referenced + * `.` in the body is rewritten to a wired input carrying that output pin. RVT rebinds on MIC. + */ +struct USHADERLAB_API FShaderLabRVTSample +{ + FName Name; // struct-instance identifier the body reads members from + FString VirtualTexture; // asset path of the default URuntimeVirtualTexture (empty = null, compiles to constants) + FString MaterialType; // ERuntimeVirtualTextureMaterialType token (e.g. "BaseColor_Normal_Roughness") + FShaderLabUV UV; + int32 Line = 0; +}; + /** A parameter in a `Surface(...)`/`Vertex(...)` entry-point signature. */ struct USHADERLAB_API FShaderLabEntryParam { @@ -168,10 +223,15 @@ struct USHADERLAB_API FShaderLabSlab FShaderLabBsdfModifiers Modifiers; }; -/** A named `Value (){ return ; }` block, used as a topology mix factor. */ +/** + * A named `SL_VALUE() floatN (){ return ; }` block. Scalar (float) blocks are topology mix + * factors / material-level outputs; a float2 block may be referenced as a pre-sample UV (SL_VTSAMPLE/ + * SL_RVTSAMPLE `UV=`). ReturnType defaults to "float" (validated by the parser: float or float2). + */ struct USHADERLAB_API FShaderLabValue { FName Name; + FString ReturnType = TEXT("float"); FString Body; int32 BodyLine = 0; }; @@ -309,6 +369,10 @@ struct USHADERLAB_API FShaderLabModel TArray Properties; /** Material Parameter Collection reads (SL_COLLECTION). Global values, not per-instance knobs. */ TArray Collections; + /** Pre-sampled streaming Virtual Texture reads (SL_VTSAMPLE). Each yields a float4 body input. */ + TArray VTSamples; + /** Pre-sampled Runtime Virtual Texture reads (SL_RVTSAMPLE). Each yields an FShaderLabRVT struct. */ + TArray RVTSamples; TArray Includes; /** @@ -354,6 +418,15 @@ struct USHADERLAB_API FShaderLabModel FName RefractionValueName; FName PixelDepthOffsetValueName; + // Runtime Virtual Texture write (optional): a `SL_RVTOUTPUT() void (inout FShaderLabRVTOutput O){...}` + // block that fills the channels written into an RVT. Additive alongside the pixel entry (the material still + // shades normally on the mesh); backed by a UMaterialExpressionRuntimeVirtualTextureOutput custom output. + bool bHasRVTOutput = false; + FString RVTOutputBody; + int32 RVTOutputBodyLine = 0; + /** Name of the inout FShaderLabRVTOutput parameter (e.g. "O"). */ + FString RVTOutputParamName; + // Vertex stage (optional). bool bHasVertex = false; TArray VertexParams; diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp index b358f3a..1f125f0 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp @@ -29,6 +29,12 @@ #include "Engine/SpecularProfile.h" #include "Engine/ToonProfile.h" #include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureSampleParameter2D.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionRuntimeVirtualTextureSampleParameter.h" +#include "Materials/MaterialExpressionRuntimeVirtualTextureOutput.h" +#include "VT/RuntimeVirtualTextureEnum.h" +#include "VT/RuntimeVirtualTexture.h" #include "Materials/MaterialExpressionVectorParameter.h" #include "Materials/MaterialExpressionVertexInterpolator.h" #include "MaterialExpressionShaderLabParameterAnchor.h" @@ -213,6 +219,92 @@ namespace ShaderLabGraph return Tex; } + /** + * Map a non-virtual SamplerType token (SL_PROPERTY(SamplerType=...)) to EMaterialSamplerType. Returns + * false for an unknown or Virtual* token (Virtual belongs to SL_VTSAMPLE, not a plain texture property). + */ + static bool MapSamplerType(const FString& Token, EMaterialSamplerType& Out) + { + if (Token == TEXT("Color")) { Out = SAMPLERTYPE_Color; return true; } + if (Token == TEXT("LinearColor")) { Out = SAMPLERTYPE_LinearColor; return true; } + if (Token == TEXT("Grayscale")) { Out = SAMPLERTYPE_Grayscale; return true; } + if (Token == TEXT("LinearGrayscale")) { Out = SAMPLERTYPE_LinearGrayscale; return true; } + if (Token == TEXT("Alpha")) { Out = SAMPLERTYPE_Alpha; return true; } + if (Token == TEXT("Normal")) { Out = SAMPLERTYPE_Normal; return true; } + if (Token == TEXT("Masks")) { Out = SAMPLERTYPE_Masks; return true; } + if (Token == TEXT("DistanceFieldFont")) { Out = SAMPLERTYPE_DistanceFieldFont; return true; } + if (Token == TEXT("Data")) { Out = SAMPLERTYPE_Data; return true; } + return false; + } + + /** Map a Virtual* SamplerType token (SL_VTSAMPLE(SamplerType=...)) to EMaterialSamplerType. */ + static bool MapVirtualSamplerType(const FString& Token, EMaterialSamplerType& Out) + { + if (Token == TEXT("VirtualColor")) { Out = SAMPLERTYPE_VirtualColor; return true; } + if (Token == TEXT("VirtualGrayscale")) { Out = SAMPLERTYPE_VirtualGrayscale; return true; } + if (Token == TEXT("VirtualAlpha")) { Out = SAMPLERTYPE_VirtualAlpha; return true; } + if (Token == TEXT("VirtualNormal")) { Out = SAMPLERTYPE_VirtualNormal; return true; } + if (Token == TEXT("VirtualMasks")) { Out = SAMPLERTYPE_VirtualMasks; return true; } + if (Token == TEXT("VirtualLinearColor")) { Out = SAMPLERTYPE_VirtualLinearColor; return true; } + if (Token == TEXT("VirtualLinearGrayscale")) { Out = SAMPLERTYPE_VirtualLinearGrayscale; return true; } + return false; + } + + /** Map an SL_RVTSAMPLE MaterialType token to ERuntimeVirtualTextureMaterialType. */ + static bool MapRVTMaterialType(const FString& Token, ERuntimeVirtualTextureMaterialType& Out) + { + if (Token == TEXT("BaseColor")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor; return true; } + if (Token == TEXT("Mask4")) { Out = ERuntimeVirtualTextureMaterialType::Mask4; return true; } + if (Token == TEXT("BaseColor_Normal_Roughness")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Roughness; return true; } + if (Token == TEXT("BaseColor_Normal_Specular")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular; return true; } + if (Token == TEXT("BaseColor_Normal_Specular_YCoCg")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_YCoCg; return true; } + if (Token == TEXT("BaseColor_Normal_Specular_Mask_YCoCg")) { Out = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Specular_Mask_YCoCg; return true; } + if (Token == TEXT("WorldHeight")) { Out = ERuntimeVirtualTextureMaterialType::WorldHeight; return true; } + if (Token == TEXT("Displacement")) { Out = ERuntimeVirtualTextureMaterialType::Displacement; return true; } + return false; + } + + // FShaderLabRVT member -> RuntimeVirtualTextureSample output pin index + Custom-input HLSL type. + // Pin order mirrors UMaterialExpressionRuntimeVirtualTextureSample::InitOutputs (MaterialExpressions.cpp). + struct FRVTMemberDef { const TCHAR* Member; int32 PinIndex; ECustomMaterialOutputType OutType; }; + static const FRVTMemberDef GRVTMembers[] = { + { TEXT("BaseColor"), 0, CMOT_Float3 }, + { TEXT("Specular"), 1, CMOT_Float1 }, + { TEXT("Roughness"), 2, CMOT_Float1 }, + { TEXT("Normal"), 3, CMOT_Float3 }, + { TEXT("WorldHeight"), 4, CMOT_Float1 }, + { TEXT("Mask"), 5, CMOT_Float1 }, + { TEXT("Displacement"), 6, CMOT_Float1 }, + { TEXT("Mask4"), 7, CMOT_Float4 }, + }; + + // FShaderLabRVTOutput field -> RuntimeVirtualTextureOutput input pin + Custom-output HLSL type. + struct FRVTOutFieldDef { const TCHAR* Field; ECustomMaterialOutputType OutType; }; + static const FRVTOutFieldDef GRVTOutFields[] = { + { TEXT("BaseColor"), CMOT_Float3 }, + { TEXT("Specular"), CMOT_Float1 }, + { TEXT("Roughness"), CMOT_Float1 }, + { TEXT("Normal"), CMOT_Float3 }, + { TEXT("WorldHeight"), CMOT_Float1 }, + { TEXT("Opacity"), CMOT_Float1 }, + { TEXT("Mask"), CMOT_Float1 }, + { TEXT("Displacement"), CMOT_Float1 }, + { TEXT("Mask4"), CMOT_Float4 }, + }; + static FExpressionInput* GetRVTOutputPin(UMaterialExpressionRuntimeVirtualTextureOutput* N, const FString& F) + { + if (F == TEXT("BaseColor")) return &N->BaseColor; + if (F == TEXT("Specular")) return &N->Specular; + if (F == TEXT("Roughness")) return &N->Roughness; + if (F == TEXT("Normal")) return &N->Normal; + if (F == TEXT("WorldHeight")) return &N->WorldHeight; + if (F == TEXT("Opacity")) return &N->Opacity; + if (F == TEXT("Mask")) return &N->Mask; + if (F == TEXT("Displacement")) return &N->Displacement; + if (F == TEXT("Mask4")) return &N->Mask4; + return nullptr; + } + static EMaterialDomain MapDomain(EShaderLabDomain D) { switch (D) @@ -500,6 +592,20 @@ namespace ShaderLabGraph int32 OutputIndex = 0; }; + /** + * Pre-sampled VT/RVT read nodes available to a (pixel) body, keyed by the DSL name. Because a virtual + * texture cannot be sampled inside an opaque Custom node, these real sample nodes are built up-front and + * their outputs wired into the body as inputs by PrepareBody: a VT `` becomes a float4 input; an + * RVT `.` is rewritten to a per-member input carrying the matching output pin. Null maps + * (vertex/interpolator bodies) mean "no VT/RVT here" — a reference there is rejected as pixel-only. + */ + struct FSampleNodes + { + const TMap* VT = nullptr; // name -> UMaterialExpressionTextureSampleParameter2D (Virtual*) + const TMap* RVT = nullptr; // name -> UMaterialExpressionRuntimeVirtualTextureSampleParameter + bool HasAny() const { return (VT && VT->Num() > 0) || (RVT && RVT->Num() > 0); } + }; + /** Split a call's argument text into trimmed, top-level comma-separated literals. */ static TArray SplitArgs(const FString& ArgsRaw) { @@ -1295,6 +1401,7 @@ namespace ShaderLabGraph const FLibraryEmit& Emit, TArray& OutWires, TSet& OutReqProps, const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, TArray& OutErrors) { // Which library functions does this body call? (Scan the original body — function names are not @@ -1305,6 +1412,67 @@ namespace ShaderLabGraph CollectCalledFunctions(InOutBody, *Emit.Program, Called); } + // Pre-sampled VT / RVT reads (pixel-only): wire each referenced sample node's output into the body. + // Done before EmitIntrinsics so the RVT member rewrite doesn't disturb intrinsic column padding of + // UE_ substitutions (RVT rewrite only touches `.`, never `UE_...`). + if (Samples.VT || Samples.RVT) + { + if (Stage != EShaderLabIntrinsicFrequency::PixelOnly) + { + // Reject VT/RVT reads outside a pixel body (vertex/interpolator). Only flag when actually used. + auto FirstUsed = [&](const TMap* Map, const TCHAR* What) -> bool + { + if (!Map) { return false; } + for (const TPair& Pair : *Map) + { + if (ReferencesToken(InOutBody, Pair.Key.ToString())) + { + OutErrors.Add(FString::Printf(TEXT("%s(%d,1): error: %s read '%s' is only allowed in a pixel body"), + *SrcPath, FMath::Max(BodyLine - 1, 1), What, *Pair.Key.ToString())); + return true; + } + } + return false; + }; + bool bMisuse = FirstUsed(Samples.VT, TEXT("Virtual texture")); + bMisuse |= FirstUsed(Samples.RVT, TEXT("Runtime virtual texture")); + if (bMisuse) { return false; } + } + else + { + // Streaming VT: `` used verbatim as a float4 -> wire the sample node's RGBA output (pin 5). + if (Samples.VT) + { + for (const TPair& Pair : *Samples.VT) + { + if (ReferencesToken(InOutBody, Pair.Key.ToString())) + { + OutWires.Add(FIntrinsicWire{ Pair.Key, Pair.Value, /*RGBA*/ 5 }); + } + } + } + // RVT: rewrite `.` -> `SLRVT__` and wire the matching output pin. + if (Samples.RVT) + { + for (const TPair& Pair : *Samples.RVT) + { + const FString NameStr = Pair.Key.ToString(); + for (const FRVTMemberDef& M : GRVTMembers) + { + const FString Access = NameStr + TEXT(".") + M.Member; + if (!ReferencesToken(InOutBody, Access)) + { + continue; + } + const FString InputVar = FString(TEXT("SLRVT_")) + NameStr + TEXT("_") + M.Member; + InOutBody.ReplaceInline(*Access, *InputVar, ESearchCase::CaseSensitive); + OutWires.Add(FIntrinsicWire{ FName(*InputVar), Pair.Value, M.PinIndex }); + } + } + } + } + } + // The body's own UE_ intrinsics FIRST: EmitIntrinsics space-pads its substitutions so compile-error // columns map accurately, and it reports offsets against THIS body — so it must run before callee-arg // injection (which inserts text mid-line and would shift those columns). @@ -1695,6 +1863,7 @@ namespace ShaderLabGraph const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, const TMap& PropertyNodes, const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, bool bAllowMaterialOutputs, int32& IoY, TArray& OutErrors) { UMaterialExpression* Bsdf = Desc.MakeNode(Material, IoY); @@ -1739,7 +1908,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray Wires; TSet ReqProps; - if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, OutErrors)) + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors)) { return nullptr; } @@ -1844,6 +2013,7 @@ namespace ShaderLabGraph const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, const TMap& PropertyNodes, const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, int32& IoY, TArray& OutErrors) { UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); @@ -1854,7 +2024,7 @@ namespace ShaderLabGraph FString Body = InBody; TArray Wires; TSet ReqProps; - if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, OutErrors)) + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors)) { return false; } @@ -1900,23 +2070,105 @@ namespace ShaderLabGraph return true; } + /** + * Build the Runtime Virtual Texture WRITE output (SL_RVTOUTPUT). Mirrors BuildEmissiveEntry: a Custom node + * runs the body filling an FShaderLabRVTOutput, its written fields become AdditionalOutputs, and each is + * wired to the matching pin of a UMaterialExpressionRuntimeVirtualTextureOutput (a custom output whose mere + * presence makes the material write into an RVT when a mesh renders into one). Unwritten fields keep the + * node's own defaults. + */ + static bool BuildRVTOutput( + UMaterial& Material, const FString& OutParamName, const FString& InBody, int32 BodyLine, + const FShaderLabModel& Model, const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, + const TMap& PropertyNodes, + const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, int32& IoY, TArray& OutErrors) + { + UMaterialExpressionRuntimeVirtualTextureOutput* RVTOut = + NewExpr(Material, IoY, -600); + + TArray Used; + for (const FRVTOutFieldDef& F : GRVTOutFields) + { + if (ReferencesToken(InBody, OutParamName + TEXT(".") + F.Field)) + { + Used.Add(&F); + } + } + if (Used.Num() == 0) + { + return true; // Empty body: the RVT output node stays at its pin defaults. + } + + UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); + Custom->Description = TEXT("ShaderLab RVT Output"); + Custom->OutputType = CMOT_Float1; + AddIncludes(*Custom, Model); + + FString Body = InBody; + TArray Wires; + TSet ReqProps; + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors)) + { + return false; + } + for (const FIntrinsicWire& Wire : Wires) + { + FCustomInput In; + In.InputName = Wire.InputName; + In.Input.Connect(Wire.OutputIndex, Wire.Expr); + Custom->Inputs.Add(In); + } + WirePropInputs(*Custom, Program, PropertyNodes, InBody, ReqProps, Model.Collections); + + const FString SrcPath = MakeLineDirectivePath(Model.SourceFilePath); + FString Code = FString::Printf(TEXT("FShaderLabRVTOutput %s = ShaderLabDefaultRVTOutput();\n{\n%s}\n"), + *OutParamName, *WrapBodyWithLineMapping(Body, BodyLine, SrcPath)); + + int32 OutputIndex = 1; + TArray> Outs; + for (const FRVTOutFieldDef* F : Used) + { + FCustomOutput Out; + Out.OutputName = FName(*(FString(TEXT("SLO_")) + F->Field)); + Out.OutputType = F->OutType; + Custom->AdditionalOutputs.Add(Out); + Code += FString::Printf(TEXT("SLO_%s = %s.%s;\n"), F->Field, *OutParamName, F->Field); + Outs.Add(TPair(F, OutputIndex)); + ++OutputIndex; + } + Code += TEXT("return 0.0f;\n"); + Custom->Code = Code; + Custom->RebuildOutputs(); + + for (const TPair& Pair : Outs) + { + if (FExpressionInput* Pin = GetRVTOutputPin(RVTOut, Pair.Key->Field)) + { + Pin->Connect(Pair.Value, Custom); + } + } + return true; + } + /** Build a Custom node whose return value is the scalar Value-block body. Output 0 is the scalar. */ static UMaterialExpressionCustom* BuildValueNode( UMaterial& Material, const FShaderLabValue& Value, const FShaderLabModel& Model, const FShaderLabResolvedProgram& Program, const FLibraryEmit& Emit, const TMap& PropertyNodes, const TMap& InterpByName, TSet& UsedInterps, + const FSampleNodes& Samples, int32& IoY, TArray& OutErrors) { UMaterialExpressionCustom* Custom = NewExpr(Material, IoY, -300); Custom->Description = FString::Printf(TEXT("ShaderLab Value %s"), *Value.Name.ToString()); - Custom->OutputType = CMOT_Float1; + Custom->OutputType = (Value.ReturnType == TEXT("float2")) ? CMOT_Float2 : CMOT_Float1; AddIncludes(*Custom, Model); FString Body = Value.Body; TArray Wires; TSet ReqProps; - if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, OutErrors)) + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, InterpByName, UsedInterps, Samples, OutErrors)) { return nullptr; } @@ -1957,7 +2209,8 @@ namespace ShaderLabGraph TSet ReqProps; const TMap EmptyInterp; TSet IgnoredUsed; - if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, EmptyInterp, IgnoredUsed, OutErrors)) + const FSampleNodes NoSamples; // VT/RVT reads are pixel-only; not available in a vertex-frequency interpolator body. + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, Body, Interp.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, Wires, ReqProps, EmptyInterp, IgnoredUsed, NoSamples, OutErrors)) { return nullptr; } @@ -2418,7 +2671,15 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode E->SortPriority = Prop.SortPriority; bool bIsNormal = false; E->Texture = ResolveDefaultTexture(Prop.TextureDefault, bIsNormal); - E->SamplerType = bIsNormal ? SAMPLERTYPE_Normal : SAMPLERTYPE_Color; + // Explicit SamplerType wins; otherwise infer (normal default token -> Normal, else Color). + EMaterialSamplerType Sampler = bIsNormal ? SAMPLERTYPE_Normal : SAMPLERTYPE_Color; + if (!Prop.SamplerType.IsEmpty()) + { + const bool bMapped = MapSamplerType(Prop.SamplerType, Sampler); + checkf(bMapped, TEXT("ShaderLab property '%s': invalid SamplerType '%s' (parser should have rejected)."), + *Prop.Name.ToString(), *Prop.SamplerType); + } + E->SamplerType = Sampler; Node.Expr = E; Node.bIsTexture = true; break; @@ -2435,7 +2696,14 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode E->ParameterName = Prop.Name; E->Group = FName(*Prop.Group); E->SortPriority = Prop.SortPriority; - E->SamplerType = SAMPLERTYPE_Color; + EMaterialSamplerType Sampler = SAMPLERTYPE_Color; + if (!Prop.SamplerType.IsEmpty()) + { + const bool bMapped = MapSamplerType(Prop.SamplerType, Sampler); + checkf(bMapped, TEXT("ShaderLab property '%s': invalid SamplerType '%s' (parser should have rejected)."), + *Prop.Name.ToString(), *Prop.SamplerType); + } + E->SamplerType = Sampler; if (Prop.TextureDefault.StartsWith(TEXT("/"))) { UTexture* DefaultTex = LoadObject(nullptr, *Prop.TextureDefault); @@ -2507,12 +2775,123 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode InterpByName.Add(Interp.Name, Node); } + // 1c) Pre-sampled VT / RVT read nodes (SL_VTSAMPLE / SL_RVTSAMPLE). A virtual texture cannot be sampled + // inside a Custom node, so we build a real sample node here (sampling at the author-chosen UV) and let + // PrepareBody wire its output(s) into the pixel bodies. Only build a node when a pixel body references it. + TMap VTNodeByName; + TMap RVTNodeByName; + { + auto ReferencedInPixelBodies = [&Model](const FString& NameStr) -> bool + { + if (Model.bHasSurface && ReferencesToken(Model.SurfaceBody, NameStr)) { return true; } + for (const FShaderLabSlab& Slab : Model.Slabs) { if (ReferencesToken(Slab.Body, NameStr)) { return true; } } + for (const FShaderLabValue& Value : Model.Values) { if (ReferencesToken(Value.Body, NameStr)) { return true; } } + if (Model.bHasRVTOutput && ReferencesToken(Model.RVTOutputBody, NameStr)) { return true; } + return false; + }; + + // Build the coordinate expression feeding a sample node's Coordinates pin. Returns true on success; + // OutCoord is null for UV=World (leave Coordinates unconnected — the node derives UV from world pos). + auto BuildUVNode = [&](const FShaderLabUV& UV, const TCHAR* What, FName Owner, UMaterialExpression*& OutCoord) -> bool + { + OutCoord = nullptr; + switch (UV.Kind) + { + case FShaderLabUV::EKind::TexCoord: + { + UMaterialExpressionTextureCoordinate* TC = NewExpr(Material, ParamY, -800); + TC->CoordinateIndex = UV.TexCoordIndex; + OutCoord = TC; + return true; + } + case FShaderLabUV::EKind::World: + return true; // Coordinates unconnected. + case FShaderLabUV::EKind::ValueBlock: + { + const FShaderLabValue* Block = Model.Values.FindByPredicate( + [&](const FShaderLabValue& V) { return V.Name == UV.ValueBlockName; }); + if (!Block) + { + OutErrors.Add(FString::Printf(TEXT("%s '%s': UV references unknown Value block '%s'"), + What, *Owner.ToString(), *UV.ValueBlockName.ToString())); + return false; + } + if (Block->ReturnType != TEXT("float2")) + { + OutErrors.Add(FString::Printf(TEXT("%s '%s': UV Value block '%s' must return float2 (got '%s')"), + What, *Owner.ToString(), *UV.ValueBlockName.ToString(), *Block->ReturnType)); + return false; + } + const FSampleNodes NoSamples; // A UV block is itself a coordinate source; it must not sample VT/RVT. + UMaterialExpressionCustom* UVNode = BuildValueNode( + Material, *Block, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, NoSamples, ParamY, OutErrors); + if (!UVNode) { return false; } + OutCoord = UVNode; + return true; + } + } + return false; + }; + + for (const FShaderLabVTSample& VT : Model.VTSamples) + { + if (!ReferencedInPixelBodies(VT.Name.ToString())) { continue; } + EMaterialSamplerType Sampler = SAMPLERTYPE_VirtualColor; + const FString Token = VT.SamplerType.IsEmpty() ? FString(TEXT("VirtualColor")) : VT.SamplerType; + const bool bMapped = MapVirtualSamplerType(Token, Sampler); + checkf(bMapped, TEXT("SL_VTSAMPLE '%s': invalid virtual SamplerType '%s' (parser should have rejected)."), + *VT.Name.ToString(), *Token); + + UMaterialExpressionTextureSampleParameter2D* S = NewExpr(Material, ParamY, -1000); + S->ParameterName = VT.Name; + S->SamplerType = Sampler; + if (!VT.DefaultTexture.IsEmpty()) + { + // Graceful: a streaming VT default is often a project asset created later; if absent, leave null + // (the artist binds it on the MIC). Unlike array/volume defaults, this is not a hard contract. + S->Texture = LoadObject(nullptr, *VT.DefaultTexture); + } + UMaterialExpression* Coord = nullptr; + if (!BuildUVNode(VT.UV, TEXT("SL_VTSAMPLE"), VT.Name, Coord)) { return false; } + if (Coord) { S->Coordinates.Connect(0, Coord); } + VTNodeByName.Add(VT.Name, S); + } + + for (const FShaderLabRVTSample& RVT : Model.RVTSamples) + { + if (!ReferencedInPixelBodies(RVT.Name.ToString())) { continue; } + ERuntimeVirtualTextureMaterialType MatType = ERuntimeVirtualTextureMaterialType::BaseColor_Normal_Roughness; + const bool bMapped = MapRVTMaterialType(RVT.MaterialType, MatType); + checkf(bMapped, TEXT("SL_RVTSAMPLE '%s': invalid MaterialType '%s' (parser should have rejected)."), + *RVT.Name.ToString(), *RVT.MaterialType); + + UMaterialExpressionRuntimeVirtualTextureSampleParameter* S = NewExpr(Material, ParamY, -1000); + S->ParameterName = RVT.Name; + S->MaterialType = MatType; + if (!RVT.VirtualTexture.IsEmpty()) + { + // Graceful: the RVT asset is a project asset (created with the level); if absent, leave null + // (the sample compiles to constants; the artist binds the RVT on the MIC). + S->VirtualTexture = LoadObject(nullptr, *RVT.VirtualTexture); + } + // Outputs (BaseColor..Mask4, all 8 pins) are populated by the node constructor's InitOutputs(); + // they exist regardless of MaterialType, so member->pin indices in GRVTMembers are stable. + UMaterialExpression* Coord = nullptr; + if (!BuildUVNode(RVT.UV, TEXT("SL_RVTSAMPLE"), RVT.Name, Coord)) { return false; } + if (Coord) { S->Coordinates.Connect(0, Coord); } + RVTNodeByName.Add(RVT.Name, S); + } + } + FSampleNodes Samples; + Samples.VT = &VTNodeByName; + Samples.RVT = &RVTNodeByName; + // 2) Build the pixel stage and connect it to FrontMaterial (what makes it a Substrate material). if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::PostProcess) { // PostProcess domain: Color -> EmissiveColor, Opacity -> Opacity (no Substrate slab). if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabPostProcess"), TEXT("ShaderLabDefaultPostProcess"), - SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors)) + SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors)) { return false; } @@ -2520,7 +2899,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode else if (Model.bHasSurface && Model.SurfaceEntry == EShaderLabEntry::UI) { if (!BuildEmissiveEntry(Material, *EditorOnly, TEXT("FShaderLabUI"), TEXT("ShaderLabDefaultUI"), - SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors)) + SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors)) { return false; } @@ -2532,7 +2911,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode UMaterialExpression* Slab = BuildBsdf( *FindBsdfDesc(EShaderLabBsdfType::Slab), Model.SurfaceModifiers, Material, *EditorOnly, SurfaceOutParam->Name, Model.SurfaceBody, Model.SurfaceBodyLine, - Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors); + Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, /*bAllowMaterialOutputs*/ true, ParamY, OutErrors); if (!Slab) { return false; @@ -2561,7 +2940,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode UMaterialExpression* Slab = BuildBsdf( *Desc, SlabDecl.Modifiers, Material, *EditorOnly, SlabDecl.OutParamName, SlabDecl.Body, SlabDecl.BodyLine, - Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors); + Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, /*bAllowMaterialOutputs*/ false, ParamY, OutErrors); if (!Slab) { return false; @@ -2578,7 +2957,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode return false; } UMaterialExpressionCustom* ValueNode = BuildValueNode( - Material, ValueDecl, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, ParamY, OutErrors); + Material, ValueDecl, Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors); if (!ValueNode) { return false; @@ -2666,7 +3045,8 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode // Vertex stage: UE_Interpolator is pixel-only, so pass an empty interpolator map (rejected there). const TMap EmptyInterp; TSet IgnoredUsedInterps; - if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, VtxIntrinsicWires, VtxReqProps, EmptyInterp, IgnoredUsedInterps, OutErrors)) + const FSampleNodes NoSamples; // VT/RVT reads are pixel-only; not available in a vertex body. + if (!PrepareBody(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), Emit, VtxIntrinsicWires, VtxReqProps, EmptyInterp, IgnoredUsedInterps, NoSamples, OutErrors)) { return false; } @@ -2722,6 +3102,17 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode } } + // 3b) Optional Runtime Virtual Texture WRITE (SL_RVTOUTPUT): an additive custom output alongside the + // pixel entry. Uses the same VT/RVT sample context as the pixel bodies (a write body may also read). + if (Model.bHasRVTOutput) + { + if (!BuildRVTOutput(Material, Model.RVTOutputParamName, Model.RVTOutputBody, Model.RVTOutputBodyLine, + Model, Program, Emit, PropertyNodes, InterpByName, UsedInterps, Samples, ParamY, OutErrors)) + { + return false; + } + } + // Contract: every declared interpolator must be consumed (read via UE_Interpolator, or used as a // topology mix factor). A dead interpolator would silently waste a scarce interpolant slot. for (const FShaderLabInterpolator& Interp : Model.Interpolators)