Calculate the bounds of the visible size of a 3D object for a perspective camera.

Hi I’m trying to make a 3D object fill my screen by changing the camera’s FOV to fit the objects bounds.

I’ve got this working well when looking directly at a plane, but for a 3D object at an angle the visible width and height is larger due to the perspective angle.

Is there a way to calculate the faux 2d bounds of a 3d object as seen through a perspective camera?

I’m using this method for calculating the fov:

    void setFovForObject(Camera camera, GameObject go)
    {
        Bounds bounds = getBounds(go);
    
        float fov1 = GetFieldOfView(camera, go.transform, bounds.size.z);
        float fov2 = getHorizontalFov(GetFieldOfView(camera, go.trasnform, bounds.size.x), ((float)camera.pixelHeight / camera.pixelWidth));

        camera.fieldOfView = Mathf.Max(fov1, fov2);
    }

    float GetFieldOfView(Camera cam, Transform g, float size)
    {
        Vector3 diff = g.position - cam.transform.position;
        float distance = Vector3.Dot(diff, cam.transform.forward);
        float angle = Mathf.Atan((size * .5f) / distance);
        return angle * 2f * Mathf.Rad2Deg;
    }

    float getHorizontalFov(float fov, float aspect)
    {
        float radAngle = fov * Mathf.Deg2Rad;
        float radHFOV = 2.0f * (float)Math.Atan(Mathf.Tan(radAngle / 2) * aspect);
        float hFOV = Mathf.Rad2Deg * radHFOV;

        return hFOV;
    }

    public Bounds getBounds(GameObject go)
    {
        Renderer renderer = go.GetComponent<Renderer>();
        Bounds combinedBounds = new Bounds();
        Renderer[] renderers = go.GetComponentsInChildren<Renderer>();
        foreach (Renderer render in renderers)
        {
            if (render != renderer) combinedBounds.Encapsulate(render.bounds);
        }

        return combinedBounds;
    }

But obviously the computed bounds I’m using here do not account for the objects size as seen through a perspective camera.

So, given a cube like this I’d like to calculate the dashed bounding box for a given camera and then set my FOV to make it fill the screen.

I’m at a loss for how to do this, any ideas?

Sorry for the bump, but does anyone know how to do this?

Any hints to help me research what to do?

you can project all corners of the bounds to screenspace, then get the min and max ones (x/y) and build a rect from those.

Thanks ValooFX,

I think I’m close with this, but not quite there - can you see what I’m missing or have got wrong?

    public static void getBounds(this GameObject go, Camera camera, out float width, out float height)
    {
        var renderer = go.GetComponent<Renderer>();
        var combinedBounds = new Bounds();
        var renderers = go.GetComponentsInChildren<Renderer>();
        foreach (Renderer render in renderers)
        {
            if (render != renderer) combinedBounds.Encapsulate(render.bounds);
        }

        Vector3 center = Vector3.zero;
        Vector3[] points = new Vector3[8];
        points[0] = center + new Vector3(combinedBounds.extents.x, combinedBounds.extents.y, combinedBounds.extents.z);
        points[1] = center + new Vector3(combinedBounds.extents.x, combinedBounds.extents.y, -combinedBounds.extents.z);
        points[2] = center + new Vector3(combinedBounds.extents.x, -combinedBounds.extents.y, combinedBounds.extents.z);
        points[3] = center + new Vector3(combinedBounds.extents.x, -combinedBounds.extents.y, -combinedBounds.extents.z);
        points[4] = center + new Vector3(-combinedBounds.extents.x, combinedBounds.extents.y, combinedBounds.extents.z);
        points[5] = center + new Vector3(-combinedBounds.extents.x, combinedBounds.extents.y, -combinedBounds.extents.z);
        points[6] = center + new Vector3(-combinedBounds.extents.x, -combinedBounds.extents.y, combinedBounds.extents.z);
        points[7] = center + new Vector3(-combinedBounds.extents.x, -combinedBounds.extents.y, -combinedBounds.extents.z);



        for (int i = 0; i < 8; i++)
        {
            points[i] = camera.transform.TransformPoint(points[i]);
        }

        float minX = points[0].x;
        float minY = points[0].y;
        float maxX = points[0].x;
        float maxY = points[0].y;


        for (int i = 0; i < 8; i++)
        {
            maxX = Mathf.Max(points[i].x, maxX);
            minX = Mathf.Min(points[i].x, minX);
            maxY = Mathf.Max(points[i].y, maxY);
            minY = Mathf.Min(points[i].y, minY);
        }
        width = maxX - minX;
        height = maxY - minY;

    }

