mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
697 lines
37 KiB
C++
697 lines
37 KiB
C++
// Copyright UShaderLab. 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 + the shader-validator extension. It writes:
|
|
// 1. <Plugin>/Shaders/Private/ShaderLabUENode.ush — the UE_ node-intrinsic stubs (+ their enums)
|
|
// generated from FShaderLabIntrinsicRegistry. (The static
|
|
// ShaderLab.ush umbrella includes it; it is IDE-only.)
|
|
// 2. <Plugin>/Shaders/Private/UEFunctions/Transform.ush + TransformPosition.ush — the generated
|
|
// UE_Transform{From}VectorTo{To} / ...PositionTo{To} stubs (all space pairs).
|
|
// 3. <Project>/.vscode/settings.json — merges (non-destructively) files.associations
|
|
// (*.usl/*.uslfunc/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
|
// (virtual shader roots -> disk, from AllShaderSourceDirectoryMappings).
|
|
// pathRemapping is machine-specific; the command regenerates it.
|
|
// 4. <Project>/.vscode/shaderlab.code-snippets — completion for the whole `.usl` DSL: macro scaffolds
|
|
// (full signature + body), specifier fills with enum value choices,
|
|
// SL_FRONTMATERIAL topology operators, and every settable SL_SETTINGS
|
|
// key (reflected from editable UMaterial properties minus the
|
|
// ShaderLab-controlled set). Substrate-only constructs are omitted at
|
|
// r.Substrate=0. Regenerated per engine version + project mode.
|
|
|
|
#include "ShaderLabIDEPrepare.h"
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "Materials/Material.h"
|
|
#include "UObject/Class.h"
|
|
#include "UObject/UnrealType.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/App.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "Serialization/JsonWriter.h"
|
|
#include "RenderUtils.h" // Substrate::IsSubstrateEnabled()
|
|
#include "ShaderCore.h"
|
|
#include "ShaderLabIntrinsicRegistry.h"
|
|
#include "ShaderLabSettingsApplier.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 `UE_Name(...)` stub declarations from the registry (sorted, deterministic). They are
|
|
* free functions (not a `namespace UE`) because shader-validator's HLSL parser has no namespace
|
|
* support; the intrinsics are written `UE_Name(...)` in .usl and matched by that prefix.
|
|
*/
|
|
static FString GenerateIntrinsicStubs()
|
|
{
|
|
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;
|
|
|
|
// Emit each reflected enum used by an intrinsic arg once, as a real HLSL enum, so the param is
|
|
// typed and its tokens complete (`UE_ViewProperty(MEVP_FieldOfView)`). The graph builder resolves
|
|
// the token by name, so the enum's numeric values are irrelevant.
|
|
{
|
|
TSet<FString> EmittedEnums;
|
|
FString Enums;
|
|
for (const FShaderLabIntrinsicDesc& D : Descs)
|
|
{
|
|
for (const FShaderLabIntrinsicParam& P : D.Params)
|
|
{
|
|
if (!P.Enum || EmittedEnums.Contains(P.Enum->GetName()))
|
|
{
|
|
continue;
|
|
}
|
|
EmittedEnums.Add(P.Enum->GetName());
|
|
Enums += FString::Printf(TEXT("enum %s\n{\n"), *P.Enum->GetName());
|
|
for (int32 Index = 0; Index < P.Enum->NumEnums(); ++Index)
|
|
{
|
|
const FString Token = P.Enum->GetNameStringByIndex(Index);
|
|
if (!Token.IsEmpty() && !Token.EndsWith(TEXT("_MAX")))
|
|
{
|
|
Enums += FString::Printf(TEXT("\t%s,\n"), *Token);
|
|
}
|
|
}
|
|
Enums += TEXT("};\n");
|
|
}
|
|
}
|
|
if (!Enums.IsEmpty())
|
|
{
|
|
Body += Enums;
|
|
Body += TEXT("\n");
|
|
}
|
|
}
|
|
|
|
for (const FShaderLabIntrinsicDesc& D : Descs)
|
|
{
|
|
if (!D.Doc.IsEmpty())
|
|
{
|
|
Body += FString::Printf(TEXT("// %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(", "); }
|
|
const FString ParamType = P.Enum ? P.Enum->GetName() : P.Type;
|
|
Params += FString::Printf(TEXT("%s %s"), *ParamType, *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("%s UE_%s(%s) { return (%s)0; }\n"),
|
|
*D.ReturnType, *D.Name.ToString(), *Params, *D.ReturnType);
|
|
}
|
|
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;
|
|
}
|
|
|
|
// One coordinate space for the transform generator: its HLSL to/from the world-space hub. `$X$` is
|
|
// the placeholder for the inner value expression. `Parameters` is referenced literally (the emitted
|
|
// functions name their parameter `Parameters`, overloaded on the pixel/vertex struct).
|
|
struct FTransformSpace
|
|
{
|
|
const TCHAR* Name;
|
|
const TCHAR* ToWorld;
|
|
const TCHAR* FromWorld;
|
|
};
|
|
|
|
static FString ApplyTemplate(const FString& Template, const FString& Inner)
|
|
{
|
|
return Template.Replace(TEXT("$X$"), *Inner);
|
|
}
|
|
|
|
// Generates Private/UEFunctions/Transform.ush: UE_Transform{From}VectorTo{To} for every space pair,
|
|
// both stages, composed From -> World -> To. Vectors are always float3 (no LWC). Named literally so
|
|
// the IDE can complete them.
|
|
static void GenerateVectorTransforms(const FString& PluginShaderDir)
|
|
{
|
|
// World hub is a plain float3 direction; View/Camera drop translation via the (float3x3) cast.
|
|
static const FTransformSpace Spaces[] = {
|
|
{ TEXT("Tangent"), TEXT("TransformTangentVectorToWorld(Parameters.TangentToWorld, $X$)"), TEXT("TransformWorldVectorToTangent(Parameters.TangentToWorld, $X$)") },
|
|
{ TEXT("Local"), TEXT("TransformLocalVectorToWorld(Parameters, $X$)"), TEXT("WSMultiplyVector($X$, GetWorldToLocal(Parameters))") },
|
|
{ TEXT("World"), TEXT("$X$"), TEXT("$X$") },
|
|
{ TEXT("View"), TEXT("mul($X$, (float3x3)ResolvedView.ViewToTranslatedWorld)"), TEXT("mul($X$, (float3x3)ResolvedView.TranslatedWorldToView)") },
|
|
{ TEXT("Camera"), TEXT("mul($X$, (float3x3)ResolvedView.CameraViewToTranslatedWorld)"), TEXT("mul($X$, (float3x3)ResolvedView.TranslatedWorldToCameraView)") },
|
|
{ TEXT("Instance"), TEXT("WSMultiplyVector($X$, GetInstanceToWorld(Parameters))"), TEXT("WSMultiplyVector($X$, GetWorldToInstance(Parameters))") },
|
|
};
|
|
|
|
FString H;
|
|
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
|
|
H += TEXT("// UE_ vector-space transforms: every (From,To) pair, both stages, composed via a world hub.\n");
|
|
H += TEXT("#pragma once\n\n");
|
|
H += TEXT("#include \"/Plugin/ShaderLab/Private/UEFunctions/_EngineStubs.ush\"\n\n");
|
|
|
|
for (const FTransformSpace& From : Spaces)
|
|
{
|
|
for (const FTransformSpace& To : Spaces)
|
|
{
|
|
if (FCString::Strcmp(From.Name, To.Name) == 0)
|
|
{
|
|
continue; // no identity transform
|
|
}
|
|
const FString Expr = ApplyTemplate(To.FromWorld, ApplyTemplate(From.ToWorld, TEXT("V")));
|
|
for (const TCHAR* ParamType : { TEXT("FMaterialPixelParameters"), TEXT("FMaterialVertexParameters") })
|
|
{
|
|
H += FString::Printf(TEXT("float3 UE_Transform%sVectorTo%s(%s Parameters, float3 V) { return %s; }\n"),
|
|
From.Name, To.Name, ParamType, *Expr);
|
|
}
|
|
}
|
|
}
|
|
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("Private"), TEXT("UEFunctions"), TEXT("Transform.ush")), H);
|
|
}
|
|
|
|
// Generates Private/UEFunctions/TransformPosition.ush: UE_Transform{From}PositionTo{To} for every
|
|
// space pair, both stages, composed via the absolute-world hub (FWSVector3). Only absolute World is
|
|
// LWC-typed; every other space is float3, so input/output types vary per space.
|
|
static void GeneratePositionTransforms(const FString& PluginShaderDir)
|
|
{
|
|
struct FPosSpace { const TCHAR* Name; bool bIsWorld; const TCHAR* ToWorld; const TCHAR* FromWorld; };
|
|
static const FPosSpace Spaces[] = {
|
|
{ TEXT("Local"), false,
|
|
TEXT("TransformLocalPositionToWorld(Parameters, $X$)"),
|
|
TEXT("WSMultiplyDemote($X$, GetWorldToLocal(Parameters))") },
|
|
{ TEXT("World"), true, TEXT("$X$"), TEXT("$X$") },
|
|
{ TEXT("TranslatedWorld"), false,
|
|
TEXT("WSSubtract(WSPromote($X$), GetPreViewTranslation(Parameters))"),
|
|
TEXT("WSAddDemote($X$, GetPreViewTranslation(Parameters))") },
|
|
{ TEXT("View"), false,
|
|
TEXT("WSSubtract(WSPromote(mul(float4($X$, 1), ResolvedView.ViewToTranslatedWorld).xyz), GetPreViewTranslation(Parameters))"),
|
|
TEXT("mul(float4(WSAddDemote($X$, GetPreViewTranslation(Parameters)), 1), ResolvedView.TranslatedWorldToView).xyz") },
|
|
{ TEXT("Camera"), false,
|
|
TEXT("WSSubtract(WSPromote(mul(float4($X$, 1), ResolvedView.CameraViewToTranslatedWorld).xyz), GetPreViewTranslation(Parameters))"),
|
|
TEXT("mul(float4(WSAddDemote($X$, GetPreViewTranslation(Parameters)), 1), ResolvedView.TranslatedWorldToCameraView).xyz") },
|
|
{ TEXT("Instance"), false,
|
|
TEXT("WSMultiply($X$, GetInstanceToWorld(Parameters))"),
|
|
TEXT("WSMultiplyDemote($X$, GetWorldToInstance(Parameters))") },
|
|
};
|
|
|
|
FString H;
|
|
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
|
|
H += TEXT("// UE_ position-space transforms: every (From,To) pair, both stages, via the absolute-world\n");
|
|
H += TEXT("// hub. Absolute World is FWSVector3 (LWC); all other spaces are float3.\n");
|
|
H += TEXT("#pragma once\n\n");
|
|
H += TEXT("#include \"/Plugin/ShaderLab/Private/UEFunctions/_EngineStubs.ush\"\n\n");
|
|
|
|
for (const FPosSpace& From : Spaces)
|
|
{
|
|
for (const FPosSpace& To : Spaces)
|
|
{
|
|
if (FCString::Strcmp(From.Name, To.Name) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
const TCHAR* InType = From.bIsWorld ? TEXT("FWSVector3") : TEXT("float3");
|
|
const TCHAR* OutType = To.bIsWorld ? TEXT("FWSVector3") : TEXT("float3");
|
|
const FString Expr = ApplyTemplate(To.FromWorld, ApplyTemplate(From.ToWorld, TEXT("V")));
|
|
for (const TCHAR* ParamType : { TEXT("FMaterialPixelParameters"), TEXT("FMaterialVertexParameters") })
|
|
{
|
|
H += FString::Printf(TEXT("%s UE_Transform%sPositionTo%s(%s Parameters, %s V) { return %s; }\n"),
|
|
OutType, From.Name, To.Name, ParamType, InType, *Expr);
|
|
}
|
|
}
|
|
}
|
|
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("Private"), TEXT("UEFunctions"), TEXT("TransformPosition.ush")), H);
|
|
}
|
|
|
|
// Generates Private/ShaderLabUENode.ush: the UE_ node-intrinsic stubs (+ their enums), from the
|
|
// registry. Included by the static ShaderLab.ush umbrella; editor-only (the parser strips the shim).
|
|
static void GenerateUENodeHeader(const FString& PluginShaderDir)
|
|
{
|
|
FString H;
|
|
H += TEXT("// GENERATED by `ShaderLab.IDE.Prepare` — do not edit by hand.\n");
|
|
H += TEXT("// UE_ node-intrinsic stubs + their enums, for IDE completion only (never compiled).\n");
|
|
H += TEXT("#pragma once\n\n");
|
|
H += GenerateIntrinsicStubs();
|
|
WriteIfChanged(FPaths::Combine(PluginShaderDir, TEXT("Private"), TEXT("ShaderLabUENode.ush")), H);
|
|
}
|
|
|
|
static FString SerializeObject(const TSharedRef<FJsonObject>& Obj)
|
|
{
|
|
FString Out;
|
|
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
|
|
FJsonSerializer::Serialize(Obj, Writer);
|
|
return Out;
|
|
}
|
|
|
|
// Virtual shader roots -> absolute disk paths, for shader-validator.pathRemapping.
|
|
static TSharedRef<FJsonObject> BuildPathRemapping()
|
|
{
|
|
const TSharedRef<FJsonObject> Remap = MakeShared<FJsonObject>();
|
|
for (const TPair<FString, FString>& Pair : AllShaderSourceDirectoryMappings())
|
|
{
|
|
// Engine mappings come back relative to the engine binaries dir; the extension needs
|
|
// absolute paths, so normalize.
|
|
Remap->SetStringField(Pair.Key, FPaths::ConvertRelativePathToFull(Pair.Value));
|
|
}
|
|
return Remap;
|
|
}
|
|
|
|
// Merge files.associations (*.usl/*.uslfunc/*.usf/*.ush -> hlsl) and shader-validator.pathRemapping
|
|
// into .vscode/settings.json, preserving all other keys. Rewrites only when something actually changed.
|
|
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 and shader-validator.pathRemapping manually."),
|
|
*SettingsPath);
|
|
return; // Never clobber an unparseable user file.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Settings = MakeShared<FJsonObject>();
|
|
}
|
|
|
|
bool bChanged = false;
|
|
|
|
// files.associations: union in the ShaderLab extensions.
|
|
const TSharedPtr<FJsonObject>* AssocPtr = nullptr;
|
|
const TSharedPtr<FJsonObject> Assoc = Settings->TryGetObjectField(TEXT("files.associations"), AssocPtr)
|
|
? *AssocPtr : MakeShared<FJsonObject>();
|
|
for (const TCHAR* Ext : { TEXT("*.usl"), TEXT("*.uslfunc"), TEXT("*.usf"), TEXT("*.ush") })
|
|
{
|
|
FString Value;
|
|
if (!Assoc->TryGetStringField(Ext, Value) || Value != TEXT("hlsl"))
|
|
{
|
|
Assoc->SetStringField(Ext, TEXT("hlsl"));
|
|
bChanged = true;
|
|
}
|
|
}
|
|
Settings->SetObjectField(TEXT("files.associations"), Assoc);
|
|
|
|
// shader-validator.pathRemapping: replace with the current (machine-specific) mapping table.
|
|
const TSharedRef<FJsonObject> Remap = BuildPathRemapping();
|
|
const TSharedPtr<FJsonObject>* ExistingRemapPtr = nullptr;
|
|
const bool bHasRemap = Settings->TryGetObjectField(TEXT("shader-validator.pathRemapping"), ExistingRemapPtr);
|
|
if (!bHasRemap || SerializeObject(ExistingRemapPtr->ToSharedRef()) != SerializeObject(Remap))
|
|
{
|
|
Settings->SetObjectField(TEXT("shader-validator.pathRemapping"), Remap);
|
|
bChanged = true;
|
|
}
|
|
|
|
// shader-validator.defines: SHADERLAB_IDE gates the editor-only engine-function stubs in
|
|
// ShaderLabFunctions.ush (so its UE_ forwarders resolve in the IDE without dragging in engine
|
|
// headers). The real shader compiler never sees this define.
|
|
const TSharedPtr<FJsonObject>* DefinesPtr = nullptr;
|
|
const TSharedPtr<FJsonObject> Defines = Settings->TryGetObjectField(TEXT("shader-validator.defines"), DefinesPtr)
|
|
? *DefinesPtr : MakeShared<FJsonObject>();
|
|
FString DefineValue;
|
|
if (!Defines->TryGetStringField(TEXT("SHADERLAB_IDE"), DefineValue) || DefineValue != TEXT("1"))
|
|
{
|
|
Defines->SetStringField(TEXT("SHADERLAB_IDE"), TEXT("1"));
|
|
Settings->SetObjectField(TEXT("shader-validator.defines"), Defines);
|
|
bChanged = true;
|
|
}
|
|
|
|
// SHADERLAB_SUBSTRATE picks the surface-struct parameterization (Substrate Slab fields vs legacy
|
|
// Metallic/Roughness) the IDE checks against. It mirrors the project's r.Substrate so authoring on
|
|
// the legacy (r.Substrate=0) branch sees the legacy FShaderLabSurface (S.BaseColor/Metallic/...) and
|
|
// does not red-underline it as "no member". The real compiler derives this from the engine's own
|
|
// SUBSTRATE_ENABLED; this only affects the editor's shader-validator.
|
|
const TCHAR* SubstrateValue = Substrate::IsSubstrateEnabled() ? TEXT("1") : TEXT("0");
|
|
FString SubstrateDefine;
|
|
if (!Defines->TryGetStringField(TEXT("SHADERLAB_SUBSTRATE"), SubstrateDefine) || SubstrateDefine != SubstrateValue)
|
|
{
|
|
Defines->SetStringField(TEXT("SHADERLAB_SUBSTRATE"), SubstrateValue);
|
|
Settings->SetObjectField(TEXT("shader-validator.defines"), Defines);
|
|
bChanged = true;
|
|
}
|
|
|
|
if (!bChanged)
|
|
{
|
|
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath);
|
|
return;
|
|
}
|
|
WriteIfChanged(SettingsPath, SerializeObject(Settings.ToSharedRef()));
|
|
}
|
|
|
|
// The VSCode snippet value placeholder (tabstop) for a leaf property: enums/bools become a choice list,
|
|
// everything else a typed placeholder.
|
|
static FString SnippetValueForLeaf(const FProperty* Leaf, int32 Tab)
|
|
{
|
|
auto EnumChoice = [Tab](const UEnum* Enum) -> FString
|
|
{
|
|
TArray<FString> Tokens;
|
|
for (int32 i = 0; i < Enum->NumEnums(); ++i)
|
|
{
|
|
const FString T = Enum->GetNameStringByIndex(i);
|
|
if (!T.IsEmpty() && !T.EndsWith(TEXT("_MAX"))) { Tokens.Add(T); }
|
|
}
|
|
return Tokens.Num() > 0
|
|
? FString::Printf(TEXT("${%d|%s|}"), Tab, *FString::Join(Tokens, TEXT(",")))
|
|
: FString::Printf(TEXT("${%d:}"), Tab);
|
|
};
|
|
|
|
if (CastField<FBoolProperty>(Leaf)) { return FString::Printf(TEXT("${%d|true,false|}"), Tab); }
|
|
if (const FByteProperty* B = CastField<FByteProperty>(Leaf)) { return B->Enum ? EnumChoice(B->Enum) : FString::Printf(TEXT("${%d:0}"), Tab); }
|
|
if (const FEnumProperty* E = CastField<FEnumProperty>(Leaf)) { return E->GetEnum() ? EnumChoice(E->GetEnum()) : FString::Printf(TEXT("${%d:0}"), Tab); }
|
|
if (CastField<FNumericProperty>(Leaf)) { return FString::Printf(TEXT("${%d:0}"), Tab); }
|
|
if (CastField<FObjectPropertyBase>(Leaf)) { return FString::Printf(TEXT("${%d:/Game/Path/Asset.Asset}"), Tab); }
|
|
return FString::Printf(TEXT("${%d:}"), Tab); // FName / FString / other leaf
|
|
}
|
|
|
|
// Add a completion snippet for a property. Struct properties expand one level into dotted
|
|
// `Prefix.SubField` keys (the material config structs are shallow); arrays are skipped (not settable).
|
|
static void EmitSettingSnippets(const TSharedRef<FJsonObject>& Out, const FString& Prefix, FProperty* Prop, int32 Depth)
|
|
{
|
|
if (CastField<FArrayProperty>(Prop)) { return; }
|
|
|
|
if (const FStructProperty* StructProp = CastField<FStructProperty>(Prop))
|
|
{
|
|
if (Depth < 2 && StructProp->Struct)
|
|
{
|
|
bool bAny = false;
|
|
for (TFieldIterator<FProperty> It(StructProp->Struct); It; ++It)
|
|
{
|
|
EmitSettingSnippets(Out, Prefix + TEXT(".") + It->GetName(), *It, Depth + 1);
|
|
bAny = true;
|
|
}
|
|
if (bAny) { return; } // expanded into sub-fields
|
|
}
|
|
// no reflected fields / too deep: fall through to a single literal placeholder
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Body;
|
|
Body.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("%s = %s"), *Prefix, *SnippetValueForLeaf(Prop, 1))));
|
|
|
|
const TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("prefix"), Prefix);
|
|
Entry->SetArrayField(TEXT("body"), Body);
|
|
Entry->SetStringField(TEXT("description"), FString::Printf(TEXT("ShaderLab material setting (%s)"), *Prop->GetCPPType()));
|
|
Entry->SetStringField(TEXT("scope"), TEXT("hlsl"));
|
|
Out->SetObjectField(FString::Printf(TEXT("SL_SETTINGS %s"), *Prefix), Entry);
|
|
}
|
|
|
|
// One hand-authored DSL snippet. `Body` is '\n'-separated (each line -> a VSCode snippet body line)
|
|
// with $0/${n:default}/${n|a,b,c|} tabstops. bSubstrateOnly entries are emitted only when the project
|
|
// runs with r.Substrate=1 (legacy rejects those constructs at build).
|
|
struct FDslSnippet { const TCHAR* Prefix; const TCHAR* Body; const TCHAR* Desc; bool bSubstrateOnly; };
|
|
|
|
static void AddSnippet(const TSharedRef<FJsonObject>& Out, const FString& Key, const FString& Prefix, const FString& Body, const FString& Desc)
|
|
{
|
|
TArray<FString> Lines;
|
|
Body.ParseIntoArray(Lines, TEXT("\n"), /*CullEmpty*/ false);
|
|
TArray<TSharedPtr<FJsonValue>> BodyArr;
|
|
for (const FString& L : Lines) { BodyArr.Add(MakeShared<FJsonValueString>(L)); }
|
|
|
|
const TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("prefix"), Prefix);
|
|
Entry->SetArrayField(TEXT("body"), BodyArr);
|
|
Entry->SetStringField(TEXT("description"), Desc);
|
|
Entry->SetStringField(TEXT("scope"), TEXT("hlsl"));
|
|
Out->SetObjectField(Key, Entry);
|
|
}
|
|
|
|
// Macro scaffolds: entry/BSDF/block macros expand the FULL declaration they annotate (function signature +
|
|
// body braces), so a single trigger drops in a compilable construct. Value tokens mirror the parser's
|
|
// accepted sets one-for-one (see ShaderLabParser.cpp). Struct FIELDS and HLSL types are intentionally NOT
|
|
// snippets — those come from the HLSL language server against the generated mode-specific Surface_*.ush.
|
|
static const FDslSnippet* MacroTemplates(int32& OutNum)
|
|
{
|
|
static const FDslSnippet T[] = {
|
|
// --- both modes ---
|
|
{ TEXT("SL_SETTINGS"), TEXT("SL_SETTINGS(Domain = ${1|Surface,PostProcess,UI,Decal,Volume,LightFunction|}, BlendMode = ${2|Opaque,Masked,Translucent,Additive,Modulate,AlphaComposite,AlphaHoldout|})"), TEXT("ShaderLab: material settings"), false },
|
|
{ TEXT("SL_PROPERTY"), TEXT("SL_PROPERTY(Category = \"${1:Surface}\")\n${2:float3} ${3:Name} = ${4:0};"), TEXT("ShaderLab: material parameter (float->Scalar, float3->Color, float4->Vector, Texture2D->texture)"), false },
|
|
{ TEXT("SL_SURFACE"), TEXT("SL_SURFACE()\nvoid Surface(inout FShaderLabSurface S)\n{\n\t$0\n}"), TEXT("ShaderLab: single-entry surface (DefaultLit in legacy)"), false },
|
|
{ TEXT("SL_POSTPROCESS"), TEXT("SL_POSTPROCESS()\nvoid PostProcess(inout FShaderLabPostProcess O)\n{\n\t$0\n}"), TEXT("ShaderLab: post-process entry (Domain = PostProcess)"), false },
|
|
{ TEXT("SL_UI"), TEXT("SL_UI()\nvoid UI(inout FShaderLabUI O)\n{\n\t$0\n}"), TEXT("ShaderLab: UI entry (Domain = UI)"), false },
|
|
{ TEXT("SL_VERTEX"), TEXT("SL_VERTEX()\nvoid Vertex(inout FShaderLabVertex V)\n{\n\t$0\n}"), TEXT("ShaderLab: vertex stage (WorldPositionOffset / CustomizedUV)"), false },
|
|
{ TEXT("SL_UNLIT"), TEXT("SL_UNLIT()\nvoid Unlit(inout FShaderLabUnlit U)\n{\n\t$0\n}"), TEXT("ShaderLab: unlit BSDF (emissive/transmittance only)"), false },
|
|
{ TEXT("SL_VALUE"), TEXT("SL_VALUE()\nfloat ${1:Name}()\n{\n\treturn ${2:0};\n}"), TEXT("ShaderLab: named float/float2 value block (mix factor / UV)"), false },
|
|
{ TEXT("SL_INTERPOLATOR"), TEXT("SL_INTERPOLATOR()\nfloat3 ${1:Name}()\n{\n\treturn ${2:0};\n}"), TEXT("ShaderLab: per-vertex interpolator (read with UE_Interpolator)"), false },
|
|
{ TEXT("SL_COLLECTION"), TEXT("SL_COLLECTION(Path = \"${1:/Game/MPC/MPC_Name}\")\n${2:float3} ${3:Name};"), TEXT("ShaderLab: Material Parameter Collection read"), false },
|
|
{ TEXT("SL_VTSAMPLE"), TEXT("SL_VTSAMPLE(DefaultTexture = \"${1:/Game/VT/T_Name}\", SamplerType = ${2|VirtualColor,VirtualNormal,VirtualGrayscale,VirtualAlpha,VirtualMasks,VirtualLinearColor,VirtualLinearGrayscale|}, UV = ${3:TexCoord0})\nfloat4 ${4:Name};"), TEXT("ShaderLab: streaming Virtual Texture read"), false },
|
|
{ TEXT("SL_RVTSAMPLE"), TEXT("SL_RVTSAMPLE(VirtualTexture = \"${1:/Game/RVT/RVT_Name}\", MaterialType = ${2|BaseColor_Normal_Roughness,BaseColor_Normal_Specular,BaseColor_Normal_Specular_YCoCg,BaseColor_Normal_Specular_Mask_YCoCg,BaseColor,Mask4,WorldHeight,Displacement|}, UV = ${3|World,TexCoord0|})\nFShaderLabRVT ${4:Terrain};"), TEXT("ShaderLab: Runtime Virtual Texture read"), false },
|
|
{ TEXT("SL_RVTOUTPUT"), TEXT("SL_RVTOUTPUT()\nvoid RVTOutput(inout FShaderLabRVTOutput O)\n{\n\t$0\n}"), TEXT("ShaderLab: Runtime Virtual Texture write block"), false },
|
|
{ TEXT("SL_IMPORT"), TEXT("SL_IMPORT(Namespace = \"${1:Detail}\")\n#include \"${2:/Project/Lib/Lib.uslfunc}\""), TEXT("ShaderLab: function-library import with namespace"), false },
|
|
{ TEXT("SL_FUNCTION"), TEXT("SL_FUNCTION()\n${1:float3} ${2:Name}(${3:float2 uv})\n{\n\treturn ${4:0};\n}"), TEXT("ShaderLab: library function (.uslfunc files only)"), false },
|
|
// --- Substrate only ---
|
|
{ TEXT("SL_SLAB"), TEXT("SL_SLAB()\nvoid ${1:Name}(inout FShaderLabSlab S)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate slab BSDF block"), true },
|
|
{ TEXT("SL_MATERIAL"), TEXT("SL_MATERIAL()\nvoid Material(inout FShaderLabMaterialOutput O)\n{\n\t$0\n}"), TEXT("ShaderLab: whole-material outputs for multi-slab (Substrate)"), true },
|
|
{ TEXT("SL_HAIR"), TEXT("SL_HAIR()\nvoid ${1:Name}(inout FShaderLabHair H)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate hair BSDF"), true },
|
|
{ TEXT("SL_EYE"), TEXT("SL_EYE()\nvoid ${1:Name}(inout FShaderLabEye E)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate eye BSDF"), true },
|
|
{ TEXT("SL_WATER"), TEXT("SL_WATER()\nvoid ${1:Name}(inout FShaderLabWater W)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate water BSDF"), true },
|
|
{ TEXT("SL_CLEARCOAT"), TEXT("SL_CLEARCOAT()\nvoid ${1:Name}(inout FShaderLabClearCoat C)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate clear-coat BSDF"), true },
|
|
{ TEXT("SL_TOON"), TEXT("SL_TOON()\nvoid ${1:Name}(inout FShaderLabToon T)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate toon BSDF"), true },
|
|
{ TEXT("SL_VOLUME"), TEXT("SL_VOLUME()\nvoid ${1:Name}(inout FShaderLabVolume V)\n{\n\t$0\n}"), TEXT("ShaderLab: Substrate volume BSDF (Domain = Volume)"), true },
|
|
{ TEXT("SL_LIGHTFUNCTION"), TEXT("SL_LIGHTFUNCTION()\nvoid ${1:Name}(inout FShaderLabLightFunction L)\n{\n\t$0\n}"), TEXT("ShaderLab: light-function BSDF (Domain = LightFunction)"), true },
|
|
{ TEXT("SL_FRONTMATERIAL"), TEXT("SL_FRONTMATERIAL(${1:VerticalLayer(Top, Base, 0.5)})"), TEXT("ShaderLab: compose slabs into the front material (Substrate)"), true },
|
|
};
|
|
OutNum = UE_ARRAY_COUNT(T);
|
|
return T;
|
|
}
|
|
|
|
// Standalone specifier fills (Key = value), with enum value choices where the parser enumerates them.
|
|
static const FDslSnippet* SpecifierSnippets(int32& OutNum)
|
|
{
|
|
static const FDslSnippet S[] = {
|
|
// SL_SETTINGS first-class
|
|
{ TEXT("Domain"), TEXT("Domain = ${1|Surface,PostProcess,UI,Decal,Volume,LightFunction|}"), TEXT("SL_SETTINGS: material domain"), false },
|
|
{ TEXT("BlendMode"), TEXT("BlendMode = ${1|Opaque,Masked,Translucent,Additive,Modulate,AlphaComposite,AlphaHoldout|}"), TEXT("SL_SETTINGS: blend mode"), false },
|
|
{ TEXT("TwoSided"), TEXT("TwoSided = ${1|true,false|}"), TEXT("SL_SETTINGS: two-sided"), false },
|
|
// SL_PROPERTY
|
|
{ TEXT("Category"), TEXT("Category = \"${1:Surface}\""), TEXT("SL_PROPERTY: editor group"), false },
|
|
{ TEXT("SortPriority"), TEXT("SortPriority = ${1:0}"), TEXT("SL_PROPERTY: sort order in group"), false },
|
|
{ TEXT("DefaultTexture"), TEXT("DefaultTexture = \"${1:white}\""), TEXT("SL_PROPERTY/SL_VTSAMPLE: default texture (white/black/grey/normal token or asset path)"), false },
|
|
{ TEXT("ClampMin"), TEXT("ClampMin = ${1:0}"), TEXT("SL_PROPERTY: scalar slider min"), false },
|
|
{ TEXT("ClampMax"), TEXT("ClampMax = ${1:1}"), TEXT("SL_PROPERTY: scalar slider max"), false },
|
|
{ TEXT("CustomPrimitiveData"), TEXT("CustomPrimitiveData = ${1:0}"), TEXT("SL_PROPERTY: per-instance Custom Primitive Data index (Scalar/Vector)"), false },
|
|
{ TEXT("SamplerType"), TEXT("SamplerType = ${1|Color,LinearColor,Grayscale,LinearGrayscale,Alpha,Normal,Masks,DistanceFieldFont,Data|}"), TEXT("SL_PROPERTY: texture sampler type"), false },
|
|
// SL_COLLECTION / SL_IMPORT / SL_RVTSAMPLE
|
|
{ TEXT("Path"), TEXT("Path = \"${1:/Game/MPC/MPC_Name}\""), TEXT("SL_COLLECTION: parameter collection asset"), false },
|
|
{ TEXT("Parameter"), TEXT("Parameter = \"${1:ParamName}\""), TEXT("SL_COLLECTION: in-collection parameter name"), false },
|
|
{ TEXT("Namespace"), TEXT("Namespace = \"${1:Detail}\""), TEXT("SL_IMPORT: promoted-parameter namespace prefix"), false },
|
|
{ TEXT("VirtualTexture"), TEXT("VirtualTexture = \"${1:/Game/RVT/RVT_Name}\""), TEXT("SL_RVTSAMPLE: runtime virtual texture asset"), false },
|
|
{ TEXT("MaterialType"), TEXT("MaterialType = ${1|BaseColor_Normal_Roughness,BaseColor_Normal_Specular,BaseColor_Normal_Specular_YCoCg,BaseColor_Normal_Specular_Mask_YCoCg,BaseColor,Mask4,WorldHeight,Displacement|}"), TEXT("SL_RVTSAMPLE: RVT layout"), false },
|
|
{ TEXT("UV"), TEXT("UV = ${1|TexCoord0,World|}"), TEXT("SL_VTSAMPLE/SL_RVTSAMPLE: UV source (TexCoord<N> / World / SL_VALUE block name)"), false },
|
|
// BSDF modifiers (Substrate only)
|
|
{ TEXT("SubsurfaceProfile"), TEXT("SubsurfaceProfile = \"${1:/Game/Profiles/PP_SSS}\""), TEXT("Slab/Eye modifier (Substrate)"), true },
|
|
{ TEXT("SpecularProfile"), TEXT("SpecularProfile = \"${1:/Game/Profiles/PP_Spec}\""), TEXT("Slab modifier (Substrate)"), true },
|
|
{ TEXT("ToonProfile"), TEXT("ToonProfile = \"${1:/Game/Profiles/PP_Toon}\""), TEXT("Toon modifier (Substrate)"), true },
|
|
{ TEXT("SubSurfaceType"), TEXT("SubSurfaceType = ${1:Wrap}"), TEXT("Slab modifier (Substrate)"), true },
|
|
};
|
|
OutNum = UE_ARRAY_COUNT(S);
|
|
return S;
|
|
}
|
|
|
|
// SL_FRONTMATERIAL topology operators (Substrate only).
|
|
static const FDslSnippet* TopologySnippets(int32& OutNum)
|
|
{
|
|
static const FDslSnippet T[] = {
|
|
{ TEXT("VerticalLayer"), TEXT("VerticalLayer(${1:Top}, ${2:Base}, ${3:0.5})"), TEXT("SL_FRONTMATERIAL: layer Top over Base by factor"), true },
|
|
{ TEXT("HorizontalMix"), TEXT("HorizontalMix(${1:A}, ${2:B}, ${3:0.5})"), TEXT("SL_FRONTMATERIAL: blend A and B by factor"), true },
|
|
{ TEXT("Add"), TEXT("Add(${1:A}, ${2:B})"), TEXT("SL_FRONTMATERIAL: add A and B"), true },
|
|
{ TEXT("Weight"), TEXT("Weight(${1:A}, ${2:0.5})"), TEXT("SL_FRONTMATERIAL: scale coverage of A"), true },
|
|
{ TEXT("Select"), TEXT("Select(${1:A}, ${2:B}, ${3:0.5})"), TEXT("SL_FRONTMATERIAL: pick A or B by threshold"), true },
|
|
};
|
|
OutNum = UE_ARRAY_COUNT(T);
|
|
return T;
|
|
}
|
|
|
|
// Generates <Project>/.vscode/shaderlab.code-snippets — completion for the whole `.usl` DSL:
|
|
// * macro scaffolds (full function signature + body), specifier fills (with enum value choices), and
|
|
// SL_FRONTMATERIAL topology operators — hand-authored, mirroring the parser's grammar/value tokens;
|
|
// * every settable SL_SETTINGS key — reflected from editable UMaterial properties minus the
|
|
// ShaderLab-controlled set (FShaderLabSettingsApplier::GetControlledSettingReason), enums/bools as
|
|
// value choices, struct settings as dotted `Struct.Field` keys.
|
|
// Substrate-only constructs are omitted when the project runs with r.Substrate=0. Struct fields, HLSL
|
|
// types and variables are deliberately left to the HLSL language server (generated Surface_*.ush switched
|
|
// by the SHADERLAB_SUBSTRATE define). Regenerated per engine version + project mode.
|
|
static void GenerateSnippets(const FString& ProjectDir)
|
|
{
|
|
const bool bSubstrate = Substrate::IsSubstrateEnabled();
|
|
const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
|
|
|
|
int32 Num = 0;
|
|
const FDslSnippet* Macros = MacroTemplates(Num);
|
|
for (int32 i = 0; i < Num; ++i) { if (!Macros[i].bSubstrateOnly || bSubstrate) { AddSnippet(Root, FString::Printf(TEXT("SL macro %s"), Macros[i].Prefix), Macros[i].Prefix, Macros[i].Body, Macros[i].Desc); } }
|
|
|
|
const FDslSnippet* Specs = SpecifierSnippets(Num);
|
|
for (int32 i = 0; i < Num; ++i) { if (!Specs[i].bSubstrateOnly || bSubstrate) { AddSnippet(Root, FString::Printf(TEXT("SL spec %s"), Specs[i].Prefix), Specs[i].Prefix, Specs[i].Body, Specs[i].Desc); } }
|
|
|
|
if (bSubstrate)
|
|
{
|
|
const FDslSnippet* Topo = TopologySnippets(Num);
|
|
for (int32 i = 0; i < Num; ++i) { AddSnippet(Root, FString::Printf(TEXT("SL topo %s"), Topo[i].Prefix), Topo[i].Prefix, Topo[i].Body, Topo[i].Desc); }
|
|
}
|
|
|
|
// Reflected SL_SETTINGS material keys (every editable, non-controlled UMaterial property).
|
|
for (TFieldIterator<FProperty> It(UMaterial::StaticClass()); It; ++It)
|
|
{
|
|
FProperty* Prop = *It;
|
|
if (!Prop->HasAnyPropertyFlags(CPF_Edit) || Prop->HasAnyPropertyFlags(CPF_EditConst | CPF_Deprecated)) { continue; }
|
|
if (CastField<FArrayProperty>(Prop)) { continue; }
|
|
if (FShaderLabSettingsApplier::GetControlledSettingReason(Prop->GetName()) != nullptr) { continue; }
|
|
EmitSettingSnippets(Root, Prop->GetName(), Prop, 0);
|
|
}
|
|
|
|
// Retire the earlier settings-only file (superseded by the combined DSL snippet file).
|
|
IFileManager::Get().Delete(*FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("shaderlab-settings.code-snippets")), /*RequireExists*/ false, /*EvenReadOnly*/ false, /*Quiet*/ true);
|
|
WriteIfChanged(FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("shaderlab.code-snippets")), SerializeObject(Root));
|
|
}
|
|
|
|
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 builder 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:"));
|
|
GenerateUENodeHeader(PluginShaderDir);
|
|
GenerateVectorTransforms(PluginShaderDir);
|
|
GeneratePositionTransforms(PluginShaderDir);
|
|
MergeVSCodeSettings(ProjectDir);
|
|
GenerateSnippets(ProjectDir);
|
|
UE_LOG(LogShaderLabIDE, Log, TEXT("ShaderLab.IDE.Prepare: done."));
|
|
}
|
|
|
|
static TAutoConsoleVariable<bool> CVarAutoPrepare(
|
|
TEXT("ShaderLab.IDE.AutoPrepare"), true,
|
|
TEXT("On editor startup, auto-generate the .usl IDE assets (.vscode snippets/settings + stubs) if the ")
|
|
TEXT("project's .vscode/shaderlab.code-snippets is missing. Set 0 to disable."),
|
|
ECVF_Default);
|
|
|
|
static void EnsureGeneratedIfMissing()
|
|
{
|
|
// Interactive editor only — never during cook / -game (GIsEditor false) / commandlet / headless
|
|
// automation. Generating VSCode authoring assets makes no sense in those, and they must not write files.
|
|
if (!GIsEditor || IsRunningCommandlet() || FApp::IsUnattended()) { return; }
|
|
if (!CVarAutoPrepare.GetValueOnGameThread()) { return; }
|
|
|
|
// Our exclusive marker file. .vscode is git-ignored, so a fresh clone lacks it -> first-launch setup.
|
|
const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
|
|
const FString Marker = FPaths::Combine(ProjectDir, TEXT(".vscode"), TEXT("shaderlab.code-snippets"));
|
|
if (FPaths::FileExists(Marker)) { return; } // already set up — don't churn on every launch
|
|
|
|
UE_LOG(LogShaderLabIDE, Display,
|
|
TEXT("ShaderLab: IDE assets missing (%s) — auto-generating .vscode + stubs. Re-run `ShaderLab.IDE.Prepare` to refresh after an engine upgrade."),
|
|
*Marker);
|
|
Run(TArray<FString>());
|
|
}
|
|
}
|
|
|
|
void ShaderLabIDE_RunPrepare() { ShaderLabIDEPrepare_Private::Run(TArray<FString>()); }
|
|
void ShaderLabIDE_EnsureGeneratedIfMissing() { ShaderLabIDEPrepare_Private::EnsureGeneratedIfMissing(); }
|
|
|
|
static FAutoConsoleCommand GShaderLabIDEPrepareCommand(
|
|
TEXT("ShaderLab.IDE.Prepare"),
|
|
TEXT("Generate IDE completion assets for .usl: Private/ShaderLabUENode.ush, Private/UEFunctions/Transform.ush + TransformPosition.ush, .vscode/settings.json, and .vscode/shaderlab.code-snippets (full DSL)."),
|
|
FConsoleCommandWithArgsDelegate::CreateStatic(&ShaderLabIDEPrepare_Private::Run));
|