跳转到内容

粒子

粒子用于火焰、烟雾、爆炸、火花、雨、魔法等特效。Estella 在 C++/WebAssembly 内核里用 CPU 模拟粒子,并按发射器GPU 实例化为单次 draw call 渲染,所以一个场景可以廉价地 跑上千个粒子。你用一个 ParticleEmitter 组件声明式地描述特效,再通过 Particle 资源 控制播放。

每个发射器拥有一个最多 maxParticles 的固定粒子池。它每帧按 rate(每秒个数)生成新 粒子(外加可选的爆发),每个粒子:

  1. 生成在发射器形状内的某处,初速度的大小和方向在配置的 min/max 之间随机取。
  2. 存活 lifetimeMin…lifetimeMax 秒(随机),期间它的颜色尺寸从起始值 动画到结束值。
  3. 运动受自身速度加上 gravitydamping 影响,并可选地自转(angularVelocity)。
  4. 生命耗尽时消亡,回到池中。

一切都是逐粒子…Min…Max 之间随机——两者设相等得到统一外观,拉开则得到 变化。随生命的动画默认用 startColor/endColor + startSize/endSize 配一条缓动曲线, 或用完整的渐变/曲线做精确控制(见下)。

粒子系统在主包里。编辑器和 Web 运行时会自动安装插件。在自定义 App 里手动加一次:

import { particlePlugin } from 'esengine';
app.addPlugin(particlePlugin);

给实体加一个 ParticleEmitter 它就开始发射(playOnStart 默认开)。下面做一个循环的 火星喷泉,从黄色渐隐为透明红并在重力下抛出:

import {
defineSystem, Commands, Transform, ParticleEmitter, EmitterShape,
} from 'esengine';
const spawnEmbers = defineSystem([Commands()], (cmds) => {
cmds.spawn()
.insert(Transform, { position: { x: 0, y: -200, z: 0 } })
.insert(ParticleEmitter, {
rate: 120,
lifetimeMin: 1.2, lifetimeMax: 2.0,
shape: EmitterShape.Cone, shapeAngle: 20,
speedMin: 300, speedMax: 520,
startSizeMin: 12, startSizeMax: 20,
endSizeMin: 0, endSizeMax: 0,
startColor: { r: 1, g: 0.9, b: 0.3, a: 1 },
endColor: { r: 1, g: 0.2, b: 0.1, a: 0 },
gravity: { x: 0, y: -400 },
});
});

每个字段都是可选的——省略即取下方默认值。尺寸、距离、速度以世界单位计(默认 PPU 下即 像素);角度以度计。

属性 类型 默认 说明
rate number 10 持续发射,每秒粒子数。0 = 只用爆发。
burstCount number 0 每次爆发释放的粒子数。0 禁用爆发。
burstInterval number 1 两次爆发之间的秒数。
duration number 5 发射器停止前运行的秒数(见 looping)。
looping boolean true duration 后重新开始,而不是停止。
playOnStart boolean true 实体一存在就开始发射。
maxParticles number 1000 池大小——存活粒子的硬上限。
属性 类型 默认 说明
lifetimeMin number 5 最短粒子寿命,秒。
lifetimeMax number 5 最长粒子寿命,秒。

粒子从哪生成及其初始方向。见 发射器形状

属性 类型 默认 说明
shape EmitterShape Cone Point / Circle / Rectangle / Cone
shapeRadius number 100 Circle / Cone 的半径。
shapeSize Vec2 {100, 100} Rectangle 的盒子范围。
shapeAngle number 25 Cone 的锥半角(°)。
属性 类型 默认 说明
speedMin number 500 最慢初速度。
speedMax number 500 最快初速度。
angleSpreadMin number 0 发射方向弧的起始(°)。
angleSpreadMax number 360 发射方向弧的结束(°)。
gravity Vec2 {0, 0} 施加到每个粒子的恒定加速度。
damping number 0 每秒速度阻尼(0 = 无)。
属性 类型 默认 说明
noiseStrength number 0 湍流推动粒子的峰值流速(px/s)。0 = 关闭该模块。
noiseFrequency number 0.01 场的空间尺度 —— 越大漩涡越紧、越繁密。
noiseScrollSpeed number 0 场随时间漂移的速度(0 = 静止场)。
noiseOctaves number 1 分形细节层数。1 = 平滑流动;越多越细碎的湍流。

