grains of vision
grainsOfVision · effectsGRAINS OF VISION — a granular VIDEO synthesizer. It shatters the incoming picture into a swarm of tiny windowed patches (grains) sampled from source A at jittered positions AND jittered moments in time (from a short frame-history ring, so grains have a real TEMPORAL axis — the novel part of granular VIDEO), re-scatters them into a new frame, then runs that through a FEEDBACK block and a video REVERB block on ONE fixed linear chain (video -> granular engine -> feedback -> reverb -> out) — NOT a node graph. The grain engine is a single fullscreen pass that is O(1) per pixel regardless of density: the frame is a jittered grid of cells (DENSITY = cells across), each cell hosts one hashed grain, and every pixel gathers its 3x3 cell neighbourhood, window-weights each grain and normalises — so density changes grain SIZE, not the cost. Knobs map the audio-granular vocabulary onto the image: SIZE (window radius), WINDOW (hard chip -> soft gaussian), SPRAY (position scatter of the grain + source-read; 0 reconstructs the picture faithfully, rising clouds it into abstraction), RATE (how far back in the frame history grains read — the pitch/rate analog), T-SPRAY (temporal scatter for a shimmering time-smear), ORIENT (per-grain rotation). The FEEDBACK block mixes the transformed previous OUTPUT back in (FB amount / decay / per-pass zoom + rotate that compound into tunnels and spirals); FB amount 0 (or FB Dry) is a transparent dry passthrough. The REVERB block is a true VIDEO reverb — the image analogue of an FDN/Schroeder tail: a decaying, spatially-diffused temporal accumulator (rev = blur(feedbackImg + decay*prevRev)), so each bright grain blooms outward (Rev Size = room size, Rev Diffuse = tap scatter) and lingers, fading over frames (Rev Decay = tail length), mixed dry/wet by Rev Mix; Rev Mix 0 (or Rev Dry) is a transparent dry passthrough (its blur passes skipped). Patch a second source into B and pick a COMPOSITE mode to have B modulate A grains region-by-region: density-map (B brightness thins/thickens grains), displace (B chroma warps where grains read from), size-map (B scales grain size), rate-map (B scrubs the per-region history time); with only A patched it runs mono-source (composite off). TWO video outputs: OUT (the full chain) and GRAINS (the raw granular scatter tap, pre-feedback/pre-reverb, resolved via read outputTexture:grains — a clean grains-only output to key/mix elsewhere, optional). Rendered at half engine resolution (SwiftShader/CI budget). Like every video processor an unpatched input renders black; the defaults are tuned so any patched source is immediately alive. All ports live on the yellow drill-down PATCH PANEL (no raw side jacks). The grain window / hash / temporal-blend / feedback-UV / reverb-accumulate / composite-mode math is a 1:1 CPU mirror unit-tested in $lib/video/modules/grainsOfVision.
the faceplate
inputs
| id | cable | what it does |
|---|---|---|
in_a | video | PRIMARY video source — the material that is shattered into grains. Unpatched, the output is black. RGB video stream |
in_b | video | MODULATOR video source (optional). When patched AND a COMPOSITE mode is selected, B modulates A's grain data region-by-region (Comp mode picks how). Unpatched, the module runs mono-source (composite off). RGB video stream |
density | cv | CV that modulates Density (grid cells across → grain count), swept linearly over its 2..48 range. control voltage (CV); modulates density (additive offset — ±1 CV sweeps the full range, centered on the knob) |
grain_size | cv | CV that modulates Size (grain window radius in cell units), swept linearly over 0.2..2.5. control voltage (CV); modulates grain_size (additive offset — ±1 CV sweeps the full range, centered on the knob) |
spray | cv | CV that modulates Spray (position scatter of the grain centre + source-read), swept linearly over 0..1. control voltage (CV); modulates spray (additive offset — ±1 CV sweeps the full range, centered on the knob) |
time_spray | cv | CV that modulates T-Spray (temporal scatter of the grains across the frame history), swept linearly over 0..1. control voltage (CV); modulates time_spray (additive offset — ±1 CV sweeps the full range, centered on the knob) |
rate | cv | CV that modulates Rate (how far back in the frame history grains read — the pitch/rate analog), swept linearly over 0..1. control voltage (CV); modulates rate (additive offset — ±1 CV sweeps the full range, centered on the knob) |
orient | cv | CV that modulates Orient (per-grain patch rotation), swept linearly over 0..1. control voltage (CV); modulates orient (additive offset — ±1 CV sweeps the full range, centered on the knob) |
window | cv | CV that modulates Window (grain edge falloff, hard chip → soft gaussian), swept linearly over 0..1. control voltage (CV); modulates window (additive offset — ±1 CV sweeps the full range, centered on the knob) |
feedback | cv | CV that modulates FB (feedback amount, 0 = dry passthrough), swept linearly over 0..0.98. control voltage (CV); modulates feedback (additive offset — ±1 CV sweeps the full range, centered on the knob) |
fb_decay | cv | CV that modulates FB Dec (feedback colour persistence per pass), swept linearly over 0..1. control voltage (CV); modulates fb_decay (additive offset — ±1 CV sweeps the full range, centered on the knob) |
fb_zoom | cv | CV that modulates FB Zoom (per-pass zoom of the feedback tap — the tunnel maker), swept linearly over 0.8..1.2. control voltage (CV); modulates fb_zoom (additive offset — ±1 CV sweeps the full range, centered on the knob) |
fb_rotate | cv | CV that modulates FB Rot (per-pass rotation of the feedback tap — the spiral maker), swept linearly over -20..20 degrees. control voltage (CV); modulates fb_rotate (additive offset — ±1 CV sweeps the full range, centered on the knob) |
rev_mix | cv | CV that modulates Rev Mix (reverb dry/wet, 0 = dry passthrough), swept linearly over 0..1. control voltage (CV); modulates rev_mix (additive offset — ±1 CV sweeps the full range, centered on the knob) |
rev_size | cv | CV that modulates Rev Sz (reverb spatial spread / room size), swept linearly over 0..1. control voltage (CV); modulates rev_size (additive offset — ±1 CV sweeps the full range, centered on the knob) |
rev_decay | cv | CV that modulates Rev Dec (reverb tail length / temporal persistence), swept linearly over 0..0.99. control voltage (CV); modulates rev_decay (additive offset — ±1 CV sweeps the full range, centered on the knob) |
rev_diffuse | cv | CV that modulates Rev Dif (reverb tap scatter — how isotropic the smear is), swept linearly over 0..1. control voltage (CV); modulates rev_diffuse (additive offset — ±1 CV sweeps the full range, centered on the knob) |
composite | cv | CV that SELECTS Comp (the composite mode) using a discrete cvScale, snapping to the 5 modes (off / density / displace / size / rate). Inert while B is unpatched. NOTE the discrete cvScale REPLACES the control rather than offsetting it: while a cable is patched here the Comp buttons on the card and the faceplate are inert, and a cable resting at 0 V selects mode 2 (displace), not the dial's setting. control voltage (CV); modulates composite (integer buckets — CV selects a discrete step) |
comp_amount | cv | CV that modulates Cmp Amt (composite modulation depth), swept linearly over 0..1. control voltage (CV); modulates comp_amount (additive offset — ±1 CV sweeps the full range, centered on the knob) |
outputs
| id | cable | what it does |
|---|---|---|
out | video | The full chain output: the granular scatter after the feedback block and the reverb block. RGB video stream |
grains | video | The RAW granular scatter tap — the grain field BEFORE feedback and reverb (a clean grains-only output to key, mix, or feed elsewhere). Optional; leave it unpatched to ignore. RGB video stream |
params
| id | label | range | default | curve |
|---|---|---|---|---|
density | Density | 2..48 | — | linear |
grain_size | Size | 0.2..2.5 | — | linear |
spray | Spray | 0..1 | — | linear |
time_spray | T-Spray | 0..1 | — | linear |
rate | Rate | 0..1 | — | linear |
orient | Orient | 0..1 | — | linear |
window | Window | 0..1 | — | linear |
feedback | FB | 0..0.98 | — | linear |
fb_decay | FB Dec | 0..1 | — | linear |
fb_zoom | FB Zoom | ?..? | — | linear |
fb_rotate | FB Rot | ?..? | — | linear |
fb_dry | FB Dry | 0..1 | — | discrete |
rev_mix | Rev Mix | 0..1 | — | linear |
rev_size | Rev Sz | 0..1 | — | linear |
rev_decay | Rev Dec | 0..0.99 | — | linear |
rev_diffuse | Rev Dif | 0..1 | — | linear |
rev_dry | Rev Dry | 0..1 | — | discrete |
composite | Comp | 0..? | — | discrete |
comp_amount | Cmp Amt | 0..1 | — | linear |
freeze | Freeze | 0..1 | — | linear |
controls
| control | what it does |
|---|---|
| Cmp Amt | Cmp Amt (0..1, default 0.7): the depth of the COMPOSITE modulation. 0 leaves B inert even when a mode is selected; higher pushes B's influence toward extreme. |
| Comp | Comp (discrete 0..4, default 1 = density): how a patched B modulates A's grains — off, density (B brightness thins/thickens grains), displace (B warps where grains read from), size (B scales grain size), rate (B scrubs the per-region history time). Inert while B is unpatched. |
| Density | Density (2..48, default 14): grains across the frame. Higher packs more, smaller grains; lower gives fewer, larger chunks. (Per-pixel cost is constant — density changes grain SIZE, not the work.) |
| FB Dec | FB Dec (0..1, default 0.9): the per-pass colour persistence of the fed-back frame. Lower fades trails faster; near 1 they linger. |
| FB Dry | FB Dry (0/1, default 0): hard bypass of the feedback block. At 1 the block is a transparent passthrough regardless of the FB amount. |
| FB Rot | FB Rot (-20..20 deg, default 0): per-pass rotation of the feedback tap about centre. Combined with FB Zoom off 1.0 the echoes twist into a spiral. |
| FB Zoom | FB Zoom (0.8..1.2, default 1.0 = identity): per-pass zoom of the feedback tap about centre. Off 1.0 the echo re-zooms every pass, compounding into a tunnel (>1 zoom-in, <1 expanding). |
| FB | FB (0..0.98, default 0.4): feedback amount — how much of the transformed previous OUTPUT is mixed back over the grains. 0 = a transparent dry passthrough (feedback block off); higher builds trails/tunnels (clamped each frame so it never blows out). |
| Freeze | Freeze (0/1, default 0): hidden determinism toggle. At >=0.5 draw() is a no-op so the ring + output hold their last frame for deterministic VRT capture. No card control. |
| Size | Size (0.2..2.5, default 1.1): each grain's window radius in cell units. >=1 makes grains overlap and blend smoothly; small values leave gaps of the dim fallback. |
| Orient | Orient (0..1, default 0.25): per-grain rotation of the sampled patch. 0 = every grain upright; higher tumbles each grain to a random angle. |
| Rate | Rate (0..1, default 0.15): the pitch/rate analog — how many frames back the grains' history tap reaches. 0 = live (no temporal effect); higher pulls the smear deeper into the past. |
| Rev Dec | Rev Dec (0..0.99, default 0.85): reverb tail length — how long the diffuse smear persists and accumulates frame to frame before fading. |
| Rev Dif | Rev Dif (0..1, default 0.5): reverb tap scatter — jitters and rotates the blur taps so the smear is a soft isotropic cloud rather than a boxy axis-aligned blur. |
| Rev Dry | Rev Dry (0/1, default 0): hard bypass of the reverb block. At 1 the block is a transparent passthrough regardless of Rev Mix. |
| Rev Mix | Rev Mix (0..1, default 0.25): reverb dry/wet. 0 = a transparent dry passthrough (reverb block off, its blur passes skipped); higher blends in the diffuse glowing tail. |
| Rev Sz | Rev Sz (0..1, default 0.5): reverb room size — the spatial extent of the blur, i.e. how far each grain blooms outward. |
| Spray | Spray (0..1, default 0.35): position scatter. At 0 grains sit on a tidy grid and read the source at their own location, faithfully reconstructing the picture; rising it jitters both the grain positions and where in the source they grab from, dissolving the image into a granular cloud. |
| T-Spray | T-Spray (0..1, default 0.2): temporal scatter. Spreads the grains across the frame-history from live toward the Rate depth, so different grains show slightly different moments — a shimmering time-smear. 0 = all grains live. |
| Window | Window (0..1, default 0.6): the grain window falloff. 0 = a near-hard chip (crisp mosaic); 1 = a soft gaussian shoulder (grains bloom and cross-fade). |
source
grainsOfVision.ts on GitHub.