
做三维场景的朋友多半遇到过这样的场面需求方指着屏幕说这里爆炸得有点感觉火得烧起来。打开 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 还是 CircleEmitterCesium 内置了四种发射器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 会很频繁。点击触发则用ScreenSpaceEventHandlerconst 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.BoxGraphics10×10×30 米高的那个方向看它是不是竖直的。粒子完全不显示。按这个顺序查viewer.clock.shouldAnimate是不是被设成了 falseviewer.scene.requestRenderMode是不是开着但没主动请求渲染lifetime是不是给了 0image是不是加载失败跨域图片会被静默丢弃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 粒子系统做出来的特效永远比在离线渲染器里看到的效果“薄”一些因为它没有体积光、没有真正的软粒子、也没有粒子间的相互遮挡。所以不要在单个粒子上死磕真实感把精力花在层次、节奏和位置匹配上——一个位置对得上、时机卡得准、三四个系统配合良好的简单爆炸观感远好于一个参数调到极致但位置飘在半空的复杂爆炸。还有一点特效的时长远比你想的短更耐看爆炸从爆开到消散控制在两秒以内火焰的收口要果断拖长了就变成了一团挂在场景里的橙色云。