第七章 Cesium 3D 粒子烟花效果案例解析:从原理到完整代码

发布于:2025-09-07 ⋅ 阅读:(12) ⋅ 点赞:(0)

效果图

本文详细介绍如何使用 Cesium.js 创建逼真的 3D 粒子烟花效果,包括核心原理、实现步骤和完整代码,并解决开发过程中常见的 "只读对象修改" 错误。

效果展示与技术原理

我们将在 Cesium 地球表面创建一组动态烟花效果,每个烟花由数百个粒子组成,具有随机的位置、颜色和爆炸范围,最终形成绚丽的 3D 视觉效果。

核心技术原理:

  • 利用 Cesium 的粒子系统 (ParticleSystem) 创建和管理粒子
  • 通过坐标系转换实现粒子在 3D 空间中的精确定位
  • 运用向量运算控制粒子运动轨迹
  • 使用生命周期管理实现粒子的产生、运动和消失

开发准备

环境依赖

  • Cesium.js 1.95(粒子系统 API 稳定版本)
  • 现代浏览器(支持 WebGL)

基础配置

<!-- 引入Cesium核心库 -->
<script src="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Cesium.js"></script>
<!-- 引入Cesium样式表 -->
<link href="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Widgets/widgets.css" rel="stylesheet">

实现步骤详解

1. 初始化 Cesium 场景

首先创建基础的 3D 地球场景,并配置卫星影像图层:

// 创建Cesium Viewer实例
const viewer = new Cesium.Viewer("cesiumContainer", {
    shouldAnimate: true,  // 启用自动动画,确保粒子系统实时更新
    baseLayerPicker: false // 关闭图层选择器
});

// 清除默认图层,添加ArcGIS卫星影像
viewer.imageryLayers.removeAll();
viewer.imageryLayers.addImageryProvider(
    new Cesium.ArcGisMapServerImageryProvider({
        url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
    })
);

// 获取场景对象
const scene = viewer.scene;
// 显示帧率信息(调试用)
scene.debugShowFramesPerSecond = true;

2. 配置核心参数

将所有可调整的参数集中管理,便于后续维护和扩展:

const CONFIG = {
    // 粒子发射的经纬度位置(美国费城附近)
    emissionCoord: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
    // 发射器初始高度(地面以上100米)
    initialHeight: 100.0,
    // 粒子在屏幕上的大小
    particleSize: new Cesium.Cartesian2(7.0, 7.0),
    // 每次粒子爆发的数量
    burstCount: 400,
    // 单个粒子系统的生命周期
    systemLifetime: 10.0,
    // 总烟花数量
    totalFireworks: 20,
    // 烟花爆炸范围
    explosionRange: { min: 30.0, max: 100.0 },
    // 烟花位置的随机偏移范围
    offsetRange: { x: [-100, 100], y: [-80, 100], z: [-50, 50] },
    // 烟花颜色方案
    colorSchemes: [
        { minRed: 0.75, green: 0.0, minBlue: 0.8, alpha: 1.0 }, // 粉紫色系
        { red: 0.0, minGreen: 0.75, minBlue: 0.8, alpha: 1.0 },  // 青绿色系
        { red: 0.0, green: 0.0, minBlue: 0.8, alpha: 1.0 },      // 蓝色系
        { minRed: 0.75, minGreen: 0.75, blue: 0.0, alpha: 1.0 }  // 黄色系
    ]
};

3. 创建粒子纹理

定义粒子的外观,使用 canvas 创建白色圆形作为粒子基础纹理:

// 粒子纹理缓存
let particleTexture;

/**
 * 创建粒子纹理(白色圆形)
 * @returns {HTMLCanvasElement} 粒子纹理画布
 */
const getParticleTexture = () => {
    // 如果纹理已创建,直接返回缓存的纹理
    if (Cesium.defined(particleTexture)) return particleTexture;
    
    // 创建20x20的画布
    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 20;
    const ctx = canvas.getContext("2d");
    
    // 绘制白色圆形
    ctx.arc(8, 8, 8, 0, Cesium.Math.TWO_PI, true);
    ctx.fillStyle = "#fff";
    ctx.fill();
    
    // 缓存纹理并返回
    particleTexture = canvas;
    return canvas;
};

