Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions ShowControl/FundingCAPTCHA/shaders/rainbow_poop/buffer_b.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Buffer B — poops and splats. iChannel0 = Body (A), iChannel1 = this buffer, iChannel2 = blobs.
// Layout: see common.glsl. Each texel reads the previous frame of every other texel; texels that
// depend on the same event (a drop, a hit) recompute it from the same inputs.
// Positions are worked in screen heights (sh) and stored as UV.

#define BODY iChannel0
#define B iChannel1
#define BLOBS iChannel2

// 0–1, per frame: the rounding offset for tick(). A golden-ratio (Weyl) sequence, not a random
// hash: its running sum stays within about one AGE_Q of exact, where independent random
// offsets wander (≈ ±0.17 s over 15 s at 30 fps).
float DITHER;

// x + dt, kept in multiples of AGE_Q (see common.glsl) and clamped at T_CLAMP.
float tick(float x, float dt) { return min(x + floor(dt / AGE_Q + DITHER) * AGE_Q, T_CLAMP); }
float quant(float x) { return floor(x / AGE_Q + 0.5) * AGE_Q; }

// ── Who is there ──────────────────────────────────────────────────────────────
// The record in slot k carries a different person than SLOT k remembers (or none).
bool isFresh(int k) {
vec4 idn = recIdn(BLOBS, k);
return isState(idn.y, S_EMPTY) || abs(idn.x - sSlot(B, k).x) > 0.5;
}

vec4 initial(ivec2 c) {
if (c.y == 0 && c.x < SLOTS) return vec4(0.0, FIRST_BOMB_S, 0.0, 0.0); // SLOT
if (c.y == 4 && c.x < SLOTS * SPLATS) return vec4(0.0, 0.0, T_CLAMP, 0.0); // SPLAT
if (c.y == 5 && c.x < GROUNDS) return vec4(0.0, 0.0, T_CLAMP, 0.0); // GROUND
return vec4(0.0);
}

// ── Dropping ──────────────────────────────────────────────────────────────────
// Slot k's person is due a poop this frame: confirmed, overdue, and theirs isn't still falling.
bool drops(int k) {
return !isFresh(k) && isState(recIdn(BLOBS, k).y, S_CONFIRMED)
&& sSlot(B, k).y <= 0.0 && sDrop(B, k).x < 0.5;
}

// Where to drop it (UV x): over their centre of mass, ahead of them by the time it takes to
// fall to their head (homing does the rest).
float dropX(int k, float aim) {
vec4 box = recBox(BLOBS, k), an = sAnchor(B, k);
float fall = max(SPAWN_Y - box.w, 0.0) / FALL_SPEED;
float lead = clamp(toScreen(recMot(BLOBS, k).zw).x * fall, -LEAD_MAX, LEAD_MAX);
float x = toScreen(an.xy).x + aim * an.z + lead;
return clamp(toUv(vec2(x, 0.0)).x, 0.02, 0.98); // always seen falling
}

// The falling poop's position (sh): a steady fall with a sway about its sway centre.
vec2 poopAt(vec4 poop, float age) {
vec2 c = toScreen(poop.xy);
return vec2(c.x + SWAY * sin(age * SWAY_W + poop.z), c.y);
}

// The tracked person whose box holds `uv`, preferring `prefer`; −1 if none.
int slotAt(vec2 uv, int prefer) {
const float M = 0.02; // boxes ease; allow a little slack
int found = -1;
for (int k = 0; k < SLOTS; k++) {
if (!isHere(recIdn(BLOBS, k).y)) continue;
vec4 b = recBox(BLOBS, k);
if (uv.x < b.x - M || uv.x > b.z + M || uv.y < b.y - M || uv.y > b.w + M) continue;
if (k == prefer) return k;
if (found < 0) found = k;
}
return found;
}

struct Poop { vec4 poop; vec4 drop; int hitSlot; bool ground; vec2 at; };

