vue中使用高德地图自定义掩膜背景结合threejs

854 阅读4分钟

前言

众所周知一些奇葩领导提的需求总是花里胡哨,像传统的大屏项目都是典型例子,这里使用高德地图再适合不过了,高德有很多特效可以直接拿过来用,话不多说直接开撸

效果图

123.png

技术架构

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);
                                
                     }
		});
	

ok,我们这里就已经有了整个城市的轮廓

微信图片_20240919172258.png

我们的需求是自定义背景,首先我们需要获取到除了合肥市以外的地区,这里我们使用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);
	}

一次性写的代码有点多,那一步错的可以评论区问,我尽量回答,下面放出效果图

123.png

因为加上了法线贴图所以他会像这样

image.png

image.png

123123.png

加入了高德地图各种特效之后他是这样的效果

image.png

素材放在下面了

bg.png

bg_normal.png