Frank 4k WebGL demo – Lessons learned

So, I’ve just released my first proper 4k WebGL demo: Frank 4k! It’s a neat multi-part demo with 3D graphics and a low-fi synth tune, all written entirely in JavaScript (as far as I know, it’s the first of its kind).

To be able to use several shaders, and still keep the size down, I decided to make good use of the DEFLATE encoder (used by CrunchMe) by making the shaders as similar as possible (meaning a lot of repeated strings that get magically compressed away). Another solution could have been to make some sort of shader builder system (similar to how it’s done in Muon Baryon), but the former approach seemed simpler.

As for what to display, I decided to go for some simple fragment shader based ray tracing. Showing several different parts became a simple case of switching between different shader programs along a time line, like this:

I toyed around with different combinations of shaders until my compression pipeline gave me a result of somewhere between 6K and 8K. That’s when I started optimizing…

Optimizing Code – Part 1

The first step taken was to minimize the sound synth. I essentially did three things:

Remove parts of the music data that wasn’t actually used (such as unused patterns).

Remove parts of the sound generation code that wasn’t used, based on the instrument settings (e.g. notch filters weren’t used at all in my tune).

Since I was using WebGL, the demo already required typed arrays, so I changed the slightly bulky CanvasPixelArray code to use Int32Array instead.

This gave some significant savings. Rationale: If you start out with a generic solution (e.g. the Sonant music synth in this case), strip it down as far as possible based on your specific needs.

Compression

The CrunchMe compression tool was great, but I was sure that it could do even better. After looking around for different alternatives to the zlib DEFLATE implementation, I finally arrived at Ken’s PNGOUT tool. Now, it’s not open source, but making my CrunchMe tool call the command line tool to do an extra compression pass was simple enough (hey! all tricks allowed!).

This gave me another 100-200 bytes. Rationale: Use the best compression tools available, event if it means that your build system gets a bit “funny”.

Decompression

Apart from doing compression, the CrunchMe tool adds some decompression code to the final code. Here, I mainly did two things that helped remove quite a few bytes:

Generate code more dynamically. In particular, I replaced generic expressions like a.length and x.width*x.height with constant expressions.

Pollute the global name space (no closures, no vars).

Rationale: Write and generate specialized code, and don’t care if you pollute the global name space.

Optimizing Code – Part 2

At this point, I think I was about 800 bytes from my goal, and started doing all sorts of optimizations. Here are a few tips:

Your code does not have to do exactly what you first designed it to do, as long as it still looks and sounds OK. Simplify expressions. Do approximations. Drop precision in constants.

In the same sense, your GLSL code does not have to be correct – you will always get a result! Very subtle changes can create quite interesting effects. For instance, the over-bright second tunnel part is the result of changing a + to a – in a distance calculation equation.

The Closure Compiler will rename your variables & functions to shorter names. However, it will not shorten names of public APIs (e.g. document.getElementById), so stay away from those and/or find shorter variants that do the same.

The Closure Compiler will not touch the GLSL code (obviously). Mangle the code yourself (or use a GLSL minification tool). Use short names, and find short GLSL representations (e.g. length(a-b) is slightly shorter than distance(a,b)).

Utilize the DEFLATE algorithm as much as possible by using similar constructs across your code (e.g. prefer using only cos() instead of mixing sin() and cos()).

Forget code structure & OO programming. Merge all classes and name spaces! This removes code and usually helps the Closure Compiler to do a better job.

When you’re close to your goal, do silly things like re-arranging your code and changing non-critical constants to help the compression algorithm find that sweet spot. For instance, changing a constant 9999 to 9090 helped me get below the 4096 limit for the first time.

And as usual (in the 4k arena): size matters more than speed!

Result

So, the result is an HTML file (32 bytes) and a JavaScript file (4060 bytes) for a total of 4092 bytes. You can see the un-compressed original source code here: frank-unpacked.js.

System Requirements

Theoretically, this demo should run in any browser that supports WebGL, provided that there is adequate hardware support. It has been tested in Chrome, Firefox and Opera 12 alpha under Linux and Windows on NVIDIA, ATI and Intel graphics cards.

If you run into problems with the 4k version, try the “safe” version (at least it should give you some error message if it fails).

Note to Windows users with Firefox or Chrome: You may experience a bug in ANGLE (something like “Shader@0x0644E000(49,145): error X3000”) if you don’t force your browser to use the OpenGL back end rather than the DirectX back end (Firefox: about:config > webgl.prefer-native-gl = true, Chrome: chrome.exe –use-gl=desktop).

Note to Firefox users: The demo may appear to have a poor video refresh rate, due to a bug in the HTML Audio currentTime implementation. Try Chrome or Opera for a more fluent experience.

Thanks! Though a simple hack, it’s been very successful (top-ranked at Google and hundreds of visitors each day). I’d be really interested in knowing what people think about it and how well it works, so I should probably set up some sort of feedback channel… some day.