Skip to main content
vfx-water-ripple is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. Water-ripple effect: animated normal-distortion ripples on a water plane or Decal, set off by impacts.

When your agent uses it

  • Authoring a water-ripple effect — raindrop ripples, splash ripples, footstep in a puddle, fish jumps
  • Rain, footsteps, or falling objects should disturb a water surface with spreading ripples

Install

summer setup installs every skill for your agent. To add only this one:
Swap claude-code for your agent, such as codex or cursor. See Skills for every option.

The skill

Concentric expanding rings on a water surface, normal-distorted so the ripple actually refracts the reflection. The shader keeps a small ring buffer of (origin, time, amplitude) tuples and renders all of them simultaneously over a base water plane. GDScript exposes add_ripple(world_pos, amplitude) so any system (rain, footsteps, projectile impacts, fish) can spawn one with a single call.

When to use

  • “Add ripples when the player walks through the puddle.”
  • “Rain hitting the lake should make ripples.”
  • “When the bullet hits the water, ripple.”
  • “Fish jumping leaves ripples.”
  • “Footsteps on the wet street should ripple.”
  • A water plane already exists and looks too static.

When NOT to use

  • The user wants the whole water surface to wave (Gerstner / FFT ocean) — that needs an ocean wave shader; this recipe adds impact ripples on top.
  • The user wants a 2D top-down ripple effect on a UI map — use canvas_item shader, not this spatial one.
  • The user wants splash particles flying upward on impact — pair this with vfx-hit-spark (recolor blue) for the droplets; this only handles the ring on the surface.
  • The user wants the ripple to physically displace floating objects — this is visual only. Use Area3D impulses for the simulation side.

Recipe

1. Files to create

2. Shader code

addons/vfx/water-ripple/water_ripple.gdshader:

3. GDScript controller

addons/vfx/water-ripple/water_ripple_surface.gd:

4. Node tree

5. Wire it in (MCP calls)

Write the two files, then wire the surface with ONE summer_run_script call with an explicit scenePath — transactional, and it builds the subdivided PlaneMesh directly, so no plane.tres sidecar is needed:
Set the mesh BEFORE the script if the controller reads it in _ready; either way, ctx.add_node has already owned the node, so it survives the save. Tuning one mesh value later is one summer_set_resource_property call (resourceProperty="mesh", subProperty="subdivide_width"): it reaches into a resource, it never assigns one.

5a. Verify

The shader and the controller are compiled code, and the controller preloads the shader — a shader that failed to write takes the script down at parse time.
Then trigger ripples from any system. Footstep:
Rain:
Bullet impact:

6. Parameters to tune

Cookbook — named variants

puddle-shallow (default)

Tight, fast ripples, high contrast.

lake-calm

Slow, wide, long-lived ripples.

lava-glow (recolor for hot liquid)

blood-pool

Sticky, slow, dark.

Anti-patterns

  • Spawning more than 16 ripples per surface. The shader has a fixed array. Older ripples get overwritten — fine for rain (they were fading anyway), bad if you spawn 50 in one frame. Either raise MAX_RIPPLES (and the GLSL MAX_RIPPLES) together, or throttle spawns.
  • Ripple position in local instead of world space. The shader expects world XZ. Always convert in the GDScript before pushing.
  • No normal map assigned. The surface looks like flat-shaded plastic. Always assign a tileable water normals texture (any blue noise normals will do).
  • Writing a decoded ±1 vector into NORMAL_MAP. The engine decodes it again (normal_map.xy = normal_map.xy * 2.0 - 1.0), so a flat surface reads as permanent maximum tilt. Re-encode with * 0.5 + 0.5 before assigning, as section 2 does.
  • Timing ripples against shader TIME. TIME wraps at rendering/limits/time/time_rollover_secs (3600 s default) and is not Time.get_ticks_msec() / 1000. Push your own accumulated clock as a uniform and compare against that; otherwise every ripple silently vanishes after an hour of uptime.
  • PlaneMesh too low-res. The vertex displacement needs ~2–4 verts per meter or the silhouette wobble looks blocky. Use subdivide_width = 32, subdivide_depth = 32 minimum on a 10×10 plane.
  • depth_draw_disabled on the water material. You’ll see the bottom of the puddle through the water at every angle. Use depth_draw_opaque.
  • Triggering ripples in _process without spacing. Spamming 60/sec floods the buffer. Throttle to ~5 ripples/sec for rain unless tuned.

