Mythralen – UE5 Procedural RPG World (Work in Progress)

Unreal Engine 5.7 · procedural worldgen

Mythralen

A medieval RPG world generated end to end in C++ — terrain, river, city plan, buildings and dungeons — where the goal is that a generated town reads as though someone laid it out on purpose.

C++ / UE 5.7LumenNanitei5-13600KRTX 3060 Ti 8 GB

One capital, measured

Every figure on this page is read from the build log of a single capital, not estimated.

1,223buildings

782blocks

7wards

0invariant failures

55,776trees

210×210cells @ 8 m

How a city is planned

The planner runs in a fixed order, and each stage proves a property before the next one is allowed to depend on it. The numbering below is the actual execution order.

  1. Site

    A settlement mask is grown from a seed and a site radius, giving the irregular hull the walls will later be fitted to. The grid is 210×210 cells at 8 m.

  2. Streets and blocks

    Roads are laid as curves and the land between them is cut into blocks. Blocks that fill badly are re-cut on the other axis rather than accepted.

    782 blocks from one capital seed

  3. Plots

    Each block is subdivided into oriented plots — a rectangle with a frontage direction, not an axis-aligned box. A house can then be measured in the same frame its plot was fitted in, so the building’s angle drops out of the arithmetic.

    no two plots overlap, by construction

  4. Buildings

    Ward rules choose silhouette, storeys and surface; the catalogue supplies the mesh; placement claims ground so the next building cannot take it. Anything that still will not fit shrinks, then is refused.

    1,294 plots → 1,223 buildings

The distinction that matters: overlap is treated as an invariant rather than a quantity to minimise. An earlier version measured overlap after the fact and tried to tune it downward — but 3% of 3,444 buildings is still a hundred visibly interpenetrating houses spread across every street.

Buildings

Houses are procedurally generated, then baked to a static-mesh catalogue and placed as instances. Baking took the map from 938 MB of actors to 7.5 MB of instanced meshes, and gave the lighting the distance fields it needed.

Wards are rule sets rather than a colour on a map. A ward chooses which silhouettes it builds from — the slums build single-storey cottages and nothing else, a market street builds narrow townhouses, the noble quarter builds halls and towers — and then how densely it fills its plots.

Ward Builds Plot fill
Slums Single-storey cottages, timber 76–94%
Craftsmen Cottages and workshop halls 94%
Market Narrow townhouses, continuous frontage 100%
Noble Halls and towers, stone 55%
Temple Halls with towers 70%
Military Barracks and wall works 85%

The noble quarter takes barely half its ground on purpose — a great house needs grounds around it, and the gap is the statement.

Everything is audited

The generator is large enough that eyeballing a screenshot stopped working a long time ago. Each build measures itself and prints the result.

Audit Asks Current
Ground Is every placement on the surface? 2,305 checked · 0 floating · 0 buried
Waterline Is anything standing in the river? 3 (was 23)
Coverage How much of the walled city is building? 33.1%
Water sheet Does the river lie over dry streets? 55 quads withheld of 709
Wall clearance Does the curtain cross a house? nearest corner 2,642 uu

Coverage is the one I keep returning to. Real medieval walled cities ran 50–70% building coverage inside the walls; this capital is at 33.1% — 324,539 m² built on 981,312 m² intramural. That gap is why the town still reads as roomier than the thing it is imitating.

Open problems

The parts I would most like an experienced pair of eyes on. Each one is stated with the measurement behind it rather than an impression.

Instancing fragments under varietyperformance

The houses are Nanite and instanced, but variety splits them hard. Twelve catalogue variants across sizes and functions, then split again per ward so each ward can carry its own surface, leaves roughly two buildings per component and about five material sections each.

Fewer variants is the obvious answer and it is visibly repetitive — that number was raised from six to twelve precisely because streets looked like two houses alternating. Is there a standard way to hold high variety without fragmenting instancing like this?

1,081 buildings across 523 mesh components
4,457 draws · 21.6 fps · 1.57 M tris (excl Nanite)
texture 1,746 MB of 3,000 MB pool — GPU-bound, not VRAM-bound

