Three.js官网进阶难?GIS三维可视化实战技巧与源码解析(附:WebGIS开发路线图)

编程与开发
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

如果你觉得Three.js官网进阶难?GIS三维可视化实战技巧与源码解析(附:WebGIS开发路线图)这个问题正好戳中痛点,通常不是因为 Three.js 本身“不适合 GIS”,而是因为官网示例更偏通用三维图形,而 GIS 三维可视化还额外涉及坐标转换、地图底图、地形、瓦片、海量要素、相机控制和空间数据组织。

本文以 WebGIS 开发者视角,拆解 Three.js 在 GIS 三维场景中的常见用法:如何理解官网示例、如何把经纬度数据放进三维场景、如何组织建筑物和轨迹数据、如何优化性能,并给出一条适合 GIS 学习者的 WebGIS 开发路线图。

Three.js GIS三维可视化与WebGIS开发路线图示意
Three.js 做 GIS 三维可视化时,核心流程是空间数据处理、坐标转换、三维对象构建和 WebGIS 场景交互。

引言:为什么 Three.js 官网看懂了,GIS 三维项目还是做不出来

很多 GIS 学生和 WebGIS 初学者会遇到一个相似的问题:Three.js 官网示例能跑起来,旋转立方体、加载模型、添加灯光都没问题,但一到“把真实地理数据做成三维地图”,马上卡住。

原因很简单:Three.js 解决的是浏览器三维渲染问题,而 GIS 三维可视化解决的是“空间数据如何被正确表达”的问题。前者关注几何体、材质、相机、灯光;后者还要关注坐标系、投影、瓦片、地形、数据精度、空间索引和交互查询。

因此,学习 Three.js 做 GIS 三维可视化,不能只照着官网案例逐行改代码,而要把官网示例转译成 GIS 工作流。

背景:Three.js 在 WebGIS 三维可视化中的典型应用

在 GIS 项目中,Three.js 常用于以下场景:

  • 城市建筑物白模展示,例如根据楼层数或高度字段进行建筑拉伸。
  • 三维轨迹可视化,例如车辆轨迹、飞行航线、物流路径。
  • 点云、模型或传感器数据展示,例如设备位置、管线节点、监测站点。
  • 专题三维地图,例如人口密度柱状图、用地强度立体图、灾害风险三维表达。
  • 与 Leaflet、OpenLayers、Mapbox GL JS 或 Cesium 的混合使用。

需要注意的是,Three.js 不是完整的 GIS 平台。它没有内置坐标系管理、空间查询、瓦片调度和地形切片体系。GIS 开发者使用 Three.js 时,通常要自己补齐这些环节,或者与成熟 WebGIS 框架配合使用。

原理:从 Three.js 官网示例到 GIS 三维场景的关键转换

1. 官网坐标是局部坐标,GIS 数据是地理坐标

Three.js 官网案例中的坐标通常是这样的:

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(0, 0, 0);

这里的 0、1、100 都是 Three.js 场景中的局部坐标,不代表经纬度,也不代表米。而 GIS 数据通常是:

  • 经纬度坐标,例如 EPSG:4326,单位是度。
  • Web Mercator 坐标,例如 EPSG:3857,单位接近米。
  • 地方投影坐标,例如 CGCS2000、高斯投影等,单位通常是米。

GIS 三维可视化的第一步,就是把真实空间坐标转换成适合 Three.js 渲染的局部坐标。

2. 不要直接把经纬度当成 Three.js 坐标

一个常见错误是把经纬度直接写进 position:

mesh.position.set(116.391, 39.907, 50);

这在视觉上也许能显示一个物体,但它没有正确表达空间距离。经纬度的单位是度,不同纬度下 1 度经度对应的实际距离不同。做建筑、轨迹、缓冲区、三维柱状图时,这会导致比例失真。

更合理的做法是先把经纬度转换为投影坐标,再减去一个场景原点,得到局部坐标。

function lonLatToMercator(lon, lat) {
  const x = lon * 20037508.34 / 180;
  let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
  y = y * 20037508.34 / 180;
  return { x, y };
}

const origin = lonLatToMercator(116.391, 39.907);

function toScenePosition(lon, lat, height = 0) {
  const p = lonLatToMercator(lon, lat);
  return {
    x: p.x - origin.x,
    y: height,
    z: -(p.y - origin.y)
  };
}

