Fix -Game no matieral error

This commit is contained in:
Eragon-Brisingr
2026-07-05 11:26:32 +08:00
parent 799848f87f
commit a6fbdf10f8
30 changed files with 331 additions and 206 deletions

View File

@@ -1,57 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#include "MaterialExpressionShaderLabParameterAnchor.h"
#include "MaterialCompiler.h"
UMaterialExpressionShaderLabParameterAnchor::UMaterialExpressionShaderLabParameterAnchor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
#if WITH_EDITOR
TArrayView<FExpressionInput*> UMaterialExpressionShaderLabParameterAnchor::GetInputsView()
{
CachedInputs.Reset(Inputs.Num());
for (FExpressionInput& Input : Inputs)
{
CachedInputs.Add(&Input);
}
return CachedInputs;
}
FExpressionInput* UMaterialExpressionShaderLabParameterAnchor::GetInput(int32 InputIndex)
{
return Inputs.IsValidIndex(InputIndex) ? &Inputs[InputIndex] : nullptr;
}
FName UMaterialExpressionShaderLabParameterAnchor::GetInputName(int32 InputIndex) const
{
return FName(*FString::Printf(TEXT("Param%d"), InputIndex));
}
int32 UMaterialExpressionShaderLabParameterAnchor::Compile(FMaterialCompiler* Compiler, int32 OutputIndex)
{
// Compiled in the before-attributes pass. Compiling each input forces its static-switch selector
// (and the selected `#define`-emitting Custom node) to compile here — emitting `#define <Switch> 0/1`
// for the current permutation ahead of every body's `#if`. The returned value is unused.
int32 Last = INDEX_NONE;
for (FExpressionInput& Input : Inputs)
{
if (Input.GetTracedInput().Expression)
{
const int32 Code = Input.Compile(Compiler);
if (Code != INDEX_NONE)
{
Last = Code;
}
}
}
return Last != INDEX_NONE ? Last : Compiler->Constant(0.0f);
}
void UMaterialExpressionShaderLabParameterAnchor::GetCaption(TArray<FString>& OutCaptions) const
{
OutCaptions.Add(TEXT("ShaderLab Static-Switch Defines / Parameter Anchor"));
}
#endif // WITH_EDITOR

View File

@@ -1,63 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "MaterialValueType.h"
#include "Materials/MaterialExpressionCustomOutput.h"
#include "MaterialExpressionShaderLabParameterAnchor.generated.h"
/**
* Internal ShaderLab node with two jobs, both keeping workaround machinery off the user's body nodes:
*
* 1. Anchors parameters so the Material Instance editor lists them. StaticBool parameters are consumed
* only inside HLSL `#if`, so they would otherwise be unconnected; the instance editor's visibility
* walk only reaches parameters connected to a material output or a CustomOutput. This is a
* CustomOutput, so it is always traversed; each anchored parameter reaches one of its inputs (via
* its static-switch selector).
*
* 2. Injects per-permutation static-switch `#define`s with ZERO engine modifications. Each input is a
* StaticSwitch over two `#define <Switch> 1` / `#define <Switch> 0` Custom nodes. This node returns
* ShouldCompileBeforeAttributes()==true, so FHLSLMaterialTranslator compiles it BEFORE the material
* attributes (FrontMaterial / body). Its Compile() compiles each input, so the selected `#define`
* (only the taken StaticSwitch branch is compiled) is emitted ahead of every body's `#if`. The
* `#define` leaks forward (preprocessor is global) and the body's `#if <Switch>` sees the current
* permutation's value — including a Material Instance's static override.
*
* It only ever lives in the editor graph (never serialized into a cooked package).
*/
UCLASS(MinimalAPI, collapsecategories, hidecategories = Object)
class UMaterialExpressionShaderLabParameterAnchor : public UMaterialExpressionCustomOutput
{
GENERATED_UCLASS_BODY()
/** One input per static-switch selector (a StaticSwitch over two `#define`-emitting Custom nodes). */
UPROPERTY()
TArray<FExpressionInput> Inputs;
//~ Begin UMaterialExpressionCustomOutput Interface
// One output so the translator actually compiles us (a zero-output CustomOutput is skipped). The
// output value is unused; compiling us is purely to emit our inputs' `#define`s before attributes.
virtual int32 GetNumOutputs() const override { return 1; }
virtual FString GetFunctionName() const override { return TEXT("ShaderLabParameterAnchor"); }
#if WITH_EDITOR
virtual bool NeedsCustomOutputDefines() override { return false; } // don't emit NUM_MATERIAL_OUTPUTS_*
virtual bool ShouldCompileBeforeAttributes() override { return true; } // emit #defines ahead of body #if
#endif
//~ End UMaterialExpressionCustomOutput Interface
#if WITH_EDITOR
//~ Begin UMaterialExpression Interface
virtual TArrayView<FExpressionInput*> GetInputsView() override;
virtual FExpressionInput* GetInput(int32 InputIndex) override;
virtual FName GetInputName(int32 InputIndex) const override;
virtual EMaterialValueType GetInputValueType(int32 InputIndex) override { return MCT_Float1; }
virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) override;
virtual void GetCaption(TArray<FString>& OutCaptions) const override;
//~ End UMaterialExpression Interface
#endif
private:
/** Scratch pointer view rebuilt by GetInputsView(). */
TArray<FExpressionInput*> CachedInputs;
};

View File

