diff --git a/Packages/src/CHANGELOG.md b/Packages/src/CHANGELOG.md
index ff6a9cc..8edf0a2 100644
--- a/Packages/src/CHANGELOG.md
+++ b/Packages/src/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [Unreleased]
+
+### Features
+
+* add `Exclude From Preloaded Assets When Build Player` option: temporarily removes settings from `PlayerSettings.preloadedAssets` during Player Build only, enabling AssetBundle / Addressables hot-update workflows while keeping normal Editor behavior unchanged
+
# [3.5.0](https://github.com/mob-sakai/SoftMaskForUGUI/compare/3.4.1...3.5.0) (2025-11-08)
diff --git a/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs b/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs
index 9589ab4..e110502 100644
--- a/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs
+++ b/Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs
@@ -11,8 +11,20 @@ internal class UISoftMaskProjectSettingsEditor : Editor
public override void OnInspectorGUI()
{
+ serializedObject.Update();
EditorGUIUtility.labelWidth = 180;
- base.OnInspectorGUI();
+
+ // Setting
+ EditorGUILayout.LabelField("Setting", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_SoftMaskEnabled"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_StereoEnabled"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_TransformSensitivity"));
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_SoftMaskable"));
+
+ // Editor
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Editor", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(serializedObject.FindProperty("m_HideGeneratedComponents"));
// Shader registry.
EditorGUILayout.Space();
@@ -29,6 +41,34 @@ public override void OnInspectorGUI()
}
_shaderVariantRegistryEditor.Draw();
+
+ // Advanced
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Advanced", EditorStyles.boldLabel);
+ var excludeProp = serializedObject.FindProperty("m_ExcludeFromPreloadedAssetsWhenBuildPlayer");
+ EditorGUILayout.PropertyField(excludeProp);
+
+ if (excludeProp.boolValue)
+ {
+ var shadersProp = serializedObject.FindProperty("m_ShaderVariantRegistry")
+ .FindPropertyRelative("m_RegisteredShaders");
+ EditorGUI.BeginDisabledGroup(true);
+ EditorGUI.indentLevel++;
+ for (var i = 0; i < shadersProp.arraySize; i++)
+ {
+ EditorGUILayout.PropertyField(shadersProp.GetArrayElementAtIndex(i), GUIContent.none);
+ }
+ EditorGUI.indentLevel--;
+ EditorGUI.EndDisabledGroup();
+
+ if (shadersProp.arraySize == 0)
+ {
+ EditorGUILayout.HelpBox(
+ "No shaders registered. Re-import or modify the settings asset to trigger sync.",
+ MessageType.Warning);
+ }
+ }
+
serializedObject.ApplyModifiedProperties();
// Upgrade All Assets For V3.
diff --git a/Packages/src/README.md b/Packages/src/README.md
index 3dcf259..f172ac6 100644
--- a/Packages/src/README.md
+++ b/Packages/src/README.md
@@ -415,6 +415,13 @@ You can adjust the project-wide settings for SoftMaskForUGUI. (`Edit > Project S
> - The setting file is usually saved in `Assets/ProjectSettings/UISoftMaskProjectSettings.asset`. Include this file in your version control system.
> - The setting file is automatically added as a preloaded asset in `ProjectSettings/ProjectSettings.asset`.
+#### Advanced
+
+- **Exclude From Preloaded Assets When Build Player**: When enabled, the settings asset will be **temporarily removed** from `PlayerSettings.preloadedAssets` **during Player Build only**, so it is NOT included in the built player. The asset remains in PreloadedAssets during normal Editor operation.
+ - Use this for AssetBundle / Addressables based hot-update workflows where settings and shaders are delivered externally instead of being built into the player.
+ - Shader references are serialized from the `ShaderVariantCollection` at edit-time, so `Shader.Find()` is not required at runtime.
+ - When toggled off, serialized shader references are automatically cleared.
+
### Usage with Scripts
diff --git a/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs b/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs
index 1908b11..3130007 100644
--- a/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs
+++ b/Packages/src/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs
@@ -14,6 +14,37 @@ namespace Coffee.UISoftMaskInternal
public abstract class PreloadedProjectSettings : ScriptableObject
#if UNITY_EDITOR
{
+ [Tooltip("When enabled, this settings asset will be temporarily removed from " +
+ "PlayerSettings.preloadedAssets during Player Build, so it is NOT included " +
+ "in the built player. The asset remains in PreloadedAssets during normal " +
+ "Editor operation.\n\n" +
+ "Enable this when you deliver settings and shaders via AssetBundles or " +
+ "Addressables for hot-update support. Shaders are then resolved via direct " +
+ "serialized references instead of Shader.Find().")]
+ [SerializeField]
+ private bool m_ExcludeFromPreloadedAssetsWhenBuildPlayer;
+
+ ///
+ /// When enabled, this settings asset will be temporarily removed from
+ /// only during Player Build,
+ /// so it is NOT included in the built player. The asset remains in
+ /// PreloadedAssets during normal Editor operation.
+ ///
+ /// Enable this when you deliver settings and shaders via AssetBundles or
+ /// Addressables for hot-update support. Shaders are resolved via direct
+ /// serialized references in the shader variant registry instead of
+ /// , since AB-loaded shaders are not registered
+ /// in the global shader name table.
+ ///
+ ///
+ public bool excludeFromPreloadedAssetsWhenBuildPlayer
+ {
+ get => m_ExcludeFromPreloadedAssetsWhenBuildPlayer;
+ set => m_ExcludeFromPreloadedAssetsWhenBuildPlayer = value;
+ }
+
+ protected static bool s_BuildingPlayer;
+
private class Postprocessor : AssetPostprocessor
{
private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___, string[] ____)
@@ -22,6 +53,47 @@ private static void OnPostprocessAllAssets(string[] _, string[] __, string[] ___
}
}
+ private class ExcludeFromBuild : IPreprocessBuildWithReport, IPostprocessBuildWithReport
+ {
+ int IOrderedCallback.callbackOrder => -1;
+
+ void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report)
+ {
+ s_BuildingPlayer = true;
+
+ foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>)))
+ {
+ var settings = GetDefaultSettings(t);
+ if (!settings || !settings.m_ExcludeFromPreloadedAssetsWhenBuildPlayer) continue;
+
+ PlayerSettings.SetPreloadedAssets(
+ PlayerSettings.GetPreloadedAssets()
+ .Where(x => x && x.GetType() != t)
+ .ToArray());
+
+ Debug.Log($"[PreloadedProjectSettings] Build started: removed '{settings.name}' " +
+ $"({t.Name}) from PreloadedAssets. " +
+ $"It will be restored after build completes.");
+ }
+ }
+
+ void IPostprocessBuildWithReport.OnPostprocessBuild(BuildReport report)
+ {
+ s_BuildingPlayer = false;
+
+ Initialize();
+
+ foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>)))
+ {
+ var settings = GetDefaultSettings(t);
+ if (!settings || !settings.m_ExcludeFromPreloadedAssetsWhenBuildPlayer) continue;
+
+ Debug.Log($"[PreloadedProjectSettings] Build finished: restored '{settings.name}' " +
+ $"({t.Name}) to PreloadedAssets.");
+ }
+ }
+ }
+
private class PreprocessBuildWithReport : IPreprocessBuildWithReport
{
int IOrderedCallback.callbackOrder => 0;
@@ -41,11 +113,11 @@ private static void Initialize()
{
// When create a new instance, automatically set it as default settings.
defaultSettings = CreateInstance(t) as PreloadedProjectSettings;
- SetDefaultSettings(defaultSettings);
+ if (!s_BuildingPlayer) SetDefaultSettings(defaultSettings);
}
else if (GetPreloadedSettings(t).Length != 1)
{
- SetDefaultSettings(defaultSettings);
+ if (!s_BuildingPlayer) SetDefaultSettings(defaultSettings);
}
if (defaultSettings)
@@ -151,7 +223,7 @@ public static T instance
return s_Instance;
}
- SetDefaultSettings(s_Instance);
+ if (!s_BuildingPlayer) SetDefaultSettings(s_Instance);
return s_Instance;
}
}
diff --git a/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs b/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs
index 4a2d14e..2bd79c2 100644
--- a/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs
+++ b/Packages/src/Runtime/Internal/Utilities/ShaderVariantRegistry.cs
@@ -46,6 +46,7 @@ public override int GetHashCode()
}
private Dictionary _cachedOptionalShaders = new Dictionary();
+ private Dictionary _shaderByName;
[SerializeField]
private List m_OptionalShaders = new List();
@@ -53,6 +54,9 @@ public override int GetHashCode()
[SerializeField]
internal ShaderVariantCollection m_Asset;
+ [SerializeField]
+ private List m_RegisteredShaders = new List();
+
#if UNITY_EDITOR
[SerializeField]
private bool m_ErrorOnUnregisteredVariant = false;
@@ -64,6 +68,43 @@ public override int GetHashCode()
public ShaderVariantCollection shaderVariantCollection => m_Asset;
public Func onShaderRequested;
+ ///
+ /// Build the runtime name-to-shader lookup from .
+ /// When shaders are delivered via AssetBundles (i.e. not in the player build),
+ /// cannot locate them by name. This lookup provides
+ /// direct references serialized at edit-time as a reliable alternative.
+ ///
+ public void InitializeShaderLookup()
+ {
+ var count = m_RegisteredShaders.Count;
+ if (count == 0) return;
+
+ if (_shaderByName == null)
+ _shaderByName = new Dictionary(count);
+ else
+ _shaderByName.Clear();
+
+ for (var i = 0; i < count; i++)
+ {
+ var s = m_RegisteredShaders[i];
+ if (s)
+ _shaderByName[s.name] = s;
+ }
+ }
+
+ ///
+ /// Find a shader by name. Prefers the registered direct reference from
+ /// when available, falls back to
+ /// otherwise. This ensures correct behavior in both
+ /// built-in delivery (PreloadedAssets) and external delivery (AssetBundle) modes.
+ ///
+ public Shader FindShaderByName(string name)
+ {
+ if (_shaderByName != null && _shaderByName.TryGetValue(name, out var shader) && shader)
+ return shader;
+ return Shader.Find(name);
+ }
+
public Shader FindOptionalShader(Shader shader,
string requiredName,
string format,
@@ -75,7 +116,7 @@ public Shader FindOptionalShader(Shader shader,
var id = shader.GetInstanceID();
if (_cachedOptionalShaders.TryGetValue(id, out var optionalShaderName))
{
- return Shader.Find(optionalShaderName);
+ return FindShaderByName(optionalShaderName);
}
// The shader has required name.
@@ -91,7 +132,7 @@ public Shader FindOptionalShader(Shader shader,
foreach (var pair in m_OptionalShaders)
{
if (pair.key != shaderName) continue;
- optionalShader = Shader.Find(pair.value);
+ optionalShader = FindShaderByName(pair.value);
if (optionalShader)
{
_cachedOptionalShaders[id] = pair.value;
@@ -101,7 +142,7 @@ public Shader FindOptionalShader(Shader shader,
// Find optional shader by format.
optionalShaderName = string.Format(format, shaderName);
- optionalShader = Shader.Find(optionalShaderName);
+ optionalShader = FindShaderByName(optionalShaderName);
if (optionalShader)
{
_cachedOptionalShaders[id] = optionalShaderName;
@@ -111,13 +152,13 @@ public Shader FindOptionalShader(Shader shader,
#if UNITY_EDITOR
if (onShaderRequested?.Invoke(optionalShaderName) ?? false)
{
- return Shader.Find(defaultOptionalShaderName);
+ return FindShaderByName(defaultOptionalShaderName);
}
#endif
// Find default optional shader.
_cachedOptionalShaders[id] = defaultOptionalShaderName;
- return Shader.Find(defaultOptionalShaderName);
+ return FindShaderByName(defaultOptionalShaderName);
}
#if UNITY_EDITOR
@@ -204,10 +245,48 @@ public void InitializeIfNeeded(Object owner)
AssetDatabase.SaveAssets();
}
+ SyncRegisteredShaders(owner);
ClearCache();
Profiler.EndSample();
}
+ ///
+ /// Sync based on the current delivery mode.
+ /// When is enabled,
+ /// extracts shader references from the SVC so they can be resolved at runtime
+ /// without . Otherwise clears the list to avoid
+ /// stale references and unnecessary asset dependencies.
+ ///
+ private void SyncRegisteredShaders(Object owner)
+ {
+ var settings = owner as PreloadedProjectSettings;
+ var needsDirectRef = settings && settings.excludeFromPreloadedAssetsWhenBuildPlayer;
+
+ if (!needsDirectRef || !m_Asset)
+ {
+ if (m_RegisteredShaders.Count > 0)
+ {
+ m_RegisteredShaders.Clear();
+ EditorUtility.SetDirty(owner);
+ }
+ return;
+ }
+
+ var so = new SerializedObject(m_Asset);
+ var shaders = so.FindProperty("m_Shaders");
+ m_RegisteredShaders.Clear();
+ for (var i = 0; i < shaders.arraySize; i++)
+ {
+ var shaderRef = shaders.GetArrayElementAtIndex(i)
+ .FindPropertyRelative("first")
+ .objectReferenceValue as Shader;
+ if (shaderRef && !m_RegisteredShaders.Contains(shaderRef))
+ m_RegisteredShaders.Add(shaderRef);
+ }
+
+ EditorUtility.SetDirty(owner);
+ }
+
internal void RegisterVariant(Material material, string path)
{
if (!material || !material.shader || !m_Asset) return;
diff --git a/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs b/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs
index 8b934ac..289230c 100644
--- a/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs
+++ b/Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs
@@ -31,7 +31,8 @@ protected override void OnEnable()
{
if (!s_SharedTerminalMaterial)
{
- s_SharedTerminalMaterial = new Material(Shader.Find("Hidden/UI/TerminalMaskingShape"))
+ s_SharedTerminalMaterial = new Material(UISoftMaskProjectSettings.shaderRegistry
+ .FindShaderByName("Hidden/UI/TerminalMaskingShape"))
{
hideFlags = HideFlags.DontSave | HideFlags.NotEditable
};
diff --git a/Packages/src/Runtime/UISoftMaskProjectSettings.cs b/Packages/src/Runtime/UISoftMaskProjectSettings.cs
index fdcfeee..ca522e4 100644
--- a/Packages/src/Runtime/UISoftMaskProjectSettings.cs
+++ b/Packages/src/Runtime/UISoftMaskProjectSettings.cs
@@ -118,6 +118,14 @@ private static void ResetAllSoftMasks()
InternalListPool.Return(ref softMasks);
}
+#if !UNITY_EDITOR
+ protected override void OnEnable()
+ {
+ base.OnEnable();
+ m_ShaderVariantRegistry.InitializeShaderLookup();
+ }
+#endif
+
#if UNITY_EDITOR
[InitializeOnLoadMethod]
private static void InitializeOnLoadMethod()
@@ -172,6 +180,7 @@ private static void InitializeOnLoadMethod()
protected override void OnEnable()
{
base.OnEnable();
+ m_ShaderVariantRegistry.InitializeShaderLookup();
m_ShaderVariantRegistry.onShaderRequested = ShaderSampleImporter.ImportShaderIfSelected;
m_ShaderVariantRegistry.ClearCache();
}
diff --git a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs
index 8babd9b..69f5f87 100644
--- a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs
+++ b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs
@@ -152,7 +152,8 @@ private static Material GetSoftMaskingMaterial(ref Material mat, BlendOp op)
{
if (mat) return mat;
- mat = new Material(Shader.Find("Hidden/UI/SoftMask"))
+ mat = new Material(UISoftMaskProjectSettings.shaderRegistry
+ .FindShaderByName("Hidden/UI/SoftMask"))
{
hideFlags = HideFlags.DontSave | HideFlags.NotEditable
};