An Algorithm for city map generation

22/06/2024

Introduction and Scope

This article is only about street generation. However, this is the first part of a longer series that will explore several generation algorithms to create an entire city, down to individual apartments.

That sounds like a huge scope, but a lot of code has already been written. Here's a peek a few months into the future if you're worried :

aerial view of a generated 3D city
We'll get there eventually. Maybe. Please ignore the aliasing.

Objectives

Motivation

There are quite a few articles about terrain generation. The "Hello, world!" of procedural generation is "Make a heightmap using Perlin/value/gradient/simplex noise". Some people then take the concept up to entire planets, add biomes, oceans, weather, which is very cool.

Articles about city generation however are rarer. The most influential writing I've seen about city generation outside academia was done by the late Shamus Young and his Pixel City (and its remake in Unity). There was also a lot of work done on (the unpublished) Subversion by Introversion Software.

The goal of these articles is to describe the algorithms I'm using, at a level where casually interested people don't run away screaming, but detailed enough that anyone interested can implement them and get similar results.

Desired results

For this algorithm, I want to be able to generate a set of streets that looks like a city map.
a black and white map of what appears to be a city
Just so you don't have to scroll all the way down to see the result, this is what I end up with.

Due to the nature of the project, I want to be able to generate streets inside an already existing network, and use the generated streets as inputs to other generators.
Due to the type of city I want to output, I can't really use a grid. I don't want planned cities like the typical US city. This of course increases the complexity by a lot. But I have something up my sleeve : planar graphs!

If you know your planar graphs, just skip to the start of the algorithm.

Graph theory

I'm not going to bore you with an entire course on graph theory, and you don't even need to understand all of it to understand what I'm doing. But here's the gist : It's just a bunch of dots and lines connecting them. Seriously that's it.

These are graphs :

a graph with a single node a graph with two nodes a graph with two nodes linked by an edge
One node, two nodes, two nodes connected by an edge.
a graph with four nodes, one at the center and three others connected to it a graph with many nodes and edges crossing each other
An intersection, and a messy graph.
a more complex graph showing multiple nodes and edges connected together
This one is less messy.

Not all graphs are suited to our purpose. Let's add a few restrictions :

What's a planar graph ? It's a graph you can draw on a flat plane (an infinite sheet of paper) without any edges crossing each other. Such a drawing is called an embedding of the graph on the plane. You might be familiar with the three utilities problem. Draw lines so that each red house is connected to each blue utility without any line crossing.
a graph with three blue nodes and three red nodes
You cannot untangle this.

It is impossible on a flat plane, because this graph isn't a planar graph, so it has no embedding on the plane. You can do a little trick by bending the sheet of paper and turning it into a torus or a Möbius strip, but that's not a plane.

A city road network is mostly a planar graph. Nodes are intersections and edges are street segments. I'm going to completely ignore the existence of bridges and tunnels. Planar graphs have some neat properties that we can use later down the road. To illustrate, look at this map of a random part of Paris and the graph underlying its road structure.

a map of some streets in Paris with a graph overlaid on top
Map overlay from OpenStreetMap
This is what we want to generate.

The Algorithm, part 1

This is not a very complex algorithm, but it has layers, so don't get lost. It can be applied recursively by adding an extra step (splitting edges), we'll see how later.

Generate Nodes

This is the easy part. The goal is to create a bunch of Nodes that are not too close to each other. Here I've detailed two ways to do this.

Nodes around seed points

For this, we start with a set of nodes. We can use any number of them. We also associate a number to each of them, the Split Number. It corresponds to the maximum number of generations this node can create. A node with split number 2 will create nodes with split number 1 which will create nodes with split number 0. A decimal number like 0.4 means that node has a 40% chance to be able to split.

We create a list containing these starting nodes.
While that list has elements in it :

Output with : 5 seed nodes, one at the center and one in each cardinal direction. Split number = 3.5

Nodes inside a loop

The algorithm is similar, except we remove split numbers and add a boundary check instead.
Create an "open" list with at least one point inside the boundary. You can also use the points on the boundary.
While that list has elements in it :
a bunch of nodes generated inside the boundary defined by an ellipse-shaped road
Output with a previously built "ring road", a slightly squished ellipse

Create the Edges

This is the difficult part. But it's not that hard. It's just playing connect the dots to make a pretty shape.

First, we pick a node at random from the graph.

Create a street from the Node

From the node :
Picking the best node
This is the core of the algorithm. We evaluate each node within the connection radius and choose one to create an Edge. To do this, we apply multiple constraints.
No crossings
a graph with two edges crossing each other
If connecting the two nodes crosses another edge, then it is invalid. This is the only true hard constraint. It's also annoyingly expensive to check, but it's just a bunch of segment-segment intersection tests. This is probably the part that should be optimized the hardest, if you care about speed of generation.
No doubling up
If the two nodes are already connected, don't link them again! Technically this is enforced by the no crossing rule, but this is much faster to check.
No triangles
a graph shaped like a triangle with one of the edges crossed out
Unlike in most 3D applications, triangles are bad.
Triangles are not unheard of in cities, but they are at least uncommon. So we just don't allow them. You can if you want. Checking this is simple : does the target node and the origin node both connect to a third one? If so, then it would create a triangle.
No small angles
a graph showing a very acute angle between two edges
The angle shown in red is too small.

This is ugly. Once again, they do exist, but even in real life I think they suck. This is enforced by using a simple dot product between the candidate edge and the edges starting from each of the nodes. If the candidate is too aligned with a existing edge, it is eliminated.

If this "minimum angle" parameter is set to more than 60°, this will also enforce the "no triangle" rule, since a triangle has at least one angle <= 60°.

After some testing, I've found that values between 55° and 65° have the best results. Too low and it looks bad, too high and the generator doesn't connect enough nodes, making the final graph also look bad.

No 5+-way intersections
Once again, not an impossibility, just not very pretty. It's an optional constraint and more up to your sense of aesthetics. It's a very simple check : look at the degree of the candidate node, reject it if it already has degree 4 or more. Done.
Scoring
If there are multiple candidates after all the constraints are applied, we can score each node and pick the best one. What makes a node better? We can add other metrics here, but just those two are sufficient. It's actually not that important, the constraints are doing the heavy lifting here.
Repeat the process
Once we have the new node, we move over to that node and record the direction we just took. Should the street be extended again? We check the degree of the new node.

Remove unconnected Nodes

After this process, sometimes some Nodes are unreachable and unconnected. Those are called isolated nodes. We remove them for the next part.

The Algorithm, Part 2

Don't worry, this is almost over. After everything we have a graph like this :
a graph showing interconnected roads
This is taking shape.
I mentioned an extra step.

Edge Splitting

Let's pick a smaller size for edges, and split all existing edges to that size.
This in essence creates new Nodes that can be used as intersections for the next part of the algorithm. Smaller streets need "anchor points" to connect to the larger streets.
two graphs, the second one is similar to the first one but with additional nodes in the middle of its edges
Like I said earlier, a simple step.

Divide

Now we're going to take advantage of the planar nature of our graph.
Technically we don't have to, but dividing the plane makes it more optimizable.
All of this should have generated faces. By its nature, any planar graph with a least one cycle divides the plane in multiple faces. Think of a face as the area the paint bucket would fill in an image editor.
the previous graph, with each face colored
Did you know that you can color any map so that two adjacent regions never have the same color using only 4 colors?

The algorithm to divide our graph into faces is surprisingly simple, anyone who knows how to solve a maze should understand it quickly.

You follow a wall until you come back to your starting point.

If you want to sound cool you can say you're "finding all the chordless cycles in a planar graph".

First, let's note that an edge can be part of at most two faces. One on its right side, one on its left side. Both can be on the same face, it's fine.

Let's make a set containing all edges twice, once for each direction. For the maze analogy, think of it as both walls of the corridor. For an edge, think of it as whether you're going from point A to point B, or point B to point A.
While the set has elements in it:

Conquer

For each face, we do what we did for Part 1, but with smaller distances, using the "nodes inside a loop" algorithm to generate the nodes, and we connect them again. I draw these new edges with a thinner line.

BOOM.

a graph with even more densely packed and connected roads
The power of recursion.

Neat, huh? There's a special face that needs our attention : the outer face. We can't use the method to generate nodes inside a loop here, so we just use the regular generation. The split number is up to you, a higher number means more sprawl, while a number of 0 confines the city to its inner faces.

Note that the outer face gets complicated if your graph isn't connected, so we just assume it is. A graph is connected if you can draw a path from any node to any other node.

If you're not happy with that, find all the subgraphs and either connect them together, or just delete the smaller ones. I would suggest doing that during the "remove unconnected Nodes" step.

AGAIN

Divide the existing edges into even shorter Edges. Do it again.
a graph of a city road network
THE POWER OF RECURSION.

And we're done. Hurray!

Gallery

Here's a few more outputs, on a larger scale :

The same seed and starting ring road, but with a larger radius.

Starting with a central node and an extra node on each cardinal point.

Starting with a line creates a narrow and long city.

This one uses three seed nodes.

This one too.

A few other cities using the ring road method. I think they look neat.

Parameters and inputs

This is a generator, so it's better if there's a bunch of knobs to twist and turn to tune the output. Here are the ones I've found were useful :

Further improvements

I haven't had time to explore some possibilities, or I thought they were too distant from my original scope. Here are some of them.

Up next

Blocks and zoning!

If the above is not a link to the next article, that means it hasn't been written yet. Use whatever method you want to get notified when that happens. It might take a while, I'm a slow writer.


< Back to the Index