Skip to content
LongadoPublic

About

Cut a neighbourhood out of a web 3D city map (I3S / 3D Tiles) and keep it as a GLB: real metres, one object per building, with names and attributes.

Topics

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

A chunk of Cologne, cut out with meshcity

meshcity

Cut a neighbourhood out of a web 3D city map and keep it as a real model.

For Blender, game engines and 3D printing. Reads I3S and 3D Tiles, the two formats nearly every web 3D city streams.

MIT tested on macOS, Linux, Windows Python 3.10+ latest release

中文 · How it works · Quick start · Walk it in a game · Tested places · Build with us

How it works, in one sentence: a web 3D map is a stream of public tiles that your browser downloads as you look around; meshcity requests the same tiles for the square you mark, decodes them and joins them into one model.

Cologne Cathedral as a textured photogrammetry mesh
Cologne Cathedral, photogrammetry mesh (I3S) · Land NRW, Datenlizenz Deutschland – Zero 2.0
Umeda Sky Building, Osaka, textured
Umeda Sky Building, Osaka (3D Tiles) · Project PLATEAU, MLIT Japan
Federal Palace, Bern, with coloured roofs
Federal Palace, Bern (3D Tiles) · © swisstopo
Te Aro, Wellington, textured buildings
Te Aro, Wellington (I3S) · Wellington City Council and AAM Ltd, CC BY 4.0

Each picture is a 240 m square cut out by meshcity and rendered in Blender, not a screenshot of the source map.

Only the tiles your area touches · No API keys, no logins, nothing bypassed · Refuses areas over 1 km² unless you raise the limit

What it is

City 3D maps in the browser (ArcGIS, GeoScene, CesiumJS, national open-data viewers) stream the city as a tree of tiles: coarse at the top, fine at the bottom. Almost all of them use one of two open formats: Esri I3S (…/SceneServer) or OGC 3D Tiles (tileset.json). meshcity walks that tree, fetches only the finest tiles that touch the square you asked for, decodes them, and writes one GLB in real metres, with one object per building, named and carrying its attributes when the source has them.

It is also a Claude Code skill: ask your agent for "the buildings around Tokyo Station as a Blender file" and it finds the stream, checks the license, cuts the area and shows you a preview.

Version 1.2. Real places were cut on macOS (the tested places: both formats, I3S 1.5–1.10 and 3D Tiles 1.0/1.1); the test suite also runs on Linux and Windows. Not supported yet: 3D Tiles implicit tiling, i3dm instanced models, point clouds. See Known issues.

What you can make with it

  • A game level on real streets. Load the GLB in Godot, Unity or Unreal; every building is its own object with a name, so you can make one enterable, clickable or destructible. See Walk it in a game.
  • Context for a design. Put the real neighbours around a site in Blender, Rhino or any tool that opens OBJ or FBX, and check sun, shade and views.
  • Pictures for an article or an explainer. The four above took one command and one Blender render each.
  • Numbers about a block. The CSV lists every building's attributes, such as height, storeys and use where the source has them.
  • A 3D print. The Blender script writes STL.

How it works

You… It…
give a point and a radius (default 200 m) or a bounding box walks the tile tree and downloads only the finest tiles touching that square
get back one GLB origin at the area's centre, +X east, +Y up, −Z north, metres; y = 0 at the lowest point, or the source's own heights with --keep-heights
look at a building in the file it is its own object, named from the data, attributes in extras and in an optional CSV, with the source's object id when it has one
need it back on the map the GLB records its origin, height offset and ECEF transform; --tileset also writes a tileset.json that opens it in place in CesiumJS, ArcGIS or QGIS
cut several layers at once pass several URLs (buildings, roads, trees…): one group per source in the GLB, a layer column in the CSV
open a textured layer textures come along; I3S texture atlases are split so tiling still works
lose the connection re-run: every download is cached in ~/.cache/meshcity; a tile that still fails is left out with a note instead of stopping the cut
cut a photogrammetry mesh it trims the mesh at the edge of your area, triangle by triangle
pick an area that needs more than 400 tiles at full detail it uses the finest level that fits in 400 and says so; --max-tiles raises the limit
ask for more than 1 km² it refuses until you raise --max-area
leave it running it pauses 0.15 s between requests and uses 4 connections

