☰
Cesium三维GIS视频融合:虚拟相机视锥投影与工程实现
2026/10/26 3:42:37 网站建设 项目流程

简介:这份Cesium三维GIS场景搭建与场景视频融合资料包,面向三维GIS、WebGL开发者及高年级学生,聚焦从二维GIS转向三维可视化、构建数字城市或虚拟流域场景的实际参考需求。压缩包共收录2000个文件,约54.64MB,以js、html、css等前端页面与交互脚本,java后端代码,png、gif、jpg等地图瓦片与界面素材,以及json、xml、b3dm、gltf、kml等三维模型和场景配置数据为主,另含少量音视频与字体资源,可支撑场景加载、模型组织、视频叠加和基础服务端联调。已有992人学习或下载,说明其在Cesium场景构建方向具有一定参考价值。资料中保留了较完整的前后端目录与三维瓦片样例,读者可据此理解Cesium场景初始化、3D Tiles模型加载、视频融合投影及资源组织方式,也可用于课程设计、项目原型验证和技术选型对比。

1. 指挥中心那块屏,为什么非要“贴”上去

凌晨两点,值班室墙上九宫格监控还在闪,盯屏的人早就晕了。换成三维 GIS 场景之后,大屏上是一片带地形的城区,二十多路摄像头的画面按各自的朝向、俯仰角、视场角,直接铺在它真正覆盖的那几栋楼和那片空地上——哪个方向有盲区,转一圈就看出来了。这个压缩包干的就是这件事:用 Cesium 把三维 GIS 场景搭起来,再把实时视频按摄像机真实姿态融合进去。它不是套模板的 GIS 大屏,也不是单纯播视频,核心全在“融合”两个字。适合手上已经有 Cesium 基础、想往上加业务图层和视频能力的开发者,也适合第一次接三维项目、需要一份能跑起来的骨架的人。

2. 工程骨架与技术选型:目录、依赖和本地起跑

拿到包先别急着开浏览器,这套东西跑不起来的原因十有八九不是代码写得不对,而是静态资源没对上。Cesium 的 Worker、Assets、Widgets 三块东西必须能被 http 服务正经访问到,直接双击index.html用 file:// 协议打开,大概率白屏加一串跨域报错。我一般先把目录结构和启动链路摸清楚,再动代码。

2.1 目录结构和模块职责

这个项目的代码组织基本是按“场景构建”和“视频融合”两条线分开的,中间用一份配置文件串起来。真实项目里这套结构会稍微再拆细一点,但主干差不多是这样:

cesium-video-fusion/ ├── public/ │ ├── cesium/ # Cesium 静态资源:Workers / Assets / Widgets │ ├── videos/ # 测试用的视频文件 │ └── data/ # GeoJSON、模型、标绘数据 ├── src/ │ ├── main.js # 入口,挂载 Vue 实例 │ ├── App.vue │ ├── cesium/ │ │ ├── viewer.js # Viewer 单例 + 初始化参数 │ │ ├── terrain.js # 地形加载与降级 │ │ ├── imagery.js # 影像图层管理 │ │ ├── camera.js # 视角漫游、飞行定位 │ │ └── layer.js # 业务图层:点位、标绘、模型 │ ├── fusion/ │ │ ├── VideoProjection.js # 视频投影的核心类 │ │ ├── frustum.js # 虚拟相机与视锥体计算 │ │ └── groundPatch.js # 贴地网格生成 │ ├── config/ │ │ └── cameras.json # 摄像头参数(经纬度、姿态、视场角) │ └── components/ │ ├── SceneView.vue │ └── CameraPanel.vue ├── vite.config.js └── package.json

src/cesium/这一层只负责“把场景搭出来”,不做任何业务判断;src/fusion/只负责“把视频投进去”,不关心底图是影像还是地形。两边唯一的耦合点是config/cameras.json,这个隔离很关键——后面要换成实时流或者加云台控制,改动面能压在一个目录里。components/只做 UI 和状态,不碰 Cesium 的 API,这样即使以后大屏要换框架,三维部分能整块搬走。

2.2 依赖与本地启动

不锁版本号,装 npm 上的当前稳定版就够,这套方案不依赖什么冷门包:

npm i cesium npm i -D vite vite-plugin-cesium

