reze-engine 0.1.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/src/model.ts ADDED
@@ -0,0 +1,586 @@
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
+ }