Townscaper Grid
Townscaper is a 2021 developed by Oskar Stålberg. It's a fun town design toy, that stands out because of its unusual flowing grid.
We'll look at creating a similar grid using the tools available in Sylves. The docs on creating a grid give more detail on what other options Sylves has.
You can find the finished code in the Extras folder.
Fortunately, the developer has left detailed discussions for how to create such a grid on Twitter.
Creating a hex
The townscaper grid is infinite, but it's composed out of small collections of collects in a hexagon shape. First we'll look at creating a single hex.
I present:
— Oskar Stålberg (@OskSta) July 7, 2019
Fairly even irregular quads grid in a hex
Todo:
1. Heuristic to reduce (or eliminate) 6 quad verts
2. Tile these babies to infinity pic.twitter.com/o0kU68uovZ
This breaks down as:
- Create a hexagon filled with equilateral triangles
- Randomly merge pairs of triangles
- Subdivide everything into quads
- Relax the mesh
Creating a grid of triangles in Sylves is easy, this is a grid library after all.
var triangleGrid = new TriangleGrid(0.5f, TriangleOrientation.FlatSides, bound: TriangleBound.Hexagon(4));
This creates a grid of triangles with side 0.5, and restricts it to a hexagon that has 4 triangles per side.
Then, we convert to a mesh. The next steps are then all mesh operations Sylves supplies.
var meshData = triangleGrid.ToMeshData();
// Randomly pair the triangles of that grid
meshData = meshData.RandomPairing();
// Split into quads
meshData = ConwayOperators.Ortho(meshData);
// Weld duplicate vertices together (needed for Relax)
meshData = meshData.Weld();
// Smooth the resulting mesh
meshData = meshData.Relax();
Making an infinite grid.
To go from a single hex to an infinite grid, we need to to create a hex grid, and fill each one of the hexes with a mesh generated in the above fashion. Let's turn the above into a function.
// The grid of our hexes that need filling with meshes.
// Each hex is 4 from top to bottom, which perfectly matches the size of meshes generated.
HexGrid hexGrid = new HexGrid(4, HexOrientation.PointyTopped);
MeshData GetMeshData(Cell hex)
{
var triangleGrid = new TriangleGrid(0.5f, TriangleOrientation.FlatSides, bound: TriangleBound.Hexagon(4));
var meshData = triangleGrid.ToMeshData();
meshData = Matrix4x4.Translate(hexGrid.GetCellCenter(hex)) * meshData;
var seed = HashUtils.Hash(hex);
meshData = meshData.RandomPairing(new Random(seed).NextDouble);
meshData = ConwayOperators.Ortho(meshData);
meshData = meshData.Weld();
}
The key differences are:
- The mesh is translated to the position of the hex we want it to fill.
- The random seed is now fixed based on what hex is being calculated. This ensures the grid is deterministic.
- The mesh is no longer being relaxed.
We need the mesh to exactly fill the hex so that we can detect which edges border another hex, and relaxation would get in the way. Relaxation will be handled later instead.
With this function, we can use PlanarLazyMeshGrid, a powerful grid for customization. It takes a mesh function such as the above, and lazily evaluates it to fill the infinite plane as needed. It supports any periodic setup, but we'll use it with a HexGrid specifically.
var unrelaxedGrid = new PlanarLazyMeshGrid(GetMeshData, hexGrid);
Relaxation
Finally, we apply a relaxation modifier to the grid. We couldn't relax each mesh separately as then they wouldn't join up with each other. But the relax modifier works with the entire infinite grid, ensuring a smooth final shape.
// While not strictly necessary, we use the same chunk size for relaxation as building the grid
var townscaperGrid = new RelaxModifier(unrelaxedGrid, 4);
Enhancements
The above is enough to give an infinite irregular grid of quads with a nice organic look. The actual Townscaper grid is actually a bit more sophisticated:
- Sometimes a hex will randomly decide to use a different mesh generator instead, adding variety to the map
- It uses a slightly more efficient relaxation regime
- The relaxation rule encourages squarish cell shapes