Skip to content
Sample of Shane Flow Field — particles in an fBM flow

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.

Family
shader_legends
Source
shader legends
Evolution
particle
Render
path_tracing
Complexity
medium
Agents
500
Year
2018
Author
Shane (Shadertoy 'Noisey Image Flow Lines', 7sSSzw)

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

See all shader_legends artworks →