Getting uneven click-detection on 3d-objects on mapbox with raycaster
Asked Answered
F

1

0

I am using raycaster in mapbox with 3d-objects. I need to detect the click on the 3d-object.But When I click on the object I get a empty raycaster.intersectObject (array) sometimes and filled array sometimes, rather it should always have the 3d object that I have clicked.

I have a simple map as explained [here][1]. I have only added the raycasting part to it to keep it as simple as possible.

Can anybody help me out here please?

Fiddling answered 7/5, 2020 at 10:55 Comment(10)
Do you expect to have more than a single models in the scene? If yes, will you need a custom rotation for each of them (currently you store it in modelTransform3 but I would like to get rid of that in my answer.Cammie
@isolin yes i will be having multiple models in the scene.I am planning to store them in a array of different modelTransform3. i.e. just after modelTransform3 initialisation and just before the line THREE = window.THREE; I will add myArr.push(modelTransform3). And then will loop over this array whenever neccesary.Fiddling
That will be an issue then, since there is only one render call per scene and currently you multiply with the correction matrix you store per-object. So this l matrix needs to be constant per-scene and not per-object. But I am sure you can move that part to the onAdd function. For details see my answer over here.Cammie
@isolin thanks for your answer, it works for one object but even after moving l matrix to onAdd it didn't worked for more than one model. I have edited my question above.Fiddling
As I've noted above, l matrix needs to be constant per-scene, not per-object. In your edited code you overwrite l inside of onAdd in each iteration. So picking works only for the last object. You must use the position information to put the 3D objects into proper relative positions in 3D space. Tomorrow I will see if I can extend my answer accordingly.Cammie
BTW: map.on('zoom',function(){ map.jumpTo({ center: [longitude,latitude] }); }); is very unfriendly. It violates basic UX principles. I had to deleted to use your map properly.Cammie
I might have misled you. I just noticed that you create a new layer for each 3D object. If you plan to have just a few objects it should not affect the performance and it should work without moving the matrix stuff to onAdd, basically using the first solution I posted. If you plan to have a lot of objects, I would advice to put them all into a single scene, i.e. a single layer, if it doesn't interfere with other functionality you plan to have.Cammie
hey @Cammie many thanks for such a detailed answer.I just wanted to ask one little thing. Currently I am distinguishing between the objects by names as intersects[0].object.name. But in case I have two same models, then is there any other way of differentiating about which object out of the two was actually clicked? I am new to three.js and this question was bugging me for some time now..Fiddling
That would already be the third question within this question. Questions & answers should be clear and focused. This last question of you would drift away from the original problem. The short answer is to assign Object3D.name uniquely (e.g. take the index in the assetArr). If you need a more detailed explanation: 1) look for examples using a search engine or 2) start a new question.Cammie
@Cammie okay, got it.thanksFiddling
C
2

Click call

The code you provided is quite messy, it would be very helpful (for any one who will read your question in the future) if you could clean it up. Please remember this also when asking next questions.

I had to add a call to the onClick event handler ad the end of showMap(callback):

    map.on('click', e => {
        onClick(e);
    });

Raycaster

The solution is simple provided you read my answer to another MapBox+Three question. Just replace your onClick with the following code:

function onClick( event ) {
    event.preventDefault();
    //I had to change the changedTouches to point to adapt
    //  to the incoming event object as for me there was no such property
    mouse.x = ( event.point.x / window.innerWidth ) * 2 - 1;
    mouse.y = - ( event.point.y / window.innerHeight ) * 2 + 1;

    const camInverseProjection = 
        new THREE.Matrix4().getInverse(this.camera.projectionMatrix);
    const cameraPosition =
        new THREE.Vector3().applyMatrix4(camInverseProjection);
    const mousePosition =
        new THREE.Vector3(mouse.x, mouse.y, 1)
        .applyMatrix4(camInverseProjection);
    const viewDirection = mousePosition.clone()
        .sub(cameraPosition).normalize();

    this.raycaster.set(cameraPosition, viewDirection);

    //no change from here on
    var intersects = raycaster.intersectObjects( scene.children, true );
    console.log("Here",intersects);
    if ( intersects.length > 0 ) {
        alert("hi");
        console.log( 'Intersection:', intersects[ 0 ].object.name == "");
    }
}

Several objects

Your code is a chaotic extension of the example you referenced. In your original code sample you prepared it the way that you would create a new custom layer for each 3D object. This should kind-of work, but the performance would suffer soon with more objects added. The reason for that is that you would have a separate THREE instance including a scene, render-loop etc. for each singl e3D object.

I think the proper solution is to put all 3D objects into a single common layer with a single THREE scene that loads all the objects. Such change requires larger changes in your code. I will briefly summarize them here. You can see the whole working example in this fiddle. Be patient, the second object loads really slow.

  1. In map.on('load', ...) you need to load the new layer (replacing the model3 calls. Let's call it addThreeLayer().

  2. addThreeLayer() should first store the scene origin in a global variable. The origin is free to chose, I took the position of the first object. The following code also prepares the transformation matrix to flip the left-handed to a right-handed system and scale to meter units. This corresponds to your l matrix.

const originAsset = assetArr[0].cord;
const mc = mapboxgl.MercatorCoordinate.fromLngLat([originAsset.lng, originAsset.lat], 0);
const meterScale = mc.meterInMercatorCoordinateUnits();

sceneTransform = {};
sceneTransform.matrix = new THREE.Matrix4()
   .makeTranslation(mc.x, mc.y, mc.z)
   .scale(new THREE.Vector3(meterScale, -meterScale, meterScale));
sceneTransform.origin = new THREE.Vector3(mc.x, mc.y, mc.z); //not in meters!
  1. Inside of onAdd the GLTFLoader should loop over all models, load their meshes and place them relatively to the selected origin.
var loader = new THREE.GLTFLoader();
// use the three.js GLTF loader to add the 3D model
// to the three.js scene            
for (var i = 0; i < assetArr.length; i++) {                
    const modelOrigin3 = [assetArr[i].cord.lng, assetArr[i].cord.lat];
    const modelAltitude3 = 0;
    const modelRotate3 = new THREE.Euler(Math.PI / 2, 0, 0, 'XYZ');                
    const modelScale = assetArr[i].scaleFactor;

    const mc = mapboxgl.MercatorCoordinate.fromLngLat(modelOrigin3, modelAltitude3);
    loader.load(assetArr[i].url, function (gltf) {
        const scene = gltf.scene;
        const origin = sceneTransform.origin;
        // division necessary, since the scene is in meters
        // but mc and origin are not
        scene.position.set(
           (mc.x - origin.x) / meterScale,
          -(mc.y - origin.y) / meterScale,
           (mc.z - origin.z) / meterScale);
        scene.quaternion.setFromEuler(modelRotate3);
        scene.scale.set(modelScale, modelScale, modelScale)
        this.scene.add(gltf.scene);
    }.bind(this));
}
  1. The render function needs to be adapted to use sceneTransform
render: function (gl, matrix) {
    this.camera.projectionMatrix =  new THREE.Matrix4().fromArray(matrix).multiply(sceneTransform.matrix);
    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);
    this.map.triggerRepaint();
}
Cammie answered 7/5, 2020 at 13:5 Comment(2)
hey @Cammie when I changed the lat longs of both the objects in the assetArr then the second model is not getiing added to the scene at correct lat long.For ex- I changed the first one to longitude = 77.125959; latitude = 28.707724; and the second one to lng: 77.132078, lat: 28.703576. In this case second model was shown in the wrong place. Does your solution needs the objects to be at the same latlongs?Fiddling
You are right! Thank you for reporting that. I fixed the answer and the fiddle. A minus sign was missing. I also deleted the origCoord from the model data, since it was quite misleading for me and it is never used. Remember what I said about a clean code? Please always 1) format the code properly 2) remove all unused stuff.Cammie

© 2022 - 2024 — McMap. All rights reserved.