I’ve been struggling with this for some time now… Seems I’m out of my depth :frowning:

using System;
using System.Collections.Generic;
using UnityEngine;

public class ScreenSpaceSizeTest : MonoBehaviour
{
    public GameObject target;

    [Range(0, 1f)]
    public float RendererOrMeshBounds;

    public Rect GetScreenSpaceRectFromRendererBounds(GameObject go, Camera cam)
    {
        if (go == null || cam == null)
        {
            return Rect.zero;
        }
        var renderers = go.GetComponentsInChildren<Renderer>();
        var bounds = renderers[0].bounds;
        for (var i = 1; i < renderers.Length; i++)
        {
            bounds.Encapsulate(renderers[i].bounds);
        }
        var ext = bounds.extents;
        var screenPositions = new List<Vector2>();
        for (var x = -1; x < 2; x+=2)
        {
            for (var y = -1; y < 2; y+=2)
            {
                for (var z = -1; z < 2; z+=2)
                {
                    var vect = bounds.center+new Vector3(ext.x*x, ext.y*y, ext.z*z);
                    var wts = cam.WorldToScreenPoint(vect);
                    wts.y = Screen.height - wts.y;
                    screenPositions.Add(wts);
                }
            }
        }
        var rect = new Rect(screenPositions[0].x, screenPositions[0].y, 1, 1);
        Action<Vector2> FindMinMax = vec =>
        {
            if (vec.x < rect.xMin) rect.xMin = vec.x;
            if (vec.x > rect.xMax) rect.xMax = vec.x;
            if (vec.y < rect.yMin) rect.yMin = vec.y;
            if (vec.y > rect.yMax) rect.yMax = vec.y;
        };
        screenPositions.ForEach(FindMinMax);
        return rect;
    }

    public Rect GetScreenSpaceRectFromMeshBounds(GameObject go, Camera cam)
    {
        if (go == null || cam == null)
        {
            return Rect.zero;
        }
        var mFilters = new List<MeshFilter>(go.GetComponentsInChildren<MeshFilter>());
        mFilters.RemoveAll(filter => filter.sharedMesh == null);
        var bounds = mFilters[0].sharedMesh.bounds;
        bounds.center = go.transform.TransformPoint(bounds.center);
        for (var i = 1; i < mFilters.Count; i++)
        {
            var b = mFilters[i].sharedMesh.bounds;
            b.center = go.transform.TransformPoint(b.center);
            bounds.Encapsulate(b);
        }
        var ext = bounds.extents;
        ext.Scale(go.transform.localScale);
        var screenPositions = new List<Vector2>();
        for (var x = -1; x < 2; x += 2)
        {
            for (var y = -1; y < 2; y += 2)
            {
                for (var z = -1; z < 2; z += 2)
                {
                    var vect = bounds.center + new Vector3(ext.x * x, ext.y * y, ext.z * z);
                    var wts = cam.WorldToScreenPoint(vect);
                    wts.y = Screen.height - wts.y;
                    screenPositions.Add(wts);
                }
            }
        }
        var rect = new Rect(screenPositions[0].x, screenPositions[0].y, 1, 1);
        Action<Vector2> FindMinMax = vec =>
        {
            if (vec.x < rect.xMin) rect.xMin = vec.x;
            if (vec.x > rect.xMax) rect.xMax = vec.x;
            if (vec.y < rect.yMin) rect.yMin = vec.y;
            if (vec.y > rect.yMax) rect.yMax = vec.y;
        };
        screenPositions.ForEach(FindMinMax);
        return rect;
    }

    void OnDrawGizmos()
    {
        var rend = target.GetComponent<Renderer>();
        Gizmos.DrawWireCube(rend.bounds.center, rend.bounds.size);
    }

    void OnGUI()
    {
        var r = GetScreenSpaceRectFromRendererBounds(target, Camera.main);
        var r2 = GetScreenSpaceRectFromMeshBounds(target, Camera.main);
        r.xMin = Mathf.Lerp(r.xMin, r2.xMin, RendererOrMeshBounds);
        r.xMax = Mathf.Lerp(r.xMax, r2.xMax, RendererOrMeshBounds);
        r.yMin = Mathf.Lerp(r.yMin, r2.yMin, RendererOrMeshBounds);
        r.yMax = Mathf.Lerp(r.yMax, r2.yMax, RendererOrMeshBounds);
        GUI.Box(r, "");
    }
}

That should give you an idea.
The most accurate method though, i think, should be rendering your object like a mask to a RenderTexture and from that calculate the min/max values.

Thanks a lot ValooFX, I’ll give the a look over tomorrow!

Very much appreciated!

how was it?? Im trying soon…