前言
实现思路
初始化高德3D地图
this.map = new AMap.Map(this.container, {
//缩放范围
zooms: [2, 20],
zoom: 4.5,
//倾斜角度
pitch: 0,
//隐藏标签
showLabel: false,
//3D地图模式
viewMode: '3D',
//初始化地图中心点
center: this.center,
//暗色风格
mapStyle: 'amap://styles/dark'
});
自定义高德地图经纬度转px工具,即墨卡托投影坐标转换
// 数据转换工具
this.customCoords = this.map.customCoords;
//设置坐标转换中心
this.customCoords.setCenter(this.center);
高德地图添加WebGL自定义图层,初始化Three.js配置
// 创建 GL 图层
var gllayer = new AMap.GLCustomLayer({
//zIndex: 10,
// 初始化的操作,创建图层过程中执行一次。
init: (gl) => {
//初始化Three相机
this.camera = new THREE.PerspectiveCamera(
60,
this.container.offsetWidth / this.container.offsetHeight,
1,
1 << 30
);
//初始化Three渲染器
this.renderer = new THREE.WebGLRenderer({
context: gl // 地图的 gl 上下文
});
this.renderer.setPixelRatio(window.devicePixelRatio);
// 自动清空画布这里必须设置为 false,否则地图底图将无法显示
this.renderer.autoClear = false;
//初始化场景
this.scene = new THREE.Scene();
this.createChart();
},
render: () => {
// 这里必须执行!!重新设置 three 的 gl 上下文状态。
this.renderer.resetState();
//设置坐标转换中心
this.customCoords.setCenter(this.center);
var { near, far, fov, up, lookAt, position } = this.customCoords.getCameraParams();
// 这里的顺序不能颠倒,否则可能会出现绘制卡顿的效果。
this.camera.near = near;
this.camera.far = far;
this.camera.fov = fov;
this.camera.position.set(...position);
this.camera.up.set(...up);
this.camera.lookAt(...lookAt);
this.camera.updateProjectionMatrix();
//渲染器渲染场景
this.renderer.render(this.scene, this.camera);
// 这里必须执行!!重新设置 three 的 gl 上下文状态。
this.renderer.resetState();
}
});
this.map.add(gllayer);
https://datav.aliyun.com/portal/school/atlas/area_selector
//绘制中国大陆运动边界
createChinaLine() {
return new Promise((resolve) => {
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000.json')
.then((res) => res.json())
.then((res) => {
let path = res.features[0].geometry.coordinates[0][0];
//截取10%的线段
let len = Math.floor(path.length * 0.1);
//边界折线
let polyline = new AMap.Polyline({
path: path,
strokeWeight: 4,
strokeColor: 'white',
lineJoin: 'round',
strokeOpacity: 1
});
this.map.add(polyline);
//利用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.length), path.slice(0, len - c))
);
}
})
.start();
resolve();
});
});
}
//Tween动画
animateAction() {
if (TWEEN.getAll().length) {
TWEEN.update();
}
}
运动边界实现简单,但效果一级棒!并且2D和3D地图都能用,优秀!
三、绘制升起山峰
顶点着色器
precision mediump float;
uniform float uTime;
uniform float uHeight;
varying float vD;
float PI = acos(-1.0);
vec2 center = vec2(0.5);
void main(void) {
//离中线的距离
float d = length(uv - center) * 2.0;
//沿中心点往外减少
vD = pow(1.0 - d, 3.0);
//山峰高度,随着uTime变化
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);
}
片元着色器
precision mediump float;
uniform vec3 uColor;
varying float vD;
void main(void) {
if(vD < 0.01)//透明度太小则不渲染颜色
discard;
else//透明度随着距离中心点的变化
gl_FragColor = vec4(uColor, vD * 2.0);
}
添加shaderMaterial的平面
const material = new THREE.ShaderMaterial({
uniforms: {
//随时间变化
uTime: { value: 0 },
//高度
uHeight: { value: this.size },
//颜色
uColor: { value: new THREE.Color(data.color) }
},
//开启透明度
transparent: true,
vertexShader: ``,
fragmentShader: ``
});
this.material = material;
//平面形状,方便复用
if (!this.ageometry) {
//平面的面数一定要足够才能形成山峰
const geometry = new THREE.PlaneGeometry(this.size, this.size, 500, 500);
this.ageometry = geometry;
}
const plane = new THREE.Mesh(this.ageometry, material);
//转换经纬度作为px
const d = this.customCoords.lngLatToCoord(data.pos);
plane.position.set(d[0], d[1], 0);
this.scene.add(plane);
注意:
1、平面的面数一定要足够才能形成山峰。
2、平面的大小和高度要足够大,在中国地图上才能看到,这里的this.size=500000,500k。在小的地图范围内,大小也要对应缩小。
3、高德地图坐标与three.js的坐标有些许不同,经纬度lng,lat对应xy,而高度对应z坐标。
其中提到的高度数据为虚拟数据。
添加山峰升起动画
addAnimate(start, end, time, update) {
return new Promise((resolve) => {
const tween = new TWEEN.Tween(start)
.to(end, time)
.onUpdate(update)
.easing(TWEEN.Easing.Quadratic.In)
.onComplete(() => {
resolve(tween);
})
.start();
});
}
const tw = await this.addAnimate({ time: 0 }, { time: 1 }, 1000, (obj) => {
this.material.uniforms.uTime.value = obj.time;
});
//播放完删除动画
TWEEN.remove(tw);
四、绘制浮动四棱锥
//转换经纬度坐标
const d = this.customCoords.lngLatToCoord(data.pos);
const r = this.size * 0.1;
//四棱锥图形,方便复用
if (!this.cgeometry) {
const geometry = new THREE.ConeGeometry(r, r * 2, 4, 1);
this.cgeometry = geometry;
}
const material = new THREE.MeshLambertMaterial({ color: new THREE.Color(data.color) });
//创建四棱锥网格
const cone = new THREE.Mesh(this.cgeometry, material);
//旋转90度,让四棱锥倒立
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 });
设置垂直高度坐标,让四棱锥上下浮动:遇到最大或最小高度时改变速度speed方向。
animateAction() {
if (this.cones.length) {
this.cones.forEach((c) => {
//高低浮动
if (c.obj.position.z >= this.maxHeight) {//最大高度
c.step = -this.speed;
} else if (c.obj.position.z <= this.minHeight) {//最小高度
c.step = this.speed;
}
c.obj.position.z += c.step;
});
}
}
五、绘制文本标牌
原本尝试用Marker的自定义内容content来实现html标签的,但文档上说Marker高度属性height只有在2.1版本生效,但目前只有2.0版本,暂时无法使用高度属性。
通过咨询高德地图官方,官方推荐用Loca数据可视化里面的ZMarkerLayer。
https://lbs.amap.com/demo/loca-v2/demos/zmarker/house-price
var triangleZMarker = new Loca.ZMarkerLayer({
loca: loca,
zIndex: 119,
depth: false,
});
triangleZMarker.setSource(geo);//设置数据集
triangleZMarker.setStyle({
content: (i, feat) => {//html自定义内容
return (
'<div style="width: 120px; height: 120px; background: url(/cover?u=VCs0NCsvZ05mSzVWeVBLeGQrNVA0Y0ZKVnlJbkNpcy9TcjRxblFYUmw2c3dka0xLVmNieEJ6cVB1SEJvZlZBTlZvOHhaS0ZtZjRwb0lDcWkyVllPVUxLcHcrcTFaczVETjBSOTJ4M01rVlhKeXpsY0s2VVkwSjJOSHV2VUtvNlpiUkYvdkNDbC81Q3lPU0JpWmcrWUR0MlBZT2NSODlCaEtIaU5keHdBQkR4ZzRhdTdUeFdSS3RNVk1oOThvRHZkYmVHMGdDbG5ydU52Yk5RSk5kaVFxUjhyTndiY0Rpck44cUl2MkVaSjBxTzd6SVVzLzI2cVowMzA5dVNlcWl0aGJSRi92Q0NsLzVDeU9TQmlaZytZRHQyUFlPY1I4OUJoS0hpTmR4d0FCRHhnNGF1N1R4V1JLdE1WTWg5OG9EdmR6OVp2ekhRVjJGOEkyZkNmSTRmOExGV2V2SW1kZ3Z2aDdsQkRQSm9JcXBYN3VkVXJmbkl2ZDZLWUlvamRkU0lhQ0RESi9yWkRieXZBS3N2TnFPejhVeDhyTndiY0Rpck44cUl2MkVaSjBxUHRYa2Iya3FZQlRsL2xSd3lobHpNcnk5SVRUSTdtbWhLRjZiZURrdWZPdjJVT21oVXlJRGN4Qjh4bEQ3YW1La1ZvQ01Nd01VMTdJWnRuL3pjbWY0V055UU5zZWh1Z3FYOTNNTTBFWTlMOU1GNTA4dGJIL1lxRGZUbkpjMENVLzAzOExRTHJUZnp2bGR1WkUxYXUwTnFtblFvUmN0eTBEVkRWamtCSnJFeTBMSkJna2JXNW1TbW96bVFwN3oyZzRDVFQrMjAxdC9RcStCbTR4Unh1ZDVNZXRsMmlUc21CTURZZmJZa3grNEgveklHbU1HeEZlWVIwYWgySVJ0YXhRZ3NHUWo2a2xReXdQdlFLcTkwajFXaitXakcwb1hIZU1JOUNkaERIMkZlZHk1MDV6M0dVcHZEdDFsM2xra0s5T3lGS3pxNERHaEVHUnFoS2dGd1hsbUpkck9JS3Q2UkFSVnU0R2VWako5eVNHVUE3QS9pVWtLQXVMMTI5SzRTRXJHdmtvMWFBcnN2QkRrR0k4M3hTVkJobnF0RmdXQ1F5M005YWFkRTd4L01pOUlHUEhKN215R2swSld0OURlTFF0YUU9)
+ '.png);"></div>'
);
},
unit: 'meter',
rotation: 0,
alwaysFront: true,
size: [60, 60],
altitude: 15,//高度
});
triangleZMarker.addAnimate({//动画
key: 'altitude',
value: [0, 1],
random: true,
transform: 1000,
delay: 2000,
yoyo: true,
repeat: 999999,
});
在GLCustomLayer的init中添加初始化CSS2DRender
let labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(container.offsetWidth, container.offsetHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
//不妨碍界面上点击冲突
labelRenderer.domElement.style.pointerEvents = 'none';
this.container.appendChild(labelRenderer.domElement);
this.labelRenderer = labelRenderer;
在GLCustomLayer的init中添加CSS2DRender渲染
this.labelRenderer.render(this.scene, this.camera);
添加Label标牌
addLabel(dom, pos) {
//label的dom可以触发事件
dom.style.pointerEvents = 'auto';
const label = new CSS2DObject(dom);
label.position.set(...pos);
this.scene.add(label);
return label;
}
addALabel(data) {
const div = document.createElement('div');
div.innerHTML = `<div class="tip-box" style="background:${data.bg};--base-color:${data.color}"><span class="circle" ></span><span class="text">${data.name}</span></div>`;
//坐标转换
const d = this.customCoords.lngLatToCoord(data.pos);
const label = this.addLabel(div, [d[0], d[1], this.size * 1.5]);
}
标牌动画和样式直接用css
//变大弹出
big {
{
transform: scale(0);
}
{
transform: scale(1);
}
}
//闪烁
flash {
{
opacity: 0.3;
}
{
opacity: 1;
}
}
{
dodgerblue; :
border: solid 1px var(--base-color);
rgba(30, 144, 255, 0.3); :
color: white;
nowrap; :
8px; :
16px; :
height: 32px;
animation: big 1s ease-in;
16px; :
display: flex;
center; :
0 0 8px var(--base-color); :
}
.circle {
var(--base-color); :
height: 16px;
width: 16px;
50%; :
animation: flash 0.5s ease-in alternate infinite;
}
.text {
margin: 0 8px;
}
DOM对象上如果有动画css请在外部包裹一层,避免css样式冲突,导致动画失效。
六、绘制飞线
飞线由管道形状绘制,通过着色器来改成头大尾小的形状。
顶点着色器
float PI = acos(-1.0);
varying float vT;
varying float vS;
uniform float uTime;
uniform float uSize;
uniform float uLen;
void main(void) {
//取模循环
float d = mod(uv.x - uTime, 1.0);
//截取uLen长度
vS = smoothstep(0.0, uLen, d);
//不在范围内不渲染
if(vS < 0.01 || d > uLen)
return;
//头大尾小的飞线坐标点
vec3 pos = position + normal * sin(PI * 0.5 * (vS - 0.6)) * uSize;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
片元着色器
varying float vS;
uniform vec3 uColor;
void main(void) {
//透明度随着飞线头尾变小
gl_FragColor = vec4(uColor, vS);
}
添加管道飞线,用贝塞尔曲线形成弯曲
addLine(posList, color) {
//经纬度坐标点统一转换
const d = this.customCoords.lngLatsToCoords(posList);
//贝塞尔曲线形成弯曲
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();
七、动画连续起来
最终效果:地图绘制运动边界,镜头放大倾斜,对应地点长出尖尖的“山峰”,然后弹出一个上下浮动四棱锥和文本标牌,随即生出一条飞线,镜头跟随,跳到下一个景点,再次弹出四棱锥和文本标牌,重复,走过所有地点后,定格到最终视角。
//运动边界线
await this.createChinaLine();
//绘制山峰
await this.createA(this.tags[0]);
//视角变化
this.map.setPitch(68, false, 3000);
this.map.setRotation(24, false, 3000);
await this.sleep(2000);
{
//升起山峰
const tw = await this.addAnimate({ time: 0 }, { time: 1 }, 1000, (obj) => {
this.material.uniforms.uTime.value = obj.time;
});
TWEEN.remove(tw);
//添加标牌
this.addALabel(this.tags[0]);
}
await this.sleep(2000);
//绘制飞线
this.addLine([this.tags[0].pos, this.tags[1].pos], this.tags[0].color);
for (let i = 1; i < this.tags.length; i++) {
const data = this.tags[i];
//视角跟随到新地点
this.map.setCenter(data.pos, false, 1000);
this.map.setZoom(6, false, 1000);
await this.sleep(1000);
//绘制山峰
await this.createA(data);
//升起山峰
const tw = await this.addAnimate({ time: 0 }, { time: 1 }, 1000, (obj) => {
this.material.uniforms.uTime.value = obj.time;
});
TWEEN.remove(tw);
this.addALabel(data);
await this.sleep(1000);
//添加飞线
if (i < this.tags.length - 1) {
this.addLine([data.pos, this.tags[i + 1].pos], data.color);
} else {
//最终视角
this.map.setPitch(73.2, false, 3000);
this.map.setZoom(5, false, 1000);
this.map.setRotation(58.7, false, 3000);
this.map.setCenter([101.6, 35.6], false, 1000);
}
}
高德地图3D地图视角设置真的很方便,还自带动画功能,效果真的很棒!
setZoom设置缩放大小
setPitch设置倾斜角度
setCenter设置地图中心经纬度
setRotation设置旋转角度
八、GitHub地址
https://github.com/xiaolidan00/my-earth
仅代表作者个人观点