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.
In map.on('load', ...)
you need to load the new layer (replacing the model3
calls. Let's call it addThreeLayer()
.
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!
- 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));
}
- 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();
}
modelTransform3
but I would like to get rid of that in my answer. – CammiemodelTransform3
. i.e. just after modelTransform3 initialisation and just before the lineTHREE = window.THREE;
I will addmyArr.push(modelTransform3)
. And then will loop over this array whenever neccesary. – Fiddlingl
matrix needs to be constant per-scene and not per-object. But I am sure you can move that part to theonAdd
function. For details see my answer over here. – Cammiel
matrix toonAdd
it didn't worked for more than one model. I have edited my question above. – Fiddlingl
matrix needs to be constant per-scene, not per-object. In your edited code you overwritel
inside ofonAdd
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. – Cammiemap.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. – CammieonAdd
, 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. – Cammieintersects[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.. – FiddlingObject3D.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