解锁Cesium可视化编辑新玩法:支持模型和点位拖拽、旋转、缩放,通通一键搞定!

1,011 阅读4分钟

大家好,我是日拱一卒的攻城师不浪,致力于前沿科技探索,摸索小而美工作室。这是2025年输出的第4/100篇文章。

前言

在Cesium中实现对模型的拖拉拽旋转缩放功能可以为许多应用场景增加交互性和用户体验

  1. 地理信息系统:允许用户在三维地图中查看和操作模型,比如放大细节、旋转以获取不同角度的视图,或者拖拽以重新排列模型位置。

  2. 虚拟仿真与培训:在虚拟现实或仿真环境中,用户可以通过拖拽、旋转和缩放模型来学习和训练特定任务,如操作机械设备或学习建筑结构。

  3. 建筑与城市规划:建筑师和规划者可以使用这些功能来查看建筑物或城市布局的不同方面,进行实时调整和评估。

主要是通过这个能力,用户可以更直观地与数据和模型进行互动,提升应用的用户友好性和操作性,支持用户自定义模型形态等。

功能开发

实现这样一个控制器插件涉及的知识点非常多,最终的功能是能实现对模型以及点位同时进行编辑。

所以首先我们要对开发的功能有个整体认知,梳理一下它实现的原理,看看它都需要哪些能力?

  • 交互控制器
    • 移动轴(X/Y/Z)
    • 旋转环
    • 缩放控制点
    • 平面控制器(XY/YZ/XZ平面)

  • 坐标系统转换
    • 世界坐标系(WGS84)
    • 模型本地坐标系
    • 屏幕坐标系
  • 变换矩阵
    • 平移矩阵
    • 旋转矩阵
    • 缩放矩阵

交互控制器

需要创建一个坐标轴(XYZ轴)、旋转轴、缩放轴和平面等交互元素,直观地展示给用户,让用户可以操作的模型控制器。

createPrimitive(isModel = true) {
    // 创建线集合用于存放控制轴
    const plc = new PolylineCollection({
        modelMatrix: this.modelMatrix 
    });
    
    // 创建各种控制器
    this.createOrignPoint();  // 原点
    this.translateEnabled && this.createMoveAxis();  // 移动轴
    this.translateEnabled && this.createAxisPlane(); // 平面
    isModel && this.rotateEnabled && this.createRotateAxis(); // 旋转环
    isModel && this.scaleEnabled && this.createScaleAxis(); // 缩放控制点
}

事件处理

利用 ScreenSpaceEventHandler 监听鼠标事件(如点击拖拽释放等),当鼠标点击激活某个轴线平面,要进行识别并根据拖拽计算模型的变换量。

addEventListener() {
    // 鼠标按下
    handler.setInputAction((e) => {
        const feat = viewer.scene.pick(e.position);
        if(this._primitives.includes(feat.primitive)) {
            // 激活选中的控制器
            this.active(feat.primitive);
            
            // 添加移动事件
            handler.setInputAction((e) => {
                this.transform(startPosition, endPosition);
            }, ScreenSpaceEventType.MOUSE_MOVE);
        }
    }, ScreenSpaceEventType.LEFT_DOWN);
}

变换实现

模型变换(平移、旋转、缩放)通过修改模型的 modelMatrix 来实现。

1. 平移变换:

translate(offset) {
    // 根据选中轴过滤偏移量
    if (axis.indexOf("X") === -1) offset.x = 0;
    if (axis.indexOf("Y") === -1) offset.y = 0; 
    if (axis.indexOf("Z") === -1) offset.z = 0;

    // 创建平移矩阵
    const matrix = Matrix4.fromTranslation(offset);
    
    // 应用变换
    Matrix4.multiply(this._modelMatrix, matrix, this._modelMatrix);
}

2. 旋转变换:

rotate(angle) {
    // 获取旋转轴
    const axis = this.activePrimitive.normal;
    
    // 创建旋转矩阵
    const q = Quaternion.fromAxisAngle(axis, angle, _q);
    const rotateMatrix = Matrix3.fromQuaternion(q, rm3);
    
    // 应用变换(需要先平移到原点)
    Matrix4.multiply(this.modelMatrix, translation, this.modelMatrix);
    Matrix4.multiplyByMatrix3(this.modelMatrix, rotateMatrix, this.modelMatrix);
    Matrix4.multiply(this.modelMatrix, inverseTranslation, this.modelMatrix);
}

3. 缩放变换:

scale(scale) {
    // 创建缩放矩阵
    const scaleMatrix = Matrix4.fromScale(scale, mat4);
    
    // 应用变换
    Matrix4.multiply(this._modelMatrix, scaleMatrix, this._modelMatrix);
}

其它关键技术点

射线拾取

getPositionInPlane(pixel, helper) {
    // 获取射线
    const ray = this._viewer.camera.getPickRay(pixel);
    
    // 创建平面
    const plane = Plane.fromPointNormal(helper.center, helper.activePrimitive.normal, plane);
    
    // 计算交点
    IntersectionTests.rayPlane(ray, plane, mousedownCartesian);
}

坐标转换

// 世界坐标转本地坐标
Matrix4.multiplyByPoint(this._inverseModelMatrix, position, localPosition);

// 本地坐标转世界坐标
Matrix4.multiplyByPoint(this._modelMatrix, position, worldPosition);

辅助线绘制

createAux(offset) {
    // 根据偏移量创建辅助线
    const p1 = Cartesian3.clone(this.xAxis.positions[0]);
    const p2 = Cartesian3.clone(this.xAxis.positions[0]);
    p2.x += -offset.x;
    this.xAux.positions = [p1, p2];
}

性能优化

涉及到一些实时操作以及事件监听,无疑会消耗很多性能,所以,我们需要做一些代码上的性能优化。

缓存常用对象

// 预创建常用的向量和矩阵对象
const cartesian2_1 = new Cartesian2();
const mat4 = new Matrix4();
const _q = new Quaternion();

事件节流

在鼠标移动事件中应用节流,避免过于频繁的计算。

按需创建

// 根据需要创建控制器
this.translateEnabled && this.createMoveAxis();
this.rotateEnabled && this.createRotateAxis();

如何使用

封装的这个插件支持同时对模型以及点位进行拖拉拽等编辑操作;

编辑模型

// 加载模型
const model = new CesiumModel({
    url: "/models/Cesium_Air.glb",
    position: new LonLat(120.36, 36.09),
  })

__viewer.depthTest = true;
model.delegate.then(delegate => {
  __viewer.scene.primitives.add(delegate);
  __viewer.camera.flyTo({
    destination: new Cesium.Cartesian3.fromDegrees(120.36, 36.09, 50),
    orientation: {
      heading: Cesium.Math.toRadians(0),
      pitch: Cesium.Math.toRadians(-90),
      roll: 0.0
    }
  });
  // 初始化编辑器
  const helper = new TransformHelper({
    rotateEnabled: true,
    translateEnabled: true,
    scaleEnabled: true,
    xAxisLength: 10,
    yAxisLength: 10,
    zAxisLength: 10,
    scaleAxisLength: 10,
    rotatePlaneRadius: 8
  });
  // 将辅助编辑器加载进场景
  helper.addTo(__viewer);
  // 绑定模型
  helper.bind(delegate);
})

编辑点位

也支持对点位的拖拉拽等操作。

const point = entities.add({
    id: `point_1`,
    position: Cesium.Cartesian3.fromDegrees(120.36059043724143, 36.090180875828736, 20),
    billboard: {
      image: "/images/mark-icon.png",
      heightReference: Cesium.HeightReference.CLAMP_TO_3D_TILE,
      eyeOffset: new Cesium.Cartesian3(0.0, 0.0, 0.0),
      verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
      horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
      pixelOffset: new Cesium.Cartesian2(0, 0),
    },
  });

__viewer.zoomTo(point)
const helper1 = new TransformHelper({
  rotateEnabled: true,
  translateEnabled: true,
  scaleEnabled: true,
  xAxisLength: 10,
  yAxisLength: 10,
  zAxisLength: 10,
  scaleAxisLength: 10,
  rotatePlaneRadius: 8
})
helper1.addTo(__viewer);
helper1.bindPosition(point.position);
// 如果绑定的是一个点位,需要手动更新
helper1.postTransformEvent.addEventListener(modelMatrix => {
  const position = new Cesium.Cartesian3();
  Cesium.Matrix4.getTranslation(modelMatrix, position);
  point.position = position
})

可视化编辑器详细代码在不浪的教程《Cesium从入门到实战》中,教程将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,并最终完成一个智慧城市的完整项目,课程也在不断更新迭代中,想了解+作者:brown_7778(备注来意)。

有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意)。