Physics Interpolation - refactor Camera and fix get_camera_transform()

* Moves 3D Camera interpolation scene side.
* Automatically switches `get_camera_transform()` to report interpolated transform during `_process()`.
* Fixes `ClippedCamera` to work with physics interpolation.
This commit is contained in:
lawnjelly
2024-06-05 07:45:03 +01:00
parent b203829361
commit 0b30d77384
20 changed files with 359 additions and 274 deletions

View File

@ -1267,7 +1267,7 @@ void CPUParticles2D::_notification(int p_what) {
}
}
}
if (p_what == NOTIFICATION_RESET_PHYSICS_INTERPOLATION) {
if (p_what == NOTIFICATION_RESET_PHYSICS_INTERPOLATION && is_inside_tree()) {
// Make sure current is up to date with any pending global transform changes.
_interpolation_data.global_xform_curr = get_global_transform_const();
_interpolation_data.global_xform_prev = _interpolation_data.global_xform_curr;

View File

@ -33,8 +33,11 @@
#include "collision_object.h"
#include "core/engine.h"
#include "core/math/camera_matrix.h"
#include "core/math/transform_interpolator.h"
#include "scene/resources/material.h"
#include "scene/resources/surface_tool.h"
#include "servers/visual/visual_server_constants.h"
void Camera::_update_audio_listener_state() {
}
@ -79,7 +82,16 @@ void Camera::_update_camera() {
return;
}
VisualServer::get_singleton()->camera_set_transform(camera, get_camera_transform());
if (!is_physics_interpolated_and_enabled()) {
VisualServer::get_singleton()->camera_set_transform(camera, get_camera_transform());
} else {
// Ideally we shouldn't be moving a physics interpolated camera within a frame,
// because it will break smooth interpolation, but it may occur on e.g. level load.
if (!Engine::get_singleton()->is_in_physics_frame() && camera.is_valid()) {
_physics_interpolation_ensure_transform_calculated(true);
VisualServer::get_singleton()->camera_set_transform(camera, _interpolation_data.camera_xform_interpolated);
}
}
// here goes listener stuff
/*
@ -99,7 +111,61 @@ void Camera::_update_camera() {
}
void Camera::_physics_interpolated_changed() {
VisualServer::get_singleton()->camera_set_interpolated(camera, is_physics_interpolated());
_update_process_mode();
}
void Camera::_physics_interpolation_ensure_data_flipped() {
// The curr -> previous update can either occur
// on the INTERNAL_PHYSICS_PROCESS OR
// on NOTIFICATION_TRANSFORM_CHANGED,
// if NOTIFICATION_TRANSFORM_CHANGED takes place
// earlier than INTERNAL_PHYSICS_PROCESS on a tick.
// This is to ensure that the data keeps flowing, but the new data
// doesn't overwrite before prev has been set.
// Keep the data flowing.
uint64_t tick = Engine::get_singleton()->get_physics_frames();
if (_interpolation_data.last_update_physics_tick != tick) {
_interpolation_data.xform_prev = _interpolation_data.xform_curr;
_interpolation_data.last_update_physics_tick = tick;
physics_interpolation_flip_data();
}
}
void Camera::_physics_interpolation_ensure_transform_calculated(bool p_force) const {
DEV_CHECK_ONCE(!Engine::get_singleton()->is_in_physics_frame());
InterpolationData &id = _interpolation_data;
uint64_t frame = Engine::get_singleton()->get_frames_drawn();
if (id.last_update_frame != frame || p_force) {
id.last_update_frame = frame;
TransformInterpolator::interpolate_transform(id.xform_prev, id.xform_curr, id.xform_interpolated, Engine::get_singleton()->get_physics_interpolation_fraction());
Transform &tr = id.camera_xform_interpolated;
tr = _get_adjusted_camera_transform(id.xform_interpolated);
}
}
void Camera::set_desired_process_modes(bool p_process_internal, bool p_physics_process_internal) {
_desired_process_internal = p_process_internal;
_desired_physics_process_internal = p_physics_process_internal;
_update_process_mode();
}
void Camera::_update_process_mode() {
bool process = _desired_process_internal;
bool physics_process = _desired_physics_process_internal;
if (is_physics_interpolated_and_enabled()) {
if (is_current()) {
process = true;
physics_process = true;
}
}
set_process_internal(process);
set_physics_process_internal(physics_process);
}
void Camera::_notification(int p_what) {
@ -116,15 +182,48 @@ void Camera::_notification(int p_what) {
viewport->_camera_set(this);
}
} break;
case NOTIFICATION_INTERNAL_PROCESS: {
if (is_physics_interpolated_and_enabled() && camera.is_valid()) {
_physics_interpolation_ensure_transform_calculated();
#ifdef VISUAL_SERVER_DEBUG_PHYSICS_INTERPOLATION
print_line("\t\tinterpolated Camera: " + rtos(_interpolation_data.xform_interpolated.origin.x) + "\t( prev " + rtos(_interpolation_data.xform_prev.origin.x) + ", curr " + rtos(_interpolation_data.xform_curr.origin.x) + " ) on tick " + itos(Engine::get_singleton()->get_physics_frames()));
#endif
VisualServer::get_singleton()->camera_set_transform(camera, _interpolation_data.camera_xform_interpolated);
}
} break;
case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
if (is_physics_interpolated_and_enabled()) {
_physics_interpolation_ensure_data_flipped();
_interpolation_data.xform_curr = get_global_transform();
}
} break;
case NOTIFICATION_TRANSFORM_CHANGED: {
if (is_physics_interpolated_and_enabled()) {
_physics_interpolation_ensure_data_flipped();
_interpolation_data.xform_curr = get_global_transform();
#if defined(DEBUG_ENABLED) && defined(TOOLS_ENABLED)
if (!Engine::get_singleton()->is_in_physics_frame()) {
PHYSICS_INTERPOLATION_NODE_WARNING(get_instance_id(), "Interpolated Camera triggered from outside physics process");
}
#endif
}
_request_camera_update();
if (doppler_tracking != DOPPLER_TRACKING_DISABLED) {
velocity_tracker->update_position(get_global_transform().origin);
}
// Allow auto-reset when first adding to the tree, as a convenience.
if (_is_physics_interpolation_reset_requested() && is_inside_tree()) {
_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
_set_physics_interpolation_reset_requested(false);
}
} break;
case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
if (is_physics_interpolated()) {
VisualServer::get_singleton()->camera_reset_physics_interpolation(camera);
if (is_inside_tree()) {
_interpolation_data.xform_curr = get_global_transform();
_interpolation_data.xform_prev = _interpolation_data.xform_curr;
}
} break;
case NOTIFICATION_EXIT_WORLD: {
@ -148,22 +247,33 @@ void Camera::_notification(int p_what) {
if (viewport) {
viewport->find_world()->_register_camera(this);
}
_update_process_mode();
} break;
case NOTIFICATION_LOST_CURRENT: {
if (viewport) {
viewport->find_world()->_remove_camera(this);
}
_update_process_mode();
} break;
}
}
Transform Camera::get_camera_transform() const {
Transform tr = get_global_transform().orthonormalized();
Transform Camera::_get_adjusted_camera_transform(const Transform &p_xform) const {
Transform tr = p_xform.orthonormalized();
tr.origin += tr.basis.get_axis(1) * v_offset;
tr.origin += tr.basis.get_axis(0) * h_offset;
return tr;
}
Transform Camera::get_camera_transform() const {
if (is_physics_interpolated_and_enabled() && !Engine::get_singleton()->is_in_physics_frame()) {
_physics_interpolation_ensure_transform_calculated();
return _interpolation_data.camera_xform_interpolated;
}
return _get_adjusted_camera_transform(get_global_transform());
}
void Camera::set_perspective(float p_fovy_degrees, float p_z_near, float p_z_far) {
if (!force_change && fov == p_fovy_degrees && p_z_near == near && p_z_far == far && mode == PROJECTION_PERSPECTIVE) {
return;
@ -365,6 +475,10 @@ Point2 Camera::unproject_position(const Vector3 &p_pos) const {
Plane p(get_camera_transform().xform_inv(p_pos), 1.0);
p = cm.xform4(p);
// Prevent divide by zero.
// TODO : Investigate, this was causing Nans.
ERR_FAIL_COND_V(p.d == 0, Point2());
p.normal /= p.d;
Point2 res;
@ -692,25 +806,48 @@ float ClippedCamera::get_margin() const {
return margin;
}
void ClippedCamera::set_process_mode(ProcessMode p_mode) {
if (is_physics_interpolated_and_enabled() && p_mode == CLIP_PROCESS_IDLE) {
p_mode = CLIP_PROCESS_PHYSICS;
WARN_PRINT_ONCE("[Physics interpolation] Forcing ClippedCamera to PROCESS_PHYSICS mode.");
}
if (process_mode == p_mode) {
return;
}
process_mode = p_mode;
set_process_internal(process_mode == CLIP_PROCESS_IDLE);
set_physics_process_internal(process_mode == CLIP_PROCESS_PHYSICS);
set_desired_process_modes(process_mode == CLIP_PROCESS_IDLE, process_mode == CLIP_PROCESS_PHYSICS);
}
ClippedCamera::ProcessMode ClippedCamera::get_process_mode() const {
return process_mode;
}
Transform ClippedCamera::get_camera_transform() const {
Transform t = Camera::get_camera_transform();
void ClippedCamera::physics_interpolation_flip_data() {
_interpolation_data.clip_offset_prev = _interpolation_data.clip_offset_curr;
}
void ClippedCamera::_physics_interpolated_changed() {
// Switch process mode to physics if we are turning on interpolation.
// Idle process mode doesn't work well with physics interpolation.
set_process_mode(get_process_mode());
Camera::_physics_interpolated_changed();
}
Transform ClippedCamera::_get_adjusted_camera_transform(const Transform &p_xform) const {
Transform t = Camera::_get_adjusted_camera_transform(p_xform);
t.origin += -t.basis.get_axis(Vector3::AXIS_Z).normalized() * clip_offset;
return t;
}
void ClippedCamera::_notification(int p_what) {
if (p_what == NOTIFICATION_INTERNAL_PROCESS || p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) {
if (p_what == NOTIFICATION_ENTER_TREE) {
// Switch process mode to physics if we are turning on interpolation.
// Idle process mode doesn't work well with physics interpolation.
set_process_mode(get_process_mode());
}
if (((p_what == NOTIFICATION_INTERNAL_PROCESS) && process_mode == CLIP_PROCESS_IDLE) || ((p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) && process_mode == CLIP_PROCESS_PHYSICS)) {
Spatial *parent = Object::cast_to<Spatial>(get_parent());
if (!parent) {
return;
@ -732,7 +869,7 @@ void ClippedCamera::_notification(int p_what) {
Vector3 ray_from = parent_plane.project(cam_pos);
clip_offset = 0; //reset by defau;t
_interpolation_data.clip_offset_curr = 0; // Reset by default.
{ //check if points changed
Vector<Vector3> local_points = get_near_plane_points();
@ -758,15 +895,29 @@ void ClippedCamera::_notification(int p_what) {
float closest_safe = 1.0f, closest_unsafe = 1.0f;
if (dspace->cast_motion(pyramid_shape, xf, cam_pos - ray_from, margin, closest_safe, closest_unsafe, exclude, collision_mask, clip_to_bodies, clip_to_areas)) {
clip_offset = cam_pos.distance_to(ray_from + (cam_pos - ray_from) * closest_safe);
_interpolation_data.clip_offset_curr = cam_pos.distance_to(ray_from + (cam_pos - ray_from) * closest_safe);
}
// Default to use the current value
// (in the case of non-interpolated).
if (!is_physics_interpolated_and_enabled()) {
clip_offset = _interpolation_data.clip_offset_curr;
}
_update_camera();
}
if (is_physics_interpolated_and_enabled() && (p_what == NOTIFICATION_INTERNAL_PROCESS)) {
clip_offset = ((_interpolation_data.clip_offset_curr - _interpolation_data.clip_offset_prev) * Engine::get_singleton()->get_physics_interpolation_fraction()) + _interpolation_data.clip_offset_prev;
}
if (p_what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED) {
update_gizmo();
}
if (p_what == NOTIFICATION_RESET_PHYSICS_INTERPOLATION) {
_interpolation_data.clip_offset_prev = _interpolation_data.clip_offset_curr;
}
}
void ClippedCamera::set_collision_mask(uint32_t p_mask) {
@ -881,9 +1032,11 @@ void ClippedCamera::_bind_methods() {
}
ClippedCamera::ClippedCamera() {
margin = 0;
clip_offset = 0;
process_mode = CLIP_PROCESS_PHYSICS;
set_physics_process_internal(true);
// Force initializing to physics (prevent noop check).
process_mode = CLIP_PROCESS_IDLE;
set_process_mode(CLIP_PROCESS_PHYSICS);
collision_mask = 1;
set_notify_local_transform(Engine::get_singleton()->is_editor_hint());
points.resize(5);

View File

@ -61,6 +61,7 @@ public:
private:
bool force_change;
bool current;
Viewport *viewport;
Projection mode;
@ -90,13 +91,44 @@ private:
Ref<SpatialVelocityTracker> velocity_tracker;
bool affect_lod = true;
///////////////////////////////////////////////////////
// INTERPOLATION FUNCTIONS
void _physics_interpolation_ensure_transform_calculated(bool p_force = false) const;
void _physics_interpolation_ensure_data_flipped();
// These can be set by derived Cameras,
// if they wish to do processing (while still
// allowing physics interpolation to function).
bool _desired_process_internal = false;
bool _desired_physics_process_internal = false;
mutable struct InterpolationData {
Transform xform_curr;
Transform xform_prev;
Transform xform_interpolated;
Transform camera_xform_interpolated; // After modification according to camera type.
uint32_t last_update_physics_tick = 0;
uint32_t last_update_frame = UINT32_MAX;
} _interpolation_data;
void _update_process_mode();
protected:
// Use from derived classes to set process modes instead of setting directly.
// This is because physics interpolation may need to request process modes additionally.
void set_desired_process_modes(bool p_process_internal, bool p_physics_process_internal);
// Opportunity for derived classes to interpolate extra attributes.
virtual void physics_interpolation_flip_data() {}
virtual void _physics_interpolated_changed();
virtual Transform _get_adjusted_camera_transform(const Transform &p_xform) const;
///////////////////////////////////////////////////////
void _update_camera();
virtual void _request_camera_update();
void _update_camera_mode();
virtual void _physics_interpolated_changed();
void _notification(int p_what);
virtual void _validate_property(PropertyInfo &p_property) const;
@ -135,7 +167,7 @@ public:
void set_znear(float p_znear);
void set_frustum_offset(Vector2 p_offset);
virtual Transform get_camera_transform() const;
Transform get_camera_transform() const;
virtual Vector3 project_ray_normal(const Point2 &p_pos) const;
virtual Vector3 project_ray_origin(const Point2 &p_pos) const;
@ -195,7 +227,9 @@ private:
ProcessMode process_mode;
RID pyramid_shape;
float margin;
float clip_offset;
float clip_offset = 0;
uint32_t collision_mask;
bool clip_to_areas;
bool clip_to_bodies;
@ -204,10 +238,21 @@ private:
Vector<Vector3> points;
///////////////////////////////////////////////////////
// INTERPOLATION FUNCTIONS
struct InterpolatedData {
float clip_offset_curr = 0;
float clip_offset_prev = 0;
} _interpolation_data;
protected:
virtual Transform _get_adjusted_camera_transform(const Transform &p_xform) const;
virtual void physics_interpolation_flip_data();
virtual void _physics_interpolated_changed();
///////////////////////////////////////////////////////
void _notification(int p_what);
static void _bind_methods();
virtual Transform get_camera_transform() const;
public:
void set_clip_to_areas(bool p_clip);

View File

@ -900,14 +900,6 @@ void CPUParticles::_particles_process(float p_delta) {
p.velocity.z = 0.0;
p.transform.origin.z = 0.0;
}
// Teleport if starting a new particle, so
// we don't get a streak from the old position
// to this new start.
if (_interpolated) {
p.copy_to(particles_prev[i]);
}
} else if (!p.active) {
continue;
} else if (p.time > p.lifetime) {
@ -1012,6 +1004,13 @@ void CPUParticles::_particles_process(float p_delta) {
}
p.transform.origin += p.velocity * local_delta;
// Teleport if starting a new particle, so
// we don't get a streak from the old position
// to this new start.
if (restart && _interpolated) {
p.copy_to(particles_prev[i]);
}
}
}

View File

@ -359,7 +359,7 @@ void Spatial::_disable_client_physics_interpolation() {
}
Transform Spatial::_get_global_transform_interpolated(real_t p_interpolation_fraction) {
ERR_FAIL_NULL_V(is_inside_tree(), Transform());
ERR_FAIL_COND_V(!is_inside_tree(), Transform());
// set in motion the mechanisms for client side interpolation if not already active
if (!_is_physics_interpolated_client_side()) {

View File

@ -99,12 +99,11 @@ void VisualInstance::_notification(int p_what) {
case NOTIFICATION_TRANSFORM_CHANGED: {
if (_is_vi_visible() || is_physics_interpolated_and_enabled()) {
if (!_is_using_identity_transform()) {
Transform gt = get_global_transform();
VisualServer::get_singleton()->instance_set_transform(instance, gt);
VisualServer::get_singleton()->instance_set_transform(instance, get_global_transform());
// For instance when first adding to the tree, when the previous transform is
// unset, to prevent streaking from the origin.
if (_is_physics_interpolation_reset_requested()) {
if (_is_physics_interpolation_reset_requested() && is_physics_interpolated_and_enabled() && is_inside_tree()) {
if (_is_vi_visible()) {
_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
}
@ -114,7 +113,14 @@ void VisualInstance::_notification(int p_what) {
}
} break;
case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
if (_is_vi_visible() && is_physics_interpolated()) {
if (_is_vi_visible() && is_physics_interpolated() && is_inside_tree()) {
// We must ensure the VisualServer transform is up to date before resetting.
// This is because NOTIFICATION_TRANSFORM_CHANGED is deferred,
// and cannot be relied to be called in order before NOTIFICATION_RESET_PHYSICS_INTERPOLATION.
if (!_is_using_identity_transform()) {
VisualServer::get_singleton()->instance_set_transform(instance, get_global_transform());
}
VisualServer::get_singleton()->instance_reset_physics_interpolation(instance);
}
} break;

View File

@ -101,6 +101,12 @@ void Node::_notification(int p_notification) {
get_tree()->node_count++;
orphan_node_count--;
// Allow physics interpolated nodes to automatically reset when added to the tree
// (this is to save the user doing this manually each time).
if (get_tree()->is_physics_interpolation_enabled()) {
_set_physics_interpolation_reset_requested(true);
}
} break;
case NOTIFICATION_EXIT_TREE: {
ERR_FAIL_COND(!get_viewport());
@ -221,14 +227,14 @@ void Node::_propagate_physics_interpolated(bool p_interpolated) {
data.blocked--;
}
void Node::_propagate_physics_interpolation_reset_requested() {
void Node::_propagate_physics_interpolation_reset_requested(bool p_requested) {
if (is_physics_interpolated()) {
data.physics_interpolation_reset_requested = true;
data.physics_interpolation_reset_requested = p_requested;
}
data.blocked++;
for (int i = 0; i < data.children.size(); i++) {
data.children[i]->_propagate_physics_interpolation_reset_requested();
data.children[i]->_propagate_physics_interpolation_reset_requested(p_requested);
}
data.blocked--;
}
@ -885,15 +891,23 @@ void Node::set_physics_interpolation_mode(PhysicsInterpolationMode p_mode) {
// if swapping from interpolated to non-interpolated, use this as
// an extra means to cause a reset
if (is_physics_interpolated() && !interpolate) {
reset_physics_interpolation();
if (is_physics_interpolated() && !interpolate && is_inside_tree()) {
propagate_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
}
_propagate_physics_interpolated(interpolate);
}
void Node::reset_physics_interpolation() {
propagate_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
if (is_inside_tree()) {
propagate_notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
// If `reset_physics_interpolation()` is called explicitly by the user
// (e.g. from scripts) then we prevent deferred auto-resets taking place.
// The user is trusted to call reset in the right order, and auto-reset
// will interfere with their control of prev / curr, so should be turned off.
_propagate_physics_interpolation_reset_requested(false);
}
}
float Node::get_physics_process_delta_time() const {
@ -1297,12 +1311,6 @@ void Node::_add_child_nocheck(Node *p_child, const StringName &p_name) {
//recognize children created in this node constructor
p_child->data.parent_owned = data.in_constructor;
add_child_notify(p_child);
// 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) {

View File

@ -209,7 +209,7 @@ private:
void _propagate_exit_tree();
void _propagate_after_exit_branch(bool p_exiting_tree);
void _propagate_physics_interpolated(bool p_interpolated);
void _propagate_physics_interpolation_reset_requested();
void _propagate_physics_interpolation_reset_requested(bool p_requested);
void _print_stray_nodes();
void _propagate_pause_owner(Node *p_owner);
void _propagate_groups_dirty();

View File

@ -555,6 +555,9 @@ void SceneTree::client_physics_interpolation_remove_spatial(SelfList<Spatial> *p
void SceneTree::iteration_prepare() {
if (_physics_interpolation_enabled) {
// Make sure any pending transforms from the last tick / frame
// are flushed before pumping the interpolation prev and currents.
flush_transform_notifications();
VisualServer::get_singleton()->tick();
}
}