vite-plugin-cesium干的事说起来很朴素但省了很多事:构建时把 Cesium 的 Workers、Assets、Widgets 拷到产物目录,并且自动注入CESIUM_BASE_URL。老项目里手写CopyWebpackPlugin再手动改window.CESIUM_BASE_URL的那套,属于血泪经验,能不用就不用。

// vite.config.js import { defineConfig } from 'vite'; import cesium from 'vite-plugin-cesium'; export default defineConfig({ plugins: [cesium()], server: { host: '0.0.0.0', port: 5173, // 瓦片和视频都走本地静态服务,跨域问题一次性解决 proxy: { '/tiles': { target: 'http://127.0.0.1:8081', changeOrigin: true } } } });

server.proxy这段看着不起眼,实际是必须的。视频纹理在 WebGL 里上传需要同源或者带 CORS 头,本地开发时视频放在public/videos/下自然同源,但一旦你换成后端流媒体地址,没有代理或者服务端 CORS 头,纹理就是一片黑,控制台还不一定报错。/tiles同理,离线瓦片服务基本都不带跨域头。

启动就两行:

npm run dev # 浏览器打开 http://localhost:5173

2.3 静态资源路径的两种处理方式

Cesium 的静态资源放哪儿,决定了你打包之后部署到子路径下会不会挂。两种做法我都用过:

第一种是放public/cesium/,靠vite-plugin-cesium自动注入基础路径,适合部署在域名根目录或者能拿到 Vite 的base配置的场景。第二种是扔到独立的静态文件服务,构建时显式指定:

// main.js 顶部,必须在 new Viewer 之前执行 window.CESIUM_BASE_URL = import.meta.env.PROD ? 'https://static.example-domain.com/cesium/' : '/cesium/';

CESIUM_BASE_URL必须在任何 Cesium 模块初始化之前赋值,晚一步就找不到 Worker,报的是Failed to construct 'Worker',看起来像是浏览器兼容问题,实际上是路径问题。部署到子目录(比如/gis/)时,base配置和这个变量必须一起改,只改一个会得到一半资源 404 的诡异状态。

提示:import.meta.env.PROD是 Vite 的写法,换成别的构建工具要对应调整,别直接抄。

3. 三维场景搭建:地形、影像、相机与业务图层

场景搭建这块最容易犯的错是“一次全开”。地形、影像、光照、后处理、3D Tiles 一股脑加进去,第一次跑起来帧率个位数,还以为是 Cesium 性能不行。合理的顺序是先让 Viewer 干净地起来,再一层一层往上加,每加一层确认帧率和位置都对,这比最后统一调优省事得多。

3.1 Viewer 初始化:关掉的比打开的重要

// src/cesium/viewer.js import * as Cesium from 'cesium'; import 'cesium/Build/Cesium/Widgets/widgets.css'; export function createViewer(container) { const viewer = new Cesium.Viewer(container, { // 三维业务场景不需要这些 UI,全关掉,省得挡视线 animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, infoBox: false, selectionIndicator: false, // 离线环境必须显式关掉默认在线底图,否则它会自己去请求影像服务 baseLayer: false, // 先用椭球当地形把场景跑通,再接真实地形 terrainProvider: new Cesium.EllipsoidTerrainProvider(), contextOptions: { webgl: { // 截图和融合效果比对时要用到,不开这个 canvas.toDataURL 拿到的是黑图 preserveDrawingBuffer: true } } }); // 视频融合场景底色要干净,天空盒和大气关掉 viewer.scene.skyBox.show = false; viewer.scene.backgroundColor = Cesium.Color.BLACK; // 关键:开启地形深度检测,否则贴地的投影面会“浮”在空中 viewer.scene.globe.depthTestAgainstTerrain = true; // 关键:关掉按需渲染。视频帧变化不会触发重绘,开了这个视频会卡在第一帧 viewer.scene.requestRenderMode = false; return viewer; }

参数里真正需要解释的是两个。baseLayer: false在带网环境下无所谓,一旦部署到内网,默认的在线底图请求会一直超时,每个图层都在重试,控制台滚屏,看起来像是性能问题。requestRenderMode是视频融合里最常见的翻车点:它本来是省电的,只在相机移动或数据变化时重绘,但视频元素的帧更新它感知不到,结果是相机不动视频就定住,动了才跳一帧,特别像“视频解码卡了”。

3.2 地形与影像:接不上就降级,别死等

