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);
}