references/expert-vfx-patterns.md
# Expert VFX patterns
> Collision sub-emitters, custom particle shaders, and VFX pooling. Common explosion/smoke material UI → Official Docs.
## Collision sub-emitters (GPU audio sync limits)
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.
```gdscript
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.
---
## Fluid / swarm particle shaders
For high-performance liquid or swarm effects, bypass `ParticleProcessMaterial` and use a custom `particles` shader with state persistence.
```glsl
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;
}
}
```
---
## VFX pool manager
Prevent frame-spikes from frequent `instantiate()` and `queue_free()` calls by pooling and reusing one-shot particle systems.
```gdscript
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()
```
references/migration-notes.md
# Migration notes: godot-particles
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 3.x → 4.0
Official: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html)
- `Particles`/`Particles2D` → `GPUParticles3D`/`GPUParticles2D`.
- `ParticlesMaterial` → `ParticleProcessMaterial`; `set_flag` → `set_particle_flag`.
- CPU particle flag enums renamed (`PARTICLE_FLAG_*`).
- Re-author process material curves after convert.
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `RenderingServer.global_shader_parameter_get_list` / RD shader version lists return `Array[StringName]`.
- `RenderingDevice.draw_list_begin` storage_textures typed as `Array[RID]`.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
- **Mesh format upgrade** — run Project → Tools → Upgrade Mesh Surfaces before GPU particle meshes rely on legacy `.mesh` data.
- ImporterMesh/MeshDataTool/SurfaceTool compression flag widths → `uint64`.
- `RenderingDevice` BarrierMask enum values changed — update custom GPU particle compute barriers.
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- **Reverse Z** depth — update custom particle shaders comparing depth or using `POSITION.z`.
- Decal `modulate` converted sRGB→linear — attached particle decal tints look different; rebalance color values.
- `RenderingDevice` draw_list barrier API simplified (post_barrier params removed).
- **GPUParticles2D** stutter when parented to fast physics bodies — prefer `CPUParticles2D` with `fract_delta = true` for high-speed 2D trails (behavioral; not an API rename).
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- `CPUParticles2D/3D` and `GPUParticles2D/3D.restart()` gain optional **`keep_seed`** — use for deterministic one-shot burst replay in VFX tests.
- `RenderingDevice.draw_list_begin` signature overhauled — update custom GPU particle draw helpers.
- `Shader` default texture parameter types use `Texture` / `TextureLayered`.
- `VisualShaderNodeVec4Constant` input type → Vector4 — recreate vec4 constants in visual particle graphs.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- `RenderingServer.instance_reset_physics_interpolation` / `instance_set_interpolated` removed — drop physics-interpolation toggles on particle mesh instances.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- Environment glow default blend mode **`Screen`** (brighter) — retune `Environment.glow_*` when particle bloom looks blown out.
- Volumetric fog default blending brighter — reduce fog density/energy behind particle-heavy scenes.
- New Windows projects default **D3D12** driver — re-test GPU particles if switching render backends.
- Sky `reflection roughness_layers` default 7 (was 8) — may shift lit particle specular in outdoor scenes.
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- `request_particles_process` / `particles_request_process_time` gain `process_time_residual` — use when syncing one-shot bursts to gameplay clocks.
- `Texture2D.get_format()` unified on base class — branch import/shader paths on format without casting to `ImageTexture`.
- `LinearToSRGB` visual shader no longer clamps `[0,1]` on Mobile/Forward+ — HDR particle trails may need manual clamp in shader.
scripts/2d_physics_interpolation_fix.gd
# 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.")
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_cpuparticles2d.html
# - https://docs.godotengine.org/en/stable/tutorials/2d/particle_systems_2d.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-2d-physics/SKILL.md — physics-parented trails that stutter on GPUParticles2D
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — player/projectile-attached 2D particle trails
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/custom_particle_logic.gdshader
# 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;
}
// =============================================================================
// GDSkills research links (agents) — does not affect runtime
// Official docs:
// - https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/particle_shader.html
// - https://docs.godotengine.org/en/stable/tutorials/3d/particles/process_material_properties.html
// - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
// Related skills:
// - ../godot-shaders-basics/SKILL.md — CUSTOM/USERDATA particle process shaders
// - ../godot-performance-optimization/SKILL.md — GPU orbit/wind without per-particle CPU
// Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
// =============================================================================
scripts/dynamic_userdata_modulation.gd
# 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))
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/particle_shader.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
# - https://docs.godotengine.org/en/stable/classes/class_shadermaterial.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md — USERDATA uniforms without duplicating ShaderMaterials
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — preserve GPU batching across emitter instances
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/local_vs_global_coords.gd
# 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
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/properties.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/trails.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md — projectile smoke trails in global space
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ability-system/SKILL.md — aura FX that must follow the caster (local_coords)
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/massive_swarm_multimesh.gd
# 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)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html
# - https://docs.godotengine.org/en/stable/classes/class_multimesh.html
# - https://docs.godotengine.org/en/stable/classes/class_multimeshinstance3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — MultiMesh cutover when GPUParticles amount ceilings fail
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-procedural-generation/SKILL.md — dense swarm/fish/insect instance buffers
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/particle_attractor_opt.gd
# 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
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/attractors.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticlesattractor3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/process_material_properties.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — cull_mask isolation vs global attractor cost
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md — layered environmental VFX zones
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/particle_burst_emitter.gd
# 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
## )
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/properties.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/creating_a_3d_particle_system.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md — one-shot hit/explosion bursts from damage events
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — finished-driven cleanup after bursts
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/particle_lod_manager.gd
# 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
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html
# - https://docs.godotengine.org/en/stable/classes/class_geometryinstance3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/properties.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — distance culling environmental torches/fires
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — range thresholds relative to active camera
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/screenspace_weather_heightfield.gd
# 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
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/collision.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticlescollisionheightfield3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/creating_a_3d_particle_system.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md — follow_camera_enabled weather collision volumes
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-world-building/SKILL.md — outdoor rain/snow against terrain height
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/smart_oneshot_recycler.gd
# 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()
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/properties.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — finished connections for pool return/free
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — restart() vs emitting=true after GPU async finish
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
scripts/sub_emitter_impact.gdshader
# 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;
}
}
// =============================================================================
// GDSkills research links (agents) — does not affect runtime
// Official docs:
// - https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/particle_shader.html
// - https://docs.godotengine.org/en/stable/tutorials/3d/particles/subemitters.html
// - https://docs.godotengine.org/en/stable/tutorials/3d/particles/collision.html
// Related skills:
// - ../godot-combat-system/SKILL.md — splash/debris sub-emitters on impact
// - ../godot-audio-systems/SKILL.md — GPU collision limits vs looped impact SFX
// Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
// =============================================================================
scripts/vfx_pool_manager.gd
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()
scripts/vfx_shader_manager.gd
# 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.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/particle_shader.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/particles/creating_a_3d_particle_system.html
# - https://docs.godotengine.org/en/stable/classes/class_gpuparticles3d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md — ShaderMaterial assignment on GPUParticles process_material
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — visibility-driven emitting/process culling
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-particles/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-particles
description: "Expert blueprint for GPU particle systems (explosions, magic effects, weather, trails) using GPUParticles2D/3D, ParticleProcessMaterial, gradients, sub-emitters, and custom shaders. Use when creating VFX, environmental effects, or visual feedback. Keywords GPUParticles2D, ParticleProcessMaterial, emission_shape, color_ramp, sub_emitter, one_shot."
---
## NEVER Do in Particle Systems
- **NEVER use `amount_ratio` to optimize performance dynamically** — It does not save GPU memory or improve processing; the full `amount` is still allocated. Change the `amount` property 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 use a Timer to lifetime-cleanup one-shots** — Prefer [smart_oneshot_recycler.gd](scripts/smart_oneshot_recycler.gd): `finished` + `restart()`, or `queue_free()` only on truly disposable instances.
- **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 `CPUParticles2D` with `fract_delta = true` for 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.
## Choose Table (load only the matching script)
> **MANDATORY** for the chosen row. **Do NOT Load** unused particle scripts for a single effect.
| Goal | Prefer | Script |
|------|--------|--------|
| Burst / one-shot VFX (hit, muzzle, explode) | `GPUParticles*` + recycle | **MANDATORY** [particle_burst_emitter.gd](scripts/particle_burst_emitter.gd) + [smart_oneshot_recycler.gd](scripts/smart_oneshot_recycler.gd) |
| Trails behind movers | `local_coords = false` | **MANDATORY** [local_vs_global_coords.gd](scripts/local_vs_global_coords.gd) |
| Weather (rain/snow) heightfield | camera-snapped collision | **MANDATORY** [screenspace_weather_heightfield.gd](scripts/screenspace_weather_heightfield.gd) |
| Million-entity swarms | MultiMesh, not GPUParticles | **MANDATORY** [massive_swarm_multimesh.gd](scripts/massive_swarm_multimesh.gd) |
| Custom GPU motion / userdata | process material shader | [custom_particle_logic.gdshader](scripts/custom_particle_logic.gdshader), [dynamic_userdata_modulation.gd](scripts/dynamic_userdata_modulation.gd) |
| Impact sub-emitters | collision subparticle | [sub_emitter_impact.gdshader](scripts/sub_emitter_impact.gdshader) |
| Attractors without global cost | cull_mask isolation | [particle_attractor_opt.gd](scripts/particle_attractor_opt.gd) |
| Distant env VFX LOD | visibility_range | [particle_lod_manager.gd](scripts/particle_lod_manager.gd) |
| 2D physics-parented trails stutter | `CPUParticles2D` + fract_delta | **MANDATORY** [2d_physics_interpolation_fix.gd](scripts/2d_physics_interpolation_fix.gd) |
| Shader param orchestration | material helpers | [vfx_shader_manager.gd](scripts/vfx_shader_manager.gd) |
**GPUParticles vs CPUParticles vs MultiMesh**
- **GPUParticles*** — default for desktop/console VFX amount budgets.
- **CPUParticles2D** — only when 2D physics interpolation / smooth parenting is required (see NEVER).
- **MultiMesh** — when entity count leaves the particle domain (fish/insects/debris fields).
## Available Scripts
### [smart_oneshot_recycler.gd](scripts/smart_oneshot_recycler.gd)
Golden path for one-shot lifecycle: `finished` + `restart()` — never Timer-based free.
### [particle_burst_emitter.gd](scripts/particle_burst_emitter.gd)
One-shot bursts wired to the recycler.
### [local_vs_global_coords.gd](scripts/local_vs_global_coords.gd)
Aura vs trail coordinate space + teleport `restart()`.
### [screenspace_weather_heightfield.gd](scripts/screenspace_weather_heightfield.gd)
Global weather via camera-snapped heightfield collision.
### [massive_swarm_multimesh.gd](scripts/massive_swarm_multimesh.gd)
Million-entity path with `set_buffer_interpolated()`.
### [custom_particle_logic.gdshader](scripts/custom_particle_logic.gdshader)
Procedural GPU particle motion with CUSTOM/USERDATA.
### [sub_emitter_impact.gdshader](scripts/sub_emitter_impact.gdshader)
Collision-driven `emit_subparticle()` impacts.
### [particle_attractor_opt.gd](scripts/particle_attractor_opt.gd)
Attractor `cull_mask` isolation.
### [dynamic_userdata_modulation.gd](scripts/dynamic_userdata_modulation.gd)
Runtime USERDATA without breaking GPU batches.
### [particle_lod_manager.gd](scripts/particle_lod_manager.gd)
`visibility_range` hierarchy for env VFX.
### [2d_physics_interpolation_fix.gd](scripts/2d_physics_interpolation_fix.gd)
CPUParticles2D + `fract_delta` for physics-parented 2D trails.
### [vfx_shader_manager.gd](scripts/vfx_shader_manager.gd)
Custom shader integration helpers for particle materials.
## Expert Pointers
- One-shots: `emitting = false` at scene origin → place → `restart()` ([smart_oneshot_recycler.gd](scripts/smart_oneshot_recycler.gd)).
- Trails: `local_coords = false` or the trail sticks to the projectile.
- Do not invent explosion/smoke/sparkle material recipes here — Official Docs cover material UI; this skill owns lifecycle, coords, LOD, and swarm routing.
## Deep dives (on demand)
- Collision sub-emitters, fluid shaders, VFX pools → [expert-vfx-patterns.md](references/expert-vfx-patterns.md)
- VFX pool recycle pattern → [vfx_pool_manager.gd](scripts/vfx_pool_manager.gd)
- **WHY** GPU particles cannot drive per-collision SFX — CPU has no collision callbacks; sub-emitters or looping impact beds only.
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Particle systems (2D)](https://docs.godotengine.org/en/stable/tutorials/2d/particle_systems_2d.html) — GPUParticles2D/CPUParticles2D setup, amount/lifetime/one-shot, and when 2D trails need CPU particles for smooth motion.
- [ParticleProcessMaterial 2D](https://docs.godotengine.org/en/stable/tutorials/2d/particle_process_material_2d.html) — emission shapes, gravity/velocity curves, and color ramps that drive most 2D VFX without custom shaders.
- [Creating a 3D particle system](https://docs.godotengine.org/en/stable/tutorials/3d/particles/creating_a_3d_particle_system.html) — GPUParticles3D scene wiring, process material assignment, and first-emission checklist for 3D VFX.
- [Process material properties](https://docs.godotengine.org/en/stable/tutorials/3d/particles/process_material_properties.html) — ParticleProcessMaterial emission, forces, scale/color curves, and collision/sub-emitter modes used by expert patterns.
- [Particle properties](https://docs.godotengine.org/en/stable/tutorials/3d/particles/properties.html) — node-level amount, lifetime, explosiveness, local_coords, visibility AABB, preprocess, and restart/finished lifecycle.
- [Particle subemitters](https://docs.godotengine.org/en/stable/tutorials/3d/particles/subemitters.html) — chaining impact/debris systems and why a particle system cannot recurse as its own sub-emitter.
- [Particle collision](https://docs.godotengine.org/en/stable/tutorials/3d/particles/collision.html) — GPUParticlesCollision* shapes, rigid/hide modes, and GPU collision limits versus CPU-synced SFX.
- [Particle attractors](https://docs.godotengine.org/en/stable/tutorials/3d/particles/attractors.html) — attractor types plus cull_mask/layer isolation so global weather does not pay every attractor cost.
- [Particle trails](https://docs.godotengine.org/en/stable/tutorials/3d/particles/trails.html) — trail ribbons and why smoke/fire trails must use global space (`local_coords = false`).
- [Particle shader](https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/particle_shader.html) — `shader_type particles`, CUSTOM/USERDATA, COLLIDED/`emit_subparticle()`, and keep_data process loops.
- [Using MultiMesh](https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html) — when millions of entities should bypass GPUParticles via MultiMesh + interpolated buffers.
- [Visibility ranges](https://docs.godotengine.org/en/stable/tutorials/3d/visibility_ranges.html) — GeometryInstance3D distance fade/hysteresis that stops distant environmental particle processing.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — scenes, resources, and import basics before packing VFX Prefabs and GradientTexture1D materials.
- [godot-gdscript-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md) — typed GPUParticles APIs, `finished` handlers, and safe `restart()`/await patterns used by pools and burst spawners.
- [godot-shaders-basics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-shaders-basics/SKILL.md) — ShaderMaterial workflow and shading-language fundamentals required before `shader_type particles` process logic.
#### Complements
- [godot-3d-materials](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-materials/SKILL.md) — draw materials, transparency sorting, and next_pass stacks that render quads/meshes spawned by GPUParticles3D.
- [godot-3d-lighting](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-3d-lighting/SKILL.md) — emissive fire/sparks vs environment exposure; pair particle albedo with real lights when VFX must light the scene.
- [godot-audio-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-audio-systems/SKILL.md) — impact/loop SFX while GPU emitters are active when per-particle collision audio is unavailable.
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — amount budgets, visibility AABB, attractor masks, and MultiMesh cutovers when VFX dominate GPU time.
- [godot-camera-systems](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md) — camera-follow heightfields, visibility-range thresholds, and frustum-aware weather emitters.
- [godot-signal-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md) — `finished` and one-shot connection hygiene for pooled recyclers that must not leak ghost callbacks.
- [godot-2d-physics](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-2d-physics/SKILL.md) — physics-parented 2D trails where CPUParticles2D + interpolation replaces stuttering GPUParticles2D.
#### Downstream / consumers
- [godot-combat-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md) — hit sparks, blood/debris bursts, and muzzle FX spawned from damage resolution.
- [godot-ability-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ability-system/SKILL.md) — cast/channel/impact VFX attached to ability lifecycle and targeting feedback.
- [godot-genre-shooter](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-genre-shooter/SKILL.md) — muzzle flash, tracers, explosions, and environmental smoke stacks built on these particle patterns.
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — when VFX density/readability changes perceived difficulty or telegraph clarity, simulate juice budgets with combat outcomes.
#### Master
- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — library router and mirrored module entry for cross-skill discovery.