做三维场景的朋友多半遇到过这样的场面:需求方指着屏幕说,这里爆炸得有点感觉,火得烧起来。打开 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 粒子系统做出来的特效,永远比在离线渲染器里看到的效果“薄”一些,因为它没有体积光、没有真正的软粒子、也没有粒子间的相互遮挡。所以不要在单个粒子上死磕真实感,把精力花在层次、节奏和位置匹配上——一个位置对得上、时机卡得准、三四个系统配合良好的简单爆炸,观感远好于一个参数调到极致但位置飘在半空的复杂爆炸。还有一点,特效的时长远比你想的短更耐看,爆炸从爆开到消散控制在两秒以内,火焰的收口要果断,拖长了就变成了一团挂在场景里的橙色云。