cesium学习(六)-Entity

4 阅读11分钟

什么是 Entity

Entity 是 Cesium 提供的高层业务对象系统。

它的作用是:用更接近业务的方式描述场景中的对象,比如点、线、面、图标、文字、模型、轨迹等。

Entity = 数据描述 + 显示样式 + 时间动态

比如一个无人机,可以被描述成:

  • 当前位置
  • 图标
  • 名称标签
  • 飞行轨迹
  • 模型
  • 随时间变化的位置

这些都可以放在一个 Entity 里。

Entity 和 Primitive 的关系

Entity 本身不是最终的底层渲染对象。

真实渲染链路是:

Entity
  ↓
Visualizer
  ↓
Primitive
  ↓
Scene
  ↓
GPU

也就是说:

Entity 负责好用
Primitive 负责渲染

Entity 更适合业务开发,Primitive 更适合性能优化和大规模静态数据。

Entity 在 Viewer 中的位置

Entity 通常通过 viewer.entities 管理。

const entity = viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  point: {
    pixelSize: 10,
    color: Cesium.Color.RED
  }
})

结构关系:

Viewer
 ├─ entities
 │  ├─ Entity
 │  ├─ Entity
 │  └─ Entity
 └─ scene
    └─ primitives

viewer.entities 本质上是一个 EntityCollection

Entity 的关键字段

每个 Entity 除了具体图形(point / billboard / polyline 等),还有一些和图形无关的通用字段:

字段说明
id唯一标识,业务里常直接用业务 id
name名称,会显示在 InfoBox 标题里
descriptionHTML 描述,点击 Entity 后弹出 InfoBox
position空间位置,可以是常量也可以是 Property
orientation方向(四元数),影响模型 / 几何体姿态
parent父 Entity,子节点跟随父节点变换
show是否显示
availability时间可用范围

配合 description 可以直接出弹窗:

viewer.entities.add({
  id: 'device-001',
  name: '设备 001',
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0),
  description: `
    <h3>设备 001</h3>
    <p>状态:在线</p>
    <p>电量:92%</p>
  `,
  billboard: { image: '/device.png' }
})

Cesium 默认会在点击时显示 InfoBoxSelectionIndicator。如果业务自己做了弹窗,可以在 Viewer 创建时关掉:

const viewer = new Cesium.Viewer('cesium', {
  infoBox: false,
  selectionIndicator: false
})

Entity 可以表示什么

一个 Entity 可以同时包含多个图形属性。

属性表现
position空间位置
point
billboard图标
label文字
polyline线
polygon
ellipse椭圆 / 圆
rectangle矩形
box盒子
cylinder圆柱 / 圆锥
ellipsoid椭球
modelglTF / glb 模型
path轨迹

例如,一个 Entity 可以同时有图标和文字:

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  billboard: {
    image: '/marker.png',
    scale: 1
  },
  label: {
    text: '设备 A',
    font: '16px sans-serif',
    pixelOffset: new Cesium.Cartesian2(0, -30)
  }
})

创建点

viewer.entities.add({
  id: 'point-1',
  name: '测试点',
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  point: {
    pixelSize: 10,
    color: Cesium.Color.YELLOW,
    outlineColor: Cesium.Color.BLACK,
    outlineWidth: 2
  }
})

常用属性:

属性说明
pixelSize点大小,单位像素
color点颜色
outlineColor边框颜色
outlineWidth边框宽度
heightReference高度参考
disableDepthTestDistance距离多远后关闭深度测试

创建图标 Billboard

billboard 常用于地图标记点、设备图标、告警图标。

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  billboard: {
    image: '/marker.png',
    width: 32,
    height: 32,
    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
    heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND
  }
})

常用属性:

属性说明
image图标地址
width / height图标大小
scale缩放
pixelOffset屏幕像素偏移
verticalOrigin垂直对齐方式
horizontalOrigin水平对齐方式
heightReference高度参考

创建文字 Label

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  label: {
    text: '北京',
    font: '16px sans-serif',
    fillColor: Cesium.Color.WHITE,
    outlineColor: Cesium.Color.BLACK,
    outlineWidth: 2,
    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
    pixelOffset: new Cesium.Cartesian2(0, -30)
  }
})

常见用途:

  • 设备名称
  • 地名标注
  • 测点编号
  • 区域名称
  • 告警信息

创建线 Polyline

