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