@@ -1,352 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabAnchorCapabilityRegistry.h"
#include "ShaderLabModel.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpressionSceneTexture.h"
#include "Materials/MaterialExpressionDistanceToNearestSurface.h"
#include "MaterialSceneTextureId.h"
namespace ShaderLabAnchorCapability
{
static bool IsIdentChar(TCHAR C) { return FChar::IsAlnum(C) || C == TEXT('_'); }
/** True if `Token` appears in `Body` delimited by non-identifier characters (same rule as the builder). */
static bool ReferencesToken(const FString& Body, const TCHAR* Token)
{
const int32 TokenLen = FCString::Strlen(Token);
int32 From = 0;
while (true)
{
const int32 Idx = Body.Find(Token, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
if (Idx == INDEX_NONE) { return false; }
const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' ');
const int32 AfterIdx = Idx + TokenLen;
const TCHAR After = (AfterIdx < Body.Len()) ? Body[AfterIdx] : TEXT(' ');
if (!IsIdentChar(Before) && !IsIdentChar(After)) { return true; }
From = Idx + TokenLen;
}
}
/** 1-based line of character offset `Index` within `Body`, given the body starts on `BodyLine`. */
static int32 LineOf(const FString& Body, int32 Index, int32 BodyLine)
{
int32 Line = FMath::Max(BodyLine, 1);
for (int32 i = 0; i < Index && i < Body.Len(); ++i)
{
if (Body[i] == TEXT('\n')) { ++Line; }
}
return Line;
}
// --- SceneTexture -------------------------------------------------------------------------------
struct FSceneTextureIdToken
{
const TCHAR* Token; // SL_ST_* DSL token
ESceneTextureId Id; // engine id it aliases (mirrors UEFunctions/SceneTexture.ush)
const TCHAR* Display; // human name for error messages
};
static const FSceneTextureIdToken GSceneTextureIds[] =
{
{ TEXT("SL_ST_SceneColor"), PPI_SceneColor, TEXT("SceneColor") },
{ TEXT("SL_ST_SceneDepth"), PPI_SceneDepth, TEXT("SceneDepth") },
{ TEXT("SL_ST_CustomDepth"), PPI_CustomDepth, TEXT("CustomDepth") },
{ TEXT("SL_ST_CustomStencil"), PPI_CustomStencil, TEXT("CustomStencil") },
{ TEXT("SL_ST_WorldNormal"), PPI_WorldNormal, TEXT("WorldNormal") },
{ TEXT("SL_ST_PostProcessInput0"), PPI_PostProcessInput0, TEXT("PostProcessInput0") },
{ TEXT("SL_ST_PostProcessInput1"), PPI_PostProcessInput1, TEXT("PostProcessInput1") },
{ TEXT("SL_ST_PostProcessInput2"), PPI_PostProcessInput2, TEXT("PostProcessInput2") },
{ TEXT("SL_ST_PostProcessInput3"), PPI_PostProcessInput3, TEXT("PostProcessInput3") },
{ TEXT("SL_ST_PostProcessInput4"), PPI_PostProcessInput4, TEXT("PostProcessInput4") },
};
static const FSceneTextureIdToken* FindIdToken(const FString& Token)
{
for (const FSceneTextureIdToken& E : GSceneTextureIds)
{
if (Token.Equals(E.Token, ESearchCase::CaseSensitive)) { return &E; }
}
return nullptr;
}
static const TCHAR* DisplayNameOf(ESceneTextureId Id)
{
for (const FSceneTextureIdToken& E : GSceneTextureIds)
{
if (E.Id == Id) { return E.Display; }
}
return TEXT("<unknown>");
}
/**
* Scan `Body` for `UE_SceneTexture(` call sites, extract each first argument, and require it to be a
* literal SL_ST_* token (contract). Recognized ids are added to OutIds; a non-literal / unknown first
* argument appends a `.usl`-mapped error and returns false.
*/
static bool ExtractSceneTextureCallIds(const FString& Body, int32 BodyLine, const FString& SourcePath,
TSet<ESceneTextureId>& OutIds, TArray<FString>& OutErrors)
{
const TCHAR* Fn = TEXT("UE_SceneTexture");
const int32 FnLen = FCString::Strlen(Fn);
bool bOk = true;
int32 From = 0;
while (true)
{
const int32 Idx = Body.Find(Fn, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
if (Idx == INDEX_NONE) { break; }
From = Idx + FnLen;
// Delimited match only (skip UE_SceneTextureFoo and the like).
const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' ');
if (IsIdentChar(Before)) { continue; }
// Expect '(' after optional whitespace.
int32 P = Idx + FnLen;
while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; }
if (P >= Body.Len() || Body[P] != TEXT('(')) { continue; } // not a call (e.g. a #define alias)
++P;
while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; }
// Read a bare identifier for the first argument.
const int32 ArgStart = P;
while (P < Body.Len() && IsIdentChar(Body[P])) { ++P; }
const FString Arg = Body.Mid(ArgStart, P - ArgStart);
while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; }
const bool bArgIsLoneIdent = !Arg.IsEmpty() && P < Body.Len() && Body[P] == TEXT(',');
const FSceneTextureIdToken* IdTok = bArgIsLoneIdent ? FindIdToken(Arg) : nullptr;
if (!IdTok)
{
const int32 Line = LineOf(Body, Idx, BodyLine);
OutErrors.Add(FString::Printf(
TEXT("%s(%d): Scene-texture read: UE_SceneTexture id must be a literal SL_ST_* token (got '%s'). A computed id cannot be bound and would return black at runtime."),
*SourcePath, Line, Arg.IsEmpty() ? TEXT("<expression>") : *Arg));
bOk = false;
continue;
}
OutIds.Add(IdTok->Id);
}
return bOk;
}
/** Per-id domain legality, mirroring the engine's UseSceneTextureId rules (HLSLMaterialTranslator). */
static bool IsIdLegalInDomain(ESceneTextureId Id, EShaderLabDomain Domain, bool bNonOpaqueSurface, FString& OutReason)
{
const bool bPostProcess = (Domain == EShaderLabDomain::PostProcess);
const bool bDecal = (Domain == EShaderLabDomain::Decal);
switch (Id)
{
case PPI_SceneColor:
if (!(Domain == EShaderLabDomain::Surface && bNonOpaqueSurface))
{
OutReason = TEXT("SceneColor can only be read in a translucent Surface material (PostProcess should read PostProcessInput0)");
return false;
}
return true;
case PPI_PostProcessInput0:
case PPI_PostProcessInput1:
case PPI_PostProcessInput2:
case PPI_PostProcessInput3:
case PPI_PostProcessInput4:
if (!bPostProcess)
{
OutReason = TEXT("PostProcessInput* can only be read in a PostProcess material");
return false;
}
return true;
case PPI_WorldNormal:
if (!(bPostProcess || bDecal))
{
OutReason = TEXT("WorldNormal can only be read in a PostProcess or Decal material");
return false;
}
return true;
case PPI_SceneDepth:
case PPI_CustomDepth:
case PPI_CustomStencil:
if (!(bPostProcess || bDecal || bNonOpaqueSurface))
{
OutReason = TEXT("SceneDepth/CustomDepth/CustomStencil require a PostProcess, Decal, or translucent Surface material");
return false;
}
return true;
default:
OutReason = TEXT("this scene-texture id is not supported by ShaderLab");
return false;
}
}
static bool EmitSceneTexture(const FShaderLabAnchorCapabilityContext& Ctx,
TArray<UMaterialExpression*>& OutNodes, TArray<FString>& OutErrors)
{
const EShaderLabDomain Domain = Ctx.Model.Settings.Domain;
const bool bNonOpaqueSurface = (Domain == EShaderLabDomain::Surface)
&& Ctx.Model.Settings.BlendMode != EShaderLabBlendMode::Opaque
&& Ctx.Model.Settings.BlendMode != EShaderLabBlendMode::Masked;
TSet<ESceneTextureId> Ids;
bool bOk = true;
for (const FShaderLabScannedBody& B : Ctx.Bodies)
{
check(B.Text);
// Convenience wrappers with fixed ids.
if (ReferencesToken(*B.Text, TEXT("UE_SceneDepth"))) { Ids.Add(PPI_SceneDepth); }
if (ReferencesToken(*B.Text, TEXT("UE_CustomDepth"))) { Ids.Add(PPI_CustomDepth); }
if (ReferencesToken(*B.Text, TEXT("UE_CustomStencil"))) { Ids.Add(PPI_CustomStencil); }
// General form: UE_SceneTexture(<literal SL_ST_* token>, uv).
bOk &= ExtractSceneTextureCallIds(*B.Text, B.SourceLine, Ctx.SourcePath, Ids, OutErrors);
}
if (!bOk)
{
return false;
}
if (Ids.Num() == 0)
{
// A trigger token matched but resolved to no concrete read (e.g. the token appears only in a
// comment, or as a bare mention with no call). Nothing to bind — emit nothing.
return true;
}
// Per-id domain validation (friendly, .usl-level; the engine's UseSceneTextureId also enforces this).
for (const ESceneTextureId Id : Ids)
{
FString Reason;
if (!IsIdLegalInDomain(Id, Domain, bNonOpaqueSurface, Reason))
{
OutErrors.Add(FString::Printf(TEXT("Scene-texture read '%s': %s."), DisplayNameOf(Id), *Reason));
bOk = false;
}
}
if (!bOk)
{
return false;
}
// One hidden node per distinct id. Compiling it (before attributes, via the anchor) runs
// UseSceneTextureId(id) -> sets the UsedSceneTextures bit so that id's SRV is bound at runtime. The
// node's own fetch result is unused (the real read is the raw HLSL SceneTextureLookup in the body) and
// is optimized out; only the binding side effect matters.
for (const ESceneTextureId Id : Ids)
{
UMaterialExpressionSceneTexture* Node =
CastChecked<UMaterialExpressionSceneTexture>(Ctx.MakeNode(UMaterialExpressionSceneTexture::StaticClass()));
Node->SceneTextureId = Id;
OutNodes.Add(Node);
}
return true;
}
// --- Global Distance Field ----------------------------------------------------------------------
/** True if `Body` has a call `Fn(...)` with a non-empty argument list — the parameterized helper form. */
static bool HasCallWithArgs(const FString& Body, const TCHAR* Fn)
{
const int32 FnLen = FCString::Strlen(Fn);
int32 From = 0;
while (true)
{
const int32 Idx = Body.Find(Fn, ESearchCase::CaseSensitive, ESearchDir::FromStart, From);
if (Idx == INDEX_NONE) { return false; }
From = Idx + FnLen;
const TCHAR Before = (Idx > 0) ? Body[Idx - 1] : TEXT(' ');
if (IsIdentChar(Before)) { continue; } // delimited match only
int32 P = Idx + FnLen;
while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; }
if (P >= Body.Len() || Body[P] != TEXT('(')) { continue; }
++P;
while (P < Body.Len() && FChar::IsWhitespace(Body[P])) { ++P; }
if (P < Body.Len() && Body[P] != TEXT(')')) { return true; } // non-empty arg list
}
}
static bool EmitGlobalDistanceField(const FShaderLabAnchorCapabilityContext& Ctx,
TArray<UMaterialExpression*>& OutNodes, TArray<FString>& /*OutErrors*/)
{
// Only the parameterized helper form (UE_DistanceToNearestSurface(worldPos) / UE_DistanceFieldGradient(
// worldPos)) needs us: the nullary form is a wired intrinsic whose real node already sets the flag. So
// fire only when a call carries an argument; a nullary-only shader adds no redundant hidden node.
bool bParameterized = false;
for (const FShaderLabScannedBody& B : Ctx.Bodies)
{
check(B.Text);
if (HasCallWithArgs(*B.Text, TEXT("UE_DistanceToNearestSurface"))
|| HasCallWithArgs(*B.Text, TEXT("UE_DistanceFieldGradient")))
{
bParameterized = true;
break;
}
}
if (!bParameterized)
{
return true; // nullary-only: nothing to do.
}
// A single hidden node suffices: its Compile sets MaterialCompilationOutput.bUsesGlobalDistanceField,
// which drives the material relevance the renderer uses to prepare the global distance field for the
// view. The node's optional Position input is left unconnected (the real, body-computed position flows
// through the raw HLSL helper). Platform support is enforced by the engine (ErrorUnlessPlatformSupports).
OutNodes.Add(Ctx.MakeNode(UMaterialExpressionDistanceToNearestSurface::StaticClass()));
return true;
}
}
FShaderLabAnchorCapabilityRegistry& FShaderLabAnchorCapabilityRegistry::Get()
{
static FShaderLabAnchorCapabilityRegistry Instance;
return Instance;
}
FShaderLabAnchorCapabilityRegistry::FShaderLabAnchorCapabilityRegistry()
{
RegisterBuiltins();
}
void FShaderLabAnchorCapabilityRegistry::Register(FShaderLabAnchorCapability Capability)
{
check(!Capability.Id.IsNone());
check(Capability.TriggerTokens.Num() > 0);
check(Capability.Emit);
for (FShaderLabAnchorCapability& Existing : Capabilities)
{
if (Existing.Id == Capability.Id)
{
Existing = MoveTemp(Capability); // Override by Id.
return;
}
}
Capabilities.Add(MoveTemp(Capability));
}
void FShaderLabAnchorCapabilityRegistry::ForEach(TFunctionRef<void(const FShaderLabAnchorCapability&)> Fn) const
{
for (const FShaderLabAnchorCapability& Cap : Capabilities)
{
Fn(Cap);
}
}
void FShaderLabAnchorCapabilityRegistry::RegisterBuiltins()
{
{
FShaderLabAnchorCapability Cap;
Cap.Id = TEXT("SceneTexture");
Cap.TriggerTokens = { TEXT("UE_SceneTexture"), TEXT("UE_SceneDepth"), TEXT("UE_CustomDepth"), TEXT("UE_CustomStencil") };
Cap.Emit = &ShaderLabAnchorCapability::EmitSceneTexture;
Register(MoveTemp(Cap));
}
{
FShaderLabAnchorCapability Cap;
Cap.Id = TEXT("GlobalDistanceField");
Cap.TriggerTokens = { TEXT("UE_DistanceToNearestSurface"), TEXT("UE_DistanceFieldGradient") };
Cap.Emit = &ShaderLabAnchorCapability::EmitGlobalDistanceField;
Register(MoveTemp(Cap));
}
}