viewer.entities.add({
  name: '测试线',
  polyline: {
    positions: Cesium.Cartesian3.fromDegreesArray([
      116.3, 39.8,
      116.4, 39.9,
      116.5, 39.85
    ]),
    width: 4,
    material: Cesium.Color.CYAN
  }
})

如果需要带高度:

viewer.entities.add({
  polyline: {
    positions: Cesium.Cartesian3.fromDegreesArrayHeights([
      116.3, 39.8, 100,
      116.4, 39.9, 200,
      116.5, 39.85, 150
    ]),
    width: 4,
    material: Cesium.Color.RED
  }
})

贴地线:

viewer.entities.add({
  polyline: {
    positions: Cesium.Cartesian3.fromDegreesArray([
      116.3, 39.8,
      116.4, 39.9
    ]),
    width: 4,
    material: Cesium.Color.YELLOW,
    clampToGround: true
  }
})

Polyline 常用 material

material 不止是颜色,还可以传材质 Property:

材质效果
Color纯色
PolylineOutlineMaterialProperty带描边的线
PolylineDashMaterialProperty虚线
PolylineArrowMaterialProperty箭头线,常用于方向
PolylineGlowMaterialProperty流光 / 发光
polyline: {
  positions: ...,
  width: 6,
  material: new Cesium.PolylineDashMaterialProperty({
    color: Cesium.Color.CYAN,
    dashLength: 16
  })
}

箭头线常用于方向标记:

material: new Cesium.PolylineArrowMaterialProperty(Cesium.Color.RED)

创建面 Polygon

viewer.entities.add({
  polygon: {
    hierarchy: Cesium.Cartesian3.fromDegreesArray([
      116.3, 39.8,
      116.5, 39.8,
      116.5, 40.0,
      116.3, 40.0
    ]),
    material: Cesium.Color.RED.withAlpha(0.4),
    outline: true,
    outlineColor: Cesium.Color.WHITE
  }
})

贴地面:

viewer.entities.add({
  polygon: {
    hierarchy: Cesium.Cartesian3.fromDegreesArray([
      116.3, 39.8,
      116.5, 39.8,
      116.5, 40.0,
      116.3, 40.0
    ]),
    material: Cesium.Color.GREEN.withAlpha(0.4),
    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
  }
})

注意:贴地效果依赖地形和渲染能力,复杂面或大量面可能需要考虑 GroundPrimitive

拉伸成 3D 体块:

viewer.entities.add({
  polygon: {
    hierarchy: Cesium.Cartesian3.fromDegreesArray([
      116.3, 39.8,
      116.5, 39.8,
      116.5, 40.0,
      116.3, 40.0
    ]),
    extrudedHeight: 5000,
    material: Cesium.Color.ORANGE.withAlpha(0.6),
    outline: true,
    outlineColor: Cesium.Color.BLACK
  }
})

带洞的多边形(外环 + 内环):

viewer.entities.add({
  polygon: {
    hierarchy: new Cesium.PolygonHierarchy(
      Cesium.Cartesian3.fromDegreesArray([
        116.3, 39.8,
        116.5, 39.8,
        116.5, 40.0,
        116.3, 40.0
      ]),
      [
        new Cesium.PolygonHierarchy(
          Cesium.Cartesian3.fromDegreesArray([
            116.35, 39.85,
            116.45, 39.85,
            116.45, 39.95,
            116.35, 39.95
          ])
        )
      ]
    ),
    material: Cesium.Color.RED.withAlpha(0.4)
  }
})

创建模型 Model

Entity 可以直接加载 glTF / glb 模型。

viewer.entities.add({
  name: '无人机',
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 300),
  model: {
    uri: '/models/drone.glb',
    scale: 1,
    minimumPixelSize: 64,
    maximumScale: 200
  }
})

常用属性:

属性说明
uri模型地址
scale缩放
minimumPixelSize最小显示像素
maximumScale最大缩放
heightReference高度参考
silhouetteColor轮廓颜色
silhouetteSize轮廓宽度

如果需要大量模型或更底层控制,可以使用 Cesium.Model.fromGltfAsync() 加到 scene.primitives

Box / Cylinder / Ellipsoid

除了点、线、面、模型,Entity 还支持几种常见几何体。

盒子:

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 50),
  box: {
    dimensions: new Cesium.Cartesian3(100, 100, 100),
    material: Cesium.Color.BLUE.withAlpha(0.5),
    outline: true,
    outlineColor: Cesium.Color.BLACK
  }
})

