I am trying to add a 3D model as a custom layer on Mapbox using Three.js. I also want to add a 3D terrain on the map. I followed the individual examples to add a 3D model and 3D terrain from Mapbox.
The problem I am facing is that the 3D model is appearing for a second till the terrain loads and then disappears once the terrain completes loading. If I only load the model, it is displaying properly.
This loadTerrain() function gets called on load of the map that then calls the loadModel() function:
async function loadTerrain() {
mapRef.current?.getMap().addSource("mapbox-dem", {
type: "raster-dem",
url: "mapbox://mapbox.mapbox-terrain-dem-v1",
tileSize: 512,
});
mapRef.current
?.getMap()
.setTerrain({ source: "mapbox-dem", exaggeration: 1.5 });
loadModel();
}
function loadModel(fragGroup: any) {
const modelOrigin: [number, number] = [76.9558, 11.0168];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
};
const customLayer: any = {
id: "3d-model",
type: "custom",
renderingMode: "3d",
layout: {
visibility: "visible",
},
onAdd: function (map: any, gl: any) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
const loader = new THREE.GLTFLoader();
loader.load(
"https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf",
(gltf) => {
this.scene.add(gltf.scene);
}
);
this.map = map;
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true,
});
this.renderer.autoClear = false;
},
render: function (gl: any, matrix: any) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
const m = new THREE.Matrix4().fromArray(matrix);
const 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);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
},
};
mapRef.current?.getMap().addLayer(customLayer);
}
I am using react-map-gl to render the mapbox map on a Next.js application.
I am expecting to load the model and terrain together but my model is getting disappeared as soon as terrain gets loading.
Using the standard example, you will need to recalculate the projection Matrix using the elevation of the anchor point once the terrain layer is loaded.
Maybe you want to take a look to Threebox Camera projection.
there's an specific example positioning a 3D model over terrain with the Glacier d'Argentiere. I'm maintaining this repo since 4 years ago and it has been included as an official example in Mapbox.
Hope it helps!