ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cesium+Vue地形开挖组件:基于ClippingPlanes的实时裁剪实现

Cesium+Vue地形开挖组件:基于ClippingPlanes的实时裁剪实现 简介面向Cesium与VUE开发者这是一套解决三维场景中地形开挖功能的完整组件方案代码未加密、未压缩可直接调用运行帮助快速实现地形裁剪、局部开挖与效果展示。压缩包共6个文件包含Vue组件、三个核心JavaScript逻辑模块及两张效果预览图整体仅148KB轻量紧凑便于直接集成到项目。目前已有2621人学习研究。源码将开挖逻辑拆分为独立JS与Vue组件职责划分清晰配合底部和侧面的预览图可直观核对开挖区域形态同时支持根据实际需求调整开挖边界与深度方便迁移到GIS、智慧城市等三维可视化场景。使用中如遇问题可通过文章评论或私信获得作者及时回复。1. 地形开挖组件的设计思路为什么选择裁剪而非挖洞在Cesium中做地形开挖第一反应可能是用GlobeSampleHeight采样高度然后修改地形数据。但这种方式在运行时几乎不可行因为地形瓦片是按LOD动态加载的改一处高度会牵动整块网格重算。这套基于CesiumVUE的TerrainExcavation组件走的是另一条路用ClippingPlanes对Globe做实时裁剪让地形直接“塌陷”出指定多边形区域。源码里包含TerrainExcavation.vue、TerrainExcavation.js以及CreatePolygonOnGround.js和ReminderTip.js代码没有加密把Vue组件文件引入现有Cesium项目传入viewer实例就能跑起来。它不是简单的弹窗工具而是把绘制多边形、生成裁剪平面、拖拽编辑、重置清理都串成完整交互链路的工程组件。适合要做土方量估算、地下管线可视化、矿山边坡分析的WebGIS项目你不需要手写Cesium的裁剪数学但看完它的实现你能清楚知道每个参数是怎么影响最终开挖效果的。2. Cesium地形开挖核心ClippingPlanes与集合裁剪的实现2.1 从多边形成环到ClippingPlane集合地形开挖在Cesium里的本质是用一组平面去切地形体的模型坐标被切掉的那一侧就不再渲染。每个ClippingPlane有一个法线和一个距离distance为0时平面穿过原点正值表示沿法线方向平移。对地形来讲我们需要的是一组竖向的平面围成的无限延伸的竖井顶部不切所以这组平面要都垂直于地面。常见做法是把用户在地图上画的多边形点序列转成Cesium.Cartesian3然后用每个相邻点计算三角形面片的法线。这里有个坑直接取多边形的positions再逐个做向量叉积得到的法线方向可能朝外也可能朝内跟顶点顺序有关。CreatePolygonOnGround.js里做的事情就是在画完多边形后用SceneTransforms.worldToWindowCoordinates把三维点映射到屏幕坐标再围绕屏幕中心判断点是顺时针还是逆时针统一翻转法线方向保证挖出来的坑是“往下”而不是“往上凸”。2.2 代码实现与参数说明下面是我在实际项目里整理过的最小可运行裁剪逻辑去掉了监听事件只保留核心function createClippingPlanes(positions) { // positions 是开挖多边形的地表笛卡尔坐标数组 const planes []; for (let i 0; i positions.length; i) { const nextIndex (i 1) % positions.length; const cur positions[i]; const next positions[nextIndex]; // 该边垂直向上方向的向量顶点向上一点 const up Cesium.Cartesian3.UNIT_Z; const right Cesium.Cartesian3.subtract(next, cur, new Cesium.Cartesian3()); // 法线 右向量 x 上向量 const normal Cesium.Cartesian3.cross(right, up, new Cesium.Cartesian3()); Cesium.Cartesian3.normalize(normal, normal); // 若法线点向多边形外侧则反转 const center Cesium.BoundingSphere.fromPoints(positions).center; const toCenter Cesium.Cartesian3.subtract(center, cur, new Cesium.Cartesian3()); if (Cesium.Cartesian3.dot(normal, toCenter) 0) { Cesium.Cartesian3.negate(normal, normal); } planes.push(new Cesium.ClippingPlane(normal, 0.0)); } return planes; }逻辑说明这里用每条边的方向向量和单位上向量做叉积得到的法线同时垂直于边和竖直方向也就是竖直平面的法线。再用多边形中心点判断法线是否朝外如果朝内就取反保证每个平面都朝多边形外侧裁剪。最后交给ClippingPlane的distance统一不给值是因为地形本身原点在椭球中心我们后续还要把平面集合放到地形模型矩阵的局部坐标里真正的偏移量在组件内用modelMatrix转换处理。参数说明positions必须是从viewer.scene.pickEllipsoid或Cesium.Cartesian3.fromDegreesArray得到的笛卡尔坐标不能是经纬度数组。法线必须单位化否则裁剪平面会出现不可预测的偏移。BoundingSphere计算中心比Cesium.Cartesian3.midpoint更稳定尤其凹多边形时。2.3 开挖策略凹多边形与凸包处理如果你在页面上画了一个“L”形区域直接用上面的循环生成平面凹进去的地方会出现“剪过头”的问题。因为ClippingPlanes对每个平面都是全地形裁切凹多边形的内部在几何上无法用半平面交集完美表示。处理方式有两种把凹多边形拆分成多个凸多边形每个凸多边形单独生成一组裁剪平面顺序分组应用到同一个Globe上。更省性能的办法对用户输入的点做一次三角剖分把三角形当作凸区域逐个子开挖。TerrainExcavation.js里采用的是先判断多边形凹凸性凹多边形自动按耳朵裁剪法Ear Clipping拆分。这一点在demo里看不出来但当你画一个“C”形地块时就会看到clippingPolygons数组长度大于1。遇到这种场景我的调试经验是在拆分后打印每组的点数如果某组小于3说明剖分失败回退到凸包算法。2.4 与3DTiles裁剪的差异说明很多初看这个组件的人会问为什么不能调用现成的ClippingPlanes集合直接给3DTiles用因为地形的Globe是一个曲面它内部的tile节点在屏幕空间被剪裁后边缘会露出地底下方的纹理而3DTiles是三角网模型有明确的包围盒。所以地形开挖的平面集合在使用时还需要加一个modelMatrix把裁剪平面从椭球经纬度坐标转换到世界坐标。TerrainExcavation.js中有一句关键的tileClippingPlanes.modelMatrix Cesium.Transforms.eastNorthUpToFixedFrame(center)作用就是让平面集合“贴”在地表指定位置。去掉这句你的坑会开在错误的地方甚至完全不生效。3. Vue中封装地形开挖组件的通信与状态管理3.1 组件划分与props/emit设计原始的demo把功能拆成TerrainExcavation.vue、TerrainExcavation.js和ReminderTip.js这个拆分是有讲究的.vue文件只负责模板、按钮和viewer的注入.js文件保持纯函数便于单测和复用。我的建议是外层再包一层parent component把viewer对象放到provide/inject里而不是用ref去穿透多层。props设计建议如下prop名类型默认值说明viewerObject必填Cesium的Viewer实例editableBooleantrue开挖后允许拖拽编辑clipHeightNumber0从地表往下裁剪的高度米默认0表示挖穿edgeColorColorCesium.Color.YELLOW开挖洞口边缘描边颜色3.2 组件挂载时初始化template div classterrain-excavation button clickstartDraw :disableddrawing开始绘制/button button clickclearAll v-ifclippingPlanes清除开挖/button /div /template script import * as Cesium from cesium; import { createClippingPlanes, createPolygonOnGround } from ./TerrainExcavation.js; export default { name: TerrainExcavation, props: { viewer: { type: Object, required: true }, clipHeight: { type: Number, default: 0 } }, data() { return { drawing: false, positions: [], clippingPlanes: null, gfxGroundPolygon: null }; }, methods: { startDraw() { this.drawing true; const handler new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); handler.setInputAction((movement) { const cartesian this.viewer.scene.pickEllipsoid(movement.position); if (!cartesian) return; this.positions.push(cartesian); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); handler.setInputAction(() this.finishDraw(handler), Cesium.ScreenSpaceEventType.RIGHT_CLICK); }, finishDraw(handler) { handler.destroy(); this.drawing false; if (this.positions.length 3) return; const planes createClippingPlanes(this.positions); const center Cesium.BoundingSphere.fromPoints(this.positions).center; this.clippingPlanes new Cesium.ClippingPlaneCollection({ planes: planes, modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center) }); this.viewer.scene.globe.clippingPlanes this.clippingPlanes; // 画一个地表区域标记开挖范围 this.gfxGroundPolygon createPolygonOnGround(this.viewer, this.positions); } } }; /script逻辑说明startDraw里注册了左键选点、右键完成绘制每次左键点地形时用pickEllipsoid拿到的笛卡尔坐标直接存入positions。绘制完成后调用createClippingPlanes生成平面集合再用eastNorthUpToFixedFrame生成以开挖区域中心为原点的模型矩阵。最后把clippingPlanes赋给scene.globe.clippingPlanes这一步才真正开始裁剪地形。参数说明这里clipHeight在demo中默认挖到地心如果只挖一定深度需要在ClippingPlane的distance上减去高度偏移并在modelMatrix上叠加平移矩阵简单做法是modelMatrix.multiply(Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(0, 0, -clipHeight)))。注意不能在CLICK事件里不加判断地连续接收否则双击会记录两个点demo源码里用drawing标志和事件销毁来控制。3.3 组件销毁时清理裁剪这类组件最常见的bug是路由跳转后旧viewer的globe.clippingPlanes还残留导致下次打开地图地形被切掉一块。TerrainExcavation.vue的beforeDestroy里必须做三件事beforeDestroy() { if (this.viewer this.viewer.scene this.viewer.scene.globe) { this.viewer.scene.globe.clippingPlanes undefined; } if (this.gfxGroundPolygon) { this.viewer.entities.remove(this.gfxGroundPolygon); } }这里clippingPlanes必须设为undefined而不是拷贝空集合因为Cesium内部会监听这个属性变化空集合也能被渲染一次产生一秒的闪烁。如果你的项目用的是Vue 3 Composition API建议把TerrainExcavation.js里的纯函数放进onUnmounted也没问题核心原则是大对象viewer不要直接放进reactive会让响应式追踪开销变大。4. 交互编辑与多边形拖拽让开挖范围可改4.1 拖拽点的生成与更新只画一次多边形的裁剪没多大意思工程场景下需要反复调整边界。TerrainExcavation.js里用CallbackProperty实现地表多边形的动态更新类似这样function createPolygonOnGround(viewer, positions) { const polygon viewer.entities.add({ polygon: { hierarchy: new Cesium.CallbackProperty(() { return new Cesium.PolygonHierarchy(positions); }, false), material: Cesium.Color.RED.withAlpha(0.3), classificationType: Cesium.ClassificationType.BOTH } }); return polygon; }CallbackProperty的第一个参数是回调函数每次渲染时Cesium会要求它返回最新的几何数据。这里注意false表示该属性不随时间变化但Cesium仍会在相机移动时重新调用因为我们每次拖拽点都会修改positions数组本身所以不需要主动刷新。若把第二个参数设为true它会被当成时间相关属性会导致每一帧都重新创建多边形性能明显下降。4.2 拖拽逻辑的实现坑点一般来说我给每个位置点添加一个PointGraphics再用ScreenSpaceEventHandler接收左键按下事件通过viewer.scene.pick判断是否拾取到点。如果拾取到进入拖拽状态在MOUSE_MOVE事件里用pickEllipsoid更新点位坐标。注意不要用Camera.pickEllipsoid那只能拾取屏幕中心要用scene.pickEllipsoid。拖拽过程中有一个方向性问题更新positions[i]后需要同步更新clippingPlanes里对应平面的法线。许多从网上copy的代码只更新地面多边形裁剪面不更新结果就是拖拽完地上挖的洞还是原来位置。我一般把createClippingPlanes重新执行一遍然后用clippingPlanes.removeAll()再add新平面集合直接给clippingPlanes.planes newPlanes会报Uncaught TypeError因为planes属性是只读的。4.3 编辑状态下的Undo/Redo简化方案组件源码里没有封装撤销但我强烈建议在调用finishDraw前把positions.slice()深拷贝一份放进历史栈别引用同一个数组。拖拽完成时记录快照这样用户点撤销时先把clippingPlanes移除再回退快照并重新执行createClippingPlanes和eastNorthUpToFixedFrame。快照里除了点坐标还要记录当时的clipHeight不然撤销深度会变化。5. 一个实用技巧用Region遮罩让开挖边缘不再锯齿闪烁最后聊一个很多人都会遇到却搜不到答案的问题裁剪平面设置后地形洞口边缘出现黑色锯齿而且相机远离时会整片闪烁。这个现象不是Cesium渲染bug而是裁剪平面集合作用于Globe时没有同时限制地形LOD的细分。当地形瓦片越远三角形越大裁剪边界就显示为大锯齿相机移动时瓦片细分变化锯齿边会跟着跳。我的处理办法是给开挖洞口盖一个“假面”多边形用GroundPrimitive把洞口边缘围起来同时对这个多边形开启material: Cesium.Material.fromType(Color)并设置translucent: false。由于地面多边形的classificationType默认可以盖住地形裁剪边界就会被这个多边形的边缘压住视觉上锯齿感大幅减轻。具体参数可以这样调整const edgePrimitive new Cesium.GroundPrimitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.PolygonGeometry({ polygonHierarchy: new Cesium.PolygonHierarchy(positions), vertexFormat: Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT }) }), appearance: new Cesium.EllipsoidSurfaceAppearance({ material: Cesium.Material.fromType(Color, { color: Cesium.Color.YELLOW.withAlpha(0.6) }) }) });为验证是否有效可以在camera.setView({destination: positions[0]})后把scene.globe.maximumScreenSpaceError调到1默认2观察边界。maximumScreenSpaceError越小瓦片切得越细锯齿越小但会影响整体加载性能。更可靠的做法是拖拽结束、相机停稳后主动更新globe.tileCacheSize并调用scene.requestRender()强制重绘。另外一个实用判定如果开挖后地形消失的部分不是你想挖的区域先检查clippingPlanes.modelMatrix的中心点是否是positions的几何中心而不是第一个点。这是大多数人复制eastNorthUpToFixedFrame(viewer.camera.positionWC)的错误写法导致的组件demo里已经用BoundingSphere修正了直接拿来用即可。最后建议你把ReminderTip.js里的提示做一次i18n硬化因为默认提示是中文硬编码在英文浏览器里不会影响功能但测试人员容易误以为是国际化bug。本文还有配套的精品资源点击获取
返回列表