见下文噪声与湍流

属性 类型 默认 说明
startSizeMin number 100 生成时的最小尺寸。
startSizeMax number 100 生成时的最大尺寸。
endSizeMin number 100 消亡时的最小尺寸。
endSizeMax number 100 消亡时的最大尺寸。
sizeEasing ParticleEasing Linear 从起始到结束尺寸的插值。
sizeCurve Curve 完整的随生命尺寸曲线;覆盖 start/end(见)。
属性 类型 默认 说明
startColor Color {1,1,1,1} 生成时的颜色(RGBA,0..1)。
endColor Color {1,1,1,0} 消亡时的颜色——默认把 alpha 渐隐到 0。
colorEasing ParticleEasing Linear 从起始到结束颜色的插值。
colorGradient Gradient 完整的随生命颜色渐变;覆盖 start/end。
属性 类型 默认 说明
rotationMin number 0 生成旋转的最小值(°)。
rotationMax number 0 生成旋转的最大值(°)。
angularVelocityMin number 0 最慢自转(°/s)。
angularVelocityMax number 0 最快自转(°/s)。

贴图与精灵表 (Texture & sprite sheet)

Section titled “贴图与精灵表 (Texture & sprite sheet)”
属性 类型 默认 说明
texture asset 粒子贴图。无贴图的粒子是柔和的方块。
spriteColumns number 1 精灵表列数(1 = 整张贴图)。
spriteRows number 1 精灵表行数。
spriteFPS number 10 播放精灵表时的每秒帧数。
spriteLoop boolean true 循环精灵表 vs 停在最后一帧。
属性 类型 默认 说明
blendMode BlendMode Additive 粒子如何合成。见 混合模式
layer number 0 决定绘制顺序的排序层。
material asset 可选的自定义材质/着色器。
simulationSpace SimulationSpace World WorldLocal(见)。
enabled boolean true 关掉即冻结并隐藏,而不移除组件。
属性 类型 默认 说明
subEmitter entity 粒子触发时发射的子发射器实体。0/未设 = 关闭。
subEmitterTrigger SubEmitterTrigger Death 在每个粒子的 Death(死亡)或 Birth(出生)时触发。
subEmitterChance number 1 触发粒子发射子爆发的概率(0..1)。
subEmitterInheritVelocity number 0 传给子爆发的父粒子速度比例。

见下文子发射器

属性 类型 默认 说明
trailEnabled boolean false 每个粒子拖一条渐细的 ribbon。关 = 不记录历史。
trailWidth number 8 ribbon 头部宽度;向尾部收细到一点。
trailPoints number 6 历史点数(2..12)—— 越长越平滑但越贵。
trailMinDistance number 6 记录新拖尾点前粒子须移动的最小世界距离。

见下文逐粒子拖尾

视口里的发射器形状 gizmo——锥形、圆形、矩形

视口里的发射器形状 gizmo——锥形、圆形、矩形,各自画在粒子生成的位置(开 Preview FX 可在编辑态看它们运行)。

EmitterShape 设定生成区域与初始方向:

形状 使用 行为
Point 所有粒子在实体原点生成。
Circle shapeRadius 在圆盘内生成;方向向外辐射。
Rectangle shapeSize 在矩形盒内生成。
Cone shapeRadiusshapeAngle 在锥内生成——一束有方向的喷射(喷泉、推进器)。

angleSpreadMin…Max 弧进一步约束发射方向,所以任何形状都能定向(例如一个只向上发射的 Circle,用 0…180)。

