shader_legends · ShaneFlowFieldFBM
Shane Flow Field — particles in an fBM flow
Inspired by Shane's Noisey Image Flow Lines (Shadertoy 7sSSzw). 500 particles drift through a 2D flow field whose velocity at each point is the *normalised gradient* of an fBM noise (Quilez-style, with octaves and persistence). Each particle is a short coloured stroke whose length is constant in canvas units, so the canvas reads as continuous flow lines rather than a heat-map. Trails accumulate in an offscreen buffer with low-alpha fade so older strokes ghost out gracefully.
Original: https://www.shadertoy.com/view/7sSSzw
▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.
Math essence
For each pixel (x, y), let n = fbm(x, y, t). The velocity is the spatial gradient of n, normalised: (vx, vy) = ((n(x+ε) - n(x)) / ε, (n(y+ε) - n(y)) / ε) / ||.||. Each particle steps (px, py) += step_length * (vx, vy) at the particle's current position. Strokes are anti-aliased segments drawn into an RGB trail buffer which fades by trail_fade each step. Particle hues are pre-hashed from starting positions.
Parameters
| Name | Type | Default | Range | Description |
|---|---|---|---|---|
dim | int | 400 | 200–800 | Square output size in pixels. |
num_particles | int | 500 | 50–4000 | Number of active particles. |
fbm_scale | float | 0.045 | 0.005–0.4 | Spatial scale of the fBM noise driving the flow. |
fbm_octaves | int | 4 | 1–6 | Octaves in the fBM noise. |
step_length | float | 1.4 | 0.2–6 | Pixels per particle step. |
trail_fade | float | 0.965 | 0.85–0.999 | Multiplier applied to the trail buffer each step (older trails decay). |
hue_jitter | float | 0.05 | 0–1 | Hue spread across particles. |
Reference
First 60 lines of reference.py. Full source:
library_external/shader_legends/ShaneFlowFieldFBM/reference.py
from __future__ import annotations
from typing import ClassVar
import numpy as np
def _hash22(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
p3 = np.stack([x, y, x], axis=-1).astype(np.float64)
p3 = p3 * np.array([0.1031, 0.1030, 0.0973])
p3 = p3 - np.floor(p3)
p3 = p3 + np.sum(p3 * (p3[..., (1, 2, 0)] + 33.33), axis=-1, keepdims=True)
xx = p3[..., 0]
yz = np.stack([p3[..., 1], p3[..., 2]], axis=-1)
out = (np.stack([xx, xx], axis=-1) + yz) * np.stack([p3[..., 2], p3[..., 1]], axis=-1)
return (out[..., 0] - np.floor(out[..., 0])).astype(np.float32), (out[..., 1] - np.floor(out[..., 1])).astype(np.float32)
def _value_noise(x: np.ndarray, y: np.ndarray, seed: int) -> np.ndarray:
xi = np.floor(x).astype(np.int64)
yi = np.floor(y).astype(np.int64)
xf = (x - xi).astype(np.float32)
yf = (y - yi).astype(np.float32)
xi_i = (xi % (1 << 30)).astype(np.int32)
yi_i = (yi % (1 << 30)).astype(np.int32)
seed_x = np.int32((seed * 17) % (1 << 24))
seed_y = np.int32((seed * 31) % (1 << 24))
u = xf * xf * (3.0 - 2.0 * xf)
v = yf * yf * (3.0 - 2.0 * yf)
n00, _ = _hash22(xi_i + seed_x, yi_i + seed_y)
n10, _ = _hash22(xi_i + np.int32(1) + seed_x, yi_i + seed_y)
n01, _ = _hash22(xi_i + seed_x, yi_i + np.int32(1) + seed_y)
n11, _ = _hash22(xi_i + np.int32(1) + seed_x, yi_i + np.int32(1) + seed_y)
nx0 = n00 * (1 - u) + n10 * u
nx1 = n01 * (1 - u) + n11 * u
return nx0 * (1 - v) + nx1 * v
def _fbm_scalar(x: np.ndarray, y: np.ndarray, seed: int, octaves: int = 4) -> np.ndarray:
f = 1.0
a = 1.0
total = np.zeros_like(x, dtype=np.float32)
norm = 0.0
for k in range(octaves):
total += a * _value_noise(x * f + 31.0 * k, y * f - 17.0 * k, seed + k)
norm += a
f *= 2.0
a *= 0.55
return total / max(norm, 1e-6)
class ShaneFlowFieldFBM:
"""Shane Flow Field — particles in an fBM gradient flow.
Inspired by Shane's Noisey Image Flow Lines (Shadertoy 7sSSzw):
500 particles drift through a 2D flow field whose velocity at
each pixel is the *normalised gradient* of an fBM noise. Each
particle's stroke length is constant (1 unit) so the canvas
reads as continuous flow lines rather than a velocity-coloured
heat map. Trails accumulate in an offscreen buffer with low-alpha Try running it locally
The reference implementation is self-contained Python with numpy. Install the package and run:
python library_external/shader_legends/ShaneFlowFieldFBM/reference.py Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.
More from shader legends
Domain Repetition — Inigo Quilez tile-with-warp
shader_legends · IQDomainRepetition
Inigo Quilez's 'Domain Repetition' technique from his 2008 article.
Hash without Sine — Dave Hoskins hash art
shader_legends · TaterHashArt
Dave Hoskins's celebrated 'Hash without Sine' family of cross-platform-stable pseudo-random hash functions, presented as art.
Improved Noise — Ken Perlin (SIGGRAPH 2002, faithful reimplementation)
shader_legends · KenPerlinNoisePure
Ken Perlin's Improved Noise (SIGGRAPH 2002) ported faithfully to numpy.
Pixel City — top-down procedural city
shader_legends · PolygoncityPixelCity
A top-down 50x50 procedural city built from a deterministic hash of grid cells.
Shane Organic Noise — layered Perlin with rotation and twist
shader_legends · ShaneOrganicNoise
Shane Olsson's 'organic noise' Shadertoy technique: a base Perlin-style gradient noise is sampled in multiple octaves, with each successive layer rotated by an angle proportional to the layer index and twisted by sinusoidal warping about the canvas centre.