// Slot k's poop, one frame on: dropped, falling, landed on someone, or landed on the floor.
Poop poopStep(int k, float dt) {
Poop o;
o.poop = sPoop(B, k); o.drop = sDrop(B, k); o.hitSlot = -1; o.ground = false; o.at = vec2(0.0);
if (drops(k)) {
float aim = AIM_SPREAD * (2.0 * hash11(recIdn(BLOBS, k).x * 3.7 + iTime * 1.3) - 1.0);
o.poop = vec4(dropX(k, aim), SPAWN_Y, 6.2832 * hash11(iTime * 2.9 + float(k)), aim);
o.drop = vec4(1.0, recBox(BLOBS, k).y, recIdn(BLOBS, k).x, 0.0);
return o;
}
if (o.drop.x < 0.5) return o; // nothing falling

// Homing: drift toward the person it was dropped on, while they're still in this slot.
vec4 idn = recIdn(BLOBS, k);
float vx = 0.0;
if (abs(idn.x - o.drop.z) < 0.5 && isHere(idn.y)) {
vec4 an = sAnchor(B, k);
float off = toScreen(an.xy).x + o.poop.w * an.z - toScreen(o.poop.xy).x;
vx = clamp(off * HOMING_GAIN, -HOMING_MAX, HOMING_MAX);
}
float h = dt / float(FALL_STEPS), age = o.drop.w;
for (int s = 0; s < FALL_STEPS; s++) {
o.poop.y -= FALL_SPEED * h; // UV y is sh
o.poop.x += toUv(vec2(vx * h, 0.0)).x;
age += h;
vec2 uv = toUv(poopAt(o.poop, age));
bool onScreen = uv.x > 0.0 && uv.x < 1.0 && uv.y > 0.0 && uv.y < 1.0;
if (onScreen && texture(BODY, uv).r > 0.5) {
int m = slotAt(uv, k);
if (m >= 0) { o.hitSlot = m; o.at = uv; o.poop = vec4(0.0); o.drop = vec4(0.0); return o; }
}
if (uv.y <= o.drop.y) {
o.ground = uv.x > -0.05 && uv.x < 1.05;
o.at = vec2(uv.x, o.drop.y); o.poop = vec4(0.0); o.drop = vec4(0.0); return o;
}
}
o.drop.w = min(age, T_CLAMP);
return o;
}

// ── Per person ────────────────────────────────────────────────────────────────
vec4 slotNext(int k, float dt) {
vec4 idn = recIdn(BLOBS, k);
float here = isHere(idn.y) ? 1.0 : 0.0;
if (isFresh(k))
return vec4(isState(idn.y, S_EMPTY) ? 0.0 : idn.x, quant(FIRST_BOMB_S + FIRST_JITTER * hash11(idn.x * 1.37)), here, 0.0);
vec4 s = sSlot(B, k);
if (drops(k)) s.y = quant(BOMB_EVERY_S + BOMB_JITTER_S * (2.0 * hash11(iTime * 7.7 + float(k)) - 1.0));
else if (isState(idn.y, S_CONFIRMED)) s.y = max(-tick(-s.y, dt), -10.0);
s.z = here > 0.5 ? 1.0 : max(s.z - dt / LEAVE_FADE_S, 0.0);
return s;
}

// The silhouette inside slot k's (widened) box, from this frame's Body: centre of mass (UV) and
// size (sh). Falls back to the box when it holds too little body to measure.
vec4 measure(int k) {
vec4 box = recBox(BLOBS, k);
vec4 b = clamp(box + vec4(-ANCHOR_MARGIN, -ANCHOR_MARGIN, ANCHOR_MARGIN, ANCHOR_MARGIN), 0.0, 1.0);
vec2 span = b.zw - b.xy, sum = vec2(0.0);
float n = 0.0;
for (int j = 0; j < ANCHOR_NY; j++)
for (int i = 0; i < ANCHOR_NX; i++) {
vec2 uv = b.xy + span * (vec2(i, j) + 0.5) / vec2(ANCHOR_NX, ANCHOR_NY);
float on = step(0.5, texture(BODY, uv).r);
sum += on * uv; n += on;
}
if (n < 6.0) return vec4((box.x + box.z) * 0.5, (box.y + box.w) * 0.5, max(box.w - box.y, 0.05), 0.0);
vec2 cell = toScreen(span) / vec2(ANCHOR_NX, ANCHOR_NY);
return vec4(sum / n, SIZE_K * sqrt(n * cell.x * cell.y), 0.0);
}

