threejs achieve 3d administrative map

47 阅读2分钟

help me use geoJSON + d3-geo + threejs to achieve 3d administrative map display on coplilot and cursor

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Administrative Map</title>
    <script src="./js/three.js"></script>
    <script src="./js/OrbitControls.js"></script>
    <script src="./js/d3-geo.v1.min.js"></script>
    <style>
        body {
            margin: 0;
        }

        #webgl {
            width: 100vw;
            height: 100vh;
        }

        .tooltip {
            position: absolute;
            padding: 10px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            border-radius: 4px;
            font-size: 14px;
            pointer-events: none;
            display: none;
        }
    </style>
</head>

<body>
    cursor
    <div id="webgl"></div>
    <div class="tooltip"></div>
    <script>
        // 初始化场景、相机和渲染器
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        document.getElementById('webgl').appendChild(renderer.domElement);

        // 添加轨道控制器
        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;

        // 设置相机位置
        camera.position.set(0, 0, 100);

        // 添加环境光和平行光
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
        scene.add(ambientLight);
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
        directionalLight.position.set(10, 10, 10);
        scene.add(directionalLight);

        // 创建投影
        const projection = d3.geoMercator()
            .center([113.280637, 23.125178])
            .scale(100)
            .translate([0, 0]);

        // 用于鼠标拾取
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
        const tooltip = document.querySelector('.tooltip');

        // 颜色数组
        const colors = [
            0x66c2a5, 0xfc8d62, 0x8da0cb, 0xe78ac3,
            0xa6d854, 0xffd92f, 0xe5c494, 0xb3b3b3
        ];

        // 加载GeoJSON数据
        const loader = new THREE.FileLoader();
        const meshes = new Map(); // 存储mesh和对应的区域信息

        // loader.load('./json/gz.json', function(data) {
        loader.load('./json/china.json', function (data) {
            const json = JSON.parse(data);

            json.features.forEach((feature, index) => {
                if (feature.geometry) {
                    const shape = new THREE.Shape();
                    let isFirst = true;

                    // 处理多边形数据
                    feature.geometry.coordinates[0][0].forEach(coord => {
                        const [x, y] = projection(coord);
                        if (isFirst) {
                            shape.moveTo(x, y);
                            isFirst = false;
                        } else {
                            shape.lineTo(x, y);
                        }
                    });

                    // 随机生成区域高度(实际项目中可以根据真实数据设置)
                    const height = Math.random() * 5 + 1;

                    // 创建拉伸几何体
                    const extrudeSettings = {
                        depth: height,
                        bevelEnabled: false
                    };
                    const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);

                    // 创建材质
                    const material = new THREE.MeshPhongMaterial({
                        color: colors[index % colors.length],
                        transparent: true,
                        opacity: 0.8,
                        side: THREE.DoubleSide
                    });

                    // 创建网格并添加到场景
                    const mesh = new THREE.Mesh(geometry, material);
                    mesh.rotation.x = Math.PI;

                    // 存储原始颜色
                    mesh.userData.originalColor = material.color.getHex();
                    mesh.userData.name = feature.properties.name || `区域 ${index + 1}`;
                    mesh.userData.value = height.toFixed(1); // 存储随机生成的高度值

                    meshes.set(mesh.id, mesh);
                    scene.add(mesh);

                    // 添加文字标签(这里使用简单的HTML元素作为标签)
                    const [labelX, labelY] = projection(feature.geometry.coordinates[0][0][0]);
                    const label = document.createElement('div');
                    label.className = 'map-label';
                    label.style.position = 'absolute';
                    label.style.color = 'white';
                    label.style.fontSize = '12px';
                    label.textContent = mesh.userData.name;
                    document.body.appendChild(label);
                }
            });

            // 渲染循环
            function animate() {
                requestAnimationFrame(animate);
                controls.update();
                renderer.render(scene, camera);
            }
            animate();
        });

        // 鼠标移动事件处理
        function onMouseMove(event) {
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(scene.children);

            // 重置所有mesh的颜色
            meshes.forEach(mesh => {
                mesh.material.color.setHex(mesh.userData.originalColor);
            });

            if (intersects.length > 0) {
                const intersected = intersects[0].object;
                // 高亮选中的区域
                intersected.material.color.setHex(0xff0000);

                // 显示工具提示
                tooltip.style.display = 'block';
                tooltip.style.left = event.clientX + 10 + 'px';
                tooltip.style.top = event.clientY + 10 + 'px';
                tooltip.innerHTML = `
                    区域:${intersected.userData.name}<br>
                    数值:${intersected.userData.value}
                `;
            } else {
                tooltip.style.display = 'none';
            }
        }

        // 窗口大小调整处理
        window.addEventListener('resize', onWindowResize, false);
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        // 添加鼠标事件监听
        window.addEventListener('mousemove', onMouseMove, false);
    </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D 行政区划地图展示</title>

  <!-- 引入 Three.js、OrbitControls、d3‐geo -->
  <script src="./js/three.js"></script>
  <script src="./js/OrbitControls.js"></script>
  <script src="./js/d3-geo.v1.min.js"></script>

  <style>
    /* 让 #webgl 占满全屏 */
    body,
    html {
      margin: 0;
      overflow: hidden;
      width: 100%;
      height: 100%;
    }

    #webgl {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }

    /* tooltip 样式 */
    .tooltip {
      position: absolute;
      pointer-events: none;
      padding: 4px 8px;
      background-color: rgba(0, 0, 0, 0.7);
      color: #fff;
      font-size: 13px;
      border-radius: 3px;
      white-space: nowrap;
      display: none;
      transform: translate(-50%, -120%);
      z-index: 10;
    }
  </style>
</head>

<body>
  chatgpt
  <!-- Three.js 渲染容器 -->
  <div id="webgl"></div>
  <!-- 悬浮在页面上,用来显示省名 -->
  <div class="tooltip"></div>

  <script>
    // —— 一、基础场景初始化 —— 
    const container = document.getElementById('webgl');
    const tooltip = document.querySelector('.tooltip');

    // 场景、相机、渲染器
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      5000
    );
    // 将相机稍微抬高并倾斜,居中看向 (0,0,0)
    // camera.position.set(0, -800, 600);
    camera.position.set(0, 0, 1500);
    camera.lookAt(scene.position);

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    container.appendChild(renderer.domElement);

    // OrbitControls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.1;
    controls.rotateSpeed = 0.6;
    controls.minDistance = 200;
    controls.maxDistance = 2000;

    // 环境光 + 方向光
    const ambientLight = new THREE.AmbientLight(0x888888);
    scene.add(ambientLight);
    const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
    dirLight.position.set(100, -200, 300);
    scene.add(dirLight);

    // Raycaster 用于拾取
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    // 窗口大小变化时,更新 renderer 和 camera
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // —— 二、定义 D3 投影 —— 
    // 这里使用“墨卡托投影” (geoMercator)。中心坐标设为中国大致中心 (104E, 35N)。scale 数值可调。
    const projection = d3.geoMercator()
      .center([104, 35])
      .scale(800)       // 缩放比例:可根据 GeoJSON 范围做微调
      .translate([0, 0]); // translate(0,0),因为后面我们不做 DOM 渲染,只读投影值

    // —— 三、加载 GeoJSON 并构造几何体 —— 
    const loader = new THREE.FileLoader();
    loader.load(
      './json/china.json',
      (data) => {
        const geojson = JSON.parse(data);
        addGeoJSONToScene(geojson);
      },
      undefined,
      (err) => {
        console.error('GeoJSON 加载错误:', err);
      }
    );

    /**
     * 遍历 geojson.features,把每个省份挤压成 3D 形状加入场景
     * @param {Object} geojson 整个 GeoJSON 对象
     */
    function addGeoJSONToScene(geojson) {
      const features = geojson.features;

      // 遍历每个要素(省/市)
      features.forEach((feature) => {
        const prop = feature.properties || {};
        const name = prop.name || prop.NAME || '未知地区'; // 常见属性字段:name、NAME 等

        // 不同类型做不同处理:Polygon / MultiPolygon
        if (feature.geometry.type === 'Polygon') {
          const lst = [feature.geometry.coordinates];
          buildPolygon(lst, name);
        } else if (feature.geometry.type === 'MultiPolygon') {
          buildPolygon(feature.geometry.coordinates, name);
        }
      });
    }

    /**
     * 传入一组 polygon 数组 (可能包含多个 ring),把所有环都做成 THREE.Shape 并挤压
     * @param {Array} polygons  格式为 [ [ [ [lon, lat], ... ], [ /* hole? */

    function buildPolygon(polygons, name) {
      polygons.forEach((polygon) => {
        // polygon 本身是一个“环的数组”,第一个环是外轮廓,后面可能是 hole(但大部分行政区通常没洞)
        const outerRing = polygon[0]; // [ [lon, lat], [lon, lat], ... ]
        // D3 投影:lon,lat → [x, y]
        const shapePoints = outerRing.map((coord) => {
          const [lon, lat] = coord;
          const [x, y] = projection([lon, lat]);
          // Three.js 中 Y 轴默认朝上,D3 投影的 y 轴向下,所以这里把 y 取反
          return new THREE.Vector2(x, -y);
        });

        // 构造 Shape
        const shape = new THREE.Shape(shapePoints);

        // TODO: 如果需要在省内部做“挖洞”,可以参考 polygon.slice(1) 里的 hole 环,
        //      并用 shape.holes.push(new THREE.Path(holePoints)) 来做。但这里暂不演示。

        // 挤压设置:depth = 拉伸高度,bevelEnabled = false 保持直边
        const extrudeSettings = {
          depth: 8,          // 挤压高度:可以根据需求自己调(8 个单位是示例)
          bevelEnabled: false
        };

        // 生成几何体
        const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);

        // 给每个省份随机一个较为“柔和”的颜色,方便区分
        const color = new THREE.Color(`hsl(${Math.random() * 360}, 50%, 60%)`);
        const material = new THREE.MeshLambertMaterial({
          color: color,
          transparent: false,
          opacity: 1.0,
          side: THREE.DoubleSide
        });

        const mesh = new THREE.Mesh(geometry, material);

        // 将每个省份名称存入 userData,供后续 tooltip 使用
        mesh.userData.name = name;
        // 为了让地图被相机居中,我们可以把 geometry 平移一下:让所有点的中心大致在 (0,0)
        // 这里简化:不做额外平移,后面可以手动调相机或 OrbitControls 来看整个地图

        scene.add(mesh);
      });
    }

    // —— 四、交互:鼠标移动时用 Raycaster 拾取 —— 
    function onMouseMove(event) {
      // 计算归一化设备坐标 (NDC):[-1,1]
      const rect = renderer.domElement.getBoundingClientRect();
      mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
      mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);

      // 拾取所有场景中的可见对象(如果你场景里只有省份 Mesh,也可直接 scene.children)
      const intersects = raycaster.intersectObjects(scene.children, true);

      if (intersects.length > 0) {
        const picked = intersects[0].object;
        const name = picked.userData.name || '未知区域';
        // 在鼠标附近显示 tooltip
        tooltip.style.display = 'block';
        tooltip.innerText = name;
        tooltip.style.left = event.clientX + 'px';
        tooltip.style.top = event.clientY + 'px';
      } else {
        tooltip.style.display = 'none';
      }
    }
    renderer.domElement.addEventListener('mousemove', onMouseMove, false);

    // —— 五、渲染循环 —— 
    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();
  </script>
</body>

</html>