学习:threejs案例—大屏3D地图可视化

一、Vite搭建Vue,

vue3

复制代码
npm create vite@latest

# 安装依赖
npm install

threejs

复制代码
npm install three

创建twin文件夹

CreateTwin.js

javascript 复制代码
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

export default class CreateTwin {
  constructor() {
    //场景
    this.scene = new THREE.Scene();

    //辅助观察的坐标系
    this.axesHelper = new THREE.AxesHelper(1000);
    this.scene.add(this.axesHelper);

    //相机
    const width = window.innerWidth;
    const height = window.innerHeight;
    this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 3000);
    this.camera.position.set(292, 223, 185);
    this.camera.lookAt(0, 0, 0);

    // WebGL渲染器设置
    this.renderer = new THREE.WebGLRenderer({
      antialias: true, //开启优化锯齿
    });
    this.renderer.setPixelRatio(window.devicePixelRatio); //防止输出模糊
    this.renderer.setSize(width, height);
    document.body.appendChild(this.renderer.domElement);
    //创建一个GLTF加载器
    this.loader = new GLTFLoader();

    //光源设置
    // 平行光
    this.directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    this.directionalLight.position.set(100, 60, 50);
    this.scene.add(this.directionalLight);
    //环境光
    this.ambient = new THREE.AmbientLight(0xffffff, 0.4);
    this.scene.add(this.ambient);

    // 渲染循环
    this.renderer.setAnimationLoop(() => {
      this.renderer.render(this.scene, this.camera);
    });
    // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.target.set(0, 0, 0);
    this.controls.update();

    // 画布尺寸随着窗口变化
    window.addEventListener("resize", () => {
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
    });
  }
}

twin.js

javascript 复制代码
import CreateTwin from "../twin/CreateTwin";
let twin = null;

export function initTwin() {
  if (twin) {
    // 清理旧的实例
    disposeTwin();
  }
  twin = new CreateTwin();
  return twin;
}

export function disposeTwin() {
  if (twin && twin.renderer) {
    twin.renderer.dispose();
    if (twin.renderer.domElement) {
      twin.renderer.domElement.remove();
    }
  }
  twin = null;
}

export default initTwin;

App.vue

javascript 复制代码
<script setup>
import { onMounted, onUnmounted } from "vue";
import initTwin, { disposeTwin } from "./twin/twin";
let twin = null;
onMounted(() => {
  twin = initTwin();
    twin.loader.load("/工厂.glb", function (gltf) {
  twin.scene.add(gltf.scene);
    });

});


onUnmounted(() => {
  disposeTwin();
});
</script>

<template></template>

<style scoped></style>

public放入工厂.glb

二、解析边界线(几何体顶点和Line模型)

回顾9-18.几何体顶点颜色数数据

javascript 复制代码
// 引入three.js
import * as THREE from 'three';

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 25, 0, //顶点3坐标
]);
// 顶点位置数据
geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);

//类型数组创建顶点颜色color数据
const colors = new Float32Array([
    1, 0, 0, //顶点1颜色 
    0, 0, 1, //顶点2颜色
    0, 1, 0, //顶点3颜色
]);
// 设置几何体attributes属性的颜色color属性
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3); 


// 线模型渲染几何体顶点颜色,可以看到直线颜色渐变效果
const material = new THREE.LineBasicMaterial({
    vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);

export default line;

解析中国地图边界

CreateTwin.js,china.json文件放入public文件夹中

javascript 复制代码
export default class CreateTwin {
  constructor() {
    //场景
    this.scene = new THREE.Scene();

    this.loader = new THREE.FileLoader();
    this.loader.setResponseType("json");

    this.mapGroup = new THREE.Group();
    this.scene.add(this.mapGroup);

    this.loader.load("./china.json", (data) => {
      data.features.forEach((area) => {
        // "Polygon":国家area有一个封闭轮廊
        if (area.geometry.type === "Polygon") {
          var pointArr = []; //边界线顶点坐标
          area.geometry.coordinates[0].forEach((elem) => {
            //z坐标设置为0.这样地图轮廓位于xoY平面上
            pointArr.push(elem[0], elem[1], 0);
          });
          this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
          //"MultiPolygon":国家area有多个封闭轮廓
        } else if (area.geometry.type === "MultiPolygon") {
          // 解析所有封闭轮廓边界坐标area.geometry.coordinates
          area.geometry.coordinates.forEach((polygon) => {
            var pointArr = []; //边界线顶点坐标
            polygon[0].forEach((elem) => {
              pointArr.push(elem[0], elem[1], 0);
            });
            this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
          });
        }
      });
    });
     
    //辅助观察的坐标系
        ...
  }

  createLine(pointArr) {
    // 创建线条几何体
    const geometry = new THREE.BufferGeometry();
    // 设置顶点位置
    const vertices = new Float32Array(pointArr);
    // 创建属性缓冲区对象
    geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
    // 创建线条材质
    const material = new THREE.LineBasicMaterial({
      color: 0x00ffff,
      transparent: true,
      opacity: 0.8,
    });
    const line = new THREE.Line(geometry, material);
    return line;
  }
}
javascript 复制代码
        //  遍历地图数据的每个区域
        // "Polygon":国家area有一个封闭轮廊
        if (area.geometry.type === "Polygon") {
          var pointArr = []; //边界线顶点坐标
          area.geometry.coordinates[0].forEach((elem) => {
            //z坐标设置为0.这样地图轮廓位于xoY平面上
            pointArr.push(elem[0], elem[1], 0);
          });
          this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
          //"MultiPolygon":国家area有多个封闭轮廓
        } else if (area.geometry.type === "MultiPolygon") {
          // 解析所有封闭轮廓边界坐标area.geometry.coordinates
          area.geometry.coordinates.forEach((polygon) => {
            var pointArr = []; //边界线顶点坐标
            polygon[0].forEach((elem) => {
              pointArr.push(elem[0], elem[1], 0);
            });
            this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
          });
        }

简写成

javascript 复制代码
        if (area.geometry.type === "Polygon") {
          area.geometry.coordinates = [area.geometry.coordinates];
        }
        this.mapGroup.add(this.createLine(area.geometry.coordinates));

createLine 方法需要调整,因为现在传入的是嵌套数组,GeoJSON 的坐标是 [经度, 纬度] 格式,需要转换为三维坐标 [x, y, z],createLine 方法需要补齐三维坐标

javascript 复制代码
createLine(polygons) {
    const group = new THREE.Group();
    polygons.forEach((polygon) => {
      const pointArr = polygon[0]; // 获取第一个轮廓
      const vertices = [];

      pointArr.forEach((elem) => {
        // 将 GeoJSON 坐标 [经度, 纬度] 转换为三维坐标
        vertices.push(elem[0], elem[1], 0);
      });

      // 创建线条几何体
      const geometry = new THREE.BufferGeometry();
      // 设置顶点位置
      const vertexArray = new Float32Array(vertices);
      // 创建属性缓冲区对象
      geometry.setAttribute("position", new THREE.BufferAttribute(vertexArray, 3));
      // 创建线条材质
      const material = new THREE.LineBasicMaterial({
        color: 0x00ffff,
        transparent: true,
        opacity: 0.8,
      });
      const line = new THREE.Line(geometry, material);
      group.add(line);
    });
    return group;
  }

计算中心点并将地图居中

javascript 复制代码
      const allPoints = [];

      data.features.forEach((area) => {
        if (area.geometry.type === "Polygon") {
          area.geometry.coordinates = [area.geometry.coordinates];
        }
        this.mapGroup.add(this.createLine(area.geometry.coordinates));

        // 收集所有坐标点用于计算中心
        area.geometry.coordinates.forEach(polygon => {
          polygon[0].forEach(point => {
            allPoints.push(point[0], point[1]);
          });
        });
      });

      // 计算中心点
      const minLon = Math.min(...allPoints.filter((_, i) => i % 2 === 0));
      const maxLon = Math.max(...allPoints.filter((_, i) => i % 2 === 0));
      const minLat = Math.min(...allPoints.filter((_, i) => i % 2 === 1));
      const maxLat = Math.max(...allPoints.filter((_, i) => i % 2 === 1));

      const centerX = (minLon + maxLon) / 2;
      const centerY = (minLat + maxLat) / 2;

      // 将地图移动到坐标原点
      this.mapGroup.position.set(-centerX, -centerY, 0);

使用正投影相机

javascript 复制代码
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

export default class CreateTwin {
  constructor() {
    //场景
    ...
    //创建一个加载器
    ...

      data.features.forEach((area) => {
    ...
      });
      this.mapGroup.position.set(-104, -28, 0);
    });

    //辅助观察的坐标系
    ...
    
    //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height;
    const s = 20; //三维场景显示范围控制系数,系数越大,显示的范围越大
    this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 8000);
    this.camera.position.set(0, 0, 50); //相机正对地图
    this.camera.lookAt(0,0,0);

    // WebGL渲染器设置
    ...

    //光源设置
    // 平行光
    ...
    //环境光
    ...

    // 渲染循环
    ...
    // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableRotate = false;
    this.controls.target.set(0,0,0);
    this.controls.update();

    // 画布尺寸随着窗口变化
      ...
  }

  createLine(polygons) {
        ...
  }
}

三、Shape几何体填充行政区域

javascript 复制代码
     this.mapGroup.add(this.createShape(area.geometry.coordinates));
javascript 复制代码
createShape(pointArrs) {
    const shapeArr = [];
    pointArrs.forEach((polygon) => {
      const pointArr = polygon[0]; // 获取第一个轮廓
      const vectorArr = [];
      pointArr.forEach((elem) => {
        // 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
        vectorArr.push(new THREE.Vector2(elem[0], elem[1]));
      });
      const shape = new THREE.Shape(vectorArr);
      shapeArr.push(shape);
    });

    // 创建填充几何体
    const geometry = new THREE.ShapeGeometry(shapeArr);
    // 创建材质
    const material = new THREE.MeshBasicMaterial({
      color: 0x009393,
      transparent: true,
      opacity: 0.6,
      side: THREE.DoubleSide,
    });
    const mesh = new THREE.Mesh(geometry, material);
    return mesh;
  }

深度冲突

line适当偏移

