-
-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add object position interpolation between physics frames #671
Comments
This looks like extrapolation then. Otherwise the idea looks good to me. |
Related to godotengine/godot#30791. reduz is still against having it in core last time I checked (due to the additional work required when teleporting objects), so lawnjelly's smoothing add-on is our best bet for now. As a 144 Hz monitor user, I'd still like to have built-in support for physics interpolation 🙂
Keep in mind doing this increases latency in a noticeable manner. Even if you use physics interpolation, I'd recommend not doing this unless you have absolutely no other option. |
@groud I've updated the images to better visualize it, but let me explain better what I mean. Godot is already rendering 1 physics frame behind this mean that what you are seeing is already the past. Instead of hardly set the new position what I'm proposing is to interpolate it between the old and the new one. We are still 1 physics frame behind, but now we interpolate between the old and the new position.
In Overshooting can still happen, during frame drop, but since we are using the previous frame it's right away updated in the new iterations. |
@Calinou yes, we should provide an API to teleport the objects or to move the object smoothly. However, this problem should be fixed because the rendering is laggy even with 120frames rendered per second.
This is exactly the problem that I'm trying to solve. I can notice this high latency with my 144hz monitor and physics 60hz which is the same ratio of monitor 60Hz and physics 30Hz. The solution for me is not increase the physics frames to 120hz so to balance it with the rendering, but rather provide a smoothing position update. |
Ah, indeed in that case, this is interpolation. Another question is how to handle things that aren't managed by the physics engine itself. I guess that for rigidbodies, the interpolation would be built-in, but for variables set by the user in |
@groud The user script final result is something that moves in the screen, so just interpolate the result (the object position) should be enough. |
This is often referred to as fixed timestep interpolation, see: See my article here too, it has more references: I added this PR godotengine/godot#30226 in middle of last year to provide basic support for this, and you will notice you have the I wrote a c++ module to do it: These 2 might be a little out of date but show roughly how the addons work: I also researched adding it to core, and actually got it working as part of Spatial, among other things (which was quite tricky to retrofit, due to the design of Spatial, hence why I went for separate nodes in the module / addon). However just to warn you, reduz was against adding it (in any form) to core (at the time), although he was ok with adding the interpolation_fraction PR, because the change was quite small and it allowed users to do the interpolation (without this there is no way to accurately calculate the interpolation fraction in user code). This goes with the whole philosophy of keep everything but the essential out of core, and leaving the rest to addons etc. Of course in practice probably only a small percentage of users make use of addons / modules etc. Related .. I've also got delta smoothing working in my own branch both fixed function and also custom function from gdscript, although haven't made a PR because of lack of interest: You've also seen the semi-fixed timestep PR which is an alternative solution and has been available since July 2019: To clarify, I tend to use fixed timestep interpolation myself although semi-fixed can be useful in some cases, and I would think it useful to be available too. So yeah, it is all very technically doable and I'm all for it, but to get it in core you'd need to get enough 'people power' to convince reduz I think (although maybe his opinion may have changed in meantime?). |
This comment has been minimized.
This comment has been minimized.
No, this is incorrect. It was discussed at the time, check the IRC logs. Relevant bit was here:
Of course everyone can change their mind though! 😁 Especially with version 4.0 being available for breaking changes. I do understand the reasoning though, some factors:
The alternative suggestion reduz gave was to change timestep to match refresh rate depending on which machine the user is running on. This is problematic in terms of giving a repeatable gameplay experience of course, but my semi-fixed timestep PR can do this if I remember right. |
I did this myself and the result is much smoother even on a normal 60fps monitor, before that i always had an annoying stutter even on high framerates. |
Why not just increase the physics FPS? Godot allows you to do this. If the concern is with performance, wouldn't object interpolation also be an intensive operation? Also, interpolating only the position wouldn't fully solve the problem. If the goal is to make things smoother, there is also the matter of interpolating rotations, for a rotating body. Also, interpolating between 2 frames requires the next frame be calculated already, so the only way to plausibly do this in real time is for the displayed objects to always be 1 physics frame behind. This adds latency, and for some use cases this is just as bad as the physics FPS being half. |
There are a few reasons why changing the physics tick rate can be a bad idea, but that kind of thing is used as a strategy in some games.
Changing physics tick rate is also possible with semi-fixed timestep. I have a PR that will do this: Also fixed timestep with interpolation is often the choice in networked games, especially server authoritative. Having different players with different tick rates could be problematic.
Interpolation is usually MUCH, much cheaper than physics. Orders of magnitude. It's practically free compared to rendering overheads.
Yeah you have to interpolate most things. See my addon, it does basis interpolation or quaternion interpolation depending on mode.
Yup, this is one of the trade-offs. You can use extrapolation too, but I generally prefer interpolation. Some games use semi-fixed timestep as an alternative to get faster response. Just as a side note, this whole area isn't controversial .. fixed, or at the least semi-fixed, has been pretty much an industry standard technique in the toolbox of AAA games for 20+ years (either directly or indirectly via physics engines). A lot of teams wouldn't use anything else without good reason. Apart from anything else, just consider alone the beta testing problems that can be caused by varying physics / logic tick rates - it can cost millions of dollars with hard to reproduce bugs and delays. |
Accidentally hit the Close button, sorry 🙏. I totally agree with @lawnjelly. The physics frame requires much much more processing power than interpolation. Just think about that some collision detection algorithm (the box) perform some matrix manipulation, to put the other object in the box coordinate system, before doing some other matrix manipulation to check separation axis. Also, consider that when godot detects that an object got moved by the physics engine, it recalculates all the transformations of all child of a rigid body. I could continue by add the custom code in the _physics process, that is usually equally slow, etc.. Despite performance, the real problem is not solved increasing physics frames to 144hz. In case there is a good pc with a good monitor (250hz) the physics is still to slow at (144hz),and the game would not be fluid. In conclusion, the physics and the rendering, should be completely decoupled and one must not be dependent from the other for the best result always. |
Just forgot to mention that Godot is already 1 physics frame behind. |
Interpolation needs two frames, which means the most recent frame and the previous one. If Godot is already 1 physics frame behind, then that would be the most recent frame, and then interpolation would make it up to 2 frames behind, if I'm understanding this correctly. |
Yes, I mean that for a bug we are already another frame behind: godotengine/godot#37702 than the normal 1 frame. So it's not so noticeable as you would expect. |
The other day, discussing with @lawnjelly, we come with some ideas to integrate the frame interpolation in a transparent way that doesn't require a huge engine change nor the addition of the First thing first, this issue is about decoupling physics and rendering. The interpolator can submit interpolated transforms directly into the function The function As you can see it has 0 API change, it's completely transparent, and it will provide us the physics interpolation. |
Godot really really needs this. I can only think that the majority of users are making games in their 60Hz monitors without knowing that they look really jittery on all 75, 144, and 240Hz monitors. I, as a begginer, had to ask the teacher of my course why his project was so jittery and he didn't seem to know so I thought that the teacher wasn't very skilled until I saw that every official demo was jittery on my PC. Only setting my physics tick rate to 144 gives me a smooth experience but that is not ideal as it is not performant and also doesn't adapt to each player. I love the engine so all my hopes are in this idea right now. |
// This work has been kindly sponsored by IMVU.
// Version 3.1: faster and better recovering algorithm, more optimized, supports a dynamic physics frame.
/*************************************************************************/
/* frame_interpolator.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
/** @author: AndreaCatania */
#ifndef FRAME_INTERPOLATOR_H
#define FRAME_INTERPOLATOR_H
#include "core/local_vector.h"
#include "core/math/math_defs.h"
#include "core/math/math_funcs.h"
#include "core/object.h"
#include "core/print_string.h"
// Used to debug the interpolation.
//#define DEBUG_FRAME_INTERPOLATOR
template <class T>
class FrameInterpolator {
T next_data;
T current_data;
real_t time_ahead = 0.0;
real_t speed_factor = 1.0;
real_t last_insert_delta_time = 0.0;
real_t last_remaining_time_at_insert = 0.0;
/// Defines how many dynamic frames the interpolation is behind.
/// The speedup mechanism will keep the interpolation behind by:
/// (ideal_frames_ahead * delta) + physics_delta
real_t ideal_frames_ahead = 3.0;
/// The bounds for the speed_factor, default 20% of delta.
real_t speed_factor_bounds = 0.2;
/// The sigmoid factor can be used to change the sigmoid shape so that
/// the speed of the recovery is also changed.
/// With a factor of 1.0 the mechanism will try to recover the change in 1.0
/// frame. A too high factor can make the mechanism too hard and the recoverage
/// will be too noticeable.
real_t sigmoid_factor = 0.05;
/// The rate of change at witch the interpolation speed changes.
/// This allow to smooth the interpolation speed so to remove the noise
/// caused by the dynamic delta.
/// The speedup per frame: `lerp(speed_factor, desired_speed_factor, speedup * p_delta)`
real_t speedup = 10.0;
/// How many physical frames it's allowed to stay behind.
/// This is useful to fastforward the interpolator to more up-to-date
/// information.
/// This happens when you have a really long frame and many physics frames
/// are processed in the same frame.
uint32_t fallback_amount = 10;
public:
/// Reset the frame interpolation. Call this whenever the data store are
/// outdated.
void reset(const T &p_current);
/// Push the next information to interpolate. Must be called each
/// physics_process.
void push_data(const T &p_data, real_t p_delta);
/// Returns the interpolated data. This must be called each `process`.
T get_next_frame_data(real_t p_delta);
};
template <class T>
void FrameInterpolator<T>::reset(
const T &p_current) {
next_data = p_current;
current_data = p_current;
time_ahead = 0.0;
last_insert_delta_time = 0.0;
last_remaining_time_at_insert = 0.0;
}
template <class T>
void FrameInterpolator<T>::push_data(const T &p_data, real_t p_delta) {
next_data = p_data;
last_insert_delta_time = p_delta;
last_remaining_time_at_insert = time_ahead;
#ifdef DEBUG_FRAME_INTERPOLATOR
print_line("Time ahead: " + rtos(time_ahead) + ", Delta: " + rtos(p_delta));
#endif
time_ahead += p_delta;
}
template <class T>
T FrameInterpolator<T>::get_next_frame_data(real_t p_delta) {
// TODO this is an hack because sometimes the engine gives 0 delta when the
// physics_iteration is changed.
p_delta = MAX(p_delta, CMP_EPSILON);
if (time_ahead > 0.0) {
if (unlikely(time_ahead > (real_t(fallback_amount) * last_insert_delta_time))) {
// Move the timeline forward till the 20% fallback.
p_delta = time_ahead - (real_t(fallback_amount) * 0.2 * last_insert_delta_time);
} else {
// Computes the speed_factor
// The `ideal` is used to know the amount of time the interpolator
// has to stay behind the new received frame.
const real_t ideal = p_delta * ideal_frames_ahead;
// The `delta_ideal` represents the distance to the `ideal` time.
// It's used to determine the speedup amount and direction.
const real_t delta_ideal = last_remaining_time_at_insert - ideal;
// The `delta_ideal` is feed into the sigmoid function `tanh` that
// returns the value compressed in the range of -1 / 1.
const real_t sigmoid = Math::tanh((delta_ideal / p_delta) * sigmoid_factor);
// The `sigmoid` is now converted to the `desired_speed_factor` for
// this frame.
const real_t desired_speed_factor = 1.0 + sigmoid * speed_factor_bounds;
// The `speed_factor` is interpolated to the `desired_speed_factor`
// so that we smoothly transition to the new value.
// In this way we can remove the noise of the `p_delta`.
speed_factor = Math::lerp(speed_factor, desired_speed_factor, speedup * p_delta);
speed_factor = CLAMP(speed_factor, 1.0 - speed_factor_bounds, 1.0 + speed_factor_bounds);
#ifdef DEBUG_FRAME_INTERPOLATOR
print_line("Speed factor: " + rtos(speed_factor) + " ~~ Desired speed factor: " + rtos(desired_speed_factor) + " ~~ Ideal frame: " + rtos(ideal) + " ~~ Delta ideal: " + rtos(delta_ideal) + " ~~ Sigmoid: " + rtos(sigmoid) + " ~~ Time ahead: " + rtos(time_ahead));
#endif
}
// Advance the time.
const real_t adjusted_delta = MIN(p_delta * speed_factor, time_ahead);
const real_t interpolation_factor = adjusted_delta / time_ahead;
time_ahead -= adjusted_delta;
current_data = current_data.interpolate_with(next_data, interpolation_factor);
} else {
// Computes the speed_factor
speed_factor -= speedup * p_delta;
speed_factor = MAX(speed_factor, 1.0 - speed_factor_bounds);
}
return current_data;
}
class TransformFrameInterpolator : public Object {
GDCLASS(TransformFrameInterpolator, Object);
FrameInterpolator<Transform> interpolator;
public:
static void _bind_methods();
TransformFrameInterpolator();
void reset(const Transform &p_current);
void push_data(const Transform &p_data, real_t p_delta);
Transform get_next_frame_data(real_t p_delta);
};
#endif // This work has been kindly sponsored by IMVU.
// Version 3.1: faster and better recovering algorithm, more optimized, supports a dynamic physics frame.
/*************************************************************************/
/* frame_interpolator.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2019 Godot Engine contributors (cf. AUTHORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
/** @author: AndreaCatania */
#include "frame_interpolator.h"
void TransformFrameInterpolator::_bind_methods() {
ClassDB::bind_method(D_METHOD("reset", "current_transform"), &TransformFrameInterpolator::reset);
ClassDB::bind_method(D_METHOD("push_data", "transform", "delta"), &TransformFrameInterpolator::push_data);
ClassDB::bind_method(D_METHOD("get_next_frame_data", "delta"), &TransformFrameInterpolator::get_next_frame_data);
}
TransformFrameInterpolator::TransformFrameInterpolator() {}
void TransformFrameInterpolator::reset(const Transform &p_current) {
interpolator.reset(p_current);
}
void TransformFrameInterpolator::push_data(const Transform &p_data, real_t p_delta) {
interpolator.push_data(p_data, p_delta);
}
Transform TransformFrameInterpolator::get_next_frame_data(const real_t p_delta) {
return interpolator.get_next_frame_data(p_delta);
} The above code is the implementation of the utility that I'm using to interpolate the position of RigidBody so to unbound the physics and the rendering. The below one is the change that I did to the RigidBody in order to integrate it: diff --git a/scene/3d/physics_body.cpp b/scene/3d/physics_body.cpp
index 828d7d08bb..a5d5862bdc 100644
--- a/scene/3d/physics_body.cpp
+++ b/scene/3d/physics_body.cpp
@@ -470,8 +470,7 @@ void RigidBody::_direct_state_changed(Object *p_state) {
state = (PhysicsDirectBodyState *)p_state; //trust it
#endif
- set_ignore_transform_notification(true);
- set_global_transform(state->get_transform());
+ frame_interpolator.push_data(state->get_transform());
linear_velocity = state->get_linear_velocity();
angular_velocity = state->get_angular_velocity();
if (sleeping != state->is_sleeping()) {
@@ -480,7 +479,6 @@ void RigidBody::_direct_state_changed(Object *p_state) {
}
if (get_script_instance())
get_script_instance()->call("_integrate_forces", state);
- set_ignore_transform_notification(false);
if (contact_monitor) {
@@ -572,20 +570,38 @@ void RigidBody::_direct_state_changed(Object *p_state) {
void RigidBody::_notification(int p_what) {
-#ifdef TOOLS_ENABLED
- if (p_what == NOTIFICATION_ENTER_TREE) {
- if (Engine::get_singleton()->is_editor_hint()) {
- set_notify_local_transform(true); //used for warnings and only in editor
- }
- }
+ switch (p_what) {
+ case NOTIFICATION_INTERNAL_PROCESS: {
- if (p_what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED) {
- if (Engine::get_singleton()->is_editor_hint()) {
- update_configuration_warning();
- }
- }
+ const real_t delta = get_process_delta_time();
+
+ set_ignore_transform_notification(true);
+ set_global_transform(frame_interpolator.get_next_frame_data(delta));
+ set_ignore_transform_notification(false);
+ } break;
+ case NOTIFICATION_TRANSFORM_CHANGED:
+ case NOTIFICATION_READY:
+ if (Engine::get_singleton()->is_editor_hint() == false) {
+ set_process_internal(true);
+ frame_interpolator.reset(
+ Engine::get_singleton()->get_iterations_per_second(),
+ get_global_transform());
+ }
+ break;
+#ifdef TOOLS_ENABLED
+ case NOTIFICATION_ENTER_TREE:
+ if (Engine::get_singleton()->is_editor_hint()) {
+ set_notify_local_transform(true); //used for warnings and only in editor
+ }
+ break;
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED:
+ if (Engine::get_singleton()->is_editor_hint()) {
+ update_configuration_warning();
+ }
+ break;
#endif
+ }
} As you can see, the required modification is completely contained into the RigidBody and it's possible to have the following result Physics 10Hz - interpolation: That can be compared to, Physics 10Hz - no interpolation: Side by side comparison: https://youtu.be/En-zhIU8zbI Note: Teleport is fully handled transparently. |
The code is based on a comment from a Godot proposal for adding the functionality to the engine: godotengine/godot-proposals#671 (comment)
@AndreaCatania Can your solution also interpolate linear and angular velocity? I would find it helpful in my project. But I can't see it in the code snippet you posted. |
@swift502 Yes you have linear and angular interpolation, it happens thanks to |
If there was a Windows C# Godot build with this feature available, I'd be happy to test it and confirm proper functionality in my main project. |
Sorry to bother @pouleyKetchoupp but are there any plans to address physics interpolation with the physics refactors you're working on for 4.0? Could we possibly stick a 4.0 or 4.x milestone on this? Thanks for any info on this. |
@swift502 Physics interpolation is on the roadmap, although it needs more discussion within the physics team so it might be only for 4.1. |
Closing in favor of #2753, which is more detailed and closer to the final implementation present in 3.5beta so far. |
Describe the problem or limitation you are having in your project:
Usually the rendering is much faster than the physics frames and 144hz monitors are cheap enough to be a standard device for gaming PCs nowadays.
In Godot, the physics updates the position of almost anything, usually at fixed rate of 60Hz, this mean that no matter how faster your machine is to process a frame the object rendered will change at fixed rate of 60 frame per seconds.
In other words, even with high frame rate, the new rendered frame will be the same of the previous until the next physics frame change object positions (computation waste).
The result is that high frame rate monitors are useless and the rendering is not fluid as you would expect.
However, you may want to lower the physics frame rate from 60 to 30, because your game doesn't need such precision and so to unload the CPU; doing so you will notice that the game is not fluid and you are forced to keep using 60 frames per seconds.
Describe the feature / enhancement and how it helps to overcome the problem or limitation:
This limitation can be solved by integrating the position interpolation between the frames.
We know that the physics produces a new position at a rate of 60 frames per second and between those frames the renderer produces other three identical frames. The idea is to interpolate between the old position produced by the physics engine and the new so that the intermediate frames are different each other. The problem with such approach is that we need to know the rendering speed beforehand and since this is not always stable we need a way to determine it.
The idea is to count the amount of intermediate rendering frames between the old physics update and the current one, and spread the interpolation delta between those; so even during the phases where the frame rate is not perfectly stable we are able to interpolate the position in a plausible manner.
Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
Without interpolation
With interpolation
The text was updated successfully, but these errors were encountered: