Cesium快速入门到精通系列教程七:粒子效果
在 Cesium 1.93 中实现粒子效果需通过 ParticleSystem 类进行配置,结合发射器、生命周期、物理模拟等参数实现火焰、烟雾、雨雪等动态效果。
一、粒子系统基础
概念:粒子系统是一种模拟复杂物理效应的图形技术,由大量小粒子组成,这些小粒子可以代表火花、烟雾、雨滴、火焰等效果,通过控制单个粒子的行为来呈现出复杂的 “模糊” 对象效果。
作用:用于增强 Cesium 中的三维场景的视觉效果,比如模拟飞机引擎爆炸、飞机坠毁烟雾轨迹、火箭尾焰、天气现象等。
二、粒子系统核心参数配置
参数 | 作用 | 示例值 | 引用 |
---|---|---|---|
image | 粒子贴图(需透明PNG) | "smoke.png" |
startColor /endColor | 粒子颜色渐变(含透明度) | Cesium.Color.RED.withAlpha(0.8) → 黄色透明 | |
startScale /endScale | 粒子大小变化(初始到结束的缩放倍率) | 1.0 → 4.0 (烟雾膨胀) | |
emissionRate | 每秒发射粒子数量(控制密度) | 火焰:20-50 ;烟雾:5-10 | |
emitter | 发射器类型:<br>- CircleEmitter (圆形)<br>- ConeEmitter (锥形,适合火焰) | new Cesium.ConeEmitter(0.8) | |
speed /minimumSpeed | 粒子初始速度(米/秒) | 雨滴:-5.0 (向下);烟雾:1.0~4.0 | |
particleLife | 粒子存活时间(秒) | 火焰:1.5-2.0 ;烟雾:5.0 | |
updateCallback | 自定义物理效果(如重力、风力) | 模拟重力:修改粒子速度向量 |
三、实现步骤(以卡车烟雾为例)
1、初始化实体与矩阵
绑定粒子位置到模型(如卡车引擎):
const truckEntity = viewer.entities.add({position: Cesium.Cartesian3.fromDegrees(116.39, 39.9, 100),model: { uri: "truck.glb" }
});
const modelMatrix = entity.computeModelMatrix(viewer.clock.currentTime);
2、设置发射器偏移
通过 emitterModelMatrix 微调局部位置:
function computeEmitterMatrix() {const offset = new Cesium.Cartesian3(-4.0, 0.0, 1.4); // 引擎位置偏移return Cesium.Matrix4.fromTranslation(offset);
}
3、创建粒子系统
配置参数并添加到场景:
const particleSystem = viewer.scene.primitives.add(new Cesium.ParticleSystem({image: "smoke.png",startColor: Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),endColor: Cesium.Color.WHITE.withAlpha(0.0),startScale: 1.0,endScale: 4.0,emissionRate: 5.0,particleLife: 5.0,minimumSpeed: 1.0,maximumSpeed: 4.0,modelMatrix: modelMatrix, // 绑定卡车位置emitterModelMatrix: computeEmitterMatrix(), // 引擎偏移emitter: new Cesium.CircleEmitter(0.5) // 圆形发射})
);
四、常见效果示例
1. 雨滴效果
image: "raindrop.png",
startColor: Cesium.Color.WHITE.withAlpha(0.8),
endColor: Cesium.Color.BLUE.withAlpha(0.2),
minimumSpeed: -5.0, // 向下运动
emissionRate: 1000.0,
particleLife: 3.0,
emitter: new Cesium.BoxEmitter(new Cesium.Cartesian3(10, 10, 0)) // 盒状区域
关键:速度负值模拟下落,盒状发射器覆盖天空
2. 火焰效果
image: "fire.png",
startColor: Cesium.Color.RED.withAlpha(0.9),
endColor: Cesium.Color.YELLOW.withAlpha(0.5),
emissionRate: 30.0,
particleLife: 1.5,
emitter: new Cesium.ConeEmitter(Cesium.Math.toRadians(45)) // 锥形向上
贴图建议:使用红黄渐变纹理增强真实感。
五、高级技巧
1、物理模拟
通过 updateCallback 添加重力或风力:
function applyGravity(particle, dt) {particle.velocity.z -= 9.8 * dt; // 重力加速度
}
// 在粒子系统中配置:
updateCallback: applyGravity
2、矩阵定位
- modelMatrix:将粒子绑定到实体世界坐标。
- emitterModelMatrix:在实体局部坐标系偏移(避免从中心发射) 。
3、动态属性
使用 bursts 实现爆炸效果:
bursts: [new Cesium.ParticleBurst({ time: 5.0, minimum: 50, maximum: 100 })
]
六、注意事项
- 贴图路径:使用绝对路径或托管于Cesium Ion,避免跨域问题。
- 性能优化:高密度粒子(如雨雪)需控制 emissionRate,避免卡顿。
- 版本兼容:1.93中 ParticleSystem API 与旧版一致,可直接迁移。
七、完整实例
<template><div id="cesiumContainer" class="fullSize"></div><div id="loadingOverlay"><h1>Loading...</h1></div><div id="toolbar"><table><tbody><tr><td>Rate</td><td><input type="range" min="0.0" max="100.0" step="1" data-bind="value: emissionRate, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: emissionRate"></td></tr><tr><td>Size</td><td><input type="range" min="2" max="60.0" step="1" data-bind="value: particleSize, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: particleSize"></td></tr><tr><td>Min Life</td><td><input type="range" min="0.1" max="30.0" step="1"data-bind="value: minimumParticleLife, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: minimumParticleLife"></td></tr><tr><td>Max Life</td><td><input type="range" min="0.1" max="30.0" step="1"data-bind="value: maximumParticleLife, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: maximumParticleLife"></td></tr><tr><td>Min Speed</td><td><input type="range" min="0.0" max="30.0" step="1" data-bind="value: minimumSpeed, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: minimumSpeed"></td></tr><tr><td>Max Speed</td><td><input type="range" min="0.0" max="30.0" step="1" data-bind="value: maximumSpeed, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: maximumSpeed"></td></tr><tr><td>Start Scale</td><td><input type="range" min="0.0" max="10.0" step="1" data-bind="value: startScale, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: startScale"></td></tr><tr><td>End Scale</td><td><input type="range" min="0.0" max="10.0" step="1" data-bind="value: endScale, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: endScale"></td></tr><tr><td>Gravity</td><td><input type="range" min="-20.0" max="20.0" step="1" data-bind="value: gravity, valueUpdate: 'input'"><input type="text" size="5" data-bind="value: gravity"></td></tr></tbody></table></div>
</template><script setup>
Cesium.Ion.defaultAccessToken = '你的defaultAccessToken'
import { onMounted } from "vue";
import * as Cesium from "cesium";
import "./Widgets/widgets.css";window.CESIUM_BASE_URL = "/"; // 设置Cesium静态资源路径(public目录)onMounted(async () => {const viewer = new Cesium.Viewer("cesiumContainer");//Set the random number seed for consistent results.Cesium.Math.setRandomNumberSeed(3);//Set bounds of our simulation timeconst start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));const stop = Cesium.JulianDate.addSeconds(start, 120, new Cesium.JulianDate());//Make sure viewer is at the desired time.viewer.clock.startTime = start.clone();viewer.clock.stopTime = stop.clone();viewer.clock.currentTime = start.clone();viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the endviewer.clock.multiplier = 1;viewer.clock.shouldAnimate = true;//Set timeline to simulation boundsviewer.timeline.zoomTo(start, stop);const viewModel = {emissionRate: 5.0,gravity: 0.0,minimumParticleLife: 1.2,maximumParticleLife: 1.2,minimumSpeed: 1.0,maximumSpeed: 4.0,startScale: 1.0,endScale: 5.0,particleSize: 25.0,};Cesium.knockout.track(viewModel);const toolbar = document.getElementById("toolbar");Cesium.knockout.applyBindings(viewModel, toolbar);const entityPosition = new Cesium.Cartesian3();const entityOrientation = new Cesium.Quaternion();const rotationMatrix = new Cesium.Matrix3();const modelMatrix = new Cesium.Matrix4();function computeModelMatrix(entity, time) {return entity.computeModelMatrix(time, new Cesium.Matrix4());}const emitterModelMatrix = new Cesium.Matrix4();const translation = new Cesium.Cartesian3();const rotation = new Cesium.Quaternion();let hpr = new Cesium.HeadingPitchRoll();const trs = new Cesium.TranslationRotationScale();function computeEmitterModelMatrix() {hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, hpr);trs.translation = Cesium.Cartesian3.fromElements(-4.0, 0.0, 1.4, translation);trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, rotation);return Cesium.Matrix4.fromTranslationRotationScale(trs, emitterModelMatrix);}const pos1 = Cesium.Cartesian3.fromDegrees(-75.15787310614596, 39.97862668312678);const pos2 = Cesium.Cartesian3.fromDegrees(-75.1633691390455, 39.95355089912078);const position = new Cesium.SampledPositionProperty();position.addSample(start, pos1);position.addSample(stop, pos2);const entity = viewer.entities.add({availability: new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({start: start,stop: stop,}),]),model: {uri: "/model/CesiumMilkTruck.glb",minimumPixelSize: 64,},viewFrom: new Cesium.Cartesian3(-100.0, 0.0, 100.0),position: position,orientation: new Cesium.VelocityOrientationProperty(position),});viewer.trackedEntity = entity;const scene = viewer.scene;const particleSystem = scene.primitives.add(new Cesium.ParticleSystem({image: "/model/smoke.png",startColor: Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),endColor: Cesium.Color.WHITE.withAlpha(0.0),startScale: viewModel.startScale,endScale: viewModel.endScale,minimumParticleLife: viewModel.minimumParticleLife,maximumParticleLife: viewModel.maximumParticleLife,minimumSpeed: viewModel.minimumSpeed,maximumSpeed: viewModel.maximumSpeed,imageSize: new Cesium.Cartesian2(viewModel.particleSize,viewModel.particleSize,),emissionRate: viewModel.emissionRate,bursts: [// these burst will occasionally sync to create a multicolored effectnew Cesium.ParticleBurst({time: 5.0,minimum: 10,maximum: 100,}),new Cesium.ParticleBurst({time: 10.0,minimum: 50,maximum: 100,}),new Cesium.ParticleBurst({time: 15.0,minimum: 200,maximum: 300,}),],lifetime: 16.0,emitter: new Cesium.CircleEmitter(2.0),emitterModelMatrix: computeEmitterModelMatrix(),updateCallback: applyGravity,}),);const gravityScratch = new Cesium.Cartesian3();function applyGravity(p, dt) {// We need to compute a local up vector for each particle in geocentric space.const position = p.position;Cesium.Cartesian3.normalize(position, gravityScratch);Cesium.Cartesian3.multiplyByScalar(gravityScratch,viewModel.gravity * dt,gravityScratch,);p.velocity = Cesium.Cartesian3.add(p.velocity, gravityScratch, p.velocity);}viewer.scene.preUpdate.addEventListener(function (scene, time) {particleSystem.modelMatrix = computeModelMatrix(entity, time);// Account for any changes to the emitter model matrix.particleSystem.emitterModelMatrix = computeEmitterModelMatrix();// Spin the emitter if enabled.if (viewModel.spin) {viewModel.heading += 1.0;viewModel.pitch += 1.0;viewModel.roll += 1.0;}});Cesium.knockout.getObservable(viewModel, "emissionRate").subscribe(function (newValue) {particleSystem.emissionRate = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "particleSize").subscribe(function (newValue) {const particleSize = parseFloat(newValue);particleSystem.minimumImageSize.x = particleSize;particleSystem.minimumImageSize.y = particleSize;particleSystem.maximumImageSize.x = particleSize;particleSystem.maximumImageSize.y = particleSize;});Cesium.knockout.getObservable(viewModel, "minimumParticleLife").subscribe(function (newValue) {particleSystem.minimumParticleLife = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "maximumParticleLife").subscribe(function (newValue) {particleSystem.maximumParticleLife = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "minimumSpeed").subscribe(function (newValue) {particleSystem.minimumSpeed = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "maximumSpeed").subscribe(function (newValue) {particleSystem.maximumSpeed = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "startScale").subscribe(function (newValue) {particleSystem.startScale = parseFloat(newValue);});Cesium.knockout.getObservable(viewModel, "endScale").subscribe(function (newValue) {particleSystem.endScale = parseFloat(newValue);});const options = [{text: "Circle Emitter",onselect: function () {particleSystem.emitter = new Cesium.CircleEmitter(2.0);},},{text: "Sphere Emitter",onselect: function () {particleSystem.emitter = new Cesium.SphereEmitter(2.5);},},{text: "Cone Emitter",onselect: function () {particleSystem.emitter = new Cesium.ConeEmitter(Cesium.Math.toRadians(45.0),);},},{text: "Box Emitter",onselect: function () {particleSystem.emitter = new Cesium.BoxEmitter(new Cesium.Cartesian3(10.0, 10.0, 10.0),);},},];Sandcastle.addToolbarMenu(options);
})</script><style scoped>
@import url(@/assets/bucket.css);* {margin: 0;padding: 0;
}#cesiumContainer {width: 100wh;height: 100vh;
}#toolbar {background: rgba(42, 42, 42, 0.8);padding: 4px;border-radius: 4px;
}#toolbar input {vertical-align: middle;padding-top: 2px;padding-bottom: 2px;
}#toolbar .header {font-weight: bold;
}
</style>