A road can change halfway along
Roads in the editor can now gain and lose lanes, carry bus and truck lanes, and decide their own markings. Making that work exposed how many different things a painted line can mean.
A road can change halfway along
Part twelve. The previous article moved road rendering into Unreal. At that point the road could look better, but its cross section was still mostly fixed from one end to the other.
That became the next problem almost immediately.
A road in a generated grid can get away with being two lanes everywhere. A road somebody edits by hand cannot. It may grow a turning lane before a junction, lose a shoulder, pass a bus stop or change from a divided road into an ordinary street. Even the paint changes near the ends. A dashed lane divider usually becomes solid on the approach to a junction.
I had treated the road profile as a property. It needed to become a thing that changes along the road.
The cross section is now the source
A road can own an ordered list of slots. A slot has a kind, width, direction, speed limit and usage. Driving, parking, shoulder and median are the obvious kinds. There is also an empty slot, which is useful when one side of a profile needs to become grass without changing the index of everything beside it.
Usage is separate from kind. A driving slot may be an ordinary lane, a bus lane or a truck lane. The truck case is there for the storm chasing game this work is meant to support.
The order matters. The list runs across the road, and the same index means the same lateral place as the profile changes. That sounds restrictive, but it solves a difficult question: when a lane disappears, which remaining lane does it join?
The answer is the nearest drivable neighbour at another index going in the same direction.

The wide profile narrowing towards the junction. The coloured overlays show the underlying lanes and slot edges.
Changing the profile without a seam
A profile key stores a new slot list at a position along the road. It also stores the length of its taper. At the start of the taper, the old profile is still present. At the key, the new one has taken over.
If the new list is shorter, it is padded on the left with empty, zero-width slots. This keeps the other indices lined up. A lane that disappears does not get deleted in one step. Its width reaches zero while the neighbour takes over the space.
This is one of those features where the editor UI was the easy-looking part and the data model was the actual work. The authored road has to keep the profile keys. The bake has to cut the road at both ends of every taper. Curved roads and roads built from several waypoints have to find the change inside their own spans. The generated lanes then need merge relations where the zero-width lane meets its neighbour.
The editor can apply class defaults, store the current profile as a default and clear a road's custom list. When a road owns a custom list, the old lane-count and lane-width controls are disabled. Letting both sets of controls edit the same fact would create two conflicting versions of the road.
One Unreal detail caused a bug here. Editing a field inside an array element reports the inner field name, such as WidthCentimeters, rather than the name of the slot list. Listening for a Slots change therefore misses the edit. The tool now compares the edited list with the last copy it read from Rust. It is less elegant, but it catches the change Unreal actually reports.

The road profile in the editor. The selected key replaces the slot list over a 30 metre taper.
A line belongs between lanes
Once the profile could change, the old way of thinking about road markings stopped working.
A lane line does not belong to a lane. It sits on the edge between two slots. The meaning comes from both sides.
- Two driving slots in the same direction usually get a dashed line.
- Opposing driving slots get a solid line.
- Driving beside parking, a shoulder or a median gets a solid line.
- Near either end of a road, an automatic dashed line becomes solid for the last 15 metres.
Each slot can override the automatic choice on its edge. The available rules include dashed, solid, double solid, and the mixed solid-dashed pairs. The order of the pair matters. solid_dashed viewed from one side becomes dashed_solid when the style rule matches it from the other side.
That direction issue is why the line query produces its own spline instead of asking Unreal to infer a line from a lane later. Rust knows the two adjacent slots, the road direction and the exact portion where the rule applies. Unreal only has to choose the material.
The approach line uncovered a bad first design. I initially split the road itself 15 metres from each end. That made the paint easy, but it also changed the baked road and lanes for a visual rule. A marking should not alter the simulation geometry. The road now stays whole. Only the line spline is cut into an approach piece and a middle piece.
That also required changing the style matcher. It used to match one line rule per road section. An approach piece and a middle piece share the same section, so they need to be matched per line spline instead. Otherwise the solid approach inherits the dashed style selected for the middle.
Junction paint is not one system
After ordinary lane lines, the remaining markings looked like one feature. They were not.
Stop lines are short splines across each lane entering a junction. Their type comes from the control that the driver actually sees there: signal, stop, give way or none. A crossing near the lane end moves the stop line one metre before the crossing.
Guide lines through a junction come from real permitted movements. Drawing both edges of every connector produced a dense fan of lines, especially with crossing-style routing where every entering lane may connect to several exits. The useful result is one outer pair for each movement from one road arm to another. It shows the envelope of the turn without drawing its internal lane connections.
Turn arrows are points rather than splines. Rust checks the real connectors out of an entering lane and packs left, straight, right and U-turn into a bit mask. Unreal uses that number to pick a cell from a texture atlas. The arrow itself is only a flat instanced quad, one centimetre above the surface.
Bus and truck marks are also points. They are spread along lanes with the matching usage. Crosswalks are wide splines across the whole road, and Unreal turns them into zebra ribbons. A taper is another spline, following a driving slot as it narrows to zero.
Keeping these as separate outputs made the Unreal graph much easier to work with. It can switch each kind on only when needed. Rust provides placement and meaning, while the graph owns the paint, mesh or decal used to show it.

Several outputs together in the debug view. The coloured curves are lane and movement data, while the white geometry is the generated road paint.
The cell boundary bug
The city is generated in spatial cells. Each query returns only the road pieces needed inside one view box, and each piece has an owner so two neighbouring cells do not draw it twice.
A crossing cuts a road into runs. That exposed an ownership bug. A cell could own one of those runs but not receive the road section needed to style it. The run existed, but the style matcher skipped it because its cross section was missing. On screen, a marking or surface could disappear next to a crossing depending on which PCG cell built it.
The fix was to include a section in every view box that owns any of its runs. Ownership still prevents duplicate output, but the styling query now has the context it needs.
This is the kind of bug that only appears after several individually reasonable systems meet. Cutting at crossings was correct. Cell ownership was correct. Matching a run against its section was correct. Their combination was not.
What is still wrong
The editor side is ahead of the driver now.
When a lane reaches zero width, the baked graph records a merge into the neighbouring lane. Drivers currently understand merge priority at junction connectors, but they do not perform a proper lane-change manoeuvre in the middle of a road. Enabling those lane-change relations made the traffic harness worse in eight more tests and produced sharp motion near 50 metres per second squared. I turned that part off again. The profile renders correctly, but it is not ready to carry traffic through an arbitrary mid-road merge.
Usage marks and taper markings have data outputs but are not wired into the proof graph yet.
There is also a useful odd case in the turn arrows. With assigned routing on a two-lane crossroad, the inner lane received left, straight and right while the outer lane received only straight. That is not an atlas bug. The expected outer right-turn connector was already missing because of the junction geometry, and the routing fallback kept the nearest valid connection. The arrow correctly revealed a problem elsewhere.
That is what I like about deriving the markings from the road data. The paint cannot quietly claim that a movement exists when the lane graph says it does not. Sometimes the result looks wrong because the marking code is wrong. Sometimes it looks wrong because it has made an older road problem visible.
There is more to finish, especially the actual driving through a taper. But the road editor can now describe something much closer to a real street. A road is no longer one width and one lane count stretched between two junctions. It can change its mind halfway along, and the paint changes with it.
DrivEngine used to give one cross section to a whole road. That is enough for a generated grid, but not for a road with a turning lane, a bus lane or a shoulder that ends.
A road can now carry its own ordered list of slots. Each slot can be a driving lane, parking, shoulder, median or empty ground. Profile keys replace that list at a chosen point along the road. The change happens over a taper, so a lane can narrow to zero and merge into its neighbour instead of vanishing at a hard seam.
The same data now produces the painted lines. A line belongs to the edge between two slots, not to either lane. The default is dashed between lanes moving the same way, solid between opposing traffic, and solid near a junction. An explicit rule can replace any of those choices.
Stop lines, junction guides, turn arrows, bus and truck marks, zebra crossings and lane tapers come out as separate queries. Rust reports their meaning and placement. Unreal decides how to draw them.
It is not finished. Mid-road merges exist in the lane graph, but drivers do not yet perform a smooth lane change through them. The editor side is useful now. The driving side still has to catch up.
This is the short version. The full article has the reasoning, the numbers, and the parts that went wrong.