Shadowed surfaces read blacklighting

A dense city under a low sun shadows itself heavily, and a shadowed surface here receives very little fill, so shadow reads as black rather than dim. Raising the skylight to a key-to-fill of about 3.8:1 fixed the sunlit case; overcast and fog still lose the roofs entirely.

Suspected root cause is Lumen surface-cache coverage on meshes baked at runtime — the docs are explicit that areas without coverage will not bounce light and appear black — but that is still unverified.

sun 34 · sky 9.0 · key-to-fill 3.8:1
weather does not currently drive sun/sky balance at all

Building coverage well under the real figureworldgen

At 33.1% against the 50–70% a real walled city ran, the town is too roomy. The chain from block to rectangle to plot to catalogue entry loses a little at every step, and the catalogue’s fixed size table is the last and largest loss: a plot deeper than its entry is filled by the next size down.

324,539 m² built · 981,312 m² intramural · 15,333 cells

Trees standing in the riverworldgen

The forest planter clears the city and the dungeon mouths but does not test the water mask, so several hundred trees grow out of the channel.

55,776 trees from 149,769 candidates (37%) — 463 on the water

Also in the world

System Detail
River 10.2 cells wide · channel 713 uu · water 321 uu below street
Water surface 654 quads swept along 159 centreline samples
Terrain 576 baked chunks · 16.3 MB
Wall polygon curtain, 8 gates, towers on real corners
Bridges 3 crossings, span measured bank top to bank top
Market kit 228 stalls · 36 carts · 103 awnings
CityEngine Vitruvio, hero plots only — 265,694 tris each

The CityEngine integration is worth a note: Esri’s Vitruvio plugin generates a building to a footprint, which is exactly the shape of the coverage gap above. It is restricted to the largest plots because each building costs roughly 8 MiB of always-resident ray-tracing geometry against a 400 MiB budget — triangles were never the binding constraint, the acceleration Full technical overview with measured figures and the problems I’m stuck on:

Short version. An ordered planner runs site → blocks → plots → buildings, and each stage proves a property before the next depends on it — no two plots can overlap by construction rather than by discarding bad ones afterwards. A capital comes out at 782 blocks, 1,294 plots, 1,223 buildings across 7 wards, 0 invariant failures, on a 210×210 grid at 8 m. Buildings are procedurally generated then baked to a static-mesh catalogue and placed as instances, which took the map from 938 MB to 7.5 MB.

The thing I’d most like help with: draw calls, and specifically how to keep visual variety without fragmenting instancing.

The houses are Nanite and instanced, but 12 catalogue variants across sizes and functions, then split again per ward so each ward carries its own surface, leaves roughly two buildings per component at ~5 material sections each. Fewer variants is the obvious answer and it’s visibly repetitive — I raised that number from 6 to 12 precisely because streets looked like two houses alternating.

Is there a standard approach here? I’ve considered merging material slots at bake time, and per-ward material parameters instead of per-ward components, but I’d rather hear what’s actually worked for people.

There’s a second question on the page about CityEngine/Vitruvio facades — they look exactly right but cost ~8 MiB of always-resident ray-tracing geometry each against a 400 MiB budget, so they’re restricted to hero plots.

Other known problems are listed on the page rather than here, so nobody wastes time on things I’ve already measured.structure was.

Figures

Two views of one capital seed: the plan from directly overhead, and the same town from outside its wall.

One capital seed, shot from 420 m. The radial street network, the blocks cut between the roads, the wall following the settlement hull, and the density difference between wards are all generator output — nothing here is placed by hand. Roofs read dark from directly overhead and the riverbank is still untextured; both are on the list above.

The same capital from outside the wall. The curtain follows the settlement hull rather than a drawn shape, the river passes through it under a water gate, and the wood runs to the wall — the field belt a real town would keep between the two is one of the things still missing.

Built on: Intel i5-13600K (14C/20T) · RTX 3060 Ti 8 GB · 2×16 GB DDR5 · Unreal Engine 5.7. Generation is C++; asset pipeline and diagnostics are Python run through the editor.

Figures are read from a single capital’s build log. Where a number is an estimate or unverified, this page says so.