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
mob-sakai 2020-05-11 11:37:27 +09:00
parent 0389363798
commit 34515216a3
13 changed files with 330 additions and 558 deletions

View File

@ -10,11 +10,14 @@ namespace Coffee.UIExtensions.Demos
[SerializeField] RawImage[] softMaskBufferViewer; [SerializeField] RawImage[] softMaskBufferViewer;
[SerializeField] SoftMask[] softMask; [SerializeField] SoftMask[] softMask;
[SerializeField] Text text; [SerializeField] Text text;
[SerializeField] GameObject title;
// Use this for initialization // Use this for initialization
void OnEnable() void OnEnable()
{ {
title.SetActive(true);
text.text = string.Format("GPU: {0}\nDeviceType: {1}\nShaderLevel: {2}\nUVStartsAtTop: {3}", text.text = string.Format("GPU: {0}\nDeviceType: {1}\nShaderLevel: {2}\nUVStartsAtTop: {3}",
SystemInfo.graphicsDeviceName, SystemInfo.graphicsDeviceName,
SystemInfo.graphicsDeviceType, SystemInfo.graphicsDeviceType,
@ -25,13 +28,6 @@ namespace Coffee.UIExtensions.Demos
{ {
softMaskBufferViewer[i].texture = softMask[i].softMaskBuffer; softMaskBufferViewer[i].texture = softMask[i].softMaskBuffer;
} }
}
// Update is called once per frame
void Update()
{
} }
public void SetWorldSpase(bool flag) public void SetWorldSpase(bool flag)

File diff suppressed because it is too large Load Diff

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e702140c28f4425fac896f9394a31b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be6c8de8d4ec241fdbfad99aca2497d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,7 +1,4 @@
using System.Collections; using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using UnityEngine.UI; using UnityEngine.UI;

View File

@ -1,10 +1,8 @@
using System.Collections; using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.Rendering; using UnityEngine.Rendering;
using UnityEngine.UI; using UnityEngine.UI;
using MaskIntr = UnityEngine.SpriteMaskInteraction; using MaskIntr = UnityEngine.SpriteMaskInteraction;
using UnityEngine.Serialization;
namespace Coffee.UIExtensions namespace Coffee.UIExtensions
{ {
@ -17,18 +15,15 @@ namespace Coffee.UIExtensions
#else #else
[ExecuteInEditMode] [ExecuteInEditMode]
# endif # 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 kVisibleInside = (1 << 0) + (1 << 2) + (1 << 4) + (1 << 6);
const int kVisibleOutside = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 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.")] [Tooltip("The graphic will be visible only in areas where no mask is present.")]
[System.Obsolete] [System.Obsolete]
[HideInInspector] [HideInInspector]
@ -41,10 +36,16 @@ namespace Coffee.UIExtensions
[Tooltip("Use soft-masked raycast target.\n\nNote: This option is expensive.")] [Tooltip("Use soft-masked raycast target.\n\nNote: This option is expensive.")]
[SerializeField] bool m_RaycastFilter = false; [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> /// <summary>
/// Perform material modification in this function. /// Perform material modification in this function.
/// </summary> /// </summary>
@ -71,34 +72,46 @@ namespace Coffee.UIExtensions
parentTransform = parentTransform.parent; parentTransform = parentTransform.parent;
} }
Material result = baseMaterial; var oldHash = _effectMaterialHash;
var modifiedMaterial = baseMaterial;
if (_softMask) if (_softMask)
{ {
result = new Material(baseMaterial); _effectMaterialHash = GetMaterialHash(baseMaterial);
result.hideFlags = HideFlags.HideAndDontSave; modifiedMaterial = MaterialCache.Register(baseMaterial, _effectMaterialHash, mat =>
result.SetTexture(s_SoftMaskTexId, _softMask.softMaskBuffer); {
result.SetInt(s_StencilCompId, m_UseStencil ? (int)CompareFunction.Equal : (int)CompareFunction.Always); Debug.Log(mat.shader.name);
result.SetVector(s_MaskInteractionId, new Vector4( 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 & 0x3),
((m_MaskInteraction >> 2) & 0x3), ((m_MaskInteraction >> 2) & 0x3),
((m_MaskInteraction >> 4) & 0x3), ((m_MaskInteraction >> 4) & 0x3),
((m_MaskInteraction >> 6) & 0x3) ((m_MaskInteraction >> 6) & 0x3)
)); ));
});
StencilMaterial.Remove(baseMaterial);
ReleaseMaterial(ref _maskMaterial); ReleaseMaterial(ref _maskMaterial);
_maskMaterial = result; _maskMaterial = modifiedMaterial;
#if UNITY_EDITOR
result.EnableKeyword("SOFTMASK_EDITOR");
#endif
} }
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> /// <summary>
@ -111,15 +124,10 @@ namespace Coffee.UIExtensions
{ {
if (!isActiveAndEnabled || !_softMask) if (!isActiveAndEnabled || !_softMask)
return true; return true;
if (!RectTransformUtility.RectangleContainsScreenPoint(transform as RectTransform, sp, eventCamera)) if (!RectTransformUtility.RectangleContainsScreenPoint(transform as RectTransform, sp, eventCamera))
{
return false; return false;
} if (!m_RaycastFilter)
else if (!m_RaycastFilter)
{
return true; return true;
}
var sm = _softMask; var sm = _softMask;
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
@ -144,7 +152,7 @@ namespace Coffee.UIExtensions
if (m_MaskInteraction != intValue) if (m_MaskInteraction != intValue)
{ {
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) public void SetMaskInteraction(SpriteMaskInteraction layer0, SpriteMaskInteraction layer1, SpriteMaskInteraction layer2, SpriteMaskInteraction layer3)
{ {
m_MaskInteraction = (int)layer0 + ((int)layer1 << 2) + ((int)layer2 << 4) + ((int)layer3 << 6); m_MaskInteraction = (int)layer0 + ((int)layer1 << 2) + ((int)layer2 << 4) + ((int)layer3 << 6);
if (graphic) graphic.SetMaterialDirtyEx();
{
graphic.SetMaterialDirty();
}
} }
//################################
// 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> /// <summary>
/// This function is called when the object becomes enabled and active. /// This function is called when the object becomes enabled and active.
@ -229,10 +209,6 @@ namespace Coffee.UIExtensions
var g = graphic; var g = graphic;
if (g) 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(); g.SetMaterialDirty();
} }
_softMask = null; _softMask = null;
@ -248,15 +224,14 @@ namespace Coffee.UIExtensions
var g = graphic; var g = graphic;
if (g) if (g)
{ {
if (g.material == s_DefaultMaterial)
{
g.material = null;
}
g.SetMaterialDirty(); g.SetMaterialDirty();
} }
ReleaseMaterial(ref _maskMaterial); ReleaseMaterial(ref _maskMaterial);
_softMask = null; _softMask = null;
MaterialCache.Unregister(_effectMaterialHash);
_effectMaterialHash = k_InvalidHash;
} }
/// <summary> /// <summary>
@ -266,8 +241,6 @@ namespace Coffee.UIExtensions
{ {
if (mat) if (mat)
{ {
StencilMaterial.Remove(mat);
#if UNITY_EDITOR #if UNITY_EDITOR
if (!Application.isPlaying) 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() void ISerializationCallbackReceiver.OnBeforeSerialize()
{ {
} }
@ -296,6 +278,18 @@ namespace Coffee.UIExtensions
m_MaskInteraction = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6); m_MaskInteraction = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6);
} }
#pragma warning restore 0612 #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
} }
} }
} }

View File

@ -1,4 +1,4 @@
Shader "UI/Default-SoftMask" Shader "Hidden/UI/Default (SoftMaskable)"
{ {
Properties Properties
{ {

View File

@ -1,4 +1,4 @@
Shader "TextMeshPro/Distance Field (SoftMaskable)" { Shader "Hidden/TextMeshPro/Distance Field (SoftMaskable)" {
Properties { Properties {
_FaceTex ("Face Texture", 2D) = "white" {} _FaceTex ("Face Texture", 2D) = "white" {}

View File

@ -3,7 +3,7 @@
// - No Glow Option // - No Glow Option
// - Softness is applied on both side of the outline // - Softness is applied on both side of the outline
Shader "TextMeshPro/Mobile/Distance Field (SoftMaskable)" { Shader "Hidden/TextMeshPro/Mobile/Distance Field (SoftMaskable)" {
Properties { Properties {
_FaceColor ("Face Color", Color) = (1,1,1,1) _FaceColor ("Face Color", Color) = (1,1,1,1)

View File

@ -1,4 +1,4 @@
Shader "TextMeshPro/Sprite (SoftMaskable)" Shader "Hidden/TextMeshPro/Sprite (SoftMaskable)"
{ {
Properties Properties
{ {