多数特效用 startColorendColorstartSizeendSize 加一条缓动曲线就够了 ——它们是普通字段,代码里和编辑器里都能用。ParticleEasing 取值为 LinearEaseInEaseOutEaseInOut

要精确控制,在 ParticleEmitter 检视面板里编辑一条渐变(颜色)或曲线(尺寸倍率) ——一串在归一化生命 t(0..1)上的停靠点/关键点。引擎会把它们烘焙成一张查找表(LUT), 模拟按粒子年龄采样。编辑出的数据形如:

// colorGradient: hot white -> orange -> smoke, fading out.
{
stops: [
{ t: 0.0, color: { r: 1, g: 1, b: 0.8, a: 1 } },
{ t: 0.4, color: { r: 1, g: 0.5, b: 0.1, a: 1 } },
{ t: 1.0, color: { r: 0.2, g: 0.2, b: 0.2, a: 0 } },
],
}
// sizeCurve: swell in, then shrink away (multiplies the particle's start size).
{ keys: [{ t: 0, v: 0.2 }, { t: 0.3, v: 1 }, { t: 1, v: 0 }] }

要在运行时程序化改变这条曲线,通过资源上传一张预烘焙的查找表——颜色是 N×4 的 RGBA Float32Array,尺寸是 N 个标量。传 null 清空并回退到 start/end + 缓动:

import { defineSystem, Res, Particle } from 'esengine';
const recolor = defineSystem([Res(Particle)], (particles) => {
const lut = new Float32Array([ /* r,g,b,a, r,g,b,a, … */ ]);
particles.setColorLut(entity, lut); // or particles.setColorLut(entity, null)
});

texture 指向一张精灵表,并设 spriteColumns / spriteRows,让每个粒子按 spriteFPS 在各帧间播放。一张 4×4 的爆炸表,在每个粒子的生命里只播一次:

.insert(ParticleEmitter, {
texture: 'effects/explosion.png',
spriteColumns: 4, spriteRows: 4, spriteFPS: 24, spriteLoop: false,
});

直线运动的粒子显得机械。噪声模块在速度和重力之上叠加一层 curl 噪声流场, 让粒子沿着平滑、旋绕的气流游走 —— 卷曲的烟、飘动的火星、漂移的魔法。

这个流场是无散度的(它是噪声势场的旋度),意味着粒子不会像原始随机抖动那样 挤成一团或散出空洞 —— 运动始终均匀而自然。它是纯 CPU 数学,在每个平台 (含微信)上逐字节一致,并且当 noiseStrength0 时完全零开销。

.insert(ParticleEmitter, {
// …发射、生命周期、颜色…
noiseStrength: 120, // 气流推力有多强(px/s)
noiseFrequency: 0.01, // 越小越是宽缓的漩涡;越大越是细密的涡流
noiseScrollSpeed: 0.5, // 让场漂移,使流动持续演化
noiseOctaves: 2, // 1 = 平滑;越多越细碎的湍流
});

调参建议:

  • noiseStrength 是主旋钮 —— 从粒子 speed 的一小部分起调,逐步加大到游走感明显。
  • noiseFrequency 决定漩涡大小。上升的烟喜欢小值(~0.005);火花四溅的迸发喜欢大值(~0.03)。
  • noiseScrollSpeed > 0 能让看似静止的烟柱保持缓慢的活气;设 0 则是粒子穿流而过的固定场。
  • noiseOctaves 以开销换细节 —— 12 已覆盖多数特效。

子发射器让一个发射器在粒子所在位置生成另一个发射器的爆发 —— 经典的升空后爆炸的 烟花弹、拖着烟雾的火箭、裂成火星的火花。无需逐帧代码:你连好一个引用,模拟就会在 选定事件上触发爆发。

子发射器是一个当作模板用的普通 ParticleEmitter 实体 —— 设 rate: 0playOnStart: false,它就不会自己发射,只在父级触发时爆发。它的 burstCount 决定每次子爆发生成多少粒子。把父级的 subEmitter 指向那个子实体:

