mapbox-gl结合threejs

文章目录

  • 前言
  • 为什么使用threebox
  • 代码示例
  • 总结


前言

最近在研究threejs和mapbox的结合,花了一天多的时间,结合threebox这个mapbox的三维库,给mapbox中创建自定义图层,添加自定义几何体,基于react-hooks实现,代码不多,但是threebox官网的例子给的很少,所以不少东西还是需要自己摸索下,特此记录下来。

参考:threebox.js


为什么使用threebox

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;

总结

  • mapbox-gl :custom-layer
  • threejs
  • threebox

你可能感兴趣的:(THREE.JS,#,Mapbox,WebGIS,3d,javascript)