文章目录
电子围栏的视觉部分可以拆成两层: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。业务主线则是 目标位置 → 空间包含 → 状态变化 → 报警事件。两条线可以联动,但不能混成一个类。