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.
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 .
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.