javascript 复制代码
 group.position.z += 1;

四、相机参数适配

javascript 复制代码
    //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height; //canvas画布宽高比
    const s = 90; //控制left, right, top, bottom范围大小
    this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    this.camera.position.set(100, 0, 200); //相机正对地图
    this.camera.lookAt(100, 0, 0);
javascript 复制代码
    // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    // this.controls = new MapControls(this.camera, this.renderer.domElement);
    this.controls.target.set(100, 0, 0); //与lookAt参数保持一致
    // this.controls.enableRotate = false;
    this.controls.update();

包围盒

javascript 复制代码
      // 地图mapGroup的包围盒计算
      const box3 = new THREE.Box3();
      box3.expandByObject(this.mapGroup);
      console.log("查看包围盒box3", box3);

      // 计算包围盒长宽高尺寸
      const size = new THREE.Vector3();
      box3.getSize(size);
      console.log("查看包围盒尺寸", size);

      // 计算包围盒的几何体中心
      const center = new THREE.Vector3();
      box3.getCenter(center);
      console.log("查看几何中心", center);
javascript 复制代码
    //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height; //canvas画布宽高比
    const s = 30; //控制left, right, top, bottom范围大小
    this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    this.camera.position.set(104.29, 28.48, 200); //相机正对地图
    this.camera.lookAt(104.29, 28.48, 0);

    // WebGL渲染器设置
    ...

    //光源设置
    ...
    // 渲染循环
       ...
    // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    // this.controls = new MapControls(this.camera, this.renderer.domElement);
    this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
    // this.controls.enableRotate = false;
    this.controls.update();

五、渲染省份或城市地图轮廓

javascript 复制代码
    this.loader.load("./江苏省.json", (data) => {}

重新设置相机设置

javascript 复制代码
 //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height; //canvas画布宽高比
    const s = 3; //控制left, right, top, bottom范围大小
    this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    // this.camera.position.set(104.29, 28.48, 200); //相机正对地图
    // this.camera.lookAt(104.29, 28.48, 0);
    this.camera.position.set(119.16, 32.94, 200); //相机正对地图
    this.camera.lookAt(119.16, 32.94, 0);

六、拉伸行政区轮廓

src\twin\ExtrudeMesh.js

javascript 复制代码
import * as THREE from "three";

export function ExtrudeMesh(pointArrs, height) {
  const shapeArr = [];
  pointArrs.forEach((polygon) => {
    const pointArr = polygon[0]; // 获取第一个轮廓
    const vectorArr = [];
    pointArr.forEach((elem) => {
      // 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
      vectorArr.push(new THREE.Vector2(elem[0], elem[1]));
    });
    const shape = new THREE.Shape(vectorArr);
    shapeArr.push(shape);
  });

  // 创建填充几何体
  //   const geometry = new THREE.ShapeGeometry(shapeArr);
  const geometry = new THREE.ExtrudeGeometry(shapeArr, {
    depth: height,
    bevelEnabled: true,
  });
  // 创建材质
  const material = new THREE.MeshBasicMaterial({
    color: 0x009393,
    transparent: true,
    opacity: 0.6,
    side: THREE.DoubleSide,
  });
  const mesh = new THREE.Mesh(geometry, material);
  return mesh;
}

替换src\twin\shapeMesh.js

javascript 复制代码
        const height = 1;
        // this.mapGroup.add(createShape(area.geometry.coordinates));
        this.mapGroup.add(ExtrudeMesh(area.geometry.coordinates, height));

八、经纬度转墨卡托坐标

src\twin\lonLat2Mercator.js

javascript 复制代码
export function lonLat2Mercator(E, N) {
  const x = (E * 20037508.34) / 180;
  let y = Math.log(Math.tan(((90 + N) * Math.PI) / 360)) / (Math.PI / 180);
  y = (y * 20037508.34) / 180;
  return {
    x: x,
    y: y,
  };
}

ExtrudeMesh.js或shapeMesh.js

javascript 复制代码
      // 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
      const coord = lonLat2Mercator(elem[0], elem[1]);
      vectorArr.push(new THREE.Vector2(coord.x, coord.y));
      //   vectorArr.push(new THREE.Vector2(elem[0], elem[1]));

包围盒发生改变

javascript 复制代码
  //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height; //canvas画布宽高比
    // const s = 30; //控制left, right, top, bottom范围大小

    const mapSize = 7087871; //包围盒方向y表示地图大小
    const s = mapSize / 2;
    // this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    this.camera = new THREE.OrthographicCamera(
      -s * k,
      s * k,
      s,
      -s,
      1,
      mapSize * 10
    );

    // this.camera.position.set(104.29, 28.48, 200); //相机正对地图
    // this.camera.position.set(104.29, 28.48 - 100, 200); //相机倾斜地图
    this.camera.position.set(11626571.24, 4305445.99, mapSize * 5);
    // this.camera.lookAt(104.29, 28.48, 0);
    this.camera.lookAt(11626571.24, 4305445.99, 0);
javascript 复制代码
 // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    // this.controls = new MapControls(this.camera, this.renderer.domElement);
    // this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
    this.controls.target.set(11626571.24, 4305445.99, 0); //与lookAt参数保持一致
    // this.controls.enableRotate = false;
    this.controls.update();

