Particles
GPU-система частиц как узел. Дай ей Material и настройки эмиттера, добавь в
сцену — и она стримит частицы: дым, искры, пыль. Расширяет
Node: двигай/вращай узел, чтобы двигать эмиттер. Каждая опция также живой
сеттер, поэтому rate, color, gravity, … можно менять в рантайме.
Обзор
const sparks = new Particles({
material: Material.unlit({ color: '#ffcc44' }),
rate: 80, // частиц/секунду
shape: { type: 'point', v: [0, 0, 0] },
startVelocity: { dir: [0, 1, 0], speed: { min: 2, max: 4 } },
gravity: 9.8,
lifetime: { min: 0.6, max: 1.2 },
size: { min: 0.05, max: 0.12 },
color: dynamicColor('#ffcc44', 0.7, '#ffffff', 1, '#000000'), // затухание за время жизни
})
sparks.position = [0, 1, 0]
scene.add(sparks)
crate.addEventListener('click', () => sparks.spawn(40)) // всплеск поверх rateОпции
new Particles(options: {
material: Material // обязательно — как рисуется каждая частица
rate?: number // частиц в секунду
shape?: { type: 'point', v: Vec3Like } // спавн со смещением (относительно эмиттера)
| { type: 'box', min: Vec3Like, max: Vec3Like } // спавн случайно внутри бокса
startVelocity?: Vec3Like | VelocityValue // см. ниже
gravity?: number // ускорение вниз, мировые единицы/с²
drag?: number // затухание скорости — выше тормозит частицы быстрее
lifetime?: number | { min, max } // секунды; диапазон = случайно на частицу
color?: DynamicColor // константа, случайный диапазон или кривая dynamicColor()
size?: DynamicValue // константа, диапазон или кривая dynamic() (алиас custom[0])
rotation?: DynamicValue // то же (алиас custom[1])
noise?: { strength?, frequency?, speed? } | null // турбулентность; все поля по умолчанию 1
})Всё, кроме material, имеет нативный дефолт — new Particles({ material }) уже эмитит. Поля типа
number | { min, max } принимают плоское значение («всегда это») или диапазон («случайно на
частицу»).
rate применяет только простое число — диапазон { min, max } типизирован, но молча игнорируется.
startVelocity
Простой Vec3Like — фиксированное направление запуска; объект выбирает режим направления плюс
опциональную рандомизацию:
startVelocity: {
dir?: Vec3Like // запуск вдоль этого направления, или…
from?: Vec3Like // …прочь от этой точки, или…
to?: Vec3Like // …к этой точке
speed?: number | { min: number, max: number } // масштабирует направление; диапазон = на частицу
randomizeAngle?: { min: Vec3Like, max: Vec3Like } // дрожание угла запуска в пределах границ
}sparks.startVelocity = {
dir: [0, 1, 0],
speed: { min: 2, max: 5 },
randomizeAngle: { min: [-15, 0, -15], max: [15, 0, 15] },
}Живые сеттеры и методы
sparks.spawn(count): this // эмитировать всплеск прямо сейчас, поверх `rate` — чейнится
sparks.material // get/set — сменить материал отрисовки на лету
// живые сеттеры только-для-записи (те же типы, что опции):
sparks.rate = 120
sparks.shape = { type: 'box', min: [-1, 0, -1], max: [1, 0, 1] }
sparks.startVelocity = [0, 3, 0]
sparks.gravity = 0
sparks.drag = 0.5
sparks.lifetime = { min: 0.5, max: 1 }
sparks.color = '#88ccff'
sparks.size = dynamic(0.1, 'multiply', 0, 1, 1, 4)
sparks.rotation = { min: 0, max: 360 }
sparks.noise = { strength: 2 } // или null, чтобы отключить
sparks.custom[0] = 0.2 // сырые слоты параметров кривой 0..3; size/rotation — алиасы 0/1
sparks.custom[2] = dynamic(0, 'inc', 0, 0, 1, 1) // слоты 2/3 питают кастомный материалЖизненный цикл: система эмитит непрерывно с конструирования; sparks.destroy() (из Node) убирает
её. Для одноразового эффекта поставь rate: 0 и вызывай spawn(n).
Кривые за время жизни частицы — dynamic() / dynamicColor()
size, rotation, custom[i] и color могут анимироваться за время жизни каждой частицы. Кривая —
это плоский список стопов (t, значение), где t идёт 0..1 за жизнь частицы.
dynamic(
baseValue: number | { min, max }, // значение при рождении (диапазон = случайно на частицу)
type: 'multiply' | 'inc', // кривая умножает базу или прибавляет к ней
...stops: (number | { min, max })[] // плоско t, значение, t, значение, … (диапазон значения = случайно на частицу)
): DynamicValue
dynamicColor(
baseValue: string | { min, max }, // стартовый цвет (диапазон = случайно между двумя цветами)
...stops: (number | string | { min, max })[] // плоско t, цвет, t, цвет, …
): DynamicColor // кривая УМНОЖАЕТ базовый цветsparks.size = dynamic(0.1, 'multiply', 0, 1, 1, 4) // вырасти до 4× к концу жизни
sparks.size = dynamic(0.3, 'multiply', 0, 1, 1, 0) // сжаться в ничто
sparks.color = dynamicColor('#ffaa33', 0.7, '#ffffff', 1, '#000000') // затухание в чёрный к концуdynamicColor умножает базу, поэтому его главное применение — затухание яркости/альфы к смерти.
Держи t по возрастанию 0 → 1 и число стопов малым — стопы запекаются в короткую нативную кривую,
а не в произвольный сплайн.
Подводные камни
// ✗ покадровый spawn() для имитации частоты эмиссии
setLoop(() => sparks.spawn(1))
// ✓ для этого есть rate
sparks.rate = 60
// ✗ ждать, что диапазон rate сработает
new Particles({ material, rate: { min: 10, max: 50 } }) // игнорируется — см. заметку выше