· 8 min read · updated
Roads Are Harder Than Rails
Procedural road geometry for a voxel model railway: four street pieces, junction pads, square kerb islands, zebra crossings and the people who walk them.

Rails were the quick part. Roads were not, and the ratio still surprises me, because a road looks like the simpler object: a flat grey band with markings on it, no sleepers, no ballast, no switches. Procedural road generation turns out to be mostly special cases, and the code still shows it. Everything that draws a track tile is about forty lines; everything that draws a road tile is nearly a hundred, before a single pedestrian walks on it.
Here is where the lines went.
Track has two ways to meet. Roads have junctions
A track tile is drawn by drawing each of its paths. A road junction cannot be, and that is the difference that matters.
A piece of track connects to another piece at one of its ends, and the connection is either straight through or diverging. That is the whole vocabulary, and a first loop needs only two of its six pieces. A switch is two paths that share an end, and a crossing is two straights that happen to overlap, so the renderer lays the same ballast, sleepers and rails along every path a tile has, adds a switch stand or a buffer where one belongs, and is done.
The road palette is just as small: Street, Street curve, T-junction and Crossroads, each of which turns a quarter at a time, which makes eleven distinct tiles. Road mode's hint says "Streets work like track pieces", and for the Street and the Street curve they do. Each has one path through the cell, swept the same way a rail is: a carriageway, a kerb and footway down each side, and a dashed centre line that carries on round the curve.
Junctions are where that stops. A Crossroads has six paths through it, two straight on and four turns, and drawing it as six swept roads would lay kerbs straight across the middle. So a junction is not drawn from its paths at all. It is a square of asphalt in the middle of the cell with one approach for each edge it opens onto, and each approach carries its own kerbs and footway out to the cell edge, a zebra crossing, and a stop line ahead of it. On a T-junction's closed side the footway runs straight past, corner to corner. The middle has no markings, because the middle is where everyone is looking at each other.

Every piece reaches exactly to the cell boundary, so neighbouring tiles line up without seams. None of this is hard. There is just a lot of it, and it all has to agree at the edges.
The kerb radius problem
Kerbs and footways are swept strips, and the inside of a Street curve is the tightest sweep on the layout by a wide margin. Below about 1.5 units of radius a swept box overlaps itself into a starburst of raised fins, which is covered in more detail in the post on sweep radius.
A Street curve is a quarter turn inside one cell, so its centreline has a radius of five units. The inside kerb would run on an arc of 1.3 and the footway behind it on one of 0.6, so neither is swept. The inside corner is laid as a square of footway, one and a half units on a side, in the corner of the cell, with its kerb turned in two straight runs. Each run starts at a cell edge exactly where a Street's kerb arrives, so the corner meets its neighbours as if it were part of them.
It used to be one diagonal bar centred on the corner itself, which overshot into the carriageway at both ends. That was harmless on a lone curve. Four curves meeting made a pinwheel of kerb arms across the road.
The outside kerb of the same curve runs at a radius of 8.7, comfortably above the threshold, and is swept normally with a finer step than a straight's. The carriageway between them is swept in lanes, which is the next problem.
The lane overlap
The lanes of a curved carriageway overlap by 0.3 rather than meeting edge to edge. The carriageway is built as several parallel lanes swept side by side, each following its own radius so the outer edge of the arc has no notches, and butting them edge to edge produced thin dark radial seams that flickered as the camera moved: coincident side faces, and the renderer picking a different winner per pixel.
With the overlap, the faces that used to meet are buried inside the neighbouring lane, and the top faces that do coincide in the overlap are the same asphalt, so whichever one wins looks the same.
So the rule is: where two pieces of swept geometry in one material meet along their length, overlap them. The level crossing shows its limit. Its planking comes up flush with the rail head, so where a plank overlapped a rail, two top faces of different colours would sit in the same plane and fight in plain view. The planks stop clear of the rails on both sides instead, which leaves the flangeway open, as it should be. Tidy abutment is the wrong instinct in a depth buffer, which has no rule for who wins a tie, and overlap is only the right one when the tie does not matter.
Footways are a road, and that is the useful insight
The footway (the pavement, the sidewalk, whichever word you grew up with) started as decoration: a raised strip outside the kerb in a slightly different colour.
Then pedestrians arrived and needed somewhere to walk, and the answer was already there. A person walks the road's own path, offset to the footway: the traffic drives 1.75 units to the right of the centreline, and the people walk 4.15 out on either side, over the paving rather than the kerb. The path cursor that drives trains along rails and cars along carriageways drives people along footways too, with a different speed and a different model, and everything that follows from having a path came for free: choosing a way through a junction, and turning round at a dead end.
It also produces behaviour nobody wrote. A pedestrian reaching a junction picks a way on at random and keeps to its own side of the street. Enough of them doing that independently make what reads as a flow of people around a block, which is entirely a consequence of the junction having more than one exit.
The rest is a few rules on top. On four visits in ten to a junction or a Zebra Crossing, a walker steps off the kerb and crosses: down to the carriageway and up the other side in 3.4 seconds, on the junction's painted zebra or in the middle of the Zebra Crossing's cell. A car waits while anyone is crossing in its own cell or the one ahead. Next to a Station, Park Bench, Little Park, Bus Stop, Fountain Square, Statue Square, Fair Booth or Corner Shop, people stop for a while more often than not. How many walk at all follows what has been built, about one for every ten residents and workers, up to forty-four, and the YARD lever on the desk sets the pace of the streets: the cars follow it directly, and the people walk at anything from about a third of their speed to all of it.

Crossings need to read what is underneath
The Zebra Crossing is placed on a Street, and it has to know which way that Street runs. Its bars run with the traffic and repeat across the carriageway, and turned a quarter the whole thing is meaningless.
So the crossing reads the road tile beneath it when it is drawn and lines itself up with that, whichever way it was turned when it was placed. That is a small piece of code, and it is the reason crossings cannot be static assets: an exported mesh has one orientation and no idea what it is standing on.
The same pattern appears in exactly one other place, the Mountain with Tunnels, which carves its bores by reading the track under it. Between them they are why the line between procedural models and glTF assets is drawn where it is, which two material languages covers.
Level crossings, where a road meets a railway, are a third variant. Lay a Street across a Straight at right angles, which is the only way a road and a track may share a cell, and the cell becomes a level crossing. The planking comes up between and beside the rails, the footways stop short of it, the centre line breaks, and a pair of gates arrives, one boom on each approach on the driver's right. The booms come down whenever any car of any train is within about two and a half cells, and road traffic stops short of a lowered boom. The scenery ideas post says where on the table a crossing earns its keep. The gates are the single most satisfying thing on the layout to watch, which is a poor engineering justification and an excellent design one.

Would I do it differently
Probably not, but I would do it in a different order.
Roads were built as track, but for cars, which got the sweeping and the path cursors for free and then spent a long time discovering that everything else was different. Writing the cases down first (four pieces, eleven tiles, what geometry each needs and where they have to agree) would have made the sweeping work fall out of it rather than the other way round.
The general version: when the second system looks like the first, the parts that are genuinely shared will be obvious and cheap. It is the parts that are nearly the same that take the time, and those are worth listing before you start rather than discovering one at a time.
- engine
- roads
- geometry



