My JS1k 2015 demo

2015-02-26

I published a demo for this years competition. It's a webgl demo that shows a train. Choo choo! Now frankly a webgl demo is a bit of an odd choice for me for two reasons; I've only (actually) started doing webgl last week and I think this should still be a JavaScript competition, more than a shader competition. But.

Having dug into webgl myself though I wanted to see how easy it would be to create a demo. How much is actually involved. How much you can do with a simple shader. How much space you have for a shader. And so forth. I ended up with this demo.

Golfing also forces you to dig deeper into the spec which is a good learning experience in general. And it was. I shaved off over 100 bytes from a 700 byte shader that was already minimal as far as I knew at that point. Like using vec4(1.0,1.0,0.0,1.0), or maybe vec4(1.,1.,0.,1.) oh but hey also vec4(1,1,0,1) or simply p.xxyx. So yeah, golfing :)

I actually planned to do another interpreter demo this year. I've done a few in the past (BrainFuck and Whitespace. This year was gonna be Chicken! But I was too busy with other stuff to get into it.

After making the webgl distortion effect I just wanted to see whether I could get that to work in a JS1k demo. The setup needed for running the shader is fairly simple:

Code:
// vertex shader: pixel mapping set vec4 to gl_Position
var vertShaderObj = gl.createShader(gl.VERTEX_SHADER);
var vertexShaderSrc = document.querySelector('[type="vertex"]').textContent;
gl.shaderSource(vertShaderObj, vertexShaderSrc);
gl.compileShader(vertShaderObj);
gl.attachShader(program, vertShaderObj);

// fragment shader: pixel coloring, set vec4 to gl_FragColor
var fragShaderObj = gl.createShader(gl.FRAGMENT_SHADER);
var fragmentShaderSrc = document.querySelector('[type="fragment"]').textContent;
gl.shaderSource(fragShaderObj, fragmentShaderSrc);
gl.compileShader(fragShaderObj);
gl.attachShader(program, fragShaderObj);

gl.linkProgram(program);
gl.useProgram(program);

This is fairly simple to trim down. You wrap it in a with(g) statement and do the standard golfing:

Code:
with(g)
g[C='shaderSource'](f=g[A='createShader'](E=35633),'attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}'),
g[B='compileShader'](f),
g[D='attachShader'](p=createProgram(),f),
g[C](f=g[A](E-1),'vertexshader'),
g[B](f),
g[D](p,f),
linkProgram(p),
useProgram(p),

The numbers are gl constants and I have it on good authority that they are set in stone. So we can safely replace long stuff like g.FRAGMENT_SHADER with 35633. And you're basically compiling a shader twice so those methods can be reused. Caching the names is actually juuust a bit shorter despite needing to add 3 bytes under the with statement (g[X] vs X), not counting the double quotes at least once.

The vertex shader is pretty much just an identity shader and there's little to minify in it. It is used by a drawArrays(g.TRIANGLE_STRIP) which needs only 4 vertexes:

Code:
var positionLocation = gl.getAttribLocation(program, 'p');
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer( positionLocation, 2, gl.FLOAT, false, 0, 0);

Or minified:

Code:
bindBuffer(U=34962,createBuffer()),
bufferData(U,new Float32Array([f=-1,f,h=1,f,f,1,1,1]),U+82),
enableVertexAttribArray(pl=getAttribLocation(p,'p')),
vertexAttribPointer(pl,2,5126,!1,0,0),

We'll use this to draw two rectangles with gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); which creates a rectangle that fills the screen using two triangles. That's a lot of bytes for just even painting the screen and this won't even do anything because that's what the fragment shader will do.

We also pass on the current frame counter (uniform3f(g[G='getUniformLocation'](p,'m'),0,++f,1)) to create an actual animation, and the current mouse coordinates (a.onmousemove=function(e){uniform3f(g[G](p,'y'),e.clientX,e.clientY,1)}) to get some interaction going.

So that leaves us with the actual magic, the fragment shader. Without this shader you'd just get a black screen no matter how much you would wiggle your mouse. Ok you would actually get an error, but with an identity fragment shader you'd get a black screen, is the point :)

This is pretty much the fragment shader of the demo "unminified". That is, this was the state right before I actually started golfing :)

Code:
precision highp float;

uniform vec2 m;
uniform float t;

