Three.js前端三维图形开发案例集锦,GIS场景如何应用?(附:源码)

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

《Three.js前端三维图形开发案例集锦,GIS场景如何应用?(附:源码)》这篇文章面向正在做 WebGIS、三维可视化或数字孪生原型的同学,重点不是泛泛介绍 Three.js,而是说明它在 GIS 场景里能解决哪些具体问题、如何组织源码、以及与 Cesium、Mapbox GL JS、OpenLayers 等工具配合时要注意什么。

引言:Three.js在GIS三维场景中适合做什么

Three.js 是一个基于 WebGL 的前端三维图形库,可以在浏览器中渲染模型、点云、线框、粒子、材质、灯光和交互动画。对于 GIS 读者来说,Three.js 的价值主要体现在三类场景:

  • 三维专题表达:例如建筑物拉伸、道路流向、热力柱状图、轨迹动画、管线剖面。
  • 轻量三维交互:例如选中建筑、鼠标拾取、模型高亮、属性面板联动。
  • 自定义可视化效果:例如扩散圆、飞线、扫描线、粒子迁徙、动态材质。

如果项目需要完整地球、倾斜摄影、地形瓦片和 3D Tiles,Cesium 往往更合适;如果项目需要在一个已有二维地图或业务页面中嵌入定制三维效果,Three.js 会更灵活。

Three.js前端三维图形开发案例集锦 GIS场景应用流程图
Three.js 在 GIS 场景中的典型开发流程:数据准备、坐标转换、三维渲染和地图交互联动。

背景:GIS项目为什么会用到Three.js

很多 GIS 项目一开始只需要二维地图:点位、面图层、弹窗、查询和统计。但随着业务深入,常见需求会变成:

  • 园区建筑要做三维楼块展示。
  • 管线、道路、河流需要用动态线表达流向。
  • 人口、能耗、告警数量需要用三维柱状图表达强弱。
  • 车辆、船舶、无人机轨迹需要在地图上做动态播放。
  • BIM、glTF 模型需要放到地图指定位置。

这些需求如果全部交给传统 GIS 框架,有时会遇到样式受限、动画不够灵活、模型控制不细的问题。Three.js 的优势是底层图形能力强,可以精细控制几何体、材质、光照、相机、动画和交互。

不过,Three.js 本身不是 GIS 框架。它不直接理解 EPSG 坐标系、地图投影、瓦片金字塔、空间索引或属性查询。因此,在 GIS 场景中使用 Three.js,核心难点不是“画一个立方体”,而是“如何把真实地理数据正确放到三维场景里”。

原理:Three.js与GIS数据结合的关键逻辑

1. Three.js的基本对象

一个最小的 Three.js 项目通常包括以下对象:

  • Scene:场景,负责容纳所有三维对象。
  • Camera:相机,决定从哪里看、怎么看。
  • Renderer:渲染器,把三维场景绘制到浏览器 canvas 上。
  • Geometry:几何体,定义点、线、面的形状。
  • Material:材质,定义颜色、透明度、贴图、发光等视觉效果。
  • Mesh:网格对象,由几何体和材质组成。

GIS 开发中经常会把 GeoJSON 面数据转成建筑楼块,把 LineString 转成道路或飞线,把 Point 转成图标、模型或粒子。

2. GIS坐标不能直接当作Three.js坐标

Three.js 使用的是普通三维笛卡尔坐标,而 GIS 数据常见坐标包括经纬度坐标、Web Mercator 坐标、地方投影坐标等。直接把经纬度作为 Three.js 的 x、y 坐标通常会出现两个问题:

  • 经纬度单位是“度”,不是米,距离和面积不适合直接用于三维尺寸。
  • 坐标值可能过大或过小,导致模型显示比例异常、相机控制困难或精度抖动。

实际项目中通常会先选一个局部原点,例如园区中心点,然后把所有地理坐标转换为相对坐标。这样可以让 Three.js 场景的坐标值保持在较小范围内,渲染更稳定。

3. 常见GIS数据到Three.js对象的映射

