I have to admit that the design is kind of simple. One of the reasons is that I mostly focused on fitting it all into less than 4096 bytes.
Strategy & Goals
The goals were, quite simply: make a multi-part WebGL demo with synthesized music, and make it fit into less than 4096 bytes. The strategy was equally simple:
- Use Sonant Live to create the music.
- Use Shader Toy to design my GLSL shaders.
- Create a simple framework in JS to play the music and run the WebGL stuff.
- Finally compress it all with Google Closure Compiler & CrunchMe.
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:
parts = [ [30, 6], // Tunnel [45, 2], // Mirrored slow [60, 1], // Wild [75, 0], // Basic [90, 5], // Even wilder [105, 4], // Mirrored fast [120, 7], // Tunnel [135, 3], // Sepia mirrored [9000, 0] ]; var prg = p; // NOTE: This is the first part for (var i = 0; t > parts[i]; i++) prg = p[parts[i]]; gl.useProgram(prg);
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.
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”.
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!
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.