diff --git a/core/math/transform_interpolator.cpp b/core/math/transform_interpolator.cpp
index 7cfe880b5a59..3baaa49afbfb 100644
--- a/core/math/transform_interpolator.cpp
+++ b/core/math/transform_interpolator.cpp
@@ -31,6 +31,7 @@
#include "transform_interpolator.h"
#include "core/math/transform_2d.h"
+#include "core/math/transform_3d.h"
void TransformInterpolator::interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction) {
// Extract parameters.
@@ -74,3 +75,340 @@ void TransformInterpolator::interpolate_transform_2d(const Transform2D &p_prev,
r_result = Transform2D(Math::atan2(v.y, v.x), p1.lerp(p2, p_fraction));
r_result.scale_basis(s1.lerp(s2, p_fraction));
}
+
+void TransformInterpolator::interpolate_transform_3d(const Transform3D &p_prev, const Transform3D &p_curr, Transform3D &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);
+}
+
+void TransformInterpolator::interpolate_basis(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction) {
+ Method method = find_method(p_prev, p_curr);
+ interpolate_basis_via_method(p_prev, p_curr, r_result, p_fraction, method);
+}
+
+void TransformInterpolator::interpolate_transform_3d_via_method(const Transform3D &p_prev, const Transform3D &p_curr, Transform3D &r_result, real_t p_fraction, TransformInterpolator::Method p_method) {
+ r_result.origin = p_prev.origin + ((p_curr.origin - p_prev.origin) * p_fraction);
+ interpolate_basis_via_method(p_prev.basis, p_curr.basis, r_result.basis, p_fraction, p_method);
+}
+
+void TransformInterpolator::interpolate_basis_via_method(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction, TransformInterpolator::Method p_method) {
+ switch (p_method) {
+ default: {
+ interpolate_basis_linear(p_prev, p_curr, r_result, p_fraction);
+ } break;
+ case INTERP_SLERP: {
+ r_result = _basis_slerp_unchecked(p_prev, p_curr, p_fraction);
+ } break;
+ case INTERP_SCALED_SLERP: {
+ interpolate_basis_scaled_slerp(p_prev, p_curr, r_result, p_fraction);
+ } break;
+ }
+}
+
+Quaternion TransformInterpolator::_basis_to_quat_unchecked(const Basis &p_basis) {
+ Basis m = p_basis;
+ real_t trace = m.rows[0][0] + m.rows[1][1] + m.rows[2][2];
+ real_t temp[4];
+
+ if (trace > 0.0) {
+ real_t s = Math::sqrt(trace + 1.0f);
+ temp[3] = (s * 0.5f);
+ s = 0.5f / s;
+
+ temp[0] = ((m.rows[2][1] - m.rows[1][2]) * s);
+ temp[1] = ((m.rows[0][2] - m.rows[2][0]) * s);
+ temp[2] = ((m.rows[1][0] - m.rows[0][1]) * s);
+ } else {
+ int i = m.rows[0][0] < m.rows[1][1]
+ ? (m.rows[1][1] < m.rows[2][2] ? 2 : 1)
+ : (m.rows[0][0] < m.rows[2][2] ? 2 : 0);
+ int j = (i + 1) % 3;
+ int k = (i + 2) % 3;
+
+ real_t s = Math::sqrt(m.rows[i][i] - m.rows[j][j] - m.rows[k][k] + 1.0f);
+ temp[i] = s * 0.5f;
+ s = 0.5f / s;
+
+ temp[3] = (m.rows[k][j] - m.rows[j][k]) * s;
+ temp[j] = (m.rows[j][i] + m.rows[i][j]) * s;
+ temp[k] = (m.rows[k][i] + m.rows[i][k]) * s;
+ }
+
+ return Quaternion(temp[0], temp[1], temp[2], temp[3]);
+}
+
+Quaternion TransformInterpolator::_quat_slerp_unchecked(const Quaternion &p_from, const Quaternion &p_to, real_t p_fraction) {
+ Quaternion to1;
+ real_t omega, cosom, sinom, scale0, scale1;
+
+ // Calculate cosine.
+ cosom = p_from.dot(p_to);
+
+ // Adjust signs (if necessary)
+ if (cosom < 0.0f) {
+ cosom = -cosom;
+ to1.x = -p_to.x;
+ to1.y = -p_to.y;
+ to1.z = -p_to.z;
+ to1.w = -p_to.w;
+ } else {
+ to1.x = p_to.x;
+ to1.y = p_to.y;
+ to1.z = p_to.z;
+ to1.w = p_to.w;
+ }
+
+ // Calculate coefficients.
+
+ // This check could possibly be removed as we dealt with this
+ // case in the find_method() function, but is left for safety, it probably
+ // isn't a bottleneck.
+ if ((1.0f - cosom) > (real_t)CMP_EPSILON) {
+ // standard case (slerp)
+ omega = Math::acos(cosom);
+ sinom = Math::sin(omega);
+ scale0 = Math::sin((1.0f - p_fraction) * omega) / sinom;
+ scale1 = Math::sin(p_fraction * omega) / sinom;
+ } else {
+ // "from" and "to" quaternions are very close
+ // ... so we can do a linear interpolation
+ scale0 = 1.0f - p_fraction;
+ scale1 = p_fraction;
+ }
+ // Calculate final values.
+ return Quaternion(
+ scale0 * p_from.x + scale1 * to1.x,
+ scale0 * p_from.y + scale1 * to1.y,
+ scale0 * p_from.z + scale1 * to1.z,
+ scale0 * p_from.w + scale1 * to1.w);
+}
+
+Basis TransformInterpolator::_basis_slerp_unchecked(Basis p_from, Basis p_to, real_t p_fraction) {
+ Quaternion from = _basis_to_quat_unchecked(p_from);
+ Quaternion to = _basis_to_quat_unchecked(p_to);
+
+ Basis b(_quat_slerp_unchecked(from, to, p_fraction));
+ return b;
+}
+
+void TransformInterpolator::interpolate_basis_scaled_slerp(Basis p_prev, Basis p_curr, Basis &r_result, real_t p_fraction) {
+ // Normalize both and find lengths.
+ Vector3 lengths_prev = _basis_orthonormalize(p_prev);
+ Vector3 lengths_curr = _basis_orthonormalize(p_curr);
+
+ r_result = _basis_slerp_unchecked(p_prev, p_curr, p_fraction);
+
+ // Now the result is unit length basis, we need to scale.
+ Vector3 lengths_lerped = lengths_prev + ((lengths_curr - lengths_prev) * p_fraction);
+
+ // Keep a note that the column / row order of the basis is weird,
+ // so keep an eye for bugs with this.
+ r_result[0] *= lengths_lerped;
+ r_result[1] *= lengths_lerped;
+ r_result[2] *= lengths_lerped;
+}
+
+void TransformInterpolator::interpolate_basis_linear(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction) {
+ // Interpolate basis.
+ r_result = p_prev.lerp(p_curr, p_fraction);
+
+ // It turns out we need to guard against zero scale basis.
+ // This is kind of silly, as we should probably fix the bugs elsewhere in Godot that can't deal with
+ // zero scale, but until that time...
+ for (int n = 0; n < 3; n++) {
+ Vector3 &axis = r_result[n];
+
+ // Not ok, this could cause errors due to bugs elsewhere,
+ // so we will bodge set this to a small value.
+ const real_t smallest = 0.0001f;
+ const real_t smallest_squared = smallest * smallest;
+ if (axis.length_squared() < smallest_squared) {
+ // Setting a different component to the smallest
+ // helps prevent the situation where all the axes are pointing in the same direction,
+ // which could be a problem for e.g. cross products...
+ axis[n] = smallest;
+ }
+ }
+}
+
+// Returns length.
+real_t TransformInterpolator::_vec3_normalize(Vector3 &p_vec) {
+ real_t lengthsq = p_vec.length_squared();
+ if (lengthsq == 0.0f) {
+ p_vec.x = p_vec.y = p_vec.z = 0.0f;
+ return 0.0f;
+ }
+ real_t length = Math::sqrt(lengthsq);
+ p_vec.x /= length;
+ p_vec.y /= length;
+ p_vec.z /= length;
+ return length;
+}
+
+// Returns lengths.
+Vector3 TransformInterpolator::_basis_orthonormalize(Basis &r_basis) {
+ // Gram-Schmidt Process.
+
+ Vector3 x = r_basis.get_column(0);
+ Vector3 y = r_basis.get_column(1);
+ Vector3 z = r_basis.get_column(2);
+
+ Vector3 lengths;
+
+ lengths.x = _vec3_normalize(x);
+ y = (y - x * (x.dot(y)));
+ lengths.y = _vec3_normalize(y);
+ z = (z - x * (x.dot(z)) - y * (y.dot(z)));
+ lengths.z = _vec3_normalize(z);
+
+ r_basis.set_column(0, x);
+ r_basis.set_column(1, y);
+ r_basis.set_column(2, z);
+
+ return lengths;
+}
+
+TransformInterpolator::Method TransformInterpolator::_test_basis(Basis p_basis, bool r_needed_normalize, Quaternion &r_quat) {
+ // Axis lengths.
+ Vector3 al = Vector3(p_basis.get_column(0).length_squared(),
+ p_basis.get_column(1).length_squared(),
+ p_basis.get_column(2).length_squared());
+
+ // Non unit scale?
+ if (r_needed_normalize || !_vec3_is_equal_approx(al, Vector3(1.0, 1.0, 1.0), (real_t)0.001f)) {
+ // If the basis is not normalized (at least approximately), it will fail the checks needed for slerp.
+ // So we try to detect a scaled (but not sheared) basis, which we *can* slerp by normalizing first,
+ // and lerping the scales separately.
+
+ // If any of the axes are really small, it is unlikely to be a valid rotation, or is scaled too small to deal with float error.
+ const real_t sl_epsilon = 0.00001f;
+ if ((al.x < sl_epsilon) ||
+ (al.y < sl_epsilon) ||
+ (al.z < sl_epsilon)) {
+ return INTERP_LERP;
+ }
+
+ // Normalize the basis.
+ Basis norm_basis = p_basis;
+
+ al.x = Math::sqrt(al.x);
+ al.y = Math::sqrt(al.y);
+ al.z = Math::sqrt(al.z);
+
+ norm_basis.set_column(0, norm_basis.get_column(0) / al.x);
+ norm_basis.set_column(1, norm_basis.get_column(1) / al.y);
+ norm_basis.set_column(2, norm_basis.get_column(2) / al.z);
+
+ // This doesn't appear necessary, as the later checks will catch it.
+ // if (!_basis_is_orthogonal_any_scale(norm_basis)) {
+ // return INTERP_LERP;
+ // }
+
+ p_basis = norm_basis;
+
+ // Orthonormalize not necessary as normal normalization(!) works if the
+ // axes are orthonormal.
+ // p_basis.orthonormalize();
+
+ // If we needed to normalize one of the two bases, we will need to normalize both,
+ // regardless of whether the 2nd needs it, just to make sure it takes the path to return
+ // INTERP_SCALED_LERP on the 2nd call of _test_basis.
+ r_needed_normalize = true;
+ }
+
+ // Apply less stringent tests than the built in slerp, the standard Godot slerp
+ // is too susceptible to float error to be useful.
+ real_t det = p_basis.determinant();
+ if (!Math::is_equal_approx(det, 1, (real_t)0.01f)) {
+ return INTERP_LERP;
+ }
+
+ if (!_basis_is_orthogonal(p_basis)) {
+ return INTERP_LERP;
+ }
+
+ // TODO: This could possibly be less stringent too, check this.
+ r_quat = _basis_to_quat_unchecked(p_basis);
+ if (!r_quat.is_normalized()) {
+ return INTERP_LERP;
+ }
+
+ return r_needed_normalize ? INTERP_SCALED_SLERP : INTERP_SLERP;
+}
+
+// This check doesn't seem to be needed but is preserved in case of bugs.
+bool TransformInterpolator::_basis_is_orthogonal_any_scale(const Basis &p_basis) {
+ Vector3 cross = p_basis.get_column(0).cross(p_basis.get_column(1));
+ real_t l = _vec3_normalize(cross);
+ // Too small numbers, revert to lerp.
+ if (l < 0.001f) {
+ return false;
+ }
+
+ const real_t epsilon = 0.9995f;
+
+ real_t dot = cross.dot(p_basis.get_column(2));
+ if (dot < epsilon) {
+ return false;
+ }
+
+ cross = p_basis.get_column(1).cross(p_basis.get_column(2));
+ l = _vec3_normalize(cross);
+ // Too small numbers, revert to lerp.
+ if (l < 0.001f) {
+ return false;
+ }
+
+ dot = cross.dot(p_basis.get_column(0));
+ if (dot < epsilon) {
+ return false;
+ }
+
+ return true;
+}
+
+bool TransformInterpolator::_basis_is_orthogonal(const Basis &p_basis, real_t p_epsilon) {
+ Basis identity;
+ Basis m = p_basis * p_basis.transposed();
+
+ // Less stringent tests than the standard Godot slerp.
+ if (!_vec3_is_equal_approx(m[0], identity[0], p_epsilon) || !_vec3_is_equal_approx(m[1], identity[1], p_epsilon) || !_vec3_is_equal_approx(m[2], identity[2], p_epsilon)) {
+ return false;
+ }
+ return true;
+}
+
+real_t TransformInterpolator::checksum_transform_3d(const Transform3D &p_transform) {
+ // just a really basic checksum, this can probably be improved
+ real_t sum = _vec3_sum(p_transform.origin);
+ sum -= _vec3_sum(p_transform.basis.rows[0]);
+ sum += _vec3_sum(p_transform.basis.rows[1]);
+ sum -= _vec3_sum(p_transform.basis.rows[2]);
+ return sum;
+}
+
+TransformInterpolator::Method TransformInterpolator::find_method(const Basis &p_a, const Basis &p_b) {
+ bool needed_normalize = false;
+
+ Quaternion q0;
+ Method method = _test_basis(p_a, needed_normalize, q0);
+ if (method == INTERP_LERP) {
+ return method;
+ }
+
+ Quaternion q1;
+ method = _test_basis(p_b, needed_normalize, q1);
+ if (method == INTERP_LERP) {
+ return method;
+ }
+
+ // Are they close together?
+ // Apply the same test that will revert to lerp as is present in the slerp routine.
+ // Calculate cosine.
+ real_t cosom = Math::abs(q0.dot(q1));
+ if ((1.0f - cosom) <= (real_t)CMP_EPSILON) {
+ return INTERP_LERP;
+ }
+
+ return method;
+}
diff --git a/core/math/transform_interpolator.h b/core/math/transform_interpolator.h
index a9bce2bd7fea..cc556707e4c6 100644
--- a/core/math/transform_interpolator.h
+++ b/core/math/transform_interpolator.h
@@ -32,15 +32,64 @@
#define TRANSFORM_INTERPOLATOR_H
#include "core/math/math_defs.h"
+#include "core/math/vector3.h"
+
+// Keep all the functions for fixed timestep interpolation together.
+// There are two stages involved:
+// Finding a method, for determining the interpolation method between two
+// keyframes (which are physics ticks).
+// And applying that pre-determined method.
+
+// Pre-determining the method makes sense because it is expensive and often
+// several frames may occur between each physics tick, which will make it cheaper
+// than performing every frame.
struct Transform2D;
+struct Transform3D;
+struct Basis;
+struct Quaternion;
class TransformInterpolator {
+public:
+ enum Method {
+ INTERP_LERP,
+ INTERP_SLERP,
+ INTERP_SCALED_SLERP,
+ };
+
private:
- static bool _sign(real_t p_val) { return p_val >= 0; }
+ _FORCE_INLINE_ static bool _sign(real_t p_val) { return p_val >= 0; }
+ static real_t _vec3_sum(const Vector3 &p_pt) { return p_pt.x + p_pt.y + p_pt.z; }
+ static real_t _vec3_normalize(Vector3 &p_vec);
+ _FORCE_INLINE_ static bool _vec3_is_equal_approx(const Vector3 &p_a, const Vector3 &p_b, real_t p_tolerance) {
+ return Math::is_equal_approx(p_a.x, p_b.x, p_tolerance) && Math::is_equal_approx(p_a.y, p_b.y, p_tolerance) && Math::is_equal_approx(p_a.z, p_b.z, p_tolerance);
+ }
+ static Vector3 _basis_orthonormalize(Basis &r_basis);
+ static Method _test_basis(Basis p_basis, bool r_needed_normalize, Quaternion &r_quat);
+ static Basis _basis_slerp_unchecked(Basis p_from, Basis p_to, real_t p_fraction);
+ static Quaternion _quat_slerp_unchecked(const Quaternion &p_from, const Quaternion &p_to, real_t p_fraction);
+ static Quaternion _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 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);
public:
static void interpolate_transform_2d(const Transform2D &p_prev, const Transform2D &p_curr, Transform2D &r_result, real_t p_fraction);
+
+ // Generic functions, use when you don't know what method should be used, e.g. from GDScript.
+ // These will be slower.
+ static void interpolate_transform_3d(const Transform3D &p_prev, const Transform3D &p_curr, Transform3D &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);
+
+ // Optimized function when you know ahead of time the method.
+ static void interpolate_transform_3d_via_method(const Transform3D &p_prev, const Transform3D &p_curr, Transform3D &r_result, real_t p_fraction, Method p_method);
+ static void interpolate_basis_via_method(const Basis &p_prev, const Basis &p_curr, Basis &r_result, real_t p_fraction, Method p_method);
+
+ static real_t checksum_transform_3d(const Transform3D &p_transform);
+
+ static Method find_method(const Basis &p_a, const Basis &p_b);
};
#endif // TRANSFORM_INTERPOLATOR_H
diff --git a/core/os/main_loop.h b/core/os/main_loop.h
index e48541d07415..9c22cbaf3c2d 100644
--- a/core/os/main_loop.h
+++ b/core/os/main_loop.h
@@ -64,6 +64,7 @@ class MainLoop : public Object {
virtual void initialize();
virtual void iteration_prepare() {}
virtual bool physics_process(double p_time);
+ virtual void iteration_end() {}
virtual bool process(double p_time);
virtual void finalize();
diff --git a/doc/classes/Node3D.xml b/doc/classes/Node3D.xml
index 125c7ef3eec8..cb068d67ba33 100644
--- a/doc/classes/Node3D.xml
+++ b/doc/classes/Node3D.xml
@@ -46,6 +46,13 @@
Returns all the gizmos attached to this [Node3D].
+
+
+
+ When using physics interpolation, there will be circumstances in which you want to know the interpolated (displayed) transform of a node rather than the standard transform (which may only be accurate to the most recent physics tick).
+ This is particularly important for frame-based operations that take place in [method Node._process], rather than [method Node._physics_process]. Examples include [Camera3D]s focusing on a node, or finding where to fire lasers from on a frame rather than physics tick.
+
+
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 5ac4c96d931c..e5450d865935 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -617,6 +617,10 @@
Maximum call stack allowed for debugging GDScript.
+
+ If [code]true[/code], enables warnings which can help pinpoint where nodes are being incorrectly updated, which will result in incorrect interpolation and visual glitches.
+ When a node is being interpolated, it is essential that the transform is set during [method Node._physics_process] (during a physics tick) rather than [method Node._process] (during a frame).
+
Maximum number of functions per frame allowed when profiling.
@@ -2302,7 +2306,8 @@
Controls how much physics ticks are synchronized with real time. For 0 or less, the ticks are synchronized. Such values are recommended for network games, where clock synchronization matters. Higher values cause higher deviation of in-game clock and real clock, but allows smoothing out framerate jitters. The default value of 0.5 should be good enough for most; values above 2 could cause the game to react to dropped frames with a noticeable delay and are not recommended.
- [b]Note:[/b] When using a physics interpolation solution (such as enabling [member physics/common/physics_interpolation] or using a custom solution), the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0.0[/code].
+ [b]Note:[/b] Jitter fix is automatically disabled at runtime when [member physics/common/physics_interpolation] is enabled.
+ [b]Note:[/b] When using a custom physics interpolation solution, the physics jitter fix should be disabled by setting [member physics/common/physics_jitter_fix] to [code]0.0[/code].
[b]Note:[/b] This property is only read when the project starts. To change the physics jitter fix at runtime, set [member Engine.physics_jitter_fix] instead.
diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml
index 3ddc0d8f7b1a..07fb183bef99 100644
--- a/doc/classes/RenderingServer.xml
+++ b/doc/classes/RenderingServer.xml
@@ -110,6 +110,14 @@
[b]Note:[/b] The equivalent node is [Camera3D].
+
+
+
+
+ Prevents physics interpolation for the current physics tick.
+ This is useful when moving a camera to a new location, to give an instantaneous change rather than interpolation from the previous location.
+
+
@@ -153,6 +161,14 @@
Sets camera to use frustum projection. This mode allows adjusting the [param offset] argument to create "tilted frustum" effects.
+
+
+
+
+
+ Turns on and off physics interpolation for the camera.
+
+
@@ -1855,6 +1871,14 @@
Sets the visibility range values for the given geometry instance. Equivalent to [member GeometryInstance3D.visibility_range_begin] and related properties.
+
+
+
+
+ Prevents physics interpolation for the current physics tick.
+ This is useful when moving an instance to a new location, to give an instantaneous change rather than interpolation from the previous location.
+
+
@@ -1896,6 +1920,14 @@
If [code]true[/code], ignores both frustum and occlusion culling on the specified 3D geometry instance. This is not the same as [member GeometryInstance3D.ignore_occlusion_culling], which only ignores occlusion culling and leaves frustum culling intact.
+
+
+
+
+
+ Turns on and off physics interpolation for the instance.
+
+
diff --git a/doc/classes/Viewport.xml b/doc/classes/Viewport.xml
index b288ee7ff63a..f57185ae872c 100644
--- a/doc/classes/Viewport.xml
+++ b/doc/classes/Viewport.xml
@@ -301,6 +301,7 @@
If [code]true[/code], the viewport will use a unique copy of the [World3D] defined in [member world_3d].
+
If [code]true[/code], the objects rendered by viewport become subjects of mouse picking process.
[b]Note:[/b] The number of simultaneously pickable objects is limited to 64 and they are selected in a non-deterministic order, which can be different in each picking process.
diff --git a/main/main.cpp b/main/main.cpp
index 20ffdf5ae19b..100ea5d340a9 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -2355,6 +2355,7 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
GLOBAL_DEF("debug/settings/stdout/print_fps", false);
GLOBAL_DEF("debug/settings/stdout/print_gpu_profile", false);
GLOBAL_DEF("debug/settings/stdout/verbose_stdout", false);
+ GLOBAL_DEF("debug/settings/physics_interpolation/enable_warnings", true);
if (!OS::get_singleton()->_verbose_stdout) { // Not manually overridden.
OS::get_singleton()->_verbose_stdout = GLOBAL_GET("debug/settings/stdout/verbose_stdout");
@@ -3998,16 +3999,16 @@ bool Main::iteration() {
uint64_t physics_begin = OS::get_singleton()->get_ticks_usec();
-#ifndef _3D_DISABLED
- PhysicsServer3D::get_singleton()->sync();
- PhysicsServer3D::get_singleton()->flush_queries();
-#endif // _3D_DISABLED
-
// Prepare the fixed timestep interpolated nodes BEFORE they are updated
// by the physics server, otherwise the current and previous transforms
// may be the same, and no interpolation takes place.
OS::get_singleton()->get_main_loop()->iteration_prepare();
+#ifndef _3D_DISABLED
+ PhysicsServer3D::get_singleton()->sync();
+ PhysicsServer3D::get_singleton()->flush_queries();
+#endif // _3D_DISABLED
+
PhysicsServer2D::get_singleton()->sync();
PhysicsServer2D::get_singleton()->flush_queries();
@@ -4017,6 +4018,7 @@ bool Main::iteration() {
#endif // _3D_DISABLED
PhysicsServer2D::get_singleton()->end_sync();
+ Engine::get_singleton()->_in_physics = false;
exit = true;
break;
}
@@ -4040,6 +4042,8 @@ bool Main::iteration() {
message_queue->flush();
+ OS::get_singleton()->get_main_loop()->iteration_end();
+
physics_process_ticks = MAX(physics_process_ticks, OS::get_singleton()->get_ticks_usec() - physics_begin); // keep the largest one for reference
physics_process_max = MAX(OS::get_singleton()->get_ticks_usec() - physics_begin, physics_process_max);
Engine::get_singleton()->_physics_frames++;
diff --git a/main/main_timer_sync.cpp b/main/main_timer_sync.cpp
index d358d9fa933e..569930d42777 100644
--- a/main/main_timer_sync.cpp
+++ b/main/main_timer_sync.cpp
@@ -299,17 +299,6 @@ int64_t MainTimerSync::DeltaSmoother::smooth_delta(int64_t p_delta) {
// before advance_core considers changing the physics_steps return from
// the typical values as defined by typical_physics_steps
double MainTimerSync::get_physics_jitter_fix() {
- // Turn off jitter fix when using fixed timestep interpolation.
- // Note this shouldn't be on UNTIL 3d interpolation is implemented,
- // otherwise we will get people making 3d games with the physics_interpolation
- // set to on getting jitter fix disabled unexpectedly.
-#if 0
- if (Engine::get_singleton()->is_physics_interpolation_enabled()) {
- // Would be better to write a simple bypass for jitter fix but this will do to get started.
- return 0.0;
- }
-#endif
-
return Engine::get_singleton()->get_physics_jitter_fix();
}
diff --git a/scene/3d/camera_3d.cpp b/scene/3d/camera_3d.cpp
index 8515aacba7c1..49364724f733 100644
--- a/scene/3d/camera_3d.cpp
+++ b/scene/3d/camera_3d.cpp
@@ -97,6 +97,10 @@ void Camera3D::_update_camera() {
get_viewport()->_camera_3d_transform_changed_notify();
}
+void Camera3D::_physics_interpolated_changed() {
+ RenderingServer::get_singleton()->camera_set_interpolated(camera, is_physics_interpolated());
+}
+
void Camera3D::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_WORLD: {
@@ -125,6 +129,12 @@ void Camera3D::_notification(int p_what) {
}
} break;
+ case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+ if (is_physics_interpolated()) {
+ RenderingServer::get_singleton()->camera_reset_physics_interpolation(camera);
+ }
+ } break;
+
case NOTIFICATION_EXIT_WORLD: {
if (!is_part_of_edited_scene()) {
if (is_current()) {
diff --git a/scene/3d/camera_3d.h b/scene/3d/camera_3d.h
index dbf2ffc1dd06..83fa7c4dc68e 100644
--- a/scene/3d/camera_3d.h
+++ b/scene/3d/camera_3d.h
@@ -103,6 +103,8 @@ class Camera3D : public Node3D {
virtual void _request_camera_update();
void _update_camera_mode();
+ virtual void _physics_interpolated_changed() override;
+
void _notification(int p_what);
void _validate_property(PropertyInfo &p_property) const;
diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp
index 2e08afb30db1..30f5d6f88a8a 100644
--- a/scene/3d/node_3d.cpp
+++ b/scene/3d/node_3d.cpp
@@ -30,6 +30,7 @@
#include "node_3d.h"
+#include "core/math/transform_interpolator.h"
#include "scene/3d/visual_instance_3d.h"
#include "scene/main/viewport.h"
#include "scene/property_utils.h"
@@ -176,6 +177,7 @@ void Node3D::_notification(int p_what) {
data.parent = nullptr;
data.C = nullptr;
_update_visibility_parent(true);
+ _disable_client_physics_interpolation();
} break;
case NOTIFICATION_ENTER_WORLD: {
@@ -226,6 +228,12 @@ void Node3D::_notification(int p_what) {
}
#endif
} break;
+
+ case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+ if (data.client_physics_interpolation_data) {
+ data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
+ }
+ } break;
}
}
@@ -341,6 +349,112 @@ Transform3D Node3D::get_transform() const {
return data.local_transform;
}
+// Return false to timeout and remove from the client interpolation list.
+bool Node3D::update_client_physics_interpolation_data() {
+ if (!is_inside_tree() || !_is_physics_interpolated_client_side()) {
+ return false;
+ }
+
+ ERR_FAIL_NULL_V(data.client_physics_interpolation_data, false);
+ ClientPhysicsInterpolationData &pid = *data.client_physics_interpolation_data;
+
+ uint64_t tick = Engine::get_singleton()->get_physics_frames();
+
+ // Has this update been done already this tick?
+ // (For instance, get_global_transform_interpolated() could be called multiple times.)
+ if (pid.current_physics_tick != tick) {
+ // Timeout?
+ if (tick >= pid.timeout_physics_tick) {
+ return false;
+ }
+
+ if (pid.current_physics_tick == (tick - 1)) {
+ // Normal interpolation situation, there is a continuous flow of data
+ // from one tick to the next...
+ pid.global_xform_prev = pid.global_xform_curr;
+ } else {
+ // There has been a gap, we cannot sensibly offer interpolation over
+ // a multitick gap, so we will teleport.
+ pid.global_xform_prev = get_global_transform();
+ }
+ pid.current_physics_tick = tick;
+ }
+
+ pid.global_xform_curr = get_global_transform();
+ return true;
+}
+
+void Node3D::_disable_client_physics_interpolation() {
+ // Disable any current client side interpolation.
+ // (This can always restart as normal if you later re-attach the node to the SceneTree.)
+ if (data.client_physics_interpolation_data) {
+ memdelete(data.client_physics_interpolation_data);
+ data.client_physics_interpolation_data = nullptr;
+
+ SceneTree *tree = get_tree();
+ if (tree && _client_physics_interpolation_node_3d_list.in_list()) {
+ tree->client_physics_interpolation_remove_node_3d(&_client_physics_interpolation_node_3d_list);
+ }
+ }
+ _set_physics_interpolated_client_side(false);
+}
+
+Transform3D Node3D::_get_global_transform_interpolated(real_t p_interpolation_fraction) {
+ ERR_FAIL_COND_V(is_inside_tree(), Transform3D());
+
+ // Set in motion the mechanisms for client side interpolation if not already active.
+ if (!_is_physics_interpolated_client_side()) {
+ _set_physics_interpolated_client_side(true);
+
+ ERR_FAIL_NULL_V(data.client_physics_interpolation_data, Transform3D());
+ data.client_physics_interpolation_data = memnew(ClientPhysicsInterpolationData);
+ data.client_physics_interpolation_data->global_xform_curr = get_global_transform();
+ data.client_physics_interpolation_data->global_xform_prev = data.client_physics_interpolation_data->global_xform_curr;
+ data.client_physics_interpolation_data->current_physics_tick = Engine::get_singleton()->get_physics_frames();
+ }
+
+ // Storing the last tick we requested client interpolation allows us to timeout
+ // and remove client interpolated nodes from the list to save processing.
+ // We use some arbitrary timeout here, but this could potentially be user defined.
+
+ // Note: This timeout has to be larger than the number of ticks in a frame, otherwise the interpolated
+ // data will stop flowing before the next frame is drawn. This should only be relevant at high tick rates.
+ // We could alternatively do this by frames rather than ticks and avoid this problem, but then the behavior
+ // would be machine dependent.
+ data.client_physics_interpolation_data->timeout_physics_tick = Engine::get_singleton()->get_physics_frames() + 256;
+
+ // Make sure data is up to date.
+ update_client_physics_interpolation_data();
+
+ // Interpolate the current data.
+ const Transform3D &xform_curr = data.client_physics_interpolation_data->global_xform_curr;
+ const Transform3D &xform_prev = data.client_physics_interpolation_data->global_xform_prev;
+
+ Transform3D res;
+ TransformInterpolator::interpolate_transform_3d(xform_prev, xform_curr, res, p_interpolation_fraction);
+
+ SceneTree *tree = get_tree();
+
+ // This should not happen, as is_inside_tree() is checked earlier.
+ ERR_FAIL_NULL_V(tree, res);
+ if (!_client_physics_interpolation_node_3d_list.in_list()) {
+ tree->client_physics_interpolation_add_node_3d(&_client_physics_interpolation_node_3d_list);
+ }
+
+ return res;
+}
+
+Transform3D Node3D::get_global_transform_interpolated() {
+ // Pass through if physics interpolation is switched off.
+ // This is a convenience, as it allows you to easy turn off interpolation
+ // without changing any code.
+ if (Engine::get_singleton()->is_in_physics_frame() || !is_physics_interpolated_and_enabled()) {
+ return get_global_transform();
+ }
+
+ return _get_global_transform_interpolated(Engine::get_singleton()->get_physics_interpolation_fraction());
+}
+
Transform3D Node3D::get_global_transform() const {
ERR_FAIL_COND_V(!is_inside_tree(), Transform3D());
@@ -1140,6 +1254,7 @@ void Node3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_global_transform", "global"), &Node3D::set_global_transform);
ClassDB::bind_method(D_METHOD("get_global_transform"), &Node3D::get_global_transform);
+ ClassDB::bind_method(D_METHOD("get_global_transform_interpolated"), &Node3D::get_global_transform_interpolated);
ClassDB::bind_method(D_METHOD("set_global_position", "position"), &Node3D::set_global_position);
ClassDB::bind_method(D_METHOD("get_global_position"), &Node3D::get_global_position);
ClassDB::bind_method(D_METHOD("set_global_basis", "basis"), &Node3D::set_global_basis);
@@ -1236,4 +1351,27 @@ void Node3D::_bind_methods() {
}
Node3D::Node3D() :
- xform_change(this) {}
+ xform_change(this), _client_physics_interpolation_node_3d_list(this) {
+ // Default member initializer for bitfield is a C++20 extension, so:
+
+ data.top_level = false;
+ data.inside_world = false;
+
+ data.ignore_notification = false;
+ data.notify_local_transform = false;
+ data.notify_transform = false;
+
+ data.visible = true;
+ data.disable_scale = false;
+ data.vi_visible = true;
+
+#ifdef TOOLS_ENABLED
+ data.gizmos_disabled = false;
+ data.gizmos_dirty = false;
+ data.transform_gizmo_visible = true;
+#endif
+}
+
+Node3D::~Node3D() {
+ _disable_client_physics_interpolation();
+}
diff --git a/scene/3d/node_3d.h b/scene/3d/node_3d.h
index c1667221df9f..217ee28cf1ab 100644
--- a/scene/3d/node_3d.h
+++ b/scene/3d/node_3d.h
@@ -85,7 +85,15 @@ class Node3D : public Node {
DIRTY_GLOBAL_TRANSFORM = 4
};
+ struct ClientPhysicsInterpolationData {
+ Transform3D global_xform_curr;
+ Transform3D global_xform_prev;
+ uint64_t current_physics_tick = 0;
+ uint64_t timeout_physics_tick = 0;
+ };
+
mutable SelfList xform_change;
+ SelfList _client_physics_interpolation_node_3d_list;
// This Data struct is to avoid namespace pollution in derived classes.
@@ -101,8 +109,19 @@ class Node3D : public Node {
Viewport *viewport = nullptr;
- bool top_level = false;
- bool inside_world = false;
+ bool top_level : 1;
+ bool inside_world : 1;
+
+ // This is cached, and only currently kept up to date in visual instances.
+ // This is set if a visual instance is (a) in the tree AND (b) visible via is_visible_in_tree() call.
+ bool vi_visible : 1;
+
+ bool ignore_notification : 1;
+ bool notify_local_transform : 1;
+ bool notify_transform : 1;
+
+ bool visible : 1;
+ bool disable_scale : 1;
RID visibility_parent;
@@ -110,18 +129,13 @@ class Node3D : public Node {
List children;
List::Element *C = nullptr;
- bool ignore_notification = false;
- bool notify_local_transform = false;
- bool notify_transform = false;
-
- bool visible = true;
- bool disable_scale = false;
+ ClientPhysicsInterpolationData *client_physics_interpolation_data = nullptr;
#ifdef TOOLS_ENABLED
Vector[> gizmos;
- bool gizmos_disabled = false;
- bool gizmos_dirty = false;
- bool transform_gizmo_visible = true;
+ bool gizmos_disabled : 1;
+ bool gizmos_dirty : 1;
+ bool transform_gizmo_visible : 1;
#endif
} data;
@@ -150,6 +164,11 @@ class Node3D : public Node {
_FORCE_INLINE_ void _update_local_transform() const;
_FORCE_INLINE_ void _update_rotation_and_scale() const;
+ void _set_vi_visible(bool p_visible) { data.vi_visible = p_visible; }
+ bool _is_vi_visible() const { return data.vi_visible; }
+ Transform3D _get_global_transform_interpolated(real_t p_interpolation_fraction);
+ void _disable_client_physics_interpolation();
+
void _notification(int p_what);
static void _bind_methods();
@@ -208,6 +227,9 @@ class Node3D : public Node {
Quaternion get_quaternion() const;
Transform3D get_global_transform() const;
+ Transform3D get_global_transform_interpolated();
+ bool update_client_physics_interpolation_data();
+
#ifdef TOOLS_ENABLED
virtual Transform3D get_global_gizmo_transform() const;
virtual Transform3D get_local_gizmo_transform() const;
@@ -279,6 +301,7 @@ class Node3D : public Node {
NodePath get_visibility_parent() const;
Node3D();
+ ~Node3D();
};
VARIANT_ENUM_CAST(Node3D::RotationEditMode)
diff --git a/scene/3d/skeleton_ik_3d.cpp b/scene/3d/skeleton_ik_3d.cpp
index 78a21ba9e1cf..52781897b0f2 100644
--- a/scene/3d/skeleton_ik_3d.cpp
+++ b/scene/3d/skeleton_ik_3d.cpp
@@ -511,7 +511,11 @@ Transform3D SkeletonIK3D::_get_target_transform() {
Node3D *target_node_override = cast_to(target_node_override_ref.get_validated_object());
if (target_node_override && target_node_override->is_inside_tree()) {
- return target_node_override->get_global_transform();
+ // Make sure to use the interpolated transform as target.
+ // When physics interpolation is off this will pass through to get_global_transform().
+ // When using interpolation, ensure that the target matches the interpolated visual position
+ // of the target when updating the IK each frame.
+ return target_node_override->get_global_transform_interpolated();
} else {
return target;
}
diff --git a/scene/3d/visual_instance_3d.cpp b/scene/3d/visual_instance_3d.cpp
index f14ae3a2856e..380608140419 100644
--- a/scene/3d/visual_instance_3d.cpp
+++ b/scene/3d/visual_instance_3d.cpp
@@ -30,6 +30,8 @@
#include "visual_instance_3d.h"
+#include "core/config/project_settings.h"
+
AABB VisualInstance3D::get_aabb() const {
AABB ret;
GDVIRTUAL_CALL(_get_aabb, ret);
@@ -41,7 +43,38 @@ void VisualInstance3D::_update_visibility() {
return;
}
- RS::get_singleton()->instance_set_visible(get_instance(), is_visible_in_tree());
+ bool already_visible = _is_vi_visible();
+ bool visible = is_visible_in_tree();
+ _set_vi_visible(visible);
+
+ // If making visible, make sure the rendering server is up to date with the transform.
+ if (visible && !already_visible) {
+ if (!_is_using_identity_transform()) {
+ Transform3D gt = get_global_transform();
+ RS::get_singleton()->instance_set_transform(instance, gt);
+ }
+ }
+
+ RS::get_singleton()->instance_set_visible(instance, visible);
+}
+
+void VisualInstance3D::_physics_interpolated_changed() {
+ RenderingServer::get_singleton()->instance_set_interpolated(instance, is_physics_interpolated());
+}
+
+void VisualInstance3D::set_instance_use_identity_transform(bool p_enable) {
+ // Prevent sending instance transforms when using global coordinates.
+ _set_use_identity_transform(p_enable);
+
+ if (is_inside_tree()) {
+ if (p_enable) {
+ // Want to make sure instance is using identity transform.
+ RS::get_singleton()->instance_set_transform(instance, Transform3D());
+ } else {
+ // Want to make sure instance is up to date.
+ RS::get_singleton()->instance_set_transform(instance, get_global_transform());
+ }
+ }
}
void VisualInstance3D::_notification(int p_what) {
@@ -53,13 +86,46 @@ void VisualInstance3D::_notification(int p_what) {
} break;
case NOTIFICATION_TRANSFORM_CHANGED: {
- Transform3D gt = get_global_transform();
- RenderingServer::get_singleton()->instance_set_transform(instance, gt);
+ if (_is_vi_visible() || is_physics_interpolated_and_enabled()) {
+ if (!_is_using_identity_transform()) {
+ Transform3D gt = get_global_transform();
+ RenderingServer::get_singleton()->instance_set_transform(instance, gt);
+
+ // For instance when first adding to the tree, when the previous transform is
+ // unset, to prevent streaking from the origin.
+ if (_is_physics_interpolation_reset_requested()) {
+ if (_is_vi_visible()) {
+ _notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
+ }
+ _set_physics_interpolation_reset_requested(false);
+ }
+ }
+ }
+ } break;
+
+ case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
+ if (_is_vi_visible() && is_physics_interpolated()) {
+ RenderingServer::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: {
RenderingServer::get_singleton()->instance_set_scenario(instance, RID());
RenderingServer::get_singleton()->instance_attach_skeleton(instance, RID());
+ _set_vi_visible(false);
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
diff --git a/scene/3d/visual_instance_3d.h b/scene/3d/visual_instance_3d.h
index 59ede26ac1b1..9b02c928b7ac 100644
--- a/scene/3d/visual_instance_3d.h
+++ b/scene/3d/visual_instance_3d.h
@@ -45,6 +45,9 @@ class VisualInstance3D : public Node3D {
protected:
void _update_visibility();
+ virtual void _physics_interpolated_changed() override;
+ void set_instance_use_identity_transform(bool p_enable);
+
void _notification(int p_what);
static void _bind_methods();
void _validate_property(PropertyInfo &p_property) const;
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index 884fc6de074e..b39fc5f59d53 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -431,6 +431,18 @@ void Node::_propagate_physics_interpolated(bool p_interpolated) {
data.blocked--;
}
+void Node::_propagate_physics_interpolation_reset_requested() {
+ if (is_physics_interpolated()) {
+ data.physics_interpolation_reset_requested = true;
+ }
+
+ data.blocked++;
+ for (KeyValue &K : data.children) {
+ K.value->_propagate_physics_interpolation_reset_requested();
+ }
+ data.blocked--;
+}
+
void Node::move_child(Node *p_child, int p_index) {
ERR_FAIL_COND_MSG(data.inside_tree && !Thread::is_main_thread(), "Moving child node positions inside the SceneTree is only allowed from the main thread. Use call_deferred(\"move_child\",child,index).");
ERR_FAIL_NULL(p_child);
@@ -1551,6 +1563,12 @@ void Node::_add_child_nocheck(Node *p_child, const StringName &p_name, InternalM
add_child_notify(p_child);
notification(NOTIFICATION_CHILD_ORDER_CHANGED);
emit_signal(SNAME("child_order_changed"));
+
+ // Allow physics interpolated nodes to automatically reset when added to the tree
+ // (this is to save the user doing this manually each time).
+ if (is_inside_tree() && get_tree()->is_physics_interpolation_enabled()) {
+ p_child->_propagate_physics_interpolation_reset_requested();
+ }
}
void Node::add_child(Node *p_child, bool p_force_readable_name, InternalMode p_internal) {
@@ -3814,6 +3832,9 @@ Node::Node() {
data.unhandled_key_input = false;
data.physics_interpolated = true;
+ data.physics_interpolation_reset_requested = false;
+ data.physics_interpolated_client_side = false;
+ data.use_identity_transform = false;
data.parent_owned = false;
data.in_constructor = true;
diff --git a/scene/main/node.h b/scene/main/node.h
index 6b937244780c..5e2e01299fa2 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -225,6 +225,21 @@ class Node : public Object {
// is switched on.
bool physics_interpolated : 1;
+ // We can auto-reset physics interpolation when e.g. adding a node for the first time.
+ bool physics_interpolation_reset_requested : 1;
+
+ // Most nodes need not be interpolated in the scene tree, physics interpolation
+ // is normally only needed in the RenderingServer. However if we need to read the
+ // interpolated transform of a node in the SceneTree, it is necessary to duplicate
+ // the interpolation logic client side, in order to prevent stalling the RenderingServer
+ // by reading back.
+ bool physics_interpolated_client_side : 1;
+
+ // For certain nodes (e.g. CPU particles in global mode)
+ // it can be useful to not send the instance transform to the
+ // RenderingServer, and specify the mesh in world space.
+ bool use_identity_transform : 1;
+
bool parent_owned : 1;
bool in_constructor : 1;
bool use_placeholder : 1;
@@ -263,6 +278,7 @@ class Node : public Object {
void _propagate_exit_tree();
void _propagate_after_exit_tree();
void _propagate_physics_interpolated(bool p_interpolated);
+ void _propagate_physics_interpolation_reset_requested();
void _propagate_process_owner(Node *p_owner, int p_pause_notification, int p_enabled_notification);
void _propagate_groups_dirty();
Array _get_node_and_resource(const NodePath &p_path);
@@ -334,6 +350,15 @@ class Node : public Object {
void _set_owner_nocheck(Node *p_owner);
void _set_name_nocheck(const StringName &p_name);
+ void _set_physics_interpolated_client_side(bool p_enable) { data.physics_interpolated_client_side = p_enable; }
+ bool _is_physics_interpolated_client_side() const { return data.physics_interpolated_client_side; }
+
+ void _set_physics_interpolation_reset_requested(bool p_enable) { data.physics_interpolation_reset_requested = p_enable; }
+ bool _is_physics_interpolation_reset_requested() const { return data.physics_interpolation_reset_requested; }
+
+ void _set_use_identity_transform(bool p_enable) { data.use_identity_transform = p_enable; }
+ bool _is_using_identity_transform() const { return data.use_identity_transform; }
+
//call from SceneTree
void _call_input(const Ref &p_event);
void _call_shortcut_input(const Ref &p_event);
diff --git a/scene/main/scene_tree.cpp b/scene/main/scene_tree.cpp
index ced6d9aaa63a..1f2779879e2b 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -59,6 +59,7 @@
#include "servers/navigation_server_3d.h"
#include "servers/physics_server_2d.h"
#ifndef _3D_DISABLED
+#include "scene/3d/node_3d.h"
#include "scene/resources/3d/world_3d.h"
#include "servers/physics_server_3d.h"
#endif // _3D_DISABLED
@@ -118,6 +119,29 @@ void SceneTreeTimer::release_connections() {
SceneTreeTimer::SceneTreeTimer() {}
+#ifndef _3D_DISABLED
+// This should be called once per physics tick, to make sure the transform previous and current
+// is kept up to date on the few Node3Ds that are using client side physics interpolation.
+void SceneTree::ClientPhysicsInterpolation::physics_process() {
+ for (SelfList *E = _node_3d_list.first(); E;) {
+ Node3D *node_3d = E->self();
+
+ SelfList *current = E;
+
+ // Get the next element here BEFORE we potentially delete one.
+ E = E->next();
+
+ // This will return false if the Node3D has timed out ..
+ // i.e. if get_global_transform_interpolated() has not been called
+ // for a few seconds, we can delete from the list to keep processing
+ // to a minimum.
+ if (!node_3d->update_client_physics_interpolation_data()) {
+ _node_3d_list.remove(current);
+ }
+ }
+}
+#endif
+
void SceneTree::tree_changed() {
emit_signal(tree_changed_name);
}
@@ -466,6 +490,18 @@ bool SceneTree::is_physics_interpolation_enabled() const {
return _physics_interpolation_enabled;
}
+#ifndef _3D_DISABLED
+void SceneTree::client_physics_interpolation_add_node_3d(SelfList *p_elem) {
+ // This ensures that _update_physics_interpolation_data() will be called at least once every
+ // physics tick, to ensure the previous and current transforms are kept up to date.
+ _client_physics_interpolation._node_3d_list.add(p_elem);
+}
+
+void SceneTree::client_physics_interpolation_remove_node_3d(SelfList *p_elem) {
+ _client_physics_interpolation._node_3d_list.remove(p_elem);
+}
+#endif
+
void SceneTree::iteration_prepare() {
if (_physics_interpolation_enabled) {
RenderingServer::get_singleton()->tick();
@@ -475,6 +511,13 @@ void SceneTree::iteration_prepare() {
bool SceneTree::physics_process(double p_time) {
current_frame++;
+#ifndef _3D_DISABLED
+ // Any objects performing client physics interpolation
+ // should be given an opportunity to keep their previous transforms
+ // up to take before each new physics tick.
+ _client_physics_interpolation.physics_process();
+#endif
+
flush_transform_notifications();
if (MainLoop::physics_process(p_time)) {
@@ -503,6 +546,14 @@ bool SceneTree::physics_process(double p_time) {
return _quit;
}
+void SceneTree::iteration_end() {
+ // When physics interpolation is active, we want all pending transforms
+ // to be flushed to the RenderingServer before finishing a physics tick.
+ if (_physics_interpolation_enabled) {
+ flush_transform_notifications();
+ }
+}
+
bool SceneTree::process(double p_time) {
if (MainLoop::process(p_time)) {
_quit = true;
@@ -570,6 +621,10 @@ bool SceneTree::process(double p_time) {
#endif // _3D_DISABLED
#endif // TOOLS_ENABLED
+ if (_physics_interpolation_enabled) {
+ RenderingServer::get_singleton()->pre_draw(true);
+ }
+
return _quit;
}
@@ -1761,6 +1816,13 @@ SceneTree::SceneTree() {
set_physics_interpolation_enabled(GLOBAL_DEF("physics/common/physics_interpolation", false));
+ // Always disable jitter fix if physics interpolation is enabled -
+ // Jitter fix will interfere with interpolation, and is not necessary
+ // when interpolation is active.
+ if (is_physics_interpolation_enabled()) {
+ Engine::get_singleton()->set_physics_jitter_fix(0);
+ }
+
// Initialize network state.
set_multiplayer(MultiplayerAPI::create_default_interface());
diff --git a/scene/main/scene_tree.h b/scene/main/scene_tree.h
index 6f0a61ec51ed..7e445411050f 100644
--- a/scene/main/scene_tree.h
+++ b/scene/main/scene_tree.h
@@ -41,6 +41,9 @@
class PackedScene;
class Node;
+#ifndef _3D_DISABLED
+class Node3D;
+#endif
class Window;
class Material;
class Mesh;
@@ -120,6 +123,13 @@ class SceneTree : public MainLoop {
bool changed = false;
};
+#ifndef _3D_DISABLED
+ struct ClientPhysicsInterpolation {
+ SelfList::List _node_3d_list;
+ void physics_process();
+ } _client_physics_interpolation;
+#endif
+
Window *root = nullptr;
double physics_process_time = 0.0;
@@ -315,6 +325,7 @@ class SceneTree : public MainLoop {
virtual void iteration_prepare() override;
virtual bool physics_process(double p_time) override;
+ virtual void iteration_end() override;
virtual bool process(double p_time) override;
virtual void finalize() override;
@@ -423,6 +434,11 @@ class SceneTree : public MainLoop {
void set_physics_interpolation_enabled(bool p_enabled);
bool is_physics_interpolation_enabled() const;
+#ifndef _3D_DISABLED
+ void client_physics_interpolation_add_node_3d(SelfList *p_elem);
+ void client_physics_interpolation_remove_node_3d(SelfList *p_elem);
+#endif
+
SceneTree();
~SceneTree();
};
diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp
index 26128a08ab3a..08ac3dc1076d 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -5015,6 +5015,13 @@ Viewport::Viewport() {
#endif // _3D_DISABLED
set_sdf_oversize(sdf_oversize); // Set to server.
+
+ // Physics interpolation mode for viewports is a special case.
+ // Typically viewports will be housed within Controls,
+ // and Controls default to PHYSICS_INTERPOLATION_MODE_OFF.
+ // Viewports can thus inherit physics interpolation OFF, which is unexpected.
+ // Setting to ON allows each viewport to have a fresh interpolation state.
+ set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_ON);
}
Viewport::~Viewport() {
diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp
index b02d3def88f3..c8180fadb6ba 100644
--- a/servers/rendering/renderer_scene_cull.cpp
+++ b/servers/rendering/renderer_scene_cull.cpp
@@ -38,6 +38,11 @@
#include
+#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+// This is used only to obtain node paths for user-friendly physics interpolation warnings.
+#include "scene/main/node.h"
+#endif
+
/* HALTON SEQUENCE */
#ifndef _3D_DISABLED
@@ -53,8 +58,32 @@ static float get_halton_value(int p_index, int p_base) {
}
#endif // _3D_DISABLED
+/* EVENT QUEUING */
+
+void RendererSceneCull::tick() {
+ if (_interpolation_data.interpolation_enabled) {
+ update_interpolation_tick(true);
+ }
+}
+
+void RendererSceneCull::pre_draw(bool p_will_draw) {
+ if (_interpolation_data.interpolation_enabled) {
+ update_interpolation_frame(p_will_draw);
+ }
+}
+
/* CAMERA API */
+Transform3D RendererSceneCull::Camera::get_transform_interpolated() const {
+ if (!interpolated) {
+ return transform;
+ }
+
+ Transform3D final_xform;
+ TransformInterpolator::interpolate_transform_3d_via_method(transform_prev, transform, final_xform, Engine::get_singleton()->get_physics_interpolation_fraction(), interpolation_method);
+ return final_xform;
+}
+
RID RendererSceneCull::camera_allocate() {
return camera_owner.allocate_rid();
}
@@ -93,7 +122,57 @@ void RendererSceneCull::camera_set_frustum(RID p_camera, float p_size, Vector2 p
void RendererSceneCull::camera_set_transform(RID p_camera, const Transform3D &p_transform) {
Camera *camera = camera_owner.get_or_null(p_camera);
ERR_FAIL_NULL(camera);
+
camera->transform = p_transform.orthonormalized();
+
+ if (_interpolation_data.interpolation_enabled) {
+ if (camera->interpolated) {
+ if (!camera->on_interpolate_transform_list) {
+ _interpolation_data.camera_transform_update_list_curr->push_back(p_camera);
+ camera->on_interpolate_transform_list = true;
+ }
+
+ // Decide on the interpolation method... slerp if possible.
+ camera->interpolation_method = TransformInterpolator::find_method(camera->transform_prev.basis, camera->transform.basis);
+
+#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+ if (!Engine::get_singleton()->is_in_physics_frame()) {
+ // Effectively a WARN_PRINT_ONCE but after a certain number of occurrences.
+ static int32_t warn_count = -256;
+ if ((warn_count == 0) && GLOBAL_GET("debug/settings/physics_interpolation/enable_warnings")) {
+ WARN_PRINT("[Physics interpolation] Camera3D interpolation is being triggered from outside physics process, this might lead to issues (possibly benign).");
+ }
+ warn_count++;
+ }
+#endif
+
+ } else {
+#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+ if (Engine::get_singleton()->is_in_physics_frame()) {
+ static int32_t warn_count = -256;
+ if ((warn_count == 0) && GLOBAL_GET("debug/settings/physics_interpolation/enable_warnings")) {
+ WARN_PRINT("[Physics interpolation] Non-interpolated Camera3D is being triggered from physics process, this might lead to issues (possibly benign).");
+ }
+ warn_count++;
+ }
+#endif
+ }
+ }
+}
+
+void RendererSceneCull::camera_reset_physics_interpolation(RID p_camera) {
+ Camera *camera = camera_owner.get_or_null(p_camera);
+ ERR_FAIL_NULL(camera);
+
+ if (_interpolation_data.interpolation_enabled && camera->interpolated) {
+ _interpolation_data.camera_teleport_list.push_back(p_camera);
+ }
+}
+
+void RendererSceneCull::camera_set_interpolated(RID p_camera, bool p_interpolated) {
+ Camera *camera = camera_owner.get_or_null(p_camera);
+ ERR_FAIL_NULL(camera);
+ camera->interpolated = p_interpolated;
}
void RendererSceneCull::camera_set_cull_mask(RID p_camera, uint32_t p_layers) {
@@ -924,8 +1003,58 @@ void RendererSceneCull::instance_set_transform(RID p_instance, const Transform3D
Instance *instance = instance_owner.get_or_null(p_instance);
ERR_FAIL_NULL(instance);
- if (instance->transform == p_transform) {
- return; //must be checked to avoid worst evil
+ if (!(_interpolation_data.interpolation_enabled && instance->interpolated) || !instance->scenario) {
+ if (instance->transform == p_transform) {
+ return; // Must be checked to avoid worst evil.
+ }
+
+#ifdef DEBUG_ENABLED
+
+ for (int i = 0; i < 4; i++) {
+ const Vector3 &v = i < 3 ? p_transform.basis.rows[i] : p_transform.origin;
+ ERR_FAIL_COND(!v.is_finite());
+ }
+
+#endif
+ instance->transform = p_transform;
+ _instance_queue_update(instance, true);
+
+#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+ if ((_interpolation_data.interpolation_enabled && !instance->interpolated) && (Engine::get_singleton()->is_in_physics_frame())) {
+ static int32_t warn_count = 0;
+ warn_count++;
+ if (((warn_count % 2048) == 0) && GLOBAL_GET("debug/settings/physics_interpolation/enable_warnings")) {
+ String node_name;
+ ObjectID id = instance->object_id;
+ if (id.is_valid()) {
+ if (ObjectDB::get_instance(id)) {
+ Node *node = Object::cast_to(ObjectDB::get_instance(id));
+ if (node && node->is_inside_tree()) {
+ node_name = "\"" + String(node->get_path()) + "\"";
+ } else {
+ node_name = "\"unknown\"";
+ }
+ }
+ }
+
+ WARN_PRINT("[Physics interpolation] Non-interpolated Instance is being triggered from physics process, this might lead to issues: " + node_name + " (possibly benign).");
+ }
+ }
+#endif
+
+ return;
+ }
+
+ float new_checksum = TransformInterpolator::checksum_transform_3d(p_transform);
+ bool checksums_match = (instance->transform_checksum_curr == new_checksum) && (instance->transform_checksum_prev == new_checksum);
+
+ // We can't entirely reject no changes because we need the interpolation
+ // system to keep on stewing.
+
+ // Optimized check. First checks the checksums. If they pass it does the slow check at the end.
+ // Alternatively we can do this non-optimized and ignore the checksum... if no change.
+ if (checksums_match && (instance->transform_curr == p_transform) && (instance->transform_prev == p_transform)) {
+ return;
}
#ifdef DEBUG_ENABLED
@@ -936,8 +1065,76 @@ void RendererSceneCull::instance_set_transform(RID p_instance, const Transform3D
}
#endif
- instance->transform = p_transform;
+
+ instance->transform_curr = p_transform;
+
+ // Keep checksums up to date.
+ instance->transform_checksum_curr = new_checksum;
+
+ if (!instance->on_interpolate_transform_list) {
+ _interpolation_data.instance_transform_update_list_curr->push_back(p_instance);
+ instance->on_interpolate_transform_list = true;
+ } else {
+ DEV_ASSERT(_interpolation_data.instance_transform_update_list_curr->size());
+ }
+
+ // If the instance is invisible, then we are simply updating the data flow, there is no need to calculate the interpolated
+ // transform or anything else.
+ // Ideally we would not even call the VisualServer::set_transform() when invisible but that would entail having logic
+ // to keep track of the previous transform on the SceneTree side. The "early out" below is less efficient but a lot cleaner codewise.
+ if (!instance->visible) {
+ return;
+ }
+
+ // Decide on the interpolation method... slerp if possible.
+ instance->interpolation_method = TransformInterpolator::find_method(instance->transform_prev.basis, instance->transform_curr.basis);
+
+ if (!instance->on_interpolate_list) {
+ _interpolation_data.instance_interpolate_update_list.push_back(p_instance);
+ instance->on_interpolate_list = true;
+ } else {
+ DEV_ASSERT(_interpolation_data.instance_interpolate_update_list.size());
+ }
+
_instance_queue_update(instance, true);
+
+#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
+ if (!Engine::get_singleton()->is_in_physics_frame()) {
+ static int32_t warn_count = 0;
+ warn_count++;
+ if (((warn_count % 2048) == 0) && GLOBAL_GET("debug/settings/physics_interpolation/enable_warnings")) {
+ String node_name;
+ ObjectID id = instance->object_id;
+ if (id.is_valid()) {
+ if (ObjectDB::get_instance(id)) {
+ Node *node = Object::cast_to(ObjectDB::get_instance(id));
+ if (node && node->is_inside_tree()) {
+ node_name = "\"" + String(node->get_path()) + "\"";
+ } else {
+ node_name = "\"unknown\"";
+ }
+ }
+ }
+
+ WARN_PRINT("[Physics interpolation] Instance interpolation is being triggered from outside physics process, this might lead to issues: " + node_name + " (possibly benign).");
+ }
+ }
+#endif
+}
+
+void RendererSceneCull::instance_set_interpolated(RID p_instance, bool p_interpolated) {
+ Instance *instance = instance_owner.get_or_null(p_instance);
+ ERR_FAIL_NULL(instance);
+ instance->interpolated = p_interpolated;
+}
+
+void RendererSceneCull::instance_reset_physics_interpolation(RID p_instance) {
+ Instance *instance = instance_owner.get_or_null(p_instance);
+ ERR_FAIL_NULL(instance);
+
+ if (_interpolation_data.interpolation_enabled && instance->interpolated) {
+ _interpolation_data.instance_teleport_list.push_back(p_instance);
+ }
}
void RendererSceneCull::instance_attach_object_instance_id(RID p_instance, ObjectID p_id) {
@@ -990,6 +1187,23 @@ void RendererSceneCull::instance_set_visible(RID p_instance, bool p_visible) {
if (p_visible) {
if (instance->scenario != nullptr) {
+ // Special case for physics interpolation, we want to ensure the interpolated data is up to date
+ if (_interpolation_data.interpolation_enabled && instance->interpolated && !instance->on_interpolate_list) {
+ // Do all the extra work we normally do on instance_set_transform(), because this is optimized out for hidden instances.
+ // This prevents a glitch of stale interpolation transform data when unhiding before the next physics tick.
+ instance->interpolation_method = TransformInterpolator::find_method(instance->transform_prev.basis, instance->transform_curr.basis);
+ _interpolation_data.instance_interpolate_update_list.push_back(p_instance);
+ instance->on_interpolate_list = true;
+
+ // We must also place on the transform update list for a tick, so the system
+ // can auto-detect if the instance is no longer moving, and remove from the interpolate lists again.
+ // If this step is ignored, an unmoving instance could remain on the interpolate lists indefinitely
+ // (or rather until the object is deleted) and cause unnecessary updates and drawcalls.
+ if (!instance->on_interpolate_transform_list) {
+ _interpolation_data.instance_transform_update_list_curr->push_back(p_instance);
+ instance->on_interpolate_transform_list = true;
+ }
+ }
_instance_queue_update(instance, true, false);
}
} else if (instance->indexer_id.is_valid()) {
@@ -1574,11 +1788,22 @@ void RendererSceneCull::instance_geometry_get_shader_parameter_list(RID p_instan
void RendererSceneCull::_update_instance(Instance *p_instance) {
p_instance->version++;
+ // When not using interpolation the transform is used straight.
+ const Transform3D *instance_xform = &p_instance->transform;
+
+ // Can possibly use the most up to date current transform here when using physics interpolation ...
+ // uncomment the next line for this..
+ // if (_interpolation_data.interpolation_enabled && p_instance->interpolated) {
+ // instance_xform = &p_instance->transform_curr;
+ // }
+ // However it does seem that using the interpolated transform (transform) works for keeping AABBs
+ // up to date to avoid culling errors.
+
if (p_instance->base_type == RS::INSTANCE_LIGHT) {
InstanceLightData *light = static_cast(p_instance->base_data);
- RSG::light_storage->light_instance_set_transform(light->instance, p_instance->transform);
- RSG::light_storage->light_instance_set_aabb(light->instance, p_instance->transform.xform(p_instance->aabb));
+ RSG::light_storage->light_instance_set_transform(light->instance, *instance_xform);
+ RSG::light_storage->light_instance_set_aabb(light->instance, instance_xform->xform(p_instance->aabb));
light->make_shadow_dirty();
RS::LightBakeMode bake_mode = RSG::light_storage->light_get_bake_mode(p_instance->base);
@@ -1601,7 +1826,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
} else if (p_instance->base_type == RS::INSTANCE_REFLECTION_PROBE) {
InstanceReflectionProbeData *reflection_probe = static_cast(p_instance->base_data);
- RSG::light_storage->reflection_probe_instance_set_transform(reflection_probe->instance, p_instance->transform);
+ RSG::light_storage->reflection_probe_instance_set_transform(reflection_probe->instance, *instance_xform);
if (p_instance->scenario && p_instance->array_index >= 0) {
InstanceData &idata = p_instance->scenario->instance_data[p_instance->array_index];
@@ -1610,17 +1835,17 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
} else if (p_instance->base_type == RS::INSTANCE_DECAL) {
InstanceDecalData *decal = static_cast(p_instance->base_data);
- RSG::texture_storage->decal_instance_set_transform(decal->instance, p_instance->transform);
+ RSG::texture_storage->decal_instance_set_transform(decal->instance, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_LIGHTMAP) {
InstanceLightmapData *lightmap = static_cast(p_instance->base_data);
- RSG::light_storage->lightmap_instance_set_transform(lightmap->instance, p_instance->transform);
+ RSG::light_storage->lightmap_instance_set_transform(lightmap->instance, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_VOXEL_GI) {
InstanceVoxelGIData *voxel_gi = static_cast(p_instance->base_data);
- scene_render->voxel_gi_instance_set_transform_to_data(voxel_gi->probe_instance, p_instance->transform);
+ scene_render->voxel_gi_instance_set_transform_to_data(voxel_gi->probe_instance, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_PARTICLES) {
- RSG::particles_storage->particles_set_emission_transform(p_instance->base, p_instance->transform);
+ RSG::particles_storage->particles_set_emission_transform(p_instance->base, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_PARTICLES_COLLISION) {
InstanceParticlesCollisionData *collision = static_cast(p_instance->base_data);
@@ -1628,13 +1853,13 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
if (RSG::particles_storage->particles_collision_is_heightfield(p_instance->base)) {
heightfield_particle_colliders_update_list.insert(p_instance);
}
- RSG::particles_storage->particles_collision_instance_set_transform(collision->instance, p_instance->transform);
+ RSG::particles_storage->particles_collision_instance_set_transform(collision->instance, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_FOG_VOLUME) {
InstanceFogVolumeData *volume = static_cast(p_instance->base_data);
- scene_render->fog_volume_instance_set_transform(volume->instance, p_instance->transform);
+ scene_render->fog_volume_instance_set_transform(volume->instance, *instance_xform);
} else if (p_instance->base_type == RS::INSTANCE_OCCLUDER) {
if (p_instance->scenario) {
- RendererSceneOcclusionCull::get_singleton()->scenario_set_instance(p_instance->scenario->self, p_instance->self, p_instance->base, p_instance->transform, p_instance->visible);
+ RendererSceneOcclusionCull::get_singleton()->scenario_set_instance(p_instance->scenario->self, p_instance->self, p_instance->base, *instance_xform, p_instance->visible);
}
}
@@ -1654,7 +1879,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
}
AABB new_aabb;
- new_aabb = p_instance->transform.xform(p_instance->aabb);
+ new_aabb = instance_xform->xform(p_instance->aabb);
p_instance->transformed_aabb = new_aabb;
if ((1 << p_instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) {
@@ -1681,11 +1906,11 @@ void RendererSceneCull::_update_instance(Instance *p_instance) {
}
ERR_FAIL_NULL(geom->geometry_instance);
- geom->geometry_instance->set_transform(p_instance->transform, p_instance->aabb, p_instance->transformed_aabb);
+ geom->geometry_instance->set_transform(*instance_xform, p_instance->aabb, p_instance->transformed_aabb);
}
// note: we had to remove is equal approx check here, it meant that det == 0.000004 won't work, which is the case for some of our scenes.
- if (p_instance->scenario == nullptr || !p_instance->visible || p_instance->transform.basis.determinant() == 0) {
+ if (p_instance->scenario == nullptr || !p_instance->visible || instance_xform->basis.determinant() == 0) {
p_instance->prev_transformed_aabb = p_instance->transformed_aabb;
return;
}
@@ -2571,7 +2796,7 @@ void RendererSceneCull::render_camera(const Ref &p_render_bu
// Setup Camera(s)
if (p_xr_interface.is_null()) {
// Normal camera
- Transform3D transform = camera->transform;
+ Transform3D transform = (_interpolation_data.interpolation_enabled && camera->interpolated) ? camera->get_transform_interpolated() : camera->transform;
Projection projection;
bool vaspect = camera->vaspect;
bool is_orthogonal = false;
@@ -4154,6 +4379,9 @@ bool RendererSceneCull::free(RID p_rid) {
}
if (camera_owner.owns(p_rid)) {
+ Camera *camera = camera_owner.get_or_null(p_rid);
+ _interpolation_data.notify_free_camera(p_rid, *camera);
+
camera_owner.free(p_rid);
} else if (scenario_owner.owns(p_rid)) {
@@ -4180,6 +4408,8 @@ bool RendererSceneCull::free(RID p_rid) {
Instance *instance = instance_owner.get_or_null(p_rid);
+ _interpolation_data.notify_free_instance(p_rid, *instance);
+
instance_geometry_set_lightmap(p_rid, RID(), Rect2(), 0);
instance_set_scenario(p_rid, RID());
instance_set_base(p_rid, RID());
@@ -4240,6 +4470,172 @@ void RendererSceneCull::set_scene_render(RendererSceneRender *p_scene_render) {
geometry_instance_pair_mask = scene_render->geometry_instance_get_pair_mask();
}
+/* INTERPOLATION API */
+
+void RendererSceneCull::update_interpolation_tick(bool p_process) {
+ // TODO (MultiMesh): Update interpolation in storage.
+
+ // INSTANCES
+
+ // Detect any that were on the previous transform list that are no longer active;
+ // we should remove them from the interpolate list.
+
+ for (unsigned int n = 0; n < _interpolation_data.instance_transform_update_list_prev->size(); n++) {
+ const RID &rid = (*_interpolation_data.instance_transform_update_list_prev)[n];
+ Instance *instance = instance_owner.get_or_null(rid);
+
+ bool active = true;
+
+ // No longer active? (Either the instance deleted or no longer being transformed.)
+ if (instance && !instance->on_interpolate_transform_list) {
+ active = false;
+ instance->on_interpolate_list = false;
+
+ // Make sure the most recent transform is set...
+ instance->transform = instance->transform_curr;
+
+ // ... and that both prev and current are the same, just in case of any interpolations.
+ instance->transform_prev = instance->transform_curr;
+
+ // Make sure instances are updated one more time to ensure the AABBs are correct.
+ _instance_queue_update(instance, true);
+ }
+
+ if (!instance) {
+ active = false;
+ }
+
+ if (!active) {
+ _interpolation_data.instance_interpolate_update_list.erase(rid);
+ }
+ }
+
+ // Now for any in the transform list (being actively interpolated), keep the previous transform
+ // value up to date, ready for the next tick.
+ if (p_process) {
+ for (unsigned int n = 0; n < _interpolation_data.instance_transform_update_list_curr->size(); n++) {
+ const RID &rid = (*_interpolation_data.instance_transform_update_list_curr)[n];
+ Instance *instance = instance_owner.get_or_null(rid);
+ if (instance) {
+ instance->transform_prev = instance->transform_curr;
+ instance->transform_checksum_prev = instance->transform_checksum_curr;
+ instance->on_interpolate_transform_list = false;
+ }
+ }
+ }
+
+ // We maintain a mirror list for the transform updates, so we can detect when an instance
+ // is no longer being transformed, and remove it from the interpolate list.
+ SWAP(_interpolation_data.instance_transform_update_list_curr, _interpolation_data.instance_transform_update_list_prev);
+
+ // Prepare for the next iteration.
+ _interpolation_data.instance_transform_update_list_curr->clear();
+
+ // CAMERAS
+
+ // Detect any that were on the previous transform list that are no longer active.
+ for (unsigned int n = 0; n < _interpolation_data.camera_transform_update_list_prev->size(); n++) {
+ const RID &rid = (*_interpolation_data.camera_transform_update_list_prev)[n];
+ Camera *camera = camera_owner.get_or_null(rid);
+
+ // No longer active? (Either the instance deleted or no longer being transformed.)
+ if (camera && !camera->on_interpolate_transform_list) {
+ camera->transform = camera->transform_prev;
+ }
+ }
+
+ // Swap any current with previous.
+ for (unsigned int n = 0; n < _interpolation_data.camera_transform_update_list_curr->size(); n++) {
+ const RID &rid = (*_interpolation_data.camera_transform_update_list_curr)[n];
+ Camera *camera = camera_owner.get_or_null(rid);
+ if (camera) {
+ camera->transform_prev = camera->transform;
+ camera->on_interpolate_transform_list = false;
+ }
+ }
+
+ // We maintain a mirror list for the transform updates, so we can detect when a camera
+ // is no longer being transformed, and remove it from the interpolate list.
+ SWAP(_interpolation_data.camera_transform_update_list_curr, _interpolation_data.camera_transform_update_list_prev);
+
+ // Prepare for the next iteration.
+ _interpolation_data.camera_transform_update_list_curr->clear();
+}
+
+void RendererSceneCull::update_interpolation_frame(bool p_process) {
+ // TODO (MultiMesh): Update interpolation in storage.
+
+ // Teleported instances.
+ for (unsigned int n = 0; n < _interpolation_data.instance_teleport_list.size(); n++) {
+ const RID &rid = _interpolation_data.instance_teleport_list[n];
+ Instance *instance = instance_owner.get_or_null(rid);
+ if (instance) {
+ instance->transform_prev = instance->transform_curr;
+ instance->transform_checksum_prev = instance->transform_checksum_curr;
+ }
+ }
+
+ _interpolation_data.instance_teleport_list.clear();
+
+ // Camera teleports.
+ for (unsigned int n = 0; n < _interpolation_data.camera_teleport_list.size(); n++) {
+ const RID &rid = _interpolation_data.camera_teleport_list[n];
+ Camera *camera = camera_owner.get_or_null(rid);
+ if (camera) {
+ camera->transform_prev = camera->transform;
+ }
+ }
+
+ _interpolation_data.camera_teleport_list.clear();
+
+ if (p_process) {
+ real_t f = Engine::get_singleton()->get_physics_interpolation_fraction();
+
+ for (unsigned int i = 0; i < _interpolation_data.instance_interpolate_update_list.size(); i++) {
+ const RID &rid = _interpolation_data.instance_interpolate_update_list[i];
+ Instance *instance = instance_owner.get_or_null(rid);
+ if (instance) {
+ TransformInterpolator::interpolate_transform_3d_via_method(instance->transform_prev, instance->transform_curr, instance->transform, f, instance->interpolation_method);
+
+ // Make sure AABBs are constantly up to date through the interpolation.
+ _instance_queue_update(instance, true);
+ }
+ }
+ }
+}
+
+void RendererSceneCull::set_physics_interpolation_enabled(bool p_enabled) {
+ _interpolation_data.interpolation_enabled = p_enabled;
+}
+
+void RendererSceneCull::InterpolationData::notify_free_camera(RID p_rid, Camera &r_camera) {
+ r_camera.on_interpolate_transform_list = false;
+
+ if (!interpolation_enabled) {
+ return;
+ }
+
+ // If the camera was on any of the lists, remove.
+ camera_transform_update_list_curr->erase_multiple_unordered(p_rid);
+ camera_transform_update_list_prev->erase_multiple_unordered(p_rid);
+ camera_teleport_list.erase_multiple_unordered(p_rid);
+}
+
+void RendererSceneCull::InterpolationData::notify_free_instance(RID p_rid, Instance &r_instance) {
+ r_instance.on_interpolate_list = false;
+ r_instance.on_interpolate_transform_list = false;
+
+ if (!interpolation_enabled) {
+ return;
+ }
+
+ // If the instance was on any of the lists, remove.
+ instance_interpolate_update_list.erase_multiple_unordered(p_rid);
+ instance_transform_update_list_curr->erase_multiple_unordered(p_rid);
+ instance_transform_update_list_prev->erase_multiple_unordered(p_rid);
+ instance_teleport_list.erase_multiple_unordered(p_rid);
+}
+
RendererSceneCull::RendererSceneCull() {
render_pass = 1;
singleton = this;
diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h
index 0039d1447516..5ad744e22caa 100644
--- a/servers/rendering/renderer_scene_cull.h
+++ b/servers/rendering/renderer_scene_cull.h
@@ -32,6 +32,7 @@
#define RENDERER_SCENE_CULL_H
#include "core/math/dynamic_bvh.h"
+#include "core/math/transform_interpolator.h"
#include "core/templates/bin_sorted_array.h"
#include "core/templates/local_vector.h"
#include "core/templates/paged_allocator.h"
@@ -66,6 +67,11 @@ class RendererSceneCull : public RenderingMethod {
static RendererSceneCull *singleton;
+ /* EVENT QUEUING */
+
+ void tick();
+ void pre_draw(bool p_will_draw);
+
/* CAMERA API */
struct Camera {
@@ -80,12 +86,21 @@ class RendererSceneCull : public RenderingMethod {
float size;
Vector2 offset;
uint32_t visible_layers;
- bool vaspect;
RID env;
RID attributes;
RID compositor;
Transform3D transform;
+ Transform3D transform_prev; // This is only used when using fixed timestep interpolation.
+
+ bool vaspect : 1;
+
+ bool interpolated : 1;
+ bool on_interpolate_transform_list : 1;
+
+ TransformInterpolator::Method interpolation_method : 3;
+
+ Transform3D get_transform_interpolated() const;
Camera() {
visible_layers = 0xFFFFFFFF;
@@ -96,6 +111,9 @@ class RendererSceneCull : public RenderingMethod {
size = 1.0;
offset = Vector2();
vaspect = false;
+ interpolated = true;
+ on_interpolate_transform_list = false;
+ interpolation_method = TransformInterpolator::INTERP_LERP;
}
};
@@ -108,6 +126,8 @@ class RendererSceneCull : public RenderingMethod {
virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far);
virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far);
virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform);
+ virtual void camera_set_interpolated(RID p_camera, bool p_enable);
+ virtual void camera_reset_physics_interpolation(RID p_camera);
virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers);
virtual void camera_set_environment(RID p_camera, RID p_env);
virtual void camera_set_camera_attributes(RID p_camera, RID p_attributes);
@@ -406,8 +426,16 @@ class RendererSceneCull : public RenderingMethod {
RID mesh_instance; //only used for meshes and when skeleton/blendshapes exist
+ // This is the main transform to be drawn with ...
+ // This will either be the interpolated transform (when using fixed timestep interpolation)
+ // or the ONLY transform (when not using FTI).
Transform3D transform;
+ // For interpolation we store the current transform (this physics tick)
+ // and the transform in the previous tick.
+ Transform3D transform_curr;
+ Transform3D transform_prev;
+
float lod_bias;
bool ignore_occlusion_culling;
@@ -418,13 +446,23 @@ class RendererSceneCull : public RenderingMethod {
RS::ShadowCastingSetting cast_shadows;
uint32_t layer_mask;
- //fit in 32 bits
- bool mirror : 8;
- bool receive_shadows : 8;
- bool visible : 8;
- bool baked_light : 2; //this flag is only to know if it actually did use baked light
- bool dynamic_gi : 2; //same above for dynamic objects
- bool redraw_if_visible : 4;
+ // Fit in 32 bits.
+ bool mirror : 1;
+ bool receive_shadows : 1;
+ bool visible : 1;
+ bool baked_light : 1; // This flag is only to know if it actually did use baked light.
+ bool dynamic_gi : 1; // Same as above for dynamic objects.
+ bool redraw_if_visible : 1;
+
+ bool on_interpolate_list : 1;
+ bool on_interpolate_transform_list : 1;
+ bool interpolated : 1;
+ TransformInterpolator::Method interpolation_method : 3;
+
+ // For fixed timestep interpolation.
+ // Note 32 bits is plenty for checksum, no need for real_t
+ float transform_checksum_curr;
+ float transform_checksum_prev;
Instance *lightmap = nullptr;
Rect2 lightmap_uv_scale;
@@ -574,6 +612,14 @@ class RendererSceneCull : public RenderingMethod {
baked_light = true;
dynamic_gi = false;
redraw_if_visible = false;
+
+ on_interpolate_list = false;
+ on_interpolate_transform_list = false;
+ interpolated = true;
+ interpolation_method = TransformInterpolator::INTERP_LERP;
+ transform_checksum_curr = 0.0;
+ transform_checksum_prev = 0.0;
+
lightmap_slice_index = 0;
lightmap = nullptr;
lightmap_cull_index = 0;
@@ -1027,6 +1073,8 @@ class RendererSceneCull : public RenderingMethod {
virtual void instance_set_layer_mask(RID p_instance, uint32_t p_mask);
virtual void instance_set_pivot_data(RID p_instance, float p_sorting_offset, bool p_use_aabb_center);
virtual void instance_set_transform(RID p_instance, const Transform3D &p_transform);
+ virtual void instance_set_interpolated(RID p_instance, bool p_interpolated);
+ virtual void instance_reset_physics_interpolation(RID p_instance);
virtual void instance_attach_object_instance_id(RID p_instance, ObjectID p_id);
virtual void instance_set_blend_shape_weight(RID p_instance, int p_shape, float p_weight);
virtual void instance_set_surface_override_material(RID p_instance, int p_surface, RID p_material);
@@ -1393,6 +1441,29 @@ class RendererSceneCull : public RenderingMethod {
virtual void update_visibility_notifiers();
+ /* INTERPOLATION */
+
+ void update_interpolation_tick(bool p_process = true);
+ void update_interpolation_frame(bool p_process = true);
+ virtual void set_physics_interpolation_enabled(bool p_enabled);
+
+ struct InterpolationData {
+ void notify_free_camera(RID p_rid, Camera &r_camera);
+ void notify_free_instance(RID p_rid, Instance &r_instance);
+ LocalVector instance_interpolate_update_list;
+ LocalVector instance_transform_update_lists[2];
+ LocalVector *instance_transform_update_list_curr = &instance_transform_update_lists[0];
+ LocalVector *instance_transform_update_list_prev = &instance_transform_update_lists[1];
+ LocalVector instance_teleport_list;
+
+ LocalVector camera_transform_update_lists[2];
+ LocalVector *camera_transform_update_list_curr = &camera_transform_update_lists[0];
+ LocalVector *camera_transform_update_list_prev = &camera_transform_update_lists[1];
+ LocalVector camera_teleport_list;
+
+ bool interpolation_enabled = false;
+ } _interpolation_data;
+
RendererSceneCull();
virtual ~RendererSceneCull();
};
diff --git a/servers/rendering/rendering_method.h b/servers/rendering/rendering_method.h
index aa5e7d83cc84..e28d0b5dada5 100644
--- a/servers/rendering/rendering_method.h
+++ b/servers/rendering/rendering_method.h
@@ -51,6 +51,8 @@ class RenderingMethod {
virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far) = 0;
virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far) = 0;
virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform) = 0;
+ virtual void camera_set_interpolated(RID p_camera, bool p_enable) = 0;
+ virtual void camera_reset_physics_interpolation(RID p_camera) = 0;
virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers) = 0;
virtual void camera_set_environment(RID p_camera, RID p_env) = 0;
virtual void camera_set_camera_attributes(RID p_camera, RID p_attributes) = 0;
@@ -83,6 +85,8 @@ class RenderingMethod {
virtual void instance_set_layer_mask(RID p_instance, uint32_t p_mask) = 0;
virtual void instance_set_pivot_data(RID p_instance, float p_sorting_offset, bool p_use_aabb_center) = 0;
virtual void instance_set_transform(RID p_instance, const Transform3D &p_transform) = 0;
+ virtual void instance_set_interpolated(RID p_instance, bool p_interpolated) = 0;
+ virtual void instance_reset_physics_interpolation(RID p_instance) = 0;
virtual void instance_attach_object_instance_id(RID p_instance, ObjectID p_id) = 0;
virtual void instance_set_blend_shape_weight(RID p_instance, int p_shape, float p_weight) = 0;
virtual void instance_set_surface_override_material(RID p_instance, int p_surface, RID p_material) = 0;
@@ -350,6 +354,16 @@ class RenderingMethod {
virtual bool free(RID p_rid) = 0;
+ /* Physics interpolation */
+
+ virtual void update_interpolation_tick(bool p_process = true) = 0;
+ virtual void set_physics_interpolation_enabled(bool p_enabled) = 0;
+
+ /* Event queueing */
+
+ virtual void tick() = 0;
+ virtual void pre_draw(bool p_will_draw) = 0;
+
RenderingMethod();
virtual ~RenderingMethod();
};
diff --git a/servers/rendering/rendering_server_default.cpp b/servers/rendering/rendering_server_default.cpp
index 51ff009eaf5d..86efccef9a97 100644
--- a/servers/rendering/rendering_server_default.cpp
+++ b/servers/rendering/rendering_server_default.cpp
@@ -381,12 +381,9 @@ void RenderingServerDefault::_thread_loop() {
/* INTERPOLATION */
-void RenderingServerDefault::tick() {
- RSG::canvas->tick();
-}
-
void RenderingServerDefault::set_physics_interpolation_enabled(bool p_enabled) {
RSG::canvas->set_physics_interpolation_enabled(p_enabled);
+ RSG::scene->set_physics_interpolation_enabled(p_enabled);
}
/* EVENT QUEUING */
@@ -411,6 +408,15 @@ void RenderingServerDefault::draw(bool p_swap_buffers, double frame_step) {
}
}
+void RenderingServerDefault::tick() {
+ RSG::canvas->tick();
+ RSG::scene->tick();
+}
+
+void RenderingServerDefault::pre_draw(bool p_will_draw) {
+ RSG::scene->pre_draw(p_will_draw);
+}
+
void RenderingServerDefault::_call_on_render_thread(const Callable &p_callable) {
p_callable.call();
}
diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h
index 164ec3cc09fe..069a6cd0d2c7 100644
--- a/servers/rendering/rendering_server_default.h
+++ b/servers/rendering/rendering_server_default.h
@@ -583,6 +583,8 @@ class RenderingServerDefault : public RenderingServer {
FUNC4(camera_set_orthogonal, RID, float, float, float)
FUNC5(camera_set_frustum, RID, float, Vector2, float, float)
FUNC2(camera_set_transform, RID, const Transform3D &)
+ FUNC2(camera_set_interpolated, RID, bool)
+ FUNC1(camera_reset_physics_interpolation, RID)
FUNC2(camera_set_cull_mask, RID, uint32_t)
FUNC2(camera_set_environment, RID, RID)
FUNC2(camera_set_camera_attributes, RID, RID)
@@ -802,6 +804,8 @@ class RenderingServerDefault : public RenderingServer {
FUNC2(instance_set_layer_mask, RID, uint32_t)
FUNC3(instance_set_pivot_data, RID, float, bool)
FUNC2(instance_set_transform, RID, const Transform3D &)
+ FUNC2(instance_set_interpolated, RID, bool)
+ FUNC1(instance_reset_physics_interpolation, RID)
FUNC2(instance_attach_object_instance_id, RID, ObjectID)
FUNC3(instance_set_blend_shape_weight, RID, int, float)
FUNC3(instance_set_surface_override_material, RID, int, RID)
@@ -1048,7 +1052,6 @@ class RenderingServerDefault : public RenderingServer {
/* INTERPOLATION */
- virtual void tick() override;
virtual void set_physics_interpolation_enabled(bool p_enabled) override;
/* EVENT QUEUING */
@@ -1060,6 +1063,8 @@ class RenderingServerDefault : public RenderingServer {
virtual bool has_changed() const override;
virtual void init() override;
virtual void finish() override;
+ virtual void tick() override;
+ virtual void pre_draw(bool p_will_draw) override;
virtual bool is_on_render_thread() override {
return Thread::get_caller_id() == server_thread;
diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp
index 5cfd8d3cd2b4..7fe1e5bfe592 100644
--- a/servers/rendering_server.cpp
+++ b/servers/rendering_server.cpp
@@ -2756,6 +2756,8 @@ void RenderingServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("camera_set_orthogonal", "camera", "size", "z_near", "z_far"), &RenderingServer::camera_set_orthogonal);
ClassDB::bind_method(D_METHOD("camera_set_frustum", "camera", "size", "offset", "z_near", "z_far"), &RenderingServer::camera_set_frustum);
ClassDB::bind_method(D_METHOD("camera_set_transform", "camera", "transform"), &RenderingServer::camera_set_transform);
+ ClassDB::bind_method(D_METHOD("camera_set_interpolated", "camera", "interpolated"), &RenderingServer::camera_set_interpolated);
+ ClassDB::bind_method(D_METHOD("camera_reset_physics_interpolation", "camera"), &RenderingServer::camera_reset_physics_interpolation);
ClassDB::bind_method(D_METHOD("camera_set_cull_mask", "camera", "layers"), &RenderingServer::camera_set_cull_mask);
ClassDB::bind_method(D_METHOD("camera_set_environment", "camera", "env"), &RenderingServer::camera_set_environment);
ClassDB::bind_method(D_METHOD("camera_set_camera_attributes", "camera", "effects"), &RenderingServer::camera_set_camera_attributes);
@@ -3113,6 +3115,8 @@ void RenderingServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("instance_set_layer_mask", "instance", "mask"), &RenderingServer::instance_set_layer_mask);
ClassDB::bind_method(D_METHOD("instance_set_pivot_data", "instance", "sorting_offset", "use_aabb_center"), &RenderingServer::instance_set_pivot_data);
ClassDB::bind_method(D_METHOD("instance_set_transform", "instance", "transform"), &RenderingServer::instance_set_transform);
+ ClassDB::bind_method(D_METHOD("instance_set_interpolated", "instance", "interpolated"), &RenderingServer::instance_set_interpolated);
+ ClassDB::bind_method(D_METHOD("instance_reset_physics_interpolation", "instance"), &RenderingServer::instance_reset_physics_interpolation);
ClassDB::bind_method(D_METHOD("instance_attach_object_instance_id", "instance", "id"), &RenderingServer::instance_attach_object_instance_id);
ClassDB::bind_method(D_METHOD("instance_set_blend_shape_weight", "instance", "shape", "weight"), &RenderingServer::instance_set_blend_shape_weight);
ClassDB::bind_method(D_METHOD("instance_set_surface_override_material", "instance", "surface", "material"), &RenderingServer::instance_set_surface_override_material);
diff --git a/servers/rendering_server.h b/servers/rendering_server.h
index e15dba43536f..1ee1282a1491 100644
--- a/servers/rendering_server.h
+++ b/servers/rendering_server.h
@@ -843,6 +843,8 @@ class RenderingServer : public Object {
virtual void camera_set_orthogonal(RID p_camera, float p_size, float p_z_near, float p_z_far) = 0;
virtual void camera_set_frustum(RID p_camera, float p_size, Vector2 p_offset, float p_z_near, float p_z_far) = 0;
virtual void camera_set_transform(RID p_camera, const Transform3D &p_transform) = 0;
+ virtual void camera_set_interpolated(RID p_camera, bool p_enable) = 0;
+ virtual void camera_reset_physics_interpolation(RID p_camera) = 0;
virtual void camera_set_cull_mask(RID p_camera, uint32_t p_layers) = 0;
virtual void camera_set_environment(RID p_camera, RID p_env) = 0;
virtual void camera_set_camera_attributes(RID p_camera, RID p_camera_attributes) = 0;
@@ -1344,6 +1346,8 @@ class RenderingServer : public Object {
virtual void instance_set_layer_mask(RID p_instance, uint32_t p_mask) = 0;
virtual void instance_set_pivot_data(RID p_instance, float p_sorting_offset, bool p_use_aabb_center) = 0;
virtual void instance_set_transform(RID p_instance, const Transform3D &p_transform) = 0;
+ virtual void instance_set_interpolated(RID p_instance, bool p_interpolated) = 0;
+ virtual void instance_reset_physics_interpolation(RID p_instance) = 0;
virtual void instance_attach_object_instance_id(RID p_instance, ObjectID p_id) = 0;
virtual void instance_set_blend_shape_weight(RID p_instance, int p_shape, float p_weight) = 0;
virtual void instance_set_surface_override_material(RID p_instance, int p_surface, RID p_material) = 0;
@@ -1657,7 +1661,6 @@ class RenderingServer : public Object {
/* INTERPOLATION */
- virtual void tick() = 0;
virtual void set_physics_interpolation_enabled(bool p_enabled) = 0;
/* EVENT QUEUING */
@@ -1669,6 +1672,8 @@ class RenderingServer : public Object {
virtual bool has_changed() const = 0;
virtual void init();
virtual void finish() = 0;
+ virtual void tick() = 0;
+ virtual void pre_draw(bool p_will_draw) = 0;
/* STATUS INFORMATION */
]