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,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" />
|