ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cesium动态单体化实战:BatchTable、GPU拾取与高亮还原

Cesium动态单体化实战:BatchTable、GPU拾取与高亮还原 简介面向Cesium与VUE开发者的动态单体化示例围绕整幢建筑与分层分户两种模式提供可直接运行的完整演示程序与未加密源代码。压缩包共有5个文件其中两个Vue组件承担单体化交互逻辑两个JSON文件存放建筑及户型配置数据另有一个动态单体化功能模块目录承载核心效果包体整体仅13KB由于模型数据体积较大资源内未附带模型需要自行准备以还原完整效果。代码未压缩命名和注释清晰适合GIS、三维可视化从业者以及对Cesium和VUE集成感兴趣的开发者能够快速理解建筑整体高亮、分层分户独立选中、点击拾取与样式切换等关键实现思路。目前已有4178人学习下载可直接作为动态单体化功能开发时的参考起点也可用于搭建轻量的Cesium三维交互演示。1. 动态单体化Cesium实战里最容易被绕晕的一环做过三维GIS的人基本都有体会倾斜摄影模型加载很流畅但用户一句点一下这栋楼要高亮再点一下房间要弹属性团队往往要卡上好几天。倾斜摄影本质上是一整片三角网模型里没有建筑边界也没有楼层划分。动态单体化的思路是在3D Tiles的BatchTable里给每个对象分配批次编号运行时通过GPU拾取拿到像素对应的batchId不改几何、只改颜色与混合模式整片模型就能逻辑化成一栋栋、一户户可点选的对象。下面这套Cesium Vue的完整demo直接拆解整幢建筑DynamicMonomerWhole.vue和分层分户DynamicMonomerSingle.vue两套实现。源码未加密未压缩可以直接运行唯一缺口是模型数据因为体积大所以评论区留邮箱获取。适合被3D Tiles拾取、高亮、状态还原折腾过的前端和GIS开发。2. 原理先行BatchTable、GPU拾取与Vue生命周期三者如何交汇2.1 动态单体化和静态单体化的本质差异静态单体化是在建模阶段就把每一栋楼、每一层切分并导出为独立的模型对象。加载后每个对象各自占用一个节点点击识别靠模型节点的id实现逻辑非常直观。但代价是整个场景三角面成倍增加渲染批次大幅上升。一个几十栋建筑的园区还能跑做到成百上千栋时draw call会直接拖垮帧率。动态单体化则完全换了一套思路。3D Tiles的BatchTable批量表为模型里的每个对象保存了一条属性记录包括建筑名、楼层、用途、编号等这些记录和模型的三角面片通过batchId关联。渲染时几何体只有一份GPU一个draw call输出整片范围CPU端需要做的只是在点击时根据像素位置找到对应的batchId再去查这条属性。因此它的性能瓶颈从渲染转移到了数据量和点击时的查找而这两点都是可控的。对于园区级、城市级场景动态单体化几乎是目前唯一现实的选择。它不是一种更高级的方案而是为了把模型体量和交互体验同时保住而做的工程取舍。理解这一点后后续看demo代码就不会被为什么点一下要遍历整个tileset这类问题带偏。2.2 拾取链路从click事件到batchId需要走完哪几步Cesium的拾取链路看起来就一个函数调用但拆开看有四步。第一步通过ScreenSpaceEventHandler监听LEFT_CLICK第二步调用viewer.scene.pick(position)得到拾取结果第三步判断拾取结果是否是Cesium3DTileFeature类型第四步通过getProperty或batchId取得业务属性。const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position) if (!Cesium.defined(picked)) return if (picked.id instanceof Cesium.Cesium3DTileFeature) { const feature picked.id const batchId feature.batchId const name feature.getProperty(name) console.log(选中batchId:, batchId, 名称:, name) } }, Cesium.ScreenSpaceEventType.LEFT_CLICK)代码逻辑很直白movement.position是鼠标在画布上的像素坐标pick做的是反选把像素坐标映射到场景中的几何体。类型判断不可省因为点击天空盒、地形或普通Entity时pick返回的对象结构完全不同直接访问batchId会抛异常。getProperty读取的就是BatchTable里的字段字段内容和建模时写入的属性表一一对应。这里有一个常见误用有人在点击后试图用viewer.scene.pickPosition(movement.position)代替pick去取坐标。pickPosition返回的是世界坐标而pick返回的是对象引用两者用途不同。如果后续要做点击位置弹出浮层那类需求通常需要同时调用一个拿feature一个拿坐标。只在tileset上做颜色高亮时pick一个就够了。2.3 Vue里集成Cesium初始化时机的选择与销毁时机Cesium操作的是原生WebGL和DOM和Vue的响应式系统不太合拍。实践中最稳妥的姿势是把Viewer实例挂到组件的this上而不是放进data。一旦放进dataVue会为viewer内部对象递归创建响应式代理初始化时还好运行久了会出现帧率波动严重的直接白屏或崩溃这类问题在cesium 3d地球滚动出现崩溃的报错里非常典型。export default { data() { return { tilesetReady: false } }, mounted() { this.viewer new Cesium.Viewer(this.$refs.container, { animation: false, timeline: false, baseLayerPicker: false, shouldAnimate: false }) this.loadTileset() }, beforeDestroy() { if (this.viewer) { this.viewer.destroy() this.viewer null } } }animation、timeline、baseLayerPicker这些控件在业务系统里基本用不到关掉能省出底部一块DOM空间。beforeDestroy里手动destroy()是很多人忽略的一步Vue组件卸载不会自动回收WebGL上下文反复进入离开页面会把显存耗尽。关于环境配置Vue CLI还是Vite都可以关键是把Cesium的静态资源和tileset放进public目录保持路径稳定。npm包里的cesium自带widgets.css和构建好的入口文件按官方方式import后不需要额外配置。查API时注意Cesium中文文档更新偏慢像Cesium3DTileset.fromUrl这类新方法在旧翻译文档里查不到建议直接对照官方英文API。3. 整幢建筑单体化DynamicMonomerWhole.vue的完整实现拆解3.1 default.json属性表与3D Tiles是如何关联起来的demo附带的数据文件data/default.json是典型的GeoJSON FeatureCollection它承载了每个单体建筑的编号、名称、层数等业务属性。和3D Tiles原始batch table不同GeoJSON的好处是前端可以直接用fetch加载跟普通Web数据请求没有区别。在tileset加载完成后需要把batchId与GeoJSON里的要素做一个映射点击时才能把feature还原成业务对象。映射关系非常简单通常是拿两者都有的id字段。Cesium的tileset模型属性表里如果存在和GeoJSON一致的id直接遍历比对即可fetch(./data/default.json) .then(res res.json()) .then(geoJsonData { this.bizFeatureMap new Map() geoJsonData.features.forEach((feat) { const props feat.properties // 用业务id作为key后续点击时直接查 this.bizFeatureMap.set(props.id, props) }) })用一个Map来存id到业务属性的映射比数组遍历查找效率高GeoJSON或者batchTable通常也就几千条内存开销可以忽略。需要记住bizFeatureMap的key是业务id不是batchId两者在建模导出时可能一致也可能不一致取决于数据生产方的约定。如果建模时batchId没有直接暴露还要通过feature.getProperty(id)来对齐路径是通的只是多一次属性读取。3.2 拾取、高亮、还原三个步骤一次说清DynamicMonomerWhole.vue核心的点击处理逻辑就三个动作拾取、高亮、还原上一次高亮。顺序上有个细节——先还原上一次再处理当前点击否则连续点两栋楼时颜色会叠在一起handler.setInputAction((movement) { this.cancelHighlight() const picked viewer.scene.pick(movement.position) if (!Cesium.defined(picked)) return if (!(picked.id instanceof Cesium.Cesium3DTileFeature)) return const feature picked.id this.highlightedFeature feature // 高亮的本质覆写颜色并和原纹理做混合 feature.color Cesium.Color.fromCssColorString(#13E6FF).withAlpha(0.65) feature.colorBlendMode Cesium.ColorBlendMode.MIX feature.colorBlendAmount 0.7 }, Cesium.ScreenSpaceEventType.LEFT_CLICK)高亮的核心不是改几何而是覆写颜色并设置混合模式。feature.color是Cesium为3D Tiles要素提供的覆写色渲染时会和模型原有纹理混合。ColorBlendMode.MIX按colorBlendAmount的比例把覆写色和原色混合0.7意味着新颜色占七成保留了三成原始纹理细节。这个值对视觉影响很大调到1.0时建筑看起来像纯色玩具调到0.3以下高亮效果又不明显。还原方法对应写成独立函数cancelHighlight() { if (!this.highlightedFeature) return const f this.highlightedFeature // 不能设为undefined必须把颜色和混合状态都重置 f.color Cesium.Color.WHITE.withAlpha(1) f.colorBlendMode Cesium.ColorBlendMode.HIGHLIGHT f.colorBlendAmount 0 this.highlightedFeature null }很多初学者想直接把color设为undefined来还原结果发现模型颜色回不去了。原因在于设置为undefined表示不覆写但Cesium渲染管线里blendMode和amount仍然是上次留下的状态。正确姿势是把颜色重置为白色、混合模式重置为HIGHLIGHT、比例归零。HIGHLIGHT模式在没有设颜色时会让建筑略微发白视觉上接近原始状态。提示feature.color设置的是覆写色不需要担心破坏原始模型数据切换视角或重新加载瓦片后颜色状态会被Cesium自动重置。3.3 色调、透明度、混合模式一张参数表解决配置问题参数可选值对视觉效果的影响建议feature.color任意Cesium.Color决定高亮颜色青色系、半透明透明度0.5~0.8colorBlendModeREPLACE / MIX / HIGHLIGHTREPLACE完全替换纹理MIX与纹理混合HIGHLIGHT纯提亮选MIXcolorBlendAmount0~1新颜色占比0.6~0.8maximumScreenSpaceError默认16值越小细节越足性能开销越大16~32maximumMemoryUsage默认512MB限制显存占用512~1024maximumScreenSpaceError和maximumMemoryUsage是加载3D Tiles时的tileset参数不是feature参数但和单体化表现关系很大。前者控制LOD切换的敏感度值调低到8时会明显增加顶点数据请求量后者是显存上限做城市级数据时调高能减少瓦片重复加载造成的卡顿。async loadTileset() { try { this.tileset await Cesium.Cesium3DTileset.fromUrl(/data/3dtiles/tileset.json, { maximumScreenSpaceError: 16, maximumMemoryUsage: 512 }) this.viewer.scene.primitives.add(this.tileset) this.viewer.flyTo(this.tileset) } catch (e) { console.error(tileset加载失败, e) } }Cesium3DTileset.fromUrl返回的是Promise旧项目里常见的new Cesium.Cesium3DTileset({ url })写法已经废弃。另外模型自身的坐标如果不在合适位置flyTo会跳到错误方向。可以把this.viewer.scene.globe.depthTestAgainstTerrain设为true再拾取能避免很多因为地形遮挡导致的点击异常。4. 分层分户单体化DynamicMonomerSingle.vue的精细化控制4.1 分层分户的数据模型与先楼栋后楼层的交互逻辑整幢建筑单体化只能回答这栋楼是谁分层分户要回答的是这一户在哪里、属于哪层、面积多大。数据模型上BatchTable里的每条记录除了batchId还必须带楼层、房号、用途等字段。模型生产方会在建模软件里把每层每户分离出来并写入属性前端拿到的就是一个楼层户粒度的属性表。交互上我建议先按楼栋选中再在当前楼栋内部切换楼层和户。直接让用户在一栋几十层的大楼外立面上点某一户体验很差稍微换个视角楼上阳台或装饰条就会把目标户挡住点到的根本不是用户看到的那一面。demo的DynamicMonomerSingle.vue采用的就是选中楼栋后展示楼层列表点击楼层高亮本层所有户再点击户高亮单独一户的逻辑。有个认知误区需要澄清3D Tiles的feature是业务属性的载体和Cesium Primitive或Model的自然节点node不是一回事。改节点颜色对整块tileset无效单体化必须作用在feature层面也就是batchId所对应的对象上。4.2 核心实现按属性字段批量高亮楼层或户分层分户最核心的代码是遍历tileset里所有feature按属性字段筛选并统一上色。highlightByFloor(floorNo) { const tileset this.tileset const content tileset.content const featuresLength content.featuresLength this.clearHighlight() for (let i 0; i featuresLength; i) { const feature content.getFeature(i) if (!feature.hasProperty(floor)) continue if (feature.getProperty(floor) floorNo) { // 同一楼层所有户统一高亮 feature.color Cesium.Color.fromCssColorString(#FFB84D).withAlpha(0.72) feature.colorBlendMode Cesium.ColorBlendMode.MIX feature.colorBlendAmount 0.75 this.highlightedFeatures.push(feature) } } }tileset.content对应的是一块瓦片内容getFeature(i)拿到的是该瓦片序号为i的要素。注意content.featuresLength可能很大但遍历只是访问属性表不做几何计算几万个要素遍历一次也就十几毫秒。更好的做法是在tileset加载完成后就把属性索引建好按楼层分组存储feature集合这样交互时连遍历都省了buildFloorIndex() { this.floorMap {} const content this.tileset.content for (let i 0; i content.featuresLength; i) { const f content.getFeature(i) const floor f.getProperty(floor) if (!this.floorMap[floor]) this.floorMap[floor] [] this.floorMap[floor].push(f) } }这里要特别留意3D Tiles的content.getFeature(i)在瓦片被卸载或LOD切换后拿到的feature引用可能失效。小场景、静态视角下问题不大城市级大场景强烈建议每次操作时重新遍历一次tileset.content而不是把floorMap里的引用长期保存。注意瓦片LOD切换后content.getFeature(i)返回的对象可能变化长期持有feature引用并不安全。4.3 点击到户时的容错与参数调试选中某一户时点击返回的feature可能同时具备floor和unit属性但更多时候是只有其中一个。读取属性前先做hasProperty判断这是必要操作if (!feature.hasProperty(unit)) { this.$message.warning(当前要素没有户编号属性) return }另一个高频问题是属性值类型不一致。建模软件里导出的101可能是字符串也可能被导出成数字101直接用比较会匹配不上。保险的做法是统一转成字符串再比较const floor String(feature.getProperty(floor)) const room String(feature.getProperty(unit))这是我实际调试中遇到最多的一类问题。排查思路很简单在tileset加载完成后用getProperty(floor)打印几组数据看看类型和值再决定比较逻辑。不要默认生产方的数据类型模型的属性表是建模软件导出的前端这边几乎是不可控的。分层分户里还有一个容易踩的坑整幢高亮时透明度过低会导致内部结构全部可见看起来像X光片但层级太高又看不清被选中的楼层。实践值参考——楼层颜色用暖色橙色系透明度0.72户级别用青色透明度0.6两种颜色在视觉上区分开用户就不会在高亮后分不清自己选的是楼层还是户。5. 模型数据缺失时如何快速验证demo逻辑与排查问题5.1 用GeoJSON生成临时楼栋验证拾取与高亮demo缺失的是倾斜摄影模型数据但单体化的拾取、上色、还原逻辑并不依赖逼真的纹理。最快速的验证方法是直接用Cesium的GeoJsonDataSource加载default.json生成带高度的简易楼栋const dataSource await Cesium.GeoJsonDataSource.load(./data/default.json, { clampToGround: true }) dataSource.entities.values.forEach(entity { const height entity.properties.height || 20 // 按GeoJSON面状边界挤出高度用于验证点击链路 entity.polygon.height height }) this.viewer.dataSources.add(dataSource)这个方案用GeoJSON的几何边界挤出简单高度验证属性映射和点击事件的效果完全够用。等拿到真实的b3dm/3D Tiles数据后只需把加载代码替换回Cesium3DTileset.fromUrl交互逻辑不需要改动。5.2 踩过的坑与排查清单常见报错直接列排查思路第一Cannot read properties of undefined先怀疑viewer没初始化成功第二feature.getProperty is not a function说明拾取结果不是tileset feature回看类型判断第三点击没有任何反应打开Network面板确认tileset.json是否真的请求成功、有没有通过本地http-server启动第四高亮后模型整体变暗检查还原时是否清除了colorBlendMode。数据联调阶段按这个顺序验证先确认帧率稳定再打开瓦片调试着色然后打印点击feature的batchId、floor、unit最后做连续快速点击测试还原是否正确。还有一点容易被忽视相机远离时周边瓦片会退到低精度点击位置对应的楼层可能和肉眼看到的不一致验证时先把相机拉近等精细层瓦片加载完成后再点。四步都过了单体化逻辑基本可以交付。本文还有配套的精品资源点击获取
返回列表