2
\$\begingroup\$

I'm developing an animation application with a 2D virtual camera. The camera viewport can be positioned and scaled in the keyframes and is then interpolated to render the final animation from the viewpoint of the camera. I'm looking for the best way to interpolate the camera's parameters of x,y position, and scale so that objects in the scene are transformed by the camera change size at a constant rate and so that all objects/pixels in the scene travel in a straight line.

The transform matrix for rendering the scene from the point of view of the camera is calculated from the position and scale as follows, where DimX, DimY are the dimensions of the scene image, Pos and Scale are the position and scale of the camera (the variables that I want to interpolate).

  LCen := PointF(DimX*0.5, DimY*0.5);
  CamTransformInv := TMatrix.CreateTranslation(-(Pos.X + LCen.X), -(Pos.Y + LCen.Y));
  LScaleInv := 1 / Scale;
  CamTransformInv := CamTransformInv * TMatrix.CreateScaling(LScaleInv, LScaleInv);
  CamTransformInv := CamTransformInv * TMatrix.CreateTranslation(LCen.X, LCen.Y);

Below is an animation created by linearly interpolating the scale and position. The interpolation is as follows, where k is the interpolation ratio.

  CamT.Scale := LinearInterpolate(Cam1.Scale, Cam2.Scale, k);
  CamT.Pos.X := LinearInterpolate(Cam1.Pos.X, Cam2.Pos.X, k);
  CamT.Pos.Y := LinearInterpolate(Cam1.Pos.Y, Cam2.Pos.Y, k);

The black line extends from the center of the viewport in the start position to the center of the viewport in the end position. You can see the effect of it appearing to speed up as it zooms in and then suddenly slowing down as it repeats, which I'd like to avoid. On the plus side, all objects in the scene move in a straight line. I've made the animation loop to make the acceleration effect more obvious.

linear linear

Here's what the camera viewport is doing during the interpolation. The position and scale are both changing.

linear viewport

So I modified my code to linearly interpolate the Ln of the scale and take Exp of the result.

  CamT.Scale := Exp(LinearInterpolate(Ln(Cam1.Scale), Ln(Cam2.Scale), k));
  CamT.Pos.X := LinearInterpolate(Cam1.Pos.X, Cam2.Pos.X, k);
  CamT.Pos.Y := LinearInterpolate(Cam1.Pos.Y, Cam2.Pos.Y, k);

This results in an exponential interpolation with the scale change slowing down as it zooms in which looks good since objects in the scene then grow at a constant rate. This makes sense because objects in the scene get multiplied by scale whereas objects get added by position, so interpolation of scale should be multiplicative. This is achieved by taking the log before interpolating. The position is still as linear as before. The problem now is that parts of the image to the sides of the line move in a curve. It doesn't look right (see the top of the tower).

Log linear

Here's what the camera viewport is doing during the interpolation. Note that the top left and bottom right corners of the viewport are traveling along curves.

Log viewport

It occurred to me that the problem is because I'm interpolating the scale non-linearly and the position linearly. If I made the position decelerate in the same way as the scale is decelerating then it would look correct. However, I can't think how this would be computed as the position and scale are coupled. If there's no scale change then the position should change linearly, but the greater the scale change the greater the non-linearity of the position should be. The position interpolation should depend on the scale somehow.

Is there a standard way of doing this? I have tried the following thinking that it should work, but it's not right either.

  CamT.Scale := Exp(LinearInterpolate(Ln(Cam1.Scale), Ln(Cam2.Scale), k));
  CamT.Pos.X := LinearInterpolate(Cam1.Pos.X/Cam1.Scale, Cam2.Pos.X/Cam2.Scale, k) * CamT.Scale;
  CamT.Pos.Y := LinearInterpolate(Cam1.Pos.Y/Cam1.Scale, Cam2.Pos.Y/Cam2.Scale, k) * CamT.Scale;

I did find a similar question from over 10 years ago. The suggestion there was to interpolate the inverse of scale, but that doesn't seem correct mathematically and didn't work when I tried it (I got the opposite acceleration effect).

Zooming and panning a camera simultaneously causes a swooping effect

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

For a smooth loop like this, we want to scale the camera viewport from 1 x at the start to \$r\$ x at the end of the first loop (where \$r\$ is the ratio between the inner image size and the outer image), then \$r^2\$ x at the end of the second loop, \$r^3\$ at the end of the third loop, etc.

