reze-engine 0.1.5 → 0.1.6

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,418 +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
- }
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
- }
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
+ }