From summer
Spawns one-shot additive billboard particle bursts (hit sparks) oriented to a surface normal. Handles impact, ricochet, sword clash, footstep, and mining spark effects in Godot.
How this skill is triggered — by the user, by Claude, or both
Slash command
/summer:hit-spark**/*.tscn**/*.gd**/*.gdshaderaddons/vfx/**This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Tiny stretched additive billboards spraying outward from an impact point, oriented to the surface normal so the burst points the right way. Used for: bullets hitting metal, sword clashes, footsteps on stone, ricochets, lightning endpoints, hammer strikes. The recipe is a `GPUParticles3D` configured for one-shot bursts plus a `spawn_hit_spark(position, normal)` static helper that orients the emi...
Tiny stretched additive billboards spraying outward from an impact point, oriented to the surface normal so the burst points the right way. Used for: bullets hitting metal, sword clashes, footsteps on stone, ricochets, lightning endpoints, hammer strikes. The recipe is a GPUParticles3D configured for one-shot bursts plus a spawn_hit_spark(position, normal) static helper that orients the emitter, restarts it, and frees after lifetime.
lightning and muzzle-flash recipes (they reference this).muzzle-flash (recolor it).RigidBody3D shards, then sparks on top.water-ripple for the ring on the surface.fire particles for residual embers.addons/vfx/hit-spark/hit_spark.gd
addons/vfx/hit-spark/hit_spark.tscn
No custom shader — uses the canonical additive billboard material (see _building-blocks/additive-billboard-particles.md). The default BaseMaterial3D with the right flags is enough.
addons/vfx/hit-spark/hit_spark.gd:
@tool
class_name HitSpark
extends GPUParticles3D
@export_group("Spark size")
@export_range(8, 256) var spark_count: int = 24 :
set(v): spark_count = v; _apply()
@export_range(0.05, 1.5) var burst_speed: float = 0.6 :
set(v): burst_speed = v; _apply()
@export_range(0.05, 1.5) var spark_lifetime: float = 0.35 :
set(v): spark_lifetime = v; _apply()
@export_range(5.0, 90.0) var spread_degrees: float = 35.0 :
set(v): spread_degrees = v; _apply()
@export_range(0.0, 9.8) var gravity_strength: float = 4.0 :
set(v): gravity_strength = v; _apply()
@export_group("Look")
@export var spark_color: Color = Color(1.0, 0.85, 0.45)
@export_range(0.0, 12.0) var emission_boost: float = 5.0
@export var stretch_to_velocity: bool = true
func _ready() -> void:
one_shot = true
emitting = false
explosiveness = 1.0 # all particles spawn in frame 1
_apply()
_ensure_material()
func _apply() -> void:
amount = spark_count
lifetime = spark_lifetime
var pm := process_material as ParticleProcessMaterial
if pm == null:
pm = ParticleProcessMaterial.new()
process_material = pm
pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_POINT
pm.direction = Vector3.UP # local +Y; the controller orients the node to the surface normal
pm.spread = spread_degrees
pm.initial_velocity_min = burst_speed * 6.0
pm.initial_velocity_max = burst_speed * 12.0
pm.gravity = Vector3(0, -gravity_strength, 0)
pm.scale_min = 0.04
pm.scale_max = 0.10
pm.color = spark_color
pm.damping_min = 1.5
pm.damping_max = 3.0
if stretch_to_velocity:
pm.particle_flag_align_y = true
pm.scale_curve = _make_streak_curve()
func _ensure_material() -> void:
if draw_pass_1 == null:
var mesh := QuadMesh.new()
mesh.size = Vector2(0.08, 0.30) if stretch_to_velocity else Vector2(0.10, 0.10)
var bm := BaseMaterial3D.new()
bm.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
bm.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
bm.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
bm.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
bm.albedo_color = spark_color
bm.emission_enabled = true
bm.emission = spark_color
bm.emission_energy_multiplier = emission_boost
mesh.material = bm
draw_pass_1 = mesh
func _make_streak_curve() -> CurveTexture:
var c := Curve.new()
c.add_point(Vector2(0.0, 1.0))
c.add_point(Vector2(0.8, 1.0))
c.add_point(Vector2(1.0, 0.0)) # shrink at end of life
var ct := CurveTexture.new()
ct.curve = c
return ct
## Static helper. Spawns a transient one-shot burst at world `position`, oriented to `normal`.
## parent: where to attach the spark instance (defaults to the scene tree root)
## position: world-space impact point
## normal: world-space surface normal at the impact
## intensity: 0.0–1.0+ scales the burst (light tap = 0.4, heavy hit = 1.0, explosion = 1.6)
static func spawn_hit_spark(
parent: Node,
position: Vector3,
normal: Vector3 = Vector3.UP,
intensity: float = 1.0,
scene_path: String = "res://addons/vfx/hit-spark/hit_spark.tscn"
) -> HitSpark:
if not ResourceLoader.exists(scene_path):
push_error("HitSpark: scene missing at %s" % scene_path)
return null
var inst := load(scene_path).instantiate() as HitSpark
parent.add_child(inst)
inst.global_position = position
# Orient local +Y (cone direction) to the surface normal.
if normal.length_squared() > 0.0001:
inst.look_at(position + normal, _safe_up(normal))
inst.rotate_object_local(Vector3.RIGHT, deg_to_rad(-90.0)) # because look_at uses -Z forward
inst.amount = max(4, int(inst.spark_count * intensity))
inst.restart()
inst.emitting = true
var t := inst.get_tree().create_timer(inst.spark_lifetime + 0.1)
t.timeout.connect(inst.queue_free)
return inst
static func _safe_up(n: Vector3) -> Vector3:
return Vector3.RIGHT if absf(n.dot(Vector3.UP)) > 0.99 else Vector3.UP
GPUParticles3D ("HitSpark") [script: hit_spark.gd]
├── process_material: ParticleProcessMaterial (configured by script)
└── draw_pass_1: QuadMesh (0.08 × 0.30, additive billboard)
The script builds material and process material on _ready if missing, so the .tscn can be empty besides the script.
This recipe is a .tscn you instantiate per impact, not added once to the scene. After writing the files, the rest is gameplay code:
# No scene mutation — just create the .tscn at addons/vfx/hit-spark/hit_spark.tscn
# with one root GPUParticles3D node, script attached, and save it.
Then from any impact handler:
# Bullet impact:
var hit := space_state.intersect_ray(query)
if hit:
HitSpark.spawn_hit_spark(get_tree().root, hit.position, hit.normal, 1.0)
# Sword clash (perpendicular spray, normal = velocity reflection):
HitSpark.spawn_hit_spark(get_tree().root, clash_point, clash_normal, 0.7)
# Pickaxe on stone (heavier burst):
HitSpark.spawn_hit_spark(get_tree().root, hit_pos, hit_normal, 1.4)
| Parameter | Range | Effect |
|---|---|---|
spark_count | 8–256 | particles per burst (24 is default; 64 for heavy hits) |
burst_speed | 0.05–1.5 | how fast they fly outward |
spark_lifetime | 0.05–1.5 s | how long visible (short = snappy, long = lingering trails) |
spread_degrees | 5–90° | cone width; 35° = focused outward, 90° = hemisphere |
gravity_strength | 0.0–9.8 | how much they arc downward |
spark_color | Color | tint and emission color |
emission_boost | 0.0–12.0 | bloom strength |
stretch_to_velocity | bool | true = streaked sparks (metal); false = round dots (water) |
Bright orange, fast, gravity arcs them downward.
spark_count = 24
burst_speed = 0.7
spark_lifetime = 0.35
spread_degrees = 35
gravity_strength = 5.0
spark_color = Color(1.0, 0.85, 0.45)
emission_boost = 5.0
stretch_to_velocity = true
Wide spray, brief, white-hot.
spark_count = 36
burst_speed = 0.6
spark_lifetime = 0.25
spread_degrees = 60
gravity_strength = 4.0
spark_color = Color(1.0, 0.95, 0.75)
emission_boost = 7.0
No emission boost, blue-white, gravity-heavy, round dots.
spark_count = 18
burst_speed = 0.5
spark_lifetime = 0.6
spread_degrees = 45
gravity_strength = 9.8
spark_color = Color(0.85, 0.95, 1.0)
emission_boost = 0.5
stretch_to_velocity = false
Recolor + heavy gravity. Pair with a small decal on the floor where they land.
spark_count = 32
burst_speed = 0.8
spark_lifetime = 0.5
spread_degrees = 30
gravity_strength = 9.8
spark_color = Color(0.55, 0.05, 0.05)
emission_boost = 0.8
stretch_to_velocity = true
Cool blue, low gravity (sparks float briefly), high emission.
spark_count = 20
burst_speed = 0.5
spark_lifetime = 0.20
spread_degrees = 70
gravity_strength = 1.0
spark_color = Color(0.55, 0.85, 1.0)
emission_boost = 9.0
one_shot = true. Continuous-emission sparks look like a sparkler held still. One-shot + restart pattern is mandatory.explosiveness < 1.0 for a one-shot. Particles spawn over the lifetime instead of in frame 1; the burst looks dribbly. Set explosiveness = 1.0.spawn_hit_spark helper handles look_at + the −90° rotate-around-X (because look_at uses −Z as forward).BLEND_MODE_MIX instead of BLEND_MODE_ADD. Sparks don't bloom; they look like flat orange triangles.spawn_hit_spark helper creates a Timer-equivalent via get_tree().create_timer to free after lifetime. Otherwise spent emitters accumulate forever.spark_lifetime > 1.0 for sharp impacts. Sparks lingering for a full second look like fairy dust. Keep ≤ 0.5 s for snappy hits.look_at + rotate_object_local pair is the most expensive part of the spawn (one matrix decompose). If you spawn 100/frame, cache the orientation math.spark_count to 12, spark_lifetime to 0.20.look_at with up = UP fails. The _safe_up helper picks RIGHT in that case.Vector3.UP if normal.length_squared() < 0.0001.Decal for the burn mark and skip particles.Engine.time_scale slows the particle simulation. No special handling needed.VFX is code, no MCP required:
addons/vfx/hit-spark/ with the two files above.GPUParticles3D to a new scene, attach hit_spark.gd. Save as hit_spark.tscn.HitSpark.spawn_hit_spark(get_tree().root, position, normal).After firing this recipe, suggest:
summer:visual-effects/recipes/muzzle-flash — pair on the gun side; this recipe handles the bullet-impact side.summer:visual-effects/recipes/lightning — automatically calls this at endpoints.summer:visual-effects/recipes/water-ripple — pair the water-droplet variant with a ripple ring at the impact for water surfaces.summer:visual-effects/game-feel — CameraShake.add_trauma(0.15) on heavy hits, hit-stop for crit kills.summer:audio/sound-effect — generate metal-on-metal clang, sharp transient, short ring, 350ms per spark variant._building-blocks/additive-billboard-particles.md — canonical additive material settings (this recipe uses them)summer:visual-effects/recipes/muzzle-flash — the gun-end companionsummer:visual-effects/recipes/lightning — caller; spawns sparks at bolt endpointssummer:visual-effects/recipes/water-ripple — surface companion for water impactssummer:visual-effects/game-feel — shake/hit-stop pairingsnpx claudepluginhub summerengine/summer-engine-agent --plugin summerCreates a one-shot ~80 ms muzzle flash visual effect using a billboarded quad with a star-burst shader and optional OmniLight3D. Trigger on weapon fire or spell-cast burst.
Implements particle effects in Godot 4.3+ using GPUParticles2D/3D, ParticleProcessMaterial, emission shapes, subemitters, trails, attractors, collision, and VFX recipes.
Creating visual effects using particle systems, physics simulation, and post-processing for polished, dynamic game graphics.