reze-engine 0.3.3 → 0.3.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/dist/player.js CHANGED
@@ -1,5 +1,4 @@
1
- import { bezierInterpolate } from "./bezier-interpolate";
2
- import { Quat, Vec3 } from "./math";
1
+ import { Quat, Vec3, bezierInterpolate } from "./math";
3
2
  import { VMDLoader } from "./vmd-loader";
4
3
  export class Player {
5
4
  constructor() {
@@ -16,60 +15,14 @@ export class Player {
16
15
  this.startTime = 0; // Real-time when playback started
17
16
  this.pausedTime = 0; // Accumulated paused duration
18
17
  this.pauseStartTime = 0;
19
- this.audioLoaded = false;
20
18
  }
21
19
  /**
22
- * Load VMD animation file and optionally audio
20
+ * Load VMD animation file
23
21
  */
24
- async loadVmd(vmdUrl, audioUrl) {
22
+ async loadVmd(vmdUrl) {
25
23
  // Load animation
26
24
  this.frames = await VMDLoader.load(vmdUrl);
27
25
  this.processFrames();
28
- // Load audio if provided
29
- if (audioUrl) {
30
- await this.loadAudio(audioUrl);
31
- }
32
- }
33
- /**
34
- * Load audio file
35
- */
36
- async loadAudio(url) {
37
- this.audioUrl = url;
38
- this.audioLoaded = false;
39
- return new Promise((resolve, reject) => {
40
- const audio = new Audio(url);
41
- audio.preload = "auto";
42
- // iOS Safari requires playsinline attribute for inline playback
43
- // This must be set before loading
44
- audio.setAttribute("playsinline", "true");
45
- audio.setAttribute("webkit-playsinline", "true");
46
- // Set volume to ensure audio is ready
47
- audio.volume = 1.0;
48
- // iOS sometimes requires audio element to be in DOM
49
- // Add it hidden to the document body
50
- audio.style.display = "none";
51
- audio.style.position = "absolute";
52
- audio.style.visibility = "hidden";
53
- audio.style.width = "0";
54
- audio.style.height = "0";
55
- document.body.appendChild(audio);
56
- audio.addEventListener("loadeddata", () => {
57
- this.audioElement = audio;
58
- this.audioLoaded = true;
59
- resolve();
60
- });
61
- audio.addEventListener("error", (e) => {
62
- console.warn("Failed to load audio:", url, e);
63
- this.audioLoaded = false;
64
- // Remove from DOM on error
65
- if (audio.parentNode) {
66
- audio.parentNode.removeChild(audio);
67
- }
68
- // Don't reject - animation should still work without audio
69
- resolve();
70
- });
71
- audio.load();
72
- });
73
26
  }
74
27
  /**
75
28
  * Process frames into tracks
@@ -140,6 +93,7 @@ export class Player {
140
93
  }
141
94
  /**
142
95
  * Start or resume playback
96
+ * Note: For iOS, this should be called synchronously from a user interaction event
143
97
  */
144
98
  play() {
145
99
  if (this.frames.length === 0)
@@ -156,22 +110,6 @@ export class Player {
156
110
  this.pausedTime = 0;
157
111
  }
158
112
  this.isPlaying = true;
159
- // Play audio if available
160
- if (this.audioElement && this.audioLoaded) {
161
- // Ensure audio is ready for iOS
162
- this.audioElement.currentTime = this.currentTime;
163
- this.audioElement.muted = false;
164
- this.audioElement.volume = 1.0;
165
- // iOS requires play() to be called synchronously during user interaction
166
- // This must happen directly from the user's click/touch event
167
- const playPromise = this.audioElement.play();
168
- if (playPromise !== undefined) {
169
- playPromise.catch((error) => {
170
- // Log error but don't block animation playback
171
- console.warn("Audio play failed:", error, error.name);
172
- });
173
- }
174
- }
175
113
  }
176
114
  /**
177
115
  * Pause playback
@@ -181,10 +119,6 @@ export class Player {
181
119
  return;
182
120
  this.isPaused = true;
183
121
  this.pauseStartTime = performance.now();
184
- // Pause audio if available
185
- if (this.audioElement) {
186
- this.audioElement.pause();
187
- }
188
122
  }
189
123
  /**
190
124
  * Stop playback and reset to beginning
@@ -195,11 +129,6 @@ export class Player {
195
129
  this.currentTime = 0;
196
130
  this.startTime = 0;
197
131
  this.pausedTime = 0;
198
- // Stop audio if available
199
- if (this.audioElement) {
200
- this.audioElement.pause();
201
- this.audioElement.currentTime = 0;
202
- }
203
132
  }
204
133
  /**
205
134
  * Seek to specific time
@@ -212,10 +141,6 @@ export class Player {
212
141
  this.startTime = performance.now() - clampedTime * 1000;
213
142
  this.pausedTime = 0;
214
143
  }
215
- // Seek audio if available
216
- if (this.audioElement && this.audioLoaded) {
217
- this.audioElement.currentTime = clampedTime;
218
- }
219
144
  }
220
145
  /**
221
146
  * Update playback and return current pose
@@ -238,14 +163,6 @@ export class Player {
238
163
  this.pause(); // Auto-pause at end
239
164
  return this.getPoseAtTime(this.currentTime);
240
165
  }
241
- // Sync audio if present (with tolerance)
242
- if (this.audioElement && this.audioLoaded) {
243
- const audioTime = this.audioElement.currentTime;
244
- const syncTolerance = 0.1; // 100ms tolerance
245
- if (Math.abs(audioTime - this.currentTime) > syncTolerance) {
246
- this.audioElement.currentTime = this.currentTime;
247
- }
248
- }
249
166
  return this.getPoseAtTime(this.currentTime);
250
167
  }
251
168
  /**
@@ -403,34 +320,4 @@ export class Player {
403
320
  isPausedState() {
404
321
  return this.isPaused;
405
322
  }
406
- /**
407
- * Check if has audio
408
- */
409
- hasAudio() {
410
- return this.audioElement !== undefined && this.audioLoaded;
411
- }
412
- /**
413
- * Set audio volume (0.0 to 1.0)
414
- */
415
- setVolume(volume) {
416
- if (this.audioElement) {
417
- this.audioElement.volume = Math.max(0, Math.min(1, volume));
418
- }
419
- }
420
- /**
421
- * Mute audio
422
- */
423
- mute() {
424
- if (this.audioElement) {
425
- this.audioElement.muted = true;
426
- }
427
- }
428
- /**
429
- * Unmute audio
430
- */
431
- unmute() {
432
- if (this.audioElement) {
433
- this.audioElement.muted = false;
434
- }
435
- }
436
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -29,7 +29,3 @@ export async function loadAmmo(): Promise<AmmoInstance> {
29
29
 
30
30
  return ammoPromise
31
31
  }
32
-
33
- export function getAmmoInstance(): AmmoInstance | null {
34
- return ammoInstance
35
- }
package/src/engine.ts CHANGED
@@ -1267,8 +1267,8 @@ export class Engine {
1267
1267
  this.lightData[3] = 0.0 // Padding for vec3f alignment
1268
1268
  }
1269
1269
 
1270
- public async loadAnimation(url: string, audioUrl?: string) {
1271
- await this.player.loadVmd(url, audioUrl)
1270
+ public async loadAnimation(url: string) {
1271
+ await this.player.loadVmd(url)
1272
1272
  this.hasAnimation = true
1273
1273
 
1274
1274
  // Show first frame (time 0) immediately
package/src/ik-solver.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Mat4, Quat, Vec3 } from "./math"
8
- import { Bone, IKLink, IKSolver, IKChainInfo, EulerRotationOrder, SolveAxis } from "./model"
8
+ import { Bone, IKLink, IKSolver, IKChainInfo } from "./model"
9
9
 
10
10
  const enum InternalEulerRotationOrder {
11
11
  YXZ = 0,
@@ -89,13 +89,9 @@ export class IKSolverSystem {
89
89
  localRotations: Float32Array,
90
90
  localTranslations: Float32Array,
91
91
  worldMatrices: Float32Array,
92
- ikChainInfo: IKChainInfo[],
93
- usePhysics: boolean = false
92
+ ikChainInfo: IKChainInfo[]
94
93
  ): void {
95
94
  for (const solver of ikSolvers) {
96
- if (usePhysics && solver.canSkipWhenPhysicsEnabled) {
97
- continue
98
- }
99
95
  this.solveIK(solver, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
100
96
  }
101
97
  }
@@ -135,9 +131,9 @@ export class IKSolverSystem {
135
131
 
136
132
  // Update chain bones and target bone world matrices (initial state, no IK yet)
137
133
  for (let i = chains.length - 1; i >= 0; i--) {
138
- this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices)
134
+ this.updateWorldMatrix(chains[i].boneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
139
135
  }
140
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
136
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
141
137
 
142
138
  // Re-read positions after initial update
143
139
  const updatedIkPosition = this.getWorldTranslation(ikBoneIndex, worldMatrices)
@@ -355,9 +351,9 @@ export class IKSolverSystem {
355
351
  // Update world matrices for affected bones (using IK-modified rotations)
356
352
  for (let i = chainIndex; i >= 0; i--) {
357
353
  const link = solver.links[i]
358
- this.updateWorldMatrixWithIK(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
354
+ this.updateWorldMatrix(link.boneIndex, bones, localRotations, localTranslations, worldMatrices, ikChainInfo)
359
355
  }
360
- this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices)
356
+ this.updateWorldMatrix(targetBoneIndex, bones, localRotations, localTranslations, worldMatrices, undefined)
361
357
  }
362
358
 
363
359
  private static limitAngle(angle: number, min: number, max: number, useAxis: boolean): number {
@@ -401,29 +397,33 @@ export class IKSolverSystem {
401
397
  )
402
398
  }
403
399
 
404
- private static updateWorldMatrixWithIK(
400
+ private static updateWorldMatrix(
405
401
  boneIndex: number,
406
402
  bones: Bone[],
407
403
  localRotations: Float32Array,
408
404
  localTranslations: Float32Array,
409
405
  worldMatrices: Float32Array,
410
- ikChainInfo: IKChainInfo[]
406
+ ikChainInfo?: IKChainInfo[]
411
407
  ): void {
412
408
  const bone = bones[boneIndex]
413
409
  const qi = boneIndex * 4
414
410
  const ti = boneIndex * 3
415
411
 
416
- // Use IK-modified rotation if available
412
+ // Get local rotation
417
413
  const localRot = new Quat(
418
414
  localRotations[qi],
419
415
  localRotations[qi + 1],
420
416
  localRotations[qi + 2],
421
417
  localRotations[qi + 3]
422
418
  )
423
- const chainInfo = ikChainInfo[boneIndex]
419
+
420
+ // Apply IK rotation if available
424
421
  let finalRot = localRot
425
- if (chainInfo && chainInfo.ikRotation) {
426
- finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
422
+ if (ikChainInfo) {
423
+ const chainInfo = ikChainInfo[boneIndex]
424
+ if (chainInfo && chainInfo.ikRotation) {
425
+ finalRot = chainInfo.ikRotation.multiply(localRot).normalize()
426
+ }
427
427
  }
428
428
  const rotateM = Mat4.fromQuat(finalRot.x, finalRot.y, finalRot.z, finalRot.w)
429
429
 
@@ -446,43 +446,4 @@ export class IKSolverSystem {
446
446
  worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
447
447
  }
448
448
  }
449
-
450
- private static updateWorldMatrix(
451
- boneIndex: number,
452
- bones: Bone[],
453
- localRotations: Float32Array,
454
- localTranslations: Float32Array,
455
- worldMatrices: Float32Array
456
- ): void {
457
- const bone = bones[boneIndex]
458
- const qi = boneIndex * 4
459
- const ti = boneIndex * 3
460
-
461
- const localRot = new Quat(
462
- localRotations[qi],
463
- localRotations[qi + 1],
464
- localRotations[qi + 2],
465
- localRotations[qi + 3]
466
- )
467
- const rotateM = Mat4.fromQuat(localRot.x, localRot.y, localRot.z, localRot.w)
468
-
469
- const localTx = localTranslations[ti]
470
- const localTy = localTranslations[ti + 1]
471
- const localTz = localTranslations[ti + 2]
472
-
473
- const localM = Mat4.identity()
474
- .translateInPlace(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2])
475
- .multiply(rotateM)
476
- .translateInPlace(localTx, localTy, localTz)
477
-
478
- const worldOffset = boneIndex * 16
479
- if (bone.parentIndex >= 0) {
480
- const parentOffset = bone.parentIndex * 16
481
- const parentMat = new Mat4(worldMatrices.subarray(parentOffset, parentOffset + 16))
482
- const worldMat = parentMat.multiply(localM)
483
- worldMatrices.subarray(worldOffset, worldOffset + 16).set(worldMat.values)
484
- } else {
485
- worldMatrices.subarray(worldOffset, worldOffset + 16).set(localM.values)
486
- }
487
- }
488
449
  }
package/src/math.ts CHANGED
@@ -26,6 +26,10 @@ export class Vec3 {
26
26
  return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z)
27
27
  }
28
28
 
29
+ lengthSquared(): number {
30
+ return this.x * this.x + this.y * this.y + this.z * this.z
31
+ }
32
+
29
33
  normalize(): Vec3 {
30
34
  const len = this.length()
31
35
  if (len === 0) return new Vec3(0, 0, 0)
@@ -123,14 +127,6 @@ export class Quat {
123
127
  )
124
128
  }
125
129
 
126
- // Rotate a vector by this quaternion (Babylon.js style naming)
127
- rotate(v: Vec3): Vec3 {
128
- const qv = new Vec3(this.x, this.y, this.z)
129
- const uv = qv.cross(v)
130
- const uuv = qv.cross(uv)
131
- return v.add(uv.scale(2 * this.w)).add(uuv.scale(2))
132
- }
133
-
134
130
  // Static method: create quaternion that rotates from one direction to another
135
131
  static fromTo(from: Vec3, to: Vec3): Quat {
136
132
  const dot = from.dot(to)
@@ -553,3 +549,46 @@ export class Mat4 {
553
549
  return new Mat4(out)
554
550
  }
555
551
  }
552
+
553
+ /**
554
+ * Bezier interpolation function
555
+ * @param x1 First control point X (0-127, normalized to 0-1)
556
+ * @param x2 Second control point X (0-127, normalized to 0-1)
557
+ * @param y1 First control point Y (0-127, normalized to 0-1)
558
+ * @param y2 Second control point Y (0-127, normalized to 0-1)
559
+ * @param t Interpolation parameter (0-1)
560
+ * @returns Interpolated value (0-1)
561
+ */
562
+ export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
563
+ // Clamp t to [0, 1]
564
+ t = Math.max(0, Math.min(1, t))
565
+
566
+ // Binary search for the t value that gives us the desired x
567
+ // We're solving for t in the Bezier curve: x(t) = 3*(1-t)^2*t*x1 + 3*(1-t)*t^2*x2 + t^3
568
+ let start = 0
569
+ let end = 1
570
+ let mid = 0.5
571
+
572
+ // Iterate until we find the t value that gives us the desired x
573
+ for (let i = 0; i < 15; i++) {
574
+ // Evaluate Bezier curve at mid point
575
+ const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
576
+
577
+ if (Math.abs(x - t) < 0.0001) {
578
+ break
579
+ }
580
+
581
+ if (x < t) {
582
+ start = mid
583
+ } else {
584
+ end = mid
585
+ }
586
+
587
+ mid = (start + end) / 2
588
+ }
589
+
590
+ // Now evaluate the y value at this t
591
+ const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
592
+
593
+ return y
594
+ }
package/src/model.ts CHANGED
@@ -50,24 +50,6 @@ export interface IKLink {
50
50
  hasLimit: boolean
51
51
  minAngle?: Vec3 // Minimum Euler angles (radians)
52
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
53
  }
72
54
 
73
55
  // IK solver definition
@@ -78,7 +60,6 @@ export interface IKSolver {
78
60
  iterationCount: number
79
61
  limitAngle: number // Max rotation per iteration (radians)
80
62
  links: IKLink[] // Chain bones from effector to root
81
- canSkipWhenPhysicsEnabled: boolean
82
63
  }
83
64
 
84
65
  // IK chain info per bone (runtime state)
@@ -128,7 +109,6 @@ export interface SkeletonRuntime {
128
109
  localRotations: Float32Array // quat per bone (x,y,z,w) length = boneCount*4
129
110
  localTranslations: Float32Array // vec3 per bone length = boneCount*3
130
111
  worldMatrices: Float32Array // mat4 per bone length = boneCount*16
131
- computedBones: boolean[] // length = boneCount
132
112
  ikChainInfo?: IKChainInfo[] // IK chain info per bone (only for IK chain bones)
133
113
  ikSolvers?: IKSolver[] // All IK solvers in the model
134
114
  }
@@ -245,7 +225,6 @@ export class Model {
245
225
  acc[bone.name] = index
246
226
  return acc
247
227
  }, {} as Record<string, number>),
248
- computedBones: new Array(boneCount).fill(false),
249
228
  }
250
229
 
251
230
  const rotations = this.runtimeSkeleton.localRotations
@@ -283,18 +262,6 @@ export class Model {
283
262
  for (let i = 0; i < boneCount; i++) {
284
263
  const bone = bones[i]
285
264
  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
265
  const solver: IKSolver = {
299
266
  index: solverIndex++,
300
267
  ikBoneIndex: i,
@@ -302,7 +269,6 @@ export class Model {
302
269
  iterationCount: bone.ikIteration ?? 1,
303
270
  limitAngle: bone.ikLimitAngle ?? Math.PI,
304
271
  links: bone.ikLinks,
305
- canSkipWhenPhysicsEnabled,
306
272
  }
307
273
  ikSolvers.push(solver)
308
274
  }
@@ -550,14 +516,10 @@ export class Model {
550
516
  state.targetQuat[qi + 3]
551
517
  )
552
518
  const result = Quat.slerp(startQuat, targetQuat, e)
553
- const cx = result.x
554
- const cy = result.y
555
- const cz = result.z
556
- const cw = result.w
557
- sx = cx
558
- sy = cy
559
- sz = cz
560
- sw = cw
519
+ sx = result.x
520
+ sy = result.y
521
+ sz = result.z
522
+ sw = result.w
561
523
  }
562
524
 
563
525
  state.startQuat[qi] = sx
@@ -706,10 +668,9 @@ export class Model {
706
668
  // Animated change
707
669
  const state = this.morphTweenState
708
670
  const now = performance.now()
709
- const currentWeight = this.runtimeMorph.weights[idx]
710
671
 
711
672
  // If already tweening, start from current interpolated value
712
- let startWeight = currentWeight
673
+ let startWeight = this.runtimeMorph.weights[idx]
713
674
  if (state.active[idx] === 1) {
714
675
  const startMs = state.startTimeMs[idx]
715
676
  const prevDur = Math.max(1, state.durationMs[idx])
@@ -802,13 +763,13 @@ export class Model {
802
763
  this.applyMorphs()
803
764
  }
804
765
 
805
- // Compute initial world matrices (needed for IK solving)
766
+ // Compute initial world matrices (needed for IK solving to read bone positions)
806
767
  this.computeWorldMatrices()
807
768
 
808
- // Solve IK chains (modifies localRotations)
769
+ // Solve IK chains (modifies localRotations with final IK rotations)
809
770
  this.solveIKChains()
810
771
 
811
- // Recompute world matrices with IK rotations applied
772
+ // Recompute world matrices with final IK rotations applied to localRotations
812
773
  this.computeWorldMatrices()
813
774
 
814
775
  return hasActiveMorphTweens
@@ -827,8 +788,7 @@ export class Model {
827
788
  this.runtimeSkeleton.localRotations,
828
789
  this.runtimeSkeleton.localTranslations,
829
790
  this.runtimeSkeleton.worldMatrices,
830
- ikChainInfo,
831
- false // usePhysics - can be enhanced later
791
+ ikChainInfo
832
792
  )
833
793
  }
834
794
 
@@ -837,11 +797,13 @@ export class Model {
837
797
  const localRot = this.runtimeSkeleton.localRotations
838
798
  const localTrans = this.runtimeSkeleton.localTranslations
839
799
  const worldBuf = this.runtimeSkeleton.worldMatrices
840
- const computed = this.runtimeSkeleton.computedBones.fill(false)
841
800
  const boneCount = bones.length
842
801
 
843
802
  if (boneCount === 0) return
844
803
 
804
+ // Local computed array (avoids instance field overhead)
805
+ const computed = new Array<boolean>(boneCount).fill(false)
806
+
845
807
  const computeWorld = (i: number): void => {
846
808
  if (computed[i]) return
847
809
 
@@ -880,14 +842,9 @@ export class Model {
880
842
  ay = -ay
881
843
  az = -az
882
844
  }
883
- const identityQuat = new Quat(0, 0, 0, 1)
884
845
  const appendQuat = new Quat(ax, ay, az, aw)
885
- const result = Quat.slerp(identityQuat, appendQuat, absRatio)
886
- const rx = result.x
887
- const ry = result.y
888
- const rz = result.z
889
- const rw = result.w
890
- rotateM = Mat4.fromQuat(rx, ry, rz, rw).multiply(rotateM)
846
+ const result = Quat.slerp(new Quat(0, 0, 0, 1), appendQuat, absRatio)
847
+ rotateM = Mat4.fromQuat(result.x, result.y, result.z, result.w).multiply(rotateM)
891
848
  }
892
849
 
893
850
  if (b.appendMove) {