首页 / 资讯中心 / 文章详情

Cesium三维场景搭建与视频融合实战指南

Cesium三维场景搭建与视频融合实战指南 ★ FEATURED ARTICLE
简介面向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 LTSVite 5.xCesium 1.112包管理器用的 pnpm 8不用npmNode 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-tsJavaScript 纯项目可选 vanilla pnpm create vite cesium-gis-demo --template vanilla-ts # 进入目录并安装基础依赖 cd cesium-gis-demo pnpm install # 安装 Cesium 本体 pnpm add cesium1.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 idmonitorVideo autoplay muted playsinline crossoriginanonymous 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 RenderCesium必挂。我一般在客户环境里先跑一段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三维场景和视频融合跑通。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站