Co-authors: Mathieu Henri, Martin Kleppe, Subzey, Anders Kaare, LiterallyLara, HellMood, innovati, Bálint Csala, Frank Force, Senokay, Román Cortés
This page presents the world's smallest WebGL shader playground and all the golfing steps that led us to it.
In the code above, replace [𝗬𝗢𝗨𝗥 𝗦𝗛𝗔𝗗𝗘𝗥 𝗛𝗘𝗥𝗘] with your GLSL demo. You can use the following inputs:
...and you need to set gl_FragColor, representing the color of each pixel.
.
After welcoming LiterallyLara and HellMood who won JS1K 2017 with a WebGL entry, the codegolf team gave itself a nice challenge: develop the smallest possible HTML/JS boilerplate that's able to run a WebGL fragment shader, à la Shadertoy. Our starting point was MiniShadertoy, that we simplified into MiniShadertoyLite, and after weeks of intense golfing, we managed reduce the code down to 349b!
We made an entry for JS1k 2017 featuring all the tricks of this article, plus a nice UI and full Shadertoy compatibility. It was ranked #7! (ENTRY / COMMENTED CODE).
We managed to remove the uniform time variable (by recompiling the program at each frame with a new value).
We also got rid of the verbose triangle generation (by rendering everything on a big square point). You can find more info about drawing points in this tutorial.
Our Raymarching / sdf post has been updated accordingly.
// Step 1 (April 2016, 308 bytes): start from MiniShadertoyLite, remove the textarea (hardcoded shader), minify, apply RegPack's webgl method hashing, replace all the constants with their numeric value
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P;${V="void main(){gl_"}Position=vec4(P,0,1);}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T;${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),35044)'><canvas id=a>
// Step 2: simplify `gl_Position=vec4(P,0,1)` to `gl_Position.xy=P`
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P;${V="void main(){gl_"}Position.xy=P;}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T;${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),35044)'><canvas id=a>
// Step 3: place the `;` in the template, reuse B at the end
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];setInterval(`g.uniform1f(g.goa(P,"T"),T++);g.da(6,0,3)`,9);with(g)P=cr(),so(S=ch(35633),`attribute vec2 P${V=";void main(){gl_"}Position.xy=P;}`),cS(S),ah(P,S),so(S=ch(35632),`precision lowp float;uniform float T${V}FragColor=[SHADER]}`),cS(S),ah(P,S),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,new Int8Array([-3,1,1,-3,1,1]),B+82)'><canvas id=a>
// Step 4: `with(g)` including the setInterval, arrow function inside setInterval, `int8array.of()`
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),so(S=ch(35633),`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),(A=x=>cS(S)+ah(P,S))(),so(S=ch(35632),`uniform lowp float T${V}FragColor=[SHADER]}`),A(),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'><canvas id=a>
// Step 5: move some reused code inside the `A()` function, and reuse B better
<body onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),B=35633,(A=s=>so(S=ch(B--),s)+cS(S)+ah(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'><canvas id=a>
// Step 6: replace "body onload=..." with "svg onload=..."
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[7]+[i[13]]]=g[i];with(g)setInterval(x=>uniform1f(goa(P,"T"),T++)+da(6,0,3),9),P=cr(),B=35633,(A=s=>so(S=ch(B--),s)+cS(S)+ah(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lg(P),ur(P),bf(B=34962,cu()),eet(T=0),vto(0,2,5120,0,0,0),ba(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>
// Step 7: use a simpler hashing (`g[i[0]+i[6]]` instead of `g[i[0]+i[7]+[i[13]]]`)
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),T++)+dr(6,0,3),9),P=cP(),B=35633,(A=s=>sS(S=cS(B--),s)+ce(S)+aS(P,S))(`attribute vec4 P${V=";varying lowp vec4 c;void main(){gl_"}Position=c=P;}`),A(`uniform lowp float T${V}FragColor=[SHADER]}`),lo(P),ug(P),bf(B=34962,cB()),eV(T=0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>
// Step 8: replace the first usages of `B` (36532, 36533) with hashed properties (`g.FN` and `g.FN+1`, simplified to `g.FN++` called twice)
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),T++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=34962,cB()),eV(T=0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>
// Step 9: replace `T` with `g.NO`, which is already set to 0
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),NO++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=34962,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>
// Step 10: simplify the constant `34962` with `g.ET-3`
<canvas id=a><svg onload='for(i in g=a.getContext(`webgl`))g[i[0]+i[6]]=g[i];with(g)setInterval(x=>uniform1f(gf(P,"T"),NO++)+dr(6,0,3),9),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float T${V=";varying lowp vec4 c;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=c=P;}`),lo(P),ug(P),bf(B=ET-3,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82)'>
// Step 11: Subzey joins the game... remove `getContext` parenthesis, replace the second param of `g.dr` (0) with the result of the neighbour function `uniform1f()`. Renamem c into p and T to t
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=p=P;}`),lo(P),ug(P),bf(B=ET-3,cB()),eV(0),vA(0,2,5120,0,0,0),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>
// Step 12: get rid of some more zeros
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(),(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),vA(A(`attribute vec4 P${V}Position=p=P;}`),2,5120,lo(P),ug(P),eV(bf(B=ET-3,cB()))),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>
// Step 13: move `A` declaration into `cP()`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),P=cP(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S)),A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),vA(A(`attribute vec4 P${V}Position=p=P;}`),2,5120,lo(P),ug(P),eV(bf(B=ET-3,cB()))),bD(B,Int8Array.of(-3,1,1,-3,1,1),B+82))'>
// Step 14: move `vA`, replace the ones in `Int8Array.of()` with neighbor functions
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),!vA(P=cP(A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S)),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(-3,1,!A(`attribute vec4 P${V}Position=p=P;}`),-3,!lo(P),!ug(P)),B+82)))'>
// Step 15: move `vA(P=cP(...))` around the `setInterval`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(-3,1,!A(`attribute vec4 P${V}Position=p=P;}`),-3,!lo(P),!ug(P)),B+82))'>
// Step 16: change the Int8Array's vertices to `3,-1,-1,3,-1,-1` and use `~` to turn `null` into `-1`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)+ce(S)+aS(P,S))),2,5120,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(3,-1,~A(`attribute vec4 P${V}Position=p=P;}`),3,~lo(P),~ug(P)),B+82))'>
// Step 17: Make `A()` always return `-1`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|~aS(P,S))),2,5120,0,eV(bf(B=ET-3,cB())),bD(B,Int8Array.of(3,A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),A(`attribute vec4 P${V}Position=p=P;}`),3,~lo(P),~ug(P)),B+82))'>
// Step 18: change the shape of the triangle, using the return value of `setInterval` (usually between 1 and 127) as first param of `Int8Array`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,uniform1f(gf(P,"t"),NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`uniform lowp float t${V=";varying lowp vec4 p;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P${V}Position=p=P;}`)),B+82),lo(P),ug(P))'>
// Step 19: introduce C and T as attributes, set `t.x=T.x`, and use `vA` (vertexAttrib1f) instead of `uniform1f`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t.x=T.x;}`)),B+82),lo(P),ug(P))'>
// Step 20: Set `t=T` directly
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(),2,5120,bD(B=ET-3,Int8Array.of(B,setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S)),!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),lo(P),ug(P))'>
// Step 21: Set 5th parameter of vA (i.e. `vertexAttribPointer`), stride, to 1, to make the coordinate pairs overlap: `int8Array.of(x1 = 1, y1 = x2 = -3, y2 = x3 = 1, y3 = 1)`. Also, the setTimeout is moved into `cP` (i.e. `createProgram`)
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(!A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),!A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>
// Step 22: Make `A` return 1 in order to avoid calling `!A()` with a `!` twice
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(A(`${V="varying lowp vec4 p,t;void main(){gl_"}FragColor=[SHADER]}`),B,!eV(bf(B,cB())),A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>
// Step 23: Rearrange `Int8Array.of()` arguments to place `!eV()` first, and move `V` declaration into `cb()`
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(!eV(bf(B,cB(V="varying lowp vec4 p,t;void main(){gl_"))),B,A(V+`FragColor=[SHADER]}`),A(`attribute vec4 P,T;${V}Position=p=P;t=T;}`)),B+82),!lo(P),ug(P))'>
// Step 24: Use the svg's id (which is an empty string by default)
<canvas id=a><svg onload='for(i in g=a.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)vA(P=cP(setInterval(x=>dr(6,vertexAttrib1f(1,NO++),3),A=s=>sS(S=cS(FN++),id+"varying lowp vec4 p,t;void main(){gl_"+s)|ce(S)|!aS(P,S))),2,5120,bD(B=ET-3,Int8Array.of(A`FragColor=[SHADER]}`,B,!eV(bf(B,cB(id="attribute vec4 P,T;"))),A`Position=p=P;t=T;}`),B+82),!lo(P),ug(P))'>
// Step 25 (11/2019): Replace the triangle with a single, huge point. No need to use a buffer object anymore! (we lose Safari compatibility here)
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)p=cP(setInterval(z=>dr(uniform1f(gf(p,"t"),NO++),0,1),A=s=>sS(S=cS(FN++),`precision lowp float;uniform float t;void main(){${s};}`)|ce(S)|aS(p,S))),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,lo(p),ug(p)'>
// Step 26: Get rid of the uniform time variable by recompiling the program at each frame. Also, replace the parameters of drawArrays (0,0,1) with "lo(),ug(p),!dp(p)" (warning: perf drop may happen at this point)
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];with(g)A=s=>sS(S=cS(h++),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S),setInterval(z=>{p=cP(h=FN),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,dr(lo(p),ug(p),!dP(p))})'>
// Step 27: Use a string parameter for setInterval instead of a function
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)A=s=>sS(S=cS(h++),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S),p=cP(h=FN),A`[SHADER]`,A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,dr(lo(p),ug(p),!dP(p))")'>
// Step 28: Remove h, use FN^=1 to alternate between 35632 and 35633 at each frame instead. Put A into cP().
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)p=cP(A=s=>sS(S=cS(FN^=1),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`,dr(lo(p),ug(p),!dP(p))")'>
// Step 29: Rearrange the code: "dr(lo(p=cP(A=...),A(...),A(...)),ug(p),!dP(p)" instead of "p=cP(A=...),A(...),A(...),dr(lo(p),ug(p),!dP(p)))"
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`precision lowp float;void main(){float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`),ug(p),!dP(p))")'>
// Step 30: Remove "precision lowp float", and write "lowp" before each float/vec2/vec3/vec4 instead
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position=vec4(0,0,0,gl_PointSize=3e2)`,A`[SHADER]`),ug(p),!dP(p))")'>
// Step 31: Only set gl_Position.w to 300, which makes the values x, y and z default to 0
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|ce(S)|aS(p,S)),A`gl_Position.w=gl_PointSize=3e2`,A`[SHADER]`),ug(p),!dP(p))")'>
// Step 32 (01/2022): rename "ce" to "createShader" to fix Webkit browsers
<canvas id=c><svg onload='for(i in g=c.getContext`webgl`)g[i[0]+i[6]]=g[i];setInterval("with(g)dr(lo(p=cP(A=s=>sS(S=cS(FN^=1),`void main(){lowp float t=${NO++}.;${s};}`)|createShader(S)|aS(p,S)),A`gl_Position.w=gl_PointSize=3e2`,A`[SHADER]`),ug(p),!dP(p))")'>