Skip to content

Commit

Permalink
2D Fixed Timestep Interpolation
Browse files Browse the repository at this point in the history
Adds support to canvas items and Camera2D.
  • Loading branch information
lawnjelly committed Jul 24, 2023
1 parent ac5d7dc commit a77f699
Show file tree
Hide file tree
Showing 28 changed files with 732 additions and 140 deletions.
45 changes: 45 additions & 0 deletions core/math/transform_interpolator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,51 @@

#include "transform_interpolator.h"

#include "core/math/transform_2d.h"

void TransformInterpolator::interpolate_transform2D(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);
Expand Down
4 changes: 3 additions & 1 deletion core/math/transform_interpolator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -66,6 +66,7 @@ class TransformInterpolator {
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);
Expand All @@ -75,6 +76,7 @@ class TransformInterpolator {
// 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_transform2D(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);
Expand Down
1 change: 1 addition & 0 deletions core/os/main_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class MainLoop : public Object {
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);
Expand Down
1 change: 1 addition & 0 deletions doc/classes/Control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
78 changes: 78 additions & 0 deletions doc/classes/VisualServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand All @@ -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" />
Expand All @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
94 changes: 78 additions & 16 deletions scene/2d/camera_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -179,7 +194,11 @@ Transform2D Camera2D::get_camera_transform() {
}

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 {
Expand Down Expand Up @@ -234,17 +253,52 @@ 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();
// DO NOT get_camera_transform here to fill curr.
// This is because INTERNAL_PHYSICS_PROCESS happens too early,
// the user may move the camera during PHYSICS_PROCESS, or it may
// move due to transforms propagating in the tree.
// We therefore want to only update current AFTER it has been set,
// which can be triggered at NOTIFICATION_TRANFORM_CHANGED.
} else {
_update_scroll();
}
} break;
case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
_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: {
Expand Down Expand Up @@ -400,10 +454,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();
}
}

Expand All @@ -413,6 +472,7 @@ void Camera2D::_set_current(bool p_current) {
}

current = p_current;
_update_process_mode();
update();
}

Expand All @@ -427,13 +487,15 @@ 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() {
current = false;
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) {
Expand Down

0 comments on commit a77f699

Please sign in to comment.