简介:这是一份面向计算机相关专业学生与开发者的Vue+Ceisum三维地理信息可视化学习项目,适用于毕业设计、课程设计及GIS方向入门实践。资源包含完整可运行的前端工程源码、详细使用说明文档及多源底图接入方案(Cesium Ion、天地图、上海地形图DEM数据),覆盖坐标系转换、地形切片处理、角度弧度换算等核心GIS开发要点。压缩包共488个文件,以107个JS逻辑文件、17个Vue组件、54个CSS样式文件、36个JSON配置及173张PNG素材图为主,辅以B3DM三维模型、GLB场景、KML/KMZ矢量数据等,整体6.01MB,结构清晰便于模块化学习与功能扩展。已有156人下载学习,项目经实测可直接运行,既可作为零基础入门范例,也支持进阶者基于现有代码二次开发,快速构建定制化三维WebGIS应用。
1. 这不是个“Vue + Cesium” Hello World,而是一套可跑通的三维地理信息训练闭环
你打开一个 Vue 项目,npm run serve启动后看到地球旋转——这不叫掌握 Cesium。真正卡住大多数人的,是底图加载失败、地形切片黑块、坐标转换后模型飘在空中、.b3dm文件加载无响应、甚至CesiumWidget初始化就报undefined is not a function。这个资源包里没有花哨的 UI 组件库堆砌,而是用lr.b3dm/ul.b3dm/parent.b3dm等真实地形瓦片文件,配合widgets.css和lighter.css的轻量样式,构建了一个从坐标系理解、底图接入、地形加载、到节点操作的完整训练链路。它专为计算机类专业学生设计:毕业设计要交可演示的三维场景,课程设计需体现 GIS 数据处理能力,大作业得覆盖 Vue 生命周期与 Cesium 异步资源管理的协同逻辑。如果你正被“Cesium 加载天地图白屏”“上海 DEM 高程不生效”“Cartographic 转 Cartesian3 偏移 500 米”这些问题反复打断,这个源码包就是你调试时能逐行断点、改参数、看控制台日志的真实沙盒。
2. 底图接入与多源服务配置:从 Cesium Ion 到天地图再到本地 DEM
2.1 为什么必须区分三种底图类型?——服务协议、坐标系与瓦片结构决定加载方式
Cesium 支持三类底图:托管服务(如 Cesium Ion)、标准 WMTS/TMS(如天地图)、本地静态瓦片(如上海地形图.tif转出的.b3dm)。它们本质差异在于坐标系声明、URL 模板和认证机制。Cesium Ion 使用WebMercatorTilingScheme,天地图采用GeographicTilingScheme,而本地 DEM 必须通过HeightmapTerrainData解析二进制高程数据。若混用createTileMapServiceImageryProvider加载天地图却未设置ellipsoid,或用IonImageryProvider请求本地.png瓦片,必然触发Failed to load image错误。本项目在src/utils/imageryProviders.js中明确分离三类 Provider 实例:
// src/utils/imageryProviders.js import * as Cesium from 'cesium'; export const cesiumIonProvider = new Cesium.IonImageryProvider({ assetId: 3954, // Cesium World Terrain ID accessToken: 'your_access_key_here' // 替换为 https://cesium.com/ion/tokens 获取的 token }); export const tiandituProvider = new Cesium.WebMapTileServiceImageryProvider({ url: 'https://t0.tianditu.gov.cn/img_w/wmts', layer: 'img', style: 'default', format: 'tiles', tileMatrixSetID: 'w', maximumLevel: 18, credit: '天地图', tilingScheme: new Cesium.GeographicTilingScheme() // 关键:必须匹配天地图地理坐标系 }); export const shanghaiDemProvider = new Cesium.HeightmapTerrainData({ buffer: new Uint16Array(), // 实际由 /data/shanghai_dem.bin 加载 width: 256, height: 256, hasWaterMask: false, hasNoData: true, noDataValue: -9999 });提示:
accessToken不是永久有效,Cesium Ion 免费 tier 每月 10GB 流量,超限后cesiumIonProvider会静默降级为白底。生产环境务必在main.js中注入 token 并监听Cesium.Ion.defaultAccessToken变更。
2.2 天地图 URL 拆解与跨域代理配置——绕过 Referer 校验与 CORS 限制
天地图接口强制校验Referer头且返回Access-Control-Allow-Origin: *不稳定。直接在浏览器中请求https://t0.tianditu.gov.cn/img_w/wmts?...会触发net::ERR_FAILED。解决方案是在 Vue CLI 中配置vue.config.js代理:
// vue.config.js module.exports = { devServer: { proxy: { '/tianditu': { target: 'https://t0.tianditu.gov.cn', changeOrigin: true, pathRewrite: { '^/tianditu': '' } } } } };然后在tiandituProvider的url中使用相对路径:
url: '/tianditu/img_w/wmts', // 代理后实际请求 https://t0.tianditu.gov.cn/img_w/wmts同时,天地图瓦片 URL 模板需严格匹配其文档规范:
https://t0.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0& layer=img&style=default&format=tiles&tileMatrixSet=w&tileMatrix={level}& tileRow={row}&tileCol={column}&tk=your_tk_here其中tk是天地图开发者密钥(需注册获取),tileMatrixSet=w对应 Web Mercator 投影,{level}范围为 1–18。项目中src/config/tianditu.js封装了 tk 生成逻辑,避免硬编码泄露。
2.3 上海 DEM 数据接入:从 GeoTIFF 到 HeightmapTerrainData 的转换流程
项目提供的shanghai_dem.tif是 GDEMV3 30M 分辨率数据,覆盖经度 121.5°–122.5°、纬度 30.5°–31.5°。但 Cesium 无法直接读取 GeoTIFF,需转为HeightmapTerrainData兼容的二进制格式。本项目附带scripts/convert-dem.js脚本(Node.js 环境):
// scripts/convert-dem.js const gdal = require('gdal'); const fs = require('fs'); const dataset = gdal.open('./data/shanghai_dem.tif'); const band = dataset.bands.get(1); const width = dataset.rasterSize.x; const height = dataset.rasterSize.y; // 读取为 Int16Array(Cesium Heightmap 要求) const buffer = new Int16Array(width * height); band.pixels.read(0, 0, width, height, buffer); // 写入二进制文件 fs.writeFileSync('./public/data/shanghai_dem.bin', Buffer.from(buffer.buffer)); console.log(`Converted ${width}x${height} DEM to ./public/data/shanghai_dem.bin`);关键参数说明:
band.pixels.read()第四参数buffer必须为Int16Array,因 CesiumHeightmapTerrainData默认解析为 signed 16-bit;width/height需为 2 的幂(如 256、512),否则Cesium.TerrainProvider初始化失败;- 输出
.bin文件需放在public/目录下,确保webpack-dev-server可直接访问。
加载时需指定terrainProvider:
const viewer = new Cesium.Viewer('cesiumContainer', { terrainProvider: new Cesium.CesiumTerrainProvider({ url: '/data/shanghai_dem.bin', // 注意路径前缀 requestVertexNormals: true }) });3. 地形切片(.b3dm)加载与坐标系转换实战:从屏幕点击到三维定位
3.1 .b3dm 文件结构解析与动态加载策略
项目中的lr.b3dm、ul.b3dm等文件是 3D Tiles 规范下的批次 3D 模型(Batched 3D Model),包含几何、纹理、材质及可选的RTC_CENTER(Relative To Center)偏移。它们不是独立渲染单元,而是3DTileset的子节点。直接new Cesium.Cesium3DTileset({ url: 'lr.b3dm' })会报错,正确做法是:
- 创建
3DTileset指向根 JSON 文件(本项目为tileset.json); - 在
tileset.json中声明root节点的children数组,每个 child 指向.b3dm; - 使用
viewer.scene.primitives.add(tileset)加入场景。
tileset.json示例节选:
{ "asset": { "version": "1.0" }, "geometricError": 100, "root": { "boundingVolume": { "region": [121.5, 30.5, 122.5, 31.5, 0, 1000] }, "geometricError": 50, "refine": "ADD", "children": [ { "boundingVolume": { "region": [121.5, 30.5, 122.0, 31.0, 0, 1000] }, "content": { "uri": "lr.b3dm" } }, { "boundingVolume": { "region": [122.0, 30.5, 122.5, 31.0, 0, 1000] }, "content": { "uri": "ur.b3dm" } } ] } }注意:
region字段顺序为[west, south, east, north, minimumHeight, maximumHeight],单位为弧度(WGS84),非度。121.5° 需转为121.5 * Math.PI / 180。
3.2 屏幕坐标 → 地理坐标 → 笛卡尔坐标的三级转换链
Cesium 坐标系转换不是单次调用,而是依赖上下文精度的链式操作。项目src/utils/coordinateConvert.js提供了可复用函数:
// src/utils/coordinateConvert.js import * as Cesium from 'cesium'; export function screenToCartographic(viewer, position) { // 1. 屏幕像素坐标转笛卡尔方向向量(从相机出发) const ray = viewer.camera.getPickRay(position); if (!ray) return null; // 2. 射线与地形相交,获取交点笛卡尔坐标 const intersection = Cesium.SceneTransforms.wgs84ToWindowCoordinates( viewer.scene.globe.ellipsoid.cartesianToCartographic( Cesium.IntersectionTests.rayPlane(ray, Cesium.Plane.fromPointNormal(Cesium.Cartesian3.ZERO, Cesium.Cartesian3.UNIT_Z)) ), viewer.scene ); if (!intersection) return null; // 3. 笛卡尔坐标转地理坐标(经度、纬度、高度) const cartographic = Cesium.Cartographic.fromCartesian(intersection); return { longitude: Cesium.Math.toDegrees(cartographic.longitude), latitude: Cesium.Math.toDegrees(cartographic.latitude), height: cartographic.height }; } export function cartographicToCartesian(longitude, latitude, height = 0) { const cartographic = Cesium.Cartographic.fromDegrees(longitude, latitude, height); return Cesium.Ellipsoid.WGS84.cartographicToCartesian(cartographic); }参数说明:
screenToCartographic中position是{ x: number, y: number },单位为像素,原点在左上角;cartographicToCartesian的height单位为米,若传0表示 WGS84 椭球面,非海平面;- 所有角度输入/输出均需
Cesium.Math.toDegrees()或Cesium.Math.toRadians()显式转换,Cesium 内部全用弧度。
3.3 点击事件绑定与模型节点高亮:基于 Entity 与 Primitive 的双模式交互
项目src/components/CesiumViewer.vue实现了点击高亮.b3dm中特定建筑的功能。核心逻辑分两层:
Entity 模式(适合少量动态对象):
const entity = viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(121.8, 31.2, 10), point: { pixelSize: 10, color: Cesium.Color.RED }, name: 'Shanghai Tower' });Primitive 模式(适合海量静态模型):
// 监听 3DTileset 点击事件 viewer.scene.globe.depthTestAgainstTerrain = true; const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) => { const pickedObject = viewer.scene.pick(movement.position); if (pickedObject && pickedObject.id) { // 获取 pickedObject.id._batchTable 读取属性 console.log('Picked batch ID:', pickedObject.id._batchTable.batchLength); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
关键区别:Entity可直接修改point.color,但性能差;3DTileset的pick返回Cesium.PickedObject,需通过_batchTable访问原始属性(如建筑名称、楼层),本项目widgets.css中.highlight类定义了高亮边框样式,通过viewer.scene.postRender.addEventListener动态注入着色器实现。
4. Vue 生命周期与 Cesium 资源管理:避免内存泄漏与初始化竞态
4.1mountedvsnextTick:Cesium 容器 DOM 尺寸就绪时机判断
Vue 组件mounted钩子触发时,<div id="cesiumContainer">已挂载,但其offsetWidth/offsetHeight可能为0(尤其当父组件使用v-if或 CSSdisplay: none)。直接new Cesium.Viewer('cesiumContainer')会导致 canvas 渲染区域异常。本项目在CesiumViewer.vue中采用双重校验:
<script> export default { mounted() { this.initCesium(); }, beforeUnmount() { if (this.viewer) { this.viewer.destroy(); // 必须调用,否则 WebGL 上下文残留 this.viewer = null; } }, methods: { initCesium() { // 1. 等待 DOM 尺寸就绪 const checkSize = () => { const container = document.getElementById('cesiumContainer'); if (container && container.offsetWidth > 0 && container.offsetHeight > 0) { this.createViewer(container); } else { requestAnimationFrame(checkSize); // 比 setTimeout 更精准 } }; checkSize(); }, createViewer(container) { this.viewer = new Cesium.Viewer(container, { terrainProvider: Cesium.createWorldTerrain(), baseLayerPicker: false, geocoder: false, timeline: false, animation: false }); // 2. 监听窗口 resize window.addEventListener('resize', this.handleResize); }, handleResize() { if (this.viewer && this.viewer._container) { this.viewer.resize(); // 强制重置 canvas 尺寸 } } } }; </script>提示:
requestAnimationFrame比this.$nextTick更可靠,因后者仅保证 Vue 更新队列清空,不保证浏览器 layout 完成。
4.2 异步资源加载状态管理:用 Vuex 模块跟踪底图、地形、模型加载进度
项目store/modules/cesium.js定义了加载状态机:
// store/modules/cesium.js const state = { imageryStatus: 'idle', // 'loading' | 'success' | 'error' terrainStatus: 'idle', tilesetStatus: 'idle', loadingProgress: 0 // 0-100 }; const mutations = { SET_IMAGERY_STATUS(state, status) { state.imageryStatus = status; }, SET_LOADING_PROGRESS(state, progress) { state.loadingProgress = Math.min(100, Math.max(0, progress)); } }; const actions = { async loadImagery({ commit }, provider) { commit('SET_IMAGERY_STATUS', 'loading'); try { await provider.readyPromise; // 等待 ImageryProvider 初始化完成 commit('SET_IMAGERY_STATUS', 'success'); } catch (e) { commit('SET_IMAGERY_STATUS', 'error'); console.error('Imagery load failed:', e); } } };在组件中通过mapActions(['loadImagery'])调用,并在watch中响应状态变化:
watch: { 'cesium.imageryStatus'(newVal) { if (newVal === 'success') { this.$message.success('底图加载完成'); } else if (newVal === 'error') { this.$message.error('底图加载失败,请检查网络或密钥'); } } }4.3 Cesium Viewer 销毁与内存回收:destroy()的隐式依赖清理
viewer.destroy()并非简单释放 WebGL 上下文,它会:
- 移除所有
Scene事件监听器; - 清空
DataSourceCollection中的 Entity; - 释放
Cesium3DTileset的 GPU 缓存; - 但不会自动清除
ScreenSpaceEventHandler实例。
因此beforeUnmount中必须手动销毁:
beforeUnmount() { if (this.handler) { this.handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK); this.handler.destroy(); // 必须显式调用 } if (this.viewer) { this.viewer.destroy(); } }验证内存是否回收:打开 Chrome DevTools → Memory → Take Heap Snapshot,对比切换组件前后的Cesium.*对象数量。若Cesium.Viewer实例数持续增长,即存在泄漏。
5. 进阶技巧:基于 lighter.css 的轻量主题定制与性能优化开关
5.1 widgets.css 与 lighter.css 的样式覆盖优先级控制
项目提供两套 CSS:widgets.css(Cesium 官方控件默认样式)和lighter.css(精简版,移除了baseLayerPicker、geocoder等冗余 UI)。二者共存时,lighter.css通过!important覆盖关键属性,但需注意加载顺序:
<!-- public/index.html --> <link rel="stylesheet" href="./widgets.css"> <link rel="stylesheet" href="./lighter.css"> <!-- 后加载,高优先级 -->lighter.css中典型覆盖项:
/* 隐藏默认控件 */ .cesium-viewer-toolbar, .cesium-viewer-bottom, .cesium-viewer-animationContainer { display: none !important; } /* 重定义鼠标悬停提示 */ .cesium-viewer-tooltip { background: rgba(0,0,0,0.7) !important; color: #fff !important; font-size: 12px !important; }注意:
!important仅用于覆盖 Cesium 内联样式,避免在业务 CSS 中滥用,否则难以维护。
5.2 性能开关表:通过 Cesium API 动态关闭非必要渲染通道
Cesium 默认启用多项高级渲染特性,但在低端设备或纯地理分析场景下可关闭以提升帧率。项目src/utils/performanceTuning.js提供开关函数:
| 开关项 | API 调用 | 效果 | 推荐场景 |
|---|---|---|---|
| 阴影计算 | viewer.scene.globe.shadows = false | 关闭地形阴影 | 移动端、快速漫游 |
| 大气散射 | viewer.scene.globe.showSkyAtmosphere = false | 移除蓝色天穹 | 夜间模式、室内三维 |
| 深度测试 | viewer.scene.globe.depthTestAgainstTerrain = false | 加速地形穿透检测 | 仅需表面可视化 |
| 抗锯齿 | viewer.scene.fxaa = false | 关闭 FXAA 后处理 | CPU/GPU 资源紧张 |
使用示例:
// 启用性能模式 export function enablePerformanceMode(viewer) { viewer.scene.globe.shadows = false; viewer.scene.globe.showSkyAtmosphere = false; viewer.scene.globe.depthTestAgainstTerrain = false; viewer.scene.fxaa = false; viewer.scene.logarithmicDepthBuffer = false; // 关闭对数深度缓冲 }5.3 自定义坐标显示:将 Cartesian3 实时转为度分秒格式并注入 DOM
项目src/components/CoordDisplay.vue实现了右下角实时坐标显示,核心是监听camera.moveEnd事件并转换:
// src/components/CoordDisplay.vue export default { data() { return { coordText: '等待定位...' }; }, mounted() { this.viewer.camera.moveEnd.addEventListener(this.updateCoord); }, beforeUnmount() { this.viewer.camera.moveEnd.removeEventListener(this.updateCoord); }, methods: { updateCoord() { const position = this.viewer.camera.position; const cartographic = Cesium.Cartographic.fromCartesian(position); const lon = Cesium.Math.toDegrees(cartographic.longitude); const lat = Cesium.Math.toDegrees(cartographic.latitude); const height = cartographic.height; // 转度分秒 const toDMS = (deg) => { const d = Math.floor(Math.abs(deg)); const m = Math.floor((Math.abs(deg) - d) * 60); const s = ((Math.abs(deg) - d - m/60) * 3600).toFixed(2); return `${d}°${m}'${s}"`; }; this.coordText = `${toDMS(lon)} ${toDMS(lat)} ${height.toFixed(1)}m`; } } };此实现避免了Cesium.SceneTransforms.wgs84ToWindowCoordinates的性能开销,直接从相机位置推导视点地理坐标,适用于教学演示中强调“我在哪”的直观反馈。
本文还有配套的精品资源,点击获取