一.前言


        由于最近在学习three.js,所以观摩了一下掘金,csdn等网站上的有关这部分的内容,刚好看到一个带你入门three.js——从0到1实现一个3d可视化地图 - 掘金 (juejin.cn),再加上我的专业属性是地理相关,可以说是专业对口,但文章已经是三年以前写的,而且没有在框架底下完成,有关three的很多API也发生了更改,所以我的思路是来自该篇文章,我进行了模仿和相应的修改,但是大致没有发生改变,可以说是站在前人的肩膀上。

二.预览


 

三.实现


         首先就是开启一个vue项目,再npm install --save three,再引入一下d3就可以了,配置方面没有什么好配置的,这方面大家应该是没问题的。将代码写在子组件里,再引入到App.vue中展示就可以了。需要注意用到的全国的json数据来自DataV.GeoAtlas地理小工具系列 (aliyun.com)

子组件xx.vue对应代码

<template>
  <div id="container" ref="canvasContainer"></div>
  <div id="tooltip" ref="tooltip"></div>
</template>

<script setup>
    import * as THREE from 'three';
    //OrbitControls 是一个附加组件,必须显式导入
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    //墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标,d3里面自带墨卡托投影转换
    //该引入方式是查阅官网得到的
    import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
    import { onMounted, onUnmounted,ref } from 'vue';

    let canvasContainer = ref(null);
    let tooltip = ref(null)
    let scene,camera,renderer,ambientLight,raycaster,mouse;
    let lastPick = null;

    //初始化摄像机
    function initCamera(){
        camera = new THREE.PerspectiveCamera(75,canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight, 0.1, 1000);
        camera.position.set(0,0,120);
        camera.lookAt(scene.position);
    }
    //初始化renderer
    function initRenderer(){
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(canvasContainer.value.offsetWidth,canvasContainer.value.offsetHeight)
    }
    //初始化灯光
    function initLight(){
        ambientLight = new THREE.AmbientLight(0xffffff,20);
    }
    //加载json数据
    function loadJson(){
        const loader = new THREE.FileLoader();
        loader.load('src/assets/中华人民共和国.json',(data)=>{
            const jsondata = JSON.parse(data);
            generateGeometry(jsondata)
            console.log(jsondata);
        })
    }

    // 根据JSON数据生成地图几何体
    function generateGeometry(jsondata){
        let map = new THREE.Object3D();
        // 使用d3的地图投影
        const projection = d3.geoMercator().center([104.0,37.5]).translate([0,0]);
         // 遍历每个省份,创建几何体
        jsondata.features.forEach((element)=>{
            let province = new THREE.Object3D();
            const coordinates = element.geometry.coordinates;
            if(Array.isArray(coordinates[0][0][0])){
                coordinates.forEach((multiPolygon)=>{
                    multiPolygon.forEach((polygon)=>{
                        const shape = new THREE.Shape();
                        const points = [];
                        polygon.forEach((coord,i)=>{
                            const [x,y] = projection(coord);
                            if(i===0) shape.moveTo(x,-y);
                            else shape.lineTo(x,-y);
                            points.push(new THREE.Vector3(x,-y,5));
                        })
                        const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
                        const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
                        const line = new THREE.Line(lineGeometry, lineMaterial);

                        const extrudeSettings = { depth: 10, bevelEnabled: false };
                        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
                        const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
                        const material1 = new THREE.MeshBasicMaterial({
                            color: '#3480C4',
                            transparent: true,
                            opacity: 0.5,
                        })
                        const mesh = new THREE.Mesh(geometry, [material,material1]);
                        province.properties = element.properties;
                        province.add(mesh);
                        province.add(line);
                    })
                })
            }else if(Array.isArray(coordinates[0][0])){
                coordinates.forEach((polygon)=>{
                    const shape = new THREE.Shape();
                    const points = [];
                    polygon.forEach((coord,i)=>{
                        const [x,y] = projection(coord);
                        if(i===0) shape.moveTo(x,-y);
                        else shape.lineTo(x,-y);
                        points.push(new THREE.Vector3(x,-y,5));
                    })
                    const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
                    const lineMaterial = new THREE.LineBasicMaterial({ color: 'white' });
                    const line = new THREE.Line(lineGeometry, lineMaterial);

                    const extrudeSettings = { depth: 10, bevelEnabled: false };
                    const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
                    const material = new THREE.MeshBasicMaterial({ color: '#2defff', transparent: true, opacity: 0.6 });
                    const material1 = new THREE.MeshBasicMaterial({
                        color: '#3480C4',
                        transparent: true,
                        opacity: 0.5,
                    })
                    const mesh = new THREE.Mesh(geometry, [material,material1]);
                    province.properties = element.properties;
                    province.add(mesh);
                    province.add(line);
                })
            }
            map.add(province);
        })
        scene.add(map);
    }

    // 设置光线投射器和鼠标位置,用于检测鼠标悬停对象
    function setRaycaster(){
        raycaster = new THREE.Raycaster();
        mouse = new THREE.Vector2();
        const onMouseMove = (event) => {
            mouse.x = (event.clientX / canvasContainer.value.offsetWidth) * 2 - 1
            mouse.y = -(event.clientY / canvasContainer.value.offsetHeight) * 2 + 1
            tooltip.value.style.left = event.clientX + 2 + 'px'
            tooltip.value.style.top = event.clientY + 2 + 'px'
        }

    window.addEventListener('mousemove', onMouseMove, false)
    }

    // 显示或隐藏工具提示
    function showTip(){
        if(lastPick){
            const properties = lastPick.object.parent.properties;
            tooltip.value.textContent = properties.name;
            tooltip.value.style.visibility = 'visible';
            console.log(tooltip.value.textContent);
        }else{
            tooltip.value.style.visibility = 'hidden';
        }
    }

    // 动画循环,用于渲染场景和更新状态
    function animate() {
        requestAnimationFrame(animate);
        raycaster.setFromCamera(mouse,camera);
        const intersects = raycaster.intersectObjects(scene.children,true);
        if (lastPick) {
          lastPick.object.material[0].color.set('#2defff')
          lastPick.object.material[1].color.set('#3480C4')
        }
        lastPick = null
        lastPick = intersects.find(
          (item) => item.object.material && item.object.material.length === 2
        )
        if (lastPick) {
          lastPick.object.material[0].color.set(0xff0000)
          lastPick.object.material[1].color.set(0xff0000)
        }
        showTip();
        renderer.render(scene, camera);
    }

    //窗口大小改变时,更新摄像机的宽高比和渲染器的大小
    function handleResize(){
        if(camera && renderer && canvasContainer.value){
          camera.aspect = canvasContainer.value.offsetWidth / canvasContainer.value.offsetHeight;
          camera.updateProjectionMatrix();
          renderer.setSize(canvasContainer.value.offsetWidth, canvasContainer.value.offsetHeight);
        }
    }

    // 组件挂载时的初始化逻辑
    onMounted(()=>{
        scene = new THREE.Scene();
        setRaycaster();
        initLight();
        scene.add(ambientLight);
        initCamera();
        loadJson();
        initRenderer();
        canvasContainer.value.appendChild(renderer.domElement);
        new OrbitControls(camera,canvasContainer.value)
        animate();
        window.addEventListener('resize',handleResize)
    })

    onUnmounted(()=>{
        window.removeEventListener('resize',handleResize)
    })
</script>

<style>
    body{
        margin: 0;
        padding: 0;
        overflow: hidden;
    }
    #container{
        /* border: 1px solid black; */
        width: 100vw;
        height: 100vh;
    }
    #tooltip {
        position: absolute;
        z-index: 2;
        background: white;
        padding: 10px;
        border-radius: 5px;
        visibility: hidden;
    }
</style>

注意在用JSON数据生成地图集合体时分两种情况是因为:

不同省份数据数组嵌套的层数不一样,类似于下面这两地

 

四.总结

        共勉,如果对于实现的步骤还有疑惑,可以转至我在前言分享的那篇文章 ,它对于实现步骤更详细,可以结合着看。

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