Skip to content

Commit 56fc0fd

Browse files
rburinglawnjelly
andcommitted
CPUParticles2D - Add ability to follow physics interpolated target
Allows a non-interpolated particle system to closely follow an interpolated target without tracking ahead of the target, by performing fixed timestep interpolation on the particle system global transform, and using this for emission. Co-authored-by: lawnjelly <[email protected]>
1 parent a7146ef commit 56fc0fd

File tree

12 files changed

+157
-34
lines changed

12 files changed

+157
-34
lines changed

doc/classes/CPUParticles2D.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
<member name="particle_flag_align_y" type="bool" setter="set_particle_flag" getter="get_particle_flag" default="false">
242242
Align Y axis of particle with the direction of its velocity.
243243
</member>
244+
<member name="physics_interpolation_mode" type="int" setter="set_physics_interpolation_mode" getter="get_physics_interpolation_mode" overrides="Node" enum="Node.PhysicsInterpolationMode" default="2" />
244245
<member name="preprocess" type="float" setter="set_pre_process_time" getter="get_pre_process_time" default="0.0">
245246
Particle system starts as if it had already run for this many seconds.
246247
</member>

scene/2d/camera_2d.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,9 @@ void Camera2D::_notification(int p_what) {
315315
}
316316
if (is_physics_interpolated_and_enabled()) {
317317
_ensure_update_interpolation_data();
318-
_interpolation_data.xform_curr = get_camera_transform();
318+
if (Engine::get_singleton()->is_in_physics_frame()) {
319+
_interpolation_data.xform_curr = get_camera_transform();
320+
}
319321
}
320322
} break;
321323

scene/2d/cpu_particles_2d.cpp

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "cpu_particles_2d.compat.inc"
3333

