🏠 Sibbe 3D Buildings — Review

MapLibre GL JS · OpenFreeMap vector tiles · Mapterhorn terrain · 3D Tiles (BAG buildings Sibbe)

Live preview

Full page: open index.html ↗

Features

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 buildings – implementation

How it works

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.

Building selection highlight

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.

Technical stack

ComponentVersionCDN
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

3D Tiles dataset (Sibbe)

PropertyValue
Generatorpg2b3dm 2.27.0.0
3D Tiles version1.1
Tiling schemeImplicit QUADTREE, 3 levels, subtrees at level 0
Content files1_0_0.glb, 1_0_1.glb, 1_1_1.glb
Total features676 + 244 + 86 = 1 006 buildings
ECEF translation[4 014 073.5, 410 267.5, 4 923 026.0] m
LocationSibbe, municipality of Valkenburg, Limburg (NL)
Metadata extensionsEXT_mesh_features, EXT_structural_metadata
Available attributesidentificatie (BAG building ID)

Architecture notes

No frameworks, no build tool

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.

Terrain + vector tiles combination

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.

GLTFExtensionsPlugin

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.

Implicit tiling (3D Tiles 1.1)

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.

Shared WebGL context

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.