Просмотр исходного кода

Merge pull request #76252 from lawnjelly/fti_2d

[3.x] 2D Fixed Timestep Interpolation
Rémi Verschelde 2 лет назад
Родитель
Сommit
c8295720e4

+ 45 - 0
core/math/transform_interpolator.cpp

@@ -30,6 +30,51 @@
 
 #include "transform_interpolator.h"
 
+#include "core/math/transform_2d.h"
+
+void TransformInterpolator::interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction) {
+	// Extract parameters.
+	Vector2 p1 = p_prev.get_origin();
+	Vector2 p2 = p_curr.get_origin();
+
+	// Special case for physics interpolation, if flipping, don't interpolate basis.
+	// If the determinant polarity changes, the handedness of the coordinate system changes.
+	if (_sign(p_prev.determinant()) != _sign(p_curr.determinant())) {
+		r_result.elements[0] = p_curr.elements[0];
+		r_result.elements[1] = p_curr.elements[1];
+		r_result.set_origin(Vector2::linear_interpolate(p1, p2, p_fraction));
+		return;
+	}
+
+	real_t r1 = p_prev.get_rotation();
+	real_t r2 = p_curr.get_rotation();
+
+	Size2 s1 = p_prev.get_scale();
+	Size2 s2 = p_curr.get_scale();
+
+	// Slerp rotation.
+	Vector2 v1(Math::cos(r1), Math::sin(r1));
+	Vector2 v2(Math::cos(r2), Math::sin(r2));
+
+	real_t dot = v1.dot(v2);
+
+	dot = CLAMP(dot, -1, 1);
+
+	Vector2 v;
+
+	if (dot > 0.9995f) {
+		v = Vector2::linear_interpolate(v1, v2, p_fraction).normalized(); // Linearly interpolate to avoid numerical precision issues.
+	} else {
+		real_t angle = p_fraction * Math::acos(dot);
+		Vector2 v3 = (v2 - v1 * dot).normalized();
+		v = v1 * Math::cos(angle) + v3 * Math::sin(angle);
+	}
+
+	// Construct matrix.
+	r_result = Transform2D(Math::atan2(v.y, v.x), Vector2::linear_interpolate(p1, p2, p_fraction));
+	r_result.scale_basis(Vector2::linear_interpolate(s1, s2, p_fraction));
+}
+
 void TransformInterpolator::interpolate_transform(const Transform &p_prev, const Transform &p_curr, Transform &r_result, real_t p_fraction) {
 	r_result.origin = p_prev.origin + ((p_curr.origin - p_prev.origin) * p_fraction);
 	interpolate_basis(p_prev.basis, p_curr.basis, r_result.basis, p_fraction);

+ 3 - 1
core/math/transform_interpolator.h

@@ -46,7 +46,7 @@
 // several frames may occur between each physics tick, which will make it cheaper
 // than performing every frame.
 
-class Transform;
+struct Transform2D;
 
 class TransformInterpolator {
 public:
@@ -66,6 +66,7 @@ private:
 	static Quat _basis_to_quat_unchecked(const Basis &p_basis);
 	static bool _basis_is_orthogonal(const Basis &p_basis, real_t p_epsilon = 0.01f);
 	static bool _basis_is_orthogonal_any_scale(const Basis &p_basis);
+	static bool _sign(real_t p_val) { return p_val >= 0; }
 
 	static void interpolate_basis_linear(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction);
 	static void interpolate_basis_scaled_slerp(Basis p_prev, Basis p_curr, Basis &r_result, real_t p_fraction);
@@ -75,6 +76,7 @@ public:
 	// These will be slower.
 	static void interpolate_transform(const Transform &p_prev, const Transform &p_curr, Transform &r_result, real_t p_fraction);
 	static void interpolate_basis(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction);
+	static void interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction);
 
 	// Optimized function when you know ahead of time the method
 	static void interpolate_transform_via_method(const Transform &p_prev, const Transform &p_curr, Transform &r_result, real_t p_fraction, Method p_method);

+ 1 - 0
core/os/main_loop.h

@@ -67,6 +67,7 @@ public:
 	virtual void input_text(const String &p_text);
 
 	virtual void init();
+	virtual void iteration_prepare() {}
 	virtual bool iteration(float p_time);
 	virtual void iteration_end() {}
 	virtual bool idle(float p_time);

+ 1 - 0
doc/classes/Control.xml

@@ -825,6 +825,7 @@
 		<member name="mouse_filter" type="int" setter="set_mouse_filter" getter="get_mouse_filter" enum="Control.MouseFilter" default="0">
 			Controls whether the control will be able to receive mouse button input events through [method _gui_input] and how these events should be handled. Also controls whether the control can receive the [signal mouse_entered], and [signal mouse_exited] signals. See the constants to learn what each does.
 		</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="rect_clip_content" type="bool" setter="set_clip_contents" getter="is_clipping_contents" default="false">
 			Enables whether rendering of [CanvasItem] based children should be clipped to this control's rectangle. If [code]true[/code], parts of a child which would be visibly outside of this control's rectangle will not be rendered.
 		</member>

+ 1 - 0
doc/classes/ParallaxLayer.xml

@@ -23,6 +23,7 @@
 		<member name="motion_scale" type="Vector2" setter="set_motion_scale" getter="get_motion_scale" default="Vector2( 1, 1 )">
 			Multiplies the ParallaxLayer's motion. If an axis is set to [code]0[/code], it will not scroll.
 		</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>

+ 78 - 0
doc/classes/VisualServer.xml

@@ -325,6 +325,14 @@
 				Once finished with your RID, you will want to free the RID using the VisualServer's [method free_rid] static method.
 			</description>
 		</method>
+		<method name="canvas_item_reset_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="item" type="RID" />
+			<description>
+				Prevents physics interpolation for the current physics tick.
+				This is useful when moving a canvas item to a new location, to give an instantaneous change rather than interpolation from the previous location.
+			</description>
+		</method>
 		<method name="canvas_item_set_clip">
 			<return type="void" />
 			<argument index="0" name="item" type="RID" />
@@ -375,6 +383,14 @@
 				Sets the index for the [CanvasItem].
 			</description>
 		</method>
+		<method name="canvas_item_set_interpolated">
+			<return type="void" />
+			<argument index="0" name="item" type="RID" />
+			<argument index="1" name="interpolated" type="bool" />
+			<description>
+				Turns on and off physics interpolation for the canvas item.
+			</description>
+		</method>
 		<method name="canvas_item_set_light_mask">
 			<return type="void" />
 			<argument index="0" name="item" type="RID" />
@@ -463,6 +479,16 @@
 				Sets the [CanvasItem]'s Z index, i.e. its draw order (lower indexes are drawn first).
 			</description>
 		</method>
+		<method name="canvas_item_transform_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="item" type="RID" />
+			<argument index="1" name="xform" type="Transform2D" />
+			<description>
+				Transforms both the current and previous stored transform for a canvas item.
+				This allows transforming a canvas item without creating a "glitch" in the interpolation.
+				This is particularly useful for large worlds utilising a shifting origin.
+			</description>
+		</method>
 		<method name="canvas_light_attach_to_canvas">
 			<return type="void" />
 			<argument index="0" name="light" type="RID" />
@@ -493,6 +519,14 @@
 				Once finished with your RID, you will want to free the RID using the VisualServer's [method free_rid] static method.
 			</description>
 		</method>
+		<method name="canvas_light_occluder_reset_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="occluder" type="RID" />
+			<description>
+				Prevents physics interpolation for the current physics tick.
+				This is useful when moving an occluder to a new location, to give an instantaneous change rather than interpolation from the previous location.
+			</description>
+		</method>
 		<method name="canvas_light_occluder_set_enabled">
 			<return type="void" />
 			<argument index="0" name="occluder" type="RID" />
@@ -501,6 +535,14 @@
 				Enables or disables light occluder.
 			</description>
 		</method>
+		<method name="canvas_light_occluder_set_interpolated">
+			<return type="void" />
+			<argument index="0" name="occluder" type="RID" />
+			<argument index="1" name="interpolated" type="bool" />
+			<description>
+				Turns on and off physics interpolation for the occluder.
+			</description>
+		</method>
 		<method name="canvas_light_occluder_set_light_mask">
 			<return type="void" />
 			<argument index="0" name="occluder" type="RID" />
@@ -525,6 +567,24 @@
 				Sets a light occluder's [Transform2D].
 			</description>
 		</method>
+		<method name="canvas_light_occluder_transform_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="occluder" type="RID" />
+			<argument index="1" name="xform" type="Transform2D" />
+			<description>
+				Transforms both the current and previous stored transform for an occluder.
+				This allows transforming an occluder without creating a "glitch" in the interpolation.
+				This is particularly useful for large worlds utilising a shifting origin.
+			</description>
+		</method>
+		<method name="canvas_light_reset_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="light" type="RID" />
+			<description>
+				Prevents physics interpolation for the current physics tick.
+				This is useful when moving a light to a new location, to give an instantaneous change rather than interpolation from the previous location.
+			</description>
+		</method>
 		<method name="canvas_light_set_color">
 			<return type="void" />
 			<argument index="0" name="light" type="RID" />
@@ -557,6 +617,14 @@
 				Sets a canvas light's height.
 			</description>
 		</method>
+		<method name="canvas_light_set_interpolated">
+			<return type="void" />
+			<argument index="0" name="light" type="RID" />
+			<argument index="1" name="interpolated" type="bool" />
+			<description>
+				Turns on and off physics interpolation for the light.
+			</description>
+		</method>
 		<method name="canvas_light_set_item_cull_mask">
 			<return type="void" />
 			<argument index="0" name="light" type="RID" />
@@ -679,6 +747,16 @@
 				Sets the Z range of objects that will be affected by this light. Equivalent to [member Light2D.range_z_min] and [member Light2D.range_z_max].
 			</description>
 		</method>
+		<method name="canvas_light_transform_physics_interpolation">
+			<return type="void" />
+			<argument index="0" name="light" type="RID" />
+			<argument index="1" name="xform" type="Transform2D" />
+			<description>
+				Transforms both the current and previous stored transform for a light.
+				This allows transforming a light without creating a "glitch" in the interpolation.
+				This is particularly useful for large worlds utilising a shifting origin.
+			</description>
+		</method>
 		<method name="canvas_occluder_polygon_create">
 			<return type="RID" />
 			<description>

+ 6 - 0
main/main.cpp

@@ -2324,6 +2324,12 @@ bool Main::iteration() {
 
 		PhysicsServer::get_singleton()->flush_queries();
 
+		// Prepare the fixed timestep interpolated nodes
+		// BEFORE they are updated by the physics 2D,
+		// otherwise the current and previous transforms
+		// may be the same, and no interpolation takes place.
+		OS::get_singleton()->get_main_loop()->iteration_prepare();
+
 		Physics2DServer::get_singleton()->sync();
 		Physics2DServer::get_singleton()->flush_queries();
 

+ 82 - 16
scene/2d/camera_2d.cpp

@@ -55,8 +55,12 @@ void Camera2D::_update_scroll() {
 	if (current) {
 		ERR_FAIL_COND(custom_viewport && !ObjectDB::get_instance(custom_viewport_id));
 
-		Transform2D xform = get_camera_transform();
-
+		Transform2D xform;
+		if (is_physics_interpolated_and_enabled()) {
+			xform = _interpolation_data.xform_prev.interpolate_with(_interpolation_data.xform_curr, Engine::get_singleton()->get_physics_interpolation_fraction());
+		} else {
+			xform = get_camera_transform();
+		}
 		viewport->set_canvas_transform(xform);
 
 		Size2 screen_size = viewport->get_visible_rect().size;
@@ -67,13 +71,24 @@ void Camera2D::_update_scroll() {
 }
 
 void Camera2D::_update_process_mode() {
-	// smoothing can be enabled in the editor but will never be active
-	if (process_mode == CAMERA2D_PROCESS_IDLE) {
-		set_process_internal(smoothing_active);
-		set_physics_process_internal(false);
+	if (is_physics_interpolated_and_enabled()) {
+		set_process_internal(is_current());
+		set_physics_process_internal(is_current());
+
+#ifdef TOOLS_ENABLED
+		if (process_mode == CAMERA2D_PROCESS_IDLE) {
+			WARN_PRINT_ONCE("Camera2D overridden to physics process mode due to use of physics interpolation.");
+		}
+#endif
 	} else {
-		set_process_internal(false);
-		set_physics_process_internal(smoothing_active);
+		// Smoothing can be enabled in the editor but will never be active.
+		if (process_mode == CAMERA2D_PROCESS_IDLE) {
+			set_process_internal(smoothing_active);
+			set_physics_process_internal(false);
+		} else {
+			set_process_internal(false);
+			set_physics_process_internal(smoothing_active);
+		}
 	}
 }
 
@@ -178,8 +193,19 @@ Transform2D Camera2D::get_camera_transform() {
 			}
 		}
 
+		// TODO ..
+		// There is a bug here.
+		// Smoothing occurs rather confusingly
+		// during the call to get_camera_transform().
+		// It may be called MULTIPLE TIMES on certain frames,
+		// therefore smoothing is not currently applied only once per frame / tick,
+		// which will result in some haphazard results.
 		if (smoothing_active) {
-			float c = smoothing * (process_mode == CAMERA2D_PROCESS_PHYSICS ? get_physics_process_delta_time() : get_process_delta_time());
+			// Note that if we are using physics interpolation,
+			// processing will always be physics based (it ignores the process mode set in the UI).
+			bool physics_process = (process_mode == CAMERA2D_PROCESS_PHYSICS) || is_physics_interpolated_and_enabled();
+			float delta = physics_process ? get_physics_process_delta_time() : get_process_delta_time();
+			float c = smoothing * delta;
 			smoothed_camera_pos = ((camera_pos - smoothed_camera_pos) * c) + smoothed_camera_pos;
 			ret_camera_pos = smoothed_camera_pos;
 		} else {
@@ -234,17 +260,49 @@ Transform2D Camera2D::get_camera_transform() {
 	return (xform).affine_inverse();
 }
 
+void Camera2D::_ensure_update_interpolation_data() {
+	// 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;
+	}
+}
+
 void Camera2D::_notification(int p_what) {
 	switch (p_what) {
-		case NOTIFICATION_INTERNAL_PROCESS:
-		case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+		case NOTIFICATION_INTERNAL_PROCESS: {
 			_update_scroll();
-
+		} break;
+		case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+			if (is_physics_interpolated_and_enabled()) {
+				_ensure_update_interpolation_data();
+				_interpolation_data.xform_curr = get_camera_transform();
+			} else {
+				_update_scroll();
+			}
+		} break;
+		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+			// Force the limits etc to update.
+			_interpolation_data.xform_curr = get_camera_transform();
+			_interpolation_data.xform_prev = _interpolation_data.xform_curr;
 		} break;
 		case NOTIFICATION_TRANSFORM_CHANGED: {
-			if (!smoothing_active) {
+			if (!smoothing_active && !is_physics_interpolated_and_enabled()) {
 				_update_scroll();
 			}
+			if (is_physics_interpolated_and_enabled()) {
+				_ensure_update_interpolation_data();
+				_interpolation_data.xform_curr = get_camera_transform();
+			}
 
 		} break;
 		case NOTIFICATION_ENTER_TREE: {
@@ -400,10 +458,15 @@ Camera2D::Camera2DProcessMode Camera2D::get_process_mode() const {
 }
 
 void Camera2D::_make_current(Object *p_which) {
+	bool new_current = false;
+
 	if (p_which == this) {
-		current = true;
-	} else {
-		current = false;
+		new_current = true;
+	}
+
+	if (new_current != current) {
+		current = new_current;
+		_update_process_mode();
 	}
 }
 
@@ -413,6 +476,7 @@ void Camera2D::_set_current(bool p_current) {
 	}
 
 	current = p_current;
+	_update_process_mode();
 	update();
 }
 
@@ -427,6 +491,7 @@ void Camera2D::make_current() {
 		get_tree()->call_group_flags(SceneTree::GROUP_CALL_REALTIME, group_name, "_make_current", this);
 	}
 	_update_scroll();
+	_update_process_mode();
 }
 
 void Camera2D::clear_current() {
@@ -434,6 +499,7 @@ void Camera2D::clear_current() {
 	if (is_inside_tree()) {
 		get_tree()->call_group_flags(SceneTree::GROUP_CALL_REALTIME, group_name, "_make_current", (Object *)nullptr);
 	}
+	_update_process_mode();
 }
 
 void Camera2D::set_limit(Margin p_margin, int p_limit) {

+ 7 - 0
scene/2d/camera_2d.h

@@ -84,6 +84,7 @@ protected:
 	void _update_process_mode();
 	void _update_scroll();
 	void _setup_viewport();
+	void _ensure_update_interpolation_data();
 
 	void _make_current(Object *p_which);
 	void _set_current(bool p_current);
@@ -94,6 +95,12 @@ protected:
 
 	Camera2DProcessMode process_mode;
 
+	struct InterpolationData {
+		Transform2D xform_curr;
+		Transform2D xform_prev;
+		uint32_t last_update_physics_tick = 0;
+	} _interpolation_data;
+
 protected:
 	virtual Transform2D get_camera_transform();
 	void _notification(int p_what);

+ 10 - 0
scene/2d/canvas_item.cpp

@@ -562,6 +562,10 @@ void CanvasItem::_exit_canvas() {
 	}
 }
 
+void CanvasItem::_physics_interpolated_changed() {
+	VisualServer::get_singleton()->canvas_item_set_interpolated(canvas_item, is_physics_interpolated());
+}
+
 void CanvasItem::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_TREE: {
@@ -605,6 +609,12 @@ void CanvasItem::_notification(int p_what) {
 			}
 			global_invalid = true;
 		} break;
+		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+			if (is_visible_in_tree() && is_physics_interpolated()) {
+				VisualServer::get_singleton()->canvas_item_reset_physics_interpolation(canvas_item);
+			}
+		} break;
+
 		case NOTIFICATION_DRAW:
 		case NOTIFICATION_TRANSFORM_CHANGED: {
 		} break;

+ 1 - 0
scene/2d/canvas_item.h

@@ -218,6 +218,7 @@ private:
 	void _exit_canvas();
 
 	void _notify_transform(CanvasItem *p_node);
+	virtual void _physics_interpolated_changed();
 
 	void _set_on_top(bool p_on_top) { set_draw_behind_parent(!p_on_top); }
 	bool _is_on_top() const { return !is_draw_behind_parent_enabled(); }

+ 24 - 15
scene/2d/light_2d.cpp

@@ -297,22 +297,31 @@ Color Light2D::get_shadow_color() const {
 	return shadow_color;
 }
 
-void Light2D::_notification(int p_what) {
-	if (p_what == NOTIFICATION_ENTER_TREE) {
-		VS::get_singleton()->canvas_light_attach_to_canvas(canvas_light, get_canvas());
-		_update_light_visibility();
-	}
-
-	if (p_what == NOTIFICATION_TRANSFORM_CHANGED) {
-		VS::get_singleton()->canvas_light_set_transform(canvas_light, get_global_transform());
-	}
-	if (p_what == NOTIFICATION_VISIBILITY_CHANGED) {
-		_update_light_visibility();
-	}
+void Light2D::_physics_interpolated_changed() {
+	VisualServer::get_singleton()->canvas_light_set_interpolated(canvas_light, is_physics_interpolated());
+}
 
-	if (p_what == NOTIFICATION_EXIT_TREE) {
-		VS::get_singleton()->canvas_light_attach_to_canvas(canvas_light, RID());
-		_update_light_visibility();
+void Light2D::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			VS::get_singleton()->canvas_light_attach_to_canvas(canvas_light, get_canvas());
+			_update_light_visibility();
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			VS::get_singleton()->canvas_light_attach_to_canvas(canvas_light, RID());
+			_update_light_visibility();
+		} break;
+		case NOTIFICATION_TRANSFORM_CHANGED: {
+			VS::get_singleton()->canvas_light_set_transform(canvas_light, get_global_transform());
+		} break;
+		case NOTIFICATION_VISIBILITY_CHANGED: {
+			_update_light_visibility();
+		} break;
+		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+			if (is_visible_in_tree() && is_physics_interpolated()) {
+				VisualServer::get_singleton()->canvas_light_reset_physics_interpolation(canvas_light);
+			}
+		} break;
 	}
 }
 

+ 1 - 0
scene/2d/light_2d.h

@@ -80,6 +80,7 @@ private:
 	void _update_light_visibility();
 
 	virtual void owner_changed_notify();
+	virtual void _physics_interpolated_changed();
 
 protected:
 	void _notification(int p_what);

+ 41 - 32
scene/2d/light_occluder_2d.cpp

@@ -159,43 +159,52 @@ void LightOccluder2D::_poly_changed() {
 #endif
 }
 
-void LightOccluder2D::_notification(int p_what) {
-	if (p_what == NOTIFICATION_ENTER_CANVAS) {
-		VS::get_singleton()->canvas_light_occluder_attach_to_canvas(occluder, get_canvas());
-		VS::get_singleton()->canvas_light_occluder_set_transform(occluder, get_global_transform());
-		VS::get_singleton()->canvas_light_occluder_set_enabled(occluder, is_visible_in_tree());
-	}
-	if (p_what == NOTIFICATION_TRANSFORM_CHANGED) {
-		VS::get_singleton()->canvas_light_occluder_set_transform(occluder, get_global_transform());
-	}
-	if (p_what == NOTIFICATION_VISIBILITY_CHANGED) {
-		VS::get_singleton()->canvas_light_occluder_set_enabled(occluder, is_visible_in_tree());
-	}
+void LightOccluder2D::_physics_interpolated_changed() {
+	VisualServer::get_singleton()->canvas_light_occluder_set_interpolated(occluder, is_physics_interpolated());
+}
 
-	if (p_what == NOTIFICATION_DRAW) {
-		if (Engine::get_singleton()->is_editor_hint()) {
-			if (occluder_polygon.is_valid()) {
-				PoolVector<Vector2> poly = occluder_polygon->get_polygon();
-
-				if (poly.size()) {
-					if (occluder_polygon->is_closed()) {
-						Vector<Color> color;
-						color.push_back(Color(0, 0, 0, 0.6));
-						draw_polygon(Variant(poly), color);
-					} else {
-						int ps = poly.size();
-						PoolVector<Vector2>::Read r = poly.read();
-						for (int i = 0; i < ps - 1; i++) {
-							draw_line(r[i], r[i + 1], Color(0, 0, 0, 0.6), 3);
+void LightOccluder2D::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_CANVAS: {
+			VS::get_singleton()->canvas_light_occluder_attach_to_canvas(occluder, get_canvas());
+			VS::get_singleton()->canvas_light_occluder_set_transform(occluder, get_global_transform());
+			VS::get_singleton()->canvas_light_occluder_set_enabled(occluder, is_visible_in_tree());
+		} break;
+		case NOTIFICATION_TRANSFORM_CHANGED: {
+			VS::get_singleton()->canvas_light_occluder_set_transform(occluder, get_global_transform());
+		} break;
+		case NOTIFICATION_VISIBILITY_CHANGED: {
+			VS::get_singleton()->canvas_light_occluder_set_enabled(occluder, is_visible_in_tree());
+		} break;
+		case NOTIFICATION_DRAW: {
+			if (Engine::get_singleton()->is_editor_hint()) {
+				if (occluder_polygon.is_valid()) {
+					PoolVector<Vector2> poly = occluder_polygon->get_polygon();
+
+					if (poly.size()) {
+						if (occluder_polygon->is_closed()) {
+							Vector<Color> color;
+							color.push_back(Color(0, 0, 0, 0.6));
+							draw_polygon(Variant(poly), color);
+						} else {
+							int ps = poly.size();
+							PoolVector<Vector2>::Read r = poly.read();
+							for (int i = 0; i < ps - 1; i++) {
+								draw_line(r[i], r[i + 1], Color(0, 0, 0, 0.6), 3);
+							}
 						}
 					}
 				}
 			}
-		}
-	}
-
-	if (p_what == NOTIFICATION_EXIT_CANVAS) {
-		VS::get_singleton()->canvas_light_occluder_attach_to_canvas(occluder, RID());
+		} break;
+		case NOTIFICATION_EXIT_CANVAS: {
+			VS::get_singleton()->canvas_light_occluder_attach_to_canvas(occluder, RID());
+		} break;
+		case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+			if (is_visible_in_tree() && is_physics_interpolated()) {
+				VisualServer::get_singleton()->canvas_light_occluder_reset_physics_interpolation(occluder);
+			}
+		} break;
 	}
 }
 

+ 1 - 0
scene/2d/light_occluder_2d.h

@@ -86,6 +86,7 @@ class LightOccluder2D : public Node2D {
 	Ref<OccluderPolygon2D> occluder_polygon;
 
 	void _poly_changed();
+	virtual void _physics_interpolated_changed();
 
 protected:
 	void _notification(int p_what);

+ 3 - 0
scene/2d/parallax_layer.cpp

@@ -163,4 +163,7 @@ void ParallaxLayer::_bind_methods() {
 
 ParallaxLayer::ParallaxLayer() {
 	motion_scale = Size2(1, 1);
+
+	// ParallaxLayer is always updated every frame so there is no need to interpolate.
+	set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF);
 }

+ 0 - 11
scene/3d/visual_instance.cpp

@@ -117,17 +117,6 @@ void VisualInstance::_notification(int p_what) {
 			if (_is_vi_visible() && is_physics_interpolated()) {
 				VisualServer::get_singleton()->instance_reset_physics_interpolation(instance);
 			}
-#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
-			else if (GLOBAL_GET("debug/settings/physics_interpolation/enable_warnings")) {
-				String node_name = is_inside_tree() ? String(get_path()) : String(get_name());
-				if (!_is_vi_visible()) {
-					WARN_PRINT("[Physics interpolation] NOTIFICATION_RESET_PHYSICS_INTERPOLATION only works with unhidden nodes: \"" + node_name + "\".");
-				}
-				if (!is_physics_interpolated()) {
-					WARN_PRINT("[Physics interpolation] NOTIFICATION_RESET_PHYSICS_INTERPOLATION only works with interpolated nodes: \"" + node_name + "\".");
-				}
-			}
-#endif
 		} break;
 		case NOTIFICATION_EXIT_WORLD: {
 			VisualServer::get_singleton()->instance_set_scenario(instance, RID());

+ 2 - 0
scene/gui/control.cpp

@@ -3003,6 +3003,8 @@ Control::Control() {
 	}
 	data.focus_mode = FOCUS_NONE;
 	data.modal_prev_focus_owner = 0;
+
+	set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF);
 }
 
 Control::~Control() {

+ 6 - 4
scene/main/scene_tree.cpp

@@ -553,6 +553,12 @@ void SceneTree::client_physics_interpolation_remove_spatial(SelfList<Spatial> *p
 	_client_physics_interpolation._spatials_list.remove(p_elem);
 }
 
+void SceneTree::iteration_prepare() {
+	if (_physics_interpolation_enabled) {
+		VisualServer::get_singleton()->tick();
+	}
+}
+
 void SceneTree::iteration_end() {
 	// When physics interpolation is active, we want all pending transforms
 	// to be flushed to the VisualServer before finishing a physics tick.
@@ -566,10 +572,6 @@ bool SceneTree::iteration(float p_time) {
 
 	current_frame++;
 
-	if (_physics_interpolation_enabled) {
-		VisualServer::get_singleton()->tick();
-	}
-
 	// Any objects performing client physics interpolation
 	// should be given an opportunity to keep their previous transforms
 	// up to take before each new physics tick.

+ 1 - 0
scene/main/scene_tree.h

@@ -313,6 +313,7 @@ public:
 	virtual void input_event(const Ref<InputEvent> &p_event);
 	virtual void init();
 
+	virtual void iteration_prepare();
 	virtual bool iteration(float p_time);
 	virtual void iteration_end();
 	virtual bool idle(float p_time);

+ 32 - 5
servers/visual/rasterizer.h

@@ -725,9 +725,12 @@ public:
 	};
 
 	struct Light : public RID_Data {
-		bool enabled;
+		bool enabled : 1;
+		bool on_interpolate_transform_list : 1;
+		bool interpolated : 1;
 		Color color;
-		Transform2D xform;
+		Transform2D xform_curr;
+		Transform2D xform_prev;
 		float height;
 		float energy;
 		float scale;
@@ -766,6 +769,8 @@ public:
 
 		Light() {
 			enabled = true;
+			on_interpolate_transform_list = false;
+			interpolated = true;
 			color = Color(1, 1, 1);
 			shadow_color = Color(0, 0, 0, 0);
 			height = 0;
@@ -977,13 +982,19 @@ public:
 			Rect2 rect;
 		};
 
-		Transform2D xform;
+		// For interpolation we store the current local xform,
+		// and the previous xform from the previous tick.
+		Transform2D xform_curr;
+		Transform2D xform_prev;
+
 		bool clip : 1;
 		bool visible : 1;
 		bool behind : 1;
 		bool update_when_visible : 1;
 		bool distance_field : 1;
 		bool light_masked : 1;
+		bool on_interpolate_transform_list : 1;
+		bool interpolated : 1;
 		mutable bool custom_rect : 1;
 		mutable bool rect_dirty : 1;
 		mutable bool bound_dirty : 1;
@@ -1030,6 +1041,13 @@ public:
 		// in local space.
 		Rect2 local_bound;
 
+		// When using interpolation, the local bound for culling
+		// should be a combined bound of the previous and current.
+		// To keep this up to date, we need to keep track of the previous
+		// bound separately rather than just the combined bound.
+		Rect2 local_bound_prev;
+		uint32_t local_bound_last_update_tick;
+
 		const Rect2 &get_rect() const {
 			if (custom_rect) {
 				return rect;
@@ -1196,6 +1214,7 @@ public:
 				memdelete(skinning_data);
 				skinning_data = nullptr;
 			}
+			on_interpolate_transform_list = false;
 		}
 		Item() {
 			light_mask = 1;
@@ -1215,6 +1234,9 @@ public:
 			distance_field = false;
 			light_masked = false;
 			update_when_visible = false;
+			on_interpolate_transform_list = false;
+			interpolated = true;
+			local_bound_last_update_tick = 0;
 		}
 		virtual ~Item() {
 			clear();
@@ -1233,12 +1255,15 @@ public:
 	virtual void canvas_debug_viewport_shadows(Light *p_lights_with_shadow) = 0;
 
 	struct LightOccluderInstance : public RID_Data {
-		bool enabled;
+		bool enabled : 1;
+		bool on_interpolate_transform_list : 1;
+		bool interpolated : 1;
 		RID canvas;
 		RID polygon;
 		RID polygon_buffer;
 		Rect2 aabb_cache;
-		Transform2D xform;
+		Transform2D xform_curr;
+		Transform2D xform_prev;
 		Transform2D xform_cache;
 		int light_mask;
 		VS::CanvasOccluderPolygonCullMode cull_cache;
@@ -1250,6 +1275,8 @@ public:
 			next = nullptr;
 			light_mask = 1;
 			cull_cache = VS::CANVAS_OCCLUDER_POLYGON_CULL_DISABLED;
+			on_interpolate_transform_list = false;
+			interpolated = true;
 		}
 	};
 

+ 231 - 16
servers/visual/visual_server_canvas.cpp

@@ -29,6 +29,7 @@
 /**************************************************************************/
 
 #include "visual_server_canvas.h"
+#include "core/math/transform_interpolator.h"
 #include "visual_server_globals.h"
 #include "visual_server_raster.h"
 #include "visual_server_viewport.h"
@@ -65,7 +66,7 @@ void _collect_ysort_children(VisualServerCanvas::Item *p_canvas_item, Transform2
 				r_items[r_index] = child_items[i];
 				child_items[i]->ysort_modulate = p_modulate;
 				child_items[i]->ysort_xform = p_transform;
-				child_items[i]->ysort_pos = p_transform.xform(child_items[i]->xform.elements[2]);
+				child_items[i]->ysort_pos = p_transform.xform(child_items[i]->xform_curr.elements[2]);
 				child_items[i]->material_owner = child_items[i]->use_parent_material ? p_material_owner : nullptr;
 				child_items[i]->ysort_index = r_index;
 			}
@@ -74,7 +75,7 @@ void _collect_ysort_children(VisualServerCanvas::Item *p_canvas_item, Transform2
 
 			if (child_items[i]->sort_y) {
 				_collect_ysort_children(child_items[i],
-						p_transform * child_items[i]->xform,
+						p_transform * child_items[i]->xform_curr,
 						child_items[i]->use_parent_material ? p_material_owner : child_items[i],
 						p_modulate * child_items[i]->modulate,
 						r_items, r_index);
@@ -241,6 +242,37 @@ void VisualServerCanvas::_calculate_canvas_item_bound(Item *p_canvas_item, Rect2
 	}
 
 	_finalize_and_merge_local_bound_to_branch(ci, r_branch_bound);
+
+	// If we are interpolating, we want to modify the local_bound (combined)
+	// to include both the previous AND current bounds.
+	if (local_bound && _interpolation_data.interpolation_enabled && ci->interpolated) {
+		Rect2 bound_prev = ci->local_bound_prev;
+
+		// Keep track of the previously assigned exact bound for the next tick.
+		ci->local_bound_prev = ci->local_bound;
+
+		// The combined bound is the exact current bound merged with the previous exact bound.
+		ci->local_bound = ci->local_bound.merge(bound_prev);
+
+		// This can overflow, it's no problem, it is just rough to detect when items stop
+		// having local bounds updated, so we can set prev to curr.
+		ci->local_bound_last_update_tick = Engine::get_singleton()->get_physics_frames();
+
+		// Detect special case of overflow.
+		// This is omitted but included for reference.
+		// It is such a rare possibility, and even if it did occur
+		// so it should just result in slightly larger culling bounds
+		// probably for one tick (and no visual errors).
+		// Would occur once every 828.5 days at 60 ticks per second
+		// with uint32_t counter.
+#if 0
+		if (!ci->local_bound_last_update_tick) {
+			// Prevents it being treated as non-dirty.
+			// Just has an increased delay of one tick in this very rare occurrence.
+			ci->local_bound_last_update_tick = 1;
+		}
+#endif
+	}
 }
 
 void VisualServerCanvas::_finalize_and_merge_local_bound_to_branch(Item *p_canvas_item, Rect2 *r_branch_bound) {
@@ -275,7 +307,7 @@ void VisualServerCanvas::_merge_local_bound_to_branch(Item *p_canvas_item, Rect2
 		return;
 	}
 
-	Rect2 this_item_total_local_bound = p_canvas_item->xform.xform(p_canvas_item->local_bound);
+	Rect2 this_item_total_local_bound = p_canvas_item->xform_curr.xform(p_canvas_item->local_bound);
 
 	if (!r_branch_bound->has_no_area()) {
 		*r_branch_bound = r_branch_bound->merge(this_item_total_local_bound);
@@ -297,10 +329,17 @@ void VisualServerCanvas::_render_canvas_item_cull_by_item(Item *p_canvas_item, c
 	}
 
 	Rect2 rect = ci->get_rect();
-	Transform2D xform = ci->xform;
-	xform = p_transform * xform;
 
-	Rect2 global_rect = xform.xform(rect);
+	Transform2D final_xform;
+	if (!_interpolation_data.interpolation_enabled || !ci->interpolated) {
+		final_xform = ci->xform_curr;
+	} else {
+		real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
+		TransformInterpolator::interpolate_transform_2d(ci->xform_prev, ci->xform_curr, final_xform, f);
+	}
+	final_xform = p_transform * final_xform;
+
+	Rect2 global_rect = final_xform.xform(rect);
 	global_rect.position += p_clip_rect.position;
 
 	if (ci->use_parent_material && p_material_owner) {
@@ -360,14 +399,14 @@ void VisualServerCanvas::_render_canvas_item_cull_by_item(Item *p_canvas_item, c
 			continue;
 		}
 		if (ci->sort_y) {
-			_render_canvas_item_cull_by_item(child_items[i], xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner);
+			_render_canvas_item_cull_by_item(child_items[i], final_xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner);
 		} else {
-			_render_canvas_item_cull_by_item(child_items[i], xform, p_clip_rect, modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, p_material_owner);
+			_render_canvas_item_cull_by_item(child_items[i], final_xform, p_clip_rect, modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, p_material_owner);
 		}
 	}
 
 	if (ci->copy_back_buffer) {
-		ci->copy_back_buffer->screen_rect = xform.xform(ci->copy_back_buffer->rect).clip(p_clip_rect);
+		ci->copy_back_buffer->screen_rect = final_xform.xform(ci->copy_back_buffer->rect).clip(p_clip_rect);
 	}
 
 	if (ci->update_when_visible) {
@@ -376,7 +415,7 @@ void VisualServerCanvas::_render_canvas_item_cull_by_item(Item *p_canvas_item, c
 
 	if ((!ci->commands.empty() && p_clip_rect.intersects(global_rect, true)) || ci->vp_render || ci->copy_back_buffer) {
 		//something to draw?
-		ci->final_transform = xform;
+		ci->final_transform = final_xform;
 		ci->final_modulate = Color(modulate.r * ci->self_modulate.r, modulate.g * ci->self_modulate.g, modulate.b * ci->self_modulate.b, modulate.a * ci->self_modulate.a);
 		ci->global_rect_cache = global_rect;
 		ci->global_rect_cache.position -= p_clip_rect.position;
@@ -401,9 +440,9 @@ void VisualServerCanvas::_render_canvas_item_cull_by_item(Item *p_canvas_item, c
 			continue;
 		}
 		if (ci->sort_y) {
-			_render_canvas_item_cull_by_item(child_items[i], xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner);
+			_render_canvas_item_cull_by_item(child_items[i], final_xform * child_items[i]->ysort_xform, p_clip_rect, modulate * child_items[i]->ysort_modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, (Item *)child_items[i]->material_owner);
 		} else {
-			_render_canvas_item_cull_by_item(child_items[i], xform, p_clip_rect, modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, p_material_owner);
+			_render_canvas_item_cull_by_item(child_items[i], final_xform, p_clip_rect, modulate, p_z, z_list, z_last_list, (Item *)ci->final_clip_owner, p_material_owner);
 		}
 	}
 }
@@ -418,9 +457,22 @@ void VisualServerCanvas::_render_canvas_item_cull_by_node(Item *p_canvas_item, c
 	// This should have been calculated as a pre-process.
 	DEV_ASSERT(!ci->bound_dirty);
 
+	// If we are interpolating, and the updates have stopped, we can reduce the local bound.
+	if (ci->local_bound_last_update_tick && (ci->local_bound_last_update_tick != Engine::get_singleton()->get_physics_frames())) {
+		// The combined bound is reduced to the last calculated exact bound.
+		ci->local_bound = ci->local_bound_prev;
+		ci->local_bound_last_update_tick = 0;
+	}
+
 	Rect2 rect = ci->get_rect();
 
-	Transform2D final_xform = ci->xform;
+	Transform2D final_xform;
+	if (!_interpolation_data.interpolation_enabled || !ci->interpolated) {
+		final_xform = ci->xform_curr;
+	} else {
+		real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
+		TransformInterpolator::interpolate_transform_2d(ci->xform_prev, ci->xform_curr, final_xform, f);
+	}
 	final_xform = p_transform * final_xform;
 
 	Rect2 global_rect = final_xform.xform(rect);
@@ -825,7 +877,16 @@ void VisualServerCanvas::canvas_item_set_transform(RID p_item, const Transform2D
 	Item *canvas_item = canvas_item_owner.getornull(p_item);
 	ERR_FAIL_COND(!canvas_item);
 
-	canvas_item->xform = p_transform;
+	if (_interpolation_data.interpolation_enabled && canvas_item->interpolated) {
+		if (!canvas_item->on_interpolate_transform_list) {
+			_interpolation_data.canvas_item_transform_update_list_curr->push_back(p_item);
+			canvas_item->on_interpolate_transform_list = true;
+		} else {
+			DEV_ASSERT(_interpolation_data.canvas_item_transform_update_list_curr->size());
+		}
+	}
+
+	canvas_item->xform_curr = p_transform;
 
 	// Special case!
 	// Modifying the transform DOES NOT affect the local bound.
@@ -1435,6 +1496,64 @@ void VisualServerCanvas::canvas_item_set_skeleton_relative_xform(RID p_item, Tra
 	}
 }
 
+// Useful especially for origin shifting.
+void VisualServerCanvas::canvas_item_transform_physics_interpolation(RID p_item, Transform2D p_transform) {
+	Item *canvas_item = canvas_item_owner.getornull(p_item);
+	ERR_FAIL_COND(!canvas_item);
+	canvas_item->xform_prev = p_transform * canvas_item->xform_prev;
+	canvas_item->xform_curr = p_transform * canvas_item->xform_curr;
+}
+
+void VisualServerCanvas::canvas_item_reset_physics_interpolation(RID p_item) {
+	Item *canvas_item = canvas_item_owner.getornull(p_item);
+	ERR_FAIL_COND(!canvas_item);
+	canvas_item->xform_prev = canvas_item->xform_curr;
+}
+
+void VisualServerCanvas::canvas_item_set_interpolated(RID p_item, bool p_interpolated) {
+	Item *canvas_item = canvas_item_owner.getornull(p_item);
+	ERR_FAIL_COND(!canvas_item);
+	canvas_item->interpolated = p_interpolated;
+}
+
+void VisualServerCanvas::canvas_light_set_interpolated(RID p_light, bool p_interpolated) {
+	RasterizerCanvas::Light *clight = canvas_light_owner.get(p_light);
+	ERR_FAIL_COND(!clight);
+	clight->interpolated = p_interpolated;
+}
+
+void VisualServerCanvas::canvas_light_reset_physics_interpolation(RID p_light) {
+	RasterizerCanvas::Light *clight = canvas_light_owner.get(p_light);
+	ERR_FAIL_COND(!clight);
+	clight->xform_prev = clight->xform_curr;
+}
+
+void VisualServerCanvas::canvas_light_transform_physics_interpolation(RID p_light, Transform2D p_transform) {
+	RasterizerCanvas::Light *clight = canvas_light_owner.get(p_light);
+	ERR_FAIL_COND(!clight);
+	clight->xform_prev = p_transform * clight->xform_prev;
+	clight->xform_curr = p_transform * clight->xform_curr;
+}
+
+void VisualServerCanvas::canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated) {
+	RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_occluder);
+	ERR_FAIL_COND(!occluder);
+	occluder->interpolated = p_interpolated;
+}
+
+void VisualServerCanvas::canvas_light_occluder_reset_physics_interpolation(RID p_occluder) {
+	RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_occluder);
+	ERR_FAIL_COND(!occluder);
+	occluder->xform_prev = occluder->xform_curr;
+}
+
+void VisualServerCanvas::canvas_light_occluder_transform_physics_interpolation(RID p_occluder, Transform2D p_transform) {
+	RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_occluder);
+	ERR_FAIL_COND(!occluder);
+	occluder->xform_prev = p_transform * occluder->xform_prev;
+	occluder->xform_curr = p_transform * occluder->xform_curr;
+}
+
 void VisualServerCanvas::canvas_item_attach_skeleton(RID p_item, RID p_skeleton) {
 	Item *canvas_item = canvas_item_owner.getornull(p_item);
 	ERR_FAIL_COND(!canvas_item);
@@ -1573,7 +1692,16 @@ void VisualServerCanvas::canvas_light_set_transform(RID p_light, const Transform
 	RasterizerCanvas::Light *clight = canvas_light_owner.get(p_light);
 	ERR_FAIL_COND(!clight);
 
-	clight->xform = p_transform;
+	if (_interpolation_data.interpolation_enabled && clight->interpolated) {
+		if (!clight->on_interpolate_transform_list) {
+			_interpolation_data.canvas_light_transform_update_list_curr->push_back(p_light);
+			clight->on_interpolate_transform_list = true;
+		} else {
+			DEV_ASSERT(_interpolation_data.canvas_light_transform_update_list_curr->size());
+		}
+	}
+
+	clight->xform_curr = p_transform;
 }
 void VisualServerCanvas::canvas_light_set_texture(RID p_light, RID p_texture) {
 	RasterizerCanvas::Light *clight = canvas_light_owner.get(p_light);
@@ -1760,7 +1888,16 @@ void VisualServerCanvas::canvas_light_occluder_set_transform(RID p_occluder, con
 	RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_occluder);
 	ERR_FAIL_COND(!occluder);
 
-	occluder->xform = p_xform;
+	if (_interpolation_data.interpolation_enabled && occluder->interpolated) {
+		if (!occluder->on_interpolate_transform_list) {
+			_interpolation_data.canvas_light_occluder_transform_update_list_curr->push_back(p_occluder);
+			occluder->on_interpolate_transform_list = true;
+		} else {
+			DEV_ASSERT(_interpolation_data.canvas_light_occluder_transform_update_list_curr->size());
+		}
+	}
+
+	occluder->xform_curr = p_xform;
 }
 void VisualServerCanvas::canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask) {
 	RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_occluder);
@@ -1871,6 +2008,7 @@ bool VisualServerCanvas::free(RID p_rid) {
 		Item *canvas_item = canvas_item_owner.get(p_rid);
 		ERR_FAIL_COND_V(!canvas_item, true);
 		_make_bound_dirty(canvas_item);
+		_interpolation_data.notify_free_canvas_item(p_rid, *canvas_item);
 
 		if (canvas_item->parent.is_valid()) {
 			if (canvas_owner.owns(canvas_item->parent)) {
@@ -1904,6 +2042,7 @@ bool VisualServerCanvas::free(RID p_rid) {
 	} else if (canvas_light_owner.owns(p_rid)) {
 		RasterizerCanvas::Light *canvas_light = canvas_light_owner.get(p_rid);
 		ERR_FAIL_COND_V(!canvas_light, true);
+		_interpolation_data.notify_free_canvas_light(p_rid, *canvas_light);
 
 		if (canvas_light->canvas.is_valid()) {
 			Canvas *canvas = canvas_owner.get(canvas_light->canvas);
@@ -1924,6 +2063,7 @@ bool VisualServerCanvas::free(RID p_rid) {
 	} else if (canvas_light_occluder_owner.owns(p_rid)) {
 		RasterizerCanvas::LightOccluderInstance *occluder = canvas_light_occluder_owner.get(p_rid);
 		ERR_FAIL_COND_V(!occluder, true);
+		_interpolation_data.notify_free_canvas_light_occluder(p_rid, *occluder);
 
 		if (occluder->polygon.is_valid()) {
 			LightOccluderPolygon *occluder_poly = canvas_light_occluder_polygon_owner.get(occluder->polygon);
@@ -2056,6 +2196,81 @@ void VisualServerCanvas::_print_tree_down(int p_child_id, int p_depth, const Ite
 
 #endif
 
+void VisualServerCanvas::tick() {
+	if (_interpolation_data.interpolation_enabled) {
+		update_interpolation_tick(true);
+	}
+}
+
+void VisualServerCanvas::update_interpolation_tick(bool p_process) {
+#define GODOT_UPDATE_INTERPOLATION_TICK(LIST_PREV, LIST_CURR, TYPE, OWNER_LIST)              \
+	/* Detect any that were on the previous transform list that are no longer active. */     \
+	for (unsigned int n = 0; n < _interpolation_data.LIST_PREV->size(); n++) {               \
+		const RID &rid = (*_interpolation_data.LIST_PREV)[n];                                \
+		TYPE *item = OWNER_LIST.getornull(rid);                                              \
+		/* no longer active? (either the instance deleted or no longer being transformed) */ \
+		if (item && !item->on_interpolate_transform_list) {                                  \
+			item->xform_prev = item->xform_curr;                                             \
+		}                                                                                    \
+	}                                                                                        \
+	/* and now for any in the transform list (being actively interpolated), */               \
+	/* keep the previous transform value up to date and ready for next tick */               \
+	if (p_process) {                                                                         \
+		for (unsigned int n = 0; n < _interpolation_data.LIST_CURR->size(); n++) {           \
+			const RID &rid = (*_interpolation_data.LIST_CURR)[n];                            \
+			TYPE *item = OWNER_LIST.getornull(rid);                                          \
+			if (item) {                                                                      \
+				item->xform_prev = item->xform_curr;                                         \
+				item->on_interpolate_transform_list = false;                                 \
+			}                                                                                \
+		}                                                                                    \
+	}                                                                                        \
+	SWAP(_interpolation_data.LIST_CURR, _interpolation_data.LIST_PREV);                      \
+	_interpolation_data.LIST_CURR->clear();
+
+	GODOT_UPDATE_INTERPOLATION_TICK(canvas_item_transform_update_list_prev, canvas_item_transform_update_list_curr, Item, canvas_item_owner);
+	GODOT_UPDATE_INTERPOLATION_TICK(canvas_light_transform_update_list_prev, canvas_light_transform_update_list_curr, RasterizerCanvas::Light, canvas_light_owner);
+	GODOT_UPDATE_INTERPOLATION_TICK(canvas_light_occluder_transform_update_list_prev, canvas_light_occluder_transform_update_list_curr, RasterizerCanvas::LightOccluderInstance, canvas_light_occluder_owner);
+
+#undef GODOT_UPDATE_INTERPOLATION_TICK
+}
+
+void VisualServerCanvas::InterpolationData::notify_free_canvas_item(RID p_rid, VisualServerCanvas::Item &r_canvas_item) {
+	r_canvas_item.on_interpolate_transform_list = false;
+
+	if (!interpolation_enabled) {
+		return;
+	}
+
+	// If the instance was on any of the lists, remove.
+	canvas_item_transform_update_list_curr->erase_multiple_unordered(p_rid);
+	canvas_item_transform_update_list_prev->erase_multiple_unordered(p_rid);
+}
+
+void VisualServerCanvas::InterpolationData::notify_free_canvas_light(RID p_rid, RasterizerCanvas::Light &r_canvas_light) {
+	r_canvas_light.on_interpolate_transform_list = false;
+
+	if (!interpolation_enabled) {
+		return;
+	}
+
+	// If the instance was on any of the lists, remove.
+	canvas_light_transform_update_list_curr->erase_multiple_unordered(p_rid);
+	canvas_light_transform_update_list_prev->erase_multiple_unordered(p_rid);
+}
+
+void VisualServerCanvas::InterpolationData::notify_free_canvas_light_occluder(RID p_rid, RasterizerCanvas::LightOccluderInstance &r_canvas_light_occluder) {
+	r_canvas_light_occluder.on_interpolate_transform_list = false;
+
+	if (!interpolation_enabled) {
+		return;
+	}
+
+	// If the instance was on any of the lists, remove.
+	canvas_light_occluder_transform_update_list_curr->erase_multiple_unordered(p_rid);
+	canvas_light_occluder_transform_update_list_prev->erase_multiple_unordered(p_rid);
+}
+
 VisualServerCanvas::VisualServerCanvas() {
 	z_list = (RasterizerCanvas::Item **)memalloc(z_range * sizeof(RasterizerCanvas::Item *));
 	z_last_list = (RasterizerCanvas::Item **)memalloc(z_range * sizeof(RasterizerCanvas::Item *));

+ 39 - 3
servers/visual/visual_server_canvas.h

@@ -256,9 +256,13 @@ public:
 	void canvas_item_set_use_parent_material(RID p_item, bool p_enable);
 
 	void canvas_item_attach_skeleton(RID p_item, RID p_skeleton);
+	void _canvas_item_skeleton_moved(RID p_item);
 	void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform);
 	Rect2 _debug_canvas_item_get_rect(RID p_item);
-	void _canvas_item_skeleton_moved(RID p_item);
+
+	void canvas_item_set_interpolated(RID p_item, bool p_interpolated);
+	void canvas_item_reset_physics_interpolation(RID p_item);
+	void canvas_item_transform_physics_interpolation(RID p_item, Transform2D p_transform);
 
 	RID canvas_light_create();
 	void canvas_light_attach_to_canvas(RID p_light, RID p_canvas);
@@ -274,9 +278,7 @@ public:
 	void canvas_light_set_layer_range(RID p_light, int p_min_layer, int p_max_layer);
 	void canvas_light_set_item_cull_mask(RID p_light, int p_mask);
 	void canvas_light_set_item_shadow_cull_mask(RID p_light, int p_mask);
-
 	void canvas_light_set_mode(RID p_light, VS::CanvasLightMode p_mode);
-
 	void canvas_light_set_shadow_enabled(RID p_light, bool p_enabled);
 	void canvas_light_set_shadow_buffer_size(RID p_light, int p_size);
 	void canvas_light_set_shadow_gradient_length(RID p_light, float p_length);
@@ -284,6 +286,10 @@ public:
 	void canvas_light_set_shadow_color(RID p_light, const Color &p_color);
 	void canvas_light_set_shadow_smooth(RID p_light, float p_smooth);
 
+	void canvas_light_set_interpolated(RID p_light, bool p_interpolated);
+	void canvas_light_reset_physics_interpolation(RID p_light);
+	void canvas_light_transform_physics_interpolation(RID p_light, Transform2D p_transform);
+
 	RID canvas_light_occluder_create();
 	void canvas_light_occluder_attach_to_canvas(RID p_occluder, RID p_canvas);
 	void canvas_light_occluder_set_enabled(RID p_occluder, bool p_enabled);
@@ -291,6 +297,10 @@ public:
 	void canvas_light_occluder_set_transform(RID p_occluder, const Transform2D &p_xform);
 	void canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask);
 
+	void canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated);
+	void canvas_light_occluder_reset_physics_interpolation(RID p_occluder);
+	void canvas_light_occluder_transform_physics_interpolation(RID p_occluder, Transform2D p_transform);
+
 	RID canvas_occluder_polygon_create();
 	void canvas_occluder_polygon_set_shape(RID p_occluder_polygon, const PoolVector<Vector2> &p_shape, bool p_closed);
 	void canvas_occluder_polygon_set_shape_as_lines(RID p_occluder_polygon, const PoolVector<Vector2> &p_shape);
@@ -298,6 +308,32 @@ public:
 	void canvas_occluder_polygon_set_cull_mode(RID p_occluder_polygon, VS::CanvasOccluderPolygonCullMode p_mode);
 
 	bool free(RID p_rid);
+
+	// Interpolation
+	void tick();
+	void update_interpolation_tick(bool p_process = true);
+	void set_physics_interpolation_enabled(bool p_enabled) { _interpolation_data.interpolation_enabled = p_enabled; }
+
+	struct InterpolationData {
+		void notify_free_canvas_item(RID p_rid, VisualServerCanvas::Item &r_canvas_item);
+		void notify_free_canvas_light(RID p_rid, RasterizerCanvas::Light &r_canvas_light);
+		void notify_free_canvas_light_occluder(RID p_rid, RasterizerCanvas::LightOccluderInstance &r_canvas_light_occluder);
+
+		LocalVector<RID> canvas_item_transform_update_lists[2];
+		LocalVector<RID> *canvas_item_transform_update_list_curr = &canvas_item_transform_update_lists[0];
+		LocalVector<RID> *canvas_item_transform_update_list_prev = &canvas_item_transform_update_lists[1];
+
+		LocalVector<RID> canvas_light_transform_update_lists[2];
+		LocalVector<RID> *canvas_light_transform_update_list_curr = &canvas_light_transform_update_lists[0];
+		LocalVector<RID> *canvas_light_transform_update_list_prev = &canvas_light_transform_update_lists[1];
+
+		LocalVector<RID> canvas_light_occluder_transform_update_lists[2];
+		LocalVector<RID> *canvas_light_occluder_transform_update_list_curr = &canvas_light_occluder_transform_update_lists[0];
+		LocalVector<RID> *canvas_light_occluder_transform_update_list_prev = &canvas_light_occluder_transform_update_lists[1];
+
+		bool interpolation_enabled = false;
+	} _interpolation_data;
+
 	VisualServerCanvas();
 	~VisualServerCanvas();
 };

+ 14 - 0
servers/visual/visual_server_raster.cpp

@@ -136,6 +136,20 @@ void VisualServerRaster::draw(bool p_swap_buffers, double frame_step) {
 void VisualServerRaster::sync() {
 }
 
+void VisualServerRaster::set_physics_interpolation_enabled(bool p_enabled) {
+	VSG::scene->set_physics_interpolation_enabled(p_enabled);
+	VSG::canvas->set_physics_interpolation_enabled(p_enabled);
+}
+
+void VisualServerRaster::tick() {
+	VSG::scene->tick();
+	VSG::canvas->tick();
+}
+
+void VisualServerRaster::pre_draw(bool p_will_draw) {
+	VSG::scene->pre_draw(p_will_draw);
+}
+
 bool VisualServerRaster::has_changed(ChangedPriority p_priority) const {
 	switch (p_priority) {
 		default: {

+ 20 - 13
servers/visual/visual_server_raster.h

@@ -454,9 +454,6 @@ public:
 
 	/* EVENT QUEUING */
 
-	BIND0N(tick)
-	BIND1N(pre_draw, bool)
-
 	/* CAMERA API */
 
 	BIND0R(RID, camera_create)
@@ -559,10 +556,6 @@ public:
 #undef BINDBASE
 #define BINDBASE VSG::scene
 
-	/* INTERPOLATION */
-
-	BIND1(set_physics_interpolation_enabled, bool)
-
 	/* SCENARIO API */
 
 	BIND0R(RID, scenario_create)
@@ -719,17 +712,19 @@ public:
 	BIND2(canvas_item_set_z_index, RID, int)
 	BIND2(canvas_item_set_z_as_relative_to_parent, RID, bool)
 	BIND3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &)
-	BIND2(canvas_item_attach_skeleton, RID, RID)
-	BIND2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
-	BIND1R(Rect2, _debug_canvas_item_get_rect, RID)
-
 	BIND1(canvas_item_clear, RID)
 	BIND2(canvas_item_set_draw_index, RID, int)
-
 	BIND2(canvas_item_set_material, RID, RID)
-
 	BIND2(canvas_item_set_use_parent_material, RID, bool)
 
+	BIND2(canvas_item_attach_skeleton, RID, RID)
+	BIND2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
+	BIND1R(Rect2, _debug_canvas_item_get_rect, RID)
+
+	BIND2(canvas_item_set_interpolated, RID, bool)
+	BIND1(canvas_item_reset_physics_interpolation, RID)
+	BIND2(canvas_item_transform_physics_interpolation, RID, Transform2D)
+
 	BIND0R(RID, canvas_light_create)
 	BIND2(canvas_light_attach_to_canvas, RID, RID)
 	BIND2(canvas_light_set_enabled, RID, bool)
@@ -754,6 +749,10 @@ public:
 	BIND2(canvas_light_set_shadow_color, RID, const Color &)
 	BIND2(canvas_light_set_shadow_smooth, RID, float)
 
+	BIND2(canvas_light_set_interpolated, RID, bool)
+	BIND1(canvas_light_reset_physics_interpolation, RID)
+	BIND2(canvas_light_transform_physics_interpolation, RID, Transform2D)
+
 	BIND0R(RID, canvas_light_occluder_create)
 	BIND2(canvas_light_occluder_attach_to_canvas, RID, RID)
 	BIND2(canvas_light_occluder_set_enabled, RID, bool)
@@ -761,6 +760,10 @@ public:
 	BIND2(canvas_light_occluder_set_transform, RID, const Transform2D &)
 	BIND2(canvas_light_occluder_set_light_mask, RID, int)
 
+	BIND2(canvas_light_occluder_set_interpolated, RID, bool)
+	BIND1(canvas_light_occluder_reset_physics_interpolation, RID)
+	BIND2(canvas_light_occluder_transform_physics_interpolation, RID, Transform2D)
+
 	BIND0R(RID, canvas_occluder_polygon_create)
 	BIND3(canvas_occluder_polygon_set_shape, RID, const PoolVector<Vector2> &, bool)
 	BIND2(canvas_occluder_polygon_set_shape_as_lines, RID, const PoolVector<Vector2> &)
@@ -780,12 +783,16 @@ public:
 
 	virtual void request_frame_drawn_callback(Object *p_where, const StringName &p_method, const Variant &p_userdata);
 
+	virtual void tick();
+	virtual void pre_draw(bool p_will_draw);
 	virtual void draw(bool p_swap_buffers, double frame_step);
 	virtual void sync();
 	virtual bool has_changed(ChangedPriority p_priority = CHANGED_PRIORITY_ANY) const;
 	virtual void init();
 	virtual void finish();
 
+	virtual void set_physics_interpolation_enabled(bool p_enabled);
+
 	/* STATUS INFORMATION */
 
 	virtual uint64_t get_render_info(RenderInfo p_info);

+ 22 - 6
servers/visual/visual_server_viewport.cpp

@@ -135,7 +135,14 @@ void VisualServerViewport::_draw_viewport(Viewport *p_viewport, ARVRInterface::E
 
 					Vector2 offset = tsize / 2.0;
 					cl->rect_cache = Rect2(-offset + cl->texture_offset, tsize);
-					cl->xform_cache = xf * cl->xform;
+
+					if (!VSG::canvas->_interpolation_data.interpolation_enabled || !cl->interpolated) {
+						cl->xform_cache = xf * cl->xform_curr;
+					} else {
+						real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
+						TransformInterpolator::interpolate_transform_2d(cl->xform_prev, cl->xform_curr, cl->xform_cache, f);
+						cl->xform_cache = xf * cl->xform_cache;
+					}
 
 					if (clip_rect.intersects_transformed(cl->xform_cache, cl->rect_cache)) {
 						cl->filter_next_ptr = lights;
@@ -180,13 +187,22 @@ void VisualServerViewport::_draw_viewport(Viewport *p_viewport, ARVRInterface::E
 				Transform2D xf = _canvas_get_transform(p_viewport, canvas, &E->get(), clip_rect.size);
 
 				for (Set<RasterizerCanvas::LightOccluderInstance *>::Element *F = canvas->occluders.front(); F; F = F->next()) {
-					if (!F->get()->enabled) {
+					RasterizerCanvas::LightOccluderInstance *occluder = F->get();
+					if (!occluder->enabled) {
 						continue;
 					}
-					F->get()->xform_cache = xf * F->get()->xform;
-					if (shadow_rect.intersects_transformed(F->get()->xform_cache, F->get()->aabb_cache)) {
-						F->get()->next = occluders;
-						occluders = F->get();
+
+					if (!VSG::canvas->_interpolation_data.interpolation_enabled || !occluder->interpolated) {
+						occluder->xform_cache = xf * occluder->xform_curr;
+					} else {
+						real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
+						TransformInterpolator::interpolate_transform_2d(occluder->xform_prev, occluder->xform_curr, occluder->xform_cache, f);
+						occluder->xform_cache = xf * occluder->xform_cache;
+					}
+
+					if (shadow_rect.intersects_transformed(occluder->xform_cache, occluder->aabb_cache)) {
+						occluder->next = occluders;
+						occluders = occluder;
 					}
 				}
 			}

+ 24 - 0
servers/visual/visual_server_wrap_mt.cpp

@@ -70,6 +70,30 @@ void VisualServerWrapMT::thread_loop() {
 
 /* EVENT QUEUING */
 
+void VisualServerWrapMT::set_physics_interpolation_enabled(bool p_enabled) {
+	if (Thread::get_caller_id() != server_thread) {
+		command_queue.push(visual_server, &VisualServer::set_physics_interpolation_enabled, p_enabled);
+	} else {
+		visual_server->set_physics_interpolation_enabled(p_enabled);
+	}
+}
+
+void VisualServerWrapMT::tick() {
+	if (Thread::get_caller_id() != server_thread) {
+		command_queue.push(visual_server, &VisualServer::tick);
+	} else {
+		visual_server->tick();
+	}
+}
+
+void VisualServerWrapMT::pre_draw(bool p_will_draw) {
+	if (Thread::get_caller_id() != server_thread) {
+		command_queue.push(visual_server, &VisualServer::pre_draw, p_will_draw);
+	} else {
+		visual_server->pre_draw(p_will_draw);
+	}
+}
+
 void VisualServerWrapMT::sync() {
 	if (create_thread) {
 		command_queue.push_and_sync(this, &VisualServerWrapMT::thread_flush);

+ 19 - 12
servers/visual/visual_server_wrap_mt.h

@@ -464,10 +464,6 @@ public:
 	FUNC7(environment_set_fog_depth, RID, bool, float, float, float, bool, float)
 	FUNC5(environment_set_fog_height, RID, bool, float, float, float)
 
-	/* INTERPOLATION API */
-
-	FUNC1(set_physics_interpolation_enabled, bool)
-
 	/* SCENARIO API */
 
 	FUNCRID(scenario)
@@ -620,17 +616,19 @@ public:
 	FUNC2(canvas_item_set_z_index, RID, int)
 	FUNC2(canvas_item_set_z_as_relative_to_parent, RID, bool)
 	FUNC3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &)
-	FUNC2(canvas_item_attach_skeleton, RID, RID)
-	FUNC2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
-	FUNC1R(Rect2, _debug_canvas_item_get_rect, RID)
-
 	FUNC1(canvas_item_clear, RID)
 	FUNC2(canvas_item_set_draw_index, RID, int)
-
 	FUNC2(canvas_item_set_material, RID, RID)
-
 	FUNC2(canvas_item_set_use_parent_material, RID, bool)
 
+	FUNC2(canvas_item_attach_skeleton, RID, RID)
+	FUNC2(canvas_item_set_skeleton_relative_xform, RID, Transform2D)
+	FUNC1R(Rect2, _debug_canvas_item_get_rect, RID)
+
+	FUNC2(canvas_item_set_interpolated, RID, bool)
+	FUNC1(canvas_item_reset_physics_interpolation, RID)
+	FUNC2(canvas_item_transform_physics_interpolation, RID, Transform2D)
+
 	FUNC0R(RID, canvas_light_create)
 	FUNC2(canvas_light_attach_to_canvas, RID, RID)
 	FUNC2(canvas_light_set_enabled, RID, bool)
@@ -655,6 +653,10 @@ public:
 	FUNC2(canvas_light_set_shadow_color, RID, const Color &)
 	FUNC2(canvas_light_set_shadow_smooth, RID, float)
 
+	FUNC2(canvas_light_set_interpolated, RID, bool)
+	FUNC1(canvas_light_reset_physics_interpolation, RID)
+	FUNC2(canvas_light_transform_physics_interpolation, RID, Transform2D)
+
 	FUNCRID(canvas_light_occluder)
 	FUNC2(canvas_light_occluder_attach_to_canvas, RID, RID)
 	FUNC2(canvas_light_occluder_set_enabled, RID, bool)
@@ -662,6 +664,10 @@ public:
 	FUNC2(canvas_light_occluder_set_transform, RID, const Transform2D &)
 	FUNC2(canvas_light_occluder_set_light_mask, RID, int)
 
+	FUNC2(canvas_light_occluder_set_interpolated, RID, bool)
+	FUNC1(canvas_light_occluder_reset_physics_interpolation, RID)
+	FUNC2(canvas_light_occluder_transform_physics_interpolation, RID, Transform2D)
+
 	FUNCRID(canvas_occluder_polygon)
 	FUNC3(canvas_occluder_polygon_set_shape, RID, const PoolVector<Vector2> &, bool)
 	FUNC2(canvas_occluder_polygon_set_shape_as_lines, RID, const PoolVector<Vector2> &)
@@ -683,11 +689,12 @@ public:
 
 	virtual void init();
 	virtual void finish();
+	virtual void tick();
+	virtual void pre_draw(bool p_will_draw);
 	virtual void draw(bool p_swap_buffers, double frame_step);
 	virtual void sync();
-	FUNC0(tick)
-	FUNC1(pre_draw, bool)
 	FUNC1RC(bool, has_changed, ChangedPriority)
+	virtual void set_physics_interpolation_enabled(bool p_enabled);
 
 	/* RENDER INFO */
 

+ 9 - 0
servers/visual_server.cpp

@@ -2198,6 +2198,9 @@ void VisualServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("canvas_item_set_z_index", "item", "z_index"), &VisualServer::canvas_item_set_z_index);
 	ClassDB::bind_method(D_METHOD("canvas_item_set_z_as_relative_to_parent", "item", "enabled"), &VisualServer::canvas_item_set_z_as_relative_to_parent);
 	ClassDB::bind_method(D_METHOD("canvas_item_set_copy_to_backbuffer", "item", "enabled", "rect"), &VisualServer::canvas_item_set_copy_to_backbuffer);
+	ClassDB::bind_method(D_METHOD("canvas_item_set_interpolated", "item", "interpolated"), &VisualServer::canvas_item_set_interpolated);
+	ClassDB::bind_method(D_METHOD("canvas_item_reset_physics_interpolation", "item"), &VisualServer::canvas_item_reset_physics_interpolation);
+	ClassDB::bind_method(D_METHOD("canvas_item_transform_physics_interpolation", "item", "xform"), &VisualServer::canvas_item_transform_physics_interpolation);
 	ClassDB::bind_method(D_METHOD("canvas_item_clear", "item"), &VisualServer::canvas_item_clear);
 	ClassDB::bind_method(D_METHOD("canvas_item_set_draw_index", "item", "index"), &VisualServer::canvas_item_set_draw_index);
 	ClassDB::bind_method(D_METHOD("canvas_item_set_material", "item", "material"), &VisualServer::canvas_item_set_material);
@@ -2224,6 +2227,9 @@ void VisualServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("canvas_light_set_shadow_filter", "light", "filter"), &VisualServer::canvas_light_set_shadow_filter);
 	ClassDB::bind_method(D_METHOD("canvas_light_set_shadow_color", "light", "color"), &VisualServer::canvas_light_set_shadow_color);
 	ClassDB::bind_method(D_METHOD("canvas_light_set_shadow_smooth", "light", "smooth"), &VisualServer::canvas_light_set_shadow_smooth);
+	ClassDB::bind_method(D_METHOD("canvas_light_set_interpolated", "light", "interpolated"), &VisualServer::canvas_light_set_interpolated);
+	ClassDB::bind_method(D_METHOD("canvas_light_reset_physics_interpolation", "light"), &VisualServer::canvas_light_reset_physics_interpolation);
+	ClassDB::bind_method(D_METHOD("canvas_light_transform_physics_interpolation", "light", "xform"), &VisualServer::canvas_light_transform_physics_interpolation);
 
 	ClassDB::bind_method(D_METHOD("canvas_light_occluder_create"), &VisualServer::canvas_light_occluder_create);
 	ClassDB::bind_method(D_METHOD("canvas_light_occluder_attach_to_canvas", "occluder", "canvas"), &VisualServer::canvas_light_occluder_attach_to_canvas);
@@ -2231,6 +2237,9 @@ void VisualServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_polygon", "occluder", "polygon"), &VisualServer::canvas_light_occluder_set_polygon);
 	ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_transform", "occluder", "transform"), &VisualServer::canvas_light_occluder_set_transform);
 	ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_light_mask", "occluder", "mask"), &VisualServer::canvas_light_occluder_set_light_mask);
+	ClassDB::bind_method(D_METHOD("canvas_light_occluder_set_interpolated", "occluder", "interpolated"), &VisualServer::canvas_light_occluder_set_interpolated);
+	ClassDB::bind_method(D_METHOD("canvas_light_occluder_reset_physics_interpolation", "occluder"), &VisualServer::canvas_light_occluder_reset_physics_interpolation);
+	ClassDB::bind_method(D_METHOD("canvas_light_occluder_transform_physics_interpolation", "occluder", "xform"), &VisualServer::canvas_light_occluder_transform_physics_interpolation);
 
 	ClassDB::bind_method(D_METHOD("canvas_occluder_polygon_create"), &VisualServer::canvas_occluder_polygon_create);
 	ClassDB::bind_method(D_METHOD("canvas_occluder_polygon_set_shape", "occluder_polygon", "shape", "closed"), &VisualServer::canvas_occluder_polygon_set_shape);

+ 15 - 6
servers/visual_server.h

@@ -1052,6 +1052,10 @@ public:
 	virtual void canvas_item_set_z_index(RID p_item, int p_z) = 0;
 	virtual void canvas_item_set_z_as_relative_to_parent(RID p_item, bool p_enable) = 0;
 	virtual void canvas_item_set_copy_to_backbuffer(RID p_item, bool p_enable, const Rect2 &p_rect) = 0;
+	virtual void canvas_item_clear(RID p_item) = 0;
+	virtual void canvas_item_set_draw_index(RID p_item, int p_index) = 0;
+	virtual void canvas_item_set_material(RID p_item, RID p_material) = 0;
+	virtual void canvas_item_set_use_parent_material(RID p_item, bool p_enable) = 0;
 
 	virtual void canvas_item_attach_skeleton(RID p_item, RID p_skeleton) = 0;
 	virtual void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform) = 0;
@@ -1065,12 +1069,9 @@ public:
 	}
 	virtual Rect2 _debug_canvas_item_get_rect(RID p_item) = 0;
 
-	virtual void canvas_item_clear(RID p_item) = 0;
-	virtual void canvas_item_set_draw_index(RID p_item, int p_index) = 0;
-
-	virtual void canvas_item_set_material(RID p_item, RID p_material) = 0;
-
-	virtual void canvas_item_set_use_parent_material(RID p_item, bool p_enable) = 0;
+	virtual void canvas_item_set_interpolated(RID p_item, bool p_interpolated) = 0;
+	virtual void canvas_item_reset_physics_interpolation(RID p_item) = 0;
+	virtual void canvas_item_transform_physics_interpolation(RID p_item, Transform2D p_transform) = 0;
 
 	virtual RID canvas_light_create() = 0;
 	virtual void canvas_light_attach_to_canvas(RID p_light, RID p_canvas) = 0;
@@ -1087,6 +1088,10 @@ public:
 	virtual void canvas_light_set_item_cull_mask(RID p_light, int p_mask) = 0;
 	virtual void canvas_light_set_item_shadow_cull_mask(RID p_light, int p_mask) = 0;
 
+	virtual void canvas_light_set_interpolated(RID p_light, bool p_interpolated) = 0;
+	virtual void canvas_light_reset_physics_interpolation(RID p_light) = 0;
+	virtual void canvas_light_transform_physics_interpolation(RID p_light, Transform2D p_transform) = 0;
+
 	enum CanvasLightMode {
 		CANVAS_LIGHT_MODE_ADD,
 		CANVAS_LIGHT_MODE_SUB,
@@ -1119,6 +1124,10 @@ public:
 	virtual void canvas_light_occluder_set_transform(RID p_occluder, const Transform2D &p_xform) = 0;
 	virtual void canvas_light_occluder_set_light_mask(RID p_occluder, int p_mask) = 0;
 
+	virtual void canvas_light_occluder_set_interpolated(RID p_occluder, bool p_interpolated) = 0;
+	virtual void canvas_light_occluder_reset_physics_interpolation(RID p_occluder) = 0;
+	virtual void canvas_light_occluder_transform_physics_interpolation(RID p_occluder, Transform2D p_transform) = 0;
+
 	virtual RID canvas_occluder_polygon_create() = 0;
 	virtual void canvas_occluder_polygon_set_shape(RID p_occluder_polygon, const PoolVector<Vector2> &p_shape, bool p_closed) = 0;
 	virtual void canvas_occluder_polygon_set_shape_as_lines(RID p_occluder_polygon, const PoolVector<Vector2> &p_shape) = 0;