View File

@@ -3,108 +3,42 @@
#include "DirectoryWatcherModule.h"
#include "Editor.h"
#include "Engine/Engine.h"
#include "HAL/FileManager.h"
#include "IDirectoryWatcher.h"
#include "Interfaces/IPluginManager.h"
#include "MaterialEditingLibrary.h"
#include "MaterialShared.h"
#include "Materials/Material.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "ShaderCore.h"
#include "Misc/FileHelper.h"
#include "ShaderLabDiscovery.h"
#include "ShaderLabGraphBuilder.h"
#include "ShaderLabImportResolver.h"
#include "ShaderLabMaterialInstanceConstant.h"
#include "ShaderLabMaterialRegistry.h"
#include "ShaderLabModel.h"
#include "ShaderLabParser.h"
#include "ShaderLabSubsystem.h"
#include "ShaderLabVSCodeButton.h"
#include "UObject/Package.h"
#include "UObject/UObjectIterator.h"
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabEditor, Log, All);
// This module hosts only the interactive-editor UX for ShaderLab (GIsEditor-gated): the .usl directory
// watcher for hot reload, the "open in VSCode" toolbar button, the Material Instance factory, and
// refreshing open Material Instance editors after a rebuild. The material-graph-building core moved to
// UShaderLabBuilder (an UncookedOnly module) so it also runs under `-game`, where this Editor module and
// its editor-UI dependencies (UnrealEd/MaterialEditor) are absent.
namespace
{
/** Editor-side reaction to a base material needing build: rebuild graph + trigger compile. */
void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model)
/**
* Editor-only reaction to a base material finishing its (re)build: refresh any open Material Instance
* editors so newly added/removed parameters (scalars, textures, static-bool switches) appear without
* reopening the editor. RegenerateArrays pulls the parameter set from the freshly rebuilt parent; mirrors
* what the stock material editor does after RecompileMaterial. Bound to OnMaterialGraphBuilt (not
* OnBuildMaterial) so it runs strictly AFTER the builder finished the graph + compile. Guarded on GEditor:
* the builder also runs during cook/headless registration and under `-game`, where there is no interactive
* editor (and RebuildMaterialInstanceEditors dereferences GEditor).
*/
void RefreshInstanceEditors(UMaterial& Material)
{
// Absolute, forward-slashed source path for the poison node's `#line` mapping (matches the graph
// builder's own convention so compiler errors click through to the .usl).
FString SrcPath = FPaths::ConvertRelativePathToFull(Model.SourceFilePath);
SrcPath.ReplaceInline(TEXT("\\"), TEXT("/"));
if (Model.LoadErrors.Num() > 0)
{
// Parse-stage failure: the .usl never produced a valid model. Route the diagnostics through
// the shader compiler as a poison material so they surface in the MIC editor / cook, not just
// the log.
for (const FString& E : Model.LoadErrors)
{
UE_LOG(LogShaderLabEditor, Error, TEXT("ShaderLab parse '%s': %s"), *Model.ShaderName, *E);
}
FShaderLabGraphBuilder::BuildPoisonInto(Material, Model.LoadErrors, SrcPath);
}
else
{
TArray<FString> Errors;
if (!FShaderLabGraphBuilder::BuildInto(Material, Model, Errors))
{
for (const FString& E : Errors)
{
UE_LOG(LogShaderLabEditor, Error, TEXT("ShaderLab build '%s': %s"), *Model.ShaderName, *E);
}
// BuildInto already cleared the graph before failing — don't leave the base silently empty.
// Replace it with a poison material so the build errors compile-fail with the same
// visibility as an HLSL error, instead of vanishing into the log.
FShaderLabGraphBuilder::BuildPoisonInto(Material, Errors, SrcPath);
}
}
// Recompile the base AND propagate to everything that depends on it. A bare PostEditChange
// recompiles only the base shell; the instances carry their own static-permutation shader maps
// (and the level components render those), so on a hot-reload they must be recompiled and
// re-pushed too. FMaterialUpdateContext does exactly that on scope exit: recompile the listed
// material, then recache every dependent instance and refresh the components/viewports using it.
{
FMaterialUpdateContext UpdateContext;
UpdateContext.AddMaterial(&Material);
Material.PreEditChange(nullptr);
Material.PostEditChange();
}
// Re-derive the cook-inclusion data (CachedExpressionData copy + profile overrides) on every ROOT
// UShaderLabMaterialInstanceConstant of this base. The FMaterialUpdateContext above recompiles the
// dependent shader maps but does NOT refresh that derived data (it's built once at PostLoad and pinned by
// bLoadedCachedExpressionData=true), so a hot-reload that added/removed an MPC or profile would otherwise
// leave a resident MIC stale — and an in-editor iterative cook would serialize the stale references.
//
// Inheritance chain: only a ROOT (immediate parent == this base) carries the derived data; CHILD instances
// (parent is another instance, however deep) hold none and resolve up the chain to the root at query time,
// and their shared shader maps were just recompiled by FMaterialUpdateContext — so refreshing the roots is
// sufficient and children need no separate pass. RefreshShaderLabDerivedData self-guards via
// GetShaderLabRootBase(), so it is a safe no-op on anything that isn't a root of this base. Skip CDOs and
// objects being GC'd.
for (TObjectIterator<UShaderLabMaterialInstanceConstant> It(
/*AdditionalExclusionFlags*/ RF_ClassDefaultObject,
/*bIncludeDerivedClasses*/ true,
/*InternalExclusionFlags*/ EInternalObjectFlags::Garbage); It; ++It)
{
if (It->Parent == &Material)
{
It->RefreshShaderLabDerivedData();
}
}
// Refresh any open Material Instance editors built on this base so newly added/removed parameters
// (scalars, textures, static-bool switches) appear without reopening the editor. RegenerateArrays
// pulls the parameter set from the freshly rebuilt parent; mirrors what the stock material editor
// does after RecompileMaterial. Must run after PostEditChange so the parent's parameters are current.
// Guarded on GEditor: this handler also runs during cook/headless registration, where there is no
// interactive editor (and RebuildMaterialInstanceEditors dereferences GEditor).
if (GEditor)
{
UMaterialEditingLibrary::RebuildMaterialInstanceEditors(&Material);
@@ -117,45 +51,8 @@ class FShaderLabEditorModule : public IModuleInterface
public:
virtual void StartupModule() override
{
// Map the plugin's Shaders/ directory so generated Custom nodes can #include
// "/Plugin/ShaderLab/Private/ShaderLabCommon.ush" at shader-compile time.
if (const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UShaderLab")))
{
const FString ShaderDir = FPaths::Combine(Plugin->GetBaseDir(), TEXT("Shaders"));
if (!AllShaderSourceDirectoryMappings().Contains(TEXT("/Plugin/ShaderLab")))
{
AddShaderSourceDirectoryMapping(TEXT("/Plugin/ShaderLab"), ShaderDir);
}
}
// Map the project's Shaders/ directory to the virtual root "/Project" so user .ush library
// files placed there (alongside .usl) can be #included from a shader's Includes { } block.
{
const FString ProjectShaderDir = FPaths::Combine(FPaths::ProjectDir(), TEXT("Shaders"));
if (FPaths::DirectoryExists(ProjectShaderDir) && !AllShaderSourceDirectoryMappings().Contains(TEXT("/Project")))
{
AddShaderSourceDirectoryMapping(TEXT("/Project"), ProjectShaderDir);
}
}
// Map the plugin's Intermediate/ShaderLabGen directory (a build-time artifact dir) so the graph
// builder can #include per-shader generated local-code headers (`/UShaderLabGen/<Name>.gen.ush`).
// AddShaderSourceDirectoryMapping requires the real dir to already exist, so create it first.
{
const FString GenDir = FShaderLabGraphBuilder::GetGeneratedShaderDir();
const TCHAR* GenRoot = FShaderLabGraphBuilder::GetGeneratedVirtualRoot();
if (!GenDir.IsEmpty())
{
IFileManager::Get().MakeDirectory(*GenDir, /*Tree*/ true);
if (FPaths::DirectoryExists(GenDir) && !AllShaderSourceDirectoryMappings().Contains(GenRoot))
{
AddShaderSourceDirectoryMapping(GenRoot, GenDir);
}
}
}
// Fill in / recompile base materials whenever the registry asks (startup + hot reload).
BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile);
// After the builder rebuilds a base material's graph, refresh any open MIC editors on it.
GraphBuiltHandle = FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().AddStatic(&RefreshInstanceEditors);
StartWatchingSources();
@@ -169,10 +66,10 @@ public:
{
StopWatchingSources();
ShaderLabVSCodeButton::Unregister();
if (BuildHandle.IsValid())
if (GraphBuiltHandle.IsValid())
{
FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle);
BuildHandle.Reset();
FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().Remove(GraphBuiltHandle);
GraphBuiltHandle.Reset();
}
}
@@ -219,25 +116,6 @@ private:
WatchedRoots.Reset();
}
/** Resolve a virtual shader path to disk via the registered shader-source directory mappings. */
static bool ResolveVirtualToDisk(const FString& VirtualPath, FString& OutDiskPath)
{
FString Best, BestDir;
for (const TPair<FString, FString>& Pair : AllShaderSourceDirectoryMappings())
{
if ((VirtualPath.StartsWith(Pair.Key + TEXT("/")) || VirtualPath == Pair.Key) && Pair.Key.Len() > Best.Len())
{
Best = Pair.Key;
BestDir = Pair.Value;
}
}
if (Best.IsEmpty()) { return false; }
FString Rest = VirtualPath.Mid(Best.Len());
Rest.RemoveFromStart(TEXT("/"));
OutDiskPath = FPaths::Combine(BestDir, Rest);
return true;
}
static FString NormalizePath(const FString& Path)
{
FString N = FPaths::ConvertRelativePathToFull(Path);
@@ -269,7 +147,7 @@ private:
FShaderLabImportResolver::FSourceLoader Loader =
[](const FString& VPath, FString& OutSrc, FString& OutDisk, FString& OutErr) -> bool
{
if (!ResolveVirtualToDisk(VPath, OutDisk)) { OutErr = TEXT("unmapped"); return false; }
if (!FShaderLabGraphBuilder::ResolveVirtualShaderFile(VPath, OutDisk)) { OutErr = TEXT("unmapped"); return false; }
if (!FFileHelper::LoadFileToString(OutSrc, *OutDisk)) { OutErr = TEXT("read failed"); return false; }
return true;
};
@@ -331,7 +209,7 @@ private:
}
}
FDelegateHandle BuildHandle;
FDelegateHandle GraphBuiltHandle;
TMap<FString, FDelegateHandle> WatchedRoots;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,434 +0,0 @@
// 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. <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.
#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
#include "HAL/IConsoleManager.h"
#include "UObject/Class.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 `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;
}
if (!bChanged)
{
UE_LOG(LogShaderLabIDE, Log, TEXT(" unchanged: %s"), *SettingsPath);
return;
}
WriteIfChanged(SettingsPath, SerializeObject(Settings.ToSharedRef()));
}
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:"));
GenerateUENodeHeader(PluginShaderDir);
GenerateVectorTransforms(PluginShaderDir);
GeneratePositionTransforms(PluginShaderDir);
MergeVSCodeSettings(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

@@ -1,68 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#include "ShaderLabIntrinsicRegistry.h"
#include "ShaderLabIntrinsics.h"
void ShaderLabIntrinsic_Private::AddSimple(
FShaderLabIntrinsicRegistry& Registry,
const TCHAR* Name,
EShaderLabIntrinsicFrequency Frequency,
const TCHAR* ReturnType,
TFunction<UMaterialExpression*(UMaterial&)> Make,
int32 OutputIndex)
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(Name);
Desc.Frequency = Frequency;
Desc.ReturnType = ReturnType;
Desc.OutputIndex = OutputIndex;
Desc.MakeNode = [Make = MoveTemp(Make)](UMaterial& M, const TArray<FString>&, FString&) { return Make(M); };
Registry.Register(MoveTemp(Desc));
}
FShaderLabIntrinsicRegistry& FShaderLabIntrinsicRegistry::Get()
{
static FShaderLabIntrinsicRegistry Instance;
return Instance;
}
FShaderLabIntrinsicRegistry::FShaderLabIntrinsicRegistry()
{
RegisterBuiltins();
}
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));
}
const FShaderLabIntrinsicDesc* FShaderLabIntrinsicRegistry::Find(FName Name) const
{
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()
{
RegisterMeshIntrinsics(*this);
RegisterViewIntrinsics(*this);
RegisterObjectIntrinsics(*this);
RegisterInstancingIntrinsics(*this);
RegisterTimeIntrinsics(*this);
RegisterParticleIntrinsics(*this);
RegisterDecalIntrinsics(*this);
RegisterAtmosphereIntrinsics(*this);
RegisterDistanceFieldIntrinsics(*this);
}

View File

@@ -1,47 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Internal shared helpers + per-category registration entry points for the builtin `UE_` intrinsics.
// Registration is split by category (mesh/view/object/instancing/time/particle/decal/atmosphere) into
// separate .cpp files for maintainability; each defines one RegisterXxxIntrinsics(registry) that the
// registry's RegisterBuiltins() calls.
#pragma once
#include "CoreMinimal.h"
#include "Materials/Material.h"
#include "ShaderLabIntrinsicRegistry.h"
namespace ShaderLabIntrinsic_Private
{
/** NewObject a no-config expression node and add it to the material's collection. */
template <typename T>
UMaterialExpression* MakeSimple(UMaterial& Material)
{
T* Expr = NewObject<T>(&Material);
Material.GetExpressionCollection().AddExpression(Expr);
return Expr;
}
/**
* Register a no-arg intrinsic that maps to a source node. `OutputIndex` selects which output pin to
* connect (non-zero for one facet of a multi-output node, e.g. a directional light's Direction).
*/
void AddSimple(
FShaderLabIntrinsicRegistry& Registry,
const TCHAR* Name,
EShaderLabIntrinsicFrequency Frequency,
const TCHAR* ReturnType,
TFunction<UMaterialExpression*(UMaterial&)> Make,
int32 OutputIndex = 0);
}
// Per-category registration (defined in ShaderLabIntrinsics_<Category>.cpp).
void RegisterMeshIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterViewIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterObjectIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterInstancingIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry);
void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry);

