问题
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. I have only added the raycasting part to it to keep it as simple as possible.
Here is my code.
var longitude, latitude, diff, map, destLongitude, destLatitude, distFrmlast = 0;
var interval = 5000; // initial condition
var assetArr = [];
var modelOrigin, modelOrigin2;
var modelAltitude, modelAltitude2;
var modelRotate, modelRotate2;
var modelAsMercatorCoordinate, modelAsMercatorCoordinate2;
var modelTransform, modelTransform2;
var THREE;
var customLayer, customLayer2;
var previousDistance = 0, currentDistance = 0;
var clock = new THREE.Clock();
var mixer;
var renderer = null;
$(document).ready(function(){
longitude = 77.123643;
latitude = 28.707272;
assetArr.push({id:"3d0", cord:{lng:longitude,lat:latitude}, url:'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf', scaleFactor:3, rad: 0.02, origCoord:{lng:longitude,lat:latitude}});
showmap();
});
function showmap(callback){
mapboxgl.accessToken = 'pk.eyJ1IjoibWFzaDEyIiwiYSI6ImNrNzBhMHc2aTFhMnkza3J2OG51azV6aW0ifQ.1FeBAzRjqkSQex-u-GiPyw';
map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v11',
center: [longitude,latitude],
zoom: 17.6,
pitch: 95,
bearing: -17.6,
container: 'map',
antialias: false,
});
var geojson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [longitude,latitude]
},
'properties': {
'title': 'Mapbox',
'description': 'Park Centra'
}
}
]
};
// The 'building' layer in the mapbox-streets vector source contains building-height
// data from OpenStreetMap.
map.on('zoom',function(){
map.jumpTo({ center: [longitude,latitude] });
});
map.on('rotate',function(){
document.getElementById('info').innerHTML = JSON.stringify(longitude +" : : "+latitude+" : : "+diff);
map.jumpTo({center: [longitude,latitude]});
});
map.on('load', function() {
// Insert the layer beneath any symbol layer.
console.log("map loaded");
var layers = map.getStyle().layers;
console.log(layers);
var labelLayerId;
for (var i = 0; i < layers.length; i++) {
if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
labelLayerId = layers[i].id;
break;
}
}
// map.addLayer(customLayer, 'road-label');
map.addLayer(
{
'id': '3d-buildings',
'source': 'composite',
'source-layer': 'building',
'filter': ['==', 'extrude', 'true'],
'type': 'fill-extrusion',
'minzoom': 15,
'paint': {
'fill-extrusion-color': '#aaa',
// use an 'interpolate' expression to add a smooth transition effect to the
// buildings as the user zooms in
'fill-extrusion-height': [
'interpolate',
['linear'],
['zoom'],
15,
0,
15.05,
['get', 'height']
],
'fill-extrusion-base': [
'interpolate',
['linear'],
['zoom'],
15,
0,
15.05,
['get', 'min_height']
],
'fill-extrusion-opacity': 0.6
}
},
labelLayerId
);
model3(assetArr[0].id, assetArr[0].cord, assetArr[0].url , assetArr[0].scaleFactor);
map.addLayer(customLayer3, 'waterway-label');
});
}
var renderFlag = true,scene, camera;
function model3(Id, coordinates, gltfUrl , scaleFactor){
// parameters to ensure the model is georeferenced correctly on the map
//var modelOrigin = [77.052024, 28.459822];
console.log("Values ",coordinates.lng, coordinates.lat);
modelOrigin3 = [coordinates.lng, coordinates.lat];
modelAltitude3 = 0;
modelRotate3 = [Math.PI / 2, 0, 0];
modelAsMercatorCoordinate3 = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin3,
modelAltitude3
);
// transformation parameters to position, rotate and scale the 3D model onto the map
modelTransform3 = {
translateX: modelAsMercatorCoordinate3.x,
translateY: modelAsMercatorCoordinate3.y,
translateZ: modelAsMercatorCoordinate3.z,
rotateX: modelRotate3[0],
rotateY: modelRotate3[1],
rotateZ: modelRotate3[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: modelAsMercatorCoordinate3.meterInMercatorCoordinateUnits(),
langlat: modelOrigin3
};
THREE = window.THREE;
// configuration of the custom layer for a 3D model per the CustomLayerInterface
customLayer3 = {
id: Id,
type: 'custom',
renderingMode: '3d',
onAdd: function (map, gl) {
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
this.scene = new THREE.Scene();
camera = this.camera;
scene = this.scene;
// create two three.js lights to illuminate the model
var light1 = new THREE.AmbientLight( 0xffffff );
light1.position.set(0, -70, 100).normalize();
this.scene.add(light1);
var light2 = new THREE.AmbientLight( 0xffffff );
light2.position.set(0, 70, 100).normalize();
this.scene.add(light2);
// use the three.js GLTF loader to add the 3D model to the three.js scene
var loader = new THREE.GLTFLoader();
loader.load(
gltfUrl,
function (gltf) {
this.scene.add(gltf.scene);
}.bind(this)
);
this.map = map;
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
if(renderFlag == true){
renderFlag = false;
this.renderer.domElement.addEventListener( 'touchend', onClick, false );
}
},
render: function (gl, matrix) {
var rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform3.rotateX
);
var rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform3.rotateY
);
var rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform3.rotateZ
);
var m = new THREE.Matrix4().fromArray(matrix);
var l = new THREE.Matrix4()
.makeTranslation(
modelTransform3.translateX,
modelTransform3.translateY,
modelTransform3.translateZ
)
.scale(
new THREE.Vector3(
modelTransform3.scale*scaleFactor,
-modelTransform3.scale*scaleFactor,
modelTransform3.scale*scaleFactor
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
}
};
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2( Infinity, Infinity );
function onClick( event ) {
event.preventDefault();
mouse.x = ( event.changedTouches[0].clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.changedTouches[0].clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( scene.children, true );
console.log("Here",intersects);
if ( intersects.length > 0 ) {
alert("hi");
console.log( 'Intersection:', intersects[ 0 ].object.name == "");
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Add a 3D model</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v1.9.1/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<script src="https://unpkg.com/three@0.106.2/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.106.2/examples/js/loaders/GLTFLoader.js"></script>
<div id="map"></div>
<script src = "js/test.js"> </script>
</body>
</html>
The log sometimes on clicking tells me this
Here [] (js line 347).
while sometimes it is like
Here
[{…}]
0:
distance: 45.707979283756266
face: Tb {a: 6, b: 89, c: 23, normal: n, vertexNormals: Array(0), …}
faceIndex: 98
object: ra {uuid: "C0D03F0E-CE94-4B4B-A9B2-C8117268E863", name: "Briks_302", type: "Mesh", parent: G, children: Array(0), …}
point: n {x: 12.70179089111657, y: 2.0773969954241096, z: -43.858503167413765}
uv: C {x: 0.940550558052383, y: 0.42664070999192283}
__proto__: Object
length: 1
__proto__: Array(0)
Why am I getting this uneven behaivior on clicking the 3d object? I am stuck at this point. Can anybody help me out here please?
Edit
Sorry for the messy code. I have tried to remove the unwanted code this time.
I tried to move the l
matrix to onAdd
function, but as per your answer here when i tried to use MercatorCoordinate.fromLngLat I got a MercatorCoordinate cordinate undefined error. Then i tried moving the l
matrix to onAdd
like this below, but still the intersect object is only filled on second object, an not on the first object. here is the updated js
var longitude, latitude, diff, map;
var assetArr = [];
var modelOrigin3;
var modelAltitude3;
var modelRotate3;
var modelAsMercatorCoordinate3;
var modelTransform3;
var THREE;
var renderer = null;
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2( Infinity, Infinity );
var lastsArr = [],ar = [],renderFlag = true, scene, camera;
$(document).ready(function(){
longitude = 77.122279;
latitude = 28.706246;
assetArr.push({ id:"3d1", cord:{lng:77.122279,lat:28.706246}, url:'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf', scaleFactor: 0.5 , rad: 0.015, origCoord:{lng:77.122279,lat:28.706246}});
assetArr.push({ id:"3d2", cord:{lng:77.125959,lat:28.707724}, url:'https://www.zamit.one/location/gun/scene.gltf', scaleFactor: 9 , rad: 0.015, origCoord:{lng:77.125959,lat:28.707724}});
showmap();
});
function showmap(callback){
mapboxgl.accessToken = 'pk.eyJ1IjoibWFzaDEyIiwiYSI6ImNrNzBhMHc2aTFhMnkza3J2OG51azV6aW0ifQ.1FeBAzRjqkSQex-u-GiPyw';
map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/streets-v11',
center: [longitude,latitude],
zoom: 17.6,
pitch: 95,
bearing: -17.6,
container: 'map',
antialias: false,
dragPan: true,
dragRotate: false,
maxZoom: 18.7,
minZoom: 16
});
var geojson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [longitude,latitude]
},
'properties': {
'title': 'Mapbox',
'description': 'Park Centra'
}
}
]
};
// The 'building' layer in the mapbox-streets vector source contains building-height
// data from OpenStreetMap.
map.on('zoom',function(){
map.jumpTo({ center: [longitude,latitude] });
});
map.on('rotate',function(){
document.getElementById('info').innerHTML = JSON.stringify(longitude +" : : "+latitude+" : : "+diff);
map.jumpTo({center: [longitude,latitude]});
});
map.on('load', function() {
// Insert the layer beneath any symbol layer.
console.log("map loaded");
var layers = map.getStyle().layers;
console.log(layers);
var labelLayerId;
for (var i = 0; i < layers.length; i++) {
if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
labelLayerId = layers[i].id;
break;
}
}
map.on('click', e => {
onClick(e);
});
map.style.stylesheet.layers.forEach(function(layer) {
if (layer.type === 'symbol' && layer.id != 'waterway-label') {
map.removeLayer(layer.id);
}
});
for(var i = 0; i< assetArr.length; i++){
model3(assetArr[i].id, assetArr[i].cord, assetArr[i].url , assetArr[i].scaleFactor,i);
}
for(var i = 0;i<lastsArr.length; i++){
map.addLayer(lastsArr[i], 'waterway-label');
console.log("i ",i,lastsArr[i]);
}
});
}
function model3(Id, coordinates, gltfUrl , scaleFactor,i){
// parameters to ensure the model is georeferenced correctly on the map
//var modelOrigin = [77.052024, 28.459822];
console.log("Values ",coordinates.lng, coordinates.lat);
modelOrigin3 = [coordinates.lng, coordinates.lat];
modelAltitude3 = 0;
modelRotate3 = [Math.PI / 2, 0, 0];
modelAsMercatorCoordinate3 = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin3,
modelAltitude3
);
// transformation parameters to position, rotate and scale the 3D model onto the map
modelTransform3 = {
translateX: modelAsMercatorCoordinate3.x,
translateY: modelAsMercatorCoordinate3.y,
translateZ: modelAsMercatorCoordinate3.z,
rotateX: modelRotate3[0],
rotateY: modelRotate3[1],
rotateZ: modelRotate3[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: modelAsMercatorCoordinate3.meterInMercatorCoordinateUnits(),
langlat: modelOrigin3
};
ar.push(modelTransform3);
var l;
THREE = window.THREE;
// configuration of the custom layer for a 3D model per the CustomLayerInterface
customLayer3 = {
id: Id,
type: 'custom',
renderingMode: '3d',
onAdd: function (map, gl) {
// this.camera = new THREE.Camera();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
this.scene = new THREE.Scene();
camera = this.camera;
scene = this.scene;
var rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
ar[i].rotateX
);
var rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
ar[i].rotateY
);
var rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
ar[i].rotateZ
);
l = new THREE.Matrix4()
.makeTranslation(
ar[i].translateX,
ar[i].translateY,
ar[i].translateZ
)
.scale(
new THREE.Vector3(
ar[i].scale*scaleFactor,
-ar[i].scale*scaleFactor,
ar[i].scale*scaleFactor
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
var light1 = new THREE.AmbientLight( 0xffffff );
light1.position.set(0, -70, 100).normalize();
this.scene.add(light1);
var light2 = new THREE.AmbientLight( 0xffffff );
light2.position.set(0, 70, 100).normalize();
this.scene.add(light2);
// use the three.js GLTF loader to add the 3D model to the three.js scene
var loader = new THREE.GLTFLoader();
loader.load(
gltfUrl,
function (gltf) {
this.scene.add(gltf.scene);
}.bind(this)
);
this.map = map;
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
if(renderFlag == true){
renderFlag = false;
}
},
render: function (gl, matrix) {
var m = new THREE.Matrix4().fromArray(matrix);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
}
lastsArr.push(customLayer3)
};
function onClick( event ) {
event.preventDefault();
//I had to change the changedTouches to point to adapt to the incoming event object
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");
for(var i = 0;i<intersects.length;i++){
console.log( 'Intersection:', intersects[ 0 ].object.name);
}
}
}
回答1:
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 themodel3
calls. Let's call itaddThreeLayer()
.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 yourl
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
theGLTFLoader
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();
}
来源:https://stackoverflow.com/questions/61656054/getting-uneven-click-detection-on-3d-objects-on-mapbox-with-raycaster