3d行政区划-中国地图
前言
技术调研:做底代码平台的3d行政区组件 写的demo
效果图:
实现的功能项
地标、打点、飞线、three.js 3d 中国地图的一些基础配置
补充
geo中国地图文件获取
其他项:包
"dependencies": {
"d3": "^7.9.0",
"d3-geo": "^3.1.0",
"three": "^0.142.0",
"vue": "^3.3.4"
},
源码
以下代码所需的图片可以在iconfont官网进行搜索,这里不做提供
<script setup>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import { Line2 } from 'three/examples/jsm/lines/Line2.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js';
import * as d3 from "d3";
// 渲染性能监测工具
const stats = new Stats();
document.body.appendChild(stats.dom);
// 初始化场景
const scene = new THREE.Scene();
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerHeight / window.innerHeight,
0.1,
100000
);
// 设置相机位置
camera.position.set(0, 0, 120);
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
scene.add(camera);
// // 加入辅助轴,帮助我们查看3维坐标轴
// const axesHelper = new THREE.AxesHelper(10);
// scene.add(axesHelper);
// 添加环境光和直射光
const light = new THREE.AmbientLight(0xffffff, 100); // soft white light
scene.add(light);
const directionalLight = new THREE.DirectionalLight(0xffffff, 100);
scene.add(directionalLight);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ alpha: false, antialias: true });
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器添加到body
document.body.appendChild(renderer.domElement);
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;
// 监听屏幕大小改变的变化,动态设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
const canvas = renderer.domElement;
// 构造生成三维物体对象
const map = new THREE.Object3D();
// 地图geojson的json文件得到的坐标点是经纬度数据,需要把它转为坐标数据,
// 这里使用插件d3 geoMercator()方法转换
// .center: 以北京经纬度坐标(116.23, 39.54)为中心点
// .translate 移动地图位置
const projection = d3.geoMercator().center([103.8, 35.0]).translate([0, 0, 0]);
const fileLoader = new THREE.FileLoader();
fileLoader.load("../public/中华人民共和国.geojson", (data) => {
const loader = new FontLoader();
loader.load("Alibaba PuHuiTi_Regular.json", (font) => {
const chinaJsonData = JSON.parse(data);
handleData(chinaJsonData, font);
})
})
/**
* 处理地图数据
* @param {Object} jsonData
*/
function handleData(jsonData, font) {
// createPlane()
// 全国信息
const features = jsonData.features;
features.forEach((feature) => {
// 单个省份 对象
const province = new THREE.Object3D();
// 地址
province.propertiesName = feature.properties.name;
const coordinates = feature.geometry.coordinates;
const color = "#ffffff"; //
if (feature.geometry.type === "MultiPolygon") {
// 多个,多边形
const text = drawText(feature.properties, projection, font)
if (text) {
province.add(text)
}
coordinates.forEach((coordinate) => {
// coordinate 多边形数据
coordinate.forEach((coord) => {
const mesh = drawExtrudeMesh(coord, 0x3a6877, projection, {
depth: 6, borderColor: 0xffffff, borderWidth: 0.1, code:feature.properties.code
});
// const line = drawLine(coord, color, projection);
// 唯一标识
mesh.name = feature.properties.name;
mesh.userData.originalColor = 0x3a6877; // 初始颜色
province.add(mesh);
// province.add(line);
});
});
}
if (feature.geometry.type === "Polygon") {
const text = drawText(feature.properties, projection, font)
if (text) {
province.add(text)
}
// 多边形
coordinates.forEach((coordinate) => {
const mesh = drawExtrudeMesh(coordinate, 0x3a6877, projection, {
depth: 6, borderColor: 0xffffff, borderWidth: 0.1, code: feature.properties.code||feature.properties.filename
});
// const line = drawLine(coordinate, color, projection);
// 唯一标识
mesh.userData.originalColor = 0x3a6877; // 初始颜色
mesh.name = feature.properties.name;
province.add(mesh);
// province.add(line);
});
}
map.add(province);
map.rotation.x = -Math.PI / 5;
});
scene.add(map);
createLine(projection)
}
function createPlane() {
var gridHelper = new THREE.GridHelper(300, 15, 0xff0000, 0x003333);
gridHelper.material.depthWrite = false; // 禁止写入深度缓冲区
gridHelper.material.depthTest = false;
gridHelper.position.y = -10.1
gridHelper.rotateX(Math.PI / 3);
scene.add(gridHelper);
// 创建一个平面作为地面
var geometry = new THREE.PlaneGeometry(310, 310);
var material = new THREE.MeshLambertMaterial({
color: 0xffffff, // 白色
transparent: true, // 开启透明效果
opacity: 0.1, // 设置透明度
side: THREE.DoubleSide, // 双面渲染
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
mesh.position.y = -10
mesh.rotateX(-Math.PI / 6);
}
function drawText(properties, projection, font) {
// console.log('properties', properties)
let center = properties.centroid || properties.center
let name = properties.name
const geometry = new TextGeometry(name, {
font,
depth: 0.1,
size: 1, // 文本大小,默认为100
width: 16,
height:0.2 // 文字厚度
});
geometry.computeBoundingBox(); // 计算包围盒
const boundingBox = geometry.boundingBox;
const xOffset = (boundingBox.max.x - boundingBox.min.x) / 2; // X 方向偏移
const yOffset = (boundingBox.max.y - boundingBox.min.y) / 2; // Y 方向偏移
geometry.translate(-xOffset, -yOffset, 0);
const textMaterial = new THREE.MeshBasicMaterial({
color: 0x00fff0||0xb0c4ca,
});
const mesh = new THREE.Mesh(geometry, textMaterial);
if (center) {
let [x, y] = projection(center)
mesh.position.set(x, -y, 6.5);
return mesh
}
return null
}
/**
* 根据经纬度坐标生成几何物体
* @param {Array} polygon 经纬度坐标数据
* @param {string} color 物体颜色
* @param {Function} projectionFun 经纬度转平面坐标函数
* @returns THREE.Mesh
*/
function drawExtrudeMesh(polygon, color, projectionFun, options = {}) {
const { depth = 6, borderColor = 0xffffff, borderWidth = 0.1 } = options;
// 创建形状
const shape = new THREE.Shape();
polygon.forEach((row, i) => {
const [x, y] = projectionFun(row);
if (i === 0) {
shape.moveTo(x, -y);
}
shape.lineTo(x, -y);
});
// 挤压缓冲几何体
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: depth, // 这里是高度参数
bevelEnabled: true,
bevelSegments: 10,
steps: 1, // 用于分段的数量,默认 1
bevelSize: 0.5, // 倒角大小
bevelThickness: 0.5 // 倒角深度
});
// 材质
const randomColor = (Math.random() * 0.5 + 0.5) * 0xffffff;
const material = new THREE.MeshBasicMaterial({
color: 0x3a6877 || randomColor,
transparent: true,
opacity: 0.5,
});
const mesh = new THREE.Mesh(geometry, material);
// 添加边界
const edges = new THREE.EdgesGeometry(geometry);
const lineMaterial = new THREE.LineBasicMaterial({
color: borderColor, // 边界颜色
linewidth: borderWidth, // 边界线宽度
});
const edgeLines = new THREE.LineSegments(edges, lineMaterial);
mesh.add(edgeLines);
mesh.userData = {
originalColor: color,
fillMaterial: material, // 默认材质
originalBorderMaterial: lineMaterial, // 保存默认材质
edgeLines: edgeLines, // 保存边界线
};
return mesh;
}
/**
* 根据坐标点一条连续的线
* @param {*} polygon 经纬度坐标数据
* @param {*} color 线的颜色
* @param {*} projectionFun 经纬度转平面坐标函数
* @returns THREE.Line
*/
function drawLine(polygon, color, projectionFun) {
const lineGeometry = new THREE.BufferGeometry();
const pointsArr = [];
polygon.forEach((row) => {
const [x, y] = projectionFun(row);
// 创建三维点
pointsArr.push(new THREE.Vector3(x, -y, 6.4));
});
// console.log(pointsArr, 'pointsArr')
// 放入多个点
lineGeometry.setFromPoints(pointsArr);
// 生成随机颜色
const lineColor = new THREE.Color(
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5
);
const lineMaterial = new THREE.LineBasicMaterial({
color: '#ffffff',
linewidth: 200
});
return new THREE.Line(lineGeometry, lineMaterial);
}
// 地标
const rippleMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0.0 }, // 时间
uColor: { value: new THREE.Color(0xff00ff) }, // 气泡颜色
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float dist = length(vUv - 0.5); // 计算像素到中心的距离
float ripple = sin(dist * 10.0 - uTime) * 0.5 + 0.5; // 波纹效果
gl_FragColor = vec4(uColor, ripple * (1.0 - 0.0));
}
`,
transparent: true, // 使气泡透明
});
let pos = [[116.23, 39.54], [117.2857, 31.8612]]
for (let p of pos) {
const geometry = new THREE.CircleGeometry(3, 64); // 半径1,分段64
const ripple = new THREE.Mesh(geometry, rippleMaterial);
const [x, y] = projection(p);
console.log(x, y, 'xy')
ripple.position.set(x, -y, 7)
map.add(ripple);
}
// 创建圆形波纹的几何体 引入图片 打点
let pos2 = [
[116.4074, 39.9042], // 北京
[117.3616, 39.3434], // 天津
[112.9834, 28.1127], // 湖南
[113.4244, 23.3417], // 广东
[108.7880, 23.8298], // 广西
[110.3492, 20.0174], // 海南
[104.0665, 30.5723], // 四川
];
for (let p of pos2) {
const texture = new THREE.TextureLoader().load('../public/坐标-fill.png');
const material = new THREE.MeshBasicMaterial({ map: texture, color: 0x7777ff, transparent: true, side: THREE.DoubleSide });
const plane = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material);
const [x, y] = projection(p);
plane.position.set(x, -y, 8.5);
plane.rotateX(Math.PI / 2) // 将平面旋转到水平位置
// plane.rotateY(Math.PI / 4); // 旋转 Y 轴,增加立体感
plane.lookAt(camera.position);
// plane.renderOrder = 1;
map.add(plane);
}
// 创建圆形波纹的几何体
function getPoint(startPoint, endPoint, projection) {
// const startPoint = [116.4074, 39.9042]
// const endPoint = [87.6177, 43.7928]
let [sx, sy] = projection(startPoint)
let [ex, ey] = projection(endPoint)
// 定义起始点和终止点
const sp = new THREE.Vector3(sx, -sy, 7.5);
const ep = new THREE.Vector3(ex, -ey, 7.5);
return [sp, ep]
}
let curvature = 0.3;
const curveList = []
function createCurve(endPoint, startPoint) {
// 计算起点到终点的方向向量和距离
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize(); // 单位方向向量
const distance = startPoint.distanceTo(endPoint); // 两点间距离
// 计算垂直于方向向量的偏移向量(用来确定控制点)
// const perpendicular = new THREE.Vector3(-direction.y, direction.x, 0).normalize();
// 计算控制点位置:沿垂直方向偏移,偏移量与距离和曲率相关
const controlPoint = new THREE.Vector3().addVectors(
startPoint.clone().add(endPoint).multiplyScalar(0.5), // 中点
new THREE.Vector3(0, 0, curvature * distance)
// perpendicular.multiplyScalar(curvature * distance * 0.5) // 曲率控制的偏移量
);
// 创建二次贝塞尔曲线
const curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint);
const points = curve.getPoints(500);
if (points.some(point => isNaN(point.x) || isNaN(point.y) || isNaN(point.z))) {
console.error("生成的点数组包含无效值");
}
return {
curve,
points
};
}
function createLine(projection) {
let pointList = [[[116.4074, 39.9042], [117.2857, 31.8612]], [[110.3492, 20.0174], [117.2857, 31.8612]],
[[87.6177, 43.7928], [117.2857, 31.8612]], [[91.1164, 29.6500], [117.2857, 31.8612]],
[[126.6617, 45.7421], [117.2857, 31.8612]], [[120.0934, 29.1828], [117.2857, 31.8612]], [[102.7097, 25.0453], [117.2857, 31.8612]]
]
for (let point of pointList) {
let [sp, ep] = point
let [startPoint, endPoint] = getPoint(sp, ep, projection)
const texture = new THREE.TextureLoader().load('飞机-客机.png');
texture.center.set(0.5, 0.5);
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide });
const planeMesh = new THREE.Mesh(new THREE.PlaneGeometry(6, 6), material);
planeMesh.position.set(endPoint.x, endPoint.y, 8.5);
map.add(planeMesh);
// 创建线材质和几何体
const lineMaterial = new LineMaterial({
color: 0xffffff,
linewidth: 4, // 设置线条宽度
opacity: 0,
transparent: true,
depthWrite: false,
depthTest: false
});
lineMaterial.needsUpdate = true;
lineMaterial.resolution.set(window.innerWidth, window.innerHeight);
// 创建线对象
const lineGeometry = new LineGeometry();
let { points, curve } = createCurve(startPoint, endPoint);
let plane = createFlyLine(points)
curveList.push({
points,
curve,
plane,
planeMesh
})
let pr = []
for (let p of points) {
pr.push(p.x, p.y, p.z)
}
lineGeometry.setPositions(pr); // 设置点的位置
const line = new Line2(lineGeometry, lineMaterial);
line.computeLineDistances(); // 计算线段的距离
line.renderOrder = 1;
map.add(line);
}
}
const uniforms = {
// uTime: { value: 0.0 },
// uSColor: { value: new THREE.Color(0xff0000) },
// uEColor: { value: new THREE.Color(0xffffff) },
// uWidth: { value: 5.0 },
// uSpeed: { value: 1 },
// uLength: { value: 0.8 }
uTime: { value: 0.0 }, // 用于控制时间的 uniform
uScale: { value: 1.0 }, // 点的缩放比例
uColor: { value: new THREE.Color(0xffffff) }, // 颜色数组
};
const commonUniforms = {
u_time: {
value: 0.0
}
}
function initLineMaterial(setting) {
let number = setting ? (Number(setting.number) || 1.0) : 4.0; // 在一个路径中同时存在的个数
let speed = setting ? (Number(setting.speed) || 1.0) : 0.6;// 速度约大越快
let length = setting ? (Number(setting.length) || 0.5) : 0.3;// 单根线的长度0-1之间1代表全满
let size = setting ? (Number(setting.size) || 3.0) : 6.0;// 在最大的地方的大小 默认为3像素
let color = setting ? setting.color || new THREE.Vector3(0, 1, 1) : new THREE.Vector3(0, 1, 1);// 颜色此处以Vector3的方式传入分别为RBG值 都是0-1的范围
let singleUniforms = {
u_time: commonUniforms.u_time,
number: { type: 'f', value: number },
speed: { type: 'f', value: speed },
length: { type: 'f', value: length },
size: { type: 'f', value: size },
color: { type: 'v3', value: color },
initialOpacity: { value: 1 }
};
let lineMaterial = new THREE.ShaderMaterial({
uniforms: singleUniforms,
vertexShader: `
varying vec2 vUv;
attribute float percent;
varying float opacity;
uniform float u_time;
uniform float number;
uniform float speed;
uniform float length;
uniform float size;
void main()
{
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
float l = clamp(1.0-length,0.0,1.0);//空白部分长度占比
gl_PointSize = clamp(fract(percent*number + l - u_time*number*speed)-l ,0.0,1.) * size * (1./length);
opacity = gl_PointSize/size;
gl_Position = projectionMatrix * mvPosition;
}
`,//顶点着色器部分
fragmentShader: `
precision mediump float;
varying float opacity;
uniform vec3 color;
uniform float initialOpacity;
void main(){
if(opacity <=0.1){
discard;
}
gl_FragColor = vec4(color,1.0);
}
`,// 片元着色器部分
transparent: true,
blending:THREE.AdditiveBlending,
});
return lineMaterial;
}
function createFlyLine(points) {
var geometry = new THREE.BufferGeometry().setFromPoints(points);
let length = points.length;
var percents = new Float32Array(length);
for (let i = 0; i < points.length; i += 1) {
percents[i] = (i / length);
}
geometry.setAttribute('percent', new THREE.BufferAttribute(percents, 1));
let lineMaterial = initLineMaterial({
color: new THREE.Color(0xffffff),
});
var flyLine = new THREE.Points(geometry, lineMaterial);
// let euler = new THREE.Euler(Math.random() * Math.PI, Math.random() * Math.PI, 0);
// flyLine.setRotationFromEuler(euler);
map.add(flyLine)
}
let t = 0
function animate(time) {
controls.update();
stats.update();
if(rippleMaterial&&rippleMaterial.uniforms){
rippleMaterial.uniforms.uTime.value += 0.1;
}
requestAnimationFrame(animate);
// console.log(uniforms, 'uniforms')
const speed = 0.001
for (let obj of curveList) {
// obj.planeMesh.uTime.value += 0.001;
const curve = obj.curve;
const position = curve.getPointAt(t);
const tangent = curve.getTangentAt(t).normalize();
const axis = new THREE.Vector3(0, 1, 0);
const quaternion = new THREE.Quaternion().setFromUnitVectors(axis, tangent);
// planeMesh.position.copy(position); // 更新位置
obj.planeMesh.lookAt(position.clone().add(tangent)); // 设置朝向
obj.planeMesh.position.set(position.x, position.y, position.z + 0.2)
obj.planeMesh.quaternion.copy(quaternion);
}
t += speed;
if (t > 1) t = 0;
// uniforms.uTime.value += 0.01;
if(commonUniforms&&commonUniforms.u_time){
commonUniforms.u_time.value += 0.01;
}
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}
animate();
</script>