feat: support graphic connector
The rendering material is automatically generated. You don't need to pre-generate the material. The generated materials are cached and properly batched. Close #75, close #76, close #80 BREAKING CHANGE: The name of the custom SoftMaskable shader must be changed. For more information, see the ‘Support soft masks with your custom shaders’ section of the README.vr
parent
0389363798
commit
34515216a3
Packages/SoftMaskForUGUI
Samples/Demo
|
@ -10,11 +10,14 @@ namespace Coffee.UIExtensions.Demos
|
|||
[SerializeField] RawImage[] softMaskBufferViewer;
|
||||
[SerializeField] SoftMask[] softMask;
|
||||
[SerializeField] Text text;
|
||||
[SerializeField] GameObject title;
|
||||
|
||||
|
||||
// Use this for initialization
|
||||
void OnEnable()
|
||||
{
|
||||
title.SetActive(true);
|
||||
|
||||
text.text = string.Format("GPU: {0}\nDeviceType: {1}\nShaderLevel: {2}\nUVStartsAtTop: {3}",
|
||||
SystemInfo.graphicsDeviceName,
|
||||
SystemInfo.graphicsDeviceType,
|
||||
|
@ -25,13 +28,6 @@ namespace Coffee.UIExtensions.Demos
|
|||
{
|
||||
softMaskBufferViewer[i].texture = softMask[i].softMaskBuffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void SetWorldSpase(bool flag)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Coffee.UIExtensions
|
||||
{
|
||||
internal static class GraphicConnectorExtension
|
||||
{
|
||||
public static void SetVerticesDirtyEx(this Graphic graphic)
|
||||
{
|
||||
GraphicConnector.FindConnector(graphic).SetVerticesDirty(graphic);
|
||||
}
|
||||
|
||||
public static void SetMaterialDirtyEx(this Graphic graphic)
|
||||
{
|
||||
GraphicConnector.FindConnector(graphic).SetMaterialDirty(graphic);
|
||||
}
|
||||
|
||||
public static Shader FindEffectShader(this Graphic graphic)
|
||||
{
|
||||
return GraphicConnector.FindConnector(graphic).FindEffectShader(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class GraphicConnector
|
||||
{
|
||||
|
||||
private static readonly List<GraphicConnector> s_Connectors = new List<GraphicConnector>();
|
||||
private static readonly Dictionary<Type, GraphicConnector> s_ConnectorMap = new Dictionary<Type, GraphicConnector>();
|
||||
private static readonly GraphicConnector s_EmptyConnector = new GraphicConnector();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.InitializeOnLoadMethod]
|
||||
#endif
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Init()
|
||||
{
|
||||
AddConnector(new GraphicConnector());
|
||||
}
|
||||
|
||||
protected static void AddConnector(GraphicConnector connector)
|
||||
{
|
||||
s_Connectors.Add(connector);
|
||||
s_Connectors.Sort((x, y) => y.priority - x.priority);
|
||||
}
|
||||
|
||||
public static GraphicConnector FindConnector(Graphic graphic)
|
||||
{
|
||||
if (!graphic) return s_EmptyConnector;
|
||||
|
||||
var type = graphic.GetType();
|
||||
GraphicConnector connector = null;
|
||||
if (s_ConnectorMap.TryGetValue(type, out connector)) return connector;
|
||||
|
||||
foreach (var c in s_Connectors)
|
||||
{
|
||||
if (!c.IsValid(graphic)) continue;
|
||||
|
||||
s_ConnectorMap.Add(type, c);
|
||||
return c;
|
||||
}
|
||||
|
||||
return s_EmptyConnector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connector priority.
|
||||
/// </summary>
|
||||
protected virtual int priority
|
||||
{
|
||||
get { return -1; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Find effect shader.
|
||||
/// </summary>
|
||||
public virtual Shader FindEffectShader(Graphic graphic)
|
||||
{
|
||||
return Shader.Find("Hidden/UI/SoftMaskable");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The connector is valid for the component.
|
||||
/// </summary>
|
||||
protected virtual bool IsValid(Graphic graphic)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void SetVerticesDirty(Graphic graphic)
|
||||
{
|
||||
if (graphic)
|
||||
graphic.SetVerticesDirty();
|
||||
}
|
||||
|
||||
public virtual void SetMaterialDirty(Graphic graphic)
|
||||
{
|
||||
if (graphic)
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0e702140c28f4425fac896f9394a31b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -0,0 +1,80 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Coffee.UIExtensions
|
||||
{
|
||||
internal class MaterialCache
|
||||
{
|
||||
public delegate void ModifyAction(Material material, Graphic graphic);
|
||||
|
||||
static Dictionary<Hash128, MaterialEntry> materialMap = new Dictionary<Hash128, MaterialEntry>();
|
||||
|
||||
private class MaterialEntry
|
||||
{
|
||||
public Material material;
|
||||
public int referenceCount;
|
||||
|
||||
public void Release()
|
||||
{
|
||||
if (material)
|
||||
{
|
||||
UnityEngine.Object.DestroyImmediate(material, false);
|
||||
}
|
||||
|
||||
material = null;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.InitializeOnLoadMethod]
|
||||
private static void ClearCache()
|
||||
{
|
||||
foreach (var entry in materialMap.Values)
|
||||
{
|
||||
entry.Release();
|
||||
}
|
||||
|
||||
materialMap.Clear();
|
||||
}
|
||||
#endif
|
||||
|
||||
public static Material Register(Material material, Hash128 hash, Action<Material> onModify)
|
||||
{
|
||||
if (!hash.isValid) return null;
|
||||
|
||||
MaterialEntry entry;
|
||||
if (!materialMap.TryGetValue(hash, out entry))
|
||||
{
|
||||
entry = new MaterialEntry()
|
||||
{
|
||||
material = new Material(material)
|
||||
{
|
||||
hideFlags = HideFlags.HideAndDontSave,
|
||||
},
|
||||
};
|
||||
|
||||
onModify(entry.material);
|
||||
materialMap.Add(hash, entry);
|
||||
}
|
||||
|
||||
entry.referenceCount++;
|
||||
//Debug.LogFormat("Register: {0}, {1} (Total: {2})", hash, entry.referenceCount, materialMap.Count);
|
||||
return entry.material;
|
||||
}
|
||||
|
||||
public static void Unregister(Hash128 hash)
|
||||
{
|
||||
MaterialEntry entry;
|
||||
if (!hash.isValid || !materialMap.TryGetValue(hash, out entry)) return;
|
||||
//Debug.LogFormat("Unregister: {0}, {1}", hash, entry.referenceCount -1);
|
||||
|
||||
if (--entry.referenceCount > 0) return;
|
||||
|
||||
entry.Release();
|
||||
materialMap.Remove(hash);
|
||||
//Debug.LogFormat("Unregister: Release Emtry: {0}, {1} (Total: {2})", hash, entry.referenceCount, materialMap.Count);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: be6c8de8d4ec241fdbfad99aca2497d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
|
@ -1,7 +1,4 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UI;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UI;
|
||||
using MaskIntr = UnityEngine.SpriteMaskInteraction;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Coffee.UIExtensions
|
||||
{
|
||||
|
@ -17,18 +15,15 @@ namespace Coffee.UIExtensions
|
|||
#else
|
||||
[ExecuteInEditMode]
|
||||
# endif
|
||||
public class SoftMaskable : MonoBehaviour, IMaterialModifier, ICanvasRaycastFilter, ISerializationCallbackReceiver
|
||||
public class SoftMaskable : MonoBehaviour, IMaterialModifier, ICanvasRaycastFilter
|
||||
#if UNITY_EDITOR
|
||||
, ISerializationCallbackReceiver
|
||||
# endif
|
||||
{
|
||||
//################################
|
||||
// Constant or Static Members.
|
||||
//################################
|
||||
const int kVisibleInside = (1 << 0) + (1 << 2) + (1 << 4) + (1 << 6);
|
||||
const int kVisibleOutside = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6);
|
||||
static readonly Hash128 k_InvalidHash = new Hash128();
|
||||
|
||||
|
||||
//################################
|
||||
// Serialize Members.
|
||||
//################################
|
||||
[Tooltip("The graphic will be visible only in areas where no mask is present.")]
|
||||
[System.Obsolete]
|
||||
[HideInInspector]
|
||||
|
@ -41,10 +36,16 @@ namespace Coffee.UIExtensions
|
|||
[Tooltip("Use soft-masked raycast target.\n\nNote: This option is expensive.")]
|
||||
[SerializeField] bool m_RaycastFilter = false;
|
||||
|
||||
Graphic _graphic = null;
|
||||
SoftMask _softMask = null;
|
||||
Material _maskMaterial = null;
|
||||
static int s_SoftMaskTexId;
|
||||
static int s_StencilCompId;
|
||||
static int s_MaskInteractionId;
|
||||
static List<SoftMaskable> s_ActiveSoftMaskables;
|
||||
static int[] s_Interactions = new int[4];
|
||||
Hash128 _effectMaterialHash;
|
||||
|
||||
//################################
|
||||
// Public Members.
|
||||
//################################
|
||||
/// <summary>
|
||||
/// Perform material modification in this function.
|
||||
/// </summary>
|
||||
|
@ -71,34 +72,46 @@ namespace Coffee.UIExtensions
|
|||
parentTransform = parentTransform.parent;
|
||||
}
|
||||
|
||||
Material result = baseMaterial;
|
||||
var oldHash = _effectMaterialHash;
|
||||
var modifiedMaterial = baseMaterial;
|
||||
if (_softMask)
|
||||
{
|
||||
result = new Material(baseMaterial);
|
||||
result.hideFlags = HideFlags.HideAndDontSave;
|
||||
result.SetTexture(s_SoftMaskTexId, _softMask.softMaskBuffer);
|
||||
result.SetInt(s_StencilCompId, m_UseStencil ? (int)CompareFunction.Equal : (int)CompareFunction.Always);
|
||||
result.SetVector(s_MaskInteractionId, new Vector4(
|
||||
_effectMaterialHash = GetMaterialHash(baseMaterial);
|
||||
modifiedMaterial = MaterialCache.Register(baseMaterial, _effectMaterialHash, mat =>
|
||||
{
|
||||
Debug.Log(mat.shader.name);
|
||||
mat.shader = Shader.Find(string.Format("Hidden/{0} (SoftMaskable)", mat.shader.name));
|
||||
#if UNITY_EDITOR
|
||||
mat.EnableKeyword("SOFTMASK_EDITOR");
|
||||
#endif
|
||||
mat.SetTexture(s_SoftMaskTexId, _softMask.softMaskBuffer);
|
||||
mat.SetInt(s_StencilCompId, m_UseStencil ? (int)CompareFunction.Equal : (int)CompareFunction.Always);
|
||||
mat.SetVector(s_MaskInteractionId, new Vector4(
|
||||
(m_MaskInteraction & 0x3),
|
||||
((m_MaskInteraction >> 2) & 0x3),
|
||||
((m_MaskInteraction >> 4) & 0x3),
|
||||
((m_MaskInteraction >> 6) & 0x3)
|
||||
));
|
||||
|
||||
StencilMaterial.Remove(baseMaterial);
|
||||
});
|
||||
ReleaseMaterial(ref _maskMaterial);
|
||||
_maskMaterial = result;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
result.EnableKeyword("SOFTMASK_EDITOR");
|
||||
#endif
|
||||
_maskMaterial = modifiedMaterial;
|
||||
}
|
||||
else
|
||||
|
||||
MaterialCache.Unregister(oldHash);
|
||||
return modifiedMaterial;
|
||||
}
|
||||
|
||||
private Hash128 GetMaterialHash(Material material)
|
||||
{
|
||||
baseMaterial.SetTexture(s_SoftMaskTexId, Texture2D.whiteTexture);
|
||||
}
|
||||
if (!isActiveAndEnabled || !material || !material.shader)
|
||||
return k_InvalidHash;
|
||||
|
||||
return result;
|
||||
return new Hash128(
|
||||
(uint) material.GetInstanceID(),
|
||||
(uint) m_MaskInteraction,
|
||||
(uint) (m_UseStencil ? 1 : 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -111,15 +124,10 @@ namespace Coffee.UIExtensions
|
|||
{
|
||||
if (!isActiveAndEnabled || !_softMask)
|
||||
return true;
|
||||
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(transform as RectTransform, sp, eventCamera))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (!m_RaycastFilter)
|
||||
{
|
||||
if (!m_RaycastFilter)
|
||||
return true;
|
||||
}
|
||||
|
||||
var sm = _softMask;
|
||||
for (int i = 0; i < 4; i++)
|
||||
|
@ -144,7 +152,7 @@ namespace Coffee.UIExtensions
|
|||
if (m_MaskInteraction != intValue)
|
||||
{
|
||||
m_MaskInteraction = intValue;
|
||||
graphic.SetMaterialDirty();
|
||||
graphic.SetMaterialDirtyEx();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,37 +185,9 @@ namespace Coffee.UIExtensions
|
|||
public void SetMaskInteraction(SpriteMaskInteraction layer0, SpriteMaskInteraction layer1, SpriteMaskInteraction layer2, SpriteMaskInteraction layer3)
|
||||
{
|
||||
m_MaskInteraction = (int)layer0 + ((int)layer1 << 2) + ((int)layer2 << 4) + ((int)layer3 << 6);
|
||||
if (graphic)
|
||||
{
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
graphic.SetMaterialDirtyEx();
|
||||
}
|
||||
|
||||
//################################
|
||||
// Private Members.
|
||||
//################################
|
||||
Graphic _graphic = null;
|
||||
SoftMask _softMask = null;
|
||||
Material _maskMaterial = null;
|
||||
static int s_SoftMaskTexId;
|
||||
static int s_StencilCompId;
|
||||
static int s_MaskInteractionId;
|
||||
static List<SoftMaskable> s_ActiveSoftMaskables;
|
||||
static int[] s_Interactions = new int[4];
|
||||
static Material s_DefaultMaterial;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only).
|
||||
/// </summary>
|
||||
void OnValidate()
|
||||
{
|
||||
if (graphic)
|
||||
{
|
||||
graphic.SetMaterialDirty();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// This function is called when the object becomes enabled and active.
|
||||
|
@ -229,10 +209,6 @@ namespace Coffee.UIExtensions
|
|||
var g = graphic;
|
||||
if (g)
|
||||
{
|
||||
if (!g.material || g.material == Graphic.defaultGraphicMaterial)
|
||||
{
|
||||
g.material = s_DefaultMaterial ?? (s_DefaultMaterial = new Material(Resources.Load<Shader>("UI-Default-SoftMask")) { hideFlags = HideFlags.HideAndDontSave, });
|
||||
}
|
||||
g.SetMaterialDirty();
|
||||
}
|
||||
_softMask = null;
|
||||
|
@ -248,15 +224,14 @@ namespace Coffee.UIExtensions
|
|||
var g = graphic;
|
||||
if (g)
|
||||
{
|
||||
if (g.material == s_DefaultMaterial)
|
||||
{
|
||||
g.material = null;
|
||||
}
|
||||
g.SetMaterialDirty();
|
||||
}
|
||||
ReleaseMaterial(ref _maskMaterial);
|
||||
|
||||
_softMask = null;
|
||||
|
||||
MaterialCache.Unregister(_effectMaterialHash);
|
||||
_effectMaterialHash = k_InvalidHash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -266,8 +241,6 @@ namespace Coffee.UIExtensions
|
|||
{
|
||||
if (mat)
|
||||
{
|
||||
StencilMaterial.Remove(mat);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
|
@ -283,6 +256,15 @@ namespace Coffee.UIExtensions
|
|||
}
|
||||
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only).
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
graphic.SetMaterialDirtyEx();
|
||||
}
|
||||
|
||||
void ISerializationCallbackReceiver.OnBeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
@ -296,6 +278,18 @@ namespace Coffee.UIExtensions
|
|||
m_MaskInteraction = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6);
|
||||
}
|
||||
#pragma warning restore 0612
|
||||
|
||||
var current = this;
|
||||
UnityEditor.EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (current && graphic && graphic.material && graphic.material.shader && graphic.material.shader.name == "Hidden/UI/Default (SoftMaskable)")
|
||||
{
|
||||
Debug.LogFormat("OnAfterDeserialize: reset material {0}",current);
|
||||
graphic.material = null;
|
||||
graphic.SetMaterialDirtyEx();
|
||||
}
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Shader "UI/Default-SoftMask"
|
||||
Shader "Hidden/UI/Default (SoftMaskable)"
|
||||
{
|
||||
Properties
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
Shader "TextMeshPro/Distance Field (SoftMaskable)" {
|
||||
Shader "Hidden/TextMeshPro/Distance Field (SoftMaskable)" {
|
||||
|
||||
Properties {
|
||||
_FaceTex ("Face Texture", 2D) = "white" {}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// - No Glow Option
|
||||
// - Softness is applied on both side of the outline
|
||||
|
||||
Shader "TextMeshPro/Mobile/Distance Field (SoftMaskable)" {
|
||||
Shader "Hidden/TextMeshPro/Mobile/Distance Field (SoftMaskable)" {
|
||||
|
||||
Properties {
|
||||
_FaceColor ("Face Color", Color) = (1,1,1,1)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Shader "TextMeshPro/Sprite (SoftMaskable)"
|
||||
Shader "Hidden/TextMeshPro/Sprite (SoftMaskable)"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue