Every wall in the town strobes when the camera moves
The pub in the middle of town had started flashing. Not subtly — walk past the Slag & Slurry in the editor or in a play session and the brickwork on the side elevation would crawl and strobe, and in one particular spot you could plainly see the interior wallpaper rendering on the outside of the building. That is z-fighting: two surfaces occupying exactly the same plane, with the depth buffer picking a winner per pixel per frame, and the winner changing as the camera moves.
It is easy to file that under "ugly, fix it later". I would rather not, for a reason that has nothing to do with looking nice: a full-screen area of high-contrast surface swapping colour every frame is a photosensitivity hazard. A game that does that to somebody is not a game with a rough edge, it is a game that hurts people. So yesterday became a whole-level audit rather than a spot fix.

What I was trying to do
Find every coincident-surface defect in the town, rank them by how much actual harm they do, and fix the worst. The town is a World Partition level with 722 distinct static meshes in it and roughly 693,000 triangles at LOD0.
The tools were the usual pair for this project: the Unreal editor driven over two MCP servers, with the actual geometry work done in Blender, because almost every building here is generated by a Python script rather than modelled by hand. That last fact turns out to matter enormously, and it is the most transferable thing in this entry.
The scan would have taken days, and the obvious speed-up silently broke it
Finding coplanar overlapping triangles is an all-pairs problem. Naively that is 3.3 billion pairs across this mesh set — days of Python, which is not a tool, it is a wish.
My first idea was the elegant one: bucket triangles by their plane. Two coplanar triangles share a normal and a plane constant, so bucket on that and you only compare within a bucket. It ran beautifully fast and it silently turned failures into passes. Meshes I already knew were broken came back clean.
The mechanism is worth internalising. The plane constant of two triangles that are nearly coplanar drifts with distance from the world origin — the further from the origin, the more a tiny difference in normal direction moves the constant. So two genuinely fighting triangles land in different buckets and are never compared, and the further from the origin the geometry sits, the more often it happens. The bug is invisible in the output, because the output is a shorter list, and a shorter list looks like good news.
What shipped instead is a spatial AABB grid. The argument for it is that it cannot lose a pair: two triangles can only be reported if they are coplanar within a tolerance and overlap in 2D, and that forces their slightly-inflated 3D bounding boxes to overlap. AABB overlap is a strict superset of the answer. To prove it rather than assert it, I kept the original brute-force version and ran both over 143 meshes looking for byte-identical verdict lines. They matched, at roughly 300× the speed on the large meshes.
If you replace a slow correct thing with a fast clever thing, keep the slow one and diff them. The temptation to delete it is strong and it is exactly wrong.
The scanner reported duplicate faces that do not exist in the source file
This one cost real time and is the trap I would most like somebody else to avoid.
The terraced house is the most-placed building in the game, 565 actors of it. The scan said it had 151 duplicate faces and a lot of brick-versus-plaster coincidence. I opened the source file it was built from and there were zero.
The scanner reads LOD0 render data out of the mesh asset. On a Nanite mesh, LOD0 render data is not the authored geometry — it is the decimated fallback proxy the engine keeps for the non-Nanite path. Decimation invents coincident faces: where the model has a 2 cm plaster skin on a brick wall, the simplifier collapses that gap and puts brick and plaster on one plane. The defect the tool found was real, in the sense that the data really contains it. It was also entirely an artefact of the measurement, describing geometry that only renders on a path the game does not normally take.
126 of the 722 meshes here are Nanite — about 8% of the total ranked risk. Judge and fix a Nanite mesh on its source, never on what the asset's LOD0 hands you. I now have a census script whose only job is to say which meshes fall into this category, so the question gets asked before the work starts.
The worst mesh in the ranking was not the worst mesh
The audit scored each fighting pair by area, so that a hairline seam would rank below a whole wall. It scored the pair as the area of the smaller triangle.
That is the whole triangle — even when the two triangles overlap along a one-centimetre sliver. This building kit is modelled as solid boxes whose slabs and walls deliberately overlap each other by a centimetre or two, so the metric was crediting entire wall and floor triangles for a seam nobody will ever see. Measured properly, with a real polygon clip, the terraced house claimed 42.1 m² and actually had 20.6 m². The overstatement is not a constant factor either — it depends on how each kit is triangulated — so you cannot even correct for it in your head.
The ranking was still useful. It was a priority order, not a measurement, and the fix was to stop treating a sorted list as a set of numbers and re-measure each specific mesh before starting on it.
The fix has to go into the generator, and the generator is where you should measure
Here is the thing that changed how I work on procedural geometry.
Every one of these buildings is emitted by a Python script. So the fix never belongs in the exported mesh — it belongs in the code that emits it, or the next rebuild throws it away. But I was doing the measuring on the shipped asset, which meant every iteration was a full export, import, scan and read-back cycle, and when a defect was found the report told me where in the world the fighting faces were, which is close to useless when you are looking at 600 lines of geometry code.
The thing that broke it open was building an analyser that runs the generator in memory and monkey-patches the primitive emitters — the box, quad and triangle calls — so that every face it produces remembers the line of source that emitted it. Then it clusters the results exactly the way the asset-side scanner does.
It matched the shipped asset to within 0.01 m² before I changed anything, which is what made it trustworthy, and then it named all six defects in the pub in a single run, each one attributed to the line that caused it. A ceiling drawn twice. Both side walls where the exterior brick and the interior liner shared one plane over the full nine-and-a-half-metre height — the "wallpaper on the outside". 564 groups of floorboards emitted twice over.
If you generate geometry with code, instrument the generator, not the output. The round-trip you save is the smallest part of it; the attribution is the real prize.
Nudging the surfaces apart made it worse, twice
The instinct with two surfaces on one plane is to move one of them. That instinct is half-right and the wrong half is expensive.
Two things I tried made the total worse, not neutral:
- Lapping the corners. Extending the render skin around the corner to overlap the brick
return put a sheet of the wrong material on a face that is in plain view. Fighting became visibly-wrong-material, which is not an improvement.
- Laying the floor deck exactly to the plan bounds. Tidier by every reading of the code,
and it took the fighting area up from 4.14 to 4.81 m².
The rule that came out of it: lap what is hidden, butt what is exposed. Only same-facing pairs actually fight — two faces back-to-back get culled and never argue — so a clean butt joint at the plane where two parts meet is a genuine fix, not a fudge, and an overlap is only safe where nobody can see the loser.
Reimporting one mesh of a mirrored pair silently unbound all twelve materials
The terrace ships as a normal and a mirrored twin. Reimporting the twin on its own left it wearing the default checker material on all twelve slots.
The cause is a piece of history baked into the asset. The two were originally imported in the same batch, and because both source files named their materials identically, the engine invented disambiguated slot names for the second one. Reimported alone, years later, the source file has no need to disambiguate, the slots come back with plain names, nothing matches the invented ones, and every binding falls through to the fallback material. Rebinding from the unmirrored twin fixes it, and works because component overrides on placed actors are indexed, not named.
Expect this on any second-of-a-pair mesh reimported by itself. There is a related trap one layer up: loading two source files into a single Blender session renames the second one's materials too, with a suffix, and Unreal rematches by name on reimport — so the exporter has to purge material and mesh data between loads, not just objects.
Both editor bridges said "Unreal Editor not running" for six minutes and nothing was wrong
Writing the new source files under the project's art folder triggered the editor's automatic reimport — twenty building shells, no prompt, no harness, no confirmation. While that was running, both MCP servers answered every call with an editor-not-running error. The port was open the entire time.
That error means busy at least as often as it means gone, and diagnosing a crash from it sends you off to restart an editor that is working perfectly. The behavioural fix is simple: export source files only at the moment you are ready for the reimport to happen, and check material bindings afterwards regardless of whether you asked for anything.
I also lost one round of work to a genuine editor crash during one of those auto-reimports. Nothing was corrupted — the reimports were unsaved, so the on-disk assets were simply the last saved versions — and redoing them one mesh at a time worked. Cause not established. I am recording that honestly rather than inventing a theory.
The bug that was also blocking the stairs
The best moment of the day was accidental.
One of the two shared defects in the amenity-building generator was a floor slab drawing its underside inside itself and ignoring the voids cut through it. Fixing it dropped the hospital's fighting area from an absurd 100,079 m² to 35. But the soffit it was wrongly drawing was also sealing every stairwell it covered — an invisible sheet of geometry across the hole the stairs go through.
Seventeen blocked stairwells across fourteen buildings, found by a geometry cleanup, not by anyone testing navigation. I checked the number honestly by monkey-patching the old broken version back in and confirming the checker reported those seventeen again, because a stairwell checker that reports "all clear" against code that was never broken is not evidence of anything. Calibrate the tool that says you are fine.
Where it stands
Fixed and confirmed on the shipped assets: the terraced houses and their mirrored twin (565 placed actors, from 20.62 m² of cross-material coincidence each to 0.02), the church (411.91 → 0.42), the hospital, the council depot, twenty amenity shells, twelve duplicate road markings deleted, and the pub itself (54.56 → 2.54).
Every one of those is a geometric fact, measured on the asset. Not one of them has been looked at in a play session yet. The pub in particular needs eyes on it: the floorboard grid moved two centimetres, and while the toilets, landings and stairs were explicitly protected from moving, "the numbers say it is fine" and "it looks right when you walk in" are different claims and I have only earned the first one.
Still fighting: the petrol station, the railway station, the primary school entrance, and the nine parade shop units — which all show near-identical numbers, which is itself the clue that they share one defect in one function.
There is also a deliberate landmine left in the pub toilets: two surfaces share a plane there, harmlessly, only because both are currently bound to the same placeholder material. The moment the tile materials get built it will start fighting. It is written down in the handover with the one-line fix, which is the honest way to leave something you have decided not to do today.
What to take from this
- **A high-contrast surface flickering across a big part of the screen is an accessibility
problem, not a polish problem.** That reframing is what got the whole day funded.
- Keep the slow correct implementation and diff against it. The fast clever broadphase
produced a shorter list, and a shorter list looks exactly like success.
- On a Nanite mesh, LOD0 render data is a decimated proxy that invents coincident faces.
Judge the source.
- A ranking is not a measurement. Check what the score actually computes before you
believe the top of the list.
- If code generates your geometry, measure inside the generator and make every face
remember which line emitted it. Attribution beats coordinates.
- Lap what is hidden, butt what is exposed. Same-facing pairs fight; back-to-back pairs
are culled.
- Calibrate any checker that tells you things are fine — run it against the known-broken
version and confirm it still complains.
- "Editor not running" from a tool bridge often means "editor busy". Do not restart on
the strength of it.
- Measured-clean is not the same as seen-clean. Say which one you have.