mirror of
https://github.com/Eragon-Brisingr/UShaderLab.git
synced 2026-09-15 14:54:36 +00:00
Fix cook/runtime error
This commit is contained in:
@@ -2,30 +2,44 @@
|
||||
|
||||
#include "ShaderLabMaterialInstanceConstant.h"
|
||||
|
||||
#include "Engine/SpecularProfile.h"
|
||||
#include "Engine/SubsurfaceProfile.h"
|
||||
#include "Engine/ToonProfile.h"
|
||||
#include "MaterialCachedData.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialInstance.h"
|
||||
#include "ShaderLabMaterialAssetUserData.h"
|
||||
#include "ShaderLabMaterialRegistry.h"
|
||||
#include "ShaderLabModel.h"
|
||||
#include "UObject/AssetRegistryTagsContext.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
const FName UShaderLabMaterialInstanceConstant::ShaderLabPathTagName(TEXT("ShaderLabPath"));
|
||||
|
||||
bool UShaderLabMaterialInstanceConstant::HasOverridenBaseProperties() const
|
||||
const UMaterial* UShaderLabMaterialInstanceConstant::GetShaderLabRootBase() const
|
||||
{
|
||||
// Force a self-contained permutation only at the ROOT — where our immediate parent is a ShaderLab
|
||||
// in-memory /Script base material (which has no serialized shader map). For child instances (parent
|
||||
// is another instance) fall through to stock behavior so they DEFER to and SHARE the root's shader
|
||||
// map — this is what bounds the shader-map count (no combinatorial explosion across variants).
|
||||
//
|
||||
// The condition compares the parent's package against the ShaderLab /Script package POINTER (not a
|
||||
// string name): a non-instance parent in that exact package is one of our bases.
|
||||
// A ROOT instance's immediate parent is a non-instance UMaterial living in the ShaderLab /Script package.
|
||||
// Compare against the package POINTER (not a string name). Child instances (parent is another instance)
|
||||
// return nullptr — they defer to and share the root's shader map + derived data.
|
||||
if (const UMaterial* BaseMaterial = Cast<UMaterial>(Parent))
|
||||
{
|
||||
if (BaseMaterial->GetOutermost() == FShaderLabMaterialRegistry::Get().GetPackage())
|
||||
{
|
||||
return true;
|
||||
return BaseMaterial;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool UShaderLabMaterialInstanceConstant::HasOverridenBaseProperties() const
|
||||
{
|
||||
// Force a self-contained permutation only at the ROOT — where our immediate parent is a ShaderLab
|
||||
// in-memory /Script base material (which has no serialized shader map). For child instances fall through
|
||||
// to stock behavior so they DEFER to and SHARE the root's shader map (bounds the shader-map count).
|
||||
if (GetShaderLabRootBase() != nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Super::HasOverridenBaseProperties();
|
||||
}
|
||||
@@ -47,7 +61,168 @@ FString UShaderLabMaterialInstanceConstant::GetShaderLabPath(const UMaterialInte
|
||||
return FString();
|
||||
}
|
||||
|
||||
void UShaderLabMaterialInstanceConstant::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
|
||||
#if WITH_EDITOR
|
||||
// The stock UMaterialInstanceConstant::UpdateCachedData() (called from Super::PostLoad) builds this
|
||||
// instance's OWN CachedExpressionData only when it has material layers; otherwise it leaves it null and
|
||||
// defers to the parent's. Our parent is the MEMORY-ONLY /Script base material, whose CachedExpressionData
|
||||
// is never serialized/cooked — so any /Game asset it references (Material Parameter Collections via
|
||||
// SL_COLLECTION, texture assets, functions) would be invisible to the cook's reference gathering AND never
|
||||
// loaded at runtime (e.g. an MPC's uniform buffer → fatal "Failed to find parameter collection buffer").
|
||||
//
|
||||
// Force the ROOT instance to build its OWN CachedExpressionData by analyzing the base graph. The engine's
|
||||
// standard UMaterialInterface::Serialize then harvests + serializes its hard references (see
|
||||
// MaterialInterface.cpp), so the cook includes them and the runtime loads them. The data is DERIVED from the
|
||||
// current base graph on every load/cook (never written to the editor .uasset), so editing the .usl needs no
|
||||
// MIC re-save — the memory base stays resource-transparent.
|
||||
BuildSelfContainedCachedExpressionData();
|
||||
ApplyShaderLabProfileOverrides();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void UShaderLabMaterialInstanceConstant::RefreshShaderLabDerivedData()
|
||||
{
|
||||
if (GetShaderLabRootBase() == nullptr)
|
||||
{
|
||||
return; // Only root instances carry the derived cook-inclusion data.
|
||||
}
|
||||
|
||||
// Invalidate the derived state so it rebuilds from the (just hot-reloaded) base graph. Without the reset,
|
||||
// BuildSelfContainedCachedExpressionData early-outs on the existing copy, and bLoadedCachedExpressionData=true
|
||||
// makes stock UpdateCachedData skip refreshing it — the resident MIC would keep stale references.
|
||||
CachedExpressionData.Reset();
|
||||
bLoadedCachedExpressionData = false;
|
||||
|
||||
// Clear stale profile overrides too, so a profile removed from the .usl actually drops off (rather than
|
||||
// lingering because ApplyShaderLabProfileOverrides only ever sets, never clears).
|
||||
bOverrideSubsurfaceProfile = false;
|
||||
SubsurfaceProfile = nullptr;
|
||||
bOverrideSpecularProfile = false;
|
||||
SpecularProfileOverride = nullptr;
|
||||
bOverrideToonProfile = false;
|
||||
ToonProfileOverride = nullptr;
|
||||
|
||||
BuildSelfContainedCachedExpressionData();
|
||||
ApplyShaderLabProfileOverrides();
|
||||
}
|
||||
|
||||
void UShaderLabMaterialInstanceConstant::BuildSelfContainedCachedExpressionData()
|
||||
{
|
||||
// Only the ROOT instance (immediate parent is a ShaderLab /Script base material) owns the self-contained
|
||||
// shader map; child instances share the root's map and its cached data (see HasOverridenBaseProperties).
|
||||
if (GetShaderLabRootBase() == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect an existing build (e.g. a layered instance that already populated its own via the stock path).
|
||||
if (CachedExpressionData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UMaterial* BaseMaterial = GetMaterial();
|
||||
check(BaseMaterial); // A ShaderLab root instance always resolves to its /Script base material.
|
||||
|
||||
// Ensure the base material's cached data reflects the current graph, then take our OWN copy of it. We copy
|
||||
// rather than call FMaterialCachedExpressionData::AnalyzeMaterial directly because that method is not
|
||||
// ENGINE_API-exported, whereas UpdateCachedExpressionData()/GetCachedExpressionData() are. The copy carries
|
||||
// the base graph's referenced parameter collections (with StateIds), texture assets and function infos.
|
||||
BaseMaterial->UpdateCachedExpressionData();
|
||||
CachedExpressionData = MakeUnique<FMaterialCachedExpressionData>(BaseMaterial->GetCachedExpressionData());
|
||||
|
||||
// Mirror the editor-only side, exactly as the stock UMaterialInstanceConstant path does after a build.
|
||||
if (UMaterialInstanceEditorOnlyData* EditorData = GetEditorOnlyData())
|
||||
{
|
||||
EditorData->CachedExpressionData = CachedExpressionData->EditorOnlyData;
|
||||
}
|
||||
|
||||
// CRITICAL: the stock UMaterialInstanceConstant::UpdateCachedData() rebuilds (for layered) or RESETS TO NULL
|
||||
// (for non-layered — our case) CachedExpressionData inside an `if (!bLoadedCachedExpressionData)` block. The
|
||||
// cook calls that again after PostLoad (via BeginCacheForCookedPlatformData → UpdateStaticPermutation), which
|
||||
// would wipe the copy we just made before it gets serialized. Marking it as "loaded" makes those subsequent
|
||||
// calls skip the reset, so our copy survives to cook-save (and is re-derived fresh on the next load/cook).
|
||||
bLoadedCachedExpressionData = true;
|
||||
}
|
||||
|
||||
void UShaderLabMaterialInstanceConstant::ApplyShaderLabProfileOverrides()
|
||||
{
|
||||
// Substrate profiles (Subsurface/Specular/Toon) are NOT part of FMaterialCachedExpressionData — they live in
|
||||
// UMaterial::SubsurfaceProfiles/SpecularProfiles/ToonProfiles on the memory-only /Script base and resolve via
|
||||
// the parent chain, so the CachedExpressionData copy above does not cover them and they would be missing from
|
||||
// the cook (→ profile-shaded demos render dark). Set them as this instance's OWN overrides (hard TObjectPtr
|
||||
// UPROPERTYs) so the cook harvests + serializes them and the runtime loads + applies them. Resolved from the
|
||||
// source `.usl` model (not the base's compiled Substrate info, which may not be populated yet at PostLoad).
|
||||
const UMaterial* ImmediateBase = GetShaderLabRootBase();
|
||||
if (!ImmediateBase)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const TMap<FName, FShaderLabModel>& Models = FShaderLabMaterialRegistry::Get().GetRegisteredModels();
|
||||
const FShaderLabModel* Model = Models.Find(ImmediateBase->GetFName());
|
||||
if (!Model)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the profile path of each type across the surface entry + all slabs. A MIC carries a SINGLE override
|
||||
// per profile type, and at runtime an instance override replaces the profile for ALL slots — so a shader using
|
||||
// two DIFFERENT profiles of the same type across slabs cannot be represented and would silently render both
|
||||
// with one profile. Fail loud on that (contract-style) rather than ship a wrong material; repeats of the SAME
|
||||
// path are fine.
|
||||
FString SubsurfacePath, SpecularPath, ToonPath;
|
||||
auto Collect = [Model](FString& Dst, const FString& Src, const TCHAR* Type)
|
||||
{
|
||||
if (Src.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
checkf(Dst.IsEmpty() || Dst == Src,
|
||||
TEXT("ShaderLab '%s': two distinct %s profiles across slabs ('%s' vs '%s'); a material instance carries ")
|
||||
TEXT("one override per profile type, so multi-profile-per-type is unsupported. Use a single %s profile."),
|
||||
*Model->ShaderName, Type, *Dst, *Src, Type);
|
||||
Dst = Src;
|
||||
};
|
||||
Collect(SubsurfacePath, Model->SurfaceModifiers.SubsurfaceProfilePath, TEXT("Subsurface"));
|
||||
Collect(SpecularPath, Model->SurfaceModifiers.SpecularProfilePath, TEXT("Specular"));
|
||||
Collect(ToonPath, Model->SurfaceModifiers.ToonProfilePath, TEXT("Toon"));
|
||||
for (const FShaderLabSlab& Slab : Model->Slabs)
|
||||
{
|
||||
Collect(SubsurfacePath, Slab.Modifiers.SubsurfaceProfilePath, TEXT("Subsurface"));
|
||||
Collect(SpecularPath, Slab.Modifiers.SpecularProfilePath, TEXT("Specular"));
|
||||
Collect(ToonPath, Slab.Modifiers.ToonProfilePath, TEXT("Toon"));
|
||||
}
|
||||
|
||||
// Contract-style: an author-declared profile path that fails to load is a hard error (it would silently ship a
|
||||
// dark/incorrect material) — the same class of bug this override exists to prevent.
|
||||
if (!SubsurfacePath.IsEmpty())
|
||||
{
|
||||
USubsurfaceProfile* Profile = LoadObject<USubsurfaceProfile>(nullptr, *SubsurfacePath);
|
||||
checkf(Profile, TEXT("ShaderLab '%s': SubsurfaceProfile '%s' failed to load."), *Model->ShaderName, *SubsurfacePath);
|
||||
bOverrideSubsurfaceProfile = true;
|
||||
SubsurfaceProfile = Profile;
|
||||
}
|
||||
if (!SpecularPath.IsEmpty())
|
||||
{
|
||||
USpecularProfile* Profile = LoadObject<USpecularProfile>(nullptr, *SpecularPath);
|
||||
checkf(Profile, TEXT("ShaderLab '%s': SpecularProfile '%s' failed to load."), *Model->ShaderName, *SpecularPath);
|
||||
bOverrideSpecularProfile = true;
|
||||
SpecularProfileOverride = Profile;
|
||||
}
|
||||
if (!ToonPath.IsEmpty())
|
||||
{
|
||||
UToonProfile* Profile = LoadObject<UToonProfile>(nullptr, *ToonPath);
|
||||
checkf(Profile, TEXT("ShaderLab '%s': ToonProfile '%s' failed to load."), *Model->ShaderName, *ToonPath);
|
||||
bOverrideToonProfile = true;
|
||||
ToonProfileOverride = Profile;
|
||||
}
|
||||
}
|
||||
|
||||
void UShaderLabMaterialInstanceConstant::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const
|
||||
{
|
||||
Super::GetAssetRegistryTags(Context);
|
||||
|
||||
@@ -32,6 +32,10 @@ public:
|
||||
virtual bool HasOverridenBaseProperties() const override;
|
||||
//~ End UMaterialInstance interface
|
||||
|
||||
//~ UObject interface
|
||||
virtual void PostLoad() override;
|
||||
//~ End UObject interface
|
||||
|
||||
#if WITH_EDITOR
|
||||
//~ UObject interface
|
||||
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
|
||||
@@ -47,4 +51,63 @@ public:
|
||||
* material. Works for any instance depth (child instances inherit the root's path).
|
||||
*/
|
||||
static FString GetShaderLabPath(const UMaterialInterface* Material);
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Re-derive this ROOT instance's cook-inclusion data (CachedExpressionData copy + profile overrides) from the
|
||||
* CURRENT base graph. Called by the editor's `.usl` hot-reload path after the base material is rebuilt: the
|
||||
* base graph's referenced assets may have changed (MPC/profile added/removed), but BuildSelfContainedCachedExpressionData
|
||||
* early-outs on the existing copy and the anti-reset flag keeps stock UpdateCachedData from refreshing it — so
|
||||
* the resident MIC would otherwise keep stale data (and an in-editor iterative cook would serialize it). Resets
|
||||
* the derived state first, then rebuilds. No-op on non-root instances. (The packaged BuildCookRun path loads the
|
||||
* MIC fresh → PostLoad re-derives, so the shipped pak is never stale; this only hardens the in-editor session.)
|
||||
*/
|
||||
void RefreshShaderLabDerivedData();
|
||||
|
||||
/**
|
||||
* Test/diagnostic: does this instance carry its OWN FMaterialCachedExpressionData (vs. falling back to the
|
||||
* parent's via GetCachedExpressionData)? Only the own copy is serialized into the cook, so a regression that
|
||||
* dropped it — or the anti-reset flag that keeps it alive — is only observable through this, not through
|
||||
* GetCachedExpressionData() (which hides the distinction behind the parent fallback).
|
||||
*/
|
||||
bool HasOwnShaderLabCachedExpressionData() const { return CachedExpressionData != nullptr; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
/**
|
||||
* Return this instance's immediate parent iff it is a ShaderLab /Script base material (i.e. this is a ROOT
|
||||
* instance — see the class comment / HasOverridenBaseProperties); nullptr otherwise. The single source of truth
|
||||
* for "am I a root", used by HasOverridenBaseProperties, the cook-data builders and the hot-reload refresh.
|
||||
*/
|
||||
const UMaterial* GetShaderLabRootBase() const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Give this ROOT instance its OWN FMaterialCachedExpressionData (a copy of the /Script base material's).
|
||||
* Stock UMaterialInstanceConstant only builds its own for layered instances; otherwise it defers to the
|
||||
* parent's. Our parent is the MEMORY-ONLY /Script base material, whose cached data is never serialized —
|
||||
* so the /Game assets the base graph references (Material Parameter Collections, texture assets, functions)
|
||||
* would be invisible to the cook's reference gathering AND never loaded at runtime (e.g. an MPC's uniform
|
||||
* buffer → fatal "Failed to find parameter collection buffer"). Copying the base's cached data onto this
|
||||
* instance makes the engine's standard UMaterialInterface::Serialize harvest + serialize those hard refs.
|
||||
* FMaterialCachedExpressionData is serialized ONLY under a cooking archive (MaterialInterface.cpp), so a
|
||||
* plain in-editor save does NOT persist it — it is re-derived each load/cook, keeping the memory base
|
||||
* resource-transparent (editing the .usl needs no MIC re-save).
|
||||
*/
|
||||
void BuildSelfContainedCachedExpressionData();
|
||||
|
||||
/**
|
||||
* Set this ROOT instance's Subsurface/Specular/Toon profile overrides from the source `.usl` model's profile
|
||||
* paths. Unlike Material Parameter Collections, Substrate profiles are not part of FMaterialCachedExpressionData;
|
||||
* they live on the memory-only /Script base and resolve via the parent chain, so they are never cooked. Setting
|
||||
* them as this instance's own hard-ref overrides makes the cook include them and the runtime apply them.
|
||||
*
|
||||
* NOTE: unlike CachedExpressionData, the override fields (bOverride*Profile / *ProfileOverride) are ordinary
|
||||
* UPROPERTYs — a plain in-editor save DOES persist them. That is harmless: PostLoad re-derives them every load
|
||||
* (self-healing if the .usl's profile path later changes), and the generating commandlet does not save after
|
||||
* PostLoad, so the checked-in demo .uassets carry no override. It is NOT cook-only-serialized the way the
|
||||
* cached-expression copy is.
|
||||
*/
|
||||
void ApplyShaderLabProfileOverrides();
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Copyright UShaderLab. All Rights Reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class UShaderLab : ModuleRules
|
||||
@@ -24,10 +26,67 @@ public class UShaderLab : ModuleRules
|
||||
"Projects",
|
||||
});
|
||||
|
||||
// Stage the .usl sources into packaged builds so the cooked runtime can rebuild base-material
|
||||
// shells by re-parsing them (no serialized side data, no cook-time hook). A wildcard works here
|
||||
// because .usl are source files that exist at build time (when RuntimeDependencies are globbed),
|
||||
// unlike a cook-generated file. "..." matches recursively, including the top level.
|
||||
RuntimeDependencies.Add("$(ProjectDir)/Shaders/.../*.usl", StagedFileType.UFS);
|
||||
StageShaderLabSources(Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stage every `.usl` / `.uslfunc` under the project's `Shaders/` and any project plugin's `Shaders/` into the
|
||||
/// pak (UFS) so the cooked runtime can re-parse them to rebuild the memory base-material shells. Config-free
|
||||
/// (no Config/DefaultGame.ini edit) and plugin-generic (scans all project plugins), so it is invisible to the
|
||||
/// user of the plugin.
|
||||
///
|
||||
/// Files are enumerated here (at build-graph generation) and added EXPLICITLY, and each shader source
|
||||
/// directory is registered in ExternalDependencies so that adding/removing/editing a `.usl` changes the
|
||||
/// directory's timestamp, invalidates UBT's makefile, and re-runs this enumeration. That is what avoids the
|
||||
/// classic RuntimeDependencies-wildcard trap: UBT expands a wildcard once and caches the result, so files
|
||||
/// added afterwards are silently dropped from the pak (which made new base materials fall back to the default
|
||||
/// material at runtime).
|
||||
/// </summary>
|
||||
private void StageShaderLabSources(ReadOnlyTargetRules Target)
|
||||
{
|
||||
if (Target.ProjectFile == null)
|
||||
{
|
||||
return; // engine build with no project — nothing project-side to stage
|
||||
}
|
||||
string ProjectDir = Target.ProjectFile.Directory.FullName;
|
||||
|
||||
// Collect the shader roots: <Project>/Shaders and every "Shaders" dir under <Project>/Plugins.
|
||||
var Roots = new System.Collections.Generic.List<string>();
|
||||
string ProjectShaders = Path.Combine(ProjectDir, "Shaders");
|
||||
if (Directory.Exists(ProjectShaders))
|
||||
{
|
||||
Roots.Add(ProjectShaders);
|
||||
}
|
||||
string PluginsDir = Path.Combine(ProjectDir, "Plugins");
|
||||
if (Directory.Exists(PluginsDir))
|
||||
{
|
||||
foreach (string Dir in Directory.GetDirectories(PluginsDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (string.Equals(Path.GetFileName(Dir), "Shaders", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Roots.Add(Dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string Root in Roots)
|
||||
{
|
||||
// Watch the whole tree so an add/remove of a .usl (which bumps a directory's mtime) invalidates the
|
||||
// makefile and re-triggers this enumeration on the next build.
|
||||
ExternalDependencies.Add(Root);
|
||||
foreach (string SubDir in Directory.GetDirectories(Root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
ExternalDependencies.Add(SubDir);
|
||||
}
|
||||
|
||||
foreach (string File in Directory.GetFiles(Root, "*.usl", SearchOption.AllDirectories))
|
||||
{
|
||||
RuntimeDependencies.Add(File, StagedFileType.UFS);
|
||||
}
|
||||
foreach (string File in Directory.GetFiles(Root, "*.uslfunc", SearchOption.AllDirectories))
|
||||
{
|
||||
RuntimeDependencies.Add(File, StagedFileType.UFS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
#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);
|
||||
|
||||
@@ -74,6 +76,29 @@ namespace
|
||||
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
|
||||
|
||||
@@ -226,6 +226,26 @@ namespace ShaderLabGraph
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T* NewExpr(UMaterial& Material, int32& IoY, int32 Column);
|
||||
|
||||
// A Substrate material in the Decal domain must route its BSDF through a SubstrateConvertToDecal node — that
|
||||
// node flags the material with the SSM_Decal shading model, without which the engine's Substrate sanitization
|
||||
// (Material.cpp) silently resets MaterialDomain back to MD_Surface (→ DecalComponent then rejects it with
|
||||
// "Decal Material must use Deferred Decal Material Domain"). This mirrors exactly what the material editor
|
||||
// inserts when you pick the Deferred Decal domain. Returns the node to connect to FrontMaterial (the wrapper
|
||||
// for Decal, otherwise the BSDF unchanged).
|
||||
static UMaterialExpression* WrapBsdfForDecal(UMaterial& Material, const FShaderLabModel& Model, UMaterialExpression* Bsdf, int32& IoY)
|
||||
{
|
||||
if (Model.Settings.Domain != EShaderLabDomain::Decal)
|
||||
{
|
||||
return Bsdf;
|
||||
}
|
||||
UMaterialExpressionSubstrateConvertToDecal* Node = NewExpr<UMaterialExpressionSubstrateConvertToDecal>(Material, IoY, 300);
|
||||
Node->DecalMaterial.Connect(0, Bsdf);
|
||||
return Node;
|
||||
}
|
||||
|
||||
static EBlendMode MapBlend(EShaderLabBlendMode B)
|
||||
{
|
||||
switch (B)
|
||||
@@ -2383,14 +2403,22 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
case EShaderLabPropertyType::Texture3D:
|
||||
case EShaderLabPropertyType::TextureCubeArray:
|
||||
{
|
||||
// Array / volume texture object parameter. The engine assigns a matching-dimension default when
|
||||
// Texture is left null; the artist binds a real asset on the Material Instance. (Built-in white/
|
||||
// black/grey/normal default tokens only exist for Texture2D, so they are not applied here.)
|
||||
// Array / volume texture object parameter. Built-in white/black/grey/normal default tokens are
|
||||
// Texture2D-only and must NOT be applied here, but an explicit asset-path default (e.g.
|
||||
// "/Engine/EngineResources/DefaultVolumeTexture") of the matching dimension is honored. Left null,
|
||||
// the engine assigns a matching-dimension default and the artist binds a real asset on the instance.
|
||||
UMaterialExpressionTextureObjectParameter* E = NewExpr<UMaterialExpressionTextureObjectParameter>(Material, ParamY, -1000);
|
||||
E->ParameterName = Prop.Name;
|
||||
E->Group = FName(*Prop.Group);
|
||||
E->SortPriority = Prop.SortPriority;
|
||||
E->SamplerType = SAMPLERTYPE_Color;
|
||||
if (Prop.TextureDefault.StartsWith(TEXT("/")))
|
||||
{
|
||||
UTexture* DefaultTex = LoadObject<UTexture>(nullptr, *Prop.TextureDefault);
|
||||
checkf(DefaultTex, TEXT("ShaderLab property '%s': array/volume DefaultTexture '%s' failed to load."),
|
||||
*Prop.Name.ToString(), *Prop.TextureDefault);
|
||||
E->Texture = DefaultTex;
|
||||
}
|
||||
Node.Expr = E;
|
||||
Node.bIsTexture = true;
|
||||
break;
|
||||
@@ -2485,7 +2513,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
EditorOnly->FrontMaterial.Connect(0, Slab);
|
||||
EditorOnly->FrontMaterial.Connect(0, WrapBsdfForDecal(Material, Model, Slab, ParamY));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2562,7 +2590,7 @@ bool FShaderLabGraphBuilder::BuildInto(UMaterial& Material, const FShaderLabMode
|
||||
{
|
||||
return false;
|
||||
}
|
||||
EditorOnly->FrontMaterial.Connect(0, Root);
|
||||
EditorOnly->FrontMaterial.Connect(0, WrapBsdfForDecal(Material, Model, Root, ParamY));
|
||||
|
||||
// Material-level Opacity / OpacityMask from named Value blocks.
|
||||
auto ConnectMaterialOutput = [&](FExpressionInput& Pin, FName ValueName, const TCHAR* What) -> bool
|
||||
|
||||
Reference in New Issue
Block a user