Three.js地理空间可视化如何实现?城乡规划三维场景构建实战(附:GIS数据对接源码)
如果你正在搜索“Three.js地理空间可视化如何实现?城乡规划三维场景构建实战(附:GIS数据对接源码)”,大概率不是想看一个旋转立方体 Demo,而是想把真实 GIS 数据接入 Three.js,构建可交互、可扩展的城乡规划三维场景。本文以城乡规划常见的建筑、道路、地块边界和底图数据为例,讲清楚 Three.js 地理空间可视化的核心流程、坐标处理方法、数据对接代码和常见坑。
引言:Three.js地理空间可视化适合解决什么问题
Three.js 是一个基于 WebGL 的 JavaScript 三维渲染库。它本身不是 GIS 平台,但非常适合做轻量级、定制化的三维地理空间可视化,例如城乡规划方案展示、建筑体量分析、道路廊道表达、地块开发强度对比和三维专题图。
在城乡规划场景中,常见需求包括:
- 把建筑轮廓面拉伸为三维建筑模型。
- 把道路中心线或道路面显示为三维场景中的交通骨架。
- 将地块、用地性质、控规指标做颜色分级。
- 支持鼠标点击查询地块编号、建筑高度、用地类型等属性。
- 与后端 GIS 数据服务对接,动态加载 GeoJSON 或瓦片数据。
本文的目标不是替代 Cesium 或专业三维 GIS 平台,而是给你一套可落地的 Three.js 地理空间可视化实现思路:从 GIS 数据准备、坐标转换、场景搭建、建筑拉伸,到属性查询和性能优化。

