news 2026/9/15 12:29:48

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cesium+Vue地形开挖组件:基于ClippingPlanes的实时裁剪实现

简介:面向Cesium与VUE开发者,这是一套解决三维场景中地形开挖功能的完整组件方案,代码未加密、未压缩,可直接调用运行,帮助快速实现地形裁剪、局部开挖与效果展示。压缩包共6个文件,包含Vue组件、三个核心JavaScript逻辑模块及两张效果预览图,整体仅148KB,轻量紧凑,便于直接集成到项目。目前已有2621人学习研究。源码将开挖逻辑拆分为独立JS与Vue组件,职责划分清晰,配合底部和侧面的预览图可直观核对开挖区域形态;同时支持根据实际需求调整开挖边界与深度,方便迁移到GIS、智慧城市等三维可视化场景。使用中如遇问题,可通过文章评论或私信获得作者及时回复。

1. 地形开挖组件的设计思路:为什么选择裁剪而非挖洞

在Cesium中做地形开挖,第一反应可能是用GlobeSampleHeight采样高度,然后修改地形数据。但这种方式在运行时几乎不可行,因为地形瓦片是按LOD动态加载的,改一处高度会牵动整块网格重算。这套基于Cesium+VUE的TerrainExcavation组件,走的是另一条路:用ClippingPlanes对Globe做实时裁剪,让地形直接“塌陷”出指定多边形区域。源码里包含TerrainExcavation.vueTerrainExcavation.js以及CreatePolygonOnGround.jsReminderTip.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; }

逻辑说明:这里用每条边的方向向量和单位上向量做叉积,得到的法线同时垂直于边和竖直方向,也就是竖直平面的法线。再用多边形中心点判断法线是否朝外,如果朝内就取反,保证每个平面都朝多边形外侧裁剪。最后交给ClippingPlanedistance统一不给值,是因为地形本身原点在椭球中心,我们后续还要把平面集合放到地形模型矩阵的局部坐标里,真正的偏移量在组件内用modelMatrix转换处理。

参数说明:positions必须是从viewer.scene.pickEllipsoidCesium.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.vueTerrainExcavation.jsReminderTip.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 class="terrain-excavation"> <button @click="startDraw" :disabled="drawing">开始绘制</button> <button @click="clearAll" v-if="clippingPlanes">清除开挖</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中默认挖到地心,如果只挖一定深度,需要在ClippingPlanedistance上减去高度偏移并在modelMatrix上叠加平移矩阵,简单做法是modelMatrix.multiply(Cesium.Matrix4.fromTranslation(new Cesium.Cartesian3(0, 0, -clipHeight)))。注意不能在CLICK事件里不加判断地连续接收,否则双击会记录两个点,demo源码里用drawing标志和事件销毁来控制。

3.3 组件销毁时清理裁剪

这类组件最常见的bug是路由跳转后,旧viewer的globe.clippingPlanes还残留,导致下次打开地图地形被切掉一块。TerrainExcavation.vuebeforeDestroy里必须做三件事:

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移除,再回退快照并重新执行createClippingPlaneseastNorthUpToFixedFrame。快照里除了点坐标,还要记录当时的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。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:28:11

AI范式转变:从技术演进到产业重构

1. AI范式转变&#xff1a;从技术演进到产业重构的深度解析当AlphaGo击败李世石时&#xff0c;我们以为看到了AI的巅峰&#xff1b;当ChatGPT写出学术论文时&#xff0c;我们才发现这只是开始。AI发展正在经历一场根本性的范式转变——这不仅关乎算法改进&#xff0c;更将彻底重…

作者头像 李华
网站建设 2026/9/15 12:26:56

Flutter流式JSON解析优化与鸿蒙适配实战

1. 项目背景与核心挑战在移动应用开发领域&#xff0c;处理大型JSON数据一直是个棘手的性能瓶颈。传统JSON解析方案如dart:convert库的json.decode()方法&#xff0c;会一次性将整个JSON文档加载到内存中。当处理10MB以上的JSON文件时&#xff0c;内存占用可能瞬间飙升到原数据…

作者头像 李华
网站建设 2026/9/15 12:26:27

Vue3封装SVG边框进度条:光点跟随动效与充电场景实现

前段时间在做一个车载HMI相关的展示项目&#xff0c;设计稿里给了一个很典型的边框进度条交互&#xff1a;卡片四周一圈发光描边&#xff0c;随电量增长而增长&#xff0c;同时有一个光点动画效果沿着边框游走&#xff0c;整体风格很像新能源车中控屏上的充电画面。当时第一反应…

作者头像 李华
网站建设 2026/9/15 12:25:14

西门子1200PLC工程创建与程序下载实操指南

1. 这不是教科书&#xff0c;是我在车间熬了三个通宵后写下的实操笔记西门子1200PLC工程创建及程序下载读取——这八个字背后&#xff0c;藏着自动化工程师每天要面对的真实战场&#xff1a;新项目启动时博途TIA Portal卡在“正在加载项目模板”、下载程序时弹出“目标设备不匹…

作者头像 李华