Cesium模型裁切原理


前言

Cesium的官方示例中提供了3D模型裁切获取剖切面的示例3DTilesClippingPlanes,相信很多初次接触cesium的小伙伴第一次看时都会和我一样,虽然本地能跑起来,但是对整个流程缺乏整体的理解,最近整理了一下这个流程并记录下来。


步骤

加载模型数据

在这里我以官网内置的3d tiles示例作为例子。
在这里插入图片描述

  • 首先,通过viewer.scene.primitives.add方法加载模型,同时为他添加裁切面clippingPlanes,Cesium.ClippingPlane的参数可去官网api查阅,这里不一一介绍。
  • 其次,当模型加载完毕时,在readyPromise中进行一系列操作:如果这个模型的根转换矩阵Matrix4不等于Cesium.Matrix4.IDENTITY时,即不在cesium笛卡尔坐标原点时(一般都不可能在),要通过这个矩阵获取剖切面和模型所在的笛卡尔坐标;最终获取剖切面的转换矩阵。
  • 最后,为clippingPlanes添加面时,同时给改面绑定createPlaneUpdateFunction()方法,这个方法将会在后续绑定的鼠标事件中被不断调用。

在这里,要简单说明下模型的转换矩阵,根节点的transform,其实就是整个模型对于cesium中心的转移矩阵,他是一个4*4数组,cesium中的getTranslation方法实现了通过转换矩阵获取笛卡尔坐标的方式,通过源码可以发现转换矩阵的13-15项,实际上就是该模型的笛卡尔坐标(x,y,z)。
在这里插入图片描述
在这里插入图片描述
这个流程里还用到的方法包括:

  • Cesium.Cartographic.fromCartesian // 笛卡尔坐标=> wgs84坐标
  • Cesium.Matrix4.fromTranslatio //从一个Cartesian3对象生成Matrix4变换矩阵
  • Cesium.CallbackProperty / Cesium中的entities实现长度高度变化动画,基本都使用这个函数

这部分代码如下:

function loadTileset(url) {
      // 定义一个ClippingPlaneCollection类,用来存储裁剪面
      clippingPlanes = new Cesium.ClippingPlaneCollection({
        planes: [ // ClippingPlane对象数组集合
          new Cesium.ClippingPlane( // 裁切面
            new Cesium.Cartesian3(0.0, 0.0, -1.0), // 法线方向
            0.0 // 原点到平面的最短距离,设置0就好
          ),
        ],
        edgeWidth: 1.0, // 模型被裁切部分的截面线宽
      });

      tileset = viewer.scene.primitives.add(
        new Cesium.Cesium3DTileset({
          url: url,
          clippingPlanes: clippingPlanes,
        })
      );

      tileset.debugShowBoundingVolume = true
      return tileset.readyPromise
        .then(function () {
          // cesium.BoundingSphere(center:Cartersian3, radius:number), tileset的球状边界
          const boundingSphere = tileset.boundingSphere;
          const radius = boundingSphere.radius;

          viewer.zoomTo(
            tileset,
            new Cesium.HeadingPitchRange(0.5, -0.2, radius * 4.0)
          );
          debugger;
            
          if (
            !Cesium.Matrix4.equals(   // Cesium.Matrix4.equals(a,b)判断两个四维矩阵是否相等
              tileset.root.transform, // 整个根节点模型矩阵,该tileSet=>世界坐标系
              Cesium.Matrix4.IDENTITY // 单位矩阵,对角线值为1.0的4*4矩阵
            )
          ) {
            // 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.
            
            // 获取模型的世界坐标(笛卡尔)
            // Cesium.Matrix4.getTranslation 通过仿射变换矩阵获取该tileSet的世界坐标
            const transformCenter = Cesium.Matrix4.getTranslation(
              tileset.root.transform,
              new Cesium.Cartesian3()
            );
            debugger;
            // 将笛卡尔坐标转换为WGS84经纬度坐标(模型的)
            const transformCartographic = Cesium.Cartographic.fromCartesian(
              transformCenter
            );
            // 将笛卡尔坐标转换为WGS84经纬度坐标(截面的)
            const boundingSphereCartographic = Cesium.Cartographic.fromCartesian(
              tileset.boundingSphere.center
            );
            const height =
              boundingSphereCartographic.height - transformCartographic.height;
           
            // 从一个Cartesian3对象生成Matrix4变换矩阵(裁切面的)
            clippingPlanes.modelMatrix = Cesium.Matrix4.fromTranslation(
              new Cesium.Cartesian3(0.0, 0.0, height)
            );
          }
          // debugger;

          // 创建添加裁剪平面
          for (let i = 0; i < clippingPlanes.length; ++i) {
            const plane = clippingPlanes.get(i);
            const planeEntity = 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(
                  // 添加绑定事件,不断调用
                  createPlaneUpdateFunction(plane),
                  false
                ),
                outline: true,
                outlineColor: Cesium.Color.WHITE,
              },
            });

            planeEntities.push(planeEntity);
          }
          return tileset;
        })
        .catch(function (error) {
          console.log(error);
        });
    }

添加监听事件

分别给鼠标绑定三种事件:LEFT_DOWN,LEFT_UP,MOUSE_MOVE。当MOUSE_MOVE触发时,会不断刷新targetY 的值。LEFT_DOWN,LEFT_UP代表左键按下弹起事件,触发时需要设置scene.screenSpaceCameraController.enableInputs 禁用/恢复原本鼠标默认事件(放大缩小)。targetY值发生变化时,不断回调的CallbackProperty 会获取最新的targetY ,重新设置裁切面的distance属性实现动画效果,至于模型是怎么随着裁切面的位置隐藏的,可以去看一下源码Cesium3DTileset中对clippingPlanes这个参数的具体实现。

// 左键按下
const downHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    downHandler.setInputAction(function (movement) {
      const pickedObject = scene.pick(movement.position);
  
      console.log('pickedObject---downHandler---', pickedObject)
      if (
        Cesium.defined(pickedObject) &&
        Cesium.defined(pickedObject.id.plane)
      ) {
        selectedPlane = pickedObject.id.plane;
        selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.5);
        selectedPlane.outlineColor = Cesium.Color.WHITE;
        scene.screenSpaceCameraController.enableInputs = false;
      }
    }, Cesium.ScreenSpaceEventType.LEFT_DOWN);

    // Release plane on mouse up
    const upHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    // 左键松开
    upHandler.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);

    // Update plane on mouse move
    const moveHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    // 鼠标移动事件
    moveHandler.setInputAction(function (movement) {
      // console.log('moveHandler+++++')
      if (Cesium.defined(selectedPlane)) {
        console.log('moveHandler---')
        console.log(movement)
        const deltaY = movement.startPosition.y - movement.endPosition.y;
        targetY += deltaY;
        console.log(targetY)
      }
    }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

完整代码

用的是react,直接放到useEffect里了,相当于Vue的mounted生命周期。

// @ts-nocheck
import React, { useState, useEffect, useRef, useCallback } from 'react'
import * as Cesium from 'cesium'

const Clip = () => {

  useEffect(() => {
    // Add a clipping plane, a plane geometry to show the representation of the
    // plane, and control the magnitude of the plane distance with the mouse.
    const viewer = new Cesium.Viewer("cesiumContainer", {
      infoBox: false,
      selectionIndicator: false,
    });
    const scene = viewer.scene;

    let targetY = 0.0;
    let planeEntities = [];
    let selectedPlane;
    let clippingPlanes;

    // Select plane when mouse down
    // 绑定上移动事件
    const downHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    downHandler.setInputAction(function (movement) {
      const pickedObject = scene.pick(movement.position);
  
      console.log('pickedObject---downHandler---', pickedObject)
      if (
        Cesium.defined(pickedObject) &&
        Cesium.defined(pickedObject.id.plane)
      ) {
        selectedPlane = pickedObject.id.plane;
        selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.5);
        selectedPlane.outlineColor = Cesium.Color.WHITE;
        scene.screenSpaceCameraController.enableInputs = false;
      }
    }, Cesium.ScreenSpaceEventType.LEFT_DOWN);

    // Release plane on mouse up
    // 绑定下移动事件
    const upHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    upHandler.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);

    // Update plane on mouse move
    const moveHandler = new Cesium.ScreenSpaceEventHandler(
      viewer.scene.canvas
    );
    moveHandler.setInputAction(function (movement) {
      // console.log('moveHandler+++++')
      if (Cesium.defined(selectedPlane)) {
        console.log('moveHandler---')
        console.log(movement)
        const deltaY = movement.startPosition.y - movement.endPosition.y;
        targetY += deltaY;
        console.log(targetY)
      }
    }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

    function createPlaneUpdateFunction(plane) {
      return function () {
        plane.distance = targetY;
        return plane;
      };
    }

    let tileset: Cesium.Cesium3DTileset;
    function loadTileset(url) {
      // 定义一个ClippingPlaneCollection类,用来存储裁剪面
      clippingPlanes = new Cesium.ClippingPlaneCollection({
        planes: [ // ClippingPlane对象数组集合
          new Cesium.ClippingPlane( // 裁切面
            new Cesium.Cartesian3(0.0, 0.0, -1.0), // 法线方向
            0.0 // 原点到平面的最短距离,设置0就好
          ),
        ],
        edgeWidth: 1.0, // 模型被裁切部分的截面线宽
      });

      tileset = viewer.scene.primitives.add(
        new Cesium.Cesium3DTileset({
          url: url,
          clippingPlanes: clippingPlanes,
        })
      );

      tileset.debugShowBoundingVolume = true
      return tileset.readyPromise
        .then(function () {
          // cesium.BoundingSphere(center:Cartersian3, radius:number), tileset的球状边界
          const boundingSphere = tileset.boundingSphere;
          const radius = boundingSphere.radius;

          viewer.zoomTo(
            tileset,
            new Cesium.HeadingPitchRange(0.5, -0.2, radius * 4.0)
          );
          debugger;
            
          if (
            !Cesium.Matrix4.equals(   // Cesium.Matrix4.equals(a,b)判断两个四维矩阵是否相等
              tileset.root.transform, // 整个根节点模型矩阵,该tileSet=>世界坐标系
              Cesium.Matrix4.IDENTITY // 单位矩阵,对角线值为1.0的4*4矩阵
            )
          ) {
            // 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.
            
            // 获取模型的世界坐标(笛卡尔)
            // Cesium.Matrix4.getTranslation 通过仿射变换矩阵获取该tileSet的世界坐标
            const transformCenter = Cesium.Matrix4.getTranslation(
              tileset.root.transform,
              new Cesium.Cartesian3()
            );
            debugger;
            // 将笛卡尔坐标转换为WGS84经纬度坐标(模型的)
            const transformCartographic = Cesium.Cartographic.fromCartesian(
              transformCenter
            );
            // 将笛卡尔坐标转换为WGS84经纬度坐标(截面的)
            const boundingSphereCartographic = Cesium.Cartographic.fromCartesian(
              tileset.boundingSphere.center
            );
            const height =
              boundingSphereCartographic.height - transformCartographic.height;
           
            // 从一个Cartesian3对象生成Matrix4变换矩阵(裁切面的)
            clippingPlanes.modelMatrix = Cesium.Matrix4.fromTranslation(
              new Cesium.Cartesian3(0.0, 0.0, height)
            );
          }
          // debugger;

          // 创建添加裁剪平面
          for (let i = 0; i < clippingPlanes.length; ++i) {
            const plane = clippingPlanes.get(i);
            const planeEntity = 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(
                  // 添加绑定事件,不断调用
                  createPlaneUpdateFunction(plane),
                  false
                ),
                outline: true,
                outlineColor: Cesium.Color.WHITE,
              },
            });

            planeEntities.push(planeEntity);
          }
          return tileset;
        })
        .catch(function (error) {
          console.log(error);
        });
    }

    // function loadModel(url) {
    //   clippingPlanes = new Cesium.ClippingPlaneCollection({
    //     planes: [
    //       new Cesium.ClippingPlane(
    //         new Cesium.Cartesian3(0.0, 0.0, -1.0),
    //         0.0
    //       ),
    //     ],
    //     edgeWidth:  1.0 ,
    //   });

    //   const position = Cesium.Cartesian3.fromDegrees(
    //     -123.0744619,
    //     44.0503706,
    //     300.0
    //   );
    //   const heading = Cesium.Math.toRadians(135.0);
    //   const pitch = 0.0;
    //   const roll = 0.0;
    //   const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
    //   const orientation = Cesium.Transforms.headingPitchRollQuaternion(
    //     position,
    //     hpr
    //   );
    //   const entity = viewer.entities.add({
    //     name: url,
    //     position: position,
    //     orientation: orientation,
    //     model: {
    //       uri: url,
    //       scale: 8,
    //       minimumPixelSize: 100.0,
    //       clippingPlanes: clippingPlanes,
    //     },
    //   });

    //   viewer.trackedEntity = entity;

    //   for (let i = 0; i < clippingPlanes.length; ++i) {
    //     const plane = clippingPlanes.get(i);
    //     const planeEntity = viewer.entities.add({
    //       position: position,
    //       plane: {
    //         dimensions: new Cesium.Cartesian2(300.0, 300.0),
    //         material: Cesium.Color.WHITE.withAlpha(0.1),
    //         plane: new Cesium.CallbackProperty(
    //           createPlaneUpdateFunction(plane),
    //           false
    //         ),
    //         outline: true,
    //         outlineColor: Cesium.Color.WHITE,
    //       },
    //     });

    //     planeEntities.push(planeEntity);
    //   }
    // }

    // Power Plant design model provided by Bentley Systems
    const bimUrl = Cesium.IonResource.fromAssetId(8564);
    // const pointCloudUrl = Cesium.IonResource.fromAssetId(16421);
    // const instancedUrl =
    //   "../SampleData/Cesium3DTiles/Instanced/InstancedOrientation/tileset.json";
    // const modelUrl = "../SampleData/models/CesiumAir/Cesium_Air.glb";

    loadTileset(bimUrl);

    // Track and create the bindings for the view model
    // function reset() {
    //   viewer.entities.removeAll();
    //   viewer.scene.primitives.remove(tileset);
    //   planeEntities = [];
    //   targetY = 0.0;
    //   tileset = undefined;
    // }


  }, [])


  return (
    <>
      <div id="cesiumContainer" className="fullSize" style={{ height: '100%' }}></div>
    </>


  )
}

export default Clip

总结

经验就是,多查官网文档,多打断点调试看看每一步生成什么,慢慢就理解了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值