Skip to content
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

Closed
AndreaCatania opened this issue Apr 6, 2020 · 25 comments
Closed

Add object position interpolation between physics frames #671

AndreaCatania opened this issue Apr 6, 2020 · 25 comments

Comments

@AndreaCatania
Copy link

AndreaCatania commented Apr 6, 2020

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:

Interpolation

Without interpolation

Interpolation_2

With interpolation

Interpolation_3

@groud
Copy link
Member

groud commented Apr 6, 2020

This looks like extrapolation then.
Don't you fear that we could end up with weird behaviors at collision time ? Or with any wrongly extrapolated movement ? If an object enters another and go outside of it at the next physics frame we might have people complaining about some sort of stuttering.

Otherwise the idea looks good to me.

@Calinou
Copy link
Member

Calinou commented Apr 6, 2020

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 🙂

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.

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.

@AndreaCatania
Copy link
Author

AndreaCatania commented Apr 6, 2020

Don't you fear that we could end up with weird behaviors at collision time ? Or with any wrongly extrapolated movement ? If an object enters another and go outside of it at the next physics frame we might have people complaining about some sort of stuttering.

@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.

Interpolation_3
So in the frame 1 we already have the position 1 but instead of setting it directly to it we interpolate it.

In frame 4 we will have the position 2 and we will interpolate between it.

Overshooting can still happen, during frame drop, but since we are using the previous frame it's right away updated in the new iterations.

@AndreaCatania
Copy link
Author

@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.

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.

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.

@groud
Copy link
Member

groud commented Apr 6, 2020

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.

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 _physics_process(), we should probably design an API to be able to smooth those properties too.

@AndreaCatania
Copy link
Author

@groud The user script final result is something that moves in the screen, so just interpolate the result (the object position) should be enough.

@lawnjelly
Copy link
Member

lawnjelly commented Apr 6, 2020

This is often referred to as fixed timestep interpolation, see:
godotengine/godot#30068

See my article here too, it has more references:
https://www.gamedev.net/blogs/entry/2265460-fixing-your-timestep-and-evaluating-godot/

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 Engine.get_physics_interpolation_fraction() function available.

I wrote a c++ module to do it:
https://github.com/lawnjelly/godot-smooth
and a gdscript addon:
https://github.com/lawnjelly/smoothing-addon

These 2 might be a little out of date but show roughly how the addons work:
https://www.youtube.com/watch?v=lWhHBAcH4sM
https://www.youtube.com/watch?v=SFLwCR2KEJ8

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:
godotengine/godot#30791

You've also seen the semi-fixed timestep PR which is an alternative solution and has been available since July 2019:
godotengine/godot#30798

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?).

@Jummit

This comment has been minimized.

@lawnjelly
Copy link
Member

lawnjelly commented Apr 6, 2020

to get it in core you'd need to get enough 'people power' to convince reduz

I think the main reason this isn't core is that no one implemented it yet or even has the knowledge how to implement it. Is someone where to propose a working solution, I don't think Reduz would be against it.

No, this is incorrect. It was discussed at the time, check the IRC logs.

Relevant bit was here:
https://godot.eska.me/irc-logs/meeting/2019-07-18.log

<reduz> lawnjelly: if you want to do it manually it's fine, and if you want to expose something that makes it doing manually it's also fine, but the engine itself will never support it

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 more things in core the more things need to be supported longterm. An example given is interpolated_camera which is in core but might not need to be.
  • It is possible with something like this there may be a need to 'revise' the method if we discover a better way of doing it that may break the existing method. This can be problematic.
  • It has to be easy to use for beginners.
  • It has to play nicely with the rest of the codebase.

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.

@2plus2makes5
Copy link

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.

@Calinou Calinou changed the title Object position interpolation between physics frames. Add object position interpolation between physics frames Apr 15, 2020
@aaronfranke
Copy link
Member

aaronfranke commented May 3, 2020

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.

