< Summary

Class:DCL.Rendering.CullingController
Assembly:CullingController
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Rendering/Culling/CullingController.cs
Covered lines:164
Uncovered lines:68
Coverable lines:232
Total lines:570
Line coverage:70.6% (164 of 232)
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%
Restart()0%110100%
StartInternal()0%220100%
Stop()0%330100%
StopInternal()0%220100%
ProcessProfile()0%660100%
ProcessProfileWithEnabledCulling()0%22.8521083.87%
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
 11425        private HashSet<Renderer> hiddenRenderers = new HashSet<Renderer>();
 11426        private HashSet<Renderer> shadowlessRenderers = new HashSet<Renderer>();
 27
 28        public UniversalRenderPipelineAsset urpAsset;
 29
 108730        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
 11439        private readonly HashSet<Shader> avatarShaders = new HashSet<Shader>();
 11440        private readonly HashSet<Shader> nonAvatarShaders = new HashSet<Shader>();
 41
 34142        private BaseVariable<FeatureFlag> featureFlags => DataStore.i.featureFlags.flags;
 43
 44        public event ICullingController.DataReport OnDataReport;
 45
 46        public static CullingController Create()
 47        {
 10448            return new CullingController(
 49                GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset,
 50                new CullingControllerSettings()
 51            );
 52        }
 53
 054        private CullingController() { }
 55
 11456        public CullingController(UniversalRenderPipelineAsset urpAsset, CullingControllerSettings settings, ICullingObje
 57        {
 11458            if (cullingObjectsTracker == null)
 10459                objectsTracker = new CullingObjectsTracker();
 60            else
 1061                objectsTracker = cullingObjectsTracker;
 62
 11463            objectsTracker.SetIgnoredLayersMask(settings.ignoredLayersMask);
 64
 11465            this.urpAsset = urpAsset;
 11466            this.settings = settings;
 67
 11468            featureFlags.OnChange += OnFeatureFlagChange;
 11469            OnFeatureFlagChange(featureFlags.Get(), null);
 11470        }
 71
 72        private void OnFeatureFlagChange(FeatureFlag current, FeatureFlag previous)
 73        {
 11474            SetAnimationCulling(current.IsFeatureEnabled(ANIMATION_CULLING_STATUS_FEATURE_FLAG));
 11475        }
 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        {
 11183            if (running)
 084                return;
 85
 11186            running = true;
 11187            CommonScriptableObjects.rendererState.OnChange += OnRendererStateChange;
 11188            CommonScriptableObjects.playerUnityPosition.OnChange += OnPlayerUnityPositionChange;
 11189            MeshesInfo.OnAnyUpdated += MarkDirty;
 11190            objectsTracker?.MarkDirty();
 11191            StartInternal();
 11192        }
 93
 94        public void Restart()
 95        {
 496            Stop();
 497            Start();
 498        }
 99
 100        private void StartInternal()
 101        {
 114102            if (updateCoroutine != null)
 2103                return;
 104
 112105            RaiseDataReport();
 112106            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 112107            updateCoroutine = CoroutineStarter.Start(UpdateCoroutine());
 112108        }
 109
 110        /// <summary>
 111        /// Stops culling update coroutine.
 112        /// </summary>
 113        public void Stop()
 114        {
 120115            if (!running)
 9116                return;
 117
 111118            running = false;
 111119            CommonScriptableObjects.rendererState.OnChange -= OnRendererStateChange;
 111120            CommonScriptableObjects.playerUnityPosition.OnChange -= OnPlayerUnityPositionChange;
 111121            MeshesInfo.OnAnyUpdated -= MarkDirty;
 111122            StopInternal();
 111123            objectsTracker?.ForcePopulateRenderersList();
 111124            ResetObjects();
 111125        }
 126
 127        private void StopInternal()
 128        {
 115129            if (updateCoroutine == null)
 3130                return;
 131
 112132            CoroutineStarter.Stop(updateCoroutine);
 112133            updateCoroutine = null;
 112134        }
 135
 136        /// <summary>
 137        /// Process all sceneObject renderers with the parameters set by the given profile.
 138        /// </summary>
 139        /// <param name="profile">any CullingControllerProfile</param>
 140        /// <returns>IEnumerator to be yielded.</returns>
 141        internal IEnumerator ProcessProfile(CullingControllerProfile profile)
 142        {
 143            // If profile matches the skinned renderer profile in settings the skinned renderers are going to be used.
 14144            IReadOnlyList<Renderer> renderers = profile ==
 145                settings.rendererProfile ?
 146                objectsTracker.GetRenderers() :
 147                objectsTracker.GetSkinnedRenderers();
 148
 14149            yield return settings.enableShadowCulling
 150                ? ProcessProfileWithEnabledCulling(profile, renderers)
 151                : (object)ProcessProfileWithDisabledCulling(profile, renderers);
 12152        }
 153
 154        internal IEnumerator ProcessProfileWithEnabledCulling(CullingControllerProfile profile, IReadOnlyList<Renderer> 
 155        {
 14156            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 14157            float currentStartTime = Time.realtimeSinceStartup;
 158
 50159            foreach (Renderer r in renderers)
 160            {
 11161                if (r == null)
 162                    continue;
 163
 11164                if (Time.realtimeSinceStartup - currentStartTime >= CullingControllerSettings.MAX_TIME_BUDGET)
 165                {
 1166                    yield return null;
 1167                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 1168                    currentStartTime = Time.realtimeSinceStartup;
 169                }
 170
 11171                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 11172                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 173
 11174                float distance = Vector3.Distance(playerPosition, boundingPoint);
 11175                float boundsSize = bounds.size.magnitude;
 11176                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 177
 11178                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 179
 11180                bool shouldBeVisible =
 181                    distance < profile.visibleDistanceThreshold ||
 182                    bounds.Contains(playerPosition) ||
 183                    // At the end we perform queries for emissive and opaque conditions
 184                    // these are the last conditions because IsEmissive and IsOpaque are a bit more costly
 185                    viewportSize > profile.emissiveSizeThreshold && IsEmissive(r) ||
 186                    viewportSize > profile.opaqueSizeThreshold && IsOpaque(r)
 187                ;
 188
 11189                bool shouldHaveShadow = !settings.enableShadowCulling || TestRendererShadowRule(profile, viewportSize, d
 190
 11191                if (r is SkinnedMeshRenderer skr)
 192                {
 5193                    Material mat = skr.sharedMaterial;
 194
 5195                    if (IsAvatarRenderer(mat))
 0196                        shouldHaveShadow &= TestAvatarShadowRule(profile, distance);
 197
 5198                    skr.updateWhenOffscreen = false;
 199                }
 200
 11201                if (OnDataReport != null)
 202                {
 0203                    if (!shouldBeVisible && !hiddenRenderers.Contains(r))
 0204                        hiddenRenderers.Add(r);
 205
 0206                    if (shouldBeVisible && !shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0207                        shadowlessRenderers.Add(r);
 208                }
 209
 11210                SetCullingForRenderer(r, shouldBeVisible, shouldHaveShadow);
 211#if UNITY_EDITOR
 212                if (DRAW_GIZMOS)
 213                    DrawDebugGizmos(shouldBeVisible, bounds, boundingPoint);
 214#endif
 11215            }
 14216        }
 217
 218        internal IEnumerator ProcessProfileWithDisabledCulling(CullingControllerProfile profile, IEnumerable<Renderer> r
 219        {
 0220            Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 0221            float currentStartTime = Time.realtimeSinceStartup;
 0222            foreach (Renderer r in renderers)
 223            {
 0224                if (r == null)
 225                    continue;
 226
 0227                if (Time.realtimeSinceStartup - currentStartTime >= CullingControllerSettings.MAX_TIME_BUDGET)
 228                {
 0229                    yield return null;
 0230                    playerPosition = CommonScriptableObjects.playerUnityPosition;
 0231                    currentStartTime = Time.realtimeSinceStartup;
 232                }
 233
 0234                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 0235                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 236
 0237                float distance = Vector3.Distance(playerPosition, boundingPoint);
 0238                float boundsSize = bounds.size.magnitude;
 0239                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 240
 0241                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 0242                bool shouldHaveShadow = TestRendererShadowRule(profile, viewportSize, distance, shadowTexelSize);
 243
 0244                if (r is SkinnedMeshRenderer skr)
 0245                    skr.updateWhenOffscreen = false;
 246
 0247                if (OnDataReport != null)
 248                {
 0249                    if (!shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0250                        shadowlessRenderers.Add(r);
 251                }
 252
 0253                SetCullingForRenderer(r, true, shouldHaveShadow);
 254
 255#if UNITY_EDITOR
 256                if (DRAW_GIZMOS)
 257                    DrawDebugGizmos(true, bounds, boundingPoint);
 258#endif
 0259            }
 0260        }
 261
 262        /// <summary>
 263        /// Checks if the material is from an Avatar by checking if the shader is DCL/Toon Shader
 264        /// This Method avoids the allocation of the name getter by storing the result on a HashSet
 265        /// </summary>
 266        /// <param name="mat"></param>
 267        /// <returns></returns>
 268        private bool IsAvatarRenderer(Material mat)
 269        {
 5270            if (mat != null && mat.shader != null)
 271            {
 0272                Shader matShader = mat.shader;
 273
 0274                if (!avatarShaders.Contains(matShader) && !nonAvatarShaders.Contains(matShader))
 275                {
 276                    // This allocates memory on the GC
 0277                    bool isAvatar = matShader.name == "DCL/Toon Shader";
 278
 0279                    if (isAvatar)
 0280                        avatarShaders.Add(matShader);
 281                    else
 0282                        nonAvatarShaders.Add(matShader);
 283                }
 284
 0285                return avatarShaders.Contains(matShader);
 286
 287            }
 288
 5289            return false;
 290        }
 291
 292        /// <summary>
 293        /// Main culling loop. Controlled by Start() and Stop() methods.
 294        /// </summary>
 295        IEnumerator UpdateCoroutine()
 296        {
 5297            while (true)
 298            {
 11717299                bool shouldCheck = objectPositionsDirty || playerPositionDirty;
 300
 11717301                playerPositionDirty = false;
 11717302                objectPositionsDirty = false;
 303
 11717304                if (!shouldCheck)
 305                {
 11703306                    timeBudgetCount = 0;
 11703307                    yield return null;
 11600308                    continue;
 309                }
 310
 14311                yield return objectsTracker.PopulateRenderersList();
 312
 7313                if (resetObjectsNextFrame)
 314                {
 3315                    ResetObjects();
 3316                    resetObjectsNextFrame = false;
 317                }
 318
 7319                yield return ProcessAnimations();
 320
 7321                if (OnDataReport != null)
 322                {
 0323                    hiddenRenderers.Clear();
 0324                    shadowlessRenderers.Clear();
 325                }
 326
 7327                int profilesCount = profiles.Count;
 34328                for (int profileIndex = 0; profileIndex < profilesCount; profileIndex++)
 12329                    yield return ProcessProfile(profiles[profileIndex]);
 330
 5331                RaiseDataReport();
 5332                timeBudgetCount = 0;
 5333                yield return null;
 334            }
 335        }
 336
 337        /// <summary>
 338        /// Sets shadows and visibility for a given renderer.
 339        /// </summary>
 340        /// <param name="r">Renderer to be culled</param>
 341        /// <param name="shouldBeVisible">If false, the renderer visibility will be set to false.</param>
 342        /// <param name="shouldHaveShadow">If false, the renderer shadow will be toggled off.</param>
 343        internal void SetCullingForRenderer(Renderer r, bool shouldBeVisible, bool shouldHaveShadow)
 344        {
 21345            var targetMode = shouldHaveShadow ? ShadowCastingMode.On : ShadowCastingMode.Off;
 346
 21347            if (r.forceRenderingOff != !shouldBeVisible)
 12348                r.forceRenderingOff = !shouldBeVisible;
 349
 21350            if (r.shadowCastingMode != targetMode)
 13351                r.shadowCastingMode = targetMode;
 21352        }
 353
 354        /// <summary>
 355        /// Sets cullingType to all tracked animation components according to our culling rules.
 356        /// </summary>
 357        /// <returns>IEnumerator to be yielded.</returns>
 358        internal IEnumerator ProcessAnimations()
 359        {
 11360            if (!settings.enableAnimationCulling)
 8361                yield break;
 362
 3363            Animation[] animations = objectsTracker.GetAnimations();
 3364            int animsLength = animations.Length;
 365
 24366            for (var i = 0; i < animsLength; i++)
 367            {
 9368                if (timeBudgetCount > CullingControllerSettings.MAX_TIME_BUDGET)
 369                {
 0370                    timeBudgetCount = 0;
 0371                    yield return null;
 372                }
 373
 9374                Animation anim = animations[i];
 375
 9376                if (anim == null)
 377                    continue;
 378
 9379                float startTime = Time.realtimeSinceStartup;
 9380                Transform t = anim.transform;
 381
 9382                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 9383                float distance = Vector3.Distance(playerPosition, t.position);
 384
 9385                if (distance > settings.enableAnimationCullingDistance)
 4386                    anim.cullingType = AnimationCullingType.BasedOnRenderers;
 387                else
 5388                    anim.cullingType = AnimationCullingType.AlwaysAnimate;
 389
 9390                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 391            }
 3392        }
 393
 394        /// <summary>
 395        /// Reset all tracked renderers properties. Needed when toggling or changing settings.
 396        /// </summary>
 397        internal void ResetObjects()
 398        {
 115399            IEnumerable<Renderer> renderers = objectsTracker.GetRenderers();
 115400            IEnumerable<SkinnedMeshRenderer> skinnedRenderers = objectsTracker.GetSkinnedRenderers();
 115401            Animation[] animations = objectsTracker.GetAnimations();
 402
 246403            foreach (Renderer renderer in renderers)
 404            {
 8405                if (renderer != null)
 8406                    renderer.forceRenderingOff = false;
 407            }
 408
 232409            foreach (SkinnedMeshRenderer skinnedRenderer in skinnedRenderers)
 410            {
 1411                if (skinnedRenderer != null)
 1412                    skinnedRenderer.updateWhenOffscreen = true;
 413            }
 414
 232415            for (int i = 0; i < animations?.Length; i++)
 416            {
 1417                if (animations[i] != null)
 1418                    animations[i].cullingType = AnimationCullingType.AlwaysAnimate;
 419            }
 115420        }
 421
 422        public void Dispose()
 423        {
 113424            objectsTracker.Dispose();
 113425            Stop();
 113426            featureFlags.OnChange -= OnFeatureFlagChange;
 113427        }
 428
 429        public void Initialize()
 430        {
 104431            Start();
 104432        }
 433
 434        /// <summary>
 435        /// Method suscribed to renderer state change
 436        /// </summary>
 437        private void OnRendererStateChange(bool rendererState, bool oldRendererState)
 438        {
 7439            if (!running)
 0440                return;
 441
 7442            MarkDirty();
 443
 7444            if (rendererState)
 3445                StartInternal();
 446            else
 4447                StopInternal();
 4448        }
 449
 450        /// <summary>
 451        /// Method suscribed to playerUnityPosition change
 452        /// </summary>
 78453        private void OnPlayerUnityPositionChange(Vector3 previous, Vector3 current) { playerPositionDirty = true; }
 454
 455        /// <summary>
 456        /// Sets the scene objects dirtiness.
 457        /// In the next update iteration, all the scene objects are going to be gathered.
 458        /// This method has performance impact.
 459        /// </summary>
 64460        public void MarkDirty() { objectPositionsDirty = true; }
 461
 462        /// <summary>
 463        /// Gets the scene objects dirtiness.
 464        /// </summary>
 2465        public bool IsDirty() { return objectPositionsDirty; }
 466
 467        /// <summary>
 468        /// Set settings. This will dirty the scene objects and has performance impact.
 469        /// </summary>
 470        /// <param name="settings">Settings to be set</param>
 471        public void SetSettings(CullingControllerSettings settings)
 472        {
 17473            this.settings = settings;
 17474            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 475
 17476            objectsTracker?.SetIgnoredLayersMask(settings.ignoredLayersMask);
 17477            objectsTracker?.MarkDirty();
 17478            MarkDirty();
 17479            resetObjectsNextFrame = true;
 17480        }
 481
 482        /// <summary>
 483        /// Get current settings copy. If you need to modify it, you must set them via SetSettings afterwards.
 484        /// </summary>
 485        /// <returns>Current settings object copy.</returns>
 7486        public CullingControllerSettings GetSettingsCopy() { return settings.Clone(); }
 487
 488        /// <summary>
 489        /// Enable or disable object visibility culling.
 490        /// </summary>
 491        /// <param name="enabled">If disabled, object visibility culling will be toggled.
 492        /// </param>
 493        public void SetObjectCulling(bool enabled)
 494        {
 0495            if (settings.enableObjectCulling == enabled)
 0496                return;
 497
 0498            settings.enableObjectCulling = enabled;
 0499            resetObjectsNextFrame = true;
 0500            MarkDirty();
 0501            objectsTracker?.MarkDirty();
 0502        }
 503
 504        /// <summary>
 505        /// Enable or disable animation culling.
 506        /// </summary>
 507        /// <param name="enabled">If disabled, animation culling will be toggled.</param>
 508        public void SetAnimationCulling(bool enabled)
 509        {
 114510            if (settings.enableAnimationCulling == enabled)
 114511                return;
 512
 0513            settings.enableAnimationCulling = enabled;
 0514            resetObjectsNextFrame = true;
 0515            MarkDirty();
 0516            objectsTracker?.MarkDirty();
 0517        }
 518
 519        /// <summary>
 520        /// Enable or disable shadow culling
 521        /// </summary>
 522        /// <param name="enabled">If disabled, no shadows will be toggled.</param>
 523        public void SetShadowCulling(bool enabled)
 524        {
 0525            if (settings.enableShadowCulling == enabled)
 0526                return;
 527
 0528            settings.enableShadowCulling = enabled;
 0529            resetObjectsNextFrame = true;
 0530            MarkDirty();
 0531            objectsTracker?.MarkDirty();
 0532        }
 533
 534        /// <summary>
 535        /// Fire the DataReport event. This will be useful for showing stats in a debug panel.
 536        /// </summary>
 537        private void RaiseDataReport()
 538        {
 117539            if (OnDataReport == null)
 117540                return;
 541
 0542            int rendererCount = (objectsTracker.GetRenderers()?.Count ?? 0) + (objectsTracker.GetSkinnedRenderers()?.Cou
 543
 0544            OnDataReport.Invoke(rendererCount, hiddenRenderers.Count, shadowlessRenderers.Count);
 0545        }
 546
 547        /// <summary>
 548        /// Returns true if the culling loop is running
 549        /// </summary>
 550        public bool IsRunning()
 551        {
 8552            return updateCoroutine != null;
 553        }
 554
 555        /// <summary>
 556        /// Draw debug gizmos on the scene view.
 557        /// </summary>
 558        /// <param name="shouldBeVisible"></param>
 559        /// <param name="bounds"></param>
 560        /// <param name="boundingPoint"></param>
 561        private static void DrawDebugGizmos(bool shouldBeVisible, Bounds bounds, Vector3 boundingPoint)
 562        {
 0563            if (!shouldBeVisible)
 564            {
 0565                DrawBounds(bounds, Color.blue, 1);
 0566                DrawBounds(new Bounds() { center = boundingPoint, size = Vector3.one }, Color.red, 1);
 567            }
 0568        }
 569    }
 570}