< Summary

Class:DCL.Rendering.CullingController
Assembly:CullingController
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Rendering/Culling/CullingController.cs
Covered lines:168
Uncovered lines:68
Coverable lines:236
Total lines:577
Line coverage:71.1% (168 of 236)
Covered branches:0
Total branches:0
Covered methods:30
Total methods:34
Method coverage:88.2% (30 of 34)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
CullingController()0%2100%
CullingController(...)0%220100%
Create()0%110100%
OnFeatureFlagChange(...)0%110100%
Start()0%3.013088.89%
Restart()0%110100%
StartInternal()0%220100%
Stop()0%330100%
StopInternal()0%220100%
ProcessProfile()0%660100%
ProcessProfileWithEnabledCulling()0%23.8422084.38%
ProcessProfileWithDisabledCulling()0%1101000%
IsAvatarRenderer(...)0%22.946022.22%
UpdateCoroutine()0%13.113091.67%
SetCullingForRenderer(...)0%550100%
ProcessAnimations()0%8.068090.48%
ResetObjects()0%11110100%
Dispose()0%110100%
Initialize()0%110100%
OnRendererStateChange(...)0%3.033085.71%
OnPlayerUnityPositionChange(...)0%110100%
MarkDirty()0%110100%
IsDirty()0%110100%
SetSettings(...)0%330100%
GetSettingsCopy()0%110100%
SetObjectCulling(...)0%12300%
SetAnimationCulling(...)0%6.283028.57%
SetShadowCulling(...)0%12300%
RaiseDataReport()0%13.786040%
IsRunning()0%110100%
DrawDebugGizmos(...)0%6200%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Rendering/Culling/CullingController.cs

