Three.js前端三维图形开发案例集锦,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 会更灵活。

背景: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 项目,而不只是一个好看的三维演示。