Smooth Movement With Interpolation And All That Stuff

Started by
7 comments, last by _paf 7 years, 9 months ago

I've been using the fixed-timestep loop (as described in "Fix your timestep") for a while now but hadn't actually noticed that the movements in my game are not smooth. It's a bit harder to notice when things are accelerating, deaccelerating, stopping etc. but it does become very apparent if there's something that moves at constant speed across the screen.

I've now been trying to fix this problem for days but I have no clue where to look next or how to find a solution. I'm actually convinced that what I'm doing must be conceptually wrong instead of it being a bug that I can spot myself in my code. So I'm turning to the forums for help. I'll first try to describe what I'm doing and then post some code.

So here's what I'm doing:

I measure time between game loops right before I would call update and add this value to an accumulator. Then while the accumulator is greater equals the timestep, I update while decreasing the accumulator. I then divide the now remaining value by the timestep to obtain the alpha. The alpha is then passed to the rendering where it's used for linear interpolation. After I have obtained the interpolated position, the coordinates of an object are used to create a quad which stores them as GLdouble. All the quads are put into a buffer with glBufferData and then at the end of the rendering step I call glfwSwapBuffers.

Here's the code, I changed a lot of things to obtain a minimal example but this is what I am running and it's not working:

Loop:


int64_t timestep = 1000000 / 60;
int64_t accumulator = 0;

using clock = std::chrono::high_resolution_clock;
using unit = std::chrono::microseconds;

clock::time_point now = clock::now();

while (running)
{
 clock::time_point before = now;
 now = clock::now();
 
 unit duration = std::chrono::duration_cast<unit>(now - before);
 int64_t elapsed = duration.count();

 accumulator += elapsed;

 for(; accumulator >= timestep; accumulator -= timestep)
 {
  update();
 }

 double alpha = static_cast<double>(accumulator) / timestep;
 draw(alpha);
}

In this example I'm trying to move a rectangle across the screen.

Update:


x_before = x_now;
x_now = x_now < 0.0 ?
        800.0 :
	x_now - 1.0;

Draw:


double x_inter = (1.0 - alpha) * x_before + alpha * x_now;

// draw_rectangle(double x, double y, width, height...)
graphics.draw_rectangle(x_inter, 0.0, 100, 100, Color::White);

// Buffers data
graphics.buffer_data();

// calls glfwSwapBuffers
window.render();

Methods of Graphics component:


void Graphics::draw_rectangle(double x, double y, int16_t w, int16_t h, const Color& colour)
{
 // this creates a quad and puts it into a vector of quads. 
 // the other parameters just mean that it will be an empty rectangle filled with a colour
 quads.emplace_back(x, x + w, y, y + h, Atlas::NULLOFFSET, colour, 0.0);
}

// Here's Quad and it's constructor
class Graphics::Quad
{
 public:
  static const size_t NUM_VERTICES = 6;
  struct Vertex
  {
   GLdouble x;
   GLdouble y;
   GLdouble s;
   GLdouble t;

   Color c;
  };

  Quad(GLdouble l, GLdouble r, GLdouble t, GLdouble b,
   const Offset& o, const Color& colour, GLdouble rot);

 private:
  Vertex vertices[NUM_VERTICES];
};

// Constructor of quad
{
 vertices[0] = { l, b, o.l, o.b, colour };
 vertices[1] = { l, t, o.l, o.t, colour };
 vertices[2] = { r, b, o.r, o.b, colour };
 vertices[3] = { r, b, o.r, o.b, colour };
 vertices[4] = { r, t, o.r, o.t, colour };
 vertices[5] = { l, t, o.l, o.t, colour };
}


// Putting quads into the buffer
void Graphics::buffer_data()
{
 glClearColor(1.0, 1.0, 1.0, 1.0);
 glClear(GL_COLOR_BUFFER_BIT);

 GLsizei csize = static_cast<GLsizei>(quads.size() * sizeof(Quad));
 GLsizei fsize = static_cast<GLsizei>(quads.size() * Quad::NUM_VERTICES);
 glEnableVertexAttribArray(attribute_coord);
 glEnableVertexAttribArray(attribute_color);
 glBindBuffer(GL_ARRAY_BUFFER, vbo);
 glBufferData(GL_ARRAY_BUFFER, csize, quads.data(), GL_STREAM_DRAW);
 glDrawArrays(GL_TRIANGLES, 0, fsize);

 glDisableVertexAttribArray(attribute_coord);
 glDisableVertexAttribArray(attribute_color);
 glBindBuffer(GL_ARRAY_BUFFER, 0);
}