范围小,变化不大

line

需要把参数设大

javascript 复制代码
const height = 100000;
        // this.mapGroup.add(createShape(area.geometry.coordinates));
        this.mapGroup.add(ExtrudeMesh(area.geometry.coordinates, height));
javascript 复制代码
  lineGroup.position.z += 100000.2;

九、热点---波动光圈

准备一张光圈贴图

javascript 复制代码
//矩形平面网格模型设置背景透明的png贴图
    const geometry = new THREE.PlaneGeometry(100, 100); //默认在XoY平面上
    const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
    const material = new THREE.MeshBasicMaterial({
      color: 0x00ffff, //设置光圈颜色
      map: textureLoader.load("./光圈贴图.png"),
      transparent: true, //使用背景透明的png贴图,注意开启透明计算//
      side: THREE.DoubleSide, //双面可见
    });
    this.mesh = new THREE.Mesh(geometry, material);
    // mesh.rotateX(-Math.PI / 2);//旋转到xoz平面scene.add(mesh);
    this.mesh.position.z = 0.5;
    this.scene.add(this.mesh);
javascript 复制代码
// 渲染循环
    this.renderer.setAnimationLoop(() => {
      // 光圈缩放动画:1~2.5倍之间变化
      this._s += 0.01;
      this.mesh.scale.set(this._s, this._s, 1);
      // 缩放2.5对应0,缩放1.0对应1
      this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
      if (this._s > 2.5) this._s = 1;
      this.renderer.render(this.scene, this.camera);
    });

requestAnimationFrame 是浏览器原生的动画循环 API

javascript 复制代码
function animate() {
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}
animate();

renderer.setAnimationLoop 是 Three.js 提供的封装方法,底层也是基于 requestAnimationFrame

javascript 复制代码
renderer.setAnimationLoop(() => {
  renderer.render(scene, camera);
});

主要区别

  1. WebXR 兼容setAnimationLoop 会自动处理 VR/AR 设备的渲染帧,requestAnimationFrame 需要手动处理
  2. 简洁性setAnimationLoop 不需要递归调用,更简洁
  3. 普通项目 :两者效果相同,requestAnimationFrame 更常见

淡入淡出

javascript 复制代码
    this._s = 2.5;
    // 渲染循环
    this.renderer.setAnimationLoop(() => {
      // 光圈缩放动画:1~2.5倍之间变化
      // this._s += 0.01;
      // this.mesh.scale.set(this._s, this._s, this._s);
      // // 缩放2.5对应0,缩放1.0对应1
      // this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
      // if (this._s > 2.5) this._s = 1;

      // 淡入淡出
      this._s += 0.01;
      this.mesh.scale.set(this._s, this._s, this._s);
      if (this._s <= 1.3) {
        this.mesh.material.opacity = (this._s - 1.0) * 3.3;
      } else if (this._s > 1.3 && this._s <= 2.5) {
        this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
      } else {
        this._s = 1.0;
      }

      this.renderer.render(this.scene, this.camera);
    });
javascript 复制代码
this.loader.load("./china.json", (data) => {
      data.features.forEach((area) => {
        if (area.geometry.type === "Polygon") {
          area.geometry.coordinates = [area.geometry.coordinates];
        }
        this.mapGroup.add(this.createLine(area.geometry.coordinates));
        this.mapGroup.add(this.createShape(area.geometry.coordinates));

        // 标注省份行政中心光圈
        if (area.properties && area.properties.center) {
          const pos = area.properties.center;
          const size = Math.random() * 3 + 2;
          const geometry = new THREE.PlaneGeometry(size, size);
          const material = new THREE.MeshBasicMaterial({
            color: 0x00ffff,
            map: new THREE.TextureLoader().load("./光圈贴图.png"),
            transparent: true,
            side: THREE.DoubleSide,
          });
          const mesh = new THREE.Mesh(geometry, material);
          mesh.position.set(pos[0], pos[1], 0.5);
          mesh._s = size;
          this.cityPointGroup.add(mesh);
          this.meshes.push(mesh);
        }
      });
    });
javascript 复制代码
    // 渲染循环
    this.renderer.setAnimationLoop(() => {
      // 遍历所有省份光圈,执行缩放动画
      this.meshes.forEach((mesh) => {
        mesh._s += 0.01;
        mesh.scale.set(mesh._s, mesh._s, mesh._s);
        if (mesh._s <= 1.3) {
          mesh.material.opacity = (mesh._s - 1.0) * 3.3;
        } else if (mesh._s > 1.3 && mesh._s <= 2.5) {
          mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
        } else {
          mesh._s = 1.0;
        }
      });

      this.renderer.render(this.scene, this.camera);
    });

十、热点---旋转棱锥

src\twin\ConeMesh.js

javascript 复制代码
import * as THREE from "three";
export function createConeMesh(center) {
  const pos = center;
  const height = 4;
  const redius = 1.5;
  const geometry = new THREE.ConeGeometry(redius, height, 4);
  geometry.rotateX(-Math.PI / 2);
  geometry.translate(0, 0, height / 2);
  const material = new THREE.MeshLambertMaterial({
    color: 0x00ffff,
    transparent: true,
    opacity: 0.8,
  });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.set(pos[0], pos[1] - 1, 5.5);

  return mesh;
}
javascript 复制代码
    // 省份棱锥组
    this.coneGroup = new THREE.Group();
    this.mapGroup.add(this.coneGroup); 


