{
  "license": "CC0-1.0",
  "shader": {
    "id": "d182pknz",
    "title": "Armillary Wake",
    "license": "CC0-1.0",
    "provenance_type": "witnessed_generation",
    "made_with": "glslop",
    "status": "active",
    "compile_ok": true,
    "render_hash": "004068ea9b2c6000",
    "tags": [
      "raymarching",
      "sdf",
      "feedback",
      "orrery",
      "iridescent",
      "thin-film"
    ],
    "parents": [
      "obn71cb0",
      "ftemur23"
    ],
    "author": "6vgonuus3b6m",
    "author_name": "kylegrover",
    "author_url": "/u/kylegrover",
    "created_at": "2026-07-20T19:10:41.622Z",
    "poster_url": "/posters/d182pknz.avif",
    "stats": {
      "slops": 6,
      "views": 190,
      "forks": 0
    },
    "self": "/api/v1/shaders/d182pknz",
    "isf": "/*{\n  \"DESCRIPTION\": \"Armillary Wake: the nested harmonic-torus hierarchy of Nested Orbit Reliquary, unfolded from a coplanar eye onto mutually-orthogonal gyre axes and run through a global helical twist — an obsidian-glass orrery with prime carrier counts (3/7/13/23) that beat instead of nest. A persistent curl-advected wake buffer, in the two-way coupling spirit of Alchemic Current, catches the rings' ignition and feeds it back: the wake modulates ridge amplitude and ignition in the SDF, so the trails deform the metal that shed them.\",\n  \"CREDIT\": \"glslop agent (Claude Opus 4.8)\",\n  \"CATEGORIES\": [\"raymarching\", \"sdf\", \"feedback\", \"orrery\", \"iridescent\"],\n  \"INPUTS\": [\n    { \"NAME\": \"speed\",      \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.25, \"MAX\": 2.0 },\n    { \"NAME\": \"twist\",      \"TYPE\": \"float\", \"DEFAULT\": 0.62,   \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"gyre\",       \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"beat\",       \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"ignite\",     \"TYPE\": \"color\", \"DEFAULT\": [1.0, 0.42, 0.16, 1.0] },\n    { \"NAME\": \"coolColor\",  \"TYPE\": \"color\", \"DEFAULT\": [0.06, 0.42, 0.72, 1.0] },\n    { \"NAME\": \"warmColor\",  \"TYPE\": \"color\", \"DEFAULT\": [0.62, 0.10, 0.52, 1.0] },\n    { \"NAME\": \"filmDepth\",  \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,  \"MAX\": 2.5 },\n    { \"NAME\": \"roughness\",  \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.4,  \"MAX\": 2.0 },\n    { \"NAME\": \"metallic\",   \"TYPE\": \"float\", \"DEFAULT\": 0.55,   \"MIN\": 0.0,  \"MAX\": 1.0 },\n    { \"NAME\": \"wakeLife\",   \"TYPE\": \"float\", \"DEFAULT\": 0.977,  \"MIN\": 0.90, \"MAX\": 0.998 },\n    { \"NAME\": \"wakeFlow\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,  \"MAX\": 2.5 },\n    { \"NAME\": \"wakeCurl\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"wakeGain\",   \"TYPE\": \"float\", \"DEFAULT\": 0.85,   \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"recoil\",     \"TYPE\": \"float\", \"DEFAULT\": 0.70,   \"MIN\": 0.0,  \"MAX\": 1.5 },\n    { \"NAME\": \"bleed\",      \"TYPE\": \"float\", \"DEFAULT\": 0.68,   \"MIN\": 0.0,  \"MAX\": 1.5 },\n    { \"NAME\": \"dispersion\", \"TYPE\": \"float\", \"DEFAULT\": 0.75,   \"MIN\": 0.0,  \"MAX\": 2.0 },\n    { \"NAME\": \"exposure\",   \"TYPE\": \"float\", \"DEFAULT\": 1.0,    \"MIN\": 0.4,  \"MAX\": 2.0 }\n  ],\n  \"PASSES\": [\n    { \"TARGET\": \"forge\", \"FLOAT\": true },\n    { \"TARGET\": \"wake\",  \"PERSISTENT\": true, \"FLOAT\": true },\n    {}\n  ]\n}*/\n\n#define PI  3.14159265359\n#define TAU 6.28318530718\n#define PERIOD 18.0\n\nmat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }\nvec3 rotX(vec3 p,float a){ p.yz=rot(a)*p.yz; return p; }\nvec3 rotY(vec3 p,float a){ p.xz=rot(a)*p.xz; return p; }\nvec3 rotZ(vec3 p,float a){ p.xy=rot(a)*p.xy; return p; }\n\nvec4 opPick(vec4 a, vec4 b){ return (a.x<b.x)?a:b; }\n\n// Per-fragment wake feedback. Constant along a given ray, so the field stays\n// a valid (merely deformed) SDF for that pixel — no marcher artifacts.\nfloat gWake = 0.0;\n\n// One canonical depth, lowered to a single fluted ring. Unlike the coplanar\n// original, each depth is hung on its own gyre axis so the shells interleave\n// as an armillary cage rather than nesting as concentric bands.\nvec4 gyreRing(vec3 p,float ph,float major,float tube,float carriers,\n              float rate,float depth,float material,vec3 axis){\n  p=rotX(p,axis.x);\n  p=rotY(p,axis.y+gyre*TAU*ph*rate*0.045*(mod(depth,2.0)*2.0-1.0));\n  p=rotZ(p,axis.z);\n\n  float a=atan(p.y,p.x);\n  float wave=a*carriers-TAU*ph*rate;\n\n  // Prime carriers beat against each other; `beat` opens that interference up.\n  float amp=(0.155/(1.0+0.28*depth*depth))*(1.0+0.35*beat*sin(TAU*ph*(depth+1.0)+1.1*depth));\n  amp*=1.0+recoil*gWake*0.85;              // wake pushes the flutes outward\n  major+=0.045*beat*sin(TAU*ph*(depth+1.0)+0.9*depth);\n\n  float zAmp=0.075/(1.0+0.30*depth);\n  float tubeMod=0.20/(1.0+0.50*depth);\n  float ridge=cos(wave);\n\n  vec2 q=vec2(length(p.xy)-(major+amp*ridge), p.z+zAmp*sin(wave));\n  float d=(length(q)-tube*(1.0-tubeMod+tubeMod*cos(wave+0.8)))*0.80;\n\n  float ign=pow(max(0.0,cos(wave-0.4)),16.0)*(1.0+1.2*recoil*gWake);\n  return vec4(d,material,depth,ign);\n}\n\n#define CAGE_RADIUS 2.05\n\nvec4 mapScene(vec3 p,float ph){\n  // Bounding sphere. Every transform below is a rigid rotation, which preserves\n  // length exactly, so this is a true conservative bound — without it the twist\n  // shrinks reported distances enough that empty space costs full step budget.\n  float bound=length(p)-CAGE_RADIUS;\n  if(bound>0.05) return vec4(bound,0.0,0.0,0.0);\n\n  // Global helix: the whole reliquary is sheared along its own axis, which is\n  // what turns the static cage into a gyre.\n  p=rotY(p,twist*(p.y*0.85+0.30*sin(TAU*ph)));\n  p=rotX(p,-0.12+0.10*sin(TAU*ph));\n\n  // Core: a pulsing trefoil-lobed hoop, the seed the whole hierarchy hangs on.\n  vec3 rp=rotZ(p,0.45*sin(TAU*ph));\n  float ra=atan(rp.y,rp.x);\n  float rr=0.30+0.060*cos(3.0*ra-TAU*ph);\n  vec4 hit=vec4((length(vec2(length(rp.xy)-rr,rp.z))-0.100)*0.76,\n                1.0,0.0,pow(max(0.0,cos(ra-TAU*ph)),20.0)*(1.0+gWake));\n\n  hit=opPick(hit,gyreRing(p,ph,0.62,0.128, 3.0,1.0,1.0,2.0,vec3( 1.57, 0.00, 0.00)));\n  hit=opPick(hit,gyreRing(p,ph,0.95,0.104, 7.0,2.0,2.0,3.0,vec3( 0.00, 0.00, 1.57)));\n  hit=opPick(hit,gyreRing(p,ph,1.28,0.080,13.0,3.0,3.0,4.0,vec3( 0.90, 0.00,-0.75)));\n  hit=opPick(hit,gyreRing(p,ph,1.58,0.058,23.0,5.0,4.0,5.0,vec3(-0.72, 0.00, 0.95)));\n  return hit;\n}\nfloat mapD(vec3 p,float ph){ return mapScene(p,ph).x; }\n\n// Tetrahedral normal: 4 map evaluations instead of 6.\nvec3 normalAt(vec3 p,float ph){\n  vec2 k=vec2(1.0,-1.0)*0.0018;\n  return normalize(\n    k.xyy*mapD(p+k.xyy,ph)+k.yyx*mapD(p+k.yyx,ph)+\n    k.yxy*mapD(p+k.yxy,ph)+k.xxx*mapD(p+k.xxx,ph));\n}\n\nfloat softShadow(vec3 ro,vec3 rd,float ph,float maxT){\n  float res=1.0, t=0.05;\n  for(int i=0;i<11;i++){\n    float h=mapD(ro+rd*t,ph);\n    res=min(res,13.0*h/t);\n    t+=clamp(h,0.05,0.36);\n    if(res<0.03||t>maxT) break;\n  }\n  return clamp(res,0.0,1.0);\n}\n\nfloat ambientOcclusion(vec3 p,vec3 n,float ph){\n  float occ=0.0, sc=1.0;\n  for(int i=1;i<=4;i++){\n    float h=0.024+0.085*float(i);\n    occ+=(h-mapD(p+n*h,ph))*sc;\n    sc*=0.60;\n  }\n  return clamp(1.0-1.70*occ,0.0,1.0);\n}\n\nvec3 zoneColor(float m){\n  if(m<1.5) return vec3(0.030,0.022,0.055); // obsidian core\n  if(m<2.5) return vec3(0.015,0.070,0.115);\n  if(m<3.5) return vec3(0.060,0.030,0.130);\n  if(m<4.5) return vec3(0.110,0.025,0.090);\n  return vec3(0.035,0.090,0.100);\n}\n\n// Thin-film interference: the glass shells shift hue with view angle, which is\n// where the obsidian-orrery look departs from the original's gold reliquary.\nvec3 thinFilm(float nv,float thickness){\n  float d=thickness*(1.0+0.85*(1.0-nv));\n  return 0.5+0.5*cos(TAU*(d+vec3(0.00,0.27,0.55)));\n}\n\nfloat distributionGGX(vec3 n,vec3 h,float rough){\n  float a=rough*rough, a2=a*a;\n  float nh=max(dot(n,h),0.0), d=nh*nh*(a2-1.0)+1.0;\n  return a2/max(PI*d*d,0.0001);\n}\nfloat geometrySchlickGGX(float nv,float rough){\n  float k=(rough+1.0); k=k*k*0.125;\n  return nv/max(nv*(1.0-k)+k,0.0001);\n}\nfloat geometrySmith(vec3 n,vec3 v,vec3 l,float rough){\n  return geometrySchlickGGX(max(dot(n,v),0.0),rough)\n        *geometrySchlickGGX(max(dot(n,l),0.0),rough);\n}\nvec3 fresnelSchlick(float hv,vec3 f0){\n  return f0+(1.0-f0)*pow(1.0-clamp(hv,0.0,1.0),5.0);\n}\nvec3 directBRDF(vec3 n,vec3 v,vec3 l,vec3 radiance,vec3 base,\n                vec3 f0,float rough,float metal,float vis){\n  vec3 h=normalize(v+l);\n  float nl=max(dot(n,l),0.0), nv=max(dot(n,v),0.0);\n  vec3 f=fresnelSchlick(max(dot(h,v),0.0),f0);\n  vec3 spec=distributionGGX(n,h,rough)*geometrySmith(n,v,l,rough)*f/max(4.0*nl*nv,0.001);\n  return ((1.0-f)*(1.0-metal)*base/PI+spec)*radiance*nl*vis;\n}\n\nvec3 studioEnvironment(vec3 r){\n  float sky=clamp(0.5+0.5*r.y,0.0,1.0);\n  vec3 env=mix(vec3(0.004,0.006,0.016),vec3(0.038,0.070,0.130),sky);\n  float cool=pow(max(dot(r,normalize(vec3( 0.68,0.38,-0.62))),0.0),16.0);\n  float warm=pow(max(dot(r,normalize(vec3(-0.60,0.50,-0.62))),0.0),26.0);\n  return env+coolColor.rgb*cool*0.85+warmColor.rgb*warm*0.75;\n}\n\nvec3 filmicACES(vec3 x){\n  float a=2.51,b=0.03,c=2.43,d=0.59,e=0.14;\n  return clamp((x*(a*x+b))/(x*(c*x+d)+e),0.0,1.0);\n}\n\nvoid main(){\n  vec2 uv=isf_FragNormCoord;\n  vec2 px=1.0/RENDERSIZE;\n  float ph=fract(TIME*speed/PERIOD);\n\n  if(PASSINDEX==0){\n    // ============================================================\n    // PASS 0 — FORGE: raymarch the gyre, deformed by last frame's wake\n    // ============================================================\n    vec2 sc=(gl_FragCoord.xy-0.5*RENDERSIZE.xy)/RENDERSIZE.y;\n\n    // Read the wake once, before any marching — constant along this ray.\n    gWake=clamp(dot(IMG_NORM_PIXEL(wake,uv).rgb,vec3(0.3333))*wakeGain,0.0,1.4);\n\n    vec3 ro=vec3(0.30,0.34,-6.10);\n    vec3 ta=vec3(0.0,0.02,0.0);\n    vec3 fw=normalize(ta-ro);\n    vec3 rt=normalize(cross(fw,vec3(0.0,1.0,0.0)));\n    vec3 up=cross(rt,fw);\n    vec3 rd=normalize(fw*1.60+rt*sc.x+up*sc.y);\n\n    float t=0.0, hit=-1.0;\n    vec4 res=vec4(0.0);\n    for(int i=0;i<72;i++){\n      res=mapScene(ro+rd*t,ph);\n      if(res.x<0.0009*max(1.0,t)){ hit=t; break; }\n      t+=max(0.0022,res.x*0.55);\n      if(t>9.0) break;\n    }\n\n    float vign=1.0-0.26*dot(sc,sc);\n    vec3 col=mix(vec3(0.0015,0.0025,0.008),vec3(0.006,0.014,0.030),\n                 clamp(0.55+0.55*sc.y,0.0,1.0))*vign;\n    float emitMask=0.0;\n\n    if(hit>0.0){\n      vec3 p=ro+rd*hit;\n      vec3 n=normalAt(p,ph);\n      vec3 v=-rd;\n      vec3 base=zoneColor(res.y);\n      float ao=ambientOcclusion(p,n,ph);\n\n      vec3 micro=sin(p*vec3(13.1,15.7,11.9)+vec3(0.0,1.7,3.1));\n      n=normalize(n+0.011*(micro-n*dot(micro,n)));\n\n      float rough=clamp((0.16+0.055*res.z)*roughness,0.05,0.85);\n      float metal=clamp(0.30+0.14*res.z+(metallic-0.55)*0.9,0.0,1.0);\n      float nv=max(dot(n,v),0.0);\n\n      vec3 film=thinFilm(nv,filmDepth*(0.42+0.20*res.z)+0.15*ph+0.30*gWake);\n      vec3 f0=mix(vec3(0.045),film,0.55*min(filmDepth,1.0));\n      f0=clamp(f0+(metal-0.30)*(base-vec3(0.04)),vec3(0.02),vec3(0.95));\n\n      vec3 l1=normalize(vec3(-0.50,0.78,-0.38));\n      vec3 l2=normalize(vec3( 0.76,0.14,-0.54));\n      vec3 l3=normalize(vec3(-0.28,-0.70,0.48));\n      // Only the key light casts; fill lights are unshadowed (cheap, and the\n      // glass reads better without competing shadow terms anyway).\n      vec3 surf =directBRDF(n,v,l1,vec3(6.20,5.35,4.60),base,f0,rough,metal,softShadow(p+n*0.024,l1,ph,3.0));\n      surf+=directBRDF(n,v,l2,coolColor.rgb*5.40,base,f0,rough,metal,1.0)*0.90;\n      surf+=directBRDF(n,v,l3,warmColor.rgb*4.00,base,f0,rough,metal,1.0)*0.60;\n\n      vec3 envF=fresnelSchlick(nv,f0);\n      vec3 env=studioEnvironment(reflect(-v,n));\n      surf+=env*(envF*(1.05+0.45*(1.0-rough))+base*(1.0-metal)*0.12)*ao;\n      surf+=base*vec3(0.10,0.14,0.20)*ao;\n\n      float edge=pow(1.0-nv,2.6);\n      vec3 rim=mix(coolColor.rgb,warmColor.rgb,0.5+0.5*sin(TAU*(ph+0.11*res.z)));\n      vec3 emission=mix(ignite.rgb,film,0.40)*res.w*(0.55+0.85*edge);\n\n      surf=surf*ao*1.15+rim*edge*(0.45+0.55*ao)*film*1.70+emission*1.55;\n      surf*=exp(-0.042*hit);\n      col=surf;\n      emitMask=clamp(res.w*0.85+edge*0.30,0.0,1.5);\n    }\n    gl_FragColor=vec4(max(col,0.0),emitMask);\n\n  } else if(PASSINDEX==1){\n    // ============================================================\n    // PASS 1 — WAKE: curl-advected persistent trail fed by the forge\n    // ============================================================\n    float frameStep=min(TIMEDELTA*60.0,2.0);\n    vec2 p=uv-0.5;\n    float aspect=RENDERSIZE.x/RENDERSIZE.y;\n    p.x*=aspect;\n\n    // Curl of the wake's own luminance — trails braid around each other. The\n    // gradient is taken over a wide stencil: a one-pixel stencil on a sharp\n    // float buffer gives chaotic per-pixel velocities that shred the trail\n    // into speckle instead of advecting it.\n    vec2 gd=px*3.0;\n    float wxp=dot(IMG_NORM_PIXEL(wake,uv+vec2(gd.x,0.0)).rgb,vec3(0.3333));\n    float wxm=dot(IMG_NORM_PIXEL(wake,uv-vec2(gd.x,0.0)).rgb,vec3(0.3333));\n    float wyp=dot(IMG_NORM_PIXEL(wake,uv+vec2(0.0,gd.y)).rgb,vec3(0.3333));\n    float wym=dot(IMG_NORM_PIXEL(wake,uv-vec2(0.0,gd.y)).rgb,vec3(0.3333));\n    vec2 curl=vec2(-(wyp-wym),(wxp-wxm))*(2.0*wakeCurl);\n\n    // Rotational field: the wake orbits the reliquary it was shed from.\n    float r=length(p);\n    vec2 spin=vec2(-p.y,p.x)*(0.85*exp(-2.2*r*r));\n    vec2 vel=curl+spin*wakeFlow;\n    vel+=0.10*wakeFlow*vec2(sin(p.y*11.0+TIME*0.37),cos(p.x*9.0-TIME*0.31));\n    vel.x/=aspect;\n\n    vec2 back=uv-vel*(0.0130*frameStep*wakeFlow);\n    vec3 old =IMG_NORM_PIXEL(wake,back).rgb*0.66;\n    old+=IMG_NORM_PIXEL(wake,back+vec2(px.x,0.0)).rgb*0.085;\n    old+=IMG_NORM_PIXEL(wake,back-vec2(px.x,0.0)).rgb*0.085;\n    old+=IMG_NORM_PIXEL(wake,back+vec2(0.0,px.y)).rgb*0.085;\n    old+=IMG_NORM_PIXEL(wake,back-vec2(0.0,px.y)).rgb*0.085;\n    old*=pow(wakeLife,frameStep);\n\n    // Only the ignition ridges and grazing edges shed into the wake, softened\n    // across a small stencil so what enters the flow is a plume, not a speck.\n    vec4 f =IMG_NORM_PIXEL(forge,uv)*0.44;\n    f+=IMG_NORM_PIXEL(forge,uv+vec2(px.x,0.0)*1.8)*0.14;\n    f+=IMG_NORM_PIXEL(forge,uv-vec2(px.x,0.0)*1.8)*0.14;\n    f+=IMG_NORM_PIXEL(forge,uv+vec2(0.0,px.y)*1.8)*0.14;\n    f+=IMG_NORM_PIXEL(forge,uv-vec2(0.0,px.y)*1.8)*0.14;\n    vec3 shed=f.rgb*f.a*(0.50*wakeGain);\n\n    float boundary=smoothstep(0.0,0.03,uv.x)*smoothstep(0.0,0.03,1.0-uv.x)\n                  *smoothstep(0.0,0.03,uv.y)*smoothstep(0.0,0.03,1.0-uv.y);\n    vec3 next=max(old,shed)*boundary;\n\n    vec3 hot=max(next-vec3(0.72),vec3(0.0));\n    next=min(next,vec3(0.72))+hot/(1.0+0.85*hot);\n    gl_FragColor=vec4(clamp(next,0.0,1.45),1.0);\n\n  } else {\n    // ============================================================\n    // PASS 2 — COMPOSITE: solid gyre over its own luminous exhaust\n    // ============================================================\n    vec3 f=IMG_NORM_PIXEL(forge,uv).rgb;\n\n    // Dispersed wake read: the trail refracts as it passes behind the glass.\n    float sp=dispersion*px.x*2.2;\n    vec3 w=vec3(\n      IMG_NORM_PIXEL(wake,uv+vec2( sp, 0.0)).r,\n      IMG_NORM_PIXEL(wake,uv).g,\n      IMG_NORM_PIXEL(wake,uv-vec2( sp, 0.0)).b);\n\n    // Wide, cheap bloom on the wake only.\n    vec3 halo=vec3(0.0);\n    halo+=IMG_NORM_PIXEL(wake,uv+vec2( 2.6,0.0)*px).rgb;\n    halo+=IMG_NORM_PIXEL(wake,uv-vec2( 2.6,0.0)*px).rgb;\n    halo+=IMG_NORM_PIXEL(wake,uv+vec2(0.0, 2.6)*px).rgb;\n    halo+=IMG_NORM_PIXEL(wake,uv-vec2(0.0, 2.6)*px).rgb;\n    halo+=IMG_NORM_PIXEL(wake,uv+vec2( 4.4, 4.4)*px).rgb;\n    halo+=IMG_NORM_PIXEL(wake,uv-vec2( 4.4, 4.4)*px).rgb;\n    halo*=0.1667;\n\n    float wl=dot(w,vec3(0.3333));\n    vec3 trail=(w+halo*0.75)*bleed;\n    // max()-accumulation pulls every channel toward parity, so the raw trail\n    // reads as grey fur. Re-saturate, then roll off before it can white out.\n    trail=max(mix(vec3(dot(trail,vec3(0.3333))),trail,1.50),0.0);\n    trail+=mix(coolColor.rgb,warmColor.rgb,clamp(wl*1.6,0.0,1.0))*wl*0.55*bleed;\n    trail=trail/(1.0+0.60*trail);\n\n    // The forge sits in front; the wake glows around and through its gaps.\n    float solid=clamp(dot(f,vec3(0.3333))*3.4,0.0,1.0);\n    vec3 col=f+trail*(1.0-0.55*solid);\n    col+=f*wl*0.30*bleed;                    // shells relit by their own exhaust\n\n    col=filmicACES(col*1.06*exposure);\n    col=pow(max(col,0.0),vec3(0.94));\n    gl_FragColor=vec4(col,1.0);\n  }\n}\n",
    "generator": {
      "model": "claude-opus-4-8",
      "provider": "anthropic",
      "prompt": null,
      "params": null
    }
  }
}