From dadaf61c45d675f0e8b88fbc231748ad8247a736 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Mon, 20 Apr 2026 18:03:09 -0700 Subject: Percent frame interpolation factor for smooth animation --- simloop/src/simloop.c | 49 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) (limited to 'simloop/src/simloop.c') diff --git a/simloop/src/simloop.c b/simloop/src/simloop.c index bd5a72d..b8547fd 100644 --- a/simloop/src/simloop.c +++ b/simloop/src/simloop.c @@ -2,6 +2,8 @@ #include +static double min(double a, double b) { return a <= b ? a : b; } + static simloop_time_t ddt_from_fps(int fps) { static constexpr double NANOSECONDS = 1e9; return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps); @@ -23,8 +25,8 @@ Simloop simloop_make(const SimloopArgs* args) { .ddt = ddt_from_fps(args->max_render_fps), .time = 0, }, - .first_iter = true, - .updates_since_last_render = false, + .percent_frame = 0., + .first_iter = true, }; } @@ -33,6 +35,12 @@ static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { assert(timeline); assert(timeline->ddt > 0); + // If the update falls behind the clock, we advance by a single ddt increment + // per loop iteration here and give it a chance to catch up over subsequent + // iterations. + // This has the implication that percent_frame can fall out of range (>1) if + // we are not careful with how it is defined. See the general update function + // below. const simloop_time_t dt = sim->clock - timeline->time; const bool should_step = dt >= timeline->ddt; timeline->time += should_step ? timeline->ddt : 0; @@ -47,8 +55,8 @@ static bool step_render(const Simloop* sim, SimloopTimeline* timeline) { if (timeline->ddt > 0) { render = step_update(sim, timeline); } else { + render = timeline->time < sim->clock; timeline->time = sim->clock; - render = true; } return render; } @@ -60,24 +68,39 @@ void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) { sim->clock += dt; // Simulation update. - const bool updated = step_update(sim, &sim->update); - sim->updates_since_last_render = sim->updates_since_last_render || updated; + const bool update_this_tick = step_update(sim, &sim->update); // Simulation render. - const bool rendered = - (sim->updates_since_last_render && step_render(sim, &sim->render)) || - (sim->first_iter); // Trigger an initial render on the first frame. - sim->updates_since_last_render = - sim->updates_since_last_render && !out->should_render; + const bool render_this_tick = + step_render(sim, &sim->render) || + sim->first_iter; // Trigger an initial render on the first frame. + + // Interpolator for smooth animation. + // If rendering is not frame-rate capped, then its timeline should always be + // at least as recent as the update's. Otherwise, it is possible for the + // rendering timeline to be behind. + // If the update falls behind the clock, then percent_frame can fall out of + // range (>1) if we are not careful. We impose that it is strictly never >1 + // to account for this case. + assert(sim->update.ddt > 0); + assert( + (sim->render.ddt == 0) ? (sim->update.time <= sim->render.time) : true); + sim->percent_frame = + (sim->render.time >= sim->update.time) + ? min(1., ((double)(sim->render.time - sim->update.time) / + (double)sim->update.ddt)) + : sim->percent_frame; + assert((0. <= sim->percent_frame) && (sim->percent_frame <= 1.)); // Loop state update. - sim->frame += (updated ? 1 : 0); + sim->frame += (update_this_tick ? 1 : 0); sim->first_iter = false; out->frame = sim->frame; out->render_elapsed = sim->render.time; out->update_elapsed = sim->update.time; out->update_dt = sim->update.ddt; - out->should_update = updated; - out->should_render = rendered; + out->percent_frame = sim->percent_frame; + out->should_update = update_this_tick; + out->should_render = render_this_tick; } -- cgit v1.2.3