Skip to content

Commit

Permalink
Merge pull request #58381 from lawnjelly/fti_fix_double_ticks
Browse files Browse the repository at this point in the history
Fix get_global_transform_interpolated() with multiple ticks per frame
  • Loading branch information
akien-mga authored Feb 28, 2022
2 parents a76316c + 688dc53 commit 706d282
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 29 deletions.
1 change: 1 addition & 0 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
98 changes: 74 additions & 24 deletions scene/3d/spatial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -298,30 +332,49 @@ 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;
}

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();
}

Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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();
}
9 changes: 6 additions & 3 deletions scene/3d/spatial.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -121,10 +123,10 @@ class Spatial : public Node {
_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();
Expand Down Expand Up @@ -162,6 +164,7 @@ class Spatial : public Node {
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;
Expand Down
4 changes: 2 additions & 2 deletions scene/animation/skeleton_ik.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions scene/main/scene_tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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++;

Expand All @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions scene/main/scene_tree.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

class PackedScene;
class Node;
class Spatial;
class Viewport;
class Material;
class Mesh;
Expand Down Expand Up @@ -102,6 +103,11 @@ class SceneTree : public MainLoop {
Group() { changed = false; };
};

struct ClientPhysicsInterpolation {
SelfList<Spatial>::List _spatials_list;
void physics_process();
} _client_physics_interpolation;

Viewport *root;

uint64_t tree_version;
Expand Down Expand Up @@ -411,6 +417,9 @@ class SceneTree : public MainLoop {
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();
Expand Down

0 comments on commit 706d282

Please sign in to comment.