diff --git a/doc/classes/CPUParticle3D.xml b/doc/classes/CPUParticle3D.xml
deleted file mode 100644
index 988b228adcc..00000000000
--- a/doc/classes/CPUParticle3D.xml
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
- Contains information about an individual particle from a [CPUParticles3D] system.
-
-
- Contains information about an individual particle from a [CPUParticles3D] system.
- [CPUParticle3D] is emitted as an [Array] of objects by the [signal CPUParticles3D.particles_updated] signal.
- This can be used to make nodes (such as [Light3D]s, [AudioStreamPlayer3D]s or other [CPUParticles3D]s) follow particles individually for advanced effects.
- [b]Note:[/b] To avoid performance issues, it's recommended to only use this feature with low numbers of particles (typically a few dozen at most).
- [b]Example of placing [OmniLight3D]s to follow particles automatically and change color over time:[/b]
- [codeblock]
- extends CPUParticles3D
-
- # Used to keep track of light nodes added as children more easily.
- var lights = []
-
- func _ready():
- for i in amount:
- var light = OmniLight3D.new()
- lights.push_back(light)
- add_child(light)
-
- particles_updated.connect(_on_cpu_particles_3d_particles_updated)
-
-
- func _on_cpu_particles_3d_particles_updated(particles):
- # Only update light positions if all light nodes have been added first.
- if lights.size() >= particles.size():
- for particle_idx in particles.size():
- lights[particle_idx].visible = particles[particle_idx].is_active()
-
- # Change the light's color over the particle's lifetime.
- lights[particle_idx].light_color.r = particles[particle_idx].get_phase()
- lights[particle_idx].light_color.g = 1.0 - particles[particle_idx].get_phase()
- lights[particle_idx].light_color.b = 1.0 - particles[particle_idx].get_phase()
- lights[particle_idx].light_energy = 2.0 - particles[particle_idx].get_phase() * 2.0
-
- # Increase the light's range over the particle's lifetime.
- lights[particle_idx].omni_range = particles[particle_idx].get_phase() * 5.0
-
- # Move lights to follow particles.
- if local_coords:
- lights[particle_idx].position = particles[particle_idx].get_transform().origin
- else:
- lights[particle_idx].global_transform.origin = particles[particle_idx].get_transform().origin
- [/codeblock]
-
-
-
-
-
-
-
- Returns the particle's current color. This is the color defined by [member CPUParticles3D.color] or [member CPUParticles3D.color_initial_ramp], which is then multiplied by [member CPUParticles3D.color_ramp] over the particle's lifetime.
- [b]Note:[/b] [method get_color] does not take the material albedo color or mesh's original vertex color into account.
-
-
-
-
-
- Returns the particle's lifetime percentage. This is close to [code]0.0[/code] when the particle is freshly spawned, and close to [code]1.0[/code] when the particle is about to expire.
- [b]Note:[/b] If the particle is inactive ([method is_active] returns [code]false[/code]), [method get_phase] returns [code]0.0[/code].
-
-
-
-
-
- Returns the particle's random seed (a 32-bit [i]unsigned[/i] integer) within the particle system. This can be used to uniquely identify a given particle within the particle system during its lifetime.
- Since the seed is uniformly distributed, [method get_seed] can be used to selectively apply effects to certain particles only. This can be useful to improve performance by applying expensive effects to lower amounts of particles:
- [codeblock]
- # `particles` is an array of CPUParticle3D results from CPUParticles3D's `particles_updated` signal.
- for particle in particles:
- # The maximum value of a 32-bit unsigned integer is (2 ^ 32) - 1, which is roughly 4.2 billion.
- if particle.get_seed() <= 2 << 31: # Lower than roughly 2.1 billion.
- # Apply an effect to roughly 50% of particles here (the exact amount is random and varies constantly).
- pass
- [/codeblock]
- [b]Note:[/b] When a particle respawns after expiring, it will generate and use a different seed. This means [method get_seed] cannot be used to identify a given particle after it respawns.
-
-
-
-
-
- Returns the particle's transform. If you're only interested in the particle's position, use [code]get_transform().origin[/code].
- [b]Note:[/b] If [member CPUParticles3D.local_coords] is [code]true[/code], you'll want to set nodes' [i]local[/i] transform when moving them to match particle positions. If [member CPUParticles3D.local_coords] is [code]false[/code], you'll want to set nodes' [i]global[/i] transform when moving them to match particle positions.
- [b]Note:[/b] If the particle is inactive ([method is_active] returns [code]false[/code]), [method get_transform] returns an identity [code]Transform3D()[/code].
-
-
-
-
-
- Returns the particle's speed in units per second on each axis.
- [b]Note:[/b] If the particle is inactive ([method is_active] returns [code]false[/code]), [method get_velocity] returns [code]Vector3(0, 0, 0)[/code].
-
-
-
-
-
- Returns [code]true[/code] if the particle is currently alive, [code]false[/code] otherwise.
- Typically, all particles are active except in two scenarios:
- - The particle system has just started and its [member CPUParticles3D.explosiveness] is not equal to [code]1.0[/code]. Therefore, there are less active particles than the [member CPUParticles3D.amount] configured.
- - The particle system has just had [member CPUParticles3D.emitting] set to [code]false[/code]. After all particles have expired (plus some additional time), the signal will no longer be emitted.
-
-
-
-
diff --git a/doc/classes/CPUParticles3D.xml b/doc/classes/CPUParticles3D.xml
index 2c674293b3b..41ca102f8cf 100644
--- a/doc/classes/CPUParticles3D.xml
+++ b/doc/classes/CPUParticles3D.xml
@@ -7,6 +7,44 @@
CPU-based 3D particle node used to create a variety of particle systems and effects.
See also [GPUParticles3D], which provides the same functionality with hardware acceleration, but may not run on older devices.
Unlike [GPUParticles3D], the CPU is aware of particles' individual transforms. This means particle transforms can be used for any purpose in scripting, including gameplay-affecting elements. See [signal particles_updated] for more information.
+ CPUParticles3D can emit an [Array] of dictionaries with the [signal CPUParticles3D.particles_updated] signal. This can be used to make nodes (such as [Light3D]s, [AudioStreamPlayer3D]s or other [CPUParticles3D]s) follow particles individually for advanced effects. To avoid performance issues, it's recommended to only use this feature with low numbers of particles (typically a few dozen at most).
+ [b]Example of placing [OmniLight3D]s to follow particles automatically and change color over time:[/b]
+ [codeblock]
+ extends CPUParticles3D
+
+ # Used to keep track of light nodes added as children more easily.
+ var lights = []
+
+ func _ready():
+ for i in amount:
+ var light = OmniLight3D.new()
+ lights.push_back(light)
+ add_child(light)
+
+ particles_updated.connect(_on_cpu_particles_3d_particles_updated)
+
+
+ func _on_cpu_particles_3d_particles_updated(particles):
+ # Only update light positions if all light nodes have been added first.
+ if lights.size() >= particles.size():
+ for particle_idx in particles.size():
+ lights[particle_idx].visible = particles[particle_idx].active
+
+ # Change the light's color over the particle's lifetime.
+ lights[particle_idx].light_color.r = particles[particle_idx].phase
+ lights[particle_idx].light_color.g = 1.0 - particles[particle_idx].phase
+ lights[particle_idx].light_color.b = 1.0 - particles[particle_idx].phase
+ lights[particle_idx].light_energy = 2.0 - particles[particle_idx].phase * 2.0
+
+ # Increase the light's range over the particle's lifetime.
+ lights[particle_idx].omni_range = particles[particle_idx].phase * 5.0
+
+ # Move lights to follow particles.
+ if local_coords:
+ lights[particle_idx].position = particles[particle_idx].transform.origin
+ else:
+ lights[particle_idx].global_transform.origin = particles[particle_idx].transform.origin
+ [/codeblock]
@@ -317,16 +355,32 @@
-
+
Emitted when one or more particles is about to expire on the next particle update. This can be used to make nodes (such as [Light3D]s, [AudioStreamPlayer3D]s or other [CPUParticles3D]s) spawn at individual particle expiration positions.
+ Each particle has an associated dictionary in the array parameter with the following keys:
+ - [code]color[/code]: The particle's current color. This is the color defined by [member CPUParticles3D.color] or [member CPUParticles3D.color_initial_ramp], which is then multiplied by [member CPUParticles3D.color_ramp] over the particle's lifetime. [code]color[/code] does [i]not[/i] take the material albedo color or mesh's original vertex color into account. If the particle is inactive ([code]active[/code] is [code]false[/code]), [code]color[/code] is [code]Color(0, 0, 0, 1)[/code].
+ - [code]phase[/code]: The particle's lifetime ratio. This is close to [code]0.0[/code] when the particle is freshly spawned, and close to [code]1.0[/code] when the particle is about to expire. If the particle is inactive ([code]active[/code] is [code]false[/code]), [code]phase[/code] is [code]0.0[/code].
+ - [code]seed[/code]: The particle's random seed (a 32-bit [i]unsigned[/i] integer) within the particle system. This can be used to uniquely identify a given particle within the particle system during its lifetime. Since the seed is uniformly distributed, [code]seed[/code] can be used to selectively apply effects to certain particles only. This can be useful to improve performance by applying expensive effects to lower amounts of particles:
+ [codeblock]
+ # `particles` is an array of CPUParticle3D results from CPUParticles3D's `particles_updated` signal.
+ for particle in particles:
+ # The maximum value of a 32-bit unsigned integer is (2 ^ 32) - 1, which is roughly 4.2 billion.
+ if particle.seed <= 2 << 31: # Lower than roughly 2.1 billion.
+ # Apply an effect to roughly 50% of particles here (the exact amount is random and varies constantly).
+ pass
+ [/codeblock]
+ When a particle respawns after expiring, it will generate and use a different seed. This means [code]seed[/code] cannot be used to identify a given particle after it respawns. If the particle is inactive ([code]active[/code] is [code]false[/code]), [code]seed[/code] is [code]0[/code].
+ - [code]transform[/code]: The particle's transform. If you're only interested in the particle's position, use [code]transform.origin[/code]. If [member CPUParticles3D.local_coords] is [code]true[/code], you'll want to set nodes' [i]local[/i] transform when moving them to match particle positions. If [member CPUParticles3D.local_coords] is [code]false[/code], you'll want to set nodes' [i]global[/i] transform when moving them to match particle positions. If the particle is inactive ([code]active[/code] is [code]false[/code]), [code]transform[/code] is an identity [code]Transform3D()[/code].
+ - [code]velocity[/code]: The particle's linear velocity (movement speed) in units per second on each axis. This doesn't take the node's movement into account if [member local_coords] is [code]true[/code]. If the particle is inactive ([code]active[/code] is [code]false[/code]), [code]velocity[/code] is [code]Vector3(0, 0, 0)[/code].
+ - [code]active[/code]: [code]true[/code] if the particle is currently alive, [code]false[/code] otherwise. Typically, all particles are active except in two scenarios: (1) The particle system has just started and its [member CPUParticles3D.explosiveness] is not equal to [code]1.0[/code]. Therefore, there are less active particles than the [member CPUParticles3D.amount] configured. (2) The particle system has just had [member CPUParticles3D.emitting] set to [code]false[/code]. After all particles have expired (plus some additional time), the signal will no longer be emitted.
[b]Note:[/b] Only emitted when the particle system is currently [member emitting] or when at least 1 particle is still active.
-
+
- Emitted when one or more particles are updated (every [code]1.0 /[/code] [member fixed_fps] seconds, or every rendered frame if [member fixed_fps] is [code]0[/code]). This can be used to make nodes (such as [Light3D]s, [AudioStreamPlayer3D]s or other [CPUParticles3D]s) follow particles individually for advanced effects. See [CPUParticle3D] to access information on each individual particle.
+ Emitted when one or more particles are updated (every [code]1.0 /[/code] [member fixed_fps] seconds, or every rendered frame if [member fixed_fps] is [code]0[/code]). This can be used to make nodes (such as [Light3D]s, [AudioStreamPlayer3D]s or other [CPUParticles3D]s) follow particles individually for advanced effects. See [signal particles_expired]'s description for information on dictionary keys contained within the array parameter.
[b]Note:[/b] Only emitted when the particle system is currently [member emitting] or when at least 1 particle is still active.
diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp
index 41453fb6f9d..6037fd5d900 100644
--- a/scene/3d/cpu_particles_3d.cpp
+++ b/scene/3d/cpu_particles_3d.cpp
@@ -679,7 +679,14 @@ void CPUParticles3D::_particles_process(double p_delta) {
for (int i = 0; i < pcount; i++) {
Particle &p = parray[i];
- Ref cpu_particle = memnew(CPUParticle3D);
+ Dictionary cpu_particle;
+ // These will be overridden if the particle is active.
+ cpu_particle["active"] = false;
+ cpu_particle["transform"] = Transform3D();
+ cpu_particle["color"] = Color();
+ cpu_particle["velocity"] = Vector3();
+ cpu_particle["phase"] = 0.0f;
+ cpu_particle["seed"] = 0;
if (!emitting && !p.active) {
cpu_particles.set(i, cpu_particle);
@@ -1151,12 +1158,12 @@ void CPUParticles3D::_particles_process(double p_delta) {
// If we got down here, we got past all the `continue`s from inactive particles.
// Therefore, the particle is active by definition.
- cpu_particle->active = true;
- cpu_particle->transform = p.transform;
- cpu_particle->color = p.color;
- cpu_particle->velocity = p.velocity;
- cpu_particle->phase = p.time;
- cpu_particle->seed = p.seed;
+ cpu_particle["active"] = true;
+ cpu_particle["transform"] = p.transform;
+ cpu_particle["color"] = p.color;
+ cpu_particle["velocity"] = p.velocity;
+ cpu_particle["phase"] = p.time;
+ cpu_particle["seed"] = p.seed;
cpu_particles.set(i, cpu_particle);
// Empirically determined to work at Fixed FPS set to 0 (depends on rendering framerate),
@@ -1486,8 +1493,8 @@ void CPUParticles3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("restart"), &CPUParticles3D::restart);
- ADD_SIGNAL(MethodInfo("particles_updated", PropertyInfo(Variant::ARRAY, "particles", PROPERTY_HINT_ARRAY_TYPE, "CPUParticle3D")));
- ADD_SIGNAL(MethodInfo("particles_expired", PropertyInfo(Variant::ARRAY, "particles", PROPERTY_HINT_ARRAY_TYPE, "CPUParticle3D")));
+ ADD_SIGNAL(MethodInfo("particles_updated", PropertyInfo(Variant::ARRAY, "particles", PROPERTY_HINT_ARRAY_TYPE, "Dictionary")));
+ ADD_SIGNAL(MethodInfo("particles_expired", PropertyInfo(Variant::ARRAY, "particles", PROPERTY_HINT_ARRAY_TYPE, "Dictionary")));
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emitting"), "set_emitting", "is_emitting");
ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount");
@@ -1752,38 +1759,3 @@ CPUParticles3D::~CPUParticles3D() {
ERR_FAIL_NULL(RenderingServer::get_singleton());
RS::get_singleton()->free(multimesh);
}
-
-// CPUParticle3D
-
-bool CPUParticle3D::is_active() const {
- return active;
-}
-
-Transform3D CPUParticle3D::get_transform() const {
- return transform;
-}
-
-Color CPUParticle3D::get_color() const {
- return color;
-}
-
-Vector3 CPUParticle3D::get_velocity() const {
- return velocity;
-}
-
-float CPUParticle3D::get_phase() const {
- return phase;
-}
-
-uint32_t CPUParticle3D::get_seed() const {
- return seed;
-}
-
-void CPUParticle3D::_bind_methods() {
- ClassDB::bind_method(D_METHOD("is_active"), &CPUParticle3D::is_active);
- ClassDB::bind_method(D_METHOD("get_transform"), &CPUParticle3D::get_transform);
- ClassDB::bind_method(D_METHOD("get_color"), &CPUParticle3D::get_color);
- ClassDB::bind_method(D_METHOD("get_velocity"), &CPUParticle3D::get_velocity);
- ClassDB::bind_method(D_METHOD("get_phase"), &CPUParticle3D::get_phase);
- ClassDB::bind_method(D_METHOD("get_seed"), &CPUParticle3D::get_seed);
-}
diff --git a/scene/3d/cpu_particles_3d.h b/scene/3d/cpu_particles_3d.h
index 4d4247fbe54..d2e21906ef6 100644
--- a/scene/3d/cpu_particles_3d.h
+++ b/scene/3d/cpu_particles_3d.h
@@ -320,31 +320,4 @@ VARIANT_ENUM_CAST(CPUParticles3D::Parameter)
VARIANT_ENUM_CAST(CPUParticles3D::ParticleFlags)
VARIANT_ENUM_CAST(CPUParticles3D::EmissionShape)
-/**
- * Higher-level version of the Particle struct. Unlike the Particle struct,
- * CPUParticle3D is exposed to the scripting API for use by the `particles_updated` signal.
- */
-class CPUParticle3D : public RefCounted {
- GDCLASS(CPUParticle3D, RefCounted);
-
- friend class CPUParticles3D;
-
- bool active = false;
- Transform3D transform;
- Color color;
- Vector3 velocity;
- float phase = 0.0f;
- uint32_t seed = 0;
-
-protected:
- static void _bind_methods();
-
- bool is_active() const;
- Transform3D get_transform() const;
- Color get_color() const;
- Vector3 get_velocity() const;
- float get_phase() const;
- uint32_t get_seed() const;
-};
-
#endif // CPU_PARTICLES_3D_H
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 3118ae53838..fdb2b4909f1 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -543,7 +543,6 @@ void register_scene_types() {
GDREGISTER_CLASS(GPUParticlesAttractorSphere3D);
GDREGISTER_CLASS(GPUParticlesAttractorVectorField3D);
GDREGISTER_CLASS(CPUParticles3D);
- GDREGISTER_CLASS(CPUParticle3D);
GDREGISTER_CLASS(Marker3D);
GDREGISTER_CLASS(RootMotionView);