// Here's how I create the atlas, etc.
{
 glBindTexture(GL_TEXTURE_2D, texture);
 glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Atlas::WIDTH, Atlas::HEIGHT, 0,
  GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

 glVertexAttribPointer(attribute_coord, 4, GL_DOUBLE, GL_FALSE, sizeof(Quad::Vertex), 0);
 glVertexAttribPointer(attribute_color, 4, GL_DOUBLE, GL_FALSE, sizeof(Quad::Vertex), (const void*)32);
}

I feel like I've tried pretty much everything I could to fix it. If I do not use the fixed-timestep loop at all, but simply update once before doing a draw, the movement look's fine. I can only imagine that this is because I'm my rectangle now always moves exactly one pixel between each draw. But this of course doesn't work for me because then my simulation is no longer synchronized to the real time, which I want to be the case.

To describe the jittery movement some more: It seems to randomly skip ahead, as in, it sometimes moves one pixel and sometimes two. I have also used the console to print this to the screen. But as far as I understand the idea of the fixed-timestep loop, this shouldn't be a problem right? The whole idea is that we sometimes update once, sometimes twice or not at all and it still works.

All help and advice is appreciated, I've been working on this for a while now but I definitely see myself as a beginner still and I just can't figure this one out myself.

Advertisement

Did you checked that the jitter dosent come from precision artifacts in your calculation loop when only last part digits change in your double value?

This sounds as if the threshold would be great enougth some time that it would jump over 2 pixels instead of one. But I think you did some misconception in your simulation itself.

Why not calculating the time the object needs to reach a certain point in real time and use delta timing to the rendering loop to get a smoth working but also the realtime precision you wanted?

Did you checked that the jitter dosent come from precision artifacts in your calculation loop when only last part digits change in your double value?

This sounds as if the threshold would be great enougth some time that it would jump over 2 pixels instead of one. But I think you did some misconception in your simulation itself.

I'm not sure how to check this. But shouldn't this also make the jitter happen when just always updating once before rendering like I tested (this makes it smooth).

Why not calculating the time the object needs to reach a certain point in real time and use delta timing to the rendering loop to get a smoth working but also the realtime precision you wanted?

Do you mean basing the position on time only? I tried the following (this still jitters):


// in draw()
double x_inter = 800.0 - static_cast<double>(
 (std::chrono::high_resolution_clock::now().time_since_epoch().count() / 16000000) % 800
);

Interesting is that when using this it also jitters when always updating once, whereas in the previous case it would atleast not jitter then. I also tried the above without using doubles, and using GLshort instead for my coordinates.

One other thing I forgot to mention is that it doesn't matter wether I have vsync on or off.

Hi there.

There's an excelent article written by L. Spiro about fixed timestep implementation here.

It explains really well how you should interpolate objects' positions (among other things), so, rather than trying to re-explain it here, I prefer to just link you to the article.

Hi there.

There's an excelent article written by L. Spiro about fixed timestep implementation here.

It explains really well how you should interpolate objects' positions (among other things), so, rather than trying to re-explain it here, I prefer to just link you to the article.

I'm sorry but I just don't see it, I've read the article multiple times and I also read the other article it is linking too. I must say your post is a bit cryptic too, do you already see what is wrong with my implementation and are saying it is one of the things explained in the article?

There's an excelent article written by L. Spiro about fixed timestep implementation here.

That is one of several good ones. The bigger game engines with more comprehensive feature sets will interpolate what you see between the two simulation steps as describe.

Beyond that, I would only add that it is better practice to compute things directly rather than accumulate in small steps.

