Advertisement

Fixed timestep with interpolation - 2D movement jitter/stutter

Started by August 13, 2023 07:52 PM
29 comments, last by skinmarquee 1 year, 3 months ago

I've been trying to implement frame-rate independant motion for a simple 2D example (rectangle moving across the screen) written in C++17/SDL 2.0.10 on Windows 10.

My monitor refresh rate is 60Hz and I have my physics update running at 60 updates per second. The problem is that I'm using linear interpolation but my rectangle still jitters/stutters only when there is no fixed update during a frame render and I cannot understand why.

I have:

  • Checked for precision issues with floating-point numbers calculating alpha (seems ok)
  • Checked possible overflows with nanosecond precision (seems ok)
  • Increasing/decreasing physics update rate to be greater/less than frame rate (jittering still present)
  • Enabling/disabling VSync (no difference)
  • Logged the output of the interpolated drawing position (no abnormalities in position)

Every article I have read says that interpolation is the solution to smooth movement so why does it still jitter? What's wrong in my implementation?

Code example:

#include <cassert>
#include <SDL.h>
#include <chrono>

constexpr int WINDOW_WIDTH = 640 * 2;
constexpr int WINDOW_HEIGHT = 480 * 2;
constexpr int RECT_WIDTH = 128;
constexpr int RECT_HEIGHT = 128;

struct Window 
{
    unsigned long long lastSecond{};
    unsigned long long frames{},lastFrameTime{}, frameTime{};
    unsigned long long fixedUpdates{};
    unsigned long long accumulator{};
    unsigned long long now{};
};

Window window;
SDL_Window* mainWindow = nullptr;
SDL_Renderer* renderer = nullptr;

float cx = 0;
float cy = 64;
float px = cx;
float py = cy;

float rx = 0;
float ry = 0;

float xspeed = 1.0f;
float yspeed = 0;

auto NOW()
{
    using namespace std::chrono;
    return duration_cast<nanoseconds>(high_resolution_clock::now().time_since_epoch()).count();
}

void update()
{
    // for brevity
}

void drawRectangle(const float x, const float y, uint8_t red = 0, uint8_t green = 0, uint8_t blue = 0)
{
    // Draw rect
    auto result = SDL_SetRenderDrawColor(renderer, red, green, blue, 0xff);
    assert(result == 0);

    SDL_FRect fillRect = { x, y, RECT_WIDTH, RECT_HEIGHT };

    result = SDL_RenderFillRectF(renderer, &fillRect);
    assert(result == 0);
}

void render()
{
    window.frames++;

    // Clear black
    auto result = SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
    assert(result == 0);

    result = SDL_RenderClear(renderer);
    assert(result == 0);

    drawRectangle(rx, ry, 0xff);

    // Present to screen
    SDL_RenderPresent(renderer);
}

void fixedUpdate()
{
    window.fixedUpdates++;

    px = cx;
    py = cy;


    cx += xspeed;
    cy += yspeed;

}


