最近在项目中遇到一个需求:需要在Cesium中动态编辑模型的位置。
熟悉三维开发的朋友应该知道,Three.js提供了十分便捷的控件来操作模型(TransformControls),但是Cesium在这方面有所欠缺:虽然通过参数设置也能实现功能,但对用户来说操作不够直观。
为此,我花了两三天时间开发了一个类似的模型位置编辑控件,实现了较为直观的交互操作。
具体效果如下图所示。

整个实现过程还是有些复杂的,涉及到了坐标转换、交互处理、Cesium事件机制等。 因此在这里做个简单的分享,给大家提供一些参考。
本文主要从数据流的角度,说明整个系统的计算和变化过程:
1. 模型设置阶段的数据流
首先,需要获取到模型或组合模型的中心点,从而设置操作的中心点并生成对应的操作轴。
大致的过程:模型设置 → 计算包围球 → 生成操作轴
javascript
// 获取基准点
calcSceneBS() {
const boundingSpheres = [];
for (let i = 0; i < this._targetList.length; i++) {
const model = this._targetList[i];
let sphere = model.boundingSphere;
if (model instanceof Cesium.Model) {
sphere = this.getModelBS(model);
} else if (model instanceof Cesium.Primitive) {
const translation = Cesium.Matrix4.getTranslation(model.modelMatrix, new Cesium.Cartesian3);
const boundingSpheres = model._boundingSpheres;
sphere = new Cesium.BoundingSphere(translation, boundingSpheres.radius);
} else if (model.point) {
sphere = new Cesium.BoundingSphere(model.position.getValue(), 10);
}
if (sphere) {
boundingSpheres.push(sphere);
}
}
if (boundingSpheres.length > 0) {
this._sceneSphere = Cesium.BoundingSphere.fromBoundingSpheres(boundingSpheres, new Cesium.BoundingSphere());
}
if (this._basePt === undefined && this._sceneSphere && this._sceneSphere.center) {
this._basePt = Cesium.Cartographic.fromCartesian(this._sceneSphere.center);
this._basePt.longitude = Cesium.Math.toDegrees(this._basePt.longitude);
this._basePt.latitude = Cesium.Math.toDegrees(this._basePt.latitude);
}
}
// 创建xyz轴
_createAxis(name, color) {
const positionsCallback = new Cesium.CallbackProperty(() => {
if (!this._isValidCartesian(this._center)) return [];
const axes = this._getLocalAxes();
if (!axes) return [];
// 计算轴的方向向量
const dir = name === 'X' ? axes.xAxis : name === 'Y' ? axes.yAxis : axes.zAxis;
if (!this._isValidCartesian(dir)) return [];
const start = Cesium.Cartesian3.clone(this._center, new Cesium.Cartesian3());
const end = Cesium.Cartesian3.add(
start,
Cesium.Cartesian3.multiplyByScalar(dir, this._axisLength, new Cesium.Cartesian3()),
new Cesium.Cartesian3()
);
return [start, end];
}, false);
const entity = this._viewer.entities.add({
name,
polyline: {
positions: positionsCallback,
width: this._selectedAxis === name ? 10 : 5,
clampToGround: false,
material: new Cesium.PolylineArrowMaterialProperty(color)
}
});
this._axisEntities.push(entity);
}
2. 交互过程的数据流
完整的数据流:鼠标点击 → 拾取检测 → 坐标转换 → 变换计算 → 矩阵应用 → 视觉更新
2.1 鼠标操作
我们需要判断拾取的操作轴,根据不同的轴使用不同的拾取方法。
javascript
_onLeftDown(event) {
if (!this._isActive) return;
// 拾取操作轴
const pickedObject = this._scene.pick(event.position);
if (pickedObject && pickedObject.id) {
const axisName = Cesium.clone(pickedObject.id.name, true);
const validAxes = ["X", "Y", "Z", "XY", "XZ", "YZ",
"XY_CIRCLE", "XZ_CIRCLE", "YZ_CIRCLE",
"SCALE_X", "SCALE_Y", "SCALE_Z"];
if (validAxes.indexOf(axisName) >= 0) {
this._pickedObject = pickedObject;
this._highlightPickedObject(this._pickedObject);
this._viewer.scene.screenSpaceCameraController.enableRotate = false;
this._viewer.scene.screenSpaceCameraController.enableLook = false;
this._selectedAxis = axisName;
this._isDragging = true;
this._mouseDownPos = undefined;
this._mouseDownXY = event.position;
const X = ["XY", "XY_CIRCLE", "X", "Y", "Z"];
const Y = ["YZ", "YZ_CIRCLE"];
const Z = ["XZ", "XZ_CIRCLE"];
if (X.indexOf(this._selectedAxis) > -1) {
this._mouseDownPos = this.pickPositionOnEllipsoid(event.position);
if (this._transType.indexOf("MOVE") >= 0) this.calcFixedAxis()
}
if (Y.indexOf(this._selectedAxis) > -1) {
this._mouseDownPos = this.pickPositionOnPlane(event.position, "YZ");
if (this._transType.indexOf("MOVE") >= 0) this.calcFixedAxis()
}
if (Z.indexOf(this._selectedAxis) > -1) {
this._mouseDownPos = this.pickPositionOnPlane(event.position, "XZ");
if (this._transType.indexOf("MOVE") >= 0) this.calcFixedAxis()
}
// 触发 Transform 事件
this._raiseEvent("Transform", {
position: this._mouseDownPos,
cartographic: this._mouseDownPos,
axis: this._selectedAxis
});
}
}
}
_onMouseMove(event) {
if (!this._isActive || !this._isDragging) return;
if (this._selectedAxis === "") return;
let pos = undefined;
const X = ["XY", "XY_CIRCLE", "X", "Y", "Z"];
const Y = ["YZ", "YZ_CIRCLE"];
const Z = ["XZ", "XZ_CIRCLE"];
if (X.indexOf(this._selectedAxis) > -1) pos = this.pickPositionOnEllipsoid(event.endPosition);
if (Y.indexOf(this._selectedAxis) > -1) pos = this.pickPositionOnPlane(event.endPosition, "YZ");
if (Z.indexOf(this._selectedAxis) > -1) pos = this.pickPositionOnPlane(event.endPosition, "XZ");
// 触发 Transforming 事件
try {
this._raiseEvent("Transforming", {
MouseDownPos: this._mouseDownPos,
MouseMovePos: pos,
MouseDownXY: this._mouseDownXY,
MouseMoveXY: event.endPosition,
SelectedAxis: this._selectedAxis,
FixedAxis: this._fixedAxis
});
} catch (err) {
console.error('Error raising Transforming event:', err);
}
this._requestRender();
}
2.2 拾取操作及坐标转换
javascipt
/**
* 在椭球面上拾取位置
* @param {Cesium.Cartesian2} screenPosition 屏幕位置
* @returns {Cesium.Cartographic} 地理坐标
*/
pickPositionOnEllipsoid(screenPosition) {
if (!this._initSphere || !this._initSphere.center) return undefined;
// 获取中心点的地理坐标
const centerCartographic = Cesium.Cartographic.fromCartesian(this._initSphere.center);
// 根据中心点高度调整椭球半径
const ellipsoid = Cesium.clone(Cesium.Ellipsoid.WGS84, true);
ellipsoid.radii.x += centerCartographic.height;
ellipsoid.radii.y += centerCartographic.height;
ellipsoid.radii.z += centerCartographic.height;
const adjustedEllipsoid = new Cesium.Ellipsoid(
ellipsoid.radii.x,
ellipsoid.radii.y,
ellipsoid.radii.z
);
// 拾取位置
const cartesian = this._viewer.camera.pickEllipsoid(screenPosition, adjustedEllipsoid, new Cesium.Cartesian3());
if (cartesian === undefined) return undefined;
// 转换为地理坐标
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
return cartographic;
}
/**
* 在指定平面上拾取位置
* @param {Cesium.Cartesian2} screenPosition 屏幕位置
* @param {String} planeType 平面类型 ("XY"|"YZ"|"XZ")
* @returns {Cesium.Cartographic} 地理坐标
*/
pickPositionOnPlane(screenPosition, planeType) {
this._calcCallbackArgs();
if (!this._initSphere || !this._initSphere.center) return undefined;
if (!this._callbackCartographic) return undefined;
const r = this._axisZoom * 1e-5;
const lon = this._callbackCartographic.longitude;
const lat = this._callbackCartographic.latitude;
const height = this._callbackCartographic.height;
// 创建三角形用于平面相交测试
let i, n, a;
if (planeType === "YZ") {
i = Cesium.Cartesian3.fromRadians(lon, lat - r, -2e3);
n = Cesium.Cartesian3.fromRadians(lon, lat, height + this._axisZoom * 0.95 + 1e3);
a = Cesium.Cartesian3.fromRadians(lon, lat + r, -2e3);
} else {
// XZ 平面
i = Cesium.Cartesian3.fromRadians(lon - r, lat, -2e3);
n = Cesium.Cartesian3.fromRadians(lon, lat, height + this._axisZoom * 0.95 + 1e3);
a = Cesium.Cartesian3.fromRadians(lon + r, lat, -2e3);
}
const ray = this._viewer.camera.getPickRay(screenPosition);
if (!ray) return undefined;
// 计算平面法向量(基于经度)
const centerCartographic = Cesium.Cartographic.fromCartesian(this._initSphere.center);
const s = Math.cos(centerCartographic.longitude);
const l = Math.sin(centerCartographic.longitude);
const d = 0;
const plane = new Cesium.Plane.fromPointNormal(
new Cesium.Cartesian3(0, 0, 0),
new Cesium.Cartesian3(s, l, d)
);
// 创建射线终点
const rayEnd = new Cesium.Cartesian3();
rayEnd.x = ray.origin.x + ray.direction.x * 1e6;
rayEnd.y = ray.origin.y + ray.direction.y * 1e6;
rayEnd.z = ray.origin.z + ray.direction.z * 1e6;
// 线段与三角形相交测试
const intersection = Cesium.IntersectionTests.lineSegmentTriangle(
ray.origin,
rayEnd,
i,
n,
a
);
if (intersection === undefined) return undefined;
// 转换为地理坐标
const cartographic = Cesium.Cartographic.fromCartesian(intersection);
return cartographic;
}
/**
* 计算回调参数(用于动态更新)
*/
_calcCallbackArgs() {
if (!this._initSphere || !this._initSphere.center) return;
this._callbackCenter = this._initSphere.center;
this._callbackCartographic = Cesium.Cartographic.fromCartesian(this._callbackCenter);
this._callbackLonDeg = Cesium.Math.toDegrees(this._callbackCartographic.longitude);
this._callbackLatDeg = Cesium.Math.toDegrees(this._callbackCartographic.latitude);
this._callbackHeight = this._callbackCartographic.height;
this._viewer.scene.requestRender();
}
2.3 变换矩阵
javascript
getMatrixOfTileset(tileset, deltaLon, deltaLat, deltaHeight, heading, pitch, roll, scaleX, scaleY, scaleZ, baseLon, baseLat, baseHeight) {
let baseLonRad = baseLon;
let baseLatRad = baseLat;
let baseHeightM = baseHeight;
// 计算基准位置和目标位置
const basePos = Cesium.Cartesian3.fromRadians(baseLonRad, baseLatRad, baseHeightM);
const targetPos = Cesium.Cartesian3.fromRadians(
baseLonRad + deltaLon,
baseLatRad + deltaLat,
baseHeightM + deltaHeight
);
// 角度转换为弧度
heading = Cesium.Math.toRadians(heading);
pitch = Cesium.Math.toRadians(pitch);
roll = Cesium.Math.toRadians(roll);
// 创建四元数旋转
const quaternion = Cesium.Quaternion.fromHeadingPitchRoll(
new Cesium.HeadingPitchRoll(heading, pitch, roll)
);
const scale = new Cesium.Cartesian3(scaleX, scaleY, scaleZ);
// 创建变换矩阵(旋转 + 缩放)
const transform = Cesium.Matrix4.fromTranslationQuaternionRotationScale(
Cesium.Cartesian3.ZERO,
quaternion,
scale
);
// 坐标系变换
const baseFrame = Cesium.Transforms.eastNorthUpToFixedFrame(basePos);
const baseFrameInv = Cesium.Matrix4.inverse(baseFrame, new Cesium.Matrix4);
const targetFrame = Cesium.Transforms.eastNorthUpToFixedFrame(targetPos);
// 合成最终变换矩阵
const result = Cesium.Matrix4.multiply(transform, baseFrameInv, new Cesium.Matrix4);
return Cesium.Matrix4.multiply(targetFrame, result, new Cesium.Matrix4);
}
2.4 矩阵应用到模型
javaScript
applyTransformToTileset(tileset, deltaLon, deltaLat, deltaHeight, heading, pitch, roll, scaleX, scaleY, scaleZ, baseLon, baseLat, baseHeight) {
// 获取变换矩阵
const transformMatrix = this.getMatrixOfTileset(
tileset, deltaLon, deltaLat, deltaHeight, heading, pitch, roll,
scaleX, scaleY, scaleZ, baseLon, baseLat, baseHeight
);
if (transformMatrix) {
// 将变换矩阵应用到初始模型矩阵
const resultMatrix = Cesium.Matrix4.multiply(transformMatrix, this._mouseDownMM, new Cesium.Matrix4);
tileset.modelMatrix = resultMatrix;
}
}
2.5 轴更新
javascript
_getLocalAxes() {
let enuMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(this._center)
// 从ENU矩阵提取坐标轴
const xAxis = Cesium.Matrix4.multiplyByPointAsVector(
enuMatrix,
new Cesium.Cartesian3(1, 0, 0),
new Cesium.Cartesian3()
);
const yAxis = Cesium.Matrix4.multiplyByPointAsVector(
enuMatrix,
new Cesium.Cartesian3(0, 1, 0),
new Cesium.Cartesian3()
);
const zAxis = Cesium.Matrix4.multiplyByPointAsVector(
enuMatrix,
new Cesium.Cartesian3(0, 0, 1),
new Cesium.Cartesian3()
);
return { xAxis, yAxis, zAxis };
}
3. 简单总结一下
应该还有其他优化的部分,比如轴大小随视距缩放之类的。
有机会把完整代码放出来开源,争取达到开箱即用的效果。