import { SubEmitterTrigger } from 'esengine';
// 爆发模板 —— 一个只在被触发时才发射的普通发射器。
const shell = cmds.spawn()
.insert(Transform, {})
.insert(ParticleEmitter, {
rate: 0, playOnStart: false, burstCount: 44, // 每次爆发 44 颗火星
lifetimeMin: 0.6, lifetimeMax: 1.1, speedMin: 160, speedMax: 340,
startColor: { r: 1, g: 0.85, b: 0.35, a: 1 }, endColor: { r: 1, g: 0.3, b: 0.1, a: 0 },
gravity: { x: 0, y: -150 },
}).id();
// 火箭 —— 每个粒子死亡时,在该处触发爆发。
cmds.spawn()
.insert(Transform, { position: { x: 0, y: -230, z: 0 } })
.insert(ParticleEmitter, {
rate: 2.5, lifetimeMin: 1.4, lifetimeMax: 1.8, speedMin: 560, speedMax: 660,
angleSpreadMin: 84, angleSpreadMax: 96,
subEmitter: shell,
subEmitterTrigger: SubEmitterTrigger.Death, // 或 Birth
subEmitterInheritVelocity: 0.15, // 让火星带上一点火箭的运动
});
  • subEmitterTrigger —— Death 在每个父粒子消亡时触发(炮弹爆炸、碎片); Birth 在每个粒子出生时触发(沿着一道粒子流持续洒下子爆发)。
  • subEmitterChance —— 设为小于 1,只让部分粒子触发子级,稀释过密的效果。
  • subEmitterInheritVelocity —— 把父粒子速度的一部分带进爆发,让火星随着火箭飞,而不只是四散。

在编辑器里,subEmitter 是一个实体选择器 —— 拖入场景中任意发射器实体即可;引用会在 场景加载时自动重映射。

打开 trailEnabled,每个粒子都会沿最近的运动轨迹拖出一条渐细的 ribbon —— 彗星火花、 曳光弹、旋绕的魔法尾迹。这和独立的 TrailRenderer 不同:后者 给单个实体拖尾,而这里(可达数千个)每个粒子都有自己的尾迹。

.insert(ParticleEmitter, {
rate: 40, speedMin: 220, speedMax: 420, lifetimeMin: 0.7, lifetimeMax: 1.2,
startColor: { r: 0.6, g: 0.9, b: 1, a: 1 }, endColor: { r: 0.2, g: 0.4, b: 1, a: 0 },
trailEnabled: true,
trailWidth: 10, // 粒子处的 ribbon 宽度,向后收细到一点
trailPoints: 8, // 保留多少轨迹点(2..12)
trailMinDistance: 5, // 每移动 5 个世界单位记录一个点
});

ribbon 头部取粒子当前颜色,沿长度渐隐到透明,所以拖尾自动继承你的随生命颜色。它在 CPU 上生成,走和 TrailRenderer 相同的三角带 batch 路径 —— 无特殊 GPU 路径 —— 因此 和其他 2D 可渲染物一样排序混合。

拖尾是可选项,因为它比普通粒子贵:每粒子一小段位置历史(受 trailPoints 限制)+ 每帧 ribbon 几何。trailPoints 保持适中(68 足够),重拖尾特效请调低 maxParticles

噪声模块是烘进单个发射器的自带湍流,而力场是你放进场景的外部影响: 它是自己的实体、带 ParticleForceField 组件,作用于范围内每个发射器的世界空间粒子。 阵风、黑洞、漩涡、静止的空气团 —— 放一个进去,整个场景的粒子都会响应。

import { ParticleForceField, ForceFieldType } from 'esengine';
cmds.spawn()
.insert(Transform, { position: { x: 0, y: 0, z: 0 } })
.insert(ParticleForceField, {
type: ForceFieldType.Vortex,
strength: 450, // 加速度(px/s²);Point 场取负值即从吸引变排斥
radius: 320, // 0 = 影响整个场景;> 0 = 只影响范围内的粒子
falloff: true, // 在半径边缘把力衰减到 0
});
类型 效果
Directional 沿 direction 的恒定推力 —— 风、上升气流、水流。
Point 朝力场吸引(strength > 0)或排斥(strength < 0)—— 引力井、爆炸。
Vortex 绕力场切向旋绕 —— 漩涡、龙卷、星系。
Drag 在区域内阻尼速度 —— 减速粒子的静止空气/水团。

