CesiumJS 实战:打造动态呼吸脉冲围栏(含发光线与闪烁点)

该文章已生成可运行项目,

在三维地理可视化项目中,电子围栏(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% 透明度
        })
    }
});

表格

属性推荐值说明
width3~8线条粗细
glowPower0.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 版本)!');
};

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值