Faking orthographic zoom by scaling positions

I’m working on a 2D scene that displays a randomised solar system to scale. For display purposes, I divide the position (measured in metres, don’t worry, I’m storing it as a double) of a planet by a zoom level, so that on starting a planet 1AU from it’s star is 1 unit away in the game. I use a floating origin and do all my maths as doubles to avoid floating point errors.

In order to zoom, I adjust the zoom level, which changes the game position of the planet, giving a pseudo-orthographic zoom effect. I’m not actually using the orthographic zoom because the orthographic size becomes tiny at the planet scale.

However, I’m finding that my camera position doesn’t change enough as I zoom, it always is bit less than it’s supposed to be. Zooming towards is also implemented here, but the problem is there with or without. I expect that whatever is under the mouse to remain under the mouse, but whatever is under the mouse always ends up moving further away from the solar system origin than the camera. I’ve been scratching my head for ages now trying to figure out what I’m doing wrong. I don’t doubt that there are many things. The relevant code attached to my Main Camera is below:

private void Update()
{
    //zoom according to mouse input
    if (Input.GetAxis("Mouse ScrollWheel") > 0)
    {
        ZoomOrthoCamera(cam.ScreenToWorldPoint(Input.mousePosition), true);
    }

    if (Input.GetAxis("Mouse ScrollWheel") < 0)
    {
        ZoomOrthoCamera(cam.ScreenToWorldPoint(Input.mousePosition), false);
    }

    //pan according to input
    float panHorizontal = Input.GetAxis("Horizontal");
    float panVertical = Input.GetAxis("Vertical");
    transform.position += new Vector3(panHorizontal * panSpeed, panVertical * panSpeed, 0f);
}

The place where the magic happens (I’m using a double version of Vector3 called Vector3d and then converting back to Vector3 after all the maths, for accuracy):

private void ZoomOrthoCamera(Vector3 zoomToward, bool isZoomingIn)
{
    //change solar system zoom level by a fraction rather than a fixed amount since we want to change orders of magnitude
    SolarSystemView.instance.zoomLevel -= (isZoomingIn ? 1 : -1) * SolarSystemView.instance.zoomLevel * ActualZoomFactor(isZoomingIn);

    //since the solar system might not be at the game origin due to floating origin, we find our camera's position from the solar system's centre
    Vector3d vectorToCentre = new Vector3d(transform.position.x, transform.position.y, 0) - SolarSystemView.instance.positiond;

    //scale our solary system camera coordinates according to the new zoom level
    Vector3d newRelativePosition = new Vector3d(
        vectorToCentre.x + (isZoomingIn ? 1 : -1) * vectorToCentre.x * ActualZoomFactor(isZoomingIn),
        vectorToCentre.y + (isZoomingIn ? 1 : -1) * vectorToCentre.y * ActualZoomFactor(isZoomingIn),
        MagicNumbers.mainCameraPos.z
        );

    //convert solar system camera coordinates back into game coordinates
    Vector3d newVectorToCentre = new Vector3d(newRelativePosition.x, newRelativePosition.y, 0) + SolarSystemView.instance.positiond;

    //zoom towards mouse location
    Vector3d zoomTowardPrecise = new Vector3d(zoomToward.x, zoomToward.y);

    //since the solar system might not be at the game origin due to floating origin, we find our zoom towards position from the solar system's centre
    Vector3d zoomVectorToCentre = new Vector3d(zoomTowardPrecise.x, zoomTowardPrecise.y, 0) - SolarSystemView.instance.positiond;

    //scale our solar system zoom towards coordinates according to the new zoom level
    Vector3d zoomNewRelativePosition = new Vector3d(
        zoomVectorToCentre.x + (isZoomingIn ? 1 : -1) * zoomVectorToCentre.x * ActualZoomFactor(isZoomingIn),
        zoomVectorToCentre.y + (isZoomingIn ? 1 : -1) * zoomVectorToCentre.y * ActualZoomFactor(isZoomingIn),
        MagicNumbers.mainCameraPos.z
        );

    //convert solar system zoom towards coordinates back into game coordinates
    Vector3d zoomNewVectorToCentre = new Vector3d(zoomNewRelativePosition.x, zoomNewRelativePosition.y, 0) + SolarSystemView.instance.positiond;

    //find the vector we need to move the camera along to zoom towards the mouse position
    Vector3d zoomTranslation = (zoomNewVectorToCentre - newVectorToCentre) * ActualZoomTowardsFactor(isZoomingIn);

    //finally get new camera position in game coordinates
    transform.position = new Vector3(
        (float)(newVectorToCentre.x + (isZoomingIn ? 1 : -1) * zoomTranslation.x),
        (float)(newVectorToCentre.y + (isZoomingIn ? 1 : -1) * zoomTranslation.y),
        MagicNumbers.mainCameraPos.z
        );
}

