reze-engine 0.2.11 → 0.2.13

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/src/model.ts CHANGED
@@ -1,421 +1,421 @@
1
- import { Mat4, Quat, easeInOut } from "./math"
2
- import { Rigidbody, Joint } from "./physics"
3
-
4
- const VERTEX_STRIDE = 8
5
-
6
- export interface Texture {
7
- path: string
8
- name: string
9
- }
10
-
11
- export interface Material {
12
- name: string
13
- diffuse: [number, number, number, number]
14
- specular: [number, number, number]
15
- ambient: [number, number, number]
16
- shininess: number
17
- diffuseTextureIndex: number
18
- normalTextureIndex: number
19
- sphereTextureIndex: number
20
- sphereMode: number
21
- toonTextureIndex: number
22
- edgeFlag: number
23
- edgeColor: [number, number, number, number]
24
- edgeSize: number
25
- vertexCount: number
26
- isEye?: boolean // New: marks eye materials
27
- isFace?: boolean // New: marks face/skin materials
28
- isHair?: boolean // New: marks hair materials
29
- }
30
-
31
- export interface Bone {
32
- name: string
33
- parentIndex: number // -1 if no parent
34
- bindTranslation: [number, number, number]
35
- children: number[] // child bone indices (built on skeleton creation)
36
- appendParentIndex?: number // index of the bone to inherit from
37
- appendRatio?: number // 0..1
38
- appendRotate?: boolean
39
- appendMove?: boolean
40
- }
41
-
42
- export interface Skeleton {
43
- bones: Bone[]
44
- inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
45
- }
46
-
47
- export interface Skinning {
48
- joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
49
- weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
50
- }
51
-
52
- // Runtime skeleton pose state (updated each frame)
53
- export interface SkeletonRuntime {
54
- nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
55
- localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
56
- localTranslations: Float32Array // vec3 per bone length = boneCount*3
57
- worldMatrices: Float32Array // mat4 per bone length = boneCount*16
58
- computedBones: boolean[] // length = boneCount
59
- }
60
-
61
- // Rotation tween state per bone
62
- interface RotationTweenState {
63
- active: Uint8Array // 0/1 per bone
64
- startQuat: Float32Array // quat per bone (x,y,z,w)
65
- targetQuat: Float32Array // quat per bone (x,y,z,w)
66
- startTimeMs: Float32Array // one float per bone (ms)
67
- durationMs: Float32Array // one float per bone (ms)
68
- }
69
-
70
- export class Model {
71
- private vertexData: Float32Array<ArrayBuffer>
72
- private vertexCount: number
73
- private indexData: Uint32Array<ArrayBuffer>
74
- private textures: Texture[] = []
75
- private materials: Material[] = []
76
- // Static skeleton/skinning (not necessarily serialized yet)
77
- private skeleton: Skeleton
78
- private skinning: Skinning
79
-
80
- // Physics data from PMX
81
- private rigidbodies: Rigidbody[] = []
82
- private joints: Joint[] = []
83
-
84
- // Runtime skeleton pose state (updated each frame)
85
- private runtimeSkeleton!: SkeletonRuntime
86
-
87
- // Cached identity matrices to avoid allocations in computeWorldMatrices
88
- private cachedIdentityMat1 = Mat4.identity()
89
- private cachedIdentityMat2 = Mat4.identity()
90
-
91
- private rotTweenState!: RotationTweenState
92
-
93
- constructor(
94
- vertexData: Float32Array<ArrayBuffer>,
95
- indexData: Uint32Array<ArrayBuffer>,
96
- textures: Texture[],
97
- materials: Material[],
98
- skeleton: Skeleton,
99
- skinning: Skinning,
100
- rigidbodies: Rigidbody[] = [],
101
- joints: Joint[] = []
102
- ) {
103
- this.vertexData = vertexData
104
- this.vertexCount = vertexData.length / VERTEX_STRIDE
105
- this.indexData = indexData
106
- this.textures = textures
107
- this.materials = materials
108
- this.skeleton = skeleton
109
- this.skinning = skinning
110
- this.rigidbodies = rigidbodies
111
- this.joints = joints
112
-
113
- if (this.skeleton.bones.length == 0) {
114
- throw new Error("Model has no bones")
115
- }
116
-
117
- this.initializeRuntimeSkeleton()
118
- this.initializeRotTweenBuffers()
119
- }
120
-
121
- private initializeRuntimeSkeleton(): void {
122
- const boneCount = this.skeleton.bones.length
123
-
124
- this.runtimeSkeleton = {
125
- localRotations: new Float32Array(boneCount * 4),
126
- localTranslations: new Float32Array(boneCount * 3),
127
- worldMatrices: new Float32Array(boneCount * 16),
128
- nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
129
- acc[bone.name] = index
130
- return acc
131
- }, {} as Record<string, number>),
132
- computedBones: new Array(boneCount).fill(false),
133
- }
134
-
135
- const rotations = this.runtimeSkeleton.localRotations
136
- for (let i = 0; i < this.skeleton.bones.length; i++) {
137
- const qi = i * 4
138
- if (rotations[qi + 3] === 0) {
139
- rotations[qi] = 0
140
- rotations[qi + 1] = 0
141
- rotations[qi + 2] = 0
142
- rotations[qi + 3] = 1
143
- }
144
- }
145
- }
146
-
147
- private initializeRotTweenBuffers(): void {
148
- const n = this.skeleton.bones.length
149
- this.rotTweenState = {
150
- active: new Uint8Array(n),
151
- startQuat: new Float32Array(n * 4),
152
- targetQuat: new Float32Array(n * 4),
153
- startTimeMs: new Float32Array(n),
154
- durationMs: new Float32Array(n),
155
- }
156
- }
157
-
158
- private updateRotationTweens(): void {
159
- const state = this.rotTweenState
160
- const now = performance.now()
161
- const rotations = this.runtimeSkeleton.localRotations
162
- const boneCount = this.skeleton.bones.length
163
-
164
- for (let i = 0; i < boneCount; i++) {
165
- if (state.active[i] !== 1) continue
166
-
167
- const startMs = state.startTimeMs[i]
168
- const durMs = Math.max(1, state.durationMs[i])
169
- const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
170
- const e = easeInOut(t)
171
-
172
- const qi = i * 4
173
- const startQuat = new Quat(
174
- state.startQuat[qi],
175
- state.startQuat[qi + 1],
176
- state.startQuat[qi + 2],
177
- state.startQuat[qi + 3]
178
- )
179
- const targetQuat = new Quat(
180
- state.targetQuat[qi],
181
- state.targetQuat[qi + 1],
182
- state.targetQuat[qi + 2],
183
- state.targetQuat[qi + 3]
184
- )
185
- const result = Quat.slerp(startQuat, targetQuat, e)
186
-
187
- rotations[qi] = result.x
188
- rotations[qi + 1] = result.y
189
- rotations[qi + 2] = result.z
190
- rotations[qi + 3] = result.w
191
-
192
- if (t >= 1) state.active[i] = 0
193
- }
194
- }
195
-
196
- // Get interleaved vertex data for GPU upload
197
- // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
198
- getVertices(): Float32Array<ArrayBuffer> {
199
- return this.vertexData
200
- }
201
-
202
- // Get texture information
203
- getTextures(): Texture[] {
204
- return this.textures
205
- }
206
-
207
- // Get material information
208
- getMaterials(): Material[] {
209
- return this.materials
210
- }
211
-
212
- // Get vertex count
213
- getVertexCount(): number {
214
- return this.vertexCount
215
- }
216
-
217
- // Get index data for GPU upload
218
- getIndices(): Uint32Array<ArrayBuffer> {
219
- return this.indexData
220
- }
221
-
222
- // Accessors for skeleton/skinning
223
- getSkeleton(): Skeleton {
224
- return this.skeleton
225
- }
226
-
227
- getSkinning(): Skinning {
228
- return this.skinning
229
- }
230
-
231
- // Accessors for physics data
232
- getRigidbodies(): Rigidbody[] {
233
- return this.rigidbodies
234
- }
235
-
236
- getJoints(): Joint[] {
237
- return this.joints
238
- }
239
-
240
- // ------- Bone helpers (public API) -------
241
-
242
- getBoneNames(): string[] {
243
- return this.skeleton.bones.map((b) => b.name)
244
- }
245
-
246
- rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
247
- const state = this.rotTweenState
248
- const normalized = quats.map((q) => q.normalize())
249
- const now = performance.now()
250
- const dur = durationMs && durationMs > 0 ? durationMs : 0
251
-
252
- for (let i = 0; i < names.length; i++) {
253
- const name = names[i]
254
- const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
255
- if (idx < 0 || idx >= this.skeleton.bones.length) continue
256
-
257
- const qi = idx * 4
258
- const rotations = this.runtimeSkeleton.localRotations
259
- const [tx, ty, tz, tw] = normalized[i].toArray()
260
-
261
- if (dur === 0) {
262
- rotations[qi] = tx
263
- rotations[qi + 1] = ty
264
- rotations[qi + 2] = tz
265
- rotations[qi + 3] = tw
266
- state.active[idx] = 0
267
- continue
268
- }
269
-
270
- let sx = rotations[qi]
271
- let sy = rotations[qi + 1]
272
- let sz = rotations[qi + 2]
273
- let sw = rotations[qi + 3]
274
-
275
- if (state.active[idx] === 1) {
276
- const startMs = state.startTimeMs[idx]
277
- const prevDur = Math.max(1, state.durationMs[idx])
278
- const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
279
- const e = easeInOut(t)
280
- const startQuat = new Quat(
281
- state.startQuat[qi],
282
- state.startQuat[qi + 1],
283
- state.startQuat[qi + 2],
284
- state.startQuat[qi + 3]
285
- )
286
- const targetQuat = new Quat(
287
- state.targetQuat[qi],
288
- state.targetQuat[qi + 1],
289
- state.targetQuat[qi + 2],
290
- state.targetQuat[qi + 3]
291
- )
292
- const result = Quat.slerp(startQuat, targetQuat, e)
293
- const cx = result.x
294
- const cy = result.y
295
- const cz = result.z
296
- const cw = result.w
297
- sx = cx
298
- sy = cy
299
- sz = cz
300
- sw = cw
301
- }
302
-
303
- state.startQuat[qi] = sx
304
- state.startQuat[qi + 1] = sy
305
- state.startQuat[qi + 2] = sz
306
- state.startQuat[qi + 3] = sw
307
- state.targetQuat[qi] = tx
308
- state.targetQuat[qi + 1] = ty
309
- state.targetQuat[qi + 2] = tz
310
- state.targetQuat[qi + 3] = tw
311
- state.startTimeMs[idx] = now
312
- state.durationMs[idx] = dur
313
- state.active[idx] = 1
314
- }
315
- }
316
-
317
- getBoneWorldMatrices(): Float32Array {
318
- return this.runtimeSkeleton.worldMatrices
319
- }
320
-
321
- getBoneInverseBindMatrices(): Float32Array {
322
- return this.skeleton.inverseBindMatrices
323
- }
324
-
325
- evaluatePose(): void {
326
- this.updateRotationTweens()
327
- this.computeWorldMatrices()
328
- }
329
-
330
- private computeWorldMatrices(): void {
331
- const bones = this.skeleton.bones
332
- const localRot = this.runtimeSkeleton.localRotations
333
- const localTrans = this.runtimeSkeleton.localTranslations
334
- const worldBuf = this.runtimeSkeleton.worldMatrices
335
- const computed = this.runtimeSkeleton.computedBones.fill(false)
336
- const boneCount = bones.length
337
-
338
- if (boneCount === 0) return
339
-
340
- const computeWorld = (i: number): void => {
341
- if (computed[i]) return
342
-
343
- const b = bones[i]
344
- if (b.parentIndex >= boneCount) {
345
- console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
346
- }
347
-
348
- const qi = i * 4
349
- let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
350
- let addLocalTx = 0,
351
- addLocalTy = 0,
352
- addLocalTz = 0
353
-
354
- // Optimized append rotation check - only check necessary conditions
355
- const appendParentIdx = b.appendParentIndex
356
- const hasAppend =
357
- b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
358
-
359
- if (hasAppend) {
360
- const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
361
- const hasRatio = Math.abs(ratio) > 1e-6
362
-
363
- if (hasRatio) {
364
- const apQi = appendParentIdx * 4
365
- const apTi = appendParentIdx * 3
366
-
367
- if (b.appendRotate) {
368
- let ax = localRot[apQi]
369
- let ay = localRot[apQi + 1]
370
- let az = localRot[apQi + 2]
371
- const aw = localRot[apQi + 3]
372
- const absRatio = ratio < 0 ? -ratio : ratio
373
- if (ratio < 0) {
374
- ax = -ax
375
- ay = -ay
376
- az = -az
377
- }
378
- const identityQuat = new Quat(0, 0, 0, 1)
379
- const appendQuat = new Quat(ax, ay, az, aw)
380
- const result = Quat.slerp(identityQuat, appendQuat, absRatio)
381
- const rx = result.x
382
- const ry = result.y
383
- const rz = result.z
384
- const rw = result.w
385
- rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
386
- }
387
-
388
- if (b.appendMove) {
389
- const appendRatio = b.appendRatio ?? 1
390
- addLocalTx = localTrans[apTi] * appendRatio
391
- addLocalTy = localTrans[apTi + 1] * appendRatio
392
- addLocalTz = localTrans[apTi + 2] * appendRatio
393
- }
394
- }
395
- }
396
-
397
- // Build local matrix: identity + bind translation, then rotation, then append translation
398
- this.cachedIdentityMat1
399
- .setIdentity()
400
- .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
401
- this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
402
- const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
403
-
404
- const worldOffset = i * 16
405
- if (b.parentIndex >= 0) {
406
- const p = b.parentIndex
407
- if (!computed[p]) computeWorld(p)
408
- const parentOffset = p * 16
409
- // Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
410
- Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
411
- worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
412
- } else {
413
- worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
414
- }
415
- computed[i] = true
416
- }
417
-
418
- // Process all bones (recursion handles dependencies automatically)
419
- for (let i = 0; i < boneCount; i++) computeWorld(i)
420
- }
421
- }
1
+ import { Mat4, Quat, easeInOut } from "./math"
2
+ import { Rigidbody, Joint } from "./physics"
3
+
4
+ const VERTEX_STRIDE = 8
5
+
6
+ export interface Texture {
7
+ path: string
8
+ name: string
9
+ }
10
+
11
+ export interface Material {
12
+ name: string
13
+ diffuse: [number, number, number, number]
14
+ specular: [number, number, number]
15
+ ambient: [number, number, number]
16
+ shininess: number
17
+ diffuseTextureIndex: number
18
+ normalTextureIndex: number
19
+ sphereTextureIndex: number
20
+ sphereMode: number
21
+ toonTextureIndex: number
22
+ edgeFlag: number
23
+ edgeColor: [number, number, number, number]
24
+ edgeSize: number
25
+ vertexCount: number
26
+ isEye?: boolean // New: marks eye materials
27
+ isFace?: boolean // New: marks face/skin materials
28
+ isHair?: boolean // New: marks hair materials
29
+ }
30
+
31
+ export interface Bone {
32
+ name: string
33
+ parentIndex: number // -1 if no parent
34
+ bindTranslation: [number, number, number]
35
+ children: number[] // child bone indices (built on skeleton creation)
36
+ appendParentIndex?: number // index of the bone to inherit from
37
+ appendRatio?: number // 0..1
38
+ appendRotate?: boolean
39
+ appendMove?: boolean
40
+ }
41
+
42
+ export interface Skeleton {
43
+ bones: Bone[]
44
+ inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
45
+ }
46
+
47
+ export interface Skinning {
48
+ joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
49
+ weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
50
+ }
51
+
52
+ // Runtime skeleton pose state (updated each frame)
53
+ export interface SkeletonRuntime {
54
+ nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
55
+ localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
56
+ localTranslations: Float32Array // vec3 per bone length = boneCount*3
57
+ worldMatrices: Float32Array // mat4 per bone length = boneCount*16
58
+ computedBones: boolean[] // length = boneCount
59
+ }
60
+
61
+ // Rotation tween state per bone
62
+ interface RotationTweenState {
63
+ active: Uint8Array // 0/1 per bone
64
+ startQuat: Float32Array // quat per bone (x,y,z,w)
65
+ targetQuat: Float32Array // quat per bone (x,y,z,w)
66
+ startTimeMs: Float32Array // one float per bone (ms)
67
+ durationMs: Float32Array // one float per bone (ms)
68
+ }
69
+
70
+ export class Model {
71
+ private vertexData: Float32Array<ArrayBuffer>
72
+ private vertexCount: number
73
+ private indexData: Uint32Array<ArrayBuffer>
74
+ private textures: Texture[] = []
75
+ private materials: Material[] = []
76
+ // Static skeleton/skinning (not necessarily serialized yet)
77
+ private skeleton: Skeleton
78
+ private skinning: Skinning
79
+
80
+ // Physics data from PMX
81
+ private rigidbodies: Rigidbody[] = []
82
+ private joints: Joint[] = []
83
+
84
+ // Runtime skeleton pose state (updated each frame)
85
+ private runtimeSkeleton!: SkeletonRuntime
86
+
87
+ // Cached identity matrices to avoid allocations in computeWorldMatrices
88
+ private cachedIdentityMat1 = Mat4.identity()
89
+ private cachedIdentityMat2 = Mat4.identity()
90
+
91
+ private rotTweenState!: RotationTweenState
92
+
93
+ constructor(
94
+ vertexData: Float32Array<ArrayBuffer>,
95
+ indexData: Uint32Array<ArrayBuffer>,
96
+ textures: Texture[],
97
+ materials: Material[],
98
+ skeleton: Skeleton,
99
+ skinning: Skinning,
100
+ rigidbodies: Rigidbody[] = [],
101
+ joints: Joint[] = []
102
+ ) {
103
+ this.vertexData = vertexData
104
+ this.vertexCount = vertexData.length / VERTEX_STRIDE
105
+ this.indexData = indexData
106
+ this.textures = textures
107
+ this.materials = materials
108
+ this.skeleton = skeleton
109
+ this.skinning = skinning
110
+ this.rigidbodies = rigidbodies
111
+ this.joints = joints
112
+
113
+ if (this.skeleton.bones.length == 0) {
114
+ throw new Error("Model has no bones")
115
+ }
116
+
117
+ this.initializeRuntimeSkeleton()
118
+ this.initializeRotTweenBuffers()
119
+ }
120
+
121
+ private initializeRuntimeSkeleton(): void {
122
+ const boneCount = this.skeleton.bones.length
123
+
124
+ this.runtimeSkeleton = {
125
+ localRotations: new Float32Array(boneCount * 4),
126
+ localTranslations: new Float32Array(boneCount * 3),
127
+ worldMatrices: new Float32Array(boneCount * 16),
128
+ nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
129
+ acc[bone.name] = index
130
+ return acc
131
+ }, {} as Record<string, number>),
132
+ computedBones: new Array(boneCount).fill(false),
133
+ }
134
+
135
+ const rotations = this.runtimeSkeleton.localRotations
136
+ for (let i = 0; i < this.skeleton.bones.length; i++) {
137
+ const qi = i * 4
138
+ if (rotations[qi + 3] === 0) {
139
+ rotations[qi] = 0
140
+ rotations[qi + 1] = 0
141
+ rotations[qi + 2] = 0
142
+ rotations[qi + 3] = 1
143
+ }
144
+ }
145
+ }
146
+
147
+ private initializeRotTweenBuffers(): void {
148
+ const n = this.skeleton.bones.length
149
+ this.rotTweenState = {
150
+ active: new Uint8Array(n),
151
+ startQuat: new Float32Array(n * 4),
152
+ targetQuat: new Float32Array(n * 4),
153
+ startTimeMs: new Float32Array(n),
154
+ durationMs: new Float32Array(n),
155
+ }
156
+ }
157
+
158
+ private updateRotationTweens(): void {
159
+ const state = this.rotTweenState
160
+ const now = performance.now()
161
+ const rotations = this.runtimeSkeleton.localRotations
162
+ const boneCount = this.skeleton.bones.length
163
+
164
+ for (let i = 0; i < boneCount; i++) {
165
+ if (state.active[i] !== 1) continue
166
+
167
+ const startMs = state.startTimeMs[i]
168
+ const durMs = Math.max(1, state.durationMs[i])
169
+ const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
170
+ const e = easeInOut(t)
171
+
172
+ const qi = i * 4
173
+ const startQuat = new Quat(
174
+ state.startQuat[qi],
175
+ state.startQuat[qi + 1],
176
+ state.startQuat[qi + 2],
177
+ state.startQuat[qi + 3]
178
+ )
179
+ const targetQuat = new Quat(
180
+ state.targetQuat[qi],
181
+ state.targetQuat[qi + 1],
182
+ state.targetQuat[qi + 2],
183
+ state.targetQuat[qi + 3]
184
+ )
185
+ const result = Quat.slerp(startQuat, targetQuat, e)
186
+
187
+ rotations[qi] = result.x
188
+ rotations[qi + 1] = result.y
189
+ rotations[qi + 2] = result.z
190
+ rotations[qi + 3] = result.w
191
+
192
+ if (t >= 1) state.active[i] = 0
193
+ }
194
+ }
195
+
196
+ // Get interleaved vertex data for GPU upload
197
+ // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
198
+ getVertices(): Float32Array<ArrayBuffer> {
199
+ return this.vertexData
200
+ }
201
+
202
+ // Get texture information
203
+ getTextures(): Texture[] {
204
+ return this.textures
205
+ }
206
+
207
+ // Get material information
208
+ getMaterials(): Material[] {
209
+ return this.materials
210
+ }
211
+
212
+ // Get vertex count
213
+ getVertexCount(): number {
214
+ return this.vertexCount
215
+ }
216
+
217
+ // Get index data for GPU upload
218
+ getIndices(): Uint32Array<ArrayBuffer> {
219
+ return this.indexData
220
+ }
221
+
222
+ // Accessors for skeleton/skinning
223
+ getSkeleton(): Skeleton {
224
+ return this.skeleton
225
+ }
226
+
227
+ getSkinning(): Skinning {
228
+ return this.skinning
229
+ }
230
+
231
+ // Accessors for physics data
232
+ getRigidbodies(): Rigidbody[] {
233
+ return this.rigidbodies
234
+ }
235
+
236
+ getJoints(): Joint[] {
237
+ return this.joints
238
+ }
239
+
240
+ // ------- Bone helpers (public API) -------
241
+
242
+ getBoneNames(): string[] {
243
+ return this.skeleton.bones.map((b) => b.name)
244
+ }
245
+
246
+ rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
247
+ const state = this.rotTweenState
248
+ const normalized = quats.map((q) => q.normalize())
249
+ const now = performance.now()
250
+ const dur = durationMs && durationMs > 0 ? durationMs : 0
251
+
252
+ for (let i = 0; i < names.length; i++) {
253
+ const name = names[i]
254
+ const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
255
+ if (idx < 0 || idx >= this.skeleton.bones.length) continue
256
+
257
+ const qi = idx * 4
258
+ const rotations = this.runtimeSkeleton.localRotations
259
+ const [tx, ty, tz, tw] = normalized[i].toArray()
260
+
261
+ if (dur === 0) {
262
+ rotations[qi] = tx
263
+ rotations[qi + 1] = ty
264
+ rotations[qi + 2] = tz
265
+ rotations[qi + 3] = tw
266
+ state.active[idx] = 0
267
+ continue
268
+ }
269
+
270
+ let sx = rotations[qi]
271
+ let sy = rotations[qi + 1]
272
+ let sz = rotations[qi + 2]
273
+ let sw = rotations[qi + 3]
274
+
275
+ if (state.active[idx] === 1) {
276
+ const startMs = state.startTimeMs[idx]
277
+ const prevDur = Math.max(1, state.durationMs[idx])
278
+ const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
279
+ const e = easeInOut(t)
280
+ const startQuat = new Quat(
281
+ state.startQuat[qi],
282
+ state.startQuat[qi + 1],
283
+ state.startQuat[qi + 2],
284
+ state.startQuat[qi + 3]
285
+ )
286
+ const targetQuat = new Quat(
287
+ state.targetQuat[qi],
288
+ state.targetQuat[qi + 1],
289
+ state.targetQuat[qi + 2],
290
+ state.targetQuat[qi + 3]
291
+ )
292
+ const result = Quat.slerp(startQuat, targetQuat, e)
293
+ const cx = result.x
294
+ const cy = result.y
295
+ const cz = result.z
296
+ const cw = result.w
297
+ sx = cx
298
+ sy = cy
299
+ sz = cz
300
+ sw = cw
301
+ }
302
+
303
+ state.startQuat[qi] = sx
304
+ state.startQuat[qi + 1] = sy
305
+ state.startQuat[qi + 2] = sz
306
+ state.startQuat[qi + 3] = sw
307
+ state.targetQuat[qi] = tx
308
+ state.targetQuat[qi + 1] = ty
309
+ state.targetQuat[qi + 2] = tz
310
+ state.targetQuat[qi + 3] = tw
311
+ state.startTimeMs[idx] = now
312
+ state.durationMs[idx] = dur
313
+ state.active[idx] = 1
314
+ }
315
+ }
316
+
317
+ getBoneWorldMatrices(): Float32Array {
318
+ return this.runtimeSkeleton.worldMatrices
319
+ }
320
+
321
+ getBoneInverseBindMatrices(): Float32Array {
322
+ return this.skeleton.inverseBindMatrices
323
+ }
324
+
325
+ evaluatePose(): void {
326
+ this.updateRotationTweens()
327
+ this.computeWorldMatrices()
328
+ }
329
+
330
+ private computeWorldMatrices(): void {
331
+ const bones = this.skeleton.bones
332
+ const localRot = this.runtimeSkeleton.localRotations
333
+ const localTrans = this.runtimeSkeleton.localTranslations
334
+ const worldBuf = this.runtimeSkeleton.worldMatrices
335
+ const computed = this.runtimeSkeleton.computedBones.fill(false)
336
+ const boneCount = bones.length
337
+
338
+ if (boneCount === 0) return
339
+
340
+ const computeWorld = (i: number): void => {
341
+ if (computed[i]) return
342
+
343
+ const b = bones[i]
344
+ if (b.parentIndex >= boneCount) {
345
+ console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
346
+ }
347
+
348
+ const qi = i * 4
349
+ let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
350
+ let addLocalTx = 0,
351
+ addLocalTy = 0,
352
+ addLocalTz = 0
353
+
354
+ // Optimized append rotation check - only check necessary conditions
355
+ const appendParentIdx = b.appendParentIndex
356
+ const hasAppend =
357
+ b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
358
+
359
+ if (hasAppend) {
360
+ const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
361
+ const hasRatio = Math.abs(ratio) > 1e-6
362
+
363
+ if (hasRatio) {
364
+ const apQi = appendParentIdx * 4
365
+ const apTi = appendParentIdx * 3
366
+
367
+ if (b.appendRotate) {
368
+ let ax = localRot[apQi]
369
+ let ay = localRot[apQi + 1]
370
+ let az = localRot[apQi + 2]
371
+ const aw = localRot[apQi + 3]
372
+ const absRatio = ratio < 0 ? -ratio : ratio
373
+ if (ratio < 0) {
374
+ ax = -ax
375
+ ay = -ay
376
+ az = -az
377
+ }
378
+ const identityQuat = new Quat(0, 0, 0, 1)
379
+ const appendQuat = new Quat(ax, ay, az, aw)
380
+ const result = Quat.slerp(identityQuat, appendQuat, absRatio)
381
+ const rx = result.x
382
+ const ry = result.y
383
+ const rz = result.z
384
+ const rw = result.w
385
+ rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
386
+ }
387
+
388
+ if (b.appendMove) {
389
+ const appendRatio = b.appendRatio ?? 1
390
+ addLocalTx = localTrans[apTi] * appendRatio
391
+ addLocalTy = localTrans[apTi + 1] * appendRatio
392
+ addLocalTz = localTrans[apTi + 2] * appendRatio
393
+ }
394
+ }
395
+ }
396
+
397
+ // Build local matrix: identity + bind translation, then rotation, then append translation
398
+ this.cachedIdentityMat1
399
+ .setIdentity()
400
+ .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
401
+ this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
402
+ const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
403
+
404
+ const worldOffset = i * 16
405
+ if (b.parentIndex >= 0) {
406
+ const p = b.parentIndex
407
+ if (!computed[p]) computeWorld(p)
408
+ const parentOffset = p * 16
409
+ // Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
410
+ Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
411
+ worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
412
+ } else {
413
+ worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
414
+ }
415
+ computed[i] = true
416
+ }
417
+
418
+ // Process all bones (recursion handles dependencies automatically)
419
+ for (let i = 0; i < boneCount; i++) computeWorld(i)
420
+ }
421
+ }