diff options
| author | 3gg <3gg@shellblade.net> | 2026-04-20 18:03:09 -0700 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2026-04-20 18:03:09 -0700 |
| commit | dadaf61c45d675f0e8b88fbc231748ad8247a736 (patch) | |
| tree | e37af6f9accaa6b68ab76344ee4caa0ef6056a5d /simloop/src/simloop.c | |
| parent | 5125d6788f7765a14fbcdeb6d4f6f67742c98596 (diff) | |
Percent frame interpolation factor for smooth animation
Diffstat (limited to 'simloop/src/simloop.c')
| -rw-r--r-- | simloop/src/simloop.c | 49 |
1 files changed, 36 insertions, 13 deletions
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 @@ | |||
| 2 | 2 | ||
| 3 | #include <assert.h> | 3 | #include <assert.h> |
| 4 | 4 | ||
| 5 | static double min(double a, double b) { return a <= b ? a : b; } | ||
| 6 | |||
| 5 | static simloop_time_t ddt_from_fps(int fps) { | 7 | static simloop_time_t ddt_from_fps(int fps) { |
| 6 | static constexpr double NANOSECONDS = 1e9; | 8 | static constexpr double NANOSECONDS = 1e9; |
| 7 | return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps); | 9 | return (fps == 0) ? 0 : (simloop_time_t)(NANOSECONDS / (double)fps); |
| @@ -23,8 +25,8 @@ Simloop simloop_make(const SimloopArgs* args) { | |||
| 23 | .ddt = ddt_from_fps(args->max_render_fps), | 25 | .ddt = ddt_from_fps(args->max_render_fps), |
| 24 | .time = 0, | 26 | .time = 0, |
| 25 | }, | 27 | }, |
| 26 | .first_iter = true, | 28 | .percent_frame = 0., |
| 27 | .updates_since_last_render = false, | 29 | .first_iter = true, |
| 28 | }; | 30 | }; |
| 29 | } | 31 | } |
| 30 | 32 | ||
| @@ -33,6 +35,12 @@ static bool step_update(const Simloop* sim, SimloopTimeline* timeline) { | |||
| 33 | assert(timeline); | 35 | assert(timeline); |
| 34 | assert(timeline->ddt > 0); | 36 | assert(timeline->ddt > 0); |
| 35 | 37 | ||
| 38 | // If the update falls behind the clock, we advance by a single ddt increment | ||
| 39 | // per loop iteration here and give it a chance to catch up over subsequent | ||
| 40 | // iterations. | ||
| 41 | // This has the implication that percent_frame can fall out of range (>1) if | ||
| 42 | // we are not careful with how it is defined. See the general update function | ||
| 43 | // below. | ||
| 36 | const simloop_time_t dt = sim->clock - timeline->time; | 44 | const simloop_time_t dt = sim->clock - timeline->time; |
| 37 | const bool should_step = dt >= timeline->ddt; | 45 | const bool should_step = dt >= timeline->ddt; |
| 38 | timeline->time += should_step ? timeline->ddt : 0; | 46 | timeline->time += should_step ? timeline->ddt : 0; |
| @@ -47,8 +55,8 @@ static bool step_render(const Simloop* sim, SimloopTimeline* timeline) { | |||
| 47 | if (timeline->ddt > 0) { | 55 | if (timeline->ddt > 0) { |
| 48 | render = step_update(sim, timeline); | 56 | render = step_update(sim, timeline); |
| 49 | } else { | 57 | } else { |
| 58 | render = timeline->time < sim->clock; | ||
| 50 | timeline->time = sim->clock; | 59 | timeline->time = sim->clock; |
| 51 | render = true; | ||
| 52 | } | 60 | } |
| 53 | return render; | 61 | return render; |
| 54 | } | 62 | } |
| @@ -60,24 +68,39 @@ void simloop_update(Simloop* sim, simloop_time_t dt, SimloopOut* out) { | |||
| 60 | sim->clock += dt; | 68 | sim->clock += dt; |
| 61 | 69 | ||
| 62 | // Simulation update. | 70 | // Simulation update. |
| 63 | const bool updated = step_update(sim, &sim->update); | 71 | const bool update_this_tick = step_update(sim, &sim->update); |
| 64 | sim->updates_since_last_render = sim->updates_since_last_render || updated; | ||
| 65 | 72 | ||
| 66 | // Simulation render. | 73 | // Simulation render. |
| 67 | const bool rendered = | 74 | const bool render_this_tick = |
| 68 | (sim->updates_since_last_render && step_render(sim, &sim->render)) || | 75 | step_render(sim, &sim->render) || |
| 69 | (sim->first_iter); // Trigger an initial render on the first frame. | 76 | sim->first_iter; // Trigger an initial render on the first frame. |
| 70 | sim->updates_since_last_render = | 77 | |
| 71 | sim->updates_since_last_render && !out->should_render; | 78 | // Interpolator for smooth animation. |
| 79 | // If rendering is not frame-rate capped, then its timeline should always be | ||
| 80 | // at least as recent as the update's. Otherwise, it is possible for the | ||
| 81 | // rendering timeline to be behind. | ||
| 82 | // If the update falls behind the clock, then percent_frame can fall out of | ||
| 83 | // range (>1) if we are not careful. We impose that it is strictly never >1 | ||
| 84 | // to account for this case. | ||
| 85 | assert(sim->update.ddt > 0); | ||
| 86 | assert( | ||
| 87 | (sim->render.ddt == 0) ? (sim->update.time <= sim->render.time) : true); | ||
| 88 | sim->percent_frame = | ||
| 89 | (sim->render.time >= sim->update.time) | ||
| 90 | ? min(1., ((double)(sim->render.time - sim->update.time) / | ||
| 91 | (double)sim->update.ddt)) | ||
| 92 | : sim->percent_frame; | ||
| 93 | assert((0. <= sim->percent_frame) && (sim->percent_frame <= 1.)); | ||
| 72 | 94 | ||
| 73 | // Loop state update. | 95 | // Loop state update. |
| 74 | sim->frame += (updated ? 1 : 0); | 96 | sim->frame += (update_this_tick ? 1 : 0); |
| 75 | sim->first_iter = false; | 97 | sim->first_iter = false; |
| 76 | 98 | ||
| 77 | out->frame = sim->frame; | 99 | out->frame = sim->frame; |
| 78 | out->render_elapsed = sim->render.time; | 100 | out->render_elapsed = sim->render.time; |
| 79 | out->update_elapsed = sim->update.time; | 101 | out->update_elapsed = sim->update.time; |
| 80 | out->update_dt = sim->update.ddt; | 102 | out->update_dt = sim->update.ddt; |
| 81 | out->should_update = updated; | 103 | out->percent_frame = sim->percent_frame; |
| 82 | out->should_render = rendered; | 104 | out->should_update = update_this_tick; |
| 105 | out->should_render = render_this_tick; | ||
| 83 | } | 106 | } |
