☰
Cesium粒子系统爆炸与火焰特效调参及性能优化
2026/11/4 10:12:16 网站建设 项目流程

做三维场景的朋友多半遇到过这样的场面:需求方指着屏幕说,这里爆炸得有点感觉,火得烧起来。打开 Cesium 翻文档,找到 ParticleSystem 那一节,示例代码复制过来能跑,但改一个参数就完全不是那么回事——火苗横着飞、爆炸糊成一团白、帧率掉到二十几,最后只能把粒子系统关掉当无事发生。Cesium 粒子系统这套东西,难点从来不在 API 会不会调,而在于你得同时对发射器几何、局部坐标与模型矩阵、颜色尺寸随生命周期的插值、以及渲染管线里的深度和后处理这几件事有数,才能把爆炸效果和火焰特效调出“像那么回事”的质感。这篇就按我自己的实操路径来:先讲清楚一个 ParticleSystem 在场景里到底做了什么、为什么不用 Billboard 硬拼,再逐项拆参数,然后给出爆炸和火焰两套能直接跑的完整代码,最后聊性能预算和那几个几乎人人都会撞上的坑。前端可视化方向的同学、做应急演练或工业仿真场景的开发者、以及刚接手三维项目的初中级工程师,都能从里面挑到能直接用的东西。

1. 先把粒子的账算清楚:Cesium 粒子系统的运行机制与选型

动手写配置之前,我习惯先把两件事确认掉:这套机制内部是怎么跑的,以及它在我的场景里会吃掉多少性能。这两个问题不想明白,后面调参数就是纯靠蒙。

1.1 一个 ParticleSystem 在场景里到底做了什么

Cesium 的粒子系统本质上是一个自动管理大量 Billboard 的容器。你给它一个发射器形状、一个发射速率、一组生命期和速度范围,它在内部维护一个粒子池,每帧做四件事:按 emissionRate 决定这一帧要不要生成新粒子、给新粒子在发射器局部空间里随机一个初始位置和速度、把所有粒子的年龄往前推 dt 并按 normalizedAge 插值出当前的颜色和尺寸、最后把每个粒子的局部坐标乘上模型矩阵变成世界坐标,塞进底层的 BillboardCollection 里绘制。

这里有个关键认知:粒子不是独立图元,它最终是以 Billboard 的形式被批处理的。所以一个粒子系统的几万个粒子,绘制的 draw call 数量远小于粒子数,这也是它比“手动创建几百个 Entity”高效得多的原因。但反过来说,每个 ParticleSystem 实例会因为贴图不同而落在不同的批次里,所以“十几个小粒子系统”往往比“一个大粒子系统”更费性能,这一点在做复合特效时特别重要。

还有一点容易被忽略:粒子的位置、速度、颜色这些数据在内部都是复用对象的,Cesium 不会每帧给每个粒子 new 一个 Cartesian3。这意味着你在updateCallback里如果随手new Cesium.Cartesian3(),就会把这份性能优势直接还回去,几万个粒子每帧各 new 一个向量,GC 压力能把帧时间拉高一大截。

1.2 为什么不用 Billboard 或自定义图元硬拼

有人会问,既然粒子就是 Billboard,那我直接批量创建 Billboard 行不行?技术上可行,但代价很大。Billboard 是 Entity 体系里的东西,每个 Entity 都带着 Property、Event 这些额外结构,几百个就开始有可感知的开销;而且你要自己维护它们的生命周期、自己算颜色插值、自己处理对象的回收复用,等于把 ParticleSystem 已经写好的那套逻辑重写一遍,还大概率写得更慢。

至于自定义 Primitive 加自定义着色器,那确实是最灵活的路子,能做贴图序列帧、能做基于噪声的软粒子、能做跟场景深度做软混合的边缘消隐。但它的门槛在于你得自己写顶点和片元着色器、自己管 VBO、自己处理 Cesium 的AutomaticUniforms和坐标变换,调试成本极高。我的判断标准很朴素:如果特效只需要“随机分布的、随时间变色变大的贴图点”,就用 ParticleSystem;如果要做流体般的连续形态、序列帧动画、或者需要和场景深度做软融合,才值得上自定义 Primitive。

