Extract source-matched LoD2 buildings from Munich's ArcGIS scene or the statewide BayernAtlas feed, export them to GLB, and explore them in Three.js. Optional reconstructions add modeled facades, roofs and streets while keeping original source geometry available for comparison and download.
Requires Node.js 22.13 or newer. New extractions need network access to the geocoder and source services; existing permanent bundles are available locally.
cd website
npm install
npm run devOpen localhost:3000. The development server and generation API bind to loopback. The default sample is Rathaus 100 m in original LoD2 mode. The former Rathaus 35 m sample has been removed; old model links resolve to 100 m.
Other commands, from website/:
| Command | Purpose |
|---|---|
npm run models:catalog |
Regenerate per-address chooser metadata; also runs before development/build. |
npm run build |
Build the local website. |
npm start |
Build and serve with the local generation API. |
npm run lint |
Run the type-aware linter. |
npm run build:cloudflare |
Build static assets for /munich3d/ and browser generation. |
Location buttons switch the model, dimensions, attributes, counts, source link and downloads together. Models automatically fit the visible geometry; orbit, pan and zoom are available in the scene. Add address creates a model; the upload icon immediately beside it imports a ZIP into the site-wide chooser. The upload icon has an Import ZIP accessible label and a hover tooltip.
On phones, the title and Add/Import actions stay above the address chooser. Address buttons wrap onto additional lines so names stay fully visible. The bottom controls use aligned rows for the visible area, display style, and camera actions, with 44 px touch targets and Info always available. Enlarged text gets extra rows when needed, without horizontal scrolling in portrait. The + button opens Add address; Import ZIP stays beside it. Clear margins on both sides of the model allow vertical scrolling on short screens without rotating the model; dragging within the model still controls the camera. On phones, copyright and data attribution appear at the bottom of the main page. The information panel has a filled Close information button at the top. On phones, Add address opens a normal scrolling form so Safari can position the keyboard and text caret together. Its distance field can be cleared while editing; focusing a value of 0 selects it for replacement. Deleting a model successfully closes the mobile information panel and selects a fallback.
The bottom bar groups direct controls:
- Rotation icon / Reset view: rotation starts automatically; pause or resume it with the icon. Reset view restores LoD2, Solid and Neighbourhood, then fits the camera again. Playback stays as it was.
- Building / Neighbourhood: change visibility and fit the camera. Clicking the selected scope returns to its home framing. Explicitly grouped connected parts remain with the main building; independent neighbors hide in Building mode.
- LoD2 / Depth / Facade: original geometry, camera-relative grayscale depth, or illustrative reconstruction. Facade appears only when an area snapshot is available and preserves the current camera position.
- Solid / Wireframe: choose rendering independently, including reconstruction wireframe.
The right column opens its details by default and offers area-width choices when
multiple bundles exist. The compass follows camera orbit: X=east, Y=up, Z=south.
Building GLB with façades downloads the main address with balconies, windows
and other modeled details baked into ordinary mesh geometry. Explicitly connected
parts are included; independent neighbours, streets and decorative ground are not.
This GLB works in a standalone 3D viewer without area.json. Neighborhood GLB,
source-mesh JSON and metadata downloads retain the original LoD2 source.
Every download filename includes the address name, area size and local timestamp
(YYYY-MM-DD_HH-mm-ss). Model and JSON downloads also identify their content,
for example Neues-Rathaus-100m-complete-facade-2026-09-07_11-30-00.glb.
The timestamp is captured when you click the export or download control.
Complete façade GLB exports the entire Facade neighbourhood, including all buildings, authored façade profiles, balconies, paving, trees and mapped street furniture. It always exports the complete scene, regardless of the current scope, wireframe or depth display. Import this standalone GLB into Blender with File → Import → glTF 2.0.
Native Blender scenes are included in local Export ZIP downloads when Blender
is installed. Conversion runs in a fresh background process and removes its
temporary files afterward. The server checks the default macOS app path; set
BLENDER_PATH to another Blender executable before starting it if needed.
Hosted exports run entirely in the browser and preserve imported Blender
attachments. Standalone Blender and Three.js JSON download buttons are not shown.
Generated *.three.json files are Git-ignored and stay local.
Links can select a model with ?model=MODEL_ID; legacy ?area=MODEL_ID also
selects it. Add &view=reconstruction to select Facade on initial load.
Refreshed on 12 September 2026 from a public-only preview at 1600 × 1000, using a fresh browser session with only the public Rathaus 100 m sample. Rotation was paused for consistent captures. Private addresses and imported private data are excluded. Click an image to inspect it at full size; facade details are illustrative.
| Reconstructed neighbourhood | Building facade |
|---|---|
![]() |
![]() |
| Reconstruction wireframe | Source depth |
|---|---|
![]() |
![]() |
Choose Add address, enter a Bavarian address and a neighbor distance, then wait for extraction. Repeating the same address and distance reuses the cache. Successful generation selects the model and resumes rotation, including when the previous view was paused. Generated models persist after reload and can be removed with Delete generated model. Permanent bundles cannot be deleted through this action.
Locally, the Node server stores generated bundles in website/.runtime/models/.
In the Cloudflare build, a browser Web Worker runs the same extraction/export
pipeline and stores results in that browser's IndexedDB. Browser-generated
models are not uploaded or added to a public catalog. Address searches use the
configured Esri geocoder.
Use Export ZIP in the address column to download the selected area's original
GLB, source mesh and metadata, plus its complete façade and surface snapshot when
available. The archive also includes building-facade.glb, the standalone
main-building model with baked façade geometry. model.glb remains the unchanged
LoD2 neighbourhood. area.json retains the editable reconstruction recipe and
neighbourhood details; it is not needed to display building-facade.glb. Neighbours and their authored façade profiles are included even when
Building mode is selected. Filenames include the address, area size and local
export timestamp, for example Neues-Rathaus-100m-2026-09-05_22-30-00.zip.
The archive preserves reconstruction reference notes;
external reference photos themselves are not bundled.
On localhost with Blender available, Export ZIP also includes
complete-facade.blend, containing the full neighbourhood and modeled façade
and surface details. Import preserves this file unchanged, including on the hosted
site, and subsequent ZIP exports retain it. The hosted site cannot create
new native Blender files; generate the ZIP locally to include one.
Send the ZIP to your friend. They can open Munich3D and choose the global upload icon (Import ZIP) beside Add address. It adds the imported location to the address chooser and selects it, regardless of which address was previously selected. Import also works on localhost. The address is saved only in that browser's IndexedDB, survives reload and can be re-exported or removed with Delete imported model. No address data is uploaded or added to the public catalog. Clearing browser storage removes imports.
Archives use the versioned munich3d-address format with SHA-256 file checksums.
Import validates the source bundle before saving; limits are 100 MB compressed
and 256 MB unpacked. ZIP processing runs in a worker. Import accepts both older
archives without a baked GLB and new archives with building-facade.glb. Imported
baked geometry is stored in IndexedDB and preserved byte-for-byte on re-export;
older archives can generate the building GLB from their retained façade recipe.
extract_buildings.mjs CLI and shared geocoding/source-selection pipeline
import_bayernatlas.mjs Shared statewide 3D Tiles importer
export_model_to_glb.mjs CLI and shared source-mesh-to-GLB exporter
tests/ Pipeline regression tests
website/
server.mjs Loopback server and generation API
src/App.tsx Unified explorer and catalog discovery
components/house-viewer.tsx Model loading, camera and interaction
lib/area-reconstruction.ts Illustrative geometry and surface rendering
lib/browser-models.ts Browser generation and IndexedDB cache
lib/address-sharing.ts ZIP downloads, imports and worker orchestration
lib/address-archive.mjs Shared versioned ZIP codec and validation
lib/building-facade-export.ts Main-building and complete-scene GLB/Three.js export
scripts/ Catalog, surface preparation and validation tools
addresses/<address>/
model/ <model>.glb, .metadata.json, .source-mesh.json
area/ Bounded OSM snapshots and optional facade profiles
catalog/ Generated <model>.json chooser fragments
reconstruction/ Baked main-building GLB and input checksums
.runtime/ Ignored local jobs and generated models
.work/ Ignored extraction work and local reference material
Each address has one lowercase folder, containing all its distance variants.
Keep original bundle names and metadata intact. Catalog generation discovers
complete bundles and matching area snapshots by modelId; no separate model list
is maintained. Missing bundle members and duplicate model IDs fail generation.
Only website/addresses/muenchner-rathaus/, containing the 100 m sample, is
allowlisted for Git. Other address folders remain ignored together with their
catalogs, surfaces and profiles. Git ignore is not a build filter: a build
includes permanent assets present in the local checkout, including private ones.
Use a checkout containing only approved assets when publishing.
Do not remove private folders or reference material during cleanup. The three
root .mjs files are application source. Build output and browser-test artifacts
are reproducible; runtime models, local state and investigations may not be.
Run from the repository root:
node extract_buildings.mjs \
--address "Münchner Rathaus, Marienplatz 8, 80331 München, Germany" \
--neighbor-distance 100 \
--output .work/muenchner-rathaus-100m| Option | Meaning |
|---|---|
--address <text> |
Address to geocode; required unless coordinates are supplied. |
--coordinates <lat,lon> |
Explicit coordinates instead of geocoding. |
--neighbor-distance <metres> |
Neighbor cutoff; default 35 m. |
--target-search-distance <m> |
Nearest-building fallback distance; default 25 m. |
--output <directory> |
Destination; otherwise .work/<address-slug>/. |
--item-id <id> |
Alternate ArcGIS scene item. |
--help |
Print command help. |
Extraction writes source-mesh JSON, metadata JSON, an OBJ and a reproduction
README. Export directly from the two JSON files, using the emitted filename
stem in place of SOURCE_STEM:
node export_model_to_glb.mjs \
.work/muenchner-rathaus-100m/SOURCE_STEM.source-mesh.json \
.work/muenchner-rathaus-100m/SOURCE_STEM.metadata.json \
.work/muenchner-rathaus-100m/muenchner-rathaus-100m.glbTo install a new permanent bundle:
- Inspect the selected primary building and recorded distance rule in metadata.
- Place the GLB and both JSON files in
website/addresses/<address>/model/, giving all three the same stable model stem. Retain source contents unchanged. - Keep OBJ and the extraction README out of the permanent bundle.
- Run
npm run build --prefix website; its catalog step discovers the bundle. - Verify selection, facts, source link and all three downloads in the browser.
The default Munich source is ArcGIS item afce63c0ee9a4a33b2c4ebd29a8e71ef.
The extractor discovers its SceneServer and decodes I3S meshes. Outside that
item's extent, the shared pipeline uses BayernAtlas LoD2 3D Tiles. An explicit
--item-id keeps the requested I3S source. The BayernAtlas feed is a preview
service; unsupported formats fail explicitly.
Primary selection uses point intersection, falling back to the nearest building
within the target-search distance when needed; metadata records that fallback.
The primary is always retained. For new neighbors, the farthest decoded
horizontal vertex must be within the requested distance of primary geometry.
This is recorded as complete_geometry_within_primary_distance. Legacy bundles
may record closest_geometry_from_primary; their original rule remains authoritative.
Each GLB contains one node per source feature with its role, GML ID, OBJECTID and distance extras. Vertices, triangle topology and roof forms remain source-matched. Metadata supplies dimensions and attributes. BayernAtlas elevations are WGS84 ellipsoidal heights, labeled separately; absent OBJECTID/storeys remain unavailable.
Area snapshots associate optional reconstruction with a model. Windows, doors, balconies, roof fixtures and paving are geometry, not projected photographs. Openings and details modify a separate copy; LoD2 comparison restores the source. Gothic detailing applies only to Rathaus. Other buildings use generic details or local photo profiles with references and confidence notes. Estimated dimensions and obscured elevations are not survey measurements.
Catalog generation automatically bakes a main-building GLB for every permanent
bundle with an area snapshot. The output lives separately in
addresses/<address>/reconstruction/<model>.building-facade.glb, with a checksum
record beside it. Source GLB, area and exporter checksums trigger regeneration
when inputs change. Private outputs stay inside their ignored address folders.
To refresh all available addresses explicitly, run:
node website/scripts/export-building-facades.mjs --all
npm run models:catalog --prefix websiteThe exporter removes source-wall triangles hidden by reconstructed openings and
bakes instanced details into ordinary triangles for standard GLB importers.
Only the main feature and connectedFacades are included.
Profiles use exact GML IDs: primaryFacade for the main feature,
connectedFacades for parts retained in Building mode, and neighborFacades
for independent neighbors. Details can include inset glazing, surrounds, shutters,
plank doors, gutters, flues, tiled roofs, solar panels and rooflights.
Bounded OSM snapshots supply mapped roads, plazas, paths, vegetation and transport locations. Reconstruction adds paving joints, kerbs, drains, markings, street furniture and surface rails/sleepers/ballast. Underground, removed and proposed tracks are excluded. Ground interpolation, inferred widths and furniture shapes remain approximations. Bench direction follows mapped bearings where available; fallback directions toward nearby paths or platform tracks are inferred.
To refresh Rathaus surfaces from the repository root:
mkdir -p .work/rathaus-surfaces
curl --fail --max-time 55 --data-urlencode data@website/scripts/rathaus-surfaces.overpass \
https://overpass-api.de/api/interpreter -o .work/rathaus-surfaces/osm.json
curl --fail --max-time 55 --data-urlencode data@website/scripts/rathaus-surface-details.overpass \
https://overpass-api.de/api/interpreter -o .work/rathaus-surfaces/osm-details.json
node website/scripts/build-rathaus-surfaces.mjs \
.work/rathaus-surfaces/osm.json .work/rathaus-surfaces/osm-details.jsonFor another bundle, supply ways and relations queried around that model's
bounds, using its model ID in place of MODEL_ID:
node website/scripts/fetch-area-transport.mjs MODEL_ID .work/transport.json
node website/scripts/build-area-surfaces.mjs MODEL_ID WAYS.json RELATIONS.json .work/transport.json
npm run models:catalog --prefix websiteThe transport argument is optional; OVERPASS_URL overrides the fetch endpoint.
The builder uses the matching source transform and preserves profiles whose
source IDs still match. Fetching is a preparation step, not required on page load.
| Route | Purpose |
|---|---|
GET /api/health |
Server/pipeline health. |
GET /api/models |
List valid cached runtime models. |
POST /api/models |
Generate/reuse { address, neighborDistance, coordinates?: [lat, lon] }. |
DELETE /api/models/<local-id> |
Delete a completed runtime model only. |
GET /api/model-files/<local-id>/<file> |
Download GLB, source-mesh or metadata. |
Requests accept no output path. Addresses are 3–300 characters without C0 controls; neighbor distances are 0–250 m. Coordinates are validated and the local API limits request bodies to 16 KiB. Browser generation shares address and distance validation. Local extraction uses argument arrays without a shell.
The Cloudflare configuration serves only glaubi.net/munich3d and
glaubi.net/munich3d/*, with workers_dev: false. Its asset base is /munich3d/.
Hosted generation needs no server compute, Blender, child process or OBJ export.
Deploy Munich3D runs on every push to master,
with a manual Run workflow option in GitHub Actions. It installs locked
packages, runs pipeline/reconstruction tests, checks that only the complete
Rathaus 100 m bundle is present, builds the Cloudflare site and deploys it.
A final check compares live HTML, entry assets and sample metadata with the build.
Failed tests or unexpected address assets block deployment. Runs are serialized.
The workflow uses the CLOUDFLARE_ACCOUNT_ID repository variable and the
CLOUDFLARE_API_TOKEN Actions secret. The token needs Workers Scripts Edit,
Workers Routes Edit for glaubi.net, and the account/zone read access used by
Wrangler; scope it to the deployment account and zone. Keep the token out of Git.
See Cloudflare's GitHub Actions guide.
The CI checkout contains only committed assets. Local npm run deploy:cloudflare
also checks the public address boundary and fails when private folders are
present; deploy from a clean public checkout instead of deleting private data.
Pushing to master publishes automatically once the Actions secret is configured.
Run checks appropriate to the changed code from the repository root:
node --check extract_buildings.mjs
node --check import_bayernatlas.mjs
node --check export_model_to_glb.mjs
node --test tests/bayernatlas.test.mjs
node --test website/scripts/address-bundles.test.mjs
node --test website/scripts/address-archive.test.mjs
node website/scripts/test-area-reconstruction.mjs
node website/scripts/test-area-camera.mjs
node website/scripts/test-building-facade-export.mjs
node website/scripts/test-complete-scene-export.mjs
npm run build --prefix websiteThe area tests default to Rathaus 100 m and accept an optional model ID. They check reconstruction/source separation and camera fitting at multiple aspect ratios. Selection changes also require a second address and matching metadata, source-mesh and GLB counts. Strict-distance neighbors must satisfy the recorded maximum-distance cutoff.
Verify visible changes in a real browser: location switching, fitting, connected building visibility, Facade camera stability, depth/wireframe, compass and a clean console. Runtime changes additionally require cached/uncached generation, downloads, deletion and reload persistence. See AGENTS.md for the change-specific validation matrix.
Use a fresh, isolated browser session on the deployed site or a preview built from a public-only checkout. Do not use the normal local development catalog: Git-ignored private addresses may still appear there. Verify that the chooser contains only Neues Rathaus, and use only its ZIP for the import example. Do not clear the user's browser storage to prepare documentation.
Set the viewport to 1600 × 1000, wait for the model to finish loading, pause
rotation, and use Reset view before choosing the required scope and display mode. Wait for
button transitions and the scene to settle before each capture. Refresh the six
explorer/form images above and docs/address-import.jpg; retain the current
controls, source attribution and copyright. Review every capture for private
content and correct active modes before copying it into docs/. Keep ZIPs and
intermediate browser output under ignored website/output/playwright/.
Project website and original project content: © 2026 Robert Zaufall.
Building data: Bayerische Vermessungsverwaltung – www.geodaten.bayern.de, CC BY 4.0. Area surface data: © OpenStreetMap contributors, ODbL 1.0. Reconstruction details remain illustrative and separate from these source datasets.






