reze-engine 0.1.3 → 0.1.5

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,586 +1,418 @@
1
- const VERTEX_STRIDE = 8
2
-
3
- export interface Texture {
4
- path: string
5
- name: string
6
- }
7
-
8
- export interface Material {
9
- name: string
10
- diffuse: [number, number, number, number]
11
- specular: [number, number, number]
12
- ambient: [number, number, number]
13
- shininess: number
14
- diffuseTextureIndex: number
15
- normalTextureIndex: number
16
- sphereTextureIndex: number
17
- sphereMode: number
18
- toonTextureIndex: number
19
- edgeFlag: number
20
- edgeColor: [number, number, number, number]
21
- edgeSize: number
22
- vertexCount: number
23
- }
24
-
25
- export interface Bone {
26
- name: string
27
- parentIndex: number // -1 if no parent
28
- bindTranslation: [number, number, number]
29
- children: number[] // child bone indices (built on skeleton creation)
30
- appendParentIndex?: number // index of the bone to inherit from
31
- appendRatio?: number // 0..1
32
- appendRotate?: boolean
33
- appendMove?: boolean
34
- }
35
-
36
- export interface Skeleton {
37
- bones: Bone[]
38
- inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
39
- }
40
-
41
- export interface Skinning {
42
- joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
43
- weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
44
- }
45
-
46
- // Runtime skeleton pose state (updated each frame)
47
- export interface SkeletonRuntime {
48
- nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
49
- localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
50
- localTranslations: Float32Array // vec3 per bone length = boneCount*3
51
- worldMatrices: Float32Array // mat4 per bone length = boneCount*16
52
- computedBones: boolean[] // length = boneCount
53
- }
54
-
55
- // Rotation tween state per bone
56
- interface RotationTweenState {
57
- active: Uint8Array // 0/1 per bone
58
- startQuat: Float32Array // quat per bone (x,y,z,w)
59
- targetQuat: Float32Array // quat per bone (x,y,z,w)
60
- startTimeMs: Float32Array // one float per bone (ms)
61
- durationMs: Float32Array // one float per bone (ms)
62
- }
63
-
64
- import { Mat4, Vec3, Quat } from "./math"
65
- import { Rigidbody, Joint } from "./physics"
66
-
67
- export class Model {
68
- private vertexData: Float32Array<ArrayBuffer>
69
- private vertexCount: number
70
- private indexData: Uint32Array<ArrayBuffer>
71
- private textures: Texture[] = []
72
- private materials: Material[] = []
73
- // Static skeleton/skinning (not necessarily serialized yet)
74
- private skeleton: Skeleton
75
- private skinning: Skinning
76
-
77
- // Physics data from PMX
78
- private rigidbodies: Rigidbody[] = []
79
- private joints: Joint[] = []
80
-
81
- // Runtime skeleton pose state (updated each frame)
82
- private runtimeSkeleton: SkeletonRuntime
83
-
84
- // Cached identity matrices to avoid allocations in computeWorldMatrices
85
- private cachedIdentityMat1 = Mat4.identity()
86
- private cachedIdentityMat2 = Mat4.identity()
87
-
88
- constructor(
89
- vertexData: Float32Array<ArrayBuffer>,
90
- indexData: Uint32Array<ArrayBuffer>,
91
- textures: Texture[],
92
- materials: Material[],
93
- skeleton: Skeleton,
94
- skinning: Skinning,
95
- rigidbodies: Rigidbody[] = [],
96
- joints: Joint[] = []
97
- ) {
98
- this.vertexData = vertexData
99
- this.vertexCount = vertexData.length / VERTEX_STRIDE
100
- this.indexData = indexData
101
- this.textures = textures
102
- this.materials = materials
103
- this.skeleton = skeleton
104
- this.skinning = skinning
105
- this.rigidbodies = rigidbodies
106
- this.joints = joints
107
-
108
- // Initialize runtime skeleton pose state
109
- const boneCount = skeleton.bones.length
110
- this.runtimeSkeleton = {
111
- localRotations: new Float32Array(boneCount * 4),
112
- localTranslations: new Float32Array(boneCount * 3),
113
- worldMatrices: new Float32Array(boneCount * 16),
114
- nameIndex: {}, // Will be populated by buildBoneLookups()
115
- computedBones: new Array(boneCount).fill(false),
116
- }
117
-
118
- if (this.skeleton.bones.length > 0) {
119
- this.buildBoneLookups()
120
- this.initializeRuntimePose()
121
- this.initializeRotTweenBuffers()
122
- }
123
- }
124
-
125
- // Build caches for O(1) bone lookups and populate children arrays
126
- // Called during model initialization
127
- private buildBoneLookups(): void {
128
- const nameToIndex: Record<string, number> = {}
129
-
130
- // Initialize children arrays for all bones and build name index
131
- for (let i = 0; i < this.skeleton.bones.length; i++) {
132
- this.skeleton.bones[i].children = []
133
- nameToIndex[this.skeleton.bones[i].name] = i
134
- }
135
-
136
- // Build parent->children relationships
137
- for (let i = 0; i < this.skeleton.bones.length; i++) {
138
- const bone = this.skeleton.bones[i]
139
- const parentIdx = bone.parentIndex
140
- if (parentIdx >= 0 && parentIdx < this.skeleton.bones.length) {
141
- this.skeleton.bones[parentIdx].children.push(i)
142
- }
143
- }
144
-
145
- this.runtimeSkeleton.nameIndex = nameToIndex
146
- }
147
-
148
- private rotTweenState!: RotationTweenState
149
-
150
- private initializeRotTweenBuffers(): void {
151
- const n = this.skeleton.bones.length
152
- this.rotTweenState = {
153
- active: new Uint8Array(n),
154
- startQuat: new Float32Array(n * 4),
155
- targetQuat: new Float32Array(n * 4),
156
- startTimeMs: new Float32Array(n),
157
- durationMs: new Float32Array(n),
158
- }
159
- }
160
-
161
- private static easeInOut(t: number): number {
162
- return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
163
- }
164
-
165
- private static slerp(
166
- aX: number,
167
- aY: number,
168
- aZ: number,
169
- aW: number,
170
- bX: number,
171
- bY: number,
172
- bZ: number,
173
- bW: number,
174
- t: number
175
- ): [number, number, number, number] {
176
- let cos = aX * bX + aY * bY + aZ * bZ + aW * bW
177
- let bx = bX,
178
- by = bY,
179
- bz = bZ,
180
- bw = bW
181
- if (cos < 0) {
182
- cos = -cos
183
- bx = -bx
184
- by = -by
185
- bz = -bz
186
- bw = -bw
187
- }
188
- if (cos > 0.9995) {
189
- const x = aX + t * (bx - aX)
190
- const y = aY + t * (by - aY)
191
- const z = aZ + t * (bz - aZ)
192
- const w = aW + t * (bw - aW)
193
- const invLen = 1 / Math.hypot(x, y, z, w)
194
- return [x * invLen, y * invLen, z * invLen, w * invLen]
195
- }
196
- const theta0 = Math.acos(cos)
197
- const sinTheta0 = Math.sin(theta0)
198
- const theta = theta0 * t
199
- const s0 = Math.sin(theta0 - theta) / sinTheta0
200
- const s1 = Math.sin(theta) / sinTheta0
201
- return [s0 * aX + s1 * bx, s0 * aY + s1 * by, s0 * aZ + s1 * bz, s0 * aW + s1 * bw]
202
- }
203
-
204
- private updateRotationTweens(): void {
205
- const state = this.rotTweenState
206
- const now = performance.now()
207
- const rotations = this.runtimeSkeleton.localRotations
208
-
209
- for (let i = 0; i < this.skeleton.bones.length; i++) {
210
- if (state.active[i] !== 1) continue
211
-
212
- const startMs = state.startTimeMs[i]
213
- const durMs = Math.max(1, state.durationMs[i])
214
- const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
215
- const e = Model.easeInOut(t)
216
-
217
- const qi = i * 4
218
- const [x, y, z, w] = Model.slerp(
219
- state.startQuat[qi],
220
- state.startQuat[qi + 1],
221
- state.startQuat[qi + 2],
222
- state.startQuat[qi + 3],
223
- state.targetQuat[qi],
224
- state.targetQuat[qi + 1],
225
- state.targetQuat[qi + 2],
226
- state.targetQuat[qi + 3],
227
- e
228
- )
229
-
230
- rotations[qi] = x
231
- rotations[qi + 1] = y
232
- rotations[qi + 2] = z
233
- rotations[qi + 3] = w
234
-
235
- if (t >= 1) state.active[i] = 0
236
- }
237
- }
238
-
239
- // Get interleaved vertex data for GPU upload
240
- // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
241
- getVertices(): Float32Array<ArrayBuffer> {
242
- return this.vertexData
243
- }
244
-
245
- // Get texture information
246
- getTextures(): Texture[] {
247
- return this.textures
248
- }
249
-
250
- // Get material information
251
- getMaterials(): Material[] {
252
- return this.materials
253
- }
254
-
255
- // Get diffuse texture path for a material
256
- getDiffuseTexturePath(materialIndex: number): string | null {
257
- if (materialIndex < 0 || materialIndex >= this.materials.length) return null
258
- const material = this.materials[materialIndex]
259
- if (material.diffuseTextureIndex < 0 || material.diffuseTextureIndex >= this.textures.length) return null
260
- return this.textures[material.diffuseTextureIndex].path
261
- }
262
-
263
- // Get vertex count
264
- getVertexCount(): number {
265
- return this.vertexCount
266
- }
267
-
268
- // Get index data for GPU upload
269
- getIndices(): Uint32Array<ArrayBuffer> {
270
- return this.indexData
271
- }
272
-
273
- // Accessors for skeleton/skinning
274
- getSkeleton(): Skeleton {
275
- return this.skeleton
276
- }
277
-
278
- getSkinning(): Skinning {
279
- return this.skinning
280
- }
281
-
282
- // Accessors for physics data
283
- getRigidbodies(): Rigidbody[] {
284
- return this.rigidbodies
285
- }
286
-
287
- getJoints(): Joint[] {
288
- return this.joints
289
- }
290
-
291
- private initializeRuntimePose(): void {
292
- const rotations = this.runtimeSkeleton.localRotations
293
- for (let i = 0; i < this.skeleton.bones.length; i++) {
294
- const qi = i * 4
295
- if (rotations[qi + 3] === 0) {
296
- rotations[qi] = 0
297
- rotations[qi + 1] = 0
298
- rotations[qi + 2] = 0
299
- rotations[qi + 3] = 1
300
- }
301
- }
302
- }
303
-
304
- // ------- Bone helpers (public API) -------
305
- getBoneCount(): number {
306
- return this.skeleton.bones.length
307
- }
308
-
309
- getBoneNames(): string[] {
310
- return this.skeleton.bones.map((b) => b.name)
311
- }
312
-
313
- getBoneIndexByName(name: string): number {
314
- return this.runtimeSkeleton.nameIndex[name] ?? -1
315
- }
316
-
317
- getBoneName(index: number): string | undefined {
318
- if (index < 0 || index >= this.skeleton.bones.length) return undefined
319
- return this.skeleton.bones[index].name
320
- }
321
-
322
- getBoneLocal(index: number): { rotation: Quat; translation: Vec3 } | undefined {
323
- if (index < 0 || index >= this.skeleton.bones.length) return undefined
324
- const qi = index * 4
325
- const ti = index * 3
326
- const rot = this.runtimeSkeleton.localRotations
327
- const trans = this.runtimeSkeleton.localTranslations
328
- return {
329
- rotation: new Quat(rot[qi], rot[qi + 1], rot[qi + 2], rot[qi + 3]),
330
- translation: new Vec3(trans[ti], trans[ti + 1], trans[ti + 2]),
331
- }
332
- }
333
-
334
- rotateBones(indicesOrNames: Array<number | string>, quats: Quat[], durationMs?: number): void {
335
- const state = this.rotTweenState
336
- const normalized = quats.map((q) => q.normalize())
337
- const now = performance.now()
338
- const dur = durationMs && durationMs > 0 ? durationMs : 0
339
-
340
- for (let i = 0; i < indicesOrNames.length; i++) {
341
- const input = indicesOrNames[i]
342
- let idx: number
343
- if (typeof input === "number") {
344
- idx = input
345
- } else {
346
- const resolved = this.getBoneIndexByName(input)
347
- if (resolved < 0) continue
348
- idx = resolved
349
- }
350
- if (idx < 0 || idx >= this.skeleton.bones.length) continue
351
-
352
- const qi = idx * 4
353
- const rotations = this.runtimeSkeleton.localRotations
354
- const [tx, ty, tz, tw] = normalized[i].toArray()
355
-
356
- if (dur === 0) {
357
- rotations[qi] = tx
358
- rotations[qi + 1] = ty
359
- rotations[qi + 2] = tz
360
- rotations[qi + 3] = tw
361
- state.active[idx] = 0
362
- continue
363
- }
364
-
365
- let sx = rotations[qi]
366
- let sy = rotations[qi + 1]
367
- let sz = rotations[qi + 2]
368
- let sw = rotations[qi + 3]
369
-
370
- if (state.active[idx] === 1) {
371
- const startMs = state.startTimeMs[idx]
372
- const prevDur = Math.max(1, state.durationMs[idx])
373
- const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
374
- const e = Model.easeInOut(t)
375
- const [cx, cy, cz, cw] = Model.slerp(
376
- state.startQuat[qi],
377
- state.startQuat[qi + 1],
378
- state.startQuat[qi + 2],
379
- state.startQuat[qi + 3],
380
- state.targetQuat[qi],
381
- state.targetQuat[qi + 1],
382
- state.targetQuat[qi + 2],
383
- state.targetQuat[qi + 3],
384
- e
385
- )
386
- sx = cx
387
- sy = cy
388
- sz = cz
389
- sw = cw
390
- }
391
-
392
- state.startQuat[qi] = sx
393
- state.startQuat[qi + 1] = sy
394
- state.startQuat[qi + 2] = sz
395
- state.startQuat[qi + 3] = sw
396
- state.targetQuat[qi] = tx
397
- state.targetQuat[qi + 1] = ty
398
- state.targetQuat[qi + 2] = tz
399
- state.targetQuat[qi + 3] = tw
400
- state.startTimeMs[idx] = now
401
- state.durationMs[idx] = dur
402
- state.active[idx] = 1
403
- }
404
- }
405
-
406
- setBoneRotation(indexOrName: number | string, quat: Quat): void {
407
- const index = typeof indexOrName === "number" ? indexOrName : this.getBoneIndexByName(indexOrName)
408
- if (index < 0 || index >= this.skeleton.bones.length) return
409
- const qi = index * 4
410
- const rot = this.runtimeSkeleton.localRotations
411
- rot[qi] = quat.x
412
- rot[qi + 1] = quat.y
413
- rot[qi + 2] = quat.z
414
- rot[qi + 3] = quat.w
415
- }
416
-
417
- setBoneTranslation(indexOrName: number | string, t: Vec3): void {
418
- const index = typeof indexOrName === "number" ? indexOrName : this.getBoneIndexByName(indexOrName)
419
- if (index < 0 || index >= this.skeleton.bones.length) return
420
- const ti = index * 3
421
- const trans = this.runtimeSkeleton.localTranslations
422
- trans[ti] = t.x
423
- trans[ti + 1] = t.y
424
- trans[ti + 2] = t.z
425
- }
426
-
427
- resetBone(index: number): void {
428
- const qi = index * 4
429
- const ti = index * 3
430
- const rot = this.runtimeSkeleton.localRotations
431
- const trans = this.runtimeSkeleton.localTranslations
432
- rot[qi] = 0
433
- rot[qi + 1] = 0
434
- rot[qi + 2] = 0
435
- rot[qi + 3] = 1
436
- trans[ti] = 0
437
- trans[ti + 1] = 0
438
- trans[ti + 2] = 0
439
- }
440
-
441
- resetAllBones(): void {
442
- const rot = this.runtimeSkeleton.localRotations
443
- const trans = this.runtimeSkeleton.localTranslations
444
- const count = this.getBoneCount()
445
-
446
- for (let i = 0; i < count; i++) {
447
- const qi = i * 4
448
- const ti = i * 3
449
- rot[qi] = 0
450
- rot[qi + 1] = 0
451
- rot[qi + 2] = 0
452
- rot[qi + 3] = 1
453
- trans[ti] = 0
454
- trans[ti + 1] = 0
455
- trans[ti + 2] = 0
456
- }
457
- }
458
-
459
- getBoneWorldMatrix(index: number): Float32Array | undefined {
460
- this.evaluatePose()
461
- const start = index * 16
462
- return this.runtimeSkeleton.worldMatrices.subarray(start, start + 16)
463
- }
464
-
465
- getBoneWorldMatrices(): Float32Array {
466
- return this.runtimeSkeleton.worldMatrices
467
- }
468
-
469
- getBoneInverseBindMatrices(): Float32Array {
470
- return this.skeleton.inverseBindMatrices
471
- }
472
-
473
- getBoneWorldPosition(index: number): Vec3 {
474
- this.evaluatePose()
475
- const matIdx = index * 16
476
- const mat = this.runtimeSkeleton.worldMatrices
477
- return new Vec3(mat[matIdx + 12], mat[matIdx + 13], mat[matIdx + 14])
478
- }
479
-
480
- getBoneWorldRotation(index: number): Quat {
481
- this.evaluatePose()
482
- const start = index * 16
483
- return Mat4.toQuatFromArray(this.runtimeSkeleton.worldMatrices, start)
484
- }
485
-
486
- getBoneInverseBindMatrix(index: number): Mat4 | undefined {
487
- if (index < 0 || index >= this.skeleton.bones.length) return undefined
488
- const start = index * 16
489
- return new Mat4(this.skeleton.inverseBindMatrices.subarray(start, start + 16))
490
- }
491
-
492
- getLocalRotations(): Float32Array {
493
- return this.runtimeSkeleton.localRotations
494
- }
495
-
496
- evaluatePose(): void {
497
- this.updateRotationTweens()
498
- this.computeWorldMatrices()
499
- }
500
-
501
- private computeWorldMatrices(): void {
502
- const bones = this.skeleton.bones
503
- const localRot = this.runtimeSkeleton.localRotations
504
- const localTrans = this.runtimeSkeleton.localTranslations
505
- const worldBuf = this.runtimeSkeleton.worldMatrices
506
- const computed = this.runtimeSkeleton.computedBones.fill(false)
507
- const boneCount = bones.length
508
-
509
- if (boneCount === 0) return
510
-
511
- const computeWorld = (i: number): void => {
512
- if (computed[i]) return
513
-
514
- const b = bones[i]
515
- if (b.parentIndex >= boneCount) {
516
- console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
517
- }
518
-
519
- const qi = i * 4
520
- let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
521
- let addLocalTx = 0,
522
- addLocalTy = 0,
523
- addLocalTz = 0
524
-
525
- // Optimized append rotation check - only check necessary conditions
526
- const appendParentIdx = b.appendParentIndex
527
- const hasAppend =
528
- b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
529
-
530
- if (hasAppend) {
531
- const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
532
- const hasRatio = Math.abs(ratio) > 1e-6
533
-
534
- if (hasRatio) {
535
- const apQi = appendParentIdx * 4
536
- const apTi = appendParentIdx * 3
537
-
538
- if (b.appendRotate) {
539
- let ax = localRot[apQi]
540
- let ay = localRot[apQi + 1]
541
- let az = localRot[apQi + 2]
542
- const aw = localRot[apQi + 3]
543
- const absRatio = ratio < 0 ? -ratio : ratio
544
- if (ratio < 0) {
545
- ax = -ax
546
- ay = -ay
547
- az = -az
548
- }
549
- const [rx, ry, rz, rw] = Model.slerp(0, 0, 0, 1, ax, ay, az, aw, absRatio)
550
- rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
551
- }
552
-
553
- if (b.appendMove) {
554
- const appendRatio = b.appendRatio ?? 1
555
- addLocalTx = localTrans[apTi] * appendRatio
556
- addLocalTy = localTrans[apTi + 1] * appendRatio
557
- addLocalTz = localTrans[apTi + 2] * appendRatio
558
- }
559
- }
560
- }
561
-
562
- // Build local matrix: identity + bind translation, then rotation, then append translation
563
- this.cachedIdentityMat1
564
- .setIdentity()
565
- .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
566
- this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
567
- const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
568
-
569
- const worldOffset = i * 16
570
- if (b.parentIndex >= 0) {
571
- const p = b.parentIndex
572
- if (!computed[p]) computeWorld(p)
573
- const parentOffset = p * 16
574
- // Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
575
- Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
576
- worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
577
- } else {
578
- worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
579
- }
580
- computed[i] = true
581
- }
582
-
583
- // Process all bones (recursion handles dependencies automatically)
584
- for (let i = 0; i < boneCount; i++) computeWorld(i)
585
- }
586
- }
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
+ }
27
+
28
+ export interface Bone {
29
+ name: string
30
+ parentIndex: number // -1 if no parent
31
+ bindTranslation: [number, number, number]
32
+ children: number[] // child bone indices (built on skeleton creation)
33
+ appendParentIndex?: number // index of the bone to inherit from
34
+ appendRatio?: number // 0..1
35
+ appendRotate?: boolean
36
+ appendMove?: boolean
37
+ }
38
+
39
+ export interface Skeleton {
40
+ bones: Bone[]
41
+ inverseBindMatrices: Float32Array // One inverse-bind matrix per bone (column-major mat4, 16 floats per bone)
42
+ }
43
+
44
+ export interface Skinning {
45
+ joints: Uint16Array // length = vertexCount * 4, bone indices per vertex
46
+ weights: Uint8Array // UNORM8, length = vertexCount * 4, sums ~ 255 per-vertex
47
+ }
48
+
49
+ // Runtime skeleton pose state (updated each frame)
50
+ export interface SkeletonRuntime {
51
+ nameIndex: Record<string, number> // Cached lookup: bone name -> bone index (built on initialization)
52
+ localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
53
+ localTranslations: Float32Array // vec3 per bone length = boneCount*3
54
+ worldMatrices: Float32Array // mat4 per bone length = boneCount*16
55
+ computedBones: boolean[] // length = boneCount
56
+ }
57
+
58
+ // Rotation tween state per bone
59
+ interface RotationTweenState {
60
+ active: Uint8Array // 0/1 per bone
61
+ startQuat: Float32Array // quat per bone (x,y,z,w)
62
+ targetQuat: Float32Array // quat per bone (x,y,z,w)
63
+ startTimeMs: Float32Array // one float per bone (ms)
64
+ durationMs: Float32Array // one float per bone (ms)
65
+ }
66
+
67
+ export class Model {
68
+ private vertexData: Float32Array<ArrayBuffer>
69
+ private vertexCount: number
70
+ private indexData: Uint32Array<ArrayBuffer>
71
+ private textures: Texture[] = []
72
+ private materials: Material[] = []
73
+ // Static skeleton/skinning (not necessarily serialized yet)
74
+ private skeleton: Skeleton
75
+ private skinning: Skinning
76
+
77
+ // Physics data from PMX
78
+ private rigidbodies: Rigidbody[] = []
79
+ private joints: Joint[] = []
80
+
81
+ // Runtime skeleton pose state (updated each frame)
82
+ private runtimeSkeleton!: SkeletonRuntime
83
+
84
+ // Cached identity matrices to avoid allocations in computeWorldMatrices
85
+ private cachedIdentityMat1 = Mat4.identity()
86
+ private cachedIdentityMat2 = Mat4.identity()
87
+
88
+ private rotTweenState!: RotationTweenState
89
+
90
+ constructor(
91
+ vertexData: Float32Array<ArrayBuffer>,
92
+ indexData: Uint32Array<ArrayBuffer>,
93
+ textures: Texture[],
94
+ materials: Material[],
95
+ skeleton: Skeleton,
96
+ skinning: Skinning,
97
+ rigidbodies: Rigidbody[] = [],
98
+ joints: Joint[] = []
99
+ ) {
100
+ this.vertexData = vertexData
101
+ this.vertexCount = vertexData.length / VERTEX_STRIDE
102
+ this.indexData = indexData
103
+ this.textures = textures
104
+ this.materials = materials
105
+ this.skeleton = skeleton
106
+ this.skinning = skinning
107
+ this.rigidbodies = rigidbodies
108
+ this.joints = joints
109
+
110
+ if (this.skeleton.bones.length == 0) {
111
+ throw new Error("Model has no bones")
112
+ }
113
+
114
+ this.initializeRuntimeSkeleton()
115
+ this.initializeRotTweenBuffers()
116
+ }
117
+
118
+ private initializeRuntimeSkeleton(): void {
119
+ const boneCount = this.skeleton.bones.length
120
+
121
+ this.runtimeSkeleton = {
122
+ localRotations: new Float32Array(boneCount * 4),
123
+ localTranslations: new Float32Array(boneCount * 3),
124
+ worldMatrices: new Float32Array(boneCount * 16),
125
+ nameIndex: this.skeleton.bones.reduce((acc, bone, index) => {
126
+ acc[bone.name] = index
127
+ return acc
128
+ }, {} as Record<string, number>),
129
+ computedBones: new Array(boneCount).fill(false),
130
+ }
131
+
132
+ const rotations = this.runtimeSkeleton.localRotations
133
+ for (let i = 0; i < this.skeleton.bones.length; i++) {
134
+ const qi = i * 4
135
+ if (rotations[qi + 3] === 0) {
136
+ rotations[qi] = 0
137
+ rotations[qi + 1] = 0
138
+ rotations[qi + 2] = 0
139
+ rotations[qi + 3] = 1
140
+ }
141
+ }
142
+ }
143
+
144
+ private initializeRotTweenBuffers(): void {
145
+ const n = this.skeleton.bones.length
146
+ this.rotTweenState = {
147
+ active: new Uint8Array(n),
148
+ startQuat: new Float32Array(n * 4),
149
+ targetQuat: new Float32Array(n * 4),
150
+ startTimeMs: new Float32Array(n),
151
+ durationMs: new Float32Array(n),
152
+ }
153
+ }
154
+
155
+ private updateRotationTweens(): void {
156
+ const state = this.rotTweenState
157
+ const now = performance.now()
158
+ const rotations = this.runtimeSkeleton.localRotations
159
+ const boneCount = this.skeleton.bones.length
160
+
161
+ for (let i = 0; i < boneCount; i++) {
162
+ if (state.active[i] !== 1) continue
163
+
164
+ const startMs = state.startTimeMs[i]
165
+ const durMs = Math.max(1, state.durationMs[i])
166
+ const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
167
+ const e = easeInOut(t)
168
+
169
+ const qi = i * 4
170
+ const startQuat = new Quat(
171
+ state.startQuat[qi],
172
+ state.startQuat[qi + 1],
173
+ state.startQuat[qi + 2],
174
+ state.startQuat[qi + 3]
175
+ )
176
+ const targetQuat = new Quat(
177
+ state.targetQuat[qi],
178
+ state.targetQuat[qi + 1],
179
+ state.targetQuat[qi + 2],
180
+ state.targetQuat[qi + 3]
181
+ )
182
+ const result = Quat.slerp(startQuat, targetQuat, e)
183
+
184
+ rotations[qi] = result.x
185
+ rotations[qi + 1] = result.y
186
+ rotations[qi + 2] = result.z
187
+ rotations[qi + 3] = result.w
188
+
189
+ if (t >= 1) state.active[i] = 0
190
+ }
191
+ }
192
+
193
+ // Get interleaved vertex data for GPU upload
194
+ // Format: [x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v, ...]
195
+ getVertices(): Float32Array<ArrayBuffer> {
196
+ return this.vertexData
197
+ }
198
+
199
+ // Get texture information
200
+ getTextures(): Texture[] {
201
+ return this.textures
202
+ }
203
+
204
+ // Get material information
205
+ getMaterials(): Material[] {
206
+ return this.materials
207
+ }
208
+
209
+ // Get vertex count
210
+ getVertexCount(): number {
211
+ return this.vertexCount
212
+ }
213
+
214
+ // Get index data for GPU upload
215
+ getIndices(): Uint32Array<ArrayBuffer> {
216
+ return this.indexData
217
+ }
218
+
219
+ // Accessors for skeleton/skinning
220
+ getSkeleton(): Skeleton {
221
+ return this.skeleton
222
+ }
223
+
224
+ getSkinning(): Skinning {
225
+ return this.skinning
226
+ }
227
+
228
+ // Accessors for physics data
229
+ getRigidbodies(): Rigidbody[] {
230
+ return this.rigidbodies
231
+ }
232
+
233
+ getJoints(): Joint[] {
234
+ return this.joints
235
+ }
236
+
237
+ // ------- Bone helpers (public API) -------
238
+
239
+ getBoneNames(): string[] {
240
+ return this.skeleton.bones.map((b) => b.name)
241
+ }
242
+
243
+ rotateBones(names: string[], quats: Quat[], durationMs?: number): void {
244
+ const state = this.rotTweenState
245
+ const normalized = quats.map((q) => q.normalize())
246
+ const now = performance.now()
247
+ const dur = durationMs && durationMs > 0 ? durationMs : 0
248
+
249
+ for (let i = 0; i < names.length; i++) {
250
+ const name = names[i]
251
+ const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
252
+ if (idx < 0 || idx >= this.skeleton.bones.length) continue
253
+
254
+ const qi = idx * 4
255
+ const rotations = this.runtimeSkeleton.localRotations
256
+ const [tx, ty, tz, tw] = normalized[i].toArray()
257
+
258
+ if (dur === 0) {
259
+ rotations[qi] = tx
260
+ rotations[qi + 1] = ty
261
+ rotations[qi + 2] = tz
262
+ rotations[qi + 3] = tw
263
+ state.active[idx] = 0
264
+ continue
265
+ }
266
+
267
+ let sx = rotations[qi]
268
+ let sy = rotations[qi + 1]
269
+ let sz = rotations[qi + 2]
270
+ let sw = rotations[qi + 3]
271
+
272
+ if (state.active[idx] === 1) {
273
+ const startMs = state.startTimeMs[idx]
274
+ const prevDur = Math.max(1, state.durationMs[idx])
275
+ const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
276
+ const e = easeInOut(t)
277
+ const startQuat = new Quat(
278
+ state.startQuat[qi],
279
+ state.startQuat[qi + 1],
280
+ state.startQuat[qi + 2],
281
+ state.startQuat[qi + 3]
282
+ )
283
+ const targetQuat = new Quat(
284
+ state.targetQuat[qi],
285
+ state.targetQuat[qi + 1],
286
+ state.targetQuat[qi + 2],
287
+ state.targetQuat[qi + 3]
288
+ )
289
+ const result = Quat.slerp(startQuat, targetQuat, e)
290
+ const cx = result.x
291
+ const cy = result.y
292
+ const cz = result.z
293
+ const cw = result.w
294
+ sx = cx
295
+ sy = cy
296
+ sz = cz
297
+ sw = cw
298
+ }
299
+
300
+ state.startQuat[qi] = sx
301
+ state.startQuat[qi + 1] = sy
302
+ state.startQuat[qi + 2] = sz
303
+ state.startQuat[qi + 3] = sw
304
+ state.targetQuat[qi] = tx
305
+ state.targetQuat[qi + 1] = ty
306
+ state.targetQuat[qi + 2] = tz
307
+ state.targetQuat[qi + 3] = tw
308
+ state.startTimeMs[idx] = now
309
+ state.durationMs[idx] = dur
310
+ state.active[idx] = 1
311
+ }
312
+ }
313
+
314
+ getBoneWorldMatrices(): Float32Array {
315
+ return this.runtimeSkeleton.worldMatrices
316
+ }
317
+
318
+ getBoneInverseBindMatrices(): Float32Array {
319
+ return this.skeleton.inverseBindMatrices
320
+ }
321
+
322
+ evaluatePose(): void {
323
+ this.updateRotationTweens()
324
+ this.computeWorldMatrices()
325
+ }
326
+
327
+ private computeWorldMatrices(): void {
328
+ const bones = this.skeleton.bones
329
+ const localRot = this.runtimeSkeleton.localRotations
330
+ const localTrans = this.runtimeSkeleton.localTranslations
331
+ const worldBuf = this.runtimeSkeleton.worldMatrices
332
+ const computed = this.runtimeSkeleton.computedBones.fill(false)
333
+ const boneCount = bones.length
334
+
335
+ if (boneCount === 0) return
336
+
337
+ const computeWorld = (i: number): void => {
338
+ if (computed[i]) return
339
+
340
+ const b = bones[i]
341
+ if (b.parentIndex >= boneCount) {
342
+ console.warn(`[RZM] bone ${i} parent out of range: ${b.parentIndex}`)
343
+ }
344
+
345
+ const qi = i * 4
346
+ let rotateM = Mat4.fromQuat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
347
+ let addLocalTx = 0,
348
+ addLocalTy = 0,
349
+ addLocalTz = 0
350
+
351
+ // Optimized append rotation check - only check necessary conditions
352
+ const appendParentIdx = b.appendParentIndex
353
+ const hasAppend =
354
+ b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
355
+
356
+ if (hasAppend) {
357
+ const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
358
+ const hasRatio = Math.abs(ratio) > 1e-6
359
+
360
+ if (hasRatio) {
361
+ const apQi = appendParentIdx * 4
362
+ const apTi = appendParentIdx * 3
363
+
364
+ if (b.appendRotate) {
365
+ let ax = localRot[apQi]
366
+ let ay = localRot[apQi + 1]
367
+ let az = localRot[apQi + 2]
368
+ const aw = localRot[apQi + 3]
369
+ const absRatio = ratio < 0 ? -ratio : ratio
370
+ if (ratio < 0) {
371
+ ax = -ax
372
+ ay = -ay
373
+ az = -az
374
+ }
375
+ const identityQuat = new Quat(0, 0, 0, 1)
376
+ const appendQuat = new Quat(ax, ay, az, aw)
377
+ const result = Quat.slerp(identityQuat, appendQuat, absRatio)
378
+ const rx = result.x
379
+ const ry = result.y
380
+ const rz = result.z
381
+ const rw = result.w
382
+ rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
383
+ }
384
+
385
+ if (b.appendMove) {
386
+ const appendRatio = b.appendRatio ?? 1
387
+ addLocalTx = localTrans[apTi] * appendRatio
388
+ addLocalTy = localTrans[apTi + 1] * appendRatio
389
+ addLocalTz = localTrans[apTi + 2] * appendRatio
390
+ }
391
+ }
392
+ }
393
+
394
+ // Build local matrix: identity + bind translation, then rotation, then append translation
395
+ this.cachedIdentityMat1
396
+ .setIdentity()
397
+ .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
398
+ this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
399
+ const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
400
+
401
+ const worldOffset = i * 16
402
+ if (b.parentIndex >= 0) {
403
+ const p = b.parentIndex
404
+ if (!computed[p]) computeWorld(p)
405
+ const parentOffset = p * 16
406
+ // Use cachedIdentityMat2 as temporary buffer for parent * local multiplication
407
+ Mat4.multiplyArrays(worldBuf, parentOffset, localM.values, 0, this.cachedIdentityMat2.values, 0)
408
+ worldBuf.subarray(worldOffset, worldOffset + 16).set(this.cachedIdentityMat2.values)
409
+ } else {
410
+ worldBuf.subarray(worldOffset, worldOffset + 16).set(localM.values)
411
+ }
412
+ computed[i] = true
413
+ }
414
+
415
+ // Process all bones (recursion handles dependencies automatically)
416
+ for (let i = 0; i < boneCount; i++) computeWorld(i)
417
+ }
418
+ }