Change usl grammar same like as hlsl

This commit is contained in:
Eragon-Brisingr
2026-06-30 20:55:45 +08:00
parent f19bb1e750
commit 0845f71aea
7 changed files with 846 additions and 377 deletions

View File

@@ -0,0 +1,285 @@
// Copyright FlecsProj. All Rights Reserved.
//
// `ShaderLab.IDE.Prepare` console command — generates the IDE authoring assets so a `.usl` file
// (which is plain HLSL plus SL_* annotation macros) gets full member/function/include completion
// from Rider's UE shader support or VSCode + HLSL Tools. It writes, all from a single source of truth:
// 1. <Plugin>/Shaders/ShaderLab.ush — umbrella header: DSL macros + structs + `namespace UE`
// intrinsic stubs generated from FShaderLabIntrinsicRegistry.
// 2. <Project>/shadertoolsconfig.json — HLSL Tools virtual-directory mappings, dumped from
// AllShaderSourceDirectoryMappings() (the same table the
// runtime registers). Machine-specific → gitignored.
// 3. <Project>/.vscode/settings.json — merges *.usl/*.usf/*.ush -> hlsl into files.associations
// (non-destructive: existing keys are preserved).
#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
#include "HAL/IConsoleManager.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "ShaderCore.h"
#include "ShaderLabIntrinsicRegistry.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabIDE, Log, All);
namespace ShaderLabIDEPrepare_Private
{
// Strip line and block comments (string-aware) so JSONC settings.json parses as JSON.
static FString StripJsonComments(const FString& In)
{
FString Out;
Out.Reserve(In.Len());
const int32 N = In.Len();
for (int32 i = 0; i < N;)
{
const TCHAR C = In[i];
if (C == TEXT('"'))
{
Out.AppendChar(C);
++i;
while (i < N)
{
const TCHAR D = In[i];
Out.AppendChar(D);
++i;
if (D == TEXT('\\') && i < N) { Out.AppendChar(In[i]); ++i; }
else if (D == TEXT('"')) { break; }
}
continue;
}
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('/'))
{
while (i < N && In[i] != TEXT('\n')) { ++i; }
continue;
}
if (C == TEXT('/') && i + 1 < N && In[i + 1] == TEXT('*'))
{
i += 2;
while (i + 1 < N && !(In[i] == TEXT('*') && In[i + 1] == TEXT('/'))) { ++i; }
i += 2;
continue;
}
Out.AppendChar(C);
++i;
}
return Out;
}
// Remove trailing commas before `}`/`]` (string-aware) so VSCode-style JSONC parses as strict JSON.
static FString StripTrailingCommas(const FString& In)
{
FString Out;
Out.Reserve(In.Len());
const int32 N = In.Len();
for (int32 i = 0; i < N; ++i)
{
const TCHAR C = In[i];
if (C == TEXT('"'))
{
Out.AppendChar(C);
for (++i; i < N; ++i)
{
const TCHAR D = In[i];
Out.AppendChar(D);
if (D == TEXT('\\') && i + 1 < N) { Out.AppendChar(In[++i]); }
else if (D == TEXT('"')) { break; }
}
continue;
}
if (C == TEXT(','))
{
int32 j = i + 1;
while (j < N && FChar::IsWhitespace(In[j])) { ++j; }
if (j < N && (In[j] == TEXT('}') || In[j] == TEXT(']')))
{
continue; // drop the trailing comma
}
}
Out.AppendChar(C);
}
return Out;
}
/** Build the `namespace UE { ... }` block of stub declarations from the registry (sorted, deterministic). */
static FString GenerateIntrinsicNamespace()
{
TArray<FShaderLabIntrinsicDesc> Descs;
FShaderLabIntrinsicRegistry::Get().ForEach([&Descs](const FShaderLabIntrinsicDesc& D) { Descs.Add(D); });
Descs.Sort([](const FShaderLabIntrinsicDesc& A, const FShaderLabIntrinsicDesc& B)
{
return A.Name.LexicalLess(B.Name);
});
FString Body;
Body += TEXT("namespace UE\n{\n");
for (const FShaderLabIntrinsicDesc& D : Descs)
{
if (!D.Doc.IsEmpty())
{
Body += FString::Printf(TEXT("\t// %s\n"), *D.Doc);
}
FString Params;
for (int32 i = 0; i < D.Params.Num(); ++i)
{
const FShaderLabIntrinsicParam& P = D.Params[i];
if (i > 0) { Params += TEXT(", "); }
Params += FString::Printf(TEXT("%s %s"), *P.Type, *P.Name);
if (!P.DefaultLiteral.IsEmpty()) { Params += FString::Printf(TEXT(" = %s"), *P.DefaultLiteral); }
}
// Stub body returns a zero of the return type; this is authoring-only and never compiled
// (the graph builder rewrites UE:: calls into material-expression inputs before compile).
Body += FString::Printf(TEXT("\t%s %s(%s) { return (%s)0; }\n"),
*D.ReturnType, *D.Name.ToString(), *Params, *D.ReturnType);
}
Body += TEXT("}\n");
return Body;
}
static bool WriteIfChanged(const FString& Path, const FString& Content)
{
FString Existing;
if (FFileHelper::LoadFileToString(Existing, *Path) && Existing.Equals(Content, ESearchCase::CaseSensitive))
{
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *Path);
return true;
}
// UTF-8 (no BOM) so HLSL Tools / Rider and JSON parsers read the generated files reliably
// (FFileHelper defaults to UTF-16, which not every shader/JSON tool handles).
if (!FFileHelper::SaveStringToFile(Content, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogShaderLabIDE, Error, TEXT(" FAILED to write: %s"), *Path);
return false;
}
UE_LOG(LogShaderLabIDE, Log, TEXT(" wrote: %s"), *Path);
return true;
}
static void GenerateShaderLabHeader(const FString& PluginShaderDir)
{
FString H;
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
H += TEXT("// Authoring umbrella header: include this from every .usl for IDE completion.\n");
H += TEXT("// At real shader-compile time this header is NOT included (the parser strips it);\n");
H += TEXT("// the UE:: stubs below are for the editor only.\n");
H += TEXT("#pragma once\n\n");
H += TEXT("#include \"/Plugin/ShaderLab/ShaderLabDSL.ush\"\n");
H += TEXT("#include \"/Plugin/ShaderLab/Private/ShaderLabCommon.ush\"\n\n");
H += GenerateIntrinsicNamespace();
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("ShaderLab.ush")), H);
}
static void GenerateShaderToolsConfig(const FString& ProjectDir)
{
const TMap<FString, FString>& Mappings = AllShaderSourceDirectoryMappings();
const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("root"), true);
const TSharedRef<FJsonObject> Virt = MakeShared<FJsonObject>();
for (const TPair<FString, FString>& Pair : Mappings)
{
// Engine mappings come back relative to the engine binaries dir; HLSL Tools needs
// absolute (or config-relative) paths, so normalize to full paths.
Virt->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value));
}
Root->SetObjectField(TEXT("hlsl.virtualDirectoryMappings"), Virt);
FString Out;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Root, Writer);
WriteIfChanged(FPaths::Combine(ProjectDir, TEXT("shadertoolsconfig.json")), Out);
}
static void MergeVSCodeSettings(const FString& ProjectDir)
{
const FString SettingsPath = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("settings.json"));
TSharedPtr<FJsonObject> Settings;
FString Existing;
if (FFileHelper::LoadFileToString(Existing, *SettingsPath))
{
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(StripTrailingCommas(StripJsonComments(Existing)));
if (!FJsonSerializer::Deserialize(Reader, Settings) || !Settings.IsValid())
{
UE_LOG(LogShaderLabIDE, Warning,
TEXT(" could not parse %s — add files.associations for *.usl/*.usf/*.ush -> hlsl manually."),
*SettingsPath);
return; // Never clobber an unparseable user file.
}
}
else
{
Settings = MakeShared<FJsonObject>();
}
// Union into files.associations, preserving any existing entries. Only rewrite the file when
// we actually add a mapping, so a hand-maintained settings.json (comments, formatting) is left
// untouched once the associations are present.
const TSharedPtr<FJsonObject>* AssocPtr = nullptr;
TSharedPtr<FJsonObject> Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr)
? *AssocPtr : MakeShared<FJsonObject>();
bool bChanged = false;
for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.usf"), TEXT("*.ush") })
{
FString Existing2;
if (!Assoc->TryGetStringField(Ext, Existing2) || Existing2 != TEXT("hlsl"))
{
Assoc->SetStringField(Ext, TEXT("hlsl"));
bChanged = true;
}
}
if (!bChanged)
{
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath);
return;
}
Settings->SetObjectField(TEXT("files.associations"), Assoc);
FString Out;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Settings.ToSharedRef(), Writer);
WriteIfChanged(SettingsPath, Out);
}
static void EnsureGitIgnore(const FString& ProjectDir)
{
const FString Path = FPaths::Combine(ProjectDir, TEXT(".gitignore"));
FString GI;
FFileHelper::LoadFileToString(GI, *Path); // missing file -> empty, fine
if (GI.Contains(TEXT("shadertoolsconfig.json")))
{
return;
}
if (!GI.IsEmpty() && !GI.EndsWith(TEXT("\n")))
{
GI += TEXT("\n");
}
GI += TEXT("# Machine-specific HLSL Tools config (generated by ShaderLab.IDE.Prepare).\n");
GI += TEXT("shadertoolsconfig.json\n");
WriteIfChanged(Path, GI);
}
static void Run(const TArray<FString>& /*Args*/)
{
const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab"));
check(Plugin.IsValid()); // The command lives in this plugin's editor module; it must be findable.
const FString PluginShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders"));
const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare:"));
GenerateShaderLabHeader(PluginShaderDir);
GenerateShaderToolsConfig(ProjectDir);
MergeVSCodeSettings(ProjectDir);
EnsureGitIgnore(ProjectDir);
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare: done."));
}
}
static FAutoConsoleCommand GShaderLabIDEPrepareCommand(
TEXT("ShaderLab.IDE.Prepare"),
TEXT("Generate IDE completion assets for .usl: <Plugin>/Shaders/ShaderLab.ush, shadertoolsconfig.json, .vscode/settings.json."),
FConsoleCommandWithArgsDelegate::CreateStatic(&ShaderLabIDEPrepare_Private::Run));