// 标注省份行政中心棱锥
        if (area.properties && area.properties.center) {
          const mesh1 = createConeMesh(area.properties.center);
          this.coneGroup.add(mesh1);
          this.meshes1.push(mesh1);
        }
javascript 复制代码
    // 棱锥缩放动画参数 - 先初始化为空数组
    this.meshes1 = [];
// 渲染循环
    this.renderer.setAnimationLoop(() => {
      // 遍历所有省份光圈,执行缩放动画
      this.meshes.forEach((mesh) => {
        mesh._s += 0.01;
        mesh.scale.set(mesh._s, mesh._s, mesh._s);
        if (mesh._s <= 1.3) {
          mesh.material.opacity = (mesh._s - 1.0) * 3.3;
        } else if (mesh._s > 1.3 && mesh._s <= 2.5) {
          mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
        } else {
          mesh._s = 1.0;
        }
      });
      this.meshes1.forEach((mesh1) => {
        mesh1.rotateZ(0.01);
      });

      this.renderer.render(this.scene, this.camera);
      // console.log(this.controls.target);
    });

十一、HTML元素作为标签

javascript 复制代码
// 引入CSS3渲染器CSS3DRenderer
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
// 引入CSS3模型对象CSS3DObject
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

标签

javascript 复制代码
var div = document.createElement("div");
    div.innerHTML = "xxxx";
    div.style.padding = "5px 10px";
    div.style.color = "#fff";
    div.style.fontSize = "16px";
    div.style.position = "absolute";
    div.style.backgroundColor = "rgba(25,25,25,0.5)";
    div.style.borderRadius = "5px";
javascript 复制代码
//div元素包装为css模型对象css3Dobject,并插入场景中
    var label = new CSS3DObject(div);
    label.position.copy(); //定位css3Dobject模型对象
    label.scale.set(0.5, 0.5, 0.5);
    label.position.y += 20;
    this.scene.add(label); //css3模型标签插入到场景中
javascript 复制代码
// 创建一个CSS3渲染器CSS3DRenderer
    const css3Renderer = new CSS3DRenderer();
    css3Renderer.setSize(width, height);
    // HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
    css3Renderer.domElement.style.position = "absolute";
    css3Renderer.domElement.style.top = "0px";
    //设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
    css3Renderer.domElement.style.pointerEvents = "none";
    document.body.appendChild(css3Renderer.domElement);

渲染循环

javascript 复制代码
   css3Renderer.render(this.scene, this.camera);

十二、鼠标单击拾取选中一个行政区

javascript 复制代码
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
// 引入渲染器通道RenderPass
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
// 引入OutlinePass通道
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
javascript 复制代码
    // 标记鼠标拾取到的mesh
    let chooseMesh = null;
    // 选中网格模型变为半透明效果
    const choose = (event) => {
      // 如果之前有选中的mesh,恢复原来透明度
      if (chooseMesh) {
        chooseMesh.material.color.set(0x009393);
      }
      const Sx = event.clientX; // 鼠标单击位置横坐标
      const Sy = event.clientY; // 鼠标单击位置纵坐标

      // 屏幕坐标转WebGL标准设备坐标
      const x = (Sx / window.innerWidth) * 2 - 1; // WebGL标准设备横坐标
      const y = -(Sy / window.innerHeight) * 2 + 1; // WebGL标准设备纵坐标

      // 创建一个射线投射器Raycaster
      const raycaster = new THREE.Raycaster();
      // 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性.ray
      raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);

      // 返回.intersectObjects()参数中射线选中的网格模型对象
      // 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
      const intersects = raycaster.intersectObjects(this.mapGroup.children);
      console.log("射线器返回的对象", intersects);

      // intersects.length大于0说明,说明选中了模型
      if (intersects.length > 0) {
        intersects[0].object.material.color.set(0x00ffff);
        chooseMesh = intersects[0].object;
      }
    };

    // 监听窗口鼠标单击事件
    this.renderer.domElement.addEventListener("click", choose);
javascript 复制代码
// 创建后处理对象EffectComposer,WebGL渲染器作为参数
    const composer = new EffectComposer(this.renderer);
    //创建一个渲染器通道,场景和相机作为参数
    const renderPass = new RenderPass(this.scene, this.camera);
    composer.addPass(renderPass);
    // OutlinePass第一个参数v2的尺寸和canvas画布保持一致
    const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
    const outlinePass = new OutlinePass(v2, this.scene, this.camera);
    outlinePass.visibleEdgeColor.set(0x00ffff);
    outlinePass.edgeThickness = 4;
    outlinePass.edgeStrength = 6;

 // 渲染循环
   composer.render();

这样即使 LineExtrudeMesh 上层,射线检测也会跳过它,直接选中底层的 ExtrudeMesh。不需要修改 createLineLine 对象本身的设置。

javascript 复制代码
// intersects.length大于0说明,说明选中了模型
      if (intersects.length > 0) {
        // 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
        const selectedMesh = intersects.find((intersect) => {
          return intersect.object.type === 'Mesh' &&
                 intersect.object.geometry.type === 'ExtrudeGeometry';
        });

        if (selectedMesh) {
          selectedMesh.object.material.color.set(0x00ffff);
          chooseMesh = selectedMesh.object;
        }
      }

十三、中国地图颜色深浅可视化

javascript 复制代码
    // 加载年末常住人口数据
    this.loader.load("./年末常住人口(万人).json", (data2) => {
      // 每个省份的名字作为属性,属性值是国家对应人口
      var popObj = {};
      // 人口最高对应红色,人口最低对应白色
      var color1 = new THREE.Color(0xffffff);
      var color2 = new THREE.Color(0xff0000);
      var popMax = 12780; // 设置一个基准值,以最高的广东人口为准

      data2.features.forEach((obj) => {
        var pop = obj.value; // 当前省份人口
        popObj[obj.name] = pop; // 每个省份的名字作为属性,属性值是国家对应人口
      });

      console.log("人口数据对象", popObj);

      // this.loader.load("./江苏省.json", (data) => {
      this.loader.load("./china.json", (data) => {
        data.features.forEach((area) => {
          if (area.geometry.type === "Polygon") {
            area.geometry.coordinates = [area.geometry.coordinates];
          }
          this.mapGroup.add(createLine(area.geometry.coordinates));

          // 根据人口数据设置省份颜色
          const provinceName = area.properties.name;
          const height = 1;
          var mesh = ExtrudeMesh(area.geometry.coordinates, height);

          // 如果有对应的人口数据,设置颜色
          if (popObj[provinceName]) {
            var pop = popObj[provinceName];
            // 计算颜色插值:人口越多越红
            var colorRatio = pop / popMax;
            var color = new THREE.Color().lerpColors(color1, color2, colorRatio);
            mesh.material.color = color;
          }

          this.mapGroup.add(mesh);

          // 标注省份行政中心光圈
          if (area.properties && area.properties.center) {
            const mesh = createCityPointMesh(area.properties.center);
            this.cityPointGroup.add(mesh);
            this.meshes.push(mesh);
          }
          // 标注省份行政中心棱锥
          if (area.properties && area.properties.center) {
            const mesh1 = createConeMesh(area.properties.center);
            this.coneGroup.add(mesh1);
            this.meshes1.push(mesh1);
          }
        });
        // 地图mapGroup的包围盒计算
        const box3 = new THREE.Box3();
        box3.expandByObject(this.mapGroup);
        console.log("查看包围盒box3", box3);

        // 计算包围盒长宽高尺寸
        const size = new THREE.Vector3();
        box3.getSize(size);
        console.log("查看包围盒尺寸", size);

        // 计算包围盒的几何体中心
        const center = new THREE.Vector3();
        box3.getCenter(center);
        console.log("查看几何中心", center);
      });
    });

年末常住人口(万人).json

国家数据

每个省份根据人口数据有不同的颜色,恢复时应该恢复到原来的颜色,而不是固定的 0x009393

javascript 复制代码
 let originalColor = null; // 保存选中mesh的原始颜色

    // 选中网格模型变为半透明效果
    const choose = (event) => {
      // 如果之前有选中的mesh,恢复原来颜色
      if (chooseMesh && originalColor) {
        chooseMesh.material.color.copy(originalColor);
      }
    ...
     // intersects.length大于0说明,说明选中了模型
      if (intersects.length > 0) {
        // 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
        const selectedMesh = intersects.find((intersect) => {
          return (
            intersect.object.type === "Mesh" &&
            intersect.object.geometry.type === "ExtrudeGeometry"
          );
        });

        if (selectedMesh) {
          // 保存原始颜色
          originalColor = selectedMesh.object.material.color.clone();
          // 设置选中颜色
          selectedMesh.object.material.color.set(0x00ffff);
          chooseMesh = selectedMesh.object;
        }
      }
    }

鼠标射线拾取+div信息弹窗

src\twin\CreateTwin.js

javascript 复制代码
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
// 引入CSS3渲染器CSS3DRenderer
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
// 引入CSS3模型对象CSS3DObject
import { CSS3DObject } from "three/addons/renderers/CSS3DRenderer.js";

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";

// 引入渲染器通道RenderPass
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
// 引入OutlinePass通道
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";

import { createLine } from "./line.js";
import { ExtrudeMesh } from "./ExtrudeMesh.js";
import { createCityPointMesh } from "./cityPointMesh.js";
import { createConeMesh } from "./ConeMesh.js";

