Skip to content

Commit

Permalink
feat: add SoftMaskForUGUI support
Browse files Browse the repository at this point in the history
Allow `UIEffect` component to automatically select shaders like 'UIEffect-SoftMaskable' without any manual configuration.
close #270
  • Loading branch information
mob-sakai committed Mar 1, 2025
1 parent 91e5be7 commit ca92837
Show file tree
Hide file tree
Showing 25 changed files with 593 additions and 164 deletions.
7 changes: 6 additions & 1 deletion Packages/src/Editor/UIEffectProjectSettingsEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ public override void OnInspectorGUI()
if (_shaderVariantRegistryEditor == null)
{
var property = serializedObject.FindProperty("m_ShaderVariantRegistry");
_shaderVariantRegistryEditor = new ShaderVariantRegistryEditor(property, "(UIEffect)");
_shaderVariantRegistryEditor = new ShaderVariantRegistryEditor(property, "(UIEffect)",
() =>
{
UIEffectProjectSettings.shaderRegistry
.RegisterOptionalShaders(UIEffectProjectSettings.instance);
});
}

_shaderVariantRegistryEditor.Draw();
Expand Down
17 changes: 17 additions & 0 deletions Packages/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Combine various filters, such as grayscale, blur, and dissolve, to decorate your
- [Component: UIEffectTweener](#component-uieffecttweener)
- [Component: UIEffectReplica](#component-uieffectreplica)
- [Usage with TextMeshPro](#usage-with-textmeshpro)
- [Usage with SoftMaskForUGUI](#usage-with-softmaskforugui)
- [Runtime/Editor Preset for UIEffect](#runtimeeditor-preset-for-uieffect)
- [Usage with Code](#usage-with-code)
- [Project Settings](#project-settings)
Expand Down Expand Up @@ -345,6 +346,18 @@ Alternatively, you can manually import the resources by following these steps:
<br><br>

### Usage with SoftMaskForUGUI

![](https://github.com/user-attachments/assets/7701e765-896a-49a8-b1ed-22adb0ecce12)

[SoftMaskForUGUI](https://github.com/mob-sakai/SoftMaskForUGUI) is a package that allows you to create soft masks for UI elements.

`SoftMaskForUGUI (v3.3.0+)` supports `UIEffect (v5.6.0+)`.
When a shader included in the samples is requested, an import dialog will automatically appear.
Click the `Import` button.

<br><br>

### Runtime/Editor Preset for UIEffect

![](https://github.com/user-attachments/assets/4244d162-a768-463e-8aad-8cafbdf3a5b3)
Expand Down Expand Up @@ -418,6 +431,10 @@ You can adjust the project-wide settings for UIEffect. (`Edit > Project Settings
- **Error On Unregistered Variant**: If enabled, an error will be displayed when an unregistered shader variant is used.
- The shader variant will be automatically added to the `Unregistered Variants` list.

> [!IMPORTANT]
> - The setting file is usually saved in `Assets/ProjectSettings/UIEffectProjectSettings.asset`. Include this file in your version control system.
> - The setting file is automatically added as a preloaded asset in `ProjectSettings/ProjectSettings.asset`.
<br><br>

### :warning: Limitations
Expand Down
158 changes: 158 additions & 0 deletions Packages/src/Runtime/Internal/Utilities/ShaderSampleImporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager.UI;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;

namespace Coffee.UIEffectInternal
{
internal static class ShaderSampleImporter
{
private static (string shaderName, string sampleName, string version)[] s_Samples;
private static (string guid, string fileName)[] s_DeprecatedShaders;
private static readonly Dictionary<string, string> s_SampleNames = new Dictionary<string, string>();

public static void RegisterShaderSamples((string shaderName, string sampleName, string version)[] samples)
{
if (IsBatchOrBuilding()) return;

// Collect sample names.
s_Samples = samples;
foreach (var (shaderName, sampleName, _) in samples)
{
s_SampleNames[shaderName] = sampleName;
}
}

public static void RegisterDeprecatedShaders((string, string)[] deprecatedShaders)
{
if (IsBatchOrBuilding()) return;

s_DeprecatedShaders = deprecatedShaders;
}

/// <summary>
/// Import the sample containing the requested shader.
/// If choice 'Import' is selected, the sample is imported.
/// If choice 'Skip in this session' is selected, the sample is skipped in this session.
/// </summary>
public static bool ImportShaderIfSelected(string shaderName)
{
if (IsBatchOrBuilding()) return false;

// Find sample name.
if (s_SampleNames.TryGetValue(shaderName, out var sampleName))
{
var message = $"Import the sample '{sampleName}' to use the shader '{shaderName}'.";
return ImportSampleIfSelected(sampleName, message);
}

return false;
}

private static bool ImportSampleIfSelected(string sampleName, string message)
{
if (IsBatchOrBuilding()) return false;
if (!s_SampleNames.ContainsValue(sampleName)) return false;

// Find package info.
var pInfo = PackageInfo.FindForAssembly(typeof(ShaderSampleImporter).Assembly);
if (pInfo == null) return false;

// Find sample. If not found (resolvedPath == null), skip.
var sample = Sample.FindByPackage(pInfo.name, pInfo.version)
.FirstOrDefault(x => x.displayName == sampleName);
if (sample.resolvedPath == null) return false;

// Import the sample if selected.
if (!string.IsNullOrEmpty(message))
{
var selected = EditorUtility.DisplayDialog($"Import {sampleName}", message, "Import", "Cancel");
if (!selected) return false;
}

// Remove the imported sample name from the list.
foreach (var shaderName in s_SampleNames.Keys.ToArray())
{
if (s_SampleNames[shaderName] != sampleName) continue;
s_SampleNames.Remove(shaderName);
DeleteShader(shaderName);
}

// Import the sample in the next frame.
EditorApplication.delayCall += () => sample.Import(Sample.ImportOptions.OverridePreviousImports);
return true;
}

private static string GetVersion(string shaderName)
{
if (string.IsNullOrEmpty(shaderName)) return null;

var shader = Shader.Find(shaderName);
if (!shader) return null;

var path = AssetDatabase.GetAssetPath(shader);
if (string.IsNullOrEmpty(path)) return null;

return AssetImporter.GetAtPath(path).userData;
}

private static void DeleteShader(string shaderName)
{
var shader = Shader.Find(shaderName);
if (shader)
{
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(shader));
}
}

private static bool IsBatchOrBuilding()
{
return Application.isBatchMode || BuildPipeline.isBuildingPlayer;
}

public static void Update()
{
if (s_Samples == null || IsBatchOrBuilding()) return;

// Find package info.
var pInfo = PackageInfo.FindForAssembly(typeof(ShaderSampleImporter).Assembly);
if (pInfo == null) return;

// Find sample. If not found (resolvedPath == null), skip.
var sample = Sample.FindByPackage(pInfo.name, pInfo.version).FirstOrDefault();
if (sample.resolvedPath == null) return;

// Update the sample.
foreach (var (shaderName, sampleName, version) in s_Samples)
{
if (string.IsNullOrEmpty(version)) continue;
if (!s_SampleNames.ContainsValue(sampleName)) continue;

// If the shader exist and the version is different, add the sample to the update list.
var currentVersion = GetVersion(shaderName);
if (currentVersion != null && currentVersion != version && ImportSampleIfSelected(sampleName, null))
{
Debug.Log($"Updating '{sampleName}' to use the package '{pInfo.displayName} v{pInfo.version}'.");
}
}

// Remove deprecated shaders.
foreach (var (guid, fileName) in s_DeprecatedShaders)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path) && Path.GetFileName(path) == fileName)
{
AssetDatabase.DeleteAsset(path);
}
}

s_Samples = null;
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ca92837

Please sign in to comment.