In continuation of the
popular series of articles on the fact that
30 lines of javascript are sufficient for everyone , I offer you the translation of the article by Matthew Henry P01 about his latest work:
(further text from the first person, all "I" refer to P01)
Motivation
About a year ago,
Notch wrote a fly-
around on the world of minecrafter with procedural textures, a camera and a fog effect with a total weight of only 4 kilograms on javascript. It looked cool and worked lively. Its code is sharpened for speed having spent a little effort, it was really possible to squeeze it down to 2 or even 1 kilobyte.
')
A month ago, I released
Wulfenstein , and the community response was quite encouraging. So I took the next step and moved to full 3D.
Sources
<body onload=setInterval(F=";t+=.1;Q=Math.cos;for(x=n=c.height=300;x-=4;)for(y=n;y-=4;d.fillRect(x,y,E,Z^z?4:E))for(D=0;(E=4-D/2)&&F<F[(t+D*Q(T=x/n-.5+Q(t/9))&7)*8|(Z=3.7+D*Q(T-8)&7)*4|(6.5-D*y/nE)];z=Z)D+=1/8",t=55),d=c.getContext('2d')><canvas id=c>
Voila! 252 bytes of shamanism with voxel 3D world, camera and fog effect.
Version 248 for Ponte
<body onload=setInterval(F=";t+=.1;Q=Math.cos;for(x=n=c.height=300;x-=4;)for(y=n;y-=4;c.getContext('2d').fillRect(x,y,E,Z^z?4:E))for(D=0;(E=4-D/2)&&F<F[(t+D*Q(T=x/n-.5+Q(t/9))&7)*8|(Z=3.7+D*Q(T-8)&7)*4|(6.5-D*y/nE)];z=Z)D+=1/8",t=55)><canvas id=c>
A slightly
slower version of the Minicraft at 248 bytes , because I can! All the same, only works slower.
How ?
You are probably interested in how the 3D engine, the flight path of the camera and the three-dimensional world itself fit in 252 bytes of html and javascript. This is a hell of a mixture of different beautiful and dirty tricks; some of them will be discussed below.
Dwarf shoulders
In fact, Minicraft stands on the shoulders of its miniature predecessors - Wolfenstein,
Storm in a Glass and
Rayonchik . Many of the techniques used here have already been described earlier with these releases.
Drawing
Given the
size limit of this demo, there was no space for beautiful rendering. I had to use coarse ray tracing with a fixed pitch.
If you are wondering what lies behind these abstruse words, then everything is quite simple. The function representing the world you want to draw is actually calculated for each point in the space between the camera and the points you actually draw. In other words, you call this fat function thousands, hundreds of thousands of times a frame, without any hint of speed optimization.
Resolution
The default resolution for the canvas element is 300x150. Setting the width or height of the canvas clears it and resets its settings. If you want the highest resolution at the lowest cost, the best way is to set the height to 300 in each frame.
So, we have a canvas 300x300. However, the selected drawing method is too slow for such a number of rays. Therefore, dividing the resolution by 4, we will only trace 75x75 = 5.625 rays. Passing the beam in 1/8 increments until an opaque block or distance of 8 units is reached, we get a maximum of 75x75x80 = 450,000 checks per frame.
World of the line
With this size, you have absolutely no place to generate, save or use a dynamic distance map as a world map, so you have to use the only available data: the source code.
By placing the main loop code in the variable F, we get access to its string representation and use it as a map of transparent / opaque blocks in a 3D grid.
Since the code is short, there was only enough data for the 8x8x8 world, using the first 64 characters of the main loop as information on 512 voxels. Having studied the ASCII codes in this line, I decided to consider as an opaque block any character preceding a semicolon in the ASCII table, inclusive. In other words, any of the symbols! "# $% & '() * +, -. / 0123456789 :; means an opaque block. This choice allowed you to get a through hole in the map, which in turn simplified the camera's trajectory.
Thus, checking the intersection in Minicraft looks very simple:
Speaking of the semicolon, the attentive reader might have noticed one strange detail in the source:

See, the main loop code starts with a semicolon? This seemingly useless semicolon allows you to win an extra byte, reducing the already compact intersection check to:
F < F[x + z * 4 + y * 8]
Ray tracing and camera
As it was said above, in the 3D map we had a through opening, ideal for the passage of the camera. Rays are thrown from the camera within the pyramid of visibility for each pixel, then we gradually check all points along the beam for intersection with the map until it is found, or the distance traveled does not exceed the allowable maximum. The further we move along the beam, the brighter the shade of gray will be assigned to the corresponding pixel.
for(x=n=c.height=300;x-=4;) for(y=n;y-=4;) for(D=0;;z=Z) D+=1/8
The outer loops run over all x and y pixels and fill them with the resulting shade of gray D. The inner loop walks along the ray corresponding to the pixel at position x, y until an opaque block is found.
The three-dimensional X, Y, Z coordinates of the current position on the beam are calculated as follows:
What Y * 8 actually calculates | Z * 4 | X. If more, then:
Dithering, fog and grayscale
By default, canvas has a black fillStyle. There is not enough space to change it, so you have to look for another way to implement the palette. Shades of gray in Minicraft are obtained from rectangles with fractional width and height. Sub-pixel dimensions cause
smoothing , which makes the edges of the rectangles semi-transparent. Just like that. By drawing one rectangle for each pixel, we actually get some kind of dithering.
Lighting in the demo uses the same trick as Rayonchik with the Wolfenstein. To determine the direction of the surface normal to one or the other side, the whole parts of the Z coordinates of points on the ray are compared before and after intersection with an opaque block. Remember the main loop?
;t+=.1;Q=Math.cos;for(x=n=c.height=300;x-=4;)for(y=n;y-=4;d.fillRect(x,y,E,Z^z?4:E))for(D=0;(E=4-D/2)&&F<F[(t+D*Q(T=x/n-.5+Q(t/9))&7)*8|(Z=3.7+D*Q(T-8)&7)*4|(6.5-D*y/nE)];z=Z)D+=1/8
the code responsible for storing and comparing whole parts of the Z coordinates is in it in the expressions Z ^ z? 4 :, Z = and z = Z.
Magic numbers
We all know that floating-point numbers are not optimal for applications that require accurate representation of numbers. IEEE-754 standards are based on binary notation and powers of two, which results in rounding errors of decimal numbers.
In javascript, as in many other programming languages, 0.1 + 0.2 = 0.30000000000000004. This is inconvenient in many situations, however, when you are dealing exclusively with powers of two, IEEE 754 plays into your hands and allows you to perform a couple of tricks.
For example, in the ray calculation cycles given above,
for(x=n=c.height=300;x-=4;) for(y=n;y-=4;) for(D=0;(E=4-D/2)&&F<F[ ... ];z=Z) D+=1/8
we can easily notice that D has reached 8, because the expression (E = 4-D / 2) goes to 0, which in turn is equivalent to false and stops the cycle.
The value of E corresponds to the size of the rectangle that we draw for this ray, and at the same time serves to calculate the X coordinate for a point on the ray. In general, this focus saves 3 bytes.
Dirty trigonometry
To win 4 bytes in the code that uses Math.cos and Math.sin, a link to Math.cos is created, and the “sine” is obtained by adding 8 to the argument. The error of such an approximation does not exceed 0.15 radians.
Reviews
Feedback and suggestions are collected on the
Minicraft page on Pouet.net .