From a117a3307a9eec69c5ed7c80a0b4eb272fafc474 Mon Sep 17 00:00:00 2001 From: lawnjelly Date: Tue, 11 Jul 2023 11:45:23 +0100 Subject: [PATCH] Physics Interpolation - add support for CPUParticles2D Similar to the existing 3D CPUParticles physics interpolation. --- scene/2d/canvas_item.cpp | 18 + scene/2d/canvas_item.h | 1 + scene/2d/cpu_particles_2d.cpp | 427 ++++++++++++++---------- scene/2d/cpu_particles_2d.h | 67 +++- scene/2d/node_2d.cpp | 4 +- servers/visual/rasterizer.h | 2 + servers/visual/visual_server_canvas.cpp | 26 +- servers/visual/visual_server_canvas.h | 2 + servers/visual/visual_server_raster.h | 1 + servers/visual/visual_server_wrap_mt.h | 1 + servers/visual_server.h | 1 + 11 files changed, 371 insertions(+), 179 deletions(-) diff --git a/scene/2d/canvas_item.cpp b/scene/2d/canvas_item.cpp index 8539ce9093d..0f93aa48bec 100644 --- a/scene/2d/canvas_item.cpp +++ b/scene/2d/canvas_item.cpp @@ -714,6 +714,24 @@ int CanvasItem::get_light_mask() const { return light_mask; } +void CanvasItem::set_canvas_item_use_identity_transform(bool p_enable) { + // Prevent sending item transforms to VisualServer when using global coords. + _set_use_identity_transform(p_enable); + + // Let VisualServer know not to concatenate the parent transform during the render. + VisualServer::get_singleton()->canvas_item_set_ignore_parent_transform(get_canvas_item(), p_enable); + + if (is_inside_tree()) { + if (p_enable) { + // Make sure item is using identity transform in server. + VisualServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), Transform2D()); + } else { + // Make sure item transform is up to date in server if switching identity transform off. + VisualServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), get_transform()); + } + } +} + void CanvasItem::item_rect_changed(bool p_size_changed) { if (p_size_changed) { update(); diff --git a/scene/2d/canvas_item.h b/scene/2d/canvas_item.h index a78096e4a03..4f17d733b94 100644 --- a/scene/2d/canvas_item.h +++ b/scene/2d/canvas_item.h @@ -237,6 +237,7 @@ protected: } void item_rect_changed(bool p_size_changed = true); + void set_canvas_item_use_identity_transform(bool p_enable); void _notification(int p_what); static void _bind_methods(); diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index 1bbc042e9ef..7f7f49e5947 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -51,6 +51,7 @@ void CPUParticles2D::set_amount(int p_amount) { ERR_FAIL_COND_MSG(p_amount < 1, "Amount of particles must be greater than 0."); particles.resize(p_amount); + particles_prev.resize(p_amount); { PoolVector::Write w = particles.write(); @@ -59,13 +60,19 @@ void CPUParticles2D::set_amount(int p_amount) { memset(static_cast(&w[0]), 0, p_amount * sizeof(Particle)); // cast to prevent compiler warning .. note this relies on Particle not containing any complex types. // an alternative is to use some zero method per item but the generated code will be far less efficient. + + for (int i = 0; i < p_amount; i++) { + particles_prev[i].blank(); + } } particle_data.resize((8 + 4 + 1) * p_amount); + particle_data_prev.resize(particle_data.size()); // We must fill immediately to prevent garbage data and Nans // being sent to the visual server with set_as_bulk_array, // if this is sent before being regularly updated. particle_data.fill(0); + particle_data_prev.fill(0); VS::get_singleton()->multimesh_allocate(multimesh, p_amount, VS::MULTIMESH_TRANSFORM_2D, VS::MULTIMESH_COLOR_8BIT, VS::MULTIMESH_CUSTOM_DATA_FLOAT); @@ -95,6 +102,18 @@ void CPUParticles2D::set_lifetime_randomness(float p_random) { void CPUParticles2D::set_use_local_coordinates(bool p_enable) { local_coords = p_enable; set_notify_transform(!p_enable); + + // Prevent sending item transforms when using global coords, + // and inform VisualServer to use identity mode. +#ifdef GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY + set_canvas_item_use_identity_transform((_interpolated) && (!p_enable)); + + // Always reset this, as it is unused when interpolation is on. + // (i.e. We do particles in global space, rather than pseudo globalspace.) + inv_emission_transform = Transform2D(); +#else + set_canvas_item_use_identity_transform(!p_enable); +#endif } void CPUParticles2D::set_speed_scale(float p_scale) { @@ -519,13 +538,24 @@ static float rand_from_seed(uint32_t &seed) { return float(seed % uint32_t(65536)) / 65535.0; } -void CPUParticles2D::_update_internal() { +void CPUParticles2D::_update_internal(bool p_on_physics_tick) { if (particles.size() == 0 || !is_visible_in_tree()) { _set_redraw(false); return; } - float delta = get_process_delta_time(); + // Change update mode? + _refresh_interpolation_state(); + + float delta = 0.0f; + + // Is this update occurring on a physics tick (i.e. interpolated), or a frame tick? + if (p_on_physics_tick) { + delta = get_physics_process_delta_time(); + } else { + delta = get_process_delta_time(); + } + if (emitting) { inactive_time = 0; } else { @@ -584,6 +614,117 @@ void CPUParticles2D::_update_internal() { } _update_particle_data_buffer(); + + // If we are interpolating, we send the data to the VisualServer + // right away on a physics tick instead of waiting until a render frame. + if (p_on_physics_tick && redraw) { + _update_render_thread(); + } +} + +void CPUParticles2D::_particle_process(Particle &r_p, const Transform2D &p_emission_xform, float p_local_delta, float &r_tv) { + uint32_t alt_seed = r_p.seed; + + r_p.time += p_local_delta; + r_p.custom[1] = r_p.time / lifetime; + r_tv = r_p.time / r_p.lifetime; + + float tex_linear_velocity = 0.0; + if (curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY].is_valid()) { + tex_linear_velocity = curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY]->interpolate(r_tv); + } + + float tex_orbit_velocity = 0.0; + if (curve_parameters[PARAM_ORBIT_VELOCITY].is_valid()) { + tex_orbit_velocity = curve_parameters[PARAM_ORBIT_VELOCITY]->interpolate(r_tv); + } + + float tex_angular_velocity = 0.0; + if (curve_parameters[PARAM_ANGULAR_VELOCITY].is_valid()) { + tex_angular_velocity = curve_parameters[PARAM_ANGULAR_VELOCITY]->interpolate(r_tv); + } + + float tex_linear_accel = 0.0; + if (curve_parameters[PARAM_LINEAR_ACCEL].is_valid()) { + tex_linear_accel = curve_parameters[PARAM_LINEAR_ACCEL]->interpolate(r_tv); + } + + float tex_tangential_accel = 0.0; + if (curve_parameters[PARAM_TANGENTIAL_ACCEL].is_valid()) { + tex_tangential_accel = curve_parameters[PARAM_TANGENTIAL_ACCEL]->interpolate(r_tv); + } + + float tex_radial_accel = 0.0; + if (curve_parameters[PARAM_RADIAL_ACCEL].is_valid()) { + tex_radial_accel = curve_parameters[PARAM_RADIAL_ACCEL]->interpolate(r_tv); + } + + float tex_damping = 0.0; + if (curve_parameters[PARAM_DAMPING].is_valid()) { + tex_damping = curve_parameters[PARAM_DAMPING]->interpolate(r_tv); + } + + float tex_angle = 0.0; + if (curve_parameters[PARAM_ANGLE].is_valid()) { + tex_angle = curve_parameters[PARAM_ANGLE]->interpolate(r_tv); + } + float tex_anim_speed = 0.0; + if (curve_parameters[PARAM_ANIM_SPEED].is_valid()) { + tex_anim_speed = curve_parameters[PARAM_ANIM_SPEED]->interpolate(r_tv); + } + + float tex_anim_offset = 0.0; + if (curve_parameters[PARAM_ANIM_OFFSET].is_valid()) { + tex_anim_offset = curve_parameters[PARAM_ANIM_OFFSET]->interpolate(r_tv); + } + + Vector2 force = gravity; + Vector2 pos = r_p.transform[2]; + + // Apply linear acceleration. + force += r_p.velocity.length() > 0.0 ? r_p.velocity.normalized() * (parameters[PARAM_LINEAR_ACCEL] + tex_linear_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_LINEAR_ACCEL]) : Vector2(); + + // Apply radial acceleration. + Vector2 org = p_emission_xform[2]; + Vector2 diff = pos - org; + force += diff.length() > 0.0 ? diff.normalized() * (parameters[PARAM_RADIAL_ACCEL] + tex_radial_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_RADIAL_ACCEL]) : Vector2(); + + // Apply tangential acceleration. + Vector2 yx = Vector2(diff.y, diff.x); + force += yx.length() > 0.0 ? (yx * Vector2(-1.0, 1.0)).normalized() * ((parameters[PARAM_TANGENTIAL_ACCEL] + tex_tangential_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_TANGENTIAL_ACCEL])) : Vector2(); + + // Apply attractor forces. + r_p.velocity += force * p_local_delta; + + // Orbit velocity. + float orbit_amount = (parameters[PARAM_ORBIT_VELOCITY] + tex_orbit_velocity) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_ORBIT_VELOCITY]); + if (orbit_amount != 0.0) { + float ang = orbit_amount * p_local_delta * Math_PI * 2.0; + // Not sure why the ParticlesMaterial code uses a clockwise rotation matrix, + // but we use -ang here to reproduce its behavior. + Transform2D rot = Transform2D(-ang, Vector2()); + r_p.transform[2] -= diff; + r_p.transform[2] += rot.basis_xform(diff); + } + if (curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY].is_valid()) { + r_p.velocity = r_p.velocity.normalized() * tex_linear_velocity; + } + + if (parameters[PARAM_DAMPING] + tex_damping > 0.0) { + float v = r_p.velocity.length(); + float damp = (parameters[PARAM_DAMPING] + tex_damping) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_DAMPING]); + v -= damp * p_local_delta; + if (v < 0.0) { + r_p.velocity = Vector2(); + } else { + r_p.velocity = r_p.velocity.normalized() * v; + } + } + float base_angle = (parameters[PARAM_ANGLE] + tex_angle) * Math::lerp(1.0f, r_p.angle_rand, randomness[PARAM_ANGLE]); + base_angle += r_p.custom[1] * lifetime * (parameters[PARAM_ANGULAR_VELOCITY] + tex_angular_velocity) * Math::lerp(1.0f, rand_from_seed(alt_seed) * 2.0f - 1.0f, randomness[PARAM_ANGULAR_VELOCITY]); + r_p.rotation = Math::deg2rad(base_angle); //angle + float animation_phase = (parameters[PARAM_ANIM_OFFSET] + tex_anim_offset) * Math::lerp(1.0f, r_p.anim_offset_rand, randomness[PARAM_ANIM_OFFSET]) + r_tv * (parameters[PARAM_ANIM_SPEED] + tex_anim_speed) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_ANIM_SPEED]); + r_p.custom[2] = animation_phase; } void CPUParticles2D::_particles_process(float p_delta) { @@ -622,6 +763,12 @@ void CPUParticles2D::_particles_process(float p_delta) { continue; } + // For interpolation we need to keep a record of previous particles. + if (_interpolated) { + DEV_ASSERT((uint32_t)particles.size() == particles_prev.size()); + p.copy_to(particles_prev[i]); + } + float local_delta = p_delta; // The phase is a ratio between 0 (birth) and 1 (end of life) for each particle. @@ -777,104 +924,7 @@ void CPUParticles2D::_particles_process(float p_delta) { p.active = false; tv = 1.0; } else { - uint32_t alt_seed = p.seed; - - p.time += local_delta; - p.custom[1] = p.time / lifetime; - tv = p.time / p.lifetime; - - float tex_linear_velocity = 0.0; - if (curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY].is_valid()) { - tex_linear_velocity = curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY]->interpolate(tv); - } - - float tex_orbit_velocity = 0.0; - if (curve_parameters[PARAM_ORBIT_VELOCITY].is_valid()) { - tex_orbit_velocity = curve_parameters[PARAM_ORBIT_VELOCITY]->interpolate(tv); - } - - float tex_angular_velocity = 0.0; - if (curve_parameters[PARAM_ANGULAR_VELOCITY].is_valid()) { - tex_angular_velocity = curve_parameters[PARAM_ANGULAR_VELOCITY]->interpolate(tv); - } - - float tex_linear_accel = 0.0; - if (curve_parameters[PARAM_LINEAR_ACCEL].is_valid()) { - tex_linear_accel = curve_parameters[PARAM_LINEAR_ACCEL]->interpolate(tv); - } - - float tex_tangential_accel = 0.0; - if (curve_parameters[PARAM_TANGENTIAL_ACCEL].is_valid()) { - tex_tangential_accel = curve_parameters[PARAM_TANGENTIAL_ACCEL]->interpolate(tv); - } - - float tex_radial_accel = 0.0; - if (curve_parameters[PARAM_RADIAL_ACCEL].is_valid()) { - tex_radial_accel = curve_parameters[PARAM_RADIAL_ACCEL]->interpolate(tv); - } - - float tex_damping = 0.0; - if (curve_parameters[PARAM_DAMPING].is_valid()) { - tex_damping = curve_parameters[PARAM_DAMPING]->interpolate(tv); - } - - float tex_angle = 0.0; - if (curve_parameters[PARAM_ANGLE].is_valid()) { - tex_angle = curve_parameters[PARAM_ANGLE]->interpolate(tv); - } - float tex_anim_speed = 0.0; - if (curve_parameters[PARAM_ANIM_SPEED].is_valid()) { - tex_anim_speed = curve_parameters[PARAM_ANIM_SPEED]->interpolate(tv); - } - - float tex_anim_offset = 0.0; - if (curve_parameters[PARAM_ANIM_OFFSET].is_valid()) { - tex_anim_offset = curve_parameters[PARAM_ANIM_OFFSET]->interpolate(tv); - } - - Vector2 force = gravity; - Vector2 pos = p.transform[2]; - - //apply linear acceleration - force += p.velocity.length() > 0.0 ? p.velocity.normalized() * (parameters[PARAM_LINEAR_ACCEL] + tex_linear_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_LINEAR_ACCEL]) : Vector2(); - //apply radial acceleration - Vector2 org = emission_xform[2]; - Vector2 diff = pos - org; - force += diff.length() > 0.0 ? diff.normalized() * (parameters[PARAM_RADIAL_ACCEL] + tex_radial_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_RADIAL_ACCEL]) : Vector2(); - //apply tangential acceleration; - Vector2 yx = Vector2(diff.y, diff.x); - force += yx.length() > 0.0 ? (yx * Vector2(-1.0, 1.0)).normalized() * ((parameters[PARAM_TANGENTIAL_ACCEL] + tex_tangential_accel) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_TANGENTIAL_ACCEL])) : Vector2(); - //apply attractor forces - p.velocity += force * local_delta; - //orbit velocity - float orbit_amount = (parameters[PARAM_ORBIT_VELOCITY] + tex_orbit_velocity) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_ORBIT_VELOCITY]); - if (orbit_amount != 0.0) { - float ang = orbit_amount * local_delta * Math_PI * 2.0; - // Not sure why the ParticlesMaterial code uses a clockwise rotation matrix, - // but we use -ang here to reproduce its behavior. - Transform2D rot = Transform2D(-ang, Vector2()); - p.transform[2] -= diff; - p.transform[2] += rot.basis_xform(diff); - } - if (curve_parameters[PARAM_INITIAL_LINEAR_VELOCITY].is_valid()) { - p.velocity = p.velocity.normalized() * tex_linear_velocity; - } - - if (parameters[PARAM_DAMPING] + tex_damping > 0.0) { - float v = p.velocity.length(); - float damp = (parameters[PARAM_DAMPING] + tex_damping) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_DAMPING]); - v -= damp * local_delta; - if (v < 0.0) { - p.velocity = Vector2(); - } else { - p.velocity = p.velocity.normalized() * v; - } - } - float base_angle = (parameters[PARAM_ANGLE] + tex_angle) * Math::lerp(1.0f, p.angle_rand, randomness[PARAM_ANGLE]); - base_angle += p.custom[1] * lifetime * (parameters[PARAM_ANGULAR_VELOCITY] + tex_angular_velocity) * Math::lerp(1.0f, rand_from_seed(alt_seed) * 2.0f - 1.0f, randomness[PARAM_ANGULAR_VELOCITY]); - p.rotation = Math::deg2rad(base_angle); //angle - float animation_phase = (parameters[PARAM_ANIM_OFFSET] + tex_anim_offset) * Math::lerp(1.0f, p.anim_offset_rand, randomness[PARAM_ANIM_OFFSET]) + tv * (parameters[PARAM_ANIM_SPEED] + tex_anim_speed) * Math::lerp(1.0f, rand_from_seed(alt_seed), randomness[PARAM_ANIM_SPEED]); - p.custom[2] = animation_phase; + _particle_process(p, emission_xform, local_delta, tv); } //apply color //apply hue rotation @@ -937,6 +987,13 @@ void CPUParticles2D::_particles_process(float p_delta) { p.transform.elements[1] *= base_scale; p.transform[2] += 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]); + } } } @@ -953,6 +1010,15 @@ void CPUParticles2D::_update_particle_data_buffer() { PoolVector::Read r = particles.read(); float *ptr = w.ptr(); + PoolVector::Write w_prev; + float *ptr_prev = nullptr; + + if (_interpolated) { + DEV_ASSERT(particle_data.size() == particle_data_prev.size()); + w_prev = particle_data_prev.write(); + ptr_prev = w_prev.ptr(); + } + if (draw_order != DRAW_ORDER_INDEX) { ow = particle_order.write(); order = ow.ptr(); @@ -967,63 +1033,94 @@ void CPUParticles2D::_update_particle_data_buffer() { } } - for (int i = 0; i < pc; i++) { - int idx = order ? order[i] : i; - - Transform2D t = r[idx].transform; - + if (_interpolated) { + for (int i = 0; i < pc; i++) { + int idx = order ? order[i] : i; + _fill_particle_data(r[idx], ptr, r[idx].active); + ptr += 13; + _fill_particle_data(particles_prev[idx], ptr_prev, r[idx].active); + ptr_prev += 13; + } + } else { +#ifdef GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY if (!local_coords) { - t = inv_emission_transform * t; - } - - if (r[idx].active) { - ptr[0] = t.elements[0][0]; - ptr[1] = t.elements[1][0]; - ptr[2] = 0; - ptr[3] = t.elements[2][0]; - ptr[4] = t.elements[0][1]; - ptr[5] = t.elements[1][1]; - ptr[6] = 0; - ptr[7] = t.elements[2][1]; - - Color c = r[idx].color; - uint8_t *data8 = (uint8_t *)&ptr[8]; - data8[0] = CLAMP(c.r * 255.0, 0, 255); - data8[1] = CLAMP(c.g * 255.0, 0, 255); - data8[2] = CLAMP(c.b * 255.0, 0, 255); - data8[3] = CLAMP(c.a * 255.0, 0, 255); - - ptr[9] = r[idx].custom[0]; - ptr[10] = r[idx].custom[1]; - ptr[11] = r[idx].custom[2]; - ptr[12] = r[idx].custom[3]; - + inv_emission_transform = get_global_transform().affine_inverse(); + for (int i = 0; i < pc; i++) { + int idx = order ? order[i] : i; + _fill_particle_data(r[idx], ptr, r[idx].active); + ptr += 13; + } } else { - memset(ptr, 0, sizeof(float) * 13); + for (int i = 0; i < pc; i++) { + int idx = order ? order[i] : i; + _fill_particle_data(r[idx], ptr, r[idx].active); + ptr += 13; + } } - - ptr += 13; +#else + for (int i = 0; i < pc; i++) { + int idx = order ? order[i] : i; + _fill_particle_data(r[idx], ptr, r[idx].active); + ptr += 13; + } +#endif } } update_mutex.unlock(); } +void CPUParticles2D::_refresh_interpolation_state() { + if (!is_inside_tree()) { + return; + } + bool interpolated = is_physics_interpolated_and_enabled(); + + if (_interpolated == interpolated) { + return; + } + + bool curr_redraw = redraw; + + // Remove all connections. + // This isn't super efficient, but should only happen rarely. + _set_redraw(false); + + _interpolated = interpolated; + +#ifdef GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY + // Refresh local coords state, blank inv_emission_transform. + set_use_local_coordinates(local_coords); +#endif + + set_process_internal(!_interpolated); + set_physics_process_internal(_interpolated); + + // Re-establish all connections. + _set_redraw(curr_redraw); +} + void CPUParticles2D::_set_redraw(bool p_redraw) { if (redraw == p_redraw) { return; } redraw = p_redraw; update_mutex.lock(); + if (!_interpolated) { + if (redraw) { + VS::get_singleton()->connect("frame_pre_draw", this, "_update_render_thread"); + } else { + if (VS::get_singleton()->is_connected("frame_pre_draw", this, "_update_render_thread")) { + VS::get_singleton()->disconnect("frame_pre_draw", this, "_update_render_thread"); + } + } + } + if (redraw) { - VS::get_singleton()->connect("frame_pre_draw", this, "_update_render_thread"); VS::get_singleton()->canvas_item_set_update_when_visible(get_canvas_item(), true); VS::get_singleton()->multimesh_set_visible_instances(multimesh, -1); } else { - if (VS::get_singleton()->is_connected("frame_pre_draw", this, "_update_render_thread")) { - VS::get_singleton()->disconnect("frame_pre_draw", this, "_update_render_thread"); - } VS::get_singleton()->canvas_item_set_update_when_visible(get_canvas_item(), false); VS::get_singleton()->multimesh_set_visible_instances(multimesh, 0); @@ -1035,7 +1132,11 @@ void CPUParticles2D::_set_redraw(bool p_redraw) { void CPUParticles2D::_update_render_thread() { if (OS::get_singleton()->is_update_pending(true)) { update_mutex.lock(); - VS::get_singleton()->multimesh_set_as_bulk_array(multimesh, particle_data); + if (_interpolated) { + VS::get_singleton()->multimesh_set_as_bulk_array_interpolated(multimesh, particle_data, particle_data_prev); + } else { + VS::get_singleton()->multimesh_set_as_bulk_array(multimesh, particle_data); + } update_mutex.unlock(); } } @@ -1043,6 +1144,17 @@ void CPUParticles2D::_update_render_thread() { void CPUParticles2D::_notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE) { set_process_internal(emitting); + + // For interpolated version to update the particles right away, + // we need a sequence of events. + // First ensure we are in _interpolated mode if the Node is set to interpolated. + _refresh_interpolation_state(); + + // Now, if we are interpolating, we want to force a single tick update. + // If we don't do this, it may be an entire tick before the first update happens. + if (_interpolated) { + _update_internal(true); + } } if (p_what == NOTIFICATION_EXIT_TREE) { @@ -1051,8 +1163,8 @@ void CPUParticles2D::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { // first update before rendering to avoid one frame delay after emitting starts - if (emitting && (time == 0)) { - _update_internal(); + if (emitting && (time == 0) && !_interpolated) { + _update_internal(false); } if (!redraw) { @@ -1073,39 +1185,10 @@ void CPUParticles2D::_notification(int p_what) { } if (p_what == NOTIFICATION_INTERNAL_PROCESS) { - _update_internal(); + _update_internal(false); } - - if (p_what == NOTIFICATION_TRANSFORM_CHANGED) { - inv_emission_transform = get_global_transform().affine_inverse(); - - if (!local_coords) { - int pc = particles.size(); - - PoolVector::Write w = particle_data.write(); - PoolVector::Read r = particles.read(); - float *ptr = w.ptr(); - - for (int i = 0; i < pc; i++) { - Transform2D t = inv_emission_transform * r[i].transform; - - if (r[i].active) { - ptr[0] = t.elements[0][0]; - ptr[1] = t.elements[1][0]; - ptr[2] = 0; - ptr[3] = t.elements[2][0]; - ptr[4] = t.elements[0][1]; - ptr[5] = t.elements[1][1]; - ptr[6] = 0; - ptr[7] = t.elements[2][1]; - - } else { - memset(ptr, 0, sizeof(float) * 8); - } - - ptr += 13; - } - } + if (p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) { + _update_internal(true); } } diff --git a/scene/2d/cpu_particles_2d.h b/scene/2d/cpu_particles_2d.h index 34ee1b40912..5911aa05f5d 100644 --- a/scene/2d/cpu_particles_2d.h +++ b/scene/2d/cpu_particles_2d.h @@ -35,6 +35,8 @@ #include "scene/2d/node_2d.h" #include "scene/resources/texture.h" +#define GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY + class CPUParticles2D : public Node2D { private: GDCLASS(CPUParticles2D, Node2D); @@ -81,12 +83,25 @@ public: private: bool emitting; - // warning - beware of adding non-trivial types - // to this structure as it is zeroed to initialize in set_amount() - struct Particle { + struct ParticleBase { + void blank() { + for (int n = 0; n < 4; n++) { + custom[n] = 0.0f; + } + } Transform2D transform; Color color; float custom[4]; + }; + + // Warning - beware of adding non-trivial types + // to this structure as it is zeroed to initialize in set_amount(). + struct Particle : public ParticleBase { + void copy_to(ParticleBase &r_o) { + r_o.transform = transform; + r_o.color = color; + memcpy(r_o.custom, custom, sizeof(custom)); + } float rotation; Vector2 velocity; bool active; @@ -112,7 +127,9 @@ private: RID multimesh; PoolVector particles; + LocalVector particles_prev; PoolVector particle_data; + PoolVector particle_data_prev; PoolVector particle_order; struct SortLifetime { @@ -177,11 +194,13 @@ private: Vector2 gravity; - void _update_internal(); + void _update_internal(bool p_on_physics_tick); void _particles_process(float p_delta); + void _particle_process(Particle &r_p, const Transform2D &p_emission_xform, float p_local_delta, float &r_tv); void _update_particle_data_buffer(); Mutex update_mutex; + bool _interpolated = false; void _update_render_thread(); @@ -190,6 +209,46 @@ private: void _set_redraw(bool p_redraw); void _texture_changed(); + void _refresh_interpolation_state(); + + template + void _fill_particle_data(const ParticleBase &p_source, float *r_dest, bool p_active) const { + if (p_active) { +#ifdef GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY + Transform2D t = p_source.transform; + + if (TRANSFORM_PARTICLE) { + t = inv_emission_transform * t; + } +#else + const Transform2D &t = p_source.transform; +#endif + + r_dest[0] = t.elements[0][0]; + r_dest[1] = t.elements[1][0]; + r_dest[2] = 0; + r_dest[3] = t.elements[2][0]; + r_dest[4] = t.elements[0][1]; + r_dest[5] = t.elements[1][1]; + r_dest[6] = 0; + r_dest[7] = t.elements[2][1]; + + Color c = p_source.color; + uint8_t *data8 = (uint8_t *)&r_dest[8]; + data8[0] = CLAMP(c.r * 255.0, 0, 255); + data8[1] = CLAMP(c.g * 255.0, 0, 255); + data8[2] = CLAMP(c.b * 255.0, 0, 255); + data8[3] = CLAMP(c.a * 255.0, 0, 255); + + r_dest[9] = p_source.custom[0]; + r_dest[10] = p_source.custom[1]; + r_dest[11] = p_source.custom[2]; + r_dest[12] = p_source.custom[3]; + + } else { + memset(r_dest, 0, sizeof(float) * 13); + } + } protected: static void _bind_methods(); diff --git a/scene/2d/node_2d.cpp b/scene/2d/node_2d.cpp index 567a22b4492..c5040033cfb 100644 --- a/scene/2d/node_2d.cpp +++ b/scene/2d/node_2d.cpp @@ -304,7 +304,9 @@ void Node2D::set_transform(const Transform2D &p_transform) { _mat = p_transform; _xform_dirty = true; - VisualServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), _mat); + if (!_is_using_identity_transform()) { + VisualServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), _mat); + } if (!is_inside_tree()) { return; diff --git a/servers/visual/rasterizer.h b/servers/visual/rasterizer.h index b912445fc79..73bacc97034 100644 --- a/servers/visual/rasterizer.h +++ b/servers/visual/rasterizer.h @@ -995,6 +995,7 @@ public: bool light_masked : 1; bool on_interpolate_transform_list : 1; bool interpolated : 1; + bool ignore_parent_xform : 1; mutable bool custom_rect : 1; mutable bool rect_dirty : 1; mutable bool bound_dirty : 1; @@ -1236,6 +1237,7 @@ public: update_when_visible = false; on_interpolate_transform_list = false; interpolated = true; + ignore_parent_xform = false; local_bound_last_update_tick = 0; } virtual ~Item() { diff --git a/servers/visual/visual_server_canvas.cpp b/servers/visual/visual_server_canvas.cpp index 759615a6877..9cabd448709 100644 --- a/servers/visual/visual_server_canvas.cpp +++ b/servers/visual/visual_server_canvas.cpp @@ -40,6 +40,8 @@ void VisualServerCanvas::_render_canvas_item_tree(Item *p_canvas_item, const Tra memset(z_list, 0, z_range * sizeof(RasterizerCanvas::Item *)); memset(z_last_list, 0, z_range * sizeof(RasterizerCanvas::Item *)); + _current_camera_transform = p_transform; + if (_canvas_cull_mode == CANVAS_CULL_MODE_NODE) { _prepare_tree_bounds(p_canvas_item); _render_canvas_item_cull_by_node(p_canvas_item, p_transform, p_clip_rect, Color(1, 1, 1, 1), 0, z_list, z_last_list, nullptr, nullptr, false); @@ -337,7 +339,12 @@ void VisualServerCanvas::_render_canvas_item_cull_by_item(Item *p_canvas_item, c real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); TransformInterpolator::interpolate_transform_2d(ci->xform_prev, ci->xform_curr, final_xform, f); } - final_xform = p_transform * final_xform; + + if (!p_canvas_item->ignore_parent_xform) { + final_xform = p_transform * final_xform; + } else { + final_xform = _current_camera_transform * final_xform; + } Rect2 global_rect = final_xform.xform(rect); global_rect.position += p_clip_rect.position; @@ -473,7 +480,12 @@ void VisualServerCanvas::_render_canvas_item_cull_by_node(Item *p_canvas_item, c real_t f = Engine::get_singleton()->get_physics_interpolation_fraction(); TransformInterpolator::interpolate_transform_2d(ci->xform_prev, ci->xform_curr, final_xform, f); } - final_xform = p_transform * final_xform; + + if (!p_canvas_item->ignore_parent_xform) { + final_xform = p_transform * final_xform; + } else { + final_xform = _current_camera_transform * final_xform; + } Rect2 global_rect = final_xform.xform(rect); ci->global_rect_cache = global_rect; @@ -667,6 +679,8 @@ void VisualServerCanvas::render_canvas(Canvas *p_canvas, const Transform2D &p_tr memset(z_list, 0, z_range * sizeof(RasterizerCanvas::Item *)); memset(z_last_list, 0, z_range * sizeof(RasterizerCanvas::Item *)); + _current_camera_transform = p_transform; + #ifdef VISUAL_SERVER_CANVAS_TIME_NODE_CULLING bool measure = (Engine::get_singleton()->get_frames_drawn() % 100) == 0; measure &= !Engine::get_singleton()->is_editor_hint(); @@ -941,6 +955,14 @@ void VisualServerCanvas::canvas_item_set_draw_behind_parent(RID p_item, bool p_e _check_bound_integrity(canvas_item); } +void VisualServerCanvas::canvas_item_set_ignore_parent_transform(RID p_item, bool p_enable) { + Item *canvas_item = canvas_item_owner.getornull(p_item); + ERR_FAIL_COND(!canvas_item); + + canvas_item->ignore_parent_xform = p_enable; + _make_bound_dirty(canvas_item); +} + void VisualServerCanvas::canvas_item_set_update_when_visible(RID p_item, bool p_update) { Item *canvas_item = canvas_item_owner.getornull(p_item); ERR_FAIL_COND(!canvas_item); diff --git a/servers/visual/visual_server_canvas.h b/servers/visual/visual_server_canvas.h index 038ee1be5d9..5473a665d37 100644 --- a/servers/visual/visual_server_canvas.h +++ b/servers/visual/visual_server_canvas.h @@ -170,6 +170,7 @@ private: RasterizerCanvas::Item **z_list; RasterizerCanvas::Item **z_last_list; + Transform2D _current_camera_transform; // 3.5 and earlier had no hierarchical culling. void _render_canvas_item_cull_by_item(Item *p_canvas_item, const Transform2D &p_transform, const Rect2 &p_clip_rect, const Color &p_modulate, int p_z, RasterizerCanvas::Item **z_list, RasterizerCanvas::Item **z_last_list, Item *p_canvas_clip, Item *p_material_owner); @@ -226,6 +227,7 @@ public: void canvas_item_set_self_modulate(RID p_item, const Color &p_color); void canvas_item_set_draw_behind_parent(RID p_item, bool p_enable); + void canvas_item_set_ignore_parent_transform(RID p_item, bool p_enable); void canvas_item_set_update_when_visible(RID p_item, bool p_update); diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index 6cf0c35acfe..26362642f51 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -690,6 +690,7 @@ public: BIND2(canvas_item_set_self_modulate, RID, const Color &) BIND2(canvas_item_set_draw_behind_parent, RID, bool) + BIND2(canvas_item_set_ignore_parent_transform, RID, bool) BIND6(canvas_item_add_line, RID, const Point2 &, const Point2 &, const Color &, float, bool) BIND5(canvas_item_add_polyline, RID, const Vector &, const Vector &, float, bool) diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 101c57310c7..36219560453 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -594,6 +594,7 @@ public: FUNC2(canvas_item_set_self_modulate, RID, const Color &) FUNC2(canvas_item_set_draw_behind_parent, RID, bool) + FUNC2(canvas_item_set_ignore_parent_transform, RID, bool) FUNC6(canvas_item_add_line, RID, const Point2 &, const Point2 &, const Color &, float, bool) FUNC5(canvas_item_add_polyline, RID, const Vector &, const Vector &, float, bool) diff --git a/servers/visual_server.h b/servers/visual_server.h index 79923e8ec19..a67f5fa57fc 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -1024,6 +1024,7 @@ public: virtual void canvas_item_set_self_modulate(RID p_item, const Color &p_color) = 0; virtual void canvas_item_set_draw_behind_parent(RID p_item, bool p_enable) = 0; + virtual void canvas_item_set_ignore_parent_transform(RID p_item, bool p_enable) = 0; enum NinePatchAxisMode { NINE_PATCH_STRETCH,