Skip to content

fix: stop Wave Tank damping the interference out of existence (#184) - #237

Merged
BernardJen merged 1 commit into
mainfrom
fix/wave-tank-interference
Aug 18, 2026
Merged

fix: stop Wave Tank damping the interference out of existence (#184)#237
BernardJen merged 1 commit into
mainfrom
fix/wave-tank-interference

Conversation

@BernardJen

Copy link
Copy Markdown
Contributor

Closes #184.

The damping was the bug, and not by a little

The band was 0.972-0.984 per step. At 240 steps/s that leaves 0.5% of a wave after one
second
. The scheme's CFL number is 0.5, so a disturbance moves ~0.707 cells/step and
crossing a 2048-cell tank takes ~2900 steps — after which 0.978 leaves 1e-28 of the
wave.

Interference is this saver's entire subject and it was being annihilated twenty-eight
orders of magnitude before two ripples could meet.

damping half-life amplitude after one 12s crossing
0.978 (old) 0.03s 1e-28
0.99935 4.4s 15.2%
0.99955 6.4s 27.2%
0.99972 10.3s 44.4%

Settled at 0.99935–0.99955, the low end — a balance, not a maximum. At 0.99972 waves
outlived three crossings and the tank accumulated into exactly the uniform chop this issue
also complains about: fine at 20s, over-energised by 45s. The derivation is in the file.

A latent bug that only existed on the wall

MAX_CELLS was clamped per axis independently, so at 6000×1200 the grid came out
1024×300 — 3.41:1 against a 5:1 canvas, stretching every ripple 1.46× horizontally.

It cannot be seen at 3000×600, which never reaches the cap, so no capture would ever have
shown it. Both axes now scale by one factor.

The rest of the issue

  • Resolution: CELL_PX 4 → 2, MAX_CELLS 1024 → 2048. The old comment argued a
    quarter-res grid "looks identical" — true at 1080p; a 4× upsample at 6000×1200 is soft.
  • Wall-clock stepping. Two-steps-per-frame made wave speed and decay depend on frame
    rate — the same tank ran twice as fast on a 120Hz panel. Steps now derive from dt.
  • Caustics from the Laplacian of the height field: curvature is what converges refracted
    rays, and the four neighbours were already fetched for the gradient. This is the single
    change that makes it read as water.
  • Refraction — the floor is sampled displaced by the surface gradient, over a tiled bed,
    because distortion is only visible as distortion of something.
  • Fresnel mix into a sky gradient, so a glancing surface mirrors instead of showing floor.
  • Choreography: one event at a time, separated by 6–14s of quiet. A single large drop; two
    sustained in-phase sources for fringes (fringes are steady-state — an impulse cannot make
    them); or a line source sweeping in y to build a plane wave.
  • A barrier with a slit on 40% of activations, so diffraction is on show.

The barrier was broken, and only forcing it on revealed it

Three seeds in a row happened not to enable it. Rather than report a feature I hadn't seen,
I forced it true — and nothing rendered. Two bugs:

  1. the drawn wall was sized from uTexel, making it ~3 pixels wide;
  2. the slit term was inverted — it painted the gap dark and left the wall transparent.

The simulation barrier was correct all along, so the physics worked and was invisible. Both
passes now share BARRIER_HALF_W. Verified by measuring the darkest interior column:
0.111 → 0.035, with the slit and downstream diffraction visible.

Measurements

  • Dynamic range under 12% washout: 0.020 → 0.812. The old surface had essentially no
    contrast to wash out, which is the "soft oil slick" quantified.
  • 120.1 fps at 6000×1200, vsync-capped, against main's 120.0 — despite 6.7× the cells.
    The stencil is bandwidth-bound and was never the cost.
  • npm run lint clean, npm test 407 passed, npm run shadercheck ok=150 fail=0.

Acceptance criteria

  • Simulation resolution raised; no visible upsample blur at 6000×1200
  • Damping reduced so waves cross and reflect; interference clearly visible
  • Stable damping/speed band documented in the file, with the derivation
  • Caustics rendered on a floor below the surface
  • Refraction and Fresnel reflection; real value contrast (0.020 → 0.812 under washout)
  • Drop choreography with distinct events, not uniform chop
  • At least one structured demonstration — two-source fringes, plus a slit barrier
  • Readable at 12% washout
  • Frame cost at 6000×1200 recorded: 120.1 fps
  • Before/after at 3000×600

Not verified / honest limits

  • It is energetic rather than "mostly quiet". With damping that lets a wave cross a 5:1
    tank — which this issue explicitly asks for — waves live 10–25s, so events every 6–14s
    necessarily overlap. Genuine calm would need gaps longer than the wave lifetime, leaving
    the wall flat most of the time. Individual wavefronts are traceable across the full width,
    which is the property that matters; "quiet" is not achievable alongside "crosses the tank".
  • Wave Tank still measures 0 at frames 5/12, because the first event lands at 0.15s. Its
    baseline stays 0, so it remains one of the two documented savers the structure check skips.
    This PR does not change that, and deliberately does not force an early event just to make a
    number non-zero — a tiny density would sit under the absolute margin anyway.
  • The two-source fringe pattern is visible in motion; a still frame catches it mid-formation,
    so the captures show interference rather than a textbook fringe photograph.

A 3000x600 capture showed a soft oil slick -- no ripples, no rings, no fringes.
The physics was running; the image did not show it.

THE DAMPING WAS THE BUG, AND NOT BY A LITTLE

The band was 0.972-0.984 per step. At 240 steps/s that leaves 0.5% of a wave after
ONE SECOND. The scheme's CFL number is 0.5, so a disturbance moves sqrt(0.5) ~
0.707 cells per step and crossing a 2048-cell tank takes ~2900 steps -- after
which 0.978 leaves 1e-28 of the wave. Interference is this saver's entire subject
and it was being annihilated twenty-eight orders of magnitude before two ripples
could meet.

Surviving one crossing at 25% needs 0.99952; three crossings at 10% needs 0.99974.
Settled at 0.99935-0.99955, the LOW end, because that is a balance rather than a
maximum: at 0.99972 waves outlived three crossings and, with events arriving every
few seconds, the tank accumulated into exactly the uniform chop this issue also
complains about -- fine at 20s, over-energised by 45s. The band is documented in
the file with the derivation.

A LATENT BUG THAT ONLY EXISTED ON THE WALL

MAX_CELLS was clamped per axis independently, so at 6000x1200 the grid came out
1024x300 -- 3.41:1 against a 5:1 canvas, stretching every ripple 1.46x
horizontally. It was invisible at 3000x600, which never reaches the cap, so no
capture would ever have shown it. Both axes now scale by one factor.

THE REST

- Sim resolution: CELL_PX 4 -> 2 and MAX_CELLS 1024 -> 2048. The old comment
  argued a quarter-res grid "looks identical"; true at 1080p, and a 4x upsample at
  6000x1200 is visibly soft.
- Wall-clock stepping. Two-steps-per-frame made wave speed AND decay depend on the
  frame rate -- the same tank ran twice as fast on a 120Hz panel. Steps now come
  from dt at a fixed 240/s, clamped so a stall cannot dump a burst.
- Caustics from the Laplacian of the height field. Curvature is what converges
  refracted rays, so -laplacian is the focusing term, and the four neighbours were
  already fetched for the gradient. This is the change that makes it read as water.
- Refraction: the floor is sampled displaced by the surface gradient, over a tiled
  bed -- distortion is only visible as distortion OF something.
- Fresnel mix into a sky gradient, so a glancing surface mirrors rather than
  showing the floor.
- Choreography: one deliberate event at a time separated by 6-14s of quiet. A
  single large drop; two sustained in-phase sources for a fringe pattern (fringes
  are steady-state, so an impulse cannot produce them); or a line source sweeping
  in y to build a plane wave.
- A barrier with a slit on 40% of activations, so diffraction is on show.

THE BARRIER WAS BROKEN AND ONLY FORCING IT ON REVEALED IT

Three seeds in a row happened not to enable it. Rather than assume, I forced it
true -- and nothing rendered. Two bugs: the drawn wall was sized from uTexel, so it
was about three pixels wide, and the slit term was inverted, painting the gap dark
and leaving the wall transparent. The simulation barrier was correct all along.
Both passes now share BARRIER_HALF_W so the wall the physics blocks is the wall you
see, verified by measuring the darkest interior column (0.111 -> 0.035).

MEASUREMENTS

- Dynamic range under 12% washout: 0.020 before, 0.812 after. The old surface had
  essentially no contrast to wash out.
- 120.1 fps at 6000x1200, vsync-capped, against main's 120.0, despite 6.7x the
  cells -- the stencil is bandwidth-bound and was never the cost.
- shadercheck ok=150 fail=0; no baseline change. Wave Tank still measures 0 at
  frames 5/12 because the first event lands at 0.15s, so it remains one of the two
  documented zero-baseline savers and is still not covered by the structure check.
@BernardJen
BernardJen merged commit 3201dbd into main Aug 18, 2026
1 check passed
@BernardJen
BernardJen deleted the fix/wave-tank-interference branch August 18, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

quality: Wave Tank screensaver — stop damping the interference away, add caustics

1 participant