
Godot Particles
- 333 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
godot-particles is a Godot 4 agent skill that teaches developers to build GPU particle VFX with GPUParticles2D/3D, ParticleProcessMaterial, sub-emitters, and custom shaders for game feedback and environments.
About
godot-particles is an agent skill from thedivergentai/gd-agentic-skills for Godot 4 GPU particle systems used in explosions, magic effects, weather, trails, and combat feedback. It documents ParticleProcessMaterial emission shapes, color ramps, one-shot bursts, sub-emitter collisions, custom particle shaders, VFX pooling, LOD culling, and 2D physics interpolation workarounds. The skill bundles 12 reusable GDScript and shader scripts such as particle_burst_emitter.gd, particle_lod_manager.gd, and screenspace_weather_heightfield.gd, plus 12 documented anti-patterns to avoid GPU stalls and trail bugs. Use it when you need performant VFX in Godot instead of trial-and-error particle tuning.
- godot-particles
Godot Particles by the numbers
- 333 all-time installs (skills.sh)
- +32 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,252 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-particlesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 333 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
How do you build performant GPU particles in Godot 4?
Use godot-particles for development tasks
Who is it for?
Godot 4 developers implementing combat VFX, weather, trails, or swarm effects who need GPU-first patterns and bundled reference scripts.
Skip if: Non-Godot engines or 2D games that only need simple sprite animations without particle systems.
When should I use this skill?
The task mentions GPUParticles2D, GPUParticles3D, sub-emitters, particle shaders, VFX pooling, or Godot explosion and trail effects.
What you get
GPUParticles scenes, ParticleProcessMaterial configs, custom particle shaders, pooled one-shot VFX nodes, and LOD-managed environmental effects.
- Particle scene nodes
- Process materials and shaders
- Pooled VFX scripts
By the numbers
- Bundles 12 reusable GDScript and shader scripts
- Documents 12 GPU particle anti-patterns to avoid
- Covers GPUParticles2D and GPUParticles3D VFX workflows
Files
Particle Systems
GPU-accelerated rendering, material-based configuration, and sub-emitters define performant VFX.
Available Scripts
vfx_shader_manager.gd
Expert custom shader integration for advanced particle VFX.
particle_burst_emitter.gd
One-shot particle bursts with auto-cleanup - essential for VFX systems.
custom_particle_logic.gdshader
Expert procedural particle movement logic. Demonstrates persistent CUSTOM data and USERDATA injection for dynamic wind/orbit effects.
sub_emitter_impact.gdshader
High-performance collision handling. Triggers sub-emitters (splashes/debris) using emit_subparticle() and COLLISION_NORMAL.
particle_attractor_opt.gd
Optimization pattern using cull_mask to isolate particle-attractor interactions, preventing global performance bottlenecks.
massive_swarm_multimesh.gd
Bypassing GPUParticles for millions of entities (fish, insects). Uses set_buffer_interpolated() for jitter-free high-count movement.
dynamic_userdata_modulation.gd
Clean architectual pattern for passing runtime variables to particle shaders via USERDATA to preserve GPU batching.
local_vs_global_coords.gd
Expert logic for switching between localized (Auras) and global (Trails) space. Includes correct restart() handling for teleports.
smart_oneshot_recycler.gd
Robust lifecycle management using the finished signal and restart() to avoid async emission failures.
screenspace_weather_heightfield.gd
Optimizing global weather (Rain/Snow) using Camera-snapped GPUParticlesCollisionHeightField3D.
particle_lod_manager.gd
Hierarchical LOD for environmental VFX. Uses visibility_range and margins to cull distant torches or fires completely.
2d_physics_interpolation_fix.gd
Expert workaround for 2D particle stuttering. Switches to CPUParticles2D with fract_delta for smooth physics-parented movement.
NEVER Do in Particle Systems
- NEVER use `amount_ratio` to optimize performance dynamically — It does not save GPU memory or improve processing; the full
amountis still allocated. Change theamountproperty directly instead. - NEVER use CPUParticles2D for performance-critical effects on Desktop — Use GPUParticles unless targeting low-end mobile with no GPU support. However, use CPUParticles2D if you need Physics Interpolation for smooth trails on moving bodies in 2D.
- NEVER set `preprocess` to extremely high values — High values (e.g., 60s) will force the GPU to simulate thousands of frames in a single render tick, potentially causing an immediate GPU crash.
- NEVER leave `visibility_aabb` unconfigured for large systems — Incorrect AABBs cause frustum culling errors (particles popping out) and break LOD calculations. Generate AABBs using the editor toolbar.
- NEVER enable turbulence on Mobile/Web without testing — 3D noise evaluation per particle is extremely heavy. Disable via Feature Tags on lower-end platforms.
- NEVER forget to `queue_free()` one-shot particles — Use the
finishedsignal instead of an arbitrary Timer for safe lifecycle management. - NEVER use `local_coords = true` for trails — Smoke or fire left behind by a projectile MUST use global space (
local_coords = false) or the trail will follow the projectile like a stiff stick. - NEVER expect GPUParticles2D to interpolate correctly in Godot 4.3 — They stutter when parented to physics bodies. Use
CPUParticles2Dwithfract_delta = truefor high-speed 2D movement. - NEVER trigger `emitting = true` immediately after a `finished` signal — Async GPU state delays can cause the restart to fail. Use the
restart()method instead. - NEVER attempt recursion with sub-emitters — A particle system cannot be its own sub-emitter; it will silently fail.
- NEVER forget alpha in color gradients — Particles that disappear instantly at the end of their lifetime look harsh; always add a gradient point at 1.0 with 0.0 alpha for a smooth exit.
- NEVER use `EMISSION_SHAPE_POINT` for volumentric explosions — Spawning all particles at a single point looks flat. Use a Sphere or Box shape for natural 3D spread.
- NEVER forget to set `emitting = false` initially for one-shot VFX — This prevents unwanted emission at the scene origin before you've had a chance to position the node via script.
---
Basic Setup
# Add GPUParticles2D node
# Set Amount: 32
# Set Lifetime: 1.0
# Set One Shot: true (for explosions)Particle Material
# Create ParticleProcessMaterial
var material := ParticleProcessMaterial.new()
# Emission shape
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
material.emission_sphere_radius = 10.0
# Gravity
material.gravity = Vector3(0, 98, 0)
# Velocity
material.initial_velocity_min = 50.0
material.initial_velocity_max = 100.0
# Color
material.color = Color.ORANGE_RED
# Apply to godot-particles
$GPUParticles2D.process_material = materialCommon Effects
Explosion
extends GPUParticles2D
func _ready() -> void:
one_shot = true
amount = 64
lifetime = 0.8
explosiveness = 0.9
var mat := ParticleProcessMaterial.new()
mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
mat.emission_sphere_radius = 5.0
mat.initial_velocity_min = 100.0
mat.initial_velocity_max = 200.0
mat.gravity = Vector3(0, 200, 0)
mat.scale_min = 0.5
mat.scale_max = 1.5
process_material = mat
emitting = trueSmoke Trail
extends GPUParticles2D
func _ready() -> void:
amount = 16
lifetime = 2.0
var mat := ParticleProcessMaterial.new()
mat.direction = Vector3(0, -1, 0)
mat.initial_velocity_min = 20.0
mat.initial_velocity_max = 40.0
mat.scale_min = 0.5
mat.scale_max = 1.0
mat.color = Color(0.5, 0.5, 0.5, 0.5)
process_material = matSparkles/Stars
var mat := ParticleProcessMaterial.new()
mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
mat.emission_box_extents = Vector3(100, 100, 0)
mat.gravity = Vector3.ZERO
mat.angular_velocity_min = -180
mat.angular_velocity_max = 180
mat.scale_min = 0.1
mat.scale_max = 0.5
# Use star texture
$GPUParticles2D.texture = load("res://textures/star.png")
$GPUParticles2D.process_material = matSpawn Particles on Demand
# player.gd
const EXPLOSION_EFFECT := preload("res://effects/explosion.tscn")
func die() -> void:
var explosion := EXPLOSION_EFFECT.instantiate()
get_parent().add_child(explosion)
explosion.global_position = global_position
explosion.emitting = true
queue_free()3D Particles
extends GPUParticles3D
func _ready() -> void:
amount = 100
lifetime = 3.0
var mat := ParticleProcessMaterial.new()
mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
mat.emission_box_extents = Vector3(10, 0.1, 10)
mat.direction = Vector3.UP
mat.initial_velocity_min = 2.0
mat.initial_velocity_max = 5.0
mat.gravity = Vector3(0, -9.8, 0)
process_material = matColor Gradients
var mat := ParticleProcessMaterial.new()
# Create gradient
var gradient := Gradient.new()
gradient.add_point(0.0, Color.YELLOW)
gradient.add_point(0.5, Color.ORANGE)
gradient.add_point(1.0, Color(0.5, 0.0, 0.0, 0.0)) # Fade to transparent red
var gradient_texture := GradientTexture1D.new()
gradient_texture.gradient = gradient
mat.color_ramp = gradient_textureSub-Emitters
# Particles that spawn godot-particles (fireworks)
$ParentParticles.sub_emitter = $ChildParticles.get_path()
$ParentParticles.sub_emitter_mode = GPUParticles2D.SUB_EMITTER_AT_ENDBest Practices
1. Use Texture for Shapes
# Add texture to godot-particles
$GPUParticles2D.texture = load("res://textures/particle.png")2. Lifetime Management
# Auto-delete one-shot godot-particles
if one_shot:
await get_tree().create_timer(lifetime).timeout
queue_free()3. Performance
# Reduce amount for mobile
if OS.get_name() == "Android":
amount = amount / 2---
Expert Pattern: Particle-Audio-Syncer (Collision Sub-Emitters)
GPU particles do not emit CPU signals for individual collisions. To sync visual impacts, use the Sub-Emitter system to spawn secondary effects (sparks, dust) on contact.
func setup_collision_vfx(primary: GPUParticles3D, impact: GPUParticles3D) -> void:
# 1. Assign impact system as sub-emitter
primary.sub_emitter = primary.get_path_to(impact)
var mat := primary.process_material as ParticleProcessMaterial
if mat:
# 2. Enable collision and set trigger mode
mat.collision_mode = ParticleProcessMaterial.COLLISION_RIGID
mat.sub_emitter_mode = ParticleProcessMaterial.SUB_EMITTER_AT_COLLISION
mat.sub_emitter_amount_at_collision = 1 # Spawn 1 spark per impact[!IMPORTANT]
Since the CPU cannot track individual GPU collisions, sync audio by playing a randomized looping "impact" sound while the primary emitter is active, or use CPUParticles for precise RayCast-driven audio timing.---
Expert Pattern: Fluid-Simulation-Particles (Custom Shaders)
For high-performance liquid or swarm effects, bypass ParticleProcessMaterial and use a custom particles shader with state persistence.
shader_type particles;
// 'keep_data' allows the shader to remember state between frames
render_mode keep_data;
void start() {
if (RESTART) {
// Initialize position and custom fluid density
TRANSFORM[3].xyz = EMISSION_TRANSFORM[3].xyz;
CUSTOM.x = 1.0;
}
}
void process() {
// Apply gravity and attractor forces
VELOCITY += ATTRACTOR_FORCE * DELTA;
// Built-in GPU collision handling
if (COLLIDED) {
VELOCITY = reflect(VELOCITY, COLLISION_NORMAL) * 0.5;
TRANSFORM[3].xyz += COLLISION_NORMAL * COLLISION_DEPTH;
}
}---
Expert Pattern: VFX-Pool-Manager
Prevent frame-spikes from frequent instantiate() and queue_free() calls by pooling and reusing one-shot particle systems.
class_name VFXPool extends Node
@export var vfx_scene: PackedScene
var pool: Array[GPUParticles3D] = []
func _ready() -> void:
for i in 20:
var inst := vfx_scene.instantiate() as GPUParticles3D
add_child(inst)
inst.emitting = false
inst.finished.connect(func(): pool.append(inst))
pool.append(inst)
func spawn(pos: Vector3) -> void:
if pool.is_empty(): return
var vfx = pool.pop_back()
vfx.global_position = pos
# Use restart() to avoid async GPU state delays
vfx.restart()Reference
Related
- Master Skill: godot-master
# 2d_physics_interpolation_fix.gd
# Solving stuttering particles in 2D physics-based movement [56]
extends Node2D
func optimize_2d_trail_interpolation(particle_node: Node) -> void:
# EXPERT NOTE: GPUParticles2D are NOT natively interpolated in Godot 4.3 [56].
# If attached to a physics-moving body, they will stutter.
# FIX: Use CPUParticles2D and enable fract_delta.
if particle_node is CPUParticles2D:
particle_node.fract_delta = true # Smoother fractional time integration [57]
particle_node.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON
else:
push_warning("GPUParticles2D stutter on physics bodies. Consider CPUParticles2D.")
# custom_particle_logic.gdshader
# Expert procedural particle logic using custom shaders
shader_type particles;
// Use USERDATA to pass per-instance data without breaking batching [35]
uniform vec4 USERDATA1;
void start() {
// CUSTOM.x: Random phase
// CUSTOM.y: Persistent velocity scale
CUSTOM.x = rand_from_seed(RANDOM_SEED);
CUSTOM.y = 1.0 + rand_from_seed(RANDOM_SEED) * 0.5;
}
void process() {
float phase = CUSTOM.x * 6.28318;
float time = TIME * CUSTOM.y;
// Procedural orbit/spiral logic
VELOCITY.x += cos(time + phase) * DELTA * 10.0;
VELOCITY.z += sin(time + phase) * DELTA * 10.0;
// Apply dynamic wind intensity passed from GDScript via USERDATA
float wind = USERDATA1.x;
VELOCITY.x += wind * DELTA;
}
# dynamic_userdata_modulation.gd
# Passing runtime variables to particle shaders without breaking batching
extends GPUParticles3D
func set_vfx_intensity(intensity: float) -> void:
# USERDATA variables (1-4) are designed for per-instance scripting [35].
# This avoids duplicating the entire ShaderMaterial for every emitter.
if process_material is ShaderMaterial:
# Pack data into Vector4. x = intensity, y = spare, etc.
process_material.set_shader_parameter("USERDATA1", Vector4(intensity, 0, 0, 0))
# local_vs_global_coords.gd
# Handling local vs global coordinate space for trails and localized effects
extends GPUParticles3D
func configure_trail_mode(is_trail: bool) -> void:
# local_coords = false: Particles are left behind in global space (Smoke Trails) [36].
# local_coords = true: Particles move WITH the emitter (Magic Aura).
local_coords = !is_trail
func safe_teleport(new_pos: Vector3) -> void:
emitting = false
global_position = new_pos
# CRITICAL: If local_coords=false, teleporting leaves a visual gap.
# restart() clears the trail instantly for a clean teleport [38].
restart()
emitting = true
# massive_swarm_multimesh.gd
# Managing millions of particles via MultiMeshInstance3D with interpolation [32]
extends MultiMeshInstance3D
func _ready() -> void:
# Set high-speed interpolation for massive counts
multimesh.physics_interpolation_quality = MultiMesh.MULTIMESH_INTERP_QUALITY_FAST
func submit_interpolated_swarm(current_data: PackedFloat32Array, previous_data: PackedFloat32Array) -> void:
# Essential for smooth movement at high particle counts:
# Submission of both buffers allows the engine to jitter-free interpolate
# between physics ticks even if the frame rate is higher than physics.
multimesh.set_buffer_interpolated(current_data, previous_data)
# particle_attractor_opt.gd
# Isolating particle interactions using cull_mask/layers
extends GPUParticles3D
func setup_isolated_attractor(attractor: GPUParticlesAttractorSphere3D) -> void:
# Optimization: ONLY interact with particles on specific layers [24, 25]
# Layer 2 = (1 << 1). Prevents thousands of global particles from checking this attractor.
var specific_layer = (1 << 1)
attractor.cull_mask = specific_layer
# Ensure the particle system itself is on the matching layer
# GeometryInstance3D.layers is used for particle interaction masking [26]
self.layers = specific_layer
# Enable interaction in the material
if process_material is ParticleProcessMaterial:
process_material.attractor_interaction_enabled = true
# skills/particles/scripts/particle_burst_emitter.gd
extends GPUParticles3D
## Particle Burst Emitter Expert Pattern
## One-shot particle bursts with automatic cleanup.
class_name ParticleBurstEmitter
signal burst_completed
@export var auto_cleanup := true
func emit_burst(count: int, at_position: Vector3 = Vector3.ZERO) -> void:
global_position = at_position
amount = count
one_shot = true
emitting = true
if auto_cleanup:
await get_tree().create_timer(lifetime).timeout
burst_completed.emit()
queue_free()
func emit_burst_with_velocity(count: int, at_position: Vector3, direction: Vector3, speed_range: Vector2) -> void:
var process_mat := process_material as ParticleProcessMaterial
if not process_mat:
push_error("ParticleProcessMaterial required")
return
# Configure velocity
process_mat.direction = direction
process_mat.initial_velocity_min = speed_range.x
process_mat.initial_velocity_max = speed_range.y
emit_burst(count, at_position)
static func create_burst(
particle_scene: PackedScene,
count: int,
at_position: Vector3,
parent: Node
) -> ParticleBurstEmitter:
var instance := particle_scene.instantiate() as ParticleBurstEmitter
if not instance:
push_error("Scene must be ParticleBurstEmitter")
return null
parent.add_child(instance)
instance.emit_burst(count, at_position)
return instance
## EXPERT USAGE:
## # Method 1: Extend this script on GPUParticles3D
## extends ParticleBurstEmitter
##
## func _ready():
## emit_burst(50, Vector3.UP * 2)
##
## # Method 2: Static creation
## ParticleBurstEmitter.create_burst(
## load("res://fx/explosion.tscn"),
## 100,
## hit_position,
## get_tree().current_scene
## )
# particle_lod_manager.gd
# Managing culling and fading for massive environmental VFX counts
extends GPUParticles3D
func setup_lod_ranges(max_dist: float) -> void:
# Use GeometryInstance3D Visibility Ranges [52]
# This COMPLETELY stops particle processing when out of range.
visibility_range_begin = 0.0
visibility_range_end = max_dist
# Smoothly dither particles out at a distance (Alpha Hash / Dither) [54]
visibility_range_end_margin = max_dist * 0.1
visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
# screenspace_weather_heightfield.gd
# Optimizing global rain/snow using Camera-following HeightFields [46]
extends GPUParticlesCollisionHeightField3D
func _ready() -> void:
# Snaps the collision texture to follow the active Camera
follow_camera_enabled = true
# Optimization: only update depth when camera shifts [48]
update_mode = GPUParticlesCollisionHeightField3D.UPDATE_MODE_WHEN_MOVED
# High resolution (1024) for accurate collisions in open scenes
resolution = GPUParticlesCollisionHeightField3D.RESOLUTION_1024
# smart_oneshot_recycler.gd
# Robust lifecycle management for one-shot VFX
extends GPUParticles3D
func _ready() -> void:
one_shot = true
# Relying on the 'finished' signal is the ONLY safe way to free VFX [40].
finished.connect(_on_vfx_finished)
emitting = true
func _on_vfx_finished() -> void:
# Handle recycling or freeing
queue_free()
func trigger_restart() -> void:
# Anti-pattern fix: setting emitting=true directly after finished
# can fail due to GPU async state. Use restart() instead [41].
restart()
# sub_emitter_impact.gdshader
# Triggering sub-emitters on collision for splashes or debris
shader_type particles;
void process() {
if (COLLIDED) {
mat4 sub_transform = TRANSFORM;
// Offset slightly from collision point using normal
sub_transform[3].xyz += COLLISION_NORMAL * 0.05;
// Spawn sub-particles (debris/splash)
// Only emit if sub-emitter is assigned to the material
emit_subparticle(sub_transform,
REFLECTED_VELOCITY * 0.4,
vec4(1.0),
vec4(1.0),
FLAG_EMIT_POSITION | FLAG_EMIT_VELOCITY);
// Kill parent particle on impact
ACTIVE = false;
}
}
# skills/particles/code/vfx_shader_manager.gd
extends Node
## VFX Shader Manager Expert Pattern
## Manages custom ParticleShaders and visibility-driven culling.
@export var fx_root: Node3D
@export var gpu_particles: GPUParticles3D
func _ready() -> void:
# 1. Custom Particle Shader Assignment
# Expert logic: Bypassing the Standard ParticlesMaterial for
# custom GLSL logic (e.g., Flocking, Curl Noise, Swirling).
_setup_custom_behavior()
func _setup_custom_behavior() -> void:
var shader_material = ShaderMaterial.new()
shader_material.shader = load("res://shaders/vfx/vortex_particles.gdshader")
gpu_particles.process_material = shader_material
# 2. Emission Masks
# Use a black/white texture to mask particle spawn locations.
if gpu_particles.process_material is ShaderMaterial:
gpu_particles.process_material.set_shader_parameter("emission_mask", load("res://assets/vfx/mask.png"))
func toggle_optimization(is_visible: bool) -> void:
# 3. Visibility-Driven Culling
# Professional VFX NEVER run when off-screen.
gpu_particles.emitting = is_visible
set_process(is_visible)
## EXPERT NOTE:
## Use 'Multi-Pass Material Layers': For complex effects like glowing
## fire with smoke, use 'Material.next_pass' to render the smoke
## volume on top of the fire emission in the same system.
## For 'particles', implement 'GPU-Calculated Orbit' logic inside the
## Shader to move 1,000,000 particles with ZERO CPU cost.
## NEVER use CPUParticles for systems with >500 particles unless
## targeting low-end mobile/web without Vulkan support.
Related skills
How it compares
Reach for godot-particles over generic game-dev skills when the task is Godot-specific particle tuning, sub-emitters, or GPU VFX performance.
FAQ
What Godot nodes does godot-particles focus on?
godot-particles centers on GPUParticles2D and GPUParticles3D configured with ParticleProcessMaterial, color ramps, sub-emitters, and optional custom particles shaders. It also notes CPUParticles2D for specific 2D physics interpolation cases.
Does godot-particles include reusable scripts?
godot-particles ships 12 bundled scripts and shaders, including particle_burst_emitter.gd, particle_lod_manager.gd, vfx_shader_manager.gd, and screenspace_weather_heightfield.gd, plus expert patterns for pooling and collision-triggered sub-emitters.