vec2 distort(vec2 p, float w) {
float u_barrel_power = 1.0 + 2.0 * ((m.x / w) - 0.5);

float h = mod(t, 200.0) / 200.0;
if (mod(t, 400.0) >= 200.0) h = 1.0 - h;
u_barrel_power = 0.5 + h;

float theta = atan(p.y, p.x);
float radius = length(p);
radius = pow(radius, u_barrel_power);
p.x = radius * cos(theta);
p.y = radius * sin(theta);
return 0.5 * (p + 1.0);
return p / 4.0;
}

void main() {
float w = '+a.width+'.0;
float h = '+a.height+'.0;
vec2 wh = vec2(w, h);

float pi = 3.14;
float pi2 = 2.0 * pi;
vec2 xy = gl_FragCoord.xy;

vec2 d = distort((xy/vec2(w,h)) * 2.0 - 1.0, w);

float r = distance(0.5 * wh, d * wh);
float a = degrees(atan((0.5 - d.x) * wh.x, (0.5 - d.y) * wh.y) + pi);

float train_pos = mod(t, 360.0);

float g = mod(a - train_pos + 180.0, 360.0) - 180.0;
bool is_train = g > 0.0 && g < 90.0 && r > 190.0 && r < 260.0;
bool is_gap = floor(g) == 60.0 || floor(g) == 61.0 || floor(g) == 20.0 || floor(g) == 21.0 || floor(g) == 40.0 || floor(g) == 41.0;
bool is_chain = r > 215.0 && r < 235.0;

if (is_train && (!is_gap || r > 215.0 && r < 235.0)) {
if (is_gap && is_chain) gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
else if (floor(g) == 89.0 && (r > 200.0 && r < 210.0 || r > 220.0 && r < 230.0 || r > 240.0 && r < 250.0)) gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
else if (floor(g) == 0.0 && (r > 200.0 && r < 210.0 || r > 240.0 && r < 250.0)) gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else gl_FragColor = vec4(0.0, (clamp(r-190.0, 0.0, 35.0) - clamp(r-225.0, 0.0, 35.0)) / 35.0, 0.2, 1.0);
}
else if (r > 200.0 && r < 210.0 || r > 240.0 && r < 250.0) gl_FragColor = vec4(0.541, 0.541, 0.541, 1.0);
else if (mod(floor(a+2.0), 10.0) > 2.0 && r > 180.0 && r < 270.0) gl_FragColor = vec4(0.411, 0.298, 0.149, 1.0);
else if (mod(floor(d.x * w), 30.0) == 0.0 || mod(floor(d.y * h), 30.0) == 0.0) gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
else gl_FragColor = vec4(xy/vec2(w,h), m.y/h, 1.0);
}

It breaks down into two main parts. There's the distortion function and the train being painting.

Some simple shader minification tricks I've learned are:

- floats don't need leading or trailing zeroes (same rules as JS) but they do need a dot
- vectors (vec2 etc) actually don't need the dots so you can do vec2(1,2)
- the precision header, needed in fragment shaders, can be replaced by declaring decision in the variable declaration (lowp float x)
- ternaries are still shorter than if-else
- ternaries are relatively new so wont work everywhere (firefox/win compiles to DX using the old version so it fails on ternaries)
- ternaries should inherit the precision but IE currently has a bug where this doesnt happen but you can fix it with the header (instead of declaration prefix)
- binary operators are also specced but not yet usable
- vectors have property-ish ... properties with an interesting twist: you can swap and repeat them in arbitrary order (t=vec2(1,0); t.xxyx;)
- vectors destructure (t=vec2(1, 2); s=vec4(t, 1, 2);)
- apparently 400. can be reduced to 4e2 and it still counts as a float (huh..) saving you the dot
- semi-colons are mandatory
- variables must be declared explicitly
- local variables dont need the prefixes that static space vars need (uniform and such)
- there's no difference between assignment in var decl and assign on first occurrence (float x=5;f(x); vs float x;f(x=5);) so you should group together var declarations if you cant initialize them immediately
- it's fine to re-use variables as long as its the same type
- assigning a vector to another variable is by (deep) value not by reference
- your return value must be assigned to gl_FragColor so there's that
- your current coordinate can be read in gl_FragCoord in absolute screen coordinates
- there is no way in WebGL to get the viewport width and height without explicitly passing it on (whyyyyy) so you'll have to do it
- you can combine var declarations as usual
- ints are shorter than floats
- vector math works with destructuring (x=vec2(1,2);y=x/.5;z=x/y;)
- if you use a function only once in your shader you may as well inline it (duh)
- I was unable to figure out a short way of checking whether a number was part of the set of some numbers and ended up using simple if-chains and math hacks
- you can use unforms to get certain values in shorter

