drag the mouse: stir — a slow drag pushes the fluid, a fast flick throws it ← / →: tilt gravity and slosh the basin ↑ / ↓: particle count, 60 to 300 (rebuilds the dam break) B: brute force on/off — watch the pair-count bar jump O: obstacles D: colour by density instead of speed SPACE: pause · R — reset Bottom-left ticks are the particle count. The long amber bar is candidate pairs tested this frame, log-scaled; the thin bar under it is how many of those were real neighbours. Press B and the top bar roughly triples in length.
A fluid with no grid — smoothed particle hydrodynamics, poly6 density kernel, spiky pressure gradient, viscosity, and a spatial hash that finds each particle's neighbours in constant time. Stir it with the mouse. THE INTERESTING BIT The SMOKE project on this account is a grid solver — the fluid lives in cells and you advect through them. This is the other half of the field: there is no grid, the fluid is the particles, and every quantity is reconstructed at a point by asking the neighbours within one smoothing radius and weighting them by a kernel. Three different kernels, which looks like overkill until you try to use one: poly6 is smooth and finite at r = 0, which is what a density estimate wants, but its gradient vanishes there, so two particles sitting exactly on top of each other would feel no push apart at all. Hence the spiky kernel for pressure, whose slope is maximal at r = 0. What actually decides whether this is a demo or a toy is the **neighbour search**. Every particle needs everything within 17px of it, every frame, twice — once for density and once for force. Done naively that is O(n²). Here it is a uniform grid at exactly one smoothing radius per cell, rebuilt each frame by counting sort: tally how many particles fall in each cell, prefix-sum the tallies into start offsets, then scatter the indices so each cell is a contiguous run. O(n), no comparisons, no allocation. Measured inside the running project, candidate pairs tested per frame: | particles | brute force | spatial hash | real neighbours found | |---|---|---|---| | 200 | 19 900 | 2 348 | 970 | | 300 | 44 850 | 4 470 | 1 848 | So at 300 particles the hash tests 10× fewer pairs — and put the other way round, brute force would spend the hash's entire 300-particle pair budget on 95 particles. Switching modes back to back in the same session, the sim advanced 54 frames on the hash and 6 on brute force in the same wall clock. Two things worth flagging because measuring them was a surprise: * Only 41% of the hash's candidates are real neighbours. Cells are square and kernels are round, so a 3×3 block of 17px cells is 51×51px of area to cover a circle of radius 17. That is the price of the constant-time query, and it is why the two HUD bars do not line up. * Brute force is not a separate code path. It is the same neighbour walk with the grid set to one cell — every particle lands in cell 1, the four off-cell offsets fall outside the grid and are skipped, and what is left is exactly the j > i loop over all pairs. Zero extra blocks for the comparison, and no risk of the two paths drifting apart. The pair walk visits only the forward half of the 3×3 neighbourhood (right, and the whole row above, plus j > i in the home cell). That offset set is antisymmetric, so every pair of cells is visited from exactly one side and every pair of particles exactly once — and since W(i,j) = W(j,i) and the force on i is minus the force on j, one visit credits both ends. Half the work for free. THE STABILITY BOUNDARY, WHICH IS SHARPER THAN IT LOOKS Weakly-compressible SPH at a fixed 1/30s step is CFL-limited: the stiffness that stops the fluid collapsing into a puddle is also the stiffness that makes the pressure wave outrun the timestep. tools/reffluid.py is the same algorithm in Python and exists solely so that this could be swept without rebuilding the .sb3 forty times. All original - code, art and sound. See Inside is open.