📜 ⬆️ ⬇️

Autotiling: automatic tile transitions

Literally I just came across an article from the sandbox about the grid-tiling and decided to write my own analogue.
My transition allocation method is somewhat different from that mentioned in that article.
The beginning of this system was laid in the notorious game WarCraft III .

image

In this screenshot of the WC3 map editor, you can see the stitches, they are indicated by red arrows. Apparently, the logic of the tiles in this game is somewhat different than in most games. One tile here does not occupy a whole cell . It is located, as it were, at a point around which its corners are already drawn.

Especially well seen with the included grid.
')
image

Usually in such a situation it is proposed to divide the tile into 4 small ones. But there is one thing: what to do in such a case?

image

When are all 4 surrounding one quad tiles different? Here it is clearly visible that the lowermost tile occupies the most part.
After weighing all the pros and cons, I came to my own, quite specific, system. Add a new grid, a transition grid. In it, we can store, for example, the type int. In this case, it will be possible for us to write down for each quad of the tiles the 16 IDs of the surrounding 4 tiles with 16 transition variants. This is more than enough. No, if someone needs more ID - please use long. I decided that I would have enough of 16 auto-files for the game location, the rest will be without auto-transitions.

Next, we need a set of tiles. You can, of course, use a mask, but with a set of tiles, you will agree, with good skill (not me, no), you can achieve a very, very good picture.

image

I made this test suite myself. There are 12 transition options for one tile, you can add your own 4. I also reserved slots for future tile variations, as in WC3, but this part is quite easy and I will not describe it here.

Go to the programming part. First, let's describe the functions that will determine the desired bit mask for selecting the correct texture index. At once I will make a reservation, I am peculiar to choose rather non-standard solutions. Java + LWJGL will be used here.

This function will create a mask of bits for the quad. Bit 1 means that in this corner of the tile there is a tile adjacent to it (thus, it is possible to combine different tilesets of the same height). Oh yes. Height, about her, I forgot. Of course, we will need to determine its height for each tile in order to know what to draw above and what is below. This is solved simply by adding an obvious variable.

public int getTransitionCornerFor(World world, int x, int y, int height) { int corner = 0; if (world.getTile(x-1, y).zOrder == height) corner |= 0b0001; if (world.getTile(x, y).zOrder == height) corner |= 0b0010; if (world.getTile(x, y-1).zOrder == height) corner |= 0b0100; if (world.getTile(x-1, y-1).zOrder == height) corner |= 0b1000; return corner; } 


Each bit means its own angle. 1 bit - upper left corner, 2 - lower left, 3 - lower right, well, 4 - upper right.

