4
\$\begingroup\$

I've been implementing a game loop with a fixed timestep which uses interpolation as described in the (legendary as I have come to find out as it is referenced in almost every post I have read trying to diagnose this issue) article "Fix Your Timestep" (webarchive link given as the site appears to be down now).

From what I can tell I have implemented everything discussed as closely as I could, however after adding the interpolation phase, I instantly noticed choppiness and a blurry smear effect happening when moving my camera.

This is about as basic of a 3d scene as you can get. There is a single object loaded at a static position, and I have a first person shooter style camera that strafes with wasd and rotates (pitch/yaw) via mouse coordinates.

When my accumulator variable is greater than or equal to my fixed delta_time variable, an update is performed. The only action that is performed during this update is to move and rotate a set of vectors which are used to position my camera in a later step, and update my previous and current state objects with this updated information. After this my accumulator variable is then decremented by the value of my fixed delta_time.

Once back in my initial loop, I now perform a render by calculating an interpolated game state and positioning my camera using the vectors created during this interpolation. I then create my view/projection matrices from my camera and render the scene.

Since it is very difficult to explain what I'm seeing, I have uploaded a video to youtube that depicts my results: https://www.youtube.com/watch?v=9zhGxgmpJpA (the smear effect seems to have been lost during the upload, but the choppiness is still visible)

Below is all of the code I believe to be relevant to the question, however if I have missed a chunk somewhere just let me know and I'll throw it up as well:

// From main.cpp
int main()
{

    glm::vec2           screen_size = glm::vec2(1280, 720);
    InputState          input;

    Window              window(&screen_size, &input);
    if (window.getInitFailed()) {
        std::cout << window.getInitMessage() << std::endl;
        return -1;
    }

    auto                scene = std::make_unique<Scene>(&screen_size);
    auto                game = std::make_unique<GameLogic>(&input);

    double              delta_time = 0.03;
    double              current_time = window.time();
    double              accumulator = 0.0;

    glEnable(GL_DEPTH_TEST);    


    scene->addModel("data/models/bin/actors/knife1/spyderco_tenacious_knife.obj");


    // Render loop
    while (!window.shouldClose())
    {

        if (input.key_esc) {
            window.setToClose();
            continue;
        }

        double new_time = window.time();
        double frame_time = new_time - current_time;
        if (frame_time > 0.25) {
            frame_time = 0.25;
        }
        current_time = new_time;

        accumulator += frame_time;

        // capture input state
        window.update();

        while (accumulator >= delta_time) {

            game->update(delta_time);

            accumulator -= delta_time;

        }

        scene->render(game->getCurrentGameState(), 
        game->getPreviousGameState(), (accumulator / delta_time));

        window.vsync();
        window.swapBuffers();

    }

    return 0;
}

// From GameState.h
struct GameState {
    glm::vec3           player_position;
    glm::vec3           player_front;
    glm::vec3           view_position;
    glm::vec3           view_front;
};


// From GameLogic.cpp
GameLogic::GameLogic(InputState* is) : player(is) {}
GameLogic::~GameLogic() {}

void GameLogic::update(double delta) {

    player.update(delta);

    updateState();

}

void GameLogic::updateState() {

    previous_state = current_state;

    current_state.player_position = player.getPlayerPosition();
    current_state.player_front = player.getPlayerFront();

    current_state.view_position = player.getViewPosition();
    current_state.view_front = player.getViewFront();   

}

GameState GameLogic::getCurrentGameState() {
    return current_state;
}

GameState GameLogic::getPreviousGameState() {
    return previous_state;
}


// From Scene.cpp
Scene::Scene(glm::vec2* screen_size) : camera(screen_size), 
shader(GlobalConstants::VERTEX_SHADER, GlobalConstants::FRAGMENT_SHADER) {}
Scene::~Scene(){}

void Scene::render(GameState current_state, GameState previous_state, float alpha) {

    GameState lerp_render_state = lerpRenderState(current_state, previous_state, alpha);

    camera.setPosition(lerp_render_state.view_position);
    camera.setDirection(lerp_render_state.view_front);
    camera.update();


    // Select a color to clear the screen with and clear screen
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    shader.use();

    shader.setMat4("view", camera.getViewMatrix());
    shader.setMat4("projection", camera.getProjectionMatrix());

    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(0.f, 2.f, -3.f));
    shader.setMat4("model", model);

    for (auto &m : models) {
        m.draw(shader);
    }


}

void Scene::addModel(std::string path) {
    models.push_back(Model(path));
}

GameState Scene::lerpRenderState(GameState current_state, GameState previous_state, float alpha) {

    GameState lerp_render_state;

    lerp_render_state.player_position = glm::lerp(current_state.player_position, previous_state.player_position, alpha);
    if (current_state.player_front != previous_state.player_front) {
        lerp_render_state.player_front = glm::slerp(current_state.player_front, previous_state.player_front, alpha);
    }
    else {
        lerp_render_state.player_front = current_state.player_front;
    }

    lerp_render_state.view_position = glm::lerp(current_state.view_position, previous_state.view_position, alpha);
    if (current_state.view_front != previous_state.view_front) {
        lerp_render_state.view_front = glm::slerp(current_state.view_front, previous_state.view_front, alpha);
    }
    else {
        lerp_render_state.view_front = current_state.view_front;
    }   

    return lerp_render_state;

}

// From Camera.cpp
void Camera::update() {

    updateViewMatrix();
    updateProjectionMatrix();

}
void Camera::updateViewMatrix() {

    view_matrix = glm::lookAt(camera_position, camera_position + camera_front, camera_up);

}
\$\endgroup\$
  • \$\begingroup\$ Have you tried to disable vsync? Have you tried to output your actual frame rate? \$\endgroup\$ – Vaillancourt May 30 at 1:12
  • 1
    \$\begingroup\$ @AlexandreVaillancourt vsync does not seem to make a difference. With it enabled, framerate is steady 144fps (144hz monitor), with it disabled ~1640fps. \$\endgroup\$ – nullReference May 30 at 1:24
4
\$\begingroup\$

Your lerps and slerps are backwards:

lerp(current, previous, alpha)

should be

lerp(previous, current, alpha)

ie. increasing alpha (more time accumulated since the last update) should move us toward the future, not toward the past.

This could account for at least some of the choppiness you perceive. There could be additional judder coming from the player update methods, depending on how you've incorporated your delta time there.

It doesn't account for the smear you describe though. That could be due to not completely clearing your previous frame, or it could be an artifact of your screen's response rate (this is sometimes be more apparent on moving, dark images like your knife blade, and would explain why it wasn't captured in your video)

\$\endgroup\$
  • 1
    \$\begingroup\$ That was indeed the issue. I'm now rendering as smooth as I was prior to separating my logic from my render loop, which is very exciting! Many thanks! \$\endgroup\$ – nullReference May 30 at 13:09
  • \$\begingroup\$ Once I updated my timestep duration the jitter was re-introduced for rotation, but not for movement (wasd movement smooth, but mouse movement causing jitter). An timestep of 1/128 caused the rotation jitter, but timesteps of 1/144 (same as render w/vsync) and 1/30 were smooth. I found it odd that 1/30 was smooth while 1/128 was not. Could this possibly be due to how I am calculating the view matrix using the interpolated position and direction vec3 values (instead of quaternions)? This might not make any sense, but it's my best guess at the moment :) I have added some camera code. \$\endgroup\$ – nullReference May 31 at 0:16
  • \$\begingroup\$ Something else I noticed, If I turn vsync off, the timestep of 1/128 is also smooth. So the jitter is only happening whenever the update loop occurs at a specific interval relative to the render loop. Feel like I might be getting close. \$\endgroup\$ – nullReference May 31 at 14:36
  • \$\begingroup\$ After some more debugging, it would appear that when the number of render cycles is consistently more than the number of update cycles, the rotation is smooth. \$\endgroup\$ – nullReference May 31 at 15:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.