介绍
使用Cesium的clippingPlanes实现对3dtiles模型的剖切效果。
相关官方文档地址:ClippingPlaneCollection、Cesium3DTileset
官方Demo地址:3D Tiles Clipping Planes
官方介绍:Cesium Feature Highlight: Clipping Planes
一、效果
二、实现步骤
1.add3DTiles()
函数用于加载和显示3D Tiles模型以及剪切平面clipping planes。
在函数内部,首先创建了一个 Cesium.PrimitiveCollection 对象,用于容纳3D Tiles的各个元素。
然后,定义了一个 Cesium.ClippingPlaneCollection 对象,用于存储裁剪平面的信息,并设置了一些裁剪平面的属性。
接着,创建了一个 Cesium.Cesium3DTileset 对象,该对象表示一个3D Tiles数据集,同时将裁剪平面集合传递给这个对象。
最后,将3D Tiles添加到 PrimitiveCollection 中,再将 PrimitiveCollection 添加到Cesium场景的 primitives 中。
代码如下:
add3DTiles() {
let _this = this
let tileset = _this.tileset;
// 启用地形深度测试
viewer.scene.globe.depthTestAgainstTerrain = true;
//创建一个primitiveCollection
var primitiveCollection = new Cesium.PrimitiveCollection();
let clippingPlanes = _this.clippingPlanes
// 定义一个ClippingPlaneCollection类,用来存储裁剪面
clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [
new Cesium.ClippingPlane(
new Cesium.Cartesian3(0.0, 0.0, -1.0),
0.0
),
],
edgeColor : Cesium.Color.RED,
edgeWidth: 1.0, // 模型被裁切部分的截面线宽
});
_this.clippingPlanes = clippingPlanes;
// 创建一个3D Tileset
tileset = new Cesium.Cesium3DTileset({
url:"http://data.mars3d.cn/3dtiles/qx-dyt/tileset.json",
clippingPlanes: clippingPlanes,
skipLevelOfDetail: true,
preferLeaves: true,
maximumMemoryUsage: 1024,
});
_this.tileset = tileset;
// 将3D Tileset添加到PrimitiveCollection中
primitiveCollection.add(tileset);
// 将PrimitiveCollection添加到场景中
viewer.scene.primitives.add(primitiveCollection);
viewer.zoomTo(tileset);
var heightOffset = 6305.8;
//3d tileset模型加载后
tileset.readyPromise.then(function(tileset) {
// Position tileset
var boundingSphere = tileset.boundingSphere;
var radius = boundingSphere.radius;
//用于调整模型位置使其贴地
var cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center);
var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightOffset);
var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
//调整镜头飞向模型
viewer.zoomTo(tileset,new Cesium.HeadingPitchRange(0.5, -0.2, radius * 4.0))
viewer.scene.camera.flyToBoundingSphere(tileset.boundingSphere, { duration: 2.0 });
//处理与裁剪面(clippingPlanes)的位置和瓦片集(tileset)根节点的模型矩阵之间的关系,以确保裁剪面的位置正确与瓦片集对齐
if (
!Cesium.Matrix4.equals(
tileset.root.transform,
Cesium.Matrix4.IDENTITY
)
) {
// The clipping plane is initially positioned at the tileset's root transform.
// Apply an additional matrix to center the clipping plane on the bounding sphere center.
const transformCenter = Cesium.Matrix4.getTranslation(
tileset.root.transform,
new Cesium.Cartesian3()
);
const transformCartographic = Cesium.Cartographic.fromCartesian(
transformCenter
);
const boundingSphereCartographic = Cesium.Cartographic.fromCartesian(
tileset.boundingSphere.center
);
const height =
boundingSphereCartographic.height - transformCartographic.height;
clippingPlanes.modelMatrix = Cesium.Matrix4.fromTranslation(
new Cesium.Cartesian3(0.0, 0.0, height)
);
}
//加载切面到场景中
for (var i = 0; i < clippingPlanes.length; ++i) {
// 创建一个新的plane对象
var plane = new Cesium.ClippingPlane(
clippingPlanes.get(i).normal,
clippingPlanes.get(i).distance
)
const planeEntities = viewer.entities.add({
position: boundingSphere.center,
plane: {
dimensions: new Cesium.Cartesian2(radius * 2.5, radius * 2.5),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(_this.createPlaneUpdateFunction(plane,i), false),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
_this.planeEntities.push(planeEntities);
}
return tileset;
})
},
2.createPlaneUpdateFunction()
使用回调函数创建了平面的更新函数,以便根据鼠标操作动态更新平面的位置。
代码如下:
createPlaneUpdateFunction(plane,i) {
let _this = this
return function () {
plane.distance = _this.targetY;
_this.clippingPlanes.get(i).distance = _this.targetY-_this.heightOffset
return plane;
};
},
3.mouseHandler()
定义了鼠标在3D视图中的交互事件处理程序。它包括以下事件处理程序:
LEFT_DOWN:注册了鼠标左键点击事件,如果点击到了一个剪切面对象,则选择该剪切面对象,更改其外观,并禁用了默认的鼠标输入。
LEFT_UP:注册了鼠标左键释放事件,恢复所选剪切面的外观,并重新启用了默认的鼠标输入。
MOUSE_MOVE:注册了鼠标移动事件,计算鼠标在垂直方向上的移动距离,并更新 _this.targetY,用来控制剪切面高度的属性。
代码如下:
mouseHandler(){
let _this = this;
let selectedPlane;
// 注册鼠标按下事件
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
var pickedObject = scene.pick(movement.position);
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id) && Cesium.defined(pickedObject.id.plane)) {
selectedPlane = pickedObject.id.plane;
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.05);//改变选中切面的颜色
selectedPlane.outlineColor = Cesium.Color.WHITE;
scene.screenSpaceCameraController.enableInputs = false; // 取消默认的鼠标一切输入事件
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// 注册鼠标松开事件
viewer.screenSpaceEventHandler.setInputAction(function () {
if (Cesium.defined(selectedPlane)) {
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.1);//恢复选中前面的颜色
selectedPlane.outlineColor = Cesium.Color.WHITE;
selectedPlane = undefined;
}
scene.screenSpaceCameraController.enableInputs = true; // 恢复默认的鼠标一切输入事件
}, Cesium.ScreenSpaceEventType.LEFT_UP);
// 注册鼠标移动事件
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
if (Cesium.defined(selectedPlane)) {
var deltaY = movement.startPosition.y - movement.endPosition.y; // 计算鼠标移动的过程中产生的垂直高度距离
_this.targetY += deltaY;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
},
难点总结
需要特别注意的难点是,为了使3dtiles模型贴地,需要有一个高度偏移量heightOffset
var heightOffset = 6305.8;
//3d tileset模型加载后
tileset.readyPromise.then(function(tileset) {
// Position tileset
var boundingSphere = tileset.boundingSphere;
var radius = boundingSphere.radius;
//用于调整模型位置使其贴地
var cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center);
var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightOffset);
var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
这个高度偏移量导致了,实际起剪切效果的剪切面clippingPlanes 和 加载到场景中用于鼠标操控的剪切面plane需要处于不同的高度上,高度应该相差一个heightOffset。
举个例子来讲,当可见可操控的剪切面plane的distance=0时,该剪切面正好与模型相交,但此时实际起剪切效果的剪切面clippingPlanes,需要distance=-heightOffest才能有对应的剪切效果出现。
因此,在createPlaneUpdateFunction()函数中,需要分别控制plane.distance和 _this.clippingPlanes.get(i).distance。
plane.distance = _this.targetY;
_this.clippingPlanes.get(i).distance = _this.targetY-_this.heightOffset
由于上述的需要,又引出了第二个难点:在 JavaScript 中,变量之间赋值时,如果它们引用的是同一个对象或引用类型,那么修改其中一个变量会影响到另一个变量,因为它们实际上引用的是相同的内存地址。
在分别控制plane.distance和 _this.clippingPlanes.get(i).distance的过程中就遇到了这样的问题。
一开始在加载剪切面到场景中时,plane直接等于clippingPlanes.get(i),这就导致了plane.distance 和 _this.clippingPlanes.get(i).distance,一直互相影响,保持相同的值。
//加载切面到场景中
for (var i = 0; i < clippingPlanes.length; ++i) {
// plane直接等于clippingPlanes.get(i)
var plane = clippingPlanes.get(i),
const planeEntities = viewer.entities.add({
position: boundingSphere.center,
plane: {
dimensions: new Cesium.Cartesian2(radius * 2.5, radius * 2.5),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(_this.createPlaneUpdateFunction(plane,i), false),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
_this.planeEntities.push(planeEntities);
}
return tileset;
为了分别控制plane.distance和 _this.clippingPlanes.get(i).distance,给他们赋不同的值,需要创建一个新的plane对象。这样就能正确实现剪切效果了。正确代码如下所示。
//加载切面到场景中
for (var i = 0; i < clippingPlanes.length; ++i) {
// 创建一个新的plane对象
var plane = new Cesium.ClippingPlane(
clippingPlanes.get(i).normal,
clippingPlanes.get(i).distance
)
const planeEntities = viewer.entities.add({
position: boundingSphere.center,
plane: {
dimensions: new Cesium.Cartesian2(radius * 2.5, radius * 2.5),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(_this.createPlaneUpdateFunction(plane,i), false),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
_this.planeEntities.push(planeEntities);
}
return tileset;
主要代码
主要vue代码如下所示
<script>
export default {
data() {
return {
viewers: null,
targetY: 0,
planeEntities:[],
selectedPlane:undefined,
clippingPlanes:undefined,
tileset: undefined,
heightOffset:6305.8,
};
},
methods: {
// 初始化viewer后的传入回调:添加底图和清除加载动画
created() {
//TODO 添加三维模型
this.addMoudleText();
this.mouseHandler();
},
mouseHandler(){
let _this = this;
let selectedPlane;
// 注册鼠标按下事件
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
var pickedObject = scene.pick(movement.position);
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id) && Cesium.defined(pickedObject.id.plane)) {
selectedPlane = pickedObject.id.plane;
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.05);//改变选中切面的颜色
selectedPlane.outlineColor = Cesium.Color.WHITE;
scene.screenSpaceCameraController.enableInputs = false; // 取消默认的鼠标一切输入事件
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// 注册鼠标松开事件
viewer.screenSpaceEventHandler.setInputAction(function () {
if (Cesium.defined(selectedPlane)) {
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.1);//恢复选中前面的颜色
selectedPlane.outlineColor = Cesium.Color.WHITE;
selectedPlane = undefined;
}
scene.screenSpaceCameraController.enableInputs = true; // 恢复默认的鼠标一切输入事件
}, Cesium.ScreenSpaceEventType.LEFT_UP);
// 注册鼠标移动事件
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
if (Cesium.defined(selectedPlane)) {
var deltaY = movement.startPosition.y - movement.endPosition.y; // 计算鼠标移动的过程中产生的垂直高度距离
_this.targetY += deltaY;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
},
add3DTiles() {
let _this = this
let tileset = _this.tileset;
// 启用地形深度测试
viewer.scene.globe.depthTestAgainstTerrain = true;
//创建一个primitiveCollection
var primitiveCollection = new Cesium.PrimitiveCollection();
let clippingPlanes = _this.clippingPlanes
// 定义一个ClippingPlaneCollection类,用来存储裁剪面
clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [
new Cesium.ClippingPlane(
new Cesium.Cartesian3(0.0, 0.0, -1.0),
0.0
),
],
edgeColor : Cesium.Color.RED,
edgeWidth: 1.0, // 模型被裁切部分的截面线宽
});
_this.clippingPlanes = clippingPlanes;
// 创建一个3D Tileset
tileset = new Cesium.Cesium3DTileset({
url:"http://data.mars3d.cn/3dtiles/qx-dyt/tileset.json",
clippingPlanes: clippingPlanes,
skipLevelOfDetail: true,
preferLeaves: true,
maximumMemoryUsage: 1024,
});
_this.tileset = tileset;
console.log("tileset",tileset)
// 将3D Tileset添加到PrimitiveCollection中
primitiveCollection.add(tileset);
// 将PrimitiveCollection添加到场景中
viewer.scene.primitives.add(primitiveCollection);
viewer.zoomTo(tileset);
var heightOffset = 6305.8;
tileset.readyPromise.then(function(tileset) {
// Position tileset
var boundingSphere = tileset.boundingSphere;
var radius = boundingSphere.radius;
var cartographic = Cesium.Cartographic.fromCartesian(boundingSphere.center);
var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, heightOffset);
var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
// var modelMatrix = tileset.modelMatrix
viewer.zoomTo(tileset,new Cesium.HeadingPitchRange(0.5, -0.2, radius * 4.0))
// viewer.flyTo(tileset)
viewer.scene.camera.flyToBoundingSphere(tileset.boundingSphere, { duration: 2.0 });
if (
!Cesium.Matrix4.equals(
tileset.root.transform,
Cesium.Matrix4.IDENTITY
)
) {
// The clipping plane is initially positioned at the tileset's root transform.
// Apply an additional matrix to center the clipping plane on the bounding sphere center.
const transformCenter = Cesium.Matrix4.getTranslation(
tileset.root.transform,
new Cesium.Cartesian3()
);
const transformCartographic = Cesium.Cartographic.fromCartesian(
transformCenter
);
const boundingSphereCartographic = Cesium.Cartographic.fromCartesian(
tileset.boundingSphere.center
);
const height =
boundingSphereCartographic.height - transformCartographic.height;
clippingPlanes.modelMatrix = Cesium.Matrix4.fromTranslation(
new Cesium.Cartesian3(0.0, 0.0, height)
);
}
//加载切面到场景中
for (var i = 0; i < clippingPlanes.length; ++i) {
var plane = new Cesium.ClippingPlane(
clippingPlanes.get(i).normal,
clippingPlanes.get(i).distance
)// 创建一个新的plane对象
const planeEntities = viewer.entities.add({
position: boundingSphere.center,
plane: {
dimensions: new Cesium.Cartesian2(radius * 2.5, radius * 2.5),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(_this.createPlaneUpdateFunction(plane,i), false),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
_this.planeEntities.push(planeEntities);
}
return tileset;
})
},
createPlaneUpdateFunction(plane,i) {
let _this = this
return function () {
plane.distance = _this.targetY;
_this.clippingPlanes.get(i).distance = _this.targetY-_this.heightOffset
return plane;
};
},
addMoudleText() {
this.add3DTiles();
}
</script>