Draw Box Mesh with mapbox-gl
结合 mapbox-gl 的自定义图层渲染 Box Mesh
,此实例实现了一下几个特性:
- 使用 RTC 解决大层级下图形抖动问题。
RTC 我们一般会通过传入的顶点数据计算图形包围盒的中心点 geometry.bounds.center
,
function projectToWorld (coords) {
let i = 0;
const len = coords.length;
const position = [];
for (; i < len; i++) {
const coord = coords[i];
const mc = mbve.fromLngLat({
lng: coord[0],
lat: coord[1],
}, coord[2]);
position.push(mc.x, mc.y, mc.z);
}
return position;
}
const coordinates = [
[90.65864059134882, 39.7373406540633],
[102.51084435117338, 24.846755709924764],
[114.46396935117377, 39.232415634606724],
];
const position = projectToWorld(coordinates);
const geometry = new Geometry(renderer, {
position: {
size: 3,
data: new Float32Array(position),
},
});
// 注意这里通过外部传入顶点数据以提高计算经度,如果不传的话会使用 32 位浮点数进行计算,精度可能有一定损失。
geometry.computeBoundingSphere(position);
const center = geometry.bounds.center.toArray();
然后将各顶点数据减去中心点得到一个相对坐标
let i = 0;
const len = position.length;
for (; i < len; i += 3) {
position[i] = position[i] - center[0];
position[i + 1] = position[i + 1] - center[1];
position[i + 2] = position[i + 2] - center[2];
}
geometry.setAttributeData('position', new Float32Array(position));
然后将模型的位置设置为中心点以便于计算出 modelMatrix
mesh.position.set(center[0], center[1], center[2]);
这样模型的基准位置是以包围盒的中心点为锚点,常规情况下问题不大,但是以本示例来说如果模型真实高度为 0,那么相当于模型的一半在地面以下,在无地形
的情况下展示会有问题(深度测试),所以这个时候我们可能希望以 Box
的底部为锚点,这时我们需要调整模型的位置将 Z 轴拔高:
mesh.position.set(center[0], center[1], center[2] + mbve.mercatorZfromAltitude(500, 45.39701));
- 深度测试与 mapbox 地形的结合。
- 盒子米制单位缩放到 mapbox 墨卡托单位。
示例
Result
Loading...
Live Editor
function render(props) { const refDom = useRef(null); const store = leva.useCreateStore(); class MeshLayer { constructor (id) { this.id = id; this.type = 'custom'; this.renderingMode = '3d'; } get camera () { return this.sync.camera; } updateCamera() { this.sync.update(); } projectToWorld (coords) { let i = 0; const len = coords.length; const position = []; for (; i < len; i++) { const coord = coords[i]; const mc = mbve.fromLngLat({ lng: coord[0], lat: coord[1], }, coord[2]); position.push(mc.x, mc.y, mc.z); } return position; } onAdd (map, gl) { this.renderer = new Renderer(gl, { autoClear: false, }); this.scene = new Scene(); this.sync = new mbve.CameraSync(map, 'perspective', this.scene); this.updateCamera = this.updateCamera.bind(this); map.on('move', this.updateCamera); map.on('resize', this.updateCamera); const geometry = new Box(this.renderer, { width: 1000, height: 1000, depth: 1000, widthSegments: 1, heightSegments: 1, depthSegments: 1, }); this.program = new Program(this.renderer, { vertexShader: ` attribute vec2 uv; attribute vec3 position; uniform vec3 cameraPosition; uniform mat4 viewMatrix; uniform mat4 modelMatrix; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` precision highp float; uniform float uTime; varying vec2 vUv; void main() { gl_FragColor.rgb = 0.5 + 0.3 * sin(vUv.yxx + uTime) + vec3(0.2, 0.0, 0.1); gl_FragColor.a = 1.0; } `, uniforms: { uTime: { value: 0.5 }, }, }); this.mesh = new Mesh(this.renderer, { geometry, program: this.program, wireframe: false, }); const coords = [6.58968, 45.39701, 1913.2236908406628]; const center = this.projectToWorld([ coords, ]); // this.mesh.position.set(center[0], center[1], center[2] + mercatorZfromAltitude(500, 45.39701)); this.mesh.position.set(center[0], center[1], center[2]); const s = mbve.meterInMercatorCoordinateUnits(center[1]); this.mesh.scale.set(s, s, s); this.mesh.position.set(...center) this.scene.add(this.mesh); this.updateCamera(); } onRemove () { this.mesh.destroy(); this.program.destroy(); } prerender () { } render () { this.scene.worldMatrixNeedsUpdate = true; this.renderer.render({ scene: this.scene, camera: this.camera, }); this.renderer.resetState(false); } } const init = () => { mapboxgl.accessToken = 'pk.eyJ1IjoidTEwaW50IiwiYSI6InQtMnZvTkEifQ.c8mhXquPE7_xoB3P4Ag8cA'; const map = new mapboxgl.Map({ container: refDom.current, zoom: 12, center: [6.58968, 45.39701], pitch: 45, bearing: 0, style: 'mapbox://styles/mapbox/satellite-streets-v12', antialias: true, projection: 'mercator', // projection: 'globe', }); map.on('load', () => { map.addSource('mapbox-dem', { 'type': 'raster-dem', 'url': 'mapbox://mapbox.mapbox-terrain-dem-v1', 'tileSize': 512, 'maxzoom': 14 }); // add the DEM source as a terrain layer with exaggerated height map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1 }); const layer = new MeshLayer('mesh'); map.addLayer(layer); window.layer = layer; }); window.map = map; return map; } useEffect(() => { const map = init(); return () => { console.log(map); }; }, []); return ( <div className="live-wrap"> <div className="leva-wrap"> <Leva collapsed fill ></Leva> <LevaPanel collapsed store={store} fill></LevaPanel> </div> <div className="scene-canvas" ref={refDom}></div> </div> ); }