So, we have a procedurally generated biome map, where each pixel is an individual biome. If we zoom in, it's obviously quite pixelated. If we add sprites, it just doesn't look right (besides the lighting and colors)
We have reasonably detailed sprites on a single-colored squares. It's just ugly. We need to add some texture/detail.
Enter auto-tiling (or transition tiles): a method to automatically place tiles with texture so that each tile matches perfectly with their neighbours. It's a bit of a wild west out there in terms of approaches, so here are some resources (or resource lists) that I found useful:
Quite a few.
There are two main ways to consider art when using autotiles: using masks, or using premade textures. A good example is shown here:
Blend masks example
The premade tiles have the obvious benefit that they can be very nicely done, but of course they are tied to the content they represent. The blend masks do not look as good, but are easier to develop, and they are more flexible in terms of what textures we want to seamlessly mix. I decided to use masks as I want transitions between any biome: for 16 biome types, that's 120 unique combinations. It's not an option to ask an artist to develop 120 different autotiles, that needs quite a bit of money and time. And also, that would have no variation; each autotile would be replicated all over the place, so it would be easy to distinguish patterns.
The first naive thought that comes to mind (and I went with it for a while actually) is "ok, we have a tile, it is neighbour to 4 or 8 other tiles, so generate masks according to that relationship". Example here. As one can see, the 4-connected version is less interesting than the 8-connected version (and we don't want less-interesting), but the 8-connected version results in a lot of combinations! So what do we do? Well, we shift the grid. This way, we always have 4 potentially different tiles (quarters of them anyway)
Below, we shift the whole grid top-left by half a tile. Now, each grid cell (red) always contains parts of 4 tiles.
While this is mentioned in a few articles, it's demonstrated perfectly in a non-technical article, here. That's what sold me, as I find the results amazing!
Reducing unique combinations
So, that's what I mostly found so far in reference material. Now, a 2x2 grid as described can contain 4 different biomes. That's 4 bits, therefore 16 possible total combinations/arrangements. Here's how they look like (source):
In the "16 most basic tiles" above, we can observe the following:
- No 16 can be expressed by transforming 15 (180 deg rotation)
- No 11,13,14 can be expressed by transforming 10 ( 90,180, 270 deg rotation)
- No 3,9,7 can be expressed by transforming 1 ( 90,180, 270 deg rotation)
- No 2,6,8 can be expressed by transforming 4 ( 90,180, 270 deg rotation)
- No 5,12 contains no spatially varying data
This implies that the only unique tiles are 1,4,10,15,5,12. Furthermore, the only unique tiles with spatially varying data are 1,4,10,15. So, that is 4 tiles instead of 16. We can arrange such a mask of 4 tiles like this:
This has a nice continuous shape, if for example we want to ask an artist to draw some of those. Note that with this arrangement, the transformation will differ, as now the masks are already transformed compared to what I showed above. What's really important is that the amount of white vs black at the borders that contain both needs to always match, so that tiles are seamlessly combined. In my case above, I'm splitting them at 50%, but that's of course configurable. What I'm not going to cover, as I've given it some thought and gets very complicated, is to support variable black/white border percentages, ensuring that they match There are many more complications involved and I'm not sure if it's worth it in the end.
So, now we have 4 unique combinations. These can be nicely stored in an RGBA texture (one mask per channel) by converting the above 1x4 tile image. In the shader, given a mask value in [0,15], we effectively do the following:
Most of the above can be done in the vertex shader, whereas the last two steps (sampling the texture and getting the component) need to be done in the pixel shader. So, it's quite cheap.
So, we have a method to render tiles given a very small number of masks. How do we render the tiles? Here's the naive approach, for a 512x512 biome map:
- We have 16 biome layers, so I assign each a priority. E.g. shallow water covers coast. Water covers shallow water and coast. Deep water covers water, shallow water and coast. And so on.
- For each layer, we generate tile render data as follows:
- For each tile corner in the biome (513x513 grid of points)
- Sample the 4 adjacent biome types (clamp to border if out of bounds)
- Create the mask where we set 1 if the layer is equal or higher priority than current, or 0 if the layer is of lower priority than current
- Based on the mask value, calculate unique mask index and transform, and store in this tile's render data
- For each tile corner in the biome (513x513 grid of points)
So, now we have a list of 513x513x16 tiles = 4.21 million. That's quite a lot. But as I said, that's the naive version. Observations:
- When the unique mask index corresponds to constant 0 (mask_unique index == 4), we don't need to consider the tile for rendering.
- When all of the four biome values in a tile are of higher priority than the current layer, this means that the tile will be completely covered by higher priority layer tiles, and therefore we don't need to render it.
By applying these two, for my test map I reduced the number of tiles to 0.4 million, which is 10x better. Of course, that's still a lot, but it doesn't take into account any spatial hierarchy and other optimisations that could be done.
Here are some examples using the above un-nice mask. Zoomed-out:
Ok, so my mask looks bad, and there's little to none variation, so you can see patterns everywhere.
Using 256x256 masks, a single RGBA texture needs 256K of memory. We can have a texture array of such masks, using however many layers we can afford memory-wise. In runtime, we can select the layer based on various conditions. E.g. some texture layers could contain transition masks for particular biomes, or more generally, we can select a layer based on a function of the tile coordinates.
Next post will be about procedurally-generating lots of masks, using distance fields versus using binary masks, and also determination of locations for placement of props.