mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-16 07:14:38 +00:00
Support sample type and virtual texture
This commit is contained in:
@@ -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