The new rules for this year allowed the use of WebGL, which was good because I had a list of new stuff I wanted to learn:
Do something in 3D
Learn more about shader programming
Implement a raymarching algorithm
Implement a cartoony effect (toon shading)
The theme was Hype Train. After a few days of wandering and research for inspiration, I decided to go for a demo that will consist of a raymarched scene of trains, continuously moving, rendered with a toon shading effect.
I wanted to do something with futuristic trains, or at least related to futuristic transportation, but not necessarily realistic.
In the beginning I wanted to have a transparent tube with trains / objects moving inside the tube, like the Futurama transport tubes or the first picture below. But I dropped the idea quickly because of the potential complexity of doing transparency.
I made the demo in basically two weeks, working mostly during the evenings. The time constraints were pretty tough because at the time the contest was supposed to only last one month, and I would also be away (and away from keyboard) the last week of February. It was essential to have good tools and a good flow.
You can notice that I dropped the “squared” shapes for something more round, in the form of a cylinder. The function used to generate this is indeed way shorter in the latter case.
The source light is positioned higher. But as you can see there is no ambient occlusion :(
Colors changing over time
This was actually a very cheap improvement in terms of code size (just of few bytes).
The demo consists of one shader (vertex + fragment) displayed full screen. The fragment and vertex shaders are loaded with a tiny piece of WebGL code, mainly inspired by the awesome work of ½-bit Cheese on HBC-00012: Kornell Box.
Here is the WebGL code without the shaders. I managed to save a few bytes by re-using variable names used in the shader code (for example x) in the following WebGL code, so it compresses better. What I mean is that if you take the variable x below in the for loop, you can technically replace it by any other variable name. Since x was used a lot in the fragment shader code, I decided to use it again, but it could also be a, z, l …
Constants like g.FRAGMENT_SHADER or g.ARRAY_BUFFER are replaced by their numerical values. The for loop produces 2 iterations, one to setup the vertex shader and another one to setup the fragment shader, and the variable y decides which shader to load.
The first two lines map all the function names from the webgl context to shorter function names. You can visualize the mapping by opening the developer console, creating a canvas an running the following:
Exploring the webgl context (variable g) will give something like this, and will help to understand what enVAA, biB or atS actually mean:
It’s pretty short, but it is roughly the equivalent of this:
Original shader source code
The fragment shader used before the minification, with comments, is quite verbose:
Minified shader source code
The main strategy I chose to follow in order to reduce the size was to:
Inline most of the functions.
Duplicate code. This is sad because it had a big impact on the overall performance, but in the end it really helped to compress better.
Use manual tricks, for example approximate a three-digits number to a two-digits one when possible (100 -> 99).
After a manual minification and a pass through shader-minifier, I ended up with the following shader code. It’s a lot of duplicated code, but it compresses better with jscrush, even though it might sound counter intuitive.
The original source code is maybe a bit big due to all the duplicated code, but it compresses well. Part of the flow was the recap of all the JS files sizes after compiling / compressing, which ended up being essential to checl if a change was worth begin kept.
To illustrate how effective the compression is and the strategy I decided to use, we can take an example of a shader code that “nicely” written, and one with code duplication.
Let’s use the gist mentioned above to minify / compress everything quickly.
Running the tool on this shader (place the above code in the file shader.fs): npm run all
What if instead we group the terms of the variable r and col together (inline), so the code is transformed to this?
Again, npm run all:
The crushed code size has decreased by 11 bytes, and the result is still the same. This case illustrates an example on how JSCrush takes advantage of duplicated code (pattern).
I entered the competition quite relaxed, considering it as a personal challenge rather than a real competition. In fact I could say it was my February 30 Day Challenge, so it somehow forced myself not to give up before the end!
It was very fun and I learned a lot of new stuff, especially about 3D graphics, the raymarching algorithm and some shader programming.
One possible regret is that the overall performance is a bit low. The code duplication is one of the factors, but there are probably some other reasons. Anyway, it is still possible to run the demo in a smaller window (less pixels) to reach a better framerate.
Hopefully this recap gives enough insight into the making of the demo, or at least makes you want to do your own compo next time! If there is anything that I got wrong, or if you want more details about a specific part, please let me know.
There are so many good resources on the subject that came to be super useful. Too many so here are just a few: