当前位置: 首页 > article >正文

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>

 

http://www.kler.cn/a/419461.html

相关文章:

  • 【C语言】结构体嵌套
  • Go-MediatR:Go语言中的中介者模式
  • 芯片测试-RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度等
  • 鸿蒙开发:自定义一个任意位置弹出的Dialog
  • Pyside6-QTableView实战
  • 【和春笋一起学C++】OpenCV中数组和指针运用实例
  • 【Linux】线程安全与锁概念——自旋锁、读写锁
  • 三分钟快速掌握——Linux【vim】的使用及操作方法
  • Gooxi Eagle Stream 2U双路通用服务器:性能强劲 灵活扩展 稳定易用
  • 类型转换与IO流:C++世界的变形与交互之道
  • QT学习笔记-QStringList,QTimer
  • 生成树详解(STP、RSTP、MSTP)
  • 飞睿科技乐鑫一级代理商ESP32-C6 WiFi模块芯片闪耀 Apple WWDC 2024
  • Docker--Docker Image 实例操作
  • React Native中的Android环境搭建
  • 优先算法 —— 双指针系列 - 四数之和
  • Git操作学习
  • 【技巧】Mac上如何显示键盘和鼠标操作
  • Linux centOS 7 安装 rabbitMQ
  • Advanced Macro Techniques in C/C++: `#`, `##`, and Variadic Macros
  • vmware vsphere4---搭建Starwind iSCSI存储服务器(未成功)
  • 从零开始使用GOT-OCR2.0——多模态OCR项目:微调数据集构建 + 训练(解决训练报错,成功实验微调训练)
  • 光照贴图原理
  • 数据查找文件夹里Excel、Word文件
  • 继上一篇,设置弹框次数以及自适应图片弹框,部分机型(vivo)老手机不显示的问题
  • React 前端框架5