学习: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

左键移动

相关推荐
雨季6664 小时前
构建 OpenHarmony 简易文字行数统计器:用字符串分割实现纯文本结构感知
开发语言·前端·javascript·flutter·ui·dart
雨季6664 小时前
Flutter 三端应用实战:OpenHarmony 简易倒序文本查看器开发指南
开发语言·javascript·flutter·ui
小北方城市网4 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
console.log('npc')4 小时前
vue2 使用高德接口查询天气
前端·vue.js
2401_892000524 小时前
Flutter for OpenHarmony 猫咪管家App实战 - 添加支出实现
前端·javascript·flutter
A9better4 小时前
嵌入式开发学习日志50——任务调度与状态
stm32·嵌入式硬件·学习
天马37984 小时前
Canvas 倾斜矩形绘制波浪效果
开发语言·前端·javascript
天天向上10244 小时前
vue3 实现el-table 部分行不让勾选
前端·javascript·vue.js
非凡ghost4 小时前
ESET NupDown Tools 数据库下载工具
学习·软件需求
qx094 小时前
esm模块与commonjs模块相互调用的方法
开发语言·前端·javascript