ParticleSystem 有两个明确的短板得提前知道。第一,image只接受单张图,不支持图集和序列帧,所以那种逐帧播放的爆炸动画它做不了,只能靠多层粒子叠加来伪造层次。第二,它不直接暴露混合模式,只能靠贴图本身的透明通道来控制观感,想要加色发光的效果,得借助后处理 Bloom 或者额外叠加光晕面片。这两点在方案评审阶段就该说清楚,别等到做完了才发现做不出想要的效果。

1.3 两套矩阵:modelMatrix 与 emitterModelMatrix 的分工

这是新手最容易糊掉的地方。modelMatrix决定粒子系统这个“整体”在世界里的位置和朝向,emitterModelMatrix决定发射器相对这个整体的局部变换。两者的关系是先局部后世界:粒子在发射器的局部空间里生成,先乘emitterModelMatrix挪到系统局部空间的某个位置,再乘modelMatrix落到地球上。

实操里我通常这样分工:modelMatrix用Cesium.Transforms.eastNorthUpToFixedFrame(center)生成,center是目标的经纬度和高度,这样局部坐标系的 X 朝东、Y 朝北、Z 朝天,重力就是简单的 -Z 方向,省掉一堆坐标换算;emitterModelMatrix大多数时候直接给Cesium.Matrix4.IDENTITY,只有当发射点需要偏离系统原点(比如火焰要从烟囱口往上偏移两米)或者需要旋转发射方向时,才用Cesium.Matrix4.fromTranslation或者fromTranslationRotationScale去构造。

注意:eastNorthUpToFixedFrame生成的是 ENU 到 ECEF 的变换矩阵,局部 Z 轴就是当地天顶方向。如果你的场景里地球不是默认椭球、或者开了地形,这个“天顶”仍然是数学上的法线方向,跟实际地形坡度无关,贴着山坡放火焰时会看出偏差,需要额外抬高度。

2. 爆炸效果的核心参数拆解与配置

参数表在文档里都有,但哪个参数动一下会带来什么观感变化,文档不会告诉你。这一节按我调参的顺序来讲。

2.1 发射器选型:SphereEmitter、ConeEmitter 还是 CircleEmitter

Cesium 内置了四种发射器:CircleEmitter、ConeEmitter、SphereEmitter、BoxEmitter。选型直接决定特效的“体型”。

爆炸我固定用SphereEmitter,它的粒子从球体内随机一点向四面八方发射,天然就是各向同性的扩散,符合爆炸的物理直觉。参数只有一个半径,比如new Cesium.SphereEmitter(0.5),半径给 0.3 到 1.0 米之间比较合适,给太大粒子起点就散得太开,看起来像一团雾而不是一次爆开。

定向喷射类的效果(火焰、尾焰、喷气)用ConeEmitter,构造参数是锥半角,new Cesium.ConeEmitter(Cesium.Math.toRadians(14))表示粒子在偏离中心轴 14 度的锥内发射。角度小,火焰就细长集中;角度开到 30 度以上,就变成一团往上窜的火球。这个参数我一般从 10 度起步往上试,因为火焰在视觉上“收口”比“散开”更好看。

CircleEmitter适合地面扬尘、落点水花这种平面扩散;BoxEmitter用于需要矩形分布的场景,比如大面积烟雾带。四个发射器都能组合使用,同一位置挂两个不同发射器的系统,层次一下就出来了。

2.2 数量与时长:emissionRate、lifetime、minimumParticleLife 的三角关系

这三个参数决定了屏幕上同时存在多少粒子,直接对应性能开销,是整个配置里最需要算的部分。

公式很简单:

