Enhance VSCode coding (Use shader-validator VSCode plugin

This commit is contained in:
Eragon-Brisingr
2026-07-01 15:10:51 +08:00
parent 0845f71aea
commit 4c336ed24b
21 changed files with 976 additions and 346 deletions

View File

@@ -21,10 +21,12 @@
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "ShaderLabIntrinsicRegistry.h"
#include "ShaderLabRuntimeBuilder.h"
#include "UObject/Class.h"
#include "ShaderLabSettingsApplier.h"
#include "UObject/UObjectGlobals.h"
#define SHADERLAB_COMMON_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabCommon.ush")
#define SHADERLAB_FUNCTIONS_INCLUDE TEXT("/Plugin/ShaderLab/Private/ShaderLabUEFunctions.ush")
namespace ShaderLabGraph
{
@@ -238,16 +240,38 @@ namespace ShaderLabGraph
}
/**
* Scan a body for `UE::Name(args)` intrinsic calls, create the backing expression node for each
* Scan a body for `UE_Name(args)` intrinsic calls, create the backing expression node for each
* unique (name,args), collect the resulting Custom-node inputs, and rewrite the body so each call
* becomes its input variable — space-padded to the original call's length so line/column layout is
* preserved (keeps `#line` compile-error mapping accurate). Returns false (and fills OutErrors) on
* an unknown intrinsic, a stage/usage violation, or a bad argument.
*/
/** True if `Arg` is a numeric literal (the only non-enum arg an intrinsic's const config accepts). */
static bool IsNumericLiteral(const FString& Arg)
{
const FString T = Arg.TrimStartAndEnd();
if (T.IsEmpty())
{
return false;
}
bool bAnyDigit = false;
for (int32 i = 0; i < T.Len(); ++i)
{
const TCHAR C = T[i];
if (FChar::IsDigit(C)) { bAnyDigit = true; }
else if (C == TEXT('.') || C == TEXT('+') || C == TEXT('-')
|| C == TEXT('e') || C == TEXT('E') || C == TEXT('f') || C == TEXT('F')) { /* allowed */ }
else { return false; }
}
return bAnyDigit;
}
static bool EmitIntrinsics(
UMaterial& Material,
EShaderLabIntrinsicFrequency Stage,
FString& InOutBody,
int32 BodyLine,
const FString& SrcPath,
TArray<FIntrinsicWire>& OutWires,
TArray<FString>& OutErrors)
{
@@ -262,16 +286,30 @@ namespace ShaderLabGraph
auto IsIdent = [](TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); };
// Format a `path.usl(line,col): error: ` prefix from a body offset, so intrinsic diagnostics map
// to the real .usl location and flow through the same clickable/cook-failing path as compile errors.
auto Loc = [&Body, BodyLine, &SrcPath](int32 Offset) -> FString
{
int32 Line = BodyLine;
int32 Col = 1;
for (int32 p = 0; p < Offset && p < Body.Len(); ++p)
{
if (Body[p] == TEXT('\n')) { ++Line; Col = 1; }
else { ++Col; }
}
return FString::Printf(TEXT("%s(%d,%d): error: "), *SrcPath, Line, Col);
};
int32 i = 0;
while (i < Len)
{
const bool bBoundary = (i == 0) || !IsIdent(Body[i - 1]);
if (bBoundary && i + 4 <= Len &&
Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT(':') && Body[i + 3] == TEXT(':'))
if (bBoundary && i + 3 <= Len &&
Body[i] == TEXT('U') && Body[i + 1] == TEXT('E') && Body[i + 2] == TEXT('_'))
{
int32 j = i + 4;
int32 j = i + 3;
while (j < Len && IsIdent(Body[j])) { ++j; }
const FString Name = Body.Mid(i + 4, j - (i + 4));
const FString Name = Body.Mid(i + 3, j - (i + 3));
int32 k = j;
while (k < Len && FChar::IsWhitespace(Body[k])) { ++k; }
if (!Name.IsEmpty() && k < Len && Body[k] == TEXT('('))
@@ -288,6 +326,20 @@ namespace ShaderLabGraph
{
const FString ArgsRaw = Body.Mid(k + 1, m - (k + 1));
const int32 CallLen = (m + 1) - i;
// Only registered node-intrinsics are rewritten into material-expression inputs.
// Any other `UE_X(...)` is left for the shader compiler: it's an HLSL library
// function (e.g. UE_Noise from ShaderLabFunctions.ush, auto-included) or a typo.
// Copy just the `UE_Name` identifier and keep scanning from the '(' — so any
// intrinsic nested in the arguments (e.g. UE_Noise(UE_WorldPosition(), ...)) still
// gets rewritten.
if (!Registry.Find(FName(*Name)))
{
Result += Body.Mid(i, j - i);
i = j;
continue;
}
const FString ArgSig = MakeArgSig(ArgsRaw);
const FString Key = Name + TEXT("|") + ArgSig;
@@ -299,29 +351,59 @@ namespace ShaderLabGraph
else
{
const FShaderLabIntrinsicDesc* Desc = Registry.Find(FName(*Name));
if (!Desc)
if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
{
OutErrors.Add(FString::Printf(TEXT("Unknown intrinsic 'UE::%s'"), *Name));
bOk = false;
}
else if (Desc->Frequency == EShaderLabIntrinsicFrequency::PixelOnly && Stage == EShaderLabIntrinsicFrequency::VertexOnly)
{
OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s' is pixel-only and cannot be used in a Vertex body"), *Name));
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is pixel-only and cannot be used in a Vertex body"), *Name));
bOk = false;
}
else if (Desc->Frequency == EShaderLabIntrinsicFrequency::VertexOnly && Stage == EShaderLabIntrinsicFrequency::PixelOnly)
{
OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s' is vertex-only and cannot be used in a pixel body"), *Name));
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' is vertex-only and cannot be used in a pixel body"), *Name));
bOk = false;
}
else
{
// Usage is per-instance, so intrinsics emit their node with no base-level usage gate.
FString MakeError;
UMaterialExpression* Expr = Desc->MakeNode(Material, SplitArgs(ArgsRaw), MakeError);
if (!Expr)
// Config args are baked into the node at graph-build time, so they must be
// compile-time constants (numeric literals, or the enum's token names) — a
// variable can't configure a node field. Reject non-literals instead of
// silently coercing them (e.g. Atoi("myVar") -> 0).
const TArray<FString> CallArgs = SplitArgs(ArgsRaw);
bool bArgsOk = true;
if (CallArgs.Num() > Desc->Params.Num())
{
OutErrors.Add(FString::Printf(TEXT("Intrinsic 'UE::%s': %s"), *Name,
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' takes at most %d argument(s), got %d"),
*Name, Desc->Params.Num(), CallArgs.Num()));
bArgsOk = false;
}
for (int32 a = 0; bArgsOk && a < CallArgs.Num(); ++a)
{
const FShaderLabIntrinsicParam& P = Desc->Params[a];
if (P.Enum)
{
if (P.Enum->GetValueByNameString(CallArgs[a]) == INDEX_NONE)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a %s token, got '%s'"),
*Name, *P.Name, *P.Enum->GetName(), *CallArgs[a]));
bArgsOk = false;
}
}
else if (!IsNumericLiteral(CallArgs[a]))
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s' argument '%s' must be a compile-time constant, got '%s'"),
*Name, *P.Name, *CallArgs[a]));
bArgsOk = false;
}
}
FString MakeError;
UMaterialExpression* Expr = bArgsOk ? Desc->MakeNode(Material, CallArgs, MakeError) : nullptr;
if (!bArgsOk)
{
bOk = false;
}
else if (!Expr)
{
OutErrors.Add(Loc(i) + FString::Printf(TEXT("intrinsic 'UE_%s': %s"), *Name,
MakeError.IsEmpty() ? TEXT("failed to create node") : *MakeError));
bOk = false;
}
@@ -358,10 +440,13 @@ namespace ShaderLabGraph
bool bIsTexture = false;
};
/** Add the shared struct include plus any user `Includes { }` paths to a generated Custom node. */
/** Add the shared struct + function-library includes plus any user `Includes { }` paths. */
static void AddIncludes(UMaterialExpressionCustom& Custom, const FShaderLabModel& Model)
{
Custom.IncludeFilePaths.Add(SHADERLAB_COMMON_INCLUDE);
// The function library provides the UE_ HLSL helpers (UE_Noise, UE_RotateAboutAxis, ...) that
// are left verbatim in the body (not rewritten into nodes).
Custom.IncludeFilePaths.Add(SHADERLAB_FUNCTIONS_INCLUDE);
for (const FString& Include : Model.Includes)
{
if (!Include.IsEmpty())
@@ -407,7 +492,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return nullptr;
}
@@ -499,7 +584,7 @@ namespace ShaderLabGraph
FString Body = InBody;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return false;
}
@@ -572,7 +657,7 @@ namespace ShaderLabGraph
FString Body = Value.Body;
TArray<FIntrinsicWire> Wires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Wires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::PixelOnly, Body, Value.BodyLine, MakeLineDirectivePath(Model.SourceFilePath), Wires, OutErrors))
{
return nullptr;
}
@@ -739,7 +824,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
return false;
}
// Per-pixel context is read via UE::* intrinsics, so the Surface entry takes just the output
// Per-pixel context is read via UE_* intrinsics, so the Surface entry takes just the output
// struct: `Surface(inout FShaderLabSurface S)`. For multi-slab there is no Surface param.
const FShaderLabEntryParam* SurfaceOutParam = Model.bHasSurface && Model.SurfaceParams.Num() > 0
? &Model.SurfaceParams.Last() : nullptr;
@@ -986,7 +1071,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
if (!ConnectMaterialOutput(EditorOnly->OpacityMask, Model.OpacityMaskValueName, TEXT("OpacityMask"))) { return false; }
}
// 3) Optional Vertex stage. Per-pixel/vertex context is read via UE::* intrinsics, so the entry
// 3) Optional Vertex stage. Per-pixel/vertex context is read via UE_* intrinsics, so the entry
// takes just the output struct: `Vertex(inout FShaderLabVertex V)`.
if (Model.bHasVertex && Model.VertexParams.Num() >= 1)
{
@@ -1013,7 +1098,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
// Intrinsics (Stage = vertex).
FString VertexBody = Model.VertexBody;
TArray<FIntrinsicWire> VtxIntrinsicWires;
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, VtxIntrinsicWires, OutErrors))
if (!EmitIntrinsics(Material, EShaderLabIntrinsicFrequency::VertexOnly, VertexBody, Model.VertexBodyLine, MakeLineDirectivePath(Model.SourceFilePath), VtxIntrinsicWires, OutErrors))
{
return false;
}