vec4 anchorNext(int k, float dt) {
if (isFresh(k)) return measure(k);
if (!isHere(recIdn(BLOBS, k).y)) return sAnchor(B, k); // lost: hold where they were
return mix(sAnchor(B, k), measure(k), 1.0 - exp(-dt / ANCHOR_EMA_S));
}

vec4 splatNext(int k, int j, float dt) {
const vec4 DEAD = vec4(0.0, 0.0, T_CLAMP, 0.0);
bool fresh = isFresh(k);
for (int i = 0; i < SLOTS; i++) { // any person's poop can land on k
Poop p = poopStep(i, dt);
if (p.hitSlot != k) continue;
if (j > 0) { // shift the ring down
if (fresh) return DEAD;
vec4 s = sSplat(B, k, j - 1);
s.z = tick(s.z, dt);
return s;
}
vec4 an = anchorNext(k, dt);
vec2 q = (toScreen(p.at) - toScreen(an.xy)) / max(an.z, 0.05);
float seed = hash11(iTime * 13.7 + float(k) * 3.1 + float(i));
return vec4(q.x, q.y - SPLAT_SINK - SINK_SPREAD * hash11(seed * 91.0), 0.0, seed);
}
if (fresh) return DEAD;
vec4 s = sSplat(B, k, j);
s.z = tick(s.z, dt);
return s;
}

vec4 groundNext(int j, float dt) {
for (int i = 0; i < SLOTS; i++) {
Poop p = poopStep(i, dt);
if (!p.ground) continue;
if (j == 0) return vec4(p.at, 0.0, hash11(iTime * 5.3 + float(i)));
vec4 g = sGround(B, j - 1);
g.z = tick(g.z, dt);
return g;
}
vec4 g = sGround(B, j);
g.z = tick(g.z, dt);
return g;
}

void mainImage(out vec4 o, in vec2 fc) {
ivec2 c = ivec2(fc);
o = vec4(0.0);
if (c.y > 5) return;
if (iFrame == 0) { o = initial(c); return; }
float dt = clamp(iTimeDelta, 1e-3, DT_MAX);
DITHER = float((uint(iFrame) * 2654435769u) >> 8u) / 16777216.0; // fract(iFrame · φ), exact in integers

if (c.y == 0 && c.x < SLOTS) o = slotNext(c.x, dt);
else if (c.y == 1 && c.x < SLOTS) o = anchorNext(c.x, dt);
else if (c.y >= 2 && c.y <= 3 && c.x < SLOTS) {
Poop p = poopStep(c.x, dt);
o = c.y == 2 ? p.poop : p.drop;
}
else if (c.y == 4 && c.x < SLOTS * SPLATS) o = splatNext(c.x / SPLATS, c.x % SPLATS, dt);
else if (c.y == 5 && c.x < GROUNDS) o = groundNext(c.x, dt);
}
130 changes: 130 additions & 0 deletions ShowControl/FundingCAPTCHA/shaders/rainbow_poop/common.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Common — every tunable for rainbow poop lives here (edit, save, press R).
// Lengths are in screen heights ("sh") unless noted. Spec: docs/superpowers/specs/2026-11-08-rainbow-poop-design.md
//
// State (Buffer B) layout, texel (x, y); k = person slot 0–7:
// (k, 0) SLOT id, seconds until this person's next poop, presence 0–1 (fades splats on loss), 0
// (k, 1) ANCHOR the person's silhouette: centre of mass x, y (Body UV) and size (sh, ≈ their height;
// see SIZE_K), measured from this frame's Body, low-passed over ANCHOR_EMA_S; 0
// (k, 2) POOP the poop dropped on this person: sway centre x, y (UV), sway phase, aim offset (sizes)
// (k, 3) DROP falling 0/1, floor y (UV), target id, seconds falling
// (k·SPLATS + j, 4) SPLAT centre in the person's frame x, y (sizes from their centre of mass),
// age s, seed 0–1 — j = 0 newest
// (j, 5) GROUND x, y (UV), age s, seed 0–1 — j = 0 newest