圆柱 / 圆锥(顶半径为 0 时变圆锥):

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0),
  cylinder: {
    length: 200,
    topRadius: 0,
    bottomRadius: 50,
    material: Cesium.Color.YELLOW.withAlpha(0.5)
  }
})

球 / 椭球:

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  ellipsoid: {
    radii: new Cesium.Cartesian3(80, 80, 80),
    material: Cesium.Color.RED.withAlpha(0.5)
  }
})

这些图形的姿态由 Entity 的 orientation 控制(见后文)。

path 轨迹

path 常用于显示动态对象的历史轨迹。

const entity = viewer.entities.add({
  availability: new Cesium.TimeIntervalCollection([
    new Cesium.TimeInterval({
      start,
      stop
    })
  ]),
  position: sampledPosition,
  path: {
    resolution: 1,
    material: Cesium.Color.YELLOW,
    width: 3
  }
})

path 通常和 SampledPositionProperty 一起使用。

Entity 的核心:Property

Entity 最重要的特点是:很多属性都可以是 Property

普通写法:

point: {
  color: Cesium.Color.RED
}

动态写法:

point: {
  color: new Cesium.CallbackProperty(() => {
    return Cesium.Color.RED.withAlpha(Math.random())
  }, false)
}

可以理解为:

普通值 = 固定不变
Property = 可以随时间变化

当你写 color: Cesium.Color.RED 时,Cesium 内部会自动把它包装成 ConstantProperty。所以平时不需要手动 new ConstantProperty(),只在显式构造常量属性时才用得到。

常见 Property:

Property作用
ConstantProperty常量属性
CallbackProperty每帧动态计算
SampledPositionProperty按时间采样的位置
TimeIntervalCollectionProperty不同时间段不同值
CompositeProperty组合多个时间属性
ReferenceProperty引用其它 Entity 属性

CallbackProperty

CallbackProperty 适合做实时变化的属性。

例如动态半径:

let radius = 100

viewer.entities.add({
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0),
  ellipse: {
    semiMajorAxis: new Cesium.CallbackProperty(() => {
      radius += 1
      return radius
    }, false),
    semiMinorAxis: new Cesium.CallbackProperty(() => {
      return radius
    }, false),
    material: Cesium.Color.RED.withAlpha(0.3)
  }
})

注意:CallbackProperty 会被频繁调用,不要在里面做接口请求、复杂计算或大量对象创建。

SampledPositionProperty

SampledPositionProperty 用来描述一个对象随时间变化的位置,常用于轨迹、回放、仿真。

const property = new Cesium.SampledPositionProperty()

const start = Cesium.JulianDate.now()
const time1 = Cesium.JulianDate.addSeconds(start, 0, new Cesium.JulianDate())
const time2 = Cesium.JulianDate.addSeconds(start, 10, new Cesium.JulianDate())

property.addSample(
  time1,
  Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100)
)

property.addSample(
  time2,
  Cesium.Cartesian3.fromDegrees(116.40, 39.91, 150)
)

const entity = viewer.entities.add({
  position: property,
  point: {
    pixelSize: 10,
    color: Cesium.Color.YELLOW
  },
  path: {
    material: Cesium.Color.CYAN,
    width: 3
  }
})

viewer.clock.startTime = start.clone()
viewer.clock.currentTime = start.clone()
viewer.clock.shouldAnimate = true
viewer.trackedEntity = entity

这就是 Entity 很适合做动态轨迹的原因。

orientation 和模型朝向

模型 / 几何体 Entity 常常需要朝向运动方向。VelocityOrientationProperty 会根据 position 自动算出朝向:

entity.orientation = new Cesium.VelocityOrientationProperty(entity.position)

适合:

  • 车辆沿轨迹行驶
  • 无人机航线飞行
  • 船舶轨迹

如果只是固定朝向,可以用 HeadingPitchRoll 转四元数:

const position = Cesium.Cartesian3.fromDegrees(116.39, 39.9, 0)
const hpr = new Cesium.HeadingPitchRoll(
  Cesium.Math.toRadians(90), 0, 0
)

entity.position = position
entity.orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)

availability

availability 用来控制 Entity 在什么时间范围内可见。

entity.availability = new Cesium.TimeIntervalCollection([
  new Cesium.TimeInterval({
    start,
    stop
  })
])

常见于:

  • 轨迹回放
  • 历史车辆
  • 无人机飞行任务
  • 时间轴动态数据

trackedEntity

trackedEntity 可以让相机跟随某个 Entity。

viewer.trackedEntity = entity

常见场景:

  • 跟随无人机
  • 跟随车辆
  • 跟随人员
  • 飞行轨迹回放

取消跟随:

viewer.trackedEntity = undefined

flyTo 单次飞行

viewer.flyTo(entity) 是一次性飞过去,不会跟随:

viewer.flyTo(entity)

也支持 EntityCollectionDataSource

viewer.flyTo(dataSource)

trackedEntity 的区别:

flyTo         = 飞一次,到位后停止
trackedEntity = 一直跟着 Entity 移动

selectedEntity

viewer.selectedEntity 表示当前选中的 Entity,会同步显示 SelectionIndicatorInfoBox

viewer.selectedEntity = entity

业务上经常这样用:

handler.setInputAction((movement) => {
  const picked = viewer.scene.pick(movement.position)

  if (picked && picked.id instanceof Cesium.Entity) {
    viewer.selectedEntity = picked.id
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)

清除选中:

viewer.selectedEntity = undefined

Entity 拾取

Entity 可以通过 scene.pick 拾取。

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

handler.setInputAction((movement) => {
  const picked = viewer.scene.pick(movement.position)

  if (!Cesium.defined(picked)) {
    return
  }

  const entity = picked.id

  if (entity instanceof Cesium.Entity) {
    console.log(entity.id, entity.name)
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)

因为 Entity 的 id 可以自己设置,所以非常适合和业务数据关联。

viewer.entities.add({
  id: 'device-001',
  name: '设备 001',
  position,
  billboard: {
    image: '/device.png'
  }
})

如果多个 Entity 在同一像素位置重叠,scene.pick 只会拿到最上面那个。需要拿到所有命中可以用 drillPick

const picks = viewer.scene.drillPick(movement.position)

picks.forEach((p) => {
  if (p.id instanceof Cesium.Entity) {
    console.log(p.id.id)
  }
})

drillPickpick 重,不要每帧调用。

EntityCollection

viewer.entities 是一个 EntityCollection,常用方法如下:

方法说明
add()添加 Entity
remove()删除指定 Entity
removeById()按 id 删除
removeAll()删除全部 Entity
getById()按 id 获取
contains()判断是否包含
values获取全部 Entity

示例:

const entity = viewer.entities.getById('device-001')

if (Cesium.defined(entity)) {
  viewer.entities.remove(entity)
}

删除全部 Entity:

viewer.entities.removeAll()

注意:removeAll() 只会清理 viewer.entities,不会删除 scene.primitives 里的 3D Tiles、Primitive、Model,也不会清理 viewer.dataSources 里的 Entity。

监听 Entity 增删变化:

viewer.entities.collectionChanged.addEventListener(
  (collection, added, removed, changed) => {
    console.log('added', added.length)
    console.log('removed', removed.length)
    console.log('changed', changed.length)
  }
)

可以用来做"统计当前可见 Entity 数量"、"业务数据和场景对象同步"等。

show 控制显示隐藏

Entity 本身有 show 属性。

entity.show = false

也可以控制某个图形属性:

entity.billboard.show = false
entity.label.show = false

如果需要按业务分组显示,可以自己维护数组或 Map:

const deviceEntities = new Map()

deviceEntities.set(device.id, entity)

高度模式

Entity 的点、图标、文字、面、模型等经常会用到高度参考。

heightReference: Cesium.HeightReference.CLAMP_TO_GROUND

常见值:

含义
HeightReference.NONE使用坐标中的高度
HeightReference.CLAMP_TO_GROUND贴地(地形或 3D Tiles,按场景默认)
HeightReference.RELATIVE_TO_GROUND相对地面高度
HeightReference.CLAMP_TO_TERRAIN只贴地形
HeightReference.RELATIVE_TO_TERRAIN只相对地形
HeightReference.CLAMP_TO_3D_TILE贴在 3D Tiles 表面
HeightReference.RELATIVE_TO_3D_TILE相对 3D Tiles 表面

注意:

  • NONE 使用坐标里的高度,通常接近 HAE。
  • CLAMP_TO_GROUND 依赖地形或地表。
  • RELATIVE_TO_GROUND 表示在地面基础上抬高。
  • 贴 3D Tiles 的几种模式要在场景里加载了 Cesium3DTileset 才有效。

按距离控制显示

Entity 的图标、点、文字、模型有几个按距离生效的属性,常用于"远了看不到"或"远了变小":

属性作用
distanceDisplayCondition距离区间内才显示
scaleByDistance按距离缩放
translucencyByDistance按距离改变透明度
pixelOffsetScaleByDistance按距离缩放偏移量
billboard: {
  image: '/device.png',
  distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 50000),
  scaleByDistance: new Cesium.NearFarScalar(1000, 1.5, 50000, 0.5),
  translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 50000, 0.2)
}

"近距离显示文字、远距离只剩图标"这种业务效果,就是用 labelbillboard 分别设置不同的 distanceDisplayCondition 实现的。

DataSource 和 Entity

除了直接使用 viewer.entities,Cesium 还可以通过 DataSource 加载一组 Entity。

常见 DataSource:

DataSource用途
GeoJsonDataSource加载 GeoJSON
CzmlDataSource加载 CZML 动态数据
KmlDataSource加载 KML
CustomDataSource自定义 Entity 分组

例如加载 GeoJSON:

const dataSource = await Cesium.GeoJsonDataSource.load('/data/area.geojson', {
  clampToGround: true
})

viewer.dataSources.add(dataSource)
viewer.flyTo(dataSource)

自定义分组:

const dataSource = new Cesium.CustomDataSource('devices')

dataSource.entities.add({
  id: 'device-001',
  position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),
  point: {
    pixelSize: 10,
    color: Cesium.Color.RED
  }
})

viewer.dataSources.add(dataSource)

如果一类业务数据需要整体显示、隐藏、删除,CustomDataSource 比直接塞到 viewer.entities 更好管理。

Entity 性能边界

Entity 很方便,但不是无限适合海量数据。

大致经验:

数据规模建议
几十、几百个业务对象Entity 很合适
几千个对象需要注意样式、动态属性和更新频率
上万点位优先考虑 Primitive Collection
十万级以上使用 Primitive、3D Tiles、点云或服务端切片

Entity 卡顿常见原因:

  • 数量太多
  • 每个 Entity 都有 label / billboard / path
  • 大量 CallbackProperty
  • 每帧频繁增删 Entity
  • 每帧创建新对象
  • 没有做视野范围过滤

Entity 和 Primitive 怎么选

场景推荐
业务点、设备、车辆、无人机Entity
需要点击、名称、业务 idEntity
需要轨迹回放、时间动态Entity + Property
加载 GeoJSON、CZML、KMLDataSource + Entity
海量静态点PointPrimitiveCollection
海量图标BillboardCollection
海量静态面Primitive
城市模型、倾斜摄影Cesium3DTileset

一句话:

先用 Entity 把业务跑通,性能不够再下沉到 Primitive。

常见问题

  1. Entity 加了但看不到

    常见原因是坐标写错、高度太低、相机没飞过去、颜色透明度为 0,或者 show 被设置成了 false

  2. 点位和底图错位

    Cesium 默认使用 WGS84。如果底图是高德、腾讯、百度等国内坐标系,可能需要处理 GCJ02 / BD09 和 WGS84 的转换。

  3. 贴地对象不贴地

    贴地效果依赖地形、深度和具体图形类型。没有真实地形时,贴的是光滑椭球,不是真实地面。

  4. CallbackProperty 导致卡顿

    CallbackProperty 会频繁执行。不要在里面请求接口,也不要每次都创建大量新对象。

  5. 大量 label 很卡

    Label 本身比较重。大量文字建议做聚合、视野过滤、层级显示,或者换成 LabelCollection

  6. 每次刷新数据都 removeAll()add()

    这种做法简单但容易卡顿。更好的方式是按 id 更新已有 Entity,只新增、删除变化的部分。

  7. Entity 的位置怎么更新?

    静态更新可以直接改:

    entity.position = Cesium.Cartesian3.fromDegrees(116.4, 39.91, 100)
    

    连续动态位置更适合用 SampledPositionPropertyCallbackProperty

  8. 为什么 viewer.entities.removeAll() 没删掉 3D Tiles?

    因为 3D Tiles 加在 viewer.scene.primitives,不属于 viewer.entities

小结

Entity 是 Cesium 中最常用、最适合业务开发的对象系统。

它的核心价值是:

  • 写法简单
  • 适合点线面、图标、文字、模型
  • 支持时间动态
  • 支持拾取和业务 id
  • 能和 DataSource 配合管理数据

实际开发中可以这样选:

业务对象少、中等规模、有动态需求 -> Entity
海量静态渲染、性能压力大        -> Primitive
城市级三维模型、点云、倾斜摄影  -> 3D Tiles