Frank 4k WebGL demo – Lessons learned

Frank 4k 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).

Try it here (compressed) or here (“safe mode”, no compression, with error checks etc).

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.

If you want to know more about how I did it, read on… I hope that this post will inspire more people to try out the slightly odd art of 4k JavaScript demo programming.

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:

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[0]; // NOTE: This is the first part
for (var i = 0; t > parts[i][0]; i++) prg = p[parts[i][1]];
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:

  1. Remove parts of the music data that wasn’t actually used (such as unused patterns).
  2. 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).
  3. 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:

  1. Generate code more dynamically. In particular, I replaced generic expressions like a.length and x.width*x.height with constant expressions.
  2. 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.

20 thoughts on “Frank 4k WebGL demo – Lessons learned

  1. FYI: On my low end gpu it fails with a message that the limit of instructions is 64.

    1. m

      Too bad… No WebGL for you 😉 (at least not any advanced stuff)

  2. Dude, this is FUC**** awesome! 4k intros were the last bastion of desktop apps, and with this, that’s been nuked. Congrats!

  3. Adi

    I can’t view anything, the screen start green then tween to black and nothing happens. I’m using chrome 16 beta and Intel HD Graphics.

    When I open the demo, your ‘pac-man’ header stopped

  4. hayesmaker

    This is cool, but why such a long load time if it’s only 4096 bytes?

    1. m

      Because it’s only 4K, all the music has to be generated in JavaScript 😉

  5. zworp

    I’m on a macbook pro with an GF330M using osX 10.6.8 with Firefox 7.0.1.

    Both the compressed and the safe ones show a green screen, max CPU usage and after a while gives this error:

    A script on this page may be busy, or it may have stopped responding. You can stop the script now, or you can continue to see if the script will complete.

    Script: http://frank.bitsnbites.eu/frank-safe.js:616

    1. m

      Just let it continue… You’re on a slow computer/browser, so be patient (it’s generating the music).

  6. raer

    I get an error:
    Link program 6: Shader@0x16AAD000(49,145): error X3000: syntax error: unexpected token ‘}
    Link program 7: Shader@0x14434000(49,145): error X3000: syntax error: unexpected token ‘}

    Firefox 8 with a Quadro 600 and pretty recent drivers here.

    1. raer

      Sorry. Works with the “webgl.prefer-native-gl = true” switch… 🙂

  7. Very cool! This is a true evidence that Javascript is powerful enough for cool web demos.

  8. Stanislav

    Crab, not working on high end cpu! Can you get it? You cant do anything woth this bulshit js!

    1. m

      Well, either you’ve got a crap GPU, crap drivers, a crap browser, a crap OS, or you’re simply out of patience for the 10-20s precalc… Better luck next time 😉

  9. I know this isn’t the right place to post it, but your Subshifter is gold. Pure gold. So simple and yet it has helped me so much. Many thanks!

    1. m

      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.

  10. Very impressive! I especially like the in-depth explanation how it is put together.

Leave a Reply to Stanislav Cancel reply

Your email address will not be published.