用 Three.js 手搓一个 GIS 引擎(一):总览、架构与坐标系统

用 Three.js 手搓一个 GIS 引擎(一):总览、架构与坐标系统

系列导航:本文是《用 Three.js 手搓一个 GIS 引擎》系列的第一篇。后续会依次讲 栅格瓦片与请求调度 → 地形引擎 → 3D Tiles 与白模 → 矢量/图元/绘制 → 自研 LOD 数据管线。

这个库叫 three-gis (源自 next-three 项目的 lib/ 封装),框架无关,运行时除了 three 没有别的依赖。

为什么要"手搓"一个 GIS 库?

Three.js 擅长画,但不懂"地理"。把经纬度、瓦片、地形、3D Tiles 这些 GIS 概念硬塞进一个裸 Three.js 场景,新手最容易卡在三件事上:

  1. 坐标:经纬度是度数,瓦片是像素金字塔,相机是世界坐标------三者怎么对齐?
  2. 精度:直接拿 WGS84 的米级 ECEF 坐标当 Three.js 顶点,几公里外就开始抖(浮点误差)。
  3. 调度:瓦片、地形、3D Tiles 都要按视口动态加载/卸载,否则内存和请求数立刻爆。

three-gis 就是围绕这三个痛点搭的一套轻量引擎。它不是 Cesium 的替代品,更像"当你只需要 Three.js 那种渲染自由度、又想要 GIS 图层能力时"的中间层。

一、能力地图(本系列会逐一拆开讲)

能力 核心模块 一句话说明
坐标系 WebMercatorGIS WGS84 ↔ Web Mercator ↔ 局部 Z-up,用局部原点抗浮点误差
运行时 Scene / Map / CameraController Three.js 场景 + 相机飞行 + Z-up GIS 控制器
栅格瓦片 TileLayer / TMS / WMS / WMTS XYZ/Google、TMS、WMS、WMTS 全覆盖,带请求调度
地形 CesiumTerrainLayer Cesium quantized-mesh,Worker 解压 + 影像贴合
3D Tiles Tiles3DLayer b3dm / glTF、屏幕空间误差调度、白模样式
矢量/图元/绘制 GeoJSONLayer / Primitive* / DrawingManager 水面/道路/铁路、点线面体图元、交互绘制
LOD 数据管线 lib/sources/lod 采集 → 点云(pnts) → 倾斜摄影网格(b3dm) 自研闭环

二、三行代码跑起来

库提供了 Cesium 风格的一站式入口 createViewer,把"建场景 / 装图层 / 起帧循环"都包了:

ts 复制代码
import { createViewer } from "three-gis";

const viewer = await createViewer("app", {
  center: [116.4, 39.9],                       // 北京
  height: 1_500_000,                            // 视高(米)
  imagery: "https://mt{s}.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
});

viewer.flyTo({ center: [118.1371, 24.49], height: 120_000 }); // 飞到厦门

createViewer 内部依次做了:按 center 派生局部原点 → 建 Scene → 等渲染器就绪 → 装配影像/地形/3D Tiles/GeoJSON 图层 → 设置视口 → 注册帧循环(按节流间隔推动图层选片与更新)。

如果你想要更细的控制,可以绕过 createViewer 直接用底层原语:

ts 复制代码
import { Scene } from "three-gis";
import { WebMercatorGIS } from "three-gis";

const gis = new WebMercatorGIS(118.1371, 24.49);
const scene = new Scene(container, { gis });

await scene.ready();
scene.flyTo(118.1371, 24.49, 10000);

// React/Vue 的 cleanup 里别忘了销毁
scene.destroy();

⚠️ 三维运行时代码依赖浏览器 API,必须在 Client Component 的 useEffect/onMounted 里初始化并清理,不要在 SSR 阶段调用。

三、坐标系统:整套引擎的地基

这是 GIS 库最容易劝退人的地方,也是 three-gis 设计上最值得讲的一点。

1. 三套坐标,一个转换中枢

WebMercatorGIS 把三套坐标统一管起来:

  • WGS84 经纬度:人类语言,度。
  • Web Mercator 米:球面墨卡托投影,单位米,全球统一。
  • 局部 Three.js 坐标 :以某个原点为中心的小范围坐标,Z-up(X=东,Y=北,Z=上)。
ts 复制代码
const gis = new WebMercatorGIS(118.1371, 24.49); // 以厦门为局部原点