View File

@@ -63,6 +63,8 @@ void FShaderLabIntrinsicRegistry::Register(FShaderLabIntrinsicDesc Desc)
{
check(!Desc.Name.IsNone());
check(Desc.MakeNode);
// Contract: every intrinsic is IDE-describable so ShaderLab.IDE.Prepare can always emit a stub.
check(!Desc.ReturnType.IsEmpty());
Descs.Add(Desc.Name, MoveTemp(Desc));
}
@@ -71,35 +73,49 @@ const FShaderLabIntrinsicDesc* FShaderLabIntrinsicRegistry::Find(FName Name) con
return Descs.Find(Name);
}
void FShaderLabIntrinsicRegistry::ForEach(TFunctionRef<void(const FShaderLabIntrinsicDesc&)> Fn) const
{
for (const TPair<FName, FShaderLabIntrinsicDesc>& Pair : Descs)
{
Fn(Pair.Value);
}
}
void FShaderLabIntrinsicRegistry::RegisterBuiltins()
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
// Helper for the common no-arg case.
auto AddSimple = [this](const TCHAR* Name, EFreq Freq, TFunction<UMaterialExpression*(UMaterial&)> Make)
// Helper for the common no-arg case (ReturnType drives the generated authoring stub).
auto AddSimple = [this](const TCHAR* Name, EFreq Freq, const TCHAR* ReturnType, TFunction<UMaterialExpression*(UMaterial&)> Make)
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(Name);
Desc.Frequency = Freq;
Desc.ReturnType = ReturnType;
Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray<FString>&, FString&) { return Make(M); };
Register(MoveTemp(Desc));
};
// --- A. Mesh / surface attributes ---
AddSimple(TEXT("VertexNormalWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
AddSimple(TEXT("VertexTangentWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
AddSimple(TEXT("VertexColor"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
AddSimple(TEXT("TwoSidedSign"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
AddSimple(TEXT("LightmapUVs"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
AddSimple(TEXT("VertexNormalWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
AddSimple(TEXT("VertexTangentWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
AddSimple(TEXT("PixelNormalWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
AddSimple(TEXT("VertexColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
AddSimple(TEXT("TwoSidedSign"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
AddSimple(TEXT("LightmapUVs"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
AddSimple(TEXT("PreSkinnedNormal"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
AddSimple(TEXT("PreSkinnedPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
// TextureCoordinate(Index[, UTiling, VTiling]) — const args configure the node (mechanism: scalar/int args).
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("TextureCoordinate"));
Desc.ReturnType = TEXT("float2");
Desc.Params = { { TEXT("int"), TEXT("Index"), TEXT("") },
{ TEXT("float"), TEXT("UTiling"), TEXT("1") },
{ TEXT("float"), TEXT("VTiling"), TEXT("1") } };
Desc.Doc = TEXT("UV channel `Index`, optionally tiled.");
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
{
UMaterialExpressionTextureCoordinate* E = NewObject<UMaterialExpressionTextureCoordinate>(&M);
@@ -113,31 +129,34 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
}
// --- B. Positions / spaces ---
AddSimple(TEXT("WorldPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
AddSimple(TEXT("LocalPosition"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
AddSimple(TEXT("ActorPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
AddSimple(TEXT("CameraPositionWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
AddSimple(TEXT("CameraVectorWS"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
AddSimple(TEXT("LightVector"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
AddSimple(TEXT("WorldPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
AddSimple(TEXT("LocalPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
AddSimple(TEXT("ActorPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
AddSimple(TEXT("ObjectPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
AddSimple(TEXT("CameraPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
AddSimple(TEXT("CameraVectorWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
AddSimple(TEXT("LightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
AddSimple(TEXT("PixelDepth"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
AddSimple(TEXT("ScreenPosition"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
// --- C. Object / bounds (ObjectLocalBounds & PreSkinnedLocalBounds are multi-output; output 0) ---
AddSimple(TEXT("ObjectRadius"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
AddSimple(TEXT("ObjectBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
AddSimple(TEXT("ObjectOrientation"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
AddSimple(TEXT("ObjectRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
AddSimple(TEXT("ObjectBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
AddSimple(TEXT("ObjectLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
AddSimple(TEXT("ObjectOrientation"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
AddSimple(TEXT("PreSkinnedLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
// --- D. Instancing ---
AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
AddSimple(TEXT("PerInstanceRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
AddSimple(TEXT("PerInstanceFadeAmount"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
// PerInstanceCustomData(DataIndex[, ConstDefaultValue]) — mechanism: const args + optional input pin left at default.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("PerInstanceCustomData"));
Desc.ReturnType = TEXT("float");
Desc.Params = { { TEXT("int"), TEXT("DataIndex"), TEXT("") },
{ TEXT("float"), TEXT("ConstDefaultValue"), TEXT("0") } };
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
{
UMaterialExpressionPerInstanceCustomData* E = NewObject<UMaterialExpressionPerInstanceCustomData>(&M);
@@ -150,13 +169,16 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
}
// --- Time ---
AddSimple(TEXT("Time"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
AddSimple(TEXT("DeltaTime"), EFreq::Any, [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
AddSimple(TEXT("Time"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
AddSimple(TEXT("DeltaTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
// ViewProperty(Property) — representative of the enum-by-name const-arg mechanism.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("ViewProperty"));
Desc.ReturnType = TEXT("float4");
Desc.Params = { { TEXT("int"), TEXT("Property"), TEXT("") } };
Desc.Doc = TEXT("Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView).");
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString& OutError) -> UMaterialExpression*
{
UMaterialExpressionViewProperty* E = NewObject<UMaterialExpressionViewProperty>(&M);
@@ -183,6 +205,7 @@ void FShaderLabIntrinsicRegistry::RegisterBuiltins()
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("ParticleColor"));
Desc.ReturnType = TEXT("float4");
Desc.MakeNode = [](UMaterial& M, const TArray<FString>&, FString&) { return MakeSimple<UMaterialExpressionParticleColor>(M); };
Register(MoveTemp(Desc));
}

View File

@@ -15,10 +15,25 @@ enum class EShaderLabIntrinsicFrequency : uint8
VertexOnly,
};
/** One formal parameter of an intrinsic, used only to generate the IDE authoring stub signature. */
struct FShaderLabIntrinsicParam
{
/** HLSL type of the parameter, e.g. "int", "float". */
FString Type;
/** Parameter name shown in completion, e.g. "Index". */
FString Name;
/** Default-value literal for the stub (e.g. "1"); empty means the parameter is required. */
FString DefaultLiteral;
};
/**
* Describes one `UE::NodeName(...)` intrinsic: how to build the backing material expression node,
* which of its outputs to read, and what it requires of the material. The Custom-node input type is
* derived by the engine from the connected node's real output type, so no output type is declared here.
*
* The ReturnType/Params/Doc fields carry no runtime meaning — they exist so `ShaderLab.IDE.Prepare`
* can emit a `namespace UE { ... }` HLSL stub header from this single source of truth, giving the IDE
* member/signature completion for `UE::` calls without any drift from the real registration.
*/
struct FShaderLabIntrinsicDesc
{
@@ -30,6 +45,15 @@ struct FShaderLabIntrinsicDesc
/** Stage restriction; misuse is a build error. */
EShaderLabIntrinsicFrequency Frequency = EShaderLabIntrinsicFrequency::Any;
/** HLSL return type for the authoring stub, e.g. "float2"/"float3"/"float". Required (see Register). */
FString ReturnType;
/** Formal parameters for the authoring stub (empty for the common no-arg intrinsics). */
TArray<FShaderLabIntrinsicParam> Params;
/** Optional one-line doc surfaced as a comment above the stub. */
FString Doc;
/**
* Create the backing expression node and add it to `Material`'s expression collection. `ConstArgs`
* are the literal arguments parsed from the call (e.g. {"0","2.0"} for `UE::TextureCoordinate(0,2.0)`).
@@ -55,6 +79,9 @@ public:
/** Lookup by name; nullptr if unknown. */
const FShaderLabIntrinsicDesc* Find(FName Name) const;
/** Visit every registered intrinsic (used by ShaderLab.IDE.Prepare to generate the UE:: stub header). */
void ForEach(TFunctionRef<void(const FShaderLabIntrinsicDesc&)> Fn) const;
private:
FShaderLabIntrinsicRegistry();
void RegisterBuiltins();

View File

@@ -22,6 +22,7 @@ public class UShaderLabEditor : ModuleRules
"UShaderLab",
"RenderCore",
"RHI",
"Json",
"UnrealEd",
"AssetTools",
"MaterialEditor",