treehouse/content/programming/blog/tairu.tree
2024-07-24 18:20:12 +02:00

1062 lines
42 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%% title = "tairu - an interactive exploration of 2D autotiling techniques"
scripts = [
"treehouse/components/literate-programming.js",
"treehouse/vendor/codejar.js",
]
styles = ["page/tairu.css"]
% id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.
% id = "01HPD4XQPWPDBH6QQAZER7A05G"
- in Terraria, you can fully modify the terrain however you want, and the tiles will connect to each other seamlessly.
% id = "01HPD4XQPW8HE7681P7H686X4N"
- TODO: short videos demoing this here
% id = "01HPD4XQPWJBTJ4DWAQE3J87C9"
- once upon a time I stumbled upon a technique called…
% id = "01HQ162WWA1KXZPBDWJXSCQA1D"
- ### bitwise autotiling
% id = "01HPD4XQPW6VK3FDW5QRCE6HSS"
+ I learned about it way back when I was just a kid building 2D Minecraft clones using [Construct 2](https://www.construct.net/en/construct-2/manuals/construct-2), and I wanted my terrain to look nice as it does in Terraria
% id = "01HPD4XQPWJ1CE9ZVRW98X7HE6"
- Construct 2 was one of my first programming experiences and the first game engine I truly actually liked :smile:
% id = "01HQ162WWAMCPC5M88QAXHX4BT"
- so to help us learn, I made a little tile editor so that we can experiment with rendering tiles! have a look:
{% this could probably be written with some clever generator magic, but I'm too lazy for that %}
`<noscript>`{=html}
(…though you will need to enable JavaScript to try it out.
seriously, pinky promise I won't ever track you!
inspect the source code if you wanna.
if not, you will have to deal with static pictures.
but just keep in mind this was supposed to be an _*interactive*_ exploration of autotiling techniques.
cheers!)
`</noscript>`{=html}
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
import { TileEditor } from "tairu/editor.js";
export const tilemapSquare = Tilemap.parse(" x", [
" ",
" xxx ",
" xxx ",
" xxx ",
" ",
]);
new TileEditor({
tilemap: tilemapSquare,
tileSize: 40,
});
```
{:program=tairu :placeholder=01HQ47ZX7520PJNPJ75M793R5G}
```output
```
% id = "01HQ162WWAC3FN565QE3JAB87D"
- `Tilemap` is a class wrapping a flat [`Uint8Array`][Uint8Array] with a `width` and a `height`, so that we can index it using (x, y) coordinates.
{:program=tairu}
```javascript
console.log(tilemapSquare.at(0, 0));
console.log(tilemapSquare.at(3, 1));
```
{:program=tairu}
```output
0
1
```
[Uint8Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
% id = "01HQ162WWA090YW5BR1XW68XJN"
- `at` has a `setAt` counterpart which sets tiles instead of getting them.
% id = "01HQ162WWAMD68SY56P7TVT2DJ"
- `TileEditor` provides a graphical editor for a `Tilemap` based on a `<canvas>`.
% id = "01HQ162WWABTFQ0J83C4VZYZB5"
- this editor is _Certified Battery Efficient™_, so it won't redraw unless it needs to!\
we'll need to keep this in mind for later when we try to draw images, which may not be loaded during the initial draw.
% id = "01HQ162WWA8Y1AD22MSN71V2E4"
- to kick this off, let's set off a goal. I would like the tiles in our little renderer to connect together, like this:
![red rectangle with a black outline, made out of 3x3 tiles][pic:01HPYW5SNTY0Z0ENDE5K3XWMTH]
% id = "01HQ162WWAZV559ABQD1NVXPMA"
- let's break this down into smaller steps. drawing a border around the rectangle will involve:
% id = "01HQ162WWATV30HXGBQVWERP2M"
- determining _on which tiles_ to draw it,
% id = "01HQ162WWAA0V0SS0D1Y38BDS1"
- determining _where in these tiles_ to draw it,
% id = "01HQ162WWAGBCBDYF4VH26MX1B"
- and actually drawing it!
% id = "01HQ162WWA2PNGVV075HR3WMER"
- so let's zoom in a bit and look at the tiles one by one. in particular, let's focus on _these_ two tiles:
![the same red rectangle, now with a focus on the northern tile at its center][pic:01HPYWPJB1P0GK53BSJFJFRAGR]
% id = "01HQ162WWAYDS6CSD3T102NA9X"
- notice how the two highlighted tiles are _different._ therefore, we can infer we should probably connect together any tiles that are _the same_.
% id = "01HQ162WWATDD86D4GY7RMT0BZ"
- knowing that, we can extract the logic to a function:
{:program=tairu}
```javascript
export function shouldConnect(a, b) {
return a == b;
}
```
% id = "01HQ162WWA9M6801Q0RNRSF09H"
+ now, also note that the border around this particular tile is only drawn on its _northern_ edge -
therefore we can infer that borders should only be drawn on edges for whom `shouldConnect(thisTile, adjacentTile)` is *`false`* (not `true`!).
a tile generally has four edges - east, south, west, north - so we need to perform this check for all of them, and draw our border accordingly.
% id = "01HQ162WWAM5YYQCEXH791T0E9"
- you might be wondering why I'm using this particular order for cardinal directions - why not [north, south, east, west]? or [north, east, south, west]?
% id = "01HQ162WWABJ696HCJ09WDC0NX"
- the reason comes from math - `[cos(0) sin(0)]` is a vector pointing rightwards, not upwards!
and I chose clockwise order, because that's how the vector rotates as we increase the angle, in a coordinate space where +Y points downward - such as the `<canvas>` coordinate space.
% id = "01HQ162WWABNXV4N2AHZBQC5B7"
- this choice yields some nice orderliness in the code that handles fetching tiles for connections - first you check `+X`, then `+Y`, then `-X`, and then `-Y` -
which my pedantic mind really appreciates :ahyes:\
as `X` is first alphabetically, so checking `Y` first would feel wrong.
% id = "01HQ162WWA5W8NXSXVZY3BBQ0H"
- to do that, I'm gonna override the tile editor's `drawTilemap` function - as this is where the actual tilemap rendering happens!
{:program=tairu}
```javascript
import { TileEditor } from "tairu/editor.js";
export class TileEditorWithBorders extends TileEditor {
constructor({ borderWidth, ...options }) {
super(options);
this.borderWidth = borderWidth;
this.colorScheme.borderColor = "#000000";
}
drawTilemap() {
// Let the base class render out the infill, we'll just handle the borders.
super.drawTilemap();
this.ctx.fillStyle = this.colorScheme.borderColor;
for (let y = 0; y < this.tilemap.height; ++y) {
for (let x = 0; x < this.tilemap.width; ++x) {
let tile = this.tilemap.at(x, y);
// We only want to draw non-empty tiles, so skip tile 0.
if (tile == 0) {
continue;
}
// Check which of this tile's neighbors should _not_ connect to it.
let disjointWithEast = !shouldConnect(tile, this.tilemap.at(x + 1, y));
let disjointWithSouth = !shouldConnect(tile, this.tilemap.at(x, y + 1));
let disjointWithWest = !shouldConnect(tile, this.tilemap.at(x - 1, y));
let disjointWithNorth = !shouldConnect(tile, this.tilemap.at(x, y - 1));
let { borderWidth, tileSize } = this;
let tx = x * tileSize;
let ty = y * tileSize;
// For each disjoint neighbor, we want to draw a border between us and them.
if (disjointWithEast) {
this.ctx.fillRect(tx + tileSize - borderWidth, ty, borderWidth, tileSize);
}
if (disjointWithSouth) {
this.ctx.fillRect(tx, ty + tileSize - borderWidth, tileSize, borderWidth);
}
if (disjointWithWest) {
this.ctx.fillRect(tx, ty, borderWidth, tileSize);
}
if (disjointWithNorth) {
this.ctx.fillRect(tx, ty, tileSize, borderWidth);
}
}
}
}
}
```
and here's the result:
{:program=tairu}
```javascript
new TileEditorWithBorders({
tilemap: tilemapSquare,
tileSize: 40,
borderWidth: 4,
});
```
{:program=tairu :placeholder=01HQ49TJZFMK719KSE16SG3F7B}
```output
```
% id = "01HQ162WWAAEKW1ECV5G3ZEY47"
- this looks pretty perfect - maybe sans corners, which I'll conveniently skip for now, because most games don't actually render graphics in a vectorial way like this!
instead, the more common way is to use a tileset - a big texture with a bunch of sprites to use for rendering each tile.
% id = "01HQ162WWACD5CD7GCZE53ZPD7"
- not only does this have the advantage of allowing for richer graphics, but it is also a lot easier to modify by artists, because you no longer need knowledge of graphics APIs to draw tiles.
% template = true
classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWAADKPDQE69W3QZG0M"
- for example, here's a tileset I drew for the 3rd iteration of my game [Planet Overgamma] - though tweaked a bit because I had never used it before writing this post :hueh:
![heavy metal sheet tileset from Planet Overgamma, made out of 16 tiles. it looks like heavy embossed sheets of metal, resembling steel in its heavyness][pic:01HPHVDRV0F0251MD0A2EG66C4]
[Planet Overgamma]: https://github.com/liquidev/planet-overgamma
% classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWAS502000K8QZWVBDW"
- we can split this tileset up into 16 individual tiles, each one 8 × 8 pixels; people choose various resolutions, I chose a fairly low one to hide my lack of artistic skill.
::: horizontal-tile-strip
[]{.metal .x-0 .y-0}
[]{.metal .x-1 .y-0}
[]{.metal .x-2 .y-0}
[]{.metal .x-3 .y-0}
[]{.metal .x-0 .y-1}
[]{.metal .x-1 .y-1}
[]{.metal .x-2 .y-1}
[]{.metal .x-3 .y-1}
[]{.metal .x-0 .y-2}
[]{.metal .x-1 .y-2}
[]{.metal .x-2 .y-2}
[]{.metal .x-3 .y-2}
[]{.metal .x-0 .y-3}
[]{.metal .x-1 .y-3}
[]{.metal .x-2 .y-3}
[]{.metal .x-3 .y-3}
:::
% classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWANBTYH1JJWCTZYYVN"
- the keen eyed among you have probably noticed that this is very similar to the case we had before with drawing procedural borders -
except that instead of determining which borders to draw based on a tile's neighbors, this time we'll determine which _whole tile_ to draw based on its neighbors!
::: horizontal-tile-strip
[[E]{.east} [S]{.south}]{.metal .x-0 .y-0}
[[E]{.east} [S]{.south} [W]{.west}]{.metal .x-1 .y-0}
[[S]{.south} [W]{.west}]{.metal .x-2 .y-0}
[[S]{.south}]{.metal .x-3 .y-0}
[[E]{.east} [S]{.south} [N]{.north}]{.metal .x-0 .y-1}
[[E]{.east} [S]{.south} [W]{.west} [N]{.north}]{.metal .x-1 .y-1}
[[S]{.south} [W]{.west} [N]{.north}]{.metal .x-2 .y-1}
[[S]{.south} [N]{.north}]{.metal .x-3 .y-1}
[[E]{.east} [N]{.north}]{.metal .x-0 .y-2}
[[E]{.east} [W]{.west} [N]{.north}]{.metal .x-1 .y-2}
[[W]{.west} [N]{.north}]{.metal .x-2 .y-2}
[[N]{.north}]{.metal .x-3 .y-2}
[[E]{.east}]{.metal .x-0 .y-3}
[[E]{.east} [W]{.west}]{.metal .x-1 .y-3}
[[W]{.west}]{.metal .x-2 .y-3}
[]{.metal .x-3 .y-3}
:::
% id = "01HQ162WWA4Z6KKWFV59BR4WD3"
- previously we represented which single border to draw with a single boolean.
now we will represent which single tile to draw with _four_ booleans, because each tile can connect to four different directions.
% id = "01HQ162WWAQ9GZ6JD8KESW4N53"
- four booleans like this can easily be packed into a single integer using some bitwise operations, hence we get _*bitwise autotiling*_ - autotiling using bitwise operations!
% id = "01HQ162WWAMBM8RXKQTN3D0XR2"
- now the clever part of bitwise autotiling is that we can use this packed integer _as an array index_ - therefore selecting which tile to draw can be determined using just a single lookup table! neat, huh?
% id = "01HQ162WWA0ZGZ97JZZBFS41TF"
- but because I'm lazy, and CPU time is valuable, instead of using an array I'll just rearrange the tileset texture a bit to be able to slice it in place using this index.
% id = "01HQ162WWAQQ99TRBDY5DCSW3Z"
- say we arrange our bits like this:
{:program=tairu}
```javascript
export const E = 0b0001;
export const S = 0b0010;
export const W = 0b0100;
export const N = 0b1000;
```
% classes.branch = "tileset-cardinal-directions-demo"
id = "01HQ162WWABANND0WGT933TBMV"
- that means we'll need to arrange our tiles like so, where the leftmost tile is at index 0 (`0b0000`) and the rightmost tile is at index 15 (`0b1111`):
::: horizontal-tile-strip
[]{.metal .x-3 .y-3}
[[E]{.east}]{.metal .x-0 .y-3}
[[S]{.south}]{.metal .x-3 .y-0}
[[E]{.east} [S]{.south}]{.metal .x-0 .y-0}
[[W]{.west}]{.metal .x-2 .y-3}
[[W]{.west} [E]{.east}]{.metal .x-1 .y-3}
[[W]{.west} [S]{.south}]{.metal .x-2 .y-0}
[[W]{.west} [E]{.east} [S]{.south}]{.metal .x-1 .y-0}
[[N]{.north}]{.metal .x-3 .y-2}
[[E]{.east} [N]{.north}]{.metal .x-0 .y-2}
[[S]{.south} [N]{.north}]{.metal .x-3 .y-1}
[[E]{.east} [S]{.south} [N]{.north}]{.metal .x-0 .y-1}
[[W]{.west} [N]{.north}]{.metal .x-2 .y-2}
[[E]{.east} [W]{.west} [N]{.north}]{.metal .x-1 .y-2}
[[S]{.south} [W]{.west} [N]{.north}]{.metal .x-2 .y-1}
[[E]{.east} [S]{.south} [W]{.west} [N]{.north}]{.metal .x-1 .y-1}
:::
% id = "01HQ162WWAJPW00XA25N0K6KS7"
- packing that into a single tileset, or rather this time, a _tile strip_, we get this image:
![horizontal tile strip of 16 8x8 pixel metal tiles][pic:01HPMMR6DGKYTPZ9CK0WQWKNX5]
% id = "01HQ162WWAT2ZC7T2P9ATD6WG2"
- now it's time to actually implement it as code! I'll start by defining a _tile index_ function as a general way of looking up tiles in a tileset.
% id = "01HQ162WWA0NRHBB6HP2RERNBK"
- I want to make the tile renderer a bit more general, so being able to attach a different tile lookup function to each tileset sounds like a great feature.
% id = "01HQ162WWA9PGSHH5E97RVE1PB"
- just imagine some game where glass connects to metal, but metal doesn't connect to glass - I bet that would look pretty great!
% id = "01HQ162WWAYJ4JCG3Z24SJR8S9"
- …but anyways, here's the basic bitwise magic function:
{:program=tairu}
```javascript
export function tileIndexInBitwiseTileset(tilemap, x, y) {
let tile = tilemap.at(x, y);
let tileIndex = 0;
tileIndex |= shouldConnect(tile, tilemap.at(x + 1, y)) ? E : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x, y + 1)) ? S : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x - 1, y)) ? W : 0;
tileIndex |= shouldConnect(tile, tilemap.at(x, y - 1)) ? N : 0;
return tileIndex;
}
```
% template = true
id = "01HQ162WWAS813ANMBG1PWDZHC"
- we'll define our tilesets by their texture, tile size, and a tile indexing function. so let's create an object that will hold our tileset data:
{:program=tairu}
```javascript
// You'll probably want to host the assets on your own website rather than
// hotlinking to others. It helps longevity!
let tilesetImage = new Image();
tilesetImage.src = "{% pic 01HPMMR6DGKYTPZ9CK0WQWKNX5 %}";
export const heavyMetalTileset = {
image: tilesetImage,
tileSize: 8,
tileIndex: tileIndexInBitwiseTileset,
};
```
% id = "01HQ162WWA0SC2GA7Y3KJE0W5F"
- with all that, we should now be able to write a tile renderer which can handle textures! so let's try it:
{:program=tairu}
```javascript
import { TileEditor } from "tairu/editor.js";
export class TilesetTileEditor extends TileEditor {
constructor({ tilesets, ...options }) {
super(options);
this.tilesets = tilesets;
// The image may not be loaded once the editor is first drawn, so we need to request a
// redraw for each image that gets loaded in.
for (let tileset of this.tilesets) {
tileset.image.addEventListener("load", () => this.draw());
}
}
drawTilemap() {
// We're dealing with pixel tiles so we want our images to be pixelated,
// not interpolated.
this.ctx.imageSmoothingEnabled = false;
for (let y = 0; y < this.tilemap.height; ++y) {
for (let x = 0; x < this.tilemap.width; ++x) {
let tile = this.tilemap.at(x, y);
if (tile == 0) {
continue;
}
// Subtract one from the tile because tile 0 is always empty.
// Having to specify a null entry at array index 0 would be pretty annoying.
let tileset = this.tilesets[tile - 1];
if (tileset != null) {
let { tileSize } = this;
let tileIndex = tileset.tileIndex(this.tilemap, x, y);
this.ctx.drawImage(
tileset.image,
tileIndex * tileset.tileSize, 0, tileset.tileSize, tileset.tileSize,
x * tileSize, y * tileSize, tileSize, tileSize,
);
}
}
}
}
}
```
% id = "01HQ162WWAS2HYF41MZNJ18BXC"
- drum roll please…
{:program=tairu}
```javascript
new TilesetTileEditor({
tilemap: tilemapSquare,
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
{:program=tairu :placeholder=01HQ49X8Z57FNMN3E79FYF8CMG}
```output
```
% id = "01HQ162WWA03JAGJYCT0DRZP24"
- it works! buuuut if you play around with it you'll quickly start noticing some problems:
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
export const tilemapEdgeCase = Tilemap.parse(" x", [
" ",
" xxx ",
" x x ",
" xxx ",
" ",
]);
new TilesetTileEditor({
tilemap: tilemapEdgeCase,
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
{:program=tairu :placeholder=01HQ49YDPQXYSAT5N6P241DG3C}
```output
```
% id = "01HQ162WWAB0AYSPGB4AEVT03Z"
- where did our nice seamless connections go!?
% id = "01HQ162WWA3Q095ZGXDFZ1V2Q1"
- ### thing is, it was never good in the first place
% id = "01HQ162WWARSVDRNHZE13ZF6W6"
- I'll be blunt: we don't have enough tiles to represent _corners_! like in this case:
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" xx ",
" x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
{:program=tairu :placeholder=01HQ49Z8JWR75D85DGHCB34K8E}
```output
```
% id = "01HQ1K39AS4VDW7DVTAGQ03WFM"
- have a closer look at the top-left tile:
![the above example, showing an L shape rotated 180°, with the top left corner highlighted][pic:01HQ167GJEPTKHAKAVNW3WN1SZ]
% id = "01HQ1K39AS6Y9XMJTMMQYTWRMC"
- it should kind of _"bend"_ to fit in with the tiles to the north and the south, but it doesn't :kamien:
% id = "01HQ1K39ASQQNF7B881SYJWRC7"
- so what if we made the tile look like _this_ instead:
![mockup showing that previous L-shape but with a real corner][pic:01HQ17GYEZSZCVRBFHP4HXAJV8]
% id = "01HQ1K39ASMKRMTXFV93FRHZTG"
- that sure as heck looks a lot nicer! but there's a problem: that tile, let's zoom in on it…
![that bent tile, and just _it_ alone][pic:01HQ183RANGH4S7VZSG1ZGH0S5]
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASR81NWMW8Q0MF8QMP"
- enhance!
{% NOTE djot: I don't there's a way to achieve this in Djot alone %}
``` =html
<ul class="directions-square bend">
<li class="east">E</li>
<li class="south">S</li>
</ul>
```
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASC5WTR2A2AJN85JK2"
- huh. interesting. it connects to the east and the south. so what about this tile -
``` =html
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south">S</li>
</ul>
```
% id = "01HQ1K39ASXYBH9QJH5Q0C45JZ"
- because it _also_ connects to the east and the south :thinking:
% id = "01HQ1K39ASW5PWS52NGA2X3M0P"
- seems like we'll need something to disambiguate the two cases - and what better thing to disambiguate with than _more bits_!
% classes.branch = "tileset-four-to-eight-demo"
id = "01HPQCCV4R5N97FJ1GS36HZJZ7"
- to represent the corners, we'll turn our four cardinal directions…
``` =html
<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:
``` =html
<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>
```
% id = "01HQ1K39ASFN94YDY1RWQYS12K"
- at this point with the four extra corners 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???*_
% template = true
id = "01HQ1K39AS11M1M4GQQ60NXTY6"
- …right! let's stick with the 16 tile version for a moment.
if we arrange the tiles in a diagnonal cross like this, notice how the tile in the center would have the bits `SE | SW | NW | NE` set, which upon first glance would suggest us needing a different tile -
but it looks correct!
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" ",
" x x ",
" x ",
" x x ",
" ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset],
});
```
{:program=tairu :placeholder=01HQ4A01MPE6JT5ZZFEN9S635W}
```output
```
% id = "01HQ1K39AS7CRBZ67N1VVHCVME"
- therefore there must be _some_ bit combinations that are redundant to others. let's find them!
% classes.branch = "tileset-four-to-eight-demo"
id = "01HQ1K39ASZPJ4E23EZ1XJ5J7K"
- let's pick one corner first, then generalize to all the other ones. I pick southeast!
``` =html
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south-east">SE</li>
<li class="south">S</li>
</ul>
```
% id = "01HQ1K39ASQTR054W0VWEAV2FS"
- in this case, if we remove the tile to the southeast, we get that bent tile from before:
``` =html
<ul class="directions-square bend">
<li class="east">E</li>
<li class="south">S</li>
</ul>
```
% id = "01HQ1K39AS6RGE6Z83T8MH1R0M"
- what we can learn from this is that for `E | S`, `ES` affects the result!
% id = "01HQ1K39ASVSAQ6F8ANEZE1WQ4"
- but if we add any other corner, nothing changes. heck, let's add all of them:
``` =html
<ul class="directions-square e-s">
<li class="east">E</li>
<li class="south-east">SE</li>
<li class="south">S</li>
<li class="south-west">SW</li>
<li class="north-west">NW</li>
<li class="north-east">NE</li>
</ul>
```
% id = "01HQ1K39AST8RQTVSCDV7FSH62"
- this combination is definitely redundant!
% id = "01HQ1K39AS8VHKHANJFKA4PQJ5"
- so it seems like for any two cardinal directions such as `E` and `S`, the ordinal direction that's a combination of the two -
in this case `ES` - only matters if both the cardinal direction bits are set!
% id = "01HPQCCV4R557T2SN7ES7Z4EJ7"
- 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.
% id = "01HPSY4Y19NQ6DZN10BP1KQEZN"
+ we'll start off by redefining our bits to be ordinal directions instead. I still want to keep the [nice orderliness][branch:01HQ162WWAM5YYQCEXH791T0E9] that comes with
arranging the bits clockwise starting from east, so if we want that we can't just extend the indices with an extra four bits at the top.
{:program=tairu}
```javascript
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;
```
% id = "01HPSY4Y19HPNXC54VP6TFFHXN"
- I don't know about you, but I find the usual C-style way of checking whether a bit is set extremely hard to read, so let's take care of that:
{:program=tairu}
```javascript
export function isSet(integer, bit) {
return (integer & bit) == bit;
}
```
% id = "01HPSY4Y1984H2FX6QY6K2KHKF"
- 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.
{:program=tairu}
```javascript
// t is an existing 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;
}
```
% id = "01HPSY4Y19HWQQ9XBW1DDGW68T"
- with that, we can find a set of all unique non-redundant combinations:
{:program=tairu}
```javascript
export function ordinalDirections() {
let unique = new Set();
for (let i = 0; i <= 0b1111_1111; ++i) {
unique.add(removeRedundancies(i));
}
return Array.from(unique).sort((a, b) => a - b);
}
```
% id = "01HPSY4Y19KG8DC4SYXR1DJJ5F"
- by the way, I find it quite funny how JavaScript's [`Array.prototype.sort`][sort] defaults to ASCII ordering _for all types._
even numbers! ain't that silly?
[sort]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
% id = "01HPSY4Y19V62YKTGK3TTKEB38"
- and with all the ingredients in the pot, we now _Let It Cook™_:
{:program=tairu}
```javascript
let dirs = ordinalDirections();
console.log(dirs.length);
```
{:program=tairu}
```output
47
```
% id = "01HPSY4Y194DYYDGSAT83MPQFR"
- forty seven! that's how many unique tiles we actually need.
% id = "01HPSY4Y19C303Z595KNVXYYVS"
- you may find pixel art tutorials saying you need forty _eight_ and not forty _seven_, but that is not quite correct -
the forty eighth tile is actually just the empty tile! saying it's part of the tileset is quite misleading IMO.
% id = "01HPSY4Y19TM2K2WN06HHEM3D0"
- phew… the nesting's getting quite unwieldy, let's wrap up this tangent and return back to doing some bitwise autotiling!
% id = "01HPSY4Y192FZ37K3KXZM90K9J"
- so in reality we actually only need 47 tiles and not 256 - that's a whole lot less, that's 81.640625% less tiles we have to draw!
% id = "01HPSY4Y19HEBWBTNMDMM0AZSC"
- and it's even possible to autogenerate most of them given just a few smaller 4x4 pieces - but for now, let's not go down that path.\
maybe another time.
% id = "01HPWJB4Y047YGYAP6XQXJ3576"
- so we only need to draw 47 tiles, but to actually display them in a game we still need to pack them into an image.
% id = "01HPWJB4Y0QX6YR6TQKZ7T1C2E"
- we _could_ use a similar approach to the 16 tile version, but that would leave us with lots of wasted space!
% id = "01HPWJB4Y0HKGSDABB56CNFP9H"
- think that with this redundancy elimination approach most of the tiles will never even be looked up by the renderer, because the bit combinations will be collapsed into a more canonical form before the lookup.
% id = "01HQ1K39ASM53P1E74HKRZ1T24"
- so instead of wasting space, we can compress the tiles into a compact strip, and use a lookup table from sparse tile indices to dense tile _positions_ within the strip.
% id = "01HPWJB4Y0F9JGXQDAAVC3ERG1"
- I don't want to write the lookup table by hand, so let's generate it!
% id = "01HPWJB4Y0HTV32T4WMKCKWTVA"
- we'll start by obtaining our ordinal directions array again:
{:program=tairu}
```javascript
export let xToConnectionBitSet = ordinalDirections();
```
% id = "01HPWJB4Y03WYYZ3VTW27GP7Z3"
- 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`]:
{:program=tairu}
```javascript
export let connectionBitSetToX = new Uint8Array(256);
for (let i = 0; i < xToConnectionBitSet.length; ++i) {
connectionBitSetToX[xToConnectionBitSet[i]] = i;
}
```
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
% id = "01HPWJB4Y0CWQB9EZG6C91A0H0"
- 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!
{:program=tairu}
```javascript
console.log(connectionBitSetToX[E | SE | S]);
```
{:program=tairu}
```output
4
```
% id = "01HPWJB4Y09P9Q3NGN59XWX2X9"
+ for my own (and your) convenience, here's a complete list of _all_ the possible combinations in order.
% id = "01HPWJB4Y01VJFMHYEC1WZ353W"
- {:program=tairu}
```javascript
function toString(bitset) {
if (bitset == 0) return "0";
let directions = [];
if (isSet(bitset, E)) directions.push("E");
if (isSet(bitset, SE)) directions.push("SE");
if (isSet(bitset, S)) directions.push("S");
if (isSet(bitset, SW)) directions.push("SW");
if (isSet(bitset, W)) directions.push("W");
if (isSet(bitset, NW)) directions.push("NW");
if (isSet(bitset, N)) directions.push("N");
if (isSet(bitset, NE)) directions.push("NE");
return directions.join(" | ");
}
for (let x in xToConnectionBitSet) {
console.log(`${x} => ${toString(xToConnectionBitSet[x])}`);
}
```
{:program=tairu}
```output
0 => 0
1 => E
2 => S
3 => E | S
4 => E | SE | S
5 => W
6 => E | W
7 => S | W
8 => E | S | W
9 => E | SE | S | W
10 => S | SW | W
11 => E | S | SW | W
12 => E | SE | S | SW | W
13 => N
14 => E | N
15 => S | N
16 => E | S | N
17 => E | SE | S | N
18 => W | N
19 => E | W | N
20 => S | W | N
21 => E | S | W | N
22 => E | SE | S | W | N
23 => S | SW | W | N
24 => E | S | SW | W | N
25 => E | SE | S | SW | W | N
26 => W | NW | N
27 => E | W | NW | N
28 => S | W | NW | N
29 => E | S | W | NW | N
30 => E | SE | S | W | NW | N
31 => S | SW | W | NW | N
32 => E | S | SW | W | NW | N
33 => E | SE | S | SW | W | NW | N
34 => E | N | NE
35 => E | S | N | NE
36 => E | SE | S | N | NE
37 => E | W | N | NE
38 => E | S | W | N | NE
39 => E | SE | S | W | N | NE
40 => E | S | SW | W | N | NE
41 => E | SE | S | SW | W | N | NE
42 => E | W | NW | N | NE
43 => E | S | W | NW | N | NE
44 => E | SE | S | W | NW | N | NE
45 => E | S | SW | W | NW | N | NE
46 => E | SE | S | SW | W | NW | N | NE
```
% id = "01HPWJB4Y0NMP35M9138DV3P8W"
- with the lookup table generated, we are now able to prepare a tile strip like before - except now it's even more tedious work arranging the pieces together :ralsei_dead:
anyways I spent like 20 minutes doing that by hand, and now we have a neat tile strip just like before, except way longer:
![horizontal tile strip of 47 8x8 pixel metal tiles][pic:01HPW47SHMSVAH7C0JR9HWXWCM]
% id = "01HPWJB4Y0J3DHQV5F9GD3VNQ8"
- now let's hook it up to our tileset renderer!
% id = "01HQ1M84GS09M7PMXFYHDPRTMT"
- since we already prepared the bulk of the framework before, it should be as simple as writing a new `tileIndex` function:
{:program=tairu}
```javascript
export function tileIndexInBitwiseTileset47(tilemap, x, y) {
let tile = tilemap.at(x, y);
let tileBitset = 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y)) ? E : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y + 1)) ? SE : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x, y + 1)) ? S : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y + 1)) ? SW : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y)) ? W : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x - 1, y - 1)) ? NW : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x, y - 1)) ? N : 0;
tileBitset |= shouldConnect(tile, tilemap.at(x + 1, y - 1)) ? NE : 0;
return connectionBitSetToX[removeRedundancies(tileBitset)];
}
```
% template = true
id = "01HQ1M84GS4C99VQZC4150CMDS"
- now we can write a new tileset descriptor that uses this indexing function and the larger tile strip:
{:program=tairu}
```javascript
// Once again, use your own link here!
let tilesetImage = new Image();
tilesetImage.src = "{% pic 01HPW47SHMSVAH7C0JR9HWXWCM %}";
export const heavyMetalTileset47 = {
image: tilesetImage,
tileSize: 8,
tileIndex: tileIndexInBitwiseTileset47,
};
```
% id = "01HQ1M84GS9CC8VR1BVDC15W50"
- and Drum Roll 2: Return of the Snare please…
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: Tilemap.parse(" x", [
" x ",
" x x ",
" xxx ",
" xx ",
" x ",
]),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
{:program=tairu :placeholder=01HQ4A11RRXEQ850598GFBJN0B}
```output
```
% id = "01HQ1M84GSCXTPGVPXY840WCQ6"
- it works perfectly!
% id = "01HQ1M84GSVBG9T94ZN9XTXX58"
- but honestly, this is a bit _boring_ if we're gonna build a game with procedural worlds.
% id = "01HQ1M84GSH0KTFFZET6GZZ4V2"
- heck, it's even boring for a level designer to have to lay out all the tiles manually -
introducing variations and what not, such that the world doesn't look too bland… there has to be a better way!
% id = "01HQ1M84GSE1N5WG88DGJZH0F8"
- and a better way… there is! but I'll get to that once my nap is over.
% id = "01HQ1M84GS0KJ9NA6GPS62RC95"
- for now, have a big editor to play around with. it's a lot of fun arranging the tiles in various shapes!
{:program=tairu}
```javascript
import { Tilemap } from "tairu/tilemap.js";
new TilesetTileEditor({
tilemap: new Tilemap(25, 16),
tileSize: 40,
tilesets: [heavyMetalTileset47],
});
```
{:program=tairu :placeholder=01HQ4A45WNAEJGCT2WDMQJHK14}
```output
```
:nap: <!--
% stage = "Draft"
id = "01HQ1M84GS3WKE2X6QV2SNQX46"
- TODO: next chapter! if you're reading this, you're in on it soon. that's quite sad, but YOU CAN STILL TURN BACK! I advise you to do that immediately.
You have been warned.
% id = "01HPD4XQPWPN6HNA6M6EH507C6"
+ but one day I found a really cool project called [Tilekit](https://rxi.itch.io/tilekit)
% id = "01HPD4XQPW11EQTBDQSGXW3S52"
+ (of course it's really cool, after all [rxi](https://github.com/rxi) made it)
% id = "01HPD4XQPWYHS327BV586SB085"
- for context rxi is the genius behind the Lua-powered, simple, and modular text editor [lite](https://github.com/rxi/lite) that I was using for quite a while
% id = "01HPD4XQPWJ9QAQ5MF2J5JBB8M"
- after a while I switched to a fork - [Lite XL](https://github.com/lite-xl/lite-xl), which had better font rendering and more features
% id = "01HPD4XQPWB11TZSX5VAAJ6TCD"
- I stopped using it because VS Code was just more feature packed and usable; no need to reinvent the wheel, rust-analyzer _just works._
% id = "01HPD4XQPW3G7BXTBBTD05MB8V"
- the LSP plugin for Lite XL had some issues around autocompletions not filling in properly :pensive:
it's likely a lot better now, but back then I decided this is too much for my nerves.
while tinkering with your editor is something really cool, in my experience it's only cool up to a point.
% id = "01HPD4XQPWV1BAPA27SNDFR93B"
- the cool thing with Tilekit is that it's _more_ than just your average bitwise autotiling - of course it _can_ do basic autotiling, but it can also do so much more
% id = "01HPD4XQPWM1JSAPXVT6NBHKYY"
classes.branch_children = "branch-quote"
- if I had to describe it, it's basically something of a _shader langauge for tilesets._ this makes it really powerful, as you can do little programs like
% id = "01HPD4XQPWE7ZVR0SS67DHTGHQ"
- autotile using this base tileset
% id = "01HPD4XQPW2BFZYQQ920SYHM9M"
- if the tile above is empty AND with a 50% chance
% id = "01HPD4XQPWJB7V67TS1M3HFCYE"
- then grass
% id = "01HPD4XQPWF7K85Z0CEK4WDDBZ"
- if the tile above is solid AND with a 10% chance
% id = "01HPD4XQPW5J3N6MVT9Z2W00S9"
- then vines
% id = "01HPD4XQPWGCMCEAR5Z9EETSGP"
- if the tile above is vines AND with a 50% chance
% id = "01HPD4XQPWP847T0EAM0FJ88T4"
- then vines
% id = "01HPSY4Y19FA2HGYE4F3Y9NJ57"
- well… it's even simpler than that in terms of graphical presentation, but we'll get to that.
% id = "01HPD4XQPWK58Z63X6962STADR"
- I mean, after all - bitwise autotiling is basically a clever solution to an `if` complexity problem, so why not extend that with more logic and rules and stuff to let you build more complex maps?
% id = "01HPJ8GHDFRA2SPNHKJYD0SYPP"
- of course Tilekit's solution is a lot more simple, streamlined, and user-friendly, but you get the gist.