这里把投影后的 x 作为 Three.js 的 x,把高度作为 y,把投影 y 取反后作为 z,是为了更符合 Three.js 中常见的“水平面为 x-z,高度为 y”的场景习惯。

3. Three.js 的相机不是地图视图,需要自己设计交互

GIS 地图中常见的缩放、平移、旋转,在 Three.js 中主要由相机和控制器实现。最常用的是 OrbitControls:

const camera = new THREE.PerspectiveCamera(
  60,
  window.innerWidth / window.innerHeight,
  1,
  100000
);

camera.position.set(800, 600, 800);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);

但 OrbitControls 并不等于 WebGIS 地图交互。它适合浏览三维场景,但不自动支持比例尺、地图级别、瓦片加载范围和地理定位。如果项目强调地图业务能力,需要考虑与 Mapbox GL JS、OpenLayers 或 Cesium 配合。

步骤:用 Three.js 实现一个 GIS 三维建筑白模

步骤一:准备 GeoJSON 建筑面数据

假设我们有一个建筑物 GeoJSON,每个面要素包含高度字段 height:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "Building A",
        "height": 45
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [116.3910, 39.9070],
            [116.3915, 39.9070],
            [116.3915, 39.9074],
            [116.3910, 39.9074],
            [116.3910, 39.9070]
          ]
        ]
      }
    }
  ]
}

真实项目中,建筑数据可以来自城市基础地理数据、OSM、规划审批数据或倾斜摄影提取结果。初学阶段建议先用少量简单 Polygon 测试,不要一开始就加载整个城区。

步骤二:创建 Three.js 基础场景

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f7fa);

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

const camera = new THREE.PerspectiveCamera(
  60,
  window.innerWidth / window.innerHeight,
  1,
  200000
);

camera.position.set(1000, 800, 1000);

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

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

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

这里的关键不是“能不能显示立方体”,而是给后续 GIS 数据预留足够大的 near、far 裁剪范围,并使用合适的光照让建筑体块更容易辨认。

步骤三:把 GeoJSON 面转换为 Shape

Three.js 的二维轮廓可以用 Shape 表达,再通过 ExtrudeGeometry 拉伸成三维体块。

function polygonToShape(coordinates) {
  const ring = coordinates[0];
  const shape = new THREE.Shape();

  ring.forEach((coord, index) => {
    const pos = toScenePosition(coord[0], coord[1], 0);
    if (index === 0) {
      shape.moveTo(pos.x, pos.z);
    } else {
      shape.lineTo(pos.x, pos.z);
    }
  });

  return shape;
}

注意,这里 Shape 的二维坐标使用的是 x 和 z,后续再通过旋转或坐标映射让建筑沿 y 轴表示高度。

步骤四:根据高度字段拉伸建筑

function createBuilding(feature) {
  const height = Number(feature.properties.height || 10);
  const shape = polygonToShape(feature.geometry.coordinates);

  const geometry = new THREE.ExtrudeGeometry(shape, {
    depth: height,
    bevelEnabled: false
  });

  geometry.rotateX(Math.PI / 2);

  const material = new THREE.MeshLambertMaterial({
    color: 0x4f8fd9
  });

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

  return mesh;
}

如果建筑倒置、横躺或高度方向不对,通常是坐标轴映射和 rotateX 没处理好。GIS 三维开发中,这类问题很常见,不要急着怀疑数据,先检查坐标轴。

步骤五:批量加载建筑并设置视角

fetch('/data/buildings.geojson')
  .then(response => response.json())
  .then(geojson => {
    geojson.features.forEach(feature => {
      if (feature.geometry.type === 'Polygon') {
        createBuilding(feature);
      }
    });

    controls.target.set(0, 0, 0);
    controls.update();
  });

到这一步,一个最小可运行的 Three.js GIS 三维建筑白模就完成了。后续可以继续添加底图、道路、标签、点击查询、高亮选择和属性面板。

常见坑:Three.js 做 GIS 三维可视化最容易踩的错误

1. 坐标数值过大导致模型抖动

如果直接把 Web Mercator 坐标放入 Three.js,例如 x 为 12900000,y 为 4850000,可能出现模型抖动、边线闪烁、拾取不准等问题。这与浏览器中浮点精度有关。

解决方法是设置局部原点:用项目中心点或第一个要素坐标作为 origin,所有坐标都减去 origin 后再进入 Three.js 场景。

2. 高度单位和水平单位不一致