export default class CreateTwin {
  constructor() {
    //场景
    this.scene = new THREE.Scene();
    //创建一个加载器
    this.loader = new THREE.FileLoader();
    this.loader.setResponseType("json");

    this.mapGroup = new THREE.Group();
    this.scene.add(this.mapGroup);

    // 省份光圈组
    this.cityPointGroup = new THREE.Group();
    this.mapGroup.add(this.cityPointGroup);
    // 省份棱锥组
    this.coneGroup = new THREE.Group();
    this.mapGroup.add(this.coneGroup);
    // 光圈缩放动画参数 - 先初始化为空数组
    this.meshes = [];
    // 棱锥缩放动画参数 - 先初始化为空数组
    this.meshes1 = [];

    // 加载年末常住人口数据
    this.loader.load("./年末常住人口(万人).json", (data2) => {
      // 每个省份的名字作为属性,属性值是国家对应人口
      var popObj = {};
      // 人口最高对应红色,人口最低对应白色
      var color1 = new THREE.Color(0xffffff);
      var color2 = new THREE.Color(0xff0000);
      var popMax = 12780; // 设置一个基准值,以最高的广东人口为准

      data2.features.forEach((obj) => {
        var pop = obj.value; // 当前省份人口
        popObj[obj.name] = pop; // 每个省份的名字作为属性,属性值是国家对应人口
      });
      // console.log("人口数据对象", popObj)
      this.loader.load("./china.json", (data) => {
        data.features.forEach((area) => {
          if (area.geometry.type === "Polygon") {
            area.geometry.coordinates = [area.geometry.coordinates];
          }
          this.mapGroup.add(createLine(area.geometry.coordinates));

          // 根据人口数据设置省份颜色
          const provinceName = area.properties.name;
          const height = 1;
          var mesh = ExtrudeMesh(area.geometry.coordinates, height);

          // mesh自定义一个pop属性,用于标签设置
          mesh.pop = popObj[provinceName] || 0;

          // mesh经纬度用于控制标签位置
          mesh.center = area.properties.center;

          // 如果有对应的人口数据,设置颜色
          if (popObj[provinceName]) {
            var pop = popObj[provinceName];
            // 计算颜色插值:人口越多越红
            var colorRatio = pop / popMax;
            var color = new THREE.Color().lerpColors(
              color1,
              color2,
              colorRatio
            );
            mesh.material.color = color;
            mesh.color = color; // 记录下自身的颜色,以便选中改变mesh颜色的时候,不选中状态再改变回来
          }

          this.mapGroup.add(mesh);

          // 标注省份行政中心光圈
          if (area.properties && area.properties.center) {
            const mesh = createCityPointMesh(area.properties.center);
            this.cityPointGroup.add(mesh);
            this.meshes.push(mesh);
          }
          // 标注省份行政中心棱锥
          if (area.properties && area.properties.center) {
            const mesh1 = createConeMesh(area.properties.center);
            this.coneGroup.add(mesh1);
            this.meshes1.push(mesh1);
          }
        });
        // 地图mapGroup的包围盒计算
        const box3 = new THREE.Box3();
        box3.expandByObject(this.mapGroup);
        console.log("查看包围盒box3", box3);

        // 计算包围盒长宽高尺寸
        const size = new THREE.Vector3();
        box3.getSize(size);
        console.log("查看包围盒尺寸", size);

        // 计算包围盒的几何体中心
        const center = new THREE.Vector3();
        box3.getCenter(center);
        console.log("查看几何中心", center);
      });
    });
    // 辅助观察的坐标系
    this.axesHelper = new THREE.AxesHelper(300);
    this.scene.add(this.axesHelper);

    //相机设置
    const width = window.innerWidth;
    const height = window.innerHeight;
    const k = width / height; //canvas画布宽高比
    const s = 30; //控制left, right, top, bottom范围大小

    this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    this.camera.position.set(104.29, 28.48 - 40, 200); //相机倾斜地图
    this.camera.lookAt(104.29, 28.48, 0);

    // WebGL渲染器设置
    this.renderer = new THREE.WebGLRenderer({
      antialias: true, //开启优化锯齿
    });
    this.renderer.setPixelRatio(window.devicePixelRatio); //防止输出模糊
    this.renderer.setSize(width, height);
    document.body.appendChild(this.renderer.domElement);

    // 创建一个CSS3渲染器CSS3DRenderer
    const css3Renderer = new CSS3DRenderer();
    css3Renderer.setSize(width, height);
    // HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
    css3Renderer.domElement.style.position = "absolute";
    css3Renderer.domElement.style.top = "0px";
    //设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
    css3Renderer.domElement.style.pointerEvents = "none";
    document.body.appendChild(css3Renderer.domElement);

    // 创建标签div元素
    var div = document.createElement("div");
    div.style.padding = "1px 2px";
    div.style.color = "#fff";
    div.style.fontSize = "2px";
    div.style.position = "absolute";
    div.style.backgroundColor = "rgba(25,25,25,0.5)";
    div.style.borderRadius = "5px";
    div.style.visibility = "hidden"; // 初始隐藏标签

    //div元素包装为css模型对象css3Dobject,并插入场景中
    var label = new CSS3DObject(div);
    this.scene.add(label); //css3模型标签插入到场景中

    // 标记鼠标拾取到的mesh
    let chooseMesh = null;
    let originalColor = null; // 保存选中mesh的原始颜色

    // 选中网格模型变为半透明效果
    const choose = (event) => {
      // 如果之前有选中的mesh,恢复原来颜色
      if (chooseMesh && originalColor) {
        chooseMesh.material.color.copy(originalColor);
        label.element.style.visibility = "hidden";
      } else {
        label.element.style.visibility = "hidden"; // 没有选中mesh,隐藏标签
      }

      const Sx = event.clientX; // 鼠标单击位置横坐标
      const Sy = event.clientY; // 鼠标单击位置纵坐标

      // 屏幕坐标转WebGL标准设备坐标
      const x = (Sx / window.innerWidth) * 2 - 1; // WebGL标准设备横坐标
      const y = -(Sy / window.innerHeight) * 2 + 1; // WebGL标准设备纵坐标

      // 创建一个射线投射器Raycaster
      const raycaster = new THREE.Raycaster();
      // 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性.ray
      raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);

      // 返回.intersectObjects()参数中射线选中的网格模型对象
      // 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
      const intersects = raycaster.intersectObjects(this.mapGroup.children);
      console.log("射线器返回的对象", intersects);

      // intersects.length大于0说明,说明选中了模型
      if (intersects.length > 0) {
        // 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
        const selectedMesh = intersects.find((intersect) => {
          return (
            intersect.object.type === "Mesh" &&
            intersect.object.geometry.type === "ExtrudeGeometry"
          );
        });

        if (selectedMesh) {
          // 保存原始颜色
          originalColor = selectedMesh.object.material.color.clone();
          // 设置选中颜色
          selectedMesh.object.material.color.set(0x00ffff);
          chooseMesh = selectedMesh.object;

          // 显示标签并设置内容
          label.element.style.visibility = "visible";
          label.element.innerHTML = `省份:${chooseMesh.pop || 0}万人`;

          // 设置标签位置到mesh的center位置
          if (chooseMesh.center) {
            label.position.set(chooseMesh.center[0], chooseMesh.center[1], 0);
          } else {
            // 如果没有center,使用射线检测的点
            const point = selectedMesh.point;
            label.position.copy(point);
          }
          label.position.y += 2; // 标签在mesh上方
          label.scale.set(0.5, 0.5, 0.5);
        }
      }
    };

