reze-engine 0.2.19 → 0.3.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 CHANGED
@@ -1,5 +1,6 @@
1
- import { Mat4, Quat, easeInOut } from "./math"
1
+ import { Mat4, Quat, Vec3, easeInOut } from "./math"
2
2
  import { Rigidbody, Joint } from "./physics"
3
+ import { IKSolverSystem } from "./ik-solver"
3
4
 
4
5
  const VERTEX_STRIDE = 8
5
6
 
@@ -37,6 +38,53 @@ export interface Bone {
37
38
  appendRatio?: number // 0..1
38
39
  appendRotate?: boolean
39
40
  appendMove?: boolean
41
+ ikTargetIndex?: number // IK target bone index (if this bone is an IK effector)
42
+ ikIteration?: number // IK iteration count
43
+ ikLimitAngle?: number // IK rotation constraint (radians)
44
+ ikLinks?: IKLink[] // IK chain links
45
+ }
46
+
47
+ // IK link with angle constraints
48
+ export interface IKLink {
49
+ boneIndex: number
50
+ hasLimit: boolean
51
+ minAngle?: Vec3 // Minimum Euler angles (radians)
52
+ maxAngle?: Vec3 // Maximum Euler angles (radians)
53
+ rotationOrder?: EulerRotationOrder // YXZ, ZYX, or XZY
54
+ solveAxis?: SolveAxis // None, Fixed, X, Y, or Z
55
+ }
56
+
57
+ // Euler rotation order for angle constraints
58
+ export enum EulerRotationOrder {
59
+ YXZ = 0,
60
+ ZYX = 1,
61
+ XZY = 2,
62
+ }
63
+
64
+ // Solve axis optimization
65
+ export enum SolveAxis {
66
+ None = 0,
67
+ Fixed = 1,
68
+ X = 2,
69
+ Y = 3,
70
+ Z = 4,
71
+ }
72
+
73
+ // IK solver definition
74
+ export interface IKSolver {
75
+ index: number
76
+ ikBoneIndex: number // Effector bone (the bone that should reach the target)
77
+ targetBoneIndex: number // Target bone
78
+ iterationCount: number
79
+ limitAngle: number // Max rotation per iteration (radians)
80
+ links: IKLink[] // Chain bones from effector to root
81
+ canSkipWhenPhysicsEnabled: boolean
82
+ }
83
+
84
+ // IK chain info per bone (runtime state)
85
+ export interface IKChainInfo {
86
+ ikRotation: Quat // Accumulated IK rotation
87
+ localRotation: Quat // Cached local rotation before IK
40
88
  }
41
89
 
