Search Unity

Resolved How to retrieve bounding box of all active objects in scene

Discussion in 'Scripting' started by Matsyir, May 29, 2022.

  1. Matsyir

    Matsyir

    Joined:
    Mar 13, 2019
    Posts:
    29
    Hi guys,

    Firstly, I've made this little graphic of what I'm trying to do, should help explain most of it:
    scene-bounding-box.png

    Essentially, I'd like to get an exterior-outline of the level so that the camera can go around it for a quick visual overview of the level. This will also be used for level screenshot/thumbnail generation, using the corners as candidate positions.

    Now, I know of ways I could manually loop through all GameObjects, keep track of the min/max values for every axis, and then create the bounding box myself that way. However, I felt like there had to be a better or simpler way, given this is Unity, maybe even an existing function to directly get this. Or perhaps some built in Cinemachine behaviors to achieve something like this?

    Any advice would be greatly appreciated.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,744
    I'm not sure how it is possible to even simplify the above.

    That's pretty specific stuff. I think you just write it... even something as trivial as foreach transform in children and use the position of each one will likely get you close enough for a level preview.

    Usually though, one does not preview a level as a volume box. It's just not interesting enough. Usually you script the intro out so that the user's eye is drawn to what they should see.
     
    Matsyir likes this.
  3. Matsyir

    Matsyir

    Joined:
    Mar 13, 2019
    Posts:
    29
    I mean, it's definitely a simple problem to begin with.. But it seemed like a problem that could be common enough to have a helper function for it. I mean, every scene in Unity has... a collection of GameObjects. And you might want to know the area which they take up. It doesn't seem that far off to me.

    Fair enough man, I'm sorry if I come across as lazy - it's just I've seen so many helpful functions, I thought there'd be some kind of helper for this. Like I said, it doesn't seem like something specific to me, but maybe I am just overestimating the amount of use you could get out of this whole-scene-bounding-box I'm after.

    Yeah, I'm with you here, the problem is these animations are not manually created, they will be automatically generated for each level. Users are able to create levels themselves and I would like this little animation to automatically happen for all levels, even if it's not perfect. It would just be a quick like ~3-8 second animation while loading in, it's no core behavior of the game of anything, just a fun little extra. I'm also trying to keep the level editor very simple for the most part, it's like an MS paint of level editors, so I wouldn't want to add anything to manage this little animation. At most maybe an int 1-4 to choose which bottom corner this animation starts from, not much more than that.
     
    Kurt-Dekker likes this.
  4. Deleted User

    Deleted User

    Guest

    angrypenguin and Matsyir like this.
  5. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Yep, I was about to point out that Renderer objects already have Bounds objects, and those have the Encapsulate() method or something along those lines, and they get most of the job done for you.

    From there the question is: which objects do you want included in your bounds? And for most games that won't be "everything", which is why it's not done out of the box for us even though it is indeed a common problem to get the bounds around some stuff.
     
    Matsyir likes this.
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,744
    Just for the record, before anyone goes too far down the Bounds rathole, I have personally observed dozens of people come in here posting about how the
    renderer.bounds
    property disappoints them in some way.

    While it is an exposed API and is supported, it appears from the many posts that
    renderer.bounds
    frequently surprises users who attempt to employ it for their own purposes.

    Now the actual concept of an AABB Bounds object is solid, that's just data, just a
    struct
    , so of course that's fine, it's just numbers. I'm simply pointing out that the values people expect to see in the
    renderer.bounds
    property seems to regularly be surprising in various non-pleasant or counter-intuitive ways.

    You have been warned. Personally I would NEVER used
    renderer.bounds
    because of the observed chaos above.
     
  7. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I'm not sure what Kurt means exactly, maybe because renderer.bounds are in world space, I could see that being a source of confusion to some posters.

    This will get you a single bounds that encompasses your entire world OP:
    Code (csharp):
    1.  
    2. public sealed class Help_Bounds : MonoBehaviour
    3. {
    4.    private Bounds _worldBounds;
    5.  
    6.    private void Awake()
    7.    {
    8.        Renderer[] renderers = FindObjectsOfType<Renderer>();
    9.  
    10.        _worldBounds = renderers[0].bounds;
    11.  
    12.        for (int i = 1; i < renderers.Length; ++i)
    13.            _worldBounds.Encapsulate(renderers[i].bounds);
    14.    }
    15.  
    16.    private void OnDrawGizmos()
    17.    {
    18.        Gizmos.color = new Color(0f, 1f, 0f, 0.1f);
    19.        Gizmos.DrawCube(_worldBounds.center, _worldBounds.size);
    20.  
    21.        Gizmos.color = new Color(0f, 1f, 0f, 1f);
    22.        Gizmos.DrawWireCube(_worldBounds.center, _worldBounds.size);
    23.    }
    24. }
    25.  
    bounds.png

    You may need to also fetch all the collider bounds if you have a bunch of invisible or non-rendered objects, as well.
     
    qianyiyuan94 and Matsyir like this.
  8. Matsyir

    Matsyir

    Joined:
    Mar 13, 2019
    Posts:
    29
    Man, y'all got me a bit conflicted now, haha. Many thanks, I appreciate all the discussions & solutions. I sort of feel bad for already having done it the manual way now, because I definitely could make use of @angrypenguin / @GroZZleR 's solution, using all Colliders in my case. Because of how I'm using it to just force the camera into an animation, I don't think the fact it's in world space would be much of an issue, but I can see how it could cause some confusion depending how it needs to be used.

    In fact, the manual method I had written would probably be optimized by just encapsulating all the Collider Bounds after the level's generated, since right now I'm doing a bunch of calculations to work out the real object's size depending on level scale + node scale (essentially I'm manually creating the Bounds for each node, though unity is already doing that - but I suppose you couldn't've known that all my relevant elements do 100% include a Renderer and/or Collider, even I hadn't made the connection). It looks like I'm going to end up with a mix of both solutions here.

    Hmm, interesting, but seems more focused on in-editor behavior/debugging rather than what I'm looking for, but definitely good to keep in mind for debugging this stuff.

    Yessir, it's what I had in mind for the "manual method" I mentioned, but thanks for throwing out there anyways cause it's definitely the way to go for this.

    My initial and not-ideal solution ended up looking like this, omitting most the project-specific stuff, and all the scaling already dealt with. Just in case anyone wanted to see how I manually used Bounds for this.
    Code (CSharp):
    1. // reset level bounds
    2. loadedLevelBounds.SetMinMax(Vector3.positiveInfinity, Vector3.negativeInfinity);
    3. foreach (var node in level.nodes)
    4. {
    5.     //... partially init GO using saved node data ...
    6.     // ... calculate each axis' size ...
    7.  
    8.     // this is actually "half the size", similar to bounds.extents.
    9.     Vector3 nodeSize = new Vector3(xSize, ySize, zSize);
    10.  
    11.     Vector3 nodeLowBounds = newGo.transform.position - nodeSize;
    12.     Vector3 nodeHighBounds = newGo.transform.position + nodeSize;
    13.  
    14.     loadedLevelBounds.Encapsulate(nodeLowBounds);
    15.     loadedLevelBounds.Encapsulate(nodeHighBounds);
    16. }
    As I mentioned above, I'll rework this code to just re-use the new node's Collider bounds, and encapsulate that into the loadedLevelBounds (Like @GroZZleR's suggested Renderer solution, but for Collider). Though, I figured I'll leave this code here since it could possibly help lead someone who doesn't have the option to just use all colliders/renderers. Needless to say, if you must do it manually like this, the accuracy of your size calculations here is crucial to the accuracy of the resulting bounds.

    That's fair - although in my case it really is everything that I need, except a few hidden things like the sun, and there could also be some hard-to-understand behavior surrounding UI elements' positions, not too sure how those would play into it. However, if I understand correctly, using Renderers or Colliders will automatically exclude those elements that, well, aren't being rendered or can't be collided with. And if that's not enough, then I could bring layers into the mix and it'd work for sure. So it looks like it could definitely be a simple solution in my case, and a more optimized one than manually working out the Bounds for each level node.

    EDIT: This is the finalized version of my bounds-generating code, within the same foreach as shown above. As I said it's really just Grozzler's solution but slightly repurposed. This is a lot simpler in comparison to all the size-calculating code that was involved previously.
    Code (CSharp):
    1.  
    2. List<Collider> colliders = new List<Collider>();
    3. foreach (var node in level.nodes)
    4. {
    5.     // ... init newGo using node values ...
    6.  
    7.     colliders.AddRange(newGo.GetComponentsInChildren<Collider>());
    8. }
    9.  
    10. colliders.ForEach((c) => loadedLevelBounds.Encapsulate(c.bounds));
    11.  
    It could be done in a single FindObjectsOfType<Collider>() call after this loop, but I figured since I already have this loop anyways, it'll be sure to only include the level nodes I am expecting it to, though I don't think there will ever be conflicting new elements outside of the level.

    I made a second minor edit to this code for optimization, initially I was creating a new list on each loop execution (and this could be a very long loop at times), but it's much better like this. *1 more small edit due to realizing GetComponentsInChildren includes the GameObject it's called on.
     
    Last edited: May 30, 2022
    GroZZleR and Deleted User like this.
  9. Wilhelm_LAS

    Wilhelm_LAS

    Joined:
    Aug 18, 2020
    Posts:
    55
    I edited the code for my needs.
    Here's the complete code with continuous generation based on a GameObject change:

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [ExecuteInEditMode]
    5. public sealed class BoundsTest : MonoBehaviour
    6. {
    7.     private Bounds _worldBounds;
    8.     private static Color innerColor = new(0f, 1f, 0f, 0.1f);
    9.     private static Color outerColor = new(0f, 1f, 0f, 1f);
    10.  
    11.  
    12.     // Initialize
    13.     private void OnEnable()
    14.     {
    15.         ObjectChangeEvents.changesPublished -= OnObjectChangeEvent;
    16.         ObjectChangeEvents.changesPublished += OnObjectChangeEvent;
    17.  
    18.         ReCalculateWorldBounds();
    19.     }
    20.  
    21.     private void OnObjectChangeEvent(ref ObjectChangeEventStream stream)
    22.     {
    23.         ReCalculateWorldBounds();
    24.     }
    25.  
    26.  
    27.     // Update
    28.     private void OnDrawGizmos()
    29.     {
    30.         Gizmos.color = innerColor;
    31.         Gizmos.DrawCube(_worldBounds.center, _worldBounds.size);
    32.  
    33.         Gizmos.color = outerColor;
    34.         Gizmos.DrawWireCube(_worldBounds.center, _worldBounds.size);
    35.     }
    36.  
    37.     private void ReCalculateWorldBounds()
    38.     {
    39.         Renderer[] renderers = FindObjectsOfType<Renderer>(includeInactive: true);
    40.  
    41.         _worldBounds = renderers[0].bounds;
    42.  
    43.         for (int i = renderers.Length - 1; i >= 1; i--)
    44.             _worldBounds.Encapsulate(renderers[i].bounds);
    45.     }
    46.  
    47.  
    48.     // Dispose
    49.     private void OnDisable()
    50.     {
    51.         ObjectChangeEvents.changesPublished -= OnObjectChangeEvent;
    52.     }
    53. }
     
    Last edited: Jan 4, 2024
    Matsyir likes this.