第七章 Cesium 3D 粒子烟花效果案例解析:从原理到完整代码
效果图
本文详细介绍如何使用 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),半径8ctx.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>
扩展与优化建议
- 交互扩展:添加鼠标点击事件,允许用户在地球表面任意位置触发烟花效果
- 性能优化:
- 限制同时存在的粒子数量
- 根据设备性能动态调整粒子数量和大小
- 效果增强:
- 添加粒子尾迹效果
- 实现烟花上升阶段动画
- 增加声音效果同步
- 参数控制:添加 UI 控件,允许用户实时调整粒子大小、颜色、爆炸范围等参数
通过本文介绍的方法,可以在 Cesium 中创建出各种炫酷的粒子效果,不仅限于烟花,还可以实现火焰、烟雾、喷泉等多种视觉效果。