lecodes-cli 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/index.js +677 -230
- package/package.json +2 -2
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/bundler.ts +239 -0
- package/runtime/sdk/compile/compileProject.ts +104 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +51 -0
- package/runtime/sdk/compile/index.ts +38 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/gl/Material.ts +52 -0
- package/runtime/sdk/gl/Particles.ts +477 -116
- package/runtime/sdk/inject.ts +1 -1
- package/runtime/sdk/ui/NativeView.ts +5 -5
- package/runtime/sdk/ui/UIButton.ts +5 -5
- package/runtime/sdk/ui/UIContainer.ts +13 -13
- package/runtime/sdk/ui/UIImage.ts +5 -5
- package/runtime/sdk/ui/UIInput.ts +9 -9
- package/runtime/sdk/ui/UINode.ts +114 -34
- package/runtime/sdk/ui/UIPager.ts +5 -9
- package/runtime/sdk/ui/UIScreen.ts +5 -5
- package/runtime/sdk/ui/UIScrollable.ts +5 -5
- package/runtime/sdk/ui/UISpacer.ts +5 -5
- package/runtime/sdk/ui/UIText.ts +5 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -5
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -5
- package/runtime/sdk/ui/UIWidget.ts +5 -5
- package/runtime/sdk-types.json +1 -1
|
@@ -1,129 +1,450 @@
|
|
|
1
|
-
// GPU particle system, as a Node.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// GPU particle system, as a Node. Draws with the default point-sprite material (Material.particles)
|
|
2
|
+
// unless a custom one is passed; properties drive the native emitter.
|
|
3
|
+
//
|
|
4
|
+
// All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
|
|
5
|
+
// records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
|
|
6
|
+
// call, live setters send a one-record buffer. The tag values mirror PsTag in
|
|
7
|
+
// creator-gl/src/particles.h (guarded by tests/particles-tags.test.ts).
|
|
8
|
+
//
|
|
9
|
+
// Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
|
|
10
|
+
// below. A builder's whole state is the flat `_data` array (the exact record payload), so the
|
|
11
|
+
// chisel compiler can constant-fold literal chains into the buffer at compile time.
|
|
4
12
|
|
|
13
|
+
import { Color, type ColorInput } from "../core/color"
|
|
5
14
|
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
6
|
-
import { Material } from "./Material"
|
|
15
|
+
import { Material, _sheetColsRows, type ParticlesMaterialOptions } from "./Material"
|
|
7
16
|
import { Node } from "./Node"
|
|
8
17
|
|
|
9
18
|
type Range<T> = T | { min: T, max: T }
|
|
10
19
|
|
|
20
|
+
// --- config record tags (keep in sync with PsTag in creator-gl/src/particles.h) -----------------
|
|
21
|
+
|
|
22
|
+
const TAG_SHAPE = 1
|
|
23
|
+
const TAG_LIFETIME = 2
|
|
24
|
+
const TAG_RATE = 3
|
|
25
|
+
const TAG_VELOCITY = 4
|
|
26
|
+
const TAG_VELOCITY_RND = 5
|
|
27
|
+
const TAG_ACCELERATION = 6
|
|
28
|
+
const TAG_DRAG = 7
|
|
29
|
+
const TAG_NOISE = 8
|
|
30
|
+
const TAG_SEED = 9
|
|
31
|
+
const TAG_PARAM_CURVE = 10
|
|
32
|
+
const TAG_COLOR_CURVE = 11
|
|
33
|
+
const TAG_SPACE = 12
|
|
34
|
+
const TAG_INHERIT_VELOCITY = 13
|
|
35
|
+
const TAG_RATE_DISTANCE = 14
|
|
36
|
+
const TAG_RENDER_MODE = 15
|
|
37
|
+
|
|
38
|
+
/** @internal — exported for the tag-sync test only. */
|
|
39
|
+
export const _particleTags = {
|
|
40
|
+
SHAPE: TAG_SHAPE, LIFETIME: TAG_LIFETIME, RATE: TAG_RATE, VELOCITY: TAG_VELOCITY,
|
|
41
|
+
VELOCITY_RND: TAG_VELOCITY_RND, ACCELERATION: TAG_ACCELERATION, DRAG: TAG_DRAG,
|
|
42
|
+
NOISE: TAG_NOISE, SEED: TAG_SEED, PARAM_CURVE: TAG_PARAM_CURVE, COLOR_CURVE: TAG_COLOR_CURVE,
|
|
43
|
+
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
44
|
+
RENDER_MODE: TAG_RENDER_MODE,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- curve builders -----------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
// A value curve reads as a journey over the particle's life:
|
|
50
|
+
// curve(0.3).from(0).via(0.15, 1).via(0.6, 1).to(0) // fade in, hold, fade out — or:
|
|
51
|
+
// curve(0.3).fade(0.15, 0.4)
|
|
52
|
+
// The curve multiplies the base value (or adds to it with mode 'add'); the base and any stop can be
|
|
53
|
+
// a { min, max } range, randomized per particle. With no `.from` the curve starts at the identity
|
|
54
|
+
// (1 for multiply, 0 for add); with no `.to` it holds its last value to end of life.
|
|
55
|
+
//
|
|
56
|
+
// chisel constant-folds a literal chain into its `_data` buffer at compile time and validates it
|
|
57
|
+
// (bad stop order, >8 stops, unparsable color → a build diagnostic): chisel-core/src/curve.rs mirrors
|
|
58
|
+
// these method bodies op-for-op, against a verbatim copy of them in chisel's fixtures/curve-sdk/. Keep
|
|
59
|
+
// the two in step — a change here means a change there (chisel's scripts/curve-exact.mjs compares the
|
|
60
|
+
// fused payload against the real builder, buffer for buffer).
|
|
61
|
+
|
|
62
|
+
const lohi = (v: Range<number>): [number, number] =>
|
|
63
|
+
typeof v === "number" ? [ v, v ] : [ v.min, v.max ]
|
|
64
|
+
|
|
65
|
+
export class CurveBuilder {
|
|
66
|
+
private _kind: 0 | 1
|
|
67
|
+
private _lo: number
|
|
68
|
+
private _hi: number
|
|
69
|
+
private _stops: number[] = [] // flat (t, lo, hi)
|
|
70
|
+
|
|
71
|
+
constructor(base: Range<number>, mode: "multiply" | "add") {
|
|
72
|
+
this._kind = mode === "add" ? 1 : 0
|
|
73
|
+
;[ this._lo, this._hi ] = lohi(base)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Curve value at birth (t = 0). Must come before any other stop. */
|
|
77
|
+
from(v: Range<number>): this {
|
|
78
|
+
if (this._stops.length > 0) throw new Error("curve: .from() must come first")
|
|
79
|
+
return this.via(0, v)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Curve value at `t` (0..1 of the particle's life). Stops must be added in ascending t order. */
|
|
83
|
+
via(t: number, v: Range<number>): this {
|
|
84
|
+
const [ lo, hi ] = lohi(v)
|
|
85
|
+
this._stops.push(t, lo, hi)
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Curve value at death (t = 1). */
|
|
90
|
+
to(v: Range<number>): this {
|
|
91
|
+
return this.via(1, v)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The classic opacity envelope: rise over the first `fadeIn`, fall over the last `fadeOut`. */
|
|
95
|
+
fade(fadeIn = 0.15, fadeOut = fadeIn): this {
|
|
96
|
+
const up = Math.min(Math.max(fadeIn, 0), 1)
|
|
97
|
+
const down = Math.min(Math.max(fadeOut, 0), 1 - up)
|
|
98
|
+
if (up > 0) this.via(0, 0).via(up, 1)
|
|
99
|
+
else this.via(0, 1)
|
|
100
|
+
if (down > 0) {
|
|
101
|
+
if (1 - down > up) this.via(1 - down, 1)
|
|
102
|
+
this.via(1, 0)
|
|
103
|
+
}
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The PARAM_CURVE record payload (sans slot): [kind, baseLo, baseHi, n, (t, lo, hi)×n]. */
|
|
108
|
+
get _data(): Float32Array {
|
|
109
|
+
const stops = this._stops
|
|
110
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
111
|
+
const n = stops.length / 3 + (prepend ? 1 : 0)
|
|
112
|
+
const out = new Float32Array(4 + n * 3)
|
|
113
|
+
out[0] = this._kind
|
|
114
|
+
out[1] = this._lo
|
|
115
|
+
out[2] = this._hi
|
|
116
|
+
out[3] = n
|
|
117
|
+
let o = 4
|
|
118
|
+
if (prepend) {
|
|
119
|
+
const identity = this._kind === 1 ? 0 : 1
|
|
120
|
+
out[o + 1] = identity
|
|
121
|
+
out[o + 2] = identity
|
|
122
|
+
o += 3
|
|
123
|
+
}
|
|
124
|
+
out.set(stops, o)
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class ColorCurveBuilder {
|
|
130
|
+
private _lo: [number, number, number, number]
|
|
131
|
+
private _hi: [number, number, number, number]
|
|
132
|
+
private _stops: number[] = [] // flat (t, lo rgba, hi rgba)
|
|
133
|
+
|
|
134
|
+
constructor(base: Range<ColorInput>) {
|
|
135
|
+
if (isColorRange(base)) {
|
|
136
|
+
this._lo = Color.toRgba01(base.min)
|
|
137
|
+
this._hi = Color.toRgba01(base.max)
|
|
138
|
+
} else {
|
|
139
|
+
this._lo = Color.toRgba01(base)
|
|
140
|
+
this._hi = this._lo
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Curve color at birth (t = 0). Must come first. The curve multiplies the base color. */
|
|
145
|
+
from(c: Range<ColorInput>): this {
|
|
146
|
+
if (this._stops.length > 0) throw new Error("colorCurve: .from() must come first")
|
|
147
|
+
return this.via(0, c)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Curve color at `t` (0..1 of the particle's life), in ascending t order. */
|
|
151
|
+
via(t: number, c: Range<ColorInput>): this {
|
|
152
|
+
let lo: number[], hi: number[]
|
|
153
|
+
if (isColorRange(c)) {
|
|
154
|
+
lo = Color.toRgba01(c.min)
|
|
155
|
+
hi = Color.toRgba01(c.max)
|
|
156
|
+
} else {
|
|
157
|
+
lo = hi = Color.toRgba01(c)
|
|
158
|
+
}
|
|
159
|
+
this._stops.push(t, ...lo, ...hi)
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Curve color at death (t = 1). */
|
|
164
|
+
to(c: Range<ColorInput>): this {
|
|
165
|
+
return this.via(1, c)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The COLOR_CURVE record payload: [baseLo rgba, baseHi rgba, n, (t, lo rgba, hi rgba)×n]. */
|
|
169
|
+
get _data(): Float32Array {
|
|
170
|
+
const stops = this._stops
|
|
171
|
+
const prepend = stops.length > 0 && stops[0] > 0
|
|
172
|
+
const n = stops.length / 9 + (prepend ? 1 : 0)
|
|
173
|
+
const out = new Float32Array(9 + n * 9)
|
|
174
|
+
out.set(this._lo, 0)
|
|
175
|
+
out.set(this._hi, 4)
|
|
176
|
+
out[8] = n
|
|
177
|
+
let o = 9
|
|
178
|
+
if (prepend) {
|
|
179
|
+
for (let i = 1; i < 9; i++) out[o + i] = 1 // identity multiplier: white, t = 0
|
|
180
|
+
o += 9
|
|
181
|
+
}
|
|
182
|
+
out.set(stops, o)
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A value curve over each particle's lifetime. Default mode `multiply` (the curve scales the
|
|
188
|
+
* base); `add` offsets it instead. */
|
|
189
|
+
export const curve = (base: Range<number> = 1, mode: "multiply" | "add" = "multiply"): CurveBuilder =>
|
|
190
|
+
new CurveBuilder(base, mode)
|
|
191
|
+
|
|
192
|
+
/** A color curve over each particle's lifetime — always multiplies the base color. */
|
|
193
|
+
export const colorCurve = (base: Range<ColorInput> = "#ffffff"): ColorCurveBuilder =>
|
|
194
|
+
new ColorCurveBuilder(base)
|
|
195
|
+
|
|
196
|
+
// --- option types -------------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
export type ParticleValue = Range<number> | CurveBuilder | { _data: Float32Array }
|
|
199
|
+
export type ParticleColor = ColorInput | { min: ColorInput, max: ColorInput } | ColorCurveBuilder | { _data: Float32Array }
|
|
200
|
+
|
|
11
201
|
type VelocityValue = {
|
|
12
202
|
speed?: Range<number>
|
|
203
|
+
/** Cone half-angle in radians — jitter the launch direction uniformly within it. */
|
|
204
|
+
spread?: number
|
|
205
|
+
/** Asymmetric jitter: two angles (u, v — the x/y components) in radians. */
|
|
13
206
|
randomizeAngle?: { min: Vec3Like, max: Vec3Like }
|
|
14
207
|
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
|
|
15
208
|
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
| {
|
|
19
|
-
|
|
20
|
-
export type DynamicColor =
|
|
21
|
-
| Range<string>
|
|
22
|
-
| { value?: Range<string>, mul: string, mul_b?: string, t: Float32Array }
|
|
23
|
-
|
|
24
|
-
type Shape = { type: "point", v: Vec3Like } | { type: "box", min: Vec3Like, max: Vec3Like }
|
|
209
|
+
type Shape =
|
|
210
|
+
| { type: "point", v: Vec3Like }
|
|
211
|
+
| { type: "box", min: Vec3Like, max: Vec3Like }
|
|
212
|
+
| { type: "circle", center: Vec3Like, radius: number }
|
|
25
213
|
type Noise = { strength?: number, frequency?: number, speed?: number }
|
|
26
214
|
|
|
27
|
-
export type ParticlesOptions = {
|
|
28
|
-
material
|
|
29
|
-
|
|
215
|
+
export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
216
|
+
/** Custom draw material. Unset = the default point-sprite material, configured by the
|
|
217
|
+
* `ParticlesMaterialOptions` sugar (map/sheet/emissive/blend/soft) — which is ignored
|
|
218
|
+
* when an explicit material is passed. */
|
|
219
|
+
material?: Material
|
|
220
|
+
/** Particle pool capacity (default 1000). */
|
|
221
|
+
maxParticles?: number
|
|
222
|
+
/** Particles emitted per second. */
|
|
223
|
+
rate?: number
|
|
224
|
+
/** Simulation space. `'local'` (default) — particles ride the node's transform; `'world'` —
|
|
225
|
+
* each particle stays where it was born as the emitter moves on (trails: drift smoke, mud,
|
|
226
|
+
* wake). Switching at runtime resets live particles. */
|
|
227
|
+
space?: "local" | "world"
|
|
228
|
+
/** Fraction (usually 0..1) of the emitter's own velocity added to each particle at spawn —
|
|
229
|
+
* smoke "thrown" off a moving car. Takes effect with `space: 'world'`. */
|
|
230
|
+
inheritVelocity?: number
|
|
231
|
+
/** Particles emitted per world unit the emitter MOVES, on top of `rate`, with spawn points
|
|
232
|
+
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
233
|
+
* Meant for `space: 'world'`. */
|
|
234
|
+
rateOverDistance?: number
|
|
30
235
|
shape?: Shape
|
|
31
236
|
startVelocity?: Vec3Like | VelocityValue
|
|
237
|
+
/** Downward acceleration, world units/s² — positive pulls particles DOWN (sugar for
|
|
238
|
+
* `acceleration: [0, -g, 0]`). */
|
|
32
239
|
gravity?: number
|
|
240
|
+
/** Constant acceleration vector (gravity, wind, …). Wins over `gravity` when both are set. */
|
|
241
|
+
acceleration?: Vec3Like
|
|
33
242
|
drag?: number
|
|
34
243
|
lifetime?: Range<number>
|
|
35
|
-
color?:
|
|
36
|
-
|
|
37
|
-
|
|
244
|
+
color?: ParticleColor
|
|
245
|
+
/** World-unit diameter (alias of custom[0]). Default 1. */
|
|
246
|
+
size?: ParticleValue
|
|
247
|
+
/** Radians (alias of custom[1]). */
|
|
248
|
+
rotation?: ParticleValue
|
|
249
|
+
/** Per-particle opacity 0..1 (alias of custom[2]). Default 1. */
|
|
250
|
+
opacity?: ParticleValue
|
|
251
|
+
/** Flipbook frame index (alias of custom[3]). With `sheet` set and no explicit frame, the
|
|
252
|
+
* default is one full sheet cycle over each particle's lifetime, starting at `startFrame`. */
|
|
253
|
+
frame?: ParticleValue
|
|
254
|
+
/** Where the default flipbook cycle begins: `'random'` (default — desynchronized loops like
|
|
255
|
+
* fire/smoke) or a frame index (`0` plays the sheet in order — explosions, one-shot bursts).
|
|
256
|
+
* Only shapes the default; an explicit `frame` wins. */
|
|
257
|
+
startFrame?: number | "random"
|
|
38
258
|
noise?: Noise | null
|
|
259
|
+
/** Fixed random seed for deterministic tests. */
|
|
260
|
+
seed?: number
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- record assembly ----------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
const isColorRange = (v: unknown): v is { min: ColorInput, max: ColorInput } =>
|
|
266
|
+
typeof v === "object" && v !== null && !Array.isArray(v) && "min" in v
|
|
267
|
+
|
|
268
|
+
const hasData = (v: unknown): v is { _data: Float32Array } =>
|
|
269
|
+
typeof v === "object" && v !== null && "_data" in v
|
|
270
|
+
|
|
271
|
+
const pushParam = (out: number[], slot: number, value: ParticleValue): void => {
|
|
272
|
+
if (typeof value === "number") {
|
|
273
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value, value, 0)
|
|
274
|
+
} else if (hasData(value)) {
|
|
275
|
+
const d = value._data
|
|
276
|
+
out.push(TAG_PARAM_CURVE, d.length + 1, slot)
|
|
277
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
278
|
+
} else {
|
|
279
|
+
out.push(TAG_PARAM_CURVE, 5, slot, 0, value.min, value.max, 0)
|
|
280
|
+
}
|
|
39
281
|
}
|
|
40
282
|
|
|
283
|
+
const pushColor = (out: number[], value: ParticleColor): void => {
|
|
284
|
+
let d: Float32Array
|
|
285
|
+
if (hasData(value)) {
|
|
286
|
+
d = value._data
|
|
287
|
+
} else if (isColorRange(value)) {
|
|
288
|
+
d = new ColorCurveBuilder(value)._data
|
|
289
|
+
} else {
|
|
290
|
+
d = new ColorCurveBuilder(value as ColorInput)._data
|
|
291
|
+
}
|
|
292
|
+
out.push(TAG_COLOR_CURVE, d.length)
|
|
293
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const pushShape = (out: number[], shape: Shape): void => {
|
|
297
|
+
if (shape.type === "point") out.push(TAG_SHAPE, 4, 0, cx(shape.v), cy(shape.v), cz(shape.v))
|
|
298
|
+
else if (shape.type === "box") out.push(TAG_SHAPE, 7, 1, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
|
|
299
|
+
else out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const pushVelocity = (out: number[], val: Vec3Like | VelocityValue): void => {
|
|
303
|
+
if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
|
|
304
|
+
// a plain vector: launch along it, its length is the speed (native derives it)
|
|
305
|
+
out.push(TAG_VELOCITY, 6, 1, cx(val), cy(val), cz(val), 0, 0)
|
|
306
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
const v = val as VelocityValue
|
|
310
|
+
let mode = 0
|
|
311
|
+
let p: Vec3Like = [ 0, 0, 0 ]
|
|
312
|
+
if ("dir" in v) { mode = 1; p = v.dir }
|
|
313
|
+
else if ("from" in v) { mode = 2; p = v.from }
|
|
314
|
+
else if ("to" in v) { mode = 3; p = v.to }
|
|
315
|
+
let lo = 0, hi = 0
|
|
316
|
+
if (v.speed !== undefined) {
|
|
317
|
+
if (typeof v.speed === "number") lo = hi = v.speed
|
|
318
|
+
else { lo = v.speed.min; hi = v.speed.max }
|
|
319
|
+
}
|
|
320
|
+
out.push(TAG_VELOCITY, 6, mode, cx(p), cy(p), cz(p), lo, hi)
|
|
321
|
+
if (v.spread !== undefined) {
|
|
322
|
+
out.push(TAG_VELOCITY_RND, 5, 1, -v.spread, -v.spread, v.spread, v.spread)
|
|
323
|
+
} else if (v.randomizeAngle) {
|
|
324
|
+
const a = v.randomizeAngle
|
|
325
|
+
out.push(TAG_VELOCITY_RND, 5, 1, cx(a.min), cy(a.min), cx(a.max), cy(a.max))
|
|
326
|
+
} else {
|
|
327
|
+
out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- the node -----------------------------------------------------------------------------------
|
|
332
|
+
|
|
41
333
|
export class Particles extends Node {
|
|
42
334
|
private _material: Material
|
|
43
335
|
|
|
44
|
-
/** Custom curve
|
|
45
|
-
readonly custom:
|
|
336
|
+
/** Custom curve-param slots (0..3); size/rotation/opacity/frame alias slots 0-3. */
|
|
337
|
+
readonly custom: ParticleValue[]
|
|
46
338
|
|
|
47
|
-
constructor(options: ParticlesOptions) {
|
|
339
|
+
constructor(options: ParticlesOptions = {}) {
|
|
48
340
|
super()
|
|
49
|
-
this._material = options.material
|
|
50
|
-
_creator.createParticleSystem(this.id, Material.idOf(options.
|
|
341
|
+
this._material = options.material ?? Material.particles(options)
|
|
342
|
+
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
|
|
51
343
|
|
|
52
344
|
const id = this.id
|
|
53
|
-
this.custom = new Proxy([] as
|
|
54
|
-
set(target, prop: string, value:
|
|
345
|
+
this.custom = new Proxy([] as ParticleValue[], {
|
|
346
|
+
set(target, prop: string, value: ParticleValue) {
|
|
55
347
|
const i = parseInt(prop)
|
|
56
348
|
if (Number.isNaN(i)) { (target as any)[prop] = value; return true }
|
|
57
349
|
target[i] = value
|
|
58
|
-
|
|
350
|
+
const out: number[] = []
|
|
351
|
+
pushParam(out, i, value)
|
|
352
|
+
_creator.setParticleSystemConfig(id, Float32Array.from(out))
|
|
59
353
|
return true
|
|
60
354
|
},
|
|
61
355
|
})
|
|
62
356
|
|
|
63
|
-
|
|
64
|
-
if (options.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (options.
|
|
68
|
-
if (options.
|
|
69
|
-
if (options.
|
|
70
|
-
if (options.
|
|
71
|
-
if (options.
|
|
72
|
-
if (options.
|
|
357
|
+
const out: number[] = []
|
|
358
|
+
if (options.render !== undefined && options.render !== "point") {
|
|
359
|
+
out.push(TAG_RENDER_MODE, 1, options.render === "ribbon" ? 2 : 1)
|
|
360
|
+
}
|
|
361
|
+
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
362
|
+
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
363
|
+
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
364
|
+
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
365
|
+
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
366
|
+
if (options.startVelocity !== undefined) pushVelocity(out, options.startVelocity)
|
|
367
|
+
if (options.acceleration !== undefined) {
|
|
368
|
+
const a = options.acceleration
|
|
369
|
+
out.push(TAG_ACCELERATION, 3, cx(a), cy(a), cz(a))
|
|
370
|
+
} else if (options.gravity !== undefined) {
|
|
371
|
+
out.push(TAG_ACCELERATION, 3, 0, -options.gravity, 0)
|
|
372
|
+
}
|
|
373
|
+
if (options.drag !== undefined) out.push(TAG_DRAG, 1, options.drag)
|
|
374
|
+
if (options.lifetime !== undefined) {
|
|
375
|
+
const [ lo, hi ] = lohi(options.lifetime)
|
|
376
|
+
out.push(TAG_LIFETIME, 2, lo, hi)
|
|
377
|
+
}
|
|
378
|
+
if (options.color !== undefined) pushColor(out, options.color)
|
|
379
|
+
if (options.size !== undefined) pushParam(out, 0, options.size)
|
|
380
|
+
if (options.rotation !== undefined) pushParam(out, 1, options.rotation)
|
|
381
|
+
if (options.opacity !== undefined) pushParam(out, 2, options.opacity)
|
|
382
|
+
if (options.frame !== undefined) {
|
|
383
|
+
pushParam(out, 3, options.frame)
|
|
384
|
+
} else if (options.material === undefined) {
|
|
385
|
+
const [ cols, rows ] = _sheetColsRows(options.sheet)
|
|
386
|
+
const frames = cols * rows
|
|
387
|
+
if (frames > 1) {
|
|
388
|
+
const start = options.startFrame ?? "random"
|
|
389
|
+
const base = start === "random" ? { min: 0, max: frames - 1 } : start
|
|
390
|
+
pushParam(out, 3, curve(base, "add").from(0).to(frames))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (options.noise !== undefined) {
|
|
394
|
+
const n = options.noise
|
|
395
|
+
out.push(TAG_NOISE, 3, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0)
|
|
396
|
+
}
|
|
397
|
+
if (options.seed !== undefined) out.push(TAG_SEED, 1, options.seed)
|
|
398
|
+
|
|
399
|
+
if (out.length > 0) _creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
73
400
|
}
|
|
74
401
|
|
|
75
402
|
/** Emit `count` particles immediately (a burst). */
|
|
76
403
|
spawn(count: number): this { _creator.spawnParticles(this.id, count); return this }
|
|
77
404
|
|
|
78
|
-
|
|
79
|
-
|
|
405
|
+
private _send(out: number[]): void {
|
|
406
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
80
407
|
}
|
|
81
408
|
|
|
409
|
+
set rate(val: number) { this._send([ TAG_RATE, 1, val ]) }
|
|
410
|
+
|
|
411
|
+
/** `'world'` leaves particles behind where they were born (trails). Switching resets live ones. */
|
|
412
|
+
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
413
|
+
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
414
|
+
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
415
|
+
|
|
82
416
|
set shape(shape: Shape) {
|
|
83
|
-
|
|
84
|
-
|
|
417
|
+
const out: number[] = []
|
|
418
|
+
pushShape(out, shape)
|
|
419
|
+
this._send(out)
|
|
85
420
|
}
|
|
86
421
|
|
|
87
422
|
set startVelocity(val: Vec3Like | VelocityValue) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
const v = val as VelocityValue
|
|
93
|
-
if ("from" in v) _creator.setParticleSystemVelocityValue(this.id, 2, cx(v.from), cy(v.from), cz(v.from))
|
|
94
|
-
else if ("to" in v) _creator.setParticleSystemVelocityValue(this.id, 3, cx(v.to), cy(v.to), cz(v.to))
|
|
95
|
-
else if ("dir" in v) _creator.setParticleSystemVelocityValue(this.id, 1, cx(v.dir), cy(v.dir), cz(v.dir))
|
|
96
|
-
else _creator.setParticleSystemVelocityValue(this.id, 0, 0, 0, 0)
|
|
97
|
-
if ("speed" in v && v.speed !== undefined) {
|
|
98
|
-
if (typeof v.speed === "number") _creator.setParticleSystemVelocitySpeedValue(this.id, v.speed)
|
|
99
|
-
else _creator.setParticleSystemVelocitySpeedRange(this.id, v.speed.min, v.speed.max)
|
|
100
|
-
}
|
|
101
|
-
if (v.randomizeAngle) {
|
|
102
|
-
const a = v.randomizeAngle
|
|
103
|
-
_creator.setParticleSystemVelocityRndAngle(this.id, cx(a.min), cy(a.min), cz(a.min), cx(a.max), cy(a.max), cz(a.max))
|
|
104
|
-
}
|
|
423
|
+
const out: number[] = []
|
|
424
|
+
pushVelocity(out, val)
|
|
425
|
+
this._send(out)
|
|
105
426
|
}
|
|
106
427
|
|
|
107
|
-
|
|
108
|
-
set
|
|
428
|
+
/** Positive pulls particles down — sugar for `acceleration = [0, -g, 0]`. */
|
|
429
|
+
set gravity(val: number) { this._send([ TAG_ACCELERATION, 3, 0, -val, 0 ]) }
|
|
430
|
+
set acceleration(a: Vec3Like) { this._send([ TAG_ACCELERATION, 3, cx(a), cy(a), cz(a) ]) }
|
|
431
|
+
set drag(val: number) { this._send([ TAG_DRAG, 1, val ]) }
|
|
432
|
+
set seed(val: number) { this._send([ TAG_SEED, 1, val ]) }
|
|
109
433
|
|
|
110
434
|
set lifetime(val: Range<number>) {
|
|
111
|
-
|
|
112
|
-
|
|
435
|
+
const [ lo, hi ] = lohi(val)
|
|
436
|
+
this._send([ TAG_LIFETIME, 2, lo, hi ])
|
|
113
437
|
}
|
|
114
438
|
|
|
115
|
-
set size(val:
|
|
116
|
-
set rotation(val:
|
|
439
|
+
set size(val: ParticleValue) { this.custom[0] = val }
|
|
440
|
+
set rotation(val: ParticleValue) { this.custom[1] = val }
|
|
441
|
+
set opacity(val: ParticleValue) { this.custom[2] = val }
|
|
442
|
+
set frame(val: ParticleValue) { this.custom[3] = val }
|
|
117
443
|
|
|
118
|
-
set color(value:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (typeof value.value === "string") _creator.setParticleSystemColorValue(this.id, value.value)
|
|
123
|
-
else if (value.value) _creator.setParticleSystemColorRange(this.id, value.value.min, value.value.max)
|
|
124
|
-
if (value.mul) _creator.setParticleSystemColorCurveA(this.id, value.mul, value.t)
|
|
125
|
-
if (value.mul_b) _creator.setParticleSystemColorCurveB(this.id, value.mul_b, value.t)
|
|
126
|
-
}
|
|
444
|
+
set color(value: ParticleColor) {
|
|
445
|
+
const out: number[] = []
|
|
446
|
+
pushColor(out, value)
|
|
447
|
+
this._send(out)
|
|
127
448
|
}
|
|
128
449
|
|
|
129
450
|
get material(): Material { return this._material }
|
|
@@ -133,53 +454,93 @@ export class Particles extends Node {
|
|
|
133
454
|
}
|
|
134
455
|
|
|
135
456
|
set noise(noise: Noise | null) {
|
|
136
|
-
|
|
137
|
-
|
|
457
|
+
this._send(noise
|
|
458
|
+
? [ TAG_NOISE, 3, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1 ]
|
|
459
|
+
: [ TAG_NOISE, 3, 0, 0, 0 ])
|
|
138
460
|
}
|
|
139
461
|
}
|
|
140
462
|
|
|
141
|
-
|
|
142
|
-
if (typeof value === "number") {
|
|
143
|
-
_creator.setParticleSystemParamValue(id, i, value)
|
|
144
|
-
} else if ("min" in value) {
|
|
145
|
-
_creator.setParticleSystemParamRange(id, i, value.min, value.max)
|
|
146
|
-
} else {
|
|
147
|
-
if (typeof value.value === "number") _creator.setParticleSystemParamValue(id, i, value.value)
|
|
148
|
-
else if (value.value) _creator.setParticleSystemParamRange(id, i, value.value.min, value.value.max)
|
|
149
|
-
if (value.inc) _creator.setParticleSystemParamCurveA(id, i, value.inc, 1)
|
|
150
|
-
if (value.inc_b) _creator.setParticleSystemParamCurveB(id, i, value.inc_b)
|
|
151
|
-
if (value.mul) _creator.setParticleSystemParamCurveA(id, i, value.mul, 0)
|
|
152
|
-
if (value.mul_b) _creator.setParticleSystemParamCurveB(id, i, value.mul_b)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
463
|
+
// --- ribbon trail -------------------------------------------------------------------------------
|
|
155
464
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
465
|
+
/** `Trail` options. The look options (map/sheet/emissive/blend/soft) come from
|
|
466
|
+
* `ParticlesMaterialOptions`, same as `Particles`. */
|
|
467
|
+
export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch"> & {
|
|
468
|
+
/** Seconds each trail point lives — the trail's length in time. Default 0.5. */
|
|
469
|
+
time?: number
|
|
470
|
+
/** Strip width in world units; a curve tapers it over each point's life (head → tail).
|
|
471
|
+
* Default 0.1. */
|
|
472
|
+
width?: ParticleValue
|
|
473
|
+
/** Minimum emitter movement (world units) between recorded points. Default 0.05. */
|
|
474
|
+
minDistance?: number
|
|
475
|
+
/** Point capacity (default 128). */
|
|
476
|
+
maxPoints?: number
|
|
477
|
+
color?: ParticleColor
|
|
478
|
+
/** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
|
|
479
|
+
opacity?: ParticleValue
|
|
171
480
|
}
|
|
172
481
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
482
|
+
/** A ribbon strip that follows this node through the world — sword swings, skid marks, missile
|
|
483
|
+
* trails. Attach it to (or under) the moving node and just move; points are laid down per
|
|
484
|
+
* `minDistance` of movement, live for `time` seconds, and the strip stays glued to the node at
|
|
485
|
+
* its head. Width/opacity/color accept the same curves as `Particles`, evaluated over each
|
|
486
|
+
* point's life — i.e. along the trail from head (fresh) to tail (dying). */
|
|
487
|
+
export class Trail extends Node {
|
|
488
|
+
private _material: Material
|
|
489
|
+
private _rateDistance: number
|
|
490
|
+
|
|
491
|
+
constructor(options: TrailOptions = {}) {
|
|
492
|
+
super()
|
|
493
|
+
this._material = Material.particles({ ...options, render: "ribbon" })
|
|
494
|
+
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxPoints ?? 128)
|
|
495
|
+
|
|
496
|
+
const time = options.time ?? 0.5
|
|
497
|
+
this._rateDistance = 1 / Math.max(options.minDistance ?? 0.05, 1e-3)
|
|
498
|
+
const out: number[] = [
|
|
499
|
+
TAG_RENDER_MODE, 1, 2,
|
|
500
|
+
TAG_SPACE, 1, 1, // a trail is only meaningful in world space
|
|
501
|
+
TAG_RATE, 1, 0, // points come from movement, never from time
|
|
502
|
+
TAG_RATE_DISTANCE, 1, this._rateDistance,
|
|
503
|
+
TAG_LIFETIME, 2, time, time,
|
|
504
|
+
]
|
|
505
|
+
pushParam(out, 0, options.width ?? 0.1)
|
|
506
|
+
pushParam(out, 2, options.opacity ?? curve().to(0))
|
|
507
|
+
if (options.color !== undefined) pushColor(out, options.color)
|
|
508
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private _send(out: number[]): void {
|
|
512
|
+
_creator.setParticleSystemConfig(this.id, Float32Array.from(out))
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Trail length in seconds — how long each laid-down point lives. */
|
|
516
|
+
set time(val: number) { this._send([ TAG_LIFETIME, 2, val, val ]) }
|
|
517
|
+
|
|
518
|
+
set width(val: ParticleValue) {
|
|
519
|
+
const out: number[] = []
|
|
520
|
+
pushParam(out, 0, val)
|
|
521
|
+
this._send(out)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
set opacity(val: ParticleValue) {
|
|
525
|
+
const out: number[] = []
|
|
526
|
+
pushParam(out, 2, val)
|
|
527
|
+
this._send(out)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
set color(value: ParticleColor) {
|
|
531
|
+
const out: number[] = []
|
|
532
|
+
pushColor(out, value)
|
|
533
|
+
this._send(out)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
set minDistance(val: number) {
|
|
537
|
+
this._rateDistance = 1 / Math.max(val, 1e-3)
|
|
538
|
+
this._send([ TAG_RATE_DISTANCE, 1, this._rateDistance ])
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
|
|
542
|
+
* after e.g. a sword swing ends. */
|
|
543
|
+
set emitting(val: boolean) { this._send([ TAG_RATE_DISTANCE, 1, val ? this._rateDistance : 0 ]) }
|
|
544
|
+
|
|
545
|
+
get material(): Material { return this._material }
|
|
185
546
|
}
|