mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 23:04:37 +00:00
Fix -Game no matieral error
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user