mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 23:04:37 +00:00
Enhance VSCode coding (Use shader-validator VSCode plugin
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user