42
90
  export interface Skeleton {
@@ -81,6 +129,8 @@ export interface SkeletonRuntime {
81
129
  localTranslations: Float32Array // vec3 per bone length = boneCount*3
82
130
  worldMatrices: Float32Array // mat4 per bone length = boneCount*16
83
131
  computedBones: boolean[] // length = boneCount
132
+ ikChainInfo?: IKChainInfo[] // IK chain info per bone (only for IK chain bones)
133
+ ikSolvers?: IKSolver[] // All IK solvers in the model
84
134
  }
85
135
 
86
136
  // Runtime morph state
@@ -107,6 +157,15 @@ interface MorphWeightTweenState {
107
157
  durationMs: Float32Array // one float per morph (ms)
108
158
  }
109
159
 
160
+ // Translation tween state per bone
161
+ interface TranslationTweenState {
162
+ active: Uint8Array // 0/1 per bone
163
+ startVec: Float32Array // vec3 per bone (x,y,z)
164
+ targetVec: Float32Array // vec3 per bone (x,y,z)
165
+ startTimeMs: Float32Array // one float per bone (ms)
166
+ durationMs: Float32Array // one float per bone (ms)
167
+ }
168
+
110
169
  export class Model {
111
170
  private vertexData: Float32Array<ArrayBuffer>
112
171
  private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
@@ -136,6 +195,7 @@ export class Model {
136
195
  private cachedIdentityMat2 = Mat4.identity()
137
196
 
138
197
  private rotTweenState!: RotationTweenState
198
+ private transTweenState!: TranslationTweenState
139
199
  private morphTweenState!: MorphWeightTweenState
140
200
 
141
201
  constructor(
@@ -168,6 +228,7 @@ export class Model {
168
228
 
169
229
  this.initializeRuntimeSkeleton()
170
230
  this.initializeRotTweenBuffers()
231
+ this.initializeTransTweenBuffers()
171
232
  this.initializeRuntimeMorph()
172
233
  this.initializeMorphTweenBuffers()
173
234
  this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
@@ -197,6 +258,58 @@ export class Model {
197
258
  rotations[qi + 3] = 1
198
259
  }
199
260
  }
261
+
262
+ // Initialize IK runtime state
263
+ this.initializeIKRuntime()
264
+ }
265
+
266
+ private initializeIKRuntime(): void {
267
+ const boneCount = this.skeleton.bones.length
268
+ const bones = this.skeleton.bones
269
+
270
+ // Initialize IK chain info for all bones (will be populated for IK chain bones)
271
+ const ikChainInfo: IKChainInfo[] = new Array(boneCount)
272
+ for (let i = 0; i < boneCount; i++) {
273
+ ikChainInfo[i] = {
274
+ ikRotation: new Quat(0, 0, 0, 1),
275
+ localRotation: new Quat(0, 0, 0, 1),
276
+ }
277
+ }
278
+
279
+ // Build IK solvers from bone data
280
+ const ikSolvers: IKSolver[] = []
281
+ let solverIndex = 0
282
+
283
+ for (let i = 0; i < boneCount; i++) {
284
+ const bone = bones[i]
285
+ if (bone.ikTargetIndex !== undefined && bone.ikLinks && bone.ikLinks.length > 0) {
286
+ // Check if all links are affected by physics (for optimization)
287
+ let canSkipWhenPhysicsEnabled = true
288
+ for (const link of bone.ikLinks) {
289
+ // For now, assume no bones are physics-controlled (can be enhanced later)
290
+ // If a bone has a rigidbody attached, it's physics-controlled
291
+ const hasPhysics = this.rigidbodies.some((rb) => rb.boneIndex === link.boneIndex)
292
+ if (!hasPhysics) {
293
+ canSkipWhenPhysicsEnabled = false
294
+ break
295
+ }
296
+ }
297
+
298
+ const solver: IKSolver = {
299
+ index: solverIndex++,
300
+ ikBoneIndex: i,
301
+ targetBoneIndex: bone.ikTargetIndex,
302
+ iterationCount: bone.ikIteration ?? 1,
303
+ limitAngle: bone.ikLimitAngle ?? Math.PI,
304
+ links: bone.ikLinks,
305
+ canSkipWhenPhysicsEnabled,
306
+ }
307
+ ikSolvers.push(solver)
308
+ }
309
+ }
310
+
311
+ this.runtimeSkeleton.ikChainInfo = ikChainInfo
312
+ this.runtimeSkeleton.ikSolvers = ikSolvers
200
313
  }
201
314
 
202
315
  private initializeRotTweenBuffers(): void {
@@ -210,6 +323,17 @@ export class Model {
210
323
  }
211
324
  }
212
325
 
326
+ private initializeTransTweenBuffers(): void {
327
+ const n = this.skeleton.bones.length
328
+ this.transTweenState = {
329
+ active: new Uint8Array(n),
330
+ startVec: new Float32Array(n * 3),
331
+ targetVec: new Float32Array(n * 3),
332
+ startTimeMs: new Float32Array(n),
333
+ durationMs: new Float32Array(n),
334
+ }
335
+ }
336
+
213
337
  private initializeMorphTweenBuffers(): void {
214
338
  const n = this.morphing.morphs.length
215
339
  this.morphTweenState = {
@@ -270,6 +394,29 @@ export class Model {
270
394
  }
271
395
  }
272
396
 
397
+ private updateTranslationTweens(): void {
398
+ const state = this.transTweenState
399
+ const now = performance.now()
400
+ const translations = this.runtimeSkeleton.localTranslations
401
+ const boneCount = this.skeleton.bones.length
402
+
403
+ for (let i = 0; i < boneCount; i++) {
404
+ if (state.active[i] !== 1) continue
405
+
406
+ const startMs = state.startTimeMs[i]
407
+ const durMs = Math.max(1, state.durationMs[i])
408
+ const t = Math.max(0, Math.min(1, (now - startMs) / durMs))
409
+ const e = easeInOut(t)
410
+
411
+ const ti = i * 3
412
+ translations[ti] = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e
413
+ translations[ti + 1] = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e
414
+ translations[ti + 2] = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e
415
+
416
+ if (t >= 1) state.active[i] = 0
417
+ }
418
+ }
419
+
273
420
  private updateMorphWeightTweens(): boolean {
274
421
  const state = this.morphTweenState
275
422
  const now = performance.now()
@@ -427,6 +574,108 @@ export class Model {
427
574
  }
428
575
  }
429
576
 
577
+ // Move bones using VMD-style relative translations (relative to bind pose world position)
578
+ // This is the default behavior for VMD animations
579
+ moveBones(names: string[], relativeTranslations: Vec3[], durationMs?: number): void {
580
+ const state = this.transTweenState
581
+ const now = performance.now()
582
+ const dur = durationMs && durationMs > 0 ? durationMs : 0
583
+ const localRot = this.runtimeSkeleton.localRotations
584
+
585
+ // Compute bind pose world positions for all bones
586
+ const skeleton = this.skeleton
587
+ const computeBindPoseWorldPosition = (idx: number): Vec3 => {
588
+ const bone = skeleton.bones[idx]
589
+ const bindPos = new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
590
+ if (bone.parentIndex >= 0 && bone.parentIndex < skeleton.bones.length) {
591
+ const parentWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
592
+ return parentWorldPos.add(bindPos)
593
+ } else {
594
+ return bindPos
595
+ }
596
+ }
597
+
598
+ for (let i = 0; i < names.length; i++) {
599
+ const name = names[i]
600
+ const idx = this.runtimeSkeleton.nameIndex[name] ?? -1
601
+ if (idx < 0 || idx >= this.skeleton.bones.length) continue
602
+
603
+ const bone = this.skeleton.bones[idx]
604
+ const ti = idx * 3
605
+ const qi = idx * 4
606
+ const translations = this.runtimeSkeleton.localTranslations
607
+ const vmdRelativeTranslation = relativeTranslations[i]
608
+
609
+ // VMD translation is relative to bind pose world position
610
+ // targetWorldPos = bindPoseWorldPos + vmdRelativeTranslation
611
+ const bindPoseWorldPos = computeBindPoseWorldPosition(idx)
612
+ const targetWorldPos = bindPoseWorldPos.add(vmdRelativeTranslation)
613
+
614
+ // Convert target world position to local translation
615
+ // We need parent's bind pose world position to transform to parent space
616
+ let parentBindPoseWorldPos: Vec3
617
+ if (bone.parentIndex >= 0) {
618
+ parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex)
619
+ } else {
620
+ parentBindPoseWorldPos = new Vec3(0, 0, 0)
621
+ }
622
+
623
+ // Transform target world position to parent's local space
624
+ // In bind pose, parent's world matrix is just a translation
625
+ const parentSpacePos = targetWorldPos.subtract(parentBindPoseWorldPos)
626
+
627
+ // Subtract bindTranslation to get position after bind translation
628
+ const afterBindTranslation = parentSpacePos.subtract(
629
+ new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
630
+ )
631
+
632
+ // Apply inverse rotation to get local translation
633
+ const localRotation = new Quat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3])
634
+ const invRotation = localRotation.conjugate().normalize()
635
+ const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w)
636
+ const rm = rotationMat.values
637
+ const localTranslation = new Vec3(
638
+ rm[0] * afterBindTranslation.x + rm[4] * afterBindTranslation.y + rm[8] * afterBindTranslation.z,
639
+ rm[1] * afterBindTranslation.x + rm[5] * afterBindTranslation.y + rm[9] * afterBindTranslation.z,
640
+ rm[2] * afterBindTranslation.x + rm[6] * afterBindTranslation.y + rm[10] * afterBindTranslation.z
641
+ )
642
+
643
+ const [tx, ty, tz] = [localTranslation.x, localTranslation.y, localTranslation.z]
644
+
645
+ if (dur === 0) {
646
+ translations[ti] = tx
647
+ translations[ti + 1] = ty
648
+ translations[ti + 2] = tz
649
+ state.active[idx] = 0
650
+ continue
651
+ }
652
+
653
+ let sx = translations[ti]
654
+ let sy = translations[ti + 1]
655
+ let sz = translations[ti + 2]
656
+
657
+ if (state.active[idx] === 1) {
658
+ const startMs = state.startTimeMs[idx]
659
+ const prevDur = Math.max(1, state.durationMs[idx])
660
+ const t = Math.max(0, Math.min(1, (now - startMs) / prevDur))
661
+ const e = easeInOut(t)
662
+ sx = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e
663
+ sy = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e
664
+ sz = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e
665
+ }
666
+
667
+ state.startVec[ti] = sx
668
+ state.startVec[ti + 1] = sy
669
+ state.startVec[ti + 2] = sz
670
+ state.targetVec[ti] = tx
671
+ state.targetVec[ti + 1] = ty
672
+ state.targetVec[ti + 2] = tz
673
+ state.startTimeMs[idx] = now
674
+ state.durationMs[idx] = dur
675
+ state.active[idx] = 1
676
+ }
677
+ }
678
+
430
679
  getBoneWorldMatrices(): Float32Array {
431
680
  return this.runtimeSkeleton.worldMatrices
432
681
  }