背景:城乡规划三维场景为什么不能直接套普通 Three.js Demo
很多 Three.js 入门示例使用的是任意笛卡尔坐标,例如 x、y、z 都在几十或几百以内。但 GIS 数据通常使用经纬度坐标、投影坐标或地方坐标,数值范围大、单位复杂、坐标轴方向也不一定符合 WebGL 场景习惯。
如果直接把经纬度坐标作为 Three.js 的 x、y 坐标,你会遇到几个典型问题:
- 场景比例严重变形,建筑宽度、高度和道路长度不一致。
- 模型离原点太远,出现抖动、闪烁或点击不准。
- GeoJSON 坐标顺序、投影坐标单位和 Three.js 坐标轴对应关系混乱。
- 建筑拉伸方向不对,二维面无法正确生成三维体块。
- 数据量稍大后,浏览器帧率明显下降。
所以,Three.js 地理空间可视化的关键不是“如何画一个三维物体”,而是“如何把 GIS 数据稳定、正确、高性能地映射到 Three.js 场景”。
原理:Three.js地理空间可视化的坐标与数据模型
1. 坐标转换:先把 GIS 坐标变成场景局部坐标
GIS 数据通常有两类坐标:
- 经纬度坐标:例如 EPSG:4326,单位是度。
- 投影坐标:例如 Web Mercator EPSG:3857、CGCS2000 高斯投影、地方独立坐标,单位通常是米。
Three.js 场景更适合使用局部平面坐标,单位可以理解为米。常见做法是:
- 将经纬度转换为投影坐标。
- 选取项目范围中心点作为局部原点。
- 所有点坐标减去原点坐标,得到局部 x、y。
- 将高度字段映射到 Three.js 的 z 或 y 轴,取决于你的坐标轴约定。
在 Three.js 中,很多开发者习惯使用 x 表示东西方向,z 表示南北方向,y 表示高度。本文示例采用这种约定。
2. 数据模型:GeoJSON 是最适合入门的数据对接格式
城乡规划三维场景常见数据可以先整理成 GeoJSON:
- 建筑面:Polygon 或 MultiPolygon,属性包含 height、floor、name。
- 道路数据:LineString、MultiLineString 或 Polygon,属性包含 road_type、width。
- 地块边界:Polygon,属性包含 landuse、plot_id、far、density。
GeoJSON 易读、易调试,适合中小范围项目原型。如果数据量很大,可以进一步考虑矢量瓦片、3D Tiles、二进制格式或后端分块加载。
3. 三维表达:建筑拉伸、道路铺设、地块着色
Three.js 地理空间可视化中,最常用的三种表达方式是:
- 建筑拉伸:将二维建筑轮廓 Polygon 转为 Shape,再用 ExtrudeGeometry 按高度拉伸。
- 道路表达:线数据可用 Line、TubeGeometry 或根据道路宽度生成面。
- 地块表达:将 Polygon 转为平面 Mesh,并按用地性质或指标分级设色。
步骤:从GIS数据对接到Three.js城乡规划三维场景
步骤一:准备建筑 GeoJSON 数据
建议先用 QGIS、ArcGIS Pro 或 GeoPandas 对数据做清洗。最低要求如下:
- 几何类型为 Polygon 或 MultiPolygon。
- 坐标系统一,推荐先投影到米制坐标系。
- 建筑高度字段存在,例如 height。
- 几何无自相交、无空几何、无异常小碎面。
- 属性字段命名简洁,便于前端读取。
一个简化后的建筑 GeoJSON 结构如下:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "规划住宅楼A",
"height": 54,
"floor": 18,
"landuse": "居住用地"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[120.123456, 30.123456],
[120.123756, 30.123456],
[120.123756, 30.123756],
[120.123456, 30.123756],
[120.123456, 30.123456]
]
]
}
}
]
}
步骤二:安装前端依赖
如果你使用 Vite 创建前端项目,可以安装 Three.js 和坐标转换库 proj4:
npm install three proj4
Three.js 负责三维渲染,proj4 负责坐标转换。对于城乡规划项目,如果数据已经是米制投影坐标,也可以暂时不使用 proj4,只做局部原点平移。
步骤三:创建基础 Three.js 场景
下面代码创建一个基础三维场景,包括相机、光照、渲染器和控制器。为了让 GIS 场景更直观,示例使用 y 轴表示高度,x 和 z 表示平面位置。
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
const container = document.getElementById('app');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf4f7fb);
const camera = new THREE.PerspectiveCamera(
45,
container.clientWidth / container.clientHeight,
1,
100000
);
camera.position.set(800, 900, 1200);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.update();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(500, 1000, 800);
scene.add(directionalLight);
const grid = new THREE.GridHelper(2000, 40, 0x999999, 0dddddd);
scene.add(grid);
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
步骤四:定义坐标转换函数
如果源数据是 EPSG:4326 经纬度,可以先转为 EPSG:3857,再减去项目中心点。实际生产中,最好使用当地适合的米制投影坐标,而不是盲目使用 EPSG:3857。
import proj4 from 'proj4';
const WGS84 = 'EPSG:4326';
const WEB_MERCATOR = 'EPSG:3857';
proj4.defs(WGS84, '+proj=longlat +datum=WGS84 +no_defs');
proj4.defs(WEB_MERCATOR, '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs');
// 项目局部原点,建议使用项目范围中心点
const originLngLat = [120.123600, 30.123600];
const originMercator = proj4(WGS84, WEB_MERCATOR, originLngLat);
function lngLatToScene(lng, lat) {
const p = proj4(WGS84, WEB_MERCATOR, [lng, lat]);
const x = p[0] - originMercator[0];
const z = -(p[1] - originMercator[1]);
return new THREE.Vector2(x, z);
}
这里将北向坐标取负,是为了让屏幕观察方向更符合常见 Three.js 场景习惯。你也可以不取负,但必须在道路、地块、建筑和底图中保持同一套规则。
步骤五:把建筑 Polygon 拉伸成三维模型
Three.js 的 Shape 可以根据二维点构建面,再通过 ExtrudeGeometry 拉伸为三维建筑。下面示例处理单个 Polygon,MultiPolygon 可以拆成多个 Polygon 分别处理。
function createBuildingMesh(feature) {
const coords = feature.geometry.coordinates[0];
const height = Number(feature.properties.height || 10);
const shape = new THREE.Shape();
coords.forEach((coord, index) => {
const point = lngLatToScene(coord[0], coord[1]);
if (index === 0) {
shape.moveTo(point.x, point.y);
} else {
shape.lineTo(point.x, point.y);
}
});
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: height,
bevelEnabled: false
});
// ExtrudeGeometry 默认沿 z 方向拉伸,这里旋转后让高度进入 y 轴
geometry.rotateX(Math.PI / 2);
const material = new THREE.MeshLambertMaterial({
color: getBuildingColor(height),
transparent: true,
opacity: 0.92
});
const mesh = new THREE.Mesh(geometry, material);
mesh.userData = feature.properties;
return mesh;
}
function getBuildingColor(height) {
if (height >= 80) return 0xd73027;
if (height >= 50) return 0xfc8d59;
if (height >= 25) return 0xfee08b;
return 0x91cf60;
}
这里的 userData 用于保存 GIS 属性,后续鼠标点击查询时可以直接读取。对于城乡规划展示,按建筑高度、容积率或建筑类型设色都很常见。
步骤六:加载 GeoJSON 并添加到场景
async function loadBuildings() {
const response = await fetch('/data/planning-buildings.geojson');
const geojson = await response.json();
const group = new THREE.Group();
group.name = 'buildings';
geojson.features.forEach(feature => {
if (feature.geometry.type === 'Polygon') {
const mesh = createBuildingMesh(feature);
group.add(mesh);
}
if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates.forEach(polyCoords => {
const subFeature = {
...feature,
geometry: {
type: 'Polygon',
coordinates: polyCoords
}
};
const mesh = createBuildingMesh(subFeature);
group.add(mesh);
});
}
});
scene.add(group);
}
loadBuildings();
如果数据来自后端接口,只需要把 fetch 地址换成你的 GIS 服务地址。例如 Node.js、Python Flask、GeoServer、PostGIS API 都可以输出 GeoJSON。
步骤七:对接 PostGIS 输出 GeoJSON
在实际项目中,建筑、道路、地块通常存储在 PostGIS 中。后端可以用 SQL 将结果转成 GeoJSON,再返回给前端。
SELECT json_build_object(
'type', 'FeatureCollection',
'features', json_agg(
json_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::json,
'properties', json_build_object(
'name', name,
'height', height,
'floor', floor,
'landuse', landuse
)
)
)
) AS geojson
FROM planning_buildings
WHERE project_id = 'demo_001';
如果前端已经约定接收 EPSG:4326,则后端用 ST_Transform 转到 4326。若前端直接使用米制投影坐标,则可以输出项目投影坐标,但必须在接口文档中写清楚坐标系。
步骤八:添加鼠标点击查询 GIS 属性
Three.js 中可以使用 Raycaster 做拾取。点击建筑后读取 mesh.userData,即可显示名称、高度、楼层等属性。
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onClick(event) {
const rect = renderer.domElement.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 buildingGroup = scene.getObjectByName('buildings');
if (!buildingGroup) return;
const intersects = raycaster.intersectObjects(buildingGroup.children, true);
if (intersects.length > 0) {
const mesh = intersects[0].object;
const props = mesh.userData;
console.log('建筑名称:', props.name);
console.log('建筑高度:', props.height);
console.log('楼层:', props.floor);
console.log('用地类型:', props.landuse);
}
}
renderer.domElement.addEventListener('click', onClick);
如果需要在页面上显示弹窗,可以用普通 HTML 面板展示属性,不一定要在 Three.js 中绘制文字。这样更清晰,也更容易适配移动端。
常见坑:Three.js地理空间可视化最容易出错的地方
坑一:直接使用经纬度做场景坐标
经纬度单位是度,不是米。纬度方向 0.001 度和经度方向 0.001 度对应的实际距离不一样,而且随纬度变化。直接使用经纬度会导致比例错误。正确做法是先投影,再转局部坐标。
坑二:坐标数值太大导致模型抖动
WebGL 使用浮点数计算,坐标值过大时容易出现精度问题。即使数据已经是米制投影坐标,也不要直接把几百万级的坐标传给 Three.js。应减去项目中心点,转换为局部坐标。
坑三:Polygon 环方向和孔洞处理不完整
GeoJSON Polygon 的第一个环通常是外环,后续环可能是孔洞。简单 Demo 只处理外环没有问题,但遇到带庭院、内洞的建筑时会显示错误。生产项目需要将内环作为 Shape 的 holes 处理。
坑四:高度字段单位不统一
有的建筑数据 height 是米,有的只有 floor 楼层数,有的高度字段为空。如果直接拉伸,会出现极高或极低的异常建筑。建议统一规则:优先使用 height;没有 height 时用 floor 乘以标准层高;仍然为空时使用默认高度。
坑五:一次性加载过多 GeoJSON
GeoJSON 可读性好,但体积较大。若建筑超过数万栋,前端解析和生成 Mesh 都会变慢。此时应考虑空间分块、按视域加载、简化几何、合并材质、InstancedMesh 或改用三维瓦片方案。
方法比较:Three.js、Cesium、Mapbox GL 和专业三维GIS怎么选
| 方案 | 适合场景 | 优势 | 限制 |
|---|---|---|---|
| Three.js | 定制化城乡规划三维展示、小范围园区、方案汇报系统 | 渲染自由度高,交互可控,前端生态成熟 | GIS 能力需要自行实现,坐标和数据管理成本较高 |
| Cesium | 大范围三维地球、倾斜摄影、3D Tiles、城市级三维场景 | 地理坐标体系完整,适合大范围三维 GIS | 界面和渲染逻辑定制成本相对较高 |
| Mapbox GL / MapLibre GL | 二维地图、矢量瓦片、轻量三维建筑 | 地图底图和矢量瓦片能力强,WebGIS 集成方便 | 复杂三维模型和深度定制不如 Three.js 灵活 |
| 专业三维 GIS 平台 | 城市信息模型、规划审批、BIM+GIS 融合 | 功能完整,数据管理和业务流程成熟 | 成本较高,二次开发和前端自由度受平台影响 |
简单判断:如果你的项目重点是小范围规划场景、建筑体块表达和定制交互,Three.js 地理空间可视化很合适;如果需要全球尺度、真实地球曲率、海量 3D Tiles,则优先考虑 Cesium。
检查清单:上线前如何验证城乡规划三维场景是否可靠
- 确认所有 GIS 数据坐标系一致,接口文档写明 EPSG 编码或投影参数。
- 确认前端坐标转换函数只使用一套原点和轴向规则。
- 抽查 3 到 5 个建筑的平面位置,与 QGIS 或 ArcGIS Pro 中的位置一致。
- 抽查建筑高度,确认 height 字段单位为米。
- 检查地块、道路、建筑之间是否存在明显错位。
- 使用浏览器性能面板观察加载时间、帧率和内存占用。
- 对大数据量图层启用分块加载或按需加载。
- 为点击查询保留 feature id,便于从前端追溯到后端数据库记录。
- 对异常几何进行预处理,避免自相交 Polygon 导致拉伸失败。
- 在不同屏幕尺寸下测试相机初始视角和交互体验。
FAQ:Three.js地理空间可视化常见问题
Three.js 可以直接加载 Shapefile 吗?
不建议在前端直接加载 Shapefile。Shapefile 包含多个文件,编码、投影和几何解析都比较麻烦。更推荐在后端或桌面 GIS 中先转换为 GeoJSON、矢量瓦片或接口服务,再由 Three.js 前端加载。
Three.js 地理空间可视化一定要使用 EPSG:3857 吗?
不一定。EPSG:3857 适合 Web 地图场景,但城乡规划项目更推荐使用当地米制投影坐标,例如国家标准坐标系下的高斯投影或项目指定坐标系。关键是单位要稳定,比例要正确,并且转换为局部坐标后再进入 Three.js。
建筑拉伸后为什么方向倒了?
通常是坐标轴约定不一致导致的。Three.js 中 y 轴常作为高度,但 ExtrudeGeometry 默认拉伸方向可能与你的场景约定不同。可以通过 rotateX、坐标取负或统一 x、y、z 映射规则解决。不要在不同图层中使用不同规则。
GeoJSON 文件太大,Three.js 加载很慢怎么办?
可以从四个方向优化:一是用 QGIS 或 PostGIS 简化几何;二是按项目范围、行政区或网格分块;三是只加载当前视野附近数据;四是合并材质和几何,减少 Draw Call。若数据达到城市级规模,应考虑 Cesium 3D Tiles 或矢量瓦片。
如何把道路也做成三维效果?
如果道路是线数据,可以用 Line 或 TubeGeometry 表达;如果需要真实宽度,建议在 GIS 端先把道路中心线缓冲为面,再在 Three.js 中作为平面 Mesh 显示。规划展示中,道路通常不需要明显高度,只要稍微高于地面,避免 z-fighting 即可。
Three.js 能和 PostGIS 实时联动吗?
可以。常见架构是 PostGIS 存储空间数据,后端接口根据项目范围或视野范围查询数据并返回 GeoJSON,前端 Three.js 负责渲染。需要注意空间索引、接口分页、数据简化和缓存,避免一次性把全库数据传给浏览器。
结论:用Three.js做城乡规划三维场景,重点是GIS数据工程
Three.js 地理空间可视化的核心并不只是三维渲染,而是 GIS 数据到 WebGL 场景之间的可靠转换。对于城乡规划三维场景,建议按“数据清洗、坐标转换、局部原点、建筑拉伸、属性绑定、性能优化”这条主线实施。
如果项目范围较小、交互需求强、界面需要高度定制,Three.js 是非常灵活的选择;如果项目涉及城市级海量三维数据、真实地球坐标和 3D Tiles,则应优先评估 Cesium 或专业三维 GIS 平台。掌握本文这套流程后,你就可以把建筑、道路、地块等 GIS 数据稳定接入 Three.js,构建可用于规划展示和方案分析的三维场景。