Whenever possible it is best to compute the position directly. If it is following a path, use the start time and computed stop time compute the percent complete, then directly compute the position along the path. If you are following a direction from a source instead of accumulating a tiny step, re-compute based on the velocity multiplied by time plus starting position so you don't accumulate tiny errors every frame.

When you are accumulating small amounts, the numerical error accumulates rapidly. Over a tiny time increment your motion numbers are small, but they are being mixed with bigger numbers.

I should probably turn it into an article since this isn't the first time I've brought it up, but basically it works like this:

Float has six decimal digits of precision.

Change of position for two identical objects is calculated as : 2.34432e-1

Object position in world for one object is: 1.42393e0 (about 1.4)

Object position in world for another object is: 1.42393e3 (about 1400)

The floating point processor brings them to the same scale for calculation: 1.42393e0 + 0.23443e0 for the first, and 1.42393e3 + 0.00023e3 for the second. (Some processors will internally operate at different precisions, but it is not required and varies based on platform like Windows versus Android or PS3 for example.)

Repeat for 60 updates per second, within a few seconds your accumulated error means the position quickly becomes different for the two objects. If those were pixel positions, the first one is moving faster by about a quarter pixel per second. Every four seconds it is about a full pixel farther along. If the player stays on the screen for a minute, they are about 15 pixels different. If they stay on the screen for five minutes, they become about 75 pixels off even though they should be moving at the same speed. As the objects move farther from zero but continue at the same rate of motion, their accumulated motion will gradually slow over time until the value eventually is too small for floating point and results in no motion.

Calculate positions directly rather than accumulating error. It solves problems in the long run.

Sorry for the confusion.

I must admit I only skimmed through your post, and given the topic, I though it would be enough to link you to the article.

You already seem to have a proper fixedtime step loop, and to interpolate between your object's 2 last positions.

I inspected your code a bit better, and noticed you are moving your square (object) by one pixel every update.

In this case, the interpolation won't do any good, because if your screen can only display in 1 pixel increments, what is happening is that, in some frames, your object won't move at all (because until the delta reaches 0.5f, your object's position won't change by 1).

What I mean is, if the object's x coordinate is 400, and you're 40% on the way to the next update, it's interpolated position will be 400.4. But since, basically, you will only notice a difference, when the object moves by, at least 0.5 pixels (because it is rounded to 1), it will appear not to move.

Also, like Shaaringan said, the 2 pixels jump will also happen, probably, due to rounding/precision issues.

Basically, this is what I think might be responsible for the jittering.

Also notice, that the object moves properly when you don't use a fixedtime step, because you're always moving by 1 pixel and not interpolating between two positions separated by a single pixel.

The object won't always move at the same speed though, because should your loop run slower than normal, then your object will also slow down (one of the reasons you really should use one).

Again, sorry for the confusion.

That is one of several good ones. The bigger game engines with more comprehensive feature sets will interpolate what you see between the two simulation steps as describe.

Indeed. I find that as I get more experience coding, actual program/engine design becomes more and more of an issue, so it is really cool to see how the (much) more experience programmers do it.

Thanks for all the replies again. I will look into replacing my using doubles with direct calculations or storing integers. As for what _SKYe said above, I tried to play around with using different speeds and in fact there are some which look somewhat better.


As for what _SKYe said above, I tried to play around with using different speeds and in fact there are some which look somewhat better.

Yeah, keep in mind that the whole point of using interpolation, is to smooth out movement that would otherwise be choppy (depending on how much/fast the objects move).

So, the faster the objects are moving, the more you will notice the LACK of interpolation, should you not use it.

The great thing about it is that, if a user has a high end machine and another has a not-so-high-end one (especially graphically), then, one will be able to enjoy a richer experience (graphics wise), but both will have, essentially, the same gameplay experience (the game just looks smoother for the first case).

I will look into replacing my using doubles with direct calculations or storing integers.

Like frob said, floating point variables, in this case especially, should only really be used to pass time deltas, and never to be used as an accumulator.

So if you have to accumulate time (or perform any calculations), use integers (or 64-bit integers, if you're dealing with large numbers), and only cast it to float when you need to pass deltas.

Anyway, I'm glad you sorted it out.

This topic is closed to new replies.

Advertisement