# Zooming and panning a camera simultaneously causes a swooping effect

In a 2d game, I have a basic camera class. The guts of the class creates a transformation matrix that is later passed to the Begin() method of a SpriteBatch. The transformation is done like this:

        Transform = Matrix.Identity *
Matrix.CreateTranslation(-Position.X, -Position.Y, 0) *
Matrix.CreateScale(new Vector3(Scale, Scale, Scale)) *
Matrix.CreateTranslation(ScreenCenter.X, ScreenCenter.Y, 0);


I'm trying to create an effect that zooms in on a specific point on the screen. The issue is that the zoom/translation causes a strange "swooping" effect when applied. For example, when the zoom is set to 1.0 and the current position is (0, 0), and I change these to 2.0 and (100, 200) over a 1 second interval, the camera appears to take a curved path to (100, 200).

Here is my code to update the position / scale on each frame (I removed logic to determine when the transition is finished):

        _positionTimeElapsed += gameTime.ElapsedGameTime.TotalMilliseconds;
_scaleTimeElapsed += gameTime.ElapsedGameTime.TotalMilliseconds;

Position = Vector2.Lerp(
_positionStart,
PositionDestination.Value,
(float)(_positionTimeElapsed / TransitionTimeMilliseconds));

Scale = (ScaleDestination.Value - _scaleStart)
* (float)(_scaleTimeElapsed / TransitionTimeMilliseconds)
+ _scaleStart;


I'm relatively confident that I'm missing something basic about Matrix math. Does anyone know how to transition both position and scale simultaneously and have the camera appear to take a linear path to the destination?

UPDATE

Added two videos on YouTube that show the issue and the fix in action. These videos zoom to 10x over 5 seconds to make the bug obvious.

• At first glance I don't see anything obvious. My first check would be that order of operations matter with Matrix multiplication and simply re-ordering the operations could be a starting point to try. I'd try moving the Scale first. Also I think you can just use the scalar Scale * Matrix.Identity rather than needing CreateScale. On the other hand, you could probably just use the CreateScale entirely instead of Matrix.Identity. Sep 27, 2010 at 2:15
• At the end of the transition, everything is correctly scaled and positioned, so I'd be very surprised if it's an order of operations issue. That was the first thing that came to mind, but I believe I have ruled it out (always possible that I screwed up the math when trying to rule it out). Sep 27, 2010 at 9:22

Your swooping effect is because there is no stationary pixel during the interpolation. In a dolly and pan, there will be a point (a line actually) that doesn't change in appearance other than mip level.

You're using a zoom/scale, and because of this you're suffering from an accelerated scale problem, if you tried going from 2.0 to 0.1, then it would become more obvious why this was a non-linear operation.

You need to consider the zoom as it's inverse, then this goes away. at half-way, your zoom is 1.5, but if you took the inverse of your zoom (1.0 -> 0.5 instead of 1.0 to 2.0), then half-way becomes 1.3333 (1.0/0.75), which actually gives you the zoom you want at that part of your linear interpolation across space (the 0,0 to 100,200).

For Kylotan: To make a camera look like it's not swooping, you have to be able to pin a point in the screen. What you're after is a linear interpolation of the top,bottom,left,and right values of the render rectangle, well, that's not going to happen if you're interpolating the scale, because that's the "inverse" of the size of the rectangle.

What happened was, interpolating the scale cause the rectangle to shrink non linearly over time. example: assuming the rectangle was -100,100,-100,100 (that's left,right,top,bottom) translation moves it to 0,200,100,300, but shrinks (scaled 0.5) as the scene is zoomed to 50,150,150,250 that's the final destination of the interpolation... but, if you interpolate the zoom... zoom at 1.5 == 0.666 scale in render rectangle, leading to -16.666,116.666,33.333,166.666

At any point during the interpolation, the world point 200,400 should always appear in the same relative position to the render rectangle. At the start it is 200,400 but at scale 1, and at the end it is at 100,200 relative, but scaled up by 2 due to scene scaling, so it's still the same position relatively.

if you take the halfway point, it's at 150,300, but the scene is scaled by 1/.75 at this point so the relative position is once again 200,400 (150/0.75==200 & 300/0.75==400)

• Wow, that was easy. Thanks! Lost about three hours to this problem, and the fix was literally to take the inverse of all of my variables with "scale" in the name, and then take the inverse of that final value before assigning it to Scale. Sep 27, 2010 at 12:51
• Richard, doesn't this go away if you treat the centre of the screen as the camera position? That will move linearly and the pixel at the point will not be altered by the scale. I'm finding it hard to picture this problem in the first place and sadly I'm no wiser having read this answer! Sep 28, 2010 at 14:35
• Kylotan, I have updated the question with two YouTube videos to help with visualization. I should have done this from the start. Sep 28, 2010 at 21:31
• Thanks Alex and Richard for elaborating on the problem and the solution. Sep 29, 2010 at 12:26

I used Richard Fabian's answer and reversed my interpolation function, which worked for me.

Here is the code I used, in case it's useful for other readers:

  interpolate(p0, p1, k) {
return p0+(p1-p0)*k;
}

interpolateScale(s0, s1, k) {
return 1/this.interpolate(1/s0, 1/s1, k);
}


I was able to achieve this:

Basically, instead of linearly interpolating the zoom, I linearly interpolated the height of the camera viewport and then backwards-calculated the zoom to get that height. Here's the code I used:

camera *Camera = &Renderer->OffscreenFramebuffer.Camera;

const float DURATION = 5.0f;

const v2 TARGET_CAMERA_POS = V2(179.0f, 455.0f);
const float TARGET_CAMERA_VIEWPORT_HEIGHT = 50.0f; // In pixels (so fifty pixels).

static v2 CameraPositionSpeed;

static float CameraHeight;
static float CameraHeightSpeed;

static float TimeElapsed;

static bool IsInitialized = false;
if (!IsInitialized)
{
// Put the camera at the middle of the screen.
Camera->Position = 0.5f*WINDOW_SIZE_PIXELS;

CameraPositionSpeed = (TARGET_CAMERA_POS - Camera->Position)/DURATION;

CameraHeight = WINDOW_HEIGHT_PIXELS;
CameraHeightSpeed = (TARGET_CAMERA_VIEWPORT_HEIGHT - CameraHeight)/DURATION; // The rate at which we change the camera's height.

TimeElapsed = 0.0f;

IsInitialized = true;
}

Camera->Position += Game->dt*CameraPositionSpeed;

CameraHeight += Game->dt*CameraHeightSpeed;
Camera->Zoom = WINDOW_HEIGHT_PIXELS/CameraHeight; // Backwards-calculate the zoom based on the desired height.

// Render texture such that the bottom left is at the origin.
rect2 TextureRect = RectFromPosSize(V2_ZERO, Texture->Size);