const int SLOTS = 8, SPLATS = 8, GROUNDS = 8;
const float T_CLAMP = 99.0; // every "seconds since" counter clamps here
const float DT_MAX = 0.25; // a render hiccup longer than this is treated as this
// Ages and countdowns are kept in multiples of this, which a half float holds exactly up to 64 s;
// each frame's step is rounded to it with a golden-ratio offset, so the running error stays
// within about one step at any fps. (Plain f16 adds of 1/30 above 8 s drift by ~6%.)
const float AGE_Q = 1.0 / 32.0;

// ── Cadence (per person) ──────────────────────────────────────────────────────
const float FIRST_BOMB_S = 1.2; // s after a person is confirmed before their first poop drops …
const float FIRST_JITTER = 0.8; // … plus up to this
const float BOMB_EVERY_S = 5.0; // then one every this many seconds …
const float BOMB_JITTER_S = 1.0; // … ± this, counted from the drop

// ── The fall ──────────────────────────────────────────────────────────────────
// A poop appears just above the top of the screen, over where its person will be when it
// reaches their head, and drifts down at a steady, visible speed with a gentle sway.
const float SPAWN_Y = 1.04; // UV y the poop starts at (just off the top)
const float FALL_SPEED = 0.2; // sh/s
const float SWAY = 0.02; // sh, sideways sway amplitude
const float SWAY_W = 3.0; // rad/s
const float LEAD_MAX = 0.5; // sh: drop ahead of a moving person by at most this
const float AIM_SPREAD = 0.05; // aim within ± this × their size of their centre of mass
// While it falls, the poop drifts toward its person's centre of mass: quick enough to find someone
// standing or swaying, too slow to catch someone who walks or turns away (a walk is ~0.3 sh/s).
const float HOMING_GAIN = 3.0; // 1/s: sideways speed per sh off target …
const float HOMING_MAX = 0.18; // … up to this, sh/s
const int FALL_STEPS = 2; // body hit tests per frame along the fall
const float POOP_R = 0.022; // falling drop radius

// ── Splats ────────────────────────────────────────────────────────────────────
const float SPLAT_LIFE_S = 20.0;
const float SPLAT_FADE_S = 3.0; // the last part of the life
const float LEAVE_FADE_S = 0.6; // splats fade this fast when their person's track is lost
// Splats ride on the silhouette's centre of mass, scaled by its size: SIZE_K × √(area), about an
// upright adult's height. Both are measured from the Body each frame inside the tracker's box, so
// raised arms, strides and box jitter barely move them, and they don't lag the silhouette.
const float SIZE_K = 2.1;
const int ANCHOR_NX = 16, ANCHOR_NY = 24; // Body samples across and up the box
const float ANCHOR_MARGIN = 0.03; // UV: the box is widened by this (records ease behind the Body)
const float ANCHOR_EMA_S = 0.04; // just enough to soften sampling noise
const float SPLAT_R = 0.15; // × size
const float SPLAT_R_MIN = 0.04; // sh
const float SPLAT_SINK = 0.06; // × size: the splat centre sits this far below the impact …
const float SINK_SPREAD = 0.12; // … plus up to this, so splats spread down over head and shoulders
const float SPLAT_LUMP = 0.22; // edge lumpiness, × radius
const float POP_S = 0.18; // splat-in time
const float DRIP_LEN = 2.6; // longest drip, × splat radius
const float DRIP_S = 4.0; // drips reach full length after this
const float GROUND_R = 0.10; // sh, puddle radius before squashing
const float GROUND_LIFE_S = 8.0;
const float GROUND_FADE_S = 2.0;
const float GROUND_SQUASH = 2.6; // puddles are this many times wider than tall

// ── Look ──────────────────────────────────────────────────────────────────────
const float CLOUD_Y = 0.86;
const float INK_W = 2.2 / 360.0, INK_W_HEAVY = 2.6 / 360.0, INK_W_LIGHT = 1.6 / 360.0;

