diff --git a/content/posts/tech/3d-autotiling/02-basic-wfc/_index.md b/content/posts/tech/3d-autotiling/02-basic-wfc/_index.md new file mode 100644 index 0000000..2d74072 --- /dev/null +++ b/content/posts/tech/3d-autotiling/02-basic-wfc/_index.md @@ -0,0 +1,203 @@ +--- +title: Basic Wave Function Collapse +weight: 2 +--- + +![wfc generation in blender](generate.mp4) + +This 3D autotiling project started when I stumbled on [Martin Donald's Wave +Function Collapse video](https://www.youtube.com/watch?v=2SuvO4Gi7uY). The idea +was explained so well that I had to try it out. + +In this post I will attempt to outline the high level structure of a Wave +Function Collapse implementation with a bit of psudo-Python. The following +isn't really intended to be a tutorial, but more of an introduction that sets +the stage for some future posts. + +## Types + +We have a few foundational types that make up the implementation: + +### Prototypes + +```python +class Prototype: + name: str + mesh: str + rotation: int + + # sockets + north: str + east: str + south: str + west: str + top: str + bottom: str +``` + +A prototype represents a possible choice to put somewhere in the grid. We will +have 4 prototypes for each tile in our tileset, one for each 90 degree +rotation. + +### Sockets + +On each of the 6 faces we will have a socket. These identify which `Prototype`s +can connect to eachother in each direction. The socket is a representation of +what we see when we look at one face of the tile's bounding box. + + +![highligted socket](socket.jpg) + +For the north and south or east and west sockets to match, they must be mirrors +of one another. For a vertical face's socket to match (top to bottom) the +sockets must be identical. Sockets are arbitrary strings, except for the +suffixes on horizontal faces' sockets: `s` (symmetrical) and `f` (flipped). + +```python +def compatible_sockets(face, a, b) -> bool: + # symmetrical faces should be identical + if face in {"top", "bottom"} or a.endswith("s"): + return a == b + flipped_b = socket[:-1] if socket.endswith("f") else b + "f" + return a == flipped(b) +``` + +### Cells + +```python +class Cell: + possibilities: list[Prototype] +``` + +A cell is one element in a `Grid`, which is just a collection of `Cell`s. + +### Grid + +Turns out there are a lot of ways to represent the `Grid`. + +It could be a 3D array: + +```python +class Grid: + grid: list[list[list[Cell]]] +``` + +Or for an infinite grid, a map that lazily populated: + +```python +class Grid: + grid: dict[Vec3, Cell] + + def __getitem__(self, coord: Vec3): + if key not in self.grid: + self.grid[coord] = Cell() + return self.grid[coord] +``` + +It doesn't really batter as long as we can look things up using a 3D integer coordinate. + +## Implementation + +```python +def solve(): + work_list = [] + solved = False + iteration = 0 + while not solved and iteration < MAX_ITERATIONS: + iteration += 1 + + cell, coord = grid.find_min_entropy() + if cell is None: + solved = True + break + + cell.collapse() + work_list.append(coord) + while len(work_list) > 0: + work_list += propagate(work_list.pop()) +``` + +This is the main structure of the program. Make a random selection in one of the cells +recursively propagate that out and repeat until every cell has only one possibility. +You can run the outer loop by hand in this [web demo](https://bolddunkley.itch.io/wfc-mixed) +from Martin Donald. + +### Collapse + +In this algorithm, "entropy" is a cute name for the length of a `Cell`'s +possibility list. Zero means there is a contradiction and we will never be able +to solve the `Grid`. One means we know the `Prototype` that we've chosen for +this cell. In each iteration, we find the unsolved Cell we're most certain +about: + +```python +def min_entropy(self) -> Cell: + def entropy(cell): + if cell.entropy() < 2: + return math.inf + return cell.entropy() + return min(self.grid, key=entropy) + +``` + +Then we "collapse" it's possibilities into a random choice: + +```python +def collapse(self): + idx = randint(0, len(self.possibilities) - 1) + self.possibilities = [self.possibilities[idx]] +``` + +### Propagate + +```python +def propagate(coord): + cur_cell = self.grid.get(cur_coord) + changed_neighbors = [] + for direction in opposing_faces.keys(): + next_coord = add_vec3(cur_coord, face_deltas[direction]) + if not self.grid.is_in_bounds(next_coord): + continue + next_cell = self.grid.get(next_coord) + changed = grid[next_cell].constrain_to_neighbor(cur_cell, direction) + if changed: + changed_neighbors.append(next_coord) + self.propagation_stack.append(next_coord) + return changed_neighbors +``` + +Any time we change the possibility list of one cell, +the neighboring cells are possibly affected. We reduce the possibility list +of each neighboring cell with `constrain_to_neighbor` and if that causes +a change, we will have to propagate to _that_ cell's neighbors as well. + + +### Constrain + +```python +def constrain_to_neighbor(self, cell: Cell, face: str): + old = len(self.possibilities) + self.possibilities = [ + p + for p in self.possibilities[] + if compatible_sockets(my_face, p[my_face], cell[face]) + ] + return old != len(self.possibilities) +``` + +Here we reduce the possiblity list to only include `Prototype`s that are compatible +on the opposing face. + +There are far more efficient ways of doing this, like setting up an adjacency +table. This is also the part of the algorithm that is most likely to have edge +cases to help guide the algorithm's behavior to produce specific results. + + +## Conclusion + +While this post was mostly psuedo code, it matches up pretty well to my first +WFC attempt. The code is available +[here](https://github.com/stevenctl/basic-wfc-blender) as well as a sample +Blender file you can use to run it. This implementation has a lot of issues and +isn't scalable, but I did find I use for it that's covered in a later post. + diff --git a/content/posts/tech/3d-autotiling/02-basic-wfc/generate.mp4 b/content/posts/tech/3d-autotiling/02-basic-wfc/generate.mp4 new file mode 100644 index 0000000..61ff080 Binary files /dev/null and b/content/posts/tech/3d-autotiling/02-basic-wfc/generate.mp4 differ diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/old-tileset.png b/content/posts/tech/3d-autotiling/02-basic-wfc/old-tileset.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/old-tileset.png rename to content/posts/tech/3d-autotiling/02-basic-wfc/old-tileset.png diff --git a/content/posts/tech/3d-autotiling/02-basic-wfc/socket.jpg b/content/posts/tech/3d-autotiling/02-basic-wfc/socket.jpg new file mode 100644 index 0000000..9b6f4af Binary files /dev/null and b/content/posts/tech/3d-autotiling/02-basic-wfc/socket.jpg differ diff --git a/content/posts/tech/3d-autotiling/02-basic-wfc/socket.png b/content/posts/tech/3d-autotiling/02-basic-wfc/socket.png new file mode 100644 index 0000000..2346f5b Binary files /dev/null and b/content/posts/tech/3d-autotiling/02-basic-wfc/socket.png differ diff --git a/content/posts/tech/3d-autotiling/03-tileset/_index.md b/content/posts/tech/3d-autotiling/03-tileset/_index.md new file mode 100644 index 0000000..eb210c1 --- /dev/null +++ b/content/posts/tech/3d-autotiling/03-tileset/_index.md @@ -0,0 +1,85 @@ +--- +title: Generating a 3D Tileset +weight: 3 +--- + +## Enumerating Tiles + +In the [last post](../01-marching-squares/), we built a basic autotiler for a 2D +square grid. We will need a tileset before we can extend it to support a 3D cube +grid. For each cell, there are now eight corners and therefore 2⁸ (256) possible +ways to fill that cell. + +Rather than thinking in corners, I prefer to consider the octants after dividing +the cube in half in each dimension. In the same way as before, we assign one bit +of a now 8-bit integer to each octant. + +![bits per octant](bits.jpg) + +We can take an integer between `0` and `255` and convert it +to or from a `2x2x2` array of "filled" or "empty". + +```python +def int_to_cube(c_int: int) -> np.ndarray: + s = format(c_int, "#010b") + o = np.array([int(s[i]) for i in range(2, len(s))]).reshape((2, 2, 2)) + return o + +def cube_to_int(cube: np.ndarray) -> int: + return int("".join(cube.reshape(8).astype(str)), 2) +``` + +We're using Python 3 with NumPy because `ndarray` is very convenient. +This code doesn't need to run in our game engine and will end up +being more useful in Blender scripts, which must be Python. + +Similar to 2D, many of cases are transformations of other cases. We can generate +the unique cases pretty easily: + +```python +def possible_tiles(): + seen = set() + unique_cubes = set() + + def add(cube, unique=False): + v = cube_to_int(cube) + if unique and v not in seen: + unique_cubes.add(v) + seen.add(v) + + # skip 0 and 255 because they don't need a model + # they're empty and interior tiles + for i in range(1, 255): + if i not in seen: + unique_cubes.add(i) + seen.add(i) + + # register the transformations as "seen" + # so that we will ignore them in future interations + rc = int_to_cube(i) + for rot in range(4): + rc = np.rot90(rc, axes=(0, 1), k=rot) + seen.add(cube_to_int(rc)) + seen.add(cube_to_int(np.flip(rc, axis=1))) + + return unique_cubes + + +if __name__ == "__main__": + tiles = possible_tiles() + print(len(tiles)) +``` + +53 unique cases. Not bad! We can safely ignore the `0` and `255` cases. Because +0 is empty and 255 is completely on the interior, so we can't actually see it, +there is no need to provide a model for them. + +## Generating Meshes + +// TODO blender script to place some cubes + +## Combining Handmande Meshes + +// TODO 26 subtiles to generate the rest +// Should I figure out rotations or use WFC +// Should I write a basic WFC post first using vertex data sockets? diff --git a/content/posts/tech/3d-autotiling/03-tileset/bits.jpg b/content/posts/tech/3d-autotiling/03-tileset/bits.jpg new file mode 100644 index 0000000..f8dd50f Binary files /dev/null and b/content/posts/tech/3d-autotiling/03-tileset/bits.jpg differ diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/_index.md b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/_index.md similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/_index.md rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/_index.md diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/dual-grid.jpeg b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/dual-grid.jpeg similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/dual-grid.jpeg rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/dual-grid.jpeg diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/random-wfc-normals.png b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/random-wfc-normals.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/random-wfc-normals.png rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/random-wfc-normals.png diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/random-wfc.png b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/random-wfc.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/random-wfc.png rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/random-wfc.png diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/shape-wfc-normals.png b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/shape-wfc-normals.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/shape-wfc-normals.png rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/shape-wfc-normals.png diff --git a/content/posts/tech/3d-autotiling/wfc-01-intro/wire-sockets.png b/content/posts/tech/3d-autotiling/archive/wfc-01-intro/wire-sockets.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-01-intro/wire-sockets.png rename to content/posts/tech/3d-autotiling/archive/wfc-01-intro/wire-sockets.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/_index.md b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/_index.md similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/_index.md rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/_index.md diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/alltiles.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/alltiles.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/alltiles.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/alltiles.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/basic-4.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/basic-4.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/basic-4.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/basic-4.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/cubes.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/cubes.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/cubes.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/cubes.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/diag-edge.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/diag-edge.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/diag-edge.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/diag-edge.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/diags.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/diags.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/diags.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/diags.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/dual-grid.jpeg b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/dual-grid.jpeg similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/dual-grid.jpeg rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/dual-grid.jpeg diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/generated.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/generated.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/generated.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/generated.png diff --git a/content/posts/tech/3d-autotiling/wfc-02-tiles/lip_tiles.png b/content/posts/tech/3d-autotiling/archive/wfc-02-tiles/lip_tiles.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-02-tiles/lip_tiles.png rename to content/posts/tech/3d-autotiling/archive/wfc-02-tiles/lip_tiles.png diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/3octs.png b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/3octs.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/3octs.png rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/3octs.png diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/_index.md b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/_index.md similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/_index.md rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/_index.md diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/sock7.png b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sock7.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/sock7.png rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sock7.png diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/sock_arch.png b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sock_arch.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/sock_arch.png rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sock_arch.png diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/sockpaint.png b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sockpaint.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/sockpaint.png rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/sockpaint.png diff --git a/content/posts/tech/3d-autotiling/wfc-03-sockets/socks.png b/content/posts/tech/3d-autotiling/archive/wfc-03-sockets/socks.png similarity index 100% rename from content/posts/tech/3d-autotiling/wfc-03-sockets/socks.png rename to content/posts/tech/3d-autotiling/archive/wfc-03-sockets/socks.png