Performance notes

  • Cost is O(MAX_RIPPLES × pixels_drawn) in the fragment loop plus O(MAX_RIPPLES × verts) in the vertex loop. 16 ripples on a 5×5 m puddle at 1080p is cheap — an order-of-magnitude expectation, not a measurement. Profile if you need a number.
  • The vertex displacement loop runs per vertex; raising plane subdivision past 64 starts to hurt. 32×32 is plenty.
  • For lakes (large planes), don’t apply this shader to the entire lake — split the lake into chunks and only apply ripples to the chunk near impacts. Or use a Decal projection instead (see edge cases).
  • Mobile: drop MAX_RIPPLES to 8, disable vertex displacement (set ripple_height = 0 and skip the vertex loop), and rely on normal-only ripples.

Edge cases

  • Ripple on a non-flat water surface (Gerstner ocean). This shader assumes the water plane is flat. For wavy water, sample the wave height in the same shader and add the ripple displacement on top.
  • Want to use a Decal instead of overriding the water material. Use Decal projecting straight down with a 256×256 ring texture animated by albedo_mix. Cheaper but doesn’t refract reflections — only colors the surface.
  • Ripple needs to interact with floating debris. Visual ripples don’t push physics. Spawn an Area3D impulse alongside if you need physical effect.
  • Camera under water (POV swimming). This shader is one-sided in concept (ring expanding outward from above). For under-water POV, render a separate shader.
  • Ripple at the edge of the plane. It clips at the plane boundary because the plane has no geometry past it. Expand the plane 2 m beyond the gameplay area.

Fallback (no MCP)

Section 5 is fully automatable — summer_write_file writes the shader and the controller, and summer_add_node + summer_set_prop + summer_set_resource_property + summer_save_scene build the plane including its subdivision. Do not hand these steps to the user when the MCP tools are available. Without the MCP connection there is no engine to drive, so the user does it manually in Summer Engine:
  1. Create addons/vfx/water-ripple/ and write the three files above.
  2. Add a MeshInstance3D with a high-subdivision PlaneMesh (32×32 minimum).
  3. Attach water_ripple_surface.gd and assign a tileable water normals texture.
  4. From your trigger code, call $Puddle.add_ripple(impact_pos, amplitude).
  5. Check the Errors dock — the controller preloads the shader, so a broken shader path fails the script too.

Handoff

After firing this recipe, suggest:
  • vfx-hit-spark — recolor white-blue and spawn at the impact point for a few water droplets flying up. Pair them.
  • vfx-smoke — recolor pale gray for the steam if you’re hitting hot water or lava.
  • sound-effect — generate single water drop in shallow puddle, splash, 200ms and trigger on each add_ripple.
  • For weather-driven rain ripples, scaffold a simple Rain node that calls add_ripple 30×/sec across the puddle bounds.

See also

  • _building-blocks/additive-billboard-particles.md — for paired splash droplets
  • vfx-hit-spark — sister recipe for the upward droplets
  • vfx-smoke — for steam over hot liquids
  • gdscript-patterns — for the controller idioms

From a shell

No Summer MCP tools in this session? Run each one as a command; when summer --version is missing or below 4.0, use npx -y summer-engine@latest in place of summer. Fields are flags in kebab-case (--scene-path res://main.tscn) or one --input '<json>', and summer ops describe <op> prints the exact input. The summer-cli skill has the rest.

Files in this skill

  • scene-scripting: Build scenes, levels, HUDs and wiring with one editor GDScript (summer_run_script) instead of call chains.
  • sound-effect: Make sound effects (swings, hits, whooshes, reloads, UI): three versions to pick from, then wire them.
  • vfx-hit-spark: Hit-spark effect: a one-shot burst of additive particles oriented to the surface, fired on impact.
  • vfx-smoke: Smoke effect: a rising column or puff of soft particles with a noise and density falloff shader.

Source: library/skills/vfx-water-ripple/SKILL.md, rendered from summer-engine@4.3.0 on npm.