three.js实现3D地图+地名

<template>
    <div class="page" id="page" ref="page"
        style="width: 100% !important;height: 100% !important;border:0px solid red;position: relative;">
        <div
            style="display: flex;align-items: center;font-size:12px;color:#15aef6;position: absolute;top: 8%;right:10%;cursor: pointer;">
            <div style="width: 10px;height: 10px;border-radius: 50%;background-color: #14acf5;margin-right: 8px;"></div>
            海口市企业共有 <span style="color:#fff">113</span> 家
        </div>
        <!-- <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';
import { log } from "prettier/parser-postcss";

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';


let scale;
export default {
    data() {
        return {
            materialArr: [],
            scene: null,
            camera: null,
            renderer: null,
            controls: null,
            centerCoordinate: [0, 0], // 地图中心地理坐标
            projection: null, // Mercator 投影
            mapConfig: {
                deep: 0.2, // 挤出的深度
            },
            boundaryLineArr: [], // 边界线
            composer: "", // 后期处理
            pointData: [
                {
                    coordinates: [110.102773, 19.362916], // 假设的三亚地理位置坐标,实际请核实
                    type: 1,
                    name: "屯昌县",
                    value: 100,
                },
                // {
                //     coordinates: [109.488, 18.225], // 另一个假设的三亚地理位置坐标,实际请根据具体地点调整
                //     type: 1,
                //     name: "天涯海角",
                //     value: 100,
                // },
            ],
            pointInstanceArr: [], // 坐标点实例
            show: false, // 是否显示tooltip
            selectedPointData: {}, // 选中的坐标点数据
        };
    },

    // created() {
    //     let that = this
    //     this.$nextTick(() => {
    //         // 确保fontLoader和font已定义并加载完成
    //         let fontLoader = new FontLoader();
    //         fontLoader.load('/font.json', (font) => {
    //             // this.font = font;

    //             console.log(666, font);


    //             // // 创建文本几何体
    //             let textGeometry = new TextGeometry('三亚市', {
    //                 font,
    //                 size: 0.2, // 文本大小
    //                 height: 0.1, // 文本厚度
    //             });

    //             // // 应用纹理或颜色
    //             let textMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); // 白色

    //             // // 创建文本网格
    //             let textMesh = new THREE.Mesh(textGeometry, textMaterial);

    //             // // 设置文本位置
    //             textMesh.position.set(0, 2, 0); // 例如,在z轴上10的位置

    //             // // 添加到场景
    //             that.scene.add(textMesh);
    //         });
    //     })
    // },

    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: {
        latLonToVector3(lat, lon, radius = 10) { // 假设场景中心为原点,radius为假想地球半径
            let phi = (90 - lat) * (Math.PI / 180);
            let theta = lon * (Math.PI / 180);

            let x = radius * Math.sin(phi) * Math.cos(theta);
            let y = radius * Math.cos(phi);
            let z = radius * Math.sin(phi) * Math.sin(theta);

            return new THREE.Vector3(x, y, z);
        },

        init() {
            // 计算地图中心点
            let fileLoader = new THREE.FileLoader();
            fileLoader.load("/map.json", (data) => {
                let jsondatas = JSON.parse(data);
                jsondatas.features.forEach((item, index) => {
                    if (item.properties.name == '三沙市') {
                        jsondatas.features.splice(index, 1)
                        // console.log(111, jsondatas.features[index]);
                    }
                })
                // 假设你有一个名为geojsonData的变量包含了所有地图数据
                const centerCoordinate = this.calculateCentroid(jsondatas);
                // console.log("自动计算的中心点坐标:", centerCoordinate);
                this.centerCoordinate = centerCoordinate
            });
            setTimeout(() => {
                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.scene.background = new THREE.TextureLoader().load("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.position.set(5, 5, 45);
                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();
            }, 500)
        },
        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("循环区域", 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.$nextTick(() => {
                    let fontLoader = new FontLoader();
                    fontLoader.load('/font.json', (font) => {
                        console.log(123, font);
                        let textMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); // 应用纹理或颜色 白色 


                        let textGeometry = new TextGeometry('三亚市', { font, size: 0.15, height: 0.1 }); // 创建文本几何体  
                        let textMesh = new THREE.Mesh(textGeometry, textMaterial); // 创建文本网格 
                        textMesh.position.set(-1.3, -2.5, 0.5); // 例如,在z轴上10的位置 // 设置文本位置


                        let textGeometry2 = new TextGeometry('乐东黎族自治县', { font, size: 0.15, height: 0.1 });
                        let textMesh2 = new THREE.Mesh(textGeometry2, textMaterial);
                        textMesh2.position.set(-2.8, -1.5, 0.5);

                        let textGeometry3 = new TextGeometry('东方市', { font, size: 0.15, height: 0.1 });
                        let textMesh3 = new THREE.Mesh(textGeometry3, textMaterial);
                        textMesh3.position.set(-2.8, -0.5, 0.5);

                        let textGeometry4 = new TextGeometry('保亭黎族苗族自治县', { font, size: 0.15, height: 0.1 });
                        let textMesh4 = new THREE.Mesh(textGeometry4, textMaterial);
                        textMesh4.position.set(-1, -1.6, 0.5);

                        let textGeometry5 = new TextGeometry('陵水黎族自治县', { font, size: 0.15, height: 0.1 });
                        let textMesh5 = new THREE.Mesh(textGeometry5, textMaterial);
                        textMesh5.position.set(0, -1.9, 0.5);


                        let textGeometry6 = new TextGeometry('五指山市', { font, size: 0.15, height: 0.1 });
                        let textMesh6 = new THREE.Mesh(textGeometry6, textMaterial);
                        textMesh6.position.set(-1.15, -1, 0.5);

                        let textGeometry7 = new TextGeometry('琼中黎族苗族自治县', { font, size: 0.15, height: 0.1 });
                        let textMesh7 = new THREE.Mesh(textGeometry7, textMaterial);
                        textMesh7.position.set(-0.5, -0.5, 0.5);

                        let textGeometry8 = new TextGeometry('昌江黎族自治县', { font, size: 0.15, height: 0.1 });
                        let textMesh8 = new THREE.Mesh(textGeometry8, textMaterial);
                        textMesh8.position.set(-2.6, 0.4, 0.5);

                        let textGeometry9 = new TextGeometry('白沙黎族自治区', { font, size: 0.15, height: 0.1 });
                        let textMesh9 = new THREE.Mesh(textGeometry9, textMaterial);
                        textMesh9.position.set(-1.8, -0.2, 0.5);

                        let textGeometry10 = new TextGeometry('儋州市', { font, size: 0.15, height: 0.1 });
                        let textMesh10 = new THREE.Mesh(textGeometry10, textMaterial);
                        textMesh10.position.set(-1.3, 1, 0.5);

                        let textGeometry11 = new TextGeometry('临高县', { font, size: 0.15, height: 0.1 });
                        let textMesh11 = new THREE.Mesh(textGeometry11, textMaterial);
                        textMesh11.position.set(-0.4, 1.6, 0.5);

                        let textGeometry12 = new TextGeometry('澄迈县', { font, size: 0.15, height: 0.1 });
                        let textMesh12 = new THREE.Mesh(textGeometry12, textMaterial);
                        textMesh12.position.set(0.35, 1.5, 0.5);

                        let textGeometry13 = new TextGeometry('海口市', { font, size: 0.15, height: 0.1 });
                        let textMesh13 = new THREE.Mesh(textGeometry13, textMaterial);
                        textMesh13.position.set(1.3, 1.85, 0.5);

                        let textGeometry14 = new TextGeometry('文昌市', { font, size: 0.15, height: 0.1 });
                        let textMesh14 = new THREE.Mesh(textGeometry14, textMaterial);
                        textMesh14.position.set(2.45, 1.5, 0.5);

                        let textGeometry15 = new TextGeometry('琼海市', { font, size: 0.15, height: 0.1 });
                        let textMesh15 = new THREE.Mesh(textGeometry15, textMaterial);
                        textMesh15.position.set(1.5, 0, 0.5);

                        let textGeometry16 = new TextGeometry('万宁市', { font, size: 0.15, height: 0.1 });
                        let textMesh16 = new THREE.Mesh(textGeometry16, textMaterial);
                        textMesh16.position.set(0.8, -1.2, 0.5);

                        let textGeometry17 = new TextGeometry('定安县', { font, size: 0.15, height: 0.1 });
                        let textMesh17 = new THREE.Mesh(textGeometry17, textMaterial);
                        textMesh17.position.set(1.3, 0.9, 0.5);

                        let textGeometry18 = new TextGeometry('屯昌县', { font, size: 0.15, height: 0.1 });
                        let textMesh18 = new THREE.Mesh(textGeometry18, textMaterial);
                        textMesh18.position.set(0.4, 0.3, 0.5);


                        // 添加文字到场景
                        this.scene.add(textMesh, textMesh2, textMesh3, textMesh4, textMesh5, textMesh6, textMesh7, textMesh8, textMesh9, textMesh10, textMesh11, textMesh12, textMesh13, textMesh14, textMesh15, textMesh16, textMesh17, textMesh18);
                        // this.scene.add(textMesh2);
                    })
                })




            });
            this.scene.add(this.map);
            // let that = this
            // this.$nextTick(() => {
            //     let fontLoader = new FontLoader();
            //     fontLoader.load('/font.json', (font) => {
            //         console.log(123, font);
            //         // 创建文本几何体
            //         let textGeometry = new TextGeometry('三亚市', {
            //             font,
            //             size: 0.2, // 文本大小
            //             height: 0.1, // 文本厚度
            //         });
            //         // 应用纹理或颜色
            //         let textMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); // 白色
            //         // 创建文本网格
            //         let textMesh = new THREE.Mesh(textGeometry, textMaterial);

            //         // 设置文本位置
            //         textMesh.position.set(0, 2, 0); // 例如,在z轴上10的位置

            //         // 添加文字到场景
            //         that.scene.add(textMesh);

            //         // 然后添加地图到场景

            //     });
            // })

        },
        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(.7, .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;
                }
            });
        },
        // 地图中心点计算
        calculateCentroid(geojsonData) {
            let totalLat = 0;
            let totalLon = 0;
            let count = 0;

            geojsonData.features.forEach(feature => {
                if (feature.geometry.type === 'Polygon') {
                    feature.geometry.coordinates.forEach(ring => {
                        ring.forEach(point => {
                            totalLat += point[1];
                            totalLon += point[0];
                            count++;
                        });
                    });
                } else if (feature.geometry.type === 'MultiPolygon') {
                    feature.geometry.coordinates.forEach(polygon => {
                        polygon.forEach(ring => {
                            ring.forEach(point => {
                                totalLat += point[1];
                                totalLon += point[0];
                                count++;
                            });
                        });
                    });
                }
            });

            // 计算平均经纬度
            const avgLat = totalLat / count;
            const avgLon = totalLon / count;

            return [avgLon, avgLat]; // 返回经度在前,纬度在后,符合大多数GIS标准
        },

    },
};
</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>

 需要注意的点:

首先下载依赖

npm i three
npm i d3

1.下载的字体文件需要转换为JSON格式的:转换网址Facetype.js

2.three.module.js这个文件记得加上这4行代码

3.项目中的图片

上一篇:【微信小程序】事件传参的两种方式


下一篇:大型零售企业总部到分公司数据发放,有没有更优化的方案?