资讯详情

第67篇:空间分析-填挖方分析——网格一粗,填挖分界线就被咬成了锯齿

📅 2026/10/20 2:15:48 | 华诺云谱 👁 阅读
第67篇:空间分析-填挖方分析——网格一粗,填挖分界线就被咬成了锯齿
上篇结尾我留了这么一句话“下一篇我们先搁下地图控件这口气去算一件要真动手的事——在一片地形上挖掉多少、填上多少。空间分析-填挖方分析我们下一篇见。”前面几篇我们一直在做看得见的东西遮罩、卷帘、比例尺本质上都是把引擎已有的能力搬到一个能抓的地方。从今天起换一条线——空间分析这一回不是把数据画出来而是从数据里算出一个数而且这个数要能拿去报价、拿去施工。填挖方cut fill是这类问题里最经典的一个给你一片地形和一个设计标高问要挖掉多少立方米、要填上多少立方米。听起来简单可真动手你会发现每一步都有一个可以悄悄算错的地方。大家好我是 Cesium 酱也可以叫我本猿一名在 WebGIS 领域摸爬滚打多年的前端开发者。 本文涉及的全部代码均已开源完整的仓库地址见文末可自行取用、随意魔改。一、场景与痛点一片地上到底要动多少土 背景现状任何一块场地动工前甲方和施工方都要先对一件事达成共识这片地要挖掉多少、填上多少。这个数字直接决定土方外运的车次、买土的成本、工期甚至决定这块地到底值不值得拿。传统做法是测量员跑现场、打方格网、用水准仪读高程回来在 CAD 里算。WebGIS 把它搬到浏览器里靠的是三样现成的东西一个多边形区域 一份地形高程 一个设计标高。常见的三种偷懒做法拿整个区域的平均高差乘面积——V S × Δh一秒钟出结果也一秒钟就错地形起伏被抹平了挖和填会互相抵消只算挖不算填或反过来—— 报出来的数永远只有一半用格网法但格子粗到离谱—— 一个 100 m 的场地只切 4 格填挖分界线根本对不上。⚠️ 难点到底在哪难在填和挖的分界不是画出来的是算出来的它是地形面 设计面那条等高线。这条线落在哪儿决定了哪部分算挖、哪部分算填。而计算机只能按离散的采样点去逼近它——于是问题就从算体积变成了你的网格有多细。二、原理拆解把地形切成三棱柱再一个一个加起来2.1 一句话的原理把多边形切成三角网每个三角形配一个三棱柱柱底是设计面、柱顶是地形面柱子的体积就是这一块的填方或挖方。这就是三棱柱法也叫 TIN 体积法。它比方格网法更贴地形因为三角形的三个顶点直接踩在采样点上顶面是一片斜平面而不是一格取一个代表高度。2.2 三角网从哪来PolygonGeometry与 granularity采样网格不是我们自己写的是问 Cesium 要的constgranularityMath.PI/Math.pow(2,11)/precision.valueconstgeometryPolygonGeometry.fromPositions({positions,vertexFormat:PerInstanceColorAppearance.FLAT_VERTEX_FORMAT,granularity})constgeomPolygonGeometry.createGeometry(geometry)granularity是 Cesium 里多边形细分的角度粒度单位弧度。它默认取CesiumMath.RADIANS_PER_DEGREE也就是1°——对洲际尺度的多边形刚好对我们这种几百米的场地就等于不切。本例的演示区域是写死在代码里的 4 个顶点本猿把它们换算回经纬度项实测值经度范围117.170153°E ~ 117.170879°E纬度范围34.257988°N ~ 34.258899°N顶点椭球高4.447 ~ 4.449 m周长328.76 m面积6480.2 ㎡三角化后 6464.2 ㎡把四档精度各跑一遍网格是这样长的采样精度granularity顶点三角形平均三角形面积平均边长256默认5.9921e-6 rad1924269.34 ㎡≈16.41 m5122.9961e-6 rad619667.34 ㎡≈8.21 m10241.4980e-6 rad21738416.83 ㎡≈4.10 m20487.4901e-7 rad81715364.21 ㎡≈2.05 m对照默认 1°0.01745 rad—2——三角形数是按精度的平方涨的24 → 96 → 384 → 1536每档整 4 倍因为 granularity 跟1 / precision成正比而细分是二维的。这里有个值得单独说的认知采样精度 256听起来很高实际只切出 24 个三角形。那个 256 不是切 256 份它是分母上的一个系数。要判断网格够不够细只有平均三角形面积或平均边长这两个数说了算——本例默认档是16.4 m 一格量一个 60×100 m 的场地只有 24 块。2.3 高度从哪来两条完全不同的路网格建好之后要逐顶点取高程代码里有个很扎眼的判断constterrainProviderviewer.terrainProviderasunknownas{_layers?:unknown[]}|undefinedif(terrainProvider?._layers){constsampledawaitsampleTerrainMostDetailed(viewer.terrainProvider,cartos)heightssampled.map((c)c.height)}else{heightscartos.map((c)viewer!.scene.globe.getHeight(c)??0)}_layers是CesiumTerrainProvider的私有字段CesiumTerrainProvider.jsL110provider._layers this.layers。这里靠嗅探私有字段来判断是不是量化网格地形能跑但很脆换一个没这个字段的地形源就悄悄走另一条路。有_layers→sampleTerrainMostDetailed联网去取最高精度的高程代价是异步、要等、可能失败。没有 →globe.getHeight(c)读当前已经加载进内存的瓦片的高程瓦片没加载就返回undefined再被?? 0兜成0 米。两条路的精度差着好几个数量级而切换是静默的。这一点在第五节会变成一个真问题。2.4 体积怎么算三顶点均值的那一次精确constmean(hahbhc)/3if(meandesign){fillAreaarea;fillVolumearea*(design-mean)}elseif(meandesign){cutAreaarea;cutVolumearea*(mean-design)}else{noAreaarea}mean (ha hb hc) / 3这一步是精确的不是近似。三个顶点撑起一个平面平面在三角形重心处的高度恰好等于三个顶点高度的算术平均而斜顶棱柱的体积恰好等于底面积乘重心高。本猿用一个纯平面地形顶面 一片斜平面验证了一遍项实测值三棱柱累加172020.162631 m³解析真值172020.162631 m³相对误差0.000e0所以三棱柱法本身没问题误差全在别的地方。2.5 反直觉填挖分界线是被咬出来的注意那三个分支——一个三角形只看均值要么整块算填、要么整块算挖。可真实世界里一个三角形常常一半在填区、一半在挖区。于是那条地形 设计面的等高线被强行吸附到了三角形的边上网格越粗锯齿越大。本猿用一片合成地形斜坡 正弦脊做了一组对照真值用 1200×1200 的细网格积出来采样精度三角形平均边长挖方体积偏差填方体积偏差2562416.41 m−2.59%−2.59%512968.21 m−0.96%−0.96%10243844.10 m−0.25%−0.25%204815362.05 m−0.08%−0.08%网格每加密一倍误差大约降到原来的三分之一——这是一阶收敛因为误差主要来自那条边界带面积 ≈ 边界长度 × 格子边长。换句话说填挖方量的精度是被分界线有多长和格子有多大这两个数锁死的跟总面积多大反而关系不大。顺带说一句那个else只有mean design才会走它。高程是浮点数精确相等几乎不可能发生——所以面板上的无须填挖面积平时就是 0它真正有意义的场景见第五节。三、代码实战从多边形到六个数字3.1 主流程asyncfunctionrunAnalysis():Promisevoid{constpositionsmanualPositions.length3?[...manualPositions]:ORIGINAL_POSITIONSisAnalyzing.valuetruestatusMessage.valueawaitnewPromise((resolve)setTimeout(resolve,50))// 让出一帧把分析中…画出去try{// ① 切网格 ② 取高程 ③ 定设计标高 ④ 逐三角形累加}catch(error){statusMessage.valueerrorinstanceofError?error.message:String(error)}finally{isAnalyzing.valuefalse}}那句await new Promise(r setTimeout(r, 50))值得单独说它不是在等什么数据而是故意让出 50 毫秒好让isAnalyzing变成true之后能被 Vue 渲染成分析中…。不加这一句同步的密集计算会把状态变更一起憋住用户点下去毫无反应。这是所有同步重活都要补的一脚刹车。3.2 设计标高填 0 的时候发生了什么constmeanHeightheights.reduce((sum,h)sumh,0)/heights.lengthconstdesigndesignHeight.value0?designHeight.value:meanHeight effectiveDesignHeight.valueNumber(design.toFixed(2))面板上写着填 0 时自动采用采样区域的平均地形高度这看起来是个贴心功能平均高度就是填挖平衡的那个高度挖出来的正好填回去净方为零。但注意它是顶点高度的算术平均——顶点在三角网里被重复计数内部顶点被 6 个三角形共用边界顶点只被 2~3 个用。所以它不是面积加权平均也就不是真正的平衡高度。本猿用合成地形实测precision 1024设计标高取法数值挖方填方净方顶点算术平均demo6.1006 m9966.7 m³10132.3 m³−165.7 m³面积加权平均真平衡6.0750 m10049.7 m³10049.7 m³0.0 m³两者只差0.0256 m却留下了165.7 m³ 的净方约占单侧方量的 1.65%。工地上的土方是按车算钱的1.65% 不是小数。另外那个designHeight.value 0也值得留意填 0 会被当成没填所以你没法把设计标高设成 0 米想设成 0请用 0.01。负数同理被忽略。3.3 体积怎么变成看得见的三棱柱constinstancesselected.map((solid)newGeometryInstance({geometry:newPolygonGeometry({polygonHierarchy:newPolygonHierarchy(solid.top),perPositionHeight:true,extrudedHeight:effectiveDesignHeight.value,vertexFormat:PerInstanceColorAppearance.VERTEX_FORMAT}),attributes:{color:ColorGeometryInstanceAttribute.fromColor(colorForType(solid.type))}}))volumePrimitiveviewer.scene.primitives.add(newPrimitive({geometryInstances:instances,appearance:newPerInstanceColorAppearance({flat:true,translucent:true})}))perPositionHeight: trueextrudedHeight顶面按三个顶点的真实高程起伏底面统一压在设计标高上——正好就是那个三棱柱。挖方是橙色#ff8c2e、填方是绿色#37c870、无须填挖是灰色#8a9bb0。一个Primitive装下全部实例靠PerInstanceColorAppearance逐实例上色这一套和 #59~#61 那几篇的批量实例是同一条路。实测单个三棱柱18 个顶点 / 24 条索引 / 8 个三角面。于是 2048 档一次要建1536 个实例、27648 个顶点顶点 索引约1.09 MB。⚠️ 这里有个小浪费外观写的是flat: true而PerInstanceColorAppearance在flat: true时自己的顶点格式就是FLAT_VERTEX_FORMAT只要 positionPerInstanceColorAppearance.jsL82-84。可上面给几何传的是VERTEX_FORMATposition normal36 字节/顶点——法线算了、传了、着色器一眼都不看。2048 档下这是27648 × 12 331,776 字节约 0.32 MB 的死重量。有意思的是同一份代码里采样网格用的FLAT_VERTEX_FORMAT是对的三棱柱这一处用错了。3.4 交互三次点击定一个区域handler.setInputAction((event)onLeftClick(event),ScreenSpaceEventType.LEFT_CLICK)handler.setInputAction((event)onMouseMove(event),ScreenSpaceEventType.MOUSE_MOVE)handler.setInputAction(()finishDrawing(),ScreenSpaceEventType.RIGHT_CLICK)handler.setInputAction(()finishDrawing(),ScreenSpaceEventType.LEFT_DOUBLE_CLICK)左键一个点一个点地采右键或双击闭合。采点用的是pickPositionmeasure-lib/pick.ts它有个很实用的三级回退scene.pickPosition深度拾取最准→globe.pick地形射线→camera.pickEllipsoid椭球解析。深度拾取要 GPU 支持scene.pickPositionSupported不支持时自动降级——这套三级回退值得抄到任何需要点哪落哪的功能里。预览也有讲究 3个点时画半透明多边形不足 3 个点时退化成clampToGround的折线两种状态切换得很自然。四、运行效果初始视角是这么设的viewer.camera.setView({destination:ORIGINAL_POSITIONS[0].clone()})只给了 destination没给 orientation——按Camera.jsL1528-1530默认姿态是heading 0, pitch -π/2, roll 0也就是镜头笔直朝下。而destination是相机自己的位置不是看向的目标Camera.jsL1493-1501只有传Rectangle才会被当成看向的范围。于是相机被放在第 1 个顶点上椭球高只有4.447 m正对着脚下的地皮俯视。这里本猿要提醒一句这个视角相当危险——相机离椭球面才 4.4 米只要当地地形高于这个数绝大多数地方都会相机一上来就在地面以下。想要一个能看的初始视角请给orientation或者干脆用viewer.zoomTo(entity)/ 一个Rectangle。面板上那六个数字是这么来的面板项含义累加方式总分析面积cut fill no逐三角形面积求和填方面积 / 填方体积地形低于设计面area、area × (design − mean)挖方面积 / 挖方体积地形高于设计面area、area × (mean − design)无须填挖面积恰好相等area平时为 0五、避坑与优化5.1 精度不是越高越好但默认档一定不够三档数字摆一起看采样精度三角形体积误差可视化顶点缓冲256默认24−2.59%4320.02 MB51296−0.96%17280.07 MB1024384−0.25%69120.27 MB20481536−0.08%276481.09 MB工程上一般要求土方量误差≤ 3%所以 256 档在合规线上勉强够用但你要是拿它去对账最好先说明分界线被切成了 16 米一粒的锯齿。真要较真1024 档0.25%是个不错的平衡点。别把精度当成一个可以无脑拉满的旋钮——它是平方级的代价。5.2 采样失败时界面上什么都看不出来这是本篇最该记住的一条。两条高程来源各有各的静默失败①sampleTerrainMostDetailed失败 →undefined它的第三个参数rejectOnTileFail默认falsesampleTerrainMostDetailed.jsL42-44也就是瓦片请求失败时不抛错只把 height 留成undefined。接着mean (hahbhc)/3变成NaN而NaN design与NaN design都是 false——于是这个三角形走进else分支被记成**“无须填挖面积”**。本猿模拟了一遍全部高程 undefined挖方 0 ㎡、填方 0 ㎡、“无须填挖” 6464.2 ㎡占总面积 100%体积显示 NaN。界面上没有任何报错只是多了一个看起来很合理的无须填挖面积。顺带一提它还会在terrainProvider.availability未定义时直接 reject源码 L57-61——那种情况反而好会被catch住显示出来。②globe.getHeight未加载 →?? 0这条路更狠瓦片还没加载完时返回undefined被?? 0兜成0 米。于是没有数据变成了这里是海平面如果该地实际高程是 300 米每个三角形都会多算 300 米的挖方。修法把有没有拿到高程当成一等公民。constsampledawaitsampleTerrainMostDetailed(viewer.terrainProvider,cartos,true)// 失败就抛// 回退分支里也别用 ?? 0consthviewer.scene.globe.getHeight(c)if(hundefined){missCount;continue}至少要做到统计缺失点数超过阈值就别出结果或者先await viewer.scene.globe.tilesLoaded/ 等地形加载完再放行本例的loadWorldTerrain已经有 await但用户在加载完成前点按钮仍会踩到。5.3 设计标高要么让用户输入要么用面积加权平均2.2 节那 0.0256 m 的差别值 165.7 m³。改法很简单——加权时用面积而不是顶点letwSum0,aSum0for(consttoftris){constAarea(t);constmmean(t);wSumA*m;aSumA}constdesignaSum0?wSum/aSum:meanHeight// 这才是真正的平衡标高顺便把designHeight.value 0的判断改成是否填了值比如用??或加一个useAutoHeight开关否则用户永远设不了 0。5.4 顺手能做的几件小事vertexFormat改回FLAT_VERTEX_FORMAT外观已经是flat: true省掉 0.32 MB 的死法线。别嗅探_layers改用typeof (viewer.terrainProvider as any).sampleTerrain function或者直接判断availability是否存在私有字段随时会变。PerInstanceColorAppearance的translucent默认就是 true半透明体之间不写深度、也不排序密密麻麻的柱体会互相穿。想要扎实的效果可以把填挖体分开两个不透明 Primitive按类型筛选已经有了只差一个translucent: false。单位与量纲要写清楚面板写的是 ㎡ 和 m³输入的设计标高是米跨单位项目英尺/码请在进口处统一换算别在体积公式里改。大面积区域要分块本例只有 6480 ㎡真来一块几平方公里的场地网格顶点数会平方级爆炸参考 #59~#62 那几篇的规模算账记得做分块 进度条。5.5 上线前 checklist✅ 网格密度用平均三角形面积 / 平均边长说话别信精度 256这种数字✅ 高程采样失败要显式报错不要让undefined流进mean也不要用?? 0兜底✅ 自动设计标高用面积加权平均否则填挖平衡只是看起来平衡✅ 同步重活前让出一帧setTimeout 50否则状态变更渲染不出来✅ 外观flat: true时几何就用FLAT_VERTEX_FORMAT✅ 别依赖私有字段_layers做分支⚠️ 记得在结果旁写一句填挖分界线被网格咬成了锯齿精度档位是多少六、写在最后Cesium 是头优雅的大象。我们曾经以为算体积的难点是体积公式今天才发现公式本身精确到了小数点最后一位——难的是承认那些没量到的地方分界线被咬成了锯齿没取到的高程变成了 0 米而自动平衡其实差了 165 立方。这一篇最让本猿坐不住的是同一个算法里的两种精确第一(ha hb hc) / 3是精确的。三棱柱体积公式对斜顶面一点误差都没有实测相对误差 0.000e0。数学这一半干净利落。第二这块算填还是算挖是糙的。一个三角形只凭均值二选一于是那条等高线被强行掰到网格线上。精度 256 档下体积差2.59%——误差不来自公式来自离散化。第三最危险的不是算错是算错了还长得对。采样失败时undefined静默变成无须填挖面积未加载的瓦片静默变成 0 米高程两种情况下界面都端出一个完整、自洽、可以用来汇报的数字。一个会报错的错误只是麻烦一个不报错的错误才是事故。这个案例还不完美初始视角贴在地皮上、设计标高填不了 0、精度档位的名字有误导性、法线白算了 0.32 MB。但它把一片地要动多少土这件事从公式到网格到数字整条链路都摊开讲了一遍这就够了。下一篇我们回到地图控件那条线上——这次不是量距离而是告诉用户哪边是北。罗盘控件我们下一篇见。如果你也在项目里做过土方量计算欢迎留言说说你是怎么处理填挖分界线被网格咬碎这件事的——是拉高网格精度硬扛还是先勾出等高线再分区算 项目已开源欢迎 Star你的 Star ⭐ 就是对我最大的支持仓库地址https://github.com/jianlei-wang/CESIUM-TREASURE-BOX代码在src/cases/cut-fill-analysis/目录下核心就一个CutFillAnalysisDemo.vue把ORIGINAL_POSITIONS换成你自己的场地经纬度就能跑。Cesium酱の百宝箱 · 第 67 篇三棱柱量尽高低土一条界线只认网格那一格2026 年的秋夜与你共探没量到的地方该怎么被说出来
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