experimental backend WebGL2 · not opted in
Enable WebGPU controls in settings. WebGL2 remains unchanged. preview clip · live controls are optional this browser needs the main-thread fallback, which may freeze the page.
0.50
1.00
1.33
10.00
0.50
1.45
0.05
0.30
0.00
1.00
rgb
rgb
rgb
rgb
rgb
/*{ "ISFVSN": "2", "DESCRIPTION": "Raymarched glass metaballs drifting through a 5-stop vertical colour ramp. Exported from FXCanvas (source 'Blobs', preset 'Drifting Orbs').", "CREDIT": "SpotLab / FXCanvas", "CATEGORIES": ["Generator", "3D", "Geometry"], "INPUTS": [ { "NAME": "speed", "LABEL": "Speed", "TYPE": "float", "MIN": 0.0, "MAX": 2.0, "DEFAULT": 0.5 }, { "NAME": "zoom", "LABEL": "Zoom", "TYPE": "float", "MIN": 0.5, "MAX": 4.0, "DEFAULT": 1.0 }, { "NAME": "refraction", "LABEL": "Refraction", "TYPE": "float", "MIN": 1.0, "MAX": 2.0, "DEFAULT": 1.33 }, { "NAME": "blobCount", "LABEL": "Blob Count", "TYPE": "float", "MIN": 1.0, "MAX": 10.0, "DEFAULT": 10.0 }, { "NAME": "blobSize", "LABEL": "Blob Size", "TYPE": "float", "MIN": 0.5, "MAX": 2.0, "DEFAULT": 0.5 }, { "NAME": "spread", "LABEL": "Spread", "TYPE": "float", "MIN": 0.3, "MAX": 2.0, "DEFAULT": 1.45 }, { "NAME": "absorption", "LABEL": "Absorption", "TYPE": "float", "MIN": 0.0, "MAX": 0.2, "DEFAULT": 0.05 }, { "NAME": "rotateX", "LABEL": "Rotate X", "TYPE": "float", "MIN": -1.0, "MAX": 1.0, "DEFAULT": 0.3 }, { "NAME": "rotateY", "LABEL": "Rotate Y", "TYPE": "float", "MIN": -1.0, "MAX": 1.0, "DEFAULT": 0.0 }, { "NAME": "opacity", "LABEL": "Opacity", "TYPE": "float", "MIN": 0.0, "MAX": 1.0, "DEFAULT": 1.0 }, { "NAME": "color1", "LABEL": "Colour 1 (bottom)", "TYPE": "color", "DEFAULT": [1.0, 0.0, 0.0, 1.0] }, { "NAME": "color2", "LABEL": "Colour 2", "TYPE": "color", "DEFAULT": [1.0, 0.75, 0.0, 1.0] }, { "NAME": "color3", "LABEL": "Colour 3", "TYPE": "color", "DEFAULT": [0.0, 1.0, 0.25, 1.0] }, { "NAME": "color4", "LABEL": "Colour 4", "TYPE": "color", "DEFAULT": [0.0, 0.85, 1.0, 1.0] }, { "NAME": "color5", "LABEL": "Colour 5 (top)", "TYPE": "color", "DEFAULT": [0.35, 0.0, 1.0, 1.0] } ] }*/ // Drifting Orbs - ISF port of the FXCanvas "Blobs" source. // // Differences from the in-app shader, all forced by the ISF host contract: // * The app feeds `accumulatedOffset`, a CPU-side integral of speed over time. // With a constant speed that is just speed * time, so this uses TIME * speed. // * The app's palette arrives as a uniform vec3[20] plus a transition pair. // ISF has no array inputs, so the palette is five explicit colour stops // sampled with the same BOUNDED mapping the source uses: the ends stay // distinct, because the scalar is world-space height and does not wrap. // * vTexCoord -> isf_FragNormCoord, resolution -> RENDERSIZE, out -> gl_FragColor. const float PI = 3.1415926536; const float EYE_DISTANCE = 7.5; const float MIN_DISTANCE = 2.0; const float MAX_DISTANCE = 15.0; const float MIN_STEPSIZE = 0.2; const int MAX_STEPS = 40; // Interior march (refraction / absorption). Fixed step: the adaptive stepsize() // below is tuned for approaching the surface from outside. The step has to stay // smaller than the thin gaps between neighbouring lobes - stepping over one // misses the exit, jumps the path length to a much later crossing, and shows up // as a hard-edged band in both the lensing and the absorption. const int MAX_INTERNAL_STEPS = 40; const float INTERNAL_STEPSIZE = 0.12; const float ABSORPTION_SCALE = 3.0; // maps the 0..0.2 slider onto useful optical depth const float FRESNEL_GAIN = 0.5; // strength of the grazing-angle sheen const float FRESNEL_DEPTH_FALLOFF = 1.2; // how fast the sheen dies off into thick material // Blob positions and sizes (xyz = position, w = size) vec4 blobs[10]; int activeBlobs; float sq(float x) { return x * x; } float sq(vec3 x) { return dot(x, x); } // Bounded 5-stop ramp. Stand-in for fxcPaletteBounded() in the app's injected // preamble: bottom of the field is colour 1, top is colour 5, no wrap. vec3 fxcPaletteBounded(float t) { float s = clamp(t, 0.0, 1.0) * 4.0; if (s < 1.0) return mix(color1.rgb, color2.rgb, s); if (s < 2.0) return mix(color2.rgb, color3.rgb, s - 1.0); if (s < 3.0) return mix(color3.rgb, color4.rgb, s - 2.0); return mix(color4.rgb, color5.rgb, clamp(s - 3.0, 0.0, 1.0)); } // Metaball field function float f(vec3 p) { float sum = 0.0; for (int i = 0; i < 10; i++) { if (i >= activeBlobs) break; sum += blobs[i].w / sq(blobs[i].xyz - p); } return 1.0 - sum; } // Gradient of field (for normals) vec3 fd(vec3 p) { vec3 grad = vec3(0.0); for (int i = 0; i < 10; i++) { if (i >= activeBlobs) break; vec3 d = blobs[i].xyz - p; float sqd = sq(d); grad += blobs[i].w * d / sq(sqd); } return 2.0 * grad; } // Adaptive step size float stepsize(vec3 p) { float minDist = 1000.0; for (int i = 0; i < 10; i++) { if (i >= activeBlobs) break; minDist = min(minDist, sq(p - blobs[i].xyz)); } float md = sqrt(minDist); return max(MIN_STEPSIZE, abs(md - 1.0) * 0.667); } // Palette colour for a point in the field (vertical gradient). vec3 blobColor(vec3 p) { return fxcPaletteBounded((p.y + 3.0) / 6.0); } // Darker core, brighter rim vec3 shade(vec3 color, float facing, float rim) { return color * (0.6 + 0.4 * facing + 0.3 * rim * rim); } // Fresnel reflectance (Schlick) for a dielectric whose normal-incidence // reflectance is f0 float fresnelSchlick(float cosTheta, float f0) { return f0 + (1.0 - f0) * pow(1.0 - clamp(cosTheta, 0.0, 1.0), 5.0); } // March inside the metaball from p along dir until the field turns positive // (we've left the body). Returns the distance travelled and writes the refined // exit point to exitPos. If the ray never leaves within the step budget the path // length saturates, which is the right answer for absorption anyway. float marchInside(vec3 p, vec3 dir, out vec3 exitPos) { float k = INTERNAL_STEPSIZE * 0.5; // start just inside the surface for (int i = 0; i < MAX_INTERNAL_STEPS; ++i) { float kNext = k + INTERNAL_STEPSIZE; if (f(p + dir * kNext) >= 0.0) { // Crossed back out between k and kNext - refine float kLo = k; float kHi = kNext; for (int b = 0; b < 6; b++) { float kMid = (kLo + kHi) * 0.5; if (f(p + dir * kMid) >= 0.0) { kHi = kMid; } else { kLo = kMid; } } exitPos = p + dir * kHi; return kHi; } k = kNext; } exitPos = p + dir * k; return k; } // Raymarch: find the surface and colour it vec4 ray(vec3 p, vec3 d) { float k = MIN_DISTANCE; for (int i = 0; i < MAX_STEPS; ++i) { if (k > MAX_DISTANCE) { return vec4(0.0); // transparent background } vec3 pos = p + d * k; float fVal = f(pos); if (fVal < 0.0) { // Refine the hit float kLo = max(k - 0.5, 0.0); float kHi = k; for (int b = 0; b < 8; b++) { float kMid = (kLo + kHi) * 0.5; if (f(p + d * kMid) < 0.0) { kHi = kMid; } else { kLo = kMid; } } pos = p + d * kHi; vec3 grad = fd(pos); if (length(grad) < 0.01) { k += 0.1; continue; } vec3 n = -normalize(grad); vec3 baseColor = blobColor(pos); float facing = max(0.0, dot(n, -d)); float rim = 1.0 - facing; vec3 color = shade(baseColor, facing, rim); // Glass: bend the view ray through the body and attenuate it over // the interior path. Both tests are uniform-driven, so the whole // draw takes the same branch. At refraction 1.0 with absorption 0.0 // this is skipped entirely and the render is the plain shaded // surface above. float glass = clamp(refraction - 1.0, 0.0, 1.0); if (glass > 0.0 || absorption > 0.0) { // Entering the denser medium, so eta <= 1 and refract() can't // total-internal-reflect. Guard anyway; 0 means fall straight // through, which is also what eta == 1 gives us. vec3 rd = refract(d, n, 1.0 / max(refraction, 1.0)); if (dot(rd, rd) < 0.5) rd = d; vec3 exitPos; float pathLen = marchInside(pos, rd, exitPos); // Beer-Lambert. One knob absorbs the complement of the palette // colour, so thick regions deepen in hue rather than going grey. vec3 sigma = absorption * ABSORPTION_SCALE * (1.0 - baseColor); vec3 transmittance = exp(-sigma * pathLen); // Integrate the palette along the interior path rather than // reading only the far face. Where the bent ray clips a // neighbouring lobe tangentially, which lobe it exits through // flips abruptly, and a lone exit sample turns that into a // hard-edged band. How far in we look scales with the IOR, so a // low IOR stays a subtle warp and only real glass sees all the // way through. vec3 lensPos = mix(pos, exitPos, glass); vec3 lensed = (blobColor(mix(pos, lensPos, 0.34)) + blobColor(mix(pos, lensPos, 0.67)) + blobColor(lensPos)) / 3.0; vec3 interior = shade(lensed, facing, rim); color = mix(color, interior, glass) * transmittance; // Grazing-angle sheen, strength set by the index of refraction. // Schlick only knows the view angle, and the creases where two // lobes merge graze just as hard as a real silhouette - left // unchecked the sheen fills in exactly the shadowed seams that // let you tell touching blobs apart. A true silhouette is thin // glass and a crease has deep material behind it, so fade the // sheen out with the interior path length already measured. float f0 = sq((refraction - 1.0) / (refraction + 1.0)); float thinEdge = exp(-pathLen * FRESNEL_DEPTH_FALLOFF); color += color * fresnelSchlick(facing, f0) * glass * FRESNEL_GAIN * thinEdge; } // Subtle highlight vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3)); float spec = pow(max(0.0, dot(reflect(d, n), lightDir)), 16.0); color += vec3(0.2) * spec; return vec4(color, 1.0); // fully opaque where we hit a blob } k += stepsize(pos); } return vec4(0.0); // transparent background } void main() { float t = TIME * speed; activeBlobs = int(clamp(floor(blobCount + 0.5), 1.0, 10.0)); // Animate blob positions vec4 vs1 = cos(t * vec4(0.87, 1.13, 1.2, 1.0) + vec4(0.0, 3.32, 0.97, 2.85)) * vec4(-1.7, 2.1, 2.37, -1.9) * spread; vec4 vs2 = cos(t * vec4(1.07, 0.93, 1.1, 0.81) + vec4(0.3, 3.02, 1.15, 2.97)) * vec4(1.77, -1.81, 1.47, 1.9) * spread; vec4 vs3 = cos(t * vec4(0.97, 1.03, 0.89, 1.17) + vec4(1.2, 2.1, 0.5, 3.7)) * vec4(1.5, 1.9, -1.8, 1.6) * spread; vec4 vs4 = cos(t * vec4(0.79, 1.21, 0.95, 1.05) + vec4(2.1, 0.8, 1.9, 0.3)) * vec4(-1.6, 1.7, 2.0, -1.5) * spread; vec4 vs5 = cos(t * vec4(1.15, 0.85, 1.08, 0.92) + vec4(0.7, 2.5, 1.3, 3.1)) * vec4(1.8, -2.0, 1.4, 1.9) * spread; blobs[0] = vec4(vs1.x, 0.0, vs1.y, 1.0 * blobSize); blobs[1] = vec4(vs1.z, vs1.w, vs2.z, 0.9 * blobSize); blobs[2] = vec4(vs2.x, vs2.y, vs2.w, 0.8 * blobSize); blobs[3] = vec4(vs3.x, vs3.y, vs3.z, 0.85 * blobSize); blobs[4] = vec4(vs3.w, vs1.x * 0.5, vs2.x * 0.5, 0.75 * blobSize); blobs[5] = vec4(vs4.x, vs4.y, vs4.z, 0.82 * blobSize); blobs[6] = vec4(vs4.w, vs3.x * 0.6, vs1.z * 0.7, 0.78 * blobSize); blobs[7] = vec4(vs5.x, vs5.y, vs5.z, 0.88 * blobSize); blobs[8] = vec4(vs5.w, vs4.x * 0.5, vs2.y * 0.6, 0.72 * blobSize); blobs[9] = vec4(vs1.y * 0.8, vs5.x * 0.7, vs3.w * 0.9, 0.7 * blobSize); // Camera rotation vec2 r = vec2(-rotateX, rotateY) * PI; vec4 cs = cos(vec4(r.y, r.x, r.y - PI * 0.5, r.x - PI * 0.5)); vec3 forward = -vec3(cs.x * cs.y, cs.w, cs.z * cs.y); vec3 up = vec3(cs.x * cs.w, -cs.y, cs.z * cs.w); vec3 left = cross(up, forward); vec3 eye = -forward * EYE_DISTANCE; vec2 uv = (isf_FragNormCoord - 0.5) / zoom; uv.x *= RENDERSIZE.x / RENDERSIZE.y; vec3 dir = normalize(forward + uv.y * up + uv.x * left); // Opacity is the last operation and belongs in ALPHA ONLY. Scaling rgb here // as well premultiplies, and the downstream composite applies alpha again. vec4 lit = ray(eye, dir); gl_FragColor = vec4(lit.rgb, lit.a * clamp(opacity, 0.0, 1.0)); }
Raymarched glass metaballs drifting through a 5-stop vertical colour ramp. Exported from FXCanvas (source 'Blobs', preset 'Drifting Orbs').
by fxcanvaswitnessed_generationmade with FXCanvasCC0-1.0credit: SpotLab / FXCanvasactive·created 2026-07-31 17:27Z147 views0 forks15 paramsclaude-opus-5
export clip · render a video in your browser
Renders to video in your browser (no upload), stepping the real renderer frame by frame so multipass + feedback come out right. motion auto-animates the shader's own knobs — each export rolls a fresh take (🎲 re-roll, 🔒 to pin one you like). GIF is slower with a tighter length cap; MP4/WebM are faster. Some devices record in real time (noted below if so) — keep the tab visible.
caps: ≤1920 per side, ≤60fps, ≤18000 frames (GIF ≤300)
embed · drop this shader into any page
Renders client-side in the reader's browser (CC0 — no attribution required, but the backlink is appreciated).