once upon a time I stumbled upon a technique called…
bitwise autotilingI learned about it way back when I was just a kid building 2D Minecraft clones using Construct 2, and I wanted my terrain to look nice as it does in Terraria
now given a tileset, such as the one below that I drew a while ago, we can assign each tile to a set of cardinal directions. I’ll indicate where there’s a connection between individual tiles with the letters N, E, S, W, standing for the cardinal directions North, East, South, and West.
<ul class=”tileset-demo”> <li class=”full-image”> <img alt=”a 16-tile tileset of 8x8 pixel metal” src=”/static/pic/01HPHVDRV0F0251MD0A2EG66C4-tilemap-heavy-metal-16+pixel+width160.png”> </li> <li class=”tileset-pieces”> <span class=”metal x-0 y-0”><span class=”east”>E</span><span class=”south”>S</span></span> <span class=”metal x-1 y-0”><span class=”east”>E</span><span class=”south”>S</span><span class=”west”>W</span></span> <span class=”metal x-2 y-0”><span class=”south”>S</span><span class=”west”>W</span></span> <span class=”metal x-3 y-0”><span class=”south”>S</span></span> <span class=”metal x-0 y-1”><span class=”east”>E</span><span class=”south”>S</span><span class=”north”>N</span></span> <span class=”metal x-1 y-1”><span class=”east”>E</span><span class=”south”>S</span><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-2 y-1”><span class=”south”>S</span><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-3 y-1”><span class=”south”>S</span><span class=”north”>N</span></span> <span class=”metal x-0 y-2”><span class=”east”>E</span><span class=”north”>N</span></span> <span class=”metal x-1 y-2”><span class=”east”>E</span><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-2 y-2”><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-3 y-2”><span class=”north”>N</span></span> <span class=”metal x-0 y-3”><span class=”east”>E</span></span> <span class=”metal x-1 y-3”><span class=”east”>E</span><span class=”west”>W</span></span> <span class=”metal x-2 y-3”><span class=”west”>W</span></span> <span class=”metal x-3 y-3”></span> </li> </ul>
note that the state of connection for a given cardinal direction can be represented using two values: connected, and not connected. two values make one bit, so we can pack these four connection states into four bits, and use that as an array index!
for that to work though, we need to rearrange our tilemap somewhat such that we can index into it easily using our integer. assuming we pack our bits as
NWSE
(bit 0 is east, each next bit we go clockwise), therefore the final arrangement is this:<div class=”horizontal-tile-strip”> <span class=”metal x-3 y-3”></span> <span class=”metal x-0 y-3”><span class=”east”>E</span></span> <span class=”metal x-3 y-0”><span class=”south”>S</span></span> <span class=”metal x-0 y-0”><span class=”east”>E</span><span class=”south”>S</span></span> <span class=”metal x-2 y-3”><span class=”west”>W</span></span> <span class=”metal x-1 y-3”><span class=”east”>E</span><span class=”west”>W</span></span> <span class=”metal x-2 y-0”><span class=”south”>S</span><span class=”west”>W</span></span> <span class=”metal x-1 y-0”><span class=”east”>E</span><span class=”south”>S</span><span class=”west”>W</span></span> <span class=”metal x-3 y-2”><span class=”north”>N</span></span> <span class=”metal x-0 y-2”><span class=”east”>E</span><span class=”north”>N</span></span> <span class=”metal x-3 y-1”><span class=”south”>S</span><span class=”north”>N</span></span> <span class=”metal x-0 y-1”><span class=”east”>E</span><span class=”south”>S</span><span class=”north”>N</span></span> <span class=”metal x-2 y-2”><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-1 y-2”><span class=”east”>E</span><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-2 y-1”><span class=”south”>S</span><span class=”west”>W</span><span class=”north”>N</span></span> <span class=”metal x-1 y-1”><span class=”east”>E</span><span class=”south”>S</span><span class=”west”>W</span><span class=”north”>N</span></span> </div>
packing that into a single tilesheet, or rather tile strip, we get this image:
hint: you can actually just use the original image, but use a lookup table from these indices to (x, y) coordinates. this makes creating the assets a lot easier! (at the expense of some CPU time, though it is totally possible to offload tilemap rendering to the GPU - in that case it barely even matters.)
in JavaScript, drawing on a
<canvas>
using bitwise autotiling would look like this:javascript for (let y = 0; y < tilemap.height; ++y) { for (let x = 0; x < tilemap.width; ++x) { // Assume `tilemap.at` is a function which returns the type of tile // stored at coordinates (x, y). let tile = tilemap.at(x, y);
// We need to treat some tile as an empty (fully transparent) tile. // In our case that’ll be 0. if (tile != 0) { let tileset = tilesets[tile];
// Now it’s time to represent the tile connections as bits. // For each cardinal direction we produce a different bit value, or 0 if there is // no connection: let connectedWithEast = shouldConnect(tile, tilemap.at(x + 1, y)) ? 0b0001 : 0; let connectedWithSouth = shouldConnect(tile, tilemap.at(x, y + 1)) ? 0b0010 : 0; let connectedWithWest = shouldConnect(tile, tilemap.at(x - 1, y)) ? 0b0100 : 0; let connectedWithNorth = shouldConnect(tile, tilemap.at(x, y - 1)) ? 0b1000 : 0; // Then we OR them together into one integer. let tileIndex = connectedWithNorth | connectedWithWest | connectedWithSouth | connectedWithEast;
// With that, we can draw the correct tile. // Our strip is a single horizontal line, so we can assume let tilesetTileSize = tileset.height; let tilesetX = tileIndex * tilesetTileSize; let tilesetY = 0; ctx.drawImage( tilesets[tile], tilesetX, tilesetY, tilesetTileSize, tilesetTileSize, x * tileSize, y * tileSize, tileSize, tileSize, ); } } }
and that gives us this result:
<canvas is=”tairu-editor” data-tilemap-id=”bitwiseAutotiling” data-tile-size=”40” > Your browser does not support <canvas>. <img class=”resource” src=”/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+width640.png” data-tairu-tileset=”1”> </canvas>
but if you play around with it (or have already played around with it, and are therefore left with a non-default tilemap)
…something seems awful about it doesn’t it?
something’s off about the corners. let me give you a fresh example to illustrate what I mean:
<canvas is=”tairu-editor” data-tilemap-id=”bitwiseAutotilingChapter2” data-tile-size=”40” > Your browser does not support <canvas>. <img class=”resource” src=”/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+width640.png” data-tairu-tileset=”1”> </canvas>
the solution to that is to introduce more tiles to handle these edge cases.
to represent the corners, we’ll turn our four cardinal directions…
<ul class=”directions-square”> <li class=”east”>E</li> <li class=”south”>S</li> <li class=”west”>W</li> <li class=”north”>N</li> </ul>
into eight ordinal directions:
<ul class=”directions-square”> <li class=”east”>E</li> <li class=”south-east”>SE</li> <li class=”south”>S</li> <li class=”south-west”>SW</li> <li class=”west”>W</li> <li class=”north-west”>NW</li> <li class=”north”>N</li> <li class=”north-east”><a href=”https://github.com/NoiseStudio/NoiseEngine/” title=”NoiseEngine????”>NE</a></li> </ul>
you might think that at this point we’ll need 8 bits to represent our tiles, and that would make…
256 tiles!?
nobody in their right mind would actually draw 256 separate tiles, right? RIGHT???
…right! if you experiment with the bit combinations, you’ll quickly find out that there is no difference if, relative to a single center tile, we have tiles on the corners:
<canvas is=”tairu-editor” data-tilemap-id=”bitwiseAutotilingCorners” data-tile-size=”40” > Your browser does not support <canvas>. <img class=”resource” src=”/static/pic/01HPMMR6DGKYTPZ9CK0WQWKNX5-tilemap-heavy-metal-bitwise-16+pixel+width640.png” data-tairu-tileset=”1”> </canvas>
these should all render the same way, despite technically having some new neighbors.
what we can do about this is to ignore corners whenever zero or one of the tiles at their cardinal directions is connected - for example, in the case of
E | SE | S
:<ul class=”directions-square e-s”> <li class=”east”>E</li> <li class=”south-east”>SE</li> <li class=”south”>S</li> </ul>
we can completely ignore what happens in the northeast, northwest, and southwest, because the tile’s cardinal directions do not fully contain any of these direction pairs.
we can verify this logic with a bit of code; with a bit of luck, we should be able to narrow down our tileset into something a lot more manageable.
we’ll start off by defining a bunch of variables to represent our ordinal directions:
javascript ordinal-directions export const E = 0b0000_0001; export const SE = 0b0000_0010; export const S = 0b0000_0100; export const SW = 0b0000_1000; export const W = 0b0001_0000; export const NW = 0b0010_0000; export const N = 0b0100_0000; export const NE = 0b1000_0000; export const ALL = E | SE | S | SW | W | NW | N | NE;
as I’ve already said, we represent each direction using a single bit.
now we can write a function that will remove the aforementioned redundancies. the logic is quite simple - for southeast, we only allow it to be set if both south and east are also set, and so on and so forth.
javascript ordinal-directions // t is a tile index; variable name is short for brevity export function removeRedundancies(t) { if (isSet(t, SE) && (!isSet(t, S) || !isSet(t, E))) { t &= ~SE; } if (isSet(t, SW) && (!isSet(t, S) || !isSet(t, W))) { t &= ~SW; } if (isSet(t, NW) && (!isSet(t, N) || !isSet(t, W))) { t &= ~NW; } if (isSet(t, NE) && (!isSet(t, N) || !isSet(t, E))) { t &= ~NE; } return t; }
we could also use the approach I mentioned briefly here, which involves introducing a lookup table - which sounds reasonable, so let’s do it!
I don’t want to write the lookup table by hand, so let’s generate it! I’ll reuse the redundancy elimination code from before to make this easier.
then we’ll turn that array upside down… in other words, invert the index-value relationship, so that we can look up which X position in the tile strip to use for a specific connection combination.
remember that our array has only 256 values, so it should be pretty cheap to represent using a
Uint8Array
:javascript ordinal-directions export let connectionBitSetToX = new Uint8Array(256); for (let i = 0; i < xToConnectionBitSet.length; ++i) { connectionBitSetToX[xToConnectionBitSet[i]] = i; }
and there we go! we now have a mapping from our bitset to positions within the tile strip. try to play around with the code example to see which bitsets correspond to which position!
javascript ordinal-directions console.log(connectionBitSetToX[E | SE | S]);
output ordinal-directions 4
TODO: The value from the previous output should not leak into this one. how do we do this? do we emit extra
pushMessage
calls inbetween the editors so that they know when to end? maybe use aclassic
context instead of a module? or maybe have a way of sharing data between outputs? (return value?)
bitwise autotiling is a really cool technique that I’ve used in plenty of games in the past.
as I mentioned before, I’ve known it since my Construct 2 days, but when it comes to any released games [Planet Overgamma] would probably be the first to utilize it properly.
TODO video of some Planet Overgamma gameplay showing the autotiling in action
this accursed game has been haunting me for years since; there have been many iterations. the autotiling source code of the one in the video can be found here.