- Home /
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!
You properly formatted your code! I could hug you. Trying to push these Q's through moderation. If nobody comes along to help, tag me in a comment and I'll give this a closer look. However, this Q is significantly more complicated than what we normally get at UA. It's probably better suited to the scripting forum, or perhaps a relevant StackExchange forum. Best,
I'll try where you recommend, since there's been no response so far. Thanks though!
" I use a floating origin and do all my maths as doubles to avoid floating point errors."
Don't let this throw you off: Any non-interger numbering system with a limited number of digits will always result in precision errors. While the precision error for double is much, much smaller than for floats, precision errors ARE introduced.
"In order to zoom, I adjust the zoom level, which changes the game position of the planet," Looking at your code, it looks like you only change the position of the Camera.
"I expect that whatever is under the mouse to remain under the mouse" Hmm, this sounds tricky... I'm not quite sure how you would deter$$anonymous$$e the vector to move the camera such that a particular world point stays at the same (off-center) screen coordinate, it might even be a curve. I'll think about it, but expect it will need to use the camera's field of view angle somehow.
Edit: ok, much easier than expected with an Orthographic Camera. Not sure why you don't want to use orthographicSize; made it pretty easy (if this DOES work for you, let me know and I'll convert it to an answer):
public float zoomSpeed = 0.1f;
private void ZoomOrthoCamera(Vector3 zoomToward, bool isZoo$$anonymous$$gIn)
{
float negSpeed = zoomSpeed * (isZoo$$anonymous$$gIn ? 1 : -1);
Camera cam = this.GetComponent<Camera>();
cam.orthographicSize -= cam.orthographicSize * negSpeed;
Vector3 camToTarget = zoomToward - transform.position;
transform.position += (camToTarget* negSpeed);
}
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 isZoo$$anonymous$$gIn)
{
float negSpeed = zoomSpeed * (isZoo$$anonymous$$gIn ? 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);
}
"ok, much easier than expected with an Orthographic Camera. Not sure why you don't want to use orthographicSize; made it pretty easy"
$$anonymous$$y first approach was pretty much identical to the first snippet of code you posted. $$anonymous$$y motivation for not doing it this way was because I found myself ending up with orthographic sizes on the order of 10e-7 and less, and then my sprites all looked hideous. I toyed about with changing the clipping planes, but that didn't help.
Your second code snippet is pretty crafty, but alas I use the scaling of my objects to adjust sprite sizes. I also use line renderers to draw the orbits of the planets/moons/asteroids etc.
"Looking at your code, it looks like you only change the position of the Camera."
I did indeed neglect to post code from anywhere but my main camera's script. I use a modified version of the script found here to change the position of my SolarSystemView, which is the ultimate parent of all my individual planets etc., and they all change position/size relative to that "origin" as per the following:
public Vector3d GetDisplayedBodyPosition(CelestialBody cb)
{
return cb.position / (UInt64)zoomLevel;
}
public float GetDisplayedSize(CelestialBody cb)
{
return (float)(cb.radius / zoomLevel);
}
Reading over your comment, I suspect that you are right in that I should stick with the normal orthographic camera, and use tricks elsewhere to achieve my goals. This changes the focus of the original question somewhat, but would there be anything you'd recommend?
"alas I use the scaling of my objects to adjust sprite sizes. I also use line renderers to draw the orbits of the planets/moons/asteroids etc."
hmm, not quite clear why this is an issue: if you make the sprites ALSO children of the same parent object, they will scale along with with the planets, and everything else that's a child of the object. I would think the global position of the planets (as opposed to localPosition), would yield the correct coordinates of the scaled planets- what goes wrong with them when using that second snippet?
Possibly helpful for that: $$anonymous$$eep in $$anonymous$$d, even if working with an object that is NOT a child of the object in question: you can still transform a point using that object's transform: check out transform.TransformPoint()
I understand now why you are doing the fake zoom: and it makes sense to avoid those odd camera scales. But I would think the main problem is designating a full Astronomical Unit Distance, a value of 1.0 units. If you made an AU say... 10,000 units ins$$anonymous$$d, you would be able to use much more reasonable scales in the camera when looking at a planet or star.
If you plan on... say... zoo$$anonymous$$g out to show the whole galaxy (or zoo$$anonymous$$g in to show humans on the surface) ALSO, a zoom change of $$anonymous$$ANY orders of magnitude, only THEN I could understand the need for using the fake scaling.
Answer by Glurth · May 23, 2017 at 09:58 PM
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).
Your answer
Follow this Question
Related Questions
2D camera zoom in comparison to the height of target 2 Answers
Zoom out in orthographic view? 2 Answers
Dynamic Orthagraphic Camera Zoom 1 Answer
2d orthographic camera follow 1 Answer
ScreenToWorldPoint not accurate 0 Answers