Merge pull request #58381 from lawnjelly/fti_fix_double_ticks

Fix get_global_transform_interpolated() with multiple ticks per frame
This commit is contained in:
Rémi Verschelde 2022-02-28 21:13:48 +01:00 committed by GitHub
commit 706d282cd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 29 deletions

View file

@ -2240,6 +2240,7 @@ bool Main::iteration() {
if (OS::get_singleton()->get_main_loop()->iteration(frame_slice * time_scale)) {
exit = true;
Engine::get_singleton()->_in_physics = false;
break;
}

View file

@ -181,6 +181,7 @@ void Spatial::_notification(int p_what) {
data.parent = nullptr;
data.C = nullptr;
data.toplevel_active = false;
_disable_client_physics_interpolation();
} break;
case NOTIFICATION_ENTER_WORLD: {
data.inside_world = true;
@ -238,8 +239,8 @@ void Spatial::_notification(int p_what) {
#endif
} break;
case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
if (data.physics_interpolation_data) {
data.physics_interpolation_data->global_xform_prev = data.physics_interpolation_data->global_xform_curr;
if (data.client_physics_interpolation_data) {
data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
}
} break;
@ -275,20 +276,53 @@ Transform Spatial::get_transform() const {
return data.local_transform;
}
void Spatial::_update_physics_interpolation_data() {
if (!_is_physics_interpolated_client_side()) {
return;
// Return false to timeout and remove from the client interpolation list.
bool Spatial::update_client_physics_interpolation_data() {
if (!is_inside_tree() || !_is_physics_interpolated_client_side()) {
return false;
}
ERR_FAIL_NULL(data.physics_interpolation_data);
PhysicsInterpolationData &pid = *data.physics_interpolation_data;
ERR_FAIL_NULL_V(data.client_physics_interpolation_data, false);
ClientPhysicsInterpolationData &pid = *data.client_physics_interpolation_data;
uint64_t tick = Engine::get_singleton()->get_physics_frames();
if (tick != pid.current_physics_tick) {
pid.global_xform_prev = pid.global_xform_curr;
// Has this update been done already this tick?
// (for instance, get_global_transform_interpolated() could be called multiple times)
if (pid.current_physics_tick != tick) {
// timeout?
if (tick >= pid.timeout_physics_tick) {
return false;
}
if (pid.current_physics_tick == (tick - 1)) {
// normal interpolation situation, there is a continuous flow of data
// from one tick to the next...
pid.global_xform_prev = pid.global_xform_curr;
} else {
// there has been a gap, we cannot sensibly offer interpolation over
// a multitick gap, so we will teleport
pid.global_xform_prev = get_global_transform();
}
pid.current_physics_tick = tick;
}
pid.global_xform_curr = get_global_transform();
return true;
}
void Spatial::_disable_client_physics_interpolation() {
// Disable any current client side interpolation
// (this can always restart as normal if you later re-attach the node to the SceneTree)
if (data.client_physics_interpolation_data) {
memdelete(data.client_physics_interpolation_data);
data.client_physics_interpolation_data = nullptr;
SceneTree *tree = get_tree();
if (tree && _client_physics_interpolation_spatials_list.in_list()) {
tree->client_physics_interpolation_remove_spatial(&_client_physics_interpolation_spatials_list);
}
}
_set_physics_interpolated_client_side(false);
}
Transform Spatial::_get_global_transform_interpolated(real_t p_interpolation_fraction) {
@ -298,22 +332,41 @@ Transform Spatial::_get_global_transform_interpolated(real_t p_interpolation_fra
if (!_is_physics_interpolated_client_side()) {
_set_physics_interpolated_client_side(true);
ERR_FAIL_COND_V(data.physics_interpolation_data, Transform());
data.physics_interpolation_data = memnew(PhysicsInterpolationData);
data.physics_interpolation_data->global_xform_curr = get_global_transform();
data.physics_interpolation_data->global_xform_prev = data.physics_interpolation_data->global_xform_curr;
data.physics_interpolation_data->current_physics_tick = Engine::get_singleton()->get_physics_frames();
ERR_FAIL_COND_V(data.client_physics_interpolation_data, Transform());
data.client_physics_interpolation_data = memnew(ClientPhysicsInterpolationData);
data.client_physics_interpolation_data->global_xform_curr = get_global_transform();
data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
data.client_physics_interpolation_data->current_physics_tick = Engine::get_singleton()->get_physics_frames();
}
// Storing the last tick we requested client interpolation allows us to timeout
// and remove client interpolated nodes from the list to save processing.
// We use some arbitrary timeout here, but this could potentially be user defined.
// Note: This timeout has to be larger than the number of ticks in a frame, otherwise the interpolated
// data will stop flowing before the next frame is drawn. This should only be relevant at high tick rates.
// We could alternatively do this by frames rather than ticks and avoid this problem, but then the behaviour
// would be machine dependent.
data.client_physics_interpolation_data->timeout_physics_tick = Engine::get_singleton()->get_physics_frames() + 256;
// make sure data is up to date
_update_physics_interpolation_data();
update_client_physics_interpolation_data();
// interpolate the current data
const Transform &xform_curr = data.physics_interpolation_data->global_xform_curr;
const Transform &xform_prev = data.physics_interpolation_data->global_xform_prev;
const Transform &xform_curr = data.client_physics_interpolation_data->global_xform_curr;
const Transform &xform_prev = data.client_physics_interpolation_data->global_xform_prev;
Transform res;
TransformInterpolator::interpolate_transform(xform_prev, xform_curr, res, p_interpolation_fraction);
SceneTree *tree = get_tree();
// This should not happen, as is_inside_tree() is checked earlier
ERR_FAIL_NULL_V(tree, res);
if (!_client_physics_interpolation_spatials_list.in_list()) {
tree->client_physics_interpolation_add_spatial(&_client_physics_interpolation_spatials_list);
}
return res;
}
@ -321,7 +374,7 @@ Transform Spatial::get_global_transform_interpolated() {
// Pass through if physics interpolation is switched off.
// This is a convenience, as it allows you to easy turn off interpolation
// without changing any code.
if (!is_physics_interpolated_and_enabled()) {
if (Engine::get_singleton()->is_in_physics_frame() || !is_physics_interpolated_and_enabled()) {
return get_global_transform();
}
@ -872,7 +925,7 @@ void Spatial::_bind_methods() {
}
Spatial::Spatial() :
xform_change(this) {
xform_change(this), _client_physics_interpolation_spatials_list(this) {
data.dirty = DIRTY_NONE;
data.children_lock = 0;
@ -886,7 +939,7 @@ Spatial::Spatial() :
data.disable_scale = false;
data.vi_visible = true;
data.physics_interpolation_data = nullptr;
data.client_physics_interpolation_data = nullptr;
#ifdef TOOLS_ENABLED
data.gizmo_disabled = false;
@ -899,8 +952,5 @@ Spatial::Spatial() :
}
Spatial::~Spatial() {
if (data.physics_interpolation_data) {
memdelete(data.physics_interpolation_data);
data.physics_interpolation_data = nullptr;
}
_disable_client_physics_interpolation();
}

View file

@ -55,10 +55,11 @@ class Spatial : public Node {
// optionally stored if we need to do interpolation
// client side (i.e. not in VisualServer) so interpolated transforms
// can be read back with get_global_transform_interpolated()
struct PhysicsInterpolationData {
struct ClientPhysicsInterpolationData {
Transform global_xform_curr;
Transform global_xform_prev;
uint64_t current_physics_tick = 0;
uint64_t timeout_physics_tick = 0;
};
enum TransformDirty {
@ -69,6 +70,7 @@ class Spatial : public Node {
};
mutable SelfList<Node> xform_change;
SelfList<Spatial> _client_physics_interpolation_spatials_list;
struct Data {
mutable Transform global_transform;
@ -101,7 +103,7 @@ class Spatial : public Node {
List<Spatial *> children;
List<Spatial *>::Element *C;
PhysicsInterpolationData *physics_interpolation_data;
ClientPhysicsInterpolationData *client_physics_interpolation_data;
#ifdef TOOLS_ENABLED
Ref<SpatialGizmo> gizmo;
@ -121,10 +123,10 @@ protected:
_FORCE_INLINE_ void set_ignore_transform_notification(bool p_ignore) { data.ignore_notification = p_ignore; }
_FORCE_INLINE_ void _update_local_transform() const;
void _update_physics_interpolation_data();
void _set_vi_visible(bool p_visible);
bool _is_vi_visible() const { return data.vi_visible; }
Transform _get_global_transform_interpolated(real_t p_interpolation_fraction);
void _disable_client_physics_interpolation();
void _notification(int p_what);
static void _bind_methods();
@ -162,6 +164,7 @@ public:
Transform get_transform() const;
Transform get_global_transform() const;
Transform get_global_transform_interpolated();
bool update_client_physics_interpolation_data();
#ifdef TOOLS_ENABLED
virtual Transform get_global_gizmo_transform() const;

View file

@ -550,8 +550,8 @@ Transform SkeletonIK::_get_target_transform() {
}
if (target_node_override && target_node_override->is_inside_tree()) {
// Make sure to use the interpolated transform as target. This will pass through
// to get_global_transform() when physics interpolation is off, and when using interpolation,
// Make sure to use the interpolated transform as target.
// This will pass through to get_global_transform() when physics interpolation is off, and when using interpolation,
// ensure that the target matches the interpolated visual position of the target when updating the IK each frame.
return target_node_override->get_global_transform_interpolated();
} else {

View file

@ -104,6 +104,27 @@ SceneTreeTimer::SceneTreeTimer() {
process_pause = true;
}
// This should be called once per physics tick, to make sure the transform previous and current
// is kept up to date on the few spatials that are using client side physics interpolation
void SceneTree::ClientPhysicsInterpolation::physics_process() {
for (SelfList<Spatial> *E = _spatials_list.first(); E;) {
Spatial *spatial = E->self();
SelfList<Spatial> *current = E;
// get the next element here BEFORE we potentially delete one
E = E->next();
// This will return false if the spatial has timed out ..
// i.e. If get_global_transform_interpolated() has not been called
// for a few seconds, we can delete from the list to keep processing
// to a minimum.
if (!spatial->update_client_physics_interpolation_data()) {
_spatials_list.remove(current);
}
}
}
void SceneTree::tree_changed() {
tree_version++;
emit_signal(tree_changed_name);
@ -498,6 +519,16 @@ bool SceneTree::is_physics_interpolation_enabled() const {
return _physics_interpolation_enabled;
}
void SceneTree::client_physics_interpolation_add_spatial(SelfList<Spatial> *p_elem) {
// This ensures that _update_physics_interpolation_data() will be called at least once every
// physics tick, to ensure the previous and current transforms are kept up to date.
_client_physics_interpolation._spatials_list.add(p_elem);
}
void SceneTree::client_physics_interpolation_remove_spatial(SelfList<Spatial> *p_elem) {
_client_physics_interpolation._spatials_list.remove(p_elem);
}
bool SceneTree::iteration(float p_time) {
root_lock++;
@ -510,6 +541,11 @@ bool SceneTree::iteration(float p_time) {
}
}
// Any objects performing client physics interpolation
// should be given an opportunity to keep their previous transforms
// up to take before each new physics tick.
_client_physics_interpolation.physics_process();
flush_transform_notifications();
MainLoop::iteration(p_time);

View file

@ -41,6 +41,7 @@
class PackedScene;
class Node;
class Spatial;
class Viewport;
class Material;
class Mesh;
@ -102,6 +103,11 @@ private:
Group() { changed = false; };
};
struct ClientPhysicsInterpolation {
SelfList<Spatial>::List _spatials_list;
void physics_process();
} _client_physics_interpolation;
Viewport *root;
uint64_t tree_version;
@ -411,6 +417,9 @@ public:
void set_physics_interpolation_enabled(bool p_enabled);
bool is_physics_interpolation_enabled() const;
void client_physics_interpolation_add_spatial(SelfList<Spatial> *p_elem);
void client_physics_interpolation_remove_spatial(SelfList<Spatial> *p_elem);
static void add_idle_callback(IdleCallback p_callback);
SceneTree();
~SceneTree();