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

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!

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.

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:

float cameraDistance = 2.0f; // Constant factor
Vector3 objectSizes = bounds.max - bounds.min;
float objectSize = Mathf.Max(objectSizes.x, objectSizes.y, objectSizes.z);
float cameraView = 2.0f * Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); // Visible height 1 meter in front
float distance = cameraDistance * objectSize / cameraView; // Combined wanted distance from the object
distance += 0.5f * objectSize; // Estimated offset from the center to the outside of the object
camera.transform.position = bounds.center - distance * camera.transform.forward;
10 Likes

My goodness !! It worked like a charm. Thanks. I wish that i could understand the mathematics behind this code snippet.

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.

1 Like

You can get a bounds from either a collider or a meshRenderer (from the object you want to focus on)

    public Collider collider;
    public float elevation;
    public float cameraDistance = 2.0f;

    void Update()
    {
        Vector3 objectSizes = collider.bounds.max - collider.bounds.min;
        float objectSize = Mathf.Max(objectSizes.x, objectSizes.y, objectSizes.z);
        float cameraView = 2.0f * Mathf.Tan(0.5f * Mathf.Deg2Rad * Camera.main.fieldOfView); // Visible height 1 meter in front
        float distance = cameraDistance * objectSize / cameraView; // Combined wanted distance from the object
        distance += 0.5f * objectSize; // Estimated offset from the center to the outside of the object
        Camera.main.transform.position = collider.bounds.center - distance * Camera.main.transform.forward;
        Camera.main.transform.rotation = Quaternion.Euler(new Vector3(elevation, 0, 0));
    }
5 Likes

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 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:

sin(theta) = o/h
sin(fov/2) = r/h
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:

const float margin = 1.1f;
float maxExtent = b.extents.magnitude;
float minDistance = (maxExtent * margin) / Mathf.Sin(Mathf.Deg2Rad * _camera.fieldOfView / 2.0f);
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:

Camera.main.nearClipPlane = minDistance - maxExtent;

Which gives me maximum precision in my depth buffer by not allowing the camera any closer than minDistance.

16 Likes

Thanks a lot!

For case when object may also have renderers in children, I combined this answer with answer from this topic: https://discussions.unity.com/t/431270 .

You need 2 extension methods:

   public static Bounds GetBoundsWithChildren(this GameObject gameObject)
   {
      Renderer parentRenderer = gameObject.GetComponent<Renderer>();

      Renderer[] childrenRenderers = gameObject.GetComponentsInChildren<Renderer>();

      Bounds bounds = parentRenderer != null
         ? parentRenderer.bounds
         : childrenRenderers.FirstOrDefault(x => x.enabled).bounds;

      if (childrenRenderers.Length > 0)
      {
         foreach (Renderer renderer in childrenRenderers)
         {
            if (renderer.enabled)
            {
               bounds.Encapsulate(renderer.bounds);
            }
         }
      }

      return bounds;
   }

   public static void FocusOn(this Camera camera, GameObject focusedObject, float marginPercentage)
   {
      Bounds bounds = focusedObject.GetBoundsWithChildren();
      float maxExtent = bounds.extents.magnitude;
      float minDistance = (maxExtent * marginPercentage) / Mathf.Sin(Mathf.Deg2Rad * camera.fieldOfView / 2f);
      camera.transform.position = focusedObject.transform.position - Vector3.forward * minDistance;
camera.nearClipPlane = minDistance - maxExtent;
   }

And then you can call it like this for example:

camera.FocusOn(objectToFocusOn, 1.1f);
6 Likes
  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.
public static Bounds GetBoundsWithChildren(this GameObject gameObject)
{
    // GetComponentsInChildren() also returns components on gameobject which you call it on
    // you don't need to get component specially on gameObject
    Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();

    // If renderers.Length = 0, you'll get OutOfRangeException 
    // or null when using Linq's FirstOrDefault() and try to get bounds of null
    Bounds bounds = renderers.Length > 0 ? renderers[0].bounds : new Bounds();

    // Or if you like using Linq
    // Bounds bounds = renderers.Length > 0 ? renderers.FirstOrDefault().bounds : new Bounds();

    // Start from 1 because we've already encapsulated renderers[0]
    for (int i = 1; i < renderers.Length; i++)
    {
        if (renderers[i].enabled)
        {
            bounds.Encapsulate(renderers[i].bounds);
        }
    }

    return bounds;
}
5 Likes

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 :frowning:

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.

