1

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.

John
  • 346
  • 1
  • 11

1 Answers1

0

I am a dumb dumb.

Notice that I am creating a THREE.Camera rather than a THREE.PerspectiveCamera.

Strange that the GLTF model was rendering despite this mistake, but hurrah nevertheless.

John
  • 346
  • 1
  • 11