CesiumJS中针对Primitive的高性能聚合Clustering方法

项目中有用到CesiumJS,

大概需求是在地球上建立几千个Entity. 并且随着地球的放大缩小,可以做到按区域聚合并显示这一区域的目标数量

如果只是用Cesium中的Entity,则非常简单,api中有基于dataSource的clustering方法

官方demo: https://sandcastle.cesium.com/?src=Clustering.html

API: https://cesium.com/learn/cesiumjs/ref-doc/DataSource.html?classFilter=datas

效果如下:

image

但是重点来了

我们的项目中有几千个Entity, 甚至上万个,官方Demo中只有几百个。

Web页面中加载上万个Entity 性能消耗太大,导致帧数降为0,巨卡无比。

因为Entity封装了太多复杂的功能,而我们需要建立的Target只是一个个的静态目标,

简单来说就只需要一个Billboard和一个Label

所以用Primitive的方式就能满足。

然而Primitive并没有自带的Clustering功能,没办法,只有看看Entity的源码了 T_T


经过看源码+求大佬经验,最后实现效果如下:

(GIF有点小,上万个目标聚合帧数60帧)

image

思路简介:

一个main.js,实现添加目标功能
一个库函数cluster.js,实现对目标的聚合功能

==== main.js ===

  1. 定义两个Collection,将一个目标分为一个Label和一个Billboard添加进Collection中
targetDataSource = {
  labels: new Cesium.LabelCollection(),
  billboards: new Cesium.BillboardCollection()
}
  1. 开启Cluster功能:
Cluster.init(viewer, {
  billboards: targetDataSource.billboards,
  labels: targetDataSource.labels,
})
  1. 更新Cluster
Cluster.update(targetDataSource, true)

=== cluster.js 核心函数 ===

  1. init()函数,
export const init = (viewer, option) => {
  if (!viewer) return
  scene = viewer.scene
  entityCluster = {
    _enabled: false,
    _billboardCollection: option.billboards,
    _labelCollection: option.labels,

    _clusterBillboardCollection: undefined, //所有显示在视口内的billboard 包括target和聚合簇的billboard
    _clusterLabelCollection: undefined, //所有显示在视口内的label

    _pixelRange: option.pixelRange || 100,
    _minimumClusterSize: option.minimumClusterSize || 10,
    _previousClusters: null,
    _previousHeight: undefined,

    _labelShow: true,
  }
  setEnabled(true)
}
  1. setEnable()函数,添加相机改变的监听
/**
 * 设置是否开启聚合
 * @param {Boolean} value
 */
export const setEnabled = (value) => {
  if (entityCluster._enabled !== value) {
    entityCluster._enabled = value
    if (entityCluster._enabled && !eventListener) {
      eventListener = scene.camera.changed.addEventListener(
        createDeclutterCallback
      )
      createDeclutterCallback()
    } else {
      scene.camera.changed.removeEventListener(createDeclutterCallback)
      eventListener = null
    }
  }
}
  1. 相机监听的事件回调,大概思路就是,

(1)遍历所有的点,将他们从三维坐标转换成屏幕坐标

(2)根据pixelRange的范围画出一个区域getBoundingBox

(3)然后根据kdbush的聚类算法,找到当前点的所有邻居点的数量,进行聚合

(4)已经聚合的标识clustered = true 未聚合的标识clustered = false

这里有一个很重要的kdbush的聚类算法 ,具体原理如果想知道可以搜一下

function createDeclutterCallback(amount) {
  if (defined(amount) && amount < 0.05) {
    return
  }

  let pixelRange = entityCluster._pixelRange
  let minimumClusterSize = entityCluster._minimumClusterSize

  // 清空
  if (defined(entityCluster._clusterLabelCollection)) {
    entityCluster._clusterLabelCollection.removeAll()
  } else {
    entityCluster._clusterLabelCollection = scene.primitives.add(
      new LabelCollection({ scene: scene })
    )
  }

  if (defined(entityCluster._clusterBillboardCollection)) {
    entityCluster._clusterBillboardCollection.removeAll()
  } else {
    entityCluster._clusterBillboardCollection = scene.primitives.add(
      new BillboardCollection({ scene: scene })
    )
  }

  let ellipsoid = scene.mapProjection.ellipsoid
  let cameraPosition = scene.camera.positionWC
  let occluder = new EllipsoidalOccluder(ellipsoid, cameraPosition) //椭球坐标投影到平面坐标相关的函数
  let points = [] //points当前视口内显示的所有点以及他们在屏幕上的坐标  (label 和 billboard)

  // 被过滤了
  getScreenSpacePositions(
    entityCluster._labelCollection,
    points,
    scene,
    occluder
  )

  getScreenSpacePositions(
    entityCluster._billboardCollection,
    points,
    scene,
    occluder
  )

  let i
  let j
  let length
  let bbox
  let neighbors
  let neighborLength
  let neighborIndex
  let neighborPoint
  let ids
  let numPoints

  let collection
  let collectionIndex


  let index = kdbush(points, getX, getY, 64, Int32Array)


  for (i = 0; i < points.length; ++i) {
    let point = points[i]
    if (point.clustered) {
      continue
    }
    point.clustered = true

    collection = point.collection
    collectionIndex = point.index

    let item = collection.get(collectionIndex)
    bbox = getBoundingBox(
      item,
      point,
      pixelRange,
      entityCluster,
      pointBoundinRectangleScratch
    )

    // 时间很短
    neighbors = index.range(
      bbox.x,
      bbox.y,
      bbox.x + bbox.width,
      bbox.y + bbox.height
    )

    neighborLength = neighbors.length

    let clusterPosition = Cartesian3.clone(item.position)
    numPoints = 1
    ids = [item.id]

    numPoints = neighborLength

    if (numPoints >= minimumClusterSize) {
      for (j = 0; j < neighborLength; ++j) {
        neighborIndex = neighbors[j]
        neighborPoint = points[neighborIndex]
        if (!neighborPoint.clustered) {
          neighborPoint.clustered = true
          let neighborItem = neighborPoint.collection.get(neighborPoint.index)
          Cartesian3.add(
            neighborItem.position,
            clusterPosition,
            clusterPosition
          )
          ids.push(neighborItem.id)
        }
      }

      let position = Cartesian3.multiplyByScalar(
        clusterPosition,
        1.0 / numPoints,
        clusterPosition
      )
      addCluster(position, numPoints, ids, entityCluster)
    } else {
      addNonClusteredItem(item, point, entityCluster)
    }
  }

}

///
getScreenSpacePositions() 这个函数的就是找出目前屏幕上显示的所有target,然后转换成屏幕坐标,保存在points数组里。

/**
 * 找出所有视口区域内的点,放进points数组里
 * @param {*} collection
 * @param {*} points
 * @param {*} scene
 * @param {*} occluder
 * @returns
 */
function getScreenSpacePositions(collection, points, scene, occluder) {
  if (!defined(collection)) {
    return
  }
  let length = collection.length //所有点

  for (let i = 0; i < length; ++i) {
    let item = collection.get(i)
    if (
      !item.show ||
      (scene.mode === SceneMode.SCENE3D &&
        !occluder.isPointVisible(item.position))
    ) {
      continue
    }
    if (scene.mode === SceneMode.SCENE2D) {
      //2D模式下,根据经纬度判断目标是否在屏幕内

      //3D坐标转2D坐标
      let itemPosition = SceneTransforms.wgs84ToWindowCoordinates(
        scene,
        item.position
      )

      let canvas = viewer.scene.canvas
      let upperLeft = new Cartesian2(0, 0) //canvas左上角坐标转2d坐标
      let lowerRight = new Cartesian2(canvas.clientWidth, canvas.clientHeight) //canvas右下角坐标转2d坐标

      if (
        !(
          itemPosition.x > upperLeft.x &&
          itemPosition.x < lowerRight.x &&
          itemPosition.y > upperLeft.y &&
          itemPosition.y < lowerRight.y
        )
      ) {
        // console.log("不在屏幕内", item.id)
        // console.log(item)
        continue
      }
    }

    //过滤掉label的   只留下billboard的
    if (defined(item._text)) {
      continue
    }

    let coord = item.computeScreenSpacePosition(scene)
    if (!defined(coord)) {
      continue
    }

    points.push({
      index: i,
      collection: collection,
      clustered: false,
      coord: coord,
    })
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容