mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Support sample type and virtual texture
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
55
Shaders/Private/UEFunctions/TextureSample.ush
Normal file
55
Shaders/Private/UEFunctions/TextureSample.ush
Normal 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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user