From 530708cd294b7bd799b9c1c939ef2dde3f9ef977 Mon Sep 17 00:00:00 2001 From: Eragon-Brisingr <450614754@qq.com> Date: Fri, 3 Jul 2026 17:44:09 +0800 Subject: [PATCH] Fix cook/runtime error --- .../ShaderLabMaterialInstanceConstant.cpp | 193 +++++++++++++++++- .../ShaderLabMaterialInstanceConstant.h | 63 ++++++ Source/UShaderLab/UShaderLab.Build.cs | 69 ++++++- .../Private/ShaderLabEditorModule.cpp | 25 +++ .../Private/ShaderLabGraphBuilder.cpp | 38 +++- 5 files changed, 369 insertions(+), 19 deletions(-) diff --git a/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp b/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp index 8bff602..51cd8a6 100644 --- a/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp +++ b/Source/UShaderLab/Private/ShaderLabMaterialInstanceConstant.cpp @@ -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(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(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& 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(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(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(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); diff --git a/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h b/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h index c28d0c6..c3641bd 100644 --- a/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h +++ b/Source/UShaderLab/Public/ShaderLabMaterialInstanceConstant.h @@ -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 }; diff --git a/Source/UShaderLab/UShaderLab.Build.cs b/Source/UShaderLab/UShaderLab.Build.cs index e2e638c..409183e 100644 --- a/Source/UShaderLab/UShaderLab.Build.cs +++ b/Source/UShaderLab/UShaderLab.Build.cs @@ -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); + } + + /// + /// 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). + /// + 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: /Shaders and every "Shaders" dir under /Plugins. + var Roots = new System.Collections.Generic.List(); + 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); + } + } } } diff --git a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp index 6283ed9..0171c60 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabEditorModule.cpp @@ -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 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 diff --git a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp index edc6c7f..8750032 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabGraphBuilder.cpp @@ -226,6 +226,26 @@ namespace ShaderLabGraph } } + template + 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(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(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(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