< 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:49
Coverable lines:211
Total lines:537
Line coverage:76.7% (162 of 211)
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.0824076.92%
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
 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        {
 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
 146
 80147            foreach (Renderer r in renderers)
 148            {
 23149                if (timeBudgetCount > settings.maxTimeBudget)
 150                {
 0151                    timeBudgetCount = 0;
 0152                    yield return null;
 153                }
 154
 23155                if (r == null)
 156                    continue;
 157
 23158                bool rendererIsInIgnoreLayer = ((1 << r.gameObject.layer) & settings.ignoredLayersMask) != 0;
 159
 23160                if (rendererIsInIgnoreLayer)
 161                {
 0162                    SetCullingForRenderer(r, true, true);
 0163                    continue;
 164                }
 165
 23166                float startTime = Time.realtimeSinceStartup;
 167
 168                //NOTE(Brian): Need to retrieve positions every frame to take into account
 169                //             world repositioning.
 23170                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 171
 23172                Bounds bounds = MeshesInfoUtils.GetSafeBounds(r.bounds, r.transform.position);
 173
 23174                Vector3 boundingPoint = bounds.ClosestPoint(playerPosition);
 23175                float distance = Vector3.Distance(playerPosition, boundingPoint);
 23176                float boundsSize = bounds.size.magnitude;
 23177                float viewportSize = (boundsSize / distance) * Mathf.Rad2Deg;
 178
 23179                float shadowTexelSize = ComputeShadowMapTexelSize(boundsSize, urpAsset.shadowDistance, urpAsset.mainLigh
 180
 23181                bool shouldBeVisible =
 182                    // all objects are visible if culling is off
 183                    !settings.enableObjectCulling
 184                    // or if the player is inside the bounding box of the object
 185                    || bounds.Contains(playerPosition)
 186                    // or if the player distance is below the threshold
 187                    || distance < profile.visibleDistanceThreshold
 188                    // at last, we perform the expensive queries of emmisiveness and opaque conditions
 189                    // these are the last conditions because IsEmissive and IsOpaque perform expensive lookups
 190                    || viewportSize > profile.emissiveSizeThreshold && IsEmissive(r)
 191                    || viewportSize > profile.opaqueSizeThreshold && IsOpaque(r)
 192                ;
 193
 23194                bool shouldHaveShadow = !settings.enableShadowCulling || TestRendererShadowRule(profile, viewportSize, d
 195
 23196                if (r is SkinnedMeshRenderer skr)
 197                {
 5198                    Material mat = skr.sharedMaterial;
 199
 5200                    if (IsAvatarRenderer(mat))
 201                    {
 0202                        shouldHaveShadow &= TestAvatarShadowRule(profile, distance);
 203                    }
 204
 5205                    skr.updateWhenOffscreen = TestSkinnedRendererOffscreenRule(settings, distance);
 206                }
 207
 23208                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
 23217                SetCullingForRenderer(r, shouldBeVisible, shouldHaveShadow);
 218#if UNITY_EDITOR
 219                if (DRAW_GIZMOS) DrawDebugGizmos(shouldBeVisible, bounds, boundingPoint);
 220#endif
 23221                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 222
 23223            }
 17224        }
 225
 226        /// <summary>
 227        /// Checks if the material is from an Avatar by checking if the shader is DCL/Toon Shader
 228        /// This Method avoids the allocation of the name getter by storing the result on a HashSet
 229        /// </summary>
 230        /// <param name="mat"></param>
 231        /// <returns></returns>
 232        private bool IsAvatarRenderer(Material mat)
 233        {
 5234            if (mat != null && mat.shader != null)
 235            {
 0236                Shader matShader = mat.shader;
 237
 0238                if (!avatarShaders.Contains(matShader) && !nonAvatarShaders.Contains(matShader))
 239                {
 240                    // This allocates memory on the GC
 0241                    bool isAvatar = matShader.name == "DCL/Toon Shader";
 242
 0243                    if (isAvatar)
 0244                        avatarShaders.Add(matShader);
 245                    else
 0246                        nonAvatarShaders.Add(matShader);
 247                }
 248
 0249                return avatarShaders.Contains(matShader);
 250
 251            }
 252
 5253            return false;
 254        }
 255
 256        /// <summary>
 257        /// Main culling loop. Controlled by Start() and Stop() methods.
 258        /// </summary>
 259        IEnumerator UpdateCoroutine()
 260        {
 5261            while (true)
 262            {
 1162263                bool shouldCheck = objectPositionsDirty || playerPositionDirty;
 264
 1162265                playerPositionDirty = false;
 1162266                objectPositionsDirty = false;
 267
 1162268                if (!shouldCheck)
 269                {
 1149270                    timeBudgetCount = 0;
 1149271                    yield return null;
 1061272                    continue;
 273                }
 274
 13275                yield return objectsTracker.PopulateRenderersList();
 276
 9277                if (resetObjectsNextFrame)
 278                {
 5279                    ResetObjects();
 5280                    resetObjectsNextFrame = false;
 281                }
 282
 9283                yield return ProcessAnimations();
 284
 8285                if (OnDataReport != null)
 286                {
 0287                    hiddenRenderers.Clear();
 0288                    shadowlessRenderers.Clear();
 289                }
 290
 8291                int profilesCount = profiles.Count;
 292
 40293                for (var pIndex = 0; pIndex < profilesCount; pIndex++)
 294                {
 15295                    yield return ProcessProfile(profiles[pIndex]);
 296                }
 297
 5298                RaiseDataReport();
 5299                timeBudgetCount = 0;
 5300                yield return null;
 301            }
 302        }
 303
 304        /// <summary>
 305        /// Sets shadows and visibility for a given renderer.
 306        /// </summary>
 307        /// <param name="r">Renderer to be culled</param>
 308        /// <param name="shouldBeVisible">If false, the renderer visibility will be set to false.</param>
 309        /// <param name="shouldHaveShadow">If false, the renderer shadow will be toggled off.</param>
 310        internal void SetCullingForRenderer(Renderer r, bool shouldBeVisible, bool shouldHaveShadow)
 311        {
 33312            var targetMode = shouldHaveShadow ? ShadowCastingMode.On : ShadowCastingMode.Off;
 313
 33314            if (r.forceRenderingOff != !shouldBeVisible)
 14315                r.forceRenderingOff = !shouldBeVisible;
 316
 33317            if (r.shadowCastingMode != targetMode)
 15318                r.shadowCastingMode = targetMode;
 33319        }
 320
 321        /// <summary>
 322        /// Sets cullingType to all tracked animation components according to our culling rules.
 323        /// </summary>
 324        /// <returns>IEnumerator to be yielded.</returns>
 325        internal IEnumerator ProcessAnimations()
 326        {
 13327            if (!settings.enableAnimationCulling)
 10328                yield break;
 329
 3330            Animation[] animations = objectsTracker.GetAnimations();
 3331            int animsLength = animations.Length;
 332
 24333            for (var i = 0; i < animsLength; i++)
 334            {
 9335                if (timeBudgetCount > settings.maxTimeBudget)
 336                {
 0337                    timeBudgetCount = 0;
 0338                    yield return null;
 339                }
 340
 9341                Animation anim = animations[i];
 342
 9343                if (anim == null)
 344                    continue;
 345
 9346                float startTime = Time.realtimeSinceStartup;
 9347                Transform t = anim.transform;
 348
 9349                Vector3 playerPosition = CommonScriptableObjects.playerUnityPosition;
 9350                float distance = Vector3.Distance(playerPosition, t.position);
 351
 9352                if (distance > settings.enableAnimationCullingDistance)
 4353                    anim.cullingType = AnimationCullingType.BasedOnRenderers;
 354                else
 5355                    anim.cullingType = AnimationCullingType.AlwaysAnimate;
 356
 9357                timeBudgetCount += Time.realtimeSinceStartup - startTime;
 358            }
 3359        }
 360
 361        /// <summary>
 362        /// Reset all tracked renderers properties. Needed when toggling or changing settings.
 363        /// </summary>
 364        internal void ResetObjects()
 365        {
 101366            IEnumerable<Renderer> renderers = objectsTracker.GetRenderers();
 101367            IEnumerable<SkinnedMeshRenderer> skinnedRenderers = objectsTracker.GetSkinnedRenderers();
 101368            Animation[] animations = objectsTracker.GetAnimations();
 369
 984370            foreach (Renderer renderer in renderers)
 371            {
 391372                if (renderer != null)
 391373                    renderer.forceRenderingOff = false;
 374            }
 375
 204376            foreach (SkinnedMeshRenderer skinnedRenderer in skinnedRenderers)
 377            {
 1378                if (skinnedRenderer != null)
 1379                    skinnedRenderer.updateWhenOffscreen = true;
 380            }
 381
 204382            for (int i = 0; i < animations?.Length; i++)
 383            {
 1384                if (animations[i] != null)
 1385                    animations[i].cullingType = AnimationCullingType.AlwaysAnimate;
 386            }
 101387        }
 388
 389        public void Dispose()
 390        {
 100391            objectsTracker.Dispose();
 100392            Stop();
 100393            featureFlags.OnChange -= OnFeatureFlagChange;
 100394        }
 395
 396        public void Initialize()
 397        {
 91398            Start();
 91399        }
 400
 401        /// <summary>
 402        /// Method suscribed to renderer state change
 403        /// </summary>
 404        private void OnRendererStateChange(bool rendererState, bool oldRendererState)
 405        {
 6406            if (!running)
 0407                return;
 408
 6409            MarkDirty();
 410
 6411            if (rendererState)
 2412                StartInternal();
 413            else
 4414                StopInternal();
 4415        }
 416
 417        /// <summary>
 418        /// Method suscribed to playerUnityPosition change
 419        /// </summary>
 72420        private void OnPlayerUnityPositionChange(Vector3 previous, Vector3 current) { playerPositionDirty = true; }
 421
 422        /// <summary>
 423        /// Sets the scene objects dirtiness.
 424        /// In the next update iteration, all the scene objects are going to be gathered.
 425        /// This method has performance impact.
 426        /// </summary>
 24427        public void MarkDirty() { objectPositionsDirty = true; }
 428
 429        /// <summary>
 430        /// Gets the scene objects dirtiness.
 431        /// </summary>
 3432        public bool IsDirty() { return objectPositionsDirty; }
 433
 434        /// <summary>
 435        /// Set settings. This will dirty the scene objects and has performance impact.
 436        /// </summary>
 437        /// <param name="settings">Settings to be set</param>
 438        public void SetSettings(CullingControllerSettings settings)
 439        {
 18440            this.settings = settings;
 18441            profiles = new List<CullingControllerProfile> { settings.rendererProfile, settings.skinnedRendererProfile };
 442
 18443            objectsTracker?.SetIgnoredLayersMask(settings.ignoredLayersMask);
 18444            objectsTracker?.MarkDirty();
 18445            MarkDirty();
 18446            resetObjectsNextFrame = true;
 18447        }
 448
 449        /// <summary>
 450        /// Get current settings copy. If you need to modify it, you must set them via SetSettings afterwards.
 451        /// </summary>
 452        /// <returns>Current settings object copy.</returns>
 7453        public CullingControllerSettings GetSettingsCopy() { return settings.Clone(); }
 454
 455        /// <summary>
 456        /// Enable or disable object visibility culling.
 457        /// </summary>
 458        /// <param name="enabled">If disabled, object visibility culling will be toggled.
 459        /// </param>
 460        public void SetObjectCulling(bool enabled)
 461        {
 0462            if (settings.enableObjectCulling == enabled)
 0463                return;
 464
 0465            settings.enableObjectCulling = enabled;
 0466            resetObjectsNextFrame = true;
 0467            MarkDirty();
 0468            objectsTracker?.MarkDirty();
 0469        }
 470
 471        /// <summary>
 472        /// Enable or disable animation culling.
 473        /// </summary>
 474        /// <param name="enabled">If disabled, animation culling will be toggled.</param>
 475        public void SetAnimationCulling(bool enabled)
 476        {
 101477            if (settings.enableAnimationCulling == enabled)
 101478                return;
 479
 0480            settings.enableAnimationCulling = enabled;
 0481            resetObjectsNextFrame = true;
 0482            MarkDirty();
 0483            objectsTracker?.MarkDirty();
 0484        }
 485
 486        /// <summary>
 487        /// Enable or disable shadow culling
 488        /// </summary>
 489        /// <param name="enabled">If disabled, no shadows will be toggled.</param>
 490        public void SetShadowCulling(bool enabled)
 491        {
 0492            if (settings.enableShadowCulling == enabled)
 0493                return;
 494
 0495            settings.enableShadowCulling = enabled;
 0496            resetObjectsNextFrame = true;
 0497            MarkDirty();
 0498            objectsTracker?.MarkDirty();
 0499        }
 500
 501        /// <summary>
 502        /// Fire the DataReport event. This will be useful for showing stats in a debug panel.
 503        /// </summary>
 504        private void RaiseDataReport()
 505        {
 101506            if (OnDataReport == null)
 101507                return;
 508
 0509            int rendererCount = (objectsTracker.GetRenderers()?.Count ?? 0) + (objectsTracker.GetSkinnedRenderers()?.Cou
 510
 0511            OnDataReport.Invoke(rendererCount, hiddenRenderers.Count, shadowlessRenderers.Count);
 0512        }
 513
 514        /// <summary>
 515        /// Returns true if the culling loop is running
 516        /// </summary>
 517        public bool IsRunning()
 518        {
 6519            return updateCoroutine != null;
 520        }
 521
 522        /// <summary>
 523        /// Draw debug gizmos on the scene view.
 524        /// </summary>
 525        /// <param name="shouldBeVisible"></param>
 526        /// <param name="bounds"></param>
 527        /// <param name="boundingPoint"></param>
 528        private static void DrawDebugGizmos(bool shouldBeVisible, Bounds bounds, Vector3 boundingPoint)
 529        {
 0530            if (!shouldBeVisible)
 531            {
 0532                CullingControllerUtils.DrawBounds(bounds, Color.blue, 1);
 0533                CullingControllerUtils.DrawBounds(new Bounds() { center = boundingPoint, size = Vector3.one }, Color.red
 534            }
 0535        }
 536    }
 537}