4. 实现粒子系统核心逻辑

创建单个烟花的核心函数,控制粒子的产生、运动和消失:

/**
 * 创建单个烟花粒子系统
 * @param {Cesium.Cartesian3} offset - 烟花相对于初始位置的偏移量
 * @param {Cesium.Color} color - 烟花的颜色
 * @param {Cesium.ParticleBurst[]} bursts - 粒子爆发的时间配置
 */
const createFirework = (offset, color, bursts) => {
    // 计算烟花的实际位置 = 初始位置 + 偏移量
    const fireworkPos = Cesium.Cartesian3.add(initialPos, offset, new Cesium.Cartesian3());
    
    // 创建发射器矩阵:将发射器定位到计算出的烟花位置
    const emitterMatrix = Cesium.Matrix4.fromTranslation(fireworkPos);
    
    // 计算坐标系转换矩阵
    const localToWorld = Cesium.Matrix4.multiply(emissionMatrix, emitterMatrix, new Cesium.Matrix4());
    const worldToLocal = Cesium.Matrix4.inverseTransformation(localToWorld, localToWorld);

    // 随机生成当前烟花的爆炸范围
    const explosionSize = Cesium.Math.randomBetween(
        CONFIG.explosionRange.min,
        CONFIG.explosionRange.max
    );

    /**
     * 粒子受力回调函数
     * 控制粒子运动,超出爆炸范围时停止
     */
    const particleForce = (particle) => {
        const localPos = Cesium.Matrix4.multiplyByPoint(worldToLocal, particle.position, new Cesium.Cartesian3());
        
        // 超出爆炸范围时停止粒子运动
        if (Cesium.Cartesian3.magnitudeSquared(localPos) >= explosionSize **2) {
            // 关键修复:创建新的Cartesian3实例而非使用Cesium.Cartesian3.ZERO
            // 原因:Cesium.Cartesian3.ZERO是只读对象,直接赋值会导致错误
            particle.velocity = new Cesium.Cartesian3(0, 0, 0);
        }
    };

    // 计算粒子生命周期(与爆炸范围相关)
    const normalizedSize = (explosionSize - CONFIG.explosionRange.min) / 
                          (CONFIG.explosionRange.max - CONFIG.explosionRange.min);
    const particleLife = 0.3 + normalizedSize * 0.7;

    // 创建并添加粒子系统
    scene.primitives.add(new Cesium.ParticleSystem({
        image: getParticleTexture(),       // 粒子纹理
        startColor: color,                 // 初始颜色
        endColor: color.withAlpha(0.0),    // 结束颜色(透明,实现淡出)
        particleLife: particleLife,        // 粒子生命周期
        speed: 100.0,                      // 初始速度
        imageSize: CONFIG.particleSize,    // 粒子大小
        emissionRate: 0,                   // 不持续发射(仅通过burst)
        emitter: new Cesium.SphereEmitter(0.1),  // 发射器形状
        bursts: bursts,                    // 爆发配置
        lifetime: CONFIG.systemLifetime,   // 粒子系统生命周期
        updateCallback: particleForce,     // 粒子运动回调
        modelMatrix: emissionMatrix,       // 基础坐标系矩阵
        emitterModelMatrix: emitterMatrix  // 发射器位置矩阵
    }));
};

5. 批量创建烟花效果

循环创建多个烟花,实现多样化的视觉效果:

/**
 * 批量创建所有烟花
 */