View File

@@ -1,34 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Atmosphere / sky-light intrinsics.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionAtmosphericLightColor.h"
#include "Materials/MaterialExpressionAtmosphericLightVector.h"
#include "Materials/MaterialExpressionSkyAtmosphereLightDirection.h"
void RegisterAtmosphereIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("AtmosphericLightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionAtmosphericLightVector>(M); });
AddSimple(Registry, TEXT("AtmosphericLightColor"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionAtmosphericLightColor>(M); });
// SkyAtmosphereLightDirection(LightIndex) — const int selects the atmosphere light.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("SkyAtmosphereLightDirection"));
Desc.ReturnType = TEXT("float3");
Desc.Params = { { TEXT("int"), TEXT("LightIndex"), TEXT("0") } };
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString&) -> UMaterialExpression*
{
UMaterialExpressionSkyAtmosphereLightDirection* E = NewObject<UMaterialExpressionSkyAtmosphereLightDirection>(&M);
if (Args.Num() >= 1) { E->LightIndex = FCString::Atoi(*Args[0]); }
M.GetExpressionCollection().AddExpression(E);
return E;
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -1,23 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Decal intrinsics (meaningful in the Decal domain; the instance/domain gates usage).
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionDecalColor.h"
#include "Materials/MaterialExpressionDecalDerivative.h"
#include "Materials/MaterialExpressionDecalLifetimeOpacity.h"
void RegisterDecalIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
// DecalColor's default output pin is RGB (float3).
AddSimple(Registry, TEXT("DecalColor"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDecalColor>(M); });
AddSimple(Registry, TEXT("DecalLifetimeOpacity"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDecalLifetimeOpacity>(M); });
// DecalDerivative is multi-output (DDX, DDY) — one intrinsic per output pin.
AddSimple(Registry, TEXT("DecalDerivativeDDX"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDecalDerivative>(M); }, 0);
AddSimple(Registry, TEXT("DecalDerivativeDDY"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDecalDerivative>(M); }, 1);
}