GIS数据 Three.js对象 典型用途
GeoJSON Point Sprite、Mesh、Points 点位标注、传感器、告警点
GeoJSON LineString Line、TubeGeometry、Curve 道路、河流、管线、飞线
GeoJSON Polygon ShapeGeometry、ExtrudeGeometry 地块、建筑物、行政区拉伸
glTF/GLB模型 Group、Mesh 设备模型、建筑模型、BIM简化模型
点云 Points、BufferGeometry 激光雷达、三维扫描成果展示

步骤:一个GIS场景中的Three.js开发流程

步骤1:明确三维表达目标

在写代码之前,先把需求拆清楚。不要一上来就找炫酷案例源码。GIS 三维项目最容易失败的地方,是效果很好看,但业务数据放不上去或坐标对不上。

  • 是要展示建筑物,还是展示路径流动?
  • 数据来源是 GeoJSON、Shapefile、数据库接口,还是 glTF 模型?
  • 需要和二维地图联动,还是独立三维场景?
  • 需要真实比例,还是只做专题夸张表达?
  • 需要点击查询属性,还是只做展示动画?

步骤2:准备GIS数据

建议先把数据整理成前端容易处理的格式。对于矢量数据,GeoJSON 是最常用的中间格式;对于三维模型,glTF 或 GLB 更适合 Web 端加载。

  • 面数据:保留建筑轮廓、高度、名称、编号等字段。
  • 线数据:保留起点、终点、类型、流量、速度等字段。
  • 点数据:保留名称、状态、分类、图标、业务ID等字段。
  • 模型数据:优先使用 GLB,减少贴图路径丢失问题。

如果数据来自 Shapefile,可以先用 QGIS 或 GDAL 转成 GeoJSON。需要注意的是,中文字段名、编码、坐标系和几何有效性都要提前检查。

ogr2ogr -f GeoJSON buildings.geojson buildings.shp -t_srs EPSG:4326

如果后续要在局部平面中渲染,也可以转换到米制投影坐标系,例如某些城市项目会使用 CGCS2000 高斯投影或 Web Mercator,但要确保与底图和业务数据一致。

步骤3:建立Three.js基础场景

下面是一个简化的 Three.js 初始化结构,适合作为 GIS 三维原型的起点。

import * as THREE from 'three';

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

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

camera.position.set(0, -1200, 900);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({
  antialias: true
});

renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

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

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

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

animate();

实际项目中通常还会加入 OrbitControls,用于鼠标旋转、缩放和平移。GIS 场景如果需要固定俯视视角,也可以限制相机角度,避免用户把场景转到不符合业务认知的方向。

步骤4:把GeoJSON面数据拉伸成三维建筑

建筑物拉伸是 Three.js 在 GIS 中最常见的案例之一。基本思路是:读取 Polygon 坐标,转换成 Three.js 的二维 Shape,再用 ExtrudeGeometry 按高度字段拉伸。

function lonLatToLocal(lon, lat, originLon, originLat) {
  const meterPerDegreeLon = 111320 * Math.cos(originLat * Math.PI / 180);
  const meterPerDegreeLat = 110540;

  const x = (lon - originLon) * meterPerDegreeLon;
  const y = (lat - originLat) * meterPerDegreeLat;

  return { x, y };
}

function createBuilding(feature, originLon, originLat) {
  const coords = feature.geometry.coordinates[0];
  const shape = new THREE.Shape();

  coords.forEach((coord, index) => {
    const p = lonLatToLocal(coord[0], coord[1], originLon, originLat);
    if (index === 0) {
      shape.moveTo(p.x, p.y);
    } else {
      shape.lineTo(p.x, p.y);
    }
  });

  const height = Number(feature.properties.height || 30);

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

  const material = new THREE.MeshLambertMaterial({
    color: 0x2f80ed,
    transparent: true,
    opacity: 0.85
  });

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

  return mesh;
}

这里的坐标转换只是适合小范围原型的近似方法。如果项目范围较大,建议使用 proj4js 或后端提前完成投影转换,避免变形和距离误差。

步骤5:制作道路流向或迁徙飞线

道路流向、迁徙线和管线流动效果,本质上是把 GIS 线数据转换为 Three.js 曲线,再通过动态材质、粒子或贴图偏移表现运动。

