fx.md 7,5 КБ
Newer Older
1
2
3
4
5
6
7
# VFX своими руками — `$.fx`

Модуль `src/highlevel/fx.js`: ленты, молнии, ударные волны, вспышки и поля сил.
Никаких сторонних библиотек — всё рисуется тем же батчем, что и спрайты
(`$.gfx.push.triangle/line/ring/circle`), поэтому эффекты попадают в кадр сцены
и не добавляют draw call'ов.

8
Пост-обработка и пользовательские шейдеры — в [render.md](render.md).
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

---

## Координаты

`$.fx.*` и `$.gfx.draw.*` работают в **мировых** координатах: движок сам
переводит их в экранные через текущую камеру (и масштабирует толщины и
радиусы). Раньше перевода не было, и VFX уезжал на расстояние камеры — если
пишете свой модуль поверх `$.gfx.push.*`, помните, что `push` ждёт **экранные**
координаты: либо считайте их сами (`$.camera.worldToScreen`), либо рисуйте из
`$.render`-хука через `$.fx`/`$.gfx.draw`.

## 1. Обзор

| Метод | Что делает |
|---|---|
| `$.fx.trail(target, opts)` | лента за целью: селектор, узел, обёртка или функция точки |
| `$.fx.ribbon(points, opts)` | разовая лента по готовым точкам: трассер, след клинка |
| `$.fx.lightning(from, to, opts)` | молния с дрожанием и ветвями |
| `$.fx.shockwave(x, y, opts)` | расширяющееся кольцо |
| `$.fx.pulse(x, y, opts)` | вспышка-круг в точке (дуло, попадание) |
| `$.fx.attractor(x, y, opts)` | поле сил: притяжение и вихрь для частиц |
| `$.fx.impact(x, y, opts)` | готовый удар: волна + тряска + микро-стоп кадра |
| `$.fx.hitStop(ms, scale)` | замедление времени на удар |
| `$.fx.stats()` | сколько чего живо сейчас |
| `$.fx.clear()` | убрать всё (обычно не нужно: сцена чистит сама) |

Ленты и поля возвращают handle с `.stop()` (у `attractor` ещё `.move(x, y)` и
`.set(opts)`, у `trail` — `.options(opts)`).

```js
// Трассер: одна лента и вспышка у дула.
$.fx.ribbon([muzzle, hitPoint], { ms: 90, width: 5, color: '#ffd27f', blend: 'add' });
$.fx.pulse(muzzle.x, muzzle.y, { radius: 28, ms: 90, color: '#ffe0a0' });

// Попадание: волна, искры (частицами) и микро-стоп.
$.fx.impact(point.x, point.y, { radius: 60, shake: 4, hitStop: 60 });

// Молния между игроком и целью.
$.fx.lightning('#hero', '#enemy', { life: 120, jitter: 12, branches: 2, color: '#9fe8ff' });

// Чёрная дыра: поле живёт 2.8 с, потом схлопывается ударной волной.
const hole = $.fx.attractor(x, y, { radius: 280, strength: 1600, swirl: 1.2, life: 2800 });
$.time.after(2800, () => $.fx.shockwave(x, y, { radius: 420, ms: 520, width: 16, color: '#c9a6ff' }));
```

---

## 2. Параметры

**`trail`** — `{ ms, width, color, blend, minStep, alpha }`. `ms` — сколько живёт
точка ленты, `minStep` — минимальный шаг в пикселях (чтобы лента не копила
точки на месте), `width` — толщина у головы, к хвосту сужается сама.
Живая лента тянется за целью каждый кадр; когда цель исчезла (или `.stop()`),
лента доигрывает и убирается. Разовая (`ribbon`) — стареет и исчезает сама.

**`lightning`** — `{ life, segments, jitter, width, color, blend, branches, glow }`.
`jitter` — разброс середины в пикселях, `branches` — число ответвлений,
`glow` — рисовать ли широкую полупрозрачную подложку.

**`shockwave`** — `{ radius, ms, width, color, blend, ease }`. `ease` — `'out'`
(по умолчанию, быстро в начале) или `'linear'`.

**`attractor`** — `{ radius, strength, swirl, life, visual, color, edge }`.
`strength` — сила притяжения, `swirl` — доля тангенциальной составляющей
(закручивание), `visual: false` — невидимое поле.

---

## 3. Поля сил и частицы

`$.fx.attractor` действует на частицы `$.particles`, пока живёт: `particles.js`
читает общий список `ctx.fx_fields` и добавляет частице ускорение к центру
(с затуханием к краю радиуса) плюс вихрь. Тела Box2D поле не двигает — их
Nikitos's avatar
Nikitos включено в состав коммита
83
тянут обычными силами (`$.world` / `.applyForce`), как в демо «Типичная ночь в Мытищинском лесу».
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

Формально сила на частицу: `k = strength · (1 − d/R) · dt / max(16, d)`,
скорость получает `dx·k` и `−dy·swirl·k`.

---

## 4. Кадр и порядок

* `tickFx(dt)` вызывается в кадре **до** `tickParticles(dt)`: просроченное поле
  не должно успеть подействовать на частицы;
* рисование идёт из хука `ctx.gfx._fxFlush(cam)` внутри `render.js`, после
  узлов и до отправки батча: раньше нельзя (батч ещё не собирается), позже —
  он уже отправлен;
* эффекты отсекаются по камере (радиус видимой области + запас), поэтому
  далёкие волны не занимают буфер;
* случайность берётся из `fxRandom()` (`core.js`) — при `--seed` и
  фиксированном шаге картинка воспроизводима.

Смена сцены чистит эффекты: `resetFx()` вызывается из `scene.js` вместе с
очисткой мира.

---

## 5. Ограничения

109
110
* Пост-обработка и шейдеры **есть** (`$.gfx.post`, `$.gfx.defineShader`), но
  искажений кадра целиком (heat haze, линза чёрной дыры) в `$.fx` нет: их
111
  собирает игра из пост-обработки и пользовательского шейдера.
112
113
114
115
116
117
118
119
120
121
122
123
124
* Свет `<light>` — радиальный градиент из колец (мягкое пятно), а не честный
  источник с тенями; тени даёт `engine.light.visibility`.
* Поля сил действуют только на частицы, не на тела Box2D.
* Лента рисуется треугольниками без сглаживания стыков: на очень длинных
  лентах заметны грани (ограничитель — 128 точек).

---

## 6. Тесты

`tests/js/fx_test.mjs` (qjs, без движка) проверяет времена жизни, привязку
ленты к цели, поля сил, отсечение по камере и сброс. Демо-проверка —
`tests/agent/demos_test.py shooter_witch`.