From 2b795fb07f0b9d10fbf006f8ab2cb99951cc6a44 Mon Sep 17 00:00:00 2001 From: M Stoeckl Date: Sat, 11 Jan 2025 15:59:29 -0500 Subject: [PATCH] Move frame prediction logic for camera from Sector to Camera This makes it possible to avoid interpolating the camera position when a Camera::move or other discontinuous transition is requested. --- src/editor/editor.cpp | 2 -- src/object/camera.cpp | 49 +++++++++++++++++++++++++++++++++++++++-- src/object/camera.hpp | 20 ++++++++++++++++- src/supertux/sector.cpp | 32 ++------------------------- src/supertux/sector.hpp | 7 ------ 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/editor/editor.cpp b/src/editor/editor.cpp index 21bf751a5e..50c0dcdbc1 100644 --- a/src/editor/editor.cpp +++ b/src/editor/editor.cpp @@ -180,8 +180,6 @@ Editor::draw(Compositor& compositor) m_new_scale = 0.f; } - m_sector->pause_camera_interpolation(); - // Avoid drawing the sector if we're about to test it, as there is a dangling pointer // issue with the PlayerStatus. if (!m_leveltested) diff --git a/src/object/camera.cpp b/src/object/camera.cpp index 04d8579a83..04eb89ce4d 100644 --- a/src/object/camera.cpp +++ b/src/object/camera.cpp @@ -25,6 +25,7 @@ #include "math/random.hpp" #include "math/util.hpp" #include "object/player.hpp" +#include "supertux/constants.hpp" #include "supertux/gameconfig.hpp" #include "supertux/globals.hpp" #include "supertux/level.hpp" @@ -82,7 +83,9 @@ Camera::Camera(const std::string& name) : m_scale_easing(), m_scale_anchor(), m_minimum_scale(1.f), - m_enfore_minimum_scale(false) + m_enfore_minimum_scale(false), + m_last_translation(0.0f, 0.0f), + m_last_scale(1.0f) { } @@ -118,7 +121,9 @@ Camera::Camera(const ReaderMapping& reader) : m_scale_easing(), m_scale_anchor(), m_minimum_scale(1.f), - m_enfore_minimum_scale(false) + m_enfore_minimum_scale(false), + m_last_translation(0.0f, 0.0f), + m_last_scale(1.0f) { std::string modename; @@ -207,6 +212,35 @@ Camera::check_state() PathObject::check_state(); } +void Camera::reset_prediction_state() +{ + m_last_translation = get_translation(); + m_last_scale = get_current_scale(); +} + +Vector +Camera::get_predicted_translation(float time_offset) const +{ + if (g_config->frame_prediction) { + // TODO: extrapolate forwards instead of interpolating over the last frame + float x = time_offset * LOGICAL_FPS; + return get_translation() * x + (1 - x) * m_last_translation; + } else { + return get_translation(); + } +} + +float +Camera::get_predicted_scale(float time_offset) const { + if (g_config->frame_prediction) { + // TODO: extrapolate forwards instead of interpolating over the last frame + float x = time_offset * LOGICAL_FPS; + return get_current_scale() * x + (1 - x) * m_last_scale; + } else { + return get_current_scale(); + } +} + const Vector Camera::get_translation() const { @@ -218,6 +252,7 @@ void Camera::set_translation_centered(const Vector& translation) { m_translation = translation - m_screen_size.as_vector() / 2; + reset_prediction_state(); } Rectf @@ -237,6 +272,7 @@ Camera::reset(const Vector& tuxpos) keep_in_bounds(m_translation); m_cached_translation = m_translation; + reset_prediction_state(); } void @@ -286,6 +322,7 @@ Camera::scroll_to(const Vector& goal, float scrolltime) m_translation.x = goal.x; m_translation.y = goal.y; m_mode = Mode::MANUAL; + reset_prediction_state(); return; } @@ -313,6 +350,10 @@ Camera::draw(DrawingContext& context) void Camera::update(float dt_sec) { + // For use by camera position prediction + m_last_scale = get_current_scale(); + m_last_translation = get_translation(); + // Minimum scale should be set during the update sequence; else, reset it. m_enfore_minimum_scale = false; @@ -361,6 +402,8 @@ Camera::keep_in_bounds(const Rectf& bounds) // Remove any scale factor we may have added in the checks above. m_translation -= scale_factor; + + reset_prediction_state(); } void @@ -754,6 +797,8 @@ Camera::ease_scale(float scale, float time, easing ease, AnchorPoint anchor) m_scale = scale; if (m_mode == Mode::MANUAL) m_translation = m_scale_target_translation; + + reset_prediction_state(); } } diff --git a/src/object/camera.hpp b/src/object/camera.hpp index 3a444023b6..8f1342f317 100644 --- a/src/object/camera.hpp +++ b/src/object/camera.hpp @@ -87,9 +87,16 @@ class Camera final : public LayerObject, /** reset camera position */ void reset(const Vector& tuxpos); + /** Get the predicted position of the camera, time_offset seconds from now. */ + Vector get_predicted_translation(float time_offset) const; + /** Get the predicted scale of the camera, time_offset seconds from now. */ + float get_predicted_scale(float time_offset) const; + /** return camera position */ const Vector get_translation() const; - inline void set_translation(const Vector& translation) { m_translation = translation; } + inline void set_translation(const Vector& translation) { + m_translation = translation; reset_prediction_state(); + } void set_translation_centered(const Vector& translation); void keep_in_bounds(const Rectf& bounds); @@ -271,6 +278,10 @@ class Camera final : public LayerObject, Vector get_scale_anchor_target() const; void reload_scale(); + /** This function should be called whenever the last updates/changes + * to the camera should not be interpolated or extrapolated. */ + void reset_prediction_state(); + private: Mode m_mode; Mode m_defaultmode; @@ -318,6 +329,13 @@ class Camera final : public LayerObject, float m_minimum_scale; bool m_enfore_minimum_scale; + // Remember last camera position, to linearly interpolate camera position + // when drawing frames that are predicted forward a fraction of a game step. + // This is somewhat of a hack: ideally these variables would not be necessary + // and one could predict the next camera scale/translation directly from the + // current camera member variable values. + Vector m_last_translation; + float m_last_scale; private: Camera(const Camera&) = delete; Camera& operator=(const Camera&) = delete; diff --git a/src/supertux/sector.cpp b/src/supertux/sector.cpp index b3305afae5..d0045aa680 100644 --- a/src/supertux/sector.cpp +++ b/src/supertux/sector.cpp @@ -287,9 +287,6 @@ Sector::activate(const Vector& player_pos) if (!m_init_script.empty() && !Editor::is_active()) { run_script(m_init_script, "init-script"); } - - // Do not interpolate camera after it has been warped - pause_camera_interpolation(); } void @@ -361,12 +358,6 @@ Sector::update(float dt_sec) BIND_SECTOR(*this); - // Record last camera parameters, to allow for camera interpolation - Camera& camera = get_camera(); - m_last_scale = camera.get_current_scale(); - m_last_translation = camera.get_translation(); - m_last_dt = dt_sec; - m_squirrel_environment->update(dt_sec); GameObjectManager::update(dt_sec); @@ -483,21 +474,8 @@ Sector::draw(DrawingContext& context) context.push_transform(); Camera& camera = get_camera(); - - if (g_config->frame_prediction && m_last_dt > 0.f) { - // Interpolate between two camera settings; there are many possible ways to do this, but on - // short time scales all look about the same. This delays the camera position by one frame. - // (The proper thing to do, of course, would be not to interpolate, but instead to adjust - // the Camera class to extrapolate, and provide scale/translation at a given time; done - // right, this would make it possible to, for example, exactly sinusoidally shake the - // camera instead of piecewise linearly.) - float x = std::min(1.f, context.get_time_offset() / m_last_dt); - context.set_translation(camera.get_translation() * x + (1 - x) * m_last_translation); - context.scale(camera.get_current_scale() * x + (1 - x) * m_last_scale); - } else { - context.set_translation(camera.get_translation()); - context.scale(camera.get_current_scale()); - } + context.set_translation(camera.get_predicted_translation(context.get_time_offset())); + context.scale(camera.get_predicted_scale(context.get_time_offset())); GameObjectManager::draw(context); @@ -742,12 +720,6 @@ Sector::stop_looping_sounds() } } -void -Sector::pause_camera_interpolation() -{ - m_last_dt = 0.; -} - void Sector::play_looping_sounds() { for (const auto& object : get_objects()) { diff --git a/src/supertux/sector.hpp b/src/supertux/sector.hpp index 07606089a6..ff780564a3 100644 --- a/src/supertux/sector.hpp +++ b/src/supertux/sector.hpp @@ -99,9 +99,6 @@ class Sector final : public Base::Sector /** stops all looping sounds in whole sector. */ void stop_looping_sounds(); - /** Freeze camera position for this frame, preventing camera interpolation jumps and loops */ - void pause_camera_interpolation(); - /** continues the looping sounds in whole sector. */ void play_looping_sounds(); @@ -263,10 +260,6 @@ class Sector final : public Base::Sector TextObject& m_text_object; - Vector m_last_translation; // For camera interpolation at high frame rates - float m_last_scale; - float m_last_dt; - private: Sector(const Sector&) = delete; Sector& operator=(const Sector&) = delete;