Try these

Each of these was run as written. Downloads are cached, so a second run takes seconds.

A cathedral as photogrammetry. Cologne, from the state of North Rhine-Westphalia (no attribution required). You get a textured mesh trimmed to a 300 m square: about 590,000 triangles, 31 MB.

uv run meshcity extract "https://www.gis.nrw.de/geobasis/3D_mesh/SceneServer" --center 6.9583,50.9413 --radius 150 -o koeln.glb

Named buildings with attributes. Tokyo Station from Project PLATEAU: 69 buildings with textures. The station's parts are named 東京駅, and the CSV has measured heights for 61 of the 69.

uv run meshcity extract "https://assets.cms.plateau.reearth.io/assets/28/07d0a1-b6be-46ef-bd87-4f0683b5ef6e/13101_chiyoda-ku_pref_2025_citygml_1_op_bldg_3dtiles_13101_chiyoda-ku_lod2/tileset.json" --center 139.7671,35.6812 -o tokyo.glb --attributes tokyo.csv

A rectangle instead of a square. Bern's Federal Palace and the old town to its north: 257 buildings with coloured roofs.

uv run meshcity extract "https://3d.geo.admin.ch/ch.swisstopo.swissbuildings3d.3d/v1/tileset.json" --bbox 7.4400,46.9450,7.4480,46.9485 -o bern.glb

meshopt-compressed tiles. The Markthal in Rotterdam from 3DBAG: 57 buildings, each carrying 3DBAG's attributes (storeys, roof heights and more) in the file.

uv run --extra meshopt meshcity extract "https://data.3dbag.nl/v20250903/cesium3dtiles/lod22/tileset.json" --center 4.4879,51.9200 -o rotterdam.glb

Quick start

Option A: let your AI agent set it up. Paste this into Claude Code or Codex:

Read https://github.com/Longado/meshcity/blob/main/README.md. Check that uv is installed
(install it if not). Clone the repo into ~/.claude/skills/meshcity so it also works as a skill.
Then cut out the buildings around Tokyo Station (139.7671, 35.6812, radius 200 m) from the
PLATEAU tileset listed in examples/places.json, save tokyo.glb, and if Blender is installed,
render a preview with scripts/blender_export.py and show it to me. Tell me the data's license.

Option B: by hand. Needs uv (it installs Python and the dependencies).

git clone https://github.com/Longado/meshcity ~/.claude/skills/meshcity
cd ~/.claude/skills/meshcity
uv run meshcity extract "<layer or tileset url>" --center <lon>,<lat> --radius 200 -o area.glb

Only want the command? uv tool install git+https://github.com/Longado/meshcity puts meshcity on your PATH.

Preview and convert with Blender (optional):

blender -b -P scripts/blender_export.py -- area.glb out/            # preview + OBJ + FBX + STL
blender -b -P scripts/blender_export.py -- area.glb out/ preview    # just the picture

Some tilesets compress geometry with meshopt (3DBAG, PDOK). Add --extra meshopt to uv run for those; it builds a small C++ decoder once.

Finding the stream

uv run meshcity discover "https://some-city-3d-viewer.example"

reads the page and its own scripts and lists every SceneServer / tileset.json it mentions. When a page builds its URLs at runtime, open it with the browser's network panel and filter for SceneServer or tileset.json.

Walk it in a game

Walking and then flying around Tokyo Station in Godot

Tokyo Station area (PLATEAU, MLIT Japan) cut with meshcity and loaded in Godot 4.7 by examples/godot. Recorded from the project's scripted demo.

examples/godot is a small Godot project that loads any cut at run time, gives every building a collision shape, adds a ground plane and a sky, and puts you on the nearest open street. Look at a building and its name, height and use appear at the bottom of the screen.

uv run meshcity extract "<layer or tileset url>" --center <lon>,<lat> -o examples/godot/city.glb
godot --path examples/godot        # WASD walk, Shift run, Space jump, F fly

Keep the data's license in mind if you publish the game: the tested places are open, and only the Cologne layers need no attribution.