#LineLine coverage
 1using System.Collections;
 2using System.Collections.Generic;
 3using DCL.Models;
 4using UnityEngine;
 5using UnityEngine.Rendering;
 6using UniversalRenderPipelineAsset = UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset;
 7using static DCL.Rendering.CullingControllerUtils;
 8
 9namespace DCL.Rendering
 10{
 11    /// <summary>
 12    /// CullingController has the following responsibilities:
 13    /// - Hides small renderers (detail objects).
 14    /// - Disable unneeded shadows.
 15    /// - Enable/disable animation culling for skinned renderers and animation components.
 16    /// </summary>
 17    public class CullingController : ICullingController
 18    {
 19        private const string ANIMATION_CULLING_STATUS_FEATURE_FLAG = "animation_culling_status";
 20        private const string SMR_UPDATE_OFFSCREEN_FEATURE_FLAG = "smr_update_offscreen";
 21        private const bool DRAW_GIZMOS = false;
 22        internal List<CullingControllerProfile> profiles = null;
 23
 24        private CullingControllerSettings settings;
 25
 12726        private HashSet<Renderer> hiddenRenderers = new HashSet<Renderer>();
 12727        private HashSet<Renderer> shadowlessRenderers = new HashSet<Renderer>();
 28
 29        public UniversalRenderPipelineAsset urpAsset;
 30
 119531        public ICullingObjectsTracker objectsTracker { get; private set; }
 32        private Coroutine updateCoroutine;
 33        private float timeBudgetCount = 0;
 34        private bool resetObjectsNextFrame = false;
 35        private bool playerPositionDirty;
 36        private bool objectPositionsDirty;
 37        private bool running = false;
 12738        private bool offScreenUpdate = true;
 39
 40        // Cache to avoid allocations when getting names
 12741        private readonly HashSet<Shader> avatarShaders = new HashSet<Shader>();
 12742        private readonly HashSet<Shader> nonAvatarShaders = new HashSet<Shader>();
 12743        private int previewLayer = LayerMask.NameToLayer("CharacterPreview");
 44
 38045        private BaseVariable<FeatureFlag> featureFlags => DataStore.i.featureFlags.flags;
 46
 47        public event ICullingController.DataReport OnDataReport;
 48
 49        public static CullingController Create()
 50        {
 11751            return new CullingController(
 52                GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset,
 53                new CullingControllerSettings()
 54            );
 55        }
 56
 057        private CullingController() { }
 58
 12759        public CullingController(UniversalRenderPipelineAsset urpAsset, CullingControllerSettings settings, ICullingObje
 60        {
 12761            if (cullingObjectsTracker == null)
 11762                objectsTracker = new CullingObjectsTracker();
 63            else
 1064                objectsTracker = cullingObjectsTracker;
 65
 12766            objectsTracker.SetIgnoredLayersMask(settings.ignoredLayersMask);
 67
 12768            this.urpAsset = urpAsset;
 12769            this.settings = settings;
 70
 12771            featureFlags.OnChange += OnFeatureFlagChange;
 12772            OnFeatureFlagChange(featureFlags.Get(), null);
 12773        }
 74
 75        private void OnFeatureFlagChange(FeatureFlag current, FeatureFlag previous)
 76        {
 12777            SetAnimationCulling(current.IsFeatureEnabled(ANIMATION_CULLING_STATUS_FEATURE_FLAG));
 12778            offScreenUpdate = current.IsFeatureEnabled(SMR_UPDATE_OFFSCREEN_FEATURE_FLAG);
 12779        }
 80
 81        /// <summary>
 82        /// Starts culling update coroutine.
 83        /// The coroutine will keep running until Stop() is called or this class is disposed.
 84        /// </summary>
 85        public void Start()
 86        {
 12487            if (running)
 088                return;
 89
 12490            running = true;
 12491            CommonScriptableObjects.rendererState.OnChange += OnRendererStateChange;
 12492            CommonScriptableObjects.playerUnityPosition.OnChange += OnPlayerUnityPositionChange;
 12493            MeshesInfo.OnAnyUpdated += MarkDirty;
 12494            objectsTracker?.MarkDirty();
 12495            StartInternal();
 12496        }
 97
 98        public void Restart()
 99        {
 4100            Stop();
 4101            Start();
 4102        }
 103
 104        private void StartInternal()
 105        {
 127106            if (updateCoroutine != null)
 2107                return;
 108
 125109            RaiseDataReport();
 125110            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 125111            updateCoroutine = CoroutineStarter.Start(UpdateCoroutine());
 125112        }
 113
 114        /// <summary>
 115        /// Stops culling update coroutine.
 116        /// </summary>
 117        public void Stop()
 118        {
 133119            if (!running)
 9120                return;
 121
 124122            running = false;
 124123            CommonScriptableObjects.rendererState.OnChange -= OnRendererStateChange;
 124124            CommonScriptableObjects.playerUnityPosition.OnChange -= OnPlayerUnityPositionChange;
 124125            MeshesInfo.OnAnyUpdated -= MarkDirty;
 124126            StopInternal();
 124127            objectsTracker?.ForcePopulateRenderersList();
 124128            ResetObjects();
 124129        }
 130
 131        private void StopInternal()
 132        {
 128133            if (updateCoroutine == null)
 3134                return;
 135
 125136            CoroutineStarter.Stop(updateCoroutine);
 125137            updateCoroutine = null;
 125138        }
 139
 140        /// <summary>
 141        /// Process all sceneObject renderers with the parameters set by the given profile.
 142        /// </summary>
 143        /// <param name="profile">any CullingControllerProfile</param>
 144        /// <returns>IEnumerator to be yielded.</returns>
 145        internal IEnumerator ProcessProfile(CullingControllerProfile profile)
 146        {
 147            // If profile matches the skinned renderer profile in settings the skinned renderers are going to be used.
 8148            IReadOnlyList<Renderer> renderers = profile ==
 149                settings.rendererProfile ?
 150                objectsTracker.GetRenderers() :
 151                objectsTracker.GetSkinnedRenderers();
 152
 8153            yield return settings.enableShadowCulling
 154                ? ProcessProfileWithEnabledCulling(profile, renderers)
 155                : (object)ProcessProfileWithDisabledCulling(profile, renderers);
 6156        }
 157
 158        internal IEnumerator ProcessProfileWithEnabledCulling(CullingControllerProfile profile, IReadOnlyList<Renderer> 
 159        {
 8160            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 8161            float currentStartTime = Time.realtimeSinceStartup;
 162
 34163            foreach (Renderer r in renderers)
 164            {
 9165                if (r == null)
 166                    continue;
 167
 9168                if (Time.realtimeSinceStartup - currentStartTime >= CullingControllerSettings.MAX_TIME_BUDGET)
 169                {
 1170                    yield return null;
 1171                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 1172                    currentStartTime = Time.realtimeSinceStartup;
 173                }
 174
 9175                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 9176                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 177
 9178                float distance = Vector3.Distance(playerPosition, boundingPoint);
 9179                float boundsSize = bounds.size.magnitude;
 9180                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 181
 9182                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 183
 9184                bool isIgnoredByLayer = r.gameObject.layer == previewLayer;
 185
 9186                bool shouldBeVisible =
 187                    isIgnoredByLayer ||
 188                    distance < profile.visibleDistanceThreshold ||
 189                    bounds.Contains(playerPosition) ||
 190                    // At the end we perform queries for emissive and opaque conditions
 191                    // these are the last conditions because IsEmissive and IsOpaque are a bit more costly
 192                    viewportSize > profile.emissiveSizeThreshold && IsEmissive(r) ||
 193                    viewportSize > profile.opaqueSizeThreshold && IsOpaque(r)
 194                ;
 195
 9196                bool shouldHaveShadow = !settings.enableShadowCulling || TestRendererShadowRule(profile, viewportSize, d
 197
 9198                if (r is SkinnedMeshRenderer skr)
 199                {
 5200                    Material mat = skr.sharedMaterial;
 201
 5202                    if (IsAvatarRenderer(mat))
 0203                        shouldHaveShadow &= TestAvatarShadowRule(profile, distance);
 204
 5205                    skr.updateWhenOffscreen = offScreenUpdate;
 206                }
 207
 9208                if (OnDataReport != null)
 209                {
 0210                    if (!shouldBeVisible && !hiddenRenderers.Contains(r))
 0211                        hiddenRenderers.Add(r);
 212
 0213                    if (shouldBeVisible && !shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0214                        shadowlessRenderers.Add(r);
 215                }
 216
 9217                SetCullingForRenderer(r, shouldBeVisible, shouldHaveShadow);
 218#if UNITY_EDITOR
 219                if (DRAW_GIZMOS)
 220                    DrawDebugGizmos(shouldBeVisible, bounds, boundingPoint);
 221#endif
 9222            }
 8223        }
 224
 225        internal IEnumerator ProcessProfileWithDisabledCulling(CullingControllerProfile profile, IEnumerable<Renderer> r
 226        {
 0227            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 0228            float currentStartTime = Time.realtimeSinceStartup;
 0229            foreach (Renderer r in renderers)
 230            {
 0231                if (r == null)
 232                    continue;
 233
 0234                if (Time.realtimeSinceStartup - currentStartTime >= CullingControllerSettings.MAX_TIME_BUDGET)
 235                {
 0236                    yield return null;
 0237                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 0238                    currentStartTime = Time.realtimeSinceStartup;
 239                }
 240
 0241                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 0242                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 243
 0244                float distance = Vector3.Distance(playerPosition, boundingPoint);
 0245                float boundsSize = bounds.size.magnitude;
 0246                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 247
 0248                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 0249                bool shouldHaveShadow = TestRendererShadowRule(profile, viewportSize, distance, shadowTexelSize);
 250
 0251                if (r is SkinnedMeshRenderer skr)
 0252                    skr.updateWhenOffscreen = offScreenUpdate;
 253
 0254                if (OnDataReport != null)
 255                {
 0256                    if (!shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0257                        shadowlessRenderers.Add(r);
 258                }
 259
 0260                SetCullingForRenderer(r, true, shouldHaveShadow);
 261
 262#if UNITY_EDITOR
 263                if (DRAW_GIZMOS)
 264                    DrawDebugGizmos(true, bounds, boundingPoint);
 265#endif
 0266            }
 0267        }
 268
 269        /// <summary>
 270        /// Checks if the material is from an Avatar by checking if the shader is DCL/Toon Shader
 271        /// This Method avoids the allocation of the name getter by storing the result on a HashSet
 272        /// </summary>
 273        /// <param name="mat"></param>
 274        /// <returns></returns>
 275        private bool IsAvatarRenderer(Material mat)
 276        {
 5277            if (mat != null && mat.shader != null)
 278            {
 0279                Shader matShader = mat.shader;
 280
 0281                if (!avatarShaders.Contains(matShader) && !nonAvatarShaders.Contains(matShader))
 282                {
 283                    // This allocates memory on the GC
 0284                    bool isAvatar = matShader.name == "DCL/Toon Shader";
 285
 0286                    if (isAvatar)
 0287                        avatarShaders.Add(matShader);
 288                    else
 0289                        nonAvatarShaders.Add(matShader);
 290                }
 291
 0292                return avatarShaders.Contains(matShader);
 293
 294            }
 295
 5296            return false;
 297        }
 298
 299        /// <summary>
 300        /// Main culling loop. Controlled by Start() and Stop() methods.
 301        /// </summary>
 302        IEnumerator UpdateCoroutine()
 303        {
 2304            while (true)
 305            {
 5878306                bool shouldCheck = objectPositionsDirty || playerPositionDirty;
 307
 5878308                playerPositionDirty = false;
 5878309                objectPositionsDirty = false;
 310
 5878311                if (!shouldCheck)
 312                {
 5867313                    timeBudgetCount = 0;
 5867314                    yield return null;
 5751315                    continue;
 316                }
 317
 11318                yield return objectsTracker.PopulateRenderersList();
 319
 4320                if (resetObjectsNextFrame)
 321                {
 3322                    ResetObjects();
 3323                    resetObjectsNextFrame = false;
 324                }
 325
 4326                yield return ProcessAnimations();
 327
 4328                if (OnDataReport != null)
 329                {
 0330                    hiddenRenderers.Clear();
 0331                    shadowlessRenderers.Clear();
 332                }
 333
 4334                int profilesCount = profiles.Count;
 16335                for (int profileIndex = 0; profileIndex < profilesCount; profileIndex++)
 6336                    yield return ProcessProfile(profiles[profileIndex]);
 337
 2338                RaiseDataReport();
 2339                timeBudgetCount = 0;
 2340                yield return null;
 341            }
 342        }
 343
 344        /// <summary>
 345        /// Sets shadows and visibility for a given renderer.
 346        /// </summary>
 347        /// <param name="r">Renderer to be culled</param>
 348        /// <param name="shouldBeVisible">If false, the renderer visibility will be set to false.</param>
 349        /// <param name="shouldHaveShadow">If false, the renderer shadow will be toggled off.</param>
 350        internal void SetCullingForRenderer(Renderer r, bool shouldBeVisible, bool shouldHaveShadow)
 351        {
 19352            var targetMode = shouldHaveShadow ? ShadowCastingMode.On : ShadowCastingMode.Off;
 353
 19354            if (r.forceRenderingOff != !shouldBeVisible)
 12355                r.forceRenderingOff = !shouldBeVisible;
 356
 19357            if (r.shadowCastingMode != targetMode)
 13358                r.shadowCastingMode = targetMode;
 19359        }
 360
 361        /// <summary>
 362        /// Sets cullingType to all tracked animation components according to our culling rules.
 363        /// </summary>
 364        /// <returns>IEnumerator to be yielded.</returns>
 365        internal IEnumerator ProcessAnimations()
 366        {
 8367            if (!settings.enableAnimationCulling)
 5368                yield break;
 369
 3370            Animation[] animations = objectsTracker.GetAnimations();
 3371            int animsLength = animations.Length;
 372
 24373            for (var i = 0; i < animsLength; i++)
 374            {
 9375                if (timeBudgetCount > CullingControllerSettings.MAX_TIME_BUDGET)
 376                {
 0377                    timeBudgetCount = 0;
 0378                    yield return null;
 379                }
 380
 9381                Animation anim = animations[i];
 382
 9383                if (anim == null)
 384                    continue;
 385
 9386                float startTime = Time.realtimeSinceStartup;
 9387                Transform t = anim.transform;
 388
 9389                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 9390                float distance = Vector3.Distance(playerPosition, t.position);
 391
 9392                if (distance > settings.enableAnimationCullingDistance)
 4393                    anim.cullingType = AnimationCullingType.BasedOnRenderers;
 394                else
 5395                    anim.cullingType = AnimationCullingType.AlwaysAnimate;
 396
 9397                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 398            }
 3399        }
 400
 401        /// <summary>
 402        /// Reset all tracked renderers properties. Needed when toggling or changing settings.
 403        /// </summary>
 404        internal void ResetObjects()
 405        {
 128406            IEnumerable<Renderer> renderers = objectsTracker.GetRenderers();
 128407            IEnumerable<SkinnedMeshRenderer> skinnedRenderers = objectsTracker.GetSkinnedRenderers();
 128408            Animation[] animations = objectsTracker.GetAnimations();
 409
 272410            foreach (Renderer renderer in renderers)
 411            {
 8412                if (renderer != null)
 8413                    renderer.forceRenderingOff = false;
 414            }
 415
 530416            foreach (SkinnedMeshRenderer skinnedRenderer in skinnedRenderers)
 417            {
 137418                if (skinnedRenderer != null)
 137419                    skinnedRenderer.updateWhenOffscreen = offScreenUpdate;
 420            }
 421
 258422            for (int i = 0; i < animations?.Length; i++)
 423            {
 1424                if (animations[i] != null)
 1425                    animations[i].cullingType = AnimationCullingType.AlwaysAnimate;
 426            }
 128427        }
 428
 429        public void Dispose()
 430        {
 126431            objectsTracker.Dispose();
 126432            Stop();
 126433            featureFlags.OnChange -= OnFeatureFlagChange;
 126434        }
 435
 436        public void Initialize()
 437        {
 117438            Start();
 117439        }
 440
 441        /// <summary>
 442        /// Method suscribed to renderer state change
 443        /// </summary>
 444        private void OnRendererStateChange(bool rendererState, bool oldRendererState)
 445        {
 7446            if (!running)
 0447                return;
 448
 7449            MarkDirty();
 450
 7451            if (rendererState)
 3452                StartInternal();
 453            else
 4454                StopInternal();
 4455        }
 456
 457        /// <summary>
 458        /// Method suscribed to playerUnityPosition change
 459        /// </summary>
 52460        private void OnPlayerUnityPositionChange(Vector3 previous, Vector3 current) { playerPositionDirty = true; }
 461
 462        /// <summary>
 463        /// Sets the scene objects dirtiness.
 464        /// In the next update iteration, all the scene objects are going to be gathered.
 465        /// This method has performance impact.
 466        /// </summary>
 64467        public void MarkDirty() { objectPositionsDirty = true; }
 468
 469        /// <summary>
 470        /// Gets the scene objects dirtiness.
 471        /// </summary>
 2472        public bool IsDirty() { return objectPositionsDirty; }
 473
 474        /// <summary>
 475        /// Set settings. This will dirty the scene objects and has performance impact.
 476        /// </summary>
 477        /// <param name="settings">Settings to be set</param>
 478        public void SetSettings(CullingControllerSettings settings)
 479        {
 17480            this.settings = settings;
 17481            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 482
 17483            objectsTracker?.SetIgnoredLayersMask(settings.ignoredLayersMask);
 17484            objectsTracker?.MarkDirty();
 17485            MarkDirty();
 17486            resetObjectsNextFrame = true;
 17487        }
 488
 489        /// <summary>
 490        /// Get current settings copy. If you need to modify it, you must set them via SetSettings afterwards.
 491        /// </summary>
 492        /// <returns>Current settings object copy.</returns>
 7493        public CullingControllerSettings GetSettingsCopy() { return settings.Clone(); }
 494
 495        /// <summary>
 496        /// Enable or disable object visibility culling.
 497        /// </summary>
 498        /// <param name="enabled">If disabled, object visibility culling will be toggled.
 499        /// </param>
 500        public void SetObjectCulling(bool enabled)
 501        {
 0502            if (settings.enableObjectCulling == enabled)
 0503                return;
 504
 0505            settings.enableObjectCulling = enabled;
 0506            resetObjectsNextFrame = true;
 0507            MarkDirty();
 0508            objectsTracker?.MarkDirty();
 0509        }
 510
 511        /// <summary>
 512        /// Enable or disable animation culling.
 513        /// </summary>
 514        /// <param name="enabled">If disabled, animation culling will be toggled.</param>
 515        public void SetAnimationCulling(bool enabled)
 516        {
 127517            if (settings.enableAnimationCulling == enabled)
 127518                return;
 519
 0520            settings.enableAnimationCulling = enabled;
 0521            resetObjectsNextFrame = true;
 0522            MarkDirty();
 0523            objectsTracker?.MarkDirty();
 0524        }
 525
 526        /// <summary>
 527        /// Enable or disable shadow culling
 528        /// </summary>
 529        /// <param name="enabled">If disabled, no shadows will be toggled.</param>
 530        public void SetShadowCulling(bool enabled)
 531        {
 0532            if (settings.enableShadowCulling == enabled)
 0533                return;
 534
 0535            settings.enableShadowCulling = enabled;
 0536            resetObjectsNextFrame = true;
 0537            MarkDirty();
 0538            objectsTracker?.MarkDirty();
 0539        }
 540
 541        /// <summary>
 542        /// Fire the DataReport event. This will be useful for showing stats in a debug panel.
 543        /// </summary>
 544        private void RaiseDataReport()
 545        {
 127546            if (OnDataReport == null)
 127547                return;
 548
 0549            int rendererCount = (objectsTracker.GetRenderers()?.Count ?? 0) + (objectsTracker.GetSkinnedRenderers()?.Cou
 550
 0551            OnDataReport.Invoke(rendererCount, hiddenRenderers.Count, shadowlessRenderers.Count);
 0552        }
 553
 554        /// <summary>
 555        /// Returns true if the culling loop is running
 556        /// </summary>
 557        public bool IsRunning()
 558        {
 8559            return updateCoroutine != null;
 560        }
 561
 562        /// <summary>
 563        /// Draw debug gizmos on the scene view.
 564        /// </summary>
 565        /// <param name="shouldBeVisible"></param>
 566        /// <param name="bounds"></param>
 567        /// <param name="boundingPoint"></param>
 568        private static void DrawDebugGizmos(bool shouldBeVisible, Bounds bounds, Vector3 boundingPoint)
 569        {
 0570            if (!shouldBeVisible)
 571            {
 0572                DrawBounds(bounds, Color.blue, 1);
 0573                DrawBounds(new Bounds() { center = boundingPoint, size = Vector3.one }, Color.red, 1);
 574            }
 0575        }
 576    }
 577}