Switch to Dictionary return type to avoid allocating objects

This commit is contained in:
Hugo Locurcio 2023-08-10 09:20:29 +02:00
parent 52536bd7a2
commit 052d43c4c9
No known key found for this signature in database
GPG Key ID: 39E8F8BE30B0A49C
5 changed files with 73 additions and 182 deletions

View File

@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="CPUParticle3D" inherits="RefCounted" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Contains information about an individual particle from a [CPUParticles3D] system.
</brief_description>
<description>
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() &gt;= 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]
</description>
<tutorials>
</tutorials>
<methods>
<method name="get_color" qualifiers="const">
<return type="Color" />
<description>
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.
</description>
</method>
<method name="get_phase" qualifiers="const">
<return type="float" />
<description>
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].
</description>
</method>
<method name="get_seed" qualifiers="const">
<return type="int" />
<description>
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() &lt;= 2 &lt;&lt; 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.
</description>
</method>
<method name="get_transform" qualifiers="const">
<return type="Transform3D" />
<description>
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].
</description>
</method>
<method name="get_velocity" qualifiers="const">
<return type="Vector3" />
<description>
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].
</description>
</method>
<method name="is_active" qualifiers="const">
<return type="bool" />
<description>
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.
</description>
</method>
</methods>
</class>

View File

@ -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() &gt;= 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]
</description>
<tutorials>
</tutorials>
@ -317,16 +355,32 @@
</description>
</signal>
<signal name="particles_expired">
<argument index="0" name="particles" type="CPUParticle3D[]" />
<param index="0" name="particles" type="Dictionary[]" />
<description>
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 &lt;= 2 &lt;&lt; 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.
</description>
</signal>
<signal name="particles_updated">
<argument index="0" name="particles" type="CPUParticle3D[]" />
<param index="0" name="particles" type="Dictionary[]" />
<description>
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.
</description>
</signal>

View File

@ -679,7 +679,14 @@ void CPUParticles3D::_particles_process(double p_delta) {
for (int i = 0; i < pcount; i++) {
Particle &p = parray[i];
Ref<CPUParticle3D> 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);
}

View File

@ -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

View File

@ -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);