From summer
Generates procedural lightning bolt effects with jagged paths, glow shader, sparks, and camera shake using Godot's ImmediateMesh and ShaderMaterial.
How this skill is triggered — by the user, by Claude, or both
Slash command
/summer:lightning**/*.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
A lightning bolt is a noisy line from A to B, drawn for ~150 ms with a glow shader, sparks at both endpoints, and camera shake on cast. The recipe builds the jagged path in GDScript (midpoint displacement on a polyline), feeds the verts into an `ImmediateMesh`, applies a `ShaderMaterial` for additive bloom + flicker, spawns `hit-spark` particles at each endpoint, and calls `CameraShake.add_trau...
A lightning bolt is a noisy line from A to B, drawn for ~150 ms with a glow shader, sparks at both endpoints, and camera shake on cast. The recipe builds the jagged path in GDScript (midpoint displacement on a polyline), feeds the verts into an ImmediateMesh, applies a ShaderMaterial for additive bloom + flicker, spawns hit-spark particles at each endpoint, and calls CameraShake.add_trauma(0.6) for the impact. Used for shock spells, tesla coils, weapon discharges, electric enemies.
cast_lightning per segment)laser-beam variant)MeshInstance3D cylinder with continuous emission, not the ImmediateMesh one-shot pattern. (See variants for held-beam.)canvas_item shader, not this.Line3D/ImmediateMesh without the noise displacement; skip this recipe.addons/vfx/lightning/lightning.gdshader # spatial shader for the bolt mesh
addons/vfx/lightning/lightning_caster.gd # build path, draw, animate, spawn endpoints, shake
addons/vfx/lightning/lightning.tscn # reusable scene
addons/vfx/lightning/lightning.gdshader:
shader_type spatial;
render_mode unshaded, blend_add, depth_draw_never, cull_disabled, shadows_disabled;
uniform vec4 bolt_color : source_color = vec4(0.55, 0.75, 1.00, 1.0);
uniform vec4 core_color : source_color = vec4(1.00, 1.00, 1.00, 1.0);
uniform float intensity : hint_range(0.0, 1.0) = 1.0; // driven by the controller
uniform float emission_boost: hint_range(0.0, 16.0) = 8.0;
uniform float thickness : hint_range(0.05, 1.0) = 0.30; // visual halo width
uniform float flicker_rate : hint_range(0.0, 60.0) = 30.0;
void vertex() {
// Billboard each segment toward the camera around its own forward axis.
vec3 right = INV_VIEW_MATRIX[0].xyz;
vec3 up = INV_VIEW_MATRIX[1].xyz;
VERTEX += UV.y > 0.5 ? up * thickness * 0.5 : -up * thickness * 0.5;
}
void fragment() {
// V across the line gives 0 (top) → 1 (bottom); recenter to ±1.
float v = (UV.y - 0.5) * 2.0;
float core = smoothstep(1.0, 0.0, abs(v));
core = pow(core, 3.0);
float halo = smoothstep(1.0, 0.0, abs(v));
halo = pow(halo, 0.7);
// Per-bolt flicker (random per fragment time slice).
float t = floor(TIME * flicker_rate) / flicker_rate;
float flick = 0.85 + 0.30 * fract(sin(t * 91.7) * 43758.5);
vec3 col = mix(bolt_color.rgb, core_color.rgb, core);
float a = (core + halo * 0.4) * intensity * flick;
ALBEDO = col;
EMISSION = col * emission_boost * intensity * flick * (core + halo * 0.5);
ALPHA = clamp(a, 0.0, 1.0);
}
addons/vfx/lightning/lightning_caster.gd:
@tool
class_name LightningCaster
extends Node3D
## Static-style helper. Add one anywhere in the scene; call cast_lightning(from, to) anytime.
const LIFETIME := 0.15
const SEGMENTS := 18 ## subdivisions of the bolt
const DISPLACEMENT := 0.35 ## meters of jaggedness per segment
const BRANCH_CHANCE := 0.25 ## probability of a forked sub-bolt per segment
const BRANCH_LENGTH := 0.8 ## relative length of a forked branch
@export var bolt_color: Color = Color(0.55, 0.75, 1.00)
@export var core_color: Color = Color(1.00, 1.00, 1.00)
@export var emission_boost: float = 8.0
@export var thickness: float = 0.30
@export var trauma_amount: float = 0.6
@export var spawn_endpoint_sparks: bool = true
@export_file("*.tscn") var spark_scene_path: String = "res://addons/vfx/hit-spark/hit_spark.tscn"
## Public API. Call this to fire a bolt.
func cast_lightning(from: Vector3, to: Vector3, intensity_scale: float = 1.0) -> void:
var bolt := _build_bolt_node(from, to, intensity_scale)
add_child(bolt)
bolt.global_position = Vector3.ZERO
if spawn_endpoint_sparks:
_spawn_sparks(from)
_spawn_sparks(to)
if Engine.has_singleton("CameraShake") or _has_autoload("CameraShake"):
var cs := Engine.get_main_loop().root.get_node_or_null("/root/CameraShake")
if cs and cs.has_method("add_trauma"):
cs.add_trauma(trauma_amount * intensity_scale)
func _has_autoload(name: String) -> bool:
var root := Engine.get_main_loop().root
return root and root.has_node("/root/" + name)
func _build_bolt_node(from: Vector3, to: Vector3, intensity_scale: float) -> Node3D:
var holder := Node3D.new()
var im := MeshInstance3D.new()
var mesh := ImmediateMesh.new()
im.mesh = mesh
var mat := ShaderMaterial.new()
mat.shader = preload("res://addons/vfx/lightning/lightning.gdshader")
mat.set_shader_parameter("bolt_color", bolt_color)
mat.set_shader_parameter("core_color", core_color)
mat.set_shader_parameter("emission_boost", emission_boost)
mat.set_shader_parameter("thickness", thickness)
mat.set_shader_parameter("intensity", intensity_scale)
im.material_override = mat
holder.add_child(im)
var path := _generate_path(from, to, SEGMENTS, DISPLACEMENT)
_draw_polyline(mesh, path)
# Optional forked branches.
for i in range(1, path.size() - 1):
if randf() < BRANCH_CHANCE:
var dir := (path[i] - path[i - 1]).normalized()
var perp := dir.cross(Vector3.UP).normalized()
if perp.length_squared() < 0.01:
perp = dir.cross(Vector3.RIGHT).normalized()
var branch_end: Vector3 = path[i] + (dir + perp * randf_range(-1.5, 1.5)) * (to - from).length() * BRANCH_LENGTH * 0.25
var branch_path := _generate_path(path[i], branch_end, SEGMENTS / 3, DISPLACEMENT * 0.6)
_draw_polyline(mesh, branch_path)
var t := holder.create_tween()
t.tween_method(func(v: float) -> void:
mat.set_shader_parameter("intensity", v * intensity_scale),
1.0, 0.0, LIFETIME).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_IN)
t.tween_callback(holder.queue_free)
return holder
func _generate_path(a: Vector3, b: Vector3, n: int, displacement: float) -> PackedVector3Array:
var path: PackedVector3Array = []
var dir := (b - a).normalized()
var perp1 := dir.cross(Vector3.UP).normalized()
if perp1.length_squared() < 0.01:
perp1 = dir.cross(Vector3.RIGHT).normalized()
var perp2 := dir.cross(perp1).normalized()
for i in n + 1:
var t := float(i) / float(n)
var p := a.lerp(b, t)
# Falloff at endpoints so they meet cleanly.
var falloff := sin(t * PI)
var jitter := perp1 * randf_range(-1.0, 1.0) + perp2 * randf_range(-1.0, 1.0)
p += jitter * displacement * falloff
path.append(p)
return path
func _draw_polyline(mesh: ImmediateMesh, points: PackedVector3Array) -> void:
if points.size() < 2: return
mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
for i in points.size():
var v: float = float(i) / float(points.size() - 1)
mesh.surface_set_uv(Vector2(v, 0.0))
mesh.surface_add_vertex(points[i])
mesh.surface_set_uv(Vector2(v, 1.0))
mesh.surface_add_vertex(points[i])
mesh.surface_end()
func _spawn_sparks(at: Vector3) -> void:
if not ResourceLoader.exists(spark_scene_path):
return
var sparks := load(spark_scene_path).instantiate()
add_child(sparks)
sparks.global_position = at
if sparks.has_method("restart"):
sparks.restart()
var t := sparks.create_tween()
t.tween_interval(0.5)
t.tween_callback(sparks.queue_free)
Node3D ("LightningCaster") [script: lightning_caster.gd, autoload-friendly]
└── (children created at runtime per cast: ImmediateMesh + sparks scenes)
Recommended: register one LightningCaster as an autoload (/root/Lightning) so any system can call Lightning.cast_lightning(from, to) from anywhere.
Place one LightningCaster per "world" (or autoload it), then call from gameplay code.
summer_add_node(parent="./World", type="Node3D", name="LightningCaster")
summer_set_prop(path="./World/LightningCaster", property="script", value="res://addons/vfx/lightning/lightning_caster.gd")
summer_set_prop(path="./World/LightningCaster", property="bolt_color", value="Color(0.55, 0.75, 1.0)")
summer_set_prop(path="./World/LightningCaster", property="trauma_amount", value=0.6)
summer_save_scene
Then from the spell code:
var origin: Vector3 = $Wizard/HandTip.global_position
var target: Vector3 = enemy.global_position
$World/LightningCaster.cast_lightning(origin, target)
enemy.take_damage(35)
For chain lightning:
var prev: Vector3 = origin
for enemy in nearest_enemies(prev, 4):
$World/LightningCaster.cast_lightning(prev, enemy.global_position, 0.85)
enemy.take_damage(20)
prev = enemy.global_position
await get_tree().create_timer(0.05).timeout
| Parameter | Range | Effect |
|---|---|---|
LIFETIME (const) | 0.05–0.40 s | how long the bolt is on screen (0.15 is the sweet spot) |
SEGMENTS (const) | 6–32 | path subdivisions; more = smoother jaggedness |
DISPLACEMENT (const) | 0.05–1.5 m | how wild the jagged offset is per segment |
BRANCH_CHANCE (const) | 0.0–0.6 | probability of forks per segment |
thickness | 0.05–1.0 m | halo width of the bolt |
emission_boost | 0.0–16.0 | bloom strength (needs Bloom in WorldEnvironment) |
bolt_color / core_color | Color | recolor for fire-bolt, plasma, magic |
trauma_amount | 0.0–1.0 | how hard the camera shakes |
Cool blue-white, dramatic shake, sky-to-ground.
bolt_color = Color(0.55, 0.75, 1.00)
core_color = Color(1.0, 1.0, 1.0)
emission_boost = 8.0
thickness = 0.30
trauma_amount = 0.6
LIFETIME = 0.18
DISPLACEMENT = 0.45
Tighter, chains between targets.
bolt_color = Color(0.65, 0.85, 1.0)
emission_boost = 6.0
thickness = 0.18
trauma_amount = 0.25
LIFETIME = 0.12
DISPLACEMENT = 0.20
SEGMENTS = 14
BRANCH_CHANCE = 0.15
Short, fast, lots of forks.
bolt_color = Color(0.75, 0.95, 1.0)
emission_boost = 10.0
thickness = 0.10
LIFETIME = 0.06
DISPLACEMENT = 0.15
SEGMENTS = 24
BRANCH_CHANCE = 0.45
trauma_amount = 0.10
Override the script to NOT free after LIFETIME — use a MeshInstance3D with a stretched cylinder mesh updated each frame between two transforms; same shader.
bolt_color = Color(1.0, 0.40, 0.55)
core_color = Color(1.0, 0.85, 0.95)
emission_boost = 12.0
thickness = 0.20
DISPLACEMENT = 0.05 # nearly straight
SEGMENTS = 8
trauma_amount = 0.0 # no shake on a held beam
Line3D. Line3D doesn't billboard or accept the additive shader cleanly. Use ImmediateMesh triangle strip with the billboard vertex shader.blend_add. A bolt that doesn't bloom looks like a curved metal stick.CameraShake.add_trauma.plasma-laser variant pattern instead — one mesh, updated per-frame transform, never re-instantiated.SEGMENTS too low (< 6). Bolt looks like a zigzag triangle. Default 18 is right.DISPLACEMENT proportional to total length without falloff. Endpoints drift away from from/to. The included code uses sin(t * PI) falloff so endpoints meet cleanly.hit-spark scenes) are the bigger cost — 64 particles × 2 = 128 particles per cast. Throttle for storms (see edge cases).floor(TIME * flicker_rate), so it ages with TIME not particle time — flicker is consistent across all bolts in flight.LightningCaster mesh nodes instead of allocating per cast.depth_draw_opaque and let the depth buffer cull it (loses the additive bloom in front of geometry)._generate_path returns a degenerate strip. Add a guard: if (to - from).length() < 0.01, skip.emission_boost to 5.0; underwater bloom is muted.held-beam variant or shorten LIFETIME to 0.08.CameraShake autoload registered. The script's _has_autoload guard prevents a crash; the bolt fires without shake. Suggest _building-blocks/trauma-shake-snippet.md to wire it.VFX is code, no MCP required:
addons/vfx/lightning/ and write the three files above.Node3D, attach lightning_caster.gd. Optionally autoload as Lightning.Lightning.cast_lightning(from_pos, to_pos).CameraShake autoload from _building-blocks/trauma-shake-snippet.md for the punch.After firing this recipe, suggest:
summer:visual-effects/recipes/hit-spark — automatically called at endpoints; tune the spark color to match bolt_color.summer:visual-effects/recipes/magic-glow — for the wizard's hand glow during charge-up before the cast.summer:visual-effects/recipes/muzzle-flash — for the brief bright flash at the casting hand on release frame.summer:visual-effects/game-feel — CameraShake.add_trauma is already called; pair with hit-stop on the target for impact emphasis.summer:audio/sound-effect — generate electric crack thunder, sharp impact, rumble tail, 800ms and play on cast._building-blocks/trauma-shake-snippet.md — CameraShake autoload (REQUIRED for the shake call)_building-blocks/additive-billboard-particles.md — for the endpoint sparkssummer:visual-effects/recipes/hit-spark — endpoint companionsummer:visual-effects/recipes/magic-glow — pre-cast charge-upsummer:visual-effects/recipes/muzzle-flash — for the cast-release flashsummer:visual-effects/game-feel — full game-feel pairingnpx 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.
Writes Godot 4.x shaders (.gdshader files) for 2D canvas_item effects, 3D spatial materials, particles, sky, fog, and post-processing. Covers uniforms, hints, structure, and built-in variables.
Generates ShaderToy-compatible GLSL shaders for ray marching, SDF scenes, fluid sims, particles, procedural noise, lighting, and post-processing effects via WebGL2 HTML pages.