Search Unity

Fit object exactly into perspective camera's field of view (focus the object)

Discussion in 'General Graphics' started by Wattosan, Sep 21, 2017.

  1. Wattosan

    Wattosan

    Joined:
    Mar 22, 2013
    Posts:
    460
    Hello,

    I would like to achieve a result similar to the editor camera's focus (F) function. I would like the camera to focus on an object (without changing the camera's rotation, just like the editor function does not). But I would also like it to zoom in as much as possible. So that the bounding box of the object would remain inside the camera's field of view.

    The FOV and rotation of the camera should never change. Neither should the scale of the camera nor the object. It is only a question of positioning the camera.

    I did find 2 useful links but have failed to find a final solution. I am able to position the camera in a way that it looks at the object without changing the camera's rotation. However, it is the distance from the center of the object that I am having difficulty calculating.

    1) Unity Thread
    2) Unity Manual

    Thank you!
     
  2. geroppo

    geroppo

    Joined:
    Dec 24, 2012
    Posts:
    140
    Just a random guess but maybe it's possible to calculate the vertical length of the bounding box in screen space, and move the camera closer to the object depending on that length. In other words, trying to find how much space the object occupies in screen space, and moving the camera closer to try to maintain a certain coverage of the screen.

    EDIT: nvm I just read the first link you posted and the guy does exactly that.
     
    Last edited: Sep 22, 2017
  3. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    It all depends on how exact you want it to be. Calculating the bounding box positions in screen space can be done through the camera, but it will soon lead to an iterative approach, because it can only be done easily after the camera has been placed.

    One factor is the size of the object of course. There are various ways to determine that, but for this purpose I suggest the maximum of the three dimensions of the bounding box. Another factor is the FOV of the Camera. And then you are pretty much left with a constant factor.

    So, a fairly inexact, but simple approach would be:
    Code (csharp):
    1.  
    2. float cameraDistance = 2.0f; // Constant factor
    3. Vector3 objectSizes = bounds.max - bounds.min;
    4. float objectSize = Mathf.Max(objectSizes.x, objectSizes.y, objectSizes.z);
    5. float cameraView = 2.0f * Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); // Visible height 1 meter in front
    6. float distance = cameraDistance * objectSize / cameraView; // Combined wanted distance from the object
    7. distance += 0.5f * objectSize; // Estimated offset from the center to the outside of the object
    8. camera.transform.position = bounds.center - distance * camera.transform.forward;
    9.  
     
    magnusfox, IggyZuk, eyyub9b and 6 others like this.
  4. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    My goodness !! It worked like a charm. Thanks. I wish that i could understand the mathematics behind this code snippet.
     
  5. silviubogan

    silviubogan

    Joined:
    Nov 20, 2019
    Posts:
    10
    I also wish I could understand the maths behind this code snippet. How can I get the `bounds` variable in Unity 2018.4?

    Thank you.
     
    MFKJ likes this.
  6. Guillaume-atVolumiq

    Guillaume-atVolumiq

    Joined:
    Sep 14, 2018
    Posts:
    36
    You can get a bounds from either a collider or a meshRenderer (from the object you want to focus on)

    Code (CSharp):
    1.     public Collider collider;
    2.     public float elevation;
    3.     public float cameraDistance = 2.0f;
    4.  
    5.     void Update()
    6.     {
    7.         Vector3 objectSizes = collider.bounds.max - collider.bounds.min;
    8.         float objectSize = Mathf.Max(objectSizes.x, objectSizes.y, objectSizes.z);
    9.         float cameraView = 2.0f * Mathf.Tan(0.5f * Mathf.Deg2Rad * Camera.main.fieldOfView); // Visible height 1 meter in front
    10.         float distance = cameraDistance * objectSize / cameraView; // Combined wanted distance from the object
    11.         distance += 0.5f * objectSize; // Estimated offset from the center to the outside of the object
    12.         Camera.main.transform.position = collider.bounds.center - distance * Camera.main.transform.forward;
    13.         Camera.main.transform.rotation = Quaternion.Euler(new Vector3(elevation, 0, 0));
    14.     }
     
  7. Mersey-Viking

    Mersey-Viking

    Joined:
    Nov 3, 2012
    Posts:
    4
    I've done it slightly differently to jvo3dc, but I think the result is the same. To illustrate the math, here's a masterpiece:


    The first thing to do is to simplify the object of interest to a sphere. The easiest way is to use
    Code (CSharp):
    1. bounds.extents.magnitude
    The angle, theta, is half of the camera's field of view. r is the radius of the sphere. Note how by fitting the sphere tightly to the frustum, the tangent to the sphere is also the edge of the frustum and so is perpendicular to a line drawn from the sphere centre to the contact point, which gives us a right-triangle. We then just need to remember SOHCAHTOA Here we want to find h which is the hypotenuse, and we have r which is the opposite side, so we need to use the SOH part. So:
    Code (csharp):
    1. sin(theta) = o/h
    2. sin(fov/2) = r/h
    3. h = r / sin(fov / 2)
    Don't forget that the Mathf trig functions work in radians, but Unity works in degrees (well, internally it'll use radians I expect).

    The code I have for this then is:

    Code (CSharp):
    1. const float margin = 1.1f;
    2. float maxExtent = b.extents.magnitude;
    3. float minDistance = (maxExtent * margin) / Mathf.Sin(Mathf.Deg2Rad * _camera.fieldOfView / 2.0f);
    4. Camera.main.transform.position = Vector3.back * minDistance;
    Here margin gives us a bit of breathing space, and maxExtent represents the sphere that encloses the object's bounding box, b. Here I just set the camera along the -ve z axis far enough to fit the object in view.
    A few things to note is that the field of view is the vertical FoV by default, so should you have a portrait view, then you'll need to use the horizontal FoV. Also you need to make sure your near clip plane is sufficient to not clip the object. I'm using an orbit camera (hence the variable minDistance), so I can set my near clip plane to:
    Code (CSharp):
    1. Camera.main.nearClipPlane = minDistance - maxExtent;
    Which gives me maximum precision in my depth buffer by not allowing the camera any closer than minDistance.
     
    Last edited: Mar 25, 2020
  8. niemczaklukasz

    niemczaklukasz

    Joined:
    Dec 4, 2020
    Posts:
    1
    Thanks a lot!

    For case when object may also have renderers in children, I combined this answer with answer from this topic: https://forum.unity.com/threads/getting-the-bounds-of-the-group-of-objects.70979/ .

    You need 2 extension methods:

    Code (CSharp):
    1.    public static Bounds GetBoundsWithChildren(this GameObject gameObject)
    2.    {
    3.       Renderer parentRenderer = gameObject.GetComponent<Renderer>();
    4.  
    5.       Renderer[] childrenRenderers = gameObject.GetComponentsInChildren<Renderer>();
    6.  
    7.       Bounds bounds = parentRenderer != null
    8.          ? parentRenderer.bounds
    9.          : childrenRenderers.FirstOrDefault(x => x.enabled).bounds;
    10.  
    11.       if (childrenRenderers.Length > 0)
    12.       {
    13.          foreach (Renderer renderer in childrenRenderers)
    14.          {
    15.             if (renderer.enabled)
    16.             {
    17.                bounds.Encapsulate(renderer.bounds);
    18.             }
    19.          }
    20.       }
    21.  
    22.       return bounds;
    23.    }
    24.  
    25.    public static void FocusOn(this Camera camera, GameObject focusedObject, float marginPercentage)
    26.    {
    27.       Bounds bounds = focusedObject.GetBoundsWithChildren();
    28.       float maxExtent = bounds.extents.magnitude;
    29.       float minDistance = (maxExtent * marginPercentage) / Mathf.Sin(Mathf.Deg2Rad * camera.fieldOfView / 2f);
    30.       camera.transform.position = focusedObject.transform.position - Vector3.forward * minDistance;
    31. camera.nearClipPlane = minDistance - maxExtent;
    32.    }
    And then you can call it like this for example:
    Code (CSharp):
    1. camera.FocusOn(objectToFocusOn, 1.1f);
     
    Last edited: Jan 11, 2021
  9. djexstas9

    djexstas9

    Joined:
    Mar 26, 2019
    Posts:
    12
    1) It will fail if you have no children renderers, because Linq.FirstOrDefault() will return null and you'll try to get bounds of null mesh renderer. Consider adding case when no renderers were found and you're returning empty bounds.
    2) GetComponentsInChildren also return components contained by the gameObject which you calling it on. So you can remove the "Parent" part.

    Code (CSharp):
    1.  
    2. public static Bounds GetBoundsWithChildren(this GameObject gameObject)
    3. {
    4.     // GetComponentsInChildren() also returns components on gameobject which you call it on
    5.     // you don't need to get component specially on gameObject
    6.     Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
    7.  
    8.     // If renderers.Length = 0, you'll get OutOfRangeException
    9.     // or null when using Linq's FirstOrDefault() and try to get bounds of null
    10.     Bounds bounds = renderers.Length > 0 ? renderers[0].bounds : new Bounds();
    11.  
    12.     // Or if you like using Linq
    13.     // Bounds bounds = renderers.Length > 0 ? renderers.FirstOrDefault().bounds : new Bounds();
    14.  
    15.     // Start from 1 because we've already encapsulated renderers[0]
    16.     for (int i = 1; i < renderers.Length; i++)
    17.     {
    18.         if (renderers[i].enabled)
    19.         {
    20.             bounds.Encapsulate(renderers[i].bounds);
    21.         }
    22.     }
    23.  
    24.     return bounds;
    25. }
    26.  
     
  10. djexstas9

    djexstas9

    Joined:
    Mar 26, 2019
    Posts:
    12
    Also i'm working on rendering icons automatically from meshes and I need it to exactly fit object to camera rect without spaces.
    I've made it working but only with orthogonal camera and sorting actual vertices of a mesh, not bounds, because when you use bounds with rotated mesh or rotated camera, after you transformed bounds corners to camera view space, these corners will add extra space to object's bounding view rect and you'll never get your icon centered to camera rect. But with perspective camera it works not as expected because of projecting points on camera plane is orthogonal and you need to project them as perspective does it. So method presented above is quite working, it's not bad for runtime camera focus but it's not accurate in most cases and I can't find an info anywhere about perspective-dependent projection of points on camera plane :(
     
  11. HaMMMMer

    HaMMMMer

    Joined:
    Oct 25, 2021
    Posts:
    9
    Your code works prefect for me, but cannot understand Why the cameraDistance is 2.0f?
    Because I am trying to set this value to nearClipPlane, which from my understanding it will make the object's maxSize fill the screen properly, for example, if the max value is bbx.y, then this obj should fill the screen vertically, but it's not.
     
    Last edited: Apr 22, 2022
  12. cyavictor88

    cyavictor88

    Joined:
    Feb 28, 2022
    Posts:
    3
    camautozoom1.png camautozoom2.png camautozoom3.png camautozoom4.png
    Code (CSharp):
    1. float virtualsphereRadius = Vector3.Magnitude(bounds.max-bounds.center);
    2. float minD = (virtualsphereRadius )/ Mathf.Sin(Mathf.Deg2Rad*cam.fieldOfView/2);
    3. Vector3 normVectorBoundsCenter2CurrentCamPos= (cam.transform.position - bounds.center) / Vector3.Magnitude(cam.transform.position -  bounds.center);
    4. cam.transform.position =  minD*normVectorBoundsCenter2CurrentCamPos;
    5. cam.transform.LookAt(bounds.center);
    6. cam.nearClipPlane = minD- virtualsphereRadius;
     
    Last edited: Jun 5, 2022
  13. SxWx

    SxWx

    Joined:
    Apr 13, 2013
    Posts:
    3
    dist.jpg

    Option with sine is wrong. Calculate with tan:

    Code (CSharp):
    1. var centerAtFront = new Vector3(bounds.center.x, bounds.center.y, bounds.max.z);
    2. var centerAtFrontTop = new Vector3(bounds.center.x, bounds.max.y, bounds.max.z);
    3. var centerToTopDist = (centerAtFrontTop - centerAtFront).magnitude;
    4. var minDistance = (centerToTopDist * marginPercentage) / Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
    5.  
    6. camera.transform.position = new Vector3(bounds.center.x, bounds.center.y, -minDistance);
    7. camera.transform.LookAt(bounds.center);
     
    waleedmm likes this.
  14. NotEXEfly

    NotEXEfly

    Joined:
    Aug 24, 2020
    Posts:
    1
    How to achieve the same effect only by changing the fov instead of the camera position?
     
  15. FiveFingerStudios

    FiveFingerStudios

    Joined:
    Apr 22, 2016
    Posts:
    510
    Did you ever figure this out? I'm having the same issue you have, I'm trying to make icons from meshes and the extra space from rotating the mesh preventing me from taking good snapshots.
     
  16. FiveFingerStudios

    FiveFingerStudios

    Joined:
    Apr 22, 2016
    Posts:
    510
    Looks like I found the issue for me. It looks like the bounding box gets bigger as the gameobject moves away from world space 0,0,0 position. Not sure why that is in my case but I fixed it by moving my gameObject to 0,0,0.
     
  17. vanderFeest

    vanderFeest

    Joined:
    Apr 12, 2017
    Posts:
    8
    The proposed solutions only work for fitting in vertical camera direction; it doesn't account for wide objects or portrait screens.
    Also, they presume the camera to be facing forward and the object to be on the origin.

    I'm just sharing my code, this one uses a downward facing camera, I didn't have the need to take camera rotation into account but to be complete someone would need to do some more maths.

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEngine;
    3.  
    4. public class CameraFitter : MonoBehaviour
    5. {
    6.     public Camera Camera;
    7.     public GameObject FitObject;
    8.  
    9.     void Awake()
    10.     {
    11.         FocusOn(Camera, FitObject, .99f);
    12.     }
    13.  
    14.     public static Bounds GetBoundsWithChildren(GameObject gameObject)
    15.     {
    16.         Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
    17.         Bounds bounds = renderers.Length > 0 ? renderers.FirstOrDefault().bounds : new Bounds();
    18.  
    19.         for(int i = 1; i < renderers.Length; i++)
    20.         {
    21.             if(renderers[i].enabled)
    22.             {
    23.                 bounds.Encapsulate(renderers[i].bounds);
    24.             }
    25.         }
    26.  
    27.         return bounds;
    28.     }
    29.     public static void FocusOn(Camera camera, GameObject focusedObject, float marginPercentage)
    30.     {
    31.         Bounds bounds = GetBoundsWithChildren(focusedObject);
    32.         Vector3 centerAtFront = new(bounds.center.x, bounds.max.y, bounds.center.z);
    33.         Vector3 centerAtFrontTop = new(bounds.center.x, bounds.max.y, bounds.max.z);
    34.         float centerToTopDist = (centerAtFrontTop - centerAtFront).magnitude;
    35.         float minDistanceY = centerToTopDist * marginPercentage / Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
    36.  
    37.         Vector3 centerAtFrontRight = new(bounds.max.x, bounds.center.y, bounds.max.z);
    38.         float centerToRightDist = (centerAtFrontRight - centerAtFront).magnitude;
    39.         float minDistanceX = centerToRightDist * marginPercentage / Mathf.Tan(camera.fieldOfView * camera.aspect * Mathf.Deg2Rad);
    40.  
    41.         float minDistance = Mathf.Max(minDistanceX, minDistanceY);
    42.  
    43.         camera.transform.position = new Vector3(bounds.center.x, bounds.center.y + minDistance, bounds.center.z);
    44.         camera.transform.LookAt(bounds.center);
    45.     }
    46. }
    47.  
     
    waleedmm likes this.
  18. Bmco

    Bmco

    Joined:
    Mar 11, 2020
    Posts:
    54
    I came at this problem with a different approach. This solved the issue for me and was easy to understand.

    Code (CSharp):
    1.        
    2.         void MinMaxOnScreen()
    3.         {
    4.             Bounds bounds = goToFollow.GetComponent<MeshRenderer>().bounds;
    5.  
    6.             Vector3 ssMin = GameManager.mainCamera.WorldToScreenPoint(bounds.min);
    7.             Vector3 ssMax = GameManager.mainCamera.WorldToScreenPoint(bounds.max);
    8.             //Add more Bounds Corners for more accuracy
    9.  
    10.             float pixelBoundary = 1.5f * Screen.dpi; //A simple way to add a clearance border (1.5" of screen)
    11.  
    12.             float minX = Mathf.Min(ssMin.x, ssMax.x) - pixelBoundary;
    13.             float maxX = Mathf.Max(ssMin.x, ssMax.x) + pixelBoundary;
    14.  
    15.             float minY = Mathf.Min(ssMin.y, ssMax.y) - pixelBoundary;
    16.             float maxY = Mathf.Max(ssMin.y, ssMax.y) + pixelBoundary;
    17.  
    18.             if (minX < 0 || minY < 0 || maxX > Screen.width || maxY > Screen.height)
    19.                 Debug.Log("Partially OffScreen ");
    20.         }
     
    waleedmm likes this.