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

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