同屏粒子数 ≈ emissionRate × 粒子平均生命周期

但这里要区分两个“生命周期”。minimumParticleLife和maximumParticleLife是单个粒子的存活时间,在区间内随机取值;lifetime是整个系统的发射时长,也就是系统从开始到停止产生新粒子的时间。

爆炸的正确配法是:lifetime给一个很短的值(0.2 到 0.4 秒),让系统在瞬间喷完所有粒子就停止发射;emissionRate给高值(250 到 400),保证这 0.3 秒内能喷出足够的量;单个粒子寿命给 0.7 到 1.5 秒,让火焰有时间扩散和消散。按这组数字算:320 × 0.3 ≈ 96 个粒子被一次性喷出,每个存活 1 秒左右,所以最多同屏约 96 个,这个量级对任何设备都不算压力。

火焰正好相反:lifetime给Number.MAX_VALUE让它持续发射,emissionRate给中等值(30 到 60),单个粒子寿命短(0.5 到 1.1 秒)。40 × 0.8 ≈ 32 个同屏粒子,非常轻。

我见过最常见的错误是把爆炸的lifetime也设成无限大,结果emissionRate320 一直喷,一秒后就同屏三百多个粒子,两秒后六百多个,帧率肉眼可见地往下掉,还以为是 Cesium 性能不行。

2.3 颜色与尺寸曲线:用 startColor/endColor 做“假多段渐变”

startColor和endColor之间是线性插值,中间没有控制点。这意味着一颗粒子最多只能有“起点色”和“终点色”两种状态之间平滑过渡,做不出“白→黄→橙→红→透明”这种多段火焰色。

想做出层次,我的做法是分层叠加:亮芯一层用高亮黄白、短寿命、小尺寸;主体层用橙红、中等寿命、中尺寸;烟层用暗灰、长寿命、大尺寸、极低透明度。三层贴图共用一张径向渐变,只是颜色和尺寸不同,视觉上就会读成“一次有芯有焰有烟的爆炸”。这个思路在后面第 5 节会给出具体参数。

尺寸同理,startScale到endScale之间线性插值,最终屏幕尺寸 =imageSize×scale。所以你把imageSize设成 60×60 像素、startScale0.6、endScale2.4,实际渲染出来的就是 36 像素膨胀到 144 像素。调尺寸的时候优先动 scale,别动不动去改 imageSize,因为 imageSize 是所有粒子共享的基础尺寸,改它相当于整体缩放,调不出“由小变大”的过程感。

提示:Cesium 1.84 之后新增了sizeInMeters属性。默认 false,此时 imageSize 是像素单位,粒子不管离相机多远都是固定像素大小;设为 true 后 imageSize 按米计算,远距离会自然缩小。远景场景(比如几公里外看爆炸)必须开它,否则粒子会“贴”在屏幕上,非常出戏。

2.4 贴图从哪来:用 Canvas 现场生成径向光斑

粒子贴图的本质是一张带透明通道的白点,颜色交给 startColor 和 endColor 去染。所以千万不要做彩色贴图,那会和颜色插值叠加,出来就是脏的。

如果没有美术资源,可以直接用 Canvas 在运行时生成一张径向渐变图,省掉外部依赖,也避免跨域加载问题:

function makeSoftSprite(size, hardness) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = size; const ctx = canvas.getContext('2d'); const half = size / 2; const grd = ctx.createRadialGradient(half, half, 0, half, half, half); grd.addColorStop(0, 'rgba(255,255,255,1)'); grd.addColorStop(hardness, 'rgba(255,255,255,0.65)'); grd.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = grd; ctx.fillRect(0, 0, size, size); return canvas; }

hardness这个参数很好用:0.25 左右边缘柔和,适合烟雾和火焰外焰;0.6 以上中心瓷实,适合爆炸亮芯和火花。同一个函数生成两张不同硬度的图,一套特效的贴图资源就齐了。Cesium 的image参数直接接受HTMLCanvasElement,不需要转 dataURL,转成 dataURL 反而会因为字符串解析多一步开销。