@@ -547,14 +796,42 @@ export class Model {
547
796
 
548
797
  evaluatePose(): boolean {
549
798
  this.updateRotationTweens()
799
+ this.updateTranslationTweens()
550
800
  const hasActiveMorphTweens = this.updateMorphWeightTweens()
551
801
  if (hasActiveMorphTweens) {
552
802
  this.applyMorphs()
553
803
  }
804
+
805
+ // Compute initial world matrices (needed for IK solving)
554
806
  this.computeWorldMatrices()
807
+
808
+ // Solve IK chains (modifies localRotations)
809
+ this.solveIKChains()
810
+
811
+ // Recompute world matrices with IK rotations applied
812
+ this.computeWorldMatrices()
813
+
555
814
  return hasActiveMorphTweens
556
815
  }
557
816
 
817
+ private solveIKChains(): void {
818
+ const ikSolvers = this.runtimeSkeleton.ikSolvers
819
+ if (!ikSolvers || ikSolvers.length === 0) return
820
+
821
+ const ikChainInfo = this.runtimeSkeleton.ikChainInfo
822
+ if (!ikChainInfo) return
823
+
824
+ IKSolverSystem.solve(
825
+ ikSolvers,
826
+ this.skeleton.bones,
827
+ this.runtimeSkeleton.localRotations,
828
+ this.runtimeSkeleton.localTranslations,
829
+ this.runtimeSkeleton.worldMatrices,
830
+ ikChainInfo,
831
+ false // usePhysics - can be enhanced later
832
+ )
833
+ }
834
+
558
835
  private computeWorldMatrices(): void {
559
836
  const bones = this.skeleton.bones
560
837
  const localRot = this.runtimeSkeleton.localRotations
@@ -622,11 +899,15 @@ export class Model {
622
899
  }
623
900
  }
624
901
 
625
- // Build local matrix: identity + bind translation, then rotation, then append translation
902
+ // Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
903
+ const ti = i * 3
904
+ const localTx = localTrans[ti] + addLocalTx
905
+ const localTy = localTrans[ti + 1] + addLocalTy
906
+ const localTz = localTrans[ti + 2] + addLocalTz
626
907
  this.cachedIdentityMat1
627
908
  .setIdentity()
628
909
  .translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2])
629
- this.cachedIdentityMat2.setIdentity().translateInPlace(addLocalTx, addLocalTy, addLocalTz)
910
+ this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz)
630
911
  const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2)
631
912
 
632
913
  const worldOffset = i * 16