I am currently in the process of implementing a fixed timestep game loop following the legendary "Fix Your Timestep" article. I have managed to get everything setup and working...or so I thought, until I started to change my timestep value. I seem to be running into an issue where a stutter is produced during rotation at specific timesteps (linear movement (forwards, backwards, left, right) is always smooth when rotation is not changed). Below are the different timesteps and encountered results when running with vsync enabled on a 144hz panel:
- 144 updates per second - smooth rotation, minor stutter every once in awhile but barely noticeable (possibly in my head)
- 128 updates per second - rotation stutter noticeable (may not be completely obvious at first but as you rotate around the crate you will see it)
- 30 updates per second - smooth rotation, no stutter noticed
When the above timesteps are used with vsync disabled they all appear to have smooth rotation. The takeaway (at least my takeaway) is that the more render cycles that happen between update steps, the smoother the rotation.
I have been going back and forth as to why this could be (this is the first time I have attempted to implement something like this so it's possible I have missed something someone else might instantly notice) and the thing that throws me is the fact that the interpolation appears to be working as expected for lower update rates (as seen from the 30 updates per second test (if I attempt the 30 updates per second test with interpolation disabled everything stutters horribly as you would expect until interpolation is enabled again)), but then appears to not be working when the render cycles more closely match the number of update cycles.
I have included my code below which I believe to be relative to the question (complete source available here). Is there anything that points out as being horribly wrong??? I have been obsessing over trying to figure this out for days now. Any advice greatly appreciated!
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.0078125; // 128 tps
//double delta_time = 0.0069444444444444; // 144 tps
//double delta_time = 0.0166666666666667; // 60 tps
//double delta_time = 0.03333333333; // 30 tps
double current_time = window.time();
double accumulator = 0.0;
glEnable(GL_DEPTH_TEST);
scene->addModel("data/models/bin/props/crate1/crate1.obj");
// Render loop
while (!window.shouldClose())
{
// per-frame time logic
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;
// exit if key_esc pressed
if (input.key_esc) {
window.setToClose();
continue;
}
// capture input state
window.update();
// process game logic
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 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 Player.cpp
//-------------------------------
Player::Player(InputState* is) {
this->input_state = is;
};
Player::~Player() {};
void Player::update(double delta) {
this->delta = delta;
updateRotation();
onScroll(input_state->scroll_x, input_state->scroll_y);
move();
rotate();
look();
}
void Player::move() {
float speed = player_move_speed * delta;
if (input_state->key_w)
player_position += speed * player_front;
if (input_state->key_s)
player_position -= speed * player_front;
if (input_state->key_a)
player_position -= glm::normalize(glm::cross(player_front, player_up)) * speed;
if (input_state->key_d)
player_position += glm::normalize(glm::cross(player_front, player_up)) * speed;
view_position = player_position + view_offset;
}
void Player::rotate() {
glm::vec3 front;
front.x = cos(glm::radians(yaw));
front.y = 0.0f;
front.z = sin(glm::radians(yaw));
player_front = glm::normalize(front);
}
void Player::look() {
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(pitch));
front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
view_front = glm::normalize(front);
}
void Player::updateRotation() {
if (first_mouse)
{
last_x = input_state->mouse_xpos;
last_y = input_state->mouse_ypos;
first_mouse = false;
}
float xoffset = input_state->mouse_xpos - last_x;
float yoffset = last_y - input_state->mouse_ypos;
last_x = input_state->mouse_xpos;
last_y = input_state->mouse_ypos;
float sensitivity = 0.1f;
xoffset *= sensitivity;
yoffset *= sensitivity;
yaw += xoffset;
pitch += yoffset;
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
}
glm::vec3 Player::getPlayerPosition() {
return player_position;
}
glm::vec3 Player::getPlayerFront() {
return player_front;
}
glm::vec3 Player::getViewPosition() {
return view_position;
}
glm::vec3 Player::getViewFront() {
return view_front;
}
// 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();
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(previous_state.player_position, current_state.player_position, alpha);
if (current_state.player_front != previous_state.player_front) {
lerp_render_state.player_front = glm::slerp(previous_state.player_front, current_state.player_front, alpha);
}
else {
lerp_render_state.player_front = glm::lerp(previous_state.player_front, current_state.player_front, alpha);
}
lerp_render_state.view_position = glm::lerp(previous_state.view_position, current_state.view_position, alpha);
if (current_state.view_front != previous_state.view_front) {
lerp_render_state.view_front = glm::slerp(previous_state.view_front, current_state.view_front, alpha);
}
else {
lerp_render_state.view_front = glm::lerp(previous_state.view_front, current_state.view_front, alpha);
}
return lerp_render_state;
}
// From Camera.cpp
//-------------------------------
Camera::Camera(glm::vec2* screen_size) {
this->screen_size = screen_size;
}
Camera::~Camera() {}
void Camera::update() {
updateViewMatrix();
updateProjectionMatrix();
}
void Camera::updateViewMatrix() {
view_matrix = glm::lookAt(camera_position, camera_position + camera_front, camera_up);
}
void Camera::updateProjectionMatrix() {
projection_matrix = glm::perspective(glm::radians(fov),
(float)screen_size->x / (float)screen_size->y, 0.1f, 100.0f);
}
void Camera::setPosition(glm::vec3 position) {
camera_position = position;
}
void Camera::setDirection(glm::vec3 direction) {
camera_front = direction;
}
glm::mat4 Camera::getViewMatrix() {
return view_matrix;
}
glm::mat4 Camera::getProjectionMatrix() {
return projection_matrix;
}