经纬度是度,高度通常是米。如果水平坐标没有投影到米制坐标,建筑高度就会看起来非常夸张或几乎不可见。GIS 三维可视化中,水平单位和垂直单位必须统一理解。

3. GeoJSON 面方向和孔洞没有处理

复杂建筑可能包含内环,也就是 Polygon 的 holes。简单 polygonToShape 只处理了外环,如果遇到带庭院或洞的建筑,需要把内环转成 shape.holes。

function polygonToShapeWithHoles(coordinates) {
  const outerRing = coordinates[0];
  const shape = new THREE.Shape();

  outerRing.forEach((coord, index) => {
    const pos = toScenePosition(coord[0], coord[1], 0);
    if (index === 0) shape.moveTo(pos.x, pos.z);
    else shape.lineTo(pos.x, pos.z);
  });

  for (let i = 1; i < coordinates.length; i++) {
    const holePath = new THREE.Path();
    coordinates[i].forEach((coord, index) => {
      const pos = toScenePosition(coord[0], coord[1], 0);
      if (index === 0) holePath.moveTo(pos.x, pos.z);
      else holePath.lineTo(pos.x, pos.z);
    });
    shape.holes.push(holePath);
  }

  return shape;
}

4. 一次性加载太多要素导致页面卡死

Three.js 可以渲染大量对象,但不适合把每栋楼都作为一个独立 Mesh 无限制添加。对象数量过多会增加 draw call,导致渲染帧率下降。

可考虑以下优化:

  • 同材质建筑合并几何体。
  • 使用 InstancedMesh 表达重复符号。
  • 按瓦片或网格分块加载。
  • 视距外对象隐藏或卸载。
  • 简化 GeoJSON 面节点数量。

5. 把 Three.js 当成 Cesium 使用

Three.js 可以做三维场景,但它不是专门的地球引擎。如果项目需要全球地形、3D Tiles、倾斜摄影、影像瓦片调度、地球曲率和海量地理数据管理,Cesium 通常更合适。

如果项目是局部园区、城市片区、专题三维图表、定制三维交互,Three.js 反而更灵活。

方法比较:Three.js、Cesium、Mapbox GL JS 和 OpenLayers 怎么选

工具 适合场景 优势 限制
Three.js 局部三维场景、建筑白模、专题三维图表、定制交互 灵活、生态丰富、可控性强 缺少内置 GIS 能力,需要自己处理坐标和数据组织
Cesium 全球三维地球、3D Tiles、地形、倾斜摄影、海量三维地理数据 GIS 三维能力完整,适合大范围地球场景 定制局部视觉效果时复杂度较高
Mapbox GL JS 二维地图、矢量瓦片、轻量三维建筑、地图样式表达 地图渲染能力强,样式体系成熟 复杂三维对象和深度定制不如 Three.js 灵活
OpenLayers 传统 WebGIS、二维地图、业务图层管理、空间交互 GIS 图层能力强,适合业务系统 原生三维能力有限,常需与其他三维库结合

简单判断:如果你要做“地图业务系统”,优先 OpenLayers 或 Mapbox GL JS;如果要做“全球三维地球”,优先 Cesium;如果要做“高度定制的局部 GIS 三维可视化”,Three.js 很值得掌握。

检查清单:GIS 开发者学习 Three.js 官网时该看什么

Three.js 官网文档和示例很多,GIS 学习者不必从头逐个看。建议按项目能力拆解学习。

  • 场景基础:Scene、Camera、Renderer、Mesh、Geometry、Material。
  • 相机控制:OrbitControls、PerspectiveCamera、Raycaster。
  • GIS 几何表达:Shape、ExtrudeGeometry、BufferGeometry、Line、Points。
  • 数据加载:FileLoader、GLTFLoader、TextureLoader,以及 fetch 加载 GeoJSON。
  • 性能优化:合并几何体、InstancedMesh、LOD、对象分组、按需加载。
  • 交互查询:Raycaster 拾取对象,再关联 feature.properties。
  • 视觉表达:灯光、材质、透明度、边线、高亮、标签。

建议不要只看官网最终效果,而要重点观察每个示例解决了什么问题:是几何构建、材质控制、相机运动、模型加载,还是性能优化。这样才能把示例迁移到 GIS 三维项目中。

WebGIS开发路线图:从二维地图到 Three.js GIS 三维可视化

