
1. 为什么选这条技术路线可视域分析的业务价值和方案取舍做三维GIS的兄弟应该都遇到过这类需求某个通信铁塔建好后想看看信号到底能覆盖哪个范围某个区域要装监控摄像头想提前判断哪些位置在视野盲区或者城市规划里要在某个山头建观景台需要评估从观景台看出去到底能看到多少景观。这类问题本质上都是同一个几何学问题——从空间中的某个点出发在给定视线方向和角度范围下地形、建筑、树木等物体会遮挡视线最终能看到的区域到底是哪一块。这就是 GIS 里面非常经典的空间分析功能可视域分析Viewshed Analysis。我以前用桌面端 GIS 软件做过类似分析流程是先准备 DEM 高程数据再用软件自带的分析工具跑一遍然后把结果导出成栅格图最后叠加到二维地图上给客户看。这套流程有几个痛点第一分析结果只有一张静态图没法让客户在三维场景里从任意角度查看遮挡关系说服力大打折扣第二数据更新后要重新导一次来回沟通成本很高第三遇到底层是倾斜摄影实景模型、BIM模型这类三维数据时传统二维栅格分析根本没法把楼层遮挡算进去。所以这次拿到“vue3SuperMap iClient3D for Cesium 实现可视域分析”这个需求时我第一时间就确定了要直接在三维场景里做实时可视域分析把分析结果以三维体块或高亮面的形式叠加在场景模型上让用户能拖拽视角、能旋转场景、能看到每一处被遮挡的细节。技术选型上没有太多纠结直接锁定了 Vue3 SuperMap iClient3D for Cesium 这条路径。这套组合的好处体现在两个层面。Cesium 本身是开源三维地球引擎里的标杆WebGL 渲染、3D Tiles 加载、地形裁剪、相机交互这些能力都非常成熟生态里做数字孪生、智慧城市、军事仿真的大多数项目都用它。而 SuperMap iClient3D for Cesium 是在 Cesium 开源能力之上做了一套面向 GIS 业务场景的增强封装把数据服务对接、S3M 格式模型加载、空间分析算子这些都打包好了尤其是可视域分析这类功能用超图封装好的接口比拿原生 Cesium 从零实现要省掉大量数学和渲染层面的工作。可能有朋友会问原生 Cesium 不是也支持做可视域吗确实Cesium 社区里有不少用线积分卷积或者多边形绘制模拟可视域的例子但要做得像商业 GIS 软件那样具备像素级精确性、能和 S3M 倾斜摄影模型做真实遮挡计算还是得依赖专门封装的分析模块。这套方案适合的场景很明确项目本身已经用了超图体系的数据服务或者需要分析 S3M 格式的实景三维模型再或者你希望用一套相对成熟的接口快速把功能交付出去。如果项目里数据全是 3D Tiles 格式、分析精度要求也不高那纯 Cesium 方案也能凑合但别指望节省的工作量能超过这次说的方案。2. 环境搭建版本搭配和工程初始化的几处细节点2.1 Vue3 工程初始化工程创建直接用 Vite 就可以Node 版本建议选 16.18 以上Vite 4.x 对 Vue3 的支持最稳妥。我用的是 pnpm 作为包管理器装依赖的速度比 npm 快不少对 iClient3D 这种依赖数量比较多的包体验差异尤其明显。一条命令就能把项目拉到本地pnpm create vite viewshed-analysis --template vue-ts cd viewshed-analysis pnpm install这里强调一个细节如果团队里有人还在用 Node 14 或更低的版本建议先升级 Node 再跑上面的命令否则 Vite 4.x 启动阶段很可能报出Cannot find module rollup之类的路径错。2.2 iClient3D 依赖安装与版本锁定这一步是整个环境配置里最容易被坑的地方。SuperMap iClient3D for Cesium 在 npm 上发布的包名是supermapgis/iclient3d-cesium它自带一个特定版本的 Cesium 运行时所以不需要单独再去安装cesium包如果强行装了很可能会出现两个 Cesium 实例冲突地图控件渲染异常、事件绑定不生效这些问题都会冒出来。安装命令pnpm add supermapgis/iclient3d-cesium安装完成后检查一下 package.json 里的版本号。以我这次使用的 2024 版的 iClient3D 为例它内置的 Cesium 版本是 1.9x 系列对应关系在超图官方发布说明中写得很清楚建议严格对应不要随手升到最新版 Cesium以免接口签名对不上。还有一个容易忽略的点iClient3D 的包体积很大Vite 默认的构建配置在 dev 模式下首次加载会等挺久。可以在 vite.config.ts 里增加手动分包把依赖单独拆出来import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], build: { chunkSizeWarningLimit: 2048 } })2.3 创建三维场景与加载数据iClient3D 封装了Cesium.Viewer也可以用超图提供的初始化工具来创建场景。通常流程是先准备好一个全屏容器然后创建 Viewer再叠加底图、地形和模型数据。基础的三维场景创建代码如下import * as Cesium from supermapgis/iclient3d-cesium import { onMounted, ref } from vue const container refHTMLDivElement() let viewer: Cesium.Viewer onMounted(() { viewer new Cesium.Viewer(container.value!, { // 关闭默认自带的影像图层稍后手动加载业务底图 baseLayer: false, // 三维场景里信息框默认是弹窗形式做分析功能时建议关掉 infoBox: false, // 点击选中时的提示气泡只展示基本属性够用这里为了界面干净关掉 selectionIndicator: false, // 真彩模式让倾斜摄影和模型颜色更接近真实观感 scene3DOnly: true }) // 加载超图发布的影像底图服务 const imageryProvider new Cesium.ArcGisMapServerImageryProvider({ url: https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer }) viewer.imageryLayers.addImageryProvider(imageryProvider) // 开启地形可视域分析依赖地形高程数据做遮挡计算 const terrainProvider Cesium.createWorldTerrain() viewer.terrainProvider terrainProvider // 设置初始相机位置到分析区域附近 viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.91, 500) }) })infoBox: false这个选项不是必须的但是做了可视域分析之后场景里会有多个分析实体对象默认的弹窗很容易在点击模型时跳出来干扰分析结果查看提前关掉能省很多麻烦。2.4 加载 S3M 切片和 3D Tiles 模型可视域分析必须要有可供遮挡计算的模型数据没有模型的场景里分析结果只相当于在空地上画了个扇形没有任何业务价值。超图体系下最常见的三维数据格式是 S3M加载方式如下import { S3MTilesLayer } from supermapgis/iclient3d-cesium const s3mLayer new S3MTilesLayer({ url: http://your-supermap-server/3d-port/your-scene-name, name: terrain-model }) viewer.scene.addS3MTilesLayer(s3mLayer)如果项目里用的是通用 3D Tiles 格式数据比如倾斜摄影建模出 OSGB 转换成的 b3dmiClient3D 内部也兼容直接走原生 Cesium 的加载方式就能接入const tileset await Cesium.Cesium3DTileset.fromUrl( http://your-server/3dtiles/tileset.json ) viewer.scene.primitives.add(tileset)这里要提醒一点可视域分析的计算精度和模型加载的 LOD 级别密切相关如果场景相机拉得特别高模型加载的是低精度层级分析结果会被明显低估。在做功能验证时建议先把相机飞到分析点附近等模型进入高精度层级后再发起分析。3. 可视域分析的核心参数与遮挡计算逻辑3.1 分析的底层原理不只是画个扇形把可视域分析讲得通俗一点可以把它想象成用手电筒在黑夜里照一堵凹凸不平的墙光源位置就是观察点手电筒光束就是视线范围墙上被照亮的部分就是可视区域被鼓包挡住的部分就是盲区。计算机要实现这个过程并不是对每一个像素都做射线求交而是有一套更工程化的近似算法。具体到 Cesium 这一层常见的可视域算法会先把视线覆盖的区域按距离和角度切分成网格或者说按采样步长切分成一圈一圈的射线。每条射线从观察点出发经过地形或者模型表面时与高程做比较如果射线上的点高于地表或模型表面说明这个方向可见一旦射线穿入地形或模型内部就认为该方向被遮挡并且从这个遮挡点往后全部判定为不可见除非后续地形突然凹陷下去重新暴露出来。实际计算中的一部分判断逻辑如下起点处射线高度 地表高度判定可见射线穿越模型表面时高度 模型高程判定被遮挡相机角度抬升后射线重新高于地表该方向恢复可见但需要重新做遮挡深度排序iClient3D 封装的分析模块把这一整套几何计算封装成了参数化的接口我们要做的就是把观察点坐标、视线方位、张角范围、最大分析距离这些参数填进去然后让分析引擎去跑。3.2 参数拆解每个字段到底影响什么整理一下可视域分析里最常见的几个参数后续代码里都会用到参数含义影响观察点Observer分析起点的空间位置由经纬度、高程构成决定分析结果的空间锚点目标点/视点ViewPoint视线指向的方向位置决定初始视线朝向水平张角Horizontal Angle从视线方向往左右两侧扩展的角度张角越大覆盖的扇形范围越宽垂直张角Vertical Angle从视线方向往上下扩展的角度控制纵向可视范围最大分析距离Max Distance从观察点向外分析的最远射线长度决定分析范围边界采样密度射线间隔或网格分辨率越高精度越好但计算开销越大举个例子假设有一座 100 米高的铁塔观察点设在塔顶视线方向朝正北水平张角 90 度左右各 45 度垂直张角 30 度最大分析距离 2000 米那么最终结果就是在北方向一个 90 度扇形范围内、最远 2000 米处截断的可见区域。3.3 大多数项目里参数设置的坑第一坑是观察点高程设错了。很多人直接用手持 GPS 拿到的海拔高度填入观察点但这个高度往往没有考虑地面建筑物高度或塔架高度。比如塔架高度 80 米地面海拔 20 米观察点高程应该是 100 米而不是 20 米。第二坑是最大分析距离设得过大。距离越远采样点越多计算耗时呈非线性增长如果只是分析 500 米范围的覆盖情况没必要设置 5000 米。第三坑是垂直张角给的太保守。做通信覆盖分析时如果垂直张角只给了上下各 15 度很多低矮区域的可见性会被忽略导致结果偏悲观。我实际测试下来分析城市环境下的地面可见范围垂直张角上下 30 度是比较合理的起步值。4. 完整实现代码从分析初始化到结果渲染4.1 创建分析管理的 Vue3 组件可视域分析功能建议封装成一个独立的 composable 或者一个 Vue3 组件不要把分析逻辑直接塞进页面组件里。我采用的方式是做一个useViewshedAnalysis的 composable把分析生命周期、参数更新、结果清理都收敛到内部页面里只暴露几个操作按钮。代码结构大致是这样的import { ref, onBeforeUnmount } from vue import * as Cesium from supermapgis/iclient3d-cesium export function useViewshedAnalysis(viewer: Cesium.Viewer) { const isAnalyzing ref(false) // 可视域分析实例类型以iClient3D的API文档为准 let currentAnalysis: any const startAnalysis (params: ViewshedParams) { // 1. 解析参数字段 // 2. 创建分析实例 // 3. 设置观察点 // 4. 设置视线方向与张角 // 5. 提交到场景 } const stopAnalysis () { // 清理场景中的分析对象 } const updateObservationPoint (lon: number, lat: number, height: number) { // 动态更新观察点 } onBeforeUnmount(() { stopAnalysis() }) return { isAnalyzing, startAnalysis, stopAnalysis, updateObservationPoint } }4.2 初始化可视域分析对象iClient3D 的分析模块提供了可视域分析的封装类使用前需要先创建一个分析实例然后设置分析涉及的参数对象。从官方文档的体系来看这里面有一个核心的分析参数类包含了观察点、目标点、水平张角、垂直张角、距离、采样密度等配置项。实际调用时大致是这个形式import { Analysis } from supermapgis/iclient3d-cesium const viewshedAnalysis new Analysis.ViewshedAnalysis({ observer: { x: 116.391, // 经度 y: 39.907, // 纬度 z: 100 // 高程 }, target: { x: 116.391 0.01, y: 39.907, z: 50 }, horizontalAngle: 90, verticalAngle: 30, maxDistance: 2000, sampling: 0.5 }) viewer.scene.analysisManager.add(viewshedAnalysis)其中analysisManager是 iClient3D 在 Cesium 场景上扩展出来的分析对象管理器专门用来挂载各类空间分析实例。4.3 动态更新参教与实时反馈实际业务里很少会把观察点写死更多是要配合摄像头、雷达或者信号塔的安装位置做动态调参。我去车顶挂个 RTK 接收机测跑一圈每秒钟都会产生新的坐标。这时需要把分析对象的参数更新能力暴露出来const handleMoveObservation (lon: number, lat: number, height: number) { if (!currentAnalysis) return currentAnalysis.updateObserver({ x: lon, y: lat, z: height }) // 更新后重新执行分析计算 currentAnalysis.update() }这里有一个我实测下来很影响体验的细节连续拖拽观察点或动态播放轨迹时如果每一帧都触发完整的重分析场景会非常卡。一个简单有效的优化就是做个 200ms 的节流控制等用户停下拖动或每秒对轨迹抽样一次再触发重算流畅度改善非常明显。4.4 分析结果的展示与交互可视域分析得到的结果通常包含两个部分可视角落在模型表面的高亮区域以及遮挡边界的轮廓线。iClient3D 分析模块会自动把结果渲染成一个或者一批动态图元挂到场景中我们不需要手动画多边形但可以控制结果层的显隐、颜色和透明度。暴露到页面上的效果通常是一块半透明的绿色区域可视搭配红色区域遮挡业务人员可以直接看出盲区集中在哪些位置。为了让结果更直观还可以加一个辅助功能点击结果区域能回传当前点的经纬度和可视状态。const handleClickedResult (click: any) { const pickObj viewer.scene.pick(click.position) if (pickObj?.isViewshedResult) { infoPanel.value 该位置可视状态${pickObj.visible ? 可通视 : 遮挡} } }5. 实际项目中的踩坑全过程三个典型案例的排查链路5.1 分析结果不显示的排查过程这是我接手这个项目后遇到的第一个问题。代码写完后参数也都传了分析开始之后场景里没有任何明显反应既没有高亮区域也没有报错信息。一开始我以为是分析类不兼容直接跳到 API 调用那层排查结果浪费了不少时间。后来仔细梳理了一遍执行链路发现问题出在analysisManager的初始化和场景容器的时序上。因为我把 Viewer 和分析组件拆分到两个模块模块加载时analysisManager还不存在于viewer.scene上导致add操作被静默吞掉了。解决方案是在 Viewer 完全初始化后再调用分析组件并显式检查viewer.scene.analysisManager是否存在。排查顺序的经验是先看场景对象有没有正常挂载再看分析实例有没有被加入场景最后再怀疑参数问题。很多人习惯一头扎进参数里调来调去反而把最简单的引用问题放过去了。5.2 模型遮挡判断不准的根因功能跑通后我在某一栋建筑的侧面做了验证发现可视域结果里有一块明显应该被建筑物遮挡的区域居然被判定为可见。开始我怀疑是 S3M 模型加载的坐标系偏移导致射线没有和模型表面求交成功但做了坐标系校准后问题依旧。最后把视线从分析模块移开检查了 S3M 图层本身的尺寸和 LOD 切换逻辑发现是因为当时相机视角比较高模型加载的是低精度 LOD 版本很多建筑细部被合并简化了射线实际穿透了建筑的虚拟包围盒。解决方法是发起分析前先把相机飞行到模型近景区域等模型加载到高精度层级后再执行分析。这个问题在遥感影像底图场景下不容易出现但只要涉及倾斜摄影和精细 BIM 模型就非常容易踩中。5.3 动态切换观察点后的状态残留第三个坑出现在做多观察点对比分析时。我在页面上做了一个点位列表点击不同点位就切换观察点位置。第一次点击分析正常第二次点击后画面上出现了两套不同颜色的可视域区域后来甚至叠加了三四套场景变得很乱。原因在于更新观察点时只调了updateObserver没有先移除旧的分析结果实例。iClient3D 的analysisManager默认是叠加模式新分析不会自动替换旧分析。修复方式是在新增分析之前先调用移除接口把前一个实例清掉再做参数更新和重新计算。5.4 性能优化大范围高密度分析的卡顿处理可视域分析的本质是大量射线求交如果最大距离拉到 5000 米、采样密度设到 0.1 米计算量会瞬间暴涨帧率掉到个位数是常有的事。这种场景下我建议做两件事第一是分级加载策略界面上一开始只做低精度快速分析采样间隔大一些等到用户确定关注某个局部区域后再提高采样密度做精分析。第二是分析区域的包围盒裁剪实际项目中往往只需要分析视口内可见范围可以先判断观察点周边 180 度或 270 度范围而不要总是做全向 360 度分析。全向分析意味着几乎 2 到 4 倍的计算量但对于大多数业务需求来讲背后方向通常你是知道的没有必要覆盖。我实测过一个 2000 米范围、采样间隔 0.5 米的城市级场景全向分析耗时大约 2 秒同样的条件下改成 120 度水平张角耗时降到 0.8 秒。6. 进阶扩展多观察点联动和与业务的融合6.1 多观察点同时分析某些项目会要求同时分析多个点位比如一片区域内要部署多台监控设备希望把每个设备的可视域结果叠加在一张视图上评估整体盲区覆盖情况。做法很简单为每个观察点创建独立的ViewshedAnalysis实例分别设置不同的颜色标识全部塞进analysisManager即可。const observers [ { x: 116.39, y: 39.90, z: 80, color: Cesium.Color.CYAN }, { x: 116.41, y: 39.92, z: 60, color: Cesium.Color.ORANGE }, { x: 116.43, y: 39.88, z: 120, color: Cesium.Color.LIME } ] observers.forEach(item { const analysis new Analysis.ViewshedAnalysis({ observer: item, horizontalAngle: 120, verticalAngle: 45, maxDistance: 1200, visibleColor: item.color }) viewer.scene.analysisManager.add(analysis) })多个分析叠加后场景会显得有些杂乱建议在 UI 上增加每个点位的显隐开关方便业务人员单独查看每一个分析结果。实测下来三个点位同时分析单次重算帧率能保持在 30 帧左右对静态结果展示完全够用。6.2 与实时数据的动态联动如果项目里接入了 GPS 轨迹或者设备遥测数据可视域分析的价值会更突出。比如说一台架设在移动巡查车上的摄像头轨迹实时上报经纬度可视域分析也跟着每个轨迹点动态更新可以直接在地图上看到车辆行驶过程中覆盖区域的变化情况。实现动态联动时要注意数据更新的频率和计算开销的平衡。我的做法是把轨迹点做抽稀只保留每秒钟一个点然后对每个点做低采样密度的快速分析并且用双缓冲策略上一帧的结果保持显示下一帧的计算结果完成后才切换避免闪现或闪烁。这项细节在实际演示时观感差异非常大不做双缓冲的话观众会感觉画面一直在闪。6.3 分析结果导出与报告生成可视域分析最终往往会沉淀成项目汇报或规划评审的素材。如果只是截一张图看起来不够正式更好的做法是把分析结果转换成一个 GeoJSON 格式的面状数据输出到二维地图上做叠加展示。iClient3D 的分析模块通常提供了结果转出能力内部是把高亮区域的每一条射线终点连成多边形面再把经纬度坐标导出。我在其中一个项目里就是这么做的分析完成后一键把可视区域导出为标准 GeoJSON然后在配套的二维管理平台上用 Leaflet 展示整个流程完全贯通。从工程实现角度看这一步的代码量不大但业务价值很高因为最终客户通常是在二维汇报图上做决策的三维引擎更适合做现场交互演示。到这里Vue3 环境下基于 SuperMap iClient3D for Cesium 实现可视域分析这条链路就完整了。我个人的体会是可视域分析本身不是特别炫酷的算法但真正把它嵌入到实际业务中、能处理各种真实数据的边界情况才是这项功能价值最大的地方。如果你正在做的项目也需要这个能力先从最小的验证用例跑通再逐步增加观察点、联动和导出能力会比我一头扎进参数调优里高效得多。