Three.js+高德地图 实现 3D 散点标注

4,913 阅读4分钟

最终的成果展示

由于高德地图涉及到收费的 apikey, 所以这里不方便公开源码, 除非谁能给我一个免费的 apikey。

地图.png

2025-06-26 22-15-35_2.gif

上一篇 《Three.js 加载 glb 模型实现散点标注》 较为详细地介绍了在 Three.js 中, 怎么加载 glb 格式的 3D 模型, 本文将会跟高德地图结合起来, 在高德地图上, 加入 Three.js 自定的图层, 并实现散点标注。

一、加载高德地图相关依赖

高德地图,详细的使用方法,可以见官方文档,

首先加载 script webapi.amap.com/maps

其次再加载 script webapi.amap.com/loca

顺序不要搞混乱,由于时间关系,这里直接通过 DOM 标签,通过 promise 按顺序引入进来

详细代码如下:


private readonly pluginsList = [
    //编辑多边形
    'AMap.PolyEditor',
    //允许开发者自定义图层的绘制方法
    'AMap.CustomLayer',
    //3D 控制条,旋转、缩放
    'AMap.ControlBar',
    //热力图
    'AMap.Heatmap',
    //3D图层
    'Map3D',
    //叠加自定义的WebGL内容
    'AMap.GLCustomLayer',
    //显示和管理建筑物信息
    'AMap.Buildings',
    //地图尺寸管理
    'AMap.Size',
    //经纬度坐标点
    'AMap.LngLat',
    //加载3D瓦片数据
    'AMap.3DTilesLayer',
    //编辑多边形和折线
    'AMap.PolylineEditor',
    //驾车路线规划
    'AMap.Driving'
  ];
  
//加载高德地图相关的js
  private async loadGaoDeMapScript(){
    const AMAP_KEY = '8281b6b8f40890205d2a2755b52dbfee';
    return new Promise((resolve, reject) => {
      if (window.AMap && window.Loca){
        resolve();
      }else{
        //加载maps.js
        const script = document.createElement('script');
        script.charset = 'utf-8'
        script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}&callback=_mapLoaded&plugin=${this.pluginsList.join(',')}`;
        document.head.appendChild(script);
        script.onerror = function () {
          reject(new Error('地图API文件加载失败'));
        }
      }
      window._mapLoaded = function () {
        //加载loca.js
        const arr = [`https://webapi.amap.com/loca?v=2.0.0beta&key=${AMAP_KEY}`];
        let count = 0
        for (let i = 0; i < arr.length; i++) {
          const script = document.createElement('script');
          script.charset = 'utf-8';
          script.src = arr[i];
          document.head.appendChild(script);
          script.onload = function () {
            count = count + 1;
            if (count >= arr.length) {
              resolve();
            }
          }
          script.onerror = function () {
            reject(new Error('地图可视化API文件加载失败'));
          }
        }
      }
    })
  }

二、初始化高德地图

这里相当于把高德地图当成网页背景, 所以需要初始化高德地图。

这里会涉及到比较多的参数设置, 具体可以看高德地图官方文档:

  1. center 地图的中心位置,一般是经纬度构成的数组
  2. zooms 缩放程度, 例如 [3, 22] 表示缩放程度在 3 与 22 之间
  3. viewMode 想要 3D 效果的话, 直接赋值 “3D”
  4. pitch 视角倾斜度

代码如下:


public async createMap(){
    return new Promise((resolve, reject) => {
      this.loadGaoDeMapScript().then(() => {
        const containerId = this.config.containerId || 'container';
        const gaodeMap = new AMap.Map(containerId, {
          center: this.config.center,
          resizeEnable: true,
          zooms: [3, 22],
          viewMode: this.config.viewMode,
          defaultCursor: 'default',
          pitch: 30,
          mapStyle: this.config.mapStyle || 'amap://styles/grey',
          expandZoomRange: true,
          rotation: 0,
          zoom: this.config.zoom,
          skyColor: this.config.skyColor,
          //不显示默认建筑物
          showBuildingBlock: false,
          //不显示默认建筑物
          features: ['bg', 'road', 'point'], 
          //注意:这个Layer写与不写好像都是没有什么影响
          layers: [
            AMap.createDefaultLayer(),
            new AMap.Buildings({
              zooms: [10, 22],
              zIndex: 2,
              //修改该值会导致显示异常
              //heightFactor: 1.2, 
              roofColor: 'rgba(171,211,234,0.9)',
              wallColor: 'rgba(34,64,169,0.5)',
              opacity: 0.7,
              visible: this.config.showBuildingBlock
            })
          ],
          mask: null
        })
        this.map = gaodeMap;
        resolve(gaodeMap);
      })
      .catch((err) => {
        reject(err);
      })
    })
  }

三、自定义 3D 图层

以上第二步,已经创建了高德地图, 获取到 map 这个对象, 现在, 需要根据 map 对象, 创建一个 3D 图层, 加入高德地图中, 下面代码 createGlCustomLayer 这个方法,将会创建 3D 图层, 并在 init 里面, 实例化 Three.js 相关的 Scene、 Camera、 Renderer , 最后在 render 里面,更新下相机状态, 使之渲染不出现异常。

//创建非独立图层
  createGlCustomLayer () {
    return new Promise((resolve) => {
      const layer = new AMap.GLCustomLayer({
        zIndex: 120,
        //设置为true时才会执行init
        visible: true, 
        init: (gl) => {
          this.initThree(gl);
          resolve(layer);
        },
        render: (gl) => {
          //注意:这个方法是放在关键帧里面执行的,所以调用会非常频繁
          this.updateCamera();
          this.map.render();
        }
      })
      this.map.add(layer);
    })
  }
  
  
 //初始化three实例
  initThree (gl) {
    //第1步:创建scene
    this.scene = new Scene();
    //第2步:创建camera,注意这里并没有设置相机的位置,而是在关键帧方法里面执行updateCamera,从而设置相机位置
    const { clientWidth, clientHeight } = this.container;
    this.camera = new PerspectiveCamera(60, clientWidth / clientHeight, 100, 1 << 30);
    //第3步:创建renderer,注意这里多加设置了渲染器的上下文gl
    const renderer = new WebGLRenderer({
      alpha: true,
      antialias: false,
      precision: 'highp',
      context: gl
    });
    renderer.setSize(clientWidth, clientHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    //必须设置为false才能实现多个render的叠加
    renderer.autoClear = false;
    renderer.setClearAlpha(0);
    this.renderer = renderer;
  }

  //更新相机
  updateCamera () {
    const { scene, renderer, camera, customCoords } = this;
    if (!renderer) {
      return;
    }
    //重新定位中心,这样才能使当前图层与Loca图层共存时显示正常
    if (this.center) {
      customCoords.setCenter(this.center);
    }
    const { near, far, fov, up, lookAt, position } = customCoords.getCameraParams();
    //近平面
    camera.near = near;
    //远平面
    camera.far = far;
    //视野范围
    camera.fov = fov;
    camera.position.set(...position);
    camera.up.set(...up);
    camera.lookAt(...lookAt);
    //更新相机坐标系
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);
    //这里必须执行,重新设置 three 的 gl 上下文状态
    renderer.resetState();
  }

  //设置图层中心坐标,非常重要
  updateCenter (lngLat) {
    if (lngLat instanceof Array && lngLat.length === 2) {
      this.customCoords.setCenter(lngLat);
    }
  }

同时, 页面需要监听下尺寸变化, 尺寸变化需要更新下相机的宽高比, 使之不会变形。

 //尺寸监听
  eventListener () {
    window.addEventListener('resize', () => {
      const { clientWidth, clientHeight } = this.container;
      if (this.camera) {
        this.camera.aspect = clientWidth / clientHeight;
      }
    })
  }