如果想要更真实的不规则形状,可以在径向渐变的基础上叠一层随机噪声——往 canvas 上撒一些透明度随机的圆点,模拟火焰内部的翻腾结构。这个做法成本很低,但比纯径向光斑耐看很多。

3. 从爆炸到火焰:完整实现与逐段代码

参数讲完了,这一节直接上能跑的东西。

3.1 爆炸系统的完整配置与清理逻辑

先建一个工具函数生成模型矩阵:

function makeModelMatrix(lon, lat, height) { const center = Cesium.Cartesian3.fromDegrees(lon, lat, height); return Cesium.Transforms.eastNorthUpToFixedFrame(center); }

然后是爆炸本体。注意updateCallback里我处理阻力和重力,让粒子的初速度快速衰减,这样“炸开”的冲击感才明显:

function makeExplosion(viewer, modelMatrix, sprite) { const system = viewer.scene.primitives.add(new Cesium.ParticleSystem({ image: sprite, imageSize: new Cesium.Cartesian2(60, 60), startColor: new Cesium.Color(1.0, 0.92, 0.55, 1.0), endColor: new Cesium.Color(0.85, 0.18, 0.03, 0.0), startScale: 0.5, endScale: 2.6, minimumParticleLife: 0.7, maximumParticleLife: 1.5, minimumSpeed: 4.0, maximumSpeed: 15.0, emissionRate: 320, lifetime: 0.3, emitter: new Cesium.SphereEmitter(0.6), modelMatrix: modelMatrix, emitterModelMatrix: Cesium.Matrix4.IDENTITY, updateCallback: function (particle, dt) { // 局部坐标系 Z 轴朝天,重力直接作用在 z 分量上 particle.velocity.z -= 9.8 * dt; // 空气阻力,按指数衰减模拟 const k = Math.pow(0.12, dt); particle.velocity.x *= k; particle.velocity.y *= k; particle.velocity.z *= k; } })); return system; }

这段里Math.pow(0.12, dt)是阻力系数的正确写法。很多人直接写velocity *= 0.9,结果在高帧率下衰减得飞快、低帧率下几乎不衰减,同一个效果在不同机器上完全是两个样子。用pow(系数, dt)才是和帧率无关的。

清理逻辑必须自己写,Cesium 不会自动把到期的粒子系统从 primitives 里摘掉:

function spawnOnce(viewer, modelMatrix, sprite) { const system = makeExplosion(viewer, modelMatrix, sprite); const totalLife = 0.3 + 1.5 + 0.5; // 发射时长 + 最长粒子寿命 + 缓冲 setTimeout(function () { if (!viewer.isDestroyed()) { viewer.scene.primitives.remove(system); } }, totalLife * 1000); return system; }

这里的totalLife是按最长路径算的:发射阶段 0.3 秒,最后一颗粒子最多活 1.5 秒,再加 0.5 秒缓冲防止边界情况提前移除导致闪烁。少给缓冲时间,最外层那批粒子会被硬生生掐掉,边缘会有一帧突变的痕迹,很显眼。

3.2 动态火焰:持续发射、浮力与侧向摆动

火焰跟爆炸是两套逻辑:持续发射、寿命短、速度低、有向上的浮力加速度、还得有点左右摇摆。

function makeFlame(viewer, modelMatrix, sprite) { return viewer.scene.primitives.add(new Cesium.ParticleSystem({ image: sprite, imageSize: new Cesium.Cartesian2(44, 44), startColor: new Cesium.Color(1.0, 0.88, 0.42, 0.95), endColor: new Cesium.Color(0.55, 0.06, 0.0, 0.0), startScale: 0.85, endScale: 1.75, minimumParticleLife: 0.5, maximumParticleLife: 1.1, minimumSpeed: 1.2, maximumSpeed: 2.8, emissionRate: 42, lifetime: Number.MAX_VALUE, emitter: new Cesium.ConeEmitter(Cesium.Math.toRadians(15)), modelMatrix: modelMatrix, emitterModelMatrix: Cesium.Matrix4.IDENTITY, updateCallback: function (particle, dt) { const age = particle.normalizedAge; // 浮力:越老的粒子升得越快,模拟热气上升 particle.velocity.z += (1.2 + age * 2.0) * dt; // 侧向摆动:用正弦制造有节奏的飘动,而不是每帧随机抖动 const swayX = Math.sin(particle.age * 5.0 + particle.position.z) * 0.9; const swayY = Math.cos(particle.age * 4.2 + particle.position.x) * 0.7; particle.velocity.x += swayX * dt; particle.velocity.y += swayY * dt; // 水平阻力大于垂直阻力,火焰才收得拢 const k = Math.pow(0.35, dt); particle.velocity.x *= k; particle.velocity.y *= k; } })); }

摆动这块值得多说两句。千万不要在updateCallback里用Math.random()做抖动,那样每帧每个粒子的方向都在跳变,视觉上不是“飘动”而是“噪点抖动”,而且高帧率机器上抖得更密,看起来反而更差。用与粒子年龄、初始位置相关的正弦函数,既能保证有节奏的摆动,又天然和其他粒子错开相位,形成自然的翻滚感。

particle.position是局部坐标,量级在几米到几十米,把它当正弦的相位输入,同一次发射产生的一批粒子就会因为起点不同而各摇各的,不会整齐划一地一起摆。

3.3 位置绑定与事件触发:让爆炸落在该落的地方

爆炸一般不是常驻的,得由事件触发。常见两种:点击屏幕某处触发,或者某个动态目标到达某个位置触发。

如果要让火焰跟着一个移动的目标,不要重建粒子系统,而应该每帧更新modelMatrix。重建意味着几十毫秒内旧系统还在、新系统又开始喷,会看到明显的重影。正确做法是保留系统引用,在地点变化时重新计算矩阵:

viewer.scene.preUpdate.addEventListener(function () { if (!flameSystem || flameSystem.isDestroyed()) return; const now = Cesium.JulianDate.now(); const pos = targetProperty.getValue(now); if (!Cesium.defined(pos)) return; const m = Cesium.Transforms.eastNorthUpToFixedFrame(pos); Cesium.Matrix4.clone(m, flameSystem.modelMatrix); viewer.scene.requestRender(); });

这里有个细节:modelMatrix是直接赋值的属性,用Cesium.Matrix4.clone(m, flameSystem.modelMatrix)复用它内部已有的矩阵对象,而不是flameSystem.modelMatrix = m。后者每帧都会产生一个新矩阵对象,长跑下来 GC 会很频繁。

点击触发则用ScreenSpaceEventHandler:

const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction(function (movement) { const ray = viewer.camera.getPickRay(movement.position); const point = viewer.scene.globe.pick(ray, viewer.scene); if (!Cesium.defined(point)) return; const carto = Cesium.Cartographic.fromCartesian(point); const mm = Cesium.Transforms.eastNorthUpToFixedFrame(point); spawnOnce(viewer, mm, explosionSprite); }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

注意globe.pick拿到的是地表上的点,如果开了地形,得用viewer.scene.globe.getHeight(carto)拿地形高度再抬一点,否则爆炸会有一半嵌进地里。这个偏移量给 1 到 3 米比较稳,具体看爆炸的尺寸。

3.4 叠加 Bloom 后处理,让火苗真的“发光”

粒子本身只是贴图,最亮也就是白色,想要“发光”的观感必须靠后处理。Cesium 内置了泛光阶段,几行就能开:

viewer.scene.highDynamicRange = true; const bloom = viewer.scene.postProcessStages.bloom; bloom.enabled = true; bloom.uniforms.glowOnly = false; bloom.uniforms.contrast = 128; bloom.uniforms.brightness = -0.2; bloom.uniforms.delta = 1.0; bloom.uniforms.sigma = 2.5; bloom.uniforms.stepSize = 1.5;

contrast控制多亮的像素才算“亮到要泛光”,给 128 是比较通用的起点;brightness给负值能压住过曝,避免整个场景发灰;sigma决定光的扩散范围,2.0 到 3.0 之间比较自然,超过 4 就开始糊得像加了滤镜。

注意:Bloom 依赖highDynamicRange,部分移动端 GPU 不支持对应的浮点纹理扩展,开启后可能直接报错或者画面全黑。工程里最好做能力检测:先开着跑一帧,如果scene.context报错就回退到bloom.enabled = false,改用一层半透明的贴地发光面片顶替光晕。

4. 性能与稳定性:踩过的坑和排查方法

这段是我觉得整篇里最值钱的部分,因为文档里基本不会写。

4.1 粒子数量预算与帧率崩塌的真实原因

先给一个粗略的预算参考,来自我在中端笔记本和普通移动设备上的实测:

同屏粒子数桌面独显桌面集显中端移动设备
200 以内几乎无感几乎无感轻微
500 左右无感可感知明显掉帧
1500 以上开始掉帧明显卡顿不可用

真正拖垮帧率的往往不是粒子总数,而是粒子系统实例数量。因为每个实例是独立批次,十个各 100 粒子的系统,比一个 1000 粒子的系统慢不少。所以能合并就合并:同一张贴图、同一个位置的多层火焰,尽量塞进一个系统里用不同的 scale 范围区分,而不是拆成三四个系统。

另一个隐蔽的性能杀手是每帧创建对象。在updateCallback里写new Cesium.Color(...)或者new Cesium.Cartesian3(),几千个粒子就是每帧几千次分配,帧时间会莫名其妙地飘高,Profile 一看全是 GC。把常量提到外面用闭包变量,或者直接用数值运算改velocity.x/y/z,问题就消失了。

4.2 六个高频异常与定位路径

火焰横着飞。九成是modelMatrix建错了。eastNorthUpToFixedFrame的 Z 轴是天顶方向,如果误用了Transforms.headingPitchRollToFixedFrame且 pitch 给错,整个局部坐标系是歪的,粒子自然朝斜上飞。排查方法:在那个位置放一个小的Cesium.BoxGraphics(10×10×30 米,高的那个方向),看它是不是竖直的。

粒子完全不显示。按这个顺序查:viewer.clock.shouldAnimate是不是被设成了 false;viewer.scene.requestRenderMode是不是开着但没主动请求渲染;lifetime是不是给了 0;image是不是加载失败(跨域图片会被静默丢弃);emissionRate是不是写成了 0。这五个原因覆盖了我遇到的九成“什么都没出来”的情况。

粒子贴着屏幕不动。远景场景忘了开sizeInMeters = true。像素尺寸的粒子不随距离缩放,所以相机拉近拉远它都一样大,看起来像粘在屏幕上。

爆炸有一半埋在地里。位置高度没算地形,或者emitterModelMatrix做了向下的平移。检查modelMatrix的高度值,正常应该在地形高度往上 1 到 3 米。

开了按需渲染后粒子“卡住”。requestRenderMode = true时,Cesium 只在场景变化时渲染,粒子动画不触发变化,所以就冻住了。要么关掉按需渲染,要么在postRender里主动请求下一帧,且只在有活跃粒子系统时请求,兼顾省电。

调整了clock.multiplier之后粒子速度不对。粒子系统的时间步取的是场景时钟的差值,倍率调大粒子也跟着加速,寿命也跟着缩短。做特效时把倍率固定成 1.0,需要变速就单独处理。

4.3 常见问题速查表

现象最可能原因快速验证处理方式
完全看不到粒子时钟停摆或按需渲染打印clock.shouldAnimate开启时钟或主动请求渲染
粒子方向歪斜模型矩阵朝向错误放一个立方体参照用 ENU 矩阵重建
爆炸持续不停lifetime未设上限打印同屏粒子数设短 lifetime 并定时移除
边缘闪一下消失移除时机太早延后清理看是否消失加 0.5 秒缓冲
帧率周期性抖动updateCallback内分配对象看 GC 曲线数值运算替代 new
整体发灰过曝Bloom 参数过界关掉 Bloom 对比降 brightness、调 contrast
近处巨大远处正常开了sizeInMeters切换开关对比按场景远近取舍

5. 组合玩法与调试工作流

单个粒子系统能做的事情有限,真正的质感来自叠加和节奏。

5.1 三层叠加:亮芯、橙焰、烟

同一个modelMatrix上挂三个系统,参数各有侧重:亮芯层用高硬度贴图、startScale0.4、endScale1.2、寿命 0.3 到 0.6 秒、颜色从白黄到亮橙、发射速率给高值,负责那一瞬间的“闪光”;橙焰层用软贴图、尺寸大一倍、寿命 0.8 到 1.6 秒、颜色从橙红到深红透明,负责扩散开的火团;烟层用最软的贴图、尺寸再大 2 到 3 倍、寿命 3 到 5 秒、颜色从深灰到透明、速度极低,负责残留感。

三层的时间尺度差是层次感的来源:亮芯一闪而过,橙焰持续一秒多,烟慢慢升腾几秒。如果三层生命周期都设成一样,视觉上就是一团同步变大变淡的东西,怎么调都像假火。

再加一层“碎屑”会更完整:用SphereEmitter加高初速(10 到 25 米/秒)、小尺寸(imageSize8×8)、短寿命(0.4 到 0.9 秒)、在updateCallback里给足重力,就是爆炸飞溅的火星。这一层数量可以稍微给多一点,二三十个就够,多了反而糊。

5.2 一套顺手的参数调试流程

我自己的调参顺序是固定的,按这个来能少走很多弯路:

第一步,把startColor和endColor都设成纯白不透明,startScale和endScale都设成 1,先确认位置和数量是对的。这一步只看形状对不对,别看颜色。

第二步,调emissionRate和lifetime,把同屏粒子数调到目标范围,同时确认发射时长的节奏对不对。

第三步,调minimumSpeed和maximumSpeed,用startScale、endScale固定不动,观察扩散范围。速度大的粒子会更快飞出视野,看起来就“快而散”。

第四步,才去调颜色和透明度。这时候可以先给一个夸张的高饱和色,确认颜色插值的走向符合预期,再压回正常值。

第五步,最后加updateCallback的物理,然后开 Bloom。

这个顺序的核心逻辑是每次只让一个维度变化,不然你根本分不清观感变化是哪个参数带来的。我见过太多人一上来就颜色、尺寸、速度、物理全开,然后调两小时都在原地转圈。

另外有个小技巧:把常用参数抽成一个flamePreset对象,调的时候只改这个对象再重建系统,比在源码里改数字然后刷新快得多。再进一步,可以挂一个简单的 dat 面板做实时调参,调完把最终值写回代码,效率能翻几倍。

最后说一个我在实际项目里的体会:Cesium 粒子系统做出来的特效,永远比在离线渲染器里看到的效果“薄”一些,因为它没有体积光、没有真正的软粒子、也没有粒子间的相互遮挡。所以不要在单个粒子上死磕真实感,把精力花在层次、节奏和位置匹配上——一个位置对得上、时机卡得准、三四个系统配合良好的简单爆炸,观感远好于一个参数调到极致但位置飘在半空的复杂爆炸。还有一点,特效的时长远比你想的短更耐看,爆炸从爆开到消散控制在两秒以内,火焰的收口要果断,拖长了就变成了一团挂在场景里的橙色云。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询