    // 监听窗口鼠标单击事件
    this.renderer.domElement.addEventListener("click", choose);

    // 创建后处理对象EffectComposer,WebGL渲染器作为参数
    const composer = new EffectComposer(this.renderer);
    //创建一个渲染器通道,场景和相机作为参数
    const renderPass = new RenderPass(this.scene, this.camera);
    composer.addPass(renderPass);
    // OutlinePass第一个参数v2的尺寸和canvas画布保持一致
    const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
    const outlinePass = new OutlinePass(v2, this.scene, this.camera);
    outlinePass.visibleEdgeColor.set(0x00ffff);
    outlinePass.edgeThickness = 4;
    outlinePass.edgeStrength = 6;

    //光源设置
    // 平行光1
    this.directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
    this.directionalLight.position.set(0, 0, 50);
    this.scene.add(this.directionalLight);
    // 平行光2
    this.directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.6);
    this.directionalLight1.position.set(0, 0, -50);
    this.scene.add(this.directionalLight1);
    //环境光
    this.ambient = new THREE.AmbientLight(0xffffff, 0.6);
    this.scene.add(this.ambient);

    // 渲染循环
    this.renderer.setAnimationLoop(() => {
      // 遍历所有省份光圈,执行缩放动画
      this.meshes.forEach((mesh) => {
        mesh._s += 0.01;
        mesh.scale.set(mesh._s, mesh._s, mesh._s);
        if (mesh._s <= 1.3) {
          mesh.material.opacity = (mesh._s - 1.0) * 3.3;
        } else if (mesh._s > 1.3 && mesh._s <= 2.5) {
          mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
        } else {
          mesh._s = 1.0;
        }
      });
      this.meshes1.forEach((mesh1) => {
        mesh1.rotateZ(0.01);
      });

      composer.render();
      css3Renderer.render(this.scene, this.camera);
      this.renderer.render(this.scene, this.camera);
      // console.log(this.controls.target);
    });

    // 相机控件
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    // this.controls = new MapControls(this.camera, this.renderer.domElement);
    this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
    // this.controls.enableRotate = false;
    this.controls.update();

    // 画布尺寸随着窗口变化
    window.addEventListener("resize", () => {
      css3Renderer.setSize(window.innerWidth, window.innerHeight);
      c;
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
    });
  }
}

十四、行政区域拉伸不同高度可视化数据

javascript 复制代码
          // 高度:行政区轮廓拉伸高度,和人口大小正相关
          var height = popObj[provinceName] / 5000; // 拉伸高度

          var mesh = ExtrudeMesh(area.geometry.coordinates, height);

飞线

mergeBufferGeometries 合并多个几何体

相机控件MapControls

左键移动

相关推荐
代码搬运媛8 小时前
Jest 测试框架详解与实现指南
前端
吃好睡好便好9 小时前
在Matlab中绘制横直方图
开发语言·学习·算法·matlab
counterxing9 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq9 小时前
windows下nginx的安装
linux·服务器·前端
nashane9 小时前
HarmonyOS 6学习:CapsLock键失效诊断与长截图完整实现指南
学习·华为·harmonyos
之歆9 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜10 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai1080810 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong10 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
xian_wwq11 小时前
【学习笔记】AGC协调控制系统概述
笔记·学习