function createFlowLine(points) {
  const curvePoints = points.map(p => new THREE.Vector3(p.x, p.y, 5));

  const curve = new THREE.CatmullRomCurve3(curvePoints);

  const geometry = new THREE.TubeGeometry(
    curve,
    80,
    3,
    8,
    false
  );

  const material = new THREE.MeshBasicMaterial({
    color: 0x00ffff,
    transparent: true,
    opacity: 0.8
  });

  return new THREE.Mesh(geometry, material);
}

如果线数量较少,TubeGeometry 效果直观;如果线数量很多,建议使用 BufferGeometry 或着色器方案,否则会增加浏览器渲染压力。

步骤6:加载GLB模型并放到地图位置

对于设备、车辆、泵站、摄像头等模型,推荐使用 GLB。它把几何、材质和贴图打包到一个文件中,前端加载更稳定。

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

loader.load('/models/device.glb', (gltf) => {
  const model = gltf.scene;

  const p = lonLatToLocal(116.391, 39.907, 116.390, 39.906);

  model.position.set(p.x, p.y, 0);
  model.scale.set(2, 2, 2);

  scene.add(model);
});

加载模型后,最常见的问题是模型太大、太小、方向不对或位置偏移。建议先在 Blender 中确认模型单位、坐标轴方向和原点位置,再放入 WebGIS 页面。

步骤7:实现点击拾取和属性查询

GIS 场景不是只看效果,还需要能查属性。Three.js 中常用 Raycaster 做鼠标拾取。拾取到 Mesh 后,可以读取 userData 中保存的 GIS 属性。

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

window.addEventListener('click', (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, true);

  if (intersects.length > 0) {
    const object = intersects[0].object;
    console.log('选中对象属性:', object.userData);
  }
});

在正式项目中,不建议把所有对象都参与拾取。可以维护一个 pickableObjects 数组,只把需要交互的建筑、设备或点位放进去,这样性能更可控。

常见坑:Three.js做GIS三维最容易出错的地方

1. 坐标系没统一,模型位置全错

这是最典型的问题。底图是 EPSG:3857,业务数据是 EPSG:4326,模型又是本地坐标,三者如果没有统一转换,显示出来一定会偏移。

  • 先确认每份数据的真实坐标系。
  • 不要只看文件名判断坐标系。
  • GeoJSON 默认常见为经纬度,但不代表所有 GeoJSON 都一定是 EPSG:4326。
  • 小范围项目可用局部原点,大范围项目要使用严谨投影转换。

2. 坐标值过大导致抖动

WebGL 使用浮点数计算。坐标值过大时,模型在旋转、缩放或动画中可能出现抖动。GIS 数据中常见的 Web Mercator 坐标可能达到百万级甚至千万级,直接放进 Three.js 并不理想。

解决方式是使用局部坐标:选一个中心点作为原点,所有对象都减去这个原点。这样既保留相对位置,又能提高渲染稳定性。

3. 面数据有洞、多面或自相交

真实 GIS 面数据并不总是干净的。行政区、建筑轮廓、地块边界可能存在 MultiPolygon、内环洞、自相交、重复点等情况。直接转 Shape 可能导致拉伸失败或面翻转。

  • 用 QGIS 的“检查有效性”工具检查几何。
  • 用“修复几何”处理自相交。
  • MultiPolygon 要逐个 Polygon 转换。
  • 带洞 Polygon 要正确处理 holes。

4. 模型面数太高,浏览器卡顿

很多 BIM 或三维模型来自专业建模软件,细节很丰富,但不适合直接放到浏览器里。Web 端更关注轻量化和可交互。

  • 删除看不见的内部构件。
  • 合并材质和网格。
  • 压缩贴图尺寸。
  • 使用 glTF Draco 压缩,但要注意加载端解码配置。
  • 按楼层、区域或视距分级加载。

5. 动画效果太多,影响GIS业务操作

飞线、光圈、扫描、粒子都很吸引眼球,但 GIS 应用的核心仍然是空间信息表达。动画应服务于业务含义,例如流向、强度、告警状态,而不是单纯堆效果。

判断一个 Three.js GIS 效果是否合格,可以问一句:关闭动画后,用户是否仍然能理解空间对象、位置关系和业务属性?如果不能,说明可视化设计需要回到 GIS 信息表达本身。