力场是纯 CPU:模拟每帧收集活跃力场,和重力一样折进同一套速度积分,无新子系统。Directional 用它的 direction;Point/Vortex/Drag 从力场→粒子向量推导方向,忽略它。

两个注意:力场只作用于世界空间粒子(Local 发射器的粒子随发射器移动、忽略力场); strength: 0enabled: false 直接跳过该力场。

打开 collisionEnabled,粒子就会在世界高度 collisionFloor 处的水平地面平面上弹跳 —— 雨点砸地、火花在表面上溅跳、雪花落定。纯 CPU 碰撞,不依赖物理。

.insert(ParticleEmitter, {
rate: 60, speedMin: 200, speedMax: 400, shape: EmitterShape.Cone, shapeAngle: 50,
gravity: { x: 0, y: -500 },
collisionEnabled: true,
collisionFloor: -200, // 粒子落到的世界 Y
collisionBounce: 0.5, // 0 = 立即停住,1 = 完全弹性
collisionFriction: 0.2, // 每次弹跳损失的水平速度
collisionLifetimeLoss: 0.1, // 每次弹跳烧掉的生命比例(落定时渐隐)
});

当世界空间粒子落到 collisionFloor 以下,它被拉回平面、竖直速度按 collisionBounce 反射缩放;collisionFriction 削掉水平速度,collisionLifetimeLoss 加速其衰老,让溅 射渐隐而不是永远蹦跳。

取值 粒子跟随发射器吗? 用于
World(默认) 否——它们留在出生的地方 拖尾、尾焰、移动物体身后留下的火花
Local 是——它们随实体的 transform 移动 光环、护盾、刚性附着在移动体上的特效

BlendMode 控制粒子如何叠加到场景上:

模式 效果
Normal 标准 alpha 混合。
Additive(默认) 颜色相加——辉光、火焰、魔法、光。
Multiply 变暗——烟雾、阴影。
Screen 提亮,比 additive 更柔和。
PremultipliedAlpha 用于预乘 alpha 的贴图。

在系统里读 Particle 资源(或在 ECS 之外用 app.getResource(Particle))来命令式地 驱动发射器:

import { defineSystem, Res, Particle } from 'esengine';
const control = defineSystem([Res(Particle)], (particles) => {
particles.play(entity); // start / resume emitting
particles.stop(entity); // stop emitting (live particles finish)
particles.reset(entity); // clear all live particles immediately
const n = particles.getAliveCount(entity); // live particle count
});
方法 说明
play(entity) 开始或恢复发射。
stop(entity) 停止发射;已有粒子活完自己的寿命。
reset(entity) 杀掉所有存活粒子并重置发射器。
getAliveCount(entity) 当前存活的粒子数。
setColorLut(entity, lut | null) 上传烘焙好的颜色 LUT(见上),或清空。
setSizeLut(entity, lut | null) 上传烘焙好的尺寸 LUT,或清空。
  • 控制 maxParticles 它是固定池;按最坏情况定大小,别更高。多个小发射器比一个巨大的 更利于剔除。
  • 辉光优先用 Additive(火焰、火花、魔法)——它掩盖 overdraw 且无需排序。不透明的 烟尘留给 Normal/Multiply
  • 把粒子贴图打成图集并共享一个 material,让发射器合批。
  • 拉开 min/max 得到自然的变化;min/max 相等看起来很机械。
  • 复用发射器:用 reset + play 而不是生成/销毁。
  • 游戏代码里粒子在播放模式推进;编辑器里 Preview FX 开关让它们在创作时保持鲜活。