Search Unity

How do I keep a sphere the same angular size on the screen as the distance changes?

Discussion in 'General Graphics' started by apg7742, Oct 26, 2021.

  1. apg7742

    apg7742

    Joined:
    Oct 21, 2021
    Posts:
    2
    Theoretically this should be easy, but I'm running into some issues when sanity-checking in two ways. I'm trying to measure the sphere's visual size in degrees using <Renderer>().bounds(), but the output does not match what you would expect from simple trig. The result unexpectedly changes with the camera's field of view (which can be adjusted manually in the inspector).

    The output of the getWidthFromExtents() should match the "ground truth" analytic solution I find from the getTrigSizeDegs() function (about 28 degrees), but it does not - it changes with FOV. It is only the same when the vertical height of the sphere matches the editor window height. Ultimately, my goal is to make the angular size to be invariant over distance as well.

    The script resize.cs with these functions is below. The small project can be accessed at https://github.com/gabrielDiaz-performlab/Ball-size-on-screen.

    Any advice? Thanks.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. public class resize : MonoBehaviour
    5. {
    6. // Update is called once per frame
    7. void Update()
    8. {
    9. getWidthFromExtents();
    10. getTrigSizeDegs();
    11. }
    12. public void getTrigSizeDegs()
    13. {
    14. float dist = Vector3.Distance(Camera.main.transform.position, transform.position);
    15. float halfHeight = transform.localScale.y/2.0f;
    16. float trigSizeDegs = 2.0f * Mathf.Atan( halfHeight / dist) * Mathf.Rad2Deg;
    17. // Debug.Log(Time.frameCount.ToString() + " dist: " + dist.ToString());
    18. // Debug.Log(Time.frameCount.ToString() + " radius: " + halfHeight.ToString());
    19. Debug.Log(Time.frameCount.ToString() + " trigSizeDegs: " + trigSizeDegs.ToString());
    20. }
    21. public void getWidthFromExtents()
    22. {
    23. Renderer rend = GetComponent<Renderer>();
    24. Vector3 cen = rend.bounds.center;
    25. Vector3 ext = rend.bounds.extents;
    26. // https://docs.unity3d.com/ScriptReference/Camera.WorldToViewportPoint.html
    27. float minY = Camera.main.WorldToViewportPoint(new Vector3(cen.x, cen.y-ext.y, cen.z)).y;
    28. float maxY = Camera.main.WorldToViewportPoint(new Vector3(cen.x, cen.y+ext.y, cen.z)).y;
    29. float normHeight = (maxY-minY);
    30. float sizeDegs = Camera.main.fieldOfView * normHeight;
    31. Debug.Log(Time.frameCount.ToString() + " norm height: " + normHeight.ToString());
    32. //Debug.Log(Time.frameCount.ToString() + " Vert screen res: " + screenVertRes.ToString());
    33. //Debug.Log(Time.frameCount.ToString() + " fov: " + Camera.main.fieldOfView.ToString());
    34. Debug.Log(Time.frameCount.ToString() + " extent size: " + sizeDegs.ToString());
    35. Vector3 minVec = new Vector3(cen.x, cen.y-ext.y, cen.z);
    36. Vector3 maxVec = new Vector3(cen.x, cen.y+ext.y, cen.z);
    37. Debug.DrawLine(minVec, maxVec, Color.red, 2.5f);
    38. }
    39. }
    40.  
     
    Last edited: Oct 26, 2021
  2. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Should indeed not be that difficult, but you have to consider that the largest circle you see lies in front of the center of the sphere as I hope you can see by this sketch.

    Sphere.png

    Knowing the distance from the camera to the sphere d and the radius of the sphere r, the actual distance to the circle of the sphere that appears largest in the view is sqrt(d^2-r^2).

    The half angle of the sphere, which you want to keep constant is asin(r/d), where you might have expected that it would be atan(r/d). So, setting the half angle to a, asin(r/d) = a, meaning that r = d*sin(a).
     
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    This is a more complicated topic than it might seem.

    For a really basic implementation, the easiest solution for keeping a sphere roughly a fixed size on screen is to multiply the size of a sphere by the distance from the viewer... and that's kind of all you need to do. Measuring the depth rather than distance will be a little more accurate since it's not actually the distance want since real time rendering is using a linear projection. If you did want it to be affected by the FOV you can can also divide by the camera's projection matrix's [0][0] or [1][1] value.

    But this only really works on a 2D view facing circle, not a sphere. Sphere's are a more complex beast, especially when dealing with linear projection.

    As @jvo3dc mentioned, you'd need to take into account the 3D radius of the sphere to calculate the edge of the visible sphere. Your calculations are more in line with calculating a 2D circle. The render bounds are an axis aligned box, so you're calculating what the visible angle is between the top and bottom of that box, and not the visible area of a sphere. A sphere is always going to be slightly bigger than a 2D circle, camera facing or view facing, for the reason @jvo3dc's sketch shows. It's also not going to be a perfect circle on the screen because of perspective unless it's at the center of your screen.

    This shader toy is a great example of how crazy the screen coverage of spheres can be. This is 3 spheres rendered using a basic linear perspective projection (like any camera in Unity, or almost any GPU rendered thing, will use) and drawing an ellipses to show the screen coverage of that sphere. It also shows a number that is the approximate count for how many screen pixels that sphere covers (not accounting for occlusion by other spheres or frustum clipping).
    https://www.shadertoy.com/view/XdBGzd


    Basically, if you need to keep the sphere exactly a fixed sized on screen, maybe don't use a sphere. Use a circle.