大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室,这是2025年输出的第23/100篇文章。
前言
前几天,负责地下管网的产品经理急冲冲的跑过来问我:能不能在地表给我挖一个洞,让我能直接看到地底下的管道!?
我:这。。。

实际上,不止在地下管网
项目,包括数字孪生智慧城市
或者城市应急
项目中,都经常会涉及到一个需求:挖洞!
那么,今天我们就来看一下,如何在Cesium中实现挖洞的功能,还得支持地形挖洞
。源码请文末领取噢~
什么是地形挖洞?
地形挖洞(Terrain Excavation
)是指在三维场景中,对地形表面进行局部开挖操作,使得地表以下的结构可以被展示出来。

通过地形挖洞,我们可以在不破坏原始地形数据的情况下,实现地下空间的可视化展示。
地形挖洞的应用场景
-
地下管网展示 :展示埋在地下的
水管、电缆、燃气管
等基础设施,还能通过动态设置挖洞深度,预判是否会挖碰到地下管道,提前避免不必要的损失。 -
地质勘探与矿产开发:展示地下矿产资源分布和开采情况
-
考古模拟:模拟考古挖掘过程,展示地下文物分布
-
地下建筑可视化:地下停车场、地铁站、地下商场等建筑的展示
-
军事模拟:地下工事、地下通道等军事设施的可视化
-
城市规划:综合展示地上地下空间利用情况
实现原理
挖洞,包含几个关键技术点:
-
裁剪平面(ClippingPlane):通过裁剪平面集合定义需要挖掘的区域边界
-
实体(Entity):创建底部和侧面实体来可视化挖掘区域
-
地形采样:获取真实地形高度数据以保证挖掘结果的准确性
-
材质定义:为挖掘出的区域添加合适的纹理材质
实现流程
-
定义挖掘区域的
边界点
-
根据边界点创建一系列
裁剪平面
-
将裁剪平面应用到地球的
globe
对象上 -
创建底部
多边形实体
,表示挖掘区域的底面 -
创建
墙体实体
,表示挖掘区域的侧面 -
为底面和侧面
应用纹理材质
核心代码解析
我们会提前创建好挖洞的类,然后直接调用。
1. 基本使用方法
首先看一下如何调用这个功能:
javascript
// 创建挖洞实例
excavateInstance = new ExcavateTerrain(viewer, {
positions: mr, // 挖洞区域的边界点
height: Number(height.value), // 挖洞深度
bottom: "/images/excavationregion_top.jpg", // 底部纹理
side: "/images/excavationregion_side.jpg", // 侧面纹理
});
调用时,只需提供边界点
、挖掘深度
和纹理图片
即可。
2. 边界点处理
javascript
analysis() {
var viewer = this.viewer;
var config = this.config;
var ellipsoid = viewer.scene.globe.ellipsoid;
var arr = config.positions;
var nArr = [];
var nArr2 = [];
arr.forEach((element) => {
var catographic = Cesium.Cartographic.fromCartesian(element);
var height = Number(catographic.height.toFixed(2));
var cartographic = ellipsoid.cartesianToCartographic({
x: element.x,
y: element.y,
z: element.z,
});
var lat = Cesium.Math.toDegrees(cartographic.latitude);
var lng = Cesium.Math.toDegrees(cartographic.longitude);
nArr.push({
x: lng,
y: lat,
z: height,
});
nArr2.push({
x: lng,
y: lat,
z: height,
});
});
// ...
}
挖洞的第一步是处理边界点,将笛卡尔坐标
转换为经纬度
格式,然后保存高度信息。nArr
和nArr2
用于后续挖洞操作。
3. 裁剪平面的创建
挖洞的核心是创建裁剪平面集合:
javascript
var points = [];
arr.forEach((element) => {
points.push(Cesium.Cartesian3.fromDegrees(element.x, element.y));
});
var pointsLength = points.length;
var clippingPlanes = [];
for (var i = 0; i < pointsLength; ++i) {
var nextIndex = (i + 1) % pointsLength;
var midpoint = Cesium.Cartesian3.add(
points[i],
points[nextIndex],
new Cesium.Cartesian3()
);
midpoint = Cesium.Cartesian3.multiplyByScalar(midpoint, 0.5, midpoint);
var up = Cesium.Cartesian3.normalize(midpoint, new Cesium.Cartesian3());
var right = Cesium.Cartesian3.subtract(
points[nextIndex],
midpoint,
new Cesium.Cartesian3()
);
right = Cesium.Cartesian3.normalize(right, right);
var normal = Cesium.Cartesian3.cross(right, up, new Cesium.Cartesian3());
normal = Cesium.Cartesian3.normalize(normal, normal);
var originCenteredPlane = new Cesium.Plane(normal, 0.0);
var distance = Cesium.Plane.getPointDistance(
originCenteredPlane,
midpoint
);
clippingPlanes.push(new Cesium.ClippingPlane(normal, distance));
}
这段代码为多边形的每条边创建了一个裁剪平面:
-
计算边的中点(
midpoint
) -
确定平面的法向量(
normal
) -
计算平面与原点的距离(
distance
) -
创建并添加裁剪平面
然后将裁剪平面集合应用到地球上:
javascript
viewer.scene.globe.clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: clippingPlanes,
edgeWidth: 1.0,
edgeColor: Cesium.Color.OLIVE,
});
4. 创建底部多边形
为了让挖洞区域更加逼真,需要创建一个底部多边形:
javascript
viewer.entities.add({
id: "entityDM",
polygon: {
hierarchy: Cesium.Cartesian3.fromDegreesArray(nar),
material: new Cesium.ImageMaterialProperty({
image: config.bottom,
color: new Cesium.Color.fromCssColorString("#cbc6c2"),
repeat: new Cesium.Cartesian2(30, 30),
}),
height: hhh[0] - config.height,
},
});
这里创建了一个多边形实体,通过height
属性设置底部高度(即最低点高度减去挖掘深度),并应用了自定义纹理。
5. 创建侧面墙体
为了表现挖掘区域的侧面,需要创建墙体:
javascript
var p1 = Cesium.sampleTerrainMostDetailed(
viewer.terrainProvider,
terrainSamplePositions
);
Promise.all([p1]).then((samples) => {
console.log(samples[0]);
samples = samples[0];
for (let index = 0; index < samples.length; index++) {
maximumHeightsARR.push(samples[index].height);
minimumHeights.push(hhh[0] - config.height);
}
viewer.entities.add({
id: "entityDMBJ",
wall: {
positions: Cesium.Cartesian3.fromDegreesArray(nar22),
maximumHeights: maximumHeightsARR,
minimumHeights: minimumHeights,
material: new Cesium.ImageMaterialProperty({
image: config.side,
repeat: new Cesium.Cartesian2(30, 30),
}),
},
});
});
这段代码首先通过sampleTerrainMostDetailed
方法获取地形高度数据,然后创建墙体实体:
-
maximumHeights
: 墙体顶部高度,使用真实地形高度 -
minimumHeights
: 墙体底部高度,与底部多边形高度一致 -
同样应用自定义纹理
6. 判断多边形方向
代码中还包含了判断多边形是否为顺时针方向的函数:
javascript
isClockWise(latLngArr) {
// 找到y值最小的点
let latMin = {
i: -1,
val: 100000000,
};
for (let i = 0; i < latLngArr.length; i++) {
let y = latLngArr[i].y;
if (y < latMin.val) {
latMin.val = y;
latMin.i = i;
}
}
// 计算叉积判断方向
let i1 = (latMin.i + latLngArr.length - 1) % latLngArr.length;
let i2 = latMin.i;
let i3 = (latMin.i + 1) % latLngArr.length;
let v2_1 = {
y: latLngArr[i2].y - latLngArr[i1].y,
x: latLngArr[i2].x - latLngArr[i1].x,
};
let v3_2 = {
y: latLngArr[i3].y - latLngArr[i2].y,
x: latLngArr[i3].x - latLngArr[i2].x,
};
let result = v3_2.x * v2_1.y - v2_1.x * v3_2.y;
return result === 0 ? latLngArr[i3].x < latLngArr[i1].x : result > 0;
}
这个函数确保多边形的顶点顺序一致,以便正确创建裁剪平面。
如果多边形是顺时针
方向,则会将顶点顺序反转:
javascript
var flag = _this.isClockWise(arr);
if (flag === true) {
arr.reverse();
nArr2.reverse();
}
清除挖掘效果
javascript
function clearDigging() {
if (viewer) {
// 清除裁剪平面
if (viewer.scene.globe.clippingPlanes) {
viewer.scene.globe.clippingPlanes = undefined;
}
// 删除实体
viewer.entities.removeById("entityDM");
viewer.entities.removeById("entityDMBJ");
}
}
最后
【源码 】:github.com/jiawanlong/...
【不浪的Cesium案例集合 】:github.com/tingyuxuan2...
如果开源对您有帮助,也欢迎帮我们点一个免费的star
,以鼓励和支持我们开源更多案例!
如有任何问题或需要进一步探讨,欢迎在评论区留言交流!
想系统学习Cesium的小伙伴儿,可以了解下不浪的教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个
智慧城市
的完整项目,课程最近也更新了不少新内容,想了解+作者:brown_7778(备注来意)。
有需要进可视化&Webgis交流群
可以加我:brown_7778(备注来意)。