目录
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的的任意多边形的内外掩膜的类封装。