Commands

Command What it does
meshcity extract URL… --center lon,lat [--radius m] cut a square around a point
meshcity extract URL… --bbox w,s,e,n cut a rectangle
--attributes out.csv also write every object's attributes
--all-fields I3S: fetch all attribute fields, not just the name
--name-field FIELD which attribute names the objects (guessed otherwise)
--max-tiles N download at most N tiles (default 400); above that it steps down to a coarser level
--max-error M 3D Tiles: never go finer than tiles accurate to M metres; makes photogrammetry files smaller
several URLs merged into one scene, e.g. buildings + landmarks + roads, one group per source
a path to a tileset.json on disk read a downloaded tileset instead of a URL
--tileset out.tileset.json also write a tileset that places the GLB back on the globe
--keep-heights keep the source's heights instead of starting at y = 0
--coords gcj02 or --coords bd09 the coordinates come from Gaode/AMap or Tencent maps (gcj02) or Baidu maps (bd09); see below
meshcity discover PAGE list the 3D streams a web page loads
meshcity --version print the version

From Python

from meshcity import Area, extract

scene = extract("https://…/tileset.json", Area.around(139.7671, 35.6812, 200))  # or Area(west, south, east, north)
scene.write_glb("tokyo.glb")
scene.write_csv("tokyo.csv")                            # optional
scene.write_tileset("tokyo.tileset.json", "tokyo.glb")  # optional: back on the globe
print(scene.stats(), scene.warnings)

extract takes the same options as the command line (max_tiles, max_error, name_field, …). Problems raise MeshcityError; AreaError and FetchError are its subclasses.

Coordinates from maps in China

Maps in mainland China do not show GPS coordinates. Gaode/AMap and Tencent use GCJ-02, WGS84 moved by a published formula; Baidu moves it once more (BD-09). A point copied from them is a few hundred metres off (about 550 m in Beijing, 360 m in Chengdu). meshcity works in WGS84, so say where the numbers came from:

meshcity extract URL --center 116.40374,39.91010 --coords gcj02

It prints the WGS84 point it used. Coordinates from GPS, Google Earth or OpenStreetMap need nothing. In Python: meshcity.gcj02_to_wgs84(lon, lat) and meshcity.bd09_to_wgs84(lon, lat).

Tested places

These run in the smoke test (scripts/smoke.py) with a 120 m radius. All are open data; the attribution each one needs is in examples/places.json.

Place What you get Format License
Cologne Cathedral, Germany textured photogrammetry mesh I3S 1.7, UTM 32N Datenlizenz Deutschland – Zero 2.0
Cologne Cathedral, Germany LoD2 buildings I3S 1.10, UTM 32N Datenlizenz Deutschland – Zero 2.0
Helsinki Cathedral, Finland textured buildings I3S 1.6 CC BY 4.0
Place Bellecour, Lyon, France textured buildings I3S 1.6 Licence Ouverte
Te Aro, Wellington, New Zealand textured buildings I3S 1.8, Draco CC BY 4.0
Federal Palace, Bern, Switzerland buildings, one layer in each format I3S 1.10 and 3D Tiles 1.0 swisstopo free geodata
Tokyo Station, Japan textured LoD2 buildings with names 3D Tiles 1.0, Draco, WebP PDL 1.0 (compatible with CC BY 4.0)
Umeda Sky Building, Osaka, Japan textured LoD2 buildings 3D Tiles 1.0, Draco, WebP PDL 1.0 (compatible with CC BY 4.0)
Sakae, Nagoya, Japan LoD2 buildings 3D Tiles 1.0, Draco PDL 1.0 (compatible with CC BY 4.0)
Dom Tower, Utrecht, Netherlands buildings 3D Tiles 1.1, meshopt CC BY 4.0
Markthal, Rotterdam, Netherlands LoD2.2 buildings 3D Tiles 1.1, meshopt CC BY 4.0
Elbphilharmonie, Hamburg, Germany LoD2 buildings 3D Tiles 1.0 Datenlizenz Deutschland – Namensnennung 2.0

It has also been run against 18 other public layers in Europe, North America, Asia and Oceania, six of them photogrammetry meshes. Their licenses are unclear or restrictive, so they are not listed as examples.

