After enjoying all the great submissions of JS1k in the previous years, I wanted to participate myself this year (which, in fact, is my first participation in an online code competition ever). My entry “Minecraft” ended third. In this post I will write up some notes about the 1024 bytes of javascript of my submission.


Minecraft in 1k javascript showing water, hills, trees and fog.

First try: Wolfenstein 3D

My first idea was to make a Wolfenstein 3D clone in javascript. This idea was based on a webgl fragment shader I created last year using Shadertoy. Shadertoy is a site where you can create and share webgl fragment shaders. My Wolfenstein shader got a lot of positive feedback, so I decided to translate the shader to javascript and (hopefully) keep the size under 1kb.

The (translated) javascript of Wolfenstein 3D follows the following structure:

  1. Create a procedural map.
  2. Create procedural textures.
  3. For each frame render the screen using ray-casting. All pixels are written to an imageData object. This object is blit to the canvas using the putImageData method of the canvas element each frame.

Unfortunately I was unable to get the size near 1kb. Especially the amount of detail that I wanted for the (64×64) procedural textures resulted in a lot of functions and function-calls which took up a lot of space. So after some time of ‘shaving code’, I gave up trying to fit the demo in 1k. I added collision detection, animated doors and keyboard input (so you can walk around using the arrow-keys) instead, made some speed improvements, and submitted this final version for the ++ 2k-competition: Wolfenstein 3D in 2k of javascript.


Wolfenstein: trying to generate some well-known textures in a webgl fragment shader.

Second try: Minecraft

After my first experiments with Wolfenstein, and getting familiar with the javascript compression techniques used in the competition, I though it should be possible to fit a Minecraft renderer in 1k of javascript. Especially because the (procedural) textures are a lot smaller (16×16) and simpler to generate.

The Minecraft demo I wanted to create is based on a javascript Minecraft tunnel render (by Notch himself) and another shader on Shadertoy I created last year.

The code of the Minecraft demo follows the same structure as my Wolfenstein 3D demo:

  1. Create a procedural map.
  2. Create procedural textures.
  3. For each frame render the screen using ray-casting (in 3 dimensions).

The code

At the end I did a lot of manual optimisations, which made the source code of my demo pretty unreadable. The full (uncompressed) source code and some explanations are shown below.

First, two arrays are initialised for the textures (t) and the map (n). An imageData object is initialised and the resolution of the canvas object is divided by a factor 4 to get a fluid frame-rate.

t = [], n = [],
e = (j = c.createImageData(M = a.width /= 4, k = a.height /= 4)).data;

Two functions are declared. Function ‘r’ is the equivalent of the glsl-function mix. It returns the interpolation of two colours e and t: e * n + t * (1-n). Function ‘i’ returns the height of the map for (x,y) coordinates (e,t).

r = function (e, t, n) {
    return (t >> 16) * n + (e >> 16) * (1 - n) << 16 | 
    (t & 255 * 256) * n + (e & 255 * 256) * (1 - n) & 255 * 256 | 
    (t & 255) * n + (e & 255) * (1 - n) }; 

i = function (e, t, n) {
     return 4 + 3 * Math.sin(e / 8) * Math.sin(e / 8 + t / 5) }; 

Colors are encoded as 24-bit integers (rgb). If you interpolate two of these integers you have to mix the three 8-bit components of the integer separately, to prevent ‘bleeding’ of (for example) the green component into the blue or red component. A verbose notation (like 255 * 256) is used to get a lot of duplication in the code which gives a better final compression of the code.

Create a procedural map

The size of the map displayed is 1024 x 256 x 64. All elements of the map are stored in a one-dimensional array n. All elements below the height returned by function i have value 2 (earth) or 1 (water). The rest of the elements have value 0 (air).

for (s = 0; 2e7 > s; s++) 
    o = s >> 18 & 63, 
    n[s] = o <= i(s & 1023, s >> 10 & 255) ? 2 > o ? 1 : 2 : 0;

After that 1023 trees are randomly placed in the map. Elements with value 4 (leaves of tree) and value 5 (trunk) are written to the array n.