The extra functions referenced in the above (different in case they needed to be treated differently):

private double ActualZoomFactor(bool isZoomingIn)
    {
        //we want to be able to get back to inital zoom level when we zoom out so do some maths that allows this 
        return isZoomingIn ? zoomFraction : zoomFraction / (1d - zoomFraction);
    }
    
    private double ActualZoomTowardsFactor(bool isZoomingIn)
    {
        //we want to be able to get back to inital zoom level when we zoom out so do some maths that allows this 
        return isZoomingIn ? zoomFraction : zoomFraction / (1d - zoomFraction);
    }

Much obliged for any help or advice given!

Here is a version that can do fake scaling, leaving the orthagraphicSize untouched. It uses a parent object on which to apply the scale (rather than applying the scaling to the transform of each object in view.) The fake zoom will only work right for objects that are children of this parent object. This camera must ALSO be made a child of this object for it to work right.

 public float zoomSpeed = 0.1f;
     public bool fakeZoomWithScale=false;
     public Transform fakeZoomParentObject;// all objects that need to be scaled, and this camera object, should be children of this transform.
     private void ZoomOrthoCamera(Vector3 zoomToward, bool isZoomingIn)
     {
         float negSpeed = zoomSpeed * (isZoomingIn ? 1 : -1);
         Vector3 camToTarget = zoomToward - transform.position;
         if (!fakeZoomParentObject)
         {
             Camera cam = GetComponent<Camera>();
             cam.orthographicSize -= cam.orthographicSize * negSpeed;
         }
         else
         {
             if (fakeZoomParentObject != null)
                 fakeZoomParentObject.localScale += fakeZoomParentObject.localScale * negSpeed;
         }
         transform.position += (camToTarget * negSpeed);
     }

I would expect problems designating a full Astronomical Unit Distance, a value of 1.0 “units”. If you made an AU say… 10,000 “units” instead, you would be able to use much more reasonable scales in the camera when looking at a planet or star.

In other words (and different numbers): If a moon, is the smallest object you can see - it’s radius of 1,000miles should probably be “less than, but near 1.0” in your “units”… maybe a radius of 0.001f (a small number, but not unreasonably so), a planet can have a radius of about 0.004f (4,000 mi), and a star can have a radius of about .40f (400,000 mi). Planetary distances would be 93e3f for one AU, but still gets pretty high at 2.8e6f (for Neptune’s orbital radius of 30AU)
This RANGE does indeed make single-precision floats insufficient. E.g. Adding a moon’s radius to the position of Neptune, using a float, will have NO effect on the position, it’s too small (relatively) to register (float’s can work, -with a possible loss of accuracy- with ratios of at most, about 1 in 8.3e6- or [2^23]). The double precision SHOULD be sufficient (working ratio: aprox 1 in 4.5e15!! [2^52]). This also seems like a good reason to do the fake zoom.

Regarding the parent scaling object: yes, all children objects will be scaled by this object’s transform. So, if you have say… sprites, scaled to your planet, by making it a child of the planet: When you scale the root object, both the sprite AND the object will be scaled.

Keep in mind the “final” or Global scale of the object in a transform hierarchy is the computed “lossyScale” (readonly), not the localScale (these are the same only when the localScale object has no parents, or all the parents have an even scaling of 1.0f).