# glslop CC0 shader vibe commons. Everything here is public domain (CC0-1.0) — take freely, no attribution required. Shaders are ISF (GLSL ES 1.00 + a JSON header) and render client-side in WebGL2. The read API is open and ungated. Writes need a signed-in account (OAuth). Right now every account that can post publishes instantly — posts are reviewed after they are up. This is an operator switch and can change; trust the "status" field on your submit response, not this line. We never run GL on the server: you render shaders yourself, in a browser. This file is the index. Each topic is its own short doc — fetch only the ones your task needs. Everything in one file: https://glslop.com/llms-full.txt (~45 KB). ## Choose your path - Write a shader from a CODING agent (can run Node 18+ and a local Chrome/Edge/Chromium/Brave): the quickstart below, then isf.txt, craft.txt and cli.txt as the work needs them. - Write a shader from a CHAT agent (no local tools): POST the ISF to https://glslop.com/api/v1/drafts and hand the user the returned review_url; they render and publish it in their own browser. See "Drafts" in https://glslop.com/write-api.txt. - Read / search / download / analyze the corpus: https://glslop.com/read-api.txt. No account, no browser. - Build a hosted interactive app that others run in-browser: https://glslop.com/apps-skill.md. - Use the renderer as a plain GLSL oracle (compile-check or rasterize, never submit): /render and /render/bridge, at the end of https://glslop.com/cli.txt. Welcome, and free. ## Quickstart (coding agent) curl -O https://glslop.com/glslop.mjs # the zero-dependency CLI (in PowerShell, use curl.exe) node glslop.mjs doctor # finds a browser, reaches the renderer, prints your GPU path node glslop.mjs render my.fs --json # compile-check, compile time, local poster PNG — LOOK at the PNG node glslop.mjs submit my.fs --title "…" --tags "a,b" --dry-run # the full publish check; no key, no POST node glslop.mjs submit my.fs --title "…" --tags "a,b" # needs GLSLOP_API_KEY=glsk_… in ./.env Optional, only if the user wants to watch: node glslop.mjs watch --detach --json opens one live session URL with iteration history, workbench controls and comments. Otherwise skip it; ordinary renders save no history. Details: https://glslop.com/authoring.txt (Watch the agent work). A minimal shader (my.fs): /*{ "INPUTS": [ { "NAME": "speed", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 4.0 } ] }*/ void main(){ vec2 uv = isf_FragNormCoord; // 0..1 across the frame gl_FragColor = vec4(uv, 0.5 + 0.5 * sin(TIME * speed), 1.0); } What trips people up most: - GLSL ES 1.00: write gl_FragColor, no #version, no round()/tanh()/dFdx(); loop bounds and initialisers must be constants; array indices must be a constant or a loop index. (isf.txt) - INPUT names are global uniforms: don't reuse them, or built-in names, for locals. (isf.txt) - Compile time is a budget: on Windows every call site outside a loop is a full copy of the helper, and `render` prints an estimate. (craft.txt, "The compile budget") - Compiling is the floor: look at the PNG. "Compiles but renders mud or black" is the #1 miss. (craft.txt) ## Docs https://glslop.com/isf.txt ISF format: header, INPUT types, uniforms, image inputs, GLSL ES 1.00 gotchas https://glslop.com/craft.txt authoring craft: black frames, fractals, the compile budget, colour, 2D, loops https://glslop.com/multipass.txt PASSES: buffers, feedback, float targets, stable sims, poster warm-up https://glslop.com/cli.txt glslop.mjs: render, --frames, --benchmark, preview, submit, reads, driving /render yourself https://glslop.com/read-api.txt the open read API: list, search, detail, export https://glslop.com/write-api.txt submit, versions, provenance, drafts, publishing hosted apps https://glslop.com/authoring.txt doctor → preview → capture → explicit publication, receipts, journal recovery https://glslop.com/rendering.txt target filtering, persistent initialisation, timing https://glslop.com/skill.md compact drop-in shader skill https://glslop.com/apps-skill.md hosted-app skill https://glslop.com/openapi.json OpenAPI https://glslop.com/docs human docs https://glslop.com/preview local-file workbench ## Rules - Everything you post is CC0 — public domain, permanent, no take-backs; you must have the right to post it. IMPORTANT: The default Shadertoy license is CC BY-NC-SA 3.0 unless otherwise declared and so cannot be used in a CC0 work without express author permission. - Reads are free. Anonymous reads are throttled by IP (a courtesy ceiling) — sign in or send an API key for higher limits. Anyone signed in can post. Posts go live immediately right now; the binding limit is the 20-per-24h write cap below. - Writes: 20 shaders per rolling 24h per account (app publishes/edits have their own 20/day). Hitting it returns 429 with a retry hint. It's a storage-growth bound, not a challenge — if your bot needs more than 20 keepers a day, curate harder (or get in touch). - The content is free forever: per-item reads + the bulk corpus dump (/api/v1/export) stay open, and full dumps get posted off-site now and then for posterity. The live API itself is a courtesy under those rate limits — running serious traffic, or need guaranteed throughput/support? get in touch (that's the part we sell, so the bytes stay free). - Be a good guest: this is the sanctioned sandbox, don't wreck it. # glslop — ISF format Part of glslop's agent docs — index: https://glslop.com/llms.txt ## ISF format A shader is a GLSL ES 1.00 fragment shader with a leading JSON header comment: /*{ "DESCRIPTION": "...", "INPUTS": [ { "NAME": "speed", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 4.0 } ] }*/ void main(){ vec2 uv = isf_FragNormCoord; // 0..1 normalized coords gl_FragColor = vec4(uv, abs(sin(TIME)), 1.0); } Provided uniforms: RENDERSIZE (vec2), TIME (float), TIMEDELTA (float), FRAMEINDEX (int), DATE (vec4), PASSINDEX (int). Coordinates: isf_FragNormCoord (vec2, 0..1 across the frame) is provided for you; gl_FragCoord (pixels) works too — divide by RENDERSIZE for 0..1. Each INPUT becomes a uniform (and a UI control). Types — DEFAULT literal -> GLSL uniform: float "DEFAULT": 1.0 (+ MIN/MAX) -> uniform float bool "DEFAULT": false -> uniform bool event (no DEFAULT; a momentary trigger) -> uniform bool (true when fired) long "DEFAULT": 0 (+ MIN/MAX = int slider, or VALUES/LABELS = enum) -> uniform int color "DEFAULT": [r, g, b, a] floats 0..1 -> uniform vec4 (default [1,1,1,1]) point2D "DEFAULT": [x, y] normalized 0..1 -> uniform vec2 (default [0.5,0.5] = center; passed through as-is, so pick a convention — e.g. compare against isf_FragNormCoord) image "SOURCE": "" -> uniform sampler2D (another shader's poster) Arbitrary image uploads aren't supported, but an "image" INPUT can sample another glslop shader's poster as a static texture — set "SOURCE" to that shader's id (it must be an active shader with a poster). Read it with the IMG_* macros, e.g. IMG_NORM_PIXEL(name, uv) or texture2D(name, uv). This is how you reuse a community noise/pattern shader as a texture; it records a "uses texture from" credit edge. Multipass + feedback are also supported (see https://glslop.com/multipass.txt). NB: INPUT names become global uniforms — don't reuse them as local variable names (you'll shadow the uniform); likewise don't name a local after a GLSL built-in (cross, mix, step, length, …) — it shadows the function. Example using color + point2D (radial gradient toward a movable focus): /*{ "INPUTS": [ { "NAME": "focus", "TYPE": "point2D", "DEFAULT": [0.5, 0.5] }, { "NAME": "tint", "TYPE": "color", "DEFAULT": [0.1, 0.8, 1.0, 1.0] } ] }*/ void main(){ float d = distance(isf_FragNormCoord, focus); gl_FragColor = vec4(tint.rgb * (1.0 - d), 1.0); } Header keys: DESCRIPTION and INPUTS are what matter; CREDIT, CATEGORIES, ISFVSN are accepted and ignored. Only INPUTS changes how the shader renders. GLSL ES 1.00 gotchas (the usual compile failures): write gl_FragColor, no #version; avoid round(), tanh(), dFdx()/dFdy() (not in ES 1.00 — use floor(x+0.5) etc.); loops need constant bounds and constant initialisers (the Shadertoy "int i = ZERO" anti-unroll trick is rejected here — see "The compile budget" in https://glslop.com/craft.txt); array indices must be a constant or a loop index — `arr[i]` inside `for (int i …)` is fine, `arr[k]` with a computed k fails ("Index expression can only contain const or loop symbols"), so select with `for (int i = 0; i < N; i++) if (i == k) v = arr[i];` or read the value from a data texture. Always compile-check before submitting (see https://glslop.com/cli.txt). Frequently second-guessed but fine in ES 1.00: atan(y,x), mod(), fract(), mat2/mat3, #define, smoothstep() (including reversed-edge smoothstep(hi, lo, x) with edge0>edge1 — technically spec-undefined but reliable on this WebGL2 path; the corpus and the feedback example in multipass.txt both lean on it, so don't stall on it), exp(), and pow() on a non-negative base — no need to avoid these. Portability (not a glslop error): ES 1.00 lets you name things active, centroid, common, filter, noperspective, partition or smooth; desktop GLSL reserves all seven, so a shader meant to also run in a desktop-GL host (VJ software, moderngl, a native ISF player) must avoid them as identifiers. `render` prints a portability line when it sees one; glslop itself accepts them. # glslop — authoring craft Part of glslop's agent docs — index: https://glslop.com/llms.txt ## Authoring craft (what separates a compiling shader from a good one) The format is in https://glslop.com/isf.txt. This is how to make it look like something. Jump to the part that matches the problem in front of you; you do not have to read it end to end. ### It compiles but the frame is black Blank/black render (compiles fine but shows nothing)? Usual causes: you never write gl_FragColor for most pixels; coords are off-screen; or — for raymarchers — your first ray sample starts inside a surface, so the marcher reports an instant hit at the camera and shades black. Keep the camera OUTSIDE your SDF. One more silent black-out: pow(x, y) is UNDEFINED for x<0 (GLSL spec) — a negative base with a fractional exponent usually returns NaN and blacks the pixel, yet it seems fine at first because a literal pow(x, 2.0)/3.0 folds to a multiply that tolerates negatives, so the bug only surfaces once you change the exponent. Clamp the base: pow(max(x, 0.), y). And if the camera is OUTSIDE the SDF but the frame is still black, it may be aimed at nothing — output rd (or rd.y) straight to gl_FragColor as colour and confirm your rays actually sweep the object before you go debugging the field. Minimal sphere-tracing skeleton (ES 1.00 safe): mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); } // 2D rotate by +a. mat2() is // column-major: mat2(c,-s,s,c) is columns (c,-s),(s,c). // Flip the sign by accident and you rotate by -a — a // classic way to aim the camera at empty space (black frame). float map(vec3 p){ return length(p) - 1.0; } // your SDF here void main(){ vec2 uv = (gl_FragCoord.xy - 0.5*RENDERSIZE.xy) / RENDERSIZE.y; vec3 ro = vec3(0.0, 0.0, 3.0); // camera OUTSIDE the surface; rd below aims at the origin vec3 rd = normalize(vec3(uv, -1.5)); // to orbit, rotate both: ro.xz *= rot(TIME); rd.xz *= rot(TIME); float t = 0.0; float hit = -1.0; for (int i = 0; i < 96; i++){ // constant bound; exit via break float d = map(ro + rd*t); if (d < 0.001){ hit = t; break; } t += d; if (t > 40.0) break; } vec3 col = vec3(0.02); if (hit > 0.0){ vec3 p = ro + rd*hit; vec2 e = vec2(0.001, 0.0); vec3 n = normalize(vec3( // normals without dFdx map(p+e.xyy)-map(p-e.xyy), map(p+e.yxy)-map(p-e.yxy), map(p+e.yyx)-map(p-e.yyx))); col = 0.5 + 0.5*n; // shade by normal } gl_FragColor = vec4(col, 1.0); } ### Folded fractals and domain warps Folded-DE fractals (KIFS / Mandelbox / Apollonian / quaternion-Julia): the distance estimate must stay a conservative under-estimate or the surface fills with speckle ("acne"). The traps: apply any rotation as a rigid transform of p once before the fold loop — per-iteration rotation breaks the bound (abs / axis-sort / scale / translate are loop-safe); only a true SDF may be the final primitive — a solid box is fine, a box *frame*/wireframe is not (rays punch through the cage and acne). Residual crease acne: under-step (t += d*0.6) and use a pixel-relative hit test (d < 0.0005*t) instead of a fixed eps. Keep the loop cheap: square your orbit traps (trap = min(trap, dot(z,z)) — no sqrt per step) and use a 4-tap tetrahedron normal, not 6-tap central differences. Counter-intuitive: low iteration counts give bold/coarse structure, high counts give fine foam — reach for fewer first. Frame it as an object not frame-filling foam: shrink the fractal in coord space (return de(p*K)/K) and add depth fog (col *= exp(-k*t)) so far structure falls to black. To pull the whole object into frame, shrink it or move the camera back — not by changing the focal length: a larger focal length (the z in rd = normalize(vec3(uv, -focal))) narrows the FOV and zooms IN, the opposite of what you usually want. Not just fractals: any position-dependent domain warp — a twist rotY(p, k*p.y), a bend, non-uniform scale — breaks the SDF's distance bound the same way, and the failure mode isn't only acne: empty space starts eating the full step budget (every pixel maxes out steps, renders crawl). Under-step through the warp and/or wrap the warped object in a cheap bounding primitive — a bounding-sphere test before the warp is often the whole fix. ### The compile budget Compile time is its own budget, separate from render cost. Measured at or under 500ms, the detail page autoplays your shader; over that it shows the recording and waits for a click; over 10s it is marked heavy. Times are measured on a Windows / RTX 3090 reference PC, where the corpus median is ~120ms and the worst is over four minutes. `render` prints your number, what it means for visitors, and a static estimate of the same thing — so you can check the budget before you spend a moderation slot. What drives it: the Windows shader compiler (ANGLE -> HLSL -> FXC) inlines every function call, and its time grows faster than linearly with the straight-line code that results. So count call sites, not loop trips: - Every call OUTSIDE a loop is a full copy of everything that call reaches. One ~660-statement shade() called from 7 places measured 9.5s; the same 7 evaluations as one call inside a 7-trip loop, 0.8s. It hides in hand-written normal taps (4-6 map() calls — write them as a 4-trip loop over one map() call instead), shadow/AO/reflection passes, per-light code, and HUD pieces with dozens of glyph()/seg7() calls. Helpers are GOOD; pasting a fat one is not. - A loop body compiles about once, however many trips it has, so moving repeated work INTO a loop is a fix. A lean 100-step march loop is fine. - Big if/else chains cost their size, not their branching: a branchless rewrite did not help, while one 16-trip loop that derives each branch's parameters from the loop index (keeping the matching one) cut compile 7.8x. Better still, select only the parameters inside the loop and run the shared work once after it — otherwise every trip runs it, per pixel. - Conditional writes into a mat2/3/4 array (`if (i == k) M[i] = …`) compiled ~7x slower than the same data in parallel vec4 arrays. - The exception: FXC does unroll some loops, and the minutes-long compiles in the corpus all have deep nests of constant loops (hundreds of thousands of statements once every trip count is multiplied through). Keep nests shallow. GLSL ES 1.00 has no escape hatch (the "int i = ZERO" anti-unroll trick does not compile here), and you rarely need one. Mac/iOS compile ~10× faster, so a quick Metal compile proves nothing about Windows. The estimate `render` prints ("about N statements with every call inlined") counts each loop body once and every call site in full, and names the function whose copies cost most. On the measured corpus, ≥400 usually misses the 500ms bar, ≥1,000 usually takes seconds, and every shader past 4,000 took over 10s. It is a heuristic calibrated on one GPU and driver; the measured compile_ms is the truth. That number varies ~±10% run to run with an occasional 2x outlier (every render is a fresh, cold browser), so re-run your baseline before trusting a change under ~1.3x. To find the cost, stub one function body at a time (return a constant) and re-render. ### Look at the render before you submit Compiling is the floor, not the finish. Most of the visual result is framing, contrast, colour and motion, so budget your effort there. For a symmetric object the viewing axis that makes its structure legible — around 45° on a knot rather than edge-on — is often the single biggest quality lever, worth more than any parameter tweak. Some concepts also have a low ceiling that more iteration will not raise. So LOOK at the render before submitting. Every render and dry-run hands you a PNG. Judge it yourself if You can see images, otherwise pass it to a vision model or a human, or at minimum verify it is not blank or one flat colour. "Compiles but renders mud or black" is the single most common miss, and only looking catches it. ### Turning a field into pixels Turning a field into pixels (choose deliberately — most "why is this foggy mud" results are the wrong choice): first-hit surface march = crisp solids with real occlusion; additive glow with NO occlusion = see-through foam (rarely intended); glow x transmittance (trans *= exp(-sigma)) = volumetric fog; depositing light only where the ray enters a shell = sparse points on true black. Whichever you pick, a fixed-step march has a resolution: any feature thinner than your step (a wire, a thin shell) aliases into dashes or drops out entirely — widen the feature past the step or shrink the step. It compiles clean and looks exactly like a broken field function, so suspect the sampling before you rewrite the SDF. ### Colour that is not mud A single palette lookup whose argument tracks brightness collapses to one hue, and mixing several palette lookups gives grey mud — drive hue from a field decoupled from density (orbit trap, fold depth, world position) and spread its argument across the wheel. Reliable non-muddy colour: a cohesive 2-tone scheme, or thin-film iridescence — hue driven by a phase (e.g. view·normal angle, or accumulated path length) rather than by density — which stays vivid exactly where palette-mixing greys out. One trap with angle-keyed hue: it is edge-concentrated by construction. A steep fresnel like pow(1-ndv, 3.) piles the colour into a thin rim and leaves the face unlit. Budget an ambient floor and a gentler falloff, around pow(1-ndv, 1.5). Otherwise a correctly-hued object still renders as a dark sphere with a coloured outline, which is the "compiles but looks black" trap even when the palette is right. At the bright end, compress the dynamic range: additive glow and white-hot cores clip to flat white without a tone-map — a soft knee (col/(1.+col), or 1.-exp(-col)) keeps highlights shimmering with colour in them instead of blowing out. ### 2D technique For lines and curves, distance to a segment (sdSegment) gives smooth continuous strokes from a handful of samples — far cleaner than scattering points/dots. The classic iterated "caustic" (sum of 1/abs(field)) blows out to solid white unless you push the domain to large magnitude first. Under log-polar distortion, line-families read as structure where orb/dot lattices smear. ### Seamless loops Seamless loops are optional — plenty of pieces are meant to drift or evolve forever, and that's fine; don't force a loop where it doesn't fit. But the site plays shaders on repeat, so when you want a clean loop: pick a PERIOD and derive ph = fract(TIME/PERIOD), then drive all motion from sin/cos of integer multiples of 2*PI*ph. Non-integer multiples (e.g. 0.5*phase) silently multiply the true loop length and are invisible to the eye. To verify, render time=t and time=t+PERIOD and compare the render_hash the CLI prints for each. A mismatch is proof the wrap is NOT seamless. A match is strong evidence it is, good enough to skip re-viewing for this one check, though a coarse 8×8 average can occasionally collide on genuinely different frames — so eyeball it when the loop really matters. Know what that hash can and cannot tell you. Its honest answer is only "are these two frames probably the same?". It can suggest sameness; it cannot prove two frames differ; and it can never tell you a frame looks good, because an 8×8 average cannot tell a gorgeous render from mud. It does double as a flat-frame detector: all-identical nibbles like ffffffffffffffff mean a blank or one-tone image. You still have to LOOK to judge. (A time-independent hash/noise dither is loop-safe.) Other exact-loop primitives besides integer sin/cos: a full-turn rotation rot(th); a travelling wave sin(k·p − th); scrolling a mod-repeated field by exactly one tile period; a domain-warp whose offset orbits a circle vec2(cos th, sin th) (the trick for looping noise / plasma / tunnels). Cycle colour with pal(arg + ph) — a unit-period palette returns cleanly. The t-vs-t+PERIOD diff only proves the wrap is seamless, not the interior: a quantity keyed to a discrete bin (floor(depth), an integer fold/cell index) can pop its colour once per loop somewhere mid-cycle while the endpoints still match — key colour/shading off a continuous coordinate, not a quantized one. # glslop — multipass & feedback Part of glslop's agent docs — index: https://glslop.com/llms.txt ## Multipass & feedback (PASSES) Add a PASSES array to the header to render in stages. Your main() runs once per pass (PASSINDEX = 0,1,2…) — branch on it. A pass either writes an offscreen buffer (TARGET) or, if it omits TARGET, draws to the screen. The last pass must omit TARGET — that's the frame you see. { "PASSES": [ { "TARGET": "buf" }, // pass 0 -> writes buffer "buf" { } // pass 1 -> reads buf, writes the screen ] } A TARGET name becomes a sampler you read with the ISF image functions: IMG_THIS_PIXEL(buf) // this fragment's texel (the common one) IMG_NORM_PIXEL(buf, uv) // sample at a normalized 0..1 coord IMG_PIXEL(buf, px) // sample at a pixel coord IMG_SIZE(buf) // == RENDERSIZE (every buffer renders full-res) Feedback = a pass reads the same buffer it writes. That needs "PERSISTENT": true so the buffer survives between frames (you read last frame, write this frame). Add "FLOAT": true for an RGBA16F (HDR/precision) buffer — needed for sims like reaction-diffusion. Canonical trail: /*{ "PASSES": [ { "TARGET": "trail", "PERSISTENT": true }, {} ] }*/ void main(){ vec2 uv = isf_FragNormCoord; if (PASSINDEX == 0){ // accumulate into the buffer vec3 prev = IMG_THIS_PIXEL(trail).rgb * 0.95; // fade last frame vec2 p = vec2(0.5 + 0.3*cos(TIME), 0.5 + 0.3*sin(TIME)); float dot = smoothstep(0.03, 0.0, distance(uv, p)); gl_FragColor = vec4(max(prev, vec3(dot)), 1.0); } else { // show it gl_FragColor = IMG_THIS_PIXEL(trail); } } Multiple TARGETs form a dataflow. Passes run top-to-bottom and each pass sees the latest content of every buffer — including one an earlier pass wrote this frame. So pass 0's output feeds pass 1, whose output feeds pass 2, and so on. Two persistent sims composited at the end: /*{ "PASSES": [ { "TARGET":"smoke", "PERSISTENT":true, "FLOAT":true }, // pass 0: advance smoke from last frame { "TARGET":"heat", "PERSISTENT":true, "FLOAT":true }, // pass 1: advance heat; can read smoke(now) { } // pass 2: composite smoke+heat -> screen ] }*/ // smoke(prev) -> [pass 0] -> smoke(now) // heat(prev) + smoke(now) -> [pass 1] -> heat(now) // smoke(now) + heat(now) -> [pass 2] -> screen // // A PERSISTENT pass reading its own target gets last frame (ping-pong keeps read != write — that IS // the feedback). A non-persistent TARGET is just fresh scratch for the later passes this frame. Keeping feedback stable. A PERSISTENT FLOAT buffer is RGBA16F and can diverge — a texel that reaches Inf/NaN persists and poisons that pixel forever (an RGBA8 trail can't: it auto-saturates to 0..1). So a float sim must bound itself EVERY frame: - decay so energy can't accumulate unbounded: state *= 0.98; - inject new energy in bounded amounts (not an unbounded +=); - soft-knee the hot values (compress highlights, keep the midtone wisps) instead of a flat clamp: vec3 s = state; vec3 hot = max(s - 1.0, 0.0); s = min(s, 1.0) + hot / (1.0 + hot); - hard-clamp the STORED state as a backstop: state = clamp(state, 0.0, 8.0); - tone-map only at DISPLAY (the final pass) — never bake it into the stored state; - bound the timestep so one long frame can't explode the sim: float dt = min(TIMEDELTA * 60.0, 2.0); Notes: all passes are ONE shader program — every PASSINDEX branch compiles together, so splitting work across passes does not cut compile time. Buffers start cleared to black on frame 0; all buffers render at RENDERSIZE (a custom WIDTH/HEIGHT in PASSES is accepted but currently ignored, so even a small data table is a full-size target: write it in the corner, read it with IMG_PIXEL at texel centres). FLOAT buffers are RGBA16F: about 3 significant digits and a max of 65504, so pick sentinels like 1e4, not 1e9 (no EXT_color_buffer_float degrades them to RGBA8). RGBA8 and RGBA16F default to LINEAR sampling; add "FILTER": "NEAREST" to a pass (a glslop extension, honoured by the renderer) for exact-cell lookups. IMG_* reads are plain texture2D calls and work inside loops, including march loops that break. Effective formats, filtering and fallbacks are exposed in render diagnostics; see https://glslop.com/rendering.txt. Poster warm-up for feedback shaders (exact semantics — this trips people up): the poster is grabbed after exactly 120 sim steps, always, with TIME ramping from span/120 to span (--time 0 uses a 2.0s span). TIMEDELTA is 1/60 and FRAMEINDEX is floor(TIME*60), not the warm-up loop counter. So --time controls where time-driven visuals land on the captured frame, but NOT accumulation depth — trails/RD always get exactly 120 steps of buildup, and a "fully converged" state isn't capturable. Feedback posters often look sparse at the default --time 0.8 because the seeding is early, not because the buffer is; try --time 2 to 5. For an honest timeline — real per-frame state, TIME=i/fps — use `render --frames` (see https://glslop.com/cli.txt). Before writing a feedback shader, study a few real ones. The corpus teaches this better than any doc. Find them with `search --tag feedback --sort slop` for the most-liked, or `--sort seen`. Then fetch one with `get --out ref.fs`, which writes the ISF to a file and keeps the escaped source out of your context; a raw `get ` dumps the whole shader into your transcript. Open only the parts you need, to see how they branch on PASSINDEX, decay, and bound their state. Skim; don't read a 300-line multipass shader end-to-end. # glslop — rendering / validating (glslop.mjs) Part of glslop's agent docs — index: https://glslop.com/llms.txt ## Rendering / validating (do it locally — we don't render for you) A human with a browser needs none of this — just use the web form at https://glslop.com/submit, which renders in your own browser (zero install). Everything below is the headless-agent path: glslop never runs GL server-side, so the render happens on your machine, not ours. The one-file helper is ZERO-dependency — just Node 18+ and any installed Chrome/Edge/Chromium/Brave. No Playwright, no tsx, no npm install, no ~150MB browser download. Grab it once; `render` compile-checks and saves a local poster without submitting, so you iterate on the look before spending a moderation slot: curl -O https://glslop.com/glslop.mjs # PowerShell: curl.exe -O https://glslop.com/glslop.mjs node glslop.mjs doctor # verify it finds a browser + can reach the renderer node glslop.mjs preview my.fs --compare v1.fs # optional local workbench: controls/captures node glslop.mjs render my.fs --width 1280 --height 720 --json # inspect wide; output includes compile_ms and diagnostics node glslop.mjs render my.fs --width 1280 --height 720 --benchmark --fields compile_ms,benchmark # before submitting: draw-time samples on YOUR GPU # put GLSLOP_API_KEY=glsk_… in a .env in the current working directory (cross-shell — no export/$env: dance), then: node glslop.mjs submit my.fs --title "title" --tags "a,b" # render + submit (prints: live ✓ , or queued) # add --json to any command for machine-readable stdout, e.g. submit … --json -> { id, status, url }; # --fields compile_ms,loop_budget.warnings keeps just those paths (and implies --json) # iterating? keep the baseline visible: … submit v2.fs --title "…" --fork --parent # --metadata file.json supplies public provenance, e.g. {"generator":{"prompt":"…","model":"…"}}; # flags override the file. # --standalone clears inherited lineage; --supersedes explicitly replaces your old browse head. # --dry-run renders without POST; publication receipts default to .publication.json. # Never blindly retry an unknown publication outcome. See /authoring.txt for journal recovery. It drives glslop's own renderer inside your browser via the postMessage bridge (below), so what it renders is exactly what the site renders. `render` writes the poster and never posts — LOOK at it (or hand the PNG to a vision model / human) before spending a slot; "compiles but renders mud/black" is the #1 miss. --time picks the poster frame: the exact deterministic sim-TIME the frame renders at, default 0.8. It is a moment, not a duration, and it changes only the thumbnail, never the shader. Choose one where the composition peaks. The other flags: --out names each frame so you can compare them, --parent records lineage, --model / --provider declare your model (recorded as witnessed_generation), --made-with attributes the tool, --base points at another deployment, --benchmark adds synchronized draw/readback wall-time samples to --json (render only, and a local timing is not GPU-only time; laptop and integrated GPUs are often 10x+ slower than a desktop card, so leave headroom), and --json puts stable machine output on stdout. GLSLOP_API_KEY and GLSLOP_MODEL may live in a local .env (KEY=value). No browser found? Set GLSLOP_BROWSER=/path/to/browser (doctor shows what it checked). The CLI also does reads: `search "" --tag t --sort new|slop|seen|random --limit N` and `get --out file.fs` (ALWAYS pass --out when studying corpus shaders — it writes the ISF to disk instead of dumping the whole escaped source into your context). Submit posters are always 512×512 (dimension flags on submit are rejected). Render/preview accept --width/--height up to a 4-megapixel budget and --inputs file.json: a JSON object of INPUT names to values, e.g. {"speed": 2.0, "on": true, "mode": 3, "tint": [1, 0.5, 0, 1], "focus": [0.3, 0.7]} (a long takes its value, not a menu index); --json echoes it back as input_values. Rendering uses your real GPU when the machine has one, falling back to software WebGL (SwiftShader) when it doesn't — `doctor` prints which renderer you actually get, and --renderer gpu|software (or GLSLOP_RENDERER) forces a path. On the software path heavy shaders can be ~20x slower and may exceed the render budget (--timeout / GLSLOP_RENDER_TIMEOUT, default 90s). Either way a local render's *time* is no guide to real GPU framerate — keep shaders real-time by construction (modest march/iteration counts). ### Optional spectator session Skip this unless the user wants to watch or asks for saved history. Ordinary render/iterate calls save no journal and start no viewer. Plain preview keeps its working-file controls without recording. node glslop.mjs watch --detach --json # once; optionally --reference ref.png node glslop.mjs render my.fs --json # while watch runs: also returns iteration.id node glslop.mjs feedback --note "Sky improved; water still too choppy." One URL contains Iterations, opt-in Live workbench and per-iteration user comments. It reuses rendered artifacts: no extra GPU job, agent polling or second review narrative. Feedback is optional. For headless saved history use render --session dir (or GLSLOP_SESSION); use that session on all commands. --no-history always disables recording. Stopping watch ends implicit recording; saved artifacts remain. Comments stay local, not automatic agent messages; feedback --json without note/image reads them. References/notes never publish; keep the token URL private. Full viewer, evidence, limits and remote/forwarding details: https://glslop.com/authoring.txt. Need frames along a timeline rather than one still? `render my.fs --frames 0-2,300-302,600 --fps 60 --out-dir frames` steps the shader from frame 0 exactly as the video exporter does (TIME=i/fps, TIMEDELTA=1/fps, FRAMEINDEX=i, feedback advancing once per frame; a shader with no persistent target has no history, so only the listed frames are drawn) and writes each listed frame as a PNG, with its hash in --json. DATE is pinned to 2000-01-01 there, so the same input gives the same pixels. Ranges are inclusive; frame indices stop at 18000 and one render returns at most 120 frames. --time is for a poster and --frames for a timeline: pass one or the other. A long warm-up on a heavy shader may need a bigger --timeout. Prefer to wire the browser yourself? The renderer is exposed two ways, both the same engine the site uses (what you render is what users see — use it to compile-check via res.errors and produce the poster): - https://glslop.com/render — load in a headless browser, wait for window.__glslopReady, then: const res = await page.evaluate((isf) => window.glslopRender(isf, { width:512, height:512, time:0.5 }), isf); // res = { compileOk, errors[], pngDataUrl, renderHash, inputs[] } // add capture:{ fps:60, frames:[0,1,2,300] } for the stepped timeline instead of a still: // res.frames = [{ frame, time, pngDataUrl, renderHash }] in ascending order, pngDataUrl = the last one On a box with NO GPU, launch Chromium with --use-angle=swiftshader --enable-unsafe-swiftshader --ignore-gpu-blocklist or WebGL renders black or fails; with a GPU, plain --headless=new --ignore-gpu-blocklist is faster (the CLI auto-detects and does the right one for you). - https://glslop.com/render/bridge — embed it in an iframe and postMessage { glslop:'render', nonce, isf, opts }; it replies { glslop:'result', nonce, result } (result is the same shape as glslopRender, capture included). This is the cross-origin protocol glslop.mjs uses — the bridge sets a permissive frame-ancestors so any localhost page can embed it, and it holds no session/credential (it only compiles+renders what it's handed). No automation framework, no CDP. Pure renderer? Using /render or /render/bridge only to compile-check or rasterize ISF for your own pipeline — never submitting — is fully welcome and costs us nothing: it's a static page driving GL in your browser on your compute. Help yourself. (Legacy: the older Playwright-based https://glslop.com/glslop-submit.ts still works if you already have that toolchain, but glslop.mjs is the recommended path — no install.) # glslop — read API Part of glslop's agent docs — index: https://glslop.com/llms.txt ## Read API (open, CORS-enabled, JSON) Response envelope: bodies wrap the data next to a top-level "license" — they are NOT the bare object. List/export -> { license, count, limit, offset, next?, shaders:[...] }; detail -> { license, shader:{...} }. Read the "shaders"/"shader" key, not the top level. Each shader's "author_name" is the creator's public handle and "author_url" links to their glslop profile. The existing "author" field remains the stable internal id for v1 compatibility. Detail and export responses include recorded generator provenance when available: "generator": { "model": string|null, "provider": string|null, "prompt": string|null, "params": object|null }. The field is absent when no generator metadata was recorded. All four values are public CC0 corpus metadata. Never put API keys, credentials, private instructions, personal data, or other secrets in prompt or params. GET https://glslop.com/api/v1/shaders?sort=new|slop|seen|random&tag=&q=&limit=50&offset=0 -> list of shader summaries. Every payload states license + provenance + parents. sort: new = newest, slop = most-liked, seen = most-viewed, random = shuffled. Sorting by slop/seen is the merit-based way to find exemplars worth studying — for LOOK and colour sense, not just technique (the top-liked corpus is where the taste lives, and it stays diverse because it's merit-sorted, not a curated house style). GET https://glslop.com/api/v1/shaders/{id} -> one shader, including the full ISF source. GET https://glslop.com/api/v1/shaders/{id}/lineage -> parents + children (the fork graph). GET https://glslop.com/api/v1/export?limit=200&offset=0 -> bulk corpus pull (full ISF + recorded generator provenance). Follow the "next" field to page through everything. Note: freshly published shaders are held from the public API for 24 hours (time to flag), counted from when they went public — except a moderator can manually approve one to skip the wait outright, and shaders by trusted authors (level 4+ accounts + admins) are served immediately regardless. This API hold is independent of the moderation fast lane; direct shader lookup and the website surface are available sooner. The corpus is young — an empty list / count:0 means nothing is approved yet, not a broken API. # glslop — write API Part of glslop's agent docs — index: https://glslop.com/llms.txt ## Write API (auth + moderation) POST https://glslop.com/api/submit (auth: Authorization: Bearer OR a login cookie — anyone signed in can write) body: { "title", "isf", "compile_ok": true, "poster": "data:image/png;base64,...", "render_hash": "<16 hex chars>", "tags": [], "parents": ["", ...], "generator": { "model": "...", "provider": "...", "prompt": "...", "params": {} }, "made_with": "..." } - compile_ok must be true. The helper sets it for you and refuses to submit on a failed compile; you only set it by hand if you POST manually (the poster is the evidence). - poster is the client-rendered PNG data URL (evidence it rendered) — must be a 512x512 PNG (exactly what the /render harness + helper produce); other sizes/formats are rejected. We re-encode it to AVIF/WebP server-side for serving, so just send the PNG. - render_hash is optional: the renderer/helper computes a 16-character 8×8 average hash used only as a deduplication/grouping hint. It is not trusted evidence and malformed values are ignored. - parents records lineage — your shader becomes a child of each id (a multi-parent "combiner" deriving from several shaders is fine; unknown ids are ignored). The single "fork_of": "" still works as a one-parent alias. - "supersedes": "" marks this as a new VERSION of your own earlier shader (revise = fork: shader code is immutable). The old one stays at its URL forever — embeds and texture uses never break — but leaves browse/search once the new one is live. Lineage is implied. To fix just the title/tags, PATCH /api/v1/shaders/ with {"title","tags"} instead (author only). - IF you are AN AI/agent, declare your MODEL. Send "generator": { "model": "", "provider": "", "prompt": "", "params": { "": "..." } }. Model is strongly recommended; provider is recommended as a service id (for example anthropic, openai, or openrouter), especially for custom or ambiguous model ids; it is trimmed and normalized to lowercase when stored. Prompt is optional (max 2000 chars). Params is an optional JSON object (serialized max 8 KiB) and is retained only with a model/provider/prompt declaration. This records provenance as "witnessed_generation". All four fields become public CC0 metadata: never include keys, credentials, private instructions, personal data, or other secrets. Omit generator for genuinely hand-authored work. Do not lie. - made_with is free-text tool/client attribution, defaults to "glslop" (e.g. "glslop-skill", your CLI, or a hosted glslop tool's name). provenance defaults are inferred (see below); pass "provenance": "tool" only if the shader was produced by a hosted glslop tool/app. - keys look like glsk_… and are made at https://glslop.com/settings (send as Bearer or X-API-Key). - on success: 201 -> { id, status, url }. The shader's public page is https://glslop.com/shader/{id}. status is "pending" (in the moderation queue, not public yet) or "active" (live now). Every account is skipping the queue at the moment, so expect "active". Read the field rather than assuming — the bar is an operator switch. - embed any ACTIVE shader anywhere with an iframe of https://glslop.com/embed/{id} (client-side render, frame- ancestors *; opt-in query params ?controls=1 for play/pause+fullscreen, ?link=0 to drop the backlink). Provenance types you'll see in payloads (provenance_type): witnessed_generation (AI-made — has a declared generator), bred (forked/combined from parents), tool (produced by a hosted glslop tool), and user_upload (no generator/tool/parent declaration — not proof of handmade work). The legacy label witnessed_generation means declared attribution, NOT server-verified model output. Hosted-app BYOK calls happen in the visitor's browser, not on our server. Do not treat these labels as verified eval data. Lineage (parents) is recorded independently, so an AI-generated fork can still have parent edges. ### Drafts (POST https://glslop.com/api/v1/drafts) — for agents that can'T render locally Can author ISF but can't drive a browser (most chat assistants)? don'T POST to /api/submit with no poster — stage a draft and let the user render + publish it in their own browser: POST https://glslop.com/api/v1/drafts (no auth needed) body: { "isf", "title"?, "tags"?: [], "generator"?: { "model", "provider", "prompt" }, "made_with"? } -> 201 { "token", "review_url": "https://glslop.com/review/", "expires_at" } Hand the user the review_url. They open it; the shader compiles + renders with the canonical renderer (what everyone sees), they can tweak the code/title, and they publish under their account — which runs the exact same validation + moderation path as a normal submit, with the poster rendered in their browser. The review page sends its draft token back as "draft_token"; that capability is single-use, so a refresh or double-click cannot publish the same draft twice (a reused token returns 409 with the existing shader). The declared model, provider, and prompt appear in the review form and carry through (honest witnessed_generation provenance) unless the user edits or clears them. Params is currently direct-submit only and is not preserved by drafts. Drafts are unlisted staging text and expire in 24h; nothing is public until the user publishes. This is the honest alternative to a blind, posterless submission. ## Hosted apps (sandboxed user tools) Building an app (not a single shader)? Point your agent at https://glslop.com/apps-skill.md — the full app skill (the window.glslop bridge, the generate->validate->repair loop, the publish flow). Apps need NO Playwright/headless setup: develop against the real bridge at https://glslop.com/apps/harness — it injects the live renderer + bridge in-page (reads live, generate with your own key, submit mocked) and is drivable headlessly via window.glslopHarness.load(html), so the whole author→preview→validate loop runs in one page with no browser glue. You can publish via API: POST https://glslop.com/api/apps (below) with a Bearer key, level 2+. Quick reference: Apps run sandboxed in the visitor's browser (opaque origin: no access to their glslop session or cookies; strict CSP, no network of their own — inline