int main(int argc, char* argv[])
{
    #define NS_PER_SECOND (1000000000)
    #define TARGET_FPS (60)
    #define NS_PER_MS (1000000)

    window.lastFrameTime = NOW();
    window.lastSecond = NOW();

    auto init = SDL_Init(SDL_INIT_VIDEO);
    assert(init == 0);

    mainWindow = SDL_CreateWindow("Timestep", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL);
    assert(mainWindow);

    renderer = SDL_CreateRenderer(mainWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    assert(renderer);

    SDL_version version;
    SDL_GetVersion(&version);

    printf("SDL Version: %d.%d.%d\n", version.major, version.patch, version.minor);

    while (true) 
    {
        int thisFrameFixedUpdates = 0;

        window.now = NOW();
  
        window.frameTime = window.now - window.lastFrameTime;
        if (window.frameTime > 25 * NS_PER_MS)
        {
            window.frameTime = 25 * NS_PER_MS;
        }

        window.lastFrameTime = window.now;

        constexpr unsigned long long dt = (NS_PER_SECOND / TARGET_FPS);
        window.accumulator += window.frameTime;

        while (window.accumulator >= dt)
        {
            fixedUpdate();
            window.accumulator -= dt;
            ++thisFrameFixedUpdates;
        }

        float alpha = window.accumulator / static_cast<float>(dt);
        assert(alpha >= 0.0f && alpha <= 1.0f);

        rx = cx + ((cx - px) * alpha);
        ry = cy + ((cy - py) * alpha);

        if (thisFrameFixedUpdates > 0)
        {
            printf("--- (rx,ry) (%.2f,%.2f)\n",rx, ry);
        }
        else
        {
            printf("*** (rx,ry) (%.2f,%.2f)\n",rx, ry);
        }

        update();
        render();

    }


    return 0;
}

Help much appreciated

Thanks

Crowbone said:

rx = cx + ((cx - px) * alpha);
ry = cy + ((cy - py) * alpha);

I think your interpolation is wrong, it should be:

rx = (cx - px) * alpha + px;
ry = (cy - py) * alpha + py;
Advertisement

Aressera said:

Crowbone said:

rx = cx + ((cx - px) * alpha);
ry = cy + ((cy - py) * alpha);

I think your interpolation is wrong, it should be:

rx = (cx - px) * alpha + px;
ry = (cy - py) * alpha + py;

It still jitters/stutters with this change you suggested :(

(during the no fixed updates period)

If its still not smooth, I think it might be a problem with how you visualize the rectangle. Your rectangle coordinates may be rounded to nearest integer if they are not drawn with antialiasing, which would produce a not smooth motion when interpolating. The rectangle moves 1 pixel for every fixed time step, so if you interpolate the motion the position will not be an integer. Assuming there is no antialiasing, the rectangle will be drawn in one pixel or the other, rather than partially between the pixels. The result is that jitter is apparent.

To test this, try increasing the movement speed to 5-10px or more, so that the interpolation will function better.

Aressera said:

If its still not smooth, I think it might be a problem with how you visualize the rectangle. Your rectangle coordinates may be rounded to nearest integer if they are not drawn with antialiasing, which would produce a not smooth motion when interpolating. The rectangle moves 1 pixel for every fixed time step, so if you interpolate the motion the position will not be an integer. Assuming there is no antialiasing, the rectangle will be drawn in one pixel or the other, rather than partially between the pixels. The result is that jitter is apparent.

To test this, try increasing the movement speed to 5-10px or more, so that the interpolation will function better.

If that were the case, I think the jitter would be apparent all the time? When running the example, the movement of the rectangle is very smooth except when there is no fixed-update run during a frame (i.e thisFrameFixedUpdates = 0 ).

The position (x,y) is always a fraction e.g

--- (rx,ry) (111.31,64.00)
--- (rx,ry) (112.32,64.00)
--- (rx,ry) (113.34,64.00)
--- (rx,ry) (114.36,64.00)
--- (rx,ry) (115.32,64.00)
--- (rx,ry) (116.31,64.00)
--- (rx,ry) (117.31,64.00)
--- (rx,ry) (118.31,64.00)

I tried running it with xspeed=5 and it still jitters during the aforementioned period.

Crowbone said:
If that were the case, I think the jitter would be apparent all the time? When running the example, the movement of the rectangle is very smooth except when there is no fixed-update run during a frame (i.e thisFrameFixedUpdates = 0 ).

No, I don't think it has anything to do with pixel-perfect rendering eigther; I have the exact same setup with pixel-perfect sprites & the interpolation you mention, and even if entities move 1 px or less per frame, it doesn't jitter.

Is that code you posted the full picture? I usually only get jitter when multiple objects move with slight differences; or if the camera doesn't fully agree with the movement of a centered object. Oter than that, I do use an even other method of interpolation:

template<typename Type>
[[nodiscard]] inline constexpr Type interpolate(Type a, Type b, double alpha) noexcept
{
	return Type(a * alpha + b * (1.0 - alpha));
}

Though I'm thinking that should not make a difference. One thing that got me confused though:

the movement of the rectangle is very smooth except when there is no fixed-update run during a frame (i.e thisFrameFixedUpdates = 0 ).

Unless you have VSync on (which you said you checked), fixed-update should not be run in most of the frames. With an (technically) uncapped framerate and a fixed-update, the fixed update will run exactly X times each frame, while the whole loop should run way, way more often. In the simple code you posted, the whole loop might complete multiple thousand times per frame, since there is nothing that really takes any large time to complete. So then “thisFrameFixedUpdates == 0” should be the norm, meaning that attributing the jitter to that case would not make sense. So while your code does look fine to me, maybe there is some issue related to this?

Advertisement

Debugging FTW! On several occasions I have encountered stutter problems. Instrument your code and log time deltas of various suspected trouble spots into some memory structure. Then after a run write your log to a file and pinpoint the problem. This has always worked for me. Next time you have similar problems, you can reuse your logger. Programmers love the quick fix, but at some point if that fails you will have to get your hands dirty.

Juliean said:

Crowbone said:
If that were the case, I think the jitter would be apparent all the time? When running the example, the movement of the rectangle is very smooth except when there is no fixed-update run during a frame (i.e thisFrameFixedUpdates = 0 ).

No, I don't think it has anything to do with pixel-perfect rendering eigther; I have the exact same setup with pixel-perfect sprites & the interpolation you mention, and even if entities move 1 px or less per frame, it doesn't jitter.

Is that code you posted the full picture? I usually only get jitter when multiple objects move with slight differences; or if the camera doesn't fully agree with the movement of a centered object. Oter than that, I do use an even other method of interpolation:

template<typename Type>
[[nodiscard]] inline constexpr Type interpolate(Type a, Type b, double alpha) noexcept
{
	return Type(a * alpha + b * (1.0 - alpha));
}

Though I'm thinking that should not make a difference. One thing that got me confused though:

the movement of the rectangle is very smooth except when there is no fixed-update run during a frame (i.e thisFrameFixedUpdates = 0 ).

Unless you have VSync on (which you said you checked), fixed-update should not be run in most of the frames. With an (technically) uncapped framerate and a fixed-update, the fixed update will run exactly X times each frame, while the whole loop should run way, way more often. In the simple code you posted, the whole loop might complete multiple thousand times per frame, since there is nothing that really takes any large time to complete. So then “thisFrameFixedUpdates == 0” should be the norm, meaning that attributing the jitter to that case would not make sense. So while your code does look fine to me, maybe there is some issue related to this?

Yes the code example I posted is the full code.

Yeah I'm running with VSync on (locks frame rate to 60fps for my monitor) but it doesn't make a difference if it's on or not as jitter is still present with physics updates at 60 or 120.

My thinking was that when thisFrameFixedUpdates = 0 then we draw using the same current position and previous position (from the previous frame) but with a different alpha value which seems to be related to the issue? I have logged the drawing positions but nothing seems out of the ordinary in regards to the interpolated position.

I'd like to upload a video of the jittering but I don't know how to insert a media file here.

Crowbone said:
I have logged the drawing positions but nothing seems out of the ordinary in regards to the interpolated position.

To debug this properly, you would want to log some more stuff on each render frame:

  • cx, cy, px, py
  • alpha
  • interpolated position

From this information it should be easy to see what is going wrong. You should be able to double check things by hand to verify that the interpolated position makes sense given cx, cy, px, py, alpha.

Aressera said:

Crowbone said:
I have logged the drawing positions but nothing seems out of the ordinary in regards to the interpolated position.

To debug this properly, you would want to log some more stuff on each render frame:

  • cx, cy, px, py
  • alpha
  • interpolated position

From this information it should be easy to see what is going wrong. You should be able to double check things by hand to verify that the interpolated position makes sense given cx, cy, px, py, alpha.

I think the values look correct; here's a sample of requested points/values during a run where it was jittering:

*** (rx,ry) (158.00,64.00) (cx,cy) (158.00,64.00) (px,py) (157.00,64.00) alpha 1.00
--- (rx,ry) (159.17,64.00) (cx,cy) (160.00,64.00) (px,py) (159.00,64.00) alpha 0.17
--- (rx,ry) (160.01,64.00) (cx,cy) (161.00,64.00) (px,py) (160.00,64.00) alpha 0.01
--- (rx,ry) (161.17,64.00) (cx,cy) (162.00,64.00) (px,py) (161.00,64.00) alpha 0.17
*** (rx,ry) (161.99,64.00) (cx,cy) (162.00,64.00) (px,py) (161.00,64.00) alpha 0.99
--- (rx,ry) (163.19,64.00) (cx,cy) (164.00,64.00) (px,py) (163.00,64.00) alpha 0.19
--- (rx,ry) (164.02,64.00) (cx,cy) (165.00,64.00) (px,py) (164.00,64.00) alpha 0.02
--- (rx,ry) (165.18,64.00) (cx,cy) (166.00,64.00) (px,py) (165.00,64.00) alpha 0.18
--- (rx,ry) (166.02,64.00) (cx,cy) (167.00,64.00) (px,py) (166.00,64.00) alpha 0.02
--- (rx,ry) (167.16,64.00) (cx,cy) (168.00,64.00) (px,py) (167.00,64.00) alpha 0.16
*** (rx,ry) (168.00,64.00) (cx,cy) (168.00,64.00) (px,py) (167.00,64.00) alpha 1.00
--- (rx,ry) (169.17,64.00) (cx,cy) (170.00,64.00) (px,py) (169.00,64.00) alpha 0.17
*** (rx,ry) (170.00,64.00) (cx,cy) (170.00,64.00) (px,py) (169.00,64.00) alpha 1.00

This topic is closed to new replies.

Advertisement