for (s = 0; 1023 > s; s++)
// place tree on random coordinate (o,u,a)
    for (o = 1023 * Math.random(), u = 255 * Math.random(), a = i(o, u), 
         f = -1; 2 > f; f++)
        for (l = -1; 2 > l; l++)
            for (p = -1; 2 > p; p++) 
                // no trees in water ( a > 2 )
                2 < a && 
                // trunk
                (n[o | u << 10 | a + p + 1 << 18] = 5) && 
                // leaves
                1 - f * l * p && 
                (n[o + f | u + l << 10 | a + p + 4 << 18] = 4); 

 

Create procedural textures

Now the procedural textures are generated and stored in the (one-dimensional) array t.

// generate 16 textures
for (a = 0; 16 > a; a++)
    // generate 3 versions of each texture with different lightning
    // for the different sides of a block
    for (s = 0; 3 > s; s++)
        // textures are 16x16 (o,u)
        for (o = 0; 16 > o; o++)
            for (u = 0; 16 > u; u++)
                // random value (0.7-1.0) to multiply with base color. 
                p = 1 - Math.random() / 3, 

                // get 24-bit base color for each texture  
                f = 2 > a ? 4210943 : 5 > a ? 6990400 : 9858122, 

                // border between grass and earth (texture 2)
                l = Math.sin(2 * u) + u / 8, 

                // modify base color for different sides of block
                1 - s && (p /= 1.5 + s / 2) && 

                // earth for texture 2 if o > l+1
                2 == a && o > l + 1 && 
                (f = 9858122, p *= 1 - 1 / o), 

                // multiply base color f by p and store in t
                t[u + 16 * (15 - o) + 256 * s + 1023 * a] = r(0, f, p);

 

Render the screen

Finally the screen is rendered for each frame using a simple ray casting function.

setInterval(function () {
    var o = new Date / 10230,
        u = Math.cos(Math.sin(o)),
        a = Math.sin(Math.sin(o)),
        // position f of player
        f = [16 * o % 1023, i(16 * o % 1023, 63 + 16 * Math.sin(o)) + 2, 63 +
            16 * Math.sin(o)
        ],
        h = [],
        p = [],
        d = m = l = 0;
    // for each pixel (s,o) on screen
    for (s = 0; k > s; s++)
        for (o = 0; M > o; o++) {
            var v = o / k - 1,
                // construct ray g
                g = [a * v + u, .51 - s / k, a - u * v],
                b = 32;

            // for each dimension cast ray
            for (q = 0; 3 > q; q++) {
                var w = g[q],
                    v = f[q] - ~~f[q],
                    D = 1 / w;
                0 < w ? v = 1 - v : D = -D;                 
                var I = D * v;

                for (d = 0; 3 > d; d++)
                    p[d] = f[d] + (h[d] = g[d] * D) * v;

                for (0 > w && p[q]--; I < b;) {

                    // hit (value n[] > 0)
                    (w = n[p[0] & 1023 | 
                                (p[1] & 63) << 18 | 
                                (p[2] & 255) << 10]) && 

                        // get texture coordinates (v,d)
                        (b = I, v = p[0], d = p[2],
                        1 - q && (v += d, d = p[1]), 

                        // get color m of texture w for (v,d)
                        m = t[(16 * v & 15) + 16 * (16 * d & 15) 
                              + 1023 * w + 256 * q]);   

                    for (d = 0; 3 > d; d++)
                        p[d] += h[d];
                    I += D
                }
            }

            // add fog by interpolation of m and fog color
            // fog color is 3*water color to get code duplication
            m = r(m, 3 * 4210943, b * b / 1023);

            // write color to imageData object
            e[l++] = m >> 16 & 255;
            e[l++] = m >> 8 & 255;
            e[l++] = m & 255;
            e[l++] = 255
        }
    c.putImageData(j, 0, 0)
}, 16)

 

Tools used

I used the closure compiler to simplify and shorten my code, an online javascript compression tool to remove white spaces and  semicolons and finally compressed the code using the (excellent) compression tool: Regpack 3.

Speed vs Size

It is (of course ;)) possible to decrease the size of the given code even more: for example by making the resolution of the imageData object fixed and removing all ‘var’ statements. That way I was able to add clouds to the demo in a previous version you can find here. Making all the variables global resulted in a (huge) speed penalty. Because I enjoyed a fluid frame-rate (and fullscreen display) more than clouds, I removed the clouds for my final submission.

JS1k post-mortem Minecraft