MapLibre GL JS · OpenFreeMap vector tiles · Mapterhorn terrain · 3D Tiles (BAG buildings Sibbe)
Full page: open index.html ↗
| Feature | Status | Notes |
|---|---|---|
| OpenFreeMap vector tiles | ✓ Done | Liberty style via tiles.openfreemap.org as base map |
| Mapterhorn terrain | ✓ Done | Raster-DEM + hillshade via tiles.mapterhorn.com, exaggeration 1× |
| 3D Tiles buildings Sibbe | ✓ Done | Three.js custom layer, TilesRenderer with ECEF→Mercator transform. ImplicitTilingPlugin registered for 3D Tiles 1.1 implicit tiling (QUADTREE, 3 levels) |
| Click buildings for attributes | ✓ Done | Raycast via THREE.Raycaster + tilesCamera; attributes via GLTFExtensionsPlugin (EXT_structural_metadata / EXT_mesh_features); selected building highlighted in orange via feature-ID shader overlay |
| GitHub Pages compatible | ✓ Done | No server or build tool required; relative paths; all dependencies via CDN |
Clicking a building fires a THREE.Raycaster against tiles.group
using tilesCamera (which has properly separated projection and view matrices,
kept in sync with MapLibre every frame). The hit returns a faceIndex and
world-space point, from which barycentric coordinates are computed.
meshFeatures.getFeatures(faceIndex, barycoord) from
EXT_mesh_features yields the feature ID, which is used to look up
all BAG attributes via structuralMetadata.getPropertyTableData(tableIndex, featureId)
from EXT_structural_metadata.
The selected building is highlighted in orange using a THREE.ShaderMaterial
overlay mesh that reads the _feature_id_0 vertex attribute and
discards any fragment whose feature ID does not match the selected one.
This means only the geometry of the selected building is rendered in the overlay,
even though the whole tile mesh is reused as a geometry source.
Polygon offset (factor -2, units -2) prevents z-fighting with the original mesh.
| Component | Version | CDN |
|---|---|---|
| MapLibre GL JS | 5.21.1 | unpkg.com/maplibre-gl@5.21.1 |
| Three.js | 0.183.0 | cdn.jsdelivr.net/npm/three@0.183.0 |
| 3d-tiles-renderer | 0.4.23 | cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.23 |
| Draco decoder | via Three.js | unpkg.com/three@0.183.0/…/draco/ |
| KTX2 / Basis loader | via Three.js | unpkg.com/three@0.183.0/…/basis/ |
| OpenFreeMap style | liberty | tiles.openfreemap.org/styles/liberty |
| Mapterhorn terrain DEM | — | tiles.mapterhorn.com/tilejson.json |
| Property | Value |
|---|---|
| Generator | pg2b3dm 2.27.0.0 |
| 3D Tiles version | 1.1 |
| Tiling scheme | Implicit QUADTREE, 3 levels, subtrees at level 0 |
| Content files | 1_0_0.glb, 1_0_1.glb, 1_1_1.glb |
| Total features | 676 + 244 + 86 = 1 006 buildings |
| ECEF translation | [4 014 073.5, 410 267.5, 4 923 026.0] m |
| Location | Sibbe, municipality of Valkenburg, Limburg (NL) |
| Metadata extensions | EXT_mesh_features, EXT_structural_metadata |
| Available attributes | identificatie (BAG building ID) |
The page uses <script type="importmap"> to load ES modules
directly from CDN. No npm, webpack, or bundler is needed.
The result is a single self-contained HTML file that can be deployed to GitHub Pages
by simply committing it.
OpenFreeMap provides the full vector tile style (buildings, roads, labels).
After the style loads (style.load), the Mapterhorn raster-DEM source
is added dynamically along with a hillshade layer and map.setTerrain().
This combines the rich OpenFreeMap style with 3D terrain exaggeration.
Instead of manually wiring up GLTFLoader, DRACOLoader, and
KTX2Loader, a single GLTFExtensionsPlugin registration handles
all three. It also automatically registers GLTFStructuralMetadataExtension
and GLTFMeshFeaturesExtension, which parse EXT_structural_metadata
and EXT_mesh_features and attach meshFeatures /
structuralMetadata to each mesh's userData.
The Sibbe tileset uses implicit tiling with template URIs
(content/{level}_{x}_{y}.glb). Without registering the
ImplicitTilingPlugin, the 3d-tiles-renderer would request the
literal template string as a URL. The plugin resolves tile coordinates into
actual file paths before fetching.
Three.js shares the WebGL context with MapLibre via renderingMode: "3d".
The MapLibre projection and view matrices are synchronised to the Three.js camera
every frame. TilesRenderer automatically manages tile loading and
unloading based on the current viewpoint.