☰
Cesium 地图与地形挖坑(挖洞)与地图掩膜实现
2026/10/13 6:00:22 网站建设 项目流程

目录

1.cesium 自带地图挖洞的方式

1.1 clippingPlanesCollection实现效果与现状

1.2 ClippingPolygonCollection实现效果与现状

2.修改cesium源码实现 multiClippingPlanesCollection

2.1 MultiClippingPlaneCollection修改步骤

2.2 multiClippingPlanesCollection实现底图挖洞(挖空)与效果

2.3 代码实现DigHole

2.4 实现逻辑

2.5 影响

3. 任意多边形的地图裁剪(内外裁剪、挖空与掩膜)

3.1简要逻辑

3.2实现过程

3.3实现效果

4.总结

1.cesium 自带地图挖洞的方式

1.1 clippingPlanesCollection实现效果与现状

优点:兼容性强,裁剪 3D 瓦片和 glTF 模型。裁剪地球仪。

缺点:不支持凹多边形

效果如下:

有无地形都是重合边界

1.2 ClippingPolygonCollection实现效果与现状

优点:兼容性强,裁剪 3D 瓦片和 glTF 模型。裁剪地球仪。支持任意形状

缺点:地图的渲染边界与输入不完全重合,有碎边锯齿状,缩放地图越大,越明显,无地形状态地图的底图最明显碎边。

无地形与有地形效果如下

有地形的虽然勉强可以用,但是无地形的是真的不可用的碎边,锯齿状

项目因为有很高层级的缩放查看,因此ClippingPolygonCollection对此很不符合,我也想通过什么参数设置调节,但似乎网上没找到,最后发现了multiClippingPlanesCollection的实现方式

2.修改cesium源码实现 multiClippingPlanesCollection

自来来源参考:

Add support for concave /convex clipping planes + holes in CesiumJS · Issue #8751 · CesiumGS/cesiumhttps://github.com/CesiumGS/cesium/issues/8751

点击more,搜索108,参考源码下载;

资源我已经全部下载在下面,本人修改的cesium1.126版本也在下面:


链接: Cesium裁剪地图(挖孔)文件 提取码: XSW0

新增或修改的文件目录

​

源码新增自定义类MultiClippingPlaneCollection,getMultiClippingFunction,

修改文件内容有GlobeFS.glsl,GlobeSurfaceShaderSet.js,GlobeSurfaceTileProvider.js,Globe.js

修改注意,每个版本的逻辑可能存在不同,因此并不建议使用文件替换方式,稳妥的操作应该是,找到ClippingPlaneCollection相应的部分,然后替换,github使用直接替换文件方式适用1.108版本,本人使用1.126版本

2.1 MultiClippingPlaneCollection修改步骤

源码下载,github寻找你的版本,找到源码

​

依赖下载npm i

源码新增2个文件,修改四个文件,路径是:

//主路径 packages\engine\Source //修改文件路径 packages\engine\Source\Shaders\GlobeFS.glsl packages\engine\Source\Scene\Globe.js packages\engine\Source\Scene\GlobeSurfaceShaderSet.js packages\engine\Source\Scene\GlobeSurfaceTileProvider.js //新增文件路径 packages\engine\Source\Scene\getMultiClippingFunction.js packages\engine\Source\Scene\MultiClippingPlaneCollection.js

位置定位,被修改文件请使用clippingplane查询定位,修改参考源文件使用MultiCli定位

​

同一个名称的文件替换完成以后使用clippingPlanes搜索检验总数下即可

globe.GLSL比较特殊,需要CLIPPING_PLANES 定位;Multi_C 搜索 ,Multi_C验证,而且github的修改的代码在globe.GLSL修改逻辑存在小问题,可能会报错,globe.GLSL 4处修改的地方,正确如下:

//原始 #ifdef ENABLE_CLIPPING_PLANES uniform highp sampler2D u_clippingPlanes; uniform mat4 u_clippingPlanesMatrix; uniform vec4 u_clippingPlanesEdgeStyle; #endif //修改 #if defined(ENABLE_CLIPPING_PLANES) || defined(ENABLE_MULTI_CLIPPING_PLANES) uniform highp sampler2D u_clippingPlanes; uniform mat4 u_clippingPlanesMatrix; uniform vec4 u_clippingPlanesEdgeStyle; #endif --------分界线--------- //原始 #ifdef ENABLE_CLIPPING_PLANES float clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix); #endif //修改 float clipDistance = 0.01; #ifdef ENABLE_MULTI_CLIPPING_PLANES clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix, u_multiClippingPlanesLength); #elif defined(ENABLE_CLIPPING_PLANES) clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix); #endif --------分界线--------- //原始 #if defined(ENABLE_CLIPPING_PLANES) vec4 clippingPlanesEdgeColor = vec4(1.0); clippingPlanesEdgeColor.rgb = u_clippingPlanesEdgeStyle.rgb; float clippingPlanesEdgeWidth = u_clippingPlanesEdgeStyle.a; if (clipDistance < clippingPlanesEdgeWidth) { finalColor = clippingPlanesEdgeColor; } #endif //修改 #if defined(ENABLE_CLIPPING_PLANES) || defined(ENABLE_MULTI_CLIPPING_PLANES) vec4 clippingPlanesEdgeColor = vec4(1.0); clippingPlanesEdgeColor.rgb = u_clippingPlanesEdgeStyle.rgb; float clippingPlanesEdgeWidth = u_clippingPlanesEdgeStyle.a; if (clipDistance < clippingPlanesEdgeWidth) { finalColor = clippingPlanesEdgeColor; } #endif

修改完成以后打包:npm run build

npm run build

打包结果是:

把这里的全部文件替换当前项目引用的cesuim替换就行了

当然,他现在的逻辑是defined(ENABLE_CLIPPING_PLANES) || defined(ENABLE_MULTI_CLIPPING_PLANES)兼容公用一些变量,估计这是导致向外裁剪失败的原因,后文第三部分实现底图的凹多边形向外裁剪。

2.2 multiClippingPlanesCollection实现底图挖洞(挖空)与效果

(有填坑贴图)

(有填坑贴图)

2.3 代码实现DigHole