That last point regards a trick I pulled to replace a vec2(0,1) by piggy backing on the frame counter. The frame counter is put in a vec3 and stuffed by a 0,1 from JS. This only adds three bytes because it's equally as long to pass on two floats (uniform2f) from JS as it is to pass on four (uniform4f). This in turn allowed me to get a stable vec2(0.0, 1.0) in the shader to use as default values for certain return values of the shader. You'll see in the minified shader that most end points of the monster ternary use something like m.xxyx to get vec(0,0,1,0).

I think the above list were the main points into golfing down the shader to this pre-rename-and-whitespace-cropping version:

Code:
precision mediump float;
uniform vec3 m,t;

void main(){
float
HALF_TRAIN_WIDTH=35.,
TRAIN_ANGLE=.5+mod(t.y,4e2)/2e2,
HOEK_VERTEX,
FRADIUS,
JATAN;

vec2
WH=vec2('+[a.width,a.height]+'),
MOUSEREL=m.xy/WH,
PIXPOS=gl_FragCoord.xy/WH*2.-1.,
TRANSPOS_ABS=WH*(pow(length(PIXPOS),TRAIN_ANGLE>1.5?3.-TRAIN_ANGLE:TRAIN_ANGLE)*vec2(cos(JATAN=atan(PIXPOS.y,PIXPOS.x)),sin(JATAN))*.5+.5),
DRAW_GRID = mod(TRANSPOS_ABS,HALF_TRAIN_WIDTH);

HOEK_VERTEX=degrees(JATAN);

int
ANGLE_DIFF=int(mod(HOEK_VERTEX-t.y,360.)),
RADIUS=int(FRADIUS=distance(.5*WH,TRANSPOS_ABS)-180.),
IN_TRAIN_BETWEEN=ANGLE_DIFF-90<0&&RADIUS>0&&RADIUS<70?mod(HOEK_VERTEX-t.y,20.)<1.?-1:1:0;

bool
RAIL_RANGE=RADIUS>10&&RADIUS<20||RADIUS>50&&RADIUS<60;

gl_FragColor=IN_TRAIN_BETWEEN<0&&RAIL_RANGE?t.xxxz:IN_TRAIN_BETWEEN>0?ANGLE_DIFF==89&&RAIL_RANGE?t.zzxz:
ANGLE_DIFF<2&&RAIL_RANGE?t.zxxz:vec4(MOUSEREL.x,FRADIUS>HALF_TRAIN_WIDTH?2.-FRADIUS/HALF_TRAIN_WIDTH:
FRADIUS/HALF_TRAIN_WIDTH,.2,1):RAIL_RANGE?t.xxxz*.5:mod(HOEK_VERTEX,10.)>2.&&RADIUS>-9&&RADIUS<80?vec4(.4,.2,.1,1):
DRAW_GRID.x<1.||DRAW_GRID.y<1.?t.zzzz:vec4(TRANSPOS_ABS/WH,MOUSEREL.y,1);
}

Have you noticed how I hacked in the width/height? Remember that a is the canvas in the shim. And since we're gonna compile this shader in JS anyways, it's much easier to put the width/height into the shader this way than it is through intended channels. And as a little sugar it's using an array concat (wh='+[width,height]+'; instead of wh='+width+','+height+';) to save another two bytes.

There's also some normal golfing going of course :) Refactoring algorithms and such, mangling the original code and trying to reduce duplication as much as possible.

I dunno what else to tell you :) Minifying shaders turned out to be more fun than I thought. It's the combination of a new language with new tricks and the fact that it's not a casino game like JS minification has become with packers. ES6 will bring back some of that of course with its new language features and the desugaring. But we'll have to wait for that.

So you can find the demo here. And there's about two and a half weeks left to submit your own demo. I've made mine in about two days in a language I hardly knew. What's your excuse?