效果图:
1、下载three.js及d3依赖,我这里使用的是three r153版本
npm i three
npm i d3
2、使用d3将地理坐标转换成坐标轴xy值,在地图区域及边界线加载到场景后,通过包围盒计算完整地图的max和min坐标,再根据max和min的值重新计算每个地图区域的uv坐标,如果不重新计算uv坐标会导致贴图显示异常
3.完整代码(json替换成自己的数据)
<template>
<div class="page" id="page" ref="page">
<div class="tooltip" ref="tooltip" v-show="show">
{{ selectedPointData.name }}
</div>
</div>
</template>
<script>
import * as THREE from "three";
import * as d3 from "d3";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
export default {
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
centerCoordinate: [117.13, 31.89], // 地图中心地理坐标
projection: null, // Mercator 投影
mapConfig: {
deep: 0.2, // 挤出的深度
},
boundaryLineArr: [], // 边界线
composer: "", // 后期处理
pointData: [
{
coordinates: [117.33, 31.79],
type: 1,
name: "合肥",
value: 100,
},
{
coordinates: [118.502, 31.684],
type: 1,
name: "马鞍山",
value: 100,
},
],
pointInstanceArr: [], // 坐标点实例
show: false, // 是否显示tooltip
selectedPointData: {}, // 选中的坐标点数据
};
},
mounted() {
this.init();
window.addEventListener("resize", () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
});
},
methods: {
init() {
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
document.querySelector("#page").appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(5, 5, 26);
this.camera.lookAt(0, 0, 0);
// let axesHelp = new THREE.AxesHelper(5);
// this.scene.add(axesHelp);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// 墨卡托投影转换
this.projection = d3
.geoMercator()
.center(this.centerCoordinate)
.translate([0, 0]); // 根据地球贴图做轻微调整
// 添加地图
this.addMap();
// 给地图边界线添加outline效果
this.setLineOutline();
// 添加灯光
let ambientLight = new THREE.AmbientLight(0xffffff, 1);
this.scene.add(ambientLight);
// 添加散点
this.setPoint();
// 设置光线投射
this.setRaycaster();
this.render();
},
render() {
this.renderer.render(this.scene, this.camera);
this.controls.update();
if (this.composer) this.composer.render();
requestAnimationFrame(this.render);
},
// 添加地图
addMap() {
// 加载地图背景
const backgroundTexture = new THREE.TextureLoader().load(
require("@/assets/images/map.png")
);
// 加载地图
let fileLoader = new THREE.FileLoader();
fileLoader.load("/anhui.json", (data) => {
// 添加地图及边界线
this.addMapGeometry(data);
// 重新计算地图uv坐标
let arr = [];
let box = new THREE.Box3();
for (let v of this.map.children) {
for (let v2 of v.children) {
// 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小
if (v2.geometry instanceof THREE.ExtrudeGeometry) {
arr.push(v2);
let itemBox = new THREE.Box3().setFromObject(v2);
box.union(itemBox);
}
}
}
var bboxMin = box.min;
var bboxMax = box.max;
// 计算UV的缩放比例
var uvScale = new THREE.Vector2(
1 / (bboxMax.x - bboxMin.x),
1 / (bboxMax.y - bboxMin.y)
);
for (let v of arr) {
let uvAttribute = v.geometry.getAttribute("uv");
for (let i = 0; i < uvAttribute.count; i++) {
let u = uvAttribute.getX(i);
let v = uvAttribute.getY(i);
// 将UV坐标进行归一化
let normalizedU = (u - bboxMin.x) * uvScale.x;
let normalizedV = (v - bboxMin.y) * uvScale.y;
// 更新UV坐标
uvAttribute.setXY(i, normalizedU, normalizedV);
}
// 更新几何体的UV属性
v.geometry.setAttribute("uv", uvAttribute);
v.material.map = backgroundTexture;
v.material.needsUpdate = true;
}
});
},
addMapGeometry(jsondata) {
// 初始化一个地图对象
this.map = new THREE.Object3D();
jsondata = JSON.parse(jsondata);
jsondata.features.forEach((elem) => {
// 定一个省份3D对象
const province = new THREE.Object3D();
// 每个的 坐标 数组
const coordinates = elem.geometry.coordinates;
if (elem.geometry.type === "MultiPolygon") {
// 循环坐标数组
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
this.drawItem(elem, polygon, province);
});
});
this.map.add(province);
} else if (elem.geometry.type === "Polygon") {
// 循环坐标数组
coordinates.forEach((polygon) => {
this.drawItem(elem, polygon, province);
});
this.map.add(province);
}
});
this.scene.add(this.map);
},
drawItem(elem, polygon, province) {
const shape = new THREE.Shape();
const pointsArray = new Array();
for (let i = 0; i < polygon.length; i++) {
const [x, y] = this.projection(polygon[i]);
if (i === 0) {
shape.moveTo(x, -y);
}
shape.lineTo(x, -y);
pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
}
let curve = new THREE.CatmullRomCurve3(pointsArray);
// 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做
var tubeGeometry = new THREE.TubeGeometry(
curve,
Math.floor(pointsArray.length),
0.02,
10
);
const extrudeSettings = {
depth: this.mapConfig.deep,
bevelEnabled: false, // 对挤出的形状应用是否斜角
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geometry.computeBoundingBox();
// 创建地图区域材质
let meshMaterial = new THREE.MeshStandardMaterial({
color: "#ffffff",
transparent: true,
opacity: 1,
});
// 创建地图边界线材质
let lineMaterial = new THREE.MeshBasicMaterial({
color: "#ceebf7",
});
const mesh = new THREE.Mesh(geometry, meshMaterial);
const line = new THREE.Mesh(tubeGeometry, lineMaterial);
// 将省份的属性 加进来
province.properties = elem.properties;
province.add(mesh);
this.boundaryLineArr.push(line);
province.add(line);
},
// 给地图边界线添加outline效果
setLineOutline() {
//设置光晕
this.composer = new EffectComposer(this.renderer); //效果组合器
//创建通道
let renderScene = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderScene);
let outlinePass = new OutlinePass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
this.scene,
this.camera,
this.boundaryLineArr
);
outlinePass.renderToScreen = true;
outlinePass.edgeGlow = 2; // 光晕效果
outlinePass.usePatternTexture = false;
outlinePass.edgeThickness = 10; // 边框宽度
outlinePass.edgeStrength = 1.5; // 光晕效果
outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
outlinePass.visibleEdgeColor.set("#1acdec");
outlinePass.hiddenEdgeColor.set("#1acdec");
this.composer.addPass(outlinePass);
},
// 添加散点
setPoint() {
let pointTexture = new THREE.TextureLoader().load(
require("@/assets/images/point.png")
);
for (let v of this.pointData) {
let [x, y] = this.projection(v.coordinates);
const sprite = new THREE.Sprite(
new THREE.SpriteMaterial({
map: pointTexture,
})
);
sprite.scale.set(0.7, 0.7, 1);
sprite.position.set(x, -y, this.mapConfig.deep + 0.5);
sprite.properties = v;
this.pointInstanceArr.push(sprite);
this.scene.add(sprite);
}
},
// 光线投射
setRaycaster() {
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
this.$refs.page.addEventListener("click", (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, this.camera);
const intersects = raycaster.intersectObjects(this.pointInstanceArr);
if (intersects && intersects.length > 0) {
let tooltip = this.$refs.tooltip;
tooltip.style.left = event.pageX + "px";
tooltip.style.top = event.pageY + "px";
this.selectedPointData = intersects[0].object.properties;
this.show = true;
} else {
this.selectedPointData = {};
this.show = false;
}
});
},
},
};
</script>
<style scoped lang="scss">
.page {
height: 100vh;
.tooltip {
position: absolute;
background-color: #fff;
padding: 10px;
border-radius: 8px;
}
}
</style>
4.所用图片
5. 问题解决
node_modules包里面找到three.module.js文件大概第7177行,加四行数据解决。
8.海岸群岛demo,去除三沙市案例
<template>
<div class="page" id="page" ref="page" style="width: 100% !important;height: 100% !important;border:0px solid red;">
<div class="tooltip" ref="tooltip" v-show="show">
{{ selectedPointData.name }}
</div>
</div>
</template>
<script>
import * as THREE from "three";
import * as d3 from "d3";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { toRaw } from '@vue/reactivity';
let scale;
export default {
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
centerCoordinate: [109.512, 18.252], // 地图中心地理坐标
projection: null, // Mercator 投影
mapConfig: {
deep: 0.2, // 挤出的深度
},
boundaryLineArr: [], // 边界线
composer: "", // 后期处理
pointData: [
{
coordinates: [109.512, 18.252], // 假设的三亚地理位置坐标,实际请核实
type: 1,
name: "三亚湾",
value: 100,
},
// {
// coordinates: [109.488, 18.225], // 另一个假设的三亚地理位置坐标,实际请根据具体地点调整
// type: 1,
// name: "天涯海角",
// value: 100,
// },
],
// pointData: [
// {
// coordinates: [117.33, 31.79],
// type: 1,
// name: "合肥",
// value: 100,
// },
// {
// coordinates: [118.502, 31.684],
// type: 1,
// name: "马鞍山",
// value: 100,
// },
// ],
pointInstanceArr: [], // 坐标点实例
show: false, // 是否显示tooltip
selectedPointData: {}, // 选中的坐标点数据
};
},
mounted() {
this.init();
window.addEventListener("resize", () => {
const width = document.querySelector(".page").offsetWidth
const height = document.querySelector(".page").offsetHeight;
this.renderer.setSize(width, height);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
});
},
methods: {
init() {
this.renderer = new THREE.WebGLRenderer();
const width = document.querySelector(".page").offsetWidth
const height = document.querySelector(".page").offsetHeight;
this.renderer.setSize(width, height);
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
document.querySelector("#page").appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.scene.background = new THREE.TextureLoader().load("../assets/images/bigBg.png");
// this.camera = new THREE.PerspectiveCamera(
// 45,
// window.innerWidth / window.innerHeight,
// 0.1,
// 1000
// );
// this.camera.position.set(5, 5, 26);
this.camera = new THREE.PerspectiveCamera(
9, // 减小fov使视野更窄,让物体看起来更大
window.innerWidth / window.innerHeight,
0.5,
1000
);
this.camera.position.set(3, 3, 100); // 缩短z轴距离,让相机离地图更近
this.camera.lookAt(0, 0, 0);
// let axesHelp = new THREE.AxesHelper(5);
// this.scene.add(axesHelp);
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// 墨卡托投影转换
this.projection = d3
.geoMercator()
.center(this.centerCoordinate)
.translate([0, 0]); // 根据地球贴图做轻微调整
// 添加地图
this.addMap();
// let mapObject = this.addMap();
// 调整地图对象的位置
// addMap().position.y -= 1; // 向下移动1个单位
// 给地图边界线添加outline效果
this.setLineOutline();
// 添加灯光
let ambientLight = new THREE.AmbientLight(0xffffff, 1);
this.scene.add(toRaw(ambientLight));
// 添加散点
this.setPoint();
// 设置光线投射
this.setRaycaster();
this.render();
},
render() {
this.renderer.render(toRaw(this.scene), this.camera);
this.controls.update();
if (this.composer) this.composer.render();
requestAnimationFrame(this.render);
},
// 添加地图
addMap() {
// 加载地图背景
const backgroundTexture = new THREE.TextureLoader().load(
require("@/assets/images/map.png")
// require("@/assets/images/bigBg.png")
);
// 加载地图
let fileLoader = new THREE.FileLoader();
fileLoader.load("/map.json", (data) => {
// 添加地图及边界线
this.addMapGeometry(data);
// 重新计算地图uv坐标
let arr = [];
let box = new THREE.Box3();
for (let v of this.map.children) {
for (let v2 of v.children) {
// 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小
if (v2.geometry instanceof THREE.ExtrudeGeometry) {
arr.push(v2);
let itemBox = new THREE.Box3().setFromObject(v2);
box.union(itemBox);
}
}
}
var bboxMin = box.min;
var bboxMax = box.max;
// 计算UV的缩放比例
var uvScale = new THREE.Vector2(
1 / (bboxMax.x - bboxMin.x),
1 / (bboxMax.y - bboxMin.y)
);
for (let v of arr) {
let uvAttribute = v.geometry.getAttribute("uv");
for (let i = 0; i < uvAttribute.count; i++) {
let u = uvAttribute.getX(i);
let v = uvAttribute.getY(i);
// 将UV坐标进行归一化
let normalizedU = (u - bboxMin.x) * uvScale.x;
let normalizedV = (v - bboxMin.y) * uvScale.y;
// 更新UV坐标
uvAttribute.setXY(i, normalizedU, normalizedV);
}
// 更新几何体的UV属性
v.geometry.setAttribute("uv", uvAttribute);
v.material.map = backgroundTexture;
v.material.needsUpdate = true;
}
});
},
addMapGeometry(jsondata) {
// 初始化一个地图对象
this.map = new THREE.Object3D();
jsondata = JSON.parse(jsondata);
jsondata.features.forEach((elem) => {
console.log(666,elem.properties)
// 新增: 根据省份名称或其他条件判断是否跳过当前元素(例如排除海南)
if (elem.properties.name === "三沙市") { // 假设properties中有name或adcode标识省份
console.log("跳过了海南诸岛的数据");
return; // 直接返回,跳过当前循环的这个省份
}
// 定一个省份3D对象
const province = new THREE.Object3D();
// 每个的 坐标 数组
const coordinates = elem.geometry.coordinates;
if (elem.geometry.type === "MultiPolygon") {
// 循环坐标数组
coordinates.forEach((multiPolygon) => {
multiPolygon.forEach((polygon) => {
this.drawItem(elem, polygon, province);
});
});
this.map.add(province);
} else if (elem.geometry.type === "Polygon") {
// 循环坐标数组
coordinates.forEach((polygon) => {
this.drawItem(elem, polygon, province);
});
this.map.add(province);
}
});
this.scene.add(this.map);
},
drawItem(elem, polygon, province) {
const shape = new THREE.Shape();
const pointsArray = new Array();
for (let i = 0; i < polygon.length; i++) {
const [x, y] = this.projection(polygon[i]);
if (i === 0) {
shape.moveTo(x, -y);
}
shape.lineTo(x, -y);
pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
}
let curve = new THREE.CatmullRomCurve3(pointsArray);
// 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做
var tubeGeometry = new THREE.TubeGeometry(
curve,
Math.floor(pointsArray.length),
0.02,
10
);
const extrudeSettings = {
depth: this.mapConfig.deep,
bevelEnabled: false, // 对挤出的形状应用是否斜角
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geometry.computeBoundingBox();
// 创建地图区域材质
let meshMaterial = new THREE.MeshStandardMaterial({
color: "#ffffff",
transparent: true,
opacity: 1,
});
// 创建地图边界线材质
let lineMaterial = new THREE.MeshBasicMaterial({
color: "#ceebf7",
});
const mesh = new THREE.Mesh(geometry, meshMaterial);
const line = new THREE.Mesh(tubeGeometry, lineMaterial);
// 将省份的属性 加进来
province.properties = elem.properties;
province.add(mesh);
this.boundaryLineArr.push(line);
province.add(line);
},
// 给地图边界线添加outline效果
setLineOutline() {
//设置光晕
this.composer = new EffectComposer(this.renderer); //效果组合器
//创建通道
let renderScene = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderScene);
let outlinePass = new OutlinePass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
this.scene,
this.camera,
this.boundaryLineArr
);
outlinePass.renderToScreen = true;
outlinePass.edgeGlow = 2; // 光晕效果
outlinePass.usePatternTexture = false;
outlinePass.edgeThickness = 10; // 边框宽度
outlinePass.edgeStrength = 1.5; // 光晕效果
outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
outlinePass.visibleEdgeColor.set("#1acdec");
outlinePass.hiddenEdgeColor.set("#1acdec");
this.composer.addPass(outlinePass);
},
// 添加散点
setPoint() {
let pointTexture = new THREE.TextureLoader().load(
require("@/assets/images/point.png")
);
for (let v of this.pointData) {
let [x, y] = this.projection(v.coordinates);
const sprite = new THREE.Sprite(
new THREE.SpriteMaterial({
map: pointTexture,
})
);
sprite.scale.set(1, 1, 1);
sprite.position.set(x, -y, this.mapConfig.deep + 0.5);
sprite.properties = v;
this.pointInstanceArr.push(sprite);
this.scene.add(sprite);
}
},
// 光线投射
setRaycaster() {
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
this.$refs.page.addEventListener("click", (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, this.camera);
const intersects = raycaster.intersectObjects(this.pointInstanceArr);
if (intersects && intersects.length > 0) {
let tooltip = this.$refs.tooltip;
tooltip.style.left = event.pageX + "px";
tooltip.style.top = event.pageY + "px";
this.selectedPointData = intersects[0].object.properties;
this.show = true;
} else {
this.selectedPointData = {};
this.show = false;
}
});
},
},
};
</script>
<style scoped>
.page {
height: 100vh;
/* background: url("../assets/images/bigBg.png") no-repeat top center;
background-size: 100% 100%; */
}
.tooltip {
position: absolute;
background-color: #fff;
padding: 10px;
border-radius: 8px;
}
</style>