简介:面向WebGIS与三维可视化开发者的Cesium实践资源,围绕三维GIS场景搭建与场景视频融合展开,契合当前Web3D与零插件浏览器渲染的应用趋势。资源基于Cesium开源库,覆盖从三维场景构建、数据加载、模型展示到视频融合的完整流程,适合具备一定前端基础、希望将三维GIS技术快速落地到实际项目的研发人员与学习者。压缩包共2000个文件,整体约54.64MB,以js、html等前端代码为主,配合java后台逻辑、css样式、json配置以及png/gif可视化素材,并包含b3dm三维模型数据,目录结构清晰,便于按模块检索定位。目前已有986人学习下载,可结合项目源码直接理解Cesium在场景初始化、图层叠加、视频投影等环节的具体用法。其中提供了大量可运行或二次改造的示例页面和模型资源,既能用于快速搭建原型,也可作为深入掌握三维场景管理与视频融合机制的入门参考;前后端代码与配置文件的相互对照,则为后续扩展功能、优化渲染性能提供了便利。
1. 写代码前先装对:Cesium三维场景搭建的版本坑与落地前提
做三维GIS场景开发的人,十有八九是从Cesium入门的。它把地球、地形、影像、模型、视频融合这些能力都收进了浏览器里,不需要自己从底层写GPU渲染。但真正动手时,大部分人第一关就卡在环境上:不是Cesium本身多难,而是Node版本太老、Vite构建报错、token没配,甚至视频融合那个模块压根没装全,翻车翻得莫名其妙。
我最初接手一个「某跨平台系统」的三维监控项目时,需求是先把园区三维场景搭起来,再把实时监控视频画面贴到楼栋模型表面。当时我用的Cesium版本是1.112,配Vite 5和Node 18,踩了一圈坑才跑通。这篇就把整个落地过程拆开写——从装什么版本、怎么配参数,到视频融合的坐标换算,再到装坏了怎么救,照着走一遍基本能跑通。适合刚开始搭Cesium工程、或者正被视频融合那块卡住的开发者。
2. 环境初始化:Node 18 + Vite 5 + Cesium 1.112 的开发基座
2.1 为什么锁这三个版本:版本匹配是关键
装Cesium之前,先把宿主环境的版本定死。Cesium官方仓库从1.100之后,对ESM模块和现代构建工具的支持力度明显加大,但这也意味着老一套的webpack 4 + Node 14方案在加载Cesium核心包时会出现不少兼容问题。我这边最终用的组合是:
- Node.js 18 LTS
- Vite 5.x
- Cesium 1.112
- 包管理器用的 pnpm 8,不用npm
Node 18是当前多数构建工具兼容性最稳的LTS版本。如果你机器上还是Node 14或16,建议直接用nvm切换,不要硬撑着跑,否则Vite的依赖解析阶段就会冒出一堆ERR_OSSL_EVP_UNSUPPORTED之类的OpenSSL报错。Vite 5要求Node 18以上,而Cesium 1.112的ESM构建产物正好和Vite 5的依赖预构建策略合拍。
Cesium版本选择上,不建议追新到1.120以上。新版本对glTF模型的处理逻辑改动较大,部分旧模型材质会出现色差,而且社区里的踩坑案例还没沉淀够,出了问题能搜到的资料少。1.112这个版本序列里,地形、影像、视频融合这些核心API都稳定,网上的资料也能对上号。
2.2 初始化工程目录与安装依赖
直接上命令,我用Vite官方脚手架建一个空项目,然后装Cesium:
# 创建项目,模板选 vanilla-ts,JavaScript 纯项目可选 vanilla pnpm create vite cesium-gis-demo --template vanilla-ts # 进入目录并安装基础依赖 cd cesium-gis-demo pnpm install # 安装 Cesium 本体 pnpm add cesium@1.112 # 安装 vite 插件,用于处理 Cesium 的资源文件 pnpm add -D vite-plugin-static-copy这里vite-plugin-static-copy是Cesium跑在Vite下必不可少的一个插件。Cesium运行时要从Assets目录加载地球纹理、天空盒、地形切片这些静态资源,如果构建工具不把这些资源复制到产物目录,页面打开就是一片空白的黑屏,控制台报404。我装完Cesium后第一件事就是把这个插件配上。
2.3 Vite 配置:静态资源复制与CESIUM_BASE_URL
装完依赖后,修改项目根目录下的vite.config.ts,这是整个环境初始化里最关键的一步:
import { defineConfig } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import path from 'path'; export default defineConfig({ resolve: { alias: { // 让源码里可以直接用 cesium 包名引入 cesium: path.resolve(__dirname, 'node_modules/cesium/index.cjs'), }, }, plugins: [ viteStaticCopy({ targets: [ { src: 'node_modules/cesium/Build/Cesium/Workers/**/*', dest: 'cesium/Workers', }, { src: 'node_modules/cesium/Build/Cesium/ThirdParty/**/*', dest: 'cesium/ThirdParty', }, { src: 'node_modules/cesium/Build/Cesium/Assets/**/*', dest: 'cesium/Assets', }, { src: 'node_modules/cesium/Build/Cesium/Warehouses/**/*', dest: 'cesium/Warehouses', }, ], }), ], define: { // 关键:告诉 Cesium 运行时去哪里找静态资源 CESIUM_BASE_URL: JSON.stringify('/cesium'), }, });CESIUM_BASE_URL这个全局变量决定了Cesium加载资源时的根路径。如果不定义,Cesium会尝试用自己的相对路径去找Assets目录,在Vite开发服务器和打包产物里大概率找不到。我一般把这个值统一成/cesium,和dest里的目录名保持一致,这样开发和构建后的路径行为完全一致。另一个细节是define里的JSON.stringify不能省略,因为Vite的define替换是文本级替换,不加双引号的话,运行时读到的是一个裸字符串标识符,直接白屏。
2.4 初始化Viewer:第一个能转的地球
环境装齐后,在src/main.ts里先写一个最基础的Viewer,验证Cesium能不能正常起来:
import * as Cesium from 'cesium'; import './style.css'; // 申请一个 Cesium ion 的 Access Token,替换成自己的 Cesium.Ion.defaultAccessToken = '你的token字符串'; // 创建查看器,关闭默认的加载动画和控件,减少干扰 const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, // 关闭默认的影像层,后续手动添加 baseLayer: false, });baseLayer: false这行是画面控制的关键。Cesium默认会加载Ion上的全球影像,如果token无效或者网络访问不到Ion服务,整个地球就是黑的。我一般手动关闭默认图层,然后自己加一个稳定的影像源,这样在国内网络环境下也能稳定出图。sceneModePicker关掉是因为视频融合场景通常只需要3D模式,留着用户切到2D模式反而容易让视频贴片位置错乱。
3. 三维GIS场景搭建:从空白球到带地形影像的可用场景
3.1 影像底图与地形数据加载顺序
环境跑起来后,第二件事就是把「看得清」解决掉。Cesium默认的Ion影像在国内访问速度不稳定,我换成了一片自分发的影像瓦片服务。这里用标准WMTS格式接入,代码在Viewer创建之后执行:
// 影像底图:加载一个本地或内网可访问的 WMTS 服务 const imageryProvider = await Cesium.createWorldImageryAsync({ style: Cesium.IonWorldImageryStyle.AERIAL, }); viewer.imageryLayers.addImageryProvider(imageryProvider); // 地形:开启 3D Tiles 地形,获取高程信息 const terrainProvider = await Cesium.createWorldTerrainAsync({ requestWaterMask: true, requestVertexNormals: true, }); viewer.terrainProvider = terrainProvider;requestWaterMask和requestVertexNormals是两个容易忽略的参数。前者让水面有动态效果,后者让地表光照更自然,但代价是地形数据量变大、加载变慢。如果只是做园区级场景,建筑物本身是精细模型,地形精度反而不重要,这两个参数可以全关掉,换来更快的首屏速度。我实际项目里就只开了requestWaterMask,水面效果对监控场景的视觉判断更友好。
影像加载顺序上,先加地形再加影像,这样地形数据能先解算出高程,再让影像贴合到地形表面。反过来容易出现影像先平铺在椭球面上、等地形数据到达后再重新贴合的跳变闪烁。
3.2 添加3D Tiles建筑物与坐标系对齐
园区级三维GIS场景,建筑主体一般用3D Tiles格式加载。Cesium对3D Tiles的支持分两种:一种是自己用倾斜摄影生成的倾斜模型,另一种是手工建模转出来的白模。我这边用的是一个BIM导出的3D Tiles数据集:
// 加载3D Tiles建筑模型 const tileset = await Cesium.Cesium3DTileset.fromUrl('/data/3dtiles/plant/tileset.json', { maximumScreenSpaceError: 16, maximumMemoryUsage: 512, }); viewer.scene.primitives.add(tileset); viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0, -0.5, 200));maximumScreenSpaceError这个参数决定模型加载的精细度,值越小,模型越精细,但GPU压力也越大。默认值是16,对建筑外观展示够用;但如果需要放大看建筑细节,我会把它调到8以下。maximumMemoryUsage限制的是模型缓存占用的GPU内存,园区场景模型面数多,给512MB比较稳妥,再大会影响视频融合时的渲染帧率。
坐标系对齐是这一节最常见的坑。3D Tiles数据自带自己的地理参考系,如果你的场景里既有点云、又有BIM模型、还要贴视频,那么它们的坐标基准必须一致。我一般要求数据生产方统一输出成CGCS2000坐标系下的EPSG:4490经纬度,加载后在Cesium里就不需要做额外的位置偏移。如果模型出现和地球表面脱开几百米的偏差,先检查数据源的坐标系描述,而不是在代码里硬调位置。
3.3 场景默认视角与相机约束
场景搭好后,把相机初始视角锁定在目标建筑区域,并做一定程度的交互约束,避免用户拖拽时镜头乱飞:
// 设置初始相机视角,参数是经纬度、高度、朝向 viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.3912, 39.9072, 400), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-45), roll: 0, }, }); // 限制相机高度范围,防止缩得太低穿模或拉得太高失去场景感 viewer.scene.screenSpaceCameraController.minimumZoomDistance = 50; viewer.scene.screenSpaceCameraController.maximumZoomDistance = 2000;minimumZoomDistance限制的是相机与地表的最小距离,设为50米可以防止用户把镜头钻进建筑物内部,看到内部的背面剔除破面。maximumZoomDistance限制最高视角,统一了浏览尺度。heading和pitch的角度单位是弧度,我总是刚写代码时顺手写个90,结果视角朝着完全不对的方向,后来就强制自己都用Cesium.Math.toRadians包一下。
地形加载完成后,这个三维场景就具备可浏览、可缩放、带真实高程的GIS底座了。接下来是视频融合——把二维的监控画面,铺到这个三维场景的模型表面。
4. 场景视频融合:把监控画面铺到三维模型表面
4.1 视频融合的原理与适用条件
视频融合在Cesium里做的本质,是把一段视频画面作为纹理,贴到一个与监控摄像头视角大致匹配的几何体表面,然后再把这个几何体放进场景的正确位置。Cesium的VideoSynchronizer类是这一功能的核心,它能把一个HTML的<video>元素和Cesium的纹理同步起来,让视频播放状态和三维场景渲染帧保持同步。
但不是所有视频都能直接融合。我这边实际做完后总结出三个硬性条件:一是视频流必须是浏览器可以直接播放的格式,RTSP流不能直接给Cesium用,必须通过转流服务转成WebRTC或HLS;二是摄像头拍摄的是固定场景,云台式摄像机画面不适合做静态融合;三是视频画面里要有足够多的静止参照物,方便人工调整贴合位置。
我项目里用的是海康摄像头的RTSP流,后端用某转流中间件转成了HLS格式。注意Cesium的VideoSynchronizer对HLS的支持依赖浏览器本身的播放能力,Safari下没问题,Chrome需要装hls.js把流转成MediaSource再喂给<video>元素。
4.2 视频元素与VideoSynchronizer绑定
代码上分两步走。先在HTML里放一个隐藏的<video>元素,然后创建VideoSynchronizer把视频和Cesium场景同步起来:
<!-- index.html 中放置隐藏视频元素 --> <video id="monitorVideo" autoplay muted playsinline crossorigin="anonymous" src="/live/monitor01.m3u8" ></video>// main.ts 中创建 VideoSynchronizer const videoElement = document.getElementById('monitorVideo') as HTMLVideoElement; const synchronizer = new Cesium.VideoSynchronizer({ clock: viewer.clock, element: videoElement, }); // 如果是 HLS 流,需要先确保视频已经加载到可播放状态 videoElement.addEventListener('loadeddata', () => { videoElement.play().catch((error) => { console.warn('视频自动播放被拦截,等待用户交互后重试', error); }); });VideoSynchronizer的clock参数要传viewer.clock,这样视频播放进度会跟着Cesium场景的时钟走。Cesium暂停时视频也暂停,Cesium倍速播放时视频也跟着变化——虽然倍速看监控有点怪,但作为一个统一的多路视频管理机制,这个设计是合理的。
muted属性必须保留,大多数浏览器不允许带声音的视频自动播放。监控画面本身也不需要声音,但如果你要接的是带语音对讲的摄像机,这里需要改成静音后由用户点击某个按钮再开启声音,不能自动播放。
4.3 视频贴片几何体与位置换算
视频画面绑定到场景后,还需要一个几何体作为「幕布」。最常用的是一块PolygonGeometry或者一个RectangleGeometry,通过Material里的ImageMaterialProperty把视频元素作为纹理贴上去:
// 创建视频贴片几何体 const videoRectangle = viewer.entities.add({ id: 'video-overlay-01', // 矩形位置:西、南、东、北 四个边界经纬度 rectangle: { coordinates: Cesium.Rectangle.fromDegrees( 116.3905, // 西 39.9068, // 南 116.3920, // 东 39.9080 // 北 ), material: new Cesium.ImageMaterialProperty({ image: videoElement, // 关键:透明通道设为1,不透明 transparent: false, }), // 关闭高度贴地,手动指定一个高度让幕布立起来 height: 30, }, });这里有几个参数值得慢慢调。矩形四个边界值是视频画面覆盖的地理范围,来源是人工标定:在三维场景里找视频画面中的两个远端参照物,记下经纬度,再反向推算出矩形范围。这个过程在新项目中需要反复尝试,我的经验是先粗略框一个范围,然后加载视频纹理看偏移方向,一次调几米,两三次就能对齐。
height参数是幕布距离地面的高度。如果设为0,幕布会贴着地形走,遇到地形起伏时画面会被扯破;设为一个固定高度,比如建筑第10层的高度,幕布就是一块悬浮在空中的「虚拟屏幕」,透视关系更接近真实监控视角。但要记得把classificationType设为TERRAIN以外的值,否则这个矩形会被地形遮挡剔除掉。
4.4 视频画面与场景的透视校正
把矩形贴片调好位置后,还剩最后一块拼图——透视关系。监控摄像头有自己的朝向、俯仰角和视场角,而Cesium的矩形贴片默认是从上往下看的正交视角。两者之间必然存在偏差,看不出来说明是巧合。
Cesium里做校正用的还是ImageMaterialProperty加stRotation参数,把视频纹理在幕布上旋转一个角度:
// 调整视频纹理旋转角,单位是弧度 const material = new Cesium.ImageMaterialProperty({ image: videoElement, transparent: true, // 旋转视频画面,顺时针为正,逆时针为负 stRotation: Cesium.Math.toRadians(6), }); // 如果画面上下颠倒,检查是否旋转了180度 // 如果画面被拉伸,检查矩形的宽高比是否和视频分辨率一致stRotation的调整逻辑很简单,但每次只能解决旋转,解决不了透视形变。真正的透视校正,需要在贴片之外叠加一个PlaneGeometry,把它当作一个三维平面,手动旋转平面的heading和pitch来模拟摄像头的姿态。平面法的效果好得多,但需要预估摄像头的俯仰角,误差超过5度,画面看起来就是斜的。我这边是旋转、拉伸、平面倾斜三件套配合,先调平面姿态让参照物大致对齐,再用stRotation微调画面方向,最后调整矩形范围匹配视频宽高比。整个过程耗时半小时到两小时不等,属于人工标定环节,没捷径。
5. 避坑实录:Cesium安装与视频融合的五个典型踩坑
5.1 Cesium包引入报错:Cannot find module 'cesium' 或者纯黑屏
现象:pnpm安装完成后,代码里import * as Cesium from 'cesium'直接报模块找不到,或者编译通过但页面全黑。
原因:pnpm的node_modules结构是符号链接,某些版本的vite对pnpm的依赖解析有偏差,找不到Cesium的入口文件。另外黑屏大概率是CESIUM_BASE_URL没定义,资源加载404。
解决:先在vite.config.ts里把alias指向node_modules/cesium/index.cjs,再把CESIUM_BASE_URL通过define配置好。如果还不行,降级用npm重新安装依赖,把node_modules删掉重来一遍。
5.2 浏览器报错This browser does not support WebGL
现象:打开页面直接报WebGL不支持,或者Cesium提示当前环境无法创建WebGL上下文。
原因:分硬件和软件两种情况。硬件上可能是显卡驱动未启用GPU加速,软件上可能是浏览器的硬件加速开关被关闭。
解决:先确认浏览器设置里「硬件加速」是打开状态。如果是远程桌面或者虚拟机里跑的,禁用显卡加速后OpenGL渲染退化成Microsoft Render,Cesium必挂。我一般在客户环境里先跑一段WebGL能力探测脚本,确认支持情况再往下的步骤,不然查半天代码结果是环境本身不行。
5.3 HLS视频流在Chrome上不播放,Safari正常
现象:同样的.m3u8地址,Safari里播得好好的,Chrome里黑屏,控制台报TypeError: Failed to fetch。
原因:Chrome不支持原生HLS播放,需要hls.js转封装成MediaSource。Safari自带HLS支持,这是两套完全不同的播放链路。
解决:引入hls.js,判断浏览器是否支持原生HLS;不支持时调用Hls.js实例加载视频流,并把实例挂到<video>元素上。注意hls.js必须在使用Cesium的VideoSynchronizer之前完成视频加载和准备。
5.4 视频贴片纹理变成了绿色或者紫红色
现象:贴片几何体加载后不是视频画面,而是一整块纯色材质,绿色或紫红色最常见。
原因:视频纹理在GPU里上传失败,通常是视频分辨率不是2的幂,或者视频帧格式不被纹理系统支持。
解决:Cesium的纹理上传要求像素格式RGBA,且宽高最好是2的幂倍数。监控视频分辨率一般是1920×1080或1280×720,都不满足2的幂要求。我在转流服务端把视频强制缩放成2048×1024输出,或者在前端用canvas先绘制到2的幂尺寸再传给ImageMaterialProperty。前端处理会额外消耗性能,好在只是多一路视频,影响可接受。
5.5 视频贴片位置漂移,模型一动就错位
现象:视频画面刚开始贴合得好好的,模型编辑或者视角切到另一栋楼再切回来,视频画面就偏离了。
原因:不是融合计算错了,而是场景里同时存在多个坐标系。3D Tiles数据是CGCS2000坐标,而视频矩形用的经纬度直接转的是WGS84,两者之间的椭球体差异在园区尺度下虽然小,但也足以让几百米外的画面偏出去。
解决:给视频矩形也挂到同一个坐标系下,或者用CallbackProperty动态计算视频矩形的位置,让它始终和3D Tiles建筑保持相对位置。我这边最后用的是相对位置偏移写法:先获取建筑包围球中心点,再在这个中心点上加偏移生成视频矩形坐标,这样模型移动时视频跟着走,不漂移。
6. 验证跑通:用一组可复现的调试动作确认环境没问题
整套环境装完、视频也贴上去之后,不能光看着画面觉得没问题就直接交付。我一般固定走一遍完整的验证流程,全程大概十分钟,能覆盖掉大部分隐性故障。
第一步:打开浏览器开发者工具,在Console里观察初始化日志。Cesium会在场景初始化和3D Tiles加载完成后输出标准的加载日志。重点看有没有404、Failed to load resource、Texture creation failed这几类信息。404说明CESIUM_BASE_URL配置有误,资源路径不对;纹理创建失败说明视频帧数据格式有问题,需要回去查分辨率。
第二步:在Console手动输入一段验证代码,把当前相机坐标和场景状态打出来:
// 复制到 Console 执行,验证场景核心对象是否可用 viewer.scene.globe.getHeight(Cesium.Cartographic.fromDegrees(116.3912, 39.9072)) .then(height => console.log('当前地形高程(m):', height)); console.log('相机位置:', viewer.camera.positionCartographic.toString()); console.log('是否加载3D Tiles:', viewer.scene.primitives.length > 0); console.log('视频是否播放中:', !document.getElementById('monitorVideo').paused);这段代码能同时验证三件事:地形是否已经加载出高程数据、相机位置是否在目标区域、视频有没有真正播起来。如果高程返回undefined,说明地形Provider没有生效,去查网络请求里地形瓦片是否正常返回;如果视频paused是true,说明自动播放被拦截了,需要加用户手势触发。
第三步:绕着场景走一圈。按住鼠标左键拖动旋转视角,把场景从正上方看到贴近地面,再到从视频摄像头方向看。我每次都要走到视频贴片的正面角度,看看视频画面纹理是否有闪烁或撕裂。闪烁通常是因为帧率不足或者纹理同步延迟,把viewer.scene.maximumRenderTimeChange调大一些能缓解。
第四步:切到浏览器另一个标签页再切回来,这时候Cesium会经历一个场景恢复的过程。如果视频在这个恢复动作之后还是对齐的,说明整个链路稳定。有些机器上切回来之后视频贴片会短暂消失几秒,这是GPU上下文丢失后重新上传纹理的正常表现,但如果超过五秒还没恢复,就要检查显卡驱动了。
这套验证流程做完,基本能确认Cesium环境是健康的,视频融合链路是通的。每次经历环境问题之后,我都养成了一个习惯——不管改了什么配置,都强制自己从第二步的Console验证开始重新走一遍完整流程,不跳步。这个习惯救过我很多次,尤其是客户现场环境比自己机器复杂得多的时候。希望这篇能帮你少踩几个坑,一次把Cesium三维场景和视频融合跑通。
本文还有配套的精品资源,点击获取