Skip to content

Commit 05e06a5

Browse files
committed
enumerting tiles
1 parent 3d67881 commit 05e06a5

File tree

7 files changed

+169
-0
lines changed

7 files changed

+169
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
![bits per octant](bits.jpg)
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+
![oskar's beautiful dual tiles diagram](dual-grid.jpeg)
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+
![basic tiles](basic-4.png)
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+
![lip tiles](lip_tiles.png)
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+
![tileset](alltiles.png)
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+
![generated tiles](generated.png)
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+
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)