three.js实现3D地图

效果图:

1、下载three.js及d3依赖,我这里使用的是three r153版本

npm i three
npm i d3

2、使用d3将地理坐标转换成坐标轴xy值,在地图区域及边界线加载到场景后,通过包围盒计算完整地图的max和min坐标,再根据max和min的值重新计算每个地图区域的uv坐标,如果不重新计算uv坐标会导致贴图显示异常

3.完整代码(json替换成自己的数据)

<template>
  <div class="page" id="page" ref="page">
    <div class="tooltip" ref="tooltip" v-show="show">
      {{ selectedPointData.name }}
    </div>
  </div>
</template>
 
<script>
import * as THREE from "three";
import * as d3 from "d3";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
export default {
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      controls: null,
      centerCoordinate: [117.13, 31.89], // 地图中心地理坐标
      projection: null, // Mercator 投影
      mapConfig: {
        deep: 0.2, // 挤出的深度
      },
      boundaryLineArr: [], // 边界线
      composer: "", // 后期处理
      pointData: [
        {
          coordinates: [117.33, 31.79],
          type: 1,
          name: "合肥",
          value: 100,
        },
        {
          coordinates: [118.502, 31.684],
          type: 1,
          name: "马鞍山",
          value: 100,
        },
      ],
      pointInstanceArr: [], // 坐标点实例
      show: false, // 是否显示tooltip
      selectedPointData: {}, // 选中的坐标点数据
    };
  },
 
  mounted() {
    this.init();
    window.addEventListener("resize", () => {
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
    });
  },
 
  methods: {
    init() {
      this.renderer = new THREE.WebGLRenderer();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
      document.querySelector("#page").appendChild(this.renderer.domElement);
 
      this.scene = new THREE.Scene();
      this.camera = new THREE.PerspectiveCamera(
        45,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      );
      this.camera.position.set(5, 5, 26);
      this.camera.lookAt(0, 0, 0);
 
      // let axesHelp = new THREE.AxesHelper(5);
      // this.scene.add(axesHelp);
 
      this.controls = new OrbitControls(this.camera, this.renderer.domElement);
 
      // 墨卡托投影转换
      this.projection = d3
        .geoMercator()
        .center(this.centerCoordinate)
        .translate([0, 0]); // 根据地球贴图做轻微调整
      // 添加地图
      this.addMap();
      // 给地图边界线添加outline效果
      this.setLineOutline();
 
      // 添加灯光
      let ambientLight = new THREE.AmbientLight(0xffffff, 1);
      this.scene.add(ambientLight);
 
      // 添加散点
      this.setPoint();
      // 设置光线投射
      this.setRaycaster();
 
      this.render();
    },
    render() {
      this.renderer.render(this.scene, this.camera);
      this.controls.update();
      if (this.composer) this.composer.render();
      requestAnimationFrame(this.render);
    },
    // 添加地图
    addMap() {
      // 加载地图背景
      const backgroundTexture = new THREE.TextureLoader().load(
        require("@/assets/images/map.png")
      );
      // 加载地图
      let fileLoader = new THREE.FileLoader();
      fileLoader.load("/anhui.json", (data) => {
        // 添加地图及边界线
        this.addMapGeometry(data);
        // 重新计算地图uv坐标
        let arr = [];
        let box = new THREE.Box3();
        for (let v of this.map.children) {
          for (let v2 of v.children) {
            // 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小
            if (v2.geometry instanceof THREE.ExtrudeGeometry) {
              arr.push(v2);
              let itemBox = new THREE.Box3().setFromObject(v2);
              box.union(itemBox);
            }
          }
        }
        var bboxMin = box.min;
        var bboxMax = box.max;
        // 计算UV的缩放比例
        var uvScale = new THREE.Vector2(
          1 / (bboxMax.x - bboxMin.x),
          1 / (bboxMax.y - bboxMin.y)
        );
        for (let v of arr) {
          let uvAttribute = v.geometry.getAttribute("uv");
          for (let i = 0; i < uvAttribute.count; i++) {
            let u = uvAttribute.getX(i);
            let v = uvAttribute.getY(i);
            // 将UV坐标进行归一化
            let normalizedU = (u - bboxMin.x) * uvScale.x;
            let normalizedV = (v - bboxMin.y) * uvScale.y;
            // 更新UV坐标
            uvAttribute.setXY(i, normalizedU, normalizedV);
          }
          // 更新几何体的UV属性
          v.geometry.setAttribute("uv", uvAttribute);
          v.material.map = backgroundTexture;
          v.material.needsUpdate = true;
        }
      });
    },
    addMapGeometry(jsondata) {
      // 初始化一个地图对象
      this.map = new THREE.Object3D();
      jsondata = JSON.parse(jsondata);
      jsondata.features.forEach((elem) => {
        // 定一个省份3D对象
        const province = new THREE.Object3D();
        // 每个的 坐标 数组
        const coordinates = elem.geometry.coordinates;
 
        if (elem.geometry.type === "MultiPolygon") {
          // 循环坐标数组
          coordinates.forEach((multiPolygon) => {
            multiPolygon.forEach((polygon) => {
              this.drawItem(elem, polygon, province);
            });
          });
          this.map.add(province);
        } else if (elem.geometry.type === "Polygon") {
          // 循环坐标数组
          coordinates.forEach((polygon) => {
            this.drawItem(elem, polygon, province);
          });
          this.map.add(province);
        }
      });
      this.scene.add(this.map);
    },
    drawItem(elem, polygon, province) {
      const shape = new THREE.Shape();
      const pointsArray = new Array();
      for (let i = 0; i < polygon.length; i++) {
        const [x, y] = this.projection(polygon[i]);
        if (i === 0) {
          shape.moveTo(x, -y);
        }
        shape.lineTo(x, -y);
        pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
      }
      let curve = new THREE.CatmullRomCurve3(pointsArray);
      // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做
      var tubeGeometry = new THREE.TubeGeometry(
        curve,
        Math.floor(pointsArray.length),
        0.02,
        10
      );
 
      const extrudeSettings = {
        depth: this.mapConfig.deep,
        bevelEnabled: false, // 对挤出的形状应用是否斜角
      };
      const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
      geometry.computeBoundingBox();
      // 创建地图区域材质
      let meshMaterial = new THREE.MeshStandardMaterial({
        color: "#ffffff",
        transparent: true,
        opacity: 1,
      });
      // 创建地图边界线材质
      let lineMaterial = new THREE.MeshBasicMaterial({
        color: "#ceebf7",
      });
 
      const mesh = new THREE.Mesh(geometry, meshMaterial);
      const line = new THREE.Mesh(tubeGeometry, lineMaterial);
      // 将省份的属性 加进来
      province.properties = elem.properties;
      province.add(mesh);
      this.boundaryLineArr.push(line);
      province.add(line);
    },
    // 给地图边界线添加outline效果
    setLineOutline() {
      //设置光晕
      this.composer = new EffectComposer(this.renderer); //效果组合器
      //创建通道
      let renderScene = new RenderPass(this.scene, this.camera);
      this.composer.addPass(renderScene);
 
      let outlinePass = new OutlinePass(
        new THREE.Vector2(window.innerWidth, window.innerHeight),
        this.scene,
        this.camera,
        this.boundaryLineArr
      );
      outlinePass.renderToScreen = true;
      outlinePass.edgeGlow = 2; // 光晕效果
      outlinePass.usePatternTexture = false;
      outlinePass.edgeThickness = 10; // 边框宽度
      outlinePass.edgeStrength = 1.5; // 光晕效果
      outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
      outlinePass.visibleEdgeColor.set("#1acdec");
      outlinePass.hiddenEdgeColor.set("#1acdec");
      this.composer.addPass(outlinePass);
    },
    // 添加散点
    setPoint() {
      let pointTexture = new THREE.TextureLoader().load(
        require("@/assets/images/point.png")
      );
      for (let v of this.pointData) {
        let [x, y] = this.projection(v.coordinates);
        const sprite = new THREE.Sprite(
          new THREE.SpriteMaterial({
            map: pointTexture,
          })
        );
        sprite.scale.set(0.7, 0.7, 1);
        sprite.position.set(x, -y, this.mapConfig.deep + 0.5);
        sprite.properties = v;
        this.pointInstanceArr.push(sprite);
        this.scene.add(sprite);
      }
    },
    // 光线投射
    setRaycaster() {
      const raycaster = new THREE.Raycaster();
      const pointer = new THREE.Vector2();
      this.$refs.page.addEventListener("click", (event) => {
        pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
        pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
 
        raycaster.setFromCamera(pointer, this.camera);
        const intersects = raycaster.intersectObjects(this.pointInstanceArr);
        if (intersects && intersects.length > 0) {
          let tooltip = this.$refs.tooltip;
          tooltip.style.left = event.pageX + "px";
          tooltip.style.top = event.pageY + "px";
          this.selectedPointData = intersects[0].object.properties;
          this.show = true;
        } else {
          this.selectedPointData = {};
          this.show = false;
        }
      });
    },
  },
};
</script>
<style scoped lang="scss">
.page {
  height: 100vh;
  .tooltip {
    position: absolute;
    background-color: #fff;
    padding: 10px;
    border-radius: 8px;
  }
}
</style>

 