A function whose value compounds by some ratio each time we move a fixed distance along its domain is an exponential function. Specifically, if \$t = \frac {\text{time}} {\text{loop period}}\$, then \$\text{scale} = a \cdot r^t\$

scale = initialScale * Pow(ratio, t)

To avoid any visible variation in our movement speed or direction, the rate of travel of the center point across the image should stay in fixed proportion to the current size of the viewport. So that means that our derivative is a multiple of the formula above, and we can integrate it to get an expression for our absolute position. (Here I'm leaving off the coefficient, since we just need proportionality)

$$\int_0^t {r^x}dx\\ = \frac {r^t - 1} {\ln r}$$

At \$t = 0\$ that's 0, and at \$t = 1\$ it's \$\frac {r - 1} {\ln r}\$, so we can multiply by the reciprocal to scale it into an interpolation weight between 0 and 1:

positionInterpolation = (Pow(ratio, t) - 1)/(ratio - 1)

Now you can use this weight as the input to a lerp between your start and end positions, getting a result like this:

Looping zoom

\$\endgroup\$
7
  • \$\begingroup\$ Thanks. Your animation looks to be exactly what I want. The way you interpolate scale gives the same result as my method using log before interpolating. I believe I can work out the positionInterpolation value you gave, but I'm a little stuck after that. What do you mean by 'use this weight as the input to a lerp between your start and end position'? \$\endgroup\$
    – XylemFlow
    Commented Feb 4, 2021 at 14:34
  • \$\begingroup\$ Just the usual lerp(start, end, weight) = (1 - weight) * start + weight * end \$\endgroup\$
    – DMGregory
    Commented Feb 4, 2021 at 14:37
  • \$\begingroup\$ I should mention in my example ratio is measured in terms of the size of the camera viewport, not the magnification factor. So when zooming in like this, ratio is less than 1 (camera keeps getting smaller). If yours is measured as a magnification factor (ie > 1 when zooming in), just use its reciprocal in calculating the position interpolation weight. \$\endgroup\$
    – DMGregory
    Commented Feb 4, 2021 at 14:45
  • \$\begingroup\$ Thanks, got it. I'll update my question with the solution and the resulting animation. The next question is how would this work for cubic interpolation. I was hoping that any method I use for linear interpolation would also work for cubic, but since the lerp ratio is different for position then it may become more complex. I'll have to think about it. \$\endgroup\$
    – XylemFlow
    Commented Feb 4, 2021 at 14:48
  • 1
    \$\begingroup\$ I've realised that I'll have to be careful about divide by 0. This will happen if scales are the same at start and end. I guess I will just check for a scale ratio close to 0 and revert to normal linear interpolation of position in that case. I will update my answer. \$\endgroup\$
    – XylemFlow
    Commented Feb 4, 2021 at 15:17
1
\$\begingroup\$

Here is my solution based on the answer given by DMGregory. This is Delphi Pascal. The LinearInterpolate function is just a lerp. Care has to be taken to avoid divide by zero.

      CamT.Scale := Exp(LinearInterpolate(Ln(Cam1.Scale), Ln(Cam2.Scale), k));
      if abs(Cam2.Scale - Cam1.Scale) > 0.001 then begin
        r := Cam2.Scale / Cam1.Scale
        w := (Power(r, k) - 1) / (r - 1);
        CamT.Pos.X := LinearInterpolate(Cam1.Pos.X, Cam2.Pos.X, w);
        CamT.Pos.Y := LinearInterpolate(Cam1.Pos.Y, Cam2.Pos.Y, w);
      end else begin
        CamT.Pos.X := LinearInterpolate(Cam1.Pos.X, Cam2.Pos.X, k);
        CamT.Pos.Y := LinearInterpolate(Cam1.Pos.Y, Cam2.Pos.Y, k);
      end;

Correct zoom

\$\endgroup\$
0
\$\begingroup\$

Dunno if this is what you wanted, but this was sufficient for me:

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;
texture *Texture = GetTexture(TextureLoader, "chapter1/cutscene1/_backgrounds/FreedomTower");

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);
AddTexture(Renderer, Texture->Handle, TextureRect, Texture->TexCoords, 4);
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .