Browse Source

Physics Interpolation - Move 3D FTI to `SceneTree`

Moves 3D interpolation from `VisualServer` to the client code (`SceneTree`).
Complete rework of 3D physics interpolation, but using the same user API.
lawnjelly 6 months ago
parent
commit
66a2efe491

+ 9 - 0
core/local_vector.h

@@ -104,6 +104,15 @@ public:
 		}
 	}
 
+	bool erase_unordered(const T &p_val) {
+		int64_t idx = find(p_val);
+		if (idx >= 0) {
+			remove_unordered(idx);
+			return true;
+		}
+		return false;
+	}
+
 	U erase_multiple_unordered(const T &p_val) {
 		U from = 0;
 		U count = 0;

+ 1 - 0
doc/classes/BoneAttachment.xml

@@ -14,6 +14,7 @@
 		<member name="bone_name" type="String" setter="set_bone_name" getter="get_bone_name" default="&quot;&quot;">
 			The name of the attached bone.
 		</member>
+		<member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="1" />
 	</members>
 	<constants>
 	</constants>

+ 1 - 0
doc/classes/VehicleWheel.xml

@@ -52,6 +52,7 @@
 			[b]Note:[/b] The simulation does not take the effect of gears into account, you will need to add logic for this if you wish to simulate gears.
 			A negative value will result in the wheel reversing.
 		</member>
+		<member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="1" />
 		<member name="steering" type="float" setter="set_steering" getter="get_steering" default="0.0">
 			The steering angle for the wheel. Setting this to a non-zero value will result in the vehicle turning when it's moving.
 		</member>

+ 0 - 16
doc/classes/VisualServer.xml

@@ -1488,14 +1488,6 @@
 				Sets a material that will override the material for all surfaces on the mesh associated with this instance. Equivalent to [member GeometryInstance.material_override].
 			</description>
 		</method>
-		<method name="instance_reset_physics_interpolation">
-			<return type="void" />
-			<argument index="0" name="instance" type="RID" />
-			<description>
-				Prevents physics interpolation for the current physics tick.
-				This is useful when moving an instance to a new location, to give an instantaneous change rather than interpolation from the previous location.
-			</description>
-		</method>
 		<method name="instance_set_base">
 			<return type="void" />
 			<argument index="0" name="instance" type="RID" />
@@ -1537,14 +1529,6 @@
 				Sets a margin to increase the size of the AABB when culling objects from the view frustum. This allows you to avoid culling objects that fall outside the view frustum. Equivalent to [member GeometryInstance.extra_cull_margin].
 			</description>
 		</method>
-		<method name="instance_set_interpolated">
-			<return type="void" />
-			<argument index="0" name="instance" type="RID" />
-			<argument index="1" name="interpolated" type="bool" />
-			<description>
-				Turns on and off physics interpolation for the instance.
-			</description>
-		</method>
 		<method name="instance_set_layer_mask">
 			<return type="void" />
 			<argument index="0" name="instance" type="RID" />

+ 1 - 1
scene/3d/arvr_nodes.cpp

@@ -613,7 +613,7 @@ void ARVROrigin::_notification(int p_what) {
 		}; break;
 		case NOTIFICATION_INTERNAL_PROCESS: {
 			// set our world origin to our node transform
-			arvr_server->set_world_origin(get_global_transform());
+			arvr_server->set_world_origin(get_global_transform_interpolated());
 
 			// check if we have a primary interface
 			Ref<ARVRInterface> arvr_interface = arvr_server->get_primary_interface();

+ 1 - 0
scene/3d/bone_attachment.cpp

@@ -105,6 +105,7 @@ void BoneAttachment::_notification(int p_what) {
 }
 
 BoneAttachment::BoneAttachment() {
+	set_physics_interpolation_mode(PHYSICS_INTERPOLATION_MODE_OFF);
 	bound = false;
 }
 

+ 34 - 99
scene/3d/camera.cpp

@@ -45,6 +45,13 @@ void Camera::_request_camera_update() {
 	_update_camera();
 }
 
+void Camera::fti_update_servers() {
+	if (camera.is_valid()) {
+		Transform tr = _get_adjusted_camera_transform(_get_cached_global_transform_interpolated());
+		VisualServer::get_singleton()->camera_set_transform(camera, tr);
+	}
+}
+
 void Camera::_update_camera_mode() {
 	force_change = true;
 	switch (mode) {
@@ -85,12 +92,8 @@ void Camera::_update_camera() {
 	if (!is_physics_interpolated_and_enabled()) {
 		VisualServer::get_singleton()->camera_set_transform(camera, get_camera_transform());
 	} else {
-		// Ideally we shouldn't be moving a physics interpolated camera within a frame,
-		// because it will break smooth interpolation, but it may occur on e.g. level load.
-		if (!Engine::get_singleton()->is_in_physics_frame() && camera.is_valid()) {
-			_physics_interpolation_ensure_transform_calculated(true);
-			VisualServer::get_singleton()->camera_set_transform(camera, _interpolation_data.camera_xform_interpolated);
-		}
+		// Force a refresh next frame.
+		fti_notify_node_changed();
 	}
 
 	// here goes listener stuff
@@ -114,40 +117,6 @@ void Camera::_physics_interpolated_changed() {
 	_update_process_mode();
 }
 
-void Camera::_physics_interpolation_ensure_data_flipped() {
-	// The curr -> previous update can either occur
-	// on the INTERNAL_PHYSICS_PROCESS OR
-	// on NOTIFICATION_TRANSFORM_CHANGED,
-	// if NOTIFICATION_TRANSFORM_CHANGED takes place
-	// earlier than INTERNAL_PHYSICS_PROCESS on a tick.
-	// This is to ensure that the data keeps flowing, but the new data
-	// doesn't overwrite before prev has been set.
-
-	// Keep the data flowing.
-	uint64_t tick = Engine::get_singleton()->get_physics_frames();
-	if (_interpolation_data.last_update_physics_tick != tick) {
-		_interpolation_data.xform_prev = _interpolation_data.xform_curr;
-		_interpolation_data.last_update_physics_tick = tick;
-		physics_interpolation_flip_data();
-	}
-}
-
-void Camera::_physics_interpolation_ensure_transform_calculated(bool p_force) const {
-	DEV_CHECK_ONCE(!Engine::get_singleton()->is_in_physics_frame());
-
-	InterpolationData &id = _interpolation_data;
-	uint64_t frame = Engine::get_singleton()->get_frames_drawn();
-
-	if (id.last_update_frame != frame || p_force) {
-		id.last_update_frame = frame;
-
-		TransformInterpolator::interpolate_transform(id.xform_prev, id.xform_curr, id.xform_interpolated, Engine::get_singleton()->get_physics_interpolation_fraction());
-
-		Transform &tr = id.camera_xform_interpolated;
-		tr = _get_adjusted_camera_transform(id.xform_interpolated);
-	}
-}
-
 void Camera::set_desired_process_modes(bool p_process_internal, bool p_physics_process_internal) {
 	_desired_process_internal = p_process_internal;
 	_desired_physics_process_internal = p_physics_process_internal;
@@ -155,17 +124,8 @@ void Camera::set_desired_process_modes(bool p_process_internal, bool p_physics_p
 }
 
 void Camera::_update_process_mode() {
-	bool process = _desired_process_internal;
-	bool physics_process = _desired_physics_process_internal;
-
-	if (is_physics_interpolated_and_enabled()) {
-		if (is_current()) {
-			process = true;
-			physics_process = true;
-		}
-	}
-	set_process_internal(process);
-	set_physics_process_internal(physics_process);
+	set_process_internal(_desired_process_internal);
+	set_physics_process_internal(_desired_physics_process_internal);
 }
 
 void Camera::_notification(int p_what) {
@@ -182,56 +142,21 @@ void Camera::_notification(int p_what) {
 				viewport->_camera_set(this);
 			}
 		} break;
-		case NOTIFICATION_INTERNAL_PROCESS: {
-			if (is_physics_interpolated_and_enabled() && camera.is_valid()) {
-				_physics_interpolation_ensure_transform_calculated();
-
-#ifdef VISUAL_SERVER_DEBUG_PHYSICS_INTERPOLATION
-				print_line("\t\tinterpolated Camera: " + rtos(_interpolation_data.xform_interpolated.origin.x) + "\t( prev " + rtos(_interpolation_data.xform_prev.origin.x) + ", curr " + rtos(_interpolation_data.xform_curr.origin.x) + " ) on tick " + itos(Engine::get_singleton()->get_physics_frames()));
-#endif
-
-				VisualServer::get_singleton()->camera_set_transform(camera, _interpolation_data.camera_xform_interpolated);
-			}
-		} break;
-		case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
-			if (is_physics_interpolated_and_enabled()) {
-				_physics_interpolation_ensure_data_flipped();
-				_interpolation_data.xform_curr = get_global_transform();
-			}
-		} break;
 		case NOTIFICATION_TRANSFORM_CHANGED: {
-			if (is_physics_interpolated_and_enabled()) {
-				_physics_interpolation_ensure_data_flipped();
-				_interpolation_data.xform_curr = get_global_transform();
 #if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+			if (is_physics_interpolated_and_enabled()) {
 				if (!Engine::get_singleton()->is_in_physics_frame()) {
 					PHYSICS_INTERPOLATION_NODE_WARNING(get_instance_id(), "Interpolated Camera triggered from outside physics process");
 				}
-#endif
 			}
+#endif
 			_request_camera_update();
 			if (doppler_tracking != DOPPLER_TRACKING_DISABLED) {
 				velocity_tracker->update_position(get_global_transform().origin);
 			}
-			// Allow auto-reset when first adding to the tree, as a convenience.
-			if (_is_physics_interpolation_reset_requested() && is_inside_tree()) {
-				_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
-				_set_physics_interpolation_reset_requested(false);
-			}
-
 		} break;
 		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
-			if (is_inside_tree()) {
-				_interpolation_data.xform_curr = get_global_transform();
-				_interpolation_data.xform_prev = _interpolation_data.xform_curr;
-				_update_process_mode();
-			}
-		} break;
-		case NOTIFICATION_PAUSED: {
-			if (is_physics_interpolated_and_enabled() && is_inside_tree() && is_visible_in_tree()) {
-				_physics_interpolation_ensure_transform_calculated(true);
-				VisualServer::get_singleton()->camera_set_transform(camera, _interpolation_data.camera_xform_interpolated);
-			}
+			_update_process_mode();
 		} break;
 		case NOTIFICATION_EXIT_WORLD: {
 			if (!get_tree()->is_node_being_edited(this)) {
@@ -274,8 +199,7 @@ Transform Camera::_get_adjusted_camera_transform(const Transform &p_xform) const
 
 Transform Camera::get_camera_transform() const {
 	if (is_physics_interpolated_and_enabled() && !Engine::get_singleton()->is_in_physics_frame()) {
-		_physics_interpolation_ensure_transform_calculated();
-		return _interpolation_data.camera_xform_interpolated;
+		return _get_adjusted_camera_transform(_get_cached_global_transform_interpolated());
 	}
 
 	return _get_adjusted_camera_transform(get_global_transform());
@@ -865,9 +789,16 @@ float ClippedCamera::get_margin() const {
 	return margin;
 }
 void ClippedCamera::set_process_mode(ProcessMode p_mode) {
-	if (is_physics_interpolated_and_enabled() && p_mode == CLIP_PROCESS_IDLE) {
-		p_mode = CLIP_PROCESS_PHYSICS;
-		WARN_PRINT_ONCE("[Physics interpolation] Forcing ClippedCamera to PROCESS_PHYSICS mode.");
+	if (is_physics_interpolated_and_enabled()) {
+		if (p_mode == CLIP_PROCESS_IDLE) {
+			p_mode = CLIP_PROCESS_PHYSICS;
+			WARN_PRINT_ONCE("[Physics interpolation] Forcing ClippedCamera to PROCESS_PHYSICS mode.");
+		}
+
+		process_mode = p_mode;
+
+		set_desired_process_modes(false, true);
+		return;
 	}
 
 	if (process_mode == p_mode) {
@@ -881,8 +812,11 @@ ClippedCamera::ProcessMode ClippedCamera::get_process_mode() const {
 	return process_mode;
 }
 
-void ClippedCamera::physics_interpolation_flip_data() {
+void ClippedCamera::fti_pump() {
 	_interpolation_data.clip_offset_prev = _interpolation_data.clip_offset_curr;
+
+	// Must call the base class.
+	Spatial::fti_pump();
 }
 
 void ClippedCamera::_physics_interpolated_changed() {
@@ -899,6 +833,11 @@ Transform ClippedCamera::_get_adjusted_camera_transform(const Transform &p_xform
 	return t;
 }
 
+void ClippedCamera::fti_update_servers() {
+	clip_offset = ((_interpolation_data.clip_offset_curr - _interpolation_data.clip_offset_prev) * Engine::get_singleton()->get_physics_interpolation_fraction()) + _interpolation_data.clip_offset_prev;
+	Camera::fti_update_servers();
+}
+
 void ClippedCamera::_notification(int p_what) {
 	if (p_what == NOTIFICATION_ENTER_TREE) {
 		// Switch process mode to physics if we are turning on interpolation.
@@ -966,10 +905,6 @@ void ClippedCamera::_notification(int p_what) {
 		_update_camera();
 	}
 
-	if (is_physics_interpolated_and_enabled() && (p_what == NOTIFICATION_INTERNAL_PROCESS)) {
-		clip_offset = ((_interpolation_data.clip_offset_curr - _interpolation_data.clip_offset_prev) * Engine::get_singleton()->get_physics_interpolation_fraction()) + _interpolation_data.clip_offset_prev;
-	}
-
 	if (p_what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED) {
 		update_gizmo();
 	}

+ 4 - 21
scene/3d/camera.h

@@ -91,26 +91,10 @@ private:
 	Ref<SpatialVelocityTracker> velocity_tracker;
 	bool affect_lod = true;
 
-	///////////////////////////////////////////////////////
-	// INTERPOLATION FUNCTIONS
-	void _physics_interpolation_ensure_transform_calculated(bool p_force = false) const;
-	void _physics_interpolation_ensure_data_flipped();
-
-	// These can be set by derived Cameras,
-	// if they wish to do processing (while still
-	// allowing physics interpolation to function).
+	// These can be set by derived Cameras.
 	bool _desired_process_internal = false;
 	bool _desired_physics_process_internal = false;
 
-	mutable struct InterpolationData {
-		Transform xform_curr;
-		Transform xform_prev;
-		Transform xform_interpolated;
-		Transform camera_xform_interpolated; // After modification according to camera type.
-		uint32_t last_update_physics_tick = 0;
-		uint32_t last_update_frame = UINT32_MAX;
-	} _interpolation_data;
-
 	void _update_process_mode();
 
 protected:
@@ -118,9 +102,6 @@ protected:
 	// This is because physics interpolation may need to request process modes additionally.
 	void set_desired_process_modes(bool p_process_internal, bool p_physics_process_internal);
 
-	// Opportunity for derived classes to interpolate extra attributes.
-	virtual void physics_interpolation_flip_data() {}
-
 	virtual void _physics_interpolated_changed();
 	virtual Transform _get_adjusted_camera_transform(const Transform &p_xform) const;
 	///////////////////////////////////////////////////////
@@ -128,6 +109,7 @@ protected:
 	void _update_camera();
 	virtual void _request_camera_update();
 	void _update_camera_mode();
+	virtual void fti_update_servers();
 
 	void _notification(int p_what);
 	virtual void _validate_property(PropertyInfo &p_property) const;
@@ -248,8 +230,9 @@ private:
 
 protected:
 	virtual Transform _get_adjusted_camera_transform(const Transform &p_xform) const;
-	virtual void physics_interpolation_flip_data();
+	virtual void fti_pump();
 	virtual void _physics_interpolated_changed();
+	virtual void fti_update_servers();
 	///////////////////////////////////////////////////////
 
 	void _notification(int p_what);

+ 59 - 5
scene/3d/spatial.cpp

@@ -118,7 +118,7 @@ void Spatial::_propagate_transform_changed(Spatial *p_origin) {
 #endif
 		get_tree()->xform_change_list.add(&xform_change);
 	}
-	data.dirty |= DIRTY_GLOBAL;
+	data.dirty |= DIRTY_GLOBAL | DIRTY_GLOBAL_INTERPOLATED;
 
 	data.children_lock--;
 }
@@ -174,13 +174,27 @@ void Spatial::_notification(int p_what) {
 				_propagate_merging_allowed(merging_allowed);
 			}
 
-			data.dirty |= DIRTY_GLOBAL; //global is always dirty upon entering a scene
+			data.dirty |= DIRTY_GLOBAL | DIRTY_GLOBAL_INTERPOLATED; //global is always dirty upon entering a scene
 			_notify_dirty();
 
 			notification(NOTIFICATION_ENTER_WORLD);
 
+			if (is_physics_interpolated_and_enabled()) {
+				// Always reset FTI when entering tree.
+				fti_pump();
+
+				// No need to interpolate as we are doing a reset.
+				data.global_transform_interpolated = get_global_transform();
+
+				// Make sure servers are up to date.
+				fti_update_servers();
+			}
 		} break;
 		case NOTIFICATION_EXIT_TREE: {
+			if (is_inside_tree()) {
+				get_tree()->get_scene_tree_fti().spatial_notify_delete(this);
+			}
+
 			notification(NOTIFICATION_EXIT_WORLD, true);
 			if (xform_change.in_list()) {
 				get_tree()->xform_change_list.remove(&xform_change);
@@ -252,6 +266,13 @@ void Spatial::_notification(int p_what) {
 			if (data.client_physics_interpolation_data) {
 				data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
 			}
+			data.local_transform_prev = data.local_transform;
+		} break;
+
+		case NOTIFICATION_PAUSED: {
+			if (is_physics_interpolated_and_enabled()) {
+				data.local_transform_prev = data.local_transform;
+			}
 		} break;
 
 		default: {
@@ -281,7 +302,22 @@ void Spatial::set_global_rotation(const Vector3 &p_euler_rad) {
 	set_global_transform(transform);
 }
 
+void Spatial::fti_pump() {
+	if (data.dirty & DIRTY_LOCAL) {
+		_update_local_transform();
+	}
+
+	data.local_transform_prev = data.local_transform;
+}
+
+void Spatial::fti_notify_node_changed() {
+	if (is_inside_tree()) {
+		get_tree()->get_scene_tree_fti().spatial_notify_set_transform(*this);
+	}
+}
+
 void Spatial::set_transform(const Transform &p_transform) {
+	fti_notify_node_changed();
 	data.local_transform = p_transform;
 	data.dirty |= DIRTY_VECTORS;
 	data.dirty &= ~DIRTY_LOCAL;
@@ -404,14 +440,19 @@ Transform Spatial::_get_global_transform_interpolated(real_t p_interpolation_fra
 }
 
 Transform Spatial::get_global_transform_interpolated() {
+#if 1
 	// 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()) {
-		return get_global_transform();
+	if (data.fti_global_xform_interp_set && is_physics_interpolated_and_enabled() && !Engine::get_singleton()->is_in_physics_frame() && is_visible_in_tree()) {
+		return data.global_transform_interpolated;
 	}
 
-	// If we are in the physics frame, the interpolated global transform is meaningless.
+	return get_global_transform();
+#else
+	// OLD METHOD - deprecated since moving to SceneTreeFTI,
+	// but leaving for reference and comparison for debugging.
+
 	// However, there is an exception, we may want to use this as a means of starting off the client
 	// interpolation pump if not already started (when _is_physics_interpolated_client_side() is false).
 	if (Engine::get_singleton()->is_in_physics_frame() && _is_physics_interpolated_client_side()) {
@@ -419,6 +460,7 @@ Transform Spatial::get_global_transform_interpolated() {
 	}
 
 	return _get_global_transform_interpolated(Engine::get_singleton()->get_physics_interpolation_fraction());
+#endif
 }
 
 Transform Spatial::get_global_transform() const {
@@ -484,6 +526,7 @@ Transform Spatial::get_relative_transform(const Node *p_parent) const {
 }
 
 void Spatial::set_translation(const Vector3 &p_translation) {
+	fti_notify_node_changed();
 	data.local_transform.origin = p_translation;
 	_change_notify("transform");
 	_propagate_transform_changed(this);
@@ -493,6 +536,7 @@ void Spatial::set_translation(const Vector3 &p_translation) {
 }
 
 void Spatial::set_rotation(const Vector3 &p_euler_rad) {
+	fti_notify_node_changed();
 	if (data.dirty & DIRTY_VECTORS) {
 		data.scale = data.local_transform.basis.get_scale();
 		data.dirty &= ~DIRTY_VECTORS;
@@ -512,6 +556,7 @@ void Spatial::set_rotation_degrees(const Vector3 &p_euler_deg) {
 }
 
 void Spatial::set_scale(const Vector3 &p_scale) {
+	fti_notify_node_changed();
 	if (data.dirty & DIRTY_VECTORS) {
 		data.rotation = data.local_transform.basis.get_rotation();
 		data.dirty &= ~DIRTY_VECTORS;
@@ -1061,6 +1106,11 @@ Spatial::Spatial() :
 	data.disable_scale = false;
 	data.vi_visible = true;
 	data.merging_allowed = true;
+
+	data.fti_on_frame_list = false;
+	data.fti_on_tick_list = false;
+	data.fti_global_xform_interp_set = false;
+
 	data.merging_mode = MERGING_MODE_INHERIT;
 
 	data.client_physics_interpolation_data = nullptr;
@@ -1077,4 +1127,8 @@ Spatial::Spatial() :
 
 Spatial::~Spatial() {
 	_disable_client_physics_interpolation();
+
+	if (is_inside_tree()) {
+		get_tree()->get_scene_tree_fti().spatial_notify_delete(this);
+	}
 }

+ 38 - 1
scene/3d/spatial.h

@@ -52,6 +52,8 @@ class Spatial : public Node {
 	GDCLASS(Spatial, Node);
 	OBJ_CATEGORY("3D");
 
+	friend class SceneTreeFTI;
+
 public:
 	enum MergingMode : unsigned int {
 		MERGING_MODE_INHERIT,
@@ -74,15 +76,27 @@ private:
 		DIRTY_NONE = 0,
 		DIRTY_VECTORS = 1,
 		DIRTY_LOCAL = 2,
-		DIRTY_GLOBAL = 4
+		DIRTY_GLOBAL = 4,
+		DIRTY_GLOBAL_INTERPOLATED = 8,
 	};
 
 	mutable SelfList<Node> xform_change;
 	SelfList<Spatial> _client_physics_interpolation_spatials_list;
 
 	struct Data {
+		// Interpolated global transform - correct on the frame only.
+		// Only used with FTI.
+		Transform global_transform_interpolated;
+
+		// Current xforms are either
+		// * Used for everything (when not using FTI)
+		// * Correct on the physics tick (when using FTI)
 		mutable Transform global_transform;
 		mutable Transform local_transform;
+
+		// Only used with FTI.
+		Transform local_transform_prev;
+
 		mutable Vector3 rotation;
 		mutable Vector3 scale;
 
@@ -108,6 +122,11 @@ private:
 		bool visible : 1;
 		bool disable_scale : 1;
 
+		// Scene tree interpolation
+		bool fti_on_frame_list : 1;
+		bool fti_on_tick_list : 1;
+		bool fti_global_xform_interp_set : 1;
+
 		bool merging_allowed : 1;
 
 		int children_lock;
@@ -139,9 +158,27 @@ protected:
 
 	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);
+	const Transform &_get_cached_global_transform_interpolated() const { return data.global_transform_interpolated; }
 	void _disable_client_physics_interpolation();
 
+	// Calling this announces to the FTI system that a node has been moved,
+	// or requires an update in terms of interpolation
+	// (e.g. changing Camera zoom even if position hasn't changed).
+	void fti_notify_node_changed();
+
+	// Opportunity after FTI to update the servers
+	// with global_transform_interpolated,
+	// and any custom interpolated data in derived classes.
+	// Make sure to call the parent class fti_update_servers(),
+	// so all data is updated to the servers.
+	virtual void fti_update_servers() {}
+
+	// Pump the FTI data, also gives a chance for inherited classes
+	// to pump custom data, but they *must* call the base class here too.
+	virtual void fti_pump();
+
 	void _notification(int p_what);
 	static void _bind_methods();
 

+ 3 - 1
scene/3d/vehicle_body.cpp

@@ -44,7 +44,7 @@ public:
 
 	real_t getDiagonal() const { return m_Adiag; }
 
-	btVehicleJacobianEntry(){};
+	btVehicleJacobianEntry() {}
 	//constraint between two different rigidbodies
 	btVehicleJacobianEntry(
 			const Basis &world2A,
@@ -366,6 +366,8 @@ VehicleWheel::VehicleWheel() {
 	m_raycastInfo.m_suspensionLength = 0.0;
 
 	body = nullptr;
+
+	set_physics_interpolation_mode(PHYSICS_INTERPOLATION_MODE_OFF);
 }
 
 void VehicleBody::_update_wheel_transform(VehicleWheel &wheel, PhysicsDirectBodyState *s) {

+ 20 - 25
scene/3d/visual_instance.cpp

@@ -82,6 +82,12 @@ void VisualInstance::set_instance_use_identity_transform(bool p_enable) {
 	}
 }
 
+void VisualInstance::fti_update_servers() {
+	if (!_is_using_identity_transform()) {
+		VisualServer::get_singleton()->instance_set_transform(get_instance(), _get_cached_global_transform_interpolated());
+	}
+}
+
 void VisualInstance::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_WORLD: {
@@ -97,33 +103,26 @@ void VisualInstance::_notification(int p_what) {
 
 		} break;
 		case NOTIFICATION_TRANSFORM_CHANGED: {
-			if (_is_vi_visible() || is_physics_interpolated_and_enabled()) {
+			if (_is_vi_visible()) {
 				if (!_is_using_identity_transform()) {
-					VisualServer::get_singleton()->instance_set_transform(instance, get_global_transform());
-
-					// For instance when first adding to the tree, when the previous transform is
-					// unset, to prevent streaking from the origin.
-					if (_is_physics_interpolation_reset_requested() && is_physics_interpolated_and_enabled() && is_inside_tree()) {
-						if (_is_vi_visible()) {
-							_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
+					// Physics interpolated VIs don't need to send their transform immediately after setting,
+					// indeed it is counterproductive, because the interpolated transform will be sent
+					// to the VisualServer immediately prior to rendering.
+					if (!is_physics_interpolated_and_enabled()) {
+						VisualServer::get_singleton()->instance_set_transform(instance, get_global_transform());
+					} else {
+						// For instance when first adding to the tree, when the previous transform is
+						// unset, to prevent streaking from the origin.
+						if (_is_physics_interpolation_reset_requested() && is_inside_tree()) {
+							if (_is_vi_visible()) {
+								_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
+							}
+							_set_physics_interpolation_reset_requested(false);
 						}
-						_set_physics_interpolation_reset_requested(false);
 					}
 				}
 			}
 		} break;
-		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
-			if (_is_vi_visible() && is_physics_interpolated() && is_inside_tree()) {
-				// We must ensure the VisualServer transform is up to date before resetting.
-				// This is because NOTIFICATION_TRANSFORM_CHANGED is deferred,
-				// and cannot be relied to be called in order before NOTIFICATION_RESET_PHYSICS_INTERPOLATION.
-				if (!_is_using_identity_transform()) {
-					VisualServer::get_singleton()->instance_set_transform(instance, get_global_transform());
-				}
-
-				VisualServer::get_singleton()->instance_reset_physics_interpolation(instance);
-			}
-		} break;
 		case NOTIFICATION_EXIT_WORLD: {
 			VisualServer::get_singleton()->instance_set_scenario(instance, RID());
 			VisualServer::get_singleton()->instance_attach_skeleton(instance, RID());
@@ -140,10 +139,6 @@ void VisualInstance::_notification(int p_what) {
 	}
 }
 
-void VisualInstance::_physics_interpolated_changed() {
-	VisualServer::get_singleton()->instance_set_interpolated(instance, is_physics_interpolated());
-}
-
 RID VisualInstance::get_instance() const {
 	return instance;
 }

+ 1 - 1
scene/3d/visual_instance.h

@@ -51,8 +51,8 @@ class VisualInstance : public CullInstance {
 protected:
 	void _update_visibility();
 	virtual void _refresh_portal_mode();
-	virtual void _physics_interpolated_changed();
 	void set_instance_use_identity_transform(bool p_enable);
+	virtual void fti_update_servers();
 
 	void _notification(int p_what);
 	static void _bind_methods();

+ 1 - 1
scene/animation/skeleton_ik.cpp

@@ -276,7 +276,7 @@ void FabrikInverseKinematic::solve(Task *p_task, real_t blending_delta, bool ove
 	p_task->skeleton->set_bone_global_pose_override(p_task->chain.chain_root.bone, Transform(), 0.0, false);
 	Vector3 origin_pos = p_task->skeleton->get_bone_global_pose(p_task->chain.chain_root.bone).origin;
 
-	make_goal(p_task, p_task->skeleton->get_global_transform().affine_inverse(), blending_delta);
+	make_goal(p_task, p_task->skeleton->get_global_transform_interpolated().affine_inverse(), blending_delta);
 
 	if (p_use_magnet && p_task->chain.middle_chain_item) {
 		p_task->chain.magnet_position = p_task->chain.middle_chain_item->initial_transform.origin.linear_interpolate(p_magnet_position, blending_delta);

+ 19 - 0
scene/main/scene_tree.cpp

@@ -538,6 +538,8 @@ void SceneTree::set_physics_interpolation_enabled(bool p_enabled) {
 	_physics_interpolation_enabled = p_enabled;
 	VisualServer::get_singleton()->set_physics_interpolation_enabled(p_enabled);
 
+	get_scene_tree_fti().set_enabled(get_root(), p_enabled);
+
 	// Perform an auto reset on the root node for convenience for the user.
 	if (root) {
 		root->reset_physics_interpolation();
@@ -563,6 +565,7 @@ void SceneTree::iteration_prepare() {
 		// Make sure any pending transforms from the last tick / frame
 		// are flushed before pumping the interpolation prev and currents.
 		flush_transform_notifications();
+		get_scene_tree_fti().tick_update();
 		VisualServer::get_singleton()->tick();
 	}
 }
@@ -628,6 +631,17 @@ bool SceneTree::idle(float p_time) {
 	//print_line("node count: "+itos(get_node_count()));
 	//print_line("TEXTURE RAM: "+itos(VS::get_singleton()->get_render_info(VS::INFO_TEXTURE_MEM_USED)));
 
+	// First pass of scene tree fixed timestep interpolation.
+	if (get_scene_tree_fti().is_enabled()) {
+		// Special, we need to ensure RenderingServer is up to date
+		// with *all* the pending xforms *before* updating it during
+		// the FTI update.
+		// If this is not done, we can end up with a deferred `set_transform()`
+		// overwriting the interpolated xform in the server.
+		flush_transform_notifications();
+		get_scene_tree_fti().frame_update(get_root(), true);
+	}
+
 	root_lock++;
 
 	if (MainLoop::idle(p_time)) {
@@ -733,6 +747,11 @@ bool SceneTree::idle(float p_time) {
 
 #endif
 
+	// Second pass of scene tree fixed timestep interpolation.
+	// ToDo: Possibly needs another flush_transform_notifications here
+	// depending on whether there are side effects to _call_idle_callbacks().
+	get_scene_tree_fti().frame_update(get_root(), false);
+
 	VisualServer::get_singleton()->pre_draw(true);
 
 	return _quit;

+ 4 - 0
scene/main/scene_tree.h

@@ -35,6 +35,7 @@
 #include "core/os/main_loop.h"
 #include "core/os/thread_safe.h"
 #include "core/self_list.h"
+#include "scene/main/scene_tree_fti.h"
 #include "scene/resources/mesh.h"
 #include "scene/resources/world.h"
 #include "scene/resources/world_2d.h"
@@ -160,6 +161,7 @@ private:
 	StretchAspect stretch_aspect;
 	Size2i stretch_min;
 	real_t stretch_scale;
+	SceneTreeFTI scene_tree_fti;
 
 	void _update_font_oversampling(float p_ratio);
 	void _update_root_rect();
@@ -438,6 +440,8 @@ public:
 	void client_physics_interpolation_add_spatial(SelfList<Spatial> *p_elem);
 	void client_physics_interpolation_remove_spatial(SelfList<Spatial> *p_elem);
 
+	SceneTreeFTI &get_scene_tree_fti() { return scene_tree_fti; }
+
 	static void add_idle_callback(IdleCallback p_callback);
 	SceneTree();
 	~SceneTree();

+ 288 - 0
scene/main/scene_tree_fti.cpp

@@ -0,0 +1,288 @@
+/**************************************************************************/
+/*  scene_tree_fti.cpp                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#ifndef _3D_DISABLED
+
+#include "scene_tree_fti.h"
+#include "core/engine.h"
+#include "core/error_macros.h"
+#include "core/math/transform_interpolator.h"
+#include "core/os/os.h"
+#include "scene/3d/spatial.h"
+#include "scene/3d/visual_instance.h"
+
+void SceneTreeFTI::_reset_flags(Node *p_node) {
+	Spatial *s = Object::cast_to<Spatial>(p_node);
+
+	if (s) {
+		s->data.fti_on_frame_list = false;
+		s->data.fti_on_tick_list = false;
+
+		// In most cases the later  NOTIFICATION_RESET_PHYSICS_INTERPOLATION
+		// will reset this, but this should help cover hidden nodes.
+		s->data.local_transform_prev = s->data.local_transform;
+	}
+
+	for (int n = 0; n < p_node->get_child_count(); n++) {
+		_reset_flags(p_node->get_child(n));
+	}
+}
+
+void SceneTreeFTI::set_enabled(Node *p_root, bool p_enabled) {
+	if (data.enabled == p_enabled) {
+		return;
+	}
+
+	data.spatial_tick_list[0].clear();
+	data.spatial_tick_list[1].clear();
+
+	// Spatial flags must be reset.
+	if (p_root) {
+		_reset_flags(p_root);
+	}
+
+	data.enabled = p_enabled;
+}
+
+void SceneTreeFTI::tick_update() {
+	if (!data.enabled) {
+		return;
+	}
+
+	uint32_t curr_mirror = data.mirror;
+	uint32_t prev_mirror = curr_mirror ? 0 : 1;
+
+	LocalVector<Spatial *> &curr = data.spatial_tick_list[curr_mirror];
+	LocalVector<Spatial *> &prev = data.spatial_tick_list[prev_mirror];
+
+	// First detect on the previous list but not on this tick list.
+	for (uint32_t n = 0; n < prev.size(); n++) {
+		Spatial *s = prev[n];
+		if (!s->data.fti_on_tick_list) {
+			// Needs a reset so jittering will stop.
+			s->fti_pump();
+
+			// This may not get updated so set it to the same as global xform.
+			// TODO: double check this is the best value.
+			s->data.global_transform_interpolated = s->get_global_transform();
+
+			// Remove from interpolation list.
+			if (s->data.fti_on_frame_list) {
+				s->data.fti_on_frame_list = false;
+			}
+		}
+	}
+
+	// Now pump all on the current list.
+	for (uint32_t n = 0; n < curr.size(); n++) {
+		Spatial *s = curr[n];
+
+		// Reset, needs to be marked each tick.
+		s->data.fti_on_tick_list = false;
+
+		// Pump.
+		s->fti_pump();
+	}
+
+	// Clear previous list and flip.
+	prev.clear();
+	data.mirror = prev_mirror;
+}
+
+void SceneTreeFTI::_spatial_notify_set_transform(Spatial &r_spatial) {
+	// This may be checked by the calling routine already,
+	// but needs to be double checked for custom SceneTrees.
+	if (!data.enabled || !r_spatial.is_physics_interpolated()) {
+		return;
+	}
+
+	if (!r_spatial.data.fti_on_tick_list) {
+		r_spatial.data.fti_on_tick_list = true;
+		data.spatial_tick_list[data.mirror].push_back(&r_spatial);
+	}
+
+	if (!r_spatial.data.fti_on_frame_list) {
+		r_spatial.data.fti_on_frame_list = true;
+	}
+}
+
+void SceneTreeFTI::spatial_notify_delete(Spatial *p_spatial) {
+	if (!data.enabled) {
+		return;
+	}
+
+	if (p_spatial->data.fti_on_frame_list) {
+		p_spatial->data.fti_on_frame_list = false;
+	}
+
+	// This can potentially be optimized for large scenes with large churn,
+	// as it will be doing a linear search through the lists.
+	data.spatial_tick_list[0].erase_unordered(p_spatial);
+	data.spatial_tick_list[1].erase_unordered(p_spatial);
+}
+
+void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform, int p_depth) {
+	Spatial *s = Object::cast_to<Spatial>(p_node);
+
+	// Don't recurse into hidden branches.
+	if (s && !s->is_visible()) {
+		// NOTE : If we change from recursing entire tree, we should do an is_visible_in_tree()
+		// check for the first of the branch.
+		return;
+	}
+
+	// Not a Spatial.
+	// Could be e.g. a viewport or something
+	// so we should still recurse to children.
+	if (!s) {
+		for (int n = 0; n < p_node->get_child_count(); n++) {
+			_update_dirty_spatials(p_node->get_child(n), p_current_frame, p_interpolation_fraction, p_active, nullptr, p_depth + 1);
+		}
+		return;
+	}
+
+	// Start the active interpolation chain from here onwards
+	// as we recurse further into the SceneTree.
+	// Once we hit an active (interpolated) node, we have to fully
+	// process all ancestors because their xform will also change.
+	// Anything not moving (inactive) higher in the tree need not be processed.
+	if (!p_active) {
+		if (data.frame_start) {
+			// On the frame start, activate whenever we hit something that requests interpolation.
+			if (s->data.fti_on_frame_list) {
+				p_active = true;
+			}
+		} else {
+			// On the frame end, we want to re-interpolate *anything* that has moved
+			// since the frame start.
+			if (s->data.dirty & Spatial::DIRTY_GLOBAL_INTERPOLATED) {
+				p_active = true;
+			}
+		}
+	}
+
+	if (data.frame_start) {
+		// Mark on the Spatial whether we have set global_transform_interp.
+		// This can later be used when calling `get_global_transform_interpolated()`
+		// to know which xform to return.
+		s->data.fti_global_xform_interp_set = p_active;
+	}
+
+	if (p_active) {
+#if 0
+		bool dirty = s->data.dirty & Spatial::DIRTY_GLOBAL_INTERP;
+
+		if (data.debug) {
+			String sz;
+			for (int n = 0; n < p_depth; n++) {
+				sz += "\t";
+			}
+			print_line(sz + p_node->get_name() + (dirty ? " DIRTY" : ""));
+		}
+#endif
+
+		// First calculate our local xform.
+		// This will either use interpolation, or just use the current local if not interpolated.
+		Transform local_interp;
+		if (s->is_physics_interpolated()) {
+			TransformInterpolator::interpolate_transform(s->data.local_transform_prev, s->data.local_transform, local_interp, p_interpolation_fraction);
+		} else {
+			local_interp = s->data.local_transform;
+		}
+
+		// Concatenate parent xform.
+		if (!s->is_set_as_toplevel()) {
+			if (p_parent_global_xform) {
+				s->data.global_transform_interpolated = (*p_parent_global_xform) * local_interp;
+			} else {
+				const Spatial *parent = s->get_parent_spatial();
+
+				if (parent) {
+					const Transform &parent_glob = parent->data.fti_global_xform_interp_set ? parent->data.global_transform_interpolated : parent->data.global_transform;
+					s->data.global_transform_interpolated = parent_glob * local_interp;
+				} else {
+					s->data.global_transform_interpolated = local_interp;
+				}
+			}
+		} else {
+			s->data.global_transform_interpolated = local_interp;
+		}
+
+		// Upload to VisualServer the interpolated global xform.
+		s->fti_update_servers();
+
+	} // if active.
+
+	// Remove the dirty interp flag from EVERYTHING as we go.
+	s->data.dirty &= ~Spatial::DIRTY_GLOBAL_INTERPOLATED;
+
+	// Recurse to children.
+	for (int n = 0; n < p_node->get_child_count(); n++) {
+		_update_dirty_spatials(p_node->get_child(n), p_current_frame, p_interpolation_fraction, p_active, s->data.fti_global_xform_interp_set ? &s->data.global_transform_interpolated : &s->data.global_transform, p_depth + 1);
+	}
+}
+
+void SceneTreeFTI::frame_update(Node *p_root, bool p_frame_start) {
+	if (!data.enabled || !p_root) {
+		return;
+	}
+
+	data.frame_start = p_frame_start;
+
+	float f = Engine::get_singleton()->get_physics_interpolation_fraction();
+	uint32_t frame = Engine::get_singleton()->get_frames_drawn();
+
+// #define SCENE_TREE_FTI_TAKE_TIMINGS
+#ifdef SCENE_TREE_FTI_TAKE_TIMINGS
+	uint64_t before = OS::get_singleton()->get_ticks_usec();
+#endif
+
+	if (data.debug) {
+		print_line(String("\nScene: ") + (data.frame_start ? "start" : "end") + "\n");
+	}
+
+	// Probably not the most optimal approach as we traverse the entire SceneTree
+	// but simple and foolproof.
+	// Can be optimized later.
+	_update_dirty_spatials(p_root, frame, f, false);
+
+	if (!p_frame_start && data.debug) {
+		data.debug = false;
+	}
+
+#ifdef SCENE_TREE_FTI_TAKE_TIMINGS
+	uint64_t after = OS::get_singleton()->get_ticks_usec();
+	if ((Engine::get_singleton()->get_frames_drawn() % 60) == 0) {
+		print_line("Took " + itos(after - before) + " usec " + (data.frame_start ? "start" : "end"));
+	}
+#endif
+}
+
+#endif // ndef _3D_DISABLED

+ 112 - 0
scene/main/scene_tree_fti.h

@@ -0,0 +1,112 @@
+/**************************************************************************/
+/*  scene_tree_fti.h                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#ifndef SCENE_TREE_FTI_H
+#define SCENE_TREE_FTI_H
+
+#include <core/local_vector.h>
+#include <core/os/mutex.h>
+
+class Spatial;
+class Node;
+class Transform;
+
+#ifdef _3D_DISABLED
+// Stubs
+class SceneTreeFTI {
+public:
+	void frame_update(Node *p_root, bool p_frame_start) {}
+	void tick_update() {}
+	void set_enabled(Node *p_root, bool p_enabled) {}
+	bool is_enabled() const { return false; }
+
+	void spatial_notify_set_transform(Spatial &r_spatial) {}
+	void spatial_notify_delete(Spatial *p_spatial) {}
+};
+#else
+
+// Important.
+// This class uses raw pointers, so it is essential that on deletion, this class is notified
+// so that any references can be cleared up to prevent dangling pointer access.
+
+// This class can be used from a custom SceneTree.
+
+// Note we could potentially make SceneTreeFTI static / global to avoid the lookup through scene tree,
+// but this covers the custom case of multiple scene trees.
+
+// This class is not thread safe, but can be made thread safe easily with a mutex as in the 4.x version.
+
+class SceneTreeFTI {
+	struct Data {
+		// Prev / Curr lists of spatials having local xforms pumped.
+		LocalVector<Spatial *> spatial_tick_list[2];
+		uint32_t mirror = 0;
+
+		bool enabled = false;
+
+		// Whether we are in physics ticks, or in a frame.
+		bool in_frame = false;
+
+		// Updating at the start of the frame, or the end on second pass.
+		bool frame_start = true;
+
+		bool debug = false;
+	} data;
+
+	void _update_dirty_spatials(Node *p_node, uint32_t p_current_frame, float p_interpolation_fraction, bool p_active, const Transform *p_parent_global_xform = nullptr, int p_depth = 0);
+	void _reset_flags(Node *p_node);
+	void _spatial_notify_set_transform(Spatial &r_spatial);
+
+public:
+	// Hottest function, allow inlining the data.enabled check.
+	void spatial_notify_set_transform(Spatial &r_spatial) {
+		if (!data.enabled) {
+			return;
+		}
+		_spatial_notify_set_transform(r_spatial);
+	}
+
+	void spatial_notify_delete(Spatial *p_spatial);
+
+	// Calculate interpolated xforms, send to visual server.
+	void frame_update(Node *p_root, bool p_frame_start);
+
+	// Update local xform pumps.
+	void tick_update();
+
+	void set_enabled(Node *p_root, bool p_enabled);
+	bool is_enabled() const { return data.enabled; }
+
+	void set_debug_next_frame() { data.debug = true; }
+};
+
+#endif // ndef _3D_DISABLED
+
+#endif // SCENE_TREE_FTI_H

+ 0 - 24
servers/visual/rasterizer.h

@@ -92,16 +92,8 @@ public:
 		RID material_override;
 		RID material_overlay;
 
-		// This is the main transform to be drawn with ..
-		// This will either be the interpolated transform (when using fixed timestep interpolation)
-		// or the ONLY transform (when not using FTI).
 		Transform transform;
 
-		// for interpolation we store the current transform (this physics tick)
-		// and the transform in the previous tick
-		Transform transform_curr;
-		Transform transform_prev;
-
 		int depth_layer;
 		uint32_t layer_mask;
 
@@ -123,16 +115,6 @@ public:
 		bool baked_light : 1; //this flag is only to know if it actually did use baked light
 		bool redraw_if_visible : 1;
 
-		bool on_interpolate_list : 1;
-		bool on_interpolate_transform_list : 1;
-		bool interpolated : 1;
-		TransformInterpolator::Method interpolation_method : 3;
-
-		// For fixed timestep interpolation.
-		// Note 32 bits is plenty for checksum, no need for real_t
-		float transform_checksum_curr;
-		float transform_checksum_prev;
-
 		float depth; //used for sorting
 
 		SelfList<InstanceBase> dependency_item;
@@ -158,12 +140,6 @@ public:
 			lightmap_capture = nullptr;
 			lightmap_slice = -1;
 			lightmap_uv_rect = Rect2(0, 0, 1, 1);
-			on_interpolate_list = false;
-			on_interpolate_transform_list = false;
-			interpolated = true;
-			interpolation_method = TransformInterpolator::INTERP_LERP;
-			transform_checksum_curr = 0.0;
-			transform_checksum_prev = 0.0;
 		}
 	};
 

+ 0 - 2
servers/visual/visual_server_raster.h

@@ -573,8 +573,6 @@ public:
 	BIND2(instance_set_layer_mask, RID, uint32_t)
 	BIND3(instance_set_pivot_data, RID, float, bool)
 	BIND2(instance_set_transform, RID, const Transform &)
-	BIND2(instance_set_interpolated, RID, bool)
-	BIND1(instance_reset_physics_interpolation, RID)
 	BIND2(instance_attach_object_instance_id, RID, ObjectID)
 	BIND3(instance_set_blend_shape_weight, RID, int, float)
 	BIND3(instance_set_surface_material, RID, int, RID)

+ 3 - 213
servers/visual/visual_server_scene.cpp

@@ -673,9 +673,6 @@ void VisualServerScene::instance_set_scenario(RID p_instance, RID p_scenario) {
 			_instance_destroy_occlusion_rep(instance);
 		}
 
-		// remove any interpolation data associated with the instance in this scenario
-		_interpolation_data.notify_free_instance(p_instance, *instance);
-
 		switch (instance->base_type) {
 			case VS::INSTANCE_LIGHT: {
 				InstanceLightData *light = static_cast<InstanceLightData *>(instance->base_data);
@@ -765,27 +762,6 @@ void VisualServerScene::instance_set_pivot_data(RID p_instance, float p_sorting_
 	instance->use_aabb_center = p_use_aabb_center;
 }
 
-void VisualServerScene::instance_reset_physics_interpolation(RID p_instance) {
-	Instance *instance = instance_owner.get(p_instance);
-	ERR_FAIL_COND(!instance);
-
-	if (_interpolation_data.interpolation_enabled && instance->interpolated) {
-		instance->transform_prev = instance->transform_curr;
-		instance->transform_checksum_prev = instance->transform_checksum_curr;
-
-#ifdef VISUAL_SERVER_DEBUG_PHYSICS_INTERPOLATION
-		print_line("instance_reset_physics_interpolation .. tick " + itos(Engine::get_singleton()->get_physics_frames()));
-		print_line("\tprev " + rtos(instance->transform_prev.origin.x) + ", curr " + rtos(instance->transform_curr.origin.x));
-#endif
-	}
-}
-
-void VisualServerScene::instance_set_interpolated(RID p_instance, bool p_interpolated) {
-	Instance *instance = instance_owner.get(p_instance);
-	ERR_FAIL_COND(!instance);
-	instance->interpolated = p_interpolated;
-}
-
 void VisualServerScene::instance_set_transform(RID p_instance, const Transform &p_transform) {
 	Instance *instance = instance_owner.get(p_instance);
 	ERR_FAIL_COND(!instance);
@@ -794,47 +770,8 @@ void VisualServerScene::instance_set_transform(RID p_instance, const Transform &
 	print_line("instance_set_transform " + rtos(p_transform.origin.x) + " .. tick " + itos(Engine::get_singleton()->get_physics_frames()));
 #endif
 
-	if (!(_interpolation_data.interpolation_enabled && instance->interpolated) || !instance->scenario) {
-		if (instance->transform == p_transform) {
-			return; //must be checked to avoid worst evil
-		}
-
-#ifdef DEBUG_ENABLED
-
-		for (int i = 0; i < 4; i++) {
-			const Vector3 &v = i < 3 ? p_transform.basis.elements[i] : p_transform.origin;
-			ERR_FAIL_COND(Math::is_inf(v.x));
-			ERR_FAIL_COND(Math::is_nan(v.x));
-			ERR_FAIL_COND(Math::is_inf(v.y));
-			ERR_FAIL_COND(Math::is_nan(v.y));
-			ERR_FAIL_COND(Math::is_inf(v.z));
-			ERR_FAIL_COND(Math::is_nan(v.z));
-		}
-
-#endif
-		instance->transform = p_transform;
-		_instance_queue_update(instance, true);
-
-#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
-		if ((_interpolation_data.interpolation_enabled && !instance->interpolated) && (Engine::get_singleton()->is_in_physics_frame())) {
-			PHYSICS_INTERPOLATION_NODE_WARNING(instance->object_id, "Non-interpolated triggered from physics process");
-		}
-#endif
-
-		return;
-	}
-
-	float new_checksum = TransformInterpolator::checksum_transform(p_transform);
-	bool checksums_match = (instance->transform_checksum_curr == new_checksum) && (instance->transform_checksum_prev == new_checksum);
-
-	// we can't entirely reject no changes because we need the interpolation
-	// system to keep on stewing
-
-	// Optimized check. First checks the checksums. If they pass it does the slow check at the end.
-	// Alternatively we can do this non-optimized and ignore the checksum...
-	// if no change
-	if (checksums_match && (instance->transform_curr == p_transform) && (instance->transform_prev == p_transform)) {
-		return;
+	if (instance->transform == p_transform) {
+		return; //must be checked to avoid worst evil
 	}
 
 #ifdef DEBUG_ENABLED
@@ -850,62 +787,8 @@ void VisualServerScene::instance_set_transform(RID p_instance, const Transform &
 	}
 
 #endif
-
-	instance->transform_curr = p_transform;
-
-#ifdef VISUAL_SERVER_DEBUG_PHYSICS_INTERPOLATION
-	print_line("\tprev " + rtos(instance->transform_prev.origin.x) + ", curr " + rtos(instance->transform_curr.origin.x));
-#endif
-
-	// keep checksums up to date
-	instance->transform_checksum_curr = new_checksum;
-
-	if (!instance->on_interpolate_transform_list) {
-		_interpolation_data.instance_transform_update_list_curr->push_back(p_instance);
-		instance->on_interpolate_transform_list = true;
-	} else {
-		DEV_ASSERT(_interpolation_data.instance_transform_update_list_curr->size());
-	}
-
-	// If the instance is invisible, then we are simply updating the data flow, there is no need to calculate the interpolated
-	// transform or anything else.
-	// Ideally we would not even call the VisualServer::set_transform() when invisible but that would entail having logic
-	// to keep track of the previous transform on the SceneTree side. The "early out" below is less efficient but a lot cleaner codewise.
-	if (!instance->visible) {
-		return;
-	}
-
-	// decide on the interpolation method .. slerp if possible
-	instance->interpolation_method = TransformInterpolator::find_method(instance->transform_prev.basis, instance->transform_curr.basis);
-
-	if (!instance->on_interpolate_list) {
-		_interpolation_data.instance_interpolate_update_list.push_back(p_instance);
-		instance->on_interpolate_list = true;
-	} else {
-		DEV_ASSERT(_interpolation_data.instance_interpolate_update_list.size());
-	}
-
+	instance->transform = p_transform;
 	_instance_queue_update(instance, true);
-
-#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
-	if (!Engine::get_singleton()->is_in_physics_frame()) {
-		PHYSICS_INTERPOLATION_NODE_WARNING(instance->object_id, "Interpolated triggered from outside physics process");
-	}
-#endif
-}
-
-void VisualServerScene::InterpolationData::notify_free_instance(RID p_rid, Instance &r_instance) {
-	r_instance.on_interpolate_list = false;
-	r_instance.on_interpolate_transform_list = false;
-
-	if (!interpolation_enabled) {
-		return;
-	}
-
-	// if the instance was on any of the lists, remove
-	instance_interpolate_update_list.erase_multiple_unordered(p_rid);
-	instance_transform_update_list_curr->erase_multiple_unordered(p_rid);
-	instance_transform_update_list_prev->erase_multiple_unordered(p_rid);
 }
 
 void VisualServerScene::update_interpolation_tick(bool p_process) {
@@ -915,84 +798,11 @@ void VisualServerScene::update_interpolation_tick(bool p_process) {
 
 	// update interpolation in storage
 	VSG::storage->update_interpolation_tick(p_process);
-
-	// detect any that were on the previous transform list that are no longer active,
-	// we should remove them from the interpolate list
-
-	for (unsigned int n = 0; n < _interpolation_data.instance_transform_update_list_prev->size(); n++) {
-		const RID &rid = (*_interpolation_data.instance_transform_update_list_prev)[n];
-		Instance *instance = instance_owner.getornull(rid);
-
-		bool active = true;
-
-		// no longer active? (either the instance deleted or no longer being transformed)
-		if (instance && !instance->on_interpolate_transform_list) {
-			active = false;
-			instance->on_interpolate_list = false;
-
-			// make sure the most recent transform is set
-			instance->transform = instance->transform_curr;
-
-			// and that both prev and current are the same, just in case of any interpolations
-			instance->transform_prev = instance->transform_curr;
-
-			// make sure are updated one more time to ensure the AABBs are correct
-			_instance_queue_update(instance, true);
-		}
-
-		if (!instance) {
-			active = false;
-		}
-
-		if (!active) {
-			_interpolation_data.instance_interpolate_update_list.erase(rid);
-		}
-	}
-
-	// and now for any in the transform list (being actively interpolated), keep the previous transform
-	// value up to date ready for the next tick
-	if (p_process) {
-		for (unsigned int n = 0; n < _interpolation_data.instance_transform_update_list_curr->size(); n++) {
-			const RID &rid = (*_interpolation_data.instance_transform_update_list_curr)[n];
-			Instance *instance = instance_owner.getornull(rid);
-			if (instance) {
-				instance->transform_prev = instance->transform_curr;
-				instance->transform_checksum_prev = instance->transform_checksum_curr;
-				instance->on_interpolate_transform_list = false;
-			}
-		}
-	}
-
-	// we maintain a mirror list for the transform updates, so we can detect when an instance
-	// is no longer being transformed, and remove it from the interpolate list
-	SWAP(_interpolation_data.instance_transform_update_list_curr, _interpolation_data.instance_transform_update_list_prev);
-
-	// prepare for the next iteration
-	_interpolation_data.instance_transform_update_list_curr->clear();
 }
 
 void VisualServerScene::update_interpolation_frame(bool p_process) {
 	// update interpolation in storage
 	VSG::storage->update_interpolation_frame(p_process);
-
-	if (p_process) {
-		real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
-
-		for (unsigned int i = 0; i < _interpolation_data.instance_interpolate_update_list.size(); i++) {
-			const RID &rid = _interpolation_data.instance_interpolate_update_list[i];
-			Instance *instance = instance_owner.getornull(rid);
-			if (instance) {
-				TransformInterpolator::interpolate_transform_via_method(instance->transform_prev, instance->transform_curr, instance->transform, f, instance->interpolation_method);
-
-#ifdef VISUAL_SERVER_DEBUG_PHYSICS_INTERPOLATION
-				print_line("\t\tinterpolated: " + rtos(instance->transform.origin.x) + "\t( prev " + rtos(instance->transform_prev.origin.x) + ", curr " + rtos(instance->transform_curr.origin.x) + " ) on tick " + itos(Engine::get_singleton()->get_physics_frames()));
-#endif
-
-				// make sure AABBs are constantly up to date through the interpolation
-				_instance_queue_update(instance, true);
-			}
-		} // for n
-	}
 }
 
 void VisualServerScene::instance_attach_object_instance_id(RID p_instance, ObjectID p_id) {
@@ -1046,25 +856,6 @@ void VisualServerScene::instance_set_visible(RID p_instance, bool p_visible) {
 
 	instance->visible = p_visible;
 
-	// Special case for physics interpolation, we want to ensure the interpolated data is up to date
-	if (_interpolation_data.interpolation_enabled && p_visible && instance->interpolated && instance->scenario && !instance->on_interpolate_list) {
-		// Do all the extra work we normally do on instance_set_transform(), because this is optimized out for hidden instances.
-		// This prevents a glitch of stale interpolation transform data when unhiding before the next physics tick.
-		instance->interpolation_method = TransformInterpolator::find_method(instance->transform_prev.basis, instance->transform_curr.basis);
-		_interpolation_data.instance_interpolate_update_list.push_back(p_instance);
-		instance->on_interpolate_list = true;
-		_instance_queue_update(instance, true);
-
-		// We must also place on the transform update list for a tick, so the system
-		// can auto-detect if the instance is no longer moving, and remove from the interpolate lists again.
-		// If this step is ignored, an unmoving instance could remain on the interpolate lists indefinitely
-		// (or rather until the object is deleted) and cause unnecessary updates and drawcalls.
-		if (!instance->on_interpolate_transform_list) {
-			_interpolation_data.instance_transform_update_list_curr->push_back(p_instance);
-			instance->on_interpolate_transform_list = true;
-		}
-	}
-
 	// give the opportunity for the spatial partitioning scene to use a special implementation of visibility
 	// for efficiency (supported in BVH but not octree)
 
@@ -4496,7 +4287,6 @@ bool VisualServerScene::free(RID p_rid) {
 		update_dirty_instances();
 
 		Instance *instance = instance_owner.get(p_rid);
-		_interpolation_data.notify_free_instance(p_rid, *instance);
 
 		instance_set_use_lightmap(p_rid, RID(), RID(), -1, Rect2(0, 0, 1, 1));
 		instance_set_scenario(p_rid, RID());

+ 0 - 8
servers/visual/visual_server_scene.h

@@ -397,12 +397,6 @@ public:
 	virtual void set_physics_interpolation_enabled(bool p_enabled);
 
 	struct InterpolationData {
-		void notify_free_instance(RID p_rid, Instance &r_instance);
-		LocalVector<RID> instance_interpolate_update_list;
-		LocalVector<RID> instance_transform_update_lists[2];
-		LocalVector<RID> *instance_transform_update_list_curr = &instance_transform_update_lists[0];
-		LocalVector<RID> *instance_transform_update_list_prev = &instance_transform_update_lists[1];
-
 		bool interpolation_enabled = false;
 	} _interpolation_data;
 
@@ -662,8 +656,6 @@ public:
 	virtual void instance_set_layer_mask(RID p_instance, uint32_t p_mask);
 	virtual void instance_set_pivot_data(RID p_instance, float p_sorting_offset, bool p_use_aabb_center);
 	virtual void instance_set_transform(RID p_instance, const Transform &p_transform);
-	virtual void instance_set_interpolated(RID p_instance, bool p_interpolated);
-	virtual void instance_reset_physics_interpolation(RID p_instance);
 	virtual void instance_attach_object_instance_id(RID p_instance, ObjectID p_id);
 	virtual void instance_set_blend_shape_weight(RID p_instance, int p_shape, float p_weight);
 	virtual void instance_set_surface_material(RID p_instance, int p_surface, RID p_material);

+ 0 - 2
servers/visual/visual_server_wrap_mt.h

@@ -481,8 +481,6 @@ public:
 	FUNC2(instance_set_layer_mask, RID, uint32_t)
 	FUNC3(instance_set_pivot_data, RID, float, bool)
 	FUNC2(instance_set_transform, RID, const Transform &)
-	FUNC2(instance_set_interpolated, RID, bool)
-	FUNC1(instance_reset_physics_interpolation, RID)
 	FUNC2(instance_attach_object_instance_id, RID, ObjectID)
 	FUNC3(instance_set_blend_shape_weight, RID, int, float)
 	FUNC3(instance_set_surface_material, RID, int, RID)

+ 0 - 2
servers/visual_server.cpp

@@ -2172,8 +2172,6 @@ void VisualServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("instance_set_scenario", "instance", "scenario"), &VisualServer::instance_set_scenario);
 	ClassDB::bind_method(D_METHOD("instance_set_layer_mask", "instance", "mask"), &VisualServer::instance_set_layer_mask);
 	ClassDB::bind_method(D_METHOD("instance_set_transform", "instance", "transform"), &VisualServer::instance_set_transform);
-	ClassDB::bind_method(D_METHOD("instance_set_interpolated", "instance", "interpolated"), &VisualServer::instance_set_interpolated);
-	ClassDB::bind_method(D_METHOD("instance_reset_physics_interpolation", "instance"), &VisualServer::instance_reset_physics_interpolation);
 	ClassDB::bind_method(D_METHOD("instance_attach_object_instance_id", "instance", "id"), &VisualServer::instance_attach_object_instance_id);
 	ClassDB::bind_method(D_METHOD("instance_set_blend_shape_weight", "instance", "shape", "weight"), &VisualServer::instance_set_blend_shape_weight);
 	ClassDB::bind_method(D_METHOD("instance_set_surface_material", "instance", "surface", "material"), &VisualServer::instance_set_surface_material);

+ 0 - 2
servers/visual_server.h

@@ -878,8 +878,6 @@ public:
 	virtual void instance_set_layer_mask(RID p_instance, uint32_t p_mask) = 0;
 	virtual void instance_set_pivot_data(RID p_instance, float p_sorting_offset, bool p_use_aabb_center) = 0;
 	virtual void instance_set_transform(RID p_instance, const Transform &p_transform) = 0;
-	virtual void instance_set_interpolated(RID p_instance, bool p_interpolated) = 0;
-	virtual void instance_reset_physics_interpolation(RID p_instance) = 0;
 	virtual void instance_attach_object_instance_id(RID p_instance, ObjectID p_id) = 0;
 	virtual void instance_set_blend_shape_weight(RID p_instance, int p_shape, float p_weight) = 0;
 	virtual void instance_set_surface_material(RID p_instance, int p_surface, RID p_material) = 0;