World Alone takes a GPS point and lets you walk around it. The streets, the building footprints and the elevation arrive from OpenStreetMap and public terrain tiles while you move, and nothing is cooked ahead of time. The hard part is not downloading a map. It is that the world is built one sector at a time, in whatever order the network answers, and each sector has to agree with neighbours it has never seen.
This article covers how streaming works in SETech, where live map data plugs into it, and the rules that make independently built pieces line up. You can try it in your browser: World Alone is this code compiled to WebAssembly.
Streaming in SETech, before any map data
The engine streams cooked worlds by sector. A world is cut into a uniform grid on the ground plane, deliberately coarser than the culling grid, and each non-empty sector is cooked into its own package. A small world index stays resident and says which sectors exist and how large they are.
Streaming is opt-in. An application creates a streaming manager, points it at the world index and its scene, and ticks it each frame with the camera position. Scenes that load all at once never touch it. The manager keeps a ring of sectors around the camera with two radii, one to load and a larger one to unload, so a sector on the boundary does not thrash. An in-flight cap and an optional resident-bytes budget bound the IO and the memory.
The work is split at a frame boundary. A background IO thread reads a whole sector package into memory and parses its placement list. The main thread then mounts that package from the buffer, spawns the entities through the normal scene loader, capped per frame so a burst of new sectors does not construct everything at once, and unmounts the package again so only GPU resources stay resident.
The piece that made this cheap to build is one property of the file system: every loader in the engine opens files through it, and it resolves mounted packages first. Mount a sector's package and its models, textures and materials simply resolve, with no change to any loader. Sectors are self-contained, so eviction is equally plain: delete the entities, and the shared caches let go of whatever nothing references any more. Sectors that hold a global entity, such as the sky or the sun, are pinned so the environment never streams out.
One thing we did not build is an asynchronous GPU upload queue. We planned one, then measured: the hitch when a sector appeared was the first-use warm-up of rendering pipelines, not the reads or the uploads. Prewarming fixed it, and the per-frame spawn cap does the pacing.
Terrain follows the same idea at a different scale. There is one world-spanning quadtree heightfield with a coarse base that is always resident, and sectors patch fine tiles into it as they arrive and remove them as they leave. The horizon comes for free, there are no seams between sector terrains because there is only one terrain, and level of detail stays coherent across the whole view.
Where live map data plugs in
It would be tidy to say the OpenStreetMap world is just another provider behind that streaming manager. It is not, and we would rather say so. Today there are two streamers with the same shape: a ring around the camera, nearest first, an in-flight cap, load and unload radii. One reads cooked packages. The other downloads and builds. What they share is everything downstream of them: the heightfield tile format, the terrain marker format and the scene itself. Folding them into one streamer with two providers is on the plan and not yet built.
The seam is therefore the terrain system and the scene world. The live streamer sizes the heightfield's quadtree so that a leaf is exactly one sector, and snaps the world extent so streamed tiles align with quadtree cells. From the renderer's side, a sector built from a download is indistinguishable from one loaded from disk.
One sector, end to end
- A coarse base first. On start the streamer requests low-detail elevation over the whole extent and builds the resident base from it. If that is slow it proceeds anyway after a timeout: the world is not held hostage by one request.
- Two fetches per sector. Elevation tiles, which are shared between sectors and reference counted, and one map query for the sector's area.
- Terrain as soon as elevation lands. Terrain never waits for the map query, so the ground appears before the city does. If elevation failed, no tile is registered and the coarse base shows through.
- Then the map. Water is resolved first, because everything after it samples the ground. Then buildings, bridges, ground roads, land cover, street furniture and name signs, and water surfaces.
- Eviction. Past the unload radius, and never while a fetch is pending, the sector releases its requests and elevation references, removes its tile, and deletes its markers and entities.
Failures are surfaced, not swallowed. When a sector's map data cannot be had, the application is told, because a city quietly missing its roads looks identical to one that simply has none.
Roads are not meshes
Two representation choices keep a sector light. Buildings are not one entity each. They are merged into shared meshes per facade style. And ground roads, land cover and street name paint are not geometry at all. They are markers, baked into a camera-centred grid of Bezier bands and evaluated per fragment in the terrain shader. Edges are resolution independent, a road costs no vertices, and the bake only reruns when the grid moves or the marker set changes.
Agreeing with a neighbour you have never seen
Here is the constraint that shaped the design. Two adjacent sectors are built at different times from different downloads. Each sees a different subset of the world. A road, a viaduct or a river crosses the border between them, and the two halves must meet exactly. There is no later pass that sees both and fixes the seam. So every rule that produces geometry has to give the same answer no matter which sector asks.
A bridge solver with a horizon
Bridge decks need heights: clear the road or water below, meet the ground at the ramps, keep gradients sane. The obvious tool is an optimiser over the whole network. It is the wrong tool here, and the comment in the solver says why:
/// Why an envelope rather than an optimiser: the answer has to be identical in every
/// sector that solves it, and sectors see different subsets of the world. A least-
/// squares or LP solution has global support, so one extra way at the edge of one
/// sector's data moves the answer everywhere. This solution is pointwise, and a demand
/// stops mattering once its cone has fallen to the ground, which bounds how far any
/// input can reach and is what lets two sectors agree.That bound is a promise, not an optimisation. Influence ends exactly at a fixed horizon, and the map query for roads is padded by exactly that horizon. So inside a sector, every input that could affect the answer is present, and both neighbours compute the same heights at the border. Smoothing of the vertical curve happens after the solve with a fixed-width filter, because a smoothing term inside the solve would couple every station to its neighbours and destroy the locality.
Ownership follows from that. Every sector that can see a bridge solves it, and every sector builds only its own slice: the segments whose midpoints fall inside it. Neighbouring slices share their boundary stations bit for bit, so a viaduct crossing several sectors is watertight. A single owner for the whole bridge cannot do this, because its data is only exact near itself, and the far end would be solved from whatever that one sector happened to see. That is where our mid-air cliffs came from, before.
Junctions by identity, not by distance
/// Identity of a shared node. Two ways meeting at a junction carry the SAME OSM node
/// through the same projection, so their world positions are bit-identical and a
/// millimetre quantisation matches them exactly. This is not a proximity weld:
/// welding by radius depends on the order ways are visited, which would make the
/// answer depend on the sector.
uint64 SEOSMBridgeNetworkTools::MakeNodeKey( const SEVec2& _vPoint )
{{
int64 iX = ( int64 )llround( ( double )_vPoint.x * 1000.0 );
int64 iZ = ( int64 )llround( ( double )_vPoint.y * 1000.0 );
return ( ( uint64 )( ( uint32 )( int32 )iX ) << 32 ) | ( uint64 )( ( uint32 )( int32 )iZ );
}}Anything order dependent is a seam waiting to happen. The same thinking runs through the rest of the generator:
- Solve against ground that does not change. Fine terrain tiles get folded into the live base as they load. A bridge solved against that would change shape as its neighbours arrived, so solvers read an immutable copy of the coarse heights.
- One owner per object, decided by a point. A building belongs to the sector that contains its footprint centroid, a street label to the sector containing the way's midpoint. Half-open intervals mean exactly one sector says yes.
- Pad the query only where it pays. Roads, rails and waterways are padded by the bridge horizon, coastlines and water by the shoreline carve distance. Buildings and land use are not padded, because padding those clauses is what made queries time out.
- Overlap where mitring is local. Ground road pieces are clipped to a rectangle slightly larger than the sector, so independently mitred pieces cover the seam.
- Variation from position. Per-building variation is hashed from the quantised centroid, so a building looks the same every time it streams in, with no stored identifier.
One thing could not be made order independent, so it is reconciled. The level of a lake or river is estimated per sector, and sectors see different parts of the shore. A registry keeps the lowest level seen for each body, and sectors that used an older value are carved again. The final value is a minimum over sectors, so load order stops mattering.
The ground had buildings in it
The public elevation tiles are a surface model: buildings are in the elevation. We found this in the flattest place we could think of, a large square in central Cairo, which arrived with hills. The relief did not smooth away at higher detail, it converged, which is how you tell real content from sampling noise. The hills were two large buildings. The ground was being displaced by the height of the buildings standing on it, and then we extruded our own buildings on top of that.
The fix is a morphological opening: erode, then dilate, with a kernel wider than a city block. It deletes positive features narrower than the kernel and leaves wider ones alone, so Cairo goes flat and San Francisco keeps its hills. It is separable, which keeps it cheap. The filter runs on a grid that is snapped globally and padded by its own support, so the filtered heights two neighbouring sectors compute along their shared edge are the same numbers.
The first version ran that filter at the terrain tile's own spacing, on the main thread, and froze the frame on every location change. But an opening removes everything finer than its kernel by construction, so evaluating it finely is work whose result is thrown away. Moving it to a coarse grid and sampling up cost a small fraction of the time, for a surface identical to the eye.
A sea made from lines
OpenStreetMap has no ocean polygons. It has coastline ways, drawn with land on the left and water on the right. Per sector, the streamer stitches the coastline pieces into chains while preserving direction, because reversing a coastline swaps which side is water. It then closes each chain against the padded sector rectangle, walking the border clockwise from each exit to the next entry, which yields water rings with islands as holes.
A sector with no coastline at all still has to decide whether it is inland or open sea, and it does so from the immutable coarse bathymetry. There is one sea surface for the whole world, spawned only when a coastal sector proves it is needed, and it depth tests against carved terrain so land masks it. A city in the desert never pays for an ocean.
Inland water comes from multipolygon relations, since a large river only exists as relation rings. Bodies near sea level flood to the sea plane. Others hold their own level. Terrain is carved relative to the controlling body: a bed that shelves down from the shoreline, and a dry berm on the bank that fades inland.
On one thread, in a browser
Sector generation runs on the main thread on every platform, not only on the web. Only the fetches are asynchronous. Responsiveness comes from structure: nearest sectors first, a cap on work in flight, one terrain build per frame, terrain decoupled from the map query, and representations that are cheap to build. Applying a sector's map data is not time sliced, and that is a known weakness.
The browser build, which has a single thread for everything, pushed more work onto the GPU. Road and text paint is evaluated in the terrain shader. Sea waves are displaced on the GPU. The physical sky is marched into a small lookup texture, after we measured that turning the sky off recovered far more frame time in the browser than reducing draw counts did. The web build also keeps fewer sectors resident, which means less of everything: geometry, draws and fetches.
What it does not do
- Two streamers, not one. Cooked and live streaming share formats, not code.
- A fixed origin. There is no floating origin, and the projection is a plain equirectangular map about the start point, so a world is a city, not a continent.
- Agreement by construction, not by test. We have smoke tests for the bridge solver and the building geometry. We do not yet have a test that solves one network from two different sector subsets and compares the results.
- Known seams remain. Where along a ramp the raised deck begins is judged per sector from the stations near it, so two sectors can disagree by a small bounded amount at their border.
- Traffic is set dressing. Cars follow lanes kinematically.
- It depends on public servers. When they are slow or unreachable, so is the city.
The point
Streaming a map is mostly not a graphics problem. It is a problem of making a function of partial data return the same answer wherever it is evaluated. Bound how far any input can reach, fetch exactly that far, decide ownership by a point, key shared things by identity, solve against data that does not change, and reconcile the one thing that cannot be made local. Do that, and sectors can arrive in any order and still meet at the border.
Pick a place you know and walk it.
The map data is © OpenStreetMap contributors under the ODbL, and the demo credits it and the terrain source in its about panel.