ARTICLE DETAIL

资讯详情

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

Cesium地形开挖实战:裁剪平面原理、代码实现与避坑指南

Cesium地形开挖实战:裁剪平面原理、代码实现与避坑指南 简介面向Cesium初学者与前端开发者的地形开挖示例包通过单个HTML文件完整演示了基于Cesium的三维地形开挖核心实现。压缩包内仅含1个HTML文件大小仅1KB代码集中可直接在浏览器中运行适合作为入门模板二次开发。示例覆盖Cesium.Viewer初始化、开挖区域几何构建、Material材质配置以及GroundPrimitive地面图元添加等关键环节并涉及通过Rectangle与GeometryPipeline定义挖洞边界、利用Material自定义开挖区域外观等实用做法前端开发者可对照学习HTML、CSS与JavaScript在三维GIS场景中的应用也可基于此扩展鼠标事件以动态调整开挖位置与深度。示例将Cesium库的引入、页面容器定义与JavaScript逻辑组织得相当清晰几乎无需额外配置即可运行。目前已有1218人学习下载适合需要快速掌握Cesium地形开挖实现思路的开发者参考。1. 为什么「cesium地形开挖」让前端开发又爱又恨在三维 GIS 项目里把地形挖开一个坑是提气又熬人的需求。我最早在智慧矿山项目里碰到它要给采坑做进度动画让数字地球直接凹下去一块。第一版我用 entity 的 polygon 铺了一个黑色假坑镜头一低就直接穿帮。真正能用的 cesium地形开挖靠的是渲染管线里的裁剪平面把地形像素按平面方程一块一块丢弃只做视觉切削不改动底层高程数据。这篇笔记围绕「html 页面里做三维开发最常用的一个实例」展开目标读者是已经能加载 Cesium 地球、准备做开挖交互的前端开发工程师。下面从原理拆起把最小可运行代码、参数坑和工程化收尾一次讲清楚。2. 地形开挖的原理先立住裁剪平面、entity 与 primitive 的边界、两种开挖形态2.1 一个无限平面怎么把地形削出洞半空间与法向量Cesium 里做地形开挖的核心对象是ClippingPlane一个裁剪平面在数学上就是方程normal · p d 0。它把三维空间切成两个半空间法向量normal指向的一侧叫正侧另一侧叫负侧。渲染时如果一个片元落在多个裁剪平面的负侧GPU 就把这个片元丢弃discard于是地形上出现空洞。很多人第一次用会困惑一个平面是无限的怎么围出一个有限的坑答案是围合。用一圈平面每个平面的法向量都指向坑外。坑内部的点对每个平面来说都在负侧被丢弃坑外部的点至少对一个平面来说在正侧保留下来。这就是为什么凸多边形能挖出干净利落的坑而凹多边形会出问题——这一点后面专门讲。关键参数只有两个normal和d。normal是三维单位向量决定平面朝向d是平面到原点的偏移量单位是米。最容易翻车的点是d不是“深度”而是平面方程里的常数项。比如要让平面过某个点mid必须先用d -dot(normal, mid)反推出来。直接把d写成 0平面就会过地球原点在经纬度很大的区域里挖出个莫名其妙的斜洞看到效果时基本是一脸懵。2.2 entity 与 primitive 的区别为什么 polygon 铺不出真坑许多三维开发前端刚上手时会走同一条弯路在地形上画一个带低的 polygon以为这就是开挖。这背后其实是 Cesium 里 entity 与 primitive 的本质区别。entity 是面向业务封装的高层对象适合描述“场景里有什么”primitive 是底层渲染体直接驱动 GPU 绘制。地形本身是 globe 这一层 primitive 在渲染entity 的 polygon 只是贴在其上的一张“几何皮”它没有能力改变地形高程块的数据更不会让地球表面真的缺一块。所以真正的地形开挖必须把裁剪挂在 globe 上要裁的对象裁剪挂载点对地形是否生效全球地形globeviewer.scene.globe.clippingPlanes是3D Tiles 倾斜摄影tileset.clippingPlanes或tileset.clippingPolygons否glTF/GLB 模型modelPrimitive.clippingPlanes否entity 多边形viewer.scene.clippingPlanes只裁实体不裁地形如果你在代码里把平面挂到了tileset.clippingPlanes上倾斜摄影会切得整整齐齐地形却纹丝不动。这是三个独立系统各挂各的别指望一个数组统一生效。顺带说一句globe.clippingPlanes用的是无限平面集合而 Cesium 1.89 之后的 3D Tiles 还支持ClippingPolygonCollection可以直接用任意凹多边形裁剪倾斜摄影。但注意它只对 3D Tiles 生效对 globe 地形无效。所以「cesium地形开挖」如果目标是原始地形方案仍然是平面围合这一套。2.3 垂直开挖与放坡开挖两个形态对应一套法向量开挖形态主要分两种但代码差异只是法向量的一个分量。垂直开挖是默认形态。每个侧面的法向量保持水平方向指向多边形外部。这样挖出来的是竖直侧壁坑顶和坑底形状完全一致。适合基坑、隧道出入口这类要求侧壁垂直的场景。放坡开挖在工程里更常见为了防止土体塌方侧壁要有一个坡度。实现方法非常简单把每个侧面的法向量从水平方向“抬”起来加上一个垂直分量。法向量变成(flat.x, flat.y, slopeFactor)再归一化。其中slopeFactor的换算关系是slopeFactor 垂直变化 / 水平外扩。比如你要做一个高度 1 米、水平外扩 0.5 米的边坡也就是 1:0.5 的放坡系数那么slopeFactor 1 / 0.5 2。还有一个容易忽略的地方垂直开挖到底需要几个平面经验是只需要侧壁一圈不需要底面。侧壁平面围合出来的柱体向地下无限延伸视觉上是个无底洞坑底的“深度”由我们额外铺的一块底板实体来表现。这样视觉效果正确而且代码简单。如果非要加一个水平底面裁剪平面整个坑底以下的地球都会被削掉从侧面看场景直接裂开属于典型翻车。3. 用 HTML 在本地跑通第一个 Cesium 地形开挖实例选点、画范围、调深度这一章给出一个完整的 HTML 页面。我一般把 Cesium 在本地自托管或者直接用 CDN 引固定版本。版本建议锁定在 1.104 或更新的 LTS 附近太老的版本 API 差异比较大照着抄容易卡在莫名其妙的地方。代码里的 CDN 地址换成你自己项目里的相对路径也可以。3.1 最小页面骨架初始化 Viewer 与交互工具栏先做一个全屏三维页面加上四个按钮画范围、执行开挖、重置、以及一个深度输入框。!DOCTYPE html html langzh-cn head meta charsetutf-8 titleCesium 地形开挖实例/title link hrefhttps://cdn.jsdelivr.net/npm/cesium1.104/Build/Cesium/Widgets/widgets.css relstylesheet style html, body { margin: 0; height: 100%; } #cesiumContainer { width: 100%; height: 100%; } #toolbar { position: absolute; top: 16px; left: 16px; z-index: 10; background: rgba(255, 255, 255, 0.92); padding: 10px 14px; border-radius: 8px; font-size: 14px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } #toolbar button { margin-right: 6px; } /style /head body div idcesiumContainer/div div idtoolbar button idbtnDraw画开挖范围/button button idbtnExcavate执行开挖/button button idbtnReset重置/button label深度(m) input iddepthInput typenumber value60 stylewidth:70px/label /div script srchttps://cdn.jsdelivr.net/npm/cesium1.104/Build/Cesium/Cesium.js/script script // 默认 token 换成你自己在 cesium ion 申请的不换会在地球上方弹水印 Cesium.Ion.defaultAccessToken 换成你的 ion token; const viewer new Cesium.Viewer(cesiumContainer, { terrainProvider: Cesium.createWorldTerrainAsync(), // 如果是旧版本改回 createWorldTerrain() animation: false, timeline: false, infoBox: false, selectionIndicator: false, baseLayerPicker: false }); // 关掉地形深度测试否则坑底贴板会被地形挡住看不到 viewer.scene.globe.depthTestAgainstTerrain false; viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(116.39, 39.90, 1200), orientation: { heading: 0, pitch: -Cesium.Math.PI_OVER_TWO, roll: 0 } }); /script /body /html这段代码有两个参数值得说明。depthTestAgainstTerrain默认是 false但如果你之前在别的项目里把它设成了 true坑底部分会被地形遮挡看起来像挖了个黑洞。createWorldTerrainAsync是 1.104 之后的异步版本旧版本用同步的createWorldTerrain()如果发现地形加载不出来先检查这里。3.2 在地形上点选多边形生成开挖范围的坐标集合绘制矩形或任意多边形的逻辑是一样的监听鼠标左键用pickEllipsoid把屏幕坐标转换到椭球面上的三维坐标存进数组。双击结束绘制顺便生成一个半透明预览面。let drawing false; let picked []; const drawHandler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); drawHandler.setInputAction(function (movement) { if (!drawing) return; const cartesian viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid); if (!Cesium.defined(cartesian)) return; picked.push(cartesian); viewer.entities.add({ position: cartesian, point: { pixelSize: 8, color: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2 } }); }, Cesium.ScreenSpaceEventType.LEFT_CLICK); drawHandler.setInputAction(function () { if (!drawing || picked.length 3) return; drawing false; picked.pop(); // 双击会额外触发一次单击导致末尾多一个重复点先弹掉 viewer.entities.add({ polygon: { hierarchy: picked.slice(), material: Cesium.Color.RED.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.YELLOW } }); }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); document.getElementById(btnDraw).addEventListener(click, function () { drawing true; picked []; viewer.entities.removeAll(); // 清掉上一轮的点位和预览面 });pickEllipsoid返回的是 WGS84 椭球面上的Cartesian3坐标已经带高程不需要再手动转换。注意双击结束的时机双击本身会先触发一两次单击事件所以数组末尾会混进去一个重复点picked.pop()是必须的否则最后一条边会缩成一个零长度边生成的裁剪平面会出现异常。3.3 执行开挖的核心函数构建侧面裁剪平面并挂到 globe选完点、填好深度之后核心函数就是把多边形每一条边生成一个向外指的裁剪平面再整体挂到viewer.scene.globe.clippingPlanes上。function buildSidePlanes(positions, slopeFactor) { // 用包围球算几何中心作为“向内/向外”的参考点 const center Cesium.BoundingSphere.fromPoints(positions).center; const n positions.length; const planes []; for (let i 0; i n; i) { const p1 positions[i]; const p2 positions[(i 1) % n]; const mid Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3()); // 从中心指向边中点的向量去掉 Z 分量后归一化得到水平外法向 const outward Cesium.Cartesian3.subtract(mid, center, new Cesium.Cartesian3()); const flat new Cesium.Cartesian3(outward.x, outward.y, 0); Cesium.Cartesian3.normalize(flat, flat); // slopeFactor 为 0 是垂直开挖大于 0 是放坡法向量向上抬 const normal new Cesium.Cartesian3(flat.x, flat.y, slopeFactor || 0); Cesium.Cartesian3.normalize(normal, normal); // 平面过 mid 点d 要反推不能直接写 0 const d -Cesium.Cartesian3.dot(normal, mid); planes.push(new Cesium.ClippingPlane(normal, d)); } return planes; } document.getElementById(btnExcavate).addEventListener(click, function () { if (picked.length 3) return; const depth parseFloat(document.getElementById(depthInput).value) || 60; const sidePlanes buildSidePlanes(picked, 0); viewer.scene.globe.clippingPlanes new Cesium.ClippingPlanesCollection({ planes: sidePlanes, edgeColor: Cesium.Color.WHITE, edgeWidth: 2 }); // 计算多边形平均高程作为坑底高度的基准 const cartos picked.map(p Cesium.Cartographic.fromCartesian(p)); const avgHeight cartos.reduce((sum, c) sum c.height, 0) / cartos.length; const bottomHeight avgHeight - depth; const flatDegrees []; cartos.forEach(c { flatDegrees.push(Cesium.Math.toDegrees(c.longitude), Cesium.Math.toDegrees(c.latitude)); }); viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray(flatDegrees), height: bottomHeight, material: Cesium.Color.SANDY_BROWN.withAlpha(0.95), outline: true, outlineColor: Cesium.Color.WHITE } }); });这里有几个参数不能改错。ClippingPlanesCollection的planes是完整数组每次执行开挖我都重新建一个新的 collection 赋给globe.clippingPlanes而不是去改旧 collection 里的元素省得增删平面时触发一堆状态同步问题。edgeColor和edgeWidth是 1.104 版本支持的裁剪边缘描边效果旧版本不报错但不渲染不影响开挖本身。bottomHeight用的是“平均高程减去深度”。严格来说如果地形起伏大坑底应该是一个随地形起伏的曲面这里用平均值会造成底板在某些位置露出地表或悬空。对于多数演示场景这个简化够用如果要精确需要用开挖区域内的 DEM 数据做空间插值计算每个顶点的真实坑底高程。3.4 坑底铺板与放坡开挖两处小改动让效果像施工图执行开挖后坑实际上是一个向地心延伸的无底洞我们在视觉上用一个底板把它封住。底板的高度就是刚才算的bottomHeight颜色用土黄色更像施工图。这一步在上一节代码里已经完成但有两个细节值得展开。第一底板实体本身不在 globe 的裁剪范围内。globe.clippingPlanes只管地形不管 entity所以底板不会被侧壁平面二次裁掉这是巧合同时也是设计不需要额外处理。第二如果要把垂直开挖变成放坡开挖只需要把buildSidePlanes的第三个参数从 0 改成一个正数。例如要做 1:1 的放坡也就是下降 1 米外扩 1 米slopeFactor就等于 1// 1:1 放坡开挖侧壁与水平面夹角约 45 度 const sidePlanes buildSidePlanes(picked, 1.0);注意slopeFactor越大坡面越陡还是越缓法向量从水平向上抬坡面与水平面的夹角是atan(slopeFactor)。slopeFactor 0时坡角 0 度是垂直侧壁slopeFactor 1时坡角 45 度slopeFactor 2时约 63 度。工程上写放坡系数是 1:m即高度 1 米对应水平 m 米换算过来slopeFactor 1 / m。这个换算关系建议直接写在代码注释里项目验收时经常要调。4. 地形开挖最常见的 5 个翻车现场现象、原因和解决4.1 地形没反应倾斜摄影却被挖了 —— 裁剪对象挂错了现象写了tileset.clippingPlanes new Cesium.ClippingPlanesCollection(...)然后发现加载出来的倾斜摄影模型被切开了但底下的地形完全没变化。原因地形、3D Tiles、模型这三者的裁剪系统是独立的。tileset.clippingPlanes只管当前这个瓦片集剪不到 globe。反过来如果你目标只是地形挂到tileset上就是白写。解决目标对象是数字地球地形就用viewer.scene.globe.clippingPlanes目标是倾斜摄影用tileset.clippingPlanes。如果两个都要挖两处都要各挂一份平面数组。另外注意早期版本的 Cesium 要求手动设置clippingPlanes.modelsLength才能让模型裁剪生效新版已经自动管理如果你还在用很老的版本裁剪不生效时去中文文档里查一下这个参数会少走弯路。4.2 挖出来的不是坑而是柱子 —— 法向量方向反了现象执行开挖后多边形内部的区域保留成一个凸起的高台周围地形全部消失了像一根柱子立在地球上。原因平面法向量方向反了。法向量指向“保留侧”我们想让坑内被裁掉、坑外保留所以每个侧面的法向量必须指向坑外。计算几何中心后用mid - center得到的向量朝外但如果代码里写成了center - mid方向就反了结果是对称翻车坑外被裁坑内保留。解决把buildSidePlanes里Cesium.Cartesian3.subtract(mid, center, ...)改成subtract(center, mid, ...)。调试时有个土办法临时把depth调到 10000如果看到被切掉的是周围大片区域说明方向肯定反了。4.3 凹多边形开挖后形状不对 —— 平面裁剪本质是凸半空间求交现象选择 L 形、U 形等凹多边形作为开挖范围执行后挖出来的形状跟预期的凹轮廓完全对不上有的地方该挖的没挖有的地方外面的地形缺了一块。原因裁剪平面围合的本质是求一组半空间的交集而有限个半空间的交集一定是凸区域。凹多边形的凹角会让“坑内”和“坑外”的判定失控外部某些点反而同时落在所有平面的负侧于是被误删。解决要么把凹多边形近似成凸包接受多挖一部分要么改用tileset.clippingPolygons这个 API 直接支持凹多边形裁剪但它只对 3D Tiles 生效对 globe 地形无效。实际项目里如果地形开挖范围确实是凹的我一般建议先用turf.convex求凸包再生成平面如果产品经理不接受多挖就把原始凹多边形切分成多个凸多边形每个凸区域单独做一次开挖渲染但这在同一个 globe 上实现成本很高。4.4 坑底和侧壁发黑发紫 —— 深度测试与底板的配合现象开挖后坑的侧面出现大片黑色区域底板颜色发紫或者闪动转动视角时尤其明显。原因两个因素叠加。第一如果viewer.scene.globe.depthTestAgainstTerrain true地形之外的片段在深度测试中会被丢弃坑底实体在地形高程以下于是整个底板被遮挡露出背后的天黑色。第二底板高度取了平均高程在地形高处可能露出地表、在低处又陷入山体产生紫黑穿插。解决初始化时显式设置viewer.scene.globe.depthTestAgainstTerrain false。同时把底板height的设置改成“区域最高点高程减去深度”保证底板绝不会露出地表如果底部跟山体重叠的瑕疵仍然明显给底板再叠加一层半透明材质或者把底板向下多压几米用厚度掩盖缝隙。4.5 选点过程反复翻车 —— 双击多加点、镜头漂移和状态清理现象双击结束选点时多边形末尾多了一个折点选点过程中拖动鼠标地球跟着旋转坐标点落到意想不到的位置第二次画范围时旧的点位和预览面还留在场景里。原因双击会先触发单击事件绘制状态下没有禁用相机控制重复点击“画开挖范围”没有清理上轮实体。解决三行代码的事。双击回调里picked.pop()去掉重复点进入绘制模式时执行viewer.screenSpaceCameraController.enableInputs false双击结束后恢复true点击“画开挖范围”按钮时先viewer.entities.removeAll()。这三个坑我几乎每次演示都会踩一遍现在固定写成模板不再靠临场反应。提示如果发现选点时的坐标和实际地表位置有偏差检查一下pickEllipsoid的时机。相机还在飞行动画中时点选椭球交点会漂移。稳妥做法是先等viewer.camera.flyTo的 promise 结束再开启绘制。5. 把开挖做成可复用的渲染函数参数化配置、多坑取舍与验收口径做到这一步一个可以交互的 cesium地形开挖实例已经能跑了。但项目里不能每次都用全局变量和按钮事件堆逻辑我习惯把整套流程收敛成一个参数化函数方便真正的前端开发同学集成进业务框架。const pitConfig { depth: 60, slopeFactor: 0, region: [ [116.39, 39.90], [116.40, 39.90], [116.40, 39.91], [116.39, 39.91] ] }; function renderExcavation(cfg) { const positions cfg.region.map(function (item) { return Cesium.Cartesian3.fromDegrees(item[0], item[1]); }); const planes buildSidePlanes(positions, cfg.slopeFactor || 0); viewer.scene.globe.clippingPlanes new Cesium.ClippingPlanesCollection({ planes: planes }); // 坑底逻辑复用前面的平均高程减深度方案 }region直接放经纬度数组比在页面上手动画范围更适合对接后端接口。后端从施工图纸读出设计坐标前端拿到配置一调用开挖效果立刻出现。接入了前端 agent 或本地 MCP 工具的项目还能把renderExcavation暴露成可调用函数通过自然语言改深度和坡度再调起省掉一版又一版的沟通成本。多区域开挖要提前想清楚取舍。一个globe只有一个clippingPlanes属性多个坑要在一个场景里同时存在不能简单地把两组平面塞进同一个 collection因为平面裁剪是交集语义两组平面会互相干扰挖出错误形状。实际项目我见到的做法是优先保障单个关键坑位精确其余区域用倾斜摄影的clippingPolygons覆盖或者等后端把多个坑合并成一个 GeoJSON 区域再统一处理。验收时有一个容易忽略的口径globe.clippingPlanes只是渲染层裁剪不会修改地形数据。viewer.scene.globe.getHeight(cartographic)在开挖后拿到的仍然是原始地表高程拿它验证坑深会得到错误结论。正确做法是直接读底板实体 polygon 的height和原始平均高程的差值。这句话我每次交底都会跟测试强调一遍免得验收单上多一条假 bug。这套方案到现在我还在持续改。前阵子接手一个新项目对方要求把采坑周围做 1:0.75 的放坡我按slopeFactor 1 / 0.75换算完五分钟后效果就对了。倒是那些藏在法向量和depthTestAgainstTerrain里的小坑几乎每个月都会在某个同事的电脑上重新发生一次。希望这次梳理能帮你把 Cesium 地形开挖这条路上的雷提前拆干净也希望后面接手你代码的兄弟少掉几根头发。本文还有配套的精品资源点击获取
返回列表