// ── Palette ───────────────────────────────────────────────────────────────────
const vec3 PAPER = vec3(0.965, 0.945, 0.894); // #f6f1e4
const vec3 INK = vec3(0.067); // #111111
const vec3 WARM_WHITE = vec3(1.000, 0.980, 0.941); // #fffaf0
const vec3 SLATE = vec3(0.443, 0.604, 0.678); // #719aad
const vec3 DEEP = vec3(0.125, 0.463, 0.639); // #2076a3
const vec3 VIOLET = vec3(0.420, 0.357, 0.584); // #6b5b95
const vec3 RUST = vec3(0.737, 0.298, 0.149); // #bc4c26
const vec3 ORANGE = vec3(0.910, 0.525, 0.184); // #e8862f
const vec3 YELLOW = vec3(1.000, 0.949, 0.000); // #fff200
// The rainbow, outside in: red, orange, yellow, green, blue, violet.
const vec3 RAINBOW[6] = vec3[6](vec3(0.878, 0.271, 0.227), ORANGE, YELLOW,
vec3(0.384, 0.698, 0.341), DEEP, VIOLET);
vec3 rainbow(int i) { return RAINBOW[((i % 6) + 6) % 6]; }

// ── Blob records (ADR-0022) ───────────────────────────────────────────────────
const float S_EMPTY = 0., S_TENTATIVE = 1., S_CONFIRMED = 2., S_COASTING = 3., S_LOST = 4.;

vec4 recBox(sampler2D b, int k) { return texelFetch(b, ivec2(k, 0), 0); } // x0, y0, x1, y1 Body UV
vec4 recMot(sampler2D b, int k) { return texelFetch(b, ivec2(k, 1), 0); } // cx, cy, vx, vy UV, UV/s
vec4 recIdn(sampler2D b, int k) { return texelFetch(b, ivec2(k, 2), 0); } // id, state, age_s, near
bool isState(float s, float want) { return abs(s - want) < 0.5; }
bool isHere(float s) { return isState(s, S_TENTATIVE) || isState(s, S_CONFIRMED) || isState(s, S_COASTING); }

// ── State accessors ───────────────────────────────────────────────────────────
vec4 sSlot (sampler2D s, int k) { return texelFetch(s, ivec2(k, 0), 0); }
vec4 sAnchor(sampler2D s, int k) { return texelFetch(s, ivec2(k, 1), 0); }
vec4 sPoop (sampler2D s, int k) { return texelFetch(s, ivec2(k, 2), 0); }
vec4 sDrop (sampler2D s, int k) { return texelFetch(s, ivec2(k, 3), 0); }
vec4 sSplat (sampler2D s, int k, int j) { return texelFetch(s, ivec2(k * SPLATS + j, 4), 0); }
vec4 sGround(sampler2D s, int j) { return texelFetch(s, ivec2(j, 5), 0); }

// UV ↔ screen heights. Macros: Shadertoy's Common tab can't see iResolution.
#define toScreen(uv) ((uv) * iResolution.xy / iResolution.y)
#define toUv(sh) ((sh) * iResolution.y / iResolution.xy)

// ── Noise ─────────────────────────────────────────────────────────────────────
float hash11(float n) { return fract(sin(n * 127.1 + 311.7) * 43758.5453); }
float vnoise(float x) { // value noise, −1…1
float i = floor(x), f = fract(x);
return mix(hash11(i), hash11(i + 1.0), f * f * (3.0 - 2.0 * f)) * 2.0 - 1.0;
}

// ── Drawing helpers (lengths in screen heights) ───────────────────────────────
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0));
}
// Coverage of a pen stroke `w` wide at distance `d`, antialiased over one pixel `px`.
float pen(float d, float w, float px) {
w = max(w, px);
return 1.0 - smoothstep(0.5 * (w - px), 0.5 * (w + px), d);
}
// Coverage of the inside of a signed distance `d`.
float fill(float d, float px) { return 1.0 - smoothstep(-0.5 * px, 0.5 * px, d); }
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
Loading
Loading