资讯详情

资讯详情

Cesium三维GIS场景搭建与视频融合工程实践

简介面向三维GIS开发者与地理信息可视化学习者这份Cesium实战资料包围绕三维场景搭建和场景视频融合展开针对二维GIS难以表达真实三维空间的问题提供可参考的Web3DGIS工程实现。压缩包共2000个文件、约54.64MB以JavaScript与HTML构建前端交互页面Java代码承载服务端逻辑PNG、GIF、CSS等支撑界面与图标资源同时包含b3dm、gltf、kml等三维模型与地理数据以及wav、mp4等视频融合素材便于对照场景加载、模型组织与视频贴图流程。已有992人学习下载。读者可从中获得较完整的目录结构、前后端代码范例、三维瓦片与模型示例及视频融合实现思路适合用于课程设计、项目原型搭建和Cesium功能验证时查阅借鉴。1. 从一份 rar 到能转的三维地球Cesium 场景搭建最容易翻车的一步解压那个 rarnpm installnpm run dev浏览器里一片白——这是我见过最多的 3D 三维 GIS 场景搭建现场。有意思的是控制台里通常没有一行报错指向 Cesium 的 API全是/Workers/目录下的 404。Cesium 这类把地形、影像瓦片、WebGL 渲染、视频纹理全塞进浏览器的库装不好时的表现非常一致地球出不来或者出来了但转两下就卡死视频那一路更是黑成一块铁板。它适合已经在写前端、但要接手一个三维场景 Demo 的人也适合要把监控画面投到模型上的实施岗。下面这套流程从解压开始版本、路径、参数以及装坏之后怎么救都写清楚。2. 环境清点Node 18 与 CesiumJS 1.x 的版本对齐清单在动手装依赖之前我更愿意先花十分钟把环境钉死。三维场景这类工程对版本的敏感度比普通前端项目高一个档次因为它同时踩在三条线上构建工具链、WebGL 运行时、以及 Cesium 自身的静态资源目录结构。任何一条线错位表现出来的都是「白屏」这一个结果排查成本极高。2.1 先把三样东西定死再谈装依赖我一般会锁定下面这三样其余的编辑器、Git、终端随习惯即可。表里的「选错会怎样」不是我编的恐吓是这几年反复遇到的真实症状。组件我一般锁定选错会怎样Node.js18 LTS 或 20 LTS低于 16 时装 Vite 5、Rollup 4 这类新构建链会直接报 engine 不匹配某些 22 的早期小版本上老工程里的原生模块需要重新编译npm install卡在 rebuild 阶段包管理器npm 9 或 pnpm 8同一个工程里混用 npm 和 pnpmnode_modules里会留下两套依赖树Cesium 被解析出两份实例运行时抛Cannot read properties of undefined浏览器Chrome / Edge 最新版WebGL2 不可用时Viewer 构造完直接抛初始化失败页面只剩一个空 div先在终端里把版本念一遍确认没有惊喜node -v # 期望 v18.x 或 v20.x npm -v # 期望 9.x 以上 npm config get registry # 确认没有指向某个已经失效的内网源三条命令的含义很直白Node 版本决定构建链能不能装上npm 版本决定 lockfile 格式是否兼容lockfileVersion 在 npm 7 之后才有 2、3 的差异registry 决定你等的是三十秒还是三十分钟。最后一条最容易被忽略——很多人从别人机器上拷来的工程里带着.npmrc指向一个你根本访问不到的内网地址然后npm install挂在那里一动不动看着像网络慢其实是源不可达。顺手确认一下显卡和浏览器这一侧// 粘到浏览器控制台执行返回版本号就说明 WebGL2 可用 const canvas document.createElement(canvas); const gl canvas.getContext(webgl2); console.log(gl ? gl.getParameter(gl.VERSION) : no webgl2);如果这里返回no webgl2后面所有的配置都是白费力气。常见原因是浏览器开了「精简模式」、或者远程桌面环境下硬件加速被关掉。这种情况下去chrome://gpu看一眼 WebGL 那一行是不是被标成了 Software only是的话把硬件加速打开再重启浏览器。2.2 解压之后先判读三个文件不要急着 installrar 包在 Linux 和 macOS 上不是默认支持的别用unzip硬怼会得到一个损坏的目录。我一般用unar或者 7-Zip# Linux / macOS没有 unar 先装一个Windows 直接右键用 7-Zip 解压 unar Cesium实现三维GIS场景搭建及场景视频融合.rar -o ./cesium-demo cd ./cesium-demo ls -la解压完先别npm install先看三样东西package.json、lock 文件、以及有没有残留的node_modules。# 只打印关键字段避免一屏几百行看不清 node -e const prequire(./package.json);console.log(name:,p.name);console.log(scripts:,p.scripts);console.log(cesium:,(p.dependenciesp.dependencies.cesium)||(p.devDependenciesp.devDependencies.cesium))这条命令关心三件事。第一scripts里是vite还是webpack——这决定了后面走哪条静态资源接入路线两条路线的配置文件完全不同不能混。第二cesium是挂在dependencies还是peerDependencies——挂在 peer 里的意思是「宿主工程自己提供」你直接npm install是装不上的需要显式npm install cesium。第三包名和版本号这是后面所有路径拼接的基准。如果目录里有node_modules删掉。从别的机器上拷来的依赖目录里面写死了绝对路径的软链接和二进制文件换台机器九成跑不起来留着只会让你在报错里多绕两小时。同理.npmrc如果指向你不认识的源先注释掉再装。提示把工程放在含中文或空格的路径下某些构建工具在解析静态资源目录时会出错Windows 上尤其明显。我一般放在D:/work/cesium-demo这种纯英文短路径下。3. 工程搭建让 Cesium Viewer 在本地跑起来的三处配置环境清点完之后真正决定「能不能出地球」的其实只有三处配置依赖怎么接进来、静态资源指向哪里、Viewer 初始化时用什么底图和地形。这三处任意一处错症状都是白屏但排查方向完全不同所以要分开配、分开验。3.1 两条接入路线插件自动拷贝还是手动搬目录Cesium 和普通 npm 包最大的差别在于它除了 JS 模块之外还带一大堆运行时资源Web Worker 脚本、着色器、图标、第三方库。这些文件必须原样出现在构建产物的对应目录下浏览器才能按路径取到。接入方式就是解决「谁来搬这些文件」。如果工程用的是 Vite最省事的是插件路线// vite.config.js import { defineConfig } from vite; import cesium from vite-plugin-cesium; export default defineConfig({ plugins: [ cesium() // 负责把 Cesium 的 Workers / Assets / Widgets / ThirdParty 拷进产物 ], server: { host: 0.0.0.0, port: 5173 }, build: { chunkSizeWarningLimit: 4096 // Cesium 单个 chunk 体积大调高阈值避免满屏警告 } });plugins里的这一行做的事情等价于构建时自动执行一次目录拷贝并在开发服务器上把/cesium/这个前缀映射到node_modules/cesium/Build/Cesium。chunkSizeWarningLimit只是压掉警告不影响功能但不设的话每次 build 会刷一屏黄色提示容易把真正的报错淹掉。如果工程是 Webpack就得手动搬一个目录都不能漏// webpack.config.js const CopyWebpackPlugin require(copy-webpack-plugin); const webpack require(webpack); module.exports { plugins: [ new CopyWebpackPlugin({ patterns: [ { from: node_modules/cesium/Build/Cesium/Workers, to: cesium/Workers }, { from: node_modules/cesium/Build/Cesium/ThirdParty, to: cesium/ThirdParty }, { from: node_modules/cesium/Build/Cesium/Assets, to: cesium/Assets }, { from: node_modules/cesium/Build/Cesium/Widgets, to: cesium/Widgets } ] }), new webpack.DefinePlugin({ // 关键告诉 Cesium 运行时去哪里找上面拷出来的东西 CESIUM_BASE_URL: JSON.stringify(cesium) }) ] };四个目录各管一摊Workers是地形解码、几何计算这些跑在后台线程里的脚本缺了它地球直接不出来Assets是纹理、近似星图、默认图标Widgets是那个带时间轴和动画控件的 CSS 和图标ThirdParty是 Cesium 内部依赖的第三方库。我见过只拷Workers就以为万事大吉的结果控件全是没样式的裸按钮。DefinePlugin里那个值写的是相对路径cesium不是/cesium这个差别在部署到子目录时会要命带前导斜杠表示从域名根开始找部署在https://example.com/gis/下就会 404。3.2 CESIUM_BASE_URL 的三种写法与踩坑点除了构建期注入还有两种写法实际项目里经常混着出现得知道优先级。第一种是全局变量写在index.html里且必须在任何 Cesium 模块执行之前!-- index.html -- script // 必须早于 Cesium 模块加载否则运行时读不到 window.CESIUM_BASE_URL /cesium/; /script第二种是纯静态部署直接把node_modules/cesium/Build/Cesium整个目录扔到服务器上然后配一个反向映射。这种方式在离线内网环境里很常见好处是不依赖构建工具。判断路径对不对有个很笨但很快的办法启动开发服务器打开浏览器 Network 面板筛Workers。正常情况下应该能看到一批.js请求返回 200。如果全是 404把 404 的 URL 复制出来对照你配置的CESIUM_BASE_URL和实际文件位置一定能看出差在哪一层目录。注意改了CESIUM_BASE_URL之后要重启开发服务器热更新不会重新注入这个值改了没生效八成是没重启。3.3 Viewer 初始化底图、地形和一堆该关掉的控件到这一步地球应该能出来了但默认的 Viewer 带一大堆控件和在线服务直接上线既不好看也不可控。我一般初始化时就一次性配干净import * as Cesium from cesium; import cesium/Build/Cesium/Widgets/widgets.css; // 离线环境把这个置空并同步去掉 baseLayer / terrainProvider 里的在线服务 Cesium.Ion.defaultAccessToken 换成你自己的访问令牌; const viewer new Cesium.Viewer(cesiumContainer, { animation: false, // 时间轴动画控件 timeline: false, // 底部时间轴 baseLayerPicker: false, // 底图选择器会去拉在线列表 geocoder: false, // 地名搜索 homeButton: false, sceneModePicker: false, // 2D/3D 切换 navigationHelpButton: false, fullscreenButton: false, infoBox: false, // 点击要素弹出的信息框 selectionIndicator: false, // 选中时的绿色取景框 // 地形换成自己的瓦片服务 terrainProvider: new Cesium.CesiumTerrainProvider({ url: /terrain/, // 指向 layer.json 所在目录 requestVertexNormals: true // 需要光照效果时打开会增加显存占用 }), // 影像换成自己的瓦片服务 baseLayer: new Cesium.ImageryLayer( new Cesium.UrlTemplateImageryProvider({ url: /tiles/{z}/{x}/{y}.png, minimumLevel: 1, maximumLevel: 18, // 限定请求范围避免相机一拉远就发出上百个无效请求 rectangle: Cesium.Rectangle.fromDegrees(116.0, 39.5, 116.8, 40.1) }) ) }); // 让地物与地形做深度比较否则贴着地面的东西会被地形穿透 viewer.scene.globe.depthTestAgainstTerrain true;逐个说关键参数。terrainProvider的url要指向layer.json所在的那一层目录不是.terrain文件本身写错了会得到一个「地形加载完成但全是平地」的诡异结果。requestVertexNormals打开后地形会有正确的光照代价是每个顶点多一份法线数据瓦片数据量大概涨三成显卡吃紧时先关它。rectangle是我强烈建议加的不限定范围时相机拉远后 Cesium 会按当前视锥去请求全球范围的瓦片请求数瞬间上去本地服务直接被打满。depthTestAgainstTerrain这一行是后面视频融合能否贴住地面的前提先记住它。4. 视频融合把摄像头画面贴进三维场景的投影配置三维场景跑通了接下来才是这个工程真正的核心把摄像头的实时画面投到三维场景的表面上。这块的难点不在 Cesium 的 API在于「对位」——视频是二维的场景是三维的中间靠一组相机参数和投影矩阵把它们绑起来。参数差一点画面就歪出去几十米。4.1 三条实现路线的取舍路线做法适用场景主要代价立面贴片用一个矩形或墙体贴上视频材质人工摆到建筑立面上固定机位、画面本来就正对墙面、对精度要求不高换个机位就得重摆贴多面墙要切分几何视锥投影纹理用自定义着色器把片元的世界坐标经投影矩阵反算成视频 uv 再采样任意机位、需要同时贴合地形与建筑、多路视频拼接需要标定相机内外参着色器调试成本高离线烘焙提前把视频投影成纹理或模型文件运行时只加载结果视频内容固定、只做演示回放不是实时换个视频要重跑一遍大部分工程最后都会走到第二条。第一条做 Demo 够用但一旦客户说「摄像头往前挪了两米」所有手工摆位全部作废。第三条看起来省事实际上把问题推到了离线流程里调试更痛苦。4.2 最小可跑的视频纹理先用最土的办法确认视频链路是通的再上投影。核心就两步准备一个能自动播放的 video 元素把它当材质挂到几何体上。!-- 必须带 muted 和 playsinline否则大部分浏览器会拦截自动播放 -- video idcam01 src/video/cam01.mp4 autoplay muted loop playsinline crossoriginanonymous/videoconst videoEl document.getElementById(cam01); // play() 返回 Promise被拦截时不会抛全局错误一定要接住 videoEl.play().catch(err console.warn(视频自动播放被拦截, err)); const entity viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(116.300, 39.900, 116.320, 39.920), height: 0.5, // 抬高半米避免和地形 z-fighting 导致闪烁 material: new Cesium.VideoMaterialProperty(videoEl) } });几个参数值得单独讲。muted和playsinline不是可选项是自动播放策略的硬门槛少了任何一个play()就会被拒绝材质上显示的是视频的第一帧或者一片黑看起来像「纹理没加载」。crossoriginanonymous是为了走 CORS 请求否则视频虽然能播但作为纹理使用时会被判定为跨域污染。height: 0.5这个半米偏移是血泪经验视频平面和地形完全共面时深度值打架画面会随相机角度一闪一闪。VideoMaterialProperty接受HTMLVideoElement也接受 URL 字符串。接受字符串时由 Cesium 内部创建 video 元素省事但你拿不到播放状态我一般传元素因为后面要做暂停、切流、销毁都需要拿到这个对象。4.3 投影对齐相机位姿与视锥的四个参数真正的投影融合需要把现场摄像头的内外参搬进代码。外参是位置和姿态内参主要是视场角和宽高比。这一组值通常来自现场标定或者从设备文档里抄抄错了画面就会整体偏移。// 用一台虚拟相机承载现场摄像头的位姿再取出它的投影矩阵 const virtualCamera new Cesium.Camera(viewer.scene); virtualCamera.setView({ destination: Cesium.Cartesian3.fromDegrees(cam.lon, cam.lat, cam.height), orientation: { heading: Cesium.Math.toRadians(cam.heading), // 方位角正北为 0顺时针 pitch: Cesium.Math.toRadians(cam.pitch), // 俯仰角水平为 0向下为负 roll: Cesium.Math.toRadians(cam.roll) // 横滚角多数枪机为 0 } }); virtualCamera.frustum new Cesium.PerspectiveFrustum({ fov: Cesium.Math.toRadians(cam.hFov), // 水平视场角常见 60 到 90 aspectRatio: cam.width / cam.height, // 视频宽高比 near: 0.5, far: 5000 // 超过这个距离的片元不参与投影 }); // 视图矩阵 × 投影矩阵交给着色器把世界坐标反算成 uv const viewProj Cesium.Matrix4.multiply( virtualCamera.frustum.projectionMatrix, virtualCamera.viewMatrix, new Cesium.Matrix4() );heading、pitch、roll三个角的单位和符号是最容易错的地方。Cesium 里heading是弧度、正北为 0、顺时针为正而设备文档给的往往是度、正东为 0中间要做一次换算加一次偏移。pitch向下为负这一点也和不少设备的约定相反。我第一次接的时候就是符号搞反了画面整个上下颠倒看了半天以为是纹理坐标的问题。far不宜设得太大它决定了投影的深度范围设成几万会把远处建筑的片元也算进来白白增加采样开销。着色器那一侧负责把世界坐标投回屏幕坐标再采样const shader new Cesium.CustomShader({ uniforms: { u_video: { type: Cesium.UniformType.SAMPLER_2D, value: videoTexture }, u_viewProj: { type: Cesium.UniformType.MAT4, value: viewProj } }, fragmentShaderText: void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { // positionMC 经模型矩阵换到世界坐标属性名以所用版本的自定义着色器文档为准 vec4 worldPos czm_inverseView * vec4(fsInput.attributes.positionMC, 1.0); vec4 clip u_viewProj * worldPos; vec2 uv clip.xy / clip.w * 0.5 0.5; // NDC 映射到 0 到 1 // 视锥之外的片元不投影避免画面在背面重复出现 float inside step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0); vec4 color texture(u_video, vec2(uv.x, 1.0 - uv.y)); // 视频纹理通常要翻转 Y material.diffuse mix(material.diffuse, color.rgb, inside * color.a); } });uv.x, 1.0 - uv.y这个翻转来自视频纹理的原点在左上而 WebGL 纹理原点在左下不翻的话画面上下颠倒。inside这个系数解决的是「背面的建筑也被投上画面」的问题不加它视锥反方向的墙面上会鬼影一样出现同一段视频。4.4 多路视频与性能调参单路通了之后就是多路。我实测下来普通办公机同时解码四到六路 1080p 已经比较吃紧再多就开始掉帧而且掉的不是渲染帧是视频解码本身。几个能立刻见效的点一是同一个页面只保留一个Viewer。我见过每路视频建一个 Viewer 的写法八个 WebGL 上下文直接把浏览器拖死。多路视频应该作为多个 entity 或 primitive 挂在同一个场景里。二是控制纹理上传频率。视频每秒 25 到 30 帧但三维场景的相机不动时其实不需要每帧都重新上传纹理。可以用requestVideoFrameCallback在视频真有新帧时才更新 uniform静止场景下能省下不少开销。// 只在视频产生新帧时才更新材质避免空转上传 function bindFrameUpdate(videoEl, material) { const step () { material.uniforms.image videoEl; videoEl.requestVideoFrameCallback(step); }; videoEl.requestVideoFrameCallback(step); }三是清理要彻底。切换摄像头或者切页面时video 元素不释放会一直占着解码器和内存页面开久了内存曲线一路往上。我一般在组件卸载时统一走一遍function disposeVideo(viewer, videoEl, entity) { if (entity) viewer.entities.remove(entity); videoEl.pause(); videoEl.removeAttribute(src); videoEl.load(); // 关键清空 src 后调 load才会真正触发资源释放 videoEl.remove(); }removeAttribute(src)之后必须再调一次load()这是 HTML 规范里让浏览器终止当前资源下载并重置状态的方式只清属性不调load()解码器仍然挂着。5. 避坑与排查五类装完就崩的典型现场下面这几条都是我或者身边的人真实撞过的按症状、原因、解决写遇到了可以直接对号入座。5.1 构建期白屏与 404现象npm run dev之后页面纯白控制台里刷屏的 404 都指向/Workers/xxx.js或者/Assets/xxx.png。原因Cesium 的运行时资源没有出现在它期望的路径下。要么CESIUM_BASE_URL没配要么配了但指向的目录和实际拷贝的位置对不上要么只拷了Workers忘了Assets和Widgets。解决打开 Network 面板筛Workers把 404 的完整路径抄出来对照构建产物目录逐层比对。Vite 工程直接用vite-plugin-cesiumWebpack 工程确认四个目录都在patterns里。改完重启开发服务器。现象npm install卡住不动或者装完启动时报Cannot find module但目录里明明有。原因工程里残留了从别处拷来的node_modules或者指向不可达源的.npmrc。解决删掉node_modules和 lock 文件重装.npmrc先注释掉。这一步看着粗暴实际比逐个排查软链接快得多。5.2 运行期地形、纹理与视频现象地球出来了但是完全平的一点起伏都没有控制台偶尔报 401 或 403。原因地形服务的地址写错或者访问令牌失效。CesiumTerrainProvider的url必须指向layer.json所在目录写到具体瓦片文件上会静默失败。解决浏览器直接访问那个url能看到 JSON 就说明路径对。完全离线的环境记得把默认令牌置空并且不要在构造 Viewer 时传任何在线服务否则它还是会去请求。现象视频材质一片黑或者只显示第一帧。原因自动播放被拦截、视频编码不被浏览器支持、或者play()的 Promise 被拒绝而代码里没接。解决确认muted和playsinline都在先单独在页面上放一个 video 标签手点播放看能不能出画面能出说明是策略问题不能出说明是编码问题。H.265 在很多浏览器里就是不支持转成 H.264 基线。现象视频能播但贴上去之后画面发黑、或者报跨域相关错误。原因视频资源跨域且没有声明crossorigin纹理上传时被判定为污染。解决video 标签加crossoriginanonymous服务端补Access-Control-Allow-Origin。两边缺一不可只加一边等于没加。现象视频贴上去之后相机一转就一闪一闪或者被地面切掉一半。原因视频面和地形共面导致深度冲突或者depthTestAgainstTerrain没开导致遮挡关系错乱。解决把平面抬高 0.1 到 0.5 米同时打开viewer.scene.globe.depthTestAgainstTerrain true。抬高量别太大超过一米肉眼就能看出画面浮在空中。注意这几条里最容易误判的是「视频黑屏」它可能是播放策略、编码、跨域、纹理坐标四个原因中的任意一个。排查顺序建议从「单独播放能不能出画面」开始一次只改一个变量。6. 验证跑通三步确认场景与视频真的对上了装完不等于跑通。三维场景这类东西最坑的地方在于它「看起来能转」但地形可能没加载、视频可能贴偏了五十米。我在交付前一定会走一遍下面三步。第一步资源自检。开发服务器起来后Network 面板筛Workers和Assets全部 200 才算过。顺带看一眼有没有重复请求同一个瓦片重复请求多说明rectangle没限定或者相机在抖。第二步运行时自检。把下面这段粘到控制台五秒后看输出setTimeout(() { const scene viewer.scene; console.log(地形瓦片是否加载完成:, scene.globe.tilesLoaded); console.log(场景内实体数量:, viewer.entities.values.length); console.log(相机高度(米):, Math.round(viewer.camera.positionCartographic.height)); }, 5000); // 采样三秒的平均帧率 let frames 0; const t0 performance.now(); (function tick() { frames; if (performance.now() - t0 3000) requestAnimationFrame(tick); else console.log(平均帧率:, (frames / 3).toFixed(1)); })();tilesLoaded为false说明还有瓦片在飞或者请求失败这时候去看 Network 里有没有红色请求。帧率在静止相机下应该接近屏幕刷新率转起来掉到 30 以下就得回头查视频路数和纹理尺寸。第三步对位自检。这是最容易被跳过的一步。在场景里找一个已知坐标的地标一栋楼的角、一个路口把视频画面里同一个地标的位置和三维场景里对齐误差超过两三米就说明heading或pitch的符号有问题。我一般会在场景里临时加三个点标记对完再删掉。检查项期望结果不通过时先看哪里Workers / Assets 请求全部 200CESIUM_BASE_URL与构建产物目录globe.tilesLoaded五秒内变 true地形 url 是否指向layer.json目录静止帧率接近刷新率视频路数、纹理尺寸、是否每帧上传地标对位误差三米以内heading/pitch的符号与单位页面关闭后内存回落到初始水平disposeVideo是否调了load()这套流程我吃过亏才固定下来的。早些年交付一个演示环境现场看着一切正常回来之后对方说「视频偏了」一查是标定给的heading是度、我按弧度用了差了五十七倍画面整个甩到场景外面去。从那以后我每次接视频融合都强制走一遍「地标对位」这一步哪怕时间再紧也不跳。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →