lecodes-gl-lite 2.0.0

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.
Files changed (46) hide show
  1. package/README.md +27 -0
  2. package/package.json +33 -0
  3. package/src/assets/decoders/README.md +13 -0
  4. package/src/assets/decoders/basis_transcoder.js +19 -0
  5. package/src/assets/decoders/basis_transcoder.wasm +0 -0
  6. package/src/assets/decoders/draco_decoder.wasm +0 -0
  7. package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
  8. package/src/assets/neutral_ibl128.ktx +0 -0
  9. package/src/core/animBridge.ts +51 -0
  10. package/src/core/boneNames.ts +22 -0
  11. package/src/core/frame.ts +230 -0
  12. package/src/core/glbStructure.ts +100 -0
  13. package/src/core/headlessBridge.ts +92 -0
  14. package/src/core/mapView.ts +195 -0
  15. package/src/core/math3d.ts +275 -0
  16. package/src/core/particleBridge.ts +154 -0
  17. package/src/core/physicsBridge.ts +305 -0
  18. package/src/core/recording.ts +59 -0
  19. package/src/core/sceneCore.ts +833 -0
  20. package/src/core/serialize.ts +144 -0
  21. package/src/gen/unsupported.gen.ts +212 -0
  22. package/src/index.ts +5 -0
  23. package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
  24. package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
  25. package/src/vendor/creator-anim/js/index.ts +656 -0
  26. package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
  27. package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
  28. package/src/vendor/creator-nav/js/index.ts +400 -0
  29. package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
  30. package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
  31. package/src/vendor/creator-particles/js/index.ts +137 -0
  32. package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
  33. package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
  34. package/src/vendor/creator-physics/js/index.ts +515 -0
  35. package/src/vite-env.d.ts +1 -0
  36. package/src/webgl/batching.ts +453 -0
  37. package/src/webgl/createViewerLite.ts +1627 -0
  38. package/src/webgl/customMaterial.ts +228 -0
  39. package/src/webgl/decoders/basis.ts +123 -0
  40. package/src/webgl/decoders/draco.ts +110 -0
  41. package/src/webgl/decoders/index.ts +68 -0
  42. package/src/webgl/decoders/worker.ts +45 -0
  43. package/src/webgl/editor.ts +1297 -0
  44. package/src/webgl/glb.ts +593 -0
  45. package/src/webgl/ibl.ts +137 -0
  46. package/src/webgl/shaders.ts +779 -0
@@ -0,0 +1,275 @@
1
+ // Column-major mat4 math for the lite 3D engine. Mirrors creator-gl's transform semantics exactly
2
+ // (transformUtils.cpp): matrices are column-major (m[col*4 + row], filament/GL layout), and the
3
+ // component setters (position/quaternion/euler/scale) rewrite only their part of the local TRS
4
+ // while preserving the rest — that contract is what the SDK's Node accessors are written against.
5
+ // Generalized over ArrayLike (Mat4Like) so it runs directly on OGL Mat4 instances (Array subclasses)
6
+ // without copies. This is the single shared copy — lecodes-headless's g3/ imports it too (the former
7
+ // g3/math3d.ts was merged in here; getEulerYXZDeg/composeTRS came from that copy).
8
+
9
+ export type Mat4Like = Float32Array | number[]
10
+
11
+ /** Concrete column-major mat4 — what the allocating helpers (identity/invert/compose/…) return. */
12
+ export type Mat4 = Float32Array
13
+
14
+ export const identity = (): Float32Array => {
15
+ const m = new Float32Array(16)
16
+ m[0] = m[5] = m[10] = m[15] = 1
17
+ return m
18
+ }
19
+
20
+ /** out = a · b (column-major). `out` may not alias a or b. Returns `out` (narrowed to its type). */
21
+ export const multiply = <T extends Mat4Like>(out: T, a: Mat4Like, b: Mat4Like): T => {
22
+ for (let c = 0; c < 4; c++) {
23
+ const b0 = b[c * 4], b1 = b[c * 4 + 1], b2 = b[c * 4 + 2], b3 = b[c * 4 + 3]
24
+ out[c * 4] = a[0] * b0 + a[4] * b1 + a[8] * b2 + a[12] * b3
25
+ out[c * 4 + 1] = a[1] * b0 + a[5] * b1 + a[9] * b2 + a[13] * b3
26
+ out[c * 4 + 2] = a[2] * b0 + a[6] * b1 + a[10] * b2 + a[14] * b3
27
+ out[c * 4 + 3] = a[3] * b0 + a[7] * b1 + a[11] * b2 + a[15] * b3
28
+ }
29
+ return out
30
+ }
31
+
32
+ /** Invert an affine TRS matrix (general 3x3 inverse, so non-uniform scale is handled). */
33
+ export const invertTRS = (m: Mat4Like): Float32Array => {
34
+ const a00 = m[0], a01 = m[1], a02 = m[2]
35
+ const a10 = m[4], a11 = m[5], a12 = m[6]
36
+ const a20 = m[8], a21 = m[9], a22 = m[10]
37
+ const det = a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11)
38
+ const id = det === 0 ? 0 : 1 / det
39
+ const out = new Float32Array(16)
40
+ out[0] = (a11 * a22 - a12 * a21) * id
41
+ out[1] = (a02 * a21 - a01 * a22) * id
42
+ out[2] = (a01 * a12 - a02 * a11) * id
43
+ out[4] = (a12 * a20 - a10 * a22) * id
44
+ out[5] = (a00 * a22 - a02 * a20) * id
45
+ out[6] = (a02 * a10 - a00 * a12) * id
46
+ out[8] = (a10 * a21 - a11 * a20) * id
47
+ out[9] = (a01 * a20 - a00 * a21) * id
48
+ out[10] = (a00 * a11 - a01 * a10) * id
49
+ const tx = m[12], ty = m[13], tz = m[14]
50
+ out[12] = -(out[0] * tx + out[4] * ty + out[8] * tz)
51
+ out[13] = -(out[1] * tx + out[5] * ty + out[9] * tz)
52
+ out[14] = -(out[2] * tx + out[6] * ty + out[10] * tz)
53
+ out[15] = 1
54
+ return out
55
+ }
56
+
57
+ /** Column norms — the scale a TRS matrix carries (same as creator-gl's getScale). */
58
+ export const getScale = (m: Mat4Like): [number, number, number] => [
59
+ Math.hypot(m[0], m[1], m[2]),
60
+ Math.hypot(m[4], m[5], m[6]),
61
+ Math.hypot(m[8], m[9], m[10]),
62
+ ]
63
+
64
+ /** Rewrite the rotation part from a quaternion, preserving the current scale (setQuaternion). */
65
+ export const setQuaternionOn = (m: Mat4Like, x: number, y: number, z: number, w: number): void => {
66
+ const [sx, sy, sz] = getScale(m)
67
+ const len = Math.hypot(x, y, z, w) || 1
68
+ const nx = x / len, ny = y / len, nz = z / len, nw = w / len
69
+ const xx = nx * nx, xy = nx * ny, xz = nx * nz, xw = nx * nw
70
+ const yy = ny * ny, yz = ny * nz, yw = ny * nw
71
+ const zz = nz * nz, zw = nz * nw
72
+ m[0] = (1 - 2 * (yy + zz)) * sx
73
+ m[1] = 2 * (xy + zw) * sx
74
+ m[2] = 2 * (xz - yw) * sx
75
+ m[4] = 2 * (xy - zw) * sy
76
+ m[5] = (1 - 2 * (xx + zz)) * sy
77
+ m[6] = 2 * (yz + xw) * sy
78
+ m[8] = 2 * (xz + yw) * sz
79
+ m[9] = 2 * (yz - xw) * sz
80
+ m[10] = (1 - 2 * (xx + yy)) * sz
81
+ }
82
+
83
+ const DEG = Math.PI / 180
84
+
85
+ /** Rewrite the rotation from euler angles (degrees) in the given order, preserving scale
86
+ * (setEulerAngles; orders match transformUtils.cpp — 1 = YXZ is the SDK convention). */
87
+ export const setEulerOn = (m: Mat4Like, xDeg: number, yDeg: number, zDeg: number, order: number): void => {
88
+ const [scx, scy, scz] = getScale(m)
89
+ const x = xDeg * DEG, y = yDeg * DEG, z = zDeg * DEG
90
+ const cx = Math.cos(x), sx = Math.sin(x)
91
+ const cy = Math.cos(y), sy = Math.sin(y)
92
+ const cz = Math.cos(z), sz = Math.sin(z)
93
+ let m11 = 1, m12 = 0, m13 = 0, m21 = 0, m22 = 1, m23 = 0, m31 = 0, m32 = 0, m33 = 1
94
+ switch (order) {
95
+ case 0: // XYZ
96
+ m11 = cy * cz; m12 = -cy * sz; m13 = sy
97
+ m21 = sx * sy * cz + cx * sz; m22 = -sx * sy * sz + cx * cz; m23 = -sx * cy
98
+ m31 = -cx * sy * cz + sx * sz; m32 = cx * sy * sz + sx * cz; m33 = cx * cy
99
+ break
100
+ case 1: // YXZ
101
+ m11 = cy * cz + sy * sx * sz; m12 = -cy * sz + sy * sx * cz; m13 = sy * cx
102
+ m21 = cx * sz; m22 = cx * cz; m23 = -sx
103
+ m31 = -sy * cz + cy * sx * sz; m32 = sy * sz + cy * sx * cz; m33 = cy * cx
104
+ break
105
+ case 2: // ZXY
106
+ m11 = cy * cz - sy * sx * sz; m12 = -cx * sz; m13 = sy * cz + cy * sx * sz
107
+ m21 = cy * sz + sy * sx * cz; m22 = cx * cz; m23 = sy * sz - cy * sx * cz
108
+ m31 = -sy * cx; m32 = sx; m33 = cy * cx
109
+ break
110
+ case 3: // ZYX
111
+ m11 = cy * cz; m12 = sx * sy * cz - cx * sz; m13 = cx * sy * cz + sx * sz
112
+ m21 = cy * sz; m22 = sx * sy * sz + cx * cz; m23 = cx * sy * sz - sx * cz
113
+ m31 = -sy; m32 = sx * cy; m33 = cx * cy
114
+ break
115
+ case 4: // YZX
116
+ m11 = cy * cz; m12 = -sz; m13 = sy * cz
117
+ m21 = sx * sy + cx * cy * sz; m22 = cx * cz; m23 = -cy * sx + cx * sy * sz
118
+ m31 = -cx * sy + sx * cy * sz; m32 = sx * cz; m33 = cx * cy + sx * sy * sz
119
+ break
120
+ case 5: // XZY
121
+ m11 = cy * cz; m12 = -cy * sz; m13 = sy
122
+ m21 = cx * sz + sx * sy * cz; m22 = cx * cz - sx * sy * sz; m23 = -sx * cy
123
+ m31 = sx * sz - cx * sy * cz; m32 = sx * cz + cx * sy * sz; m33 = cx * cy
124
+ break
125
+ }
126
+ m[0] = m11 * scx; m[1] = m21 * scx; m[2] = m31 * scx
127
+ m[4] = m12 * scy; m[5] = m22 * scy; m[6] = m32 * scy
128
+ m[8] = m13 * scz; m[9] = m23 * scz; m[10] = m33 * scz
129
+ }
130
+
131
+ /** Rescale the basis columns to the given absolute scale (setScale). */
132
+ export const setScaleOn = (m: Mat4Like, x: number, y: number, z: number): void => {
133
+ const [sx, sy, sz] = getScale(m)
134
+ if (sx === 0 || sy === 0 || sz === 0) {
135
+ m[0] = x; m[1] = 0; m[2] = 0
136
+ m[4] = 0; m[5] = y; m[6] = 0
137
+ m[8] = 0; m[9] = 0; m[10] = z
138
+ return
139
+ }
140
+ const fx = x / sx, fy = y / sy, fz = z / sz
141
+ m[0] *= fx; m[1] *= fx; m[2] *= fx
142
+ m[4] *= fy; m[5] *= fy; m[6] *= fy
143
+ m[8] *= fz; m[9] *= fz; m[10] *= fz
144
+ }
145
+
146
+ /** Extract YXZ euler angles (degrees) from the rotation part — the SDK's euler convention. */
147
+ export const getEulerYXZDeg = (m: Mat4Like): [number, number, number] => {
148
+ const [sx, sy, sz] = getScale(m)
149
+ if (sx === 0 || sy === 0 || sz === 0) return [0, 0, 0]
150
+ // normalized rotation elements, math notation m_{row,col}; memory is m[col*4 + row]
151
+ const r11 = m[0] / sx, r21 = m[1] / sx, r31 = m[2] / sx
152
+ const r12 = m[4] / sy, r22 = m[5] / sy
153
+ const r13 = m[8] / sz, r23 = m[9] / sz, r33 = m[10] / sz
154
+ const x = Math.asin(-Math.min(1, Math.max(-1, r23)))
155
+ let y: number, z: number
156
+ if (Math.abs(r23) < 0.9999) {
157
+ y = Math.atan2(r13, r33)
158
+ z = Math.atan2(r21, r22)
159
+ } else { // gimbal lock: cx ≈ 0
160
+ y = Math.atan2(-r31, r11)
161
+ z = 0
162
+ }
163
+ return [x / DEG, y / DEG, z / DEG]
164
+ }
165
+
166
+ /** Compose a local matrix from glTF TRS (translation, rotation quat, scale): M = T·R·S. */
167
+ export const composeTRS = (
168
+ t: [number, number, number], r: [number, number, number, number], s: [number, number, number],
169
+ ): Float32Array => {
170
+ const m = identity()
171
+ setQuaternionOn(m, r[0], r[1], r[2], r[3])
172
+ setScaleOn(m, s[0], s[1], s[2])
173
+ m[12] = t[0]; m[13] = t[1]; m[14] = t[2]
174
+ return m
175
+ }
176
+
177
+ /** Standard perspective projection (vertical fov in degrees). */
178
+ export const perspective = (fovYDeg: number, aspect: number, near: number, far: number): Float32Array => {
179
+ const f = 1 / Math.tan((fovYDeg * DEG) / 2)
180
+ const m = new Float32Array(16)
181
+ m[0] = f / aspect
182
+ m[5] = f
183
+ m[10] = (far + near) / (near - far)
184
+ m[11] = -1
185
+ m[14] = (2 * far * near) / (near - far)
186
+ return m
187
+ }
188
+
189
+ /** Orthographic projection (for the shadow camera). */
190
+ export const orthographic = (
191
+ left: number, right: number, bottom: number, top: number, near: number, far: number,
192
+ ): Float32Array => {
193
+ const m = new Float32Array(16)
194
+ m[0] = 2 / (right - left)
195
+ m[5] = 2 / (top - bottom)
196
+ m[10] = -2 / (far - near)
197
+ m[12] = -(right + left) / (right - left)
198
+ m[13] = -(top + bottom) / (top - bottom)
199
+ m[14] = -(far + near) / (far - near)
200
+ m[15] = 1
201
+ return m
202
+ }
203
+
204
+ // ---- ray intersection (port of collider.cpp) --------------------------------------------------
205
+
206
+ /** Ray vs local-space AABB (center + half extents); returns tmin or -1 (collider.cpp intersectRay). */
207
+ export const intersectRayBox = (
208
+ center: ArrayLike<number>, extent: ArrayLike<number>,
209
+ origin: ArrayLike<number>, direction: ArrayLike<number>,
210
+ ): number => {
211
+ let tmin = -Infinity, tmax = Infinity
212
+ for (let i = 0; i < 3; i++) {
213
+ if (direction[i] === 0) continue
214
+ const tx1 = (center[i] - extent[i] - origin[i]) / direction[i]
215
+ const tx2 = (center[i] + extent[i] - origin[i]) / direction[i]
216
+ tmin = Math.max(tmin, Math.min(tx1, tx2))
217
+ tmax = Math.min(tmax, Math.max(tx1, tx2))
218
+ }
219
+ if (tmin < 0 || tmax < tmin) return -1
220
+ return tmin
221
+ }
222
+
223
+ /** Ray vs local-space sphere; nearest positive t or -1 (collider.cpp intersectRay). */
224
+ export const intersectRaySphere = (
225
+ center: ArrayLike<number>, radius: number,
226
+ origin: ArrayLike<number>, direction: ArrayLike<number>,
227
+ ): number => {
228
+ const ocx = origin[0] - center[0], ocy = origin[1] - center[1], ocz = origin[2] - center[2]
229
+ const a = direction[0] ** 2 + direction[1] ** 2 + direction[2] ** 2
230
+ const b = 2 * (ocx * direction[0] + ocy * direction[1] + ocz * direction[2])
231
+ const c = ocx * ocx + ocy * ocy + ocz * ocz - radius * radius
232
+ const disc = b * b - 4 * a * c
233
+ if (disc < 0) return -1
234
+ const sq = Math.sqrt(disc)
235
+ const t1 = (-b - sq) / (2 * a)
236
+ const t2 = (-b + sq) / (2 * a)
237
+ if (t1 > 0 && t2 > 0) return Math.min(t1, t2)
238
+ if (t1 > 0) return t1
239
+ if (t2 > 0) return t2
240
+ return -1
241
+ }
242
+
243
+ /** Transform a point/direction by an affine matrix (w=1 point, w=0 direction). */
244
+ export const transformPoint = (out: number[], m: Mat4Like, x: number, y: number, z: number, w: number): number[] => {
245
+ out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w
246
+ out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w
247
+ out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w
248
+ return out
249
+ }
250
+
251
+ /** A VIEW matrix (world → camera) for a camera at `eye` looking at `target`, Y up (falls back to
252
+ * -Z up when looking straight up/down — a top view reads with +Z pointing down the screen). Column-
253
+ * major, OpenGL convention — the same frame Filament's Camera::lookAt produces natively. */
254
+ export const lookAtView = (eye: ArrayLike<number>, target: ArrayLike<number>): Float32Array => {
255
+ let fx = target[0] - eye[0], fy = target[1] - eye[1], fz = target[2] - eye[2]
256
+ let fl = Math.hypot(fx, fy, fz)
257
+ if (fl < 1e-9) { fx = 0; fy = 0; fz = -1; fl = 1 }
258
+ fx /= fl; fy /= fl; fz /= fl
259
+ let ux = 0, uy = 1, uz = 0
260
+ if (Math.abs(fy) > 0.999) { ux = 0; uy = 0; uz = -1 }
261
+ // s = f × up, u = s × f
262
+ let sx = fy * uz - fz * uy, sy = fz * ux - fx * uz, sz = fx * uy - fy * ux
263
+ const sl = Math.hypot(sx, sy, sz) || 1
264
+ sx /= sl; sy /= sl; sz /= sl
265
+ const uxx = sy * fz - sz * fy, uyy = sz * fx - sx * fz, uzz = sx * fy - sy * fx
266
+ const out = new Float32Array(16)
267
+ out[0] = sx; out[4] = sy; out[8] = sz
268
+ out[1] = uxx; out[5] = uyy; out[9] = uzz
269
+ out[2] = -fx; out[6] = -fy; out[10] = -fz
270
+ out[12] = -(sx * eye[0] + sy * eye[1] + sz * eye[2])
271
+ out[13] = -(uxx * eye[0] + uyy * eye[1] + uzz * eye[2])
272
+ out[14] = fx * eye[0] + fy * eye[1] + fz * eye[2]
273
+ out[15] = 1
274
+ return out
275
+ }
@@ -0,0 +1,154 @@
1
+ // The web hosts' particle runtime: creator-particles' wasm behind the same `_creator` particle /
2
+ // noise method shapes creator-gl exposes natively — ONE simulation for every host. Shared by
3
+ // viewer-lite's WebGL engine (which uploads the streams) and lecodes-headless (headless, which reads
4
+ // the counts); the host only supplies the emitters' world matrices and the camera position.
5
+ //
6
+ // Lazy like the animation and physics systems: the module loads asynchronously
7
+ // (`loadCreatorParticles()`, awaited by createViewerLite / runProject), the real bridge is created on
8
+ // the first particle call, and a host that never loaded it gets the standalone noise as zeros and
9
+ // systems that record but do not simulate.
10
+ //
11
+ // On top of the simulation this layer RECORDS what the SDK configured onto the entity (Ent.particles:
12
+ // the material, the config records decoded back to readable keys, bursts) plus the live `alive` count
13
+ // after each update — the headless JSON snapshot shows the emitter as set up and as running.
14
+
15
+ import { createParticleBridge, loadCreatorParticles, isCreatorParticlesLoaded, type ParticleHost, type ParticleBridge, type ParticleFrame } from "../vendor/creator-particles/js/index"
16
+ import type { Ent } from "./sceneCore"
17
+
18
+ export { loadCreatorParticles, isCreatorParticlesLoaded }
19
+ export type { ParticleHost, ParticleBridge, ParticleFrame }
20
+
21
+ export type ParticleRuntimeHost = ParticleHost & {
22
+ ent(id: number): Ent
23
+ /** A system exists now (the renderer prepares its drawable for the material). */
24
+ created?(entityId: number, materialId: number): void
25
+ /** This frame's stream for a system (the renderer uploads it). */
26
+ frame?(entityId: number, frame: ParticleFrame): void
27
+ destroyed?(entityId: number): void
28
+ }
29
+
30
+ const noiseFallback = {
31
+ createNoise: (): number => 0,
32
+ setNoiseFrequency(): void {},
33
+ setNoiseOctaves(): void {},
34
+ setNoiseFractalLunacrity(): void {},
35
+ setNoiseFractalGain(): void {},
36
+ getNoise2D: (): number => 0,
37
+ getNoise3D: (): number => 0,
38
+ }
39
+
40
+ export const createParticleRuntime = (host: ParticleRuntimeHost) => {
41
+ let real: ParticleBridge | null = null
42
+ const get = (): ParticleBridge | null => {
43
+ if (!real && isCreatorParticlesLoaded()) real = createParticleBridge(host)
44
+ return real
45
+ }
46
+ let nextNoise = 1 // ids for the no-module fallback
47
+
48
+ const setParticle = (entityId: number, key: string, value: unknown): void => {
49
+ const e = host.ent(entityId)
50
+ if (!e.particles) e.particles = {}
51
+ if (value === undefined) delete e.particles[key]
52
+ else e.particles[key] = value
53
+ }
54
+
55
+ const methods = {
56
+ createParticleSystem(entityId: number, materialInstanceId: number, maxParticles: number): void {
57
+ host.ent(entityId).particles = { material: materialInstanceId, ...(maxParticles ? { maxParticles } : {}) }
58
+ const b = get()
59
+ if (!b) return
60
+ b.methods.createParticleSystem(entityId, materialInstanceId, maxParticles)
61
+ host.created?.(entityId, materialInstanceId)
62
+ },
63
+ spawnParticles(entityId: number, count: number): void {
64
+ const p = host.ent(entityId).particles
65
+ if (p) p.spawned = ((p.spawned as number) ?? 0) + count
66
+ get()?.methods.spawnParticles(entityId, count)
67
+ },
68
+ // Tags mirror CPART_TAG_* in creator-particles.h (via the SDK's gl/Particles.ts writer).
69
+ setParticleSystemConfig(entityId: number, data: Float32Array): void {
70
+ const range = (lo: number, hi: number) => (lo === hi ? lo : { min: lo, max: hi })
71
+ const rgba = (o: number) => [ data[o], data[o + 1], data[o + 2], data[o + 3] ]
72
+ for (let i = 0; i + 2 <= data.length; i += 2 + data[i + 1]) {
73
+ const p = i + 2
74
+ switch (data[i]) {
75
+ case 1: { // shape
76
+ const kind = data[p]
77
+ if (kind === 0) setParticle(entityId, "shape", { type: "point", at: [ data[p + 1], data[p + 2], data[p + 3] ] })
78
+ else if (kind === 1) setParticle(entityId, "shape", { type: "box", min: [ data[p + 1], data[p + 2], data[p + 3] ], max: [ data[p + 4], data[p + 5], data[p + 6] ] })
79
+ else setParticle(entityId, "shape", { type: "circle", center: [ data[p + 1], data[p + 2], data[p + 3] ], radius: data[p + 4] })
80
+ break
81
+ }
82
+ case 2: setParticle(entityId, "lifetime", range(data[p], data[p + 1])); break
83
+ case 3: setParticle(entityId, "rate", data[p]); break
84
+ case 4:
85
+ setParticle(entityId, "velocity", { kind: data[p], value: [ data[p + 1], data[p + 2], data[p + 3] ] })
86
+ setParticle(entityId, "speed", range(data[p + 4], data[p + 5]))
87
+ break
88
+ case 5:
89
+ setParticle(entityId, "spreadAngle", data[p] ? { min: [ data[p + 1], data[p + 2] ], max: [ data[p + 3], data[p + 4] ] } : undefined)
90
+ break
91
+ case 6: setParticle(entityId, "acceleration", [ data[p], data[p + 1], data[p + 2] ]); break
92
+ case 7: setParticle(entityId, "drag", data[p]); break
93
+ case 8:
94
+ setParticle(entityId, "noise", data[p] !== 0 ? { strength: data[p], frequency: data[p + 1], speed: data[p + 2] } : undefined)
95
+ break
96
+ case 9: setParticle(entityId, "seed", data[p]); break
97
+ case 10: { // param curve: slot, kind, baseLo, baseHi, n, stops…
98
+ const base = range(data[p + 2], data[p + 3])
99
+ setParticle(entityId, `param${data[p]}`, data[p + 4] > 0 ? { base, curve: data[p + 4] } : base)
100
+ break
101
+ }
102
+ case 11: { // color curve: baseLo rgba, baseHi rgba, n, stops…
103
+ const lo = rgba(p), hi = rgba(p + 4)
104
+ const base = lo.every((v, k) => v === hi[k]) ? lo : { min: lo, max: hi }
105
+ setParticle(entityId, "color", data[p + 8] > 0 ? { base, curve: data[p + 8] } : base)
106
+ break
107
+ }
108
+ case 12: setParticle(entityId, "space", data[p] ? "world" : "local"); break
109
+ case 15: setParticle(entityId, "render", [ "point", "quad", "ribbon", "mesh" ][data[p]] ?? data[p]); break
110
+ }
111
+ }
112
+ get()?.methods.setParticleSystemConfig(entityId, data)
113
+ },
114
+ ...noiseFallback,
115
+ }
116
+ // The standalone noise goes to the module when it is there (FastNoiseLite), else stays zeros.
117
+ const noiseMethods = Object.fromEntries(Object.keys(noiseFallback).map((name) => [
118
+ name,
119
+ (...args: unknown[]) => {
120
+ const b = get()
121
+ if (!b) return name === "createNoise" ? nextNoise++ : (noiseFallback as Record<string, (...a: unknown[]) => unknown>)[name](...args)
122
+ return (b.methods as unknown as Record<string, (...a: unknown[]) => unknown>)[name](...args)
123
+ },
124
+ ]))
125
+ Object.assign(methods, noiseMethods)
126
+
127
+ return {
128
+ methods,
129
+ loaded: (): boolean => real !== null,
130
+ /** After the animators (creator-gl's updateParticles): advance every system, record the live
131
+ * count, hand the streams to the renderer. */
132
+ update(dt: number): void {
133
+ if (!real) return
134
+ real.update(dt, (id, frame) => {
135
+ const p = host.ent(id).particles
136
+ if (p) p.alive = frame.alive
137
+ host.frame?.(id, frame)
138
+ })
139
+ },
140
+ /** The node (or an ancestor) was shown / hidden: a hidden system is not simulated. */
141
+ setVisible(entityId: number, visible: boolean): void { real?.setVisible(entityId, visible) },
142
+ /** The index pattern for a system's current capacity (the renderer's index buffer). */
143
+ indexPattern: (entityId: number): Uint32Array => real?.indexPattern(entityId) ?? new Uint32Array(0),
144
+ /** destroyEntity hook. */
145
+ onEntityDestroyed(entityId: number): void {
146
+ if (!real) return
147
+ if (real.materialOf(entityId) === undefined) return
148
+ real.destroy(entityId)
149
+ host.destroyed?.(entityId)
150
+ },
151
+ }
152
+ }
153
+
154
+ export type ParticleRuntime = ReturnType<typeof createParticleRuntime>