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.
- package/README.md +27 -0
- package/package.json +33 -0
- package/src/assets/decoders/README.md +13 -0
- package/src/assets/decoders/basis_transcoder.js +19 -0
- package/src/assets/decoders/basis_transcoder.wasm +0 -0
- package/src/assets/decoders/draco_decoder.wasm +0 -0
- package/src/assets/decoders/draco_wasm_wrapper.js +117 -0
- package/src/assets/neutral_ibl128.ktx +0 -0
- package/src/core/animBridge.ts +51 -0
- package/src/core/boneNames.ts +22 -0
- package/src/core/frame.ts +230 -0
- package/src/core/glbStructure.ts +100 -0
- package/src/core/headlessBridge.ts +92 -0
- package/src/core/mapView.ts +195 -0
- package/src/core/math3d.ts +275 -0
- package/src/core/particleBridge.ts +154 -0
- package/src/core/physicsBridge.ts +305 -0
- package/src/core/recording.ts +59 -0
- package/src/core/sceneCore.ts +833 -0
- package/src/core/serialize.ts +144 -0
- package/src/gen/unsupported.gen.ts +212 -0
- package/src/index.ts +5 -0
- package/src/vendor/creator-anim/dist/creatorAnim.d.ts +123 -0
- package/src/vendor/creator-anim/dist/creatorAnim.js +14 -0
- package/src/vendor/creator-anim/js/index.ts +656 -0
- package/src/vendor/creator-nav/dist/creatorNav.d.ts +56 -0
- package/src/vendor/creator-nav/dist/creatorNav.js +14 -0
- package/src/vendor/creator-nav/js/index.ts +400 -0
- package/src/vendor/creator-particles/dist/creatorParticles.d.ts +43 -0
- package/src/vendor/creator-particles/dist/creatorParticles.js +14 -0
- package/src/vendor/creator-particles/js/index.ts +137 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.d.ts +107 -0
- package/src/vendor/creator-physics/dist/creatorPhysics.js +14 -0
- package/src/vendor/creator-physics/js/index.ts +515 -0
- package/src/vite-env.d.ts +1 -0
- package/src/webgl/batching.ts +453 -0
- package/src/webgl/createViewerLite.ts +1627 -0
- package/src/webgl/customMaterial.ts +228 -0
- package/src/webgl/decoders/basis.ts +123 -0
- package/src/webgl/decoders/draco.ts +110 -0
- package/src/webgl/decoders/index.ts +68 -0
- package/src/webgl/decoders/worker.ts +45 -0
- package/src/webgl/editor.ts +1297 -0
- package/src/webgl/glb.ts +593 -0
- package/src/webgl/ibl.ts +137 -0
- 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>
|