站内搜索

输入关键词开始搜索

Cesium 动态电子围栏:WallGeometry 与流动材质

文章目录

电子围栏的视觉部分可以拆成两层:WallGeometry 负责墙体形状,自定义 Material 负责纹理、发光和流动动画。

它只解决“把边界画成动态墙体”。进入、离开和越界报警属于空间判断层,不能因为墙显示出来就认为报警功能已经完成。

把绘制结果转换成闭合边界

绘制工具通常返回 Cartesian3[]。围栏管理器可以使用经纬度数组作为稳定输入:

const coordinates = positions.map(position => {
  const point = Cesium.Cartographic.fromCartesian(position)
  return [
    Cesium.Math.toDegrees(point.longitude),
    Cesium.Math.toDegrees(point.latitude),
  ] as [number, number]
})

创建墙体前检查并闭合首尾:

function closeRing(coordinates: Array<[number, number]>) {
  if (coordinates.length < 3) {
    throw new Error('围栏至少需要三个点')
  }

  const result = coordinates.slice()
  const first = result[0]
  const last = result[result.length - 1]

  if (first[0] !== last[0] || first[1] !== last[1]) {
    result.push(first)
  }

  return result
}

WallGeometry 需要上下边界

const ring = closeRing(coordinates)
const degreesWithHeights = ring.flatMap(([longitude, latitude]) => [
  longitude,
  latitude,
  0,
])

const wallGeometry = new Cesium.WallGeometry({
  positions: Cesium.Cartesian3.fromDegreesArrayHeights(
    degreesWithHeights,
  ),
  maximumHeights: new Array(ring.length).fill(height),
  minimumHeights: new Array(ring.length).fill(0),
})

positions 决定墙沿哪条边界延伸,maximumHeights 和 minimumHeights 决定每个顶点的上下高度。

这段代码的底部是椭球高 0,不是“自动贴地”。真实地形起伏明显时,应先沿边界加密,并用 sampleTerrainMostDetailed 得到每个点的地形高程:

minimumHeights[index] = terrainHeight
maximumHeights[index] = terrainHeight + fenceHeight

自定义 Material 保存动画参数

const material = new Cesium.Material({
  fabric: {
    type: 'FlowWall',
    uniforms: {
      image: '/textures/fence/arrow.png',
      time: 0,
      speed: 1,
      repeatCount: 5,
      glowColor: Cesium.Color.CYAN,
    },
    source: `
      czm_material czm_getMaterial(czm_materialInput input)
      {
        czm_material material = czm_getDefaultMaterial(input);
        vec2 uv = vec2(
          fract(input.st.s),
          fract(repeatCount * input.st.t - time * speed)
        );

        vec4 tex = texture(image, uv);
        material.diffuse = tex.rgb * glowColor.rgb;
        material.alpha = tex.a * glowColor.a;
        material.emission = tex.rgb * glowColor.rgb;
        return material;
      }
    `,
  },
})

墙体几何本身不动。动画只修改 time,片元着色器据此平移 UV。

竖向流动:

uv = vec2(st.s, fract(repeatCount * st.t - time * speed));

横向流动:

uv = vec2(fract(repeatCount * st.s - time * speed), st.t);

fract 把坐标限制回 [0, 1),纹理因此循环出现。

组合 Primitive

const primitive = new Cesium.Primitive({
  geometryInstances: new Cesium.GeometryInstance({
    geometry: wallGeometry,
  }),
  appearance: new Cesium.MaterialAppearance({
    material,
    translucent: true,
    faceForward: true,
  }),
  asynchronous: false,
})

viewer.scene.primitives.add(primitive)

如果贴图包含透明背景,translucent 必须开启。围栏正反两侧都要能看见时,可以使用 faceForward 或检查材质、背面剔除设置。

动画控制器必须能停止

function createAnimationController(
  material: Cesium.Material,
  duration: number,
) {
  let frameId: number | null = null
  let startedAt = performance.now()

  const tick = (now: number) => {
    material.uniforms.time = ((now - startedAt) / duration) % 1
    viewer.scene.requestRender()
    frameId = requestAnimationFrame(tick)
  }

  return {
    start() {
      if (frameId !== null) return
      startedAt = performance.now()
      frameId = requestAnimationFrame(tick)
    },
    stop() {
      if (frameId === null) return
      cancelAnimationFrame(frameId)
      frameId = null
    },
  }
}

显式调用 requestRender 可以兼容 Viewer 开启 requestRenderMode 的场景。组件卸载时如果只删除 Primitive,不停止 requestAnimationFrame,闭包仍会持续访问已经移除的 Material。

用 ID 管理多道围栏

interface FenceRecord {
  primitive: Cesium.Primitive
  controller: ReturnType<typeof createAnimationController>
}

const fences = new Map<string, FenceRecord>()

function removeFence(id: string) {
  const record = fences.get(id)
  if (!record) return

  record.controller.stop()
  viewer.scene.primitives.remove(record.primitive)
  fences.delete(id)
}

function destroy() {
  for (const id of fences.keys()) removeFence(id)
}

同一个 ID 重新创建前先移除旧围栏,可以防止重复动画和残留 WebGL 资源。

视觉围栏不等于报警

要判断移动目标是否进入围栏,需要单独保存多边形坐标和目标上一次状态:

interface FenceState {
  polygon: Array<[number, number]>
  targetInside: boolean
}

function updateTarget(
  fence: FenceState,
  longitude: number,
  latitude: number,
) {
  const inside = pointInPolygon(
    longitude,
    latitude,
    fence.polygon,
  )

  if (!fence.targetInside && inside) {
    emit('enter')
  } else if (fence.targetInside && !inside) {
    emit('leave')
  }

  fence.targetInside = inside
}

这个状态在防重复报警:目标连续十帧都位于围栏内,只触发一次 enter,而不是每帧触发。

点在多边形内判断只处理二维平面。如果围栏还有限高和最低高度,必须同时判断目标高度:

const inside3D =
  inside2D &&
  targetHeight >= minimumHeight &&
  targetHeight <= maximumHeight

需要明确的边界

  • 当前墙体贴图效果依赖纹理透明通道和 UV 方向。
  • 地形起伏场景要采样上下高度,不能统一从 0 米开始。
  • 大范围和跨日期变更线的围栏不能直接使用普通二维射线法。
  • 视觉墙体、空间包含判断、报警去重和报警持久化是四个不同模块。
  • 动画销毁必须早于 Viewer 销毁。

动态电子围栏的渲染主线是 WallGeometry → Material → time uniform。业务主线则是 目标位置 → 空间包含 → 状态变化 → 报警事件。两条线可以联动,但不能混成一个类。

参考资料

相关文章

Cesium 三维风场:用 WebGL 2 GPU 粒子描绘全球气流

拆解案例中的风场数据、GPU 粒子更新、轨迹渲染、颜色映射和 Vue 参数控制

Cesium 北斗三维网格:在城市 3D Tiles 上做 GeoSOT-3D 剖分

介绍区域体素生成、建筑表面采样、结构碰撞检测、Worker 几何构建和网格编码查询