Skip to content

187. 实战:交互式地球仪

Published:

这节我们来做一个实战:交互式地球仪。

我们知道,地球仪上有各个国家的信息:

image.png

我们想找某个国家,可以转动地球仪慢慢来找。

这节我们来做一个可自动定位国家位置的地球仪。

创建项目:

npx create-vite interactive-globe

image.png

因为我们要用 div +css 来展示国家的名字,以及一个搜索框,所以选了 react 作为前端框架来开发。

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three

去掉 StrictMode 和 index.css

image.png

然后改一下 App.jsx

import { useEffect, useRef, useState } from 'react';
import { init } from './3d-init'
import './App.css'

function App() {

  useEffect(() => {
    const dom = document.getElementById('content');
    const { scene } = init(dom);
  
    return () => {
      dom.innerHTML = '';
    }
  }, []);

  return <div>
    <div id="main">
      <div id="content">
      </div>
    </div>
  </div>
}

export default App

在 App.css 写下样式:

body {
  margin: 0;
}

然后来初始化 3d 场景:

创建 3d-init.js

import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh';

export function init(dom) {

    const scene = new THREE.Scene();
    scene.add(mesh);

    const axesHelper = new THREE.AxesHelper(500);
    scene.add(axesHelper);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(500, 400, 300);
    scene.add(directionalLight);

    const ambientLight = new THREE.AmbientLight(0xffffff);
    scene.add(ambientLight);

    const width = window.innerWidth;
    const height = window.innerHeight;

    const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
    camera.position.set(0, 500, 500);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({
        antialias: true
    });
    renderer.setSize(width, height);

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

    function render(time) {
        controls.update(time);
        renderer.render(scene, camera);
        requestAnimationFrame(render);
    }

    render();

    dom.append(renderer.domElement);

    window.onresize = function () {
        const width = window.innerWidth;
        const height = window.innerHeight;

        renderer.setSize(width,height);

        camera.aspect = width / height;
        camera.updateProjectionMatrix();
    };

    return {
        scene,
        renderer,
        controls
    }
}

创建 mesh.js

import * as THREE from 'three';

const group = new THREE.Group();

const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshLambertMaterial({
    color: 'orange'
});
const mesh = new THREE.Mesh(geometry, material);

group.add(mesh);

export default group;

我们先跑起来看下:

npm run dev

image.png

2025-08-24 09.50.01.gif

然后先把地球画出来:

改下 mesh.js

import * as THREE from 'three';

const worldMap = new THREE.Group();

const loader = new THREE.FileLoader();
loader.load('./world.geo.json', function (data) {
    const geojson = JSON.parse(data);

    geojson.features.forEach(feature => {
        const province = new THREE.Group();

        if (feature.geometry.type === 'Polygon') {
            const polygon = createPolygon(feature.geometry.coordinates);
            province.add(polygon);
        } else if (feature.geometry.type === 'MultiPolygon') {
            feature.geometry.coordinates.forEach(polygonCoords => {
                const polygon = createPolygon(polygonCoords);
                province.add(polygon);
            });
        }

        worldMap.add(province);
    });
});

function createPolygon(coordinates) {
    const group = new THREE.Group();
    
    coordinates.forEach(item => {
        const bufferGeometry = new THREE.BufferGeometry();
        const vertices = [];
        item.forEach(point => {
            const {x,y,z} = lon2xyz(300, point[0], point[1]);
            vertices.push(x, y, z);
        });
        const attribute = new THREE.Float32BufferAttribute(vertices, 3);;
        bufferGeometry.attributes.position = attribute;

        const lineMaterial = new THREE.LineBasicMaterial({ 
            color: 'blue' 
        });
        const line = new THREE.Line(bufferGeometry, lineMaterial);
        group.add(line);
    });

    return group;
}

function lon2xyz(R, longitude, latitude) {
    let lon = -longitude * Math.PI / 180;
    let lat = latitude * Math.PI / 180;

    const x = R * Math.cos(lat) * Math.cos(lon);
    const y = R * Math.sin(lat);
    const z = R * Math.cos(lat) * Math.sin(lon);

    return {
        x,
        y,
        z
    }
}

function createBall() {
    const geometry = new THREE.SphereGeometry(300);
    const material = new THREE.MeshBasicMaterial({
        color: 'white'
    });
    const ball = new THREE.Mesh(geometry, material);
    return ball;
}

worldMap.add(createBall());

export default worldMap;

这是上节的代码,就不展开解释了。

还是从这里下载世界地图的 geojson

https://geojson-maps.kyd.au/

image.png

放 pulic 目录下:

image.png

看下效果:

2025-08-24 09.56.22.gif

但只有轮廓线太单调了。

如何让国家轮廓内有背景呢?

找张贴图就好了:

image.png

下载放到 public 目录下:

image.png

代码里加载下:

image.png

function createBall() {
    const loader = new THREE.TextureLoader();
    const texture = loader.load('./worldmap.jpeg');
    texture.colorSpace = THREE.SRGBColorSpace;

    const geometry = new THREE.SphereGeometry(300);
    const material = new THREE.MeshBasicMaterial({
        map: texture
    });
    const ball = new THREE.Mesh(geometry, material);
    return ball;
}

看下效果:

2025-08-24 10.08.49.gif

可以看到,纹理贴图的国家背景和我们绘制的轮廓线都严丝合缝的。

因为都是根据经纬度来的,所以自然能对上。

改一下线的颜色:

image.png

我这里把 BufferGeometry + Line 换成了 LineGeometry + Line2,这样可以设置线宽。

function createPolygon(coordinates) {
    const group = new THREE.Group();
    
    coordinates.forEach(item => {
        const geometry = new LineGeometry();
        const vertices = [];
        item.forEach(point => {
            const {x,y,z} = lon2xyz(300, point[0], point[1]);
            vertices.push(new THREE.Vector3(x, y, z));
        });
        geometry.setFromPoints(vertices);

        const lineMaterial = new LineMaterial({ 
            color: 'white',
            linewidth: 2
        });
        const line = new Line2(geometry, lineMaterial);
        group.add(line);
    });

    return group;
}

看下效果:

2025-08-24 10.16.34.gif

现在轮廓线就清晰多了。

案例代码上传了小册仓库

总结

这节我们把地球画了出来。

地球的纹理贴图 + 根据 geojson 画的国家边界线,就可以把各个国家从地球上标出来。

下节我们把国家信息详细的标识出来。

评论