// 经度/纬度/海拔(米) → 局部坐标(X 东, Y 北, Z 上)
const p = gis.lngLatToThree(118.14, 24.50, 120);
// 局部坐标 → 经纬度海拔
const ll = gis.threeToLngLat(p);

墨卡托转换就是经典公式(库里也是这么实现的):

ts 复制代码
// 经纬度 → Web Mercator 米
const mx = R * deg2rad(lng);
const my = R * Math.log(Math.tan(Math.PI / 4 + deg2rad(lat) / 2));

// Web Mercator 米 → 局部坐标(减去原点)
return new THREE.Vector3(mx - originMx, my - originMy, altitude);

2. 为什么一定要"局部原点"?

这是新手最大的坑。不要把全球米坐标直接喂给 Three.js。

WebGL 顶点是 Float32,有效精度大约 7 位十进制。ECEF 或全球墨卡托坐标动辄 6,000,000 米量级,尾数只能表示到分米甚至米级,相机稍微拉远模型就"抽搐"。

three-gis 的做法是:把局部原点设在关注区域附近(比如城市中心),所有顶点只存"离原点的偏移",数值通常在几百到几万米内,精度立刻回到毫米级。

ts 复制代码
// 好:原点就在目标城市,顶点数值小
const gis = new WebMercatorGIS(118.1371, 24.49);

// 坏:原点放 (0,0) 赤道原点,厦门的顶点都会带着 6,000,000 的偏移
const gisBad = new WebMercatorGIS(0, 0);

createViewer 里 origin 默认等于 center,就是这个道理。只有当你要在"大范围漫游且需要更小浮点误差"时才需要单独指定 origin。

3. 相机与控制器:Z-up 的 GIS 视角

Three.js 默认 Y-up,但地理场景天然是 Z-up(海拔向上)。three-gis 的 GISOrbitController 直接按 Z-up 建模,并提供地理语义的视口接口:

ts 复制代码
// setView / flyTo 用的是地理参数,不用你手写相机矩阵
viewer.setView({ center: [116.4, 39.9], height: 5000, pitch: 45, heading: 30 });
// pitch=90 正俯视,pitch=45 斜视;heading=0 正北朝上

局部坐标系约定:X 向东、Y 向北、Z 向上 。比如相机在目标正南、俯视看正北,就是 offset = (-sin(bearing)·h, -cos(bearing)·h, height)------这层换算 createViewer 已经帮你封装好了。

四、本篇亮点小结

  • 框架无关 :除了 three 零运行时依赖,原生 HTML / Vue / React 都能用,可独立打包成 npm 包。
  • 坐标中枢单一 :所有"地理 → 屏幕"的转换都收口在 WebMercatorGIS,避免各模块各算各的。
  • 局部原点抗精度:这是让 Three.js 扛得住真实地理尺度的关键一招,后面所有图层都建立在这套局部 Z-up 坐标上。

下一篇预告

有了坐标地基,下一步自然是往场景里"铺地砖"------栅格瓦片 。下一篇《栅格瓦片与请求调度》会讲清楚 XYZ/Google、TMS、WMS、WMTS 四种协议怎么统一封装,以及库自带的 RequestScheduler(按域名限并发、优先级、取消、子域轮换)和"入场节拍"淡入这种体验细节。


如果你对某一篇的主题想提前看,或者觉得这个拆分粒度不合适,欢迎留言告诉我。

相关推荐
Chen7322 天前
一套飞控内核,两个渲染后端:three.js 与 CesiumJS 同构实践
three.js
夏天要喝冰可乐3 天前
用 Codex 连接 Blender,我做了一块可以走进楼宇的大屏
前端·three.js
ab1237685 天前
three.js 完整笔记
笔记·three.js
答案answer8 天前
不用写一行Three.js代码,也能搭出复杂数字孪生场景
github·three.js
纸片人9 天前
甲方只给了一张卫星截图,我用 Three.js + Blender 建了一座 3D 能源站
three.js·数据可视化
纸片人9 天前
从一座城市钻进一个房间:Three.js + Blender 做一个五级钻取的数字楼宇大屏
three.js·数据可视化
妖精的羽翼9 天前
Web3D(Three.js)应用分级 + 学习路线规划(由浅入深,贴合接单 + 求职)
学习·three.js
踩着两条虫10 天前
我在浏览器里“盖”了一个村:Vue + Three.js 大屏开发踩坑记
前端·vue.js·three.js
子龙_10 天前
来在网页上用threejs画个蛋白质吧
前端·webgl·three.js