4.所用图片

5. 问题解决

node_modules包里面找到three.module.js文件大概第7177行,加四行数据解决。

8.海岸群岛demo,去除三沙市案例

<template>
    <div class="page" id="page" ref="page" style="width: 100% !important;height: 100% !important;border:0px solid red;">
        <div class="tooltip" ref="tooltip" v-show="show">
            {{ selectedPointData.name }}
        </div>
    </div>
</template>

<script>
import * as THREE from "three";
import * as d3 from "d3";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass.js";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { toRaw } from '@vue/reactivity';
let scale;
export default {
    data() {
        return {
            scene: null,
            camera: null,
            renderer: null,
            controls: null,
            centerCoordinate: [109.512, 18.252], // 地图中心地理坐标
            projection: null, // Mercator 投影
            mapConfig: {
                deep: 0.2, // 挤出的深度
            },
            boundaryLineArr: [], // 边界线
            composer: "", // 后期处理
            pointData: [
                {
                    coordinates: [109.512, 18.252], // 假设的三亚地理位置坐标,实际请核实
                    type: 1,
                    name: "三亚湾",
                    value: 100,
                },
                // {
                //     coordinates: [109.488, 18.225], // 另一个假设的三亚地理位置坐标,实际请根据具体地点调整
                //     type: 1,
                //     name: "天涯海角",
                //     value: 100,
                // },
            ],
            // pointData: [
            //     {
            //         coordinates: [117.33, 31.79],
            //         type: 1,
            //         name: "合肥",
            //         value: 100,
            //     },
            //     {
            //         coordinates: [118.502, 31.684],
            //         type: 1,
            //         name: "马鞍山",
            //         value: 100,
            //     },
            // ],
            pointInstanceArr: [], // 坐标点实例
            show: false, // 是否显示tooltip
            selectedPointData: {}, // 选中的坐标点数据
        };
    },

    mounted() {
        this.init();
        window.addEventListener("resize", () => {
            const width = document.querySelector(".page").offsetWidth
            const height = document.querySelector(".page").offsetHeight;
            this.renderer.setSize(width, height);
            this.camera.aspect = width / height;
            this.camera.updateProjectionMatrix();
        });
    },

    methods: {
        init() {
            this.renderer = new THREE.WebGLRenderer();
            const width = document.querySelector(".page").offsetWidth
            const height = document.querySelector(".page").offsetHeight;
            this.renderer.setSize(width, height);
            this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
            document.querySelector("#page").appendChild(this.renderer.domElement);

            this.scene = new THREE.Scene();
            this.scene.background = new THREE.TextureLoader().load("../assets/images/bigBg.png");
            // this.camera = new THREE.PerspectiveCamera(
            //     45,
            //     window.innerWidth / window.innerHeight,
            //     0.1,
            //     1000
            // );
            // this.camera.position.set(5, 5, 26);
            this.camera = new THREE.PerspectiveCamera(
                9, // 减小fov使视野更窄,让物体看起来更大
                window.innerWidth / window.innerHeight,
                0.5,
                1000
            );
            this.camera.position.set(3, 3, 100); // 缩短z轴距离,让相机离地图更近
            this.camera.lookAt(0, 0, 0);

            // let axesHelp = new THREE.AxesHelper(5);
            // this.scene.add(axesHelp);

            this.controls = new OrbitControls(this.camera, this.renderer.domElement);

            // 墨卡托投影转换
            this.projection = d3
                .geoMercator()
                .center(this.centerCoordinate)
                .translate([0, 0]); // 根据地球贴图做轻微调整
            // 添加地图
            this.addMap();
            // let mapObject = this.addMap();
            // 调整地图对象的位置
            // addMap().position.y -= 1; // 向下移动1个单位
            // 给地图边界线添加outline效果
            this.setLineOutline();

            // 添加灯光
            let ambientLight = new THREE.AmbientLight(0xffffff, 1);
            this.scene.add(toRaw(ambientLight));

            // 添加散点
            this.setPoint();
            // 设置光线投射
            this.setRaycaster();

            this.render();
        },
        render() {
            this.renderer.render(toRaw(this.scene), this.camera);
            this.controls.update();
            if (this.composer) this.composer.render();
            requestAnimationFrame(this.render);
        },
        // 添加地图
        addMap() {
            // 加载地图背景
            const backgroundTexture = new THREE.TextureLoader().load(
                require("@/assets/images/map.png")
                // require("@/assets/images/bigBg.png")
            );
            // 加载地图
            let fileLoader = new THREE.FileLoader();
            fileLoader.load("/map.json", (data) => {
                // 添加地图及边界线
                this.addMapGeometry(data);
                // 重新计算地图uv坐标
                let arr = [];
                let box = new THREE.Box3();
                for (let v of this.map.children) {
                    for (let v2 of v.children) {
                        // 判断是否为ExtrudeGeometry,只计算所有地图区域总和的包围盒大小
                        if (v2.geometry instanceof THREE.ExtrudeGeometry) {
                            arr.push(v2);
                            let itemBox = new THREE.Box3().setFromObject(v2);
                            box.union(itemBox);
                        }
                    }
                }
                var bboxMin = box.min;
                var bboxMax = box.max;
                // 计算UV的缩放比例
                var uvScale = new THREE.Vector2(
                    1 / (bboxMax.x - bboxMin.x),
                    1 / (bboxMax.y - bboxMin.y)
                );
                for (let v of arr) {
                    let uvAttribute = v.geometry.getAttribute("uv");
                    for (let i = 0; i < uvAttribute.count; i++) {
                        let u = uvAttribute.getX(i);
                        let v = uvAttribute.getY(i);
                        // 将UV坐标进行归一化
                        let normalizedU = (u - bboxMin.x) * uvScale.x;
                        let normalizedV = (v - bboxMin.y) * uvScale.y;
                        // 更新UV坐标
                        uvAttribute.setXY(i, normalizedU, normalizedV);
                    }
                    // 更新几何体的UV属性
                    v.geometry.setAttribute("uv", uvAttribute);
                    v.material.map = backgroundTexture;
                    v.material.needsUpdate = true;
                }
            });
        },
        addMapGeometry(jsondata) {
            // 初始化一个地图对象
            this.map = new THREE.Object3D();
            jsondata = JSON.parse(jsondata);
            jsondata.features.forEach((elem) => {
                console.log(666,elem.properties)
                // 新增: 根据省份名称或其他条件判断是否跳过当前元素(例如排除海南)
                if (elem.properties.name === "三沙市") { // 假设properties中有name或adcode标识省份
                    console.log("跳过了海南诸岛的数据");
                    return; // 直接返回,跳过当前循环的这个省份
                }
                // 定一个省份3D对象
                const province = new THREE.Object3D();
                // 每个的 坐标 数组
                const coordinates = elem.geometry.coordinates;

                if (elem.geometry.type === "MultiPolygon") {
                    // 循环坐标数组
                    coordinates.forEach((multiPolygon) => {
                        multiPolygon.forEach((polygon) => {
                            this.drawItem(elem, polygon, province);
                        });
                    });
                    this.map.add(province);
                } else if (elem.geometry.type === "Polygon") {
                    // 循环坐标数组
                    coordinates.forEach((polygon) => {
                        this.drawItem(elem, polygon, province);
                    });
                    this.map.add(province);
                }
            });
            this.scene.add(this.map);
        },
        drawItem(elem, polygon, province) {
            const shape = new THREE.Shape();
            const pointsArray = new Array();
            for (let i = 0; i < polygon.length; i++) {
                const [x, y] = this.projection(polygon[i]);
                if (i === 0) {
                    shape.moveTo(x, -y);
                }
                shape.lineTo(x, -y);
                pointsArray.push(new THREE.Vector3(x, -y, this.mapConfig.deep));
            }
            let curve = new THREE.CatmullRomCurve3(pointsArray);
            // 这里使用TubeGeometry没有使用line,主要考虑到line的宽度无法设置,也可以使用其他第三方依赖去做
            var tubeGeometry = new THREE.TubeGeometry(
                curve,
                Math.floor(pointsArray.length),
                0.02,
                10
            );

            const extrudeSettings = {
                depth: this.mapConfig.deep,
                bevelEnabled: false, // 对挤出的形状应用是否斜角
            };
            const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
            geometry.computeBoundingBox();
            // 创建地图区域材质
            let meshMaterial = new THREE.MeshStandardMaterial({
                color: "#ffffff",
                transparent: true,
                opacity: 1,
            });
            // 创建地图边界线材质
            let lineMaterial = new THREE.MeshBasicMaterial({
                color: "#ceebf7",
            });

            const mesh = new THREE.Mesh(geometry, meshMaterial);
            const line = new THREE.Mesh(tubeGeometry, lineMaterial);
            // 将省份的属性 加进来
            province.properties = elem.properties;
            province.add(mesh);
            this.boundaryLineArr.push(line);
            province.add(line);
        },
        // 给地图边界线添加outline效果
        setLineOutline() {
            //设置光晕
            this.composer = new EffectComposer(this.renderer); //效果组合器
            //创建通道
            let renderScene = new RenderPass(this.scene, this.camera);
            this.composer.addPass(renderScene);

            let outlinePass = new OutlinePass(
                new THREE.Vector2(window.innerWidth, window.innerHeight),
                this.scene,
                this.camera,
                this.boundaryLineArr
            );
            outlinePass.renderToScreen = true;
            outlinePass.edgeGlow = 2; // 光晕效果
            outlinePass.usePatternTexture = false;
            outlinePass.edgeThickness = 10; // 边框宽度
            outlinePass.edgeStrength = 1.5; // 光晕效果
            outlinePass.pulsePeriod = 0; // 光晕闪烁的速度
            outlinePass.visibleEdgeColor.set("#1acdec");
            outlinePass.hiddenEdgeColor.set("#1acdec");
            this.composer.addPass(outlinePass);
        },
        // 添加散点
        setPoint() {
            let pointTexture = new THREE.TextureLoader().load(
                require("@/assets/images/point.png")
            );
            for (let v of this.pointData) {
                let [x, y] = this.projection(v.coordinates);
                const sprite = new THREE.Sprite(
                    new THREE.SpriteMaterial({
                        map: pointTexture,
                    })
                );
                sprite.scale.set(1, 1, 1);
                sprite.position.set(x, -y, this.mapConfig.deep + 0.5);
                sprite.properties = v;
                this.pointInstanceArr.push(sprite);
                this.scene.add(sprite);
            }
        },
        // 光线投射
        setRaycaster() {
            const raycaster = new THREE.Raycaster();
            const pointer = new THREE.Vector2();
            this.$refs.page.addEventListener("click", (event) => {
                pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
                pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

                raycaster.setFromCamera(pointer, this.camera);
                const intersects = raycaster.intersectObjects(this.pointInstanceArr);
                if (intersects && intersects.length > 0) {
                    let tooltip = this.$refs.tooltip;
                    tooltip.style.left = event.pageX + "px";
                    tooltip.style.top = event.pageY + "px";
                    this.selectedPointData = intersects[0].object.properties;
                    this.show = true;
                } else {
                    this.selectedPointData = {};
                    this.show = false;
                }
            });
        },
    },
};
</script>
<style scoped>
.page {
    height: 100vh;
    /* background: url("../assets/images/bigBg.png") no-repeat top center;
    background-size: 100% 100%; */
}

.tooltip {
    position: absolute;
    background-color: #fff;
    padding: 10px;
    border-radius: 8px;
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

web网页精选

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值