const createAllFireworks = () => {
    for (let i = 0; i < CONFIG.totalFireworks; i++) {
        // 生成随机位置偏移
        const offset = new Cesium.Cartesian3(
            Cesium.Math.randomBetween(...CONFIG.offsetRange.x),
            Cesium.Math.randomBetween(...CONFIG.offsetRange.y),
            Cesium.Math.randomBetween(...CONFIG.offsetRange.z)
        );

        // 选择烟花颜色(循环使用颜色方案)
        const colorScheme = CONFIG.colorSchemes[i % CONFIG.colorSchemes.length];
        const color = Cesium.Color.fromRandom({
            minimumRed: colorScheme.minRed,
            red: colorScheme.red,
            minimumGreen: colorScheme.minGreen,
            green: colorScheme.green,
            minimumBlue: colorScheme.minBlue,
            blue: colorScheme.blue,
            alpha: colorScheme.alpha
        });

        // 配置粒子爆发(3次随机爆发)
        const bursts = Array.from({ length: 3 }, () => 
            new Cesium.ParticleBurst({
                time: Cesium.Math.nextRandomNumber() * CONFIG.systemLifetime,
                minimum: CONFIG.burstCount,
                maximum: CONFIG.burstCount
            })
        );

        // 创建单个烟花
        createFirework(offset, color, bursts);
    }
};

6. 配置相机视角

调整相机位置和朝向,确保烟花效果在最佳视角展示:

/**
 * 配置相机视角,聚焦烟花区域
 */
const setupCamera = () => {
    const camera = scene.camera;
    // 相机相对于发射器的偏移位置
    const cameraOffset = new Cesium.Cartesian3(-300.0, 0.0, 0.0);
    
    // 相机定位
    camera.lookAtTransform(emissionMatrix, cameraOffset);
    camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

    // 计算相机看向烟花的方向向量
    // 关键修复:使用新实例存储结果,避免修改只读对象
    const lookDir = Cesium.Cartesian3.subtract(initialPos, cameraOffset, new Cesium.Cartesian3());
    
    // 归一化方向向量
    // 关键修复:使用新实例存储归一化结果
    const normalizedDir = Cesium.Cartesian3.normalize(lookDir, new Cesium.Cartesian3());
    
    // 计算并设置相机仰角
    const pitchAngle = Cesium.Math.PI_OVER_TWO - 
                      Math.acos(Cesium.Cartesian3.dot(normalizedDir, Cesium.Cartesian3.UNIT_Z));
    camera.lookUp(pitchAngle);
};

7. 初始化执行

// 批量创建所有烟花
createAllFireworks();
// 配置相机视角
setupCamera();

常见错误及解决方案

在开发过程中,最容易遇到的是 "Cannot assign to read only property 'x' of object" 错误,这是由于 Cesium 中的一些静态对象(如Cesium.Cartesian3.ZERO)是只读的。

错误原因:直接修改 Cesium 的只读对象

// 错误示例
particle.velocity = Cesium.Cartesian3.ZERO;

解决方案:创建新的对象实例

// 正确示例
particle.velocity = new Cesium.Cartesian3(0, 0, 0);

同样,在处理向量运算时,也要注意不要直接修改原对象:

// 错误示例 - 可能修改原对象
Cesium.Cartesian3.normalize(lookDir, lookDir);

// 正确示例 - 使用新实例存储结果
const normalizedDir = Cesium.Cartesian3.normalize(lookDir, new Cesium.Cartesian3());

完整代码

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cesium粒子火花效果 - 修复版</title>
    <!-- 引入Cesium核心库:提供3D地球和粒子系统支持 -->
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Cesium.js"></script>
    <!-- 引入Cesium样式表:确保控件正常显示 -->
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.95/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        /* 确保场景占满整个屏幕 */
        body,
        #cesiumContainer {
            width: 100vw;
            height: 100vh;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <!-- Cesium场景容器:所有3D内容将渲染到这里 -->
    <div id="cesiumContainer"></div>

    <script>
        /******************************************************************************
         * 1. 初始化Cesium核心环境
         * 作用:创建3D视图,配置地图图层,为粒子效果提供基础渲染环境
         ******************************************************************************/
        // 创建Cesium Viewer实例(核心控制器)
        // shouldAnimate: true → 启用自动动画,确保粒子系统能实时更新
        // baseLayerPicker: false → 关闭图层选择器(我们将手动配置地图)
        const viewer = new Cesium.Viewer("cesiumContainer", {
            shouldAnimate: true,
            baseLayerPicker: false
        });

        // 清除默认图层(Cesium默认带的地图图层)
        viewer.imageryLayers.removeAll();
        // 添加ArcGIS卫星影像图层(高清卫星地图)
        viewer.imageryLayers.addImageryProvider(
            new Cesium.ArcGisMapServerImageryProvider({
                url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
            })
        );

        // 获取场景对象(所有渲染和粒子系统的基础)
        const scene = viewer.scene;
        // 显示帧率信息(左下角,用于调试性能)
        scene.debugShowFramesPerSecond = true;


        /******************************************************************************
         * 2. 全局配置与工具函数
         * 作用:集中管理所有常量参数和复用性工具函数
         ******************************************************************************/
        // 配置常量:所有可调整的参数集中在这里,便于维护
        const CONFIG = {
            // 粒子发射的经纬度位置(美国费城附近)
            emissionCoord: Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
            // 发射器初始高度(地面以上100米)
            initialHeight: 100.0,
            // 粒子在屏幕上的大小(宽7像素,高7像素)
            particleSize: new Cesium.Cartesian2(7.0, 7.0),
            // 每次粒子爆发的数量
            burstCount: 400,
            // 单个粒子系统的生命周期(10秒后自动消失)
            systemLifetime: 10.0,
            // 总烟花数量
            totalFireworks: 20,
            // 烟花爆炸范围的最小值和最大值(单位:米)
            explosionRange: { min: 30.0, max: 100.0 },
            // 烟花位置的随机偏移范围(单位:米)
            offsetRange: { x: [-100, 100], y: [-80, 100], z: [-50, 50] },
            // 烟花颜色方案(4种色系循环使用)
            colorSchemes: [
                { minRed: 0.75, green: 0.0, minBlue: 0.8, alpha: 1.0 }, // 粉紫色系
                { red: 0.0, minGreen: 0.75, minBlue: 0.8, alpha: 1.0 },  // 青绿色系
                { red: 0.0, green: 0.0, minBlue: 0.8, alpha: 1.0 },      // 蓝色系
                { minRed: 0.75, minGreen: 0.75, blue: 0.0, alpha: 1.0 }  // 黄色系
            ]
        };

        // 发射坐标系矩阵:将东-北-上坐标系(局部坐标系)转换到世界坐标系
        // 基于CONFIG.emissionCoord指定的经纬度创建
        const emissionMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(CONFIG.emissionCoord);

        // 发射器初始位置(在局部坐标系中的位置:x=0, y=0, z=100米)
        const initialPos = new Cesium.Cartesian3(0.0, 0.0, CONFIG.initialHeight);

        // 设置随机数种子:确保每次运行的随机效果一致,便于调试
        Cesium.Math.setRandomNumberSeed(315);

        // 粒子纹理缓存:避免重复创建canvas元素,提升性能
        let particleTexture;

        /**
         * 创建粒子纹理(白色圆形)
         * 作用:定义粒子的外观,返回一个20x20的白色圆形画布
         * @returns {HTMLCanvasElement} 粒子纹理画布
         */
        const getParticleTexture = () => {
            // 如果纹理已创建,直接返回缓存的纹理
            if (Cesium.defined(particleTexture)) return particleTexture;

            // 创建20x20的画布
            const canvas = document.createElement("canvas");
            canvas.width = canvas.height = 20;
            const ctx = canvas.getContext("2d");

            // 绘制白色圆形(粒子形状)
            ctx.arc(8, 8, 8, 0, Cesium.Math.TWO_PI, true); // 圆心(8,8),半径8
            ctx.fillStyle = "#fff"; // 白色填充
            ctx.fill();

            // 缓存纹理并返回
            particleTexture = canvas;
            return canvas;
        };


        /******************************************************************************
         * 3. 粒子系统核心逻辑(创建单个烟花)
         * 作用:根据位置偏移、颜色和爆发配置,创建一个完整的烟花粒子效果
         * @param {Cesium.Cartesian3} offset - 烟花相对于初始位置的偏移量
         * @param {Cesium.Color} color - 烟花的颜色
         * @param {Cesium.ParticleBurst[]} bursts - 粒子爆发的时间配置
         ******************************************************************************/
        const createFirework = (offset, color, bursts) => {
            // 计算烟花的实际位置 = 初始位置 + 偏移量
            const fireworkPos = Cesium.Cartesian3.add(initialPos, offset, new Cesium.Cartesian3());

            // 创建发射器矩阵:将发射器定位到计算出的烟花位置
            const emitterMatrix = Cesium.Matrix4.fromTranslation(fireworkPos);

            // 计算坐标系转换矩阵:
            // localToWorld → 将粒子的局部坐标转换为世界坐标
            // worldToLocal → 将世界坐标转换为粒子的局部坐标(用于计算粒子受力)
            const localToWorld = Cesium.Matrix4.multiply(emissionMatrix, emitterMatrix, new Cesium.Matrix4());
            const worldToLocal = Cesium.Matrix4.inverseTransformation(localToWorld, localToWorld);

            // 随机生成当前烟花的爆炸范围(在配置的最小值和最大值之间)
            const explosionSize = Cesium.Math.randomBetween(
                CONFIG.explosionRange.min,
                CONFIG.explosionRange.max
            );

            /**
             * 粒子受力回调函数
             * 作用:控制粒子的运动,当粒子超出爆炸范围时停止运动
             * @param {Cesium.Particle} particle - 单个粒子对象
             */
            const particleForce = (particle) => {
                // 将粒子的世界位置转换为局部位置(相对于发射器)
                const localPos = Cesium.Matrix4.multiplyByPoint(worldToLocal, particle.position, new Cesium.Cartesian3());

                // 判断粒子是否超出爆炸范围(使用平方距离比较,避免开方运算,提升性能)
                if (Cesium.Cartesian3.magnitudeSquared(localPos) >= explosionSize ** 2) {
                    // 【关键修复】:创建新的Cartesian3实例,而非使用Cesium.Cartesian3.ZERO
                    // 原因:Cesium.Cartesian3.ZERO是只读对象,直接赋值会导致"Cannot assign to read only property"错误
                    particle.velocity = new Cesium.Cartesian3(0, 0, 0);
                }
            };

            // 计算粒子的生命周期:爆炸范围越大,粒子生命周期越长(0.3~1.0秒)
            // 归一化爆炸范围(将范围值转换为0~1之间的比例)
            const normalizedSize = (explosionSize - CONFIG.explosionRange.min) / (CONFIG.explosionRange.max - CONFIG.explosionRange.min);
            const particleLife = 0.3 + normalizedSize * 0.7;

            // 创建粒子系统并添加到场景
            scene.primitives.add(new Cesium.ParticleSystem({
                image: getParticleTexture(),       // 粒子纹理(白色圆形)
                startColor: color,                 // 粒子初始颜色
                endColor: color.withAlpha(0.0),    // 粒子结束颜色(透明,实现淡出效果)
                particleLife: particleLife,        // 单个粒子的生命周期(秒)
                speed: 100.0,                      // 粒子初始速度(米/秒)
                imageSize: CONFIG.particleSize,    // 粒子在屏幕上的大小
                emissionRate: 0,                   // 不持续发射粒子(仅通过burst爆发)
                emitter: new Cesium.SphereEmitter(0.1),  // 发射器形状(半径0.1米的球体)
                bursts: bursts,                    // 粒子爆发的时间和数量配置
                lifetime: CONFIG.systemLifetime,   // 整个粒子系统的生命周期(秒)
                updateCallback: particleForce,     // 粒子运动的更新回调(控制受力)
                modelMatrix: emissionMatrix,       // 基础坐标系矩阵
                emitterModelMatrix: emitterMatrix  // 发射器的位置矩阵
            }));
        };


        /******************************************************************************
         * 4. 批量创建烟花
         * 作用:循环创建指定数量的烟花,每个烟花有随机位置、颜色和爆发时间
         ******************************************************************************/
        const createAllFireworks = () => {
            // 循环创建CONFIG.totalFireworks个烟花
            for (let i = 0; i < CONFIG.totalFireworks; i++) {
                // 1. 生成随机位置偏移(在配置的范围内)
                const offset = new Cesium.Cartesian3(
                    Cesium.Math.randomBetween(...CONFIG.offsetRange.x),  // x轴偏移
                    Cesium.Math.randomBetween(...CONFIG.offsetRange.y),  // y轴偏移
                    Cesium.Math.randomBetween(...CONFIG.offsetRange.z)   // z轴偏移
                );

                // 2. 选择烟花颜色(循环使用配置的颜色方案)
                const colorScheme = CONFIG.colorSchemes[i % CONFIG.colorSchemes.length];
                const color = Cesium.Color.fromRandom({
                    minimumRed: colorScheme.minRed,
                    red: colorScheme.red,
                    minimumGreen: colorScheme.minGreen,
                    green: colorScheme.green,
                    minimumBlue: colorScheme.minBlue,
                    blue: colorScheme.blue,
                    alpha: colorScheme.alpha
                });

                // 3. 配置粒子爆发(每个烟花爆发3次,时间在生命周期内随机)
                const bursts = Array.from({ length: 3 }, () =>
                    new Cesium.ParticleBurst({
                        time: Cesium.Math.nextRandomNumber() * CONFIG.systemLifetime, // 随机爆发时间
                        minimum: CONFIG.burstCount,  // 每次爆发的最小粒子数
                        maximum: CONFIG.burstCount   // 每次爆发的最大粒子数(固定值)
                    })
                );

                // 4. 创建单个烟花
                createFirework(offset, color, bursts);
            }
        };


        /******************************************************************************
         * 5. 相机视角配置
         * 作用:调整相机位置和朝向,确保烟花效果在视野中居中显示
         ******************************************************************************/
        const setupCamera = () => {
            const camera = scene.camera;
            // 相机相对于发射器的偏移位置(在烟花区域前方300米)
            const cameraOffset = new Cesium.Cartesian3(-1000.0, 0.0, 0.0);

            // 相机定位:基于发射坐标系的偏移位置
            camera.lookAtTransform(emissionMatrix, cameraOffset);
            // 重置相机坐标系(切换回世界坐标系)
            camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

            // 计算相机看向烟花的方向向量
            // 【关键修复】:使用new Cesium.Cartesian3()创建新实例存储结果
            // 原因:避免修改原对象,防止只读对象错误
            const lookDir = Cesium.Cartesian3.subtract(initialPos, cameraOffset, new Cesium.Cartesian3());

            // 归一化方向向量(将向量长度转换为1)
            // 【关键修复】:使用新实例存储归一化结果,不修改原向量
            const normalizedDir = Cesium.Cartesian3.normalize(lookDir, new Cesium.Cartesian3());

            // 计算相机仰角:让相机朝上看向烟花区域
            const pitchAngle = Cesium.Math.PI_OVER_TWO - Math.acos(Cesium.Cartesian3.dot(normalizedDir, Cesium.Cartesian3.UNIT_Z));
            camera.lookUp(pitchAngle);
        };


        /******************************************************************************
         * 6. 初始化执行
         * 作用:启动整个粒子效果流程
         ******************************************************************************/
        createAllFireworks();  // 批量创建所有烟花
        setupCamera();         // 配置相机视角,聚焦烟花区域
    </script>
</body>

</html>

扩展与优化建议

  1. 交互扩展:添加鼠标点击事件,允许用户在地球表面任意位置触发烟花效果
  2. 性能优化
    • 限制同时存在的粒子数量
    • 根据设备性能动态调整粒子数量和大小
  3. 效果增强
    • 添加粒子尾迹效果
    • 实现烟花上升阶段动画
    • 增加声音效果同步
  4. 参数控制:添加 UI 控件,允许用户实时调整粒子大小、颜色、爆炸范围等参数

通过本文介绍的方法,可以在 Cesium 中创建出各种炫酷的粒子效果,不仅限于烟花,还可以实现火焰、烟雾、喷泉等多种视觉效果。


网站公告

今日签到

点亮在社区的每一天
去签到