View File

@@ -1,43 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Global-distance-field intrinsics. These names are OVERLOADED (bAllowRawHelperWithArgs): the nullary form
// UE_DistanceToNearestSurface() / UE_DistanceFieldGradient() is this wired intrinsic (its real node reads the
// current pixel's world position and sets bUsesGlobalDistanceField), while UE_Name(worldPos) is a same-named
// raw HLSL helper in UEFunctions/DistanceField.ush whose binding is supplied by the "GlobalDistanceField"
// anchor capability. The parser routes a call with args to the helper and a nullary call to this node.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionDistanceToNearestSurface.h"
#include "Materials/MaterialExpressionDistanceFieldGradient.h"
void RegisterDistanceFieldIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = TEXT("DistanceToNearestSurface");
Desc.Frequency = EFreq::Any;
Desc.ReturnType = TEXT("float");
Desc.bAllowRawHelperWithArgs = true;
Desc.MakeNode = [](UMaterial& M, const TArray<FString>&, FString&)
{
return MakeSimple<UMaterialExpressionDistanceToNearestSurface>(M);
};
Registry.Register(MoveTemp(Desc));
}
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = TEXT("DistanceFieldGradient");
Desc.Frequency = EFreq::Any;
Desc.ReturnType = TEXT("float3");
Desc.bAllowRawHelperWithArgs = true;
Desc.MakeNode = [](UMaterial& M, const TArray<FString>&, FString&)
{
return MakeSimple<UMaterialExpressionDistanceFieldGradient>(M);
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -1,36 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Per-instance intrinsics.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionPerInstanceCustomData.h"
#include "Materials/MaterialExpressionPerInstanceFadeAmount.h"
#include "Materials/MaterialExpressionPerInstanceRandom.h"
void RegisterInstancingIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("PerInstanceRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceRandom>(M); });
AddSimple(Registry, TEXT("PerInstanceFadeAmount"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPerInstanceFadeAmount>(M); });
// PerInstanceCustomData(DataIndex[, ConstDefaultValue]) — const args + optional input pin 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);
if (Args.Num() >= 1) { E->DataIndex = static_cast<uint32>(FCString::Atoi(*Args[0])); }
if (Args.Num() >= 2) { E->ConstDefaultValue = FCString::Atof(*Args[1]); }
M.GetExpressionCollection().AddExpression(E);
return E;
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -1,51 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Mesh / surface attribute intrinsics (vertex normals/tangents, colors, UVs).
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionLightmapUVs.h"
#include "Materials/MaterialExpressionPixelNormalWS.h"
#include "Materials/MaterialExpressionPreSkinnedNormal.h"
#include "Materials/MaterialExpressionPreSkinnedPosition.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionTwoSidedSign.h"
#include "Materials/MaterialExpressionVertexColor.h"
#include "Materials/MaterialExpressionVertexNormalWS.h"
#include "Materials/MaterialExpressionVertexTangentWS.h"
void RegisterMeshIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("VertexNormalWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexNormalWS>(M); });
AddSimple(Registry, TEXT("VertexTangentWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexTangentWS>(M); });
AddSimple(Registry, TEXT("PixelNormalWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelNormalWS>(M); });
AddSimple(Registry, TEXT("VertexColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionVertexColor>(M); });
AddSimple(Registry, TEXT("TwoSidedSign"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTwoSidedSign>(M); });
AddSimple(Registry, TEXT("LightmapUVs"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightmapUVs>(M); });
AddSimple(Registry, TEXT("PreSkinnedNormal"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedNormal>(M); });
AddSimple(Registry, TEXT("PreSkinnedPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedPosition>(M); });
// TextureCoordinate(Index[, UTiling, VTiling]) — const args configure the node.
{
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);
if (Args.Num() >= 1) { E->CoordinateIndex = FCString::Atoi(*Args[0]); }
if (Args.Num() >= 2) { E->UTiling = FCString::Atof(*Args[1]); }
if (Args.Num() >= 3) { E->VTiling = FCString::Atof(*Args[2]); }
M.GetExpressionCollection().AddExpression(E);
return E;
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -1,69 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Object / primitive / bounds intrinsics.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionActorPositionWS.h"
#include "Materials/MaterialExpressionBounds.h"
#include "Materials/MaterialExpressionDistanceCullFade.h"
#include "Materials/MaterialExpressionLocalPosition.h"
#include "Materials/MaterialExpressionObjectBounds.h"
#include "Materials/MaterialExpressionObjectLocalBounds.h"
#include "Materials/MaterialExpressionObjectOrientation.h"
#include "Materials/MaterialExpressionObjectPositionWS.h"
#include "Materials/MaterialExpressionObjectRadius.h"
#include "Materials/MaterialExpressionPrecomputedAOMask.h"
#include "Materials/MaterialExpressionPreSkinnedLocalBounds.h"
void RegisterObjectIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("LocalPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLocalPosition>(M); });
AddSimple(Registry, TEXT("ActorPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionActorPositionWS>(M); });
AddSimple(Registry, TEXT("ObjectPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectPositionWS>(M); });
AddSimple(Registry, TEXT("ObjectRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectRadius>(M); });
AddSimple(Registry, TEXT("ObjectBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectBounds>(M); });
// Multi-output nodes: connect output 0 (matches existing behavior).
AddSimple(Registry, TEXT("ObjectLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectLocalBounds>(M); });
AddSimple(Registry, TEXT("ObjectOrientation"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionObjectOrientation>(M); });
AddSimple(Registry, TEXT("PreSkinnedLocalBounds"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPreSkinnedLocalBounds>(M); });
AddSimple(Registry, TEXT("DistanceCullFade"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDistanceCullFade>(M); });
AddSimple(Registry, TEXT("PrecomputedAOMask"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPrecomputedAOMask>(M); });
// Bounds(Space) — unified bounds node; Space is an EMaterialExpressionBoundsType token (MEILB_*).
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("Bounds"));
Desc.ReturnType = TEXT("float3");
Desc.Doc = TEXT("Space is an EMaterialExpressionBoundsType token (e.g. MEILB_ObjectLocal).");
FShaderLabIntrinsicParam SpaceParam;
SpaceParam.Name = TEXT("Space");
SpaceParam.Enum = StaticEnum<EMaterialExpressionBoundsType>();
check(SpaceParam.Enum);
Desc.Params = { MoveTemp(SpaceParam) };
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString& OutError) -> UMaterialExpression*
{
UMaterialExpressionBounds* E = NewObject<UMaterialExpressionBounds>(&M);
if (Args.Num() >= 1)
{
UEnum* Enum = StaticEnum<EMaterialExpressionBoundsType>();
check(Enum);
const int64 Value = Enum->GetValueByNameString(Args[0]);
if (Value == INDEX_NONE)
{
OutError = FString::Printf(TEXT("unknown bounds space '%s'"), *Args[0]);
return nullptr;
}
E->Type = static_cast<EMaterialExpressionBoundsType>(Value);
}
M.GetExpressionCollection().AddExpression(E);
return E;
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -1,44 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Particle / sprite intrinsics. These only produce meaningful values on particle vertex factories;
// the instance enables the matching usage, so there is no base-level usage gate here.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionParticleColor.h"
#include "Materials/MaterialExpressionParticleDirection.h"
#include "Materials/MaterialExpressionParticleMacroUV.h"
#include "Materials/MaterialExpressionParticleMotionBlurFade.h"
#include "Materials/MaterialExpressionParticlePositionWS.h"
#include "Materials/MaterialExpressionParticleRadius.h"
#include "Materials/MaterialExpressionParticleRandom.h"
#include "Materials/MaterialExpressionParticleRelativeTime.h"
#include "Materials/MaterialExpressionParticleSize.h"
#include "Materials/MaterialExpressionParticleSpeed.h"
#include "Materials/MaterialExpressionParticleSpriteRotation.h"
#include "Materials/MaterialExpressionParticleSubUVProperties.h"
#include "Materials/MaterialExpressionSphericalParticleOpacity.h"
void RegisterParticleIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("ParticleColor"), EFreq::Any, TEXT("float4"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleColor>(M); });
AddSimple(Registry, TEXT("ParticlePositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticlePositionWS>(M); });
AddSimple(Registry, TEXT("ParticleDirection"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleDirection>(M); });
AddSimple(Registry, TEXT("ParticleSpeed"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSpeed>(M); });
AddSimple(Registry, TEXT("ParticleRadius"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleRadius>(M); });
AddSimple(Registry, TEXT("ParticleSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSize>(M); });
AddSimple(Registry, TEXT("ParticleRelativeTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleRelativeTime>(M); });
AddSimple(Registry, TEXT("ParticleMotionBlurFade"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleMotionBlurFade>(M); });
AddSimple(Registry, TEXT("ParticleRandom"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleRandom>(M); });
AddSimple(Registry, TEXT("ParticleSpriteRotation"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSpriteRotation>(M); });
AddSimple(Registry, TEXT("ParticleMacroUV"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleMacroUV>(M); });
AddSimple(Registry, TEXT("SphericalParticleOpacity"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionSphericalParticleOpacity>(M); });
// ParticleSubUVProperties is multi-output (TexCoord0, TexCoord1, Blend) — one intrinsic per pin.
AddSimple(Registry, TEXT("ParticleSubUVTexCoord0"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSubUVProperties>(M); }, 0);
AddSimple(Registry, TEXT("ParticleSubUVTexCoord1"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSubUVProperties>(M); }, 1);
AddSimple(Registry, TEXT("ParticleSubUVBlend"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionParticleSubUVProperties>(M); }, 2);
}

View File

@@ -1,17 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// Time intrinsics.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionDeltaTime.h"
#include "Materials/MaterialExpressionTime.h"
void RegisterTimeIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("Time"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionTime>(M); });
AddSimple(Registry, TEXT("DeltaTime"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionDeltaTime>(M); });
}

View File

@@ -1,79 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
//
// View / camera / screen intrinsics.
#include "ShaderLabIntrinsics.h"
#include "Materials/MaterialExpressionCameraPositionWS.h"
#include "Materials/MaterialExpressionCameraVectorWS.h"
#include "Materials/MaterialExpressionEyeAdaptation.h"
#include "Materials/MaterialExpressionIsFirstPerson.h"
#include "Materials/MaterialExpressionIsOrthographic.h"
#include "Materials/MaterialExpressionLightVector.h"
#include "Materials/MaterialExpressionMainDirectionalLight.h"
#include "Materials/MaterialExpressionPixelDepth.h"
#include "Materials/MaterialExpressionReflectionVectorWS.h"
#include "Materials/MaterialExpressionSceneTexelSize.h"
#include "Materials/MaterialExpressionScreenPosition.h"
#include "Materials/MaterialExpressionViewProperty.h"
#include "Materials/MaterialExpressionViewSize.h"
#include "Materials/MaterialExpressionWorldPosition.h"
void RegisterViewIntrinsics(FShaderLabIntrinsicRegistry& Registry)
{
using namespace ShaderLabIntrinsic_Private;
using EFreq = EShaderLabIntrinsicFrequency;
AddSimple(Registry, TEXT("WorldPosition"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionWorldPosition>(M); });
AddSimple(Registry, TEXT("CameraPositionWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraPositionWS>(M); });
AddSimple(Registry, TEXT("CameraVectorWS"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionCameraVectorWS>(M); });
AddSimple(Registry, TEXT("LightVector"), EFreq::Any, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionLightVector>(M); });
AddSimple(Registry, TEXT("PixelDepth"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionPixelDepth>(M); });
AddSimple(Registry, TEXT("ScreenPosition"), EFreq::PixelOnly, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionScreenPosition>(M); });
AddSimple(Registry, TEXT("ReflectionVectorWS"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionReflectionVectorWS>(M); });
AddSimple(Registry, TEXT("ViewSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionViewSize>(M); });
AddSimple(Registry, TEXT("SceneTexelSize"), EFreq::Any, TEXT("float2"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionSceneTexelSize>(M); });
AddSimple(Registry, TEXT("IsOrthographic"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionIsOrthographic>(M); });
AddSimple(Registry, TEXT("EyeAdaptation"), EFreq::PixelOnly, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionEyeAdaptation>(M); });
AddSimple(Registry, TEXT("IsFirstPerson"), EFreq::Any, TEXT("float"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionIsFirstPerson>(M); });
// MainDirectionalLight is multi-output (Illuminance, Direction) — one intrinsic per output pin.
AddSimple(Registry, TEXT("MainDirectionalLightIlluminance"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionMainDirectionalLight>(M); }, 0);
AddSimple(Registry, TEXT("MainDirectionalLightDirection"), EFreq::PixelOnly, TEXT("float3"), [](UMaterial& M) { return MakeSimple<UMaterialExpressionMainDirectionalLight>(M); }, 1);
// ViewProperty(Property) — Property is an EMaterialExposedViewProperty token.
{
FShaderLabIntrinsicDesc Desc;
Desc.Name = FName(TEXT("ViewProperty"));
Desc.ReturnType = TEXT("float4");
Desc.Doc = TEXT("Property is an EMaterialExposedViewProperty token (e.g. MEVP_FieldOfView).");
FShaderLabIntrinsicParam PropertyParam;
PropertyParam.Name = TEXT("Property");
// Safe: UShaderLabEditor depends on Engine, whose reflected enums register before any editor
// code runs, and this executes lazily at first registry use (build/command/test time).
PropertyParam.Enum = StaticEnum<EMaterialExposedViewProperty>();
check(PropertyParam.Enum);
Desc.Params = { MoveTemp(PropertyParam) };
Desc.MakeNode = [](UMaterial& M, const TArray<FString>& Args, FString& OutError) -> UMaterialExpression*
{
UMaterialExpressionViewProperty* E = NewObject<UMaterialExpressionViewProperty>(&M);
if (Args.Num() >= 1)
{
UEnum* Enum = StaticEnum<EMaterialExposedViewProperty>();
check(Enum);
const int64 Value = Enum->GetValueByNameString(Args[0]);
if (Value == INDEX_NONE)
{
OutError = FString::Printf(TEXT("unknown view property '%s'"), *Args[0]);
return nullptr;
}
E->Property = static_cast<EMaterialExposedViewProperty>(Value);
}
M.GetExpressionCollection().AddExpression(E);
return E;
};
Registry.Register(MoveTemp(Desc));
}
}

View File

@@ -8,7 +8,7 @@
#include "MaterialEditorModule.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "ShaderCore.h"
#include "ShaderLabGraphBuilder.h"
#include "ShaderLabMaterialInstanceConstant.h"
#include "Styling/AppStyle.h"
#include "Textures/SlateIcon.h"
@@ -22,21 +22,16 @@ namespace ShaderLabVSCodeButton
{
static FDelegateHandle GExtenderDelegateHandle;
/** Reverse the ShaderLabPath virtual path (e.g. "/Project/Examples/Basic.usl") to a disk path via the
* registered shader mappings (generic — /Project, /Plugin/<any>, /Engine). Empty if unresolved. */
/** Reverse the ShaderLabPath virtual path (e.g. "/Project/Examples/Basic.usl") to an absolute disk path
* via the shared shader-mapping resolver (generic — /Project, /Plugin/<any>, /Engine). Empty if unresolved. */
static FString ResolveVirtualToDisk(const FString& VirtualPath)
{
int32 BestKeyLen = -1;
FString BestDisk;
for (const TPair<FString, FString>& Mapping : AllShaderSourceDirectoryMappings())
FString Disk;
if (!FShaderLabGraphBuilder::ResolveVirtualShaderFile(VirtualPath, Disk))
{
if (VirtualPath.StartsWith(Mapping.Key + TEXT("/"), ESearchCase::IgnoreCase) && Mapping.Key.Len() > BestKeyLen)
{
BestKeyLen = Mapping.Key.Len();
BestDisk = Mapping.Value / VirtualPath.RightChop(Mapping.Key.Len() + 1);
}
return FString();
}
return (BestKeyLen >= 0) ? FPaths::ConvertRelativePathToFull(BestDisk) : FString();
return FPaths::ConvertRelativePathToFull(Disk);
}
static void OpenInVSCode(TWeakObjectPtr<UShaderLabMaterialInstanceConstant> MICWeak)

View File

@@ -1,98 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
class UMaterialExpression;
struct FShaderLabModel;
/**
* One body of HLSL scanned for capability triggers: the verbatim text plus its 1-based `.usl` source line
* (so a capability can map a contract violation back to the exact authored line). Covers a shader's own
* pixel/vertex bodies AND every reachable `.uslfunc` library-function body, since a library function called
* from a body is inlined into the same Custom node and can equally reference a raw scene-texture helper.
*/
struct FShaderLabScannedBody
{
/** Non-owning pointer to the body text (lives in the FShaderLabModel / resolved program). */
const FString* Text = nullptr;
/** 1-based source line of the body's first character, for error line-mapping. */
int32 SourceLine = 0;
};
/** Everything a capability needs to detect usage, validate it, and create its hidden anchor node(s). */
struct FShaderLabAnchorCapabilityContext
{
/** The material being built (target of MakeNode). */
UMaterial& Material;
/** The parsed shader model (domain / blend mode live here for per-id domain validation). */
const FShaderLabModel& Model;
/** Shader bodies + reachable library-function bodies to scan. */
const TArray<FShaderLabScannedBody>& Bodies;
/** The shader's `.usl` absolute path (`#line` directive target for mapped errors). */
const FString& SourcePath;
/**
* Create a material expression of `Class`, add it to the material's expression collection, and lay it
* out on the canvas — the capability then sets any type-specific fields (e.g. SceneTextureId) on the
* CastChecked<> result. Mirrors the builder's internal NewExpr helper.
*/
TFunction<UMaterialExpression*(UClass* /*Class*/)> MakeNode;
};
/**
* A "capability" that a raw-HLSL body helper depends on: the helper (e.g. `SceneTextureLookup`,
* `GetDistanceToNearestSurfaceGlobal`) compiles fine as bare HLSL inside a Custom node, but the runtime
* resource binding it needs is only established as a side effect of compiling a matching UMaterialExpression
* (via `UseSceneTextureId` / `bUsesGlobalDistanceField` / ...). Because the Custom body bypasses that node,
* we synthesize a hidden expression, wired into the before-attributes ParameterAnchor, purely for the
* side effect. This registry is the general mechanism; each dynamic-input helper family registers one entry.
*
* (Helpers with NO dynamic body input — e.g. EyeAdaptation — do not need this: they are exposed as ordinary
* `UE_` wired intrinsics whose real node is connected into the Custom input, so its Compile side effect fires
* naturally. This mechanism is only for helpers that take a body-computed argument, which cannot be fed into
* a node from inside the opaque Custom body.)
*/
struct FShaderLabAnchorCapability
{
/** Identifier for diagnostics / tests (e.g. "SceneTexture", "GlobalDistanceField"). */
FName Id;
/**
* HLSL tokens whose presence in any scanned body activates this capability (e.g. "UE_SceneTexture",
* "UE_SceneDepth"). Matched delimited by non-identifier characters.
*/
TArray<FString> TriggerTokens;
/**
* Emit the hidden before-attributes node(s) for this capability into OutNodes. Runs only when at least
* one TriggerToken is present. Returns false and appends `.usl`-mapped messages to OutErrors on a
* contract violation (e.g. a non-literal id) or an illegal domain — contract style, aborting the build.
*/
TFunction<bool(const FShaderLabAnchorCapabilityContext& /*Ctx*/,
TArray<UMaterialExpression*>& /*OutNodes*/, TArray<FString>& /*OutErrors*/)> Emit;
};
/**
* Open registry of anchor side-effect capabilities. ShaderLab seeds its builtins (SceneTexture, Global
* Distance Field) on first use; other editor modules may register their own from their StartupModule via
* Get().Register(...). The graph builder queries it once per build.
*/
class USHADERLABEDITOR_API FShaderLabAnchorCapabilityRegistry
{
public:
static FShaderLabAnchorCapabilityRegistry& Get();
/** Register (or override, by Id) a capability. */
void Register(FShaderLabAnchorCapability Capability);
/** Visit every registered capability. */
void ForEach(TFunctionRef<void(const FShaderLabAnchorCapability&)> Fn) const;
private:
FShaderLabAnchorCapabilityRegistry();
void RegisterBuiltins();
TArray<FShaderLabAnchorCapability> Capabilities;
};

View File

@@ -1,51 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
struct FShaderLabModel;
/**
* Editor-only builder that turns a parsed FShaderLabModel into a UMaterial expression graph:
* property parameter nodes + a Custom HLSL node for the Surface body wired into a Substrate
* Slab BSDF (-> FrontMaterial), plus an optional Vertex Custom node for WPO/displacement/UVs.
*
* The build is fully deterministic for a given model (node creation order, input/output order,
* generated HLSL) so the cook-time and editor-time graphs — and therefore the baked uniform
* expression set — match.
*/
class USHADERLABEDITOR_API FShaderLabGraphBuilder
{
public:
/**
* Clear `Material`'s expression graph and rebuild it from `Model`, applying settings
* (domain/blend/two-sided) and updating cached expression data. Does NOT trigger shader
* compilation — callers decide when to compile. Returns false with diagnostics on failure.
*/
static bool BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors);
/**
* Replace `Material`'s graph with a minimal Substrate material whose single Custom node emits a
* `#line`-mapped `#error` for `Diagnostics`, so it deliberately fails to compile with those messages
* mapped back to `SrcPath` (an absolute, forward-slashed .usl path). This routes .usl parse/build
* errors through the standard shader-compile path — MIC editor red text, GetCompileErrors, cook —
* instead of dropping them. The graph itself is always valid; the failure is intentional and happens
* at shader compile. Does NOT trigger compilation (the caller decides when).
*/
static void BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath);
/**
* Virtual shader root under which per-shader generated local-code headers live
* (e.g. "/UShaderLabGen"). The editor module maps it to GetGeneratedShaderDir() at startup.
*/
static const TCHAR* GetGeneratedVirtualRoot();
/**
* Absolute disk directory backing GetGeneratedVirtualRoot(): the plugin's
* `Intermediate/ShaderLabGen`. A build-time artifact only (never staged into a pak); the cooked
* runtime never builds graphs and skips the mapping. Empty string if the plugin can't be found.
*/
static FString GetGeneratedShaderDir();
};

View File

@@ -1,108 +0,0 @@
// Copyright UShaderLab. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class UMaterial;
class UMaterialExpression;
class UEnum;
/** Shader stage an intrinsic is valid in (used to reject e.g. PixelDepth inside a Vertex body). */
enum class EShaderLabIntrinsicFrequency : uint8
{
Any,
PixelOnly,
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;
/**
* When non-null, this parameter is a reflected enum passed by token name (e.g.
* `UE_ViewProperty(MEVP_FieldOfView)`). The stub generator emits the enum from it and types the
* parameter as it; the arg validator accepts only its token names. Points at a process-permanent
* native `UEnum` (`StaticEnum<T>()`), so a raw pointer is safe. `Type` is ignored when this is set.
*/
const UEnum* Enum = nullptr;
};
/**
* 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 `UE_Name(...)` HLSL stub declarations from this single source of truth, giving the IDE
* member/signature completion for `UE_` calls without any drift from the real registration.
*/
struct FShaderLabIntrinsicDesc
{
FName Name;
/** Which output pin of the created node to connect (covers multi-output nodes like ObjectLocalBounds). */
int32 OutputIndex = 0;
/** Stage restriction; misuse is a build error. */
EShaderLabIntrinsicFrequency Frequency = EShaderLabIntrinsicFrequency::Any;
/**
* When true, this name is overloaded: the nullary form `UE_Name()` is this wired intrinsic, while
* `UE_Name(<args>)` is a raw-HLSL helper of the same name (see UEFunctions/*.ush). A call carrying more
* arguments than `Params` declares is the helper overload — the parser leaves it verbatim in the body
* for the shader compiler instead of rejecting it. Used e.g. by UE_DistanceToNearestSurface (nullary =
* current pixel; `(worldPos)` = distance-field lookup at an arbitrary position, whose global-distance-
* field binding is supplied by an anchor capability). Default false keeps the strict const-arg contract.
*/
bool bAllowRawHelperWithArgs = false;
/** 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)`).
* Return nullptr and set `OutError` on a bad argument. Contract: this only runs in the editor/cook
* (graphs are editor-only); the cooked runtime never builds a graph.
*/
TFunction<UMaterialExpression*(UMaterial& /*Material*/, const TArray<FString>& /*ConstArgs*/, FString& /*OutError*/)> MakeNode;
};
/**
* Open registry mapping intrinsic names to descriptors. ShaderLab seeds its builtins on first use;
* other editor modules can register intrinsics for their own custom UMaterialExpression nodes from
* their StartupModule via Get().Register(...).
*/
class USHADERLABEDITOR_API FShaderLabIntrinsicRegistry
{
public:
static FShaderLabIntrinsicRegistry& Get();
/** Register (or override) an intrinsic. */
void Register(FShaderLabIntrinsicDesc Desc);
/** 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();
TMap<FName, FShaderLabIntrinsicDesc> Descs;
};

View File

@@ -20,6 +20,7 @@ public class UShaderLabEditor : ModuleRules
PrivateDependencyModuleNames.AddRange(new string[]
{
"UShaderLab",
"UShaderLabBuilder",
"RenderCore",
"RHI",
"Json",