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,515 @@
1
+ // creator-physics' TypeScript side over the wasm build (dist/creatorPhysics.js): the physics bridge
2
+ // the WEB hosts (viewer-lite's WebGL engine, lecodes-headless headless) hand to the app — the same
3
+ // `_creator` physics / character / vehicle / ragdoll method shapes creator-gl exposes natively, so
4
+ // the SDK never knows which host it runs on, and the SAME simulation (one C++ implementation, no TS
5
+ // twin, no kinematic stand-in).
6
+ //
7
+ // The host supplies its scene graph (`PhysicsHost`): entity lookup with a local matrix per node, the
8
+ // world matrix, two pose writers (a full world TRS for bodies, a world position for characters), the
9
+ // hierarchy (parent lookup — the ground probe's "on the character or under it" rule), and the GLB
10
+ // bytes behind a model root (mesh colliders). Each frame the host calls `frame(dt, fixedUpdate)`
11
+ // (the protocol at the top of include/creator-physics/creator-physics.h) and, after its animators,
12
+ // `syncRagdolls()`.
13
+ //
14
+ // `loadCreatorPhysics()` must have resolved before `createPhysicsBridge` — the web hosts await it in
15
+ // their async setup so the SDK's first physics call finds the module ready.
16
+
17
+ import createCreatorPhysics, { type CreatorPhysicsModule } from "../dist/creatorPhysics.js"
18
+
19
+ export type Mat4Like = Float32Array | number[]
20
+
21
+ export const POSE_STRIDE = 10
22
+ const PULL_RENDER = 0
23
+ const PULL_LIVE = 1
24
+
25
+ export type PhysicsHost = {
26
+ /** Entity lookup (undefined when unknown): the local matrix + parent (0 = none). */
27
+ get(id: number): { id: number, m: Mat4Like, parent: number } | undefined
28
+ /** The entity's world matrix (column-major). */
29
+ world(id: number): Mat4Like
30
+ /** Write a world-space TRS onto the entity (into its parent's space when parented). */
31
+ setWorldTRS(id: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number, sx: number, sy: number, sz: number): void
32
+ /** Write a world position only — the node keeps its rotation and scale (characters). */
33
+ setWorldPosition(id: number, x: number, y: number, z: number): void
34
+ /** The local matrix was rewritten in place (WebGL: worldMatrixNeedsUpdate). */
35
+ localMatrixChanged(id: number): void
36
+ parentOf(id: number): number
37
+ /** The GLB bytes behind a model root (mesh colliders from a model, glbTriangles). */
38
+ glbBytes?(entityId: number): ArrayBuffer | Uint8Array | undefined
39
+ /** A ragdoll let go with a blend: seed the animator that drives `jointEntityId` from the joints'
40
+ * current locals so it transitions out of the fallen pose over `blend` seconds (creator-anim's
41
+ * animatorSeedPose). */
42
+ seedAnimator?(jointEntityId: number, blend: number): void
43
+ /** Learned ragdoll limits: how far the clips bound to the animator driving `jointEntityId` rotate each
44
+ * (child entity, parent entity) pair, measured against `frames` (8 floats per pair — creator-anim's
45
+ * animatorJointRanges). 6 radians per pair, or null when nothing is bound yet. */
46
+ jointRanges?(jointEntityId: number, pairs: ArrayLike<number>, frames: Float32Array): Float32Array | null
47
+ }
48
+
49
+ let modulePromise: Promise<CreatorPhysicsModule> | null = null
50
+ let loaded: CreatorPhysicsModule | null = null
51
+ /** The wasm module, loaded once per process. */
52
+ export const loadCreatorPhysics = (): Promise<CreatorPhysicsModule> => (modulePromise ??= createCreatorPhysics().then((m) => (loaded = m)))
53
+ export const isCreatorPhysicsLoaded = (): boolean => loaded !== null
54
+
55
+ // ---- heap helpers --------------------------------------------------------------------------------
56
+
57
+ const withF32 = <T>(mod: CreatorPhysicsModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
58
+ const ptr = mod._malloc(Math.max(4, values.length * 4))
59
+ mod.HEAPF32.set(values, ptr >> 2)
60
+ try { return fn(ptr) } finally { mod._free(ptr) }
61
+ }
62
+ const withU32 = <T>(mod: CreatorPhysicsModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
63
+ const ptr = mod._malloc(Math.max(4, values.length * 4))
64
+ mod.HEAPU32.set(values, ptr >> 2)
65
+ try { return fn(ptr) } finally { mod._free(ptr) }
66
+ }
67
+ const withBytes = <T>(mod: CreatorPhysicsModule, bytes: Uint8Array, fn: (ptr: number) => T): T => {
68
+ const ptr = mod._malloc(Math.max(1, bytes.length))
69
+ mod.HEAPU8.set(bytes, ptr)
70
+ try { return fn(ptr) } finally { mod._free(ptr) }
71
+ }
72
+ const asBytes = (buf: ArrayBuffer | Uint8Array): Uint8Array => (buf instanceof Uint8Array ? buf : new Uint8Array(buf))
73
+
74
+ // ---- matrix helpers (column-major 4×4) -----------------------------------------------------------
75
+
76
+ /** A column-major TRS → a POSE (t xyz, r xyzw, s xyz) written at `out[o..o+10)`. */
77
+ export const decomposeInto = (m: Mat4Like, out: Float32Array, o: number): void => {
78
+ const sx = Math.hypot(m[0], m[1], m[2]) || 1, sy = Math.hypot(m[4], m[5], m[6]) || 1, sz = Math.hypot(m[8], m[9], m[10]) || 1
79
+ // rotation rows/cols of the normalized upper 3×3 (r[row][col])
80
+ const r00 = m[0] / sx, r10 = m[1] / sx, r20 = m[2] / sx
81
+ const r01 = m[4] / sy, r11 = m[5] / sy, r21 = m[6] / sy
82
+ const r02 = m[8] / sz, r12 = m[9] / sz, r22 = m[10] / sz
83
+ const tr = r00 + r11 + r22
84
+ let x: number, y: number, z: number, w: number
85
+ if (tr > 0) {
86
+ const s = Math.sqrt(tr + 1) * 2
87
+ w = 0.25 * s; x = (r21 - r12) / s; y = (r02 - r20) / s; z = (r10 - r01) / s
88
+ } else if (r00 > r11 && r00 > r22) {
89
+ const s = Math.sqrt(1 + r00 - r11 - r22) * 2
90
+ w = (r21 - r12) / s; x = 0.25 * s; y = (r01 + r10) / s; z = (r02 + r20) / s
91
+ } else if (r11 > r22) {
92
+ const s = Math.sqrt(1 + r11 - r00 - r22) * 2
93
+ w = (r02 - r20) / s; x = (r01 + r10) / s; y = 0.25 * s; z = (r12 + r21) / s
94
+ } else {
95
+ const s = Math.sqrt(1 + r22 - r00 - r11) * 2
96
+ w = (r10 - r01) / s; x = (r02 + r20) / s; y = (r12 + r21) / s; z = 0.25 * s
97
+ }
98
+ out[o] = m[12]; out[o + 1] = m[13]; out[o + 2] = m[14]
99
+ out[o + 3] = x; out[o + 4] = y; out[o + 5] = z; out[o + 6] = w
100
+ out[o + 7] = sx; out[o + 8] = sy; out[o + 9] = sz
101
+ }
102
+
103
+ /** Rotate a local matrix about its own Y axis in place (a character's root-motion yaw). */
104
+ const rotateLocalY = (m: Mat4Like, yaw: number): void => {
105
+ const cs = Math.cos(yaw), sn = Math.sin(yaw)
106
+ const x0 = m[0], x1 = m[1], x2 = m[2], z0 = m[8], z1 = m[9], z2 = m[10]
107
+ const a = m as number[]
108
+ a[0] = x0 * cs - z0 * sn; a[1] = x1 * cs - z1 * sn; a[2] = x2 * cs - z2 * sn
109
+ a[8] = x0 * sn + z0 * cs; a[9] = x1 * sn + z1 * cs; a[10] = x2 * sn + z2 * cs
110
+ }
111
+
112
+ // ---- shape pictures -------------------------------------------------------------------------------
113
+
114
+ /** The triangles (9 floats each) of the convex hull Jolt would build from `positions` (xyz triples)
115
+ * scaled by (sx, sy, sz) — the editor's picture of a `Shape { mesh: 'convex' }`, straight from the
116
+ * simulation's own hull builder (256-point cap, its tolerances), so the overlay shows what collides.
117
+ * Needs no world. Null when the module is not loaded or the hull is degenerate. */
118
+ export const convexHullTriangles = (positions: ArrayLike<number>, sx = 1, sy = 1, sz = 1): Float32Array | null => {
119
+ const mod = loaded
120
+ if (!mod || positions.length < 12) return null
121
+ const shape = withF32(mod, positions, (vp) => mod._cphysShapeMesh(vp, Math.floor(positions.length / 3), 0, 0, 1, sx, sy, sz))
122
+ if (!shape) return null
123
+ const countPtr = mod._malloc(4)
124
+ const ptr = mod._cphysShapeTriangles(shape, countPtr)
125
+ const count = mod.HEAPU32[countPtr >> 2]
126
+ const out = ptr && count ? Float32Array.from(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + count)) : null
127
+ if (ptr) mod._free(ptr)
128
+ mod._free(countPtr)
129
+ mod._cphysShapeDestroy(shape)
130
+ return out
131
+ }
132
+
133
+ // ---- the bridge -----------------------------------------------------------------------------------
134
+
135
+ export function createPhysicsBridge(host: PhysicsHost) {
136
+ const mod = loaded
137
+ if (!mod) throw new Error("creator-physics: await loadCreatorPhysics() before createPhysicsBridge()")
138
+
139
+ // scratch heap cells, allocated once
140
+ const pose = mod._malloc(POSE_STRIDE * 4)
141
+ const out8 = mod._malloc(32)
142
+ const u32a = mod._malloc(4)
143
+ let stateScratch = 0, stateScratchCount = 0
144
+ const stateBuffer = (count: number): number => {
145
+ if (count > stateScratchCount) {
146
+ if (stateScratch) mod._free(stateScratch)
147
+ stateScratch = mod._malloc(count * 4)
148
+ stateScratchCount = count
149
+ }
150
+ return stateScratch
151
+ }
152
+
153
+ const characterEntity = new Map<number, number>() // charId → entity (the live shape swap writes the node)
154
+ // Per ragdoll: its part bones, the two most recent frames' post-animator world poses (activation
155
+ // hands physics each bone's measured motion; a driven body hands them on as the motors' target).
156
+ // Followers (the joints between two parts holding their pose while limp — creator-gl's
157
+ // writeRagdollFollowers) are not mirrored here: the headless twin records poses, not pixels.
158
+ type RagRec = { entities: number[], prev: Float32Array, cur: Float32Array, valid: number, dt: number, active: boolean, driven: boolean, learn: boolean, margin: number, learned: boolean }
159
+ const ragdolls = new Map<number, RagRec>()
160
+
161
+ // Learned limits (blob `limits` = 1): the joints' constraint frames from the simulation, the clips'
162
+ // ranges from the animator (host.jointRanges), the ranges + margin back to the simulation, which
163
+ // rebuilds the joints — creator-gl's learnRagdollLimits. Once, at the first activation with clips.
164
+ const learnLimits = (id: number, r: RagRec): void => {
165
+ if (!host.jointRanges) return
166
+ const n = r.entities.length
167
+ const frames = new Float32Array(n * 9)
168
+ const got = withF32(mod, frames, (fp) => { const c = mod._cphysRagdollJointFrames(id, fp); frames.set(mod.HEAPF32.subarray(fp >> 2, (fp >> 2) + n * 9)); return c })
169
+ if (got !== n) return
170
+ const pairs: number[] = [], pairFrames: number[] = [], pairPart: number[] = []
171
+ for (let i = 0; i < n; i++) {
172
+ const parent = frames[i * 9]
173
+ if (parent < 0) continue
174
+ pairs.push(r.entities[i], r.entities[parent])
175
+ for (let k = 1; k < 9; k++) pairFrames.push(frames[i * 9 + k])
176
+ pairPart.push(i)
177
+ }
178
+ if (pairs.length === 0) return
179
+ const ranges = host.jointRanges(r.entities[0], pairs, Float32Array.from(pairFrames))
180
+ if (!ranges) return
181
+ const limits = new Float32Array(n * 6)
182
+ const toDeg = 180 / Math.PI
183
+ for (let k = 0; k < pairPart.length; k++) {
184
+ for (let m = 0; m < 3; m++) {
185
+ limits[pairPart[k] * 6 + m * 2] = ranges[k * 6 + m * 2] * toDeg - r.margin
186
+ limits[pairPart[k] * 6 + m * 2 + 1] = ranges[k * 6 + m * 2 + 1] * toDeg + r.margin
187
+ }
188
+ }
189
+ if (withF32(mod, limits, (lp) => mod._cphysRagdollSetLimits(id, lp))) r.learned = true
190
+ }
191
+ const meshCache = new WeakMap<object, number[]>() // GLB buffer → unscaled base shape per convex flag
192
+ let onEvent: ((a: number, b: number, type: number) => void) | null = null
193
+
194
+ const parentFn = mod.addFunction((e: number) => host.parentOf(e) | 0, "ii")
195
+ mod._cphysSetParentLookup(parentFn)
196
+ mod._cphysInit() // the world exists from the start (creator-gl does the same at engine init)
197
+
198
+ // The entity's world transform → the scratch pose cell.
199
+ const poseOf = (entityId: number): number => {
200
+ decomposeInto(host.world(entityId), mod.HEAPF32, pose >> 2)
201
+ return pose
202
+ }
203
+ const read3 = (out: Float32Array, ptr: number): void => {
204
+ const f = mod.HEAPF32, o = ptr >> 2
205
+ out[0] = f[o]; out[1] = f[o + 1]; out[2] = f[o + 2]
206
+ }
207
+
208
+ // Pulled poses → the host's entities.
209
+ const applyBodies = (n: number): void => {
210
+ if (n === 0) return
211
+ const ids = mod.HEAPU32, idsO = mod._cphysPulledIds() >> 2
212
+ const f = mod.HEAPF32, o = mod._cphysPulledPoses() >> 2
213
+ for (let i = 0; i < n; i++) {
214
+ const p = o + i * POSE_STRIDE
215
+ host.setWorldTRS(ids[idsO + i], f[p], f[p + 1], f[p + 2], f[p + 3], f[p + 4], f[p + 5], f[p + 6], f[p + 7], f[p + 8], f[p + 9])
216
+ }
217
+ }
218
+ const applyCharacters = (n: number): void => {
219
+ if (n === 0) return
220
+ const ids = mod.HEAPU32, idsO = mod._cphysPulledIds() >> 2
221
+ const f = mod.HEAPF32, o = mod._cphysPulledPoses() >> 2
222
+ for (let i = 0; i < n; i++) {
223
+ const p = o + i * POSE_STRIDE
224
+ host.setWorldPosition(ids[idsO + i], f[p], f[p + 1], f[p + 2])
225
+ }
226
+ }
227
+
228
+ const glbBase = (entityId: number, convex: boolean): number => {
229
+ const buf = host.glbBytes?.(entityId)
230
+ if (!buf) return 0
231
+ let cached = meshCache.get(buf)
232
+ if (!cached) { cached = [0, 0]; meshCache.set(buf, cached) }
233
+ const slot = convex ? 1 : 0
234
+ if (!cached[slot]) {
235
+ const bytes = asBytes(buf)
236
+ cached[slot] = withBytes(mod, bytes, (p) => mod._cphysShapeMeshFromGlb(p, bytes.length, convex ? 1 : 0))
237
+ }
238
+ return cached[slot]
239
+ }
240
+
241
+ const bodyWithShape = (entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number =>
242
+ shapeId ? mod._cphysBodyCreate(entityId, shapeId, motion | 0, mass, sensor ? 1 : 0, pickOnly ? 1 : 0, 1, poseOf(entityId)) : 0
243
+ // Legacy coupled shape+body: the shape lives only as long as the body holds it.
244
+ const legacyBody = (entityId: number, shapeId: number, motion: number, mass: number): number => {
245
+ const body = bodyWithShape(entityId, shapeId, motion, mass, false, false)
246
+ if (shapeId) mod._cphysShapeDestroy(shapeId)
247
+ return body
248
+ }
249
+
250
+ const methods = {
251
+ physicsHasSupport: (): boolean => true,
252
+ physicsConfigure(gx: number, gy: number, gz: number, maxBodies: number): void { mod._cphysConfigure(gx, gy, gz, (maxBodies | 0) > 0 ? maxBodies | 0 : 0) },
253
+ setInterpolation(enabled: boolean): void { mod._cphysSetInterpolation(enabled ? 1 : 0) },
254
+
255
+ // --- shapes ---
256
+ physicsBuildBox: (hx: number, hy: number, hz: number): number => mod._cphysShapeBox(hx, hy, hz),
257
+ physicsBuildSphere: (radius: number): number => mod._cphysShapeSphere(radius),
258
+ physicsBuildCylinder: (halfHeight: number, radius: number): number => mod._cphysShapeCylinder(halfHeight, radius),
259
+ physicsBuildCapsule: (halfHeight: number, radius: number): number => mod._cphysShapeCapsule(halfHeight, radius),
260
+ physicsBuildMesh(vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: number, sy: number, sz: number): number {
261
+ return withF32(mod, vertices, (vp) => withU32(mod, indices, (ip) =>
262
+ mod._cphysShapeMesh(vp, Math.floor(vertices.length / 3), ip, indices.length, convex ? 1 : 0, sx, sy, sz)))
263
+ },
264
+ physicsBuildMeshFromEntity(entityId: number, convex: boolean, sx: number, sy: number, sz: number): number {
265
+ const base = glbBase(entityId, convex)
266
+ return base ? mod._cphysShapeScaled(base, sx, sy, sz) : 0
267
+ },
268
+ physicsBuildHeightField(heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, holes: Uint8Array | null, sx: number, sy: number, sz: number): number {
269
+ return withF32(mod, heights, (hp) => holes
270
+ ? withBytes(mod, holes, (op) => mod._cphysShapeHeightField(hp, sizeX, sizeZ, cellSize, op, sx, sy, sz))
271
+ : mod._cphysShapeHeightField(hp, sizeX, sizeZ, cellSize, 0, sx, sy, sz))
272
+ },
273
+ physicsUpdateHeightField(shapeId: number, heights: Float32Array, sizeX: number, sizeZ: number, x0: number, z0: number, w: number, h: number, holes: Uint8Array | null): void {
274
+ withF32(mod, heights, (hp) => holes
275
+ ? withBytes(mod, holes, (op) => mod._cphysShapeUpdateHeightField(shapeId, hp, sizeX, sizeZ, x0, z0, w, h, op))
276
+ : mod._cphysShapeUpdateHeightField(shapeId, hp, sizeX, sizeZ, x0, z0, w, h, 0))
277
+ },
278
+ glbTriangles(entityId: number): Float32Array {
279
+ const buf = host.glbBytes?.(entityId)
280
+ if (!buf) return new Float32Array(0)
281
+ const bytes = asBytes(buf)
282
+ const ptr = withBytes(mod, bytes, (p) => mod._cphysGlbTriangles(p, bytes.length, u32a))
283
+ if (!ptr) return new Float32Array(0)
284
+ const count = mod.HEAPU32[u32a >> 2]
285
+ const out = Float32Array.from(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + count))
286
+ mod._free(ptr)
287
+ return out
288
+ },
289
+ physicsSetShapeOrigin(shapeId: number, x: number, y: number, z: number): void { mod._cphysShapeSetOrigin(shapeId, x, y, z) },
290
+ physicsSetBodyShape(bodyId: number, shapeId: number, updateMass: boolean): void { mod._cphysBodySetShape(bodyId, shapeId, updateMass ? 1 : 0) },
291
+ physicsDestroyShape(shapeId: number): void { mod._cphysShapeDestroy(shapeId) },
292
+
293
+ // --- bodies ---
294
+ physicsCreateBody: (entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number =>
295
+ bodyWithShape(entityId, shapeId, motion, mass, sensor, pickOnly),
296
+ physicsSetPickable(bodyId: number, pickable: boolean): void { mod._cphysBodySetPickable(bodyId, pickable ? 1 : 0) },
297
+ physicsSetFriction(bodyId: number, friction: number): void { mod._cphysBodySetFriction(bodyId, friction) },
298
+ physicsGetFriction: (bodyId: number): number => mod._cphysBodyGetFriction(bodyId),
299
+ physicsRaycast(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDist: number, out?: Float32Array): number {
300
+ const hit = mod._cphysRaycast(ox, oy, oz, dx, dy, dz, maxDist, out8)
301
+ if (hit && out) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(7, out.length)))
302
+ return hit
303
+ },
304
+ physicsCreateBox: (entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number =>
305
+ legacyBody(entityId, mod._cphysShapeBox(hx, hy, hz), motionType, mass),
306
+ physicsCreateSphere: (entityId: number, radius: number, motionType: number, mass: number): number =>
307
+ legacyBody(entityId, mod._cphysShapeSphere(radius), motionType, mass),
308
+ physicsCreateCylinder: (entityId: number, halfHeight: number, radius: number, motionType: number, mass: number): number =>
309
+ legacyBody(entityId, mod._cphysShapeCylinder(halfHeight, radius), motionType, mass),
310
+ physicsSetLinearVelocity(bodyId: number, x: number, y: number, z: number): void { mod._cphysBodySetLinearVelocity(bodyId, x, y, z) },
311
+ physicsGetLinearVelocity(bodyId: number, out: Float32Array): void { mod._cphysBodyGetLinearVelocity(bodyId, out8); read3(out, out8) },
312
+ physicsSetAngularVelocity(bodyId: number, x: number, y: number, z: number): void { mod._cphysBodySetAngularVelocity(bodyId, x, y, z) },
313
+ physicsGetAngularVelocity(bodyId: number, out: Float32Array): void { mod._cphysBodyGetAngularVelocity(bodyId, out8); read3(out, out8) },
314
+ physicsApplyImpulse(bodyId: number, x: number, y: number, z: number): void { mod._cphysBodyApplyImpulse(bodyId, x, y, z) },
315
+ physicsApplyImpulseAt(bodyId: number, x: number, y: number, z: number, px: number, py: number, pz: number): void { mod._cphysBodyApplyImpulseAt(bodyId, x, y, z, px, py, pz) },
316
+ physicsSetBodyPosition(bodyId: number, x: number, y: number, z: number): void { mod._cphysBodySetPosition(bodyId, x, y, z) },
317
+ physicsSetBodyRotation(bodyId: number, qx: number, qy: number, qz: number, qw: number): void { mod._cphysBodySetRotation(bodyId, qx, qy, qz, qw) },
318
+ physicsRemoveBody(bodyId: number): void { mod._cphysBodyRemove(bodyId) },
319
+
320
+ // --- characters ---
321
+ characterCreate(entityId: number, shapeId: number, maxSlopeDeg: number): number {
322
+ const id = shapeId ? mod._cphysCharacterCreate(entityId, shapeId, maxSlopeDeg, poseOf(entityId)) : 0
323
+ if (id) characterEntity.set(id, entityId)
324
+ return id
325
+ },
326
+ characterDestroy(charId: number): void { mod._cphysCharacterDestroy(charId); characterEntity.delete(charId) },
327
+ characterMove(charId: number, x: number, z: number): void { mod._cphysCharacterMove(charId, x, z) },
328
+ characterMoveFree(charId: number, x: number, y: number, z: number): void { mod._cphysCharacterMoveFree(charId, x, y, z) },
329
+ characterSetVerticalVelocity(charId: number, vy: number): void { mod._cphysCharacterSetVerticalVelocity(charId, vy) },
330
+ characterSetVelocity(charId: number, x: number, y: number, z: number): void { mod._cphysCharacterSetVelocity(charId, x, y, z) },
331
+ characterSetGravityScale(charId: number, scale: number): void { mod._cphysCharacterSetGravityScale(charId, scale) },
332
+ characterSetMaxSlope(charId: number, maxSlopeDeg: number): void { mod._cphysCharacterSetMaxSlope(charId, maxSlopeDeg) },
333
+ // Discontinuous: the node is pushed NOW (the caller reads its own height this same frame).
334
+ characterSetShape(charId: number, shapeId: number): boolean {
335
+ if (!mod._cphysCharacterSetShape(charId, shapeId)) return false
336
+ const entity = characterEntity.get(charId)
337
+ if (entity !== undefined) {
338
+ mod._cphysCharacterPosition(charId, out8)
339
+ const f = mod.HEAPF32, o = out8 >> 2
340
+ host.setWorldPosition(entity, f[o], f[o + 1], f[o + 2])
341
+ }
342
+ return true
343
+ },
344
+ characterGetVelocity(charId: number, out: Float32Array): void { mod._cphysCharacterGetVelocity(charId, out8); read3(out, out8) },
345
+ characterGetGroundState: (charId: number): number => mod._cphysCharacterGetGroundState(charId),
346
+ characterSetPosition(charId: number, x: number, y: number, z: number): void { mod._cphysCharacterSetPosition(charId, x, y, z) },
347
+
348
+ // --- vehicles ---
349
+ vehicleCreate(entityId: number, shapeId: number, settings: Float32Array): number {
350
+ if (!shapeId) return 0
351
+ return withF32(mod, settings, (sp) => mod._cphysVehicleCreate(entityId, shapeId, sp, settings.length, poseOf(entityId)))
352
+ },
353
+ vehicleDestroy(vehicleId: number): void { mod._cphysVehicleDestroy(vehicleId) },
354
+ vehicleSetInput(vehicleId: number, input: Float32Array): void { withF32(mod, input, (ip) => mod._cphysVehicleSetInput(vehicleId, ip, input.length)) },
355
+ vehicleSetTuning(vehicleId: number, settings: Float32Array): void { withF32(mod, settings, (sp) => mod._cphysVehicleSetTuning(vehicleId, sp, settings.length)) },
356
+ vehicleGetState(vehicleId: number, out: Float32Array): void {
357
+ const ptr = stateBuffer(out.length)
358
+ mod._cphysVehicleGetState(vehicleId, ptr, out.length)
359
+ out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + out.length))
360
+ },
361
+ vehicleReset(vehicleId: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number): void { mod._cphysVehicleReset(vehicleId, x, y, z, qx, qy, qz, qw) },
362
+ vehicleBodyId: (vehicleId: number): number => mod._cphysVehicleBodyId(vehicleId),
363
+
364
+ // --- ragdolls ---
365
+ ragdollCreate(rootEntityId: number, bones: Uint32Array, settings: Float32Array): number {
366
+ const partCount = settings.length > 1 ? settings[1] | 0 : 0
367
+ if (partCount <= 0 || bones.length < partCount * 2) return 0
368
+ const rootPose = new Float32Array(POSE_STRIDE)
369
+ decomposeInto(host.world(rootEntityId), rootPose, 0)
370
+ // per part: the bone's pose + the `to` entity's world position
371
+ const bonePoses = new Float32Array(partCount * (POSE_STRIDE + 3))
372
+ const entities: number[] = []
373
+ for (let i = 0; i < partCount; i++) {
374
+ const o = i * (POSE_STRIDE + 3)
375
+ decomposeInto(host.world(bones[i * 2]), bonePoses, o)
376
+ entities.push(bones[i * 2])
377
+ const to = bones[i * 2 + 1]
378
+ if (to) {
379
+ const w = host.world(to)
380
+ bonePoses[o + POSE_STRIDE] = w[12]; bonePoses[o + POSE_STRIDE + 1] = w[13]; bonePoses[o + POSE_STRIDE + 2] = w[14]
381
+ }
382
+ }
383
+ const id = withF32(mod, rootPose, (rp) => withU32(mod, bones, (bp) => withF32(mod, bonePoses, (pp) => withF32(mod, settings, (sp) =>
384
+ mod._cphysRagdollCreate(rp, bp, bones.length, pp, sp, settings.length)))))
385
+ if (id) ragdolls.set(id, { entities, prev: new Float32Array(partCount * POSE_STRIDE), cur: new Float32Array(partCount * POSE_STRIDE), valid: 0, dt: 1 / 60, active: false, driven: false, learn: settings.length > 14 && settings[13] !== 0, margin: settings.length > 14 ? Math.max(0, settings[14]) : 0, learned: false })
386
+ return id
387
+ },
388
+ ragdollDestroy(ragdollId: number): void { mod._cphysRagdollDestroy(ragdollId); ragdolls.delete(ragdollId) },
389
+ /** The bones' current poses become the bodies', each moving as the last two frames measured its bone
390
+ * (plus the launch) — creator-gl's ragdollActivate. */
391
+ ragdollActivate(ragdollId: number, vx: number, vy: number, vz: number): boolean {
392
+ const r = ragdolls.get(ragdollId)
393
+ if (!r) return false
394
+ if (!r.active && r.learn && !r.learned) learnLimits(ragdollId, r)
395
+ const n = r.entities.length
396
+ const bonePoses = new Float32Array(n * POSE_STRIDE)
397
+ for (let i = 0; i < n; i++) decomposeInto(host.world(r.entities[i]), bonePoses, i * POSE_STRIDE)
398
+ let vel: Float32Array | null = null
399
+ if (!r.active && r.valid >= 2 && r.dt > 1e-5) {
400
+ vel = new Float32Array(n * 6)
401
+ const inv = 1 / r.dt
402
+ for (let i = 0; i < n; i++) {
403
+ const a = i * POSE_STRIDE, o = i * 6
404
+ vel[o] = (r.cur[a] - r.prev[a]) * inv; vel[o + 1] = (r.cur[a + 1] - r.prev[a + 1]) * inv; vel[o + 2] = (r.cur[a + 2] - r.prev[a + 2]) * inv
405
+ // the world-frame rotation from prev to cur: d = qb · conj(qa) → angle-axis / dt
406
+ const ax = r.prev[a + 3], ay = r.prev[a + 4], az = r.prev[a + 5], aw = r.prev[a + 6]
407
+ const bx = r.cur[a + 3], by = r.cur[a + 4], bz = r.cur[a + 5], bw = r.cur[a + 6]
408
+ let dx = bw * -ax + bx * aw + by * -az - bz * -ay
409
+ let dy = bw * -ay - bx * -az + by * aw + bz * -ax
410
+ let dz = bw * -az + bx * -ay - by * -ax + bz * aw
411
+ let dw = bw * aw - bx * -ax - by * -ay - bz * -az
412
+ if (dw < 0) { dx = -dx; dy = -dy; dz = -dz; dw = -dw }
413
+ const s = Math.sqrt(Math.max(0, 1 - dw * dw))
414
+ if (s > 1e-6) {
415
+ const k = 2 * Math.acos(Math.min(1, dw)) / s * inv
416
+ vel[o + 3] = dx * k; vel[o + 4] = dy * k; vel[o + 5] = dz * k
417
+ }
418
+ }
419
+ }
420
+ const ok = withF32(mod, bonePoses, (pp) => (vel ? withF32(mod, vel, (vp) => mod._cphysRagdollActivate(ragdollId, vx, vy, vz, pp, vp)) : mod._cphysRagdollActivate(ragdollId, vx, vy, vz, pp, 0))) !== 0
421
+ if (ok) r.active = true
422
+ return ok
423
+ },
424
+ /** The bodies' last pose onto the bones against the parents' current worlds, the animator seeded with
425
+ * it (blend > 0), then physics lets go. */
426
+ ragdollDeactivate(ragdollId: number, blend: number): void {
427
+ const r = ragdolls.get(ragdollId)
428
+ if (r?.active) {
429
+ applyBodies(mod._cphysRagdollPose(ragdollId))
430
+ if (blend > 0 && r.entities.length > 0) host.seedAnimator?.(r.entities[0], blend)
431
+ r.active = false
432
+ r.valid = 0
433
+ }
434
+ mod._cphysRagdollDeactivate(ragdollId)
435
+ },
436
+ ragdollSetDrive(ragdollId: number, strength: number, anchor: boolean): void {
437
+ const r = ragdolls.get(ragdollId)
438
+ if (r) r.driven = strength > 0 || anchor
439
+ mod._cphysRagdollSetDrive(ragdollId, strength, anchor ? 1 : 0)
440
+ },
441
+ ragdollActive: (ragdollId: number): boolean => mod._cphysRagdollActive(ragdollId) !== 0,
442
+ ragdollBodyId: (ragdollId: number, index: number): number => mod._cphysRagdollBodyId(ragdollId, index),
443
+
444
+ setOnPhysicsEvent(cb: (a: number, b: number, type: number) => void): void { onEvent = cb },
445
+ }
446
+
447
+ return {
448
+ methods,
449
+ /** One host frame of GAME time: the fixed steps (with the host's fixed-phase callback between
450
+ * them, reading the live poses), the interpolation snapshot, the render poses onto the entities,
451
+ * then the contact events. */
452
+ frame(dt: number, fixedUpdate: ((dt: number) => void) | null): void {
453
+ const steps = mod._cphysFrameBegin(dt)
454
+ const fixedDt = mod._cphysFixedDt()
455
+ for (let i = 0; i < steps; i++) {
456
+ if (fixedUpdate) {
457
+ applyBodies(mod._cphysPullBodies(PULL_LIVE))
458
+ applyCharacters(mod._cphysPullCharacters(PULL_LIVE))
459
+ fixedUpdate(fixedDt)
460
+ }
461
+ mod._cphysStep()
462
+ }
463
+ mod._cphysFrameEnd()
464
+ applyBodies(mod._cphysPullBodies(PULL_RENDER))
465
+ applyCharacters(mod._cphysPullCharacters(PULL_RENDER))
466
+ const n = mod._cphysEventCount()
467
+ if (n > 0 && onEvent) {
468
+ // copied first: a handler may swap the world, and the buffer is the library's
469
+ const o = mod._cphysEventData() >> 2
470
+ const events = Array.from(mod.HEAPU32.subarray(o, o + n * 3))
471
+ for (let i = 0; i < n; i++) onEvent(events[i * 3], events[i * 3 + 1], events[i * 3 + 2])
472
+ }
473
+ },
474
+ /** After the host's animators: the part bones' animated poses go into the cache (the next activation's
475
+ * velocities, a driven body's motor target), then the active ragdolls' bodies overwrite their bones
476
+ * (parent-first). `dt` = the frame's game time. */
477
+ syncRagdolls(dt: number): void {
478
+ if (ragdolls.size === 0) return
479
+ for (const [ id, r ] of ragdolls) {
480
+ const swap = r.prev; r.prev = r.cur; r.cur = swap
481
+ for (let i = 0; i < r.entities.length; i++) decomposeInto(host.world(r.entities[i]), r.cur, i * POSE_STRIDE)
482
+ if (dt > 1e-5) r.dt = dt
483
+ if (r.valid < 2) r.valid++
484
+ if (r.active && r.driven) withF32(mod, r.cur, (pp) => mod._cphysRagdollSetTarget(id, pp))
485
+ }
486
+ applyBodies(mod._cphysPullRagdolls())
487
+ },
488
+ /** Root motion from an animator on `entityId`: a CharacterController on the node or an ancestor takes
489
+ * the world travel as its external velocity for the next step and yaws its node; true when one did. */
490
+ applyRootMotion(entityId: number, wx: number, wz: number, dt: number, yaw: number): boolean {
491
+ for (let e = entityId; e; e = host.parentOf(e)) {
492
+ const c = mod._cphysCharacterForEntity(e)
493
+ if (!c) continue
494
+ mod._cphysCharacterSetExternalVelocity(c, dt > 0 ? wx / dt : 0, dt > 0 ? wz / dt : 0)
495
+ if (yaw !== 0) {
496
+ const node = host.get(e)
497
+ if (node) { rotateLocalY(node.m, yaw); host.localMatrixChanged(e) }
498
+ }
499
+ return true
500
+ }
501
+ return false
502
+ },
503
+ /** Tear the world down (a project switch). */
504
+ dispose(): void {
505
+ mod._cphysShutdown()
506
+ mod._cphysSetParentLookup(0)
507
+ mod.removeFunction(parentFn)
508
+ mod._free(pose); mod._free(out8); mod._free(u32a)
509
+ if (stateScratch) mod._free(stateScratch)
510
+ characterEntity.clear(); ragdolls.clear()
511
+ },
512
+ }
513
+ }
514
+
515
+ export type PhysicsBridge = ReturnType<typeof createPhysicsBridge>
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />