在三维地理可视化项目中,电子围栏(Geo-fence) 是一个常见需求。但静态围栏缺乏视觉冲击力。本文将带你使用 CesiumJS 构建一个 带呼吸脉冲效果的动态围栏系统,包含:
- 🌊 墙体呼吸动画
- 💡 底部发光轮廓线
- ✨ 顶部闪烁警示点
并提供 完整可复用代码,适用于安防、区域监控、智慧城市等场景。
🔧 技术栈说明
| 技术 | 用途 |
|---|---|
Cesium.WallGeometry + Primitive | 构建墙体几何体,支持自定义 Shader 动画 |
PolylineGlowMaterialProperty | 实现底部发光线条(Entity API) |
CallbackProperty + SVG 图标 | 实现顶部动态闪烁点 |
| 自定义 GLSL 材质 | 实现呼吸脉冲效果 |
💡 为什么混合使用 Primitive 和 Entity?
- Primitive:适合高性能、自定义 Shader 的几何体(如墙体)
- Entity:适合简单图形、内置材质(如发光线条、图标)
🎯 最终效果预览
| 组件 | 效果描述 |
|---|---|
| 墙体 | 深蓝色半透明墙体,亮度和透明度随时间正弦波动(呼吸感) |
| 底部线 | 蓝色辉光线条,宽度 5px,柔和发光 |
| 顶部点 | 红色圆形 SVG 图标,每个点独立闪烁,相位不同 |
✅ 支持任意多边形围栏(只需传入坐标数组)
📦 核心代码详解
1️⃣ 定义围栏坐标与高度
首先,我们定义围栏的角点坐标和高度。这些数据决定了围栏的位置和大小。
// 定义围栏的四个角点(按顺序连接形成闭合多边形)
const fencePositions = [
{ lon: 116.397194, lat: 39.909768 },// 第一个角点:左下
{ lon: 116.397194, lat: 39.900768 },// 第二个角点:右下
{ lon: 116.392194, lat: 39.900768 },// 第三个角点:右上
{ lon: 116.392194, lat: 39.909768 }// 第四个角点:左上
];
// 设置围栏墙体的垂直高度(单位:米)
const fenceHeight = 60.0;
⚠️ 注意:最后一个点会自动连接到第一个点,形成闭环。
2️⃣ 呼吸脉冲材质(GLSL Shader)
这是整个效果的核心!我们通过自定义 GLSL 片元着色器,实现墙体的呼吸动画。
// Cesium 自定义材质入口函数(必须命名为 czm_getMaterial)
czm_material czm_getMaterial(czm_materialInput materialInput)
{
// 初始化一个默认材质结构体
czm_material material = czm_getDefaultMaterial(materialInput);
// 获取当前帧号,并乘以时间系数控制动画速度(值越小越慢)
float time = czm_frameNumber * 0.01;
// 生成 [0, 1] 范围内的正弦波动值,模拟“呼吸”节奏
float pulse = 0.5 + 0.5 * sin(time);
// 定义基础颜色:深蓝色(R=0, G=0.6, B=1.0)
vec3 baseColor = vec3(0.0, 0.6, 1.0);
// 透明度随 pulse 波动:最小 0.3,最大 0.9(避免完全透明或不透明)
float alpha = 0.3 + 0.6 * pulse;
// 设置漫反射颜色(物体主色)
material.diffuse = baseColor;
// 设置透明度(需配合 ALPHA_BLEND 才生效)
material.alpha = alpha;
// 设置自发光强度:随 pulse 增强,最大为原色的 1.8 倍(增强视觉冲击)
material.emission = baseColor * pulse * 1.8;
// 返回最终计算出的材质
return material;
}
| 参数 | 作用 | 可调范围 |
|---|---|---|
0.01 | 动画速度 | 越小越慢 |
0.3 + 0.6 * pulse | 透明度范围 | [0.3, 0.9] |
pulse * 1.8 | 发光强度 | 越大越亮 |
3️⃣ 构建墙体(使用 Primitive)
为了高性能渲染,我们使用 Primitive 而非 Entity 来构建墙体。
// 遍历每一条边(从第 i 个点到第 i+1 个点)
for (let i = 0; i < fencePositions.length; i++) {
// 获取当前边的起点
const start = fencePositions[i];
// 获取当前边的终点(% 实现闭环:最后一个点连回第一个点)
const end = fencePositions[(i + 1) % fencePositions.length];
// 创建墙体几何体(WallGeometry 自动拉伸成垂直面)
const wallGeometry = new Cesium.WallGeometry({
// 将两个经纬度点转换为笛卡尔坐标数组
positions: Cesium.Cartesian3.fromDegreesArray([
start.lon, start.lat, // 起点经度、纬度
end.lon, end.lat // 终点经度、纬度
]),
// 设置墙体顶部高度(两个顶点高度相同)
maximumHeights: [fenceHeight, fenceHeight],
// 设置墙体底部高度(贴地)
minimumHeights: [0, 0],
// 指定顶点包含位置和纹理坐标(供 Shader 使用)
vertexFormat: Cesium.VertexFormat.POSITION_AND_ST
});
// 将几何体包装为可渲染的实例
const wallInstance = new Cesium.GeometryInstance({
geometry: wallGeometry
});
// 创建底层渲染对象(Primitive),性能优于 Entity
const wallPrimitive = new Cesium.Primitive({
// 绑定几何实例
geometryInstances: wallInstance,
// 设置外观:使用自定义呼吸材质
appearance: new Cesium.MaterialAppearance({
material: createPulseMaterial(), // 应用前面定义的脉冲材质
closed: false, // 不闭合(墙体是开放面)
flat: false, // 启用光照计算(非纯平色)
renderState: {
// 启用 Alpha 混合,实现半透明效果
blending: Cesium.BlendingState.ALPHA_BLEND,
// 启用深度测试,正确处理遮挡关系
depthTest: { enabled: true },
// 不写入深度缓冲,避免遮挡其他物体
depthMask: false
}
})
});
// 将墙体添加到 3D 场景中
cesiumViewer.scene.primitives.add(wallPrimitive);
}
✅
WallGeometry天然支持“墙”结构,无需手动 extrude。
4️⃣ 底部发光轮廓线(Entity API)
为了简化开发,我们使用 Entity API 添加底部发光线。
// 构建闭合折线的坐标数组(格式:lon, lat, height, ...)
const bottomLine = [];
// 遍历所有围栏点,添加到底部线(高度设为 0.1 米,略高于地面)
fencePositions.forEach(p => bottomLine.push(p.lon, p.lat, 0.1));
// 闭合多边形:将第一个点再次添加到末尾
bottomLine.push(fencePositions[0].lon, fencePositions[0].lat, 0.1);
// 使用 Entity API 添加发光折线(开发更简单)
cesiumViewer.entities.add({
polyline: {
// 将经纬度+高度数组转换为 3D 笛卡尔坐标
positions: Cesium.Cartesian3.fromDegreesArrayHeights(bottomLine),
// 线条宽度(像素)
width: 5,
// 使用 Cesium 内置的发光材质
material: new Cesium.PolylineGlowMaterialProperty({
glowPower: 0.4, // 发光强度(值越大光晕越扩散)
color: Cesium.Color.BLUE.withAlpha(0.7) // 蓝色,70% 透明度
})
}
});
表格
| 属性 | 推荐值 | 说明 |
|---|---|---|
width | 3~8 | 线条粗细 |
glowPower | 0.2~0.6 | 越大光晕越扩散 |
5️⃣ 顶部闪烁警示点
最后,我们在每个角点上方添加一个动态闪烁的红色警示图标。
// 定义一个红色圆形 SVG 图标(Base64 编码,内联使用,无外部依赖)
const imgBillboard = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iOCIgc3Ryb2tlPSIjZmY2NjAwIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiNmZjY2MDAiIGZpbGwtb3BhY2l0eT0iMC44Ii8+Cjwvc3ZnPgo=';
// 为每个围栏角点创建一个闪烁的图标
fencePositions.forEach((pos, idx) => {
cesiumViewer.entities.add({
// 设置图标位置:在围栏顶部上方 5 米处
position: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, fenceHeight + 5),
billboard: {
image: imgBillboard, // 使用内联 SVG 图标
scale: 0.8, // 图标缩放比例
// 动态颜色:每帧根据时间计算透明度
color: new Cesium.CallbackProperty(() => {
// 获取当前时间(毫秒),乘以系数控制闪烁速度
const t = Date.now() * 0.003;
// 计算透明度:0.7 ~ 1.0 之间波动,并加入相位偏移(idx * 1.5)
const intensity = 0.7 + 0.3 * Math.sin(t + idx * 1.5);
// 返回红色,透明度由 intensity 控制
return Cesium.Color.RED.withAlpha(intensity);
}, false), // false 表示不缓存,每帧重新计算
heightReference: Cesium.HeightReference.NONE // 使用绝对高度(不贴地)
}
});
});
✨ 每个点使用
idx * 1.5实现 错峰闪烁,避免同步。
📊 性能与兼容性对比
| 方案 | 渲染性能 | 开发难度 | 动画灵活性 | 适用场景 |
|---|---|---|---|---|
| Primitive + Custom Shader | ⭐⭐⭐⭐☆ | ⭐⭐⭐☆ | ⭐⭐⭐⭐⭐ | 高性能、复杂动画 |
| Entity + MaterialProperty | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 快速原型、简单效果 |
| 混合方案(本文) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 推荐!兼顾性能与开发效率 |
🚀 使用方式
将上述逻辑封装为函数后,使用非常简单:
// 1. 引入函数
import { loadPulseFence } from './pulseFence.js';
// 2. 初始化 Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer');
// 3. 调用函数,加载呼吸围栏
loadPulseFence(viewer);
✅ 支持多次调用,绘制多个围栏。
🔧 自定义建议
| 需求 | 修改位置 |
|---|---|
| 改变颜色 | 修改 baseColor 和 Cesium.Color |
| 调整高度 | 修改 fenceHeight |
| 改变闪烁频率 | 调整 Date.now() * 0.003 中的系数 |
| 增加更多点 | 在 fencePositions 中添加坐标 |


/**
* 加载呼吸脉冲围栏效果(墙体 + 底部发光 + 顶部闪烁点)
* @param {Cesium.Viewer} cesiumViewer - Cesium Viewer 实例
*/
export const loadPulseFence = (cesiumViewer) => {
// 定义围栏的四个角点坐标(经纬度),按顺时针或逆时针顺序排列
const fencePositions = [
{ lon: 116.397194, lat: 39.909768 },
{ lon: 116.397194, lat: 39.900768 },
{ lon: 116.392194, lat: 39.900768 },
{ lon: 116.392194, lat: 39.909768 }
];
// 设置围栏墙体的高度(单位:米)
const fenceHeight = 60.0;
/**
* 创建呼吸脉冲材质(用于墙体)
* 使用自定义 GLSL Shader 实现正弦波动的透明度和自发光效果
* @returns {Cesium.Material} 返回一个可复用的材质对象
*/
const createPulseMaterial = () => {
return new Cesium.Material({
// 定义材质的 fabric(织物)结构
fabric: {
// 自定义材质类型名称(任意命名,用于内部识别)
type: 'PulseFenceMaterial',
// GLSL 片元着色器源码(核心动画逻辑)
source: `
// Cesium 内置函数:根据输入生成默认材质
czm_material czm_getMaterial(czm_materialInput materialInput)
{
// 初始化一个默认材质
czm_material material = czm_getDefaultMaterial(materialInput);
// 获取当前帧号,并乘以系数控制动画速度(0.01 = 较慢)
float time = czm_frameNumber * 0.01;
// 生成 [0, 1] 范围内的正弦波动值(模拟“呼吸”节奏)
float pulse = 0.5 + 0.5 * sin(time);
// 定义基础颜色:深蓝色 (R=0, G=0.6, B=1.0)
vec3 baseColor = vec3(0.0, 0.6, 1.0);
// 透明度随 pulse 波动:范围 [0.3, 0.9]
float alpha = 0.3 + 0.6 * pulse;
// 设置漫反射颜色(主色)
material.diffuse = baseColor;
// 设置透明度(必须配合 blending 才生效)
material.alpha = alpha;
// 设置自发光强度:随 pulse 增强,最大为 baseColor * 1.8
material.emission = baseColor * pulse * 1.8;
// 返回最终材质
return material;
}
`
},
// 启用半透明渲染(必须为 true,否则 alpha 无效)
translucent: true
});
};
// ========== 第一部分:构建围栏墙体(使用 Primitive 高性能渲染) ==========
// 遍历每个边(从点 i 到点 i+1)
for (let i = 0; i < fencePositions.length; i++) {
// 获取当前起点
const start = fencePositions[i];
// 获取下一个终点(取模实现闭环:最后一个点连回第一个点)
const end = fencePositions[(i + 1) % fencePositions.length];
// 创建墙体几何体(WallGeometry 天然支持垂直拉伸)
const wallGeometry = new Cesium.WallGeometry({
// 将经纬度数组转换为笛卡尔坐标数组(格式:[lon1, lat1, lon2, lat2])
positions: Cesium.Cartesian3.fromDegreesArray([
start.lon, start.lat,
end.lon, end.lat
]),
// 设置每个顶点的最大高度(墙体顶部高度)
maximumHeights: [fenceHeight, fenceHeight],
// 设置每个顶点的最小高度(墙体底部高度,0 表示贴地)
minimumHeights: [0, 0],
// 指定顶点属性格式:包含位置和纹理坐标(ST),供 Shader 使用
vertexFormat: Cesium.VertexFormat.POSITION_AND_ST
});
// 将几何体包装为 GeometryInstance(Primitive 的基本单元)
const wallInstance = new Cesium.GeometryInstance({
geometry: wallGeometry
});
// 创建 Primitive(底层渲染对象,性能优于 Entity)
const wallPrimitive = new Cesium.Primitive({
// 绑定几何实例
geometryInstances: wallInstance,
// 设置外观:使用自定义材质
appearance: new Cesium.MaterialAppearance({
// 应用呼吸脉冲材质
material: createPulseMaterial(),
// 不闭合(墙体是开放面)
closed: false,
// 使用 Phong 光照(false 表示启用光照计算)
flat: false,
// 渲染状态配置
renderState: {
// 启用 Alpha 混合(实现半透明)
blending: Cesium.BlendingState.ALPHA_BLEND,
// 启用深度测试(正确处理前后遮挡)
depthTest: { enabled: true },
// 不写入深度缓冲(避免遮挡其他物体)
depthMask: false
}
})
});
// 将墙体添加到场景中
cesiumViewer.scene.primitives.add(wallPrimitive);
}
// ========== 第二部分:添加底部发光轮廓线(使用 Entity 简化开发) ==========
// 构建闭合的折线坐标数组(格式:[lon1, lat1, height1, lon2, lat2, height2, ...])
const bottomLine = [];
// 遍历所有围栏点,添加到底部线
fencePositions.forEach(p => bottomLine.push(p.lon, p.lat, 0.1));
// 闭合:将第一个点再次添加到末尾
bottomLine.push(fencePositions[0].lon, fencePositions[0].lat, 0.1);
// 使用 Entity API 添加发光折线
cesiumViewer.entities.add({
polyline: {
// 将经纬度+高度数组转换为笛卡尔坐标
positions: Cesium.Cartesian3.fromDegreesArrayHeights(bottomLine),
// 线条宽度(像素)
width: 5,
// 使用内置的发光材质
material: new Cesium.PolylineGlowMaterialProperty({
// 发光强度(0.0~1.0,值越大光晕越扩散)
glowPower: 0.4,
// 发光颜色(蓝色,带 70% 透明度)
color: Cesium.Color.BLUE.withAlpha(0.7)
})
}
});
// ========== 第三部分:添加顶部闪烁警示点(使用 SVG 图标 + 动态颜色) ==========
// 定义一个红色圆形 SVG 图标(Base64 编码,避免外部依赖)
const imgBillboard = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iOCIgc3Ryb2tlPSIjZmY2NjAwIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9IiNmZjY2MDAiIGZpbGwtb3BhY2l0eT0iMC44Ii8+Cjwvc3ZnPgo=';
// 为每个围栏角点添加一个闪烁的 billboard(图标)
fencePositions.forEach((pos, idx) => {
cesiumViewer.entities.add({
// 设置图标位置:在围栏顶部上方 5 米处
position: Cesium.Cartesian3.fromDegrees(pos.lon, pos.lat, fenceHeight + 5),
billboard: {
// 使用内联 SVG 图标
image: imgBillboard,
// 图标缩放比例
scale: 0.8,
// 动态颜色:使用 CallbackProperty 实现每帧更新
color: new Cesium.CallbackProperty(() => {
// 获取当前时间(毫秒),乘以系数控制闪烁速度
const t = Date.now() * 0.003;
// 计算透明度:0.7 ~ 1.0 之间波动,并加入相位偏移(idx * 1.5)使各点错峰闪烁
const intensity = 0.7 + 0.3 * Math.sin(t + idx * 1.5);
// 返回红色,透明度由 intensity 控制
return Cesium.Color.RED.withAlpha(intensity);
}, false), // false 表示不缓存结果(每帧都重新计算)
// 高度参考:NONE 表示使用绝对高度(不贴地)
heightReference: Cesium.HeightReference.NONE
}
});
});
// 打印日志,确认加载完成
console.log('【呼吸脉冲围栏】加载完成(Primitive 版本)!');
};
&spm=1001.2101.3001.5002&articleId=159209748&d=1&t=3&u=f85f21ab35b7472399511480beca54ab)
1887

被折叠的 条评论
为什么被折叠?