/** * 地图地形挖空,支持凹多边形(点数过多过密集会后后明显缝隙)请不要使用采集密集点渲染挖空地图,直接使用原始形状 */ import earcut from "earcut" export default class DigHole { constructor(viewer, positions, type = false) { this.viewer = viewer; this.positions = positions || []; this.type = type; this.areaClipping(this.viewer, this.positions, this.type) } destroy() { this.viewer.scene.globe.multiClippingPlanes = null this.viewer.scene.globe.clippingPlanes = null this.viewer = null; this.positions = null; this.type = false; } /** * 地图挖空 ClippingPlaneCollection与MultiClippingPlaneCollection只能绑定嵌套使用,不能分开后混用【不要同时使用】,否则裁剪混乱 * @param {*} viewer * @param {*} points 【x:,y:,z:】世界坐标集,clippingPlanes仅支持凸多边形 ;multiClippingPlanes只支持内部挖孔 * @param {*} type 内外 false 内 true 外 */ areaClipping(viewer, points, type = false) { if (points.length < 3) return // 获取点坐标,计算 const pointsCoor = points.map(({ x, y, z }) => new Cesium.Cartesian3(x, y, z)) let sum = 0 for (let i = 0; i < pointsCoor.length; i++) { const pointA = pointsCoor[i] const pointB = pointsCoor[(i + 1) % pointsCoor.length] const crossProduct = Cesium.Cartesian3.cross(pointA, pointB, new Cesium.Cartesian3()) // 计算pointA和pointB两个向量的叉乘 sum += crossProduct.z } if (sum > 0 && type) { // 逆时针 points.reverse() } else if (sum < 0 && !type) { // 顺时针 points.reverse() } if (this.isPolygonConvex(points)) { viewer.scene.globe.multiClippingPlanes = null let clippingPlanes = this.createClippingPlane(points); viewer.scene.globe.clippingPlanes = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes, enabled: true, modelMatrix: Cesium.Matrix4.IDENTITY, unionClippingRegions: type, // 内 || 外 edgeColor: Cesium.Color.YELLOW, edgeWidth: 1.0 }) } else {// 存储ClippingPlane集合 viewer.scene.globe.clippingPlanes = null let arr = [] let posList = this.triangulateAndPreserveZ(points) for (let i of posList) { let clippingPlanes0 = this.createClippingPlane(i); let clippingPlaneCollection0 = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes0, }); arr.push(clippingPlaneCollection0) } viewer.scene.globe.depthTestAgainstTerrain = true; viewer.scene.globe.multiClippingPlanes = new Cesium.MultiClippingPlaneCollection({ collections: arr, edgeWidth: 1, edgeColor: Cesium.Color.BLACK, }); } viewer.scene.globe.backFaceCulling = true viewer.scene.globe.showSkirts = false // viewer.scene.globe.clippingPlanes = null // 销毁 } /** * 根据多边形数组创建裁切面 * @param points_ 多边形数组集合 * @returns {[]} 返回裁切面数组 */ createClippingPlane(points_) { let points = points_; let pointsLength = points.length; let clippingPlanes = []; // 存储ClippingPlane集合 for (let i = 0; i < pointsLength; ++i) { let nextIndex = (i + 1) % pointsLength; // 计算两个坐标点的分量和,取中点。 const midpoint = Cesium.Cartesian3.add(points[i], points[nextIndex], new Cesium.Cartesian3()) Cesium.Cartesian3.multiplyByScalar(midpoint, 0.5, midpoint) // up 是指从地球中心(原点)到 midpoint 的向量,即一个指向地球正上方的单位向量。 const up = Cesium.Cartesian3.normalize(midpoint, new Cesium.Cartesian3()) // 计算points[nextIndex]和midpoint的差值,得到一个表示从 points[nextIndex] 指向 midpoint 的向量 const right = Cesium.Cartesian3.subtract(points[nextIndex], midpoint, new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(right, right) // 计算单位向量 // 通过叉乘及归一化得到单位法向量 const normal = Cesium.Cartesian3.cross(right, up, new Cesium.Cartesian3()) Cesium.Cartesian3.normalize(normal, normal) // 计算单位向量 const originCenteredPlane = new Cesium.Plane(normal, 0.0) const distance = Cesium.Plane.getPointDistance(originCenteredPlane, midpoint) // 计算平面到中点的距离 // 最后,我们得到一个平面,这个平面垂直于地球表面 clippingPlanes.push(new Cesium.ClippingPlane(normal, distance)) } return clippingPlanes; } // 判断是否为凸多边形 isPolygonConvex(positions) { const n = positions.length; if (n < 3) return true; let dir = null; for (let i = 0; i < n; i++) { const p1 = Cesium.Cartographic.fromCartesian(positions[i]); const p2 = Cesium.Cartographic.fromCartesian(positions[(i + 1) % n]); const p3 = Cesium.Cartographic.fromCartesian(positions[(i + 2) % n]); // 计算向量叉积方向 const cross = (Cesium.Math.toRadians(p2.longitude - p1.longitude) * Cesium.Math.toRadians(p3.latitude - p1.latitude)) - (Cesium.Math.toRadians(p2.latitude - p1.latitude) * Cesium.Math.toRadians(p3.longitude - p1.longitude)); if (cross === 0) continue; const newDir = cross > 0 ? 1 : -1; if (dir === null) { dir = newDir; } else if (dir !== newDir) { return false; // 不一致,说明是凹多边形 } } return true; } /** * 凹多边形获取分解凸多边形 分割多边形并保持三维坐标 * @param {Cartesian3[]} positions - 输入的三维多边形顶点数组(Cartesian3 格式) * @returns {Cartesian3[][]} - 返回由多个三角形组成的数组,每个三角形是三个 Cartesian3 的数组 */ triangulateAndPreserveZ(positions) { const resultTriangles = []; // 判断是否为凸多边形 if (this.isPolygonConvex(positions)) { resultTriangles.push(positions); // 如果是凸多边形,直接返回原数组作为唯一一个三角形 return resultTriangles; } const flat = []; const holeIndices = []; const dim = 2; // 将 Cartesian3 转换为二维平面坐标(经纬度),供 earcut 使用 for (let i = 0; i < positions.length; i++) { const carto = Cesium.Cartographic.fromCartesian(positions[i]); flat.push(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)); } // 使用 earcut 进行三角化 const triangles = earcut(flat, holeIndices, dim); // 根据索引重组三角形,返回 Cartesian3 数组 for (let i = 0; i < triangles.length; i += 3) { const p1 = positions[triangles[i]]; const p2 = positions[triangles[i + 1]]; const p3 = positions[triangles[i + 2]]; resultTriangles.push([p1, p2, p3]); } return resultTriangles; } }

前端使用挖洞(需要原始数据)且不重合,不闭合 DigHole

//挖洞(需要原始数据)且不重合,不闭合 DigHole this.dighole = new DigHole(this.viewer, positions, false)
//其中这里 triangulateAndPreserveZ 完全可以优化,因测试需要,没必要优化 if (this.isPolygonConvex(points)) { viewer.scene.globe.multiClippingPlanes = null let clippingPlanes = this.createClippingPlane(points); viewer.scene.globe.clippingPlanes = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes, enabled: true, modelMatrix: Cesium.Matrix4.IDENTITY, unionClippingRegions: type, // 内 || 外 edgeColor: Cesium.Color.YELLOW, edgeWidth: 1.0 }) } else {// 存储ClippingPlane集合 viewer.scene.globe.clippingPlanes = null let arr = [] let posList = this.triangulateAndPreserveZ(points) for (let i of posList) { let clippingPlanes0 = this.createClippingPlane(i); let clippingPlaneCollection0 = new Cesium.ClippingPlaneCollection({ planes: clippingPlanes0, }); arr.push(clippingPlaneCollection0) } viewer.scene.globe.depthTestAgainstTerrain = true; viewer.scene.globe.multiClippingPlanes = new Cesium.MultiClippingPlaneCollection({ collections: arr, edgeWidth: 1, edgeColor: Cesium.Color.BLACK, }); } viewer.scene.globe.backFaceCulling = true viewer.scene.globe.showSkirts = false // viewer.scene.globe.clippingPlanes = null // 销毁

2.4 实现逻辑

通过凸多边形并存(只支持内挖空并存)拼接,实现凹多边形,凸多边形的挖空实现

2.5 影响

* 地图挖空 实现凹多边形内部挖空,只能绑定ClippingPlaneCollection与MultiClippingPlaneCollection嵌套使用

*功能上viewer.scene.globe.clippingPlanes被viewer.scene.globe.multiClippingPlanes兼容(内挖空上兼容,外挖空不兼容)

* 可以在项目独立使用,但是不能混用【不要同时使用,要使用一个,另一个必须要清空null,否则裁剪混乱

缺点:接受点数不能太多(至少不能上百上千,它本身精度很高本需要精细采样点,下面是我精细采样点的结果,其实只有几个点的时候也有不明显的几条细线),效果如下

据此,可以实现挖坑,贴图可视无遮挡等功能

至于完整的坑实现,只要实现底图镂空,后续墙体采样,底部封盖即可

3. 任意多边形的地图裁剪(内外裁剪、挖空与掩膜)

3.1简要逻辑

如果clippingPlanesCollection外挖空和multiClippingPlanesCollection内挖空可以同时存在,那么,任意多边形内外挖空都可以实现,实现的具体逻辑是,先实现任意多边形的外包络矩形的clippingPlanesCollection外挖空,multiClippingPlanesCollection,再在基础上内挖空,通过修正啃食外矩形边界,最后实现边界重合然后就可以实现任意多边形的外挖空;关键点其实就是实现内外挖空同时存在,但目前没解决。但是进过研究:

ClippingPolygonCollection 与multiClippingPlanesCollection或者clippingPlanesCollection同时存在,那么其实ClippingPolygonCollection可以用来解决外挖空不能实现关键问题了,由于clippingPlanesCollection不能多个共存,因此使用

multiClippingPlanesCollection与ClippingPolygonCollection组合

当然,外挖空也可以使用掩膜实现。ClippingPolygonCollection原本是比较完善的,我们这些只为了解决地图锯齿状问题。

3.2实现过程

async digHole(viewer, pointsList, type = false, buffer) { if (this.isPolygonConvex(pointsList) && type == true) { this.areaClipping(viewer, pointsList, type); return } let Bounding_box = await this.getBoundingBox(pointsList, buffer, viewer) let clipBounding_box = await this.getBoundingBox(Bounding_box, buffer, viewer) console.log("包围盒输入形状") console.log(Bounding_box) let ClippingBounding_box = this.getClippingBounding_box(pointsList, clipBounding_box) if (type == false) this.areaClipping(viewer, pointsList, type) else { this.clippingPolygonCollection(viewer, Bounding_box, true) this.areaClipping(viewer, pointsList, false, ClippingBounding_box) } }

(注此处areaClipping和上文的areaClipping有微微改动,不可直接使用)

如果是任意多边形内部裁剪,使用areaClipping的multiClippingPlanes;

如果是凸多边形,且属于外部裁剪,直接使用areaClipping的clippingPlanesCollection实现,避免复杂计算

如果是凹多边形,且属于外部裁剪则使用

this.clippingPolygonCollection(viewer, Bounding_box, true) this.areaClipping(viewer, pointsList, false, ClippingBounding_box)

外部结合裁剪时,clippingPolygonCollection称为第一次裁剪,设置inverse,areaClipping称为第二次修正裁剪

clippingPolygonCollection是通过输入多边形的缓冲矩形去裁剪外部(缓冲是因为他的锯齿碎边原因,默认4米)

areaClipping则是输入了需要修正的区域三角形数组ClippingBounding_box,注意由于锯齿影响,我并没有使用同一个缓冲矩形计算修正的区域三角形数组,而是在缓冲矩形的基础上在计算一个缓冲矩形,可以称为修正缓冲矩形,使用修正缓冲矩形计算修正的区域三角形数组。

缓冲矩形的计算方式是:

async getBoundingBox(pointsList, buffer, viewer) { // 提取经纬度 let lons = [], lats = [], heights = []; pointsList.forEach(p => { let c = Cesium.Cartographic.fromCartesian(p); lons.push(c.longitude); lats.push(c.latitude); heights.push(c.height); }); let minLon = Math.min(...lons), maxLon = Math.max(...lons); let minLat = Math.min(...lats), maxLat = Math.max(...lats); let meanLat = (minLat + maxLat) / 2; const earthRadius = 6378137.0; const deltaLat = buffer / earthRadius; const deltaLon = buffer / (earthRadius * Math.cos(meanLat)); // 四角cartographic let corners = [ new Cesium.Cartographic(minLon - deltaLon, minLat - deltaLat), new Cesium.Cartographic(maxLon + deltaLon, minLat - deltaLat), new Cesium.Cartographic(maxLon + deltaLon, maxLat + deltaLat), new Cesium.Cartographic(minLon - deltaLon, maxLat + deltaLat), ]; // 检查地形是否存在(通常Cesium.createWorldTerrainAsync或类似) let hasTerrain = !(viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider); let bboxCarto; if (hasTerrain) { // 有地形,异步采样地形高程 bboxCarto = await Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, corners); } else { // 没有地形,fallback。建议用原多边形平均高程或0 let avgHeight = heights.length > 0 ? heights.reduce((a, b) => a + b, 0) / heights.length : 0; bboxCarto = corners.map(carto => { carto.height = avgHeight; return carto; }); } // 转换为Cartesian3 let bboxCartesian = bboxCarto.map(carto => Cesium.Cartesian3.fromRadians( carto.longitude, carto.latitude, carto.height )); return bboxCartesian; }

计算包围盒与内部环之间的三角形集合

getClippingBounding_box(pointsList, Bounding_box) { // 1. 不要首尾重复点 function dedupClose(arr) { if ( arr.length > 2 && arr[0].x === arr[arr.length - 1].x && arr[0].y === arr[arr.length - 1].y && arr[0].z === arr[arr.length - 1].z ) { return arr.slice(0, arr.length - 1); } return arr; } Bounding_box = dedupClose(Bounding_box); pointsList = dedupClose(pointsList); // 2. 同步生成 2D/3D 两个数组 let verts2d = []; let verts3d = []; // 外环 Bounding_box.forEach(p => { let carto = Cesium.Cartographic.fromCartesian(p); verts2d.push(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)); verts3d.push(p); }); // 内环 pointsList.forEach(p => { let carto = Cesium.Cartographic.fromCartesian(p); verts2d.push(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)); verts3d.push(p); }); // 3. 自动校正方向 let outerLen = Bounding_box.length; let innerLen = pointsList.length; let outer2d = verts2d.slice(0, outerLen * 2); let inner2d = verts2d.slice(outerLen * 2); if (!this.isClockwise(outer2d)) { outer2d = this.reverseRing(outer2d); // 同步修正3D顺序 verts3d = verts3d.slice(0, outerLen).reverse().concat(verts3d.slice(outerLen)); } if (this.isClockwise(inner2d)) { inner2d = this.reverseRing(inner2d); // 同步修正3D顺序 verts3d = verts3d.slice(0, outerLen).concat(verts3d.slice(outerLen).reverse()); } // 4. 合并2D数组(顺序已调整) verts2d = outer2d.concat(inner2d); // 5. 剖分 let holeIndices = [outerLen]; let triangles = window.earcut.default(verts2d, holeIndices, 2); // 6. 还原三角形到三维 let resultTriangles = []; for (let i = 0; i < triangles.length; i += 3) { let p1 = verts3d[triangles[i]]; let p2 = verts3d[triangles[i + 1]]; let p3 = verts3d[triangles[i + 2]]; resultTriangles.push([p1, p2, p3]); } return resultTriangles; }

3.3实现效果

下面Cesium地图掩膜效果,任意多边形都可以实现,兼容有地形无地形模式

4.总结

clippingPlanes仅支持凸多边形挖空,支持内外挖空,任意地形;

multiClippingPlanes只支持内部挖孔,支持凹凸多边形,任意地形;

ClippingPolygonCollection,支持任意形状,任意地形,但是无地形状态锯齿严重。地形状态也会有缝隙。

通过三者融合使用,完成了maskHoleMap的的任意多边形的内外掩膜的类封装。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询