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.
中文 · 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.
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
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,
i3dminstanced models, point clouds. See Known issues.
- 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.
| 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 |
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.glbNamed 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.csvA 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.glbmeshopt-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.glbOption 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.glbOnly 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 pictureSome tilesets compress geometry with meshopt (3DBAG, PDOK). Add --extra meshopt to uv run for those;
it builds a small C++ decoder once.
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.
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 flyKeep the data's license in mind if you publish the game: the tested places are open, and only the Cologne layers need no attribution.
| 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 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.
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 gcj02It 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).
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.
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.
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 withDACH_MAX: median 2 mm, all within 5 mm.scripts/check_against_attributes.pydoes 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.
- 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),
i3dminstanced 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 asnode12_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 glTFasset.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.
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.
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 GIFMIT, see LICENSE. The data you cut out keeps its own license.