// src/cesium/terrain.js export async function useWorldTerrain(viewer) { try { // 不同 Cesium 版本 API 名有差异,旧版是 createWorldTerrain() 返回 Promise const terrain = await Cesium.createWorldTerrainAsync({ requestWaterMask: false, requestVertexNormals: true // 有法线才能做光照,模型贴地形时明暗更自然 }); viewer.terrainProvider = terrain; return true; } catch (err) { // 内网环境拿不到地形很常见,退回椭球继续跑,不要整页崩掉 console.warn('地形加载失败,降级为椭球', err); viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider(); return false; } }

requestVertexNormals: true会额外请求一份法线数据,体积大概多三成,但如果你要在场景里叠模型或者做光影,没有法线模型会死黑一片。requestWaterMask只在有水体的时候有用,内网离线地形基本没有,关掉省流量。

离线地形的常见做法是本地部署一份 quantized-mesh 切片服务,然后:

viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl('/tiles/terrain', { requestVertexNormals: true, // 离线切片通常没有水面数据,别开 requestWaterMask: false });

影像图层这块,UrlTemplateImageryProvider是最通用的,XYZ 模板换什么瓦片服务都能套:

// src/cesium/imagery.js export function addXyzLayer(viewer, { url, minimumLevel = 0, maximumLevel = 18, alpha = 1 }) { const provider = new Cesium.UrlTemplateImageryProvider({ url, // 形如 /tiles/img/{z}/{x}/{y}.png minimumLevel, maximumLevel, credit: '' }); const layer = viewer.imageryLayers.addImageryProvider(provider); layer.alpha = alpha; // 叠加图层调透明度,底图压暗一点更突出业务数据 return layer; }

maximumLevel一定要和服务端实际切片层级对上。填高了,相机拉近时会请求不存在的层级,返回 404 一片空白;填低了,拉近之后画面糊成一团。这个参数我一般都是先去瓦片目录里数一下最大目录号再填,比猜靠谱。

3.3 相机视角与漫游

// src/cesium/camera.js export function flyToArea(viewer, lng, lat, height, heading = 0, pitch = -45, duration = 3) { viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(lng, lat, height), orientation: { heading: Cesium.Math.toRadians(heading), // 0 正北,顺时针增大 pitch: Cesium.Math.toRadians(pitch), // 0 水平,-90 垂直向下 roll: 0 }, duration }); }

pitch的取值约定一定要记牢,这是我见过最多人搞混的地方:Cesium 里 0 是水平看出去,负数才是俯视,-90 是完全垂直向下。从别的三维引擎(比如 Three.js 那套 Y 轴朝上的右手系)迁过来的人,凭直觉写pitch: 45,结果相机朝天看,一片黑还以为加载失败。heading是绕垂直轴旋转,0 是正北,顺时针增加,和罗盘刻度一致,这个比较符合直觉。

漫游和定点飞行之外,项目里通常还有一个“全局视角”的复位按钮:

export function resetView(viewer) { viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(120.0, 30.0, 3000), orientation: { heading: 0, pitch: Cesium.Math.toRadians(-60), roll: 0 } }); }

3.4 业务图层:点位、标绘与模型

点位和标绘用 Entity 就够了,数据量在几千个以内,Entity 的性能完全撑得住,不用上 Primitive:

// src/cesium/layer.js export async function loadGeoJson(viewer, url) { const dataSource = await Cesium.GeoJsonDataSource.load(url, { clampToGround: true, // 贴地,跟着地形起伏 stroke: Cesium.Color.fromCssColorString('#38bdf8'), fill: Cesium.Color.fromCssColorString('#38bdf8').withAlpha(0.25), strokeWidth: 2 }); viewer.dataSources.add(dataSource); return dataSource; }

clampToGround: true只在有地形数据的时候才有意义,退回椭球的时候这个参数被忽略,点位会插到椭球面上——这也是个常见疑惑,明明配了贴地,点位还是飘着。strokeWidth在多数浏览器里只支持 1 像素,想要粗线得用PolylineGraphics配材质,这是 WebGL 实现的历史限制,不是配置写错了。

模型加载:

export function addModel(viewer, url, lng, lat, height = 0, scale = 1) { const position = Cesium.Cartesian3.fromDegrees(lng, lat, height); const entity = viewer.entities.add({ position, model: { uri: url, scale, // 让模型随地球曲率自动调整 minimumPixelSize: 32, maximumScale: 20000 } }); return entity; }

模型如果朝向不对,加orientation: Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(h, p, r))就行,h/p/r依然是弧度。

4. 视频融合:把摄像头画面“贴”进三维场景

这里是整个项目最硬的部分。原理说穿了不复杂:一个监控摄像头,本质上就是一个位置固定、朝向固定、视场角固定的虚拟相机;它拍出来的视频帧,就是这个世界在这个虚拟相机下的一次透视投影。要做融合,就是拿这台虚拟相机的参数,把视频帧反向投影到三维场景的地形、建筑表面上去。理解了这个,后面所有参数都对得上号。

4.1 两条技术路线,先选清楚

第一条是贴片式:算出一个贴近地面的四边形,把视频当纹理贴上去。实现快,半小时能出效果,缺点是地形起伏大的地方会穿模,镜头稍微一动就露馅。

第二条是投影式:用摄像机的 view/projection 矩阵把视频纹理按视锥反投,地形怎么起伏纹理都跟着走。实现麻烦一些,但工程上站得住,是正经的视频融合。

两条路的取舍很现实:临时演示、工地围挡、平地园区,贴片式够用;真实的城市级场景,地形高差几十米,建筑还有立体面,只能用投影式。这个包的主线是投影式,贴片式可以作为降级方案保留。

4.2 贴片式:先让视频亮起来

// src/fusion/VideoProjection.js import * as Cesium from 'cesium'; export function addVideoPanel(viewer, videoUrl, cornerPositions) { const video = document.createElement('video'); video.src = videoUrl; video.loop = true; video.muted = true; // 不静音浏览器会直接拒绝自动播放 video.playsInline = true; // iOS Safari 不设这个会全屏播放 video.crossOrigin = 'anonymous'; video.play().catch(err => console.error('video 播放失败', err)); // 直接给 Cesium 传 video 元素,比传 url 更可控,ViewModel 层还能自己暂停 const material = new Cesium.Material({ fabric: { type: 'Video', uniforms: { image: video } } }); const entity = viewer.entities.add({ polygon: { hierarchy: cornerPositions, // 四角的世界坐标 perPositionHeight: true, // 用顶点自带高度,不做整体贴地 material, // 关掉深度写入,避免和地形打架闪烁 classificationType: Cesium.ClassificationType.TERRAIN } }); return { video, entity }; }

三个参数决定了它能不能跑起来。muted = true是现代浏览器自动播放的硬性门槛,不静音play()直接抛NotAllowedError。playsInline = true少了它 iOS 会强制全屏,三维场景就废了。crossOrigin = 'anonymous'在跨域拉视频时必须加,加了还得服务端配合给 CORS 头,只加一边等于没加。

perPositionHeight: true意味着顶点高度是你自己算的,四角的高度如果和地形对不上,画面就会悬空或者陷进地里。贴片式的所有毛病都从这个参数来。

4.3 投影式:用虚拟相机把视锥反投到场景

核心就三个矩阵:虚拟相机的 view 矩阵、投影矩阵,再加一个把 NDC(-1 到 1)映射到纹理坐标(0 到 1)的偏置矩阵。三者相乘得到textureMatrix,在片元着色器里拿世界坐标一乘,就得到采样坐标。

// src/fusion/frustum.js import * as Cesium from 'cesium'; export function buildTextureMatrix(scene, cfg) { const { longitude, latitude, height, heading, pitch, roll, fov, aspect, near, far } = cfg; const position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height); // 直接借 Cesium 的 Camera 当虚拟相机,省得自己拼坐标轴 const cam = new Cesium.Camera(scene); cam.frustum = new Cesium.PerspectiveFrustum({ fov: Cesium.Math.toRadians(fov), // 水平视场角,弧度 aspectRatio: aspect, // 宽高比,1920/1080 就是 1.778 near, // 近裁剪面,单位米,别设太小 far // 远裁剪面,超远会拉低深度精度 }); cam.setView({ destination: position, orientation: { heading: Cesium.Math.toRadians(heading), pitch: Cesium.Math.toRadians(pitch), roll: Cesium.Math.toRadians(roll) } }); // 把 NDC 的 [-1,1] 压到纹理坐标 [0,1] // Cesium.Matrix4 构造函数是列主序,别按数学教材的行主序填 const bias = new Cesium.Matrix4( 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.5, 0.5, 0.5, 1.0 ); const proj = cam.frustum.pro <p> <a href="https://download.csdn.net/download/Gefangenes/87835850" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询