基于three.js的3D地图
1.引言
基于Vue实现Three.js+GeoJSON三维可视化炫酷地图(省市标签、地图流光、动态文字、高光定位、星空背景)
three官方文档:Three.js – JavaScript 3D Library
2.实际效果
您的浏览器不支持播放该视频!
3.GeoJSON
可以通过阿里云DataV获取GeoJSON数据,也可以在其他地理信息平台获取数据并转换为GeoJson数据:DataV.GeoAtlas地理小工具系列 (aliyun.com)
4.基础代码
4.1 加载解析GeoJSON数据
import beijing from './json/beijing.json'//包含子区域
import beijingout from './json/beijingout.json'//不包含子区域
/**
* @description 解析GeoJson数据并创建地图
*/
initGeoJson() {
// 引入json数据
// 方法一 使用Three提供的FileLoader加载数据并对JSON数据进行解析:
const loader = new THREE.FileLoader();
loader.load("https://geo.datav.aliyun.com/areas_v3/bound/110000.json", (data) => {
console.log(data);
const jsonData = JSON.parse(data);
this.initMap(jsonData) //初始化加载地图
this.initMapLine(beijing)//加载地图边缘线
console.log('jsonData :>> ', jsonData);
});
// 方法二 通过import引入JSON文件再调用
// this.initMap(beijingout)//初始化加载地图
// this.initMapLine(beijing)//加载地图边缘线
},
4.2 加载地图
initMap(beijingOutJson) {
// 创建环境贴图
let textureMap = textureLoader.load(require("./mapimg/gz-map.jpeg"));
let texturefxMap = textureLoader.load(
require("./mapimg/gz-map-fx.jpeg")
);
textureMap.wrapS = THREE.RepeatWrapping; //纹理水平方向的平铺方式
textureMap.wrapT = THREE.RepeatWrapping; //纹理垂直方向的平铺方式
textureMap.flipY = texturefxMap.flipY = false;// 如果设置为true,纹理在上传到GPU的时候会进行纵向的翻转。默认值为true。
textureMap.rotation = texturefxMap.rotation = THREE.MathUtils.degToRad(45);//rotation纹理将围绕中心点旋转多少度
const scale = 0.01;
textureMap.repeat.set(scale, scale);//repeat决定纹理在表面的重复次数
texturefxMap.repeat.set(scale, scale);
textureMap.offset.set(0.5, 0.5);//offset贴图单次重复中的起始偏移量
texturefxMap.offset.set(0.5, 0.5);
// MeshPhongMaterial(一种用于具有镜面高光的光泽表面的材质)
const material = new THREE.MeshPhongMaterial({
map: textureMap,//颜色贴图
normalMap: texturefxMap,//用于创建法线贴图的纹理
// normalScale: new THREE.Vector2(12.2, 2.2),//法线贴图对材质的影响程度
color: "#7bc6c2",
combine: THREE.MultiplyOperation,//如何将表面颜色的结果与环境贴图
transparent: true,
opacity: 1,
});
// MeshLambertMaterial(一种非光泽表面的材质,没有镜面高光)
const material1 = new THREE.MeshLambertMaterial({
color: 0x123024,
transparent: true,
opacity: 0.9,
});
// d3-geo墨卡托坐标转化
const projection = d3
.geoMercator()//地图投影方式(用于绘制球形墨卡托投影)
.center(centerPos)//地图中心点经纬度坐标
.scale(2500) //缩放
.translate([0, 0]);//移动地图位置
console.log('beijingOutJson.features :>> ', beijingOutJson.features);
// 遍历省份构建模型
beijingOutJson.features.forEach((elem) => {
// 新建一个省份容器:用来存放省份对应的模型和轮廓线
const meshArrs = new THREE.Object3D();
const coordinates = elem.geometry.coordinates;//坐标合集
const properties = elem.properties;
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
//Shape使用路径以及可选的孔洞来定义一个二维形状平面
// 创建一条空路径,.currentPoint将被设置为原点。
const shape = new THREE.Shape();
var v3ps = [];
for (let i = 0; i < polygon.length; i++) {
const [x, y] = projection(polygon[i]);
if (i === 0) {
shape.moveTo(x, -y);//将.currentPoint移动到x, y
}
shape.lineTo(x, -y);//在当前路径上,从.currentPoint连接一条直线到x,y
v3ps.push(new THREE.Vector3(x, -y, 4.02));
}
const extrudeSettings = {
depth: 2, //该属性指定图形可以拉伸多高,默认值是100
bevelEnabled: false, //是否给这个形状加斜面,默认加斜面。
};
//拉升成地图(从一个形状路径中,挤压出一个BufferGeometry)
// ExtrudeGeometry --- 当使用这个几何体创建Mesh的时候,如果你希望分别对它的表面和它挤出的侧面使用单独的材质,你可以使用一个材质数组。 第一个材质将用于其表面;第二个材质则将用于其挤压出的侧面。
const geometry = new THREE.ExtrudeGeometry(
shape,
extrudeSettings
);
const mesh = new THREE.Mesh(geometry, [
material,//表面材质
material1,//侧面材质
]);
mesh.rotateX(-Math.PI / 2);//x轴旋转
mesh.position.set(0, 1.5, -3);//设置放置位置
meshArrs.add(mesh);
});
});
map.add(meshArrs);
});
this.scene.add(map);
},
4.3 加载地图边缘线
initMapLine(beijingJson) {
var matLine = new LineMaterial({
color: 0xffffff,
linewidth: 0.0013,
vertexColors: true,
dashed: false,
alphaToCoverage: true,
});
var matLine2 = new LineMaterial({
color: "#01bdc2",
linewidth: 0.0025,
vertexColors: true,
dashed: false,
alphaToCoverage: true,
});
// d3-geo转化坐标
const projection = d3
.geoMercator()
.center(centerPos)
.scale(2500)
.translate([0, 0]);
// 遍历省份构建模型
beijingJson.features.forEach((elem) => {
const province = new THREE.Object3D();
const coordinates = elem.geometry.coordinates;
const properties = elem.properties;
//这里创建光柱、文字坐标
this.initLightPoint(properties, projection);
// 创建地图上面和下面的边缘线
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
const positions = [];
var colors = [];
const color = new THREE.Color();
var linGeometry = new LineGeometry();
for (let i = 0; i < polygon.length; i += 1) {
const [x, y] = projection(polygon[i]);
positions.push(x, -y, 4.01);
color.setHSL(1, 1, 1);
colors.push(color.r, color.g, color.b);
}
// Line2方式绘制线条
linGeometry.setPositions(positions);
linGeometry.setColors(colors);
const line = new Line2(linGeometry, matLine);
const line2 = new Line2(linGeometry, matLine2);
line.computeLineDistances();
line.rotateX(-Math.PI / 2);
line2.rotateX(-Math.PI / 2);
line.position.set(0, 0.1, -3);
line2.position.set(0, -3.5, -3);
line2.computeLineDistances();
line.scale.set(1, 1, 1);
province.add(line);
province.add(line2);
});
});
map.add(province);
});
this.scene.add(map);
},
4.4 创建光柱、文字坐标
/**
* @description 创建光柱、文字坐标
* @param {*} properties 属性、详情
* @param {Function} projection d3-geo转化坐标
*/
initLightPoint(properties, projection) {
// 创建光柱
let heightScaleFactor = 8 + this.random(1, 5) / 5;
let lightCenter = properties.centroid || properties.center;
let areaName = properties.name;
// projection用来把经纬度转换成坐标
const [x, y] = projection(lightCenter);
let light = this.createLightPillar(x, y, heightScaleFactor);
light.position.z -= 3;
map.add(light);
//这里创建文字坐标
this.createTextPoint(x, y, areaName);
}
/**
* @description 创建文字坐标
* @param {*} x d3 - 经纬度转换后的x轴坐标
* @param {*} z d3 - 经纬度转换后的z轴坐标
* @param {*} areaName 地名
*/
createTextPoint(x, z, areaName) {
let tag = document.createElement("div");
tag.innerHTML = name;
// tag.className = className
tag.style.color = "#fff";
// tag.style.pointerEvents = "none";
tag.style.pointerEvents = "auto";
// tag.style.visibility = 'hidden'
tag.style.position = "absolute";
// tag.setAttribute('data-name', 'label');
// tag.addEventListener('click', this.clickLabel, false);
tag.addEventListener('mousedown', this.clickLabel, false);// 有时候PC端click事件不生效,不知道什么原因,就使用mousedown事件
tag.addEventListener('touchstart', this.clickLabel, false);
let label = new CSS2DObject(tag);
label.element.innerHTML = areaName;
label.element.style.visibility = "visible";
label.position.set(x, 5, z);
label.position.z -= 3;
this.scene.add(label)
}
/**
* @description 文字坐标点击
* @param {*} e
*/
clickLabel(e) {
console.log('e :>> ', e);
}
/**
* @description // 创建光柱
* @param {*} x d3 - 经纬度转换后的x轴坐标
* @param {*} z d3 - 经纬度转换后的z轴坐标
* @param {*} heightScaleFactor
*/
createLightPillar(x, z, heightScaleFactor = 1) {
let group = new THREE.Group();
// 柱体高度
const height = heightScaleFactor;
// 柱体的geo,6.19=柱体图片高度/宽度的倍数
const geometry = new THREE.PlaneGeometry(
height / 6.219,
height
);
// 柱体旋转90度,垂直于Y轴
// geometry.rotateX(Math.PI / 2)
// 柱体的z轴移动高度一半对齐中心点
geometry.translate(0, height / 2, 0);
// 柱子材质
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(require("./mapimg/光柱.png")),
color: 0x00ffff,
transparent: true,
depthWrite: false,
// depthTest:false,
side: THREE.DoubleSide,
});
// 光柱01
let light01 = new THREE.Mesh(geometry, material);
light01.renderOrder = 2;
light01.name = "createLightPillar01";
// 光柱02:复制光柱01
let light02 = light01.clone();
light02.renderOrder = 2;
light02.name = "createLightPillar02";
// 光柱02,旋转90°,跟 光柱01交叉
light02.rotateY(Math.PI / 2);
// 创建底部标点
const bottomMesh = this.createPointMesh(1.5);
// 创建光圈
const lightHalo = this.createLightHalo(1.5);
WaveMeshArr.push(lightHalo);
// 将光柱和标点添加到组里
group.add(bottomMesh, lightHalo, light01, light02);
// 设置组对象的姿态
// group = setMeshQuaternion(group, R, lon, lat)
group.position.set(x, 4.01, z);
return group;
},
4.5 创建底部标点
/**
* @description 创建底部标点
* @param {number} size 缩放大小
*/
createPointMesh(size) {
// 标记点:几何体,材质,
const geometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(require("./mapimg/标注.png")),
color: 0x00ffff,
side: THREE.DoubleSide,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
});
let mesh = new THREE.Mesh(geometry, material);
mesh.renderOrder = 2;
mesh.rotation.x = Math.PI / 2;
mesh.name = "createPointMesh";
// 缩放
const scale = 1 * size;
mesh.scale.set(scale, scale, scale);
return mesh;
}
/**
* @description 创建底部标点的光圈
* @param {number} size 缩放大小
*/
createLightHalo(size) {
// 标记点:几何体,材质,
const geometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({
map: textureLoader.load(require("./mapimg/标注光圈.png")),
color: 0x00ffff,
side: THREE.DoubleSide,
opacity: 0,
transparent: true,
depthWrite: false, //禁止写入深度缓冲区数据
});
let mesh = new THREE.Mesh(geometry, material);
mesh.renderOrder = 2;
mesh.name = "createLightHalo";
mesh.rotation.x = Math.PI / 2;
// 缩放
const scale = 1.5 * size;
mesh.size = scale; //自顶一个属性,表示mesh静态大小
mesh.scale.set(scale, scale, scale);
return mesh;
},
4.6 添加底部旋转背景
initFloor() {
const geometry = new THREE.PlaneGeometry(400, 400);
let texture = textureLoader.load(require("./mapimg/地板背景.png"));
const material = new THREE.MeshPhongMaterial({
color: 0xffffff,
map: texture,
// emissive:0xffffff,
// emissiveMap:Texture,
transparent: true,
opacity: 1,
depthTest: true,
// roughness:1,
// metalness:0,
depthWrite: false,
// side: THREE.DoubleSide
});
let plane = new THREE.Mesh(geometry, material);
plane.rotateX(-Math.PI / 2);
this.scene.add(plane);
let rotatingApertureTexture = textureLoader.load(
require("./mapimg/rotatingAperture.png")
);
let rotatingApertureerial = new THREE.MeshBasicMaterial({
map: rotatingApertureTexture,
transparent: true,
opacity: 1,
depthTest: true,
depthWrite: false,
});
let rotatingApertureGeometry = new THREE.PlaneGeometry(
100,
100
);
rotatingApertureMesh = new THREE.Mesh(
rotatingApertureGeometry,
rotatingApertureerial
);
rotatingApertureMesh.rotateX(-Math.PI / 2);
rotatingApertureMesh.position.y = 0.02;
rotatingApertureMesh.scale.set(1.2, 1.2, 1.2);
this.scene.add(rotatingApertureMesh);
let rotatingPointTexture = textureLoader.load(
require("./mapimg/rotating-point2.png")
);
let material2 = new THREE.MeshBasicMaterial({
map: rotatingPointTexture,
transparent: true,
opacity: 1,
depthTest: true,
depthWrite: false,
});
rotatingPointMesh = new THREE.Mesh(
rotatingApertureGeometry,
material2
);
rotatingPointMesh.rotateX(-Math.PI / 2);
rotatingPointMesh.position.y = 0.04;
rotatingPointMesh.scale.set(1, 1, 1);
this.scene.add(rotatingPointMesh);
let circlePoint = textureLoader.load(require("./mapimg/circle-point.png"));
let material3 = new THREE.MeshPhongMaterial({
color: 0x00ffff,
map: circlePoint,
transparent: true,
opacity: 1,
depthWrite: false,
// depthTest: false,
});
let plane3 = new THREE.PlaneGeometry(120, 120);
let mesh3 = new THREE.Mesh(plane3, material3);
mesh3.rotateX(-Math.PI / 2);
mesh3.position.y = 0.06;
this.scene.add(mesh3);
},
5.源码
gitee3Dthree地图源码
借鉴:https://space.bilibili.com/323405428