< Summary

Class:DCL.Rendering.CullingController
Assembly:CullingController
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Rendering/Culling/CullingController.cs
Covered lines:162
Uncovered lines:72
Coverable lines:234
Total lines:571
Line coverage:69.2% (162 of 234)
Covered branches:0
Total branches:0

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%
StartInternal()0%220100%
Stop()0%330100%
StopInternal()0%220100%
ProcessProfile()0%6.076087.5%
ProcessProfileWithEnabledCulling()0%28.5821074.19%
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 bool DRAW_GIZMOS = false;
 21        internal List<CullingControllerProfile> profiles = null;
 22
 23        private CullingControllerSettings settings;
 24
 10325        private HashSet<Renderer> hiddenRenderers = new HashSet<Renderer>();
 10326        private HashSet<Renderer> shadowlessRenderers = new HashSet<Renderer>();
 27
 28        public UniversalRenderPipelineAsset urpAsset;
 29
 9630        public ICullingObjectsTracker objectsTracker { get; private set; }
 31        private Coroutine updateCoroutine;
 32        private float timeBudgetCount = 0;
 33        private bool resetObjectsNextFrame = false;
 34        private bool playerPositionDirty;
 35        private bool objectPositionsDirty;
 36        private bool running = false;
 37
 38        // Cache to avoid allocations when getting names
 10339        private readonly HashSet<Shader> avatarShaders = new HashSet<Shader>();
 10340        private readonly HashSet<Shader> nonAvatarShaders = new HashSet<Shader>();
 41
 30842        private BaseVariable<FeatureFlag> featureFlags => DataStore.i.featureFlags.flags;
 43
 44        public event ICullingController.DataReport OnDataReport;
 45
 46        public static CullingController Create()
 47        {
 9348            return new CullingController(
 49                GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset,
 50                new CullingControllerSettings()
 51            );
 52        }
 53
 054        private CullingController() { }
 55
 10356        public CullingController(UniversalRenderPipelineAsset urpAsset, CullingControllerSettings settings, ICullingObje
 57        {
 10358            if (cullingObjectsTracker == null)
 9359                objectsTracker = new CullingObjectsTracker();
 60            else
 1061                objectsTracker = cullingObjectsTracker;
 62
 10363            objectsTracker.SetIgnoredLayersMask(settings.ignoredLayersMask);
 64
 10365            this.urpAsset = urpAsset;
 10366            this.settings = settings;
 67
 10368            featureFlags.OnChange += OnFeatureFlagChange;
 10369            OnFeatureFlagChange(featureFlags.Get(), null);
 10370        }
 71
 72        private void OnFeatureFlagChange(FeatureFlag current, FeatureFlag previous)
 73        {
 10374            SetAnimationCulling(current.IsFeatureEnabled(ANIMATION_CULLING_STATUS_FEATURE_FLAG));
 10375        }
 76
 77        /// <summary>
 78        /// Starts culling update coroutine.
 79        /// The coroutine will keep running until Stop() is called or this class is disposed.
 80        /// </summary>
 81        public void Start()
 82        {
 9783            if (running)
 084                return;
 85
 9786            running = true;
 9787            CommonScriptableObjects.rendererState.OnChange += OnRendererStateChange;
 9788            CommonScriptableObjects.playerUnityPosition.OnChange += OnPlayerUnityPositionChange;
 9789            MeshesInfo.OnAnyUpdated += MarkDirty;
 9790            objectsTracker?.MarkDirty();
 9791            StartInternal();
 9792        }
 93
 94        private void StartInternal()
 95        {
 9996            if (updateCoroutine != null)
 197                return;
 98
 9899            RaiseDataReport();
 98100            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 98101            updateCoroutine = CoroutineStarter.Start(UpdateCoroutine());
 98102        }
 103
 104        /// <summary>
 105        /// Stops culling update coroutine.
 106        /// </summary>
 107        public void Stop()
 108        {
 107109            if (!running)
 10110                return;
 111
 97112            running = false;
 97113            CommonScriptableObjects.rendererState.OnChange -= OnRendererStateChange;
 97114            CommonScriptableObjects.playerUnityPosition.OnChange -= OnPlayerUnityPositionChange;
 97115            MeshesInfo.OnAnyUpdated -= MarkDirty;
 97116            StopInternal();
 97117            objectsTracker?.ForcePopulateRenderersList(true);
 97118            ResetObjects();
 97119        }
 120
 121        private void StopInternal()
 122        {
 101123            if (updateCoroutine == null)
 3124                return;
 125
 98126            CoroutineStarter.Stop(updateCoroutine);
 98127            updateCoroutine = null;
 98128        }
 129
 130        /// <summary>
 131        /// Process all sceneObject renderers with the parameters set by the given profile.
 132        /// </summary>
 133        /// <param name="profile">any CullingControllerProfile</param>
 134        /// <returns>IEnumerator to be yielded.</returns>
 135        internal IEnumerator ProcessProfile(CullingControllerProfile profile)
 136        {
 17137            IEnumerable<Renderer> renderers = null;
 138
 139            // If profile matches the skinned renderer profile in settings,
 140            // the skinned renderers are going to be used.
 17141            if (profile == settings.rendererProfile)
 9142                renderers = objectsTracker.GetRenderers();
 143            else
 8144                renderers = objectsTracker.GetSkinnedRenderers();
 145
 17146            if (settings.enableShadowCulling)
 17147                yield return ProcessProfileWithEnabledCulling(profile, renderers);
 148            else
 0149                yield return ProcessProfileWithDisabledCulling(profile, renderers);
 14150        }
 151
 152        internal IEnumerator ProcessProfileWithEnabledCulling(CullingControllerProfile profile, IEnumerable<Renderer> re
 153        {
 17154            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 17155            float currentStartTime = Time.realtimeSinceStartup;
 156
 80157            foreach (Renderer r in renderers)
 158            {
 23159                if (r == null)
 160                    continue;
 161
 23162                if (Time.realtimeSinceStartup - currentStartTime >= settings.maxTimeBudget)
 163                {
 0164                    yield return null;
 0165                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 0166                    currentStartTime = Time.realtimeSinceStartup;
 167                }
 168
 23169                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 23170                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 171
 23172                float distance = Vector3.Distance(playerPosition, boundingPoint);
 23173                float boundsSize = bounds.size.magnitude;
 23174                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 175
 23176                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 177
 23178                bool shouldBeVisible =
 179                    distance < profile.visibleDistanceThreshold ||
 180                    bounds.Contains(playerPosition) ||
 181                    // At the end we perform queries for emissive and opaque conditions
 182                    // these are the last conditions because IsEmissive and IsOpaque are a bit more costly
 183                    viewportSize > profile.emissiveSizeThreshold && IsEmissive(r) ||
 184                    viewportSize > profile.opaqueSizeThreshold && IsOpaque(r)
 185                ;
 186
 23187                bool shouldHaveShadow = !settings.enableShadowCulling || TestRendererShadowRule(profile, viewportSize, d
 188
 23189                if (r is SkinnedMeshRenderer skr)
 190                {
 5191                    Material mat = skr.sharedMaterial;
 192
 5193                    if (IsAvatarRenderer(mat))
 0194                        shouldHaveShadow &= TestAvatarShadowRule(profile, distance);
 195
 5196                    skr.updateWhenOffscreen = false;
 197                }
 198
 23199                if (OnDataReport != null)
 200                {
 0201                    if (!shouldBeVisible && !hiddenRenderers.Contains(r))
 0202                        hiddenRenderers.Add(r);
 203
 0204                    if (shouldBeVisible && !shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0205                        shadowlessRenderers.Add(r);
 206                }
 207
 23208                SetCullingForRenderer(r, shouldBeVisible, shouldHaveShadow);
 209#if UNITY_EDITOR
 210                if (DRAW_GIZMOS)
 211                    DrawDebugGizmos(shouldBeVisible, bounds, boundingPoint);
 212#endif
 23213            }
 17214        }
 215
 216        internal IEnumerator ProcessProfileWithDisabledCulling(CullingControllerProfile profile, IEnumerable<Renderer> r
 217        {
 0218            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 0219            float currentStartTime = Time.realtimeSinceStartup;
 0220            foreach (Renderer r in renderers)
 221            {
 0222                if (r == null)
 223                    continue;
 224
 0225                if (Time.realtimeSinceStartup - currentStartTime >= settings.maxTimeBudget)
 226                {
 0227                    yield return null;
 0228                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 0229                    currentStartTime = Time.realtimeSinceStartup;
 230                }
 231
 0232                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 0233                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 234
 0235                float distance = Vector3.Distance(playerPosition, boundingPoint);
 0236                float boundsSize = bounds.size.magnitude;
 0237                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 238
 0239                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 0240                bool shouldHaveShadow = TestRendererShadowRule(profile, viewportSize, distance, shadowTexelSize);
 241
 0242                if (r is SkinnedMeshRenderer skr)
 0243                    skr.updateWhenOffscreen = false;
 244
 0245                if (OnDataReport != null)
 246                {
 0247                    if (!shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0248                        shadowlessRenderers.Add(r);
 249                }
 250
 0251                SetCullingForRenderer(r, true, shouldHaveShadow);
 252
 253#if UNITY_EDITOR
 254                if (DRAW_GIZMOS)
 255                    DrawDebugGizmos(true, bounds, boundingPoint);
 256#endif
 0257            }
 0258        }
 259
 260        /// <summary>
 261        /// Checks if the material is from an Avatar by checking if the shader is DCL/Toon Shader
 262        /// This Method avoids the allocation of the name getter by storing the result on a HashSet
 263        /// </summary>
 264        /// <param name="mat"></param>
 265        /// <returns></returns>
 266        private bool IsAvatarRenderer(Material mat)
 267        {
 5268            if (mat != null && mat.shader != null)
 269            {
 0270                Shader matShader = mat.shader;
 271
 0272                if (!avatarShaders.Contains(matShader) && !nonAvatarShaders.Contains(matShader))
 273                {
 274                    // This allocates memory on the GC
 0275                    bool isAvatar = matShader.name == "DCL/Toon Shader";
 276
 0277                    if (isAvatar)
 0278                        avatarShaders.Add(matShader);
 279                    else
 0280                        nonAvatarShaders.Add(matShader);
 281                }
 282
 0283                return avatarShaders.Contains(matShader);
 284
 285            }
 286
 5287            return false;
 288        }
 289
 290        /// <summary>
 291        /// Main culling loop. Controlled by Start() and Stop() methods.
 292        /// </summary>
 293        IEnumerator UpdateCoroutine()
 294        {
 5295            while (true)
 296            {
 1164297                bool shouldCheck = objectPositionsDirty || playerPositionDirty;
 298
 1164299                playerPositionDirty = false;
 1164300                objectPositionsDirty = false;
 301
 1164302                if (!shouldCheck)
 303                {
 1151304                    timeBudgetCount = 0;
 1151305                    yield return null;
 1061306                    continue;
 307                }
 308
 13309                yield return objectsTracker.PopulateRenderersList();
 310
 8311                if (resetObjectsNextFrame)
 312                {
 4313                    ResetObjects();
 4314                    resetObjectsNextFrame = false;
 315                }
 316
 8317                yield return ProcessAnimations();
 318
 8319                if (OnDataReport != null)
 320                {
 0321                    hiddenRenderers.Clear();
 0322                    shadowlessRenderers.Clear();
 323                }
 324
 8325                int profilesCount = profiles.Count;
 326
 40327                for (int pIndex = 0; pIndex < profilesCount; pIndex++)
 328                {
 15329                    yield return ProcessProfile(profiles[pIndex]);
 330                }
 331
 5332                RaiseDataReport();
 5333                timeBudgetCount = 0;
 5334                yield return null;
 335            }
 336        }
 337
 338        /// <summary>
 339        /// Sets shadows and visibility for a given renderer.
 340        /// </summary>
 341        /// <param name="r">Renderer to be culled</param>
 342        /// <param name="shouldBeVisible">If false, the renderer visibility will be set to false.</param>
 343        /// <param name="shouldHaveShadow">If false, the renderer shadow will be toggled off.</param>
 344        internal void SetCullingForRenderer(Renderer r, bool shouldBeVisible, bool shouldHaveShadow)
 345        {
 33346            var targetMode = shouldHaveShadow ? ShadowCastingMode.On : ShadowCastingMode.Off;
 347
 33348            if (r.forceRenderingOff != !shouldBeVisible)
 14349                r.forceRenderingOff = !shouldBeVisible;
 350
 33351            if (r.shadowCastingMode != targetMode)
 15352                r.shadowCastingMode = targetMode;
 33353        }
 354
 355        /// <summary>
 356        /// Sets cullingType to all tracked animation components according to our culling rules.
 357        /// </summary>
 358        /// <returns>IEnumerator to be yielded.</returns>
 359        internal IEnumerator ProcessAnimations()
 360        {
 12361            if (!settings.enableAnimationCulling)
 9362                yield break;
 363
 3364            Animation[] animations = objectsTracker.GetAnimations();
 3365            int animsLength = animations.Length;
 366
 24367            for (var i = 0; i < animsLength; i++)
 368            {
 9369                if (timeBudgetCount > settings.maxTimeBudget)
 370                {
 0371                    timeBudgetCount = 0;
 0372                    yield return null;
 373                }
 374
 9375                Animation anim = animations[i];
 376
 9377                if (anim == null)
 378                    continue;
 379
 9380                float startTime = Time.realtimeSinceStartup;
 9381                Transform t = anim.transform;
 382
 9383                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 9384                float distance = Vector3.Distance(playerPosition, t.position);
 385
 9386                if (distance > settings.enableAnimationCullingDistance)
 4387                    anim.cullingType = AnimationCullingType.BasedOnRenderers;
 388                else
 5389                    anim.cullingType = AnimationCullingType.AlwaysAnimate;
 390
 9391                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 392            }
 3393        }
 394
 395        /// <summary>
 396        /// Reset all tracked renderers properties. Needed when toggling or changing settings.
 397        /// </summary>
 398        internal void ResetObjects()
 399        {
 102400            IEnumerable<Renderer> renderers = objectsTracker.GetRenderers();
 102401            IEnumerable<SkinnedMeshRenderer> skinnedRenderers = objectsTracker.GetSkinnedRenderers();
 102402            Animation[] animations = objectsTracker.GetAnimations();
 403
 1010404            foreach (Renderer renderer in renderers)
 405            {
 403406                if (renderer != null)
 403407                    renderer.forceRenderingOff = false;
 408            }
 409
 206410            foreach (SkinnedMeshRenderer skinnedRenderer in skinnedRenderers)
 411            {
 1412                if (skinnedRenderer != null)
 1413                    skinnedRenderer.updateWhenOffscreen = true;
 414            }
 415
 206416            for (int i = 0; i < animations?.Length; i++)
 417            {
 1418                if (animations[i] != null)
 1419                    animations[i].cullingType = AnimationCullingType.AlwaysAnimate;
 420            }
 102421        }
 422
 423        public void Dispose()
 424        {
 102425            objectsTracker.Dispose();
 102426            Stop();
 102427            featureFlags.OnChange -= OnFeatureFlagChange;
 102428        }
 429
 430        public void Initialize()
 431        {
 93432            Start();
 93433        }
 434
 435        /// <summary>
 436        /// Method suscribed to renderer state change
 437        /// </summary>
 438        private void OnRendererStateChange(bool rendererState, bool oldRendererState)
 439        {
 6440            if (!running)
 0441                return;
 442
 6443            MarkDirty();
 444
 6445            if (rendererState)
 2446                StartInternal();
 447            else
 4448                StopInternal();
 4449        }
 450
 451        /// <summary>
 452        /// Method suscribed to playerUnityPosition change
 453        /// </summary>
 72454        private void OnPlayerUnityPositionChange(Vector3 previous, Vector3 current) { playerPositionDirty = true; }
 455
 456        /// <summary>
 457        /// Sets the scene objects dirtiness.
 458        /// In the next update iteration, all the scene objects are going to be gathered.
 459        /// This method has performance impact.
 460        /// </summary>
 24461        public void MarkDirty() { objectPositionsDirty = true; }
 462
 463        /// <summary>
 464        /// Gets the scene objects dirtiness.
 465        /// </summary>
 3466        public bool IsDirty() { return objectPositionsDirty; }
 467
 468        /// <summary>
 469        /// Set settings. This will dirty the scene objects and has performance impact.
 470        /// </summary>
 471        /// <param name="settings">Settings to be set</param>
 472        public void SetSettings(CullingControllerSettings settings)
 473        {
 18474            this.settings = settings;
 18475            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 476
 18477            objectsTracker?.SetIgnoredLayersMask(settings.ignoredLayersMask);
 18478            objectsTracker?.MarkDirty();
 18479            MarkDirty();
 18480            resetObjectsNextFrame = true;
 18481        }
 482
 483        /// <summary>
 484        /// Get current settings copy. If you need to modify it, you must set them via SetSettings afterwards.
 485        /// </summary>
 486        /// <returns>Current settings object copy.</returns>
 7487        public CullingControllerSettings GetSettingsCopy() { return settings.Clone(); }
 488
 489        /// <summary>
 490        /// Enable or disable object visibility culling.
 491        /// </summary>
 492        /// <param name="enabled">If disabled, object visibility culling will be toggled.
 493        /// </param>
 494        public void SetObjectCulling(bool enabled)
 495        {
 0496            if (settings.enableObjectCulling == enabled)
 0497                return;
 498
 0499            settings.enableObjectCulling = enabled;
 0500            resetObjectsNextFrame = true;
 0501            MarkDirty();
 0502            objectsTracker?.MarkDirty();
 0503        }
 504
 505        /// <summary>
 506        /// Enable or disable animation culling.
 507        /// </summary>
 508        /// <param name="enabled">If disabled, animation culling will be toggled.</param>
 509        public void SetAnimationCulling(bool enabled)
 510        {
 103511            if (settings.enableAnimationCulling == enabled)
 103512                return;
 513
 0514            settings.enableAnimationCulling = enabled;
 0515            resetObjectsNextFrame = true;
 0516            MarkDirty();
 0517            objectsTracker?.MarkDirty();
 0518        }
 519
 520        /// <summary>
 521        /// Enable or disable shadow culling
 522        /// </summary>
 523        /// <param name="enabled">If disabled, no shadows will be toggled.</param>
 524        public void SetShadowCulling(bool enabled)
 525        {
 0526            if (settings.enableShadowCulling == enabled)
 0527                return;
 528
 0529            settings.enableShadowCulling = enabled;
 0530            resetObjectsNextFrame = true;
 0531            MarkDirty();
 0532            objectsTracker?.MarkDirty();
 0533        }
 534
 535        /// <summary>
 536        /// Fire the DataReport event. This will be useful for showing stats in a debug panel.
 537        /// </summary>
 538        private void RaiseDataReport()
 539        {
 103540            if (OnDataReport == null)
 103541                return;
 542
 0543            int rendererCount = (objectsTracker.GetRenderers()?.Count ?? 0) + (objectsTracker.GetSkinnedRenderers()?.Cou
 544
 0545            OnDataReport.Invoke(rendererCount, hiddenRenderers.Count, shadowlessRenderers.Count);
 0546        }
 547
 548        /// <summary>
 549        /// Returns true if the culling loop is running
 550        /// </summary>
 551        public bool IsRunning()
 552        {
 6553            return updateCoroutine != null;
 554        }
 555
 556        /// <summary>
 557        /// Draw debug gizmos on the scene view.
 558        /// </summary>
 559        /// <param name="shouldBeVisible"></param>
 560        /// <param name="bounds"></param>
 561        /// <param name="boundingPoint"></param>
 562        private static void DrawDebugGizmos(bool shouldBeVisible, Bounds bounds, Vector3 boundingPoint)
 563        {
 0564            if (!shouldBeVisible)
 565            {
 0566                DrawBounds(bounds, Color.blue, 1);
 0567                DrawBounds(new Bounds() { center = boundingPoint, size = Vector3.one }, Color.red, 1);
 568            }
 0569        }
 570    }
 571}