2
0
Эх сурвалжийг харах

Merge pull request #80176 from lawnjelly/fti_2d_particles

[3.x] Physics Interpolation - add support for CPUParticles2D
Rémi Verschelde 2 жил өмнө
parent
commit
b8ff6190a8

+ 18 - 0
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();

+ 1 - 0
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();

+ 255 - 172
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<Particle>::Write w = particles.write();
 
@@ -59,13 +60,19 @@ void CPUParticles2D::set_amount(int p_amount) {
 		memset(static_cast<void *>(&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<Particle>::Read r = particles.read();
 		float *ptr = w.ptr();
 
+		PoolVector<float>::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 (!local_coords) {
-				t = inv_emission_transform * t;
+		if (_interpolated) {
+			for (int i = 0; i < pc; i++) {
+				int idx = order ? order[i] : i;
+				_fill_particle_data<false>(r[idx], ptr, r[idx].active);
+				ptr += 13;
+				_fill_particle_data<false>(particles_prev[idx], ptr_prev, r[idx].active);
+				ptr_prev += 13;
 			}
-
-			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];
-
+		} else {
+#ifdef GODOT_CPU_PARTICLES_2D_LEGACY_COMPATIBILITY
+			if (!local_coords) {
+				inv_emission_transform = get_global_transform().affine_inverse();
+				for (int i = 0; i < pc; i++) {
+					int idx = order ? order[i] : i;
+					_fill_particle_data<true>(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<false>(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<false>(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<float>::Write w = particle_data.write();
-			PoolVector<Particle>::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);
 	}
 }
 

+ 63 - 4
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<Particle> particles;
+	LocalVector<ParticleBase> particles_prev;
 	PoolVector<float> particle_data;
+	PoolVector<float> particle_data_prev;
 	PoolVector<int> 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 <bool TRANSFORM_PARTICLE>
+	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();

+ 3 - 1
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;

+ 2 - 0
servers/visual/rasterizer.h

@@ -1006,6 +1006,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;
@@ -1261,6 +1262,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() {

+ 24 - 2
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);

+ 2 - 0
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);
 

+ 1 - 0
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<Point2> &, const Vector<Color> &, float, bool)

+ 1 - 0
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<Point2> &, const Vector<Color> &, float, bool)

+ 1 - 0
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,