Now, with regards to the method of determining the desired texture indices for transitions. The method I got was bulky and ugly, well, all because of my skills. Although specifically for the article I broke it into several methods in order not to create a huge amount of indentation.

 public void updateTransitionMap(World world, int x, int y) { int w = 16, h = 16; int[] temp = new int[4]; // ,     4   4   ID  4    (.. 32      ) for (int i = 0; i < 4; i++) //            temp[i] = 0; if (tileID > 0) { for (int i = 1; i <= tilesNum; i++) { int corner = getTransitionCornerFor(world, x, y, i); int c = 0; if (corner > 0) { c = setPointTransition(world, temp, x, y, corner, c, i); //      if (c == 3) c = setCornerTransition(world, temp, x, y, corner, c, i); //,   3 (!) ,      if (c == 2) c = setEdgeTransition(world, temp, x, y, corner, c, i); //  2 (!) ,     } } } } 


And here are the methods themselves:

 public int setPointTransition(World world, int[] temp, int x, int y, int corner, int c, int i) { for (int k = 0; k < 4; k++) if ((corner >> k & 1) == 1) { int idx = 8+k; int storage = 0; storage = (idx & 0xF) << 4 | (i & 0xF); temp[k] = storage; int t = 0; for (int l = 0; l < 4; l++) { t = (t << 8) | temp[l] & 0xFF; } world.setTransition(x, y, t); c++; } return c; } 


Everything is simple here. Run over every corner, check the bit. If he is alone, we put the index 8 + k , i.e. angle (above I described the number for each side (NE, SE, SW, SE)). Next, using the crutch through the cycle, update our transition map.

Do not forget at the end to give the updated number with. Thanks to Java for not having out or passing simplest types by reference.

Methods connecting points to angles and sides:
  public int setEdgeTransition(World world, int[] temp, int x, int y, int corner, int c, int i) { for (int offset = 0; offset < 4; offset++) { boolean isSide = true; for (int k = 0; k < 2; k++) { //    if ((corner >> ((k + offset) % 4) & 1) != 1) isSide = false; else if (k == 1 && isSide) { int idx = (offset+1)%4; int storage = 0; storage = (idx & 0xF) << 4 | (i & 0xF); temp[offset] = storage; int t = 0; for (int l = 0; l < 4; l++) { t = (t << 8) | temp[l] & 0xFF; } world.setTransition(x, y, t); } } } return c; } public int setCornerTransition(World world, int[] temp, int x, int y, int corner, int c, int i) { for (int offset = 0; offset < 4; offset++) { boolean isCorner = true; for (int k = 0; k < 3; k++) { //    if ((corner >> ((k + offset) % 4) & 1) != 1) isCorner = false; else if (k == 2 && isCorner) { int idx = 4+offset; int storage = 0; storage = (idx & 0xF) << 4 | (i & 0xF); temp[offset] = storage; int t = 0; for (int l = 0; l < 4; l++) { t = (t << 8) | temp[l] & 0xFF; } world.setTransition(x, y, t); } } } return c; } 


Here is exactly the same principle. The only difference is the starting index number of the texture, so that we can take the desired and one more cycle, which sets the offset, which means from which point to start the angle. The adjacent angle (or side) is checked counterclockwise, starting from this point. If at least one point is not adjacent tile - interrupt, neither the angle nor the side is obtained.
That's it, the transition map is built here! Each tile has 5 bits. One for storing a tile (256 possible variations) and a bit at each corner for storing metadata.

It remains only to render this case. I will consider the old deprecated-method via the immediate-mode (I plan to leave for VBO, now I need to deal a little with the structure and dynamic update of VBO, as well as with drawing only the visible part of it).

Well, there is nothing complicated:

 public void renderTile(World world, int x, int y) { int w = 16, h = 16; int s = 0; if (tileID > 0) { for (int i = 0; i < 4; i++) { int t = world.getTransition(x, y); int src = ((t >> (3-i)*8) & 0xFF); int idx = src >> 4 & 0xF; int id = src & 0xF; int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48, u1 = u + w, v1 = v + h; if (id != 0) { GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1); //    ,      VBO } } } } 


What are we doing here? Yeah, we go through every 8 bits and get 4 first and 4 last, for ID and transition. Next we pass the OpenGL parameters, it already distributes the drawing.

Result:

image
(Yes, yes, LWJGL canvas built into Swing).

It seems we have forgotten something? Draw a solid piece of tile, if 4 surrounding points are native to it in height!

 public void renderTile(World world, int x, int y) { int w = 16, h = 16; int s = 0; if (tileID > 0) { int c = 0; for (int i = 0; i < 4; i++) { int t = world.getTransition(x, y); int src = ((t >> (3-i)*8) & 0xFF); int idx = src >> 4 & 0xF; int id = src & 0xF; int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48, u1 = u + w, v1 = v + h; if (id != 0) { if (id == tileID) c++; GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1); } } if (c == 4) { GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(tileID-1), 16, (tileID-1)*48+16); } } } 


image

Something is missing? That's right, we need to decide how to draw the bottom tile. To be honest, I managed to solve it almost by accident, but this particular moment still needs some work. While this can be considered a screwed crutch, but it does not affect the result.

Let's slightly change our method:

  public void renderTile(World world, int x, int y) { int w = 16, h = 16; int s = 0; if (tileID > 0) { for (int i = 1; i <= tilesNum; i++) { int corner = getTransitionCornerFor(world, x, y, i); int c = 0; if (corner > 0) { for (int k = 0; k < 4; k++) if ((corner >> k & 1) == 1) { c++; } } boolean flag = false; int fill = getFillCornerFor(world, x, y, i); if (fill > 0) for (int k = 0; k < 4; k++) if ((fill >> k & 1) == 1) { c++; if (k == 4 && c == 4) flag = true; } if (c == 4) { GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, 0, 48*(i-1), 16, (i-1)*48+16); if (flag) break; } } for (int i = 0; i < 4; i++) { int t = world.getTransition(x, y); int src = ((t >> (3-i)*8) & 0xFF); int idx = src >> 4 & 0xF; int id = src & 0xF; int u = (idx%8)*16, v = 16 + 16*(idx/8) + (id-1)*48, u1 = u + w, v1 = v + h; if (id != 0) { GRenderEngine.drawTextureQuad(x*16, y*16, 128, 144, u, v, u1, v1); } } } } 


Added another method. It is almost equivalent to the method that writes bits of adjacent tiles. Here he is:

  public int getFillCornerFor(World world, int x, int y, int height) { int corner = 0; if (world.getTile(x-1, y).zOrder > height) corner |= 0b0001; if (world.getTile(x, y).zOrder > height) corner |= 0b0010; if (world.getTile(x, y-1).zOrder > height) corner |= 0b0100; if (world.getTile(x-1, y-1).zOrder > height) corner |= 0b1000; return corner; } 


It determines all tiles in the area whose height is greater than the height of the transferred tile.

Those. we iterate over all the tiles for a given cell (of course, only auto-files are worth sorting) and see how many tiles are higher than this. Do not forget that before this we count the number of points covered by this tile. If the number of points of this tile + the sum of points of other tiles overlapping a given == 4, then we draw a full quad with this texture and interrupt the loop. These are the crutches.

The result is excellent:

image

Perhaps that's all.

PS How is this method better? Well, WC3 clearly demonstrates that with such a system you can achieve a landscape of unimaginable beauty. Personally, it seems to me that it is more flexible, which, however, creates some difficulties in its implementation. And yes, it still requires some, as I said above, refinement.

Source: https://habr.com/ru/post/227205/


All Articles