方法比较: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 作为地图容器,再叠加 Three.js 图层。如果你要做的是“完整三维地球”,优先考虑 Cesium。如果你要做的是“独立三维园区、设备模型、专题大屏”,Three.js 是很合适的选择。

检查清单:GIS项目接入Three.js前先确认这些问题

  • 数据范围:是单个园区、一个城市,还是全国尺度?
  • 坐标系统:每份数据的 EPSG 编码是否明确?
  • 坐标转换:是否需要 proj4js、后端转换或局部原点方案?
  • 数据格式:矢量是否使用 GeoJSON,模型是否使用 GLB?
  • 几何质量:面是否自相交,线是否断裂,点是否重复?
  • 高度来源:建筑高度来自字段、楼层数估算,还是统一默认值?
  • 性能控制:对象数量、面数、贴图大小是否适合浏览器?
  • 交互需求:是否需要拾取、高亮、弹窗、属性查询和图层开关?
  • 地图联动:是否需要与二维地图同步缩放、平移和旋转?
  • 部署方式:模型、贴图、GeoJSON 是否支持静态资源访问和缓存?

FAQ:Three.js GIS开发常见问题

Three.js能直接加载Shapefile吗?

一般不建议直接在前端加载 Shapefile。Shapefile 包含多个文件,编码和投影处理也比较麻烦。更推荐在后端或桌面 GIS 中先转换为 GeoJSON、矢量瓦片或接口数据,再交给 Three.js 渲染。

Three.js和Cesium哪个更适合GIS三维?

如果项目需要地球、地形、影像、倾斜摄影、3D Tiles 和大范围地理坐标管理,Cesium 更合适。如果项目强调自定义三维视觉效果、设备模型、园区场景、业务大屏和灵活动画,Three.js 更轻便。

GeoJSON建筑物拉伸后为什么上下颠倒?

常见原因是坐标轴方向不一致。GIS 中常用经度表示 x、纬度表示 y,而 Three.js 的相机视角和几何拉伸方向需要单独设计。可以检查 y 轴是否需要取反,以及 ExtrudeGeometry 的 depth 方向是否符合预期。

Three.js加载大量点位会不会卡?

如果每个点都创建一个 Mesh,数量上来后会明显卡顿。大量点位建议使用 Points、InstancedMesh 或自定义 BufferGeometry。对于 GIS 点数据,还可以按视图范围和缩放级别动态加载。

Three.js如何和OpenLayers叠加使用?

常见做法是让 OpenLayers 负责二维地图、坐标转换和图层管理,让 Three.js 的 canvas 作为覆盖层显示三维对象。关键是同步地图视图状态,包括中心点、分辨率、旋转角度和 Three.js 相机参数。这个方案适合二维地图为主、局部三维效果为辅的 WebGIS 项目。

Three.js中的高度单位应该怎么定?

建议在 GIS 场景中尽量使用“米”作为统一单位。经纬度数据需要先转换为米制局部坐标,建筑高度、管线埋深、模型尺寸也应统一到同一单位。否则会出现建筑过高、模型过小或距离不真实的问题。

结论:把Three.js当作GIS三维表达工具,而不是完整GIS平台

Three.js 前端三维图形开发在 GIS 场景中非常有用,尤其适合建筑拉伸、三维专题图、飞线动画、设备模型、园区可视化和业务大屏。但它不是完整 GIS 平台,不能替代坐标系管理、空间数据处理、瓦片服务和空间分析。

实际开发时,推荐采用“GIS工具负责数据正确,Three.js负责三维表达”的思路:先用 QGIS、GDAL、PostGIS 或后端服务把坐标、格式和属性处理好,再在前端用 Three.js 完成交互和视觉效果。这样既能保证空间位置准确,也能发挥 Three.js 在浏览器三维渲染上的灵活性。

如果你准备复用 Three.js 案例源码,建议不要只复制动画效果,而要重点检查三件事:坐标转换是否适合你的数据范围,几何对象是否能承载真实 GIS 属性,性能方案是否能支撑实际数据量。做到这三点,Three.js 才能真正服务于 GIS 项目,而不只是一个好看的三维演示。