An homage to Wolfenstein 3D in 251 bytes of HTML5
Looking back at the code of TEA STORM and MINI DISTRICT, I realized that the setup I got there had a lot more potential. Rendering some sort of axis aligned dungeons like the ones from Wolfenstein 3D should be a breeze, and it was.
Since you are here, chances are you will appreciate watching the development version of wolfensteiny which shows the map with the position of the camera and its view cone, along the time to render the frame.
<body onload=E=c.getContext("2d"),setInterval(F="t+=.2,Q=Math.cos;c.height=300;for(x=h;x--;)for(y=h;y--;E.fillRect(x*4,y*4,b-d?4:D/2,D/2))for(D=0;'.'<F[D*y/h-D/2|0?1:(d=t+D*Q(T=x/h-.5+Q(t)/8)&7)|(3.5+D*Q(T-8))<<3]&&D<8;b=d)D+=.1",t=h=75)><canvas id=c>
That's it: 251 bytes of dungeon horror!
How does it work ?
It is a small small world. With only 251 bytes there is no space to store data, change many properties, generate things procedurally, ... well, anything. You have to think out the box and use some less than subtle techniques. So how on Earth does it work ?
At such small size, there is no space to store or generate a map, so we are left with using the only data we have: The source code.
Indeed, placing the source code of the main loop into a variable allows us to compare the characters of that string and use it as a map of the empty/solid cell on a grid.
Since the source code is quite short, it was only possible to have an 8 by 8 grid which maps to the first 64 characters of the main loop. Looking at the ASCII codes used in that string, I decided to place a building for each character that is or comes before the character
. in the ASCII table. In other words any of the following characters
!"#$%&'()*+,-. is a wall. Choosing this specific character cleared a straight line in the map which allowed to have a simple camera path.
t+=.2,Q= ----> ░█░█░█░░ Math.cos ----> ░░░░█░░░ ;c.heigh ----> ░░█░░░░░ Houston, t=300;fo ----> ░░░░░░░░ <---- we have a r(x=h;x- ----> ░█░░░░░█ clear path -;)for(y ----> █░█░░░█░ =h;y--;E ----> ░░░░██░░ .fillRec ----> █░░░░░░░
As mentioned above, the camera moves on a straight line, from left to right, indefinitely and looks alternatively left and right by as much as 0.125 radians as time goes by.
In order to save 4 bytes with the trigonometry code where two
Math.sin are needed, an alias is created for
Math.cos alone. Adding 8 to the angle gives a decent approximation of
Math.sin. The margin of error is 0.15 radians.
Here we use some good old brute force: A fixed step raymarcher.
Unlike Wolfenstein 3D which is really processed in 2D and needs a single pass per vertical line on the screen, Wolfesteiny operates fully in 3D. This is a curse and a blessing because it's far less efficient, but on the other hand it is far more compact and trivial to implement.
Rays are cast from the origin of the camera in the view frustrum, and for each pixel of the rendering surface, we walk slowly along each ray to check whether we are between the floor and ceiling and haven't hit a solid cell in the map. The further the rays is walked, the bigger and darker the shade of grey for that pixel.
for(x=h;x--;)for(y=h;y--;/* draw a shade of grey D in x,y */)for(D=0;'.'<F[/* position of the ray in the map */]&&D<8;b=d)D+=.1
The outmost loops go through each x,y pixel and draw the resulting shade of grey. The inner loop walks along the ray corresponding to the x,y pixel until it hits a solid cell in the map.
The floor and ceiling collisions i checked using the integer coordinate of the Y component of the ray. When it is falsy, zero, the ray is between the floor and ceiling. Otherwise the ray reached the floor or ceiling and we can 'jump' to the position of a solid cell in the map:
D*y/h-D/2|0?1:/* ... */
The other components are computed and composed like this to check the map:
Which really means
X | Y << 3, but let's break this down:
// the angle the camera looks at is cA = Q(t) / 8 // the angle the current ray is rA = x / h - .5 + cA // D represents how far we have walked // along the ray which gives us X = t + D * Q(rA) Y = 3.5 + D * Q(rA - 8)
The default resolution of a Canvas is 300x150. Setting either the width or height clears and resets the Canvas so to maximize the resolution at the lowest cost, it is best to simply set the height to 300 at each frame.
Thus our canvas is 300x300 but we can not walk 9,000 rays for each frame. By dividing this by 4 we end up casting, and walking, 75x75 = 5,625 rays. Since we walk in 0.1 increments until we reach a solid cell or reached a distance of 8, this account for up to 75x75x80 = 450,000 tests per frame.
The shades of grey
fillStyle of Canvas is black. There is no space to change it so we need to find a way to get more colors. The shades of grey are done by drawing axis aligned rectangle with a fractional width and height. The sub-pixel dimensions introduce anti-aliasing which results in a semi transparent black edges. Simple. On top of that the animation and the dithering improves the illusion of several shades of grey.
Like in MINI DISTRICT, we check the integer X coordinate of the ray before and after hitting a wall to know whether we hit a wall facing Noth-South or East-West. You remember the main loop ?
The important parts here are the
b=d which store and compare the consecutive integer X coordinate of the ray.
Why the variable names
d ? Because it looks like a cute owl when you compare them:
b-d. That's why.
Missing anything ?
If any piece of the puzzle appears to be missing, I invite you to read the posts about TEA STORM and MINI DISTRICT which are the foundation of WOLFENSTEINY and cover additional dirty details.
As usual for my demoscene productions, WOLFENSTEINY is available on Pouet.net where any comments and thumbs up are appreciated. Hope you like this little homage.
Other recent experiments
There are many experiments and projects like WOLFENSTEINY to discover other here.
- JSCONF ASIA TALK TINY AUDIO-VISUAL DEMOS
- IMPOSSIBLE ROAD
- MANDELBROT TRACER