Houdini: Wang Tile Set, Wave Function Collapse on non-uniform geometry
What is Wave Function Collapse?
This url holds a great example
The idea comes from Quantum superposition originally. In a nutshell, Quantum superposition is simply the idea our object can be in multiple states simultaneously until measured. The example linked above illustrates this using a Wang Tile set. All clickable tiles have 16 valid states until you start picking tiles. By defining a single tile, its neighboring tiles have their “valid” states reduced.
Why 16 valid states? Optional overview on wang tiles
A mathematician named Hao Wang in 1961, described how tiles would fit together with a simple rule. When connecting 4 sided tiles, you must match the edges to tiles with the same color edge. Each tile above has 2 colors, and 4 sides. 2^4 is 16. Therefore 16 valid states in our tile’s quantum superposition.
Hint: If you were to use hexagonal tiles instead of square, you would need (2^6) 64 tiles in your tile-set. Certainly you can get away with less, but Once you get into 16+ tile territory, you’re going to want to get procedural. Procedurally generating tile-sets is a post for another time.
Lets get started distributing Wang Tiles in houdini
There’s one limiting factor when using Wang Tiles in the way it’s implemented in houdini, the expected input is a grid.
Here I’m using a distorted sphere to affect the tile’s superposition. If a primitive within the grid is also within the sphere, the selected tile is 0 or empty. This sphere is solving our tile’s “state”. Doing so simplifies the Wave function collapse aspect of this problem, so we can focus on solving the Wang Tile distribution.
Wang Tile distribution process only worked on grids due to the mathematical properties of a grid. The key to this 2d Wang Tile decoder algorithm: iterating over each polygon, determining neighbor’s tile value.
int northnum = tile_num + num_of_rows;
int southnum = tile_num - num_of_rows;
int westnum = tile_num - 1;
int eastnum = tile_num + 1;
int north; int south; int east; int west; // If we find a consumed/red tile neighbor north if (north == 1) north = 1; // If we find a consumed/red tile neighbor east if (east == 1) east = 2; // If we find a consumed/red tile neighbor west if (west == 1) west = 8; // If we find a consumed/red tile neighbor south if (south == 1) south = 4; // index = wang tile index int index = north + east + south + west;
Index is our wang tile index, an integer 0-15 in this case.
And if we don’t find a tile in a one of the 4 directions, we know we’re an edge. However, this method falls apart when we introduce grid like geometry with only 2 edges.
EG: a tube.
// Vex H19.5 // Determine NSEW primitive neighbors on a tube int cols = max(polyneighbours(0, 0)); int rows = nprimitives(0)/cols; i[]@adjprims = polyneighbours(0, @primnum); foreach(int primnum; @adjprims){ if(primnum==@primnum+1) i@east = primnum; if(primnum==@primnum-1 || primnum==@primnum-1+cols) // check if we've wrapped around a tube i@west = primnum; if(primnum==@primnum-cols) i@south = primnum; if(primnum==@primnum+cols) i@north = primnum; }
Even if we solve for tubes/cylinders, we’re still extremely dependent on the primitives being numbered in the correct order.
This sort of becomes a tangent space problem. We need tangent vectors to define a "compass" orientation per primitive. Then we can try to get the local direction vector based on the relative position of the prim we want to query.
Below I’m using our face normal with our tangent normal to create our rotational matrix. I’m then taking additional steps to convert this 3d direction to a 2d tangent space coordinate vector. I’ll be doing this 4 times per face.
// Vex prim wrangle i@_north = -1; i@_south = -1; i@_west=-1; i@_east=-1; vector pos; vector normal; vector tangent; i[]@adjprims = polyneighbours(0, @primnum); normal = v@N; tangent = v@tangentu; matrix3 m = maketransform(normal, tangent); // rotation matrix3 between two transforms foreach(int primnum; i[]@adjprims){ // loop over adjacent prims to determine NSEW pos if(diff.x == -1){ // yellow v@diff_e = diff; i@_east = primnum; } if(diff.x == 1){ // red v@diff_w = diff; i@_west = primnum; } if(diff.y == -1){ // blue v@diff_s = diff; i@_south = primnum; } if(diff.y == 1){ // green v@diff_n = diff; i@_north = primnum; } }
When we first calculate diff, it's in world-space, then the inverted local transform puts it into local space aligned with our UV map (v@tangentu derived from UV map).
We can test if our prim/face neighbor finding solution is working as expected. We’re looking for our vectors to consistently point in the x/y directions to determine if we have faces to the North, South, East and West.
You’ll also notice, I’m rotating the grid to demonstrate, no matter what the orientation of the object, the diff vectors are strictly local space. These vectors are locally pointing North/South/East/West if they have a valid neighbor in that direction.
We now have everything we need to calculate our Wang Tile number, 0-15.
// Vex houdini 19.5 // Wang Tile distribution method using true local space coordinates i@_north = -1; i@_south = -1; i@_west=-1; i@_east=-1; vector pos; vector normal; vector tangent; vector cd; i[]@adjprims = polyneighbours(0, @primnum); normal = v@N; tangent = v@tangentu; matrix3 m = maketransform(normal, tangent); int east; int west; int south; int north; foreach(int primnum; i[]@adjprims){ pos = prim(0, "P", primnum); cd = prim(0, "Cd", primnum); vector diff = pos-@P; diff = normalize(diff); diff *= invert(m); diff *= {1,1,0}; diff = normalize(diff); diff = -rint(diff); v@diff = diff; if(diff.x == -1.0){ i@_south = primnum; south = int(cd[0])*4; } if(diff.x == 1.0){ i@_north = primnum; north = int(cd[0])*1; } if(diff.y == -1.0){ i@_east = primnum; east = int(cd[0])*2; } if(diff.y == 1.0){ i@_west = primnum; west = int(cd[0])*8; } } int wang_index = north + east + south + west; if (rint(v@Cd[0]) == 1) wang_index = 15; s@name = itoa(wang_index);
We now have our wang tile index recorded on every face on our geometry. Dragging the same sphere controller, we can visualize the wang tile distribution working correctly on our non-uniform capsule.
While our distribution is correctly setup, our tiles will correctly change swap to the appropriate tile index based on the sphere controller.
You may have noticed the tiles don’t really deform well to the shape of our capsule geometry as well as they did with the grid.
In the next blog post. I’ll walk through the steps required to write our own custom lattice node to properly fill in the gaps between tiles. Demonstrated in the video above.