~3 min read

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.
ParameterGPUParticles3DCPUParticles3D
Where it is computedGPU (compute)CPU
Max particlesMillions~10,000
Compatibility rendererWorks with caveats (requires compute)Works everywhere
Web targetLimited (depends on WebGL)Works
Per-particle GDScript accessNoYes

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 ShapePOINT, 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 = true on 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:

  1. ParticleProcessMaterial.collision_mode = COLLISION_RIGID (bounce) or COLLISION_HIDE_ON_CONTACT.
  2. Add a GPUParticlesCollisionSphere3D, GPUParticlesCollisionBox3D, GPUParticlesCollisionHeightField3D, or GPUParticlesCollisionSDF3D to the scene — these are special colliders for particles only, separate from physics colliders.
No collision with regular physics

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.