|
| 1 | +--- |
| 2 | +title: Generating Tiles |
| 3 | +weight: 4 |
| 4 | +--- |
| 5 | + |
| 6 | +In the [last post](../03-driven-wfc/) we realized that the manually |
| 7 | +created tileset would need 256 tiles to cover every case. While we can rotate |
| 8 | +and flip our tiles to bump the number up, we can't be sure we covered all the cases. |
| 9 | +At the same time, we will eliminate the need to manually label our tiles |
| 10 | +with `volume` data. |
| 11 | + |
| 12 | +## Enumerating Tiles |
| 13 | + |
| 14 | +To ensure we don't miss any possibilities, let's enumerate all the cases in code. |
| 15 | +Intuitively, there can't be 256 _unique_ cases. Let's figure out how many are left |
| 16 | +if we ignore duplicates. |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +We can take an integer between `0` and `255` and convert it |
| 22 | +to or from a `2x2x2` array of "filled" or "empty". |
| 23 | + |
| 24 | +```python |
| 25 | +def int_to_cube(c_int: int) -> np.ndarray: |
| 26 | + s = format(c_int, "#010b") |
| 27 | + o = np.array([int(s[i]) for i in range(2, len(s))]).reshape((2, 2, 2)) |
| 28 | + return o |
| 29 | + |
| 30 | +def cube_to_int(cube: np.ndarray) -> int: |
| 31 | + return int("".join(cube.reshape(8).astype(str)), 2) ``` |
| 32 | + |
| 33 | +We're using Python 3 with NumPy because `ndarray` is very convenient. |
| 34 | +This code doesn't need to run in our game engine and will end up |
| 35 | +being more useful in Blender scripts, which must be Python. |
| 36 | + |
| 37 | +Similar to 2D, many of cases are transformations of other cases. We can generate |
| 38 | +the unique cases pretty easily: |
| 39 | + |
| 40 | +```python |
| 41 | +def possible_tiles(): |
| 42 | + seen = set() |
| 43 | + unique_cubes = set() |
| 44 | + |
| 45 | + def add(cube, unique=False): |
| 46 | + v = cube_to_int(cube) |
| 47 | + if unique and v not in seen: |
| 48 | + unique_cubes.add(v) |
| 49 | + seen.add(v) |
| 50 | + |
| 51 | + # skip 0 and 255 because they don't need a model |
| 52 | + # they're empty and interior tiles |
| 53 | + for i in range(1, 255): |
| 54 | + if i not in seen: |
| 55 | + unique_cubes.add(i) |
| 56 | + seen.add(i) |
| 57 | + |
| 58 | + # register the transformations as "seen" |
| 59 | + # so that we will ignore them in future iterations |
| 60 | + rc = int_to_cube(i) |
| 61 | + for rot in range(4): |
| 62 | + rc = np.rot90(rc, axes=(0, 1), k=rot) |
| 63 | + seen.add(cube_to_int(rc)) |
| 64 | + seen.add(cube_to_int(np.flip(rc, axis=1))) |
| 65 | + |
| 66 | + return unique_cubes |
| 67 | + |
| 68 | + |
| 69 | +if __name__ == "__main__": |
| 70 | + tiles = possible_tiles() |
| 71 | + print(len(tiles)) |
| 72 | +``` |
| 73 | + |
| 74 | +53 unique cases. Not bad! We can safely ignore the `0` and `255` cases. Because |
| 75 | +0 is empty and 255 is completely on the interior, so we can't actually see it, |
| 76 | +there is no need to provide a model for them. |
| 77 | + |
| 78 | +## Less Modeling |
| 79 | + |
| 80 | +If I were to manually build the 53 models, there would certainly |
| 81 | +be repetitive substructures. While iterating on art, it would be |
| 82 | +very annoying to update every variation to keep the look consistent. |
| 83 | + |
| 84 | +What if we imagine each octant as a tile? It can't be that hard to auto-tile a |
| 85 | +2x2x2 grid. In the talk [Beyond |
| 86 | +Townscapers](https://www.youtube.com/watch?v=Uxeo9c-PX-w&t=126s), and on |
| 87 | +[twitter](https://twitter.com/OskSta/status/1448248658865049605) Oskar uses |
| 88 | +this image to show tiles for a dual-grid. |
| 89 | + |
| 90 | + |
| 91 | + |
| 92 | +I tried to classify the square cases. |
| 93 | + |
| 94 | +* Empty |
| 95 | +* Corner |
| 96 | +* Edge |
| 97 | +* Diagonal |
| 98 | +* Bend |
| 99 | +* Full |
| 100 | + |
| 101 | +Those are the cases I want to end up with at the end. The combinatorial |
| 102 | +explosion into 3D is big. Let's cut those into quadrants to make the _dual-dual_ tiles. |
| 103 | +We end up eliminating the Diagonal case. Only four models are needed to start: |
| 104 | + |
| 105 | + |
| 106 | + |
| 107 | +Expanding to 3D isn't that bad. We just need walls, edges and bends along the Up axis. |
| 108 | +That can be accomplished with some simple rotations. |
| 109 | + |
| 110 | + |
| 111 | +It doesn't fully cover it though. We have to be able to transition vertically |
| 112 | +between flat tiles or edge tiles. We'll call these "lip" tiles since they're |
| 113 | +kind of skinny. There are 5. 4 for to match up to the base cases and a 5th that |
| 114 | +is a corner between two lips. It's basically the "outward" part of an "outward |
| 115 | +blob cut" approach. |
| 116 | + |
| 117 | + |
| 118 | + |
| 119 | + |
| 120 | +Finally, we duplicate all of these and flip them upside down so we have some |
| 121 | +dedicated for the top layer of our 2x2x2 grid that transition from the ceiling |
| 122 | +downward instead of from the floor upward. |
| 123 | + |
| 124 | +In total There are 26 tiles to model. Very manageable! |
| 125 | + |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | +## Assembling WFC Tiles |
| 130 | + |
| 131 | +To combine the tiles, we rely heavily on some naming/classification. |
| 132 | +The names of the Blender object concatenate: |
| 133 | + |
| 134 | +* Their base type - Lip, Corner, Bend, Edge |
| 135 | +* Their Layer - Top, Bottom |
| 136 | +* Their Direction - Horizontal, Vertical, Horizontal + Vertical, Corner |
| 137 | + |
| 138 | +We can then determine which micro-tile belongs in an octant |
| 139 | +based on which neighboring octants are also filled. The implementation is |
| 140 | +[here](https://gist.github.com/stevenctl/3492292c6461c8235e22f858c22ce6b8). |
| 141 | +I wrote it on a long flight and jetlag affected the readability. The rules are: |
| 142 | + |
| 143 | +* If 4 octants are filled in the same "plane", this is either flat or a "lip". |
| 144 | +* If 3 adjacent octants are filled, this is a 3-way bend. |
| 145 | +* If 2 adjacent octants are filled, this is a 2-way bend. |
| 146 | +* If 1 adjacent octant is filled, this is an edge. |
| 147 | +* If 0 adjacent octants are filled: this is a corner. |
| 148 | + |
| 149 | +## Orienting the Tiles |
| 150 | + |
| 151 | +We know which category of tiles go where but the simple rules above don't |
| 152 | +explain how to rotate them into alignment. I was far too lazy to figure this |
| 153 | +out. Turns out I wrote an algorithm for this already. WFC! |
| 154 | + |
| 155 | +If we run our [adjacency](../02-basic-wfc/#generating-sockets-and-prototypes) |
| 156 | +script, then setup a 2x2x2 WFC grid and set the [initial conditions](../03-driven-wfc/) |
| 157 | +of each cell to have all the rotations of the mesh that we're sure we want... |
| 158 | +it will spit out exactly what we need! |
| 159 | + |
| 160 | +## Conclusion |
| 161 | + |
| 162 | + |
| 163 | + |
| 164 | +This generation workflow greatly reduces the amount of modeling I need to do. |
| 165 | +That's important because I'm not very good at 3D modeling. There are other advantages, though: |
| 166 | + |
| 167 | +* We don't need to figure out the "volume" of each tile by hand (unless we add additional "special" tiles) |
| 168 | +* It opens up a way to use the octant bits to create adjacency data instead of vertices. (Keep reading!) |
| 169 | + |
0 commit comments