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.

for(_='atZMZh.YYsin(X*Xe/8W++)V>s;sVUfor(TUToSXo)R255Q256P&QOO*PN16L=0;KKL>J)+&&),]=>><<10|a+p+4210943TsK],=[L*p[9858122,1-,63+Rfunction([d]ImageDZa(var =-1;2>	)*n+(e()*(n);TdK3>d;dVp;e[l++=e,t,n){returnYrandom()1023tne=(j=c.creZeM=a.width/=4,k=a.height/=4)).dZa;r(tLL6|(tNNN|(tOO};i 4+3W)W+t/5)};2e7Uo=s18&63,n[so<=i(s&,s10O)?2>o?1:2:0;S=*,u=Q*,a=i(o,uf	f;fVTl	l;lVTp	p;pV2<an[o|u185)f*l*pn[o+f|u+l484);TaJa;aV3SJo;oVTuJu;uVp=/3,f=2>a?:5>a?6990400:l=X2*uu/8,sp/=1.5+s/2)2==ao>l+1f=p*=1/ot[u+(15-oP*s+*ar(0,f,p);setInterval(){o=new DZe/0,u=Ycos(Ra=XRf=[o%,i(o%2hpd=m=lKkSKM>o;oV{v=o/k-1,g=[a*v+u,.5s/k,a-u*vb=32;TqK3>q;qV{w=g[qv=f[q]-~~f[qD=1/w;0<w?v=v:D=-D;I=D*v=f+(h=g*D)*v;T0>wq]--;I<b;){(w=n[0]&|(1]&63)8|(2]O)0])b=I,v=0d=2qv+=d,d=1]m=t[(v&15(d&15*w+P*q])+=h;I+=D}}m=r(m,3*,b*b/)mLOm8OmOQ}c.putj,0,0)},L)';g=/[^ -IM[-~]/.exec(_);)with(_.split(g))_=join(shift());eval(_)Code language: JavaScript (javascript)

Full source code.

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 that 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 thought 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

In the end, I did a lot of manual optimizations, 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 initialized for the textures (t) and the map (n). An imageData object is initialized, and the resolution of the canvas object is divided by a factor of 4 to get a fluid frame rate.

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

Two functions are declared. Function ‘r’ is the equivalent of the glsl-function mix. It returns the interpolation of two colors 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) }; Code language: JavaScript (javascript)

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;Code language: JavaScript (javascript)

After that 1023 trees are randomly placed in the map. Elements with values 4 (leaves of the tree) and 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); Code language: JavaScript (javascript)

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);Code language: JavaScript (javascript)

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)Code language: JavaScript (javascript)

Tools used

I used the closure compiler to simplify and shorten my code, an online javascript compression tool to remove white spaces and semicolons. Finally, I 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 could 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.

You can find the full source code of my submission on GitHub: https://github.com/reindernijhoff/js1k/tree/master/2014_minecraft

Similar posts

If you like this post, you may also like one of my other posts:

JS1k post-mortem Minecraft
Tagged on: