Frontend Development 15 min read

Creating Stunning 3D Map Visualizations with AMap and Three.js

This tutorial demonstrates how to combine Gaode (AMap) 3D maps with Three.js to render interactive effects such as flying lines, animated boundaries, rising peaks, floating pyramids, and custom text markers, using custom coordinate conversion, WebGL layers, shaders, and CSS2D rendering for a compelling web‑based geographic visualization.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating Stunning 3D Map Visualizations with AMap and Three.js

1. AMap + Three.js

Initialize a 3D AMap instance and create a custom GL layer that integrates a Three.js scene, camera, and renderer, enabling WebGL rendering on the map.

this.map = new AMap.Map(this.container, {
  zooms: [2, 20],
  zoom: 4.5,
  pitch: 0,
  showLabel: false,
  viewMode: '3D',
  center: this.center,
  mapStyle: 'amap://styles/dark'
});

Set up a custom coordinate conversion tool for Mercator projection.

this.customCoords = this.map.customCoords;
this.customCoords.setCenter(this.center);

Add a GL custom layer and configure the Three.js camera and renderer inside its init callback.

var gllayer = new AMap.GLCustomLayer({
  init: (gl) => {
    this.camera = new THREE.PerspectiveCamera(60, this.container.offsetWidth / this.container.offsetHeight, 1, 1 << 30);
    this.renderer = new THREE.WebGLRenderer({ context: gl });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.autoClear = false;
    this.scene = new THREE.Scene();
    this.createChart();
  },
  render: () => { /* update camera params and render scene */ }
});
this.map.add(gllayer);

2. Animated Boundary

Fetch administrative boundary data, create an AMap polyline, and animate a moving segment using TWEEN.

new TWEEN.Tween({ start: 0 })
  .to({ start: path.length }, 3000)
  .repeat(Infinity)
  .onUpdate((obj) => {
    if (obj.start + len < path.length) {
      polyline.setPath(path.slice(obj.start, obj.start + len));
    } else {
      const c = path.length - obj.start;
      polyline.setPath([].concat(path.slice(obj.start), path.slice(0, len - c)));
    }
  })
  .start();

3. Rising Peaks

Use a plane geometry with a custom shader material to create a peak that grows over time based on uTime and uHeight uniforms.

precision mediump float;
uniform float uTime;
uniform float uHeight;
varying float vD;
float PI = acos(-1.0);
vec2 center = vec2(0.5);
void main() {
  float d = length(uv - center) * 2.0;
  vD = pow(1.0 - d, 3.0);
  float h = vD * uHeight * uTime;
  vec3 pos = vec3(position.x * 0.5, position.y * 0.5, h);
  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

4. Floating Pyramid

Create a THREE.ConeGeometry with four sides, animate its vertical position between minHeight and maxHeight , and store each instance for per‑frame updates.

const geometry = new THREE.ConeGeometry(r, r * 2, 4, 1);
const material = new THREE.MeshLambertMaterial({ color: new THREE.Color(data.color) });
const cone = new THREE.Mesh(geometry, material);
cone.rotateX(-Math.PI * 0.5);
cone.position.set(d[0], d[1], this.size * 1.1);
this.scene.add(cone);
this.cones.push({ obj: cone, step: this.speed });

5. Text Labels

Use CSS2DRenderer to attach HTML DOM elements as labels that can receive events, positioning them with the custom coordinate converter.

let labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(container.offsetWidth, container.offsetHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.pointerEvents = 'none';
this.container.appendChild(labelRenderer.domElement);
this.labelRenderer = labelRenderer;

addLabel(dom, pos) {
  dom.style.pointerEvents = 'auto';
  const label = new CSS2DObject(dom);
  label.position.set(...pos);
  this.scene.add(label);
  return label;
}

6. Flight Lines

Generate a quadratic Bézier curve between two points, build a THREE.TubeGeometry , and apply a shader that animates a glowing line moving from start to end.

const curve = new THREE.QuadraticBezierCurve3(
  new THREE.Vector3(d[0][0], d[0][1], 0),
  new THREE.Vector3((d[0][0] + d[1][0]) * 0.5, (d[0][1] + d[1][1]) * 0.5, this.size),
  new THREE.Vector3(d[1][0], d[1][1], 0)
);
const geometry = new THREE.TubeGeometry(curve, 32, 10000, 8, false);
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0.0 },
    uLen: { value: 0.6 },
    uSize: { value: 10000 },
    uColor: { value: new THREE.Color(color) }
  },
  transparent: true,
  vertexShader: `...`,
  fragmentShader: `...`
});
const line = new THREE.Mesh(geometry, material);
this.scene.add(line);

new TWEEN.Tween({ time: 0 })
  .to({ time: 1.0 }, 1000)
  .repeat(Infinity)
  .onUpdate((obj) => { material.uniforms.uTime.value = obj.time; })
  .start();

7. Sequencing the Animation

Combine all previous steps in an async workflow: draw the moving boundary, raise a peak, show a label, launch a flight line, move the camera, and repeat for each location, ending with a final static view.

8. GitHub Repository

Source code and demo are available at https://github.com/xiaolidan00/my-earth .

frontend developmentThree.jsWebGLAMapMap Visualization
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.