WebGL Water Demo: Check the Old State Before Swapping Buffers

A ripple’s first step should not change when you reverse row traversal. An in-place update can accidentally turn traversal order into part of the model.

Compute the original 5×5 center impulse into separate destination arrays. Inspect all interior height/velocity pairs and compare the complete forward and reverse states before native GPU work.

WebGL Water Demo: Check the Old State Before Swapping Buffers
WebGL Water Demo: Check the Old State Before Swapping Buffers
Open the full example ↗

Declare a small height-and-velocity fixture

Use an original 5×5 grid with zero-based row and column indices. The center is row 2, column 2. Its initial height is 1; all other heights are zero. Every initial velocity is zero.

Only the interior 3×3 cells update. The outer border's height and velocity remain fixed at zero. These are authored dimensionless settings, not measured water behavior or physical container dimensions.

For each interior cell, choose this original update rule with coefficient 0.25 and damping multiplier 1:

neighborDifference = oldLeft + oldRight + oldUp + oldDown - 4 * oldHeight
newVelocity = oldVelocity + 0.25 * neighborDifference
newHeight = oldHeight + newVelocity

All four neighbor values are old heights. Damping 1 leaves the calculated velocity unchanged in this exercise. There is no timer cadence, external impulse, or physical time-step interpretation in the supplied fixture.

Evan Wallace's water source provides a reference mechanism: height and velocity are updated using neighboring heights, and a step draws into one texture while reading the other before swapping. The rule and five-cell-wide fixture here are authored separately; no source shader or pool artwork is copied. Original WebGL water source.

Check every interior pair after the first step

At the center, the old neighbors sum to zero. The difference is 0 - 4 × 1 = -4, so new velocity is -1 and new height is 0.

Each of the four cardinal interior neighbors has old height zero and one neighboring old height of 1. Its new velocity is 0.25, and its new height is 0.25. A diagonal interior cell has no nonzero cardinal neighbor in the old grid, so it remains zero.

Expected interior pairs below are written as (height, velocity):

Row / column 1 2 3
1 (0, 0) (0.25, 0.25) (0, 0)
2 (0.25, 0.25) (0, -1) (0.25, 0.25)
3 (0, 0) (0.25, 0.25) (0, 0)

All sixteen outer-border cells remain (0, 0). Comparing only the visible height misses the center's retained velocity of -1. That velocity affects a later step even though its current height is zero.

Make update order irrelevant to the first step

Specify an original CPU reference calculation that reads immutable old arrays and writes separate new arrays. Test it once in forward row order and once in reverse order. Both runs should produce the same complete expected state, including velocities and fixed border cells.

The later WebGL path should follow the same state boundary:

  1. State texture A holds old height and velocity.
  2. The pass samples A and writes the complete destination state into B.
  3. Fixed-border values are explicitly written as zero.
  4. Only after the pass completes does the program swap the state roles of A and B.

Never sample the current write attachment. Sampling a separate previous-state texture is the point of the two-buffer design. Confirm the chosen texture encoding, texel addressing, and filtering before comparing GPU values to the CPU fixture; a neighbor sample must identify the intended cell.

Keep expected CPU values and actual CPU/GPU results in separate columns. If they differ, retain the discrepancy with the source revision and settings rather than changing the expected table to fit a screenshot.

Reset both states, including velocity

Reset is a state operation, not a repaint. Clear both state buffers, all velocities, and the step counter, then restore the declared initial center-height fixture to both buffers. Ensure the read/write role bookkeeping is consistent after reset.

The next first step after reset should match the same nine interior pairs and sixteen border pairs. Resetting only the currently visible buffer can leave old destination data available after a later swap. Resetting heights without velocities leaves a different initial state.

Edits to coefficients, border policy, fixture values, or shader/source revision invalidate the old acceptance ledger. Recheck the reference and native output under the new settings.

Confirm the actual floating-point path

MDN documents EXT_color_buffer_float for WebGL 2 and lists floating-point formats that become color-renderable. That description is not a guarantee that your context, selected attachments, framebuffer, or readback combination works. EXT_color_buffer_float reference.

Try the worked example

Run Forward and Reverse from the same impulse. Both arrays match, including center velocity −1 even though its height is 0. Reset and repeat the first step before translating the rule into a GPU pass. Reset velocities as well as visible heights.

This page executes a CPU stencil, not WebGL, GPU readback, or physical-fluid validation.

Recreate this example

Build one complete self-contained HTML page for this task: calculate the original heightfield CPU reference and compare traversal directions.

Use these exact original practice data and worked expectations: Use a 5×5 grid with center h = 1 and v = 0; all other heights and velocities are 0. Fix border h/v pairs at 0/0. For interior cells, vNew = vOld + 0.25 × (left + right + up + down − 4 × hOld); hNew = hOld + vNew. First-step center: h = 0, v = −1; cardinal neighbors: h = 0.25, v = 0.25; diagonals: h = 0, v = 0.

Implement the following useful local interactions: Forward and Reverse each compute the first step from the unchanged initial fixture. Compare all 25 height/velocity pairs and display 9 interior pairs. Reset both reference arrays and velocities. No WebGL or GPU claim. Preserve the supplied source text and distinguish an uncomputed state, a successful local result, and an invalid or incomplete input. Update output only from the current input snapshot. An input edit must retire stale feedback until the reader runs the relevant action again. Do not silently invent missing values or replace a failing case with a success.

Design the page as a compact source-and-result developer workbench with readable code, labeled inputs, and a separate explanation area. Put a compact task heading, actual sample data, the main result, and usable controls early enough to be useful on a narrow screen. Use readable text of at least 16px, generous paragraph spacing, strong contrast, native labels, accessible buttons, and visible keyboard focus. Stack long records at 360px and wrap code without page-level horizontal overflow.

Use type=button and explicit querySelector-based click handlers rather than relying on form submission. If Enter is useful, handle it only on the relevant input and ignore isComposing. Keep effects within this open page; no remote dependencies, network calls, persistence, fake downloads, or automated sending. This page executes a CPU stencil, not WebGL, GPU readback, or physical-fluid validation. Place worked explanations beside their data and identify teacher or synthetic examples clearly. Output only the complete HTML document, with inline styles and script and LF line endings.

Review the CPU heightfield reference calculator in NOS

Use the full numerical result when preparing a separate texture/readback test. Review the center's height 0 and velocity −1 beside the grid in NOS, and revise eligible static integration instructions in visual editing mode before translating the old-state rule into a GPU pass.

Keep reading