阶段一:掌握 GIS 与 Web 基础

  • 理解经纬度、投影坐标、EPSG:4326、EPSG:3857。
  • 熟悉 GeoJSON、Shapefile、MBTiles、矢量瓦片等常见数据格式。
  • 掌握 HTML、CSS、JavaScript、ES Module 和浏览器调试。
  • 能用 QGIS 检查数据坐标系、字段、几何有效性。

阶段二:掌握二维 WebGIS 框架

  • 使用 Leaflet 快速加载点线面和底图。
  • 使用 OpenLayers 管理多图层、投影、交互和业务查询。
  • 理解 WMS、WMTS、XYZ、Vector Tile 的区别。
  • 能完成图层开关、属性查询、空间选择和专题渲染。

阶段三:进入 Three.js 三维场景

  • 用 Three.js 创建场景、相机、光照和基础几何体。
  • 把 GeoJSON 点线面转换为 Three.js 对象。
  • 理解局部坐标原点和米制单位的重要性。
  • 实现建筑拉伸、轨迹线、三维柱状图和点击高亮。

阶段四:学习 GIS 三维工程化

  • 按范围分块加载数据,避免一次性加载全量 GeoJSON。
  • 使用 Web Worker 处理大数据解析,减少主线程卡顿。
  • 将属性查询、图层树、时间轴和三维场景联动。
  • 对接 PostGIS、GeoServer、Node.js 或 Python 后端服务。

阶段五:根据项目选择 Cesium 或 Three.js 深入

如果你的工作偏自然资源、智慧城市、倾斜摄影、三维地球和 3D Tiles,建议深入 Cesium。如果你的工作偏园区可视化、三维专题图、设备数字孪生、定制动画和 WebGL 特效,建议深入 Three.js。

FAQ:Three.js GIS三维可视化常见问题

Q1:Three.js 能直接加载 Shapefile 吗?

Three.js 本身不能直接加载 Shapefile。常见做法是先用 QGIS、ogr2ogr 或后端服务把 Shapefile 转为 GeoJSON、矢量瓦片或自定义 JSON,再在前端用 Three.js 构建几何体。

Q2:Three.js 和 Cesium 学哪个更适合 GIS?

如果目标是标准三维地球、3D Tiles、地形和倾斜摄影,优先 Cesium。如果目标是局部场景、定制三维效果、专题可视化和复杂动画,Three.js 更灵活。GIS 工程师最好了解两者边界,而不是只学一个。

Q3:为什么 Three.js 加载 GeoJSON 后位置不对?

优先检查三个问题:GeoJSON 是否为经纬度坐标,是否做了投影转换,是否设置了局部原点。很多位置不对的问题不是 Three.js 渲染错误,而是坐标系和坐标轴映射错误。

Q4:Three.js 做建筑拉伸时高度为什么异常?

通常是水平坐标单位和高度单位不一致。经纬度单位是度,高度字段多为米。如果没有将经纬度转换为米制投影坐标,建筑比例会明显不对。

Q5:Three.js 做 WebGIS 项目需要后端吗?

小型演示可以直接加载静态 GeoJSON。实际项目通常需要后端提供数据裁剪、分页、瓦片、权限控制和空间查询。常见组合是 PostGIS 存储空间数据,GeoServer 或 Node.js 提供服务,前端用 OpenLayers、Mapbox GL JS、Cesium 或 Three.js 展示。

Q6:Three.js 官网进阶难,应该从哪些示例开始?

建议从 BoxGeometry、Shape、ExtrudeGeometry、Line、Points、Raycaster、OrbitControls、GLTFLoader、InstancedMesh 这些示例开始。它们与 GIS 三维可视化的建筑、线网、点位、拾取、模型加载和性能优化关系最直接。

结论:把 Three.js 官网知识翻译成 GIS 工作流

Three.js 官网进阶难,并不是因为内容不适合学习,而是它默认读者已经理解三维图形开发,而 GIS 读者还需要额外处理空间数据逻辑。

做 GIS 三维可视化时,请始终抓住四个核心:坐标转换、局部原点、几何构建、性能组织。只要把这四件事处理好,Three.js 就可以很好地服务于建筑白模、三维轨迹、专题柱状图、园区场景和 WebGIS 定制可视化。

如果你正在规划 WebGIS 开发路线,建议先把二维地图、坐标系和空间数据格式打牢,再学习 Three.js 的场景、相机、几何体和交互。这样看官网示例时,你就不再是“照抄代码”,而是在判断:这个示例能解决我的哪个 GIS 问题。