Three.js前端三维图形开发案例集锦,GIS场景如何应用?(附:源码)
Three.js前端三维图形开发案例集锦,GIS场景如何应用?(附:源码)这篇文章面向正在做 WebGIS、三维可视化或数字孪生入门的读者,重点不是泛泛介绍 Three.js,而是把它放到 GIS 场景里:如何加载地形、建筑、点位、轨迹和专题数据,如何与地图坐标系统衔接,以及一个可运行的基础源码骨架应该怎么写。

引言:为什么 GIS 前端会用到 Three.js
很多 WebGIS 项目最初只需要二维地图:底图、点线面、弹窗、查询统计。但当需求变成三维建筑、地形起伏、飞线、轨迹回放、地下管线、园区数字孪生时,传统二维地图组件就不够用了。
Three.js 是一个基于 WebGL 的前端三维图形库,适合在浏览器中构建三维场景。它本身不是 GIS 平台,不直接理解投影坐标、瓦片规则、空间索引和地理要素语义,但它非常适合承担 GIS 三维可视化中的“渲染层”工作。
在 GIS 项目中,Three.js 常见应用包括:
- 将 GeoJSON 面数据拉伸成三维建筑或行政区块。
- 根据 DEM 高程数据生成三维地形。
- 展示三维点位、设备、传感器和告警状态。
- 绘制飞线、迁徙线、车辆轨迹和管线走向。
- 加载 glTF、OBJ、FBX 等三维模型,用于园区、厂区和城市部件展示。
- 与 Leaflet、OpenLayers、Mapbox GL JS 等 WebGIS 框架联动。
背景:Three.js 做 GIS 场景时最容易卡在哪里
很多同学能很快用 Three.js 创建一个旋转立方体,但一接触 GIS 数据就会遇到问题。原因很简单:GIS 数据关心的是地理坐标、投影、尺度和属性,而 Three.js 关心的是三维空间中的点、线、面、材质、灯光和相机。
常见问题集中在以下几类:
- 经纬度不能直接当作 Three.js 坐标使用:经纬度单位是度,不是米,直接放进场景会导致比例异常。
- 数据范围太大:真实世界坐标可能是几百万级,直接用于 Three.js 容易出现精度抖动。
- 高程和水平单位不一致:经纬度、高程米、投影米混在一起,模型会被拉伸或压扁。
- GeoJSON 面转三维体不简单:需要处理外环、内洞、顺逆时针、三角剖分和高度属性。
- 大量要素性能差:成千上万个 Mesh 会造成浏览器卡顿,需要合并几何体或使用 InstancedMesh。
- 与二维地图同步困难:相机、缩放、中心点和坐标转换必须保持一致。
因此,GIS 场景使用 Three.js 的关键不在于会不会写三维动画,而在于能否正确处理空间数据到三维坐标的转换。
原理:把 GIS 数据变成 Three.js 场景的核心思路
Three.js 的坐标系统可以理解为一个本地三维坐标系,常用的轴向约定是 X 表示横向,Y 表示高度,Z 表示纵深。GIS 数据则通常来自经纬度坐标系或投影坐标系。
实际项目中建议采用以下思路:
- 先确定项目数据坐标系,例如 WGS84 经纬度、Web Mercator 或地方投影坐标。
- 选择一个场景原点,通常使用项目中心点作为局部坐标原点。
- 把所有 GIS 坐标转换为相对于原点的局部米制坐标。
- 将局部坐标映射到 Three.js 的 X、Y、Z。
- 根据属性字段生成高度、颜色、材质和交互信息。
例如,一个常见映射方式是:
| GIS 含义 | Three.js 坐标 | 说明 |
|---|---|---|
| 东西方向距离 | X | 以项目中心为原点,向东为正 |
| 高度或海拔 | Y | 建筑高度、DEM 高程、模型高度 |
| 南北方向距离 | Z | 通常向北为负或正,需在项目中统一 |
如果项目范围较小,例如园区、校园、厂区、一个街区,可以用近似局部坐标转换。如果范围较大,例如城市级、省级或全国级三维可视化,应优先使用成熟地图引擎或三维地理引擎,并谨慎处理地球曲率和精度问题。
步骤:Three.js GIS 三维场景基础案例源码
下面给出一个基础案例:在 Three.js 中创建 GIS 场景,模拟加载建筑面数据,将建筑按高度拉伸,并支持鼠标点击查询属性。这个案例适合理解 Three.js 在 GIS 场景中的基础组织方式。
1. 准备页面容器和依赖
如果使用 Vite、Webpack 或其他前端工程环境,可以通过 npm 安装 Three.js。为了便于理解,下面用模块化写法展示核心逻辑。
npm install three
HTML 页面只需要一个容器:
<div id="map3d"></div>
容器样式建议设置为全屏或固定高度:
#map3d {
width: 100%;
height: 600px;
overflow: hidden;
}
2. 初始化 Three.js 场景、相机和渲染器
import * as THREE from 'three';
const container = document.getElementById('map3d');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf2f6fa);
const camera = new THREE.PerspectiveCamera(
45,
container.clientWidth / container.clientHeight,
1,
100000
);
camera.position.set(0, 800, 1200);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(300, 800, 500);
scene.add(light);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
这里的相机位置不是固定答案。GIS 场景中应该根据数据范围动态设置相机高度和观察点,否则很容易出现“数据加载了但看不见”的问题。
3. 设计一个经纬度转本地坐标函数
为了演示,这里使用适合小范围场景的近似转换。它不适合跨城市、跨省的大范围分析,但适合园区、街区级三维展示。
const origin = {
lng: 116.391,
lat: 39.907
};
function lngLatToLocal(lng, lat) {
const earthRadius = 6378137;
const dLng = (lng - origin.lng) * Math.PI / 180;
const dLat = (lat - origin.lat) * Math.PI / 180;
const x = dLng * earthRadius * Math.cos(origin.lat * Math.PI / 180);
const z = -dLat * earthRadius;
return { x, z };
}
注意这里将纬度方向映射到了 Z 轴,并使用负号让地图北方向在屏幕中更符合常见观察习惯。实际项目中方向可以调整,但必须全项目统一。
4. 准备一组模拟 GeoJSON 建筑数据
真实项目中可以从后端接口、GeoServer、PostGIS、静态 GeoJSON 文件或矢量瓦片中读取数据。这里用简化数据说明结构。
const buildings = [
{
name: 'A座办公楼',
height: 80,
coordinates: [
[116.3905, 39.9072],
[116.3912, 39.9072],
[116.3912, 39.9067],
[116.3905, 39.9067]
]
},
{
name: 'B座实验楼',
height: 45,
coordinates: [
[116.3914, 39.9071],
[116.3920, 39.9071],
[116.3920, 39.9066],
[116.3914, 39.9066]
]
}
];
5. 将建筑面拉伸为三维体
Three.js 可以通过 Shape 和 ExtrudeGeometry 将二维多边形拉伸为三维体。注意,这里为简单案例,没有处理内洞和复杂多边形。真实 GeoJSON 中如果存在 MultiPolygon、洞、多环面,需要额外处理。
function createBuilding(building) {
const shape = new THREE.Shape();
building.coordinates.forEach((coord, index) => {
const p = lngLatToLocal(coord[0], coord[1]);
if (index === 0) {
shape.moveTo(p.x, p.z);
} else {
shape.lineTo(p.x, p.z);
}
});
shape.closePath();
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: building.height,
bevelEnabled: false
});
geometry.rotateX(Math.PI / 2);
const material = new THREE.MeshLambertMaterial({
color: 0x3b82f6,
transparent: true,
opacity: 0.85
});
const mesh = new THREE.Mesh(geometry, material);
mesh.userData = {
name: building.name,
height: building.height
};
scene.add(mesh);
}
buildings.forEach(createBuilding);
这里有一个容易混淆的点:ExtrudeGeometry 默认沿局部 Z 方向拉伸,而我们希望高度在 Three.js 的 Y 方向,所以使用了 rotateX 进行轴向调整。实际项目中也可以通过统一坐标映射方式避免多处旋转。
6. 添加地面网格辅助观察
const grid = new THREE.GridHelper(1000, 20, 0x999999, 0xdddddd);
scene.add(grid);
GIS 三维调试时,地面网格很有用。它可以帮助你判断模型是否倒置、是否偏移、是否高度异常。
7. 添加点击查询交互
GIS 场景通常不只是展示,还需要点击查询属性。Three.js 中常用 Raycaster 进行拾取。
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
container.addEventListener('click', function (event) {
const rect = container.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const object = intersects[0].object;
if (object.userData && object.userData.name) {
console.log('名称:', object.userData.name);
console.log('高度:', object.userData.height);
}
}
});
在正式 WebGIS 项目中,可以把查询结果显示在信息面板、弹窗或属性表中。如果要和二维地图联动,还可以反向把 Three.js 拾取结果定位到二维地图中心。
8. 渲染循环和窗口自适应
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', function () {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
到这里,一个最小可运行的 Three.js GIS 三维建筑案例就完成了。它虽然简单,但已经包含了 GIS 三维场景的核心流程:数据坐标转换、几何构建、属性挂载、渲染和交互。
步骤:更多 Three.js 前端三维图形开发案例在 GIS 中怎么用
案例一:GeoJSON 行政区块三维拉伸
适用场景包括人口密度、GDP、土地利用强度、风险等级等专题图。做法是把面要素按字段值拉伸成不同高度,并用颜色表达指标等级。
- 数据源:GeoJSON、PostGIS 查询结果、矢量瓦片。
- 关键字段:行政区名称、统计值、分类等级。
- Three.js 对象:Shape、ExtrudeGeometry、MeshLambertMaterial。
- 注意事项:复杂行政区边界需要简化,否则三角面数量会非常大。
案例二:三维地形 DEM 可视化
DEM 是数字高程模型,常见格式有 GeoTIFF、ASC、栅格瓦片等。Three.js 可以用 PlaneGeometry 创建网格平面,再把每个顶点的 Y 值设置为高程。
function createTerrain(width, height, elevationArray) {
const geometry = new THREE.PlaneGeometry(1000, 1000, width - 1, height - 1);
geometry.rotateX(-Math.PI / 2);
const position = geometry.attributes.position;
for (let i = 0; i < position.count; i++) {
const elevation = elevationArray[i] || 0;
position.setY(i, elevation * 1.5);
}
position.needsUpdate = true;
geometry.computeVertexNormals();
const material = new THREE.MeshLambertMaterial({
color: 0x8bbf72,
wireframe: false
});
return new THREE.Mesh(geometry, material);
}
需要注意的是,DEM 数据通常不能直接在浏览器中读取全部原始文件。生产项目更常见的做法是后端预处理为切片、简化网格或按视野动态加载。
案例三:城市飞线和迁徙线
飞线常用于人口迁徙、物流流向、航线、网络链路等可视化。实现方式通常是用起点和终点构造一条三维贝塞尔曲线,再用 Line 或 TubeGeometry 渲染。
function createFlyLine(start, end) {
const startLocal = lngLatToLocal(start.lng, start.lat);
const endLocal = lngLatToLocal(end.lng, end.lat);
const p0 = new THREE.Vector3(startLocal.x, 10, startLocal.z);
const p2 = new THREE.Vector3(endLocal.x, 10, endLocal.z);
const distance = p0.distanceTo(p2);
const p1 = new THREE.Vector3(
(p0.x + p2.x) / 2,
distance * 0.25,
(p0.z + p2.z) / 2
);
const curve = new THREE.QuadraticBezierCurve3(p0, p1, p2);
const points = curve.getPoints(80);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({
color: 0xff9900
});
return new THREE.Line(geometry, material);
}
飞线效果看起来炫,但 GIS 项目中不要滥用。线太多会遮挡空间关系,也会增加渲染压力。建议加入筛选、聚合、分级显示和时间过滤。
案例四:轨迹回放与车辆移动
轨迹回放适用于车辆监管、巡检路线、船舶轨迹、人员定位等场景。核心是根据时间字段按顺序插值,让模型沿轨迹移动。
- 轨迹点必须有时间字段。
- 前端应按时间排序。
- 经纬度点需要转换为 Three.js 本地坐标。
- 模型朝向应根据相邻点方向计算。
- 长轨迹建议抽稀,避免一次渲染过多点。
案例五:园区三维模型加载
如果已有建筑白模、设备模型或厂区模型,可以使用 glTF 格式。glTF 是 Web 三维中常用的模型格式,适合浏览器加载。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load('/models/park.glb', function (gltf) {
const model = gltf.scene;
model.scale.set(1, 1, 1);
model.position.set(0, 0, 0);
scene.add(model);
});
模型加载后,最常见的问题是比例不对、坐标不对、方向不对。建议在建模阶段就约定单位、原点、朝向和楼层高度,不要全部留给前端临时修。
常见坑:Three.js GIS 场景调试重点
1. 经纬度直接作为 x、z 坐标
这是最常见错误。经纬度是角度单位,不是米。直接使用会导致模型比例不真实、距离不正确、场景操作困难。至少应转换为局部米制坐标。
2. 世界坐标过大导致画面抖动
如果直接使用投影坐标,例如 3857 或地方坐标中的百万级数值,Three.js 可能出现精度问题。解决方法是使用项目中心点作为原点,将所有坐标平移到本地小范围坐标。
3. 建筑高度字段单位混乱
有些数据中的高度字段是米,有些是层数,有些是字符串。拉伸前要确认字段含义。例如层数应乘以平均层高,而不是直接当米使用。
4. 面数据方向和内洞处理不正确
复杂 GeoJSON 面可能包含内洞,例如湖泊、院落或建筑中庭。简单 Shape 只能处理基础外环,遇到 holes 需要使用 shape.holes 添加内环,否则会把洞填满。
5. Mesh 数量太多导致卡顿
每个建筑、每个点位都单独创建 Mesh,数量上万时性能会明显下降。可以考虑合并几何体、使用 InstancedMesh、按视野加载、按层级简化。
6. 相机 near 和 far 设置不合理
如果 near 太小、far 太大,深度缓冲精度会变差,可能出现闪烁和穿插。GIS 场景中应根据数据范围设置相机裁剪面,而不是随便写一个很大的 far。
7. 忘记释放资源
在 Vue、React 或单页面应用中切换页面时,需要释放 geometry、material、texture 和 renderer,否则可能造成内存泄漏。
function disposeObject(object) {
object.traverse(function (child) {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(function (m) {
m.dispose();
});
} else {
child.material.dispose();
}
}
});
}
方法比较:Three.js、Cesium、Mapbox GL JS 怎么选
GIS 三维项目不一定都应该用 Three.js。选型要看项目目标。如果是纯地理三维地球、海量倾斜摄影、3D Tiles 和全球尺度数据,Cesium 通常更合适。如果是二维地图加少量三维效果,Mapbox GL JS 或 OpenLayers 结合自定义图层也可能更省事。
| 方案 | 适合场景 | 优势 | 限制 |
|---|---|---|---|
| Three.js | 园区三维、专题三维、模型展示、定制交互 | 渲染自由度高,效果可控,适合前端定制 | 不自带 GIS 坐标、瓦片、地球和空间分析能力 |
| Cesium | 三维地球、3D Tiles、倾斜摄影、全球地形 | 地理三维能力强,支持大范围地球场景 | 定制非地理三维交互时成本较高 |
| Mapbox GL JS | 二维地图加三维建筑、矢量瓦片可视化 | 地图渲染和样式能力强,可叠加自定义图层 | 复杂三维模型和自由场景不如 Three.js 灵活 |
| OpenLayers 加 Three.js | 已有 OpenLayers WebGIS 系统扩展三维效果 | 可复用现有二维地图能力 | 相机同步、坐标转换和性能优化需要额外处理 |
简单判断可以这样做:如果你的核心是“地图和地球”,优先考虑 GIS 引擎;如果核心是“自定义三维场景和模型效果”,Three.js 更灵活;如果两者都需要,常见做法是地图引擎负责地理底座,Three.js 负责三维专题图层。
检查清单:上线前必须确认的 GIS 三维问题
- 是否明确了所有数据的坐标系?
- 经纬度是否已转换为本地米制坐标?
- 是否设置了统一的项目原点?
- Three.js 的 X、Y、Z 与 GIS 东西、南北、高度是否对应清楚?
- 建筑高度字段是否经过单位校验?
- GeoJSON 是否存在 MultiPolygon、内洞和异常面?
- 是否对复杂边界做了简化?
- 是否避免一次性创建过多 Mesh?
- 点击拾取是否能正确返回 GIS 属性?
- 相机视角是否能根据数据范围自动适配?
- 窗口 resize 后画面比例是否正常?
- 路由切换或组件销毁时是否释放 Three.js 资源?
- 移动端是否测试过帧率、内存和交互体验?
FAQ:Three.js GIS 应用常见问题
Q1:Three.js 能不能直接加载 Shapefile?
Three.js 不能直接理解 Shapefile 的 GIS 语义。通常做法是先用 QGIS、GDAL、GeoPandas 或后端服务把 Shapefile 转为 GeoJSON、矢量瓦片或接口数据,再在前端转换为 Three.js 几何体。
Q2:Three.js 能不能代替 Cesium?
不能简单替代。Three.js 是通用三维渲染库,Cesium 是面向三维地理空间的引擎。如果项目重点是全球地球、地形、影像、3D Tiles 和倾斜摄影,Cesium 更合适。如果项目重点是园区、厂区、设备模型和高度定制效果,Three.js 更灵活。
Q3:Three.js GIS 场景为什么模型会偏移?
常见原因包括坐标系不一致、原点设置错误、经纬度没有转换、模型自身原点不在预期位置、单位不一致。排查时先确认数据坐标系,再确认局部坐标转换函数,最后检查模型导出单位和方向。
Q4:GeoJSON 面拉伸后为什么形状不对?
可能是坐标顺序错误、环方向错误、存在内洞没有处理、MultiPolygon 被当成 Polygon、坐标没有闭合,或者数据本身有自相交。建议先在 QGIS 中检查几何有效性,再进行前端渲染。
Q5:Three.js 加载很多 GIS 点位很卡怎么办?
不要为每个点位创建一个独立 Mesh。可以使用 InstancedMesh、合并 BufferGeometry、按瓦片或视野加载、点位聚合、LOD 分级显示。对于几十万级点数据,前端还需要结合后端抽稀和空间索引。
Q6:能否把 Three.js 叠加到 OpenLayers 或 Leaflet 上?
可以,但需要处理坐标转换、相机同步、地图缩放比例和图层层级。简单项目可以使用覆盖容器叠加,复杂项目建议封装专门的自定义图层,并保持地图中心、缩放和 Three.js 场景原点同步。
结论:Three.js 适合做 GIS 三维专题,但坐标和性能必须先想清楚
Three.js 前端三维图形开发在 GIS 场景中很有价值,尤其适合园区三维、建筑拉伸、飞线、轨迹、设备模型和专题三维可视化。它的优势是自由、灵活、效果可定制,但它不会自动解决 GIS 坐标系、投影、空间数据组织和大规模数据加载问题。
实际开发时,建议先从一个小而完整的案例入手:确定坐标系,建立局部坐标原点,加载少量 GeoJSON,完成三维拉伸和点击查询。等这条链路跑通后,再扩展 DEM 地形、模型加载、轨迹回放、图层控制和性能优化。
如果你是 GIS 学生或初级 WebGIS 开发者,记住一个原则:Three.js 负责把东西画得漂亮,GIS 逻辑负责让东西画得准确。只有两者结合好,三维 GIS 场景才既能看,也能用。