Support sample type and virtual texture

This commit is contained in:
Eragon-Brisingr
2026-07-05 02:16:11 +08:00
parent adaafb0709
commit 799848f87f
9 changed files with 977 additions and 90 deletions

View File

@@ -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
// `<Name>.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

View File

@@ -38,6 +38,25 @@
// float3 WindVector;
#define SL_COLLECTION(...)
// Pre-sampled streaming Virtual Texture read. Precedes a `float4 <Name>;` declaration; the body uses <Name>
// 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 <Name>;` declaration; the body reads
// channels via <Name>.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 <Name>(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 = <index> (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<N> (a texcoord set) | <SL_VALUE float2 block name> | World (RVT only)
//
// Topology operators (used inside SL_FRONTMATERIAL):
// VerticalLayer(Top, Base, Thickness) | HorizontalMix(Background, Foreground, Mix)

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -514,7 +514,7 @@ using namespace ShaderLabParser_Private;
namespace
{
/** Read `<retType> <name> ( <sig> ) { <body> }` after a marker macro. Returns false on error. */
bool ParseAnnotatedFunction(FScanner& S, FString& OutName, FString& OutSigInner, FString& OutBody, int32& OutBodyLine)
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 = <spec>` value: `TexCoord<N>` (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<N>' 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<N>, 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 <Name>; — 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 <Name>;` 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 <Name>; — 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 <Name>;` 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 = <ERuntimeVirtualTextureMaterialType>"), *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 <Name>(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<FShaderLabEntryParam> 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<FShaderLabEntryParam> 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; }

View File

@@ -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.<Field>") -> 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<FCachedExprLens&>(Shell);
Lens.CachedExpressionData = MakeUnique<FMaterialCachedExpressionData>(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
}

View File

@@ -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 <Name>;` 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 <Name>. 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 <Name>;` 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
* `<Name>.<Member>` 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 <Name>(){ return <float HLSL>; }` block, used as a topology mix factor. */
/**
* A named `SL_VALUE() floatN <Name>(){ return <HLSL>; }` 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=<Name>`). 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<FShaderLabProperty> Properties;
/** Material Parameter Collection reads (SL_COLLECTION). Global values, not per-instance knobs. */
TArray<FShaderLabCollectionParam> Collections;
/** Pre-sampled streaming Virtual Texture reads (SL_VTSAMPLE). Each yields a float4 body input. */
TArray<FShaderLabVTSample> VTSamples;
/** Pre-sampled Runtime Virtual Texture reads (SL_RVTSAMPLE). Each yields an FShaderLabRVT struct. */
TArray<FShaderLabRVTSample> RVTSamples;
TArray<FString> Includes;
/**
@@ -354,6 +418,15 @@ struct USHADERLAB_API FShaderLabModel
FName RefractionValueName;
FName PixelDepthOffsetValueName;
// Runtime Virtual Texture write (optional): a `SL_RVTOUTPUT() void <Name>(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<FShaderLabEntryParam> VertexParams;

View File

@@ -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 `<Name>` becomes a float4 input; an
* RVT `<Name>.<Member>` 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<FName, UMaterialExpression*>* VT = nullptr; // name -> UMaterialExpressionTextureSampleParameter2D (Virtual*)
const TMap<FName, UMaterialExpression*>* 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<FString> SplitArgs(const FString& ArgsRaw)
{
@@ -1295,6 +1401,7 @@ namespace ShaderLabGraph
const FLibraryEmit& Emit,
TArray<FIntrinsicWire>& OutWires, TSet<FName>& OutReqProps,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples,
TArray<FString>& 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 `<Name>.<Member>`, 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<FName, UMaterialExpression*>* Map, const TCHAR* What) -> bool
{
if (!Map) { return false; }
for (const TPair<FName, UMaterialExpression*>& 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: `<Name>` used verbatim as a float4 -> wire the sample node's RGBA output (pin 5).
if (Samples.VT)
{
for (const TPair<FName, UMaterialExpression*>& Pair : *Samples.VT)
{
if (ReferencesToken(InOutBody, Pair.Key.ToString()))
{
OutWires.Add(FIntrinsicWire{ Pair.Key, Pair.Value, /*RGBA*/ 5 });
}
}
}
// RVT: rewrite `<Name>.<Member>` -> `SLRVT_<Name>_<Member>` and wire the matching output pin.
if (Samples.RVT)
{
for (const TPair<FName, UMaterialExpression*>& 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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples,
bool bAllowMaterialOutputs, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpression* Bsdf = Desc.MakeNode(Material, IoY);
@@ -1739,7 +1908,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
TSet<FName> 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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(Material, IoY, -300);
@@ -1854,7 +2024,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
TSet<FName> 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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples, int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionRuntimeVirtualTextureOutput* RVTOut =
NewExpr<UMaterialExpressionRuntimeVirtualTextureOutput>(Material, IoY, -600);
TArray<const FRVTOutFieldDef*> 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<UMaterialExpressionCustom>(Material, IoY, -300);
Custom->Description = TEXT("ShaderLab RVT Output");
Custom->OutputType = CMOT_Float1;
AddIncludes(*Custom, Model);
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
TSet<FName> 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<TPair<const FRVTOutFieldDef*, int32>> 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<const FRVTOutFieldDef*, int32>(F, OutputIndex));
++OutputIndex;
}
Code += TEXT("return 0.0f;\n");
Custom->Code = Code;
Custom->RebuildOutputs();
for (const TPair<const FRVTOutFieldDef*, int32>& 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<FName, FParamNode>& PropertyNodes,
const TMap<FName, UMaterialExpression*>& InterpByName, TSet<FName>& UsedInterps,
const FSampleNodes& Samples,
int32& IoY, TArray<FString>& OutErrors)
{
UMaterialExpressionCustom* Custom = NewExpr<UMaterialExpressionCustom>(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<FIntrinsicWire> Wires;
TSet<FName> 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<FName> ReqProps;
const TMap<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> 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<UTexture>(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<FName, UMaterialExpression*> VTNodeByName;
TMap<FName, UMaterialExpression*> 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<UMaterialExpressionTextureCoordinate>(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<UMaterialExpressionTextureSampleParameter2D>(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<UTexture>(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<UMaterialExpressionRuntimeVirtualTextureSampleParameter>(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<URuntimeVirtualTexture>(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<FName, UMaterialExpression*> EmptyInterp;
TSet<FName> 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)