3434
#include "core/math/random_number_generator.h"
35+
#include "core/math/transform_interpolator.h"
3536
#include "scene/2d/gpu_particles_2d.h"
3637
#include "scene/resources/atlas_texture.h"
3738
#include "scene/resources/canvas_item_material.h"
@@ -96,7 +97,14 @@ void CPUParticles2D::set_lifetime_randomness(double p_random) {
9697

9798
void CPUParticles2D::set_use_local_coordinates(bool p_enable) {
9899
local_coords = p_enable;
99-
set_notify_transform(!p_enable);
100+
101+
// Prevent sending item transforms when using global coords,
102+
// and inform the RenderingServer to use identity mode.
103+
set_canvas_item_use_identity_transform(!local_coords);
104+
105+
// We only need NOTIFICATION_TRANSFORM_CHANGED
106+
// when following an interpolated target.
107+
set_notify_transform(_interpolation_data.interpolated_follow);
100108
}
101109

102110
void CPUParticles2D::set_speed_scale(double p_scale) {
@@ -228,6 +236,27 @@ void CPUParticles2D::_texture_changed() {
228236
}
229237
}
230238

239+
void CPUParticles2D::_refresh_interpolation_state() {
240+
if (!is_inside_tree()) {
241+
return;
242+
}
243+
244+
// The logic for whether to do an interpolated follow.
245+
// This is rather complex, but basically:
246+
// If project setting interpolation is ON and this particle system is in global mode,
247+
// we will follow the INTERPOLATED position rather than the actual position.
248+
// This is so that particles aren't generated AHEAD of the interpolated parent.
249+
bool follow = !local_coords && get_tree()->is_physics_interpolation_enabled();
250+
251+
if (follow == _interpolation_data.interpolated_follow) {
252+
return;
253+
}
254+
255+
_interpolation_data.interpolated_follow = follow;
256+
257+
set_physics_process_internal(_interpolation_data.interpolated_follow);
258+
}
259+
231260
Ref<Texture2D> CPUParticles2D::get_texture() const {
232261
return texture;
233262
}
@@ -600,6 +629,9 @@ void CPUParticles2D::_update_internal() {
600629
return;
601630
}
602631

632+
// Change update mode?
633+
_refresh_interpolation_state();
634+
603635
double delta = get_process_delta_time();
604636
if (!active && !emitting) {
605637
set_process_internal(false);
@@ -679,7 +711,11 @@ void CPUParticles2D::_particles_process(double p_delta) {
679711
Transform2D emission_xform;
680712
Transform2D velocity_xform;
681713
if (!local_coords) {
682-
emission_xform = get_global_transform();
714+
if (!_interpolation_data.interpolated_follow) {
715+
emission_xform = get_global_transform();
716+
} else {
717+
TransformInterpolator::interpolate_transform_2d(_interpolation_data.global_xform_prev, _interpolation_data.global_xform_curr, emission_xform, Engine::get_singleton()->get_physics_interpolation_fraction());
718+
}
683719
velocity_xform = emission_xform;
684720
velocity_xform[2] = Vector2();
685721
}
@@ -1142,6 +1178,17 @@ void CPUParticles2D::_notification(int p_what) {
11421178
switch (p_what) {
11431179
case NOTIFICATION_ENTER_TREE: {
11441180
set_process_internal(emitting);
1181+
1182+
_refresh_interpolation_state();
1183+
1184+
set_physics_process_internal(emitting && _interpolation_data.interpolated_follow);
1185+
1186+
// If we are interpolated following, then reset physics interpolation
1187+
// when first appearing. This won't be called by canvas item, as in the
1188+
// following mode, is_physics_interpolated() is actually FALSE.
1189+
if (_interpolation_data.interpolated_follow) {
1190+
notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION);
1191+
}
11451192
} break;
11461193

11471194
case NOTIFICATION_EXIT_TREE: {
@@ -1170,37 +1217,28 @@ void CPUParticles2D::_notification(int p_what) {
11701217
_update_internal();
11711218
} break;
11721219

1173-
case NOTIFICATION_TRANSFORM_CHANGED: {
1174-
inv_emission_transform = get_global_transform().affine_inverse();
1175-
1176-
if (!local_coords) {
1177-
int pc = particles.size();
1178-
1179-
float *w = particle_data.ptrw();
1180-
const Particle *r = particles.ptr();
1181-
float *ptr = w;
1182-
1183-
for (int i = 0; i < pc; i++) {
1184-
Transform2D t = inv_emission_transform * r[i].transform;
1185-
1186-
if (r[i].active) {
1187-
ptr[0] = t.columns[0][0];
1188-
ptr[1] = t.columns[1][0];
1189-
ptr[2] = 0;
1190-
ptr[3] = t.columns[2][0];
1191-
ptr[4] = t.columns[0][1];
1192-
ptr[5] = t.columns[1][1];
1193-
ptr[6] = 0;
1194-
ptr[7] = t.columns[2][1];
1195-
1196-
} else {
1197-
memset(ptr, 0, sizeof(float) * 8);
1198-
}
1220+
case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
1221+
if (_interpolation_data.interpolated_follow) {
1222+
// Keep the interpolated follow target updated.
1223+
_interpolation_data.global_xform_prev = _interpolation_data.global_xform_curr;
1224+
_interpolation_data.global_xform_curr = get_global_transform();
1225+
}
1226+
} break;
11991227

1200-
ptr += 16;
1228+
case NOTIFICATION_TRANSFORM_CHANGED: {
1229+
if (_interpolation_data.interpolated_follow) {
1230+
// If the transform has been updated AFTER the physics tick, keep data flowing.
1231+
if (Engine::get_singleton()->is_in_physics_frame()) {
1232+
_interpolation_data.global_xform_curr = get_global_transform();
12011233
}
12021234
}
12031235
} break;
1236+
1237+
case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: {
1238+
// Make sure current is up to date with any pending global transform changes.
1239+
_interpolation_data.global_xform_curr = get_global_transform_const();
1240+
_interpolation_data.global_xform_prev = _interpolation_data.global_xform_curr;
1241+
} break;
12041242
}
12051243
}
12061244

@@ -1559,6 +1597,12 @@ CPUParticles2D::CPUParticles2D() {
15591597
set_color(Color(1, 1, 1, 1));
15601598

15611599
_update_mesh_texture();
1600+
1601+
// CPUParticles2D defaults to interpolation off.
1602+
// This is because the result often looks better when the particles are updated every frame.
1603+
// Note that children will need to explicitly turn back on interpolation if they want to use it,
1604+
// rather than relying on inherit mode.
1605+
set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF);
15621606
}
15631607

15641608
CPUParticles2D::~CPUParticles2D() {

scene/2d/cpu_particles_2d.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ class CPUParticles2D : public Node2D {
189189

190190
Mutex update_mutex;
191191

192+
struct InterpolationData {
193+
// Whether this particle is non-interpolated, but following an interpolated parent.
194+
bool interpolated_follow = false;
195+
196+
// If doing interpolated follow, we need to keep these updated per tick.
197+
Transform2D global_xform_curr;
198+
Transform2D global_xform_prev;
199+
} _interpolation_data;
200+
192201
void _update_render_thread();
193202

194203
void _update_mesh_texture();
@@ -197,6 +206,8 @@ class CPUParticles2D : public Node2D {
197206

198207
void _texture_changed();
199208

209+
void _refresh_interpolation_state();
210+
200211
protected:
201212
static void _bind_methods();
202213
void _notification(int p_what);

scene/2d/node_2d.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ void Node2D::set_transform(const Transform2D &p_transform) {
374374
transform = p_transform;
375375
_set_xform_dirty(true);
376376

377-
RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), transform);
377+
if (!_is_using_identity_transform()) {
378+
RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), transform);
379+
}
378380

379381
_notify_transform();
380382
}

scene/main/canvas_item.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@ Transform2D CanvasItem::get_global_transform() const {
188188
return global_transform;
189189
}
190190

191+
// Same as get_global_transform() but no reset for `global_invalid`.
192+
Transform2D CanvasItem::get_global_transform_const() const {
193+
if (_is_global_invalid()) {
194+
const CanvasItem *pi = get_parent_item();
195+
if (pi) {
196+
global_transform = pi->get_global_transform_const() * get_transform();
197+
} else {
198+
global_transform = get_transform();
199+
}
200+
}
201+
202+
return global_transform;
203+
}
204+
191205
void CanvasItem::_set_global_invalid(bool p_invalid) const {
192206
if (is_group_processing()) {
193207
if (p_invalid) {
@@ -1039,6 +1053,24 @@ void CanvasItem::_physics_interpolated_changed() {
10391053
RenderingServer::get_singleton()->canvas_item_set_interpolated(canvas_item, is_physics_interpolated());
10401054
}
10411055

1056+
void CanvasItem::set_canvas_item_use_identity_transform(bool p_enable) {
1057+
// Prevent sending item transforms to RenderingServer when using global coords.
1058+
_set_use_identity_transform(p_enable);
1059+
1060+
// Let RenderingServer know not to concatenate the parent transform during the render.
1061+
RenderingServer::get_singleton()->canvas_item_set_use_identity_transform(get_canvas_item(), p_enable);
1062+
1063+
if (is_inside_tree()) {
1064+
if (p_enable) {
1065+
// Make sure item is using identity transform in server.
1066+
RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), Transform2D());
1067+
} else {
1068+
// Make sure item transform is up to date in server if switching identity transform off.
1069+
RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), get_transform());
1070+
}
1071+
}
1072+
}
1073+
10421074
Rect2 CanvasItem::get_viewport_rect() const {
10431075
ERR_READ_THREAD_GUARD_V(Rect2());
10441076
ERR_FAIL_COND_V(!is_inside_tree(), Rect2());

scene/main/canvas_item.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ class CanvasItem : public Node {
170170

171171
void item_rect_changed(bool p_size_changed = true);
172172

173+
void set_canvas_item_use_identity_transform(bool p_enable);
174+
173175
void _notification(int p_what);
174176
static void _bind_methods();
175177

@@ -339,6 +341,7 @@ class CanvasItem : public Node {
339341
virtual Transform2D get_transform() const = 0;
340342

341343
virtual Transform2D get_global_transform() const;
344+
virtual Transform2D get_global_transform_const() const;
342345
virtual Transform2D get_global_transform_with_canvas() const;
343346
virtual Transform2D get_screen_transform() const;
344347

servers/rendering/renderer_canvas_cull.cpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ void RendererCanvasCull::_dependency_deleted(const RID &p_dependency, Dependency
7070
void RendererCanvasCull::_render_canvas_item_tree(RID p_to_render_target, Canvas::ChildItem *p_child_items, int p_child_item_count, const Transform2D &p_transform, const Rect2 &p_clip_rect, const Color &p_modulate, RendererCanvasRender::Light *p_lights, RendererCanvasRender::Light *p_directional_lights, RenderingServer::CanvasItemTextureFilter p_default_filter, RenderingServer::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_vertices_to_pixel, uint32_t p_canvas_cull_mask, RenderingMethod::RenderInfo *r_render_info) {
7171
RENDER_TIMESTAMP("Cull CanvasItem Tree");
7272

73+
// This is used to avoid passing the camera transform down the rendering
74+
// function calls, as it won't be used in 99% of cases, because the camera
75+
// transform is normally concatenated with the item global transform.
76+
_current_camera_transform = p_transform;
77+
7378
memset(z_list, 0, z_range * sizeof(RendererCanvasRender::Item *));
7479
memset(z_last_list, 0, z_range * sizeof(RendererCanvasRender::Item *));
7580

@@ -242,14 +247,14 @@ void RendererCanvasCull::_attach_canvas_item_for_draw(RendererCanvasCull::Item *
242247
}
243248

244249
if (((ci->commands != nullptr || ci->visibility_notifier) && p_clip_rect.intersects(p_global_rect, true)) || ci->vp_render || ci->copy_back_buffer) {
245-
//something to draw?
250+
// Something to draw?
246251

247252
if (ci->update_when_visible) {
248253
RenderingServerDefault::redraw_request();
249254
}
250255

251256
if (ci->commands != nullptr || ci->copy_back_buffer) {
252-
ci->final_transform = p_transform;
257+
ci->final_transform = !ci->use_identity_transform ? p_transform : _current_camera_transform;
253258
ci->final_modulate = p_modulate * ci->self_modulate;
254259
ci->global_rect_cache = p_global_rect;
255260
ci->global_rect_cache.position -= p_clip_rect.position;
@@ -322,6 +327,10 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2
322327
}
323328
}
324329

330+
// Always calculate final transform as if not using identity xform.
331+
// This is so the expected transform is passed to children.
332+
// However, if use_identity_xform is set,
333+
// we can override the transform for rendering purposes for this item only.
325334
Transform2D self_xform;
326335
Transform2D final_xform;
327336
if (p_is_already_y_sorted) {
@@ -360,7 +369,12 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2
360369
ci->repeat_source_item = repeat_source_item;
361370
}
362371

363-
Rect2 global_rect = final_xform.xform(rect);
372+
Rect2 global_rect;
373+
if (!p_canvas_item->use_identity_transform) {
374+
global_rect = final_xform.xform(rect);
375+
} else {
376+
global_rect = _current_camera_transform.xform(rect);
377+
}
364378
if (repeat_source_item && (repeat_size.x || repeat_size.y)) {
365379
// Top-left repeated rect.
366380
Rect2 corner_rect = global_rect;
@@ -686,6 +700,13 @@ void RendererCanvasCull::canvas_item_set_draw_behind_parent(RID p_item, bool p_e
686700
canvas_item->behind = p_enable;
687701
}
688702

703+
void RendererCanvasCull::canvas_item_set_use_identity_transform(RID p_item, bool p_enable) {
704+
Item *canvas_item = canvas_item_owner.get_or_null(p_item);
705+
ERR_FAIL_NULL(canvas_item);
706+
707+
canvas_item->use_identity_transform = p_enable;
708+
}
709+
689710
void RendererCanvasCull::canvas_item_set_update_when_visible(RID p_item, bool p_update) {
690711
Item *canvas_item = canvas_item_owner.get_or_null(p_item);
691712
ERR_FAIL_NULL(canvas_item);

servers/rendering/renderer_canvas_cull.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ class RendererCanvasCull {
217217
RendererCanvasRender::Item **z_list;
218218
RendererCanvasRender::Item **z_last_list;
219219

220+
Transform2D _current_camera_transform;
221+
220222
public:
221223
void render_canvas(RID p_render_target, Canvas *p_canvas, const Transform2D &p_transform, RendererCanvasRender::Light *p_lights, RendererCanvasRender::Light *p_directional_lights, const Rect2 &p_clip_rect, RS::CanvasItemTextureFilter p_default_filter, RS::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_transforms_to_pixel, bool p_snap_2d_vertices_to_pixel, uint32_t p_canvas_cull_mask, RenderingMethod::RenderInfo *r_render_info = nullptr);
222224

@@ -250,6 +252,7 @@ class RendererCanvasCull {
250252
void canvas_item_set_self_modulate(RID p_item, const Color &p_color);
251253

252254
void canvas_item_set_draw_behind_parent(RID p_item, bool p_enable);
255+
void canvas_item_set_use_identity_transform(RID p_item, bool p_enable);
253256

254257
void canvas_item_set_update_when_visible(RID p_item, bool p_update);
255258

servers/rendering/renderer_canvas_render.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ class RendererCanvasRender {
321321
bool update_when_visible : 1;
322322
bool on_interpolate_transform_list : 1;
323323
bool interpolated : 1;
324+
bool use_identity_transform : 1;
324325

325326
struct CanvasGroup {
326327
RS::CanvasGroupMode mode;
@@ -486,6 +487,7 @@ class RendererCanvasRender {
486487
repeat_source = false;
487488
on_interpolate_transform_list = false;
488489
interpolated = true;
490+
use_identity_transform = false;
489491
}
490492
virtual ~Item() {
491493
clear();

0 commit comments

Comments
 (0)