3D Tiles with Three.js
Load OGC 3D Tiles into a MapLibre map using Three.js and 3d-tiles-renderer. This example uses the @mapLoaded callback to add a custom Three.js rendering layer.
gts
import Component from '@glimmer/component';
import * as THREE from 'three';
import { TilesRenderer } from '3d-tiles-renderer';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import MapLibreGL from 'ember-maplibre-gl/components/maplibre-gl';
import maplibregl from 'maplibre-gl';
const mapOptions = {
style: 'https://tiles.openfreemap.org/styles/bright',
zoom: 1,
center: [0, 0],
pitch: 60,
maxPitch: 80,
canvasContextAttributes: { antialias: true },
};
function ecefToLngLatAlt(x, y, z) {
const a = 6378137.0;
const e2 = 6.69437999014e-3;
const b = a * Math.sqrt(1 - e2);
const ep2 = (a * a - b * b) / (b * b);
const p = Math.sqrt(x * x + y * y);
const th = Math.atan2(a * z, b * p);
const lon = Math.atan2(y, x);
const lat = Math.atan2(
z + ep2 * b * Math.pow(Math.sin(th), 3),
p - e2 * a * Math.pow(Math.cos(th), 3),
);
const n = a / Math.sqrt(1 - e2 * Math.sin(lat) * Math.sin(lat));
const alt = p / Math.cos(lat) - n;
return { lng: (lon * 180) / Math.PI, lat: (lat * 180) / Math.PI, alt };
}
export default class ThreeJSTilesDemo extends Component {
onMapLoaded = (map) => {
let scene, camera, renderer, tiles, tilesCamera, localTransform;
function getModelTransform(coord) {
const mc = maplibregl.MercatorCoordinate.fromLngLat(
[coord[0], coord[1]], coord[2],
);
return {
translateX: mc.x, translateY: mc.y, translateZ: mc.z,
rotateX: Math.PI / 2, rotateY: 0, rotateZ: 0,
scale: mc.meterInMercatorCoordinateUnits(),
};
}
function updateLocalTransform(origin = [0, 0, 0]) {
const t = getModelTransform(origin);
const rx = new THREE.Matrix4().makeRotationX(t.rotateX);
const ry = new THREE.Matrix4().makeRotationY(t.rotateY);
const rz = new THREE.Matrix4().makeRotationZ(t.rotateZ);
localTransform = new THREE.Matrix4()
.makeTranslation(t.translateX, t.translateY, t.translateZ)
.scale(new THREE.Vector3(t.scale, -t.scale, t.scale))
.multiply(rx).multiply(ry).multiply(rz);
}
const customLayer = {
id: '3d-tiles',
type: 'custom',
renderingMode: '3d',
onAdd(mapArg, gl) {
camera = new THREE.PerspectiveCamera();
scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 3));
renderer = new THREE.WebGLRenderer({
canvas: mapArg.getCanvas(), context: gl, antialias: true,
});
renderer.autoClear = false;
tilesCamera = new THREE.PerspectiveCamera();
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://unpkg.com/three@0.183.0/examples/jsm/libs/draco/');
gltfLoader.setDRACOLoader(dracoLoader);
tiles = new TilesRenderer(
'https://pelican-public.s3.amazonaws.com/3dtiles/agi-hq/tileset.json',
);
tiles.group.name = 'tiles';
scene.add(tiles.group);
tiles.setCamera(tilesCamera);
tiles.setResolutionFromRenderer(tilesCamera, renderer);
tiles.manager.addHandler(/\.(gltf|glb)$/g, gltfLoader);
let handled = false;
tiles.addEventListener('load-tileset', () => {
if (handled) return;
handled = true;
const sphere = new THREE.Sphere();
tiles.getBoundingSphere(sphere);
const c = sphere.center.clone();
const { lng, lat, alt } = ecefToLngLatAlt(c.x, c.y, c.z);
map.jumpTo({ center: [lng, lat], zoom: 18, pitch: 60 });
updateLocalTransform([lng, lat, alt - 300]);
const m = tiles.root.transform || [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];
const rot = new THREE.Matrix3().set(m[0],m[1],m[2],m[8],m[9],m[10],-m[4],-m[5],-m[6]);
const move = new THREE.Matrix4().makeTranslation(-c.x, -c.y, -c.z);
tiles.group.matrix.copy(
new THREE.Matrix4().setFromMatrix3(rot).multiply(move),
);
tiles.group.matrixAutoUpdate = false;
tiles.group.updateMatrixWorld(true);
});
updateLocalTransform();
},
render(_gl, args) {
if (!camera || !renderer || !scene || !localTransform) return;
camera.projectionMatrix.fromArray(args.defaultProjectionData.mainMatrix);
camera.projectionMatrix.multiply(localTransform);
const P = new THREE.Matrix4().fromArray(args.projectionMatrix);
const V = new THREE.Matrix4().multiplyMatrices(P.clone().invert(), camera.projectionMatrix);
tilesCamera.projectionMatrix.copy(P);
tilesCamera.matrixWorldInverse.copy(V);
tilesCamera.matrixWorld.copy(V).invert();
renderer.resetState();
renderer.render(scene, camera);
if (tiles) tiles.update();
map.triggerRepaint();
},
};
map.addLayer(customLayer);
};
<template>
<MapLibreGL
@initOptions={{mapOptions}}
@mapLoaded={{this.onMapLoaded}}
style="height: 500px; width: 100%; border-radius: 8px;"
/>
</template>
}Based on the MapLibre GL JS Add 3D tiles using Three.js example.