So the below code works to show a Cesium 3d tiles model. This code works. It creates a basic scene, loads the cesium model and adds it to the scene.
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { Loader3DTiles } from 'three-loader-3dtiles';
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
Clock
} from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import maplibregl, { loadMapTilerMaps, modelOrigin, modelTransform } from 'maplibre-gl';
import { BingMapsProvider, MapView, UnitsUtils } from 'geo-three'
import { Vector3 } from 'three';
let rendered = false;
export default function Demo(props) {
const ref = useRef();
useEffect(() => {
const f = async () => {
if (rendered) {
return;
}
console.log('LOAD')
rendered = true;
await initTestOGCTiles(ref)
}
f();
})
return <div id="map" style={{ height: window.innerHeight }} ref={ref}></div>
}
// TEST 2 - only shows (cesium) tiles for proof of concept
async function initTestOGCTiles(ref) {
const scene = new Scene()
const camera = new PerspectiveCamera()
const renderer = new THREE.WebGLRenderer({
antialias: true,
// autoClear: true
});
const clock = new Clock()
renderer.setSize(window.innerWidth, window.innerHeight);
ref.current.appendChild(renderer.domElement);
let tilesRuntime = await loadOGCTiles(scene, renderer);
function render() {
const dt = clock.getDelta()
if (tilesRuntime) {
tilesRuntime.update(dt, renderer, camera)
}
renderer.render(scene, camera)
window.requestAnimationFrame(render)
}
//loadTileset()
render()
}
async function loadOGCTiles(scene, renderer) {
console.log("RENDERER", renderer)
const result = await Loader3DTiles.load({
url: 'my 3d tiles url',
renderer: renderer,//renderer,
options: {
dracoDecoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/draco',
basisTranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/basis',
}
})
const { model, runtime } = result
const b3dmTiles = model;
b3dmTiles.name = "b3dmTiles"
runtime.setDebug(true)
// b3dmTiles.position.add(new THREE.Vector3(0, 0, 0));
scene.add(b3dmTiles)
console.log("MODEL:", b3dmTiles)
console.log(runtime.getStats())
return runtime
}
I want to integrate the above code into a layer on top of MapLibre GL JS. I do this by creating a custom layer and pass the gl context into the renderer of that layer. I then create the scene, load the tiles, add the tiles to the scene and, annoyingly, it doesn't seem to load any tiles. The root tile-object is added to the scene, but no tiles are added to this root-object -> hence I am debug-printing all children of the tiles root each render.
I don't understand why this doesn't load any tiles into the scene. To prove that objects can be loaded into the scene, I am loading a wind-mill gltf object into the scene and rendering it -> this works just fine.
My hypothesis is that, something about how I am configuring the renderer/camera/scene is preventing ThreeLoader3DTiles from recognising that there are any tiles that need to be loaded/added to the scene, which would make sense of why the root-tile-node is added to the scene but without any tiles therein.
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { Loader3DTiles } from 'three-loader-3dtiles';
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
Clock
} from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import maplibregl, { loadMapTilerMaps, modelOrigin, modelTransform } from 'maplibre-gl';
import { BingMapsProvider, MapView, UnitsUtils } from 'geo-three'
import { Vector3 } from 'three';
let rendered = false;
export default function Demo(props) {
const ref = useRef();
useEffect(() => {
const f = async () => {
if (rendered) {
return;
}
console.log('LOAD')
rendered = true;
initMaps(ref)
}
f();
})
return <div id="map" style={{ height: window.innerHeight }} ref={ref}></div>
}
// TEST 1 - supposed to show maptiler terrain, with bing maps texture, with (cesium) 3D tiles on top
// FIXME: cesium tiles will not show
function initMaps(ref) {
// parameters to ensure the model is georeferenced correctly on the map
var modelOrigin = [100, -23];
var modelAltitude = 0;
var modelRotate = [Math.PI / 2, 0, 0];
var map;
//
function loadMap(baseMapsStyle) {
console.log("LOAD MAP")
map = new maplibregl.Map({
container: 'map', // container id
style: baseMapsStyle,
zoom: 12,
center: modelOrigin,
pitch: 52,
}).on('style.load', function () {
// map.addLayer(customLayer);
console.log("test")
console.log(customLayer)
console.log(map)
map.addLayer(customLayer);
});
}
initBingMaps(loadMap)
var modelAsMercatorCoordinate = maplibregl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);
// transformation parameters to position, rotate and scale the 3D model onto the map
var modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since our 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
// configuration of the custom layer for a 3D model per the CustomLayerInterface
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd: async function (map, gl) {
const camera = new THREE.Camera();
camera.position.add(new Vector3(0, 0, 0))
const scene = new THREE.Scene();
this.camera = camera;
this.scene = scene;
this.map = map;
this.clock = new Clock();
// use the MapLibre GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true,
// autoClear: true
});
console.log(Object.keys(map))
// create two three.js lights to illuminate the model
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
// use the three.js GLTF loader to add the 3D model to the three.js scene
var loader = new GLTFLoader();
loader.load(
'https://maplibre.org/maplibre-gl-js-docs/assets/34M_17/34M_17.gltf',
function (gltf) {
gltf.scene.name = "gltf scene"
gltf.scene.translateY(10);
this.scene.add(gltf.scene);
}.bind(this)
);
this.tilesRuntime = await loadOGCTiles(this.scene, this.renderer);
this.renderer.autoClear = false;
},
render: function (gl, matrix) {
this.camera.projectionMatrix = calculateProjectionMatrix(matrix, modelTransform)
const dt = this.clock.getDelta()
if (this.tilesRuntime) {
// I THINK THIS MIGHT BE THE ISSUE
this.tilesRuntime.update(dt, this.renderer, this.camera)
// FIXME: despite the root tile group being added to scene, there are no tiles added to this group
if (this.scene.getObjectByName("b3dmTiles").children.length > 0) {
console.log("TILES LOADED", this.scene.children)
} else {
console.log("NO TILES LOADED", this.scene.children)
}
}
//this.camera.position.add(new THREE.Vector3(0, 1, 0))
this.map.triggerRepaint();
this.renderer.render(this.scene, this.camera);
this.renderer.resetState();
}
};
// console.log(customLayer)
// console.log(map)
// map.addLayer(customLayer);
// map.on('styledata', function () {
// map.addLayer(customLayer);
// alert("test")
// });
}
async function loadOGCTiles(scene, renderer) {
console.log("RENDERER", renderer)
const result = await Loader3DTiles.load({
url: 'my 3d tile',
renderer: renderer,
options: {
dracoDecoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/draco',
basisTranscoderPath: 'https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/libs/basis',
}
})
const { model, runtime } = result
const b3dmTiles = model;
b3dmTiles.name = "b3dmTiles"
runtime.setDebug(true)
// b3dmTiles.position.add(new THREE.Vector3(0, 0, 0));
scene.add(b3dmTiles)
console.log("MODEL:", b3dmTiles)
console.log(runtime.getStats())
return runtime
}
function initBingMaps(loadMap) {
var BingMapsKey = 'my key';
var BingMapsImagerySet = 'Aerial'; //Alternatively, use 'AerialWithLabelsOnDemand' if you also want labels on the map.
var BingMapsImageryMetadataUrl = `https://dev.virtualearth.net/REST/V1/Imagery/Metadata/${BingMapsImagerySet}?output=json&include=ImageryProviders&key=${BingMapsKey}`;
fetch(BingMapsImageryMetadataUrl).then(r => r.json()).then(r => {
var tileInfo = r.resourceSets[0].resources[0];
//Bing Maps supports subdoamins which can make tile loading faster. Create a tile URL for each subdomain.
var tileUrls = [];
tileInfo.imageUrlSubdomains.forEach(sub => {
tileUrls.push(tileInfo.imageUrl.replace('{subdomain}', sub));
});
//Use the image provider info to create attributions.
var attributions = tileInfo.imageryProviders.map(p => {
return p.attribution;
}).join(', ');
//Create a style using a raster layer for the Bing Maps tiles.
var style = {
'version': 8,
'sources': {
'bing-maps-raster-tiles': {
'type': 'raster',
'tiles': tileUrls,
'tileSize': tileInfo.imageWidth,
'attribution': attributions,
//Offset set min/max zooms by one as Bign Maps is designed are 256 size tiles, while MapLibre is designed for 512 tiles.
'minzoom': 1,
'maxzoom': 20
}
},
'layers': [
{
'id': 'bing-maps-tiles',
'type': 'raster',
'source': 'bing-maps-raster-tiles',
'minzoom': 0,
'maxzoom': 23 //Let the imagery be overscaled to support deeper zoom levels.
}
]
};
//If you want to add terrian, you can either append it onto the stlye like this, or add it inline above.
//Add the source
style.sources.terrainSource = {
type: 'raster-dem',
//url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json',
url: 'https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=my key',
tileSize: 256
};
style.terrain = {
source: 'terrainSource',
exaggeration: 1
};
//Load MapLibre with this style.
loadMap(style);
});
}
function calculateProjectionMatrix(matrix, modelTransform) {
var rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
var rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
var rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
var m = new THREE.Matrix4().fromArray(matrix);
var l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
return m.multiply(l);
}
Please help me, the code is messy because I have spent a week trying to get these two technologies to work nicely together. You'll need bing-maps and maptiler (or alternative) keys -> I don't think it matters what map/terrain sources you use because they have no bearing on how MapLibre/ThreeLoaders3DTiles work.