map2threejs is a desktop-first React application and reusable TypeScript pipeline for turning a small real-world circular or manually drawn region into a meter-scaled Three.js scene.
- Google Maps JavaScript API supplies the satellite selection screen, while Places API (New) supplies autocomplete and text search.
- Custom areas use an editable polygon implemented without Google's retired Drawing Library.
- Circle and polygon selections share the same 1 km fetch envelope; terrain and all vector layers are clipped to the selected boundary in the geometry worker.
- MapTiler supplies
satellite-v2,terrain-rgb-v2,buildings, and Planetv4vector data for the generated scene. satellite-v2is mosaicked at its real 512×512 source-tile resolution even though its TileJSON currently omitstileSize; source pixels are not silently downsampled to the generic 256 px fallback.- The renderer enables trilinear mipmaps and device-capability-aware anisotropic filtering, capped at 16×, so the recovered satellite detail remains legible in oblique perspective views.
- Perspective framing uses the generated scene bounds as well as the selected radius, pulling back and rising above tall buildings in dense 50 m regions without changing the established 250 m and 1 km composition.
- Directional shadows remain enabled throughout the supported radius range: 2048² maps preserve small-region detail and a bounded 1024² whole-region map retains building/terrain depth at 1 km.
- Planet v4 roads, pathways, railways, aviation surfaces/lines, aerialways, exact bridge decks, waterways, dams, walls/fences/barriers, standalone trees, farmland, ice, military land, vegetation, supported land-use polygons, and street-furniture points are decoded into terrain-following Three.js geometry.
- Physical Planet POIs are selectively decoded when they identify a real scene object—transit stops, subway entrances, bicycle facilities, postal fixtures, shelters, public art, memorials, monuments, and information boards—while shops, restaurants, offices, and other label-only POIs remain non-geometry.
- Transport generation preserves paved, service, ramp, access, construction, one-way, bridge/tunnel, layer, and railway-class metadata; rail alignments receive track geometry, while schematic center lines and provider-directed one-way arrows appear only in geometry-only mode.
- Provider stairway alignments receive a dedicated merged Three.js detail mesh with terrain-sampled 3D nosings and shadows. Spacing is explicitly reported as representative because Planet v4 does not provide surveyed step count or rise.
- Line-based road and path ribbons sample Terrain RGB independently at both edges. Implausible crossfall from the coarse DEM is capped by transport class, with merged edge skirts closing any lifted terrain gap instead of twisting or burying the road surface.
- Linear waterways retain their downstream terrain grade but enforce a near-level cross-section; merged bank skirts close gaps created where the coarse DEM would otherwise twist or bury the water ribbon.
- Provider road/path bends receive terrain-sampled round joins, closing the wedge gaps created by disconnected segment quads without inventing lane geometry.
- Linear waterways sample both banks and receive the same terrain-aware bend closure; 3D wall, fence, hedge, and kerb segments receive terrain-anchored corner closures.
- Polygonal land, pedestrian, parking, bridge-deck, and river surfaces are adaptively subdivided to follow Terrain RGB elevation while holes remain empty; lake-like water polygons use one representative level.
- Terrain RGB is sampled on its continuous global pixel-center grid, including bilinear interpolation across fetched XYZ tile boundaries, so terrain-following geometry does not inherit per-tile edge plateaus.
- Every roof family declared by the MapTiler Buildings schema is generated deterministically; only unknown provider values use the visible flat fallback.
- Concave and courtyard footprints retain their topology through shared-edge subdivision and a continuous distance-field roof approximation instead of being silently flattened.
- Exact provider façade and roof colors take priority; missing values use a reported building-class palette, then a neutral fallback when classification is unavailable. Provider
building_labelclass anchors enrich otherwise unclassified containing footprints, and parent metadata is inherited by detailed building parts. - When a building still lacks classification, the worker spatially joins its footprint to the smallest containing provider land-use polygon (residential, commercial, industrial, medical, education, military, agricultural, leisure, transport or construction). This context is reported separately and supplies a semantic palette without pretending that inferred colors are surveyed façade data.
- Building entrance points from the Buildings tileset become façade-aligned door markers using the nearest real footprint edge.
- Building roofs remain level while visible foundation walls independently meet sampled terrain around sloped footprints; elevated building parts retain their provider
height_minplane. - Exact bridge polygons use a representative level deck with a merged 3D edge fascia and underside instead of being draped into the riverbed or terrain beneath them.
- Line-only bridge features use one endpoint-derived deck grade with a flat cross-section, merged edge parapets, and terrain-to-deck support piers at representative spacing rather than copying the terrain beneath every short span segment.
- Tunnel-tagged roads, paths, steps, and railways become subsurface endpoint-graded envelopes rather than being incorrectly draped over terrain; an opt-in Underground x-ray layer exposes them for inspection.
- Crossings become terrain-aligned paint and supported street objects—including gates, turnstiles, cycle barriers, signals, benches, lamps, fountains and utilities—receive class-specific primitives; unsupported semantic points are omitted and reported instead of becoming invented generic poles.
- API keys stay in memory and are never written to storage, logs, recipes, or build artifacts.
- Versioned recipes are deterministic and credential-free. They can be reopened to restore the region, polygon, quality, vertical scale, camera pose and layer visibility.
- Editable polygon handles, undo, duplicate detection, self-intersection validation and the 256-point limit keep manual boundaries buildable.
- Imagery, terrain, building and vector requests run concurrently behind a six-request network scheduler; retry delays honor
Retry-After, and decoding/build cancellation terminates the active worker. - Build quality presets and the preflight estimator expose tile requests, imagery dimensions, GPU texture memory and a conservative geometry-export range before work starts.
- Review includes feature inspection, metric distance/elevation and polygon-area measurement, camera presets, WebGL context recovery and complete layer controls.
- User-owned GeoJSON can be projected into the current metric scene and included in exports. A bounded AI-enrichment manifest can add explicitly approved, confidence-scored semantic metadata derived from user-owned imagery; it never changes coordinates or precise heights.
- Scene exports contain terrain, buildings, roads, water, land, barriers, infrastructure, street details and the region boundary. Provider satellite pixels are intentionally excluded from every exported file.
- GLB and project generation run in a dedicated export worker. Direct GLB export can optionally use Meshopt compression; standard GLB remains the compatibility default.
- Satellite and Geometry review modes provide a direct comparison between provider ground color and the independent semantic Three.js meshes.
npm install
npm run devOpen http://localhost:4173, enter browser-restricted Google Maps and MapTiler keys, test both
connections, then search, select and build a region. The Google key needs Maps JavaScript API and
Places API (New) access in the same Google Cloud project. The
MapTiler key needs access to all four source tilesets.
For the deterministic provider-free UI fixture used by browser tests, open
http://localhost:4173/?demo=1. Demo mode is clearly identified and never used by a saved recipe.
The Review screen offers four downloads:
- Download Three.js project (.zip) writes an immediately customizable Vite + TypeScript project containing
map.glb, a directly openable offlineindex.html, an editable production entry,src/main.ts, cameras, lights, OrbitControls, layer controls, feature/source-report sidecars, metadata, attribution and the credential-free recipe.npm run buildcompiles the editable source intodist/index.html. - Download GLB writes the meter-scaled geometry as a portable glTF 2.0 binary asset for Three.js, Blender and compatible game engines. Optional Meshopt compression is smaller but requires a loader configured for
EXT_meshopt_compression. - Download native Three.js JSON keeps the Three.js-specific
ObjectLoaderdocument as an advanced export. - Save scene recipe writes only the credential-free source settings so the scene can be rebuilt from live providers; it is not a 3D asset.
All asset exports use generated materials and vertex colors. They contain no API keys, provider URLs, cached tiles or MapTiler satellite pixels.
npm run check
npm run test:e2enpm run check runs lint, TypeScript checks, unit/integration coverage and production builds. Browser tests run the workflow in Chromium, Firefox and WebKit, and also build and open a customized downloaded project.
GitHub Actions runs the WebGL workflow in Chromium and WebKit because headless Firefox on GitHub's
Linux runner does not expose WebGL2; the full Firefox suite remains part of the local desktop matrix.
The public production image is published to GitHub Container Registry:
ghcr.io/ccycv/map2threejs:latest
The image contains the compiled static application behind Nginx. It listens on port 80, exposes
GET /healthz for startup, readiness and liveness checks, and includes SPA fallback routing.
Provider credentials are not baked into the image: each user enters browser-restricted Google Maps
and MapTiler keys in their own browser session.
For bunny.net Magic Containers, select GitHub Container Registry, use image
ccycv/map2threejs with tag latest, and add a CDN endpoint for container port 80. Configure
HTTP health checks against /healthz on port 80. The GitHub workflow builds only
linux/amd64, which is the architecture currently required by Magic Containers.
Every push to main publishes both latest and sha-<commit>. Git tags beginning with v
also publish a matching version tag.
The scene uses a local azimuthal-equidistant projection centered on the recipe point. One Three.js
unit is one meter: +X is east, +Y is elevation, and -Z is north. Center terrain elevation is
normalized to Y = 0.
Satellite pixels are applied to terrain and ground-level vector surfaces, preserving real markings and surface color while keeping the layers as separate geometry. Building roofs use exact provider colors when available and a clearly reported semantic or neutral fallback otherwise, because tall objects are displaced by aerial-image parallax; projecting ground pixels onto elevated roofs creates a false, visibly offset texture. The Review screen states this explicitly: vector footprints remain at their true ground coordinates even when a satellite roof appears offset from them. Street objects use representative dimensions because the provider supplies their type and position, not surveyed 3D models. Entrance orientation is derived from the nearest provider footprint edge; aerialway clearance and tower spacing remain visibly reported approximations.
Line-based transportation and waterway features are subdivided into short terrain-following sections, sample elevation at both ribbon edges, and close real provider bends with round joins. Provider polygon outlines are retained where available; otherwise class, service, ramp, and paved status drive visibly reported width rules because Planet v4 does not include surveyed carriageway or lane geometry.
Barrier alignments are assembled as 3D wall, fence, hedge, kerb, or protection meshes. Interior provider vertices receive small terrain-anchored corner closures so adjacent segment boxes remain visually continuous.
Terrain-level polygons are subdivided at a spacing derived from the reported DEM resolution instead of spanning hills with coarse boundary-only triangles. River polygons follow terrain elevation; lake-like water uses a single median boundary elevation so it does not become a visibly sloped sheet.
Normal building foundations sample the terrain around their footprint and extend each wall edge down to the local surface while retaining one median reference elevation for the roof. This prevents large buildings from floating downhill or being clipped uphill without turning their floors and roofs into sloped terrain copies.
Exact bridge polygons remain level at a representative boundary elevation. When only a road centerline bridge is available, all subdivided span segments share one endpoint-derived grade and stay flat across their width, preventing the deck from following riverbanks or depressions in the DEM.
Tunnel-tagged transport is excluded from the visible surface-road mesh and generated below the terrain with class-aware width and an endpoint-derived grade. The provider does not supply surveyed tunnel cross-sections, so clearance is representative and reported. The Underground x-ray toggle renders those otherwise occluded envelopes through the terrain for coverage and alignment review.
The result is provider-faithful visualization, not survey geometry or Google Earth photogrammetry. Terrain and building heights are coverage-dependent and approximate. Provider tiles are decoded in memory for live display only. They are not placed in the recipe, exported, or persistently cached by the application.