Skip to content

186. 球面地图可视化

Published:

前面做的都是平面地图上的可视化:

2025-08-09 20.33.23.gif

2025-08-17 22.52.06.gif

有的时候我们需要在球面上做可视化效果。

比如这样:

image.png

那如何把经纬度转成三维空间球面上的坐标呢?

记得我们是如何把经纬度转成平面坐标的么?

是用墨卡托投影。

而这里也有对应的算法。

我们不需要理解如何转换,直接用就行。

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
    }
}

就像你用墨卡托投影的时候,去了解它的原理了么?没有,这里当然也不用了解原理。

我们来写一下:

npx create-vite spherical-world-map

image.png

创建项目,进入项目,安装依赖:

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

改下 src/main.js

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

const scene = new THREE.Scene();

scene.add(mesh);

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

const light2 = new THREE.AmbientLight();
scene.add(light2);

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

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

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

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

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

render();

document.body.append(renderer.domElement);

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

创建 Scene、Light、Camera、Renderer

改下 style.css

body {
    margin: 0;
}

然后创建 mesh.js

import * as THREE from 'three';

const worldMap = new THREE.Group();


export default worldMap;

跑一下:

npm run dev

image.png

image.png

这次我们画世界地图:

从这里下载 geojson:

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

image.png

选择低分辨率、全部地区,然后点击右下角的按钮下载 geojson。

把它放到 public 目录下:

image.png

我们先用墨卡托投影在平面上把它画出来:

安装下 d3-geo

pnpm install --save d3-geo
pnpm install --save-dev @types/d3-geo

和之前画中国地图一样:

import * as THREE from 'three';
import { geoMercator } from 'd3-geo';

const worldMap = new THREE.Group();

const mercator = geoMercator()
    .center([105,34]).translate([0, 0]).scale(100)

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

    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] = mercator(point);
            vertices.push(x, -y, 0);
        });
        const attribute = new THREE.Float32BufferAttribute(vertices, 3);;
        bufferGeometry.attributes.position = attribute;

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

    return group;
}

export default worldMap;

只不过数据不一样,画法是一样的。

看下效果:

2025-08-23 20.31.25.gif

然后我们用前面那个经纬度转三维坐标的算法来算一下:

image.png

image.png

其他的不变,只不过把经纬度转二维坐标,换成了经纬度转三维坐标。

const {x,y,z} = lon2xyz(200, point[0], point[1]);
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
    }
}

看下效果:

2025-08-23 21.02.15.gif

现在就是球面上的地图效果了。

我们在下面画一个球:

image.png

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

worldMap.add(createBall());

2025-08-23 21.05.27.gif

这样,地球的效果就出来了。

案例代码上传了小册仓库

总结

这节我们用 geojson 画出了地球的效果。

之前我们都是用墨卡托投影把经纬度转为平面坐标,画二维地图。

现在我们学了新的算法,来把经纬度转为球面上的三维坐标。

之前的柱子、飞线等效果,当然也可以用在这个球面上。

其实球面的地图可视化、平面的地图可视化,原理都差不多。

评论