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,656 @@
|
|
|
1
|
+
// creator-anim's TypeScript side over the wasm build (dist/creatorAnim.js): the animation bridge the
|
|
2
|
+
// WEB hosts (viewer-lite's WebGL engine, lecodes-headless headless) hand to the app — the same
|
|
3
|
+
// `_creator` animation method shapes creator-gl exposes natively, so the SDK never knows which host
|
|
4
|
+
// it runs on, and the SAME evaluator (one C++ implementation, no TS twin).
|
|
5
|
+
//
|
|
6
|
+
// The host supplies its scene graph (`AnimHost`): entity lookup with a local matrix per node, a
|
|
7
|
+
// "local matrix changed" notification, the fetch store for loadClips, and the two sinks the
|
|
8
|
+
// evaluator's outputs go to (morph weights, root motion for a character controller).
|
|
9
|
+
//
|
|
10
|
+
// `loadCreatorAnim()` must have resolved before `createAnimBridge` — the web hosts await it in their
|
|
11
|
+
// async setup (createViewerLite / runProject) so the SDK's first animator call finds the module ready.
|
|
12
|
+
|
|
13
|
+
import createCreatorAnim, { type CreatorAnimModule } from "../dist/creatorAnim.js"
|
|
14
|
+
|
|
15
|
+
export type Mat4Like = Float32Array | number[]
|
|
16
|
+
|
|
17
|
+
export type AnimHost = {
|
|
18
|
+
/** Entity lookup (undefined when unknown). `skinJoint` marks joints of a GLB skin. */
|
|
19
|
+
get(id: number): { id: number, name: string, m: Mat4Like, parent: number, children: number[], skinJoint?: boolean } | undefined
|
|
20
|
+
/** A joint's local matrix was rewritten (WebGL: worldMatrixNeedsUpdate). */
|
|
21
|
+
localMatrixChanged(id: number): void
|
|
22
|
+
/** Fetched buffer for a systemId (loadClips). */
|
|
23
|
+
getBuffer(systemId: number): ArrayBuffer | undefined
|
|
24
|
+
/** Morph-target weights sampled for an entity (WebGL: the node's blendshape weights). */
|
|
25
|
+
setMorphWeights?(entityId: number, weights: Float32Array): void
|
|
26
|
+
/** World matrix of a node — footsteps are reported in world space. Without it the animator node's
|
|
27
|
+
* own local matrix is used (right whenever the character hangs directly off the scene root). */
|
|
28
|
+
world?(entityId: number): Mat4Like | undefined
|
|
29
|
+
/** Root motion for a CharacterController on the node or an ancestor: the node-local delta this frame.
|
|
30
|
+
* Return true when a controller took it (as a velocity); false → the evaluator moves the node. */
|
|
31
|
+
applyRootMotion?(entityId: number, dx: number, dy: number, dz: number, dt: number, yaw: number): boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ClipInfo = { name: string, duration: number, trackCount: number }
|
|
35
|
+
|
|
36
|
+
let modulePromise: Promise<CreatorAnimModule> | null = null
|
|
37
|
+
let loaded: CreatorAnimModule | null = null
|
|
38
|
+
/** The wasm module, loaded once per process. */
|
|
39
|
+
export const loadCreatorAnim = (): Promise<CreatorAnimModule> => (modulePromise ??= createCreatorAnim().then((m) => (loaded = m)))
|
|
40
|
+
export const isCreatorAnimLoaded = (): boolean => loaded !== null
|
|
41
|
+
|
|
42
|
+
// ---- heap helpers --------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const withF32 = <T>(mod: CreatorAnimModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
|
|
45
|
+
const ptr = mod._malloc(Math.max(4, values.length * 4))
|
|
46
|
+
mod.HEAPF32.set(values, ptr >> 2)
|
|
47
|
+
try { return fn(ptr) } finally { mod._free(ptr) }
|
|
48
|
+
}
|
|
49
|
+
const withU16 = <T>(mod: CreatorAnimModule, values: ArrayLike<number>, fn: (ptr: number) => T): T => {
|
|
50
|
+
const ptr = mod._malloc(Math.max(4, values.length * 2))
|
|
51
|
+
mod.HEAPU16.set(values, ptr >> 1)
|
|
52
|
+
try { return fn(ptr) } finally { mod._free(ptr) }
|
|
53
|
+
}
|
|
54
|
+
const withString = <T>(mod: CreatorAnimModule, s: string, fn: (ptr: number) => T): T => {
|
|
55
|
+
const n = mod.lengthBytesUTF8(s) + 1
|
|
56
|
+
const ptr = mod._malloc(n)
|
|
57
|
+
mod.stringToUTF8(s, ptr, n)
|
|
58
|
+
try { return fn(ptr) } finally { mod._free(ptr) }
|
|
59
|
+
}
|
|
60
|
+
const withBytes = <T>(mod: CreatorAnimModule, bytes: Uint8Array, fn: (ptr: number) => T): T => {
|
|
61
|
+
const ptr = mod._malloc(Math.max(1, bytes.length))
|
|
62
|
+
mod.HEAPU8.set(bytes, ptr)
|
|
63
|
+
try { return fn(ptr) } finally { mod._free(ptr) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- matrix helpers (column-major 4×4) -----------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** T·R·S into a column-major 4×4 (no allocation). */
|
|
69
|
+
const composeInto = (m: Mat4Like, tx: number, ty: number, tz: number, x: number, y: number, z: number, w: number, sx: number, sy: number, sz: number): void => {
|
|
70
|
+
const x2 = x + x, y2 = y + y, z2 = z + z
|
|
71
|
+
const xx = x * x2, xy = x * y2, xz = x * z2, yy = y * y2, yz = y * z2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2
|
|
72
|
+
const a = m as number[]
|
|
73
|
+
a[0] = (1 - (yy + zz)) * sx; a[1] = (xy + wz) * sx; a[2] = (xz - wy) * sx; a[3] = 0
|
|
74
|
+
a[4] = (xy - wz) * sy; a[5] = (1 - (xx + zz)) * sy; a[6] = (yz + wx) * sy; a[7] = 0
|
|
75
|
+
a[8] = (xz + wy) * sz; a[9] = (yz - wx) * sz; a[10] = (1 - (xx + yy)) * sz; a[11] = 0
|
|
76
|
+
a[12] = tx; a[13] = ty; a[14] = tz; a[15] = 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Column-major 4×4 → translation, rotation quat (x,y,z,w), scale. */
|
|
80
|
+
export const decomposeTRS = (m: Mat4Like): { t: [number, number, number], r: [number, number, number, number], s: [number, number, number] } => {
|
|
81
|
+
const a = m as ArrayLike<number>
|
|
82
|
+
let sx = Math.hypot(a[0], a[1], a[2]), sy = Math.hypot(a[4], a[5], a[6]), sz = Math.hypot(a[8], a[9], a[10])
|
|
83
|
+
const det = a[0] * (a[5] * a[10] - a[6] * a[9]) - a[4] * (a[1] * a[10] - a[2] * a[9]) + a[8] * (a[1] * a[6] - a[2] * a[5])
|
|
84
|
+
if (det < 0) sx = -sx
|
|
85
|
+
const ix = sx !== 0 ? 1 / sx : 0, iy = sy !== 0 ? 1 / sy : 0, iz = sz !== 0 ? 1 / sz : 0
|
|
86
|
+
const m00 = a[0] * ix, m10 = a[1] * ix, m20 = a[2] * ix
|
|
87
|
+
const m01 = a[4] * iy, m11 = a[5] * iy, m21 = a[6] * iy
|
|
88
|
+
const m02 = a[8] * iz, m12 = a[9] * iz, m22 = a[10] * iz
|
|
89
|
+
const trace = m00 + m11 + m22
|
|
90
|
+
let x: number, y: number, z: number, w: number
|
|
91
|
+
if (trace > 0) {
|
|
92
|
+
const s = 0.5 / Math.sqrt(trace + 1)
|
|
93
|
+
w = 0.25 / s; x = (m21 - m12) * s; y = (m02 - m20) * s; z = (m10 - m01) * s
|
|
94
|
+
} else if (m00 > m11 && m00 > m22) {
|
|
95
|
+
const s = 2 * Math.sqrt(1 + m00 - m11 - m22)
|
|
96
|
+
w = (m21 - m12) / s; x = 0.25 * s; y = (m01 + m10) / s; z = (m02 + m20) / s
|
|
97
|
+
} else if (m11 > m22) {
|
|
98
|
+
const s = 2 * Math.sqrt(1 + m11 - m00 - m22)
|
|
99
|
+
w = (m02 - m20) / s; x = (m01 + m10) / s; y = 0.25 * s; z = (m12 + m21) / s
|
|
100
|
+
} else {
|
|
101
|
+
const s = 2 * Math.sqrt(1 + m22 - m00 - m11)
|
|
102
|
+
w = (m10 - m01) / s; x = (m02 + m20) / s; y = (m12 + m21) / s; z = 0.25 * s
|
|
103
|
+
}
|
|
104
|
+
return { t: [ a[12], a[13], a[14] ], r: [ x, y, z, w ], s: [ sx, sy, sz ] }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** m = m · R_y(a): a turn about the matrix's own up axis. */
|
|
108
|
+
const rotateLocalY = (m: Mat4Like, a: number): void => {
|
|
109
|
+
const c = Math.cos(a), s = Math.sin(a)
|
|
110
|
+
const x0 = m[0], x1 = m[1], x2 = m[2], z0 = m[8], z1 = m[9], z2 = m[10]
|
|
111
|
+
m[0] = x0 * c - z0 * s; m[1] = x1 * c - z1 * s; m[2] = x2 * c - z2 * s
|
|
112
|
+
m[8] = x0 * s + z0 * c; m[9] = x1 * s + z1 * c; m[10] = x2 * s + z2 * c
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- the bridge ----------------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
type Rec = {
|
|
118
|
+
rootId: number
|
|
119
|
+
entities: number[] // library joint index → entity id (the pre-order the skeleton was described in)
|
|
120
|
+
trs: number // heap: 10 floats per joint
|
|
121
|
+
write: number // heap: 1 byte per joint
|
|
122
|
+
n: number
|
|
123
|
+
feetOn: boolean // the feet stage (lock / ground IK) is on: answer its ground probes each frame
|
|
124
|
+
// IK chains: the library handle, the nodes read each late pass (0 = none), the parameter shadow the
|
|
125
|
+
// node reads are merged into before every solve (CANIM_IK_* order)
|
|
126
|
+
iks: { ik: number, target: number, pole: number, rotation: number, params: Float32Array }[]
|
|
127
|
+
// node-driven sockets: the empty read each late pass
|
|
128
|
+
sockets: { set: string, name: string, node: number }[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A wasm predating the chains (the vendored build not yet rebuilt): warn once, keep the chains inert.
|
|
132
|
+
let warnedStale = false
|
|
133
|
+
const hasChains = (mod: CreatorAnimModule): boolean => {
|
|
134
|
+
if (typeof mod._canimAnimatorLateSolve === "function") return true
|
|
135
|
+
if (!warnedStale) { warnedStale = true; console.warn("creator-anim: this wasm predates IK chains / sockets — rebuild it (build-wasm.sh + vendor:creator); chains are inert") }
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const IK_COUNT = 19
|
|
140
|
+
const IK = { TARGET_X: 0, POLE_X: 3, HAS_POLE: 6, ROT_X: 8 }
|
|
141
|
+
|
|
142
|
+
const MAX_EVENTS = 64
|
|
143
|
+
const IDENTITY: Mat4Like = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]
|
|
144
|
+
|
|
145
|
+
export function createAnimBridge(host: AnimHost) {
|
|
146
|
+
const mod = loaded
|
|
147
|
+
if (!mod) throw new Error("creator-anim: await loadCreatorAnim() before createAnimBridge()")
|
|
148
|
+
const animators = new Map<number, Rec>()
|
|
149
|
+
let onEvent: ((animatorId: number, slot: number, type: number) => void) | null = null
|
|
150
|
+
// scratch heap cells, allocated once
|
|
151
|
+
const out4 = mod._malloc(16)
|
|
152
|
+
const out3 = mod._malloc(12)
|
|
153
|
+
const m16 = mod._malloc(64) // the animator node's world matrix, handed over per frame
|
|
154
|
+
const probeFrom = mod._malloc(12), probeTo = mod._malloc(12), out8 = mod._malloc(32)
|
|
155
|
+
const i32a = mod._malloc(4), u32a = mod._malloc(4)
|
|
156
|
+
const evPairs = mod._malloc(MAX_EVENTS * 4)
|
|
157
|
+
const pending: { id: number, slot: number, type: number }[] = []
|
|
158
|
+
const steps: { id: number, side: number, x: number, y: number, z: number }[] = []
|
|
159
|
+
let onStep: ((animatorId: number, side: number, x: number, y: number, z: number) => void) | null = null
|
|
160
|
+
|
|
161
|
+
const loadSet = (buf: ArrayBuffer | Uint8Array): number => {
|
|
162
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
|
|
163
|
+
return withBytes(mod, bytes, (ptr) => mod._canimClipsLoad(ptr, bytes.length))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const freeRec = (rec: Rec): void => { mod._free(rec.trs); mod._free(rec.write) }
|
|
167
|
+
|
|
168
|
+
const methods = {
|
|
169
|
+
loadClips(systemId: number, onComplete: (clipSetId: number) => void, onReject: (err: unknown) => void): void {
|
|
170
|
+
const buf = host.getBuffer(systemId)
|
|
171
|
+
const set = buf ? loadSet(buf) : 0
|
|
172
|
+
if (!set) { onReject(new Error("loadClips: not a GLB with animations")); return }
|
|
173
|
+
onComplete(set)
|
|
174
|
+
},
|
|
175
|
+
createClipFromTracks(names: string, data: Float32Array): number {
|
|
176
|
+
return withString(mod, names, (np) => withF32(mod, data, (dp) => mod._canimClipsFromTracks(np, dp, data.length)))
|
|
177
|
+
},
|
|
178
|
+
getClipSetInfo(clipSetId: number): ClipInfo[] {
|
|
179
|
+
const n = mod._canimClipsCount(clipSetId)
|
|
180
|
+
const out: ClipInfo[] = []
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
const np = mod._canimClipName(clipSetId, i)
|
|
183
|
+
out.push({ name: np ? mod.UTF8ToString(np) : "", duration: mod._canimClipDuration(clipSetId, i), trackCount: mod._canimClipTrackCount(clipSetId, i) })
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
},
|
|
187
|
+
sliceClip(clipSetId: number, clip: number, start: number, end: number): number { return mod._canimClipsSlice(clipSetId, clip, start, end) },
|
|
188
|
+
/** The hosts' one derivation (`AnimationClip.from`): the mirror if `mirror`, then the [start, end] window
|
|
189
|
+
* (start < 0 = whole, end < 0 = the clip's end), as a NEW single-clip set. 0 = bad id / range / no rig. */
|
|
190
|
+
deriveClip(clipSetId: number, clip: number, mirror: boolean, start: number, end: number): number {
|
|
191
|
+
return mod._canimClipsDerive(clipSetId, clip, mirror ? 1 : 0, start, end)
|
|
192
|
+
},
|
|
193
|
+
destroyClipSet(clipSetId: number): void { mod._canimClipsDestroy(clipSetId) },
|
|
194
|
+
animatorCreate(entityId: number): number {
|
|
195
|
+
const root = host.get(entityId)
|
|
196
|
+
if (!root) return 0
|
|
197
|
+
const entities: number[] = [], parents: number[] = [], names: string[] = [], rest: number[] = [], skin: number[] = []
|
|
198
|
+
const collect = (id: number, parent: number): void => {
|
|
199
|
+
const e = host.get(id)
|
|
200
|
+
if (!e) return
|
|
201
|
+
const index = entities.length
|
|
202
|
+
entities.push(id); parents.push(parent); names.push(e.name ?? ""); skin.push(e.skinJoint ? 1 : 0)
|
|
203
|
+
const { t, r, s } = decomposeTRS(e.m)
|
|
204
|
+
rest.push(t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2])
|
|
205
|
+
for (const c of e.children) collect(c, index)
|
|
206
|
+
}
|
|
207
|
+
collect(entityId, -1)
|
|
208
|
+
const n = entities.length
|
|
209
|
+
const parentsPtr = mod._malloc(Math.max(4, n * 4))
|
|
210
|
+
mod.HEAP32.set(parents, parentsPtr >> 2)
|
|
211
|
+
const skinPtr = mod._malloc(Math.max(1, n))
|
|
212
|
+
mod.HEAPU8.set(skin, skinPtr)
|
|
213
|
+
let id = 0
|
|
214
|
+
try {
|
|
215
|
+
id = withString(mod, names.join("\n"), (np) => withF32(mod, rest, (rp) => mod._canimAnimatorCreate(n, parentsPtr, np, rp, skinPtr, entityId)))
|
|
216
|
+
} finally { mod._free(parentsPtr); mod._free(skinPtr) }
|
|
217
|
+
if (!id) return 0
|
|
218
|
+
const shadowed = mod._canimAnimatorShadowedNames(id)
|
|
219
|
+
if (shadowed > 0) console.warn(`[Animator] ${shadowed} bone name(s) exist twice under this model (a leftover skeleton copy?) — bound to the skinned ones; run 'lecodes assets doctor' on the GLB`)
|
|
220
|
+
animators.set(id, { rootId: entityId, entities, trs: mod._malloc(Math.max(4, n * 40)), write: mod._malloc(Math.max(1, n)), n, feetOn: false, iks: [], sockets: [] })
|
|
221
|
+
return id
|
|
222
|
+
},
|
|
223
|
+
animatorDestroy(animatorId: number): void {
|
|
224
|
+
const rec = animators.get(animatorId)
|
|
225
|
+
if (!rec) { mod._canimAnimatorDestroy(animatorId); return }
|
|
226
|
+
// Leave the skeleton in rest pose (the next animator snapshots it as its rest pose).
|
|
227
|
+
mod._canimAnimatorRest(animatorId, rec.trs, rec.write)
|
|
228
|
+
const f = mod.HEAPF32, u = mod.HEAPU8, base = rec.trs >> 2
|
|
229
|
+
for (let j = 0; j < rec.n; j++) {
|
|
230
|
+
if (!u[rec.write + j]) continue
|
|
231
|
+
const e = host.get(rec.entities[j])
|
|
232
|
+
if (!e) continue
|
|
233
|
+
const o = base + j * 10
|
|
234
|
+
composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
|
|
235
|
+
host.localMatrixChanged(rec.entities[j])
|
|
236
|
+
}
|
|
237
|
+
mod._canimAnimatorDestroy(animatorId)
|
|
238
|
+
freeRec(rec)
|
|
239
|
+
animators.delete(animatorId)
|
|
240
|
+
},
|
|
241
|
+
animatorBind(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number { return mod._canimAnimatorBind(animatorId, clipSetId, clipIndex, layer) },
|
|
242
|
+
animatorBindMirror(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number { return mod._canimAnimatorBindMirror ? mod._canimAnimatorBindMirror(animatorId, clipSetId, clipIndex, layer) : -1 },
|
|
243
|
+
animatorBoundTracks(animatorId: number, slot: number): number { return mod._canimAnimatorBoundTracks(animatorId, slot) },
|
|
244
|
+
setClipEvents(clipSetId: number, clip: number, times: Float32Array): void {
|
|
245
|
+
withF32(mod, times, (tp) => mod._canimClipsSetEvents(clipSetId, clip, tp, times.length))
|
|
246
|
+
},
|
|
247
|
+
animatorSetBlend(animatorId: number, layer: number, dims: number, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases?: Float32Array): void {
|
|
248
|
+
const d = dims > 1 ? 2 : 1
|
|
249
|
+
const n = Math.min(slots.length, Math.floor(positions.length / d))
|
|
250
|
+
const ph = phases && phases.length >= n * 2 ? phases : null
|
|
251
|
+
withU16(mod, slots, (sp) => withF32(mod, positions, (pp) => {
|
|
252
|
+
if (ph) withF32(mod, ph, (hp) => mod._canimAnimatorSetBlend(animatorId, layer, d, sp, pp, hp, n, fade, speed))
|
|
253
|
+
else mod._canimAnimatorSetBlend(animatorId, layer, d, sp, pp, 0, n, fade, speed)
|
|
254
|
+
}))
|
|
255
|
+
},
|
|
256
|
+
animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void { mod._canimAnimatorSetBlendValue(animatorId, layer, x, y) },
|
|
257
|
+
animatorSlotSetTurn(animatorId: number, slot: number, radians: number, enabled: boolean): void { mod._canimAnimatorSlotSetTurn(animatorId, slot, radians, enabled ? 1 : 0) },
|
|
258
|
+
animatorSlotSetWindow(animatorId: number, slot: number, start: number, end: number): void { mod._canimAnimatorSlotSetWindow(animatorId, slot, start, end) },
|
|
259
|
+
animatorPlay(animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean): void {
|
|
260
|
+
mod._canimAnimatorPlay(animatorId, slot, loop ? 1 : 0, speed, fadeIn, fadeOut, restart ? 1 : 0)
|
|
261
|
+
},
|
|
262
|
+
animatorStop(animatorId: number, layer: number, slot: number, fade: number): void { mod._canimAnimatorStop(animatorId, layer, slot, fade) },
|
|
263
|
+
animatorSeek(animatorId: number, slot: number, time: number): void { mod._canimAnimatorSeek(animatorId, slot, time) },
|
|
264
|
+
animatorGetSlotTime(animatorId: number, slot: number): number { return mod._canimAnimatorSlotTime(animatorId, slot) },
|
|
265
|
+
animatorGetSlotWeight(animatorId: number, slot: number): number { return mod._canimAnimatorSlotWeight(animatorId, slot) },
|
|
266
|
+
animatorSetLayer(animatorId: number, layer: number, weight: number, additive: boolean, maskRoot: string): void {
|
|
267
|
+
withString(mod, maskRoot ?? "", (mp) => mod._canimAnimatorSetLayer(animatorId, layer, weight, additive ? 1 : 0, mp))
|
|
268
|
+
},
|
|
269
|
+
animatorSetLayerAnticipation(animatorId: number, layer: number, amount: number): void { mod._canimAnimatorSetLayerAnticipation(animatorId, layer, amount) },
|
|
270
|
+
animatorSetGlobal(animatorId: number, speed: number, paused: boolean): void { mod._canimAnimatorSetGlobal(animatorId, speed, paused ? 1 : 0) },
|
|
271
|
+
/** How far joints rotate over every clip bound to the animator that drives `jointEntityId`, per
|
|
272
|
+
* (child entity, parent entity) pair against `frames` (8 per pair: the reference relative rotation
|
|
273
|
+
* xyzw, the measuring frame xyzw in the parent's space) — creator-anim's canimAnimatorJointRanges.
|
|
274
|
+
* Returns 6 radians per pair (twist / plane / normal min-max), or null when no animator carries
|
|
275
|
+
* the joint or nothing is bound yet. */
|
|
276
|
+
animatorJointRanges(jointEntityId: number, pairs: ArrayLike<number>, frames: Float32Array): Float32Array | null {
|
|
277
|
+
for (const [ id, rec ] of animators) {
|
|
278
|
+
if (!rec.entities.includes(jointEntityId)) continue
|
|
279
|
+
const count = pairs.length >> 1
|
|
280
|
+
const idx = new Int32Array(count * 2)
|
|
281
|
+
for (let i = 0; i < count * 2; i++) idx[i] = rec.entities.indexOf(pairs[i])
|
|
282
|
+
const out = new Float32Array(count * 6)
|
|
283
|
+
const ptrI = mod._malloc(Math.max(4, count * 8))
|
|
284
|
+
mod.HEAP32.set(idx, ptrI >> 2)
|
|
285
|
+
const ptrO = mod._malloc(Math.max(4, count * 24))
|
|
286
|
+
let sampled = 0
|
|
287
|
+
try {
|
|
288
|
+
sampled = withF32(mod, frames, (fp) => mod._canimAnimatorJointRanges(id, ptrI, count, fp, ptrO))
|
|
289
|
+
if (sampled > 0) out.set(mod.HEAPF32.subarray(ptrO >> 2, (ptrO >> 2) + count * 6))
|
|
290
|
+
} finally { mod._free(ptrI); mod._free(ptrO) }
|
|
291
|
+
return sampled > 0 ? out : null
|
|
292
|
+
}
|
|
293
|
+
return null
|
|
294
|
+
},
|
|
295
|
+
/** The joints' CURRENT locals become what the animator driving `jointEntityId` showed: the next
|
|
296
|
+
* evaluation transitions from them over `blend` seconds (a ragdoll let go — creator-gl's
|
|
297
|
+
* seedAnimatorFromEntities). No animator carries the joint = nothing happens. */
|
|
298
|
+
animatorSeedPose(jointEntityId: number, blend: number): void {
|
|
299
|
+
for (const [ id, rec ] of animators) {
|
|
300
|
+
if (!rec.entities.includes(jointEntityId)) continue
|
|
301
|
+
const trs = new Float32Array(rec.n * 10)
|
|
302
|
+
for (let j = 0; j < rec.n; j++) {
|
|
303
|
+
const e = host.get(rec.entities[j])
|
|
304
|
+
const { t, r, s } = decomposeTRS(e ? e.m : IDENTITY)
|
|
305
|
+
trs.set([ t[0], t[1], t[2], r[0], r[1], r[2], r[3], s[0], s[1], s[2] ], j * 10)
|
|
306
|
+
}
|
|
307
|
+
withF32(mod, trs, (tp) => mod._canimAnimatorSeedPose(id, tp, blend))
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
animatorSetRootMotion(animatorId: number, bone: string, apply: number): void {
|
|
312
|
+
withString(mod, bone ?? "", (bp) => mod._canimAnimatorSetRootMotion(animatorId, bp, (apply | 0) & 255))
|
|
313
|
+
},
|
|
314
|
+
animatorGetRootMotion(animatorId: number, out: Float32Array): void {
|
|
315
|
+
mod._canimAnimatorRootMotion(animatorId, out3)
|
|
316
|
+
const f = mod.HEAPF32, o = out3 >> 2
|
|
317
|
+
out[0] = f[o]; out[1] = f[o + 1]; out[2] = f[o + 2]
|
|
318
|
+
},
|
|
319
|
+
setOnAnimatorEvent(callback: (animatorId: number, slot: number, type: number) => void): void { onEvent = callback },
|
|
320
|
+
|
|
321
|
+
// ---- contacts, phase, root curves (docs/animation-v2-plan.md §2.5-2.6) ----
|
|
322
|
+
animatorSetFeet(animatorId: number, left: string, right: string): void {
|
|
323
|
+
withString(mod, left, (l) => withString(mod, right, (r) => mod._canimAnimatorSetFeet(animatorId, l, r)))
|
|
324
|
+
},
|
|
325
|
+
animatorSlotCurveAt(animatorId: number, slot: number, which: number, time: number): number {
|
|
326
|
+
return mod._canimAnimatorSlotCurveAt(animatorId, slot, which, time)
|
|
327
|
+
},
|
|
328
|
+
animatorSlotCurveInfo(animatorId: number, slot: number, out: Float32Array): boolean {
|
|
329
|
+
const ok = mod._canimAnimatorSlotCurveInfo(animatorId, slot, out4)
|
|
330
|
+
if (ok) out.set(mod.HEAPF32.subarray(out4 >> 2, (out4 >> 2) + Math.min(4, out.length)))
|
|
331
|
+
return ok !== 0
|
|
332
|
+
},
|
|
333
|
+
animatorSlotTurn(animatorId: number, slot: number): number { return mod._canimAnimatorSlotTurn(animatorId, slot) },
|
|
334
|
+
animatorSlotTimeAtTurn(animatorId: number, slot: number, yaw: number): number { return mod._canimAnimatorSlotTimeAtTurn(animatorId, slot, yaw) },
|
|
335
|
+
animatorLayerPhase(animatorId: number, layer: number): number { return mod._canimAnimatorLayerPhase(animatorId, layer) },
|
|
336
|
+
animatorSeekPhase(animatorId: number, slot: number, phase: number): void { mod._canimAnimatorSeekPhase(animatorId, slot, phase) },
|
|
337
|
+
animatorSlotContacts(animatorId: number, slot: number, out: Float32Array): number {
|
|
338
|
+
const maxSpans = Math.floor(out.length / 8) // stride 8 since 2026-09-01 (+ stillFrom/stillTo)
|
|
339
|
+
if (maxSpans === 0) return mod._canimAnimatorSlotContacts(animatorId, slot, 0, 0)
|
|
340
|
+
const ptr = mod._malloc(maxSpans * 8 * 4)
|
|
341
|
+
const n = mod._canimAnimatorSlotContacts(animatorId, slot, ptr, maxSpans)
|
|
342
|
+
out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n, maxSpans) * 8))
|
|
343
|
+
mod._free(ptr)
|
|
344
|
+
return n
|
|
345
|
+
},
|
|
346
|
+
animatorSlotTreads(animatorId: number, slot: number, out: Float32Array): number {
|
|
347
|
+
if (out.length === 0) return mod._canimAnimatorSlotTreads(animatorId, slot, 0, 0)
|
|
348
|
+
const ptr = mod._malloc(out.length * 4)
|
|
349
|
+
const n = mod._canimAnimatorSlotTreads(animatorId, slot, ptr, out.length)
|
|
350
|
+
out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n + 1, out.length)))
|
|
351
|
+
mod._free(ptr)
|
|
352
|
+
return n
|
|
353
|
+
},
|
|
354
|
+
animatorSlotPhysics(animatorId: number, slot: number, time: number, out: Float32Array): number {
|
|
355
|
+
const ptr = mod._malloc(11 * 4)
|
|
356
|
+
const n = mod._canimAnimatorSlotPhysics(animatorId, slot, time, ptr)
|
|
357
|
+
if (n) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(11, out.length)))
|
|
358
|
+
mod._free(ptr)
|
|
359
|
+
return n
|
|
360
|
+
},
|
|
361
|
+
animatorSlotFeatures(animatorId: number, slot: number, time: number, out: Float32Array): number {
|
|
362
|
+
const ptr = mod._malloc(37 * 4)
|
|
363
|
+
const n = mod._canimAnimatorSlotFeatures(animatorId, slot, time, ptr)
|
|
364
|
+
if (n) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(37, out.length)))
|
|
365
|
+
mod._free(ptr)
|
|
366
|
+
return n
|
|
367
|
+
},
|
|
368
|
+
animatorKneeAxis(animatorId: number, side: number, out: Float32Array): number {
|
|
369
|
+
const n = mod._canimAnimatorKneeAxis(animatorId, side, out8)
|
|
370
|
+
if (n) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(8, out.length)))
|
|
371
|
+
return n
|
|
372
|
+
},
|
|
373
|
+
animatorSlotKneePole(animatorId: number, slot: number, side: number, time: number, out: Float32Array): number {
|
|
374
|
+
const n = mod._canimAnimatorSlotKneePole(animatorId, slot, side, time, out8)
|
|
375
|
+
if (n) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(6, out.length)))
|
|
376
|
+
return n
|
|
377
|
+
},
|
|
378
|
+
animatorSetStepWarp(animatorId: number, on: boolean, stride: number, lift: number, pitchDeg: number, slopeDeg: number): void {
|
|
379
|
+
mod._canimAnimatorSetStepWarp(animatorId, on ? 1 : 0, stride, lift, pitchDeg, slopeDeg)
|
|
380
|
+
},
|
|
381
|
+
animatorSlotFit(animatorId: number, slot: number, target: number, align: number, out: Float32Array): number {
|
|
382
|
+
if (out.length === 0) return mod._canimAnimatorSlotFit(animatorId, slot, target, align, 0, 0)
|
|
383
|
+
const ptr = mod._malloc(out.length * 4)
|
|
384
|
+
const n = mod._canimAnimatorSlotFit(animatorId, slot, target, align, ptr, out.length)
|
|
385
|
+
out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + Math.min(n, out.length)))
|
|
386
|
+
mod._free(ptr)
|
|
387
|
+
return n
|
|
388
|
+
},
|
|
389
|
+
animatorSlotExit(animatorId: number, slot: number, target: number, tolerance: number, atContact: boolean, align: number): number {
|
|
390
|
+
return mod._canimAnimatorSlotExit(animatorId, slot, target, tolerance, atContact ? 1 : 0, align)
|
|
391
|
+
},
|
|
392
|
+
animatorSlotAlign(animatorId: number, a: number, b: number, offA: number, cyclesA: number, out: Float32Array): number {
|
|
393
|
+
if (out.length < 4) return 0
|
|
394
|
+
const ptr = mod._malloc(16)
|
|
395
|
+
const ok = mod._canimAnimatorSlotAlign(animatorId, a, b, offA, cyclesA, ptr)
|
|
396
|
+
if (ok) out.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + 4))
|
|
397
|
+
mod._free(ptr)
|
|
398
|
+
return ok
|
|
399
|
+
},
|
|
400
|
+
setOnAnimatorStep(callback: (animatorId: number, side: number, x: number, y: number, z: number) => void): void { onStep = callback },
|
|
401
|
+
|
|
402
|
+
// ---- feet: foot lock + ground IK (docs/animation-v2-plan.md §2.9) ----
|
|
403
|
+
animatorSetFeetParams(animatorId: number, params: Float32Array): void {
|
|
404
|
+
withF32(mod, params, (p) => mod._canimAnimatorSetFeetParams(animatorId, p, params.length))
|
|
405
|
+
const rec = animators.get(animatorId)
|
|
406
|
+
if (rec) rec.feetOn = (params.length > 0 && params[0] > 0.5) || (params.length > 1 && params[1] > 0.5)
|
|
407
|
+
},
|
|
408
|
+
animatorFootState(animatorId: number, side: number, out: Float32Array): boolean {
|
|
409
|
+
const ok = mod._canimAnimatorFootState(animatorId, side, out8)
|
|
410
|
+
if (ok) out.set(mod.HEAPF32.subarray(out8 >> 2, (out8 >> 2) + Math.min(8, out.length)))
|
|
411
|
+
return ok !== 0
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// ---- IK chains + sockets (creator-anim anchors.cpp): solved in the LATE pass (lateSolve below) ----
|
|
415
|
+
/** kind 0 two-bone (the end entity, its parent, its grandparent) / 1 look-at. 0 = no such joint. */
|
|
416
|
+
animatorIkCreate(animatorId: number, kind: number, endEntityId: number): number {
|
|
417
|
+
const rec = animators.get(animatorId)
|
|
418
|
+
if (!rec || !hasChains(mod)) return 0
|
|
419
|
+
const end = rec.entities.indexOf(endEntityId)
|
|
420
|
+
if (end < 0) return 0
|
|
421
|
+
let mid = -1, root = -1
|
|
422
|
+
if (kind === 0) {
|
|
423
|
+
const midE = host.get(endEntityId)?.parent ?? 0
|
|
424
|
+
mid = rec.entities.indexOf(midE)
|
|
425
|
+
if (mid < 0) return 0
|
|
426
|
+
root = rec.entities.indexOf(host.get(midE)?.parent ?? 0)
|
|
427
|
+
if (root < 0) return 0
|
|
428
|
+
}
|
|
429
|
+
const ik = mod._canimAnimatorIkCreateJoints(animatorId, kind, root, mid, end)
|
|
430
|
+
if (!ik) return 0
|
|
431
|
+
const params = new Float32Array(IK_COUNT)
|
|
432
|
+
const ptr = mod._malloc(IK_COUNT * 4)
|
|
433
|
+
mod._canimAnimatorIkGet(animatorId, ik, ptr, IK_COUNT)
|
|
434
|
+
params.set(mod.HEAPF32.subarray(ptr >> 2, (ptr >> 2) + IK_COUNT))
|
|
435
|
+
mod._free(ptr)
|
|
436
|
+
rec.iks.push({ ik, target: 0, pole: 0, rotation: 0, params })
|
|
437
|
+
return ik
|
|
438
|
+
},
|
|
439
|
+
animatorIkDestroy(animatorId: number, ik: number): void {
|
|
440
|
+
mod._canimAnimatorIkDestroy(animatorId, ik)
|
|
441
|
+
const rec = animators.get(animatorId)
|
|
442
|
+
if (rec) rec.iks = rec.iks.filter((r) => r.ik !== ik)
|
|
443
|
+
},
|
|
444
|
+
animatorIkSet(animatorId: number, ik: number, params: Float32Array): void {
|
|
445
|
+
withF32(mod, params, (p) => mod._canimAnimatorIkSet(animatorId, ik, p, params.length))
|
|
446
|
+
const r = animators.get(animatorId)?.iks.find((x) => x.ik === ik)
|
|
447
|
+
if (r) r.params.set(params.subarray(0, Math.min(params.length, IK_COUNT)))
|
|
448
|
+
},
|
|
449
|
+
animatorIkNodes(animatorId: number, ik: number, targetEntity: number, poleEntity: number, rotationEntity: number): void {
|
|
450
|
+
const r = animators.get(animatorId)?.iks.find((x) => x.ik === ik)
|
|
451
|
+
if (r) { r.target = targetEntity; r.pole = poleEntity; r.rotation = rotationEntity }
|
|
452
|
+
},
|
|
453
|
+
animatorIkAnchor(animatorId: number, ik: number, socket: string): void {
|
|
454
|
+
withString(mod, socket ?? "", (sp) => mod._canimAnimatorIkAnchor(animatorId, ik, sp))
|
|
455
|
+
},
|
|
456
|
+
animatorIkState(animatorId: number, ik: number, out: Float32Array): boolean {
|
|
457
|
+
const ok = mod._canimAnimatorIkState(animatorId, ik, out4)
|
|
458
|
+
if (ok) out.set(mod.HEAPF32.subarray(out4 >> 2, (out4 >> 2) + Math.min(4, out.length)))
|
|
459
|
+
return ok !== 0
|
|
460
|
+
},
|
|
461
|
+
/** A socket on joint `jointEntityId`: a NODE socket (nodeEntityId != 0) is read every late pass, else
|
|
462
|
+
* `trs` (7 floats) fixed in the joint's space; neither = remove. */
|
|
463
|
+
animatorSetSocket(animatorId: number, set: string, name: string, jointEntityId: number, nodeEntityId: number, trs: Float32Array | null): void {
|
|
464
|
+
const rec = animators.get(animatorId)
|
|
465
|
+
if (!rec || !hasChains(mod)) return
|
|
466
|
+
rec.sockets = rec.sockets.filter((s) => !(s.set === set && s.name === name))
|
|
467
|
+
const call = (joint: number, values: ArrayLike<number> | null): void => {
|
|
468
|
+
withString(mod, set, (sp) => withString(mod, name, (np) => {
|
|
469
|
+
if (values) withF32(mod, values, (vp) => mod._canimAnimatorSetSocket(animatorId, sp, np, joint, vp))
|
|
470
|
+
else mod._canimAnimatorSetSocket(animatorId, sp, np, joint, 0)
|
|
471
|
+
}))
|
|
472
|
+
}
|
|
473
|
+
if (nodeEntityId === 0 && !trs) { call(-1, null); return }
|
|
474
|
+
const joint = rec.entities.indexOf(jointEntityId)
|
|
475
|
+
if (joint < 0) { console.warn(`[Animator] socket '${name}': the joint is no bone of this animator`); return }
|
|
476
|
+
call(joint, trs ?? [ 0, 0, 0, 0, 0, 0, 1 ])
|
|
477
|
+
if (nodeEntityId !== 0) rec.sockets.push({ set, name, node: nodeEntityId })
|
|
478
|
+
},
|
|
479
|
+
setClipAnchors(clipSetId: number, clip: number, bone: string, donor: string, sockets: string, spans: Float32Array): void {
|
|
480
|
+
if (!hasChains(mod)) return
|
|
481
|
+
withString(mod, bone, (bp) => withString(mod, donor ?? "", (dp) => withString(mod, sockets ?? "", (sp) =>
|
|
482
|
+
withF32(mod, spans, (vp) => mod._canimClipsSetAnchors(clipSetId, clip, bp, dp, sp, vp, Math.floor(spans.length / 4))))))
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
// ---- drives + warping (docs/animation-v2-plan.md §2.6 / §2.7) — the same `_creator` shapes creator-pkg exposes ----
|
|
486
|
+
/** The body's world velocity this frame: the game side of the stride / orientation warps' fit. */
|
|
487
|
+
animatorSetMotion(animatorId: number, vx: number, vy: number, vz: number): void {
|
|
488
|
+
mod._canimAnimatorSetMotion(animatorId, vx, vy, vz)
|
|
489
|
+
},
|
|
490
|
+
/** Cumulative capsule travel (m) and yaw (rad) — what distance / angle drives read. */
|
|
491
|
+
animatorSetDriveInput(animatorId: number, distance: number, angle: number): void {
|
|
492
|
+
mod._canimAnimatorSetDriveInput(animatorId, distance, angle)
|
|
493
|
+
},
|
|
494
|
+
/** A slot's clock driven by a quantity: mode 0 time / 1 distance / 2 angle, `entry` = the curve value at NOW. */
|
|
495
|
+
animatorSetSlotDrive(animatorId: number, slot: number, mode: number, entry: number): void {
|
|
496
|
+
mod._canimAnimatorSetSlotDrive(animatorId, slot, mode, entry)
|
|
497
|
+
},
|
|
498
|
+
/** Stride / orientation warping, as the engine's fixed-order CANIM_WARP_* parameter array. */
|
|
499
|
+
animatorSetWarpParams(animatorId: number, params: Float32Array): void {
|
|
500
|
+
withF32(mod, params, (p) => mod._canimAnimatorSetWarpParams(animatorId, p, params.length))
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const evaluateOne = (id: number, rec: Rec, dt: number): void => {
|
|
505
|
+
// The frame the library's pose is in, before the evaluation that uses it: the post-process stage
|
|
506
|
+
// (warps, the feet) works in world space, and the pose includes joint 0 — the animator node's own
|
|
507
|
+
// local transform — so the frame is the node's PARENT world (identity for a node at the root). The
|
|
508
|
+
// host's `world` when it has one, else the parent's local matrix (exact whenever that parent hangs
|
|
509
|
+
// directly off the scene root).
|
|
510
|
+
const node = host.get(rec.rootId)
|
|
511
|
+
const parent = node && node.parent ? host.get(node.parent) : undefined
|
|
512
|
+
const W: Mat4Like = parent ? (host.world?.(parent.id) ?? parent.m) : IDENTITY
|
|
513
|
+
mod.HEAPF32.set(W as ArrayLike<number>, m16 >> 2); mod._canimAnimatorSetWorld(id, m16)
|
|
514
|
+
const evCount = mod._canimAnimatorEvaluate(id, dt, 0, rec.trs, rec.write)
|
|
515
|
+
// The feet stage's ground for the NEXT evaluation. The lite core has no raycast, so the ground is
|
|
516
|
+
// the plane at the node's height, normal up — enough for the lock and to keep the pelvis where it
|
|
517
|
+
// is; stairs and slopes are a later lite-core addition.
|
|
518
|
+
if (rec.feetOn && node) {
|
|
519
|
+
const groundY = (host.world?.(rec.rootId) ?? node.m)[13]
|
|
520
|
+
for (let side = 0; side < 2; side++) {
|
|
521
|
+
if (!mod._canimAnimatorFootProbe(id, side, probeFrom, probeTo)) continue
|
|
522
|
+
const f = mod.HEAPF32, o = probeFrom >> 2
|
|
523
|
+
mod._canimAnimatorSetGround(id, side, 1, f[o], groundY, f[o + 2], 0, 1, 0)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const f = mod.HEAPF32, u = mod.HEAPU8, base = rec.trs >> 2
|
|
527
|
+
for (let j = 0; j < rec.n; j++) {
|
|
528
|
+
if (!u[rec.write + j]) continue
|
|
529
|
+
const e = host.get(rec.entities[j])
|
|
530
|
+
if (!e) continue
|
|
531
|
+
const o = base + j * 10
|
|
532
|
+
composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
|
|
533
|
+
host.localMatrixChanged(rec.entities[j])
|
|
534
|
+
}
|
|
535
|
+
if (host.setMorphWeights) {
|
|
536
|
+
const morphs = mod._canimAnimatorMorphCount(id)
|
|
537
|
+
for (let i = 0; i < morphs; i++) {
|
|
538
|
+
const wp = mod._canimAnimatorMorph(id, i, i32a, u32a)
|
|
539
|
+
const joint = mod.HEAP32[i32a >> 2], count = mod.HEAPU32[u32a >> 2]
|
|
540
|
+
if (!wp || joint < 0 || count === 0) continue
|
|
541
|
+
host.setMorphWeights(rec.entities[joint], Float32Array.from(mod.HEAPF32.subarray(wp >> 2, (wp >> 2) + count)))
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// root motion: apply this frame's horizontal travel (animator-node frame) + yaw per the apply mode
|
|
545
|
+
const mode = mod._canimAnimatorRootMotionFrame(id, out4)
|
|
546
|
+
if (mode !== 0) {
|
|
547
|
+
const g = mod.HEAPF32, o = out4 >> 2
|
|
548
|
+
const dx = g[o], dy = g[o + 1], dz = g[o + 2], yaw = g[o + 3]
|
|
549
|
+
let applied = false
|
|
550
|
+
if (mode === 2 && host.applyRootMotion) applied = host.applyRootMotion(rec.rootId, dx, dy, dz, dt, yaw)
|
|
551
|
+
if (!applied && (dx !== 0 || dy !== 0 || dz !== 0 || yaw !== 0)) {
|
|
552
|
+
const e = host.get(rec.rootId)
|
|
553
|
+
if (e) {
|
|
554
|
+
const m = e.m // → the parent's space (rotation + scale of the local matrix)
|
|
555
|
+
if (yaw !== 0) rotateLocalY(m, yaw) // the turn first: the travel follows the new heading
|
|
556
|
+
const px = m[0] * dx + m[4] * dy + m[8] * dz, py = m[1] * dx + m[5] * dy + m[9] * dz, pz = m[2] * dx + m[6] * dy + m[10] * dz
|
|
557
|
+
m[12] += px; m[13] += py; m[14] += pz
|
|
558
|
+
host.localMatrixChanged(rec.rootId)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// footsteps: contacts that began this evaluation, model space → world
|
|
563
|
+
if (onStep) {
|
|
564
|
+
const n = mod._canimAnimatorStepCount(id)
|
|
565
|
+
for (let i = 0; i < n; i++) {
|
|
566
|
+
const side = mod._canimAnimatorStep(id, i, out3)
|
|
567
|
+
if (side < 0) continue
|
|
568
|
+
const f = mod.HEAPF32, o = out3 >> 2
|
|
569
|
+
const x = f[o], y = f[o + 1], z = f[o + 2]
|
|
570
|
+
const m = W // the pose's frame (the node's parent), the same one the evaluation was handed
|
|
571
|
+
steps.push({ id, side,
|
|
572
|
+
x: m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
573
|
+
y: m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
574
|
+
z: m[2] * x + m[6] * y + m[10] * z + m[14] })
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (evCount > 0) {
|
|
578
|
+
const n = mod._canimAnimatorEvents(id, evPairs, MAX_EVENTS)
|
|
579
|
+
const p = mod.HEAPU16, o = evPairs >> 1
|
|
580
|
+
for (let i = 0; i < n; i++) pending.push({ id, slot: p[o + i * 2], type: p[o + i * 2 + 1] })
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The LATE pass (creator-gl's lateSolveAnimators): after the app's late phase, every animator with IK
|
|
585
|
+
// chains hands the library its joints AS THEY ARE NOW plus this frame's node targets and node sockets;
|
|
586
|
+
// the chains solve on that pose and only the joints they rotated are written back.
|
|
587
|
+
const worldOf = (id: number): Mat4Like | undefined => host.world?.(id) ?? host.get(id)?.m
|
|
588
|
+
const lateSolveOne = (id: number, rec: Rec): void => {
|
|
589
|
+
if (rec.iks.length === 0) return
|
|
590
|
+
const node = host.get(rec.rootId)
|
|
591
|
+
if (!node) return
|
|
592
|
+
const W: Mat4Like = node.parent ? (worldOf(node.parent) ?? IDENTITY) : IDENTITY
|
|
593
|
+
for (const r of rec.iks) {
|
|
594
|
+
if (!r.target && !r.pole && !r.rotation) continue
|
|
595
|
+
const tw = r.target ? worldOf(r.target) : undefined
|
|
596
|
+
if (tw) { r.params[IK.TARGET_X] = tw[12]; r.params[IK.TARGET_X + 1] = tw[13]; r.params[IK.TARGET_X + 2] = tw[14] }
|
|
597
|
+
const pw = r.pole ? worldOf(r.pole) : undefined
|
|
598
|
+
if (pw) { r.params[IK.POLE_X] = pw[12]; r.params[IK.POLE_X + 1] = pw[13]; r.params[IK.POLE_X + 2] = pw[14]; r.params[IK.HAS_POLE] = 1 }
|
|
599
|
+
const rw = r.rotation ? worldOf(r.rotation) : undefined
|
|
600
|
+
if (rw) { const q = decomposeTRS(rw).r; r.params[IK.ROT_X] = q[0]; r.params[IK.ROT_X + 1] = q[1]; r.params[IK.ROT_X + 2] = q[2]; r.params[IK.ROT_X + 3] = q[3] }
|
|
601
|
+
withF32(mod, r.params, (p) => mod._canimAnimatorIkSet(id, r.ik, p, IK_COUNT))
|
|
602
|
+
}
|
|
603
|
+
for (const s of rec.sockets) {
|
|
604
|
+
const m = worldOf(s.node)
|
|
605
|
+
if (!m) continue
|
|
606
|
+
mod.HEAPF32.set(m as ArrayLike<number>, m16 >> 2)
|
|
607
|
+
withString(mod, s.set, (sp) => withString(mod, s.name, (np) => mod._canimAnimatorSocketSetWorld(id, sp, np, m16)))
|
|
608
|
+
}
|
|
609
|
+
const f = mod.HEAPF32, base = rec.trs >> 2
|
|
610
|
+
for (let j = 0; j < rec.n; j++) {
|
|
611
|
+
const e = host.get(rec.entities[j])
|
|
612
|
+
const { t, r, s } = decomposeTRS(e ? e.m : IDENTITY)
|
|
613
|
+
const o = base + j * 10
|
|
614
|
+
f[o] = t[0]; f[o + 1] = t[1]; f[o + 2] = t[2]; f[o + 3] = r[0]; f[o + 4] = r[1]; f[o + 5] = r[2]; f[o + 6] = r[3]; f[o + 7] = s[0]; f[o + 8] = s[1]; f[o + 9] = s[2]
|
|
615
|
+
}
|
|
616
|
+
mod.HEAPF32.set(W as ArrayLike<number>, m16 >> 2)
|
|
617
|
+
if (mod._canimAnimatorLateSolve(id, m16, rec.trs, rec.write) === 0) return
|
|
618
|
+
const u = mod.HEAPU8
|
|
619
|
+
for (let j = 0; j < rec.n; j++) {
|
|
620
|
+
if (!u[rec.write + j]) continue
|
|
621
|
+
const e = host.get(rec.entities[j])
|
|
622
|
+
if (!e) continue
|
|
623
|
+
const o = base + j * 10
|
|
624
|
+
composeInto(e.m, f[o], f[o + 1], f[o + 2], f[o + 3], f[o + 4], f[o + 5], f[o + 6], f[o + 7], f[o + 8], f[o + 9])
|
|
625
|
+
host.localMatrixChanged(rec.entities[j])
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
methods,
|
|
631
|
+
/** Register a GLB's embedded clips (parsed from its bytes) — returns the set id, 0 if none. */
|
|
632
|
+
registerGlbClips(buf: ArrayBuffer): number { return loadSet(buf) },
|
|
633
|
+
/** The late pass: solve every animator's IK chains on the joints as the app's late phase left them. */
|
|
634
|
+
lateSolve(): void { for (const [ id, rec ] of animators) lateSolveOne(id, rec) },
|
|
635
|
+
/** Advance every animator by dt (after physics, before the late phase). Events fire after every
|
|
636
|
+
* animator has been evaluated (a handler may play / stop / destroy animators). */
|
|
637
|
+
evaluate(dt: number): void {
|
|
638
|
+
for (const [ id, rec ] of animators) evaluateOne(id, rec, dt)
|
|
639
|
+
if (steps.length > 0) {
|
|
640
|
+
const batch = steps.splice(0, steps.length)
|
|
641
|
+
if (onStep) for (const s of batch) onStep(s.id, s.side, s.x, s.y, s.z)
|
|
642
|
+
}
|
|
643
|
+
if (pending.length > 0 && onEvent) {
|
|
644
|
+
const batch = pending.splice(0, pending.length)
|
|
645
|
+
for (const e of batch) onEvent(e.id, e.slot, e.type)
|
|
646
|
+
} else pending.length = 0
|
|
647
|
+
},
|
|
648
|
+
/** destroyEntity hook. */
|
|
649
|
+
onEntityDestroyed(entityId: number): void {
|
|
650
|
+
mod._canimAnimatorsDestroyTagged(entityId)
|
|
651
|
+
for (const [ id, rec ] of animators) if (rec.rootId === entityId) { freeRec(rec); animators.delete(id) }
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export type AnimBridge = ReturnType<typeof createAnimBridge>
|