Skip to main content

Draw Box Mesh with mapbox-gl

结合 mapbox-gl 的自定义图层渲染 Box Mesh,此实例实现了一下几个特性:

  1. 使用 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));
  1. 深度测试与 mapbox 地形的结合。
  2. 盒子米制单位缩放到 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>
  );
}