Browse Source

Physics Interpolation - Add InterpolatedProperty

And add some basic interpolated properties to Camera.
lawnjelly 4 months ago
parent
commit
18c01b21e5

+ 8 - 4
core/error_macros.h

@@ -552,10 +552,14 @@ void _physics_interpolation_warning(const char *p_function, const char *p_file,
 #endif
 
 #ifdef DEV_ENABLED
-#define DEV_CHECK_ONCE(m_cond)                                                   \
-	if (unlikely(!(m_cond))) {                                                   \
-		ERR_PRINT_ONCE("DEV_CHECK_ONCE failed  \"" _STR(m_cond) "\" is false."); \
-	} else                                                                       \
+#define DEV_CHECK_ONCE(m_cond)                                                                                           \
+	if (true) {                                                                                                          \
+		static bool first_print = true;                                                                                  \
+		if (first_print && unlikely(!(m_cond))) {                                                                        \
+			_err_print_error(FUNCTION_STR, __FILE__, __LINE__, "DEV_CHECK_ONCE failed  \"" _STR(m_cond) "\" is false."); \
+			first_print = false;                                                                                         \
+		}                                                                                                                \
+	} else                                                                                                               \
 		((void)0)
 #else
 #define DEV_CHECK_ONCE(m_cond)

+ 46 - 0
core/interpolated_property.cpp

@@ -0,0 +1,46 @@
+/**************************************************************************/
+/*  interpolated_property.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.                 */
+/**************************************************************************/
+
+#include "interpolated_property.h"
+
+#include "core/math/math_defs.h"
+#include "core/math/vector2.h"
+
+namespace InterpolatedPropertyFuncs {
+
+float lerp(float p_a, float p_b, float p_fraction) {
+	return Math::lerp(p_a, p_b, p_fraction);
+}
+
+Vector2 lerp(const Vector2 &p_a, const Vector2 &p_b, float p_fraction) {
+	return p_a.linear_interpolate(p_b, p_fraction);
+}
+
+} //namespace InterpolatedPropertyFuncs

+ 109 - 0
core/interpolated_property.h

@@ -0,0 +1,109 @@
+/**************************************************************************/
+/*  interpolated_property.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 INTERPOLATED_PROPERTY_H
+#define INTERPOLATED_PROPERTY_H
+
+struct Vector2;
+
+namespace InterpolatedPropertyFuncs {
+float lerp(float p_a, float p_b, float p_fraction);
+Vector2 lerp(const Vector2 &p_a, const Vector2 &p_b, float p_fraction);
+} //namespace InterpolatedPropertyFuncs
+
+// This class is intended to reduce the boiler plate involved to
+// support custom properties to be physics interpolated.
+
+template <class T>
+class InterpolatedProperty {
+	// Only needs interpolating / updating the servers when
+	// curr and prev are different.
+	bool _needs_interpolating = false;
+	T _interpolated;
+	T curr;
+	T prev;
+
+public:
+	// FTI depends on the constant flow between current values
+	// (on the current tick) and stored previous values (on the previous tick).
+	// These should be updated both on each tick, and also on resets.
+	void pump() {
+		prev = curr;
+		_needs_interpolating = false;
+	}
+	void reset() { pump(); }
+
+	void set_interpolated_value(const T &p_val) {
+		_interpolated = p_val;
+	}
+	const T &interpolated() const { return _interpolated; }
+	bool needs_interpolating() const { return _needs_interpolating; }
+
+	bool interpolate(float p_interpolation_fraction) {
+		if (_needs_interpolating) {
+			_interpolated = InterpolatedPropertyFuncs::lerp(prev, curr, p_interpolation_fraction);
+			return true;
+		}
+		return false;
+	}
+
+	operator T() const {
+		return curr;
+	}
+
+	bool operator==(const T &p_o) const {
+		return p_o == curr;
+	}
+
+	bool operator!=(const T &p_o) const {
+		return p_o != curr;
+	}
+
+	InterpolatedProperty &operator=(T p_val) {
+		curr = p_val;
+		_interpolated = p_val;
+		_needs_interpolating = true;
+		return *this;
+	}
+	InterpolatedProperty(T p_val) {
+		curr = p_val;
+		_interpolated = p_val;
+		pump();
+	}
+	InterpolatedProperty() {
+		// Ensure either the constructor is run,
+		// or the memory is zeroed if using a fundamental type.
+		_interpolated = T{};
+		curr = T{};
+		prev = T{};
+	}
+};
+
+#endif // INTERPOLATED_PROPERTY_H

+ 59 - 6
scene/3d/camera.cpp

@@ -45,11 +45,63 @@ void Camera::_request_camera_update() {
 	_update_camera();
 }
 
-void Camera::fti_update_servers() {
+void Camera::fti_pump_property() {
+	switch (mode) {
+		default:
+			break;
+		case PROJECTION_PERSPECTIVE: {
+			fov.pump();
+		} break;
+		case PROJECTION_ORTHOGONAL: {
+			size.pump();
+		} break;
+		case PROJECTION_FRUSTUM: {
+			size.pump();
+			frustum_offset.pump();
+		} break;
+	}
+	near.pump();
+	far.pump();
+
+	Spatial::fti_pump_property();
+}
+
+void Camera::fti_update_servers_property() {
+	if (camera.is_valid()) {
+		float f = Engine::get_singleton()->get_physics_interpolation_fraction();
+
+		switch (mode) {
+			default:
+				break;
+			case PROJECTION_PERSPECTIVE: {
+				// If there have been changes due to interpolation, update the servers.
+				if (fov.interpolate(f) || near.interpolate(f) || far.interpolate(f)) {
+					VisualServer::get_singleton()->camera_set_perspective(camera, fov.interpolated(), near.interpolated(), far.interpolated());
+				}
+			} break;
+			case PROJECTION_ORTHOGONAL: {
+				if (size.interpolate(f) || near.interpolate(f) || far.interpolate(f)) {
+					VisualServer::get_singleton()->camera_set_orthogonal(camera, size.interpolated(), near.interpolated(), far.interpolated());
+				}
+			} break;
+			case PROJECTION_FRUSTUM: {
+				if (size.interpolate(f) || frustum_offset.interpolate(f) || near.interpolate(f) || far.interpolate(f)) {
+					VisualServer::get_singleton()->camera_set_frustum(camera, size.interpolated(), frustum_offset.interpolated(), near.interpolated(), far.interpolated());
+				}
+			} break;
+		}
+	}
+
+	Spatial::fti_update_servers_property();
+}
+
+void Camera::fti_update_servers_xform() {
 	if (camera.is_valid()) {
 		Transform tr = _get_adjusted_camera_transform(_get_cached_global_transform_interpolated());
 		VisualServer::get_singleton()->camera_set_transform(camera, tr);
 	}
+
+	Spatial::fti_update_servers_xform();
 }
 
 void Camera::_update_camera_mode() {
@@ -57,7 +109,6 @@ void Camera::_update_camera_mode() {
 	switch (mode) {
 		case PROJECTION_PERSPECTIVE: {
 			set_perspective(fov, near, far);
-
 		} break;
 		case PROJECTION_ORTHOGONAL: {
 			set_orthogonal(size, near, far);
@@ -66,6 +117,8 @@ void Camera::_update_camera_mode() {
 			set_frustum(size, frustum_offset, near, far);
 		} break;
 	}
+
+	fti_notify_node_changed(false);
 }
 
 void Camera::_validate_property(PropertyInfo &p_property) const {
@@ -812,11 +865,11 @@ ClippedCamera::ProcessMode ClippedCamera::get_process_mode() const {
 	return process_mode;
 }
 
-void ClippedCamera::fti_pump() {
+void ClippedCamera::fti_pump_xform() {
 	_interpolation_data.clip_offset_prev = _interpolation_data.clip_offset_curr;
 
 	// Must call the base class.
-	Spatial::fti_pump();
+	Camera::fti_pump_xform();
 }
 
 void ClippedCamera::_physics_interpolated_changed() {
@@ -833,9 +886,9 @@ Transform ClippedCamera::_get_adjusted_camera_transform(const Transform &p_xform
 	return t;
 }
 
-void ClippedCamera::fti_update_servers() {
+void ClippedCamera::fti_update_servers_xform() {
 	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();
+	Camera::fti_update_servers_xform();
 }
 
 void ClippedCamera::_notification(int p_what) {

+ 13 - 7
scene/3d/camera.h

@@ -31,6 +31,7 @@
 #ifndef CAMERA_H
 #define CAMERA_H
 
+#include "core/interpolated_property.h"
 #include "scene/3d/spatial.h"
 #include "scene/3d/spatial_velocity_tracker.h"
 #include "scene/main/viewport.h"
@@ -66,10 +67,12 @@ private:
 
 	Projection mode;
 
-	float fov;
-	float size;
-	Vector2 frustum_offset;
-	float near, far;
+	InterpolatedProperty<float> fov;
+	InterpolatedProperty<float> near;
+	InterpolatedProperty<float> far;
+
+	InterpolatedProperty<float> size;
+	InterpolatedProperty<Vector2> frustum_offset;
 	float v_offset;
 	float h_offset;
 	KeepAspect keep_aspect;
@@ -109,7 +112,10 @@ protected:
 	void _update_camera();
 	virtual void _request_camera_update();
 	void _update_camera_mode();
-	virtual void fti_update_servers();
+
+	virtual void fti_pump_property();
+	virtual void fti_update_servers_property();
+	virtual void fti_update_servers_xform();
 
 	void _notification(int p_what);
 	virtual void _validate_property(PropertyInfo &p_property) const;
@@ -230,9 +236,9 @@ private:
 
 protected:
 	virtual Transform _get_adjusted_camera_transform(const Transform &p_xform) const;
-	virtual void fti_pump();
+	virtual void fti_pump_xform();
+	virtual void fti_update_servers_xform();
 	virtual void _physics_interpolated_changed();
-	virtual void fti_update_servers();
 	///////////////////////////////////////////////////////
 
 	void _notification(int p_what);

+ 17 - 8
scene/3d/spatial.cpp

@@ -181,13 +181,13 @@ void Spatial::_notification(int p_what) {
 
 			if (is_physics_interpolated_and_enabled()) {
 				// Always reset FTI when entering tree.
-				fti_pump();
+				fti_pump_xform();
 
 				// 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();
+				fti_update_servers_xform();
 			}
 		} break;
 		case NOTIFICATION_EXIT_TREE: {
@@ -266,7 +266,14 @@ 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;
+
+			// In most cases, nodes derived from Spatial will have to
+			// already have reset code available for SceneTreeFTI,
+			// so it makes sense for them to reuse this method
+			// rather than respond individually to NOTIFICATION_RESET_PHYSICS_INTERPOLATION,
+			// unless they need to perform specific tasks (like changing process modes).
+			fti_pump_xform();
+			fti_pump_property();
 		} break;
 
 		case NOTIFICATION_PAUSED: {
@@ -302,7 +309,7 @@ void Spatial::set_global_rotation(const Vector3 &p_euler_rad) {
 	set_global_transform(transform);
 }
 
-void Spatial::fti_pump() {
+void Spatial::fti_pump_xform() {
 	if (data.dirty & DIRTY_LOCAL) {
 		_update_local_transform();
 	}
@@ -310,9 +317,9 @@ void Spatial::fti_pump() {
 	data.local_transform_prev = data.local_transform;
 }
 
-void Spatial::fti_notify_node_changed() {
+void Spatial::fti_notify_node_changed(bool p_transform_changed) {
 	if (is_inside_tree()) {
-		get_tree()->get_scene_tree_fti().spatial_notify_set_transform(*this);
+		get_tree()->get_scene_tree_fti().spatial_notify_changed(*this, p_transform_changed);
 	}
 }
 
@@ -1107,8 +1114,10 @@ Spatial::Spatial() :
 	data.vi_visible = true;
 	data.merging_allowed = true;
 
-	data.fti_on_frame_list = false;
-	data.fti_on_tick_list = false;
+	data.fti_on_frame_xform_list = false;
+	data.fti_on_frame_property_list = false;
+	data.fti_on_tick_xform_list = false;
+	data.fti_on_tick_property_list = false;
 	data.fti_global_xform_interp_set = false;
 
 	data.merging_mode = MERGING_MODE_INHERIT;

+ 12 - 5
scene/3d/spatial.h

@@ -123,8 +123,10 @@ private:
 		bool disable_scale : 1;
 
 		// Scene tree interpolation
-		bool fti_on_frame_list : 1;
-		bool fti_on_tick_list : 1;
+		bool fti_on_frame_xform_list : 1;
+		bool fti_on_frame_property_list : 1;
+		bool fti_on_tick_xform_list : 1;
+		bool fti_on_tick_property_list : 1;
 		bool fti_global_xform_interp_set : 1;
 
 		bool merging_allowed : 1;
@@ -166,18 +168,23 @@ protected:
 	// 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();
+	void fti_notify_node_changed(bool p_transform_changed = true);
 
 	// 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() {}
+	virtual void fti_update_servers_xform() {}
+	virtual void fti_update_servers_property() {}
 
 	// 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();
+	// This is the opportunity for classes to move current values for
+	// transforms and properties to stored previous values,
+	// and this should take place both on ticks, and during resets.
+	virtual void fti_pump_xform();
+	virtual void fti_pump_property() {}
 
 	void _notification(int p_what);
 	static void _bind_methods();

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

@@ -82,7 +82,7 @@ void VisualInstance::set_instance_use_identity_transform(bool p_enable) {
 	}
 }
 
-void VisualInstance::fti_update_servers() {
+void VisualInstance::fti_update_servers_xform() {
 	if (!_is_using_identity_transform()) {
 		VisualServer::get_singleton()->instance_set_transform(get_instance(), _get_cached_global_transform_interpolated());
 	}

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

@@ -52,7 +52,7 @@ protected:
 	void _update_visibility();
 	virtual void _refresh_portal_mode();
 	void set_instance_use_identity_transform(bool p_enable);
-	virtual void fti_update_servers();
+	virtual void fti_update_servers_xform();
 
 	void _notification(int p_what);
 	static void _bind_methods();

+ 127 - 27
scene/main/scene_tree_fti.cpp

@@ -38,12 +38,16 @@
 #include "scene/3d/spatial.h"
 #include "scene/3d/visual_instance.h"
 
+// Uncomment this to enable some slow extra DEV_ENABLED
+// checks to ensure there aren't more than one object added to the lists.
+// #define GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+
 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;
+		s->data.fti_on_frame_xform_list = false;
+		s->data.fti_on_tick_xform_list = false;
 
 		// In most cases the later  NOTIFICATION_RESET_PHYSICS_INTERPOLATION
 		// will reset this, but this should help cover hidden nodes.
@@ -60,8 +64,8 @@ void SceneTreeFTI::set_enabled(Node *p_root, bool p_enabled) {
 		return;
 	}
 
-	data.spatial_tick_list[0].clear();
-	data.spatial_tick_list[1].clear();
+	data.tick_xform_list[0].clear();
+	data.tick_xform_list[1].clear();
 
 	// Spatial flags must be reset.
 	if (p_root) {
@@ -79,57 +83,125 @@ void SceneTreeFTI::tick_update() {
 	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];
+	LocalVector<Spatial *> &curr = data.tick_xform_list[curr_mirror];
+	LocalVector<Spatial *> &prev = data.tick_xform_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) {
+		if (!s->data.fti_on_tick_xform_list) {
 			// Needs a reset so jittering will stop.
-			s->fti_pump();
+			s->fti_pump_xform();
 
 			// 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;
+			if (s->data.fti_on_frame_xform_list) {
+				s->data.fti_on_frame_xform_list = false;
+			}
+		}
+	}
+
+	LocalVector<Spatial *> &curr_prop = data.tick_property_list[curr_mirror];
+	LocalVector<Spatial *> &prev_prop = data.tick_property_list[prev_mirror];
+
+	// Detect on the previous property list but not on this tick list.
+	for (uint32_t n = 0; n < prev_prop.size(); n++) {
+		Spatial *s = prev_prop[n];
+
+		if (!s->data.fti_on_tick_property_list) {
+			// Needs a reset so jittering will stop.
+			s->fti_pump_xform();
+
+			// Remove from interpolation list.
+			if (s->data.fti_on_frame_property_list) {
+				s->data.fti_on_frame_property_list = false;
+				data.frame_property_list.erase_unordered(s);
+
+#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+				DEV_CHECK_ONCE(data.frame_property_list.find(s) == -1);
+#endif
 			}
 		}
 	}
 
+	// Pump all on the property list that are NOT on the tick list.
+	for (uint32_t n = 0; n < curr_prop.size(); n++) {
+		Spatial *s = curr_prop[n];
+
+		// Reset, needs to be marked each tick.
+		s->data.fti_on_tick_property_list = false;
+		s->fti_pump_property();
+	}
+
 	// 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;
+		s->data.fti_on_tick_xform_list = false;
 
 		// Pump.
-		s->fti_pump();
+		s->fti_pump_xform();
 	}
 
 	// Clear previous list and flip.
 	prev.clear();
+	prev_prop.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()) {
+void SceneTreeFTI::_spatial_notify_set_property(Spatial &r_spatial) {
+	if (!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);
+	DEV_CHECK_ONCE(data.enabled);
+
+	// Note that a Spatial can be on BOTH the transform list and the property list.
+	if (!r_spatial.data.fti_on_tick_property_list) {
+		r_spatial.data.fti_on_tick_property_list = true;
+
+		// Should only appear once in the property list.
+#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+		DEV_CHECK_ONCE(data.tick_property_list[data.mirror].find(&r_spatial) == -1);
+#endif
+		data.tick_property_list[data.mirror].push_back(&r_spatial);
 	}
 
-	if (!r_spatial.data.fti_on_frame_list) {
-		r_spatial.data.fti_on_frame_list = true;
+	if (!r_spatial.data.fti_on_frame_property_list) {
+		r_spatial.data.fti_on_frame_property_list = true;
+
+		// Should only appear once in the property frame list.
+#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+		DEV_CHECK_ONCE(data.frame_property_list.find(&r_spatial) == -1);
+#endif
+		data.frame_property_list.push_back(&r_spatial);
+	}
+}
+
+void SceneTreeFTI::_spatial_notify_set_xform(Spatial &r_spatial) {
+	if (!r_spatial.is_physics_interpolated()) {
+		return;
+	}
+
+	DEV_CHECK_ONCE(data.enabled);
+
+	if (!r_spatial.data.fti_on_tick_xform_list) {
+		r_spatial.data.fti_on_tick_xform_list = true;
+
+		// Should only appear once in the xform list.
+#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+		DEV_CHECK_ONCE(data.tick_xform_list[data.mirror].find(&r_spatial) == -1);
+#endif
+		data.tick_xform_list[data.mirror].push_back(&r_spatial);
+	}
+
+	if (!r_spatial.data.fti_on_frame_xform_list) {
+		r_spatial.data.fti_on_frame_xform_list = true;
 	}
 }
 
@@ -138,14 +210,33 @@ void SceneTreeFTI::spatial_notify_delete(Spatial *p_spatial) {
 		return;
 	}
 
-	if (p_spatial->data.fti_on_frame_list) {
-		p_spatial->data.fti_on_frame_list = false;
+	ERR_FAIL_NULL(p_spatial);
+
+	if (p_spatial->data.fti_on_frame_xform_list) {
+		p_spatial->data.fti_on_frame_xform_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);
+	data.tick_xform_list[0].erase_unordered(p_spatial);
+	data.tick_xform_list[1].erase_unordered(p_spatial);
+
+	data.tick_property_list[0].erase_unordered(p_spatial);
+	data.tick_property_list[1].erase_unordered(p_spatial);
+
+	data.frame_property_list.erase_unordered(p_spatial);
+
+#ifdef GODOT_SCENE_TREE_FTI_EXTRA_CHECKS
+	// There should only be one occurrence on the lists.
+	// Check this in DEV_ENABLED builds.
+	DEV_CHECK_ONCE(data.tick_xform_list[0].find(p_spatial) == -1);
+	DEV_CHECK_ONCE(data.tick_xform_list[1].find(p_spatial) == -1);
+
+	DEV_CHECK_ONCE(data.tick_property_list[0].find(p_spatial) == -1);
+	DEV_CHECK_ONCE(data.tick_property_list[1].find(p_spatial) == -1);
+
+	DEV_CHECK_ONCE(data.frame_property_list.find(p_spatial) == -1);
+#endif
 }
 
 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) {
@@ -176,7 +267,7 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
 	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) {
+			if (s->data.fti_on_frame_xform_list) {
 				p_active = true;
 			}
 		} else {
@@ -236,7 +327,7 @@ void SceneTreeFTI::_update_dirty_spatials(Node *p_node, uint32_t p_current_frame
 		}
 
 		// Upload to VisualServer the interpolated global xform.
-		s->fti_update_servers();
+		s->fti_update_servers_xform();
 
 	} // if active.
 
@@ -283,6 +374,15 @@ void SceneTreeFTI::frame_update(Node *p_root, bool p_frame_start) {
 		print_line("Took " + itos(after - before) + " usec " + (data.frame_start ? "start" : "end"));
 	}
 #endif
+
+	// Update the properties once off at the end of the frame.
+	// No need for two passes for properties.
+	if (!p_frame_start) {
+		for (uint32_t n = 0; n < data.frame_property_list.size(); n++) {
+			Spatial *s = data.frame_property_list[n];
+			s->fti_update_servers_property();
+		}
+	}
 }
 
 #endif // ndef _3D_DISABLED

+ 16 - 5
scene/main/scene_tree_fti.h

@@ -47,7 +47,7 @@ public:
 	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_changed(Spatial &r_spatial, bool p_transform_changed) {}
 	void spatial_notify_delete(Spatial *p_spatial) {}
 };
 #else
@@ -66,7 +66,13 @@ public:
 class SceneTreeFTI {
 	struct Data {
 		// Prev / Curr lists of spatials having local xforms pumped.
-		LocalVector<Spatial *> spatial_tick_list[2];
+		LocalVector<Spatial *> tick_xform_list[2];
+
+		// Prev / Curr lists of spatials having actively interpolated properties.
+		LocalVector<Spatial *> tick_property_list[2];
+
+		LocalVector<Spatial *> frame_property_list;
+
 		uint32_t mirror = 0;
 
 		bool enabled = false;
@@ -82,15 +88,20 @@ class SceneTreeFTI {
 
 	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);
+	void _spatial_notify_set_xform(Spatial &r_spatial);
+	void _spatial_notify_set_property(Spatial &r_spatial);
 
 public:
 	// Hottest function, allow inlining the data.enabled check.
-	void spatial_notify_set_transform(Spatial &r_spatial) {
+	void spatial_notify_changed(Spatial &r_spatial, bool p_transform_changed) {
 		if (!data.enabled) {
 			return;
 		}
-		_spatial_notify_set_transform(r_spatial);
+		if (p_transform_changed) {
+			_spatial_notify_set_xform(r_spatial);
+		} else {
+			_spatial_notify_set_property(r_spatial);
+		}
 	}
 
 	void spatial_notify_delete(Spatial *p_spatial);