尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cesium地形开挖实战:基于裁剪平面的地表消失与三维可视化

Cesium地形开挖实战:基于裁剪平面的地表消失与三维可视化 简介一份基于 Cesium 的地形开挖前端开发示例面向 WebGIS 开发者和三维可视化学习者演示在 HTML 页面中集成 Cesium 并结合 JavaScript 实现地形挖洞效果。压缩包内共 1 个 HTML 文件仅 1KB无需复杂工程配置可直接双击运行适合快速查看核心实现与交互逻辑。已有 1218 人学习下载可作入门参考或二次开发模板。示例覆盖 Cesium 库引入方式、Viewer 初始化、矩形边界与几何体创建、材质定义、GroundPrimitive 叠加等关键环节同时提供监听鼠标或键盘事件、动态更新挖洞位置、大小或深度的交互思路。对希望快速跑通最小示例的开发者来说代码量小但结构完整便于结合 Cesium 官方文档进一步扩展对刚接触三维场景构造的读者尤其友好还能通过修改几何参数观察场景变化是掌握地形开挖基本流程的实用起手式。1. Cesium地形开挖用裁剪平面让地表“消失”的前端实战“Cesium地形开挖”是GIS前端里特别常见的一个需求你有一块真实地表想在不修改高程数据的前提下把某一区域“挖开”露出地下管线、隧道或矿体。最直接的理解是给地球加一把无形的裁纸刀Cesium在渲染时把指定区域的地形面片直接丢弃视觉上就会出现一个天坑。这个能力通常用ClippingPlanes裁剪平面实现在HTML前端的三维开发实例中占比极高。本文不绕原理不讲空话从头把一个可交互的地形开挖工具做出来适合想快速落地的前端三维开发者和GIS工程师。2. 开挖前的底层认知globe与3D Tiles的裁剪入口、平面方向与矩形包围逻辑在动手写代码前先把Cesium里“裁剪”这块黑匣子拆开。2.1 两个裁剪入口scene.globe.clippingPlanes 与 tileset.clippingPlanesCesium裁剪平面有两条主路viewer.scene.globe.clippingPlanes针对全球地形高程表面Cesium3DTileset.clippingPlanes针对某个具体的三维瓦片模型比如倾斜摄影、点云或人工BIM模型。要做“地形开挖”绝大多数场景只要操作前者即可。但如果你把倾斜摄影模型也叠在地形上只裁剪globe还不够需要在对应的tileset上挂同一组平面否则会看到地形挖开了倾斜摄影模型还飘在原处。选型上没有绝对标准我的经验是数据源是CesiumIon的全球地形或自己的quantized-mesh地形用globe数据源是倾斜摄影、OBJ或3D Tiles模型必须用tileset。如果两个都有就两处都设置且用同一套平面数组保证视觉一致。注意老版本Cesium把地形裁剪叫scene.globe.clippingPlanes新版本依然兼容但ClippingPlaneCollection的内部实现已经换过一轮建议用当前稳定版1.111或更高版本的API别死磕老代码。这里还要提一个常见的误用很多人以为给viewer.scene加一个clippingPlanes就行结果发现没有反应。实际上scene本身没有这个属性它挂在scene.globe上。如果你拿到一个报错clippingPlanes is not defined先检查自己是不是写成了viewer.scene.clippingPlanes。类似的Cesium3DTileset实例也有clippingPlanes属性但它是独立的不共享globe的裁剪集合。2.2 平面方程与法向量到底谁在控制“挖”还是“凸”很多人第一次接触ClippingPlane时会去猜“distance是不是深度”。其实不是。Cesium的ClippingPlaneCollection最终按平面方程把空间切成两半。一个平面由normal和distance定义空间点P到平面的带符号距离是normal · P distance。当值为负时该点被裁剪掉不参与绘制当值为正或零时保留。所以你想让哪一侧消失就把法向量指向你不想消失的一侧。这个方向一旦搞反坑就会变成一座山而且法向量还和坐标系直接相关。我一般不在心里背这个公式而是画一个简单的示意图法向量箭头指向的区域是“活下来”的地方箭头背面是“被裁掉”的地方。想象你要挖一个方坑坑里的土要消失那么坑壁的法向量应该朝外也就是朝地面表面外侧。如果朝内坑里的土被保留坑外全被裁掉视觉上就是一座反向山。实现上平面通常不直接用ECEF坐标手填而是通过局部ENU东-北-上坐标系转换。以开挖范围中心为原点法向量是局部坐标里的三个轴方向再乘以旋转矩阵进全局坐标。这样你只需要关心“往东”“往北”“往西”“往南”不用去算地球曲率下的朝向。Cesium的Transforms.eastNorthUpToFixedFrame就是做这个转换的标准入口后面代码里会讲怎么用。2.3 为什么矩形开挖要用四个平面而不是一个包围盒Cesium的ClippingPlane是无限平面单条平面只能把整个地表切成两半。要挖一个有限矩形区域需要至少四个平面分别对应矩形的东、西、南、北边界。四个平面裁剪区域的交集恰好就是这个矩形内的空间。这个交集行为由ClippingPlaneCollection的unionClippingRegions参数控制false表示交集默认true表示并集。对四个包围平面来说我们要的是交集所以保持默认即可。如果你看到“设置一个平面就把一个矩形挖走了”的效果那是内部做了ClippingPolygon或用了深度纹理不是纯ClippingPlane。对于地形高程表面没有可靠的ClippingPolygon API老老实实四个平面边缘效果还更好控制。这里注意unionClippingRegions的命名很容易让人误解。字面上看“union”像是“联合多个区域”其实它代表的是“裁剪区域的并集”。设成true后四个平面两两组成了若干个区域地形会按并集裁掉结果可能是整个场景都被裁干净。所以这个参数默认false一定要保持除非你有多个分开的小洞要挖。真要多洞建议用多个ClippingPlaneCollection对象或者用Cesium.ClippingPolygon只支持3D Tiles来解别拿一个collection硬塞。2.4 坑壁和边缘不是自动生成的裁剪平面只删三角形不生产新三角形。所以坑壁不是垂直截面而是地形侧面被切开后露出的网格线。想要一个完整的竖直坑壁需要单独加几何体。常见做法是用Cesium.WallGeometry沿坑的边界坐标生成一圈围墙再贴材质或者用一个BoxGeometry反向挖但Box对起伏地形不好处理。我通常的做法是先用矩形边界生成PolygonHierarchy然后Cesium.WallGeometry沿边界点生成围墙高度设置从地表到坑底。这样即使Cesium的地形网格有起伏墙面也能遮住截面。这块不是裁剪的本职但做“开挖”项目时基本绕不开先在心里留个位置。3. 用HTMLJavaScript跑通最小地形开挖四个平面抠出一块矩形天坑3.1 搭建最小Cesium页面并加载全球地形不用框架一个HTML文件就能跑。先引入Cesium的CSS和JS然后创建Viewer。注意地形开挖必须加载真地形默认的椭圆体不会响应地形裁剪。!DOCTYPE html html langzh-cn head meta charsetutf-8 titleCesium 地形开挖示例/title link hrefhttps://cdn.jsdelivr.net/npm/cesium/Build/Cesium/Widgets/widgets.css relstylesheet script srchttps://cdn.jsdelivr.net/npm/cesium/Build/Cesium/Cesium.js/script /head body div idcesiumContainer stylewidth:100vw;height:100vh;/div script Cesium.Ion.defaultAccessToken 换成你自己的token; const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: Cesium.createWorldTerrain(), infoBox: false, selectionIndicator: false, animation: false, timeline: false }); viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.92, 2500), orientation: { heading: 0, pitch: -Cesium.Math.PI_OVER_TWO * 0.8, roll: 0 } }); /script /body /html这段代码里terrainProvider: Cesium.createWorldTerrain()是让地球有真实起伏的关键。如果你没有Ion token可以用公司内部的CesiumTerrainProvider资源只要支持quantized-mesh编码就行。viewer.camera.setView把视角拉到北京某个区域便于后续观察开挖效果。infoBox: false这些选项不影响功能只是让界面干净一点。3.2 用ENU坐标系生成四个裁剪平面在同一个script标签里写一个makeGlobalPlane辅助函数把局部平面的法向量和距离转换到ECEF全局坐标。然后根据中心点、东西宽度、南北高度生成四个平面挂到viewer.scene.globe.clippingPlanes上。function makeGlobalPlane(transform, localNormal, localDistance) { const normal Cesium.Matrix4.multiplyByPointAsVector( transform, localNormal, new Cesium.Cartesian3() ); Cesium.Cartesian3.normalize(normal, normal); // 局部平面 n·p d 0取原点距离d处的一个点再变换到全局 const localPoint Cesium.Cartesian3.multiplyByScalar( localNormal, -localDistance, new Cesium.Cartesian3() ); const point Cesium.Matrix4.multiplyByPoint( transform, localPoint, new Cesium.Cartesian3() ); return new Cesium.ClippingPlane( normal, -Cesium.Cartesian3.dot(normal, point) ); } function createRectClipPlanes(longitude, latitude, width, height) { const center Cesium.Cartesian3.fromDegrees(longitude, latitude); const transform Cesium.Transforms.eastNorthUpToFixedFrame(center); const planes [ { n: new Cesium.Cartesian3(1, 0, 0), d: -width / 2 }, // 东边界 { n: new Cesium.Cartesian3(-1, 0, 0), d: -width / 2 }, // 西边界 { n: new Cesium.Cartesian3(0, 1, 0), d: -height / 2 }, // 北边界 { n: new Cesium.Cartesian3(0, -1, 0), d: -height / 2 } // 南边界 ]; return planes.map(p makeGlobalPlane(transform, p.n, p.d)); } const clipPlanes createRectClipPlanes(116.39, 39.92, 1500, 1000); viewer.scene.globe.clippingPlanes new Cesium.ClippingPlaneCollection({ planes: clipPlanes, unionClippingRegions: false, edgeColor: Cesium.Color.WHITE, edgeWidth: 3 });这段逻辑比较绕我解释一下。eastNorthUpToFixedFrame得到的变换矩阵把一个以center为原点的东-北-上局部坐标系映射到地心ECEF。四个局部平面里东边界的法向量是(1,0,0)distance是-width/2西边界的法向量是(-1,0,0)distance也是-width/2。这样得到的效果是东、西两个平面各自把矩形外部的一侧“剪掉”南北同理。四个平面的负半空间交集正好是矩形内部所以矩形内的地形就没了。参数说明longitude/latitude开挖矩形的中心点。width/height沿东西和南北方向的米数我这里用了1500米乘1000米你要挖多大改多少。unionClippingRegions: false这个必须保持false因为我们要的是四个平面裁剪区域的交集。edgeColor和edgeWidth控制开挖边界的颜色和像素方便调试。3.3 给开挖边缘加一点质感Hard/Soft裁剪调参Cesium对3D Tiles可以设置更复杂的ClippingPlaneCollection软裁剪但对globe地形裁剪最实用的是edgeColor、edgeWidth。如果觉得白色边线太硬可以把edgeColor设成Cesium.Color.YELLOW.withAlpha(0.5)并把edgeWidth降到1。注意Cesium的ClippingPlaneCollection里还有一个frustum参数影响裁剪平面在视锥内的计算范围一般不用动。如果你想做Soft柔和裁剪常见做法不是调ClippingPlaneCollection而是对地形表面叠加一个半透明WallGeometry。地形开挖这种“真挖走”的需求硬边缘反而真实。把edgeWidth设成4时边缘线会非常扎眼设成1又可能看不清调试阶段建议先设3确认范围后再调成想要的样子。4. 让开挖区域动起来交互绘制矩形、实时更新平面与地下场景叠加4.1 用ScreenSpaceEventHandler在地形上绘制开挖矩形手动写死坐标只能算示例。现实场景是用户要在地图上框一个范围然后开挖。Cesium没有内置的“拖动框选”控件但它的事件机制足够我们做一个简易版本先监听一次左键点击得到起点再监听第二次点击得到终点用这两个经纬度画一个矩形entity并把矩形实时同步到裁剪平面。const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); let startCarto null; handler.setInputAction(function (movement) { const cartesian viewer.camera.pickEllipsoid( movement.position, viewer.scene.globe.ellipsoid ); if (!cartesian) return; const carto Cesium.Cartographic.fromCartesian(cartesian); if (!startCarto) { startCarto carto; } else { const startLon Cesium.Math.toDegrees(startCarto.longitude); const startLat Cesium.Math.toDegrees(startCarto.latitude); const endLon Cesium.Math.toDegrees(carto.longitude); const endLat Cesium.Math.toDegrees(carto.latitude); drawRectangle(startLon, startLat, endLon, endLat); updateClipByBounds(startLon, startLat, endLon, endLat); startCarto null; } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);这里pickEllipsoid会把鼠标位置投射到椭圆体上拿到经纬度。drawRectangle用来在场景上画一个半透明的白色矩形方便用户看到自己选的区域。updateClipByBounds则是下一步要做的平面更新函数。实际项目里建议把第二次点击改成鼠标移动预览也就是按下鼠标后实时拖动矩形松开时确认开挖体验会好很多。这个拖动逻辑和Cesium里拖拽模型的MOUSE_MOVE事件是同一条思路区别只是pickEllipsoid换成pick模型节点。4.2 从两个对角点计算中心与尺寸更新四面裁剪平面知道了矩形的西南角和东北角先算中心经纬度再算东西、南北方向的水平距离。不要直接用经纬度差当米数地球曲率在纬度方向还好经度方向误差很大。我这里用Cartesian3.distance来算弧长更直观。function updateClipByBounds(west, south, east, north) { const centerLon (west east) / 2; const centerLat (south north) / 2; const center Cesium.Cartesian3.fromDegrees(centerLon, centerLat); const westPoint Cesium.Cartesian3.fromDegrees(west, centerLat); const eastPoint Cesium.Cartesian3.fromDegrees(east, centerLat); const southPoint Cesium.Cartesian3.fromDegrees(centerLon, south); const northPoint Cesium.Cartesian3.fromDegrees(centerLon, north); const width Cesium.Cartesian3.distance(westPoint, eastPoint); const height Cesium.Cartesian3.distance(southPoint, northPoint); const newPlanes createRectClipPlanes(centerLon, centerLat, width, height); const collection viewer.scene.globe.clippingPlanes; if (!collection) { viewer.scene.globe.clippingPlanes new Cesium.ClippingPlaneCollection({ planes: newPlanes, unionClippingRegions: false, edgeColor: Cesium.Color.WHITE, edgeWidth: 3 }); } else { // 比较稳妥的做法是清掉旧平面再重建 collection.removeAll(); newPlanes.forEach(plane collection.add(plane)); } }这里有两个细节。第一距离计算用东西和南北方向的中线算弧长虽然不完全精确但在地图交互的尺度下足够。第二removeAll再逐个add会比直接替换整个collection更稳因为旧collection里的unionClippingRegions、edgeColor这些属性不会因为集合数量变化而丢。如果你直接赋值一个新的collection会把原来的边颜色设置也冲掉。4.3 在开挖出来的坑底叠加OBJ模型与动态光照把“开挖”变成“地下空间”开挖本身只是一个视觉裁剪坑里并没有实体。要让地下场景成立需要在坑底位置放模型。Cesium加载OBJ模型一般要转成glTF/glb原生不支持直接OBJ。常见做法是用modelMatrix放到地表以下或者放在裁剪平面的下边界。const modelOrigin Cesium.Cartesian3.fromDegrees(centerLon, centerLat, -20); const modelMatrix Cesium.Transforms.eastNorthUpToFixedFrame(modelOrigin); viewer.entities.add({ name: 地下模型, model: { uri: 地下管线.glb, modelMatrix: modelMatrix, heightReference: Cesium.HeightReference.NONE } });这里的-20表示把模型放到地表下方20米配合地形裁剪后模型就能从坑里露出来。模型放置好后如果还想做动态光照可以用viewer.scene.light调制全局光也可以给模型挂Cesium.CustomShader做边缘光照。但很多项目其实只需要一盏“探照灯”用点光源模拟就够了。这类增强与地形开挖本身是两件事但项目里常被一起提起所以我把接线方式放在这开挖区域负责“露出来”模型负责“放进去”光源负责“看清楚”。5. 地形开挖排查5个实测案例从坐标翻车到边缘发虚5.1 开挖区域跑到了地球背面矩形中心点用错了现象我在北京设的矩形开出来的坑跑到印度洋去了。原因eastNorthUpToFixedFrame的center参数用的是(0,0)或矩形中心与裁剪平面中心不一致导致局部坐标原点不在目标区域。这个函数生成的是以center为原点的ENU矩阵如果center传错后面所有平面的位置、方向都会跟着错。解决确认createRectClipPlanes的longitude/latitude和矩形中心一致。交互框选时先算中心再调用别把起点当中心。调试时可以把中心经纬度打印到控制台哪怕只差0.001度肉眼也能看到坑的位置偏移。5.2 坑变成了山平面法向量方向反了现象裁剪后地表不是凹陷而是一个凸起的山包。原因四个平面的法向量方向决定保留区域。要挖矩形内部法向量必须朝外如果法向量朝内矩形内部被保留外部被裁掉看起来就是地形反向。解决把东边界法向量从(1,0,0)改成(-1,0,0)同时把distance符号一起调。快速验证法把edgeColor设为很亮的黄色看边缘线是在坑内还是坑外。如果边缘线包裹的是凸起区域说明平面方向反了。5.3 加载真地形后裁剪没生效黑屏一片现象页面能正常显示地球但设置clippingPlanes后地形没变化或者直接黑屏。原因最常见的是只设置了3D Tiles的clippingPlanes却忘记设置viewer.scene.globe.clippingPlanes另一种是地形Provider不是基于quantized-mesh的不支持裁剪。解决先确认用的Cesium.createWorldTerrain()或公司内部地形再在viewer.scene.globe.clippingPlanes上打断点看collection里planes的数量。如果平面数量为0说明你赋值给了别的对象。黑屏通常是把全部地形裁掉了检查法向量方向和unionClippingRegions。5.4 开挖边缘发虚、混色甚至看到锯齿现象边缘线颜色变淡放大后有锯齿。原因地形裁剪的边缘是网格级别的没有做过度绘制而且ClippingPlaneCollection的edgeColor带透明度时会和地表颜色混合。解决大多数项目里直接提高edgeWidth到3或4关闭颜色透明度配合viewer.resolutionScale 2或者开启fxaa。对于地形不要指望它有3D Tiles那种平滑的软裁剪效果。如果边缘线在移动视角时闪烁可以试试把viewer.scene.postProcessStages.fxaa.enabled设为true。5.5 多次更新平面后旧的裁剪效果残留现象连续框选两次第一次的坑还在第二次的坑叠加或者整个地形开始闪烁。原因globe.clippingPlanes被重复赋值且没有释放旧collection造成多套平面同时作用。解决维护一个全局clippingPlanesCollection变量需要变更时先collection.removeAll()再add并在页面销毁时调用collection.destroy()。如果项目里有多个Viewer要注意每个Viewer的clippingPlanes是独立的别共享同一个collection实例。这个问题最容易在快速切换开挖区域时出现也是一般实际项目中比较难察觉的隐患。6. 把开挖参数抽成JSON用沙盒脚本快速验证方向顺便解决“后悔药”问题6.1 配置驱动的开挖工具当你在一个项目里反复改开挖范围时最烦的不是写代码而是每次都要重新编译刷新页面。我的做法是把所有配置抽成一个JSON结构单独放在一个config.js里同时暴露一个applyClipPlanes(json)函数。沙盒里只要改JSON就能立刻看到效果不用动业务逻辑。const clipConfig { enabled: true, center: [116.39, 39.92], width: 1500, height: 1000, edgeColor: [1.0, 1.0, 0.0, 1.0], edgeWidth: 3, unionClippingRegions: false, target: globe }; function applyClipPlanes(config) { const planes createRectClipPlanes( config.center[0], config.center[1], config.width, config.height ); const options { planes: planes, unionClippingRegions: config.unionClippingRegions, edgeColor: Cesium.Color.fromCssColorString(yellow), edgeWidth: config.edgeWidth }; if (config.target globe) { viewer.scene.globe.clippingPlanes new Cesium.ClippingPlaneCollection(options); } else { tileset.clippingPlanes new Cesium.ClippingPlaneCollection(options); } }把enabled设为false时先执行viewer.scene.globe.clippingPlanes.destroy()再置undefined这就是我的“后悔药”想撤销开挖不用重刷页面改一下配置刷新就行。验证平面方向的时候我喜欢临时写一个小函数把所有平面以PointPrimitive的形式绘制在场景里。比如在平面中心放一个很小的立方体看它是不是在坑内。这样可以避免靠视觉猜方向。这套流程用了几次之后我的习惯是先画矩形再开挖先把edgeColor调成高亮黄色确认范围没问题再关掉调试色。方向和联合区域这种坑真的只有打印出来才敢放心。地形开挖不是一个特别玄学的功能但Cesium的坐标体系、裁剪平面方向和collection生命周期三件事叠加在一起容易让人怀疑人生。希望这篇能帮你在第一次跑通时少走点弯路。本文还有配套的精品资源点击获取
返回列表