float virtualsphereRadius = Vector3.Magnitude(bounds.max-bounds.center);
float minD = (virtualsphereRadius )/ Mathf.Sin(Mathf.Deg2Rad*cam.fieldOfView/2);
Vector3 normVectorBoundsCenter2CurrentCamPos= (cam.transform.position - bounds.center) / Vector3.Magnitude(cam.transform.position -  bounds.center);
cam.transform.position =  minD*normVectorBoundsCenter2CurrentCamPos;
cam.transform.LookAt(bounds.center);
cam.nearClipPlane = minD- virtualsphereRadius;
5 Likes

8421126--1114116--dist.jpg

Option with sine is wrong. Calculate with tan:

var centerAtFront = new Vector3(bounds.center.x, bounds.center.y, bounds.max.z);
var centerAtFrontTop = new Vector3(bounds.center.x, bounds.max.y, bounds.max.z);
var centerToTopDist = (centerAtFrontTop - centerAtFront).magnitude;
var minDistance = (centerToTopDist * marginPercentage) / Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);

camera.transform.position = new Vector3(bounds.center.x, bounds.center.y, -minDistance);
camera.transform.LookAt(bounds.center);
1 Like

How to achieve the same effect only by changing the fov instead of the camera position?

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.

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.

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.

using System.Linq;
using UnityEngine;

public class CameraFitter : MonoBehaviour
{
    public Camera Camera;
    public GameObject FitObject;

    void Awake()
    {
        FocusOn(Camera, FitObject, .99f);
    }

    public static Bounds GetBoundsWithChildren(GameObject gameObject)
    {
        Renderer[] renderers = gameObject.GetComponentsInChildren<Renderer>();
        Bounds bounds = renderers.Length > 0 ? renderers.FirstOrDefault().bounds : new Bounds();

        for(int i = 1; i < renderers.Length; i++)
        {
            if(renderers[i].enabled)
            {
                bounds.Encapsulate(renderers[i].bounds);
            }
        }

        return bounds;
    }
    public static void FocusOn(Camera camera, GameObject focusedObject, float marginPercentage)
    {
        Bounds bounds = GetBoundsWithChildren(focusedObject);
        Vector3 centerAtFront = new(bounds.center.x, bounds.max.y, bounds.center.z);
        Vector3 centerAtFrontTop = new(bounds.center.x, bounds.max.y, bounds.max.z);
        float centerToTopDist = (centerAtFrontTop - centerAtFront).magnitude;
        float minDistanceY = centerToTopDist * marginPercentage / Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);

        Vector3 centerAtFrontRight = new(bounds.max.x, bounds.center.y, bounds.max.z);
        float centerToRightDist = (centerAtFrontRight - centerAtFront).magnitude;
        float minDistanceX = centerToRightDist * marginPercentage / Mathf.Tan(camera.fieldOfView * camera.aspect * Mathf.Deg2Rad);

        float minDistance = Mathf.Max(minDistanceX, minDistanceY);

        camera.transform.position = new Vector3(bounds.center.x, bounds.center.y + minDistance, bounds.center.z);
        camera.transform.LookAt(bounds.center);
    }
}
1 Like

I came at this problem with a different approach. This solved the issue for me and was easy to understand.

        void MinMaxOnScreen()
        {
            Bounds bounds = goToFollow.GetComponent<MeshRenderer>().bounds;

            Vector3 ssMin = GameManager.mainCamera.WorldToScreenPoint(bounds.min);
            Vector3 ssMax = GameManager.mainCamera.WorldToScreenPoint(bounds.max);
            //Add more Bounds Corners for more accuracy

            float pixelBoundary = 1.5f * Screen.dpi; //A simple way to add a clearance border (1.5" of screen)

            float minX = Mathf.Min(ssMin.x, ssMax.x) - pixelBoundary;
            float maxX = Mathf.Max(ssMin.x, ssMax.x) + pixelBoundary;

            float minY = Mathf.Min(ssMin.y, ssMax.y) - pixelBoundary;
            float maxY = Mathf.Max(ssMin.y, ssMax.y) + pixelBoundary;

            if (minX < 0 || minY < 0 || maxX > Screen.width || maxY > Screen.height)
                Debug.Log("Partially OffScreen ");
        }
1 Like

Sorry for the bump, but for anyone else coming to this thread interested in fitting 3D renderers/colliders into a Camera by adjusting only its FOV without changing the transform, I’ve made this package Camera FOV Fit: https://github.com/gilzoide/unity-camera-fov-fit.
Just add the component to any object, set the camera and renderer/collider in the Inspector and it’s done!

The math in it is a basic rule of three between the Bounds’ corners as converted by WorldToViewportPoint and the viewport’s corners (-1,-1) and (1,1). The proportion found is applied to either fieldOfView for perspective Cameras or orthographicSize for orthographic Cameras. It fits the 3D object both vertically and horizontally, so one can use this for both portrait and landscape Cameras without modifications.

As always, issues, discussions and contributions are very welcome in the repo. Hope it helps ^^