Search Unity

Enable / Disable the LODGroup - Disabling will turn off all renderers. (Bug?)

Discussion in 'General Graphics' started by LightStriker, Sep 27, 2018.

  1. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    As said in the documentation:

    However, this is untrue. It only display the last LOD in the chain, doesn't disable them all.

    Any way I could totally turn off a LODGroup? Even turning it off by hand in the Inspector doesn't turn them all off. Even worst, even if I have a "Culled" LOD at the end of the chain, disabling the LODGroup shows the last visible LOD, not the Culled one.
     
  2. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    So... We added a extra LOD at the end of the chain with no renderer.
    Instead of disabling the LODGroup, we force it to this empty LOD.

    However, we noticed 2 issues:

    1) LODGroup has a rather important impact on the CPU
    2) A number of camera should NOT trigger an update in LODGroup, but does. (Ex.: Sky camera)

    Right now, I'm a bit lost because the 7-9 ms of CPU time to update LODGroup is unacceptable.
    Disabling a LODGroup is unacceptable because it turn on all its renderers.
    Disabling all the renderers is also unacceptable because of the impact it has on the CPU.
    Forcing a LOD doesn't remove the CPU cost (for some reason).
     
  3. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Even my damn UI Cameras are triggering ComputeLOD!

    How am I supposed to disable that!?
     
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Ended up dumping LODGroup.
    Instead, we wrote our own LOD system, which is multithreaded.

    Between 0 and 0.1 ms per frame on the main thread, depending if the player moves or not, regardless of the number of camera.

    Tested in a 87,000 LOD scene.
     
    bgolus likes this.
  5. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,619
    Can you share how you implemented that?
     
  6. mbowen89

    mbowen89

    Joined:
    Jan 21, 2013
    Posts:
    639
    Yeah I think they changed LODGroups in some version and they don't behave as before unfortunately and we just don't have much control over them :(
     
  7. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,297
    Hi,
    Could you file a bug report, please?
    If the docs say it should do something that it is not then either the docs are wrong and need fixing or its a bug. Either way, a bug report will help us.
    Thanks
     
  8. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    https://issuetracker.unity3d.com/issues/lod-disabling-lod-group-in-play-mode-enables-the-renderers

    It was flagged "As designed". Sadly it is not a behaviour I want, regardless of what the documentation claims.

    But it's only half the issue. LODGroup shouldn't compute their LODs on ALL camera, even camera that sees no LODGroup. I can have 10-12 active cameras at some point, and only 1 needs LODs computing.
     
  9. mbowen89

    mbowen89

    Joined:
    Jan 21, 2013
    Posts:
    639
    The thing is, my game used to take advantage of the fact that when you disabled the LOD group it DID disable all renderers, then it was changed. So I guess even if that is how it's intended, that's not what it used to do, so I guess it's a preference? Not sure how to really categorize that.
     
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I can try. But you have to understand what we wrote is hook in our sectorization system too. Right now it behave exactly like Unity's LODGroup, without the nasty "ComputeLOD" on every camera, and with a proper "turn off" when not visible.

    The advantage of the following system is that it cost almost nothing, is hook to only specific cameras, and do cost nothing if no change is needed. We have scenes with 90k+ LOD components.

    The disadvantages of it right now, it's only for static objects (no position update) - which could be added easily -, it is "additive" when many cameras are running - which isn't optimal in tris count, but is in CPU time. Also, the proper LOD is always 1 frame late.

    In short, kinda made for our specific case.

    Also note I'm no multi-threading expert. It is quite possible to hit a running condition while two cameras are both updating. However, I feel the update threads should never hit the same spot in the arrays at the same time, considering cameras never render or cull at the same time.

    Code (CSharp):
    1.     [Serializable]
    2.     public struct LODRange
    3.     {
    4.         [SerializeField, Range(0, 1)]
    5.         public float range;
    6.  
    7.         [SerializeField]
    8.         public Renderer[] renderers;
    9.  
    10.         public LODRange(float range, Renderer[] renderers)
    11.         {
    12.             this.range = range;
    13.             this.renderers = renderers;
    14.         }
    15.     }
    Code (CSharp):
    1.  
    2.     public struct LODData
    3.     {
    4.         private Vector3 position;
    5.  
    6.         public Vector3 Position
    7.         {
    8.             get { return position; }
    9.         }
    10.  
    11.         private float fieldOfView;
    12.  
    13.         public float FieldOfView
    14.         {
    15.             get { return fieldOfView; }
    16.         }
    17.  
    18.         private int screenHeight;
    19.  
    20.         public int ScreenHeight
    21.         {
    22.             get { return screenHeight; }
    23.         }
    24.  
    25.         private float bias;
    26.  
    27.         public float Bias
    28.         {
    29.             get { return bias; }
    30.         }
    31.  
    32.         private float offset;
    33.  
    34.         public float Offset
    35.         {
    36.             get { return offset; }
    37.         }
    38.  
    39.         public LODData(Vector3 position, float fieldOfView, int screenHeight, float bias, float offset)
    40.         {
    41.             this.position = position;
    42.             this.fieldOfView = fieldOfView;
    43.             this.screenHeight = screenHeight;
    44.             this.bias = bias;
    45.             this.offset = offset;
    46.         }
    47.     }
    48.  
    Code (CSharp):
    1. [ExecuteInEditMode]
    2.     public class LODComponent : MonoBehaviour
    3.     {
    4.         private int index = -1;
    5.  
    6.         public int Index
    7.         {
    8.             get { return index; }
    9.             set { index = value; }
    10.         }
    11.  
    12.         internal bool visible = true;
    13.  
    14.         public bool Visible
    15.         {
    16.             get { return visible; }
    17.             set
    18.             {
    19.                 if (visible == value)
    20.                     return;
    21.  
    22.                 visible = value;
    23.  
    24.                 if (current >= levelOfDetails.Length)
    25.                     return;
    26.  
    27.                 Renderer[] renderers = levelOfDetails[current].renderers;
    28.                 for (int i = 0; i < renderers.Length; i++)
    29.                     renderers[i].enabled = visible;
    30.             }
    31.         }
    32.  
    33.         private float bound = 0;
    34.  
    35.         public float Bound
    36.         {
    37.             get { return bound; }
    38.         }
    39.  
    40.         private float[] ranges;
    41.  
    42.         public float[] Ranges
    43.         {
    44.             get { return ranges; }
    45.         }
    46.  
    47.         internal byte current = 255;
    48.  
    49.         [SerializeField]
    50.         private LODRange[] levelOfDetails = new LODRange[0];
    51.  
    52.         public LODRange[] LevelOfDetails
    53.         {
    54.             get { return levelOfDetails; }
    55.             set
    56.             {
    57.                 levelOfDetails = value;
    58.                 UpdateLODs();          
    59.             }
    60.         }
    61.  
    62.         private void OnEnable()
    63.         {
    64.             UpdateLODs();
    65.             LODManager.AddLOD(this);
    66.         }
    67.  
    68.         private void OnDisable()
    69.         {
    70.             LODManager.RemoveLOD(this);
    71.         }
    72.  
    73.         private void OnValidate()
    74.         {
    75.             LODManager.UpdateLOD(this);
    76.         }
    77.  
    78.         public void SetLOD(byte lod)
    79.         {
    80.             if (current == lod)
    81.                 return;
    82.  
    83.             Renderer[] renderers;
    84.             if (visible)
    85.             {
    86.                 if (current < levelOfDetails.Length)
    87.                 {
    88.                     renderers = levelOfDetails[current].renderers;
    89.                     for (int i = 0; i < renderers.Length; i++)
    90.                         renderers[i].enabled = false;
    91.                 }
    92.             }
    93.  
    94.             current = lod;
    95.  
    96.             if (visible)
    97.             {
    98.                 if (current < levelOfDetails.Length)
    99.                 {
    100.                     renderers = levelOfDetails[current].renderers;
    101.                     for (int i = 0; i < renderers.Length; i++)
    102.                         renderers[i].enabled = true;
    103.                 }
    104.             }
    105.         }
    106.  
    107.         private void UpdateLODs()
    108.         {
    109.             bound = 0;
    110.             current = 255;
    111.             ranges = new float[levelOfDetails.Length];
    112.             for (int i = 0; i < levelOfDetails.Length; i++)
    113.             {
    114.                 ranges[i] = levelOfDetails[i].range;
    115.                 Renderer[] renderers = levelOfDetails[i].renderers;
    116.                 for (int j = 0; j < renderers.Length; j++)
    117.                 {
    118.                     bound = Mathf.Max(bound, renderers[j].bounds.extents.magnitude);
    119.                     renderers[j].enabled = false;
    120.                 }
    121.             }
    122.  
    123.             LODManager.UpdateLOD(this);
    124.         }
    125.     }
    Code (CSharp):
    1.  
    2.     public class LODManager : Manager<LODManager>
    3.     {
    4.         private const byte off = 255;
    5.         private const int maxLODs = 250000;
    6.  
    7.         private static int count = 0;
    8.  
    9.         private static LODData data;
    10.  
    11.         private static LODComponent[] components = new LODComponent[maxLODs];
    12.         private static float[] bounds = new float[maxLODs];
    13.         private static float[][] ranges = new float[maxLODs][];
    14.         private static Vector3[] positions = new Vector3[maxLODs];
    15.  
    16.         private static volatile int changedCount = 0;
    17.         private static volatile int[] changed = new int[maxLODs];
    18.         private static volatile byte[] levels = new byte[maxLODs];
    19.  
    20.         private static Dictionary<CullingCamera, Thread> threads = new Dictionary<CullingCamera, Thread>();
    21.         private static Dictionary<CullingCamera, object> monitors = new Dictionary<CullingCamera, object>();
    22.  
    23.         [SerializeField]
    24.         private float offset = -0.05f;
    25.  
    26.         [SerializeField]
    27.         private float bias = 1f;
    28.  
    29.         static LODManager()
    30.         {
    31. #if UNITY_EDITOR
    32.             Camera.onPreCull += PreCull;
    33. #endif
    34.  
    35.             CullingCamera.PostCull += UpdateCamera;
    36.  
    37.             count = 0;
    38.             for (int i = 0; i < maxLODs; i++)
    39.             {
    40.                 components[i] = null;
    41.                 bounds[i] = 0;
    42.                 positions[i] = Vector3.zero;
    43.                 levels[i] = off;
    44.             }
    45.         }
    46.  
    47.         private void Update()
    48.         {
    49.             UpdateLevels();
    50.         }
    51.  
    52. #if UNITY_EDITOR
    53.         private static void PreCull(Camera camera)
    54.         {
    55.             if (camera.cameraType != CameraType.SceneView)
    56.                 return;
    57.  
    58.             CullingCamera culling = camera.GetComponent<CullingCamera>();
    59.             if (culling != null)
    60.                 return;
    61.  
    62.             camera.gameObject.AddComponent<CullingCamera>();
    63.         }
    64. #endif
    65.  
    66.         private static void UpdateLevels()
    67.         {
    68.             for (int i = 0; i < changedCount; i++)
    69.             {
    70.                 int index = changed[i];
    71.                 components[index].SetLOD(levels[index]);
    72.                 levels[index] = off;
    73.             }
    74.  
    75.             changedCount = 0;
    76.         }
    77.  
    78.         public static void AddCamera(CullingCamera culling)
    79.         {
    80.             if (culling == null)
    81.                 return;
    82.  
    83.             if (Application.isPlaying && culling.Camera.cameraType != CameraType.Game)
    84.                 return;
    85.  
    86.             if (!Application.isPlaying && culling.Camera.cameraType != CameraType.SceneView)
    87.                 return;
    88.  
    89.             Thread thread = new Thread(ComputeLOD);
    90.             thread.Start(culling);
    91.  
    92.             threads.Add(culling, thread);
    93.             monitors.Add(culling, new object());
    94.         }
    95.  
    96.         public static void UpdateCamera(CullingCamera culling)
    97.         {
    98.             object monitor;
    99.             if (!monitors.TryGetValue(culling, out monitor))
    100.                 return;
    101.  
    102. #if UNITY_EDITOR
    103.             if (!Application.isPlaying && culling.Camera.cameraType == CameraType.SceneView)
    104.                 UpdateLevels();
    105. #endif
    106.  
    107.             lock (monitors[culling])
    108.             {
    109.                 float bias = 1;
    110.                 float offset = 0;
    111.                 if (Instance != null)
    112.                 {
    113.                     bias = Instance.bias;
    114.                     offset = Instance.offset;
    115.                 }
    116.  
    117.                 data = new LODData(culling.transform.position, culling.Camera.fieldOfView, Screen.height, bias, offset);
    118.  
    119.                 Monitor.PulseAll(monitors[culling]);
    120.             }
    121.         }
    122.  
    123.         public static void RemoveCamera(CullingCamera culling)
    124.         {
    125.             if (culling == null)
    126.                 return;
    127.  
    128.             threads.Remove(culling);
    129.             monitors.Remove(culling);
    130.         }
    131.  
    132.         public static void AddLOD(LODComponent lod)
    133.         {
    134.             if (lod.Index >= 0)
    135.                 return;
    136.  
    137.             components[count] = lod;
    138.             bounds[count] = lod.Bound;
    139.             ranges[count] = lod.Ranges;
    140.             positions[count] = lod.transform.position;
    141.             lod.Index = count;
    142.  
    143.             count++;
    144.         }
    145.  
    146.         public static void UpdateLOD(LODComponent lod)
    147.         {
    148.             int index = lod.Index;
    149.             if (index < 0)
    150.                 return;
    151.  
    152.             bounds[index] = lod.Bound;
    153.             ranges[index] = lod.Ranges;
    154.             positions[index] = lod.transform.position;
    155.         }
    156.  
    157.         public static void RemoveLOD(LODComponent lod)
    158.         {
    159.             int index = lod.Index;
    160.             if (index < 0)
    161.                 return;
    162.  
    163.             count--;
    164.             lod.Index = -1;
    165.  
    166.             if (count > 0)
    167.             {
    168.                 components[index] = components[count];
    169.                 bounds[index] = bounds[count];
    170.                 ranges[index] = ranges[count];
    171.                 positions[index] = positions[count];
    172.             }
    173.             else
    174.             {
    175.                 components[index] = null;
    176.                 bounds[index] = 0;
    177.                 ranges[index] = null;
    178.                 positions[index] = Vector3.zero;
    179.             }
    180.         }
    181.  
    182.         private static void ComputeLOD(object cam)
    183.         {
    184.             CullingCamera culling = cam as CullingCamera;
    185.  
    186.             lock (monitors[culling])
    187.             {
    188.                 while (true)
    189.                 {
    190.                     Monitor.Wait(monitors[culling]);
    191.  
    192.                     Vector3 cameraPosition = data.Position;
    193.                     float fieldOfView = data.FieldOfView;
    194.                     int height = data.ScreenHeight;
    195.  
    196.                     float bias = data.Bias;
    197.                     float offset = data.Offset;
    198.  
    199.                     for (int i = 0; i < count; i++)
    200.                     {
    201.                         if (!components[i].visible)
    202.                             continue;
    203.  
    204.                         float distance = Vector3.Distance(positions[i], cameraPosition);
    205.                         float size = (bounds[i] * Mathf.Rad2Deg * height) / (distance * fieldOfView) * 0.01f;
    206.                         size *= bias;
    207.                         size += offset;
    208.  
    209.                         float[] r = ranges[i];
    210.                         for (byte j = 0; j < r.Length; j++)
    211.                         {
    212.                             if (size >= r[j] && levels[i] > j)
    213.                             {
    214.                                 if (components[i].current == j)
    215.                                     break;
    216.  
    217.                                 if (levels[i] == off)
    218.                                 {
    219.                                     changed[changedCount] = i;
    220.                                     changedCount++;
    221.                                 }
    222.  
    223.                                 levels[i] = j;
    224.                                 break;
    225.                             }
    226.                         }
    227.                     }
    228.  
    229.                     Monitor.PulseAll(monitors[culling]);
    230.                 }
    231.             }
    232.         }
    233.     }
    234.  
     
    Last edited: Oct 3, 2018
    st-VALVe and Peter77 like this.
  11. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    What I found out is, instead of turning on/off renderers - which is freaking slow - the LODGroup now uses an internal IsVisible flag, which is blazing fast, but we don't have access to it. However, it also means the LODGroup has to "actively" put that flag there on every culling pass or else all renderers are always visible.

    Now when we turn off the LODGroup, it is no longer present to flip that switch in the culling pass, making everything visible. My guess is, LODGroup used to work by disabling renderers, but someone found it's too slow.
     
  12. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,619
    Does this mean you LOD entire sectors, such as visibility checks per sector rather than per mesh renderer? For example, if a sector is a certain distance away, do you deactivate the entire cell, rather than each mesh renderer inside this sector individually? I'm trying to understand why your system is so much faster than Unity's implementation.
     
  13. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,297
  14. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Because we are using a simple Sector/Portal system, where sector has no spatial data, only a list of object to cull/show when a portal linked to it is visible. This means the only "culling" our system perform is camera frustum against portals, with a marching from sector to sector. A sector is able to disable LODs, renderer, light, particles, or any component implementing the ISectorizable interface.

    Usually the cost of checking portals vs frustum is about 0.05ms per Camera. However, the cost of turning on or off a sector is dependent on the number of object within a sector. We are looking for ways to do that over a number of frames instead of all at once.

    We don't do visibility check within a sector - too CPU intensive, and Unity's camera perform a frustum culling anyway.

    If a sector isn't visible, the LOD aren't computed; no point to do that. So even if we have scene near 100,000 LODComponent - almost all our environment objects have LODs - we have at most 10k computed at any moment, and the system only flags a LODComponent when a changed happened. So in the end, when the player moves around, we have only a handful of LODComponent changing renderers flags.
     
    Peter77 likes this.
  15. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Sorry, no offense, but I long gave up putting stuff on Feedback.
     
    Prodigga and tcz8 like this.
  16. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,619
    Thanks for the explanation!
     
  17. mbowen89

    mbowen89

    Joined:
    Jan 21, 2013
    Posts:
    639
    Are you having 100,000 gameobjects in the scene at once then??
     
  18. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    No, no... 100,000+ LODComponents. :)
    No clue how many GameObjects. Probably a lot? ;)
     
  19. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    https://docs.unity3d.com/ScriptReference/LODGroup.html

    Still claiming to turn off all renderers.

    Also wrote an articles about LODs: https://connect.unity.com/p/faster-level-of-details-lods
     
    Last edited: Feb 4, 2019
    Lex4art likes this.
  20. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,297
  21. AlejMC

    AlejMC

    Joined:
    Oct 15, 2013
    Posts:
    149
    What's the current status of LODGroups? is it working as intended?

    I'm also having an issue on my side where disabling an LODGroup at runtime (i.e. some gameplay event happened and that LOD for that object doesn't make sense anymore) just seems to not pay attention to the fact that it got disabled.
    It works in the Editor but not on iOS builds.
    Are LODGroups intended to be enabled or disabled when needed?

    EDIT: It works on macOS desktop builds, it doesn't on iOS builds, a bug?
     
  22. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    We save 1 ms in avarage (have a prerecorded path that I bench against) by simply setting ForceLod(0) to all lodgroups in our scene.

    So yeah, something is definitely not working with lod groups atleast not on 2020.3.2