@lawnjelly
Copy link
Member

lawnjelly commented May 3, 2020

Why not just increase the physics FPS? Godot allows you to do this.

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.

  1. Changing physics tick rate can change behaviour - break mechanics, jump distances etc
  2. It can result in non-deterministic (ish) gameplay

Changing physics tick rate is also possible with semi-fixed timestep. I have a PR that will do this:
godotengine/godot#30798

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.

If the concern is with performance, wouldn't object interpolation also be an intensive operation?

Interpolation is usually MUCH, much cheaper than physics. Orders of magnitude. It's practically free compared to rendering overheads.

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.

Yeah you have to interpolate most things. See my addon, it does basis interpolation or quaternion interpolation depending on mode.

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.

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.

@AndreaCatania
Copy link
Author

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.
this is done per each object near a box.

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..
And this is only a little part of what is needed to rendere a physics frame. So interpolation is much much less performance critical.

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 case of low end pc, the game would be too slow since the physics is running at really high speed.
In case of mobile games, or turn based games, the physics should be run at 30hz to make the game more accessible; and this should not make the objects to move jerky.

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.

@AndreaCatania
Copy link
Author

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.

Just forgot to mention that Godot is already 1 physics frame behind.

@aaronfranke
Copy link
Member

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.

@AndreaCatania
Copy link
Author

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.

@AndreaCatania
Copy link
Author

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 teleport API.

First thing first, this issue is about decoupling physics and rendering.
To achieve this we can introduce an interpolator between the PhysicsBody node and the PhysicsServer. The change would remain localized and, most important, we don't need to add the teleport API since only the physics movement are interpolated between frames.

The interpolator can submit interpolated transforms directly into the function _direct_state_change https://github.com/godotengine/godot/blob/master/scene/3d/physics_body_3d.cpp#L358 . The renderer will receive the interpolated position without knowing nothing about it.

The function get_global_transform would return the not interpolated transform as is now.
The function set_global_transform would teleport the body as is now.

As you can see it has 0 API change, it's completely transparent, and it will provide us the physics interpolation.

@phantomdesvin
Copy link

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 teleport API.

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.

@AndreaCatania
Copy link
Author

AndreaCatania commented Sep 30, 2020

// 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:
ezgif com-video-to-gif

That can be compared to, Physics 10Hz - no interpolation:
ezgif com-video-to-gif(1)

Side by side comparison: https://youtu.be/En-zhIU8zbI

Note: Teleport is fully handled transparently.
Note: I've noticed that not always the interpolation is wanted. For example, a Kinematic Body used in a networked game. So in case we would implement this, we would need a way to deactivate the interpolation for a specific RigidBody / Kinematic Body.

drwhut added a commit to drwhut/godot that referenced this issue Mar 14, 2021
The code is based on a comment from a Godot proposal for adding
the functionality to the engine:
godotengine/godot-proposals#671 (comment)
@swift502
Copy link

swift502 commented Mar 17, 2021

@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.

@AndreaCatania
Copy link
Author

@swift502 Yes you have linear and angular interpolation, it happens thanks to interpolate_with. I've just added the CPP side too, not too much code but it was missing. Let me know if it works, now.

@swift502
Copy link

swift502 commented Mar 19, 2021

Let me know if it works, now.

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.
Unfortunately the setup for a C# build looks pretty convoluted so I don't know if I can get it myself.

@swift502
Copy link

swift502 commented May 4, 2021

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.

@pouleyKetchoupp
Copy link

@swift502 Physics interpolation is on the roadmap, although it needs more discussion within the physics team so it might be only for 4.1.

@Calinou
Copy link
Member

Calinou commented Apr 21, 2022

Closing in favor of #2753, which is more detailed and closer to the final implementation present in 3.5beta so far.

@Calinou Calinou closed this as completed Apr 21, 2022
@aaronfranke aaronfranke closed this as not planned Won't fix, can't repro, duplicate, stale Sep 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants