Particles — GPUParticles3D and CPUParticles3D
ParticleProcessMaterial, emitters, sub-emitters, collision.
Godot has two particle nodes with a similar API:
- GPUParticles3D — on the GPU, up to millions of particles.
- CPUParticles3D — on the CPU, a fallback for weak hardware and the web.
| Parameter | GPUParticles3D | CPUParticles3D |
|---|---|---|
| Where it is computed | GPU (compute) | CPU |
| Max particles | Millions | ~10,000 |
| Compatibility renderer | Works with caveats (requires compute) | Works everywhere |
| Web target | Limited (depends on WebGL) | Works |
| Per-particle GDScript access | No | Yes |
Rule of thumb: GPU where you can, CPU where you must.
ParticleProcessMaterial
The main difference from Unity’s Particle System: particle behavior is defined not in the node’s
inspector, but in a separate ParticleProcessMaterial resource. This is convenient — you can save
it, reuse it, and override it per instance.
@onready var particles: GPUParticles3D = $GPUParticles3D
func _ready() -> void:
var mat: ParticleProcessMaterial = particles.process_material
mat.initial_velocity_min = 5.0
mat.initial_velocity_max = 10.0
mat.gravity = Vector3(0, -3, 0)
The main sections of ParticleProcessMaterial:
- Direction + spread — the emission direction.
- Initial Velocity (min/max) — the starting speed.
- Gravity — applied to each particle.
- Linear / Angular / Radial Accel — constant accelerations.
- Damping — velocity damping.
- Color + Color Curve — color and a curve over lifetime.
- Scale + Scale Curve — size.
- Hue Variation, Anim Speed / Offset — for texture sheets.
- Emission Shape —
POINT,SPHERE,BOX,POINTS,DIRECTED_POINTS,RING.
GPUParticles3D — the node
The main properties on the node itself:
amount— the maximum number of simultaneous particles.lifetime— lifetime in seconds.one_shot— a single burst or continuous emission.explosiveness— 0..1, how much of a “burst” they come out as (0 = uniform, 1 = all at once).emitting— emit now.fixed_fps— fixed update rate (0 = follows the FPS).local_coords— whether particle positions are local or in world space.
# Trigger a one-shot explosion
func boom(at: Vector3) -> void:
var explosion = explosion_scene.instantiate()
explosion.global_position = at
get_tree().current_scene.add_child(explosion)
explosion.emitting = true # enable emission
# Auto-removal
func _on_finished() -> void:
queue_free()
The finished signal fires after lifetime (for one_shot = true).
Trails
Each particle can leave a trail — a thin ribbon that follows behind it:
trail_enabled = trueon the GPUParticles3D node.trail_lifetime— the trail length in seconds.- Mesh trail (if you want a custom mesh) — via
draw_pass_1.mesh.
Useful for tracer bullets and magic VFX.
Sub-emitters
GPUParticles3D can emit other particles on certain events:
Explosion (GPUParticles3D — main)
└── SubEmitter (GPUParticles3D — sub)
Types of sub-emitter activation:
- CONSTANT — continuously from every parent particle.
- AT_END — at the moment the parent dies.
- AT_COLLISION — on contact with a collider (requires collision_enabled).
Example: a rocket (the parent particle) — on contact with the ground it emits 50 sparks (sub-emitter).
Collision with the world
To make particles react to collisions:
ParticleProcessMaterial.collision_mode = COLLISION_RIGID(bounce) orCOLLISION_HIDE_ON_CONTACT.- Add a GPUParticlesCollisionSphere3D, GPUParticlesCollisionBox3D, GPUParticlesCollisionHeightField3D, or GPUParticlesCollisionSDF3D to the scene — these are special colliders for particles only, separate from physics colliders.
GPU particles do NOT collide with regular StaticBody3D / colliders. They use a separate
GPUParticlesCollision* node system. This is done for the performance of GPU simulation.
Visibility AABB
GPUParticles3D have a visibility_aabb — a bounding box within which they are considered visible. If the AABB is outside the camera, the particles are not simulated.
By default the AABB is small (around the emitter), and particles that fly far away can “disappear”. The fix: manually set a large AABB or press Generate AABB in the editor (Godot picks it based on the simulation).
particles.visibility_aabb = AABB(Vector3(-20, -20, -20), Vector3(40, 40, 40))
ProcessMaterial vs ShaderMaterial for particles
ParticleProcessMaterial is the standard one and covers 90% of cases. If you need non-standard
behavior (a specific vortex, an attractor following a complex formula), write your own ShaderMaterial
with shader_type particles:
shader_type particles;
render_mode keep_data;
uniform vec3 swirl_center;
uniform float swirl_strength;
void process() {
vec3 to_center = swirl_center - TRANSFORM[3].xyz;
float dist = length(to_center);
vec3 tangent = normalize(cross(to_center, vec3(0, 1, 0)));
VELOCITY += tangent * swirl_strength * (1.0 / max(dist, 0.5));
TRANSFORM[3].xyz += VELOCITY * DELTA;
}
This is a processing shader. The particle is rendered by a separate draw pass material (via
draw_pass_1 on GPUParticles3D).
CPUParticles3D — when it’s useful
- A web build with the Compatibility renderer — GPU simulation does not work everywhere there (especially on WebGL without compute).
- Per-particle logic in GDScript — if you want to modify individual particles in code.
- Old hardware where compute shaders are unavailable.
The API is almost identical, but the settings live directly on the node (without a separate ProcessMaterial).
Profiling
GPU particles are one of the heaviest features. In Debugger → Profiler → Visual, look at the
prepare/process times. If you have 10,000+ particles on a weak phone, cut amount and lifetime.
In the next chapter — multiplayer.