Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Packages/src/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
42 changes: 41 additions & 1 deletion Packages/src/Editor/UISoftMaskProjectSettingsEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions Packages/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<br><br>

### Usage with Scripts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// When enabled, this settings asset will be temporarily removed from
/// <see cref="PlayerSettings.preloadedAssets"/> only during Player Build,
/// so it is NOT included in the built player. The asset remains in
/// PreloadedAssets during normal Editor operation.
/// <para>
/// 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
/// <see cref="Shader.Find"/>, since AB-loaded shaders are not registered
/// in the global shader name table.
/// </para>
/// </summary>
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[] ____)
Expand All @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -151,7 +223,7 @@ public static T instance
return s_Instance;
}

SetDefaultSettings(s_Instance);
if (!s_BuildingPlayer) SetDefaultSettings(s_Instance);
return s_Instance;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ public override int GetHashCode()
}

private Dictionary<int, string> _cachedOptionalShaders = new Dictionary<int, string>();
private Dictionary<string, Shader> _shaderByName;

[SerializeField]
private List<StringPair> m_OptionalShaders = new List<StringPair>();

[SerializeField]
internal ShaderVariantCollection m_Asset;

[SerializeField]
private List<Shader> m_RegisteredShaders = new List<Shader>();

#if UNITY_EDITOR
[SerializeField]
private bool m_ErrorOnUnregisteredVariant = false;
Expand All @@ -64,6 +68,43 @@ public override int GetHashCode()
public ShaderVariantCollection shaderVariantCollection => m_Asset;
public Func<string, bool> onShaderRequested;

/// <summary>
/// Build the runtime name-to-shader lookup from <see cref="m_RegisteredShaders"/>.
/// When shaders are delivered via AssetBundles (i.e. not in the player build),
/// <see cref="Shader.Find"/> cannot locate them by name. This lookup provides
/// direct references serialized at edit-time as a reliable alternative.
/// </summary>
public void InitializeShaderLookup()
{
var count = m_RegisteredShaders.Count;
if (count == 0) return;

if (_shaderByName == null)
_shaderByName = new Dictionary<string, Shader>(count);
else
_shaderByName.Clear();

for (var i = 0; i < count; i++)
{
var s = m_RegisteredShaders[i];
if (s)
_shaderByName[s.name] = s;
}
}

/// <summary>
/// Find a shader by name. Prefers the registered direct reference from
/// <see cref="m_RegisteredShaders"/> when available, falls back to
/// <see cref="Shader.Find"/> otherwise. This ensures correct behavior in both
/// built-in delivery (PreloadedAssets) and external delivery (AssetBundle) modes.
/// </summary>
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,
Expand All @@ -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.
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -204,10 +245,48 @@ public void InitializeIfNeeded(Object owner)
AssetDatabase.SaveAssets();
}

SyncRegisteredShaders(owner);
ClearCache();
Profiler.EndSample();
}

/// <summary>
/// Sync <see cref="m_RegisteredShaders"/> based on the current delivery mode.
/// When <see cref="PreloadedProjectSettings.excludeFromPreloadedAssetsWhenBuildPlayer"/> is enabled,
/// extracts shader references from the SVC so they can be resolved at runtime
/// without <see cref="Shader.Find"/>. Otherwise clears the list to avoid
/// stale references and unnecessary asset dependencies.
/// </summary>
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;
Expand Down
3 changes: 2 additions & 1 deletion Packages/src/Runtime/MaskingShape/TerminalMaskingShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
9 changes: 9 additions & 0 deletions Packages/src/Runtime/UISoftMaskProjectSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ private static void ResetAllSoftMasks()
InternalListPool<SoftMask>.Return(ref softMasks);
}

#if !UNITY_EDITOR
protected override void OnEnable()
{
base.OnEnable();
m_ShaderVariantRegistry.InitializeShaderLookup();
}
#endif

#if UNITY_EDITOR
[InitializeOnLoadMethod]
private static void InitializeOnLoadMethod()
Expand Down Expand Up @@ -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();
}
Expand Down
3 changes: 2 additions & 1 deletion Packages/src/Runtime/Utilities/SoftMaskUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
Loading