技术架构
vue3+高德地图2.0+threejs
代码步骤
这里我们就用合肥市为主要的地区,将其他地区扣除,首先使用高德的webapi的DistrictSearch功能,使用该功能之前记得检查一下初始化的时候是否添加到plugins中,然后搜索合肥市的行政数据。
district.search(targetArea.name, (status, result) => {
if (status === "complete") {
//设置中心点
map.setCenter([result.districtList[0].center.lng, result.districtList[0].center.lat], true);
//整个世界
let outer = [new AMap.LngLat(-360, 90, true), new AMap.LngLat(-360, -90, true), new AMap.LngLat(360, -90, true), new AMap.LngLat(360, 90, true)];
//合肥市边界信息
let holes = result.districtList[0].boundaries;
let pathArray = [outer];
// 绘制行政区划
pathArray.push.apply(pathArray, holes);
// 在合肥市内描边
let polygon1 = new AMap.Polygon({path: [outer],
strokeColor: "#00eeff",
strokeWeight: 5,
fillOpacity: 0,
});
//添加至地图
polygon.setPath(pathArray);
map.add(polygon);
}
});
这里就已经有了整个城市的轮廓。
我们的需求是自定义背景,首先我们需要获取到除了合肥市以外的地区,这里我们使用threejs,版本是0.157.0,大家这里可以安装和我一样的版本保证不会出错,版本太高会有问题,控制台 npm i three@0.157.0,在代码中import * as THREE from "three"导入,这里我们单独将threejs业务代码分出去。
------继续上面的代码----------
//先获取地图画布的大小
const innerHeight = mapRef.value.clientHeight;
const innerWidth = mapRef.value.clientWidth;
let options = {
pathArray,
center: [result.districtList[0].center.lng, result.districtList[0].center.lat],
size: {
innerWidth,
innerHeight,
},
};
//这里传入数据,单独写一个js文件来处理threejs业务 ,instace是AMap
createThreeLayer(instance, map, options);
收集到我们需要的数据之后单独建一个js文件,先安装一个插件叫turf,这个插件非常善于处理地图上的数学、多边型,先 npm i @turf/turf,再导入import * as turf from "@turf/turf,下面开始写逻辑。
import * as THREE from "three";
import * as turf from "@turf/turf";
let glLayer;
let camera, renderer, scene, raycaster;
let customCoords = null;
/**
* @description 创建ThreeLayer
*/
export function createThreeLayer(instance, map, options) {
const { polygon, center, size } = options;
customCoords = map.customCoords;
// 地图中心转换为墨卡托
const centerCoord = customCoords.lngLatToCoord(center);
// polygon是传入的多边形坐标,是一个非遮罩区域,outer外边框是整个地图的边界
const outer = polygon[0];
const inner = polygon[1];
// inner的bbox
const turfPoints = inner.map((item) => [item.lng, item.lat]);
const innerBbox = turf.bbox(turf.polygon([turfPoints]));
// 最小正方形
const squared = turf.square(innerBbox);
// 转为4个点
const squaredPoints = [
[squared[0], squared[1]],
[squared[0], squared[3]],
[squared[2], squared[3]],
[squared[2], squared[1]],
];
// 多边形坐标转换为世界中的坐标
const innerCoords = inner.map((item) => {
return {
...item,
coord: customCoords.lngLatToCoord([item.lng, item.lat]),
};
});
const outerCoords = outer.map((item) => {
return {
...item,
coord: customCoords.lngLatToCoord([item.lng, item.lat]),
};
});
//世界边界点
const squaredPointsCoords = squaredPoints.map((item) => customCoords.lngLatToCoord(item));
//——--------------------——------------继续写代码的位置-------------------------
}
这里将我们所有使用的点转换好了之后就可以创建自定义图层了,接下来开始绘制,这里我们准备两张图片,一张贴图,一张法线贴图(让图片凹凸部分感光,让画面更有质感),所有步骤的解释都在注释里面。
// 背景贴图
const bgTexture = new THREE.TextureLoader().load("static/three/bg.png");
// 法线背景贴图
const normalTexture = new THREE.TextureLoader().load("static/three/bg_normal.png");
if (!glLayer) {
glLayer = new instance.GLCustomLayer({
zIndex: 10,
init: (gl) => {
camera = new THREE.PerspectiveCamera(45, size.innerWidth / size.innerHeight, 100, 1 << 30);
renderer = new THREE.WebGLRenderer({
context: gl, // 地图的 gl 上下文
});
renderer.autoClear = false;
scene = new THREE.Scene();
// 环境光照和平行光
let aLight = new THREE.AmbientLight(0xffffff, 2);
let dLight = new THREE.DirectionalLight(0xffffff);
dLight.intensity = 6;
dLight.position.set(centerCoord[0], -100000, 50000);
dLight.target.position.set(centerCoord[0], centerCoord[1], 0);
dLight.target.updateMatrixWorld();
scene.add(dLight);
scene.add(aLight);
// 辅助
// const helper = new THREE.DirectionalLightHelper(dLight, 1);
// scene.add(helper);
// 辅助坐标轴
// const axesHelper = new THREE.AxesHelper(100000);
// scene.add(axesHelper);
// 创建遮罩区域,遮罩区域 = outer - inner
const maskShape = new THREE.Shape();
// 创建outer
maskShape.moveTo(outerCoords[0].coord[0], outerCoords[0].coord[1]);
outerCoords.forEach((item) => {
maskShape.lineTo(item.coord[0], item.coord[1]);
});
// 创建inner
maskShape.holes.push(new THREE.Path());
maskShape.holes[0].moveTo(innerCoords[0].coord[0], innerCoords[0].coord[1]);
innerCoords.forEach((item) => {
maskShape.holes[0].lineTo(item.coord[0], item.coord[1]);
});
// 信息添加给多边形
const maskGeometry = new THREE.ShapeGeometry(maskShape);
// 调整纹理坐标 这里是调整贴图偏移和大小
bgTexture.wrapS = THREE.RepeatWrapping;
bgTexture.wrapT = THREE.RepeatWrapping;
bgTexture.repeat.set(120, 120);
bgTexture.offset.set(0.877, 0.275);
//这里是调整法线贴图的
normalTexture.wrapS = THREE.RepeatWrapping;
normalTexture.wrapT = THREE.RepeatWrapping;
normalTexture.repeat.set(120, 120);
normalTexture.offset.set(0.877, 0.275);
// 调整 UV
maskGeometry.computeBoundingBox();
const bbox = maskGeometry.boundingBox;
let size1 = new THREE.Vector2();
bbox.getSize(size1);
const uvAttribute = maskGeometry.attributes.uv;
for (let i = 0; i < uvAttribute.count; i++) {
const uv = new THREE.Vector2().fromBufferAttribute(uvAttribute, i);
uv.x = (uv.x - bbox.min.x) / size1.x; // uv坐标映射
uv.y = (uv.y - bbox.min.y) / size1.y;
uvAttribute.setXY(i, uv.x, uv.y);
}
uvAttribute.needsUpdate = true;
const maskMaterial = new THREE.MeshStandardMaterial({
map: bgTexture,
side: THREE.DoubleSide,
transparent: true,
normalMap: normalTexture,
roughness: 0.5,
metalness: 0.5,
});
const maskMesh = new THREE.Mesh(maskGeometry, maskMaterial);
scene.add(maskMesh);
},
render: () => {
let { 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);
renderer.resetState();
},
});
map.add(glLayer);
const animate = () => {
map.render();
requestAnimationFrame(animate);
};
animate();
function onWindowResize() {
camera.aspect = size.innerWidth / size.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(size.innerWidth, size.innerHeight);
}
window.addEventListener("resize", onWindowResize);
}
下面放出效果图。
因为加上了法线贴图所以他会像这样。
加入高德地图各种特效之后是这样的效果。
素材放在下面了,需要的朋友可以自取哦~
高德开放平台第二期实战案例,三等奖作品
作者:左文韬
仅代表作者个人观点