Search code examples
three.jsmapbox-gl-jsthreebox

threebox "projectToWorld" returns values exceeding canvas, how do I fix this? (with sample code)


I recently found out there is a very handy method in three-box for placing three.js objects on the map which is "projectToworld".

While trying to place my three.js objects using the method, I realized that the Vector3 the method returns are really huge and not on the map.

According to the documentation of threebox, it says

projectToWorld

tb.projectToWorld(lnglat) : THREE.Vector3

Calculate the corresponding THREE.Vector3 for a given lnglat. It's inverse method is tb.unprojectFromWorld.

So I decided to use this method to locate my animated object in three js canvas. But what the methods returns are really huge.

enter image description here

So as I expected, these values don't place the three objects on the map and all the objects disappeared because they presumably are placed at very distant locations.

How do I fix this issue?

I made a minimal code to demonstrate this issue as below.

  1. instantiating map
var viewOrigin = [-73.8, 40.7];

var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/jotnajoa/ckpj6g4ho3nvf18pg0r98pjes/draft',
    zoom: 12,
    center: viewOrigin,
    pitch: 60,
    antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
});

var modelAltitude = 0;
  1. Set mapboxgl
function translateCoords(lon, lat) {

    let destination = [lon, lat];
    let finalCoord = mapboxgl.MercatorCoordinate.fromLngLat(destination, 0);

    return finalCoord
}
var modelAsMercatorCoordinate = translateCoords(viewOrigin[0], viewOrigin[1]);

var modelTransform = {
    translateX: modelAsMercatorCoordinate.x,
    translateY: modelAsMercatorCoordinate.y,
    translateZ: modelAsMercatorCoordinate.z,
    scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
  1. set variables for three.js render outside of mapboxgl because I need to access it outside of mapboxgl lifecycle hook.
const camera = new THREE.Camera();
const scene = new THREE.Scene();

// Setting map, canvas, renderer variable from the outside to access that from outside later

var map;
var canvas;
var renderer;
let particleGroup;
var frameCount = 0;  
 
  1. Instantiate mapboxgl customlayer
function generateMap() {

    var customLayer = {
        id: '3d-model',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function(map, gl) {
            map = map;
            canvas = map.getCanvas()
            renderer = new THREE.WebGLRenderer({
                canvas: map.getCanvas(),
                context: gl,
                antialias: true,
                preserveDrawingBuffer: true
            });
            renderer.autoClear = false;
        },
        render: function(gl, matrix) {
            frameCount += 0.1;
            rotate(frameCount)
            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
                    )
                )

            camera.projectionMatrix = m.multiply(l);
            renderer.resetState();
            renderer.render(scene, camera);
            map.triggerRepaint();

        }
    };

    map.on('style.load', function() {
        map.addLayer(customLayer, 'waterway-label');
    });

}
  1. Draw constantly revolving sphere
function fillingSpheres(group) {
    const meshGroup = new THREE.Group();

    const { count } = group.geometry.attributes.position
    const { array } = group.geometry.attributes.position

    for (let i = 0; i < count; i++) {

        let i3 = i * 3;
        const x = array[i3]
        const y = array[i3 + 1]
        const z = array[i3 + 2];
        const posVec = new THREE.Vector3(x, y, z);

        const circleGeo = new THREE.SphereGeometry(10, 10, 10);
        const circleMtl = new THREE.MeshBasicMaterial({ color: 'green' });
        const circleMesh = new THREE.Mesh(circleGeo, circleMtl);
        circleMesh.userData.destination = posExample[i % 10];
        circleMesh.position.set(posVec.x, posVec.y, posVec.z)
        meshGroup.add(circleMesh)
    };
    console.log(meshGroup)
    return meshGroup;

}
  1. Move the spheres to the corresponding location on the map (This is where the issue occurs)

First, I instantiated tb object based on the current map setting

function setThreeBox() {

    window.tb = new Threebox(
        map,
        map.getCanvas().getContext('webgl'), {
            realSunlight: true,
            enableSelectingObjects: true, //change this to false to disable 3D objects selection
            enableTooltips: true, // change this to false to disable default tooltips on fill-extrusion and 3D models
        }
    );
    tb.altitudeStep = 1;
}

Second, get the coordinates by projectToWorld and locate them

function moveSphere() {

    // instantiate threebox object based on map

    setThreeBox()

    const { children } = particleGroup
    children.forEach((d) => {
        const { destination } = d.userData;
        const coordVec = tb.projectToWorld([destination.lng, destination.lat])
        console.log(coordVec)
        d.position.set(coordVec)
    })
}

Then, all the coordinates are like x:200000 and it no longer shows the three objects on the scene.

What did I do wrong?

The reason why I based on three.js not solely on three-box is that I can't illustrate the revolving circle on the three.js coordinates space in three-box.

The running code is in the following link.

https://codepen.io/jotnajoa/pen/poeKoOW (for some reason the sample is running after refreshing the code, doesn't start immediately once it is opened)

It would be grateful if someone who knows well about three-box could help me on this. I assume it could be easier to solve this if I could directly access to scene() object in three-box only environment. If it's accessible, I could easily make the frame-based animation in three-box. (Maybe I'm wrong)

Placing is triggered when the button is clicked.


Solution

  • It's strange that no one could answer this question. So I finally figured out how to make it by myself.

    The solution is in the following link.

    The primary reason was that mapbox plots things not based on its vector configuration. It renders things through its matrix as follows.

    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))

                sphere.position.y = 0.2;
    
                camera.projectionMatrix.elements = matrix;
                camera.projectionMatrix = m.multiply(l);
                renderer.state.reset();
                renderer.render(scene, camera);
    

    https://codepen.io/jotnajoa/pen/BaWGBoL?editors=1010