mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 06:44:35 +00:00
Fix -Game no matieral error
This commit is contained in:
@@ -36,11 +36,6 @@ namespace ShaderLabResolver_Private
|
||||
return P.ToLower();
|
||||
}
|
||||
|
||||
static bool IsTexture(const FShaderLabProperty& P)
|
||||
{
|
||||
return P.Type == EShaderLabPropertyType::Texture2D || P.Type == EShaderLabPropertyType::TextureCube;
|
||||
}
|
||||
|
||||
struct FResolveState
|
||||
{
|
||||
const FShaderLabImportResolver::FSourceLoader& Loader;
|
||||
@@ -267,7 +262,12 @@ bool FShaderLabImportResolver::Resolve(
|
||||
continue;
|
||||
}
|
||||
Lib.PropRewrite.Add(Prop.Name, Promoted);
|
||||
if (IsTexture(Prop))
|
||||
// A texture property of ANY kind (2D/Cube/2DArray/3D/CubeArray) carries a companion
|
||||
// `<Name>Sampler`; its promoted name must be rewritten in lockstep with the texture, or a
|
||||
// namespaced import leaves the body's `XSampler` unrewritten while the emitted parameter is
|
||||
// `NS_XSampler` -> undefined symbol. Use the canonical predicate (ShaderLabModel.h) so this
|
||||
// stays in step with the full set of texture types.
|
||||
if (ShaderLabIsTextureType(Prop.Type))
|
||||
{
|
||||
Lib.PropRewrite.Add(
|
||||
FName(*(Prop.Name.ToString() + TEXT("Sampler"))),
|
||||
|
||||
@@ -16,9 +16,9 @@ DEFINE_LOG_CATEGORY_STATIC(LogShaderLab, Log, All);
|
||||
void FShaderLabModule::StartupModule()
|
||||
{
|
||||
#if !WITH_EDITOR
|
||||
// In cooked builds there is no editor graph builder; apply material settings when the registry
|
||||
// builds a base shell. The shell carries no shader map — instances carry their own.
|
||||
FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddLambda(
|
||||
// In cooked builds there is no graph builder; apply material settings when the registry builds a base
|
||||
// shell. The shell carries no shader map — instances carry their own.
|
||||
BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddLambda(
|
||||
[](UMaterial& Material, const FShaderLabModel& Model)
|
||||
{
|
||||
FShaderLabRuntimeBuilder::ApplySettings(Material, Model);
|
||||
@@ -29,6 +29,13 @@ void FShaderLabModule::StartupModule()
|
||||
|
||||
void FShaderLabModule::ShutdownModule()
|
||||
{
|
||||
#if !WITH_EDITOR
|
||||
if (BuildHandle.IsValid())
|
||||
{
|
||||
FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle);
|
||||
BuildHandle.Reset();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FShaderLabModule, UShaderLab)
|
||||
|
||||
@@ -42,10 +42,13 @@ void UShaderLabSubsystem::DiscoverAndRegisterAll()
|
||||
bScanned = true;
|
||||
|
||||
// Both editor and cooked runtime rebuild from the SAME .usl sources — there is no serialized
|
||||
// side data. The .usl files are staged into packaged builds (a wildcard RuntimeDependency in
|
||||
// ShaderLab.Build.cs) and the parser/discovery are runtime-safe, so a cooked build re-parses
|
||||
// them exactly as the editor does. In the editor the bound builder compiles a graph; in a cooked
|
||||
// build it only applies settings to a never-rendered base shell (the instances carry the shaders).
|
||||
// side data. The .usl files are staged into packaged builds by UShaderLab.Build.cs, which ENUMERATES
|
||||
// each file into RuntimeDependencies and registers the source dirs as ExternalDependencies so new/removed
|
||||
// files invalidate the makefile (NOT a wildcard RuntimeDependency — that was the classic trap that
|
||||
// silently dropped newly added .usl; see Build.cs + AIDoc §5). The parser/discovery are runtime-safe, so
|
||||
// a cooked build re-parses them exactly as the editor does. In the editor (and `-game`) the bound builder
|
||||
// compiles a graph; in a cooked build it only applies settings to a never-rendered base shell (the
|
||||
// instances carry the shaders).
|
||||
const TArray<FString> Files = FShaderLabDiscovery::FindShaderLabFiles();
|
||||
int32 NumOk = 0;
|
||||
for (const FString& File : Files)
|
||||
|
||||
@@ -15,6 +15,14 @@ class UMaterial;
|
||||
*/
|
||||
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnShaderLabBuildMaterial, UMaterial& /*Material*/, const FShaderLabModel& /*Model*/);
|
||||
|
||||
/**
|
||||
* Broadcast AFTER a base material's graph has been fully (re)built and compiled by the uncooked builder.
|
||||
* Split from OnBuildMaterial so the editor-only reaction (refreshing open Material Instance editors) runs
|
||||
* strictly after the build, without dragging the MaterialEditor dependency into the UncookedOnly builder
|
||||
* module (which must load in `-game`, where editor-UI modules are absent). No listeners in `-game`.
|
||||
*/
|
||||
DECLARE_MULTICAST_DELEGATE_OneParam(FOnShaderLabMaterialGraphBuilt, UMaterial& /*Material*/);
|
||||
|
||||
/**
|
||||
* Owns the in-memory base materials. They live in a compiled-in package named "/Script/UShaderLab"
|
||||
* so that saved assets (MICs) can hard-reference them by object path and have that import resolve
|
||||
@@ -55,11 +63,15 @@ public:
|
||||
|
||||
FOnShaderLabBuildMaterial& OnBuildMaterial() { return BuildMaterialDelegate; }
|
||||
|
||||
/** Fired by the uncooked builder once a base material's graph is built + compiled (see delegate doc). */
|
||||
FOnShaderLabMaterialGraphBuilt& OnMaterialGraphBuilt() { return MaterialGraphBuiltDelegate; }
|
||||
|
||||
/** Models registered so far, keyed by object name (used by the instance factory picker and the example/validation commandlets). */
|
||||
const TMap<FName, FShaderLabModel>& GetRegisteredModels() const { return RegisteredModels; }
|
||||
|
||||
private:
|
||||
UPackage* Package = nullptr;
|
||||
FOnShaderLabBuildMaterial BuildMaterialDelegate;
|
||||
FOnShaderLabMaterialGraphBuilt MaterialGraphBuiltDelegate;
|
||||
TMap<FName, FShaderLabModel> RegisteredModels;
|
||||
};
|
||||
|
||||
@@ -10,4 +10,11 @@ class FShaderLabModule : public IModuleInterface
|
||||
public:
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
|
||||
#if !WITH_EDITOR
|
||||
private:
|
||||
// Handle for the cooked-runtime OnBuildMaterial->ApplySettings binding, unbound in ShutdownModule
|
||||
// (symmetric with the builder/editor modules). Only exists in cooked builds where that binding is made.
|
||||
FDelegateHandle BuildHandle;
|
||||
#endif
|
||||
};
|
||||
|
||||
167
Source/UShaderLabBuilder/Private/ShaderLabBuilderModule.cpp
Normal file
167
Source/UShaderLabBuilder/Private/ShaderLabBuilderModule.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
#include "Engine/Engine.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "MaterialShared.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ShaderCore.h"
|
||||
#include "ShaderLabGraphBuilder.h"
|
||||
#include "ShaderLabMaterialInstanceConstant.h"
|
||||
#include "ShaderLabMaterialRegistry.h"
|
||||
#include "ShaderLabModel.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY_STATIC(LogShaderLabBuilder, Log, All);
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* Fill a base material shell: rebuild its expression graph from the model, then recompile it and every
|
||||
* dependent instance. Runs whenever the registry asks (startup discovery + editor hot reload) in ANY
|
||||
* uncooked run — the editor AND a `-game` launch on the editor binary. In `-game` the project is uncooked,
|
||||
* so each instance must translate its own static-permutation shader map from THIS graph at load: an empty
|
||||
* shell here is exactly why materials used to fall back to the default material in `-game`.
|
||||
*/
|
||||
void BuildAndCompile(UMaterial& Material, const FShaderLabModel& Model)
|
||||
{
|
||||
// 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(LogShaderLabBuilder, 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(LogShaderLabBuilder, 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();
|
||||
}
|
||||
}
|
||||
|
||||
// The graph is now built + compiled and the derived data refreshed. Notify the editor layer so it can
|
||||
// refresh any open Material Instance editors — kept out of this UncookedOnly module because that reaction
|
||||
// needs the MaterialEditor module, which is absent in `-game`. No listeners there; a no-op broadcast.
|
||||
FShaderLabMaterialRegistry::Get().OnMaterialGraphBuilt().Broadcast(Material);
|
||||
}
|
||||
}
|
||||
|
||||
class FShaderLabBuilderModule : 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). This is the
|
||||
// binding that was previously in the Editor module and never fired in `-game`; hosting it here (a
|
||||
// module loaded for all uncooked runs) is the fix for invisible materials under `-game`.
|
||||
BuildHandle = FShaderLabMaterialRegistry::Get().OnBuildMaterial().AddStatic(&BuildAndCompile);
|
||||
|
||||
UE_LOG(LogShaderLabBuilder, Log, TEXT("ShaderLabBuilder module started."));
|
||||
}
|
||||
|
||||
virtual void ShutdownModule() override
|
||||
{
|
||||
if (BuildHandle.IsValid())
|
||||
{
|
||||
FShaderLabMaterialRegistry::Get().OnBuildMaterial().Remove(BuildHandle);
|
||||
BuildHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
FDelegateHandle BuildHandle;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FShaderLabBuilderModule, UShaderLabBuilder)
|
||||
@@ -41,7 +41,6 @@
|
||||
#include "ShaderLabIntrinsicRegistry.h"
|
||||
#include "ShaderLabAnchorCapabilityRegistry.h"
|
||||
#include "ShaderLabImportResolver.h"
|
||||
#include "ShaderLabRuntimeBuilder.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "ShaderLabSettingsApplier.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
@@ -130,31 +129,9 @@ namespace ShaderLabGraph
|
||||
return Full;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a virtual shader path ("/Project/Lib/X.uslfunc", "/Plugin/ShaderLab/...", "/Engine/...") to an
|
||||
* absolute disk path via the editor's registered shader-source directory mappings (longest-prefix match).
|
||||
* Returns false if no mapping root contains the path. Used to load `.uslfunc` imports off disk.
|
||||
*/
|
||||
static bool ResolveVirtualShaderFile(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;
|
||||
}
|
||||
// Virtual-shader-path -> disk resolution is now the shared FShaderLabGraphBuilder::ResolveVirtualShaderFile
|
||||
// (defined below, exposed in the header) so the editor module (hot-reload loader) and the VSCode button
|
||||
// reuse the same longest-prefix-match logic instead of each hand-rolling it.
|
||||
|
||||
/**
|
||||
* Wrap a user HLSL body so shader-compiler errors map back to the .usl source: a `#line`
|
||||
@@ -2356,6 +2333,27 @@ namespace ShaderLabGraph
|
||||
}
|
||||
}
|
||||
|
||||
bool FShaderLabGraphBuilder::ResolveVirtualShaderFile(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;
|
||||
}
|
||||
|
||||
bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabModel& Model, TArray<FString>& OutErrors)
|
||||
{
|
||||
using namespace ShaderLabGraph;
|
||||
@@ -6,7 +6,9 @@
|
||||
// 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
|
||||
// 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.
|
||||
@@ -414,7 +416,7 @@ namespace ShaderLabIDEPrepare_Private
|
||||
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.
|
||||
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());
|
||||
@@ -430,5 +432,5 @@ namespace ShaderLabIDEPrepare_Private
|
||||
|
||||
static FAutoConsoleCommand GShaderLabIDEPrepareCommand(
|
||||
TEXT("ShaderLab.IDE.Prepare"),
|
||||
TEXT("Generate IDE completion assets for .usl: <Plugin>/Shaders/ShaderLab.ush, shadertoolsconfig.json, .vscode/settings.json."),
|
||||
TEXT("Generate IDE completion assets for .usl: Private/ShaderLabUENode.ush, Private/UEFunctions/Transform.ush + TransformPosition.ush, and .vscode/settings.json."),
|
||||
FConsoleCommandWithArgsDelegate::CreateStatic(&ShaderLabIDEPrepare_Private::Run));
|
||||
@@ -79,7 +79,7 @@ struct FShaderLabAnchorCapability
|
||||
* 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
|
||||
class USHADERLABBUILDER_API FShaderLabAnchorCapabilityRegistry
|
||||
{
|
||||
public:
|
||||
static FShaderLabAnchorCapabilityRegistry& Get();
|
||||
@@ -8,15 +8,16 @@ 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.
|
||||
* 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. Lives in the UncookedOnly UShaderLabBuilder module, so it
|
||||
* runs in the editor AND under `-game` on uncooked data (not in cooked builds — see AIDoc §2.9).
|
||||
*
|
||||
* 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
|
||||
class USHADERLABBUILDER_API FShaderLabGraphBuilder
|
||||
{
|
||||
public:
|
||||
/**
|
||||
@@ -36,6 +37,15 @@ public:
|
||||
*/
|
||||
static void BuildPoisonInto(UMaterial& Material, const TArray<FString>& Diagnostics, const FString& SrcPath);
|
||||
|
||||
/**
|
||||
* Resolve a virtual shader path ("/Project/Lib/X.uslfunc", "/Plugin/ShaderLab/...", "/Engine/...") to an
|
||||
* absolute disk path via the registered shader-source directory mappings (longest-prefix match, exact-root
|
||||
* allowed). Returns false if no mapping root contains the path. The single canonical reversal of the
|
||||
* shader-source mappings — used to load `.uslfunc` imports, by the editor hot-reload loader, and by the
|
||||
* VSCode button. (Forward direction: FShaderLabMaterialRegistry::MakeVirtualShaderPath.)
|
||||
*/
|
||||
static bool ResolveVirtualShaderFile(const FString& VirtualPath, FString& OutDiskPath);
|
||||
|
||||
/**
|
||||
* Virtual shader root under which per-shader generated local-code headers live
|
||||
* (e.g. "/UShaderLabGen"). The editor module maps it to GetGeneratedShaderDir() at startup.
|
||||
@@ -86,7 +86,7 @@ struct FShaderLabIntrinsicDesc
|
||||
* other editor modules can register intrinsics for their own custom UMaterialExpression nodes from
|
||||
* their StartupModule via Get().Register(...).
|
||||
*/
|
||||
class USHADERLABEDITOR_API FShaderLabIntrinsicRegistry
|
||||
class USHADERLABBUILDER_API FShaderLabIntrinsicRegistry
|
||||
{
|
||||
public:
|
||||
static FShaderLabIntrinsicRegistry& Get();
|
||||
40
Source/UShaderLabBuilder/UShaderLabBuilder.Build.cs
Normal file
40
Source/UShaderLabBuilder/UShaderLabBuilder.Build.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
// The material-graph-building core: turns a parsed .usl model into a UMaterial expression graph and
|
||||
// triggers its compile. This is needed whenever the engine runs UNCOOKED data — the editor, AND a
|
||||
// `-game` / Standalone launch on the editor binary against an uncooked project (GIsEditor == false).
|
||||
// It must NOT be an `Editor`-type module (those are gated on GIsEditor and never load in `-game`, which
|
||||
// left base materials as empty shells and made instances fall back to the default material). `UncookedOnly`
|
||||
// loads iff `!RequiresCookedData()`, i.e. exactly the uncooked runs, and is excluded from cooked builds
|
||||
// (where instances carry their own shader maps and the runtime module's ApplySettings path suffices).
|
||||
//
|
||||
// Deliberately depends only on Engine/RenderCore/RHI/Projects/Json — NO UnrealEd/MaterialEditor/Slate —
|
||||
// so it is safe to load in `-game` where those editor-UI modules are not initialized. The editor-only UX
|
||||
// (factory, VSCode button, hot-reload watcher, open-editor refresh) stays in UShaderLabEditor.
|
||||
public class UShaderLabBuilder : ModuleRules
|
||||
{
|
||||
public UShaderLabBuilder(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
bUseUnity = false;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"UShaderLab",
|
||||
"RenderCore",
|
||||
"RHI",
|
||||
"Json",
|
||||
"Projects",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,6 +20,7 @@ public class UShaderLabEditor : ModuleRules
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"UShaderLab",
|
||||
"UShaderLabBuilder",
|
||||
"RenderCore",
|
||||
"RHI",
|
||||
"Json",
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
"Type": "Runtime",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "UShaderLabBuilder",
|
||||
"Type": "UncookedOnly",
|
||||
"LoadingPhase": "Default"
|
||||
},
|
||||
{
|
||||
"Name": "UShaderLabEditor",
|
||||
"Type": "Editor",
|
||||
|
||||
Reference in New Issue
Block a user