├── FUNDING.yml ├── LICENSE.md ├── README.md ├── RectMask2DCulling.cs ├── RectMask2DCulling_performance_scheme.jpg ├── RectMask2DCulling_profiling.jpg └── RectMask2D_profiling.jpg /FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: mitaywalle 2 | custom: https://boosty.to/mitaywalle 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dmitry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RectMask2DCulling 2 | Unity3d | uGUI custom RectMask2D script, that allow to disable Culling / Softness for better performance 3 | ## Problem 4 | Built-in RectMask2D has overhead, that grow linearly with count of active Child UI.Graphic, main reason for this - Culling. When all this UI.Graphic is always visible (![Pooled ScrollRectList](https://github.com/disas69/Unity-Pooled-Scroll-List) or ![Optimized ScrollView Adapter](https://assetstore.unity.com/packages/tools/gui/optimized-scrollview-adapter-68436) used, for example) - Culling is useless 5 | 6 | And you can't disable it 7 | 8 | ## Solution 9 | Inherite from RectMask2D and add flags to disable Culling 10 | ## Performance 11 | - Profiled at Honor 10X Lite 12 | - 50 child TextMeshPro 13 | - 400 child other various UI.Graphic (UI.Image, UI.RawImage, UI.Text) 14 | 15 | - RectMask2D 16 | ![](https://github.com/mitay-walle/Unity3d-RectMask2DCulling/blob/main/RectMask2D_profiling.jpg) 17 | 18 | - RectMask2DCulling , Culling and Softness disabled 19 | ![](https://github.com/mitay-walle/Unity3d-RectMask2DCulling/blob/main/RectMask2DCulling_profiling.jpg) 20 | 21 | - RectMask2DCulling use reflection to get private properties of parent RectMask2D class, and it add performance overhead 22 | 23 | ![](https://github.com/mitay-walle/Unity3d-RectMask2DCulling/blob/main/RectMask2DCulling_performance_scheme.jpg) 24 | -------------------------------------------------------------------------------- /RectMask2DCulling.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.Events; 7 | using UnityEngine.UI; 8 | 9 | namespace Plugins.UI 10 | { 11 | public class RectMask2DCulling : RectMask2D 12 | { 13 | private static FieldInfo maskablesField = typeof(RectMask2D).GetField("m_MaskableTargets", BindingFlags.Instance | BindingFlags.NonPublic); 14 | private static FieldInfo ClipTargetsField = typeof(RectMask2D).GetField("m_ClipTargets", BindingFlags.Instance | BindingFlags.NonPublic); 15 | private static FieldInfo ClippersField = typeof(RectMask2D).GetField("m_Clippers", BindingFlags.Instance | BindingFlags.NonPublic); 16 | 17 | [SerializeField] private bool m_useCulling; 18 | [SerializeField] private bool m_ForceClip = true; 19 | [SerializeField] private bool m_UseSoftness; 20 | 21 | [NonSerialized] private bool m_ShouldRecalculateClipRects; 22 | [NonSerialized] private Rect m_LastClipRectCanvasSpace; 23 | [NonSerialized] private Canvas m_Canvas; 24 | 25 | private HashSet maskableTargets = new HashSet(); 26 | private HashSet clipTargets = new HashSet(); 27 | private List clippers = new List(); 28 | 29 | private Canvas Canvas 30 | { 31 | get 32 | { 33 | if (m_Canvas == null) 34 | { 35 | var list = ListPool.Get(); 36 | gameObject.GetComponentsInParent(false, list); 37 | if (list.Count > 0) 38 | m_Canvas = list[list.Count - 1]; 39 | else 40 | m_Canvas = null; 41 | ListPool.Release(list); 42 | } 43 | 44 | return m_Canvas; 45 | } 46 | } 47 | 48 | private Vector3[] _corners = new Vector3[4]; 49 | 50 | private Rect rootCanvasRect 51 | { 52 | get 53 | { 54 | rectTransform.GetWorldCorners(_corners); 55 | 56 | if (!ReferenceEquals(Canvas, null)) 57 | { 58 | Canvas rootCanvas = Canvas.rootCanvas; 59 | for (int i = 0; i < 4; ++i) 60 | _corners[i] = rootCanvas.transform.InverseTransformPoint(_corners[i]); 61 | } 62 | 63 | return new Rect(_corners[0].x, _corners[0].y, _corners[2].x - _corners[0].x, _corners[2].y - _corners[0].y); 64 | } 65 | } 66 | 67 | protected RectMask2DCulling() 68 | { 69 | } 70 | 71 | private void ActualizeFields() 72 | { 73 | maskableTargets = maskablesField.GetValue(this) as HashSet; 74 | clipTargets = ClipTargetsField.GetValue(this) as HashSet; 75 | clippers = ClippersField.GetValue(this) as List; 76 | if (!clippers.Contains(this)) clippers.Add(this); 77 | } 78 | 79 | public override void UpdateClipSoftness() 80 | { 81 | if (!m_UseSoftness) return; 82 | 83 | if (ReferenceEquals(Canvas, null)) 84 | { 85 | return; 86 | } 87 | 88 | foreach (IClippable clipTarget in clipTargets) 89 | { 90 | clipTarget.SetClipSoftness(softness); 91 | } 92 | 93 | foreach (MaskableGraphic maskableTarget in maskableTargets) 94 | { 95 | maskableTarget.SetClipSoftness(softness); 96 | } 97 | } 98 | 99 | public override void PerformClipping() 100 | { 101 | if (ReferenceEquals(Canvas, null)) 102 | { 103 | return; 104 | } 105 | 106 | ActualizeFields(); 107 | 108 | // if the parents are changed 109 | // or something similar we 110 | // do a recalculate here 111 | if (m_ShouldRecalculateClipRects) 112 | { 113 | GetRectMasksForClip(clippers); 114 | m_ShouldRecalculateClipRects = false; 115 | } 116 | 117 | // get the compound rects from 118 | // the clippers that are valid 119 | bool validRect = true; 120 | Rect clipRect = Clipping.FindCullAndClipWorldRect(clippers, out validRect); 121 | 122 | // If the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect 123 | // overlaps that of the root canvas. 124 | RenderMode renderMode = Canvas.rootCanvas.renderMode; 125 | bool maskIsCulled = 126 | (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) && 127 | !clipRect.Overlaps(rootCanvasRect, true); 128 | 129 | if (maskIsCulled) 130 | { 131 | // Children are only displayed when inside the mask. If the mask is culled, then the children 132 | // inside the mask are also culled. In that situation, we pass an invalid rect to allow callees 133 | // to avoid some processing. 134 | clipRect = Rect.zero; 135 | validRect = false; 136 | } 137 | 138 | if (clipRect != m_LastClipRectCanvasSpace) 139 | { 140 | foreach (IClippable clipTarget in clipTargets) 141 | { 142 | clipTarget.SetClipRect(clipRect, validRect); 143 | } 144 | 145 | foreach (MaskableGraphic maskableTarget in maskableTargets) 146 | { 147 | maskableTarget.SetClipRect(clipRect, validRect); 148 | if (m_useCulling) maskableTarget.Cull(clipRect, validRect); 149 | } 150 | } 151 | else if (m_ForceClip) 152 | { 153 | foreach (IClippable clipTarget in clipTargets) 154 | { 155 | clipTarget.SetClipRect(clipRect, validRect); 156 | } 157 | 158 | foreach (MaskableGraphic maskableTarget in maskableTargets) 159 | { 160 | maskableTarget.SetClipRect(clipRect, validRect); 161 | 162 | if (m_useCulling && maskableTarget.canvasRenderer.hasMoved) 163 | maskableTarget.Cull(clipRect, validRect); 164 | } 165 | } 166 | else 167 | { 168 | foreach (MaskableGraphic maskableTarget in maskableTargets) 169 | { 170 | //Case 1170399 - hasMoved is not a valid check when animating on pivot of the object 171 | if (m_useCulling) maskableTarget.Cull(clipRect, validRect); 172 | } 173 | } 174 | 175 | m_LastClipRectCanvasSpace = clipRect; 176 | //m_ForceClip = false; 177 | 178 | UpdateClipSoftness(); 179 | } 180 | 181 | /// 182 | /// Search for all RectMask2D that apply to the given RectMask2D (includes self). 183 | /// 184 | /// Starting clipping object. 185 | /// The list of Rect masks 186 | private void GetRectMasksForClip(List masks) 187 | { 188 | masks.Clear(); 189 | 190 | List canvasComponents = ListPool.Get(); 191 | List rectMaskComponents = ListPool.Get(); 192 | transform.GetComponentsInParent(false, rectMaskComponents); 193 | 194 | if (rectMaskComponents.Count > 0) 195 | { 196 | transform.GetComponentsInParent(false, canvasComponents); 197 | for (int i = rectMaskComponents.Count - 1; i >= 0; i--) 198 | { 199 | if (!rectMaskComponents[i].IsActive()) 200 | continue; 201 | bool shouldAdd = true; 202 | for (int j = canvasComponents.Count - 1; j >= 0; j--) 203 | { 204 | if (!IsDescendantOrSelf(canvasComponents[j].transform, rectMaskComponents[i].transform) && canvasComponents[j].overrideSorting) 205 | { 206 | shouldAdd = false; 207 | break; 208 | } 209 | } 210 | 211 | if (shouldAdd) 212 | masks.Add(rectMaskComponents[i]); 213 | } 214 | } 215 | 216 | ListPool.Release(rectMaskComponents); 217 | ListPool.Release(canvasComponents); 218 | } 219 | 220 | /// 221 | /// Helper function to determine if the child is a descendant of father or is father. 222 | /// 223 | /// The transform to compare against. 224 | /// The starting transform to search up the hierarchy. 225 | /// Is child equal to father or is a descendant. 226 | public static bool IsDescendantOrSelf(Transform father, Transform child) 227 | { 228 | if (father == null || child == null) 229 | return false; 230 | 231 | if (father == child) 232 | return true; 233 | 234 | while (child.parent != null) 235 | { 236 | if (child.parent == father) 237 | return true; 238 | 239 | child = child.parent; 240 | } 241 | 242 | return false; 243 | } 244 | } 245 | 246 | #region Helper Classes 247 | 248 | internal static class ListPool 249 | { 250 | // Object pool to avoid allocations. 251 | private static readonly ObjectPool> s_ListPool = new ObjectPool>(null, Clear); 252 | static void Clear(List l) 253 | { 254 | l.Clear(); 255 | } 256 | 257 | public static List Get() 258 | { 259 | return s_ListPool.Get(); 260 | } 261 | 262 | public static void Release(List toRelease) 263 | { 264 | s_ListPool.Release(toRelease); 265 | } 266 | } 267 | 268 | internal class ObjectPool where T : new() 269 | { 270 | private readonly Stack m_Stack = new Stack(); 271 | private readonly UnityAction m_ActionOnGet; 272 | private readonly UnityAction m_ActionOnRelease; 273 | 274 | public int countAll { get; private set; } 275 | 276 | public int countActive 277 | { 278 | get { return countAll - countInactive; } 279 | } 280 | 281 | public int countInactive 282 | { 283 | get { return m_Stack.Count; } 284 | } 285 | 286 | public ObjectPool(UnityAction actionOnGet, UnityAction actionOnRelease) 287 | { 288 | m_ActionOnGet = actionOnGet; 289 | m_ActionOnRelease = actionOnRelease; 290 | } 291 | 292 | public T Get() 293 | { 294 | T element; 295 | if (m_Stack.Count == 0) 296 | { 297 | element = new T(); 298 | countAll++; 299 | } 300 | else 301 | { 302 | element = m_Stack.Pop(); 303 | } 304 | 305 | if (m_ActionOnGet != null) 306 | m_ActionOnGet(element); 307 | return element; 308 | } 309 | 310 | public void Release(T element) 311 | { 312 | if (m_Stack.Count > 0 && ReferenceEquals(m_Stack.Peek(), element)) 313 | Debug.LogError("Internal error. Trying to destroy object that is already released to pool."); 314 | if (m_ActionOnRelease != null) 315 | m_ActionOnRelease(element); 316 | m_Stack.Push(element); 317 | } 318 | } 319 | 320 | #endregion 321 | #if UNITY_EDITOR 322 | [CustomEditor(typeof(RectMask2DCulling))] 323 | public class RectMask2DCullingEditor : Editor 324 | { 325 | } 326 | #endif 327 | } 328 | -------------------------------------------------------------------------------- /RectMask2DCulling_performance_scheme.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitay-walle/Unity3d-RectMask2DCulling/d0c5263fdf5a95e795dceb612c7cee51ec52c118/RectMask2DCulling_performance_scheme.jpg -------------------------------------------------------------------------------- /RectMask2DCulling_profiling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitay-walle/Unity3d-RectMask2DCulling/d0c5263fdf5a95e795dceb612c7cee51ec52c118/RectMask2DCulling_profiling.jpg -------------------------------------------------------------------------------- /RectMask2D_profiling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitay-walle/Unity3d-RectMask2DCulling/d0c5263fdf5a95e795dceb612c7cee51ec52c118/RectMask2D_profiling.jpg --------------------------------------------------------------------------------