< Summary

Class:DCL.Rendering.CullingController
Assembly:CullingController
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Rendering/Culling/CullingController.cs
Covered lines:161
Uncovered lines:49
Coverable lines:210
Total lines:539
Line coverage:76.6% (161 of 210)
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%31.6524076.32%
IsAvatarRenderer(...)0%22.946022.22%
UpdateCoroutine()0%13.113091.67%
SetCullingForRenderer(...)0%550100%
ProcessAnimations()0%8.068090.48%
ResetObjects()0%13130100%
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
 10125        private HashSet<Renderer> hiddenRenderers = new HashSet<Renderer>();
 10126        private HashSet<Renderer> shadowlessRenderers = new HashSet<Renderer>();
 27
 28        public UniversalRenderPipelineAsset urpAsset;
 29
 9430        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
 10139        private readonly HashSet<Shader> avatarShaders = new HashSet<Shader>();
 10140        private readonly HashSet<Shader> nonAvatarShaders = new HashSet<Shader>();
 41
 30242        private BaseVariable<FeatureFlag> featureFlags => DataStore.i.featureFlags.flags;
 43
 44        public event ICullingController.DataReport OnDataReport;
 45
 46        public static CullingController Create()
 47        {
 9148            return new CullingController(
 49                GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset,
 50                new CullingControllerSettings()
 51            );
 52        }
 53
 054        private CullingController() { }
 55
 10156        public CullingController(UniversalRenderPipelineAsset urpAsset, CullingControllerSettings settings, ICullingObje
 57        {
 10158            if (cullingObjectsTracker == null)
 9159                objectsTracker = new CullingObjectsTracker();
 60            else
 1061                objectsTracker = cullingObjectsTracker;
 62
 10163            objectsTracker.SetIgnoredLayersMask(settings.ignoredLayersMask);
 64
 10165            this.urpAsset = urpAsset;
 10166            this.settings = settings;
 67
 10168            featureFlags.OnChange += OnFeatureFlagChange;
 10169            OnFeatureFlagChange(featureFlags.Get(), null);
 10170        }
 71
 72        private void OnFeatureFlagChange(FeatureFlag current, FeatureFlag previous)
 73        {
 10174            SetAnimationCulling(current.IsFeatureEnabled(ANIMATION_CULLING_STATUS_FEATURE_FLAG));
 10175        }
 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        {
 9583            if (running)
 084                return;
 85
 9586            running = true;
 9587            CommonScriptableObjects.rendererState.OnChange += OnRendererStateChange;
 9588            CommonScriptableObjects.playerUnityPosition.OnChange += OnPlayerUnityPositionChange;
 9589            MeshesInfo.OnAnyUpdated += MarkDirty;
 9590            objectsTracker?.MarkDirty();
 9591            StartInternal();
 9592        }
 93
 94        private void StartInternal()
 95        {
 9796            if (updateCoroutine != null)
 197                return;
 98
 9699            RaiseDataReport();
 96100            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 96101            updateCoroutine = CoroutineStarter.Start(UpdateCoroutine());
 96102        }
 103
 104        /// <summary>
 105        /// Stops culling update coroutine.
 106        /// </summary>
 107        public void Stop()
 108        {
 105109            if (!running)
 10110                return;
 111
 95112            running = false;
 95113            CommonScriptableObjects.rendererState.OnChange -= OnRendererStateChange;
 95114            CommonScriptableObjects.playerUnityPosition.OnChange -= OnPlayerUnityPositionChange;
 95115            MeshesInfo.OnAnyUpdated -= MarkDirty;
 95116            StopInternal();
 95117            objectsTracker?.ForcePopulateRenderersList(true);
 95118            ResetObjects();
 95119        }
 120
 121        private void StopInternal()
 122        {
 99123            if (updateCoroutine == null)
 3124                return;
 125
 96126            CoroutineStarter.Stop(updateCoroutine);
 96127            updateCoroutine = null;
 96128        }
 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        {
 137            Renderer[] renderers;
 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
 146
 80147            for (var i = 0; i < renderers.Length; i++)
 148            {
 23149                if (timeBudgetCount > settings.maxTimeBudget)
 150                {
 0151                    timeBudgetCount = 0;
 0152                    yield return null;
 153                }
 154
 23155                Renderer r = renderers[i];
 156
 23157                if (r == null)
 158                    continue;
 159
 23160                bool rendererIsInIgnoreLayer = ((1 << r.gameObject.layer) & settings.ignoredLayersMask) != 0;
 161
 23162                if (rendererIsInIgnoreLayer)
 163                {
 0164                    SetCullingForRenderer(r, true, true);
 0165                    continue;
 166                }
 167
 23168                float startTime = Time.realtimeSinceStartup;
 169
 170                //NOTE(Brian): Need to retrieve positions every frame to take into account
 171                //             world repositioning.
 23172                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 173
 23174                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 175
 23176                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 23177                float distance = Vector3.Distance(playerPosition, boundingPoint);
 23178                float boundsSize = bounds.size.magnitude;
 23179                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 180
 23181                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 182
 23183                bool shouldBeVisible =
 184                    // all objects are visible if culling is off
 185                    !settings.enableObjectCulling
 186                    // or if the player is inside the bounding box of the object
 187                    || bounds.Contains(playerPosition)
 188                    // or if the player distance is below the threshold
 189                    || distance < profile.visibleDistanceThreshold
 190                    // at last, we perform the expensive queries of emmisiveness and opaque conditions
 191                    // these are the last conditions because IsEmissive and IsOpaque perform expensive lookups
 192                    || viewportSize > profile.emissiveSizeThreshold && IsEmissive(r)
 193                    || viewportSize > profile.opaqueSizeThreshold && IsOpaque(r)
 194                ;
 195
 23196                bool shouldHaveShadow = !settings.enableShadowCulling || TestRendererShadowRule(profile, viewportSize, d
 197
 23198                if (r is SkinnedMeshRenderer skr)
 199                {
 5200                    Material mat = skr.sharedMaterial;
 201
 5202                    if (IsAvatarRenderer(mat))
 203                    {
 0204                        shouldHaveShadow &= TestAvatarShadowRule(profile, distance);
 205                    }
 206
 5207                    skr.updateWhenOffscreen = TestSkinnedRendererOffscreenRule(settings, distance);
 208                }
 209
 23210                if (OnDataReport != null)
 211                {
 0212                    if (!shouldBeVisible && !hiddenRenderers.Contains(r))
 0213                        hiddenRenderers.Add(r);
 214
 0215                    if (shouldBeVisible && !shouldHaveShadow && !shadowlessRenderers.Contains(r))
 0216                        shadowlessRenderers.Add(r);
 217                }
 218
 23219                SetCullingForRenderer(r, shouldBeVisible, shouldHaveShadow);
 220#if UNITY_EDITOR
 221                if (DRAW_GIZMOS) DrawDebugGizmos(shouldBeVisible, bounds, boundingPoint);
 222#endif
 23223                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 224
 225            }
 17226        }
 227
 228        /// <summary>
 229        /// Checks if the material is from an Avatar by checking if the shader is DCL/Toon Shader
 230        /// This Method avoids the allocation of the name getter by storing the result on a HashSet
 231        /// </summary>
 232        /// <param name="mat"></param>
 233        /// <returns></returns>
 234        private bool IsAvatarRenderer(Material mat)
 235        {
 5236            if (mat != null && mat.shader != null)
 237            {
 0238                Shader matShader = mat.shader;
 239
 0240                if (!avatarShaders.Contains(matShader) && !nonAvatarShaders.Contains(matShader))
 241                {
 242                    // This allocates memory on the GC
 0243                    bool isAvatar = matShader.name == "DCL/Toon Shader";
 244
 0245                    if (isAvatar)
 0246                        avatarShaders.Add(matShader);
 247                    else
 0248                        nonAvatarShaders.Add(matShader);
 249                }
 250
 0251                return avatarShaders.Contains(matShader);
 252
 253            }
 254
 5255            return false;
 256        }
 257
 258        /// <summary>
 259        /// Main culling loop. Controlled by Start() and Stop() methods.
 260        /// </summary>
 261        IEnumerator UpdateCoroutine()
 262        {
 5263            while (true)
 264            {
 1157265                bool shouldCheck = objectPositionsDirty || playerPositionDirty;
 266
 1157267                playerPositionDirty = false;
 1157268                objectPositionsDirty = false;
 269
 1157270                if (!shouldCheck)
 271                {
 1144272                    timeBudgetCount = 0;
 1144273                    yield return null;
 1056274                    continue;
 275                }
 276
 13277                yield return objectsTracker.PopulateRenderersList();
 278
 8279                if (resetObjectsNextFrame)
 280                {
 4281                    ResetObjects();
 4282                    resetObjectsNextFrame = false;
 283                }
 284
 8285                yield return ProcessAnimations();
 286
 8287                if (OnDataReport != null)
 288                {
 0289                    hiddenRenderers.Clear();
 0290                    shadowlessRenderers.Clear();
 291                }
 292
 8293                int profilesCount = profiles.Count;
 294
 40295                for (var pIndex = 0; pIndex < profilesCount; pIndex++)
 296                {
 15297                    yield return ProcessProfile(profiles[pIndex]);
 298                }
 299
 5300                RaiseDataReport();
 5301                timeBudgetCount = 0;
 5302                yield return null;
 303            }
 304        }
 305
 306        /// <summary>
 307        /// Sets shadows and visibility for a given renderer.
 308        /// </summary>
 309        /// <param name="r">Renderer to be culled</param>
 310        /// <param name="shouldBeVisible">If false, the renderer visibility will be set to false.</param>
 311        /// <param name="shouldHaveShadow">If false, the renderer shadow will be toggled off.</param>
 312        internal void SetCullingForRenderer(Renderer r, bool shouldBeVisible, bool shouldHaveShadow)
 313        {
 33314            var targetMode = shouldHaveShadow ? ShadowCastingMode.On : ShadowCastingMode.Off;
 315
 33316            if (r.forceRenderingOff != !shouldBeVisible)
 14317                r.forceRenderingOff = !shouldBeVisible;
 318
 33319            if (r.shadowCastingMode != targetMode)
 15320                r.shadowCastingMode = targetMode;
 33321        }
 322
 323        /// <summary>
 324        /// Sets cullingType to all tracked animation components according to our culling rules.
 325        /// </summary>
 326        /// <returns>IEnumerator to be yielded.</returns>
 327        internal IEnumerator ProcessAnimations()
 328        {
 12329            if (!settings.enableAnimationCulling)
 9330                yield break;
 331
 3332            Animation[] animations = objectsTracker.GetAnimations();
 3333            int animsLength = animations.Length;
 334
 24335            for (var i = 0; i < animsLength; i++)
 336            {
 9337                if (timeBudgetCount > settings.maxTimeBudget)
 338                {
 0339                    timeBudgetCount = 0;
 0340                    yield return null;
 341                }
 342
 9343                Animation anim = animations[i];
 344
 9345                if (anim == null)
 346                    continue;
 347
 9348                float startTime = Time.realtimeSinceStartup;
 9349                Transform t = anim.transform;
 350
 9351                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 9352                float distance = Vector3.Distance(playerPosition, t.position);
 353
 9354                if (distance > settings.enableAnimationCullingDistance)
 4355                    anim.cullingType = AnimationCullingType.BasedOnRenderers;
 356                else
 5357                    anim.cullingType = AnimationCullingType.AlwaysAnimate;
 358
 9359                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 360            }
 3361        }
 362
 363        /// <summary>
 364        /// Reset all tracked renderers properties. Needed when toggling or changing settings.
 365        /// </summary>
 366        internal void ResetObjects()
 367        {
 100368            var skinnedRenderers = objectsTracker.GetSkinnedRenderers();
 100369            var renderers = objectsTracker.GetRenderers();
 100370            var animations = objectsTracker.GetAnimations();
 371
 202372            for (var i = 0; i < skinnedRenderers?.Length; i++)
 373            {
 1374                if (skinnedRenderers[i] != null)
 1375                    skinnedRenderers[i].updateWhenOffscreen = true;
 376            }
 377
 202378            for (var i = 0; i < animations?.Length; i++)
 379            {
 1380                if (animations[i] != null)
 1381                    animations[i].cullingType = AnimationCullingType.AlwaysAnimate;
 382            }
 383
 982384            for (var i = 0; i < renderers?.Length; i++)
 385            {
 391386                if (renderers[i] != null)
 391387                    renderers[i].forceRenderingOff = false;
 388            }
 100389        }
 390
 391        public void Dispose()
 392        {
 100393            objectsTracker.Dispose();
 100394            Stop();
 100395            featureFlags.OnChange -= OnFeatureFlagChange;
 100396        }
 397
 398        public void Initialize()
 399        {
 91400            Start();
 91401        }
 402
 403        /// <summary>
 404        /// Method suscribed to renderer state change
 405        /// </summary>
 406        private void OnRendererStateChange(bool rendererState, bool oldRendererState)
 407        {
 6408            if (!running)
 0409                return;
 410
 6411            MarkDirty();
 412
 6413            if (rendererState)
 2414                StartInternal();
 415            else
 4416                StopInternal();
 4417        }
 418
 419        /// <summary>
 420        /// Method suscribed to playerUnityPosition change
 421        /// </summary>
 72422        private void OnPlayerUnityPositionChange(Vector3 previous, Vector3 current) { playerPositionDirty = true; }
 423
 424        /// <summary>
 425        /// Sets the scene objects dirtiness.
 426        /// In the next update iteration, all the scene objects are going to be gathered.
 427        /// This method has performance impact.
 428        /// </summary>
 24429        public void MarkDirty() { objectPositionsDirty = true; }
 430
 431        /// <summary>
 432        /// Gets the scene objects dirtiness.
 433        /// </summary>
 3434        public bool IsDirty() { return objectPositionsDirty; }
 435
 436        /// <summary>
 437        /// Set settings. This will dirty the scene objects and has performance impact.
 438        /// </summary>
 439        /// <param name="settings">Settings to be set</param>
 440        public void SetSettings(CullingControllerSettings settings)
 441        {
 18442            this.settings = settings;
 18443            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 444
 18445            objectsTracker?.SetIgnoredLayersMask(settings.ignoredLayersMask);
 18446            objectsTracker?.MarkDirty();
 18447            MarkDirty();
 18448            resetObjectsNextFrame = true;
 18449        }
 450
 451        /// <summary>
 452        /// Get current settings copy. If you need to modify it, you must set them via SetSettings afterwards.
 453        /// </summary>
 454        /// <returns>Current settings object copy.</returns>
 7455        public CullingControllerSettings GetSettingsCopy() { return settings.Clone(); }
 456
 457        /// <summary>
 458        /// Enable or disable object visibility culling.
 459        /// </summary>
 460        /// <param name="enabled">If disabled, object visibility culling will be toggled.
 461        /// </param>
 462        public void SetObjectCulling(bool enabled)
 463        {
 0464            if (settings.enableObjectCulling == enabled)
 0465                return;
 466
 0467            settings.enableObjectCulling = enabled;
 0468            resetObjectsNextFrame = true;
 0469            MarkDirty();
 0470            objectsTracker?.MarkDirty();
 0471        }
 472
 473        /// <summary>
 474        /// Enable or disable animation culling.
 475        /// </summary>
 476        /// <param name="enabled">If disabled, animation culling will be toggled.</param>
 477        public void SetAnimationCulling(bool enabled)
 478        {
 101479            if (settings.enableAnimationCulling == enabled)
 101480                return;
 481
 0482            settings.enableAnimationCulling = enabled;
 0483            resetObjectsNextFrame = true;
 0484            MarkDirty();
 0485            objectsTracker?.MarkDirty();
 0486        }
 487
 488        /// <summary>
 489        /// Enable or disable shadow culling
 490        /// </summary>
 491        /// <param name="enabled">If disabled, no shadows will be toggled.</param>
 492        public void SetShadowCulling(bool enabled)
 493        {
 0494            if (settings.enableShadowCulling == enabled)
 0495                return;
 496
 0497            settings.enableShadowCulling = enabled;
 0498            resetObjectsNextFrame = true;
 0499            MarkDirty();
 0500            objectsTracker?.MarkDirty();
 0501        }
 502
 503        /// <summary>
 504        /// Fire the DataReport event. This will be useful for showing stats in a debug panel.
 505        /// </summary>
 506        private void RaiseDataReport()
 507        {
 101508            if (OnDataReport == null)
 101509                return;
 510
 0511            int rendererCount = (objectsTracker.GetRenderers()?.Length ?? 0) + (objectsTracker.GetSkinnedRenderers()?.Le
 512
 0513            OnDataReport.Invoke(rendererCount, hiddenRenderers.Count, shadowlessRenderers.Count);
 0514        }
 515
 516        /// <summary>
 517        /// Returns true if the culling loop is running
 518        /// </summary>
 519        public bool IsRunning()
 520        {
 6521            return updateCoroutine != null;
 522        }
 523
 524        /// <summary>
 525        /// Draw debug gizmos on the scene view.
 526        /// </summary>
 527        /// <param name="shouldBeVisible"></param>
 528        /// <param name="bounds"></param>
 529        /// <param name="boundingPoint"></param>
 530        private static void DrawDebugGizmos(bool shouldBeVisible, Bounds bounds, Vector3 boundingPoint)
 531        {
 0532            if (!shouldBeVisible)
 533            {
 0534                CullingControllerUtils.DrawBounds(bounds, Color.blue, 1);
 0535                CullingControllerUtils.DrawBounds(new Bounds() { center = boundingPoint, size = Vector3.one }, Color.red
 536            }
 0537        }
 538    }
 539}