Data and licenses

meshcity is MIT-licensed; the data it reads is not yours. Before using a cut-out, look up the source's license. The places above are all open (CC BY 4.0, Licence Ouverte, PLATEAU PDL 1.0, swisstopo free geodata, Datenlizenz Deutschland), and all but the Cologne layers need attribution. Many layers that a browser can see are not open: some say "all rights reserved", some carry a vendor's license that covers viewing in their apps only. Treat anything without a stated license as private reference. The tool does not handle API keys or logins, by design; it also skips Google Photorealistic 3D Tiles, whose terms forbid extracting geometry.

How accurate

The GLB is a copy of the source's geometry in a local frame, so the question is whether anything moves on the way. Two checks, both repeatable:

  • Against the source's own numbers. swisstopo gives every building in Bern its roof height (DACH_MAX). Cutting the Bern example above, putting each of the 257 buildings back on the globe with the transform the GLB records, and comparing its highest point with DACH_MAX: median 2 mm, all within 5 mm. scripts/check_against_attributes.py does this for any layer with an absolute height attribute.
  • Round trip. The tests write a cut with --tileset, read that tileset back with meshcity, and check that every vertex lands within 2 mm of where it was (the GLB stores coordinates as 32-bit floats).

The output inherits the source's heights. 3D Tiles are meant to be in metres above the WGS84 ellipsoid, but not every publisher does that: swisstopo's buildings sit at heights above sea level, as DACH_MAX shows. Mixed with data that uses the ellipsoid, such a layer sits lower by the local geoid height (49 m in Bern, per EGM2008). Each source's height system, where the layer states one, is recorded in the GLB.

Known issues

  • Photogrammetry is heavy. A 240 m square of a city mesh came out at 15–150 MB, and up to 2.4 million triangles, in our tests. Use a smaller radius, or for 3D Tiles --max-error 1 (Strasbourg went from 139 MB to 36 MB). The mesh has no buildings in it: each tile becomes one object.
  • Not supported yet: 3D Tiles implicit tiling (1.1 subtrees), i3dm instanced models (often trees), point clouds, DDS/KTX2-only textures (geometry is kept, textures dropped with a note).
  • Name guessing looks for common fields (NAME, name, 名称, …). If objects come out as node12_f3, pass --name-field. Unnamed objects keep a name made from their tile; the source's object id, where it has one (gml_id, UUID, OBJECTID…), is in the attributes.
  • Edges. A building is kept whole if its centre is inside the area, so the cut can reach a little past the square; photogrammetry triangles are kept or dropped by their centre.
  • Copyright text is written to the GLB only when the source provides it (I3S copyrightText, or the tiles' own glTF asset.copyright); check the attribution each source asks for yourself.
  • Slow servers. Some servers answer at 1–3 s per request from far away. The first run of a 240 m square took from a few seconds up to 15 minutes (a photogrammetry layer in Spain). Re-runs come from the cache.
  • Real places were cut on macOS. On Linux and Windows only the test suite has run.

Build with us

How much of the world's open 3D city data can one small tool open up?

If you like… You could
file formats add 3D Tiles implicit tiling, i3dm instancing, or SuperMap S3M
cities and open data add a place you know to examples/places.json with its license, and run the smoke test
geometry cut triangles exactly at the area's edge (today each triangle is kept or dropped whole)
Windows / Linux cut a real place and report what breaks
games add a Unity or Unreal example next to the Godot one

Issues and pull requests welcome.

Development

uv run --group dev pytest                                      # unit tests, offline
uv run --group dev ruff check .                                # lint
uv run --extra meshopt python scripts/smoke.py                 # run every place in examples/places.json
uv run --extra meshopt python scripts/smoke.py tokyo --previews # one place, with a Blender preview
godot --path examples/godot -- --demo area.glb                 # the scripted walk used for the GIF

License

MIT, see LICENSE. The data you cut out keeps its own license.

About

Cut a neighbourhood out of a web 3D city map (I3S / 3D Tiles) and keep it as a GLB: real metres, one object per building, with names and attributes.

Topics

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages