In the last article, I described a way to autotile multiple biomes using a minimal set of mask shapes. I used a custom map for testing. This time, I use some shaders to generate the a nice big set of masks. In particular, I can generate for example 32 variations of each of the 4 shapes at 256x256 resolution. As we have 1 shape per RGBA texture component (our masks are grayscale), we need 32 RGBA textures, or a single 32-slice array. Stiching them up, the procedural masks look like this: (rows: variations, columns: shapes)
These masks are generated using Simplex noise, and then they are post-processed to remove floating islands. Here's how:
- We know that each shape contains 1 or 2 white regions and always 1 black region
- Detect all the black regions, sort them by area, and replace all but the largest with white (so we satisfy "always 1 black region" criterion)
- Detect all white regions, sort them by area, and replace all but the largest 1 (or 2) with black (so we satisfy "always 1 or 2 white region" criterion
Here are the steps visually: left is the original image, middle is with extraneous black areas removed, right is the final, with the extraneous white areas removed:
At this stage, we calculate the distance field for each of the masks. The distance field is 256x256 at this point. The maximum distance in the distance field is the length of the diagonal diag = 256*sqrt(2); we normalize the values in the distance field from (-diag,diag) to (0,1), to be resolution independent. We now downsample the distance field to 32x32, so that it can still reconstruct the shape nicely. The data is stored in an RGBA8 texture. If each variation is an array slice, we end up with a texture array 32x32xN. To give some perspective, for 64 variations we need 32*32*64*4 bytes = 256K of memory, which is very little. Add a bit of extra for the mipmaps (which are good for filtering when zooming out further), and we're settled with the biome masks.
Rendering the biome masks
Last time I described a way to render the masked, by rendering a subset of tiles per layer. This is far from optimal (it was approach v1 after all). So, here's a better one:
- We observe that evert tile has to be rendered (duh). That means, we need a dense 2D data structure, with tile data per element. So, tile positions are now implicit.
- We observe that we have up to 4 layers per tile. The info that we need per layer is the layer index (4 bits), the mask index (3 bits) and the transform index (3 bits). That makes 10 bits per layer, so 40 bits in total. So we place the data in a 64-bit data structure ( e.g. RGBA16 or RG32) and we have 22 bits to spare.
Now we render the visible grid, and we sample this data structure to reconstruct the mask. The pseudocode is roughly as follows:
for each pixel: calculate tile index and offset in tile shift output position by half a tile // for corner offset sample autotile data based on tile index set output color as 0 for each valid layer transform tile offset using layer transform sample mask using transformed coordinates and mask index calculate color based on layer blend output color with current color based on mask value
River (and road) masks
River masks are slightly different to biome masks and have the following characteristics:
- The tiles where we need river masks are few: for my map, it was 1.5% of the total tiles.
- It is not beneficial any more to use corner offsets.
- There is no diagonal river connection.
- All river tiles connect to at least one river tile.
- There is always a source/origin tile for rivers. The origin tile is always connected to one other tile.
Given the above, we realize that we really need 5 different masks: origin, line, corner, t-junction and cross. Below is a list of examples:
We follow the same process as with the biome masks: we remove extraneous white/black regions, calculate distance fields and downsample to 32x32.
Here's also a video that demonstrates all the mask shapes, procedurally generated, parameterized by time:
As you can see, for the river masks there are typically big black holes in the middle, but they are filled out by the process I described earlier.
River and road rendering
The process is a bit different to the biome mask rendering. Now we have a sparse set of tiles that contain river/road data. The tile data required are 3 bits for the mask index, 3 bits for the transform, and 10 bits for each of the x,y coordinates of the tile (I'm using 512x512 maps for the overworld and I doubt I'd use 2048 or larger). The pseudocode for rendering is similar and a bit simpler compared to the biome mask rendering: for a tile, we unpack the autotile data, we calculate the output position based on the x,y values, and we sample the mask using transformed coordinates.
The river/road render passes take place immediately after the biome mask pass, as they use a sparse tile renderer (biome masks rendering uses a dense tile renderer) and they don't use corner offsets.
Putting it all together
Here's a video that shows all mask on the map, compared to the single color per pixel:
As a note, the original single-color-per-pixel has some additional color variation based on the vegetation density, that the new masked version does not have have yet. Also, I think there's an indexing bug for the variations, as I should have 64 different variations per shape but we can see the occasional repetition.
TODOs for next are coastal water animation utilising the distance field, color variation of the biomes (they still look quite flat) and prop locations per tile for placement of trees, etc.