Skip to content
robertZaufallPublic

About

Munich3D Viewer

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Munich3D

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.

Homepage showing the original Rathaus 100 m model and current explorer controls

Run locally

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 dev

Open 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.

Explorer controls

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.

Screenshots

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
Rathaus and its 100 m neighbourhood with modeled facades and paving Rathaus facade with neighboring buildings hidden
Reconstruction wireframe Source depth
Wireframe showing the reconstructed Rathaus detail geometry Camera-relative grayscale depth of the original Rathaus geometry

Generate an address

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.

Add address form using the public Rathaus address and a 100 m neighbor distance

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.

Share an address as a ZIP

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.

Successful import of the public Rathaus ZIP, with the global upload icon and address-level Export ZIP control

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.

Files and privacy

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.

Extract a permanent bundle

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.glb

To install a new permanent bundle:

  1. Inspect the selected primary building and recorded distance rule in metadata.
  2. Place the GLB and both JSON files in website/addresses/<address>/model/, giving all three the same stable model stem. Retain source contents unchanged.
  3. Keep OBJ and the extraction README out of the permanent bundle.
  4. Run npm run build --prefix website; its catalog step discovers the bundle.
  5. Verify selection, facts, source link and all three downloads in the browser.

Source and selection rules

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.

Reconstruction and surface preparation

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 website

The 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.json

For 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 website

The 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.

Local API and hosted build

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.

Automatic deployment

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.

Validation

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 website

The 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.

Refresh documentation screenshots

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/.

Attribution

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.

About

Munich3D Viewer

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages