From 76f69cf859097409d1c022188a617d18ebf11418 Mon Sep 17 00:00:00 2001 From: mob-sakai Date: Sun, 31 Dec 2023 20:16:52 +0900 Subject: [PATCH] feat: add 'SoftMask.softMaskingRange' option --- Packages/src/{Scripts => }/Editor.meta | 0 .../src/Editor/AlphaHitTestTargetEditor.cs | 19 + .../AlphaHitTestTargetEditor.cs.meta} | 2 +- .../Coffee.SoftMaskForUGUI.Editor.asmdef | 2 +- .../Coffee.SoftMaskForUGUI.Editor.asmdef.meta | 0 .../src/Editor/RectTransformFitterEditor.cs | 45 + .../RectTransformFitterEditor.cs.meta} | 2 +- Packages/src/Editor/SoftMaskEditor.cs | 270 ++++++ .../Editor/SoftMaskEditor.cs.meta | 0 Packages/src/{Scripts.meta => Runtime.meta} | 5 +- Packages/src/Runtime/AlphaHitTestTarget.cs | 37 + .../src/Runtime/AlphaHitTestTarget.cs.meta | 11 + Packages/src/Runtime/AssemblyInfo.cs | 4 + .../AssemblyInfo.cs.meta} | 2 +- .../src/Runtime/Coffee.SoftMaskForUGUI.asmdef | 13 + .../Coffee.SoftMaskForUGUI.asmdef.meta | 0 Packages/src/Runtime/RectTransformFitter.cs | 228 +++++ .../RectTransformFitter.cs.meta} | 2 +- Packages/src/Runtime/SoftMask.cs | 681 +++++++++++++++ .../src/{Scripts => Runtime}/SoftMask.cs.meta | 0 Packages/src/Runtime/SoftMaskable.cs | 270 ++++++ .../{Scripts => Runtime}/SoftMaskable.cs.meta | 0 Packages/src/Runtime/Utilities.meta | 8 + .../Utilities/CanvasViewChangeTrigger.cs | 81 ++ .../Utilities/CanvasViewChangeTrigger.cs.meta | 11 + Packages/src/Runtime/Utilities/MinMax01.cs | 140 ++++ .../src/Runtime/Utilities/MinMax01.cs.meta | 11 + .../src/Runtime/Utilities/SoftMaskUtils.cs | 254 ++++++ .../Runtime/Utilities/SoftMaskUtils.cs.meta | 11 + Packages/src/Runtime/Utilities/Utils.cs | 252 ++++++ Packages/src/Runtime/Utilities/Utils.cs.meta | 11 + .../src/Scripts/Coffee.SoftMaskForUGUI.asmdef | 6 - Packages/src/Scripts/Editor/EditorUtils.cs | 56 -- .../src/Scripts/Editor/ImportSampleMenu.cs | 82 -- Packages/src/Scripts/Editor/SoftMaskEditor.cs | 128 --- .../src/Scripts/Editor/SoftMaskableEditor.cs | 138 --- .../Scripts/Editor/SoftMaskableEditor.cs.meta | 12 - Packages/src/Scripts/GraphicConnector.cs | 106 --- Packages/src/Scripts/MaterialCache.cs | 82 -- Packages/src/Scripts/SoftMask.cs | 788 ------------------ Packages/src/Scripts/SoftMaskable.cs | 356 -------- 41 files changed, 2364 insertions(+), 1762 deletions(-) rename Packages/src/{Scripts => }/Editor.meta (100%) create mode 100644 Packages/src/Editor/AlphaHitTestTargetEditor.cs rename Packages/src/{Scripts/GraphicConnector.cs.meta => Editor/AlphaHitTestTargetEditor.cs.meta} (83%) rename Packages/src/{Scripts => }/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef (98%) rename Packages/src/{Scripts => }/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef.meta (100%) create mode 100644 Packages/src/Editor/RectTransformFitterEditor.cs rename Packages/src/{Scripts/Editor/ImportSampleMenu.cs.meta => Editor/RectTransformFitterEditor.cs.meta} (83%) create mode 100644 Packages/src/Editor/SoftMaskEditor.cs rename Packages/src/{Scripts => }/Editor/SoftMaskEditor.cs.meta (100%) rename Packages/src/{Scripts.meta => Runtime.meta} (58%) create mode 100644 Packages/src/Runtime/AlphaHitTestTarget.cs create mode 100644 Packages/src/Runtime/AlphaHitTestTarget.cs.meta create mode 100644 Packages/src/Runtime/AssemblyInfo.cs rename Packages/src/{Scripts/MaterialCache.cs.meta => Runtime/AssemblyInfo.cs.meta} (83%) create mode 100644 Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef rename Packages/src/{Scripts => Runtime}/Coffee.SoftMaskForUGUI.asmdef.meta (100%) create mode 100644 Packages/src/Runtime/RectTransformFitter.cs rename Packages/src/{Scripts/Editor/EditorUtils.cs.meta => Runtime/RectTransformFitter.cs.meta} (83%) create mode 100644 Packages/src/Runtime/SoftMask.cs rename Packages/src/{Scripts => Runtime}/SoftMask.cs.meta (100%) create mode 100755 Packages/src/Runtime/SoftMaskable.cs rename Packages/src/{Scripts => Runtime}/SoftMaskable.cs.meta (100%) create mode 100644 Packages/src/Runtime/Utilities.meta create mode 100755 Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs create mode 100644 Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs.meta create mode 100644 Packages/src/Runtime/Utilities/MinMax01.cs create mode 100644 Packages/src/Runtime/Utilities/MinMax01.cs.meta create mode 100644 Packages/src/Runtime/Utilities/SoftMaskUtils.cs create mode 100644 Packages/src/Runtime/Utilities/SoftMaskUtils.cs.meta create mode 100644 Packages/src/Runtime/Utilities/Utils.cs create mode 100644 Packages/src/Runtime/Utilities/Utils.cs.meta delete mode 100644 Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef delete mode 100644 Packages/src/Scripts/Editor/EditorUtils.cs delete mode 100644 Packages/src/Scripts/Editor/ImportSampleMenu.cs delete mode 100644 Packages/src/Scripts/Editor/SoftMaskEditor.cs delete mode 100644 Packages/src/Scripts/Editor/SoftMaskableEditor.cs delete mode 100644 Packages/src/Scripts/Editor/SoftMaskableEditor.cs.meta delete mode 100644 Packages/src/Scripts/GraphicConnector.cs delete mode 100644 Packages/src/Scripts/MaterialCache.cs delete mode 100644 Packages/src/Scripts/SoftMask.cs delete mode 100755 Packages/src/Scripts/SoftMaskable.cs diff --git a/Packages/src/Scripts/Editor.meta b/Packages/src/Editor.meta similarity index 100% rename from Packages/src/Scripts/Editor.meta rename to Packages/src/Editor.meta diff --git a/Packages/src/Editor/AlphaHitTestTargetEditor.cs b/Packages/src/Editor/AlphaHitTestTargetEditor.cs new file mode 100644 index 0000000..aa40b6f --- /dev/null +++ b/Packages/src/Editor/AlphaHitTestTargetEditor.cs @@ -0,0 +1,19 @@ +using UnityEditor; + +namespace Coffee.UISoftMask +{ + [CustomEditor(typeof(AlphaHitTestTarget), true)] + [CanEditMultipleObjects] + public class AlphaHitTestTargetEditor : Editor + { + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + + var current = target as AlphaHitTestTarget; + if (!current) return; + + Utils.DrawAlphaHitTestWarning(current.graphic); + } + } +} diff --git a/Packages/src/Scripts/GraphicConnector.cs.meta b/Packages/src/Editor/AlphaHitTestTargetEditor.cs.meta similarity index 83% rename from Packages/src/Scripts/GraphicConnector.cs.meta rename to Packages/src/Editor/AlphaHitTestTargetEditor.cs.meta index 403eda2..255094a 100644 --- a/Packages/src/Scripts/GraphicConnector.cs.meta +++ b/Packages/src/Editor/AlphaHitTestTargetEditor.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 0e702140c28f4425fac896f9394a31b1 +guid: 8b162084620cc4c3eabfc984c991a02e MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Scripts/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef b/Packages/src/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef similarity index 98% rename from Packages/src/Scripts/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef rename to Packages/src/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef index 00a5c0e..b92d4fa 100644 --- a/Packages/src/Scripts/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef +++ b/Packages/src/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef @@ -7,4 +7,4 @@ "Editor" ], "excludePlatforms": [] -} \ No newline at end of file +} diff --git a/Packages/src/Scripts/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef.meta b/Packages/src/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef.meta similarity index 100% rename from Packages/src/Scripts/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef.meta rename to Packages/src/Editor/Coffee.SoftMaskForUGUI.Editor.asmdef.meta diff --git a/Packages/src/Editor/RectTransformFitterEditor.cs b/Packages/src/Editor/RectTransformFitterEditor.cs new file mode 100644 index 0000000..eddf807 --- /dev/null +++ b/Packages/src/Editor/RectTransformFitterEditor.cs @@ -0,0 +1,45 @@ +using UnityEditor; +using UnityEngine; + +namespace Coffee.UISoftMask +{ + [CustomEditor(typeof(RectTransformFitter))] + [CanEditMultipleObjects] + public class RectTransformFitterEditor : Editor + { + private static readonly GUIContent s_TargetPropertiesContent = new GUIContent("Target Properties"); + private SerializedProperty _target; + private SerializedProperty _targetProperties; + + protected void OnEnable() + { + _target = serializedObject.FindProperty("m_Target"); + _targetProperties = serializedObject.FindProperty("m_TargetProperties"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // Target + EditorGUILayout.PropertyField(_target); + + // TargetProperties + EditorGUI.BeginChangeCheck(); + { + EditorGUI.showMixedValue = _targetProperties.hasMultipleDifferentValues; + var value = (RectTransformFitter.RectTransformProperties)_targetProperties.intValue; + value = (RectTransformFitter.RectTransformProperties)EditorGUILayout.EnumFlagsField( + s_TargetPropertiesContent, value); + EditorGUI.showMixedValue = false; + + if (EditorGUI.EndChangeCheck()) + { + _targetProperties.intValue = (int)value; + } + } + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Packages/src/Scripts/Editor/ImportSampleMenu.cs.meta b/Packages/src/Editor/RectTransformFitterEditor.cs.meta similarity index 83% rename from Packages/src/Scripts/Editor/ImportSampleMenu.cs.meta rename to Packages/src/Editor/RectTransformFitterEditor.cs.meta index b548db6..fcb33ae 100644 --- a/Packages/src/Scripts/Editor/ImportSampleMenu.cs.meta +++ b/Packages/src/Editor/RectTransformFitterEditor.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: aae59e0ee9bec4e71a10bd5a08f4fba4 +guid: db58fbfa4eaf04ff1bc7df04779208dd MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Editor/SoftMaskEditor.cs b/Packages/src/Editor/SoftMaskEditor.cs new file mode 100644 index 0000000..8fa1632 --- /dev/null +++ b/Packages/src/Editor/SoftMaskEditor.cs @@ -0,0 +1,270 @@ +using Coffee.UISoftMask.Internal; +using UnityEditor; +using UnityEditor.U2D; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.UI; + +namespace Coffee.UISoftMask +{ + [CustomEditor(typeof(SoftMask), true)] + [CanEditMultipleObjects] + public class SoftMaskEditor : Editor + { + private const string k_PrefsPreview = "k_PrefsPreview"; + private const int k_PreviewSize = 220; + private static readonly GUIContent s_ContentMaskingMode = new GUIContent(); + private SerializedProperty _alphaHitTest; + private SerializedProperty _antiAliasingThreshold; + private SerializedProperty _downSamplingRate; + private SerializedProperty _maskingMode; + private bool _preview; + private SerializedProperty _showMaskGraphic; + private SerializedProperty _softMaskingRange; + + protected void OnEnable() + { + _maskingMode = serializedObject.FindProperty("m_MaskingMode"); + _antiAliasingThreshold = serializedObject.FindProperty("m_AntiAliasingThreshold"); + _showMaskGraphic = serializedObject.FindProperty("m_ShowMaskGraphic"); + _downSamplingRate = serializedObject.FindProperty("m_DownSamplingRate"); + _alphaHitTest = serializedObject.FindProperty("m_AlphaHitTest"); + _softMaskingRange = serializedObject.FindProperty("m_SoftMaskingRange"); + _preview = EditorPrefs.GetBool(k_PrefsPreview, false); + + s_ContentMaskingMode.text = _maskingMode.displayName; + s_ContentMaskingMode.tooltip = _maskingMode.tooltip; + + SoftMask.onRenderSoftMaskBuffer += OnRenderSoftMaskBuffer; + } + + private void OnDisable() + { + SoftMask.onRenderSoftMaskBuffer -= OnRenderSoftMaskBuffer; + } + + public override void OnInspectorGUI() + { + var current = target as SoftMask; + if (current == null) return; + + if (!current.graphic || !current.graphic.IsActive()) + { + EditorGUILayout.HelpBox("Masking disabled due to Graphic component being disabled.", + MessageType.Warning); + } + + EditorGUILayout.PropertyField(_maskingMode); + OpenProjectSettings(current); + + if (_maskingMode.intValue == (int)SoftMask.MaskingMode.SoftMasking) + { + EditorGUILayout.PropertyField(_showMaskGraphic); + EditorGUILayout.PropertyField(_alphaHitTest); + EditorGUILayout.PropertyField(_downSamplingRate); + EditorGUILayout.PropertyField(_softMaskingRange); + + FixUiMaskIssue(current); // Fix 'UIMask' issue. + DrawSoftMaskBuffer(); // Preview soft mask buffer. + } + else + { + EditorGUILayout.PropertyField(_alphaHitTest); + EditorGUILayout.PropertyField(_antiAliasingThreshold); + } + + serializedObject.ApplyModifiedProperties(); + + // Draw alpha hit test warning + if (current.alphaHitTest) + { + Utils.DrawAlphaHitTestWarning(current.graphic); + } + } + + private void OnRenderSoftMaskBuffer(SoftMask softMask) + { + if (softMask != target) return; + + Repaint(); + } + + private static void OpenProjectSettings(SoftMask current) + { + if (current.maskingMode != SoftMask.MaskingMode.SoftMasking) return; + if (UISoftMaskProjectSettings.instance.m_SoftMaskEnabled) return; + + GUILayout.BeginHorizontal(); + EditorGUILayout.HelpBox("SoftMasking is disabled in the project settings.", MessageType.Warning); + if (GUILayout.Button("Open")) + { + SettingsService.OpenProjectSettings("Project/UI/Soft Mask"); + } + + GUILayout.EndHorizontal(); + } + + private static void FixUiMaskIssue(SoftMask current) + { + if (current.graphic is Image currentImage && IsMaskUI(currentImage.sprite)) + { + GUILayout.BeginHorizontal(); + EditorGUILayout.HelpBox( + "SoftMask does not recommend to use 'UIMask' sprite as a source image.\n" + + "(It contains only small alpha pixels.)\n" + + "Do you want to use 'UISprite' instead?", + MessageType.Warning); + if (GUILayout.Button("Fix")) + { + currentImage.sprite = AssetDatabase.GetBuiltinExtraResource("UI/Skin/UISprite.psd"); + } + + GUILayout.EndHorizontal(); + } + } + + private static bool IsMaskUI(Object obj) + { + return obj + && obj.name == "UIMask" + && AssetDatabase.GetAssetPath(obj) == "Resources/unity_builtin_extra"; + } + + private void DrawSoftMaskBuffer() + { + var current = target as SoftMask; + if (current == null || !current.SoftMaskingEnabled()) return; + + GUILayout.BeginVertical(EditorStyles.helpBox); + { + if (_preview != (_preview = EditorGUILayout.ToggleLeft("Preview Soft Mask Buffer", _preview))) + { + EditorPrefs.SetBool(k_PrefsPreview, _preview); + } + + if (_preview) + { + var tex = current._softMaskBuffer; + var depth = current.softMaskDepth; + var colorMask = GetColorMask(depth); + + if (tex) + { + GUILayout.Label($"{tex.name} (Depth: {depth} {colorMask})"); + var aspectRatio = (float)tex.width / tex.height; + EditorGUI.DrawPreviewTexture( + GUILayoutUtility.GetRect(k_PreviewSize, k_PreviewSize / aspectRatio), tex, null, + ScaleMode.ScaleToFit, aspectRatio, 0, colorMask); + } + } + } + GUILayout.EndVertical(); + } + + private static ColorWriteMask GetColorMask(int depth) + { + switch (depth) + { + case 0: return ColorWriteMask.Red; + case 1: return ColorWriteMask.Red | ColorWriteMask.Green; + case 2: return ColorWriteMask.Red | ColorWriteMask.Green | ColorWriteMask.Blue; + default: return ColorWriteMask.All; + } + } + + private static string GetWarningMessage(Graphic src) + { + if (!(src is Image || src is RawImage)) + { + return $"{src.GetType().Name} is not supported type for alpha hit test."; + } + + if (src is Image image && image) + { + var atlas = image.overrideSprite + ? image.overrideSprite.GetActiveAtlas() + : null; + if (atlas && atlas.GetPackingSettings().enableTightPacking) + { + return $"Tight packed sprite atlas '{atlas.name}' is not supported."; + } + } + + var tex = src.GetActualMainTexture(); + if (!tex) + { + return "No texture is assigned."; + } + + if (!(tex is Texture2D)) + { + return $"The texture '{tex.name}' is not Texture2D."; + } + + if (!tex.isReadable) + { + return $"The texture '{tex.name}' is not readable"; + } + + return ""; + } + + public static void DrawAlphaHitTestWarning(Graphic graphic) + { + if (!graphic) return; + + var warn = GetWarningMessage(graphic); + if (string.IsNullOrEmpty(warn)) return; + + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.HelpBox(warn, MessageType.Warning); + if (GUILayout.Button("Select")) + { + if (graphic is Image image && image) + { + var sprite = image.overrideSprite; + if (sprite) + { + Selection.activeObject = sprite.GetActiveAtlas(); + } + + if (!Selection.activeObject) + { + Selection.activeObject = image.GetActualMainTexture(); + } + } + else + { + Selection.activeObject = graphic.GetActualMainTexture(); + } + } + + EditorGUILayout.EndHorizontal(); + } + + //%%%% Context menu for editor %%%% + [MenuItem("CONTEXT/" + nameof(Mask) + "/Convert To " + nameof(SoftMask), true)] + private static bool _ConvertToSoftMask(MenuCommand command) + { + return command.context.CanConvertTo(); + } + + [MenuItem("CONTEXT/" + nameof(Mask) + "/Convert To " + nameof(SoftMask), false)] + private static void ConvertToSoftMask(MenuCommand command) + { + command.context.ConvertTo(); + } + + [MenuItem("CONTEXT/" + nameof(SoftMask) + "/Convert To " + nameof(Mask), true)] + private static bool _ConvertToMask(MenuCommand command) + { + return command.context.CanConvertTo(); + } + + [MenuItem("CONTEXT/" + nameof(SoftMask) + "/Convert To " + nameof(Mask), false)] + private static void ConvertToMask(MenuCommand command) + { + command.context.ConvertTo(); + } + } +} diff --git a/Packages/src/Scripts/Editor/SoftMaskEditor.cs.meta b/Packages/src/Editor/SoftMaskEditor.cs.meta similarity index 100% rename from Packages/src/Scripts/Editor/SoftMaskEditor.cs.meta rename to Packages/src/Editor/SoftMaskEditor.cs.meta diff --git a/Packages/src/Scripts.meta b/Packages/src/Runtime.meta similarity index 58% rename from Packages/src/Scripts.meta rename to Packages/src/Runtime.meta index a02db66..c3a6020 100644 --- a/Packages/src/Scripts.meta +++ b/Packages/src/Runtime.meta @@ -1,9 +1,8 @@ fileFormatVersion: 2 -guid: a9af7d92e858c4ae2b0c458a59b830d9 +guid: 160ef3852bd9446539d61560499ed2b0 folderAsset: yes -timeCreated: 1539755418 -licenseType: Pro DefaultImporter: + externalObjects: {} userData: assetBundleName: assetBundleVariant: diff --git a/Packages/src/Runtime/AlphaHitTestTarget.cs b/Packages/src/Runtime/AlphaHitTestTarget.cs new file mode 100644 index 0000000..2f573bb --- /dev/null +++ b/Packages/src/Runtime/AlphaHitTestTarget.cs @@ -0,0 +1,37 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Coffee.UISoftMask +{ + /// + /// Alpha-based hit testing on a UI element + /// + [ExecuteAlways] + [RequireComponent(typeof(Graphic))] + [DisallowMultipleComponent] + public class AlphaHitTestTarget : MonoBehaviour, ICanvasRaycastFilter + { + private Graphic _graphic; + + /// + /// Graphic component on this GameObject. + /// + public Graphic graphic => _graphic || TryGetComponent(out _graphic) ? _graphic : null; + + private void OnEnable() { } + + private void OnDisable() { } + + /// + /// This method is called during canvas ray-casting to determine if the hit point is valid or not. + /// + bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 sp, Camera eventCamera) + { + // Check if this component is active and enabled, if the Graphic component is not null, and if the Graphic is active. + if (!isActiveAndEnabled || !graphic || !graphic.IsActive()) return true; + + // Perform alpha-based hit testing. + return Utils.AlphaHitTestValid(graphic, sp, eventCamera, 0.01f); + } + } +} diff --git a/Packages/src/Runtime/AlphaHitTestTarget.cs.meta b/Packages/src/Runtime/AlphaHitTestTarget.cs.meta new file mode 100644 index 0000000..35a8e11 --- /dev/null +++ b/Packages/src/Runtime/AlphaHitTestTarget.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b2b9770479f64980a9c4bb5c0260ff9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: a3d38f5d5c2db4353961b5a4883798e7, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/AssemblyInfo.cs b/Packages/src/Runtime/AssemblyInfo.cs new file mode 100644 index 0000000..c120355 --- /dev/null +++ b/Packages/src/Runtime/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Coffee.SoftMaskForUGUI.Editor")] +[assembly: InternalsVisibleTo("Coffee.SoftMaskForUGUI.Demo")] diff --git a/Packages/src/Scripts/MaterialCache.cs.meta b/Packages/src/Runtime/AssemblyInfo.cs.meta similarity index 83% rename from Packages/src/Scripts/MaterialCache.cs.meta rename to Packages/src/Runtime/AssemblyInfo.cs.meta index d519c27..603dd22 100644 --- a/Packages/src/Scripts/MaterialCache.cs.meta +++ b/Packages/src/Runtime/AssemblyInfo.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: be6c8de8d4ec241fdbfad99aca2497d8 +guid: c8cb049b181614ec990bd65c0173d877 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef b/Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef new file mode 100644 index 0000000..be1ef98 --- /dev/null +++ b/Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef @@ -0,0 +1,13 @@ +{ + "name": "Coffee.SoftMaskForUGUI", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "versionDefines": [ + { + "name": "com.unity.modules.vr", + "expression": "1.0.0", + "define": "UNITY_MODULE_VR" + } + ] +} diff --git a/Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef.meta b/Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef.meta similarity index 100% rename from Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef.meta rename to Packages/src/Runtime/Coffee.SoftMaskForUGUI.asmdef.meta diff --git a/Packages/src/Runtime/RectTransformFitter.cs b/Packages/src/Runtime/RectTransformFitter.cs new file mode 100644 index 0000000..417da76 --- /dev/null +++ b/Packages/src/Runtime/RectTransformFitter.cs @@ -0,0 +1,228 @@ +using System; +using Coffee.UISoftMask.Internal; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.UI; + +namespace Coffee.UISoftMask +{ + /// + /// Fits the RectTransform to another RectTransform. + /// The target RectTransform must not be a child of this RectTransform. + /// + [ExecuteAlways] + [RequireComponent(typeof(RectTransform))] + [DisallowMultipleComponent] + public sealed class RectTransformFitter : MonoBehaviour, ILayoutElement, ILayoutIgnorer + { + [Flags] + public enum RectTransformProperties + { + PositionX = DrivenTransformProperties.AnchoredPositionX, + PositionY = DrivenTransformProperties.AnchoredPositionY, + PositionZ = DrivenTransformProperties.AnchoredPositionZ, + Position2D = PositionY | PositionX, + Position = PositionY | PositionX | PositionZ, + Rotation = DrivenTransformProperties.Rotation, + ScaleX = DrivenTransformProperties.ScaleX, + ScaleY = DrivenTransformProperties.ScaleY, + ScaleZ = DrivenTransformProperties.ScaleZ, + Scale = ScaleZ | ScaleY | ScaleX, + SizeDeltaX = DrivenTransformProperties.SizeDeltaX, + SizeDeltaY = DrivenTransformProperties.SizeDeltaY, + SizeDelta = SizeDeltaY | SizeDeltaX + } + + + [Tooltip("Target RectTransform to fit.")] [SerializeField] + private RectTransform m_Target; + + [Tooltip("Target RectTransform properties.")] [SerializeField] + private RectTransformProperties m_TargetProperties = RectTransformProperties.Position + | RectTransformProperties.Rotation + | RectTransformProperties.Scale + | RectTransformProperties.SizeDelta; + + private Action _fit; + + private RectTransform _rectTransform; + private DrivenRectTransformTracker _tracker; + + /// + /// Target RectTransform to fit. + /// + public RectTransform target + { + get => m_Target; + set => m_Target = value; + } + + /// + /// Target RectTransform properties. + /// + public RectTransformProperties targetProperties + { + get => m_TargetProperties; + set + { + if (m_TargetProperties == value) return; + m_TargetProperties = value; + OnValidate(); + } + } + + /// + /// Called when the component is enabled. + /// + private void OnEnable() + { + _rectTransform = GetComponent(); + UIExtraCallbacks.onBeforeCanvasRebuild += _fit ??= Fit; + OnValidate(); + } + + /// + /// Called when the component is disabled. + /// + private void OnDisable() + { + UIExtraCallbacks.onBeforeCanvasRebuild -= _fit ??= Fit; + OnValidate(); + } + + /// + /// Called when the component is destroyed. + /// + private void OnDestroy() + { + _rectTransform = null; + _fit = null; + } + + private void OnValidate() + { + _tracker.Clear(); + if (!isActiveAndEnabled) return; + + var trackValue = (DrivenTransformProperties)m_TargetProperties; + if (0 < (m_TargetProperties & (RectTransformProperties.SizeDelta | RectTransformProperties.Position))) + { + trackValue |= DrivenTransformProperties.Pivot | DrivenTransformProperties.Anchors; + } + + _tracker.Add(this, _rectTransform, trackValue); + } + + void ILayoutElement.CalculateLayoutInputHorizontal() + { + } + + void ILayoutElement.CalculateLayoutInputVertical() + { + } + + float ILayoutElement.minWidth => 0; + + float ILayoutElement.preferredWidth => 0; + + float ILayoutElement.flexibleWidth => 0; + + float ILayoutElement.minHeight => 0; + + float ILayoutElement.preferredHeight => 0; + + float ILayoutElement.flexibleHeight => 0; + + int ILayoutElement.layoutPriority => 0; + + bool ILayoutIgnorer.ignoreLayout => true; + + private void Fit() + { + // TODO; Child warning. + if (!m_Target || !_rectTransform || m_Target.IsChildOf(_rectTransform)) return; + + Profiler.BeginSample("(SM4UI)[RectTransformFitter] Fit"); + + // Position + if (0 < (m_TargetProperties & RectTransformProperties.Position)) + { + var targetPosition = m_Target.position; + var position = _rectTransform.position; + if (0 < (m_TargetProperties & RectTransformProperties.PositionX)) + { + position.x = targetPosition.x; + } + + if (0 < (m_TargetProperties & RectTransformProperties.PositionY)) + { + position.y = targetPosition.y; + } + + if (0 < (m_TargetProperties & RectTransformProperties.PositionZ)) + { + position.z = targetPosition.z; + } + + _rectTransform.position = position; + } + + // Rotation + if (0 < (m_TargetProperties & RectTransformProperties.Rotation)) + { + _rectTransform.rotation = m_Target.rotation; + } + + // Scale + if (0 < (m_TargetProperties & RectTransformProperties.Scale)) + { + var parent = _rectTransform.parent; + var s1 = m_Target.lossyScale; + var s2 = parent ? parent.lossyScale : Vector3.one; + var localScale = _rectTransform.localScale; + if (0 < (m_TargetProperties & RectTransformProperties.ScaleX)) + { + localScale.x = Mathf.Approximately(s2.x, 0) ? 1 : s1.x / s2.x; + } + + if (0 < (m_TargetProperties & RectTransformProperties.ScaleY)) + { + localScale.y = Mathf.Approximately(s2.y, 0) ? 1 : s1.y / s2.y; + } + + if (0 < (m_TargetProperties & RectTransformProperties.ScaleZ)) + { + localScale.z = Mathf.Approximately(s2.z, 0) ? 1 : s1.z / s2.z; + } + + _rectTransform.localScale = localScale; + } + + // SizeDelta + if (0 < (m_TargetProperties & RectTransformProperties.SizeDelta)) + { + var targetSize = m_Target.rect.size; + var sizeDelta = _rectTransform.sizeDelta; + if (0 < (m_TargetProperties & RectTransformProperties.SizeDeltaX)) + { + sizeDelta.x = targetSize.x; + } + + if (0 < (m_TargetProperties & RectTransformProperties.SizeDeltaY)) + { + sizeDelta.y = targetSize.y; + } + + _rectTransform.sizeDelta = sizeDelta; + } + + // Pivot & Anchor + if (0 < (m_TargetProperties & (RectTransformProperties.SizeDelta | RectTransformProperties.Position))) + { + _rectTransform.pivot = _rectTransform.anchorMax = _rectTransform.anchorMin = new Vector2(0.5f, 0.5f); + } + + Profiler.EndSample(); + } + } +} diff --git a/Packages/src/Scripts/Editor/EditorUtils.cs.meta b/Packages/src/Runtime/RectTransformFitter.cs.meta similarity index 83% rename from Packages/src/Scripts/Editor/EditorUtils.cs.meta rename to Packages/src/Runtime/RectTransformFitter.cs.meta index e16b794..8221746 100644 --- a/Packages/src/Scripts/Editor/EditorUtils.cs.meta +++ b/Packages/src/Runtime/RectTransformFitter.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 8829eb4004797463480fafa9a1112833 +guid: 994b48e5566c74a5a99adaf44292c616 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Runtime/SoftMask.cs b/Packages/src/Runtime/SoftMask.cs new file mode 100644 index 0000000..7b3cb2b --- /dev/null +++ b/Packages/src/Runtime/SoftMask.cs @@ -0,0 +1,681 @@ +using System; +using System.Collections.Generic; +using Coffee.UISoftMask.Internal; +using UnityEditor; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.Profiling; +using UnityEngine.Rendering; +using UnityEngine.UI; + +namespace Coffee.UISoftMask +{ + /// + /// SoftMask. + /// + [DisallowMultipleComponent] + [ExecuteAlways] + public class SoftMask : Mask, IMeshModifier, IMaskable, IMaskingShapeContainerOwner + { + /// + /// Down sampling rate. + /// + public enum DownSamplingRate + { + None = 0, + x1 = 1, + x2 = 2, + x4 = 4, + x8 = 8 + } + + public static Action onRenderSoftMaskBuffer = null; + private static readonly Camera.MonoOrStereoscopicEye[] s_MonoEyes = { Camera.MonoOrStereoscopicEye.Mono }; + + private static readonly Camera.MonoOrStereoscopicEye[] s_StereoEyes = + { Camera.MonoOrStereoscopicEye.Left, Camera.MonoOrStereoscopicEye.Right }; + + [Tooltip("Enable alpha hit test.")] + [SerializeField] + private bool m_AlphaHitTest; + + [Tooltip("The threshold for soft masking.")] + [SerializeField] + private MinMax01 m_SoftMaskingRange = new MinMax01(0, 1f); + + [Tooltip("The down sampling rate for soft mask buffer.")] + [SerializeField] + private DownSamplingRate m_DownSamplingRate = DownSamplingRate.x1; + + [SerializeField] [Obsolete] + private float m_Softness = -1; + + private CommandBuffer _cb; + + private List _children = ListPool.Rent(); + private bool _hasSoftMaskBufferDrawn; + private Mesh _mesh; + private MaterialPropertyBlock _mpb; + private Action _onBeforeCanvasRebuild; + private SoftMask _parent; + private Matrix4x4 _prevTransformMatrix; + private Action _renderSoftMaskBuffer; + private Canvas _rootCanvas; + private Action _setDirtyAndNotify; + private Action _setSoftMaskDirty; + private UnityAction _setSoftMaskDirty2; + private MaskingShapeContainer _shapeContainer; + internal RenderTexture _softMaskBuffer; + private UnityAction _updateParentSoftMask; + private CanvasViewChangeTrigger _viewChangeTrigger; + + public DownSamplingRate downSamplingRate + { + get => m_DownSamplingRate; + set + { + if (m_DownSamplingRate == value) return; + m_DownSamplingRate = value; + SetSoftMaskDirty(); + } + } + + /// + /// Enable alpha hit test. + /// + public bool alphaHitTest + { + get => m_AlphaHitTest; + set => m_AlphaHitTest = value; + } + + /// + /// The soft mask depth. + /// + public int softMaskDepth + { + get + { + var depth = -1; + for (var current = this; current; current = current._parent) + { + if (current.SoftMaskingEnabled()) + { + depth++; + } + } + + return depth; + } + } + + /// + /// The value used by the soft mask to select the area of influence defined over the soft mask's graphic. + /// + [Obsolete] + public float softness + { + get => m_Softness; + set => m_Softness = value; + } + + public bool hasSoftMaskBuffer => _softMaskBuffer; + + /// + /// The soft mask buffer. + /// + public RenderTexture softMaskBuffer + { + get + { + if (SoftMaskingEnabled()) + { + var id = GetInstanceID(); + var size = RenderTextureRepository.GetScreenSize(); + var rate = (int)downSamplingRate; + return RenderTextureRepository.Get(id, size, rate, ref _softMaskBuffer, false); + } + + RenderTextureRepository.Release(ref _softMaskBuffer); + return null; + } + } + + /// + /// The threshold for soft masking. + /// + public MinMax01 softMaskingRange + { + get => m_SoftMaskingRange; + set + { + if (m_SoftMaskingRange.Approximately(value)) return; + + m_SoftMaskingRange = value; + SetSoftMaskDirty(); + } + } + + /// + /// Clear color for the soft mask buffer. + /// + public Color clearColor + { + get; + set; + } + + public bool isDirty + { + get; + private set; + } + + /// + /// Called when the component is enabled. + /// + protected override void OnEnable() + { + UIExtraCallbacks.onBeforeCanvasRebuild += _onBeforeCanvasRebuild ??= OnBeforeCanvasRebuild; + UIExtraCallbacks.onAfterCanvasRebuild += _renderSoftMaskBuffer ??= RenderSoftMaskBuffer; + SoftMaskUtils.onChangeBufferSize += _setDirtyAndNotify ??= SetDirtyAndNotify; + + if (graphic) + { + graphic.RegisterDirtyMaterialCallback(_updateParentSoftMask ??= UpdateParentSoftMask); + graphic.RegisterDirtyVerticesCallback(_setSoftMaskDirty2 ??= SetSoftMaskDirty); + graphic.SetVerticesDirty(); + } + + AddSoftMaskableOnChildren(); + OnCanvasHierarchyChanged(); + _shapeContainer = GetComponent(); + + base.OnEnable(); + } + + /// + /// Called when the component is disabled. + /// + protected override void OnDisable() + { + UIExtraCallbacks.onBeforeCanvasRebuild -= _onBeforeCanvasRebuild ??= OnBeforeCanvasRebuild; + UIExtraCallbacks.onAfterCanvasRebuild -= _renderSoftMaskBuffer ??= RenderSoftMaskBuffer; + SoftMaskUtils.onChangeBufferSize -= _setDirtyAndNotify ??= SetDirtyAndNotify; + + if (graphic) + { + graphic.UnregisterDirtyMaterialCallback(_updateParentSoftMask ??= UpdateParentSoftMask); + graphic.UnregisterDirtyVerticesCallback(_setSoftMaskDirty2 ??= SetSoftMaskDirty); + graphic.SetVerticesDirty(); + } + + UpdateParentSoftMask(null); + _children.Clear(); + + SoftMaskUtils.meshPool.Return(ref _mesh); + SoftMaskUtils.materialPropertyBlockPool.Return(ref _mpb); + SoftMaskUtils.commandBufferPool.Return(ref _cb); + RenderTextureRepository.Release(ref _softMaskBuffer); + + UpdateCanvasViewChangeTrigger(null); + _rootCanvas = null; + _shapeContainer = null; + + UpdateAntiAlias(); + + base.OnDisable(); + } + + protected override void OnDestroy() + { + ListPool.Return(ref _children); + _onBeforeCanvasRebuild = null; + _setDirtyAndNotify = null; + _renderSoftMaskBuffer = null; + _setSoftMaskDirty = null; + _setSoftMaskDirty2 = null; + _updateParentSoftMask = null; + } + + /// + /// Called when the state of the parent Canvas is changed. + /// + protected override void OnCanvasHierarchyChanged() + { + if (!isActiveAndEnabled) return; + _rootCanvas = this.GetRootComponent(); + UpdateCanvasViewChangeTrigger(null); + } + + /// + /// Call from unity if animation properties have changed. + /// + protected override void OnDidApplyAnimationProperties() + { + SetSoftMaskDirty(); + } + + /// + /// This callback is called if an associated RectTransform has its dimensions changed. The call is also made to all child + /// rect transforms, even if the child transform itself doesn't change - as it could have, depending on its anchoring. + /// + protected override void OnRectTransformDimensionsChange() + { + SetSoftMaskDirty(); + } + + protected void OnTransformChildrenChanged() + { + AddSoftMaskableOnChildren(); + } + + protected override void OnTransformParentChanged() + { + UpdateParentSoftMask(); + UpdateCanvasViewChangeTrigger(CanvasViewChangeTrigger.Find(transform)); + } + +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + AddSoftMaskableOnChildren(); + SetDirtyAndNotify(); + UpdateAntiAlias(); + + if (graphic) + { + graphic.SetVerticesDirty(); + } + } +#endif + + void IMaskable.RecalculateMasking() + { + SetSoftMaskDirty(); + if (!SoftMaskingEnabled() && _softMaskBuffer) + { + RenderTextureRepository.Release(ref _softMaskBuffer); + } + } + + void IMaskingShapeContainerOwner.Register(MaskingShapeContainer container) + { + _shapeContainer = container; + } + + void IMeshModifier.ModifyMesh(Mesh mesh) + { + } + + void IMeshModifier.ModifyMesh(VertexHelper verts) + { + if (!SoftMaskingEnabled()) + { + SoftMaskUtils.meshPool.Return(ref _mesh); + return; + } + + Profiler.BeginSample("(SM4UI)[SoftMask] ModifyMesh"); + if (!_mesh) + { + _mesh = SoftMaskUtils.meshPool.Rent(); + } + + _mesh.Clear(false); + verts.FillMesh(_mesh); + _mesh.RecalculateBounds(); + + Profiler.EndSample(); + + Logging.Log(this, " >>>> Graphic mesh is modified."); + } + + private void AddSoftMaskableOnChildren() + { + if (!isActiveAndEnabled || !SoftMaskingEnabled()) return; + + this.AddComponentOnChildren(HideFlags.DontSave | HideFlags.NotEditable, true); + } + + private void OnBeforeCanvasRebuild() + { + switch (maskingMode) + { + // SoftMasking mode: If transform or view has changed, set dirty flag. + case MaskingMode.SoftMasking: + { + if (transform.HasChanged(ref _prevTransformMatrix, UISoftMaskProjectSettings.sensitivity)) + { + SetSoftMaskDirty(); + } + + if (!_viewChangeTrigger && _rootCanvas && _rootCanvas.renderMode == RenderMode.WorldSpace) + { + UpdateCanvasViewChangeTrigger(CanvasViewChangeTrigger.Find(transform)); + SetSoftMaskDirty(); + } + + break; + } + // AntiAliasing mode: Update anti-aliasing for graphic. + case MaskingMode.AntiAliasing: + { + if (!this || !graphic) return; + Utils.UpdateAntiAlias(graphic, isActiveAndEnabled, antiAliasingThreshold); + break; + } + } + } + + private void UpdateCanvasViewChangeTrigger(CanvasViewChangeTrigger trigger) + { + if (_viewChangeTrigger != trigger) + { + Logging.Log(this, $"UpdateCanvasViewChangeTrigger: {_viewChangeTrigger} -> {trigger}"); + + if (_viewChangeTrigger) + { + _viewChangeTrigger.onViewChange -= _setSoftMaskDirty ??= SetSoftMaskDirty; + } + + if (trigger) + { + trigger.onViewChange += _setSoftMaskDirty ??= SetSoftMaskDirty; + } + } + + _viewChangeTrigger = trigger; + } + + public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) + { + if (FrameCache.TryGet(this, nameof(IsRaycastLocationValid), out bool valid)) + { + return valid; + } + + if (!isActiveAndEnabled) + { + FrameCache.Set(this, nameof(IsRaycastLocationValid), true); + return true; + } + + // Check parent + if (_parent && !_parent.IsRaycastLocationValid(sp, eventCamera)) + { + FrameCache.Set(this, nameof(IsRaycastLocationValid), false); + return false; + } + + Profiler.BeginSample("(SM4UI)[SoftMask] IsRaycastLocationValid > Base"); + valid = base.IsRaycastLocationValid(sp, eventCamera); + Profiler.EndSample(); + + if (!SoftMaskingEnabled()) + { + FrameCache.Set(this, nameof(IsRaycastLocationValid), valid); + return valid; + } + + if (valid && alphaHitTest) + { + Profiler.BeginSample("(SM4UI)[SoftMask] IsRaycastLocationValid > Alpha hit test"); + valid = Utils.AlphaHitTestValid(graphic, sp, eventCamera, 0.01f); + Profiler.EndSample(); + } + + if (_shapeContainer) + { + Profiler.BeginSample("(SM4UI)[SoftMask] IsRaycastLocationValid > Shapes"); + valid |= _shapeContainer.IsInside(sp, eventCamera, false, 0.5f); + Profiler.EndSample(); + } + + FrameCache.Set(this, nameof(IsRaycastLocationValid), valid); + return valid; + } + + public override Material GetModifiedMaterial(Material baseMaterial) + { + if (SoftMaskingEnabled()) + { + return showMaskGraphic ? baseMaterial : null; + } + + return base.GetModifiedMaterial(baseMaterial); + } + + private void SetDirtyAndNotify() + { + SetSoftMaskDirty(); + MaskUtilities.NotifyStencilStateChanged(this); + } + + public void SetSoftMaskDirty() + { + if (isDirty) return; + + Logging.LogIf(!isDirty, this, $"! SetSoftMaskDirty {GetInstanceID()}"); + isDirty = true; + for (var i = _children.Count - 1; i >= 0; i--) + { + if (_children[i]) + { + _children[i].SetSoftMaskDirty(); + } + else + { + _children.RemoveAt(i); + } + } + } + + public bool SoftMaskingEnabled() + { + return GetActualMaskingMode() == MaskingMode.SoftMasking && MaskEnabled(); + } + + public bool AntiAliasingEnabled() + { + return GetActualMaskingMode() == MaskingMode.AntiAliasing && MaskEnabled(); + } + + internal MaskingMode GetActualMaskingMode() + { + return maskingMode == MaskingMode.Normal + ? MaskingMode.Normal + : UISoftMaskProjectSettings.softMaskEnabled && maskingMode == MaskingMode.SoftMasking + ? MaskingMode.SoftMasking + : MaskingMode.AntiAliasing; + } + + private void UpdateParentSoftMask() + { + if (SoftMaskingEnabled()) + { + var stopAfter = MaskUtilities.FindRootSortOverrideCanvas(transform); + var parentSoftMask = + transform.GetComponentInParent(false, stopAfter, x => x.SoftMaskingEnabled()); + UpdateParentSoftMask(parentSoftMask); + } + else + { + UpdateParentSoftMask(null); + } + } + + private void UpdateParentSoftMask(SoftMask newParent) + { + if (_parent && _parent._children.Contains(this)) + { + _parent._children.Remove(this); + } + + if (newParent && !newParent._children.Contains(this)) + { + newParent._children.Add(this); + } + + if (_parent != newParent) + { + SetSoftMaskDirty(); + } + + _parent = newParent; + } + + private void UpdateAntiAlias() + { + var useAA = isActiveAndEnabled && maskingMode == MaskingMode.AntiAliasing; + Utils.UpdateAntiAlias(graphic, useAA, antiAliasingThreshold); + } + + private bool IsInScreen() + { + if (graphic && graphic.IsInScreen()) return true; + if (_shapeContainer && _shapeContainer.IsInScreen()) return true; + + return false; + } + + private void RenderSoftMaskBuffer() + { + if (!SoftMaskingEnabled()) return; + + if (FrameCache.TryGet(this, nameof(RenderSoftMaskBuffer), out bool _)) return; + FrameCache.Set(this, nameof(RenderSoftMaskBuffer), true); + + if (!isDirty) return; + isDirty = false; + + if (_parent) + { + _parent.RenderSoftMaskBuffer(); + } + + var depth = softMaskDepth; + if (depth < 0 || 4 <= depth) return; + + if (_cb == null) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Rent cb"); + _cb = SoftMaskUtils.commandBufferPool.Rent(); + _cb.name = "[SoftMask] SoftMaskBuffer"; + Profiler.EndSample(); + } + + if (_mpb == null) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Rent mpb"); + _mpb = SoftMaskUtils.materialPropertyBlockPool.Rent(); + _mpb.Clear(); + Profiler.EndSample(); + } + + if (!IsInScreen()) + { + if (_hasSoftMaskBufferDrawn) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Clear"); + + _cb.Clear(); + _cb.SetRenderTarget(softMaskBuffer); + if (softMaskDepth == 0) + { + _cb.ClearRenderTarget(true, true, clearColor); + } + + Graphics.ExecuteCommandBuffer(_cb); + Profiler.EndSample(); + } + + _hasSoftMaskBufferDrawn = false; + return; + } + + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Init command buffer"); + _cb.Clear(); + _cb.SetRenderTarget(softMaskBuffer); + if (softMaskDepth == 0) + { + _cb.ClearRenderTarget(true, true, clearColor); + } + + Profiler.EndSample(); + + var eyes = graphic.canvas.IsStereoCanvas() ? s_StereoEyes : s_MonoEyes; + for (var i = 0; i < eyes.Length; i++) + { + RenderSoftMaskBuffer(eyes[i]); + } + + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Execute command buffer"); + Graphics.ExecuteCommandBuffer(_cb); + _hasSoftMaskBufferDrawn = true; + Logging.Log(this, $" >>>> SoftMaskBuffer '{softMaskBuffer.name}' will render."); + Profiler.EndSample(); + } + +#if UNITY_EDITOR + if (!Application.isPlaying) + { + EditorApplication.QueuePlayerLoopUpdate(); + } +#endif + + onRenderSoftMaskBuffer?.Invoke(this); + } + + private void RenderSoftMaskBuffer(Camera.MonoOrStereoscopicEye eye) + { + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > SetVpMatricesCommandBuffer"); + graphic.canvas.rootCanvas.GetViewProjectionMatrix(eye, out var viewMatrix, out var projectionMatrix); + _cb.SetViewProjectionMatrices(viewMatrix, projectionMatrix); + Profiler.EndSample(); + + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > ApplyMaterialPropertyBlock"); + SoftMaskUtils.ApplyMaterialPropertyBlock(_mpb, softMaskDepth, graphic.mainTexture, softMaskingRange); + Profiler.EndSample(); + } + + if (eye != Camera.MonoOrStereoscopicEye.Right && _parent) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Copy texture from parent"); + if (_parent.softMaskBuffer) + { + _cb.CopyTexture(_parent.softMaskBuffer, softMaskBuffer); + } + + Profiler.EndSample(); + } + + if (eye != Camera.MonoOrStereoscopicEye.Mono) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Set viewport"); + var w = softMaskBuffer.width * 0.5f; + var h = softMaskBuffer.height; + _cb.SetViewport(new Rect(w * (int)eye, 0f, w, h)); + Profiler.EndSample(); + } + + if (_mesh) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Draw mesh"); + var softMaterial = SoftMaskUtils.GetSoftMaskingMaterial(MaskingShape.MaskingMethod.Additive); + _cb.DrawMesh(_mesh, transform.localToWorldMatrix, softMaterial, 0, 0, _mpb); + Profiler.EndSample(); + } + + if (_shapeContainer) + { + Profiler.BeginSample("(SM4UI)[SoftMask] RenderSoftMaskBuffer > Draw shapes"); + _shapeContainer.DrawSoftMaskBuffer(_cb, softMaskDepth); + Profiler.EndSample(); + } + } + } +} diff --git a/Packages/src/Scripts/SoftMask.cs.meta b/Packages/src/Runtime/SoftMask.cs.meta similarity index 100% rename from Packages/src/Scripts/SoftMask.cs.meta rename to Packages/src/Runtime/SoftMask.cs.meta diff --git a/Packages/src/Runtime/SoftMaskable.cs b/Packages/src/Runtime/SoftMaskable.cs new file mode 100755 index 0000000..486a844 --- /dev/null +++ b/Packages/src/Runtime/SoftMaskable.cs @@ -0,0 +1,270 @@ +using System; +using Coffee.UISoftMask.Internal; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.UI; + +namespace Coffee.UISoftMask +{ + [ExecuteAlways] + public class SoftMaskable : MonoBehaviour, IMaterialModifier + { + private Action _checkGraphic; + private MaskableGraphic _graphic; + private Material _maskableMaterial; + private Action _setMaterialDirtyIfNeeded; + private SoftMask _softMask; + private int _softMaskDepth; + private int _stencilBits; + +#if UNITY_EDITOR + private Action _updateSceneViewMatrix; +#endif + + private bool isTerminal => _graphic is TerminalMaskingShape; + + private void OnEnable() + { + this.AddComponentOnChildren(HideFlags.DontSave | HideFlags.NotEditable, false); + + hideFlags = HideFlags.DontSave | HideFlags.NotEditable; + SoftMaskUtils.onChangeBufferSize += _setMaterialDirtyIfNeeded ??= SetMaterialDirtyIfNeeded; + if (TryGetComponent(out _graphic)) + { + _graphic.SetMaterialDirty(); + } + else + { + UIExtraCallbacks.onBeforeCanvasRebuild += _checkGraphic ??= CheckGraphic; + } + +#if UNITY_EDITOR + UIExtraCallbacks.onAfterCanvasRebuild += _updateSceneViewMatrix ??= UpdateSceneViewMatrix; +#endif + } + + private void OnDisable() + { + SoftMaskUtils.onChangeBufferSize -= _setMaterialDirtyIfNeeded ??= SetMaterialDirtyIfNeeded; + UIExtraCallbacks.onBeforeCanvasRebuild -= _checkGraphic ??= CheckGraphic; + if (_graphic) + { + _graphic.SetMaterialDirty(); + } + + _graphic = null; + _softMask = null; + MaterialRepository.Release(ref _maskableMaterial); + +#if UNITY_EDITOR + UIExtraCallbacks.onAfterCanvasRebuild -= _updateSceneViewMatrix ??= UpdateSceneViewMatrix; +#endif + } + + private void OnDestroy() + { + _graphic = null; + _maskableMaterial = null; + _softMask = null; + _setMaterialDirtyIfNeeded = null; + _checkGraphic = null; + +#if UNITY_EDITOR + _updateSceneViewMatrix = null; +#endif + } + + private void OnTransformChildrenChanged() + { + this.AddComponentOnChildren(HideFlags.DontSave | HideFlags.NotEditable, false); + } + + Material IMaterialModifier.GetModifiedMaterial(Material baseMaterial) + { +#if UNITY_EDITOR + if (!UISoftMaskProjectSettings.softMaskEnabled) + { + MaterialRepository.Release(ref _maskableMaterial); + return baseMaterial; + } +#endif + + if (!isActiveAndEnabled || !_graphic || !_graphic.maskable || isTerminal || baseMaterial == null) + { + MaterialRepository.Release(ref _maskableMaterial); + return baseMaterial; + } + + _stencilBits = GetStencilBitsAndSoftMask(transform, out _softMask); + _softMaskDepth = _softMask ? _softMask.softMaskDepth : -1; + + if (!_softMask || _softMaskDepth < 0 || 4 <= _softMaskDepth) + { + MaterialRepository.Release(ref _maskableMaterial); + return baseMaterial; + } + + Profiler.BeginSample("(SM4UI)[SoftMaskable] GetModifiedMaterial"); + + var isStereo = Application.isPlaying && _graphic.canvas.IsStereoCanvas(); + var hash = new Hash128( + (uint)baseMaterial.GetInstanceID(), + (uint)_softMask.softMaskBuffer.GetInstanceID(), + (uint)_stencilBits + (isStereo ? 1 << 8 : 0u), + (uint)_softMaskDepth); + MaterialRepository.Get(hash, ref _maskableMaterial, + x => SoftMaskUtils.CreateSoftMaskable(x.baseMaterial, x.softMaskBuffer, x._softMaskDepth, + x._stencilBits, x.isStereo, UISoftMaskProjectSettings.fallbackBehavior), + (baseMaterial, _softMask.softMaskBuffer, _softMaskDepth, _stencilBits, isStereo)); + Profiler.EndSample(); + + return _maskableMaterial; + } + + private void CheckGraphic() + { + if (_graphic || !TryGetComponent(out _graphic)) return; + + UIExtraCallbacks.onBeforeCanvasRebuild -= _checkGraphic ??= CheckGraphic; + + gameObject.AddComponent(); + Utils.DestroySafety(this); + } + + private static int GetStencilBitsAndSoftMask(Transform transform, out SoftMask nearestSoftMask) + { + Profiler.BeginSample("(SM4UI)[SoftMaskable] GetStencilBitsAndSoftMask"); + nearestSoftMask = null; + var stopAfter = MaskUtilities.FindRootSortOverrideCanvas(transform); + if (transform == stopAfter) + { + Profiler.EndSample(); + return 0; + } + + Mask nearestMask = null; + var depth = 0; + var stencilBits = 0; + var tr = transform.parent; + while (tr) + { + if (tr.TryGetComponent(out var mask) && mask.MaskEnabled()) + { + if (!nearestMask) + { + nearestMask = mask; + if (FrameCache.TryGet(nearestMask, nameof(GetStencilBitsAndSoftMask), out stencilBits)) + { + Profiler.EndSample(); + return stencilBits; + } + } + + stencilBits = 0 < depth++ ? stencilBits << 1 : 0; + if (mask is SoftMask softMask && softMask.SoftMaskingEnabled()) + { + if (!nearestSoftMask) + { + nearestSoftMask = softMask; + } + } + else + { + stencilBits++; + } + } + + if (tr == stopAfter) break; + tr = tr.parent; + } + + Profiler.EndSample(); + if (nearestMask) + { + FrameCache.Set(nearestMask, nameof(GetStencilBitsAndSoftMask), stencilBits); + } + + return stencilBits; + } + + private void SetMaterialDirtyIfNeeded() + { + if (_graphic && _maskableMaterial) + { + _graphic.SetMaterialDirty(); + } + } + +#if UNITY_EDITOR + private void UpdateSceneViewMatrix() + { + if (!_graphic || !_graphic.canvas || !_maskableMaterial) return; + if (FrameCache.TryGet(_maskableMaterial, nameof(UpdateSceneViewMatrix), out bool _)) + { + return; + } + + var canvas = _graphic.canvas.rootCanvas; + if (!FrameCache.TryGet(canvas, "GameVp", out Matrix4x4 gameVp) || + !FrameCache.TryGet(canvas, "GameTvp", out Matrix4x4 gameTvp)) + { + Profiler.BeginSample("(SM4UI)[SoftMaskable] (Editor) UpdateSceneViewMatrix > Calc GameVp & GameTvp"); + var rt = canvas.transform as RectTransform; + var cam = canvas.worldCamera; + if (canvas && canvas.renderMode != RenderMode.ScreenSpaceOverlay && cam) + { + var eye = canvas.IsStereoCanvas() + ? Camera.MonoOrStereoscopicEye.Left + : Camera.MonoOrStereoscopicEye.Mono; + canvas.GetViewProjectionMatrix(eye, out var vMatrix, out var pMatrix); + gameVp = gameTvp = pMatrix * vMatrix; + } + else if (rt != null) + { + var pos = rt.position; + var scale = rt.localScale.x; + var size = rt.sizeDelta; + gameVp = Matrix4x4.TRS(new Vector3(0, 0, 0.5f), Quaternion.identity, + new Vector3(2 / size.x, 2 / size.y, 0.0005f * scale)); + gameTvp = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, + new Vector3(1 / pos.x, 1 / pos.y, -2 / 2000f)) * Matrix4x4.Translate(-pos); + } + else + { + gameVp = gameTvp = Matrix4x4.identity; + } + + FrameCache.Set(canvas, "GameVp", gameVp); + FrameCache.Set(canvas, "GameTvp", gameTvp); + Profiler.EndSample(); + } + + // Set view and projection matrices. + Profiler.BeginSample("(SM4UI)[SoftMaskable] (Editor) UpdateSceneViewMatrix > Set matrices"); + _maskableMaterial.SetMatrix(SoftMaskUtils.s_GameVpId, gameVp); + _maskableMaterial.SetMatrix(SoftMaskUtils.s_GameTvpId, gameTvp); + Profiler.EndSample(); + + // Calc Right eye matrices. + if (canvas.IsStereoCanvas()) + { + if (!FrameCache.TryGet(canvas, "GameVp2", out gameVp)) + { + Profiler.BeginSample("(SM4UI)[SoftMaskable] (Editor) UpdateSceneViewMatrix > Calc GameVp2"); + var eye = Camera.MonoOrStereoscopicEye.Right; + canvas.GetViewProjectionMatrix(eye, out var vMatrix, out var pMatrix); + gameVp = pMatrix * vMatrix; + + FrameCache.Set(canvas, "GameVp2", gameVp); + Profiler.EndSample(); + } + + _maskableMaterial.SetMatrix(SoftMaskUtils.s_GameVp2Id, gameVp); + _maskableMaterial.SetMatrix(SoftMaskUtils.s_GameTvp2Id, gameVp); + } + + FrameCache.Set(_maskableMaterial, nameof(UpdateSceneViewMatrix), true); + } +#endif + } +} diff --git a/Packages/src/Scripts/SoftMaskable.cs.meta b/Packages/src/Runtime/SoftMaskable.cs.meta similarity index 100% rename from Packages/src/Scripts/SoftMaskable.cs.meta rename to Packages/src/Runtime/SoftMaskable.cs.meta diff --git a/Packages/src/Runtime/Utilities.meta b/Packages/src/Runtime/Utilities.meta new file mode 100644 index 0000000..963e606 --- /dev/null +++ b/Packages/src/Runtime/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e1f702abbd5e04d939f17a6c57ff932f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs b/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs new file mode 100755 index 0000000..c85c44c --- /dev/null +++ b/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs @@ -0,0 +1,81 @@ +using System; +using Coffee.UISoftMask.Internal; +using UnityEngine; + +namespace Coffee.UISoftMask +{ + /// + /// triggers an event when the view projection matrix of a Canvas in World Space render mode changes. + /// + [RequireComponent(typeof(Canvas))] + [ExecuteAlways] + [AddComponentMenu("")] + public class CanvasViewChangeTrigger : MonoBehaviour + { + private Canvas _canvas; + private Action _checkViewProjectionMatrix; + private int _lastCameraVpHash; + + /// + /// Called when the component is enabled. + /// + private void OnEnable() + { + hideFlags = HideFlags.DontSave | HideFlags.NotEditable; + TryGetComponent(out _canvas); + UIExtraCallbacks.onBeforeCanvasRebuild += _checkViewProjectionMatrix ??= CheckViewProjectionMatrix; + } + + /// + /// Called when the component is disabled. + /// + private void OnDisable() + { + UIExtraCallbacks.onBeforeCanvasRebuild -= _checkViewProjectionMatrix ??= CheckViewProjectionMatrix; + } + + /// + /// Called when the component is destroyed. + /// + private void OnDestroy() + { + _canvas = null; + onViewChange = null; + _checkViewProjectionMatrix = null; + } + + /// + /// Event that is triggered when the view projection matrix changes. + /// + public event Action onViewChange; + + private void CheckViewProjectionMatrix() + { + if (!_canvas || _canvas.renderMode != RenderMode.WorldSpace) return; + + // Get the view and projection matrix of the Canvas. + var prevHash = _lastCameraVpHash; + _canvas.GetViewProjectionMatrix(out var vpMatrix); + _lastCameraVpHash = vpMatrix.GetHashCode(); + + // The matrix has changed. + if (prevHash != _lastCameraVpHash) + { + Logging.Log(this, "ViewProjection changed."); + onViewChange?.Invoke(); + } + } + + /// + /// get or add a CanvasViewChangeTrigger component in the root Canvas. + /// + public static CanvasViewChangeTrigger Find(Transform transform) + { + // Find the root Canvas component. + var rootCanvas = transform.GetRootComponent(); + + // Get the CanvasViewChangeTrigger component if found, or add. + return rootCanvas ? rootCanvas.GetOrAddComponent() : null; + } + } +} diff --git a/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs.meta b/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs.meta new file mode 100644 index 0000000..fb07ba6 --- /dev/null +++ b/Packages/src/Runtime/Utilities/CanvasViewChangeTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2511cecf9478540ce9799b252fb467b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/Utilities/MinMax01.cs b/Packages/src/Runtime/Utilities/MinMax01.cs new file mode 100644 index 0000000..9ee8c71 --- /dev/null +++ b/Packages/src/Runtime/Utilities/MinMax01.cs @@ -0,0 +1,140 @@ +using System; +using UnityEditor; +using UnityEngine; + +namespace Coffee.UISoftMask +{ + [Serializable] + public struct MinMax01 + { + [SerializeField] + private float m_Min; + + [SerializeField] + private float m_Max; + + public MinMax01(float min, float max) + { + m_Min = Mathf.Clamp01(Mathf.Min(min, max)); + m_Max = Mathf.Clamp01(Mathf.Max(min, max)); + } + + public float min + { + get => m_Min; + set + { + m_Min = Mathf.Clamp01(value); + m_Max = Mathf.Max(value, m_Max); + } + } + + public float max + { + get => m_Max; + set + { + m_Max = Mathf.Clamp01(value); + m_Min = Mathf.Min(value, m_Min); + } + } + + public bool Approximately(MinMax01 other) + { + return Mathf.Approximately(m_Min, other.m_Min) && Mathf.Approximately(m_Max, other.m_Max); + } + } + +#if UNITY_EDITOR + [CustomPropertyDrawer(typeof(MinMax01))] + public class MinMaxRangeDrawer : PropertyDrawer + { + private const float k_NumWidth = 50; + private const float k_Space = 5; + private SerializedProperty _max; + private SerializedProperty _min; + + private static bool IsSingleLine(GUIContent label) + { + return EditorGUIUtility.wideMode || label == null || string.IsNullOrEmpty(label.text); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return IsSingleLine(label) ? 18 : 32; + } + + public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label) + { + EditorGUI.BeginProperty(position, label, prop); + + if (_min == null) + { + prop.Next(true); + _min = prop.Copy(); + prop.Next(true); + _max = prop.Copy(); + } + + if (IsSingleLine(label)) + { + position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); + } + else + { + EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); + var indent = (EditorGUI.indentLevel + 1) * 15f; + position = new Rect(position.x + indent, position.y + 18, position.width - indent, 18); + } + + var min = _min.floatValue; + var max = _max.floatValue; + + if (Draw(position, ref min, ref max)) + { + _min.floatValue = min; + _max.floatValue = max; + } + + EditorGUI.EndProperty(); + } + + public static bool Draw(Rect position, ref float minValue, ref float maxValue) + { + EditorGUI.BeginChangeCheck(); + + var rect = new Rect(position.x, position.y, k_NumWidth, position.height); + minValue = Mathf.Clamp(EditorGUI.FloatField(rect, minValue), 0, maxValue); + + rect.x += rect.width + k_Space; + rect.width = position.width - k_NumWidth * 2 - k_Space * 2; + EditorGUI.MinMaxSlider(rect, ref minValue, ref maxValue, 0, 1); + + rect.x += rect.width + k_Space; + rect.width = k_NumWidth; + maxValue = Mathf.Clamp(EditorGUI.FloatField(rect, maxValue), minValue, 1); + + return EditorGUI.EndChangeCheck(); + } + + public static bool DrawLayout(GUIContent label, ref float minValue, ref float maxValue) + { + Rect position; + if (IsSingleLine(label)) + { + position = EditorGUILayout.GetControlRect(true, 18f); + position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); + } + else + { + position = EditorGUILayout.GetControlRect(true, 36f); + EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); + var indent = (EditorGUI.indentLevel + 1) * 15f; + position = new Rect(position.x + indent, position.y + 18, position.width - indent, 18); + } + + return Draw(position, ref minValue, ref maxValue); + } + } +#endif +} diff --git a/Packages/src/Runtime/Utilities/MinMax01.cs.meta b/Packages/src/Runtime/Utilities/MinMax01.cs.meta new file mode 100644 index 0000000..fc4dc61 --- /dev/null +++ b/Packages/src/Runtime/Utilities/MinMax01.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a21ddb3eb3ca244b5b7a53accaa016d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs new file mode 100644 index 0000000..effd2a3 --- /dev/null +++ b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using Coffee.UISoftMask.Internal; +using UnityEditor; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.Rendering; +#if UNITY_MODULE_VR +using UnityEngine.XR; +#endif + + +namespace Coffee.UISoftMask +{ + /// + /// Utility class containing various functions and resources for soft masking. + /// + internal static class SoftMaskUtils + { + /// + /// Object pool for CommandBuffer instances. + /// + public static readonly ObjectPool commandBufferPool = + new ObjectPool( + () => new CommandBuffer(), + x => x != null, + x => x.Clear()); + + /// + /// Object pool for MaterialPropertyBlock instances. + /// + public static readonly ObjectPool materialPropertyBlockPool = + new ObjectPool( + () => new MaterialPropertyBlock(), + x => x != null, + x => x.Clear()); + + /// + /// Object pool for Mesh instances. + /// + public static readonly ObjectPool meshPool = new ObjectPool( + () => + { + var mesh = new Mesh + { + hideFlags = HideFlags.DontSave + }; + mesh.MarkDynamic(); + return mesh; + }, + mesh => mesh, + mesh => + { + if (mesh) + { + mesh.Clear(); + } + }); + + private static Material s_SoftMaskingMaterial; + private static Material s_SoftMaskingMaterialSub; + private static readonly int s_ColorMaskId = Shader.PropertyToID("_ColorMask"); + private static readonly int s_MainTexId = Shader.PropertyToID("_MainTex"); + private static readonly int s_ThresholdMinId = Shader.PropertyToID("_ThresholdMin"); + private static readonly int s_ThresholdMaxId = Shader.PropertyToID("_ThresholdMax"); + private static readonly int s_SoftMaskTexId = Shader.PropertyToID("_SoftMaskTex"); + private static readonly int s_SoftMaskColorId = Shader.PropertyToID("_SoftMaskColor"); + private static readonly int s_StencilReadMaskId = Shader.PropertyToID("_StencilReadMask"); + private static readonly int s_BlendOp = Shader.PropertyToID("_BlendOp"); + private static Vector2Int s_BufferSize; + private static int s_Count; + private static readonly FastAction s_OnChangeBufferSize = new FastAction(); + + private static readonly string[] s_SoftMaskableShaderNameFormats = + { + "{0}", + "Hidden/{0} (SoftMaskable)", + "{0} (SoftMaskable)" + }; + + private static readonly Dictionary s_SoftMaskableShaderNames = new Dictionary(); + + /// + /// Event that gets triggered when the buffer size changes. + /// + public static event Action onChangeBufferSize + { + add => s_OnChangeBufferSize.Add(value); + remove => s_OnChangeBufferSize.Remove(value); + } + +#if UNITY_EDITOR + [InitializeOnLoadMethod] +#else + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] +#endif + private static void InitializeOnLoadMethod() + { + UIExtraCallbacks.onBeforeCanvasRebuild += () => + { + var size = RenderTextureRepository.GetScreenSize(); + if (s_BufferSize == size) return; + s_BufferSize = size; + s_OnChangeBufferSize.Invoke(); + }; + } + + /// + /// Applies properties to a MaterialPropertyBlock for soft masking. + /// + public static void ApplyMaterialPropertyBlock(MaterialPropertyBlock mpb, int depth, Texture texture, + MinMax01 threshold) + { + Profiler.BeginSample("(SM4UI)[SoftMaskUtils] ApplyMaterialPropertyBlock"); + var colorMask = Vector4.zero; + colorMask[depth] = 1; + mpb.SetVector(s_ColorMaskId, colorMask); + mpb.SetTexture(s_MainTexId, texture); + mpb.SetFloat(s_ThresholdMinId, threshold.min); + mpb.SetFloat(s_ThresholdMaxId, threshold.max); + Profiler.EndSample(); + } + + /// + /// Gets the soft masking material based on the masking method. + /// + public static Material GetSoftMaskingMaterial(MaskingShape.MaskingMethod method) + { + return method == MaskingShape.MaskingMethod.Additive + ? GetSoftMaskingMaterial(ref s_SoftMaskingMaterial, BlendOp.Add) + : GetSoftMaskingMaterial(ref s_SoftMaskingMaterialSub, BlendOp.ReverseSubtract); + } + + /// + /// Gets or creates the soft masking material with a specific blend operation. + /// + private static Material GetSoftMaskingMaterial(ref Material mat, BlendOp op) + { + if (mat) return mat; + + mat = new Material(Shader.Find("Hidden/UI/SoftMask")) + { + hideFlags = HideFlags.DontSave + }; + mat.SetInt(s_BlendOp, (int)op); + return mat; + } + + public static Material CreateSoftMaskable( + Material baseMat, + Texture softMaskBuffer, + int softMaskDepth, + int stencilBits, + bool isStereo, + UISoftMaskProjectSettings.FallbackBehavior fallbackBehavior) + { + Profiler.BeginSample("(SM4UI)[SoftMaskableMaterial] Create > Create New Material"); + var mat = new Material(baseMat) + { + shader = GetSoftMaskableShader(baseMat.shader, fallbackBehavior), + hideFlags = HideFlags.HideAndDontSave + }; + + Profiler.EndSample(); + + Profiler.BeginSample("(SM4UI)[SoftMaskableMaterial] Create > Set Properties"); + mat.SetTexture(s_SoftMaskTexId, softMaskBuffer); + mat.SetInt(s_StencilReadMaskId, stencilBits); + mat.SetVector(s_SoftMaskColorId, new Vector4( + 0 <= softMaskDepth ? 1 : 0, + 1 <= softMaskDepth ? 1 : 0, + 2 <= softMaskDepth ? 1 : 0, + 3 <= softMaskDepth ? 1 : 0 + )); + + Profiler.EndSample(); + + Profiler.BeginSample("(SM4UI)[SoftMaskableMaterial] Create > Set Keywords"); + if (isStereo) + { + mat.EnableKeyword("UI_SOFT_MASKABLE_STEREO"); + } + +#if UNITY_EDITOR + mat.EnableKeyword("UI_SOFT_MASKABLE_EDITOR"); +#else + mat.EnableKeyword("UI_SOFT_MASKABLE"); +#endif + Profiler.EndSample(); + return mat; + } + + public static Shader GetSoftMaskableShader(Shader baseShader, + UISoftMaskProjectSettings.FallbackBehavior fallback) + { + Profiler.BeginSample("(SM4UI)[SoftMaskUtils] GetSoftMaskableShader > From cache"); + var id = baseShader.GetInstanceID(); + if (s_SoftMaskableShaderNames.TryGetValue(id, out var shaderName)) + { + var shader = Shader.Find(shaderName); + Profiler.EndSample(); + + return shader; + } + + Profiler.EndSample(); + + Profiler.BeginSample("(SM4UI)[SoftMaskableMaterial] GetSoftMaskableShader > Find soft maskable shader"); + shaderName = baseShader.name; + for (var i = 0; i < s_SoftMaskableShaderNameFormats.Length; i++) + { + var name = string.Format(s_SoftMaskableShaderNameFormats[i], shaderName); + if (!name.EndsWith(" (SoftMaskable)", StringComparison.Ordinal)) continue; + + var shader = Shader.Find(name); + if (!shader) continue; + + s_SoftMaskableShaderNames.Add(id, name); + Profiler.EndSample(); + return shader; + } + + Profiler.EndSample(); + + Profiler.BeginSample("(SM4UI)[SoftMaskableMaterial] GetSoftMaskableShader > Fallback"); + switch (fallback) + { + case UISoftMaskProjectSettings.FallbackBehavior.DefaultSoftMaskable: + { + s_SoftMaskableShaderNames.Add(id, "Hidden/UI/Default (SoftMaskable)"); + var shader = Shader.Find("Hidden/UI/Default (SoftMaskable)"); + Profiler.EndSample(); + return shader; + } + case UISoftMaskProjectSettings.FallbackBehavior.None: + { + s_SoftMaskableShaderNames.Add(id, shaderName); + Profiler.EndSample(); + return baseShader; + } + default: + Profiler.EndSample(); + throw new ArgumentOutOfRangeException(nameof(fallback), fallback, null); + } + } + +#if UNITY_EDITOR + internal static readonly int s_GameVpId = Shader.PropertyToID("_GameVP"); + internal static readonly int s_GameTvpId = Shader.PropertyToID("_GameTVP"); + internal static readonly int s_GameVp2Id = Shader.PropertyToID("_GameVP_2"); + internal static readonly int s_GameTvp2Id = Shader.PropertyToID("_GameTVP_2"); +#endif + } +} diff --git a/Packages/src/Runtime/Utilities/SoftMaskUtils.cs.meta b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs.meta new file mode 100644 index 0000000..43347d3 --- /dev/null +++ b/Packages/src/Runtime/Utilities/SoftMaskUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4bc04127b21d4777a30dba32a431cdb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/Utilities/Utils.cs b/Packages/src/Runtime/Utilities/Utils.cs new file mode 100644 index 0000000..9ff2ba6 --- /dev/null +++ b/Packages/src/Runtime/Utilities/Utils.cs @@ -0,0 +1,252 @@ +using Coffee.UISoftMask.Internal; +using UnityEditor; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.UI; +#if UNITY_EDITOR +using UnityEditor.U2D; +#endif + +namespace Coffee.UISoftMask +{ + internal static class Utils + { + /// + /// Update the smooth edge effect for the graphic. + /// + public static void UpdateAntiAlias(Graphic graphic, bool enabled, float threshold) + { + if (!graphic) return; + + var canvasRenderer = graphic.canvasRenderer; + var currentColor = canvasRenderer.GetColor(); + var targetAlpha = 1f; + if (enabled) + { + var currentAlpha = graphic.color.a * canvasRenderer.GetInheritedAlpha(); + if (0 < currentAlpha) + { + // Adjust the alpha value for a smooth edge based on the threshold. + threshold = Mathf.Clamp01(threshold); + targetAlpha = Mathf.Lerp(1f / 255, 0.1f, threshold) / currentAlpha; + } + } + + // Update the alpha value of the canvas renderer's color to achieve the smooth edge effect. + if (!Mathf.Approximately(currentColor.a, targetAlpha)) + { + currentColor.a = Mathf.Clamp01(targetAlpha); + canvasRenderer.SetColor(currentColor); + } + } + + /// + /// Find the nearest mask and stencil depth. + /// + public static int GetStencilDepthAndMask(Transform transform, bool includeSelf, out Mask nearestMask) + { + Profiler.BeginSample("(SM4UI)[Utils] GetStencilDepthAndMask"); + nearestMask = null; + var stopAfter = MaskUtilities.FindRootSortOverrideCanvas(transform); + var depth = 0; + if (transform == stopAfter) + { + Profiler.EndSample(); + return depth; + } + + var tr = includeSelf ? transform : transform.parent; + while (tr) + { + if (tr.TryGetComponent(out var mask) && mask.MaskEnabled() && mask.graphic && + mask.graphic.IsActive()) + { + if (!nearestMask) + { + nearestMask = mask; + if (FrameCache.TryGet(nearestMask, nameof(GetStencilDepthAndMask), out depth)) + { + Profiler.EndSample(); + return depth; + } + } + + ++depth; + + if (tr == stopAfter) + { + break; + } + } + + tr = tr.parent; + } + + Profiler.EndSample(); + + if (nearestMask) + { + FrameCache.Set(nearestMask, nameof(GetStencilDepthAndMask), depth); + } + + return depth; + } + + public static void DestroySafety(T obj) where T : Object + { + if (!obj) return; + +#if UNITY_EDITOR + if (!Application.isPlaying) + { + Object.DestroyImmediate(obj, false); + return; + } +#endif + Object.Destroy(obj); + } + + public static bool AlphaHitTestValid(Graphic src, Vector2 sp, Camera eventCamera, float threshold) + { + if (!src || !src.IsActive()) return false; + if (!(src is Image || src is RawImage)) return true; + + if (FrameCache.TryGet(src, nameof(AlphaHitTestValid), out bool valid)) + { + return valid; + } + + if (src is Image image) + { + valid = AlphaHitTestValid(image, sp, eventCamera, threshold); + } + else if (src is RawImage rawImage) + { + valid = AlphaHitTestValid(rawImage, sp, eventCamera, threshold); + } + + FrameCache.Set(src, nameof(AlphaHitTestValid), valid); + return valid; + } + + private static bool AlphaHitTestValid(Image src, Vector2 sp, Camera eventCamera, float threshold) + { + if (!src.overrideSprite || !src.overrideSprite.GetActualTexture().isReadable) return true; + + var origin = src.alphaHitTestMinimumThreshold; + if (0 < origin && origin <= 1) return true; + + Profiler.BeginSample("(SM4UI)[Utils] AlphaHitTestValid (Image)"); + src.alphaHitTestMinimumThreshold = threshold; + var valid = src.IsRaycastLocationValid(sp, eventCamera); + src.alphaHitTestMinimumThreshold = origin; + Profiler.EndSample(); + return valid; + } + + private static bool AlphaHitTestValid(RawImage src, Vector2 sp, Camera eventCamera, float threshold) + { + var texture = src.texture as Texture2D; + if (texture == null || !texture.isReadable) return true; + + Profiler.BeginSample("(SM4UI)[Utils] AlphaHitTestValid (RawImage)"); + + var rt = src.rectTransform; + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, sp, eventCamera, out var lp)) + { + Profiler.EndSample(); + return false; + } + + var rect = src.GetPixelAdjustedRect(); + var x = Mathf.Repeat((lp.x + rt.pivot.x * rect.width) / rect.width * src.uvRect.width + src.uvRect.x, 1); + var y = Mathf.Repeat((lp.y + rt.pivot.y * rect.height) / rect.height * src.uvRect.height + src.uvRect.y, 1); + + try + { + return threshold < texture.GetPixelBilinear(x, y).a; + } + catch + { + return true; + } + finally + { + Profiler.EndSample(); + } + } + +#if UNITY_EDITOR + private static string GetWarningMessage(Graphic src) + { + if (!(src is Image || src is RawImage)) + { + return $"{src.GetType().Name} is not supported type for alpha hit test."; + } + + if (src is Image image && image) + { + var atlas = image.overrideSprite + ? image.overrideSprite.GetActiveAtlas() + : null; + if (atlas && atlas.GetPackingSettings().enableTightPacking) + { + return $"Tight packed sprite atlas '{atlas.name}' is not supported."; + } + } + + var tex = src.GetActualMainTexture(); + if (!tex) + { + return "No texture is assigned."; + } + + if (!(tex is Texture2D)) + { + return $"The texture '{tex.name}' is not Texture2D."; + } + + if (!tex.isReadable) + { + return $"The texture '{tex.name}' is not readable"; + } + + return ""; + } + + public static void DrawAlphaHitTestWarning(Graphic graphic) + { + if (!graphic) return; + + var warn = GetWarningMessage(graphic); + if (string.IsNullOrEmpty(warn)) return; + + EditorGUILayout.BeginHorizontal(); + { + EditorGUILayout.HelpBox(warn, MessageType.Warning); + if (GUILayout.Button("Select")) + { + if (graphic is Image image && image) + { + var sprite = image.overrideSprite; + if (sprite) + { + Selection.activeObject = sprite.GetActiveAtlas(); + } + + if (!Selection.activeObject) + { + Selection.activeObject = image.GetActualMainTexture(); + } + } + else + { + Selection.activeObject = graphic.GetActualMainTexture(); + } + } + } + EditorGUILayout.EndHorizontal(); + } +#endif + } +} diff --git a/Packages/src/Runtime/Utilities/Utils.cs.meta b/Packages/src/Runtime/Utilities/Utils.cs.meta new file mode 100644 index 0000000..0cbd228 --- /dev/null +++ b/Packages/src/Runtime/Utilities/Utils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bb99855d1f23b4c8eb7c819655d14d88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef b/Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef deleted file mode 100644 index ccabcd1..0000000 --- a/Packages/src/Scripts/Coffee.SoftMaskForUGUI.asmdef +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Coffee.SoftMaskForUGUI", - "references": [], - "includePlatforms": [], - "excludePlatforms": [] -} \ No newline at end of file diff --git a/Packages/src/Scripts/Editor/EditorUtils.cs b/Packages/src/Scripts/Editor/EditorUtils.cs deleted file mode 100644 index 6a8c097..0000000 --- a/Packages/src/Scripts/Editor/EditorUtils.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if UNITY_2018_3_OR_NEWER -using UnityEditor.Experimental.SceneManagement; -#endif -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace Coffee.UISoftMask -{ - internal static class EditorUtils - { - internal static void MarkPrefabDirty() - { -#if UNITY_2018_3_OR_NEWER - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage == null) return; - EditorSceneManager.MarkSceneDirty(prefabStage.scene); -#endif - } - - /// - /// Verify whether it can be converted to the specified component. - /// - internal static bool CanConvertTo(Object context) where T : MonoBehaviour - { - return context && context.GetType() != typeof(T); - } - - /// - /// Convert to the specified component. - /// - internal static void ConvertTo(Object context) where T : MonoBehaviour - { - var target = context as MonoBehaviour; - var so = new SerializedObject(target); - so.Update(); - - var oldEnable = target.enabled; - target.enabled = false; - - // Find MonoScript of the specified component. - foreach (var script in Resources.FindObjectsOfTypeAll()) - { - if (script.GetClass() != typeof(T)) - continue; - - // Set 'm_Script' to convert. - so.FindProperty("m_Script").objectReferenceValue = script; - so.ApplyModifiedProperties(); - break; - } - - (so.targetObject as MonoBehaviour).enabled = oldEnable; - } - } -} diff --git a/Packages/src/Scripts/Editor/ImportSampleMenu.cs b/Packages/src/Scripts/Editor/ImportSampleMenu.cs deleted file mode 100644 index c685a96..0000000 --- a/Packages/src/Scripts/Editor/ImportSampleMenu.cs +++ /dev/null @@ -1,82 +0,0 @@ -#if !UNITY_2019_1_OR_NEWER -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using UnityEditor; - -namespace Coffee.UISoftMask -{ - public static class ImportSampleMenu - { - private const string jsonGuid = "c43fd233e88b347cdabc530c23ffe30a"; - - [MenuItem("Assets/Samples/UISoftMask/Import Demo")] - private static void ImportDemo() - { - ImportSample(jsonGuid, "Demo"); - } - - [MenuItem("Assets/Samples/UISoftMask/Import TextMeshPro Support")] - private static void ImportTextMeshProSupport() - { - ImportSample(jsonGuid, "TextMeshProSupport"); - } - - private static void ImportSample(string jsonGuid, string sampleName) - { - var jsonPath = AssetDatabase.GUIDToAssetPath(jsonGuid); - var packageRoot = Path.GetDirectoryName(jsonPath).Replace('\\', '/'); - var json = File.ReadAllText(jsonPath); - var version = Regex.Match(json, "\"version\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value; - var displayName = Regex.Match(json, "\"displayName\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value; - var src = string.Format("{0}/Samples~/{1}", packageRoot, sampleName); - var srcAlt = string.Format("{0}/Samples/{1}", packageRoot, sampleName); - var dst = string.Format("Assets/Samples/{0}/{1}/{2}", displayName, version, sampleName); - var previousPath = GetPreviousSamplePath(displayName, sampleName); - - // Remove the previous sample directory. - if (!string.IsNullOrEmpty(previousPath)) - { - var msg = "A different version of the sample is already imported at\n\n" - + previousPath - + "\n\nIt will be deleted when you update. Are you sure you want to continue?"; - if (!EditorUtility.DisplayDialog("Sample Importer", msg, "OK", "Cancel")) - return; - - FileUtil.DeleteFileOrDirectory(previousPath); - - var metaFile = previousPath + ".meta"; - if (File.Exists(metaFile)) - FileUtil.DeleteFileOrDirectory(metaFile); - } - - if (!Directory.Exists(dst)) - FileUtil.DeleteFileOrDirectory(dst); - - var dstDir = Path.GetDirectoryName(dst); - if (!Directory.Exists(dstDir)) - Directory.CreateDirectory(dstDir); - - if (Directory.Exists(src)) - FileUtil.CopyFileOrDirectory(src, dst); - else if (Directory.Exists(srcAlt)) - FileUtil.CopyFileOrDirectory(srcAlt, dst); - else - throw new DirectoryNotFoundException(src); - - AssetDatabase.Refresh(ImportAssetOptions.ImportRecursive); - } - - private static string GetPreviousSamplePath(string displayName, string sampleName) - { - var sampleRoot = string.Format("Assets/Samples/{0}", displayName); - var sampleRootInfo = new DirectoryInfo(sampleRoot); - if (!sampleRootInfo.Exists) return null; - - return sampleRootInfo.GetDirectories() - .Select(versionDir => Path.Combine(versionDir.ToString(), sampleName)) - .FirstOrDefault(Directory.Exists); - } - } -} -#endif diff --git a/Packages/src/Scripts/Editor/SoftMaskEditor.cs b/Packages/src/Scripts/Editor/SoftMaskEditor.cs deleted file mode 100644 index 5104d4c..0000000 --- a/Packages/src/Scripts/Editor/SoftMaskEditor.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using UnityEditor; -using System.Linq; - - -namespace Coffee.UISoftMask -{ - /// - /// SoftMask editor. - /// - [CustomEditor(typeof(SoftMask))] - [CanEditMultipleObjects] - public class SoftMaskEditor : Editor - { - private const int k_PreviewSize = 128; - private const string k_PrefsPreview = "SoftMaskEditor_Preview"; - private static readonly List s_Graphics = new List(); - private static bool s_Preview; - - private void OnEnable() - { - s_Preview = EditorPrefs.GetBool(k_PrefsPreview, false); - } - - public override void OnInspectorGUI() - { - base.OnInspectorGUI(); - - var current = target as SoftMask; - current.GetComponentsInChildren(true, s_Graphics); - var fixTargets = s_Graphics - .Where(x => x.gameObject != current.gameObject) - .Where(x => !x.GetComponent() && (!x.GetComponent() || x.GetComponent().showMaskGraphic)) - .ToList(); - if (0 < fixTargets.Count) - { - GUILayout.BeginHorizontal(); - EditorGUILayout.HelpBox("There are child Graphics that does not have a SoftMaskable component.\nAdd SoftMaskable component to them.", MessageType.Warning); - GUILayout.BeginVertical(); - if (GUILayout.Button("Fix")) - { - foreach (var p in fixTargets) - { - p.gameObject.AddComponent(); - } - - EditorUtils.MarkPrefabDirty(); - } - - if (GUILayout.Button("Ping")) - { - EditorGUIUtility.PingObject(fixTargets[0]); - } - - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - var currentImage = current.graphic as Image; - if (currentImage && IsMaskUI(currentImage.sprite)) - { - GUILayout.BeginHorizontal(); - EditorGUILayout.HelpBox("SoftMask does not recommend to use 'UIMask' sprite as a source image.\n(It contains only small alpha pixels.)\nDo you want to use 'UISprite' instead?", MessageType.Warning); - GUILayout.BeginVertical(); - - if (GUILayout.Button("Fix")) - { - currentImage.sprite = AssetDatabase.GetBuiltinExtraResource("UI/Skin/UISprite.psd"); - } - - GUILayout.EndVertical(); - GUILayout.EndHorizontal(); - } - - // Preview buffer. - GUILayout.BeginVertical(EditorStyles.helpBox); - if (s_Preview != (s_Preview = EditorGUILayout.ToggleLeft("Preview Soft Mask Buffer", s_Preview))) - { - EditorPrefs.SetBool(k_PrefsPreview, s_Preview); - } - - if (s_Preview) - { - var tex = current.softMaskBuffer; - var width = tex.width * k_PreviewSize / tex.height; - EditorGUI.DrawPreviewTexture(GUILayoutUtility.GetRect(width, k_PreviewSize), tex, null, ScaleMode.ScaleToFit); - Repaint(); - } - - GUILayout.EndVertical(); - } - - private static bool IsMaskUI(Object obj) - { - return obj - && obj.name == "UIMask" - && AssetDatabase.GetAssetPath(obj) == "Resources/unity_builtin_extra"; - } - - - //%%%% Context menu for editor %%%% - [MenuItem("CONTEXT/Mask/Convert To SoftMask", true)] - private static bool _ConvertToSoftMask(MenuCommand command) - { - return EditorUtils.CanConvertTo(command.context); - } - - [MenuItem("CONTEXT/Mask/Convert To SoftMask", false)] - private static void ConvertToSoftMask(MenuCommand command) - { - EditorUtils.ConvertTo(command.context); - } - - [MenuItem("CONTEXT/Mask/Convert To Mask", true)] - private static bool _ConvertToMask(MenuCommand command) - { - return EditorUtils.CanConvertTo(command.context); - } - - [MenuItem("CONTEXT/Mask/Convert To Mask", false)] - private static void ConvertToMask(MenuCommand command) - { - EditorUtils.ConvertTo(command.context); - } - } -} diff --git a/Packages/src/Scripts/Editor/SoftMaskableEditor.cs b/Packages/src/Scripts/Editor/SoftMaskableEditor.cs deleted file mode 100644 index d1814fe..0000000 --- a/Packages/src/Scripts/Editor/SoftMaskableEditor.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using UnityEditor; -using MaskIntr = UnityEngine.SpriteMaskInteraction; - -namespace Coffee.UISoftMask -{ - internal enum MaskInteraction : int - { - VisibleInsideMask = (1 << 0) + (1 << 2) + (1 << 4) + (1 << 6), - VisibleOutsideMask = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6), - Custom = -1, - } - - /// - /// SoftMaskable editor. - /// - [CustomEditor(typeof(SoftMaskable))] - [CanEditMultipleObjects] - public class SoftMaskableEditor : Editor - { - private static readonly List s_TmpMasks = new List(); - private static GUIContent s_MaskWarning; - private SerializedProperty _spMaskInteraction; - private bool _custom; - - private MaskInteraction maskInteraction - { - get - { - var value = _spMaskInteraction.intValue; - return _custom - ? MaskInteraction.Custom - : System.Enum.IsDefined(typeof(MaskInteraction), value) - ? (MaskInteraction) value - : MaskInteraction.Custom; - } - set - { - _custom = (value == MaskInteraction.Custom); - if (!_custom) - { - _spMaskInteraction.intValue = (int) value; - } - } - } - - - private void OnEnable() - { - _spMaskInteraction = serializedObject.FindProperty("m_MaskInteraction"); - _custom = (maskInteraction == MaskInteraction.Custom); - - if (s_MaskWarning == null) - { - s_MaskWarning = new GUIContent(EditorGUIUtility.FindTexture("console.warnicon.sml"), "This is not a SoftMask component."); - } - } - - private void DrawMaskInteractions() - { - var softMaskable = target as SoftMaskable; - if (softMaskable == null) return; - - softMaskable.GetComponentsInParent(true, s_TmpMasks); - s_TmpMasks.RemoveAll(x => !x.enabled); - s_TmpMasks.Reverse(); - - maskInteraction = (MaskInteraction) EditorGUILayout.EnumPopup("Mask Interaction", maskInteraction); - if (!_custom) return; - - var l = EditorGUIUtility.labelWidth; - EditorGUIUtility.labelWidth = 45; - - using (var ccs = new EditorGUI.ChangeCheckScope()) - { - var intr0 = DrawMaskInteraction(0); - var intr1 = DrawMaskInteraction(1); - var intr2 = DrawMaskInteraction(2); - var intr3 = DrawMaskInteraction(3); - - if (ccs.changed) - { - _spMaskInteraction.intValue = (intr0 << 0) + (intr1 << 2) + (intr2 << 4) + (intr3 << 6); - } - } - - EditorGUIUtility.labelWidth = l; - } - - private int DrawMaskInteraction(int layer) - { - var mask = layer < s_TmpMasks.Count ? s_TmpMasks[layer] : null; - var intr = (MaskIntr) ((_spMaskInteraction.intValue >> layer * 2) & 0x3); - if (!mask) - { - return (int) intr; - } - - GUILayout.BeginHorizontal(); - EditorGUILayout.LabelField(mask is SoftMask ? GUIContent.none : s_MaskWarning, GUILayout.Width(16)); - GUILayout.Space(-5); - EditorGUILayout.ObjectField("Mask " + layer, mask, typeof(Mask), false); - GUILayout.Space(-15); - intr = (MaskIntr) EditorGUILayout.EnumPopup(intr); - GUILayout.EndHorizontal(); - - return (int) intr; - } - - public override void OnInspectorGUI() - { - base.OnInspectorGUI(); - - serializedObject.Update(); - DrawMaskInteractions(); - - serializedObject.ApplyModifiedProperties(); - - var current = target as SoftMaskable; - if (current == null) return; - - var mask = current.softMask; - if (mask) return; - - GUILayout.BeginHorizontal(); - EditorGUILayout.HelpBox("This is unnecessary SoftMaskable.\nCan't find any SoftMask components above.", MessageType.Warning); - if (GUILayout.Button("Remove", GUILayout.Height(40))) - { - DestroyImmediate(current); - EditorUtils.MarkPrefabDirty(); - } - - GUILayout.EndHorizontal(); - } - } -} diff --git a/Packages/src/Scripts/Editor/SoftMaskableEditor.cs.meta b/Packages/src/Scripts/Editor/SoftMaskableEditor.cs.meta deleted file mode 100644 index a9fef1b..0000000 --- a/Packages/src/Scripts/Editor/SoftMaskableEditor.cs.meta +++ /dev/null @@ -1,12 +0,0 @@ -fileFormatVersion: 2 -guid: e2f5fc58cb78640d9abbb950e92109b6 -timeCreated: 1539820794 -licenseType: Pro -MonoImporter: - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Packages/src/Scripts/GraphicConnector.cs b/Packages/src/Scripts/GraphicConnector.cs deleted file mode 100644 index 1627f5f..0000000 --- a/Packages/src/Scripts/GraphicConnector.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; - -namespace Coffee.UISoftMask -{ - 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 T GetComponentInParentEx(this Component component, bool includeInactive = false) where T : MonoBehaviour - { - if (!component) return null; - var trans = component.transform; - - while (trans) - { - var c = trans.GetComponent(); - if (c && (includeInactive || c.isActiveAndEnabled)) return c; - - trans = trans.parent; - } - - return null; - } - } - - - public class GraphicConnector - { - private static readonly List s_Connectors = new List(); - private static readonly Dictionary s_ConnectorMap = new Dictionary(); - 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; - } - - /// - /// Connector priority. - /// - protected virtual int priority - { - get { return -1; } - } - - /// - /// The connector is valid for the component. - /// - 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(); - } - } -} diff --git a/Packages/src/Scripts/MaterialCache.cs b/Packages/src/Scripts/MaterialCache.cs deleted file mode 100644 index 47b80e4..0000000 --- a/Packages/src/Scripts/MaterialCache.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.Generic; -using System; -using UnityEngine; - -namespace Coffee.UISoftMask -{ - internal class MaterialEntry - { - public Material material; - public int referenceCount; - - public void Release() - { - if (material) - { -#if UNITY_EDITOR - if (!Application.isPlaying) - UnityEngine.Object.DestroyImmediate(material, false); - else -#endif - UnityEngine.Object.Destroy(material); - } - - material = null; - } - } - - internal static class MaterialCache - { - static readonly Dictionary s_MaterialMap = new Dictionary(); - -#if UNITY_EDITOR - [UnityEditor.InitializeOnLoadMethod] - private static void ClearCache() - { - foreach (var entry in s_MaterialMap.Values) - { - entry.Release(); - } - - s_MaterialMap.Clear(); - } -#endif - - public static Material Register(Material material, Hash128 hash, Action onModify) - { - if (!hash.isValid) return null; - - MaterialEntry entry; - if (!s_MaterialMap.TryGetValue(hash, out entry)) - { - entry = new MaterialEntry() - { - material = new Material(material) - { - hideFlags = HideFlags.HideAndDontSave, - }, - }; - - onModify(entry.material); - s_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 || !s_MaterialMap.TryGetValue(hash, out entry)) return; - //Debug.LogFormat("Unregister: {0}, {1}", hash, entry.referenceCount -1); - - if (--entry.referenceCount > 0) return; - - entry.Release(); - s_MaterialMap.Remove(hash); - //Debug.LogFormat("Unregister: Release Emtry: {0}, {1} (Total: {2})", hash, entry.referenceCount, materialMap.Count); - } - } -} diff --git a/Packages/src/Scripts/SoftMask.cs b/Packages/src/Scripts/SoftMask.cs deleted file mode 100644 index 7a5a9e2..0000000 --- a/Packages/src/Scripts/SoftMask.cs +++ /dev/null @@ -1,788 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Profiling; -using UnityEngine.Rendering; -using UnityEngine.Serialization; -using UnityEngine.UI; -using Object = UnityEngine.Object; - -namespace Coffee.UISoftMask -{ - /// - /// Soft mask. - /// Use instead of Mask for smooth masking. - /// - public class SoftMask : Mask, IMeshModifier - { - /// - /// Down sampling rate. - /// - public enum DownSamplingRate - { - None = 0, - x1 = 1, - x2 = 2, - x4 = 4, - x8 = 8, - } - - private static readonly List[] s_TmpSoftMasks = new List[] - { - new List(), - new List(), - new List(), - new List(), - }; - - private static readonly Color[] s_ClearColors = new Color[] - { - new Color(0, 0, 0, 0), - new Color(1, 0, 0, 0), - new Color(1, 1, 0, 0), - new Color(1, 1, 1, 0), - }; - - private static bool s_UVStartsAtTop; - private static bool s_IsMetal; - private static Shader s_SoftMaskShader; - private static Texture2D s_ReadTexture; - private static readonly List s_ActiveSoftMasks = new List(); - private static readonly List s_TempRelatables = new List(); - private static readonly Dictionary s_PreviousViewProjectionMatrices = new Dictionary(); - private static readonly Dictionary s_NowViewProjectionMatrices = new Dictionary(); - private static int s_StencilCompId; - private static int s_ColorMaskId; - private static int s_MainTexId; - private static int s_SoftnessId; - private static int s_Alpha; - private static int s_PreviousWidth; - private static int s_PreviousHeight; - private MaterialPropertyBlock _mpb; - private CommandBuffer _cb; - private Material _material; - private RenderTexture _softMaskBuffer; - private int _stencilDepth; - private Mesh _mesh; - private SoftMask _parent; - internal readonly List _children = new List(); - private bool _hasChanged = false; - private bool _hasStencilStateChanged = false; - - - [FormerlySerializedAs("m_DesamplingRate")] [SerializeField, Tooltip("The down sampling rate for soft mask buffer.")] - private DownSamplingRate m_DownSamplingRate = DownSamplingRate.x1; - - [SerializeField, Range(0, 1), Tooltip("The value used by the soft mask to select the area of influence defined over the soft mask's graphic.")] - private float m_Softness = 1; - - [SerializeField, Range(0f, 1f), Tooltip("The transparency of the whole masked graphic.")] - private float m_Alpha = 1; - - [Header("Advanced Options")] [SerializeField, Tooltip("Should the soft mask ignore parent soft masks?")] - private bool m_IgnoreParent = false; - - [SerializeField, Tooltip("Is the soft mask a part of parent soft mask?")] - private bool m_PartOfParent = false; - - [SerializeField, Tooltip("Self graphic will not be drawn to soft mask buffer.")] - private bool m_IgnoreSelfGraphic; - - - [SerializeField, Tooltip("Self graphic will not be written to stencil buffer.")] - private bool m_IgnoreSelfStencil; - - /// - /// The down sampling rate for soft mask buffer. - /// - public DownSamplingRate downSamplingRate - { - get { return m_DownSamplingRate; } - set - { - if (m_DownSamplingRate == value) return; - m_DownSamplingRate = value; - hasChanged = true; - } - } - - /// - /// The value used by the soft mask to select the area of influence defined over the soft mask's graphic. - /// - public float softness - { - get { return m_Softness; } - set - { - value = Mathf.Clamp01(value); - if (Mathf.Approximately(m_Softness, value)) return; - m_Softness = value; - hasChanged = true; - } - } - - /// - /// The transparency of the whole masked graphic. - /// - public float alpha - { - get { return m_Alpha; } - set - { - value = Mathf.Clamp01(value); - if (Mathf.Approximately(m_Alpha, value)) return; - m_Alpha = value; - hasChanged = true; - } - } - - /// - /// Should the soft mask ignore parent soft masks? - /// - /// If set to true the soft mask will ignore any parent soft mask settings. - public bool ignoreParent - { - get { return m_IgnoreParent; } - set - { - if (m_IgnoreParent == value) return; - m_IgnoreParent = value; - hasChanged = true; - OnTransformParentChanged(); - } - } - - /// - /// Is the soft mask a part of parent soft mask? - /// - public bool partOfParent - { - get { return m_PartOfParent; } - set - { - if (m_PartOfParent == value) return; - m_PartOfParent = value; - hasChanged = true; - OnTransformParentChanged(); - } - } - - /// - /// The soft mask buffer. - /// - public RenderTexture softMaskBuffer - { - get - { - if (_parent) - { - ReleaseRt(ref _softMaskBuffer); - return _parent.softMaskBuffer; - } - - // Check the size of soft mask buffer. - int w, h; - GetDownSamplingSize(m_DownSamplingRate, out w, out h); - if (_softMaskBuffer && (_softMaskBuffer.width != w || _softMaskBuffer.height != h)) - { - ReleaseRt(ref _softMaskBuffer); - } - - if (!_softMaskBuffer) - { - _softMaskBuffer = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 1, RenderTextureMemoryless.Depth); - hasChanged = true; - _hasStencilStateChanged = true; - } - - return _softMaskBuffer; - } - } - - public bool hasChanged - { - get { return _parent ? _parent.hasChanged : _hasChanged; } - private set - { - if (_parent) - { - _parent.hasChanged = value; - } - - _hasChanged = value; - } - } - - public SoftMask parent - { - get { return _parent; } - } - - public bool ignoreSelfGraphic - { - get { return m_IgnoreSelfGraphic; } - set - { - if (m_IgnoreSelfGraphic == value) return; - m_IgnoreSelfGraphic = value; - hasChanged = true; - graphic.SetVerticesDirtyEx(); - } - } - - public bool ignoreSelfStencil - { - get { return m_IgnoreSelfStencil; } - set - { - if (m_IgnoreSelfStencil == value) return; - m_IgnoreSelfStencil = value; - hasChanged = true; - graphic.SetVerticesDirtyEx(); - graphic.SetMaterialDirtyEx(); - } - } - - - Material material - { - get - { - return _material - ? _material - : _material = - new Material(s_SoftMaskShader - ? s_SoftMaskShader - : s_SoftMaskShader = Resources.Load("SoftMask")) {hideFlags = HideFlags.HideAndDontSave}; - } - } - - Mesh mesh - { - get { return _mesh ? _mesh : _mesh = new Mesh() {hideFlags = HideFlags.HideAndDontSave}; } - } - - - /// - /// Perform material modification in this function. - /// - /// Modified material. - /// Configured Material. - public override Material GetModifiedMaterial(Material baseMaterial) - { - hasChanged = true; - if (ignoreSelfStencil) return baseMaterial; - - var result = base.GetModifiedMaterial(baseMaterial); - if (m_IgnoreParent && result != baseMaterial) - { - result.SetInt(s_StencilCompId, (int) CompareFunction.Always); - } - - return result; - } - - - /// - /// Call used to modify mesh. - /// - void IMeshModifier.ModifyMesh(Mesh mesh) - { - hasChanged = true; - _mesh = mesh; - } - - /// - /// Call used to modify mesh. - /// - void IMeshModifier.ModifyMesh(VertexHelper verts) - { - if (isActiveAndEnabled) - { - if (ignoreSelfGraphic) - { - verts.Clear(); - verts.FillMesh(mesh); - } - else if (ignoreSelfStencil) - { - verts.FillMesh(mesh); - verts.Clear(); - } - else - { - verts.FillMesh(mesh); - } - } - - hasChanged = true; - } - - /// - /// Given a point and a camera is the raycast valid. - /// - /// Valid. - /// Screen position. - /// Raycast camera. - /// Target graphic. - public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera, Graphic g, int[] interactions) - { - if (!isActiveAndEnabled || (g == graphic && !g.raycastTarget)) return true; - - int x = (int) ((softMaskBuffer.width - 1) * Mathf.Clamp01(sp.x / Screen.width)); - int y = s_UVStartsAtTop && !s_IsMetal - ? (int) ((softMaskBuffer.height - 1) * (1 - Mathf.Clamp01(sp.y / Screen.height))) - : (int) ((softMaskBuffer.height - 1) * Mathf.Clamp01(sp.y / Screen.height)); - return 0.5f < GetPixelValue(x, y, interactions); - } - - public override bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) - { - return true; - } - - /// - /// This function is called when the object becomes enabled and active. - /// - protected override void OnEnable() - { - hasChanged = true; - - // Register. - if (s_ActiveSoftMasks.Count == 0) - { - Canvas.willRenderCanvases += UpdateMaskTextures; - - if (s_StencilCompId == 0) - { - s_UVStartsAtTop = SystemInfo.graphicsUVStartsAtTop; - s_IsMetal = SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal; - s_StencilCompId = Shader.PropertyToID("_StencilComp"); - s_ColorMaskId = Shader.PropertyToID("_ColorMask"); - s_MainTexId = Shader.PropertyToID("_MainTex"); - s_SoftnessId = Shader.PropertyToID("_Softness"); - s_Alpha = Shader.PropertyToID("_Alpha"); - } - } - - s_ActiveSoftMasks.Add(this); - - // Reset the parent-child relation. - GetComponentsInChildren(false, s_TempRelatables); - for (int i = s_TempRelatables.Count - 1; 0 <= i; i--) - { - s_TempRelatables[i].OnTransformParentChanged(); - } - - s_TempRelatables.Clear(); - - // Create objects. - _mpb = new MaterialPropertyBlock(); - _cb = new CommandBuffer(); - - graphic.SetVerticesDirtyEx(); - - base.OnEnable(); - _hasStencilStateChanged = false; - } - - /// - /// This function is called when the behaviour becomes disabled. - /// - protected override void OnDisable() - { - // Unregister. - s_ActiveSoftMasks.Remove(this); - if (s_ActiveSoftMasks.Count == 0) - { - Canvas.willRenderCanvases -= UpdateMaskTextures; - } - - // Reset the parent-child relation. - for (int i = _children.Count - 1; 0 <= i; i--) - { - _children[i].SetParent(_parent); - } - - _children.Clear(); - SetParent(null); - - // Destroy objects. - _mpb.Clear(); - _mpb = null; - _cb.Release(); - _cb = null; - - ReleaseObject(_mesh); - _mesh = null; - ReleaseObject(_material); - _material = null; - ReleaseRt(ref _softMaskBuffer); - - base.OnDisable(); - _hasStencilStateChanged = false; - } - - /// - /// This function is called when the parent property of the transform of the GameObject has changed. - /// - protected override void OnTransformParentChanged() - { - hasChanged = true; - SoftMask newParent = null; - if (isActiveAndEnabled && !m_IgnoreParent) - { - var parentTransform = transform.parent; - while (parentTransform && (!newParent || !newParent.enabled)) - { - newParent = parentTransform.GetComponent(); - parentTransform = parentTransform.parent; - } - } - - SetParent(newParent); - hasChanged = true; - } - - protected override void OnRectTransformDimensionsChange() - { - hasChanged = true; - } - -#if UNITY_EDITOR - /// - /// This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only). - /// - protected override void OnValidate() - { - graphic.SetVerticesDirtyEx(); - graphic.SetMaterialDirtyEx(); - OnTransformParentChanged(); - base.OnValidate(); - _hasStencilStateChanged = false; - } -#endif - - /// - /// Update all soft mask textures. - /// - private static void UpdateMaskTextures() - { - Profiler.BeginSample("UpdateMaskTextures"); - foreach (var sm in s_ActiveSoftMasks) - { - if (!sm || sm._hasChanged) - continue; - - var canvas = sm.graphic.canvas; - if (!canvas) - continue; - - if (canvas.renderMode == RenderMode.WorldSpace) - { - var cam = canvas.worldCamera; - if (!cam) - continue; - - Profiler.BeginSample("Check view projection matrix changed (world space)"); - var nowVP = cam.projectionMatrix * cam.worldToCameraMatrix; - var previousVP = default(Matrix4x4); - var id = cam.GetInstanceID(); - s_PreviousViewProjectionMatrices.TryGetValue(id, out previousVP); - s_NowViewProjectionMatrices[id] = nowVP; - - if (previousVP != nowVP) - { - sm.hasChanged = true; - } - - Profiler.EndSample(); - } - - var rt = sm.rectTransform; - if (rt.hasChanged) - { - rt.hasChanged = false; - sm.hasChanged = true; - } -#if UNITY_EDITOR - if (!Application.isPlaying) - { - sm.hasChanged = true; - } -#endif - } - - Profiler.BeginSample("Update changed soft masks"); - foreach (var sm in s_ActiveSoftMasks) - { - if (!sm || !sm._hasChanged) - continue; - - sm._hasChanged = false; - if (sm._parent) continue; - sm.UpdateMaskTexture(); - - if (!sm._hasStencilStateChanged) continue; - sm._hasStencilStateChanged = false; - - Profiler.BeginSample("Notify stencil state changed"); - MaskUtilities.NotifyStencilStateChanged(sm); - Profiler.EndSample(); - } - - Profiler.EndSample(); - - Profiler.BeginSample("Update previous view projection matrices"); - s_PreviousViewProjectionMatrices.Clear(); - foreach (var kv in s_NowViewProjectionMatrices) - { - s_PreviousViewProjectionMatrices.Add(kv.Key, kv.Value); - } - - s_NowViewProjectionMatrices.Clear(); - Profiler.EndSample(); - - Profiler.EndSample(); - -#if UNITY_EDITOR - var w = s_PreviousWidth; - var h = s_PreviousHeight; - GetDownSamplingSize(DownSamplingRate.None, out s_PreviousWidth, out s_PreviousHeight); - if (w != s_PreviousWidth || h != s_PreviousHeight) - { - Canvas.ForceUpdateCanvases(); - } -#endif - } - - /// - /// Update the mask texture. - /// - private void UpdateMaskTexture() - { - if (!graphic || !graphic.canvas) return; - Profiler.BeginSample("UpdateMaskTexture"); - - - _stencilDepth = MaskUtilities.GetStencilDepth(transform, MaskUtilities.FindRootSortOverrideCanvas(transform)); - - // Collect children soft masks. - Profiler.BeginSample("Collect children soft masks"); - var depth = 0; - s_TmpSoftMasks[0].Add(this); - while (_stencilDepth + depth < 3) - { - var count = s_TmpSoftMasks[depth].Count; - for (var i = 0; i < count; i++) - { - var children = s_TmpSoftMasks[depth][i]._children; - var childCount = children.Count; - for (var j = 0; j < childCount; j++) - { - var child = children[j]; - var childDepth = child.m_PartOfParent ? depth : depth + 1; - s_TmpSoftMasks[childDepth].Add(child); - } - } - - depth++; - } - - Profiler.EndSample(); - - // CommandBuffer. - Profiler.BeginSample("Initialize CommandBuffer"); - _cb.Clear(); - _cb.SetRenderTarget(softMaskBuffer); - _cb.ClearRenderTarget(false, true, s_ClearColors[_stencilDepth]); - Profiler.EndSample(); - - // Set view and projection matrices. - Profiler.BeginSample("Set view and projection matrices"); - var c = graphic.canvas.rootCanvas; - var cam = c.worldCamera ?? Camera.main; - if (c && c.renderMode != RenderMode.ScreenSpaceOverlay && cam) - { - var p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false); - _cb.SetViewProjectionMatrices(cam.worldToCameraMatrix, p); - } - else - { - var pos = c.transform.position; - var vm = Matrix4x4.TRS(new Vector3(-pos.x, -pos.y, -1000), Quaternion.identity, new Vector3(1, 1, -1f)); - var pm = Matrix4x4.TRS(new Vector3(0, 0, -1), Quaternion.identity, new Vector3(1 / pos.x, 1 / pos.y, -2 / 10000f)); - _cb.SetViewProjectionMatrices(vm, pm); - } - - Profiler.EndSample(); - - // Draw soft masks. - Profiler.BeginSample("Draw Mesh"); - for (var i = 0; i < s_TmpSoftMasks.Length; i++) - { - var count = s_TmpSoftMasks[i].Count; - for (var j = 0; j < count; j++) - { - var sm = s_TmpSoftMasks[i][j]; - - if (i != 0) - { - sm._stencilDepth = MaskUtilities.GetStencilDepth(sm.transform, MaskUtilities.FindRootSortOverrideCanvas(sm.transform)); - } - - // Set material property. - sm.material.SetInt(s_ColorMaskId, (int) 1 << (3 - _stencilDepth - i)); - sm._mpb.SetTexture(s_MainTexId, sm.graphic.mainTexture); - sm._mpb.SetFloat(s_SoftnessId, sm.m_Softness); - sm._mpb.SetFloat(s_Alpha, sm.m_Alpha); - - // Draw mesh. - _cb.DrawMesh(sm.mesh, sm.transform.localToWorldMatrix, sm.material, 0, 0, sm._mpb); - } - - s_TmpSoftMasks[i].Clear(); - } - - Profiler.EndSample(); - - Graphics.ExecuteCommandBuffer(_cb); - Profiler.EndSample(); - } - - /// - /// Gets the size of the down sampling. - /// - private static void GetDownSamplingSize(DownSamplingRate rate, out int w, out int h) - { -#if UNITY_EDITOR - if (!Application.isPlaying) - { - var res = UnityEditor.UnityStats.screenRes.Split('x'); - w = Mathf.Max(64, int.Parse(res[0])); - h = Mathf.Max(64, int.Parse(res[1])); - } - else -#endif - if (Screen.fullScreenMode == FullScreenMode.Windowed) - { - w = Screen.width; - h = Screen.height; - } - else - { - w = Screen.currentResolution.width; - h = Screen.currentResolution.height; - } - - if (rate == DownSamplingRate.None) - return; - - var aspect = (float) w / h; - if (w < h) - { - h = Mathf.ClosestPowerOfTwo(h / (int) rate); - w = Mathf.CeilToInt(h * aspect); - } - else - { - w = Mathf.ClosestPowerOfTwo(w / (int) rate); - h = Mathf.CeilToInt(w / aspect); - } - } - - /// - /// Release the specified obj. - /// - /// Object. - private static void ReleaseRt(ref RenderTexture tmpRT) - { - if (!tmpRT) return; - - tmpRT.Release(); - RenderTexture.ReleaseTemporary(tmpRT); - tmpRT = null; - } - - /// - /// Release the specified obj. - /// - /// Object. - private static void ReleaseObject(Object obj) - { - if (!obj) return; - -#if UNITY_EDITOR - if (!Application.isPlaying) - DestroyImmediate(obj); - else -#endif - Destroy(obj); - } - - - /// - /// Set the parent of the soft mask. - /// - /// The parent soft mask to use. - private void SetParent(SoftMask newParent) - { - if (_parent != newParent && this != newParent) - { - if (_parent && _parent._children.Contains(this)) - { - _parent._children.Remove(this); - _parent._children.RemoveAll(x => x == null); - } - - _parent = newParent; - } - - if (_parent && !_parent._children.Contains(this)) - { - _parent._children.Add(this); - } - } - - /// - /// Gets the pixel value. - /// - private float GetPixelValue(int x, int y, int[] interactions) - { - if (!s_ReadTexture) - { - s_ReadTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false); - } - - var currentRt = RenderTexture.active; - - RenderTexture.active = softMaskBuffer; - s_ReadTexture.ReadPixels(new Rect(x, y, 1, 1), 0, 0); - s_ReadTexture.Apply(false, false); - RenderTexture.active = currentRt; - - var colors = s_ReadTexture.GetRawTextureData(); - - for (int i = 0; i < 4; i++) - { - switch (interactions[(i + 3) % 4]) - { - case 0: - colors[i] = 255; - break; - case 2: - colors[i] = (byte) (255 - colors[i]); - break; - } - } - - switch (_stencilDepth) - { - case 0: return (colors[1] / 255f); - case 1: return (colors[1] / 255f) * (colors[2] / 255f); - case 2: return (colors[1] / 255f) * (colors[2] / 255f) * (colors[3] / 255f); - case 3: return (colors[1] / 255f) * (colors[2] / 255f) * (colors[3] / 255f) * (colors[0] / 255f); - default: return 0; - } - } - } -} diff --git a/Packages/src/Scripts/SoftMaskable.cs b/Packages/src/Scripts/SoftMaskable.cs deleted file mode 100755 index acf7026..0000000 --- a/Packages/src/Scripts/SoftMaskable.cs +++ /dev/null @@ -1,356 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Profiling; -using UnityEngine.Rendering; -using UnityEngine.UI; -using MaskIntr = UnityEngine.SpriteMaskInteraction; - -namespace Coffee.UISoftMask -{ - /// - /// Soft maskable. - /// Add this component to Graphic under SoftMask for smooth masking. - /// -#if UNITY_2018_3_OR_NEWER - [ExecuteAlways] -#else - [ExecuteInEditMode] -# endif - [RequireComponent(typeof(Graphic))] - public class SoftMaskable : MonoBehaviour, IMaterialModifier, ICanvasRaycastFilter -#if UNITY_EDITOR - , ISerializationCallbackReceiver -# endif - { - private const int kVisibleInside = (1 << 0) + (1 << 2) + (1 << 4) + (1 << 6); - private const int kVisibleOutside = (2 << 0) + (2 << 2) + (2 << 4) + (2 << 6); - private static readonly Hash128 k_InvalidHash = new Hash128(); - - private static int s_SoftMaskTexId; - private static int s_StencilCompId; - private static int s_MaskInteractionId; - private static int s_GameVPId; - private static int s_GameTVPId; - private static List s_ActiveSoftMaskables; - private static int[] s_Interactions = new int[4]; - - [SerializeField, HideInInspector, System.Obsolete] - private bool m_Inverse; - - [SerializeField, Tooltip("The interaction for each masks."), HideInInspector] - private int m_MaskInteraction = kVisibleInside; - - [SerializeField, Tooltip("Use stencil to mask.")] - private bool m_UseStencil = true; - - [SerializeField, Tooltip("Use soft-masked raycast target.\n\nNote: This option is expensive.")] - private bool m_RaycastFilter; - - private Graphic _graphic; - private SoftMask _softMask; - private Hash128 _effectMaterialHash; - - /// - /// The graphic will be visible only in areas where no mask is present. - /// - public bool inverse - { - get { return m_MaskInteraction == kVisibleOutside; } - set - { - var intValue = value ? kVisibleOutside : kVisibleInside; - if (m_MaskInteraction == intValue) return; - m_MaskInteraction = intValue; - graphic.SetMaterialDirtyEx(); - } - } - - /// - /// Use soft-masked raycast target. This option is expensive. - /// - public bool raycastFilter - { - get { return m_RaycastFilter; } - set { m_RaycastFilter = value; } - } - - /// - /// Use stencil to mask. - /// - public bool useStencil - { - get { return m_UseStencil; } - set - { - if (m_UseStencil == value) return; - m_UseStencil = value; - graphic.SetMaterialDirtyEx(); - } - } - - /// - /// The graphic associated with the soft mask. - /// - public Graphic graphic - { - get { return _graphic ? _graphic : _graphic = GetComponent(); } - } - - public SoftMask softMask - { - get { return _softMask ? _softMask : _softMask = this.GetComponentInParentEx(); } - } - - public Material modifiedMaterial { get; private set; } - - /// - /// Perform material modification in this function. - /// - /// Modified material. - /// Configured Material. - Material IMaterialModifier.GetModifiedMaterial(Material baseMaterial) - { - _softMask = null; - modifiedMaterial = null; - - // If this component is disabled, the material is returned as is. - // If the parents do not have a soft mask component, the material is returned as is. - if (!isActiveAndEnabled || !softMask) - { - MaterialCache.Unregister(_effectMaterialHash); - _effectMaterialHash = k_InvalidHash; - return baseMaterial; - } - - // Generate soft maskable material. - var previousHash = _effectMaterialHash; - _effectMaterialHash = new Hash128( - (uint) baseMaterial.GetInstanceID(), - (uint) softMask.GetInstanceID(), - (uint) m_MaskInteraction, - (uint) (m_UseStencil ? 1 : 0) - ); - - // Generate soft maskable material. - modifiedMaterial = MaterialCache.Register(baseMaterial, _effectMaterialHash, mat => - { - mat.shader = Shader.Find(string.Format("Hidden/{0} (SoftMaskable)", mat.shader.name)); - mat.SetTexture(s_SoftMaskTexId, softMask.softMaskBuffer); - mat.SetInt(s_StencilCompId, m_UseStencil ? (int) CompareFunction.Equal : (int) CompareFunction.Always); - -#if UNITY_EDITOR - mat.EnableKeyword("SOFTMASK_EDITOR"); - UpdateMaterialForSceneView(mat); -#endif - - var root = MaskUtilities.FindRootSortOverrideCanvas(transform); - var stencil = MaskUtilities.GetStencilDepth(transform, root); - mat.SetVector(s_MaskInteractionId, new Vector4( - 1 <= stencil ? (m_MaskInteraction >> 0 & 0x3) : 0, - 2 <= stencil ? (m_MaskInteraction >> 2 & 0x3) : 0, - 3 <= stencil ? (m_MaskInteraction >> 4 & 0x3) : 0, - 4 <= stencil ? (m_MaskInteraction >> 6 & 0x3) : 0 - )); - }); - - // Unregister the previous material. - MaterialCache.Unregister(previousHash); - - return modifiedMaterial; - } - - /// - /// Given a point and a camera is the raycast valid. - /// - /// Valid. - /// Screen position. - /// Raycast camera. - bool ICanvasRaycastFilter.IsRaycastLocationValid(Vector2 sp, Camera eventCamera) - { - if (!isActiveAndEnabled || !softMask) - return true; - if (!RectTransformUtility.RectangleContainsScreenPoint(transform as RectTransform, sp, eventCamera)) - return false; - - if (m_RaycastFilter) - { - var sm = _softMask; - for (var i = 0; i < 4; i++) - { - s_Interactions[i] = sm ? ((m_MaskInteraction >> i * 2) & 0x3) : 0; - sm = sm ? sm.parent : null; - } - - return _softMask.IsRaycastLocationValid(sp, eventCamera, graphic, s_Interactions); - } - else - { - var sm = _softMask; - for (var i = 0; i < 4; i++) - { - if (!sm) break; - - s_Interactions[i] = sm ? ((m_MaskInteraction >> i * 2) & 0x3) : 0; - var interaction = s_Interactions[i] == 1; - var rt = sm.transform as RectTransform; - var inRect = RectTransformUtility.RectangleContainsScreenPoint(rt, sp, eventCamera); - if (!sm.ignoreSelfGraphic && interaction != inRect) return false; - - foreach (var child in sm._children) - { - if (!child) continue; - - var childRt = child.transform as RectTransform; - var inRectChild = RectTransformUtility.RectangleContainsScreenPoint(childRt, sp, eventCamera); - if (!child.ignoreSelfGraphic && interaction != inRectChild) return false; - } - - sm = sm ? sm.parent : null; - } - - return true; - } - } - - /// - /// Set the interaction for each mask. - /// - public void SetMaskInteraction(MaskIntr intr) - { - SetMaskInteraction(intr, intr, intr, intr); - } - - /// - /// Set the interaction for each mask. - /// - public void SetMaskInteraction(MaskIntr layer0, MaskIntr layer1, MaskIntr layer2, MaskIntr layer3) - { - m_MaskInteraction = (int) layer0 + ((int) layer1 << 2) + ((int) layer2 << 4) + ((int) layer3 << 6); - graphic.SetMaterialDirtyEx(); - } - - - /// - /// This function is called when the object becomes enabled and active. - /// - private void OnEnable() - { - // Register. - if (s_ActiveSoftMaskables == null) - { - s_ActiveSoftMaskables = new List(); - - s_SoftMaskTexId = Shader.PropertyToID("_SoftMaskTex"); - s_StencilCompId = Shader.PropertyToID("_StencilComp"); - s_MaskInteractionId = Shader.PropertyToID("_MaskInteraction"); - -#if UNITY_EDITOR - s_GameVPId = Shader.PropertyToID("_GameVP"); - s_GameTVPId = Shader.PropertyToID("_GameTVP"); - UnityEditor.SceneView.beforeSceneGui -= SceneView_beforeSceneGui; // For safety - UnityEditor.SceneView.beforeSceneGui += SceneView_beforeSceneGui; -#endif - } - - s_ActiveSoftMaskables.Add(this); - - graphic.SetMaterialDirtyEx(); - _softMask = null; - } - - /// - /// This function is called when the behaviour becomes disabled. - /// - private void OnDisable() - { - s_ActiveSoftMaskables.Remove(this); - - graphic.SetMaterialDirtyEx(); - _softMask = null; - - MaterialCache.Unregister(_effectMaterialHash); - _effectMaterialHash = k_InvalidHash; -#if UNITY_EDITOR - UnityEditor.SceneView.beforeSceneGui -= SceneView_beforeSceneGui; -#endif - } - -#if UNITY_EDITOR - private void UpdateMaterialForSceneView(Material mat) - { - if(!mat || !graphic || !graphic.canvas || !mat.shader || !mat.shader.name.EndsWith(" (SoftMaskable)")) return; - - // Set view and projection matrices. - Profiler.BeginSample("Set view and projection matrices"); - var c = graphic.canvas.rootCanvas; - var cam = c.worldCamera ?? Camera.main; - if (c && c.renderMode != RenderMode.ScreenSpaceOverlay && cam) - { - var p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false); - var pv = p * cam.worldToCameraMatrix; - mat.SetMatrix(s_GameVPId, pv); - mat.SetMatrix(s_GameTVPId, pv); - } - else - { - var pos = c.transform.position; - var scale = c.transform.localScale.x; - var size = (c.transform as RectTransform).sizeDelta; - var gameVp = Matrix4x4.TRS(new Vector3(0, 0, 0.5f), Quaternion.identity, new Vector3(2 / size.x, 2 / size.y, 0.0005f * scale)); - var gameTvp = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, new Vector3(1 / pos.x, 1 / pos.y, -2 / 2000f)) * Matrix4x4.Translate(-pos); - - mat.SetMatrix(s_GameVPId, gameVp); - mat.SetMatrix(s_GameTVPId, gameTvp); - } - Profiler.EndSample(); - } - - private void SceneView_beforeSceneGui(UnityEditor.SceneView obj) - { - - var parentCanvas = GetComponentInParent(); // Don't think we can cache this in case this go is moved to another parent - if (parentCanvas != null && parentCanvas.enabled) // Only do this expensive call if the UI element is active - UpdateMaterialForSceneView(modifiedMaterial); - } - - - /// - /// This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only). - /// - private void OnValidate() - { - graphic.SetMaterialDirtyEx(); - } - - void ISerializationCallbackReceiver.OnBeforeSerialize() - { - } - - void ISerializationCallbackReceiver.OnAfterDeserialize() - { -#pragma warning disable 0612 - if (m_Inverse) - { - m_Inverse = false; - m_MaskInteraction = kVisibleOutside; - } -#pragma warning restore 0612 - - var current = this; - UnityEditor.EditorApplication.delayCall += () => - { - if (!current) return; - if (!graphic) return; - if (graphic.name.Contains("TMP SubMeshUI")) return; - if (!graphic.material) return; - if (!graphic.material.shader) return; - if (graphic.material.shader.name != "Hidden/UI/Default (SoftMaskable)") return; - - graphic.material = null; - graphic.SetMaterialDirtyEx(); - }; - } -#endif - } -}