最近在研究threejs和mapbox的结合,花了一天多的时间,结合threebox这个mapbox的三维库,给mapbox中创建自定义图层,添加自定义几何体,基于react-hooks实现,代码不多,但是threebox官网的例子给的很少,所以不少东西还是需要自己摸索下,特此记录下来。
参考:threebox.js
mapbox官网有使用threejs的示例,但是由于threejs使用的是右手坐标系,而mapbox作为一个时空数据的渲染库,默认使用EPSG4326坐标系,参考官网mapbox-gl中创建threejs场景代码如下。
// configuration of the custom layer for a 3D model per the CustomLayerInterface
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd: function (map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
// create two three.js lights to illuminate the model
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);
// use the three.js GLTF loader to add the 3D model to the three.js scene
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;
// 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;
},
render: function (gl, matrix) {
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();
}
};
mapbox-gl中使用threejs场景需要创建type为custom的layer,onAdd函数只触发一次,用来初始化threejs场景,render在地图缩放、移动、旋转时都会触发,重新更新模型和相机的位置,如果涉及动画,代码量会更多。
threebox提供方便的方法来管理地理坐标,以及同步地图和场景相机
。
- 添加threebox封装的球
- 使用threejs添加一个立方体,并在它的上面添加一个上下浮动的圆锥
- 基于着色器添加一个黄色的半球光罩,并由下至上渐透明
效果:
完整代码:
import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import MapboxLanguage from '@mapbox/mapbox-gl-language';
import { Threebox, THREE } from 'threebox-plugin';
import 'antd/dist/antd.css';
function App() {
const mapContainerRef = useRef();
const sphereMesh = useRef();
const coneMesh = useRef();
const lightRingMesh = useRef();
const blueMaterial = new THREE.MeshPhongMaterial({
color: '#4791ff',
side: THREE.DoubleSide
});
const redMaterial = new THREE.MeshPhongMaterial({
color: '#f73131',
side: THREE.DoubleSide
});
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vPosition;
void main(){
vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
gl_Position = projectionMatrix * viewPosition;
vPosition = position;
}
`,
fragmentShader: `
varying vec3 vPosition;
uniform float uHeight;
void main(){
float gradMix = (vPosition.z+uHeight/2.0)/uHeight;
gl_FragColor = vec4(1.0,1.0,0,1.0-gradMix);
}
`,
transparent: true,
side: THREE.DoubleSide,
})
const mapRef = useRef();
let step = 200;
let cylinderRadius = 0
// 初始化基础图层
useEffect(() => {
mapboxgl.accessToken = 'your token'
mapRef.current = new mapboxgl.Map({
zoom: 14,
center: [116.5, 39.9],
pitch: 90,
style: 'mapbox://styles/mapbox/streets-v11',
container: mapContainerRef.current,
antialias: true,
},
);
mapRef.current.addControl(new MapboxLanguage({ defaultLanguage: "zh-Hans" }))
mapRef.current.on('load', (e) => {
//地图加载完后,才能进行添加图层
//console.log('load happened', mapRef.current.style);
mapRef.current.addLayer({
id: 'custom_layer',
type: 'custom',
renderingMode: '3d',
onAdd: function (map, mbxContext) {
window.tb = new Threebox(
map,
mbxContext,
{
defaultLights: true,
// enableSelectingFeatures: true, //change this to false to disable fill-extrusion features selection
// enableSelectingObjects: true, //change this to false to disable 3D objects selection
// enableDraggingObjects: true, //change this to false to disable 3D objects drag & move once selected
// enableRotatingObjects: true, //change this to false to disable 3D objects rotation once selected
// enableTooltips: true
}
);
// 示例一,threebox封装的小球
sphereMesh.current = window.tb.sphere({ radius: 5, color: 'green', material: 'MeshStandardMaterial', anchor:'center' }).setCoords([116.48, 39.9, 200]);
window.tb.add(sphereMesh.current);
//示例二 圆锥
const coneFeometry = new THREE.ConeGeometry(50, 50, 64);
coneMesh.current = new THREE.Mesh(coneFeometry, redMaterial);
// coneMesh.current.translateZ(50)
coneMesh.current = window.tb.Object3D({ obj: coneMesh.current , units: 'meters', bbox: false, anchor:'center' })
.setCoords([116.49, 39.9, 400]);
coneMesh.current.rotation.x = -0.5 * Math.PI;
window.tb.add(coneMesh.current);
//示例三 立方体
const geometry = new THREE.BoxGeometry(150, 150, 300);
let cube = new THREE.Mesh(geometry, blueMaterial);
cube = window.tb.Object3D({ obj: cube, units: 'meters', bbox: false, anchor:'center' })
.setCoords([116.49, 39.9, 0]);
window.tb.add(cube);
// 示例三 半球光罩
let cylinderGeom = new THREE.SphereBufferGeometry(10,32,32, 0,Math.PI)
lightRingMesh.current = new THREE.Mesh(cylinderGeom, shaderMaterial);
lightRingMesh.current.rotation.x = -0.5 * Math.PI;
lightRingMesh.current.geometry.computeBoundingBox();
const { min, max } = lightRingMesh.current.geometry.boundingBox;
// 设置物体高差
let uHeight = max.y - min.y;
shaderMaterial.uniforms.uHeight = {
value: uHeight,
};
lightRingMesh.current = window.tb.Object3D({ obj: lightRingMesh.current, bbox: true, anchor:'center' })
.setCoords([116.5, 39.9, 0])
window.tb.add(lightRingMesh.current);
// 执行动画
animate()
},
// 地图更新时触发(拖拽、移动、缩放)
render: function (gl, matrix) {
window.tb.update();
}
})
});
}, []);
function animate() {
// console.log(lightRingMesh.current)
requestAnimationFrame(() => { animate() });
step += 0.03
const z = Math.abs(50 * Math.cos(step)) + 400
coneMesh.current.setCoords([116.49, 39.9, z]);
cylinderRadius += 0.01;
// 当半径大于1时,重新开始
if (cylinderRadius > 1) {
cylinderRadius = 0;
}
// console.log(lightRingMesh.current)
}
}
return (
<div style={{ display: 'flex' }}>
<div
id="map-container"
ref={mapContainerRef}
style={{ height: '100vh', width: '100vw' }}
/>
<div style={{ position: 'fixed', top: '0', right: '0' }}>
<button onClick={() => { sphereMesh.current.visible = !sphereMesh.current.visible }} style={{ marginRight: '10px' }}>修改显隐</button>
</div>
</div>
);
}
export default App;