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/ammo-loader.d.ts +0 -1
- package/dist/ammo-loader.d.ts.map +1 -1
- package/dist/ammo-loader.js +0 -3
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +2 -2
- package/dist/ik-solver.d.ts +1 -2
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +13 -37
- package/dist/math.d.ts +11 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +39 -7
- package/dist/model.d.ts +0 -16
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +13 -52
- package/dist/player.d.ts +3 -25
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +4 -117
- package/package.json +1 -1
- package/src/ammo-loader.ts +0 -4
- package/src/engine.ts +2 -2
- package/src/ik-solver.ts +16 -55
- package/src/math.ts +47 -8
- package/src/model.ts +14 -57
- package/src/player.ts +4 -141
- package/src/bezier-interpolate.ts +0 -47
package/dist/player.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { bezierInterpolate } from "./
|
|
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
|
|
20
|
+
* Load VMD animation file
|
|
23
21
|
*/
|
|
24
|
-
async loadVmd(vmdUrl
|
|
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
package/src/ammo-loader.ts
CHANGED
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
|
|
1271
|
-
await this.player.loadVmd(url
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
419
|
+
|
|
420
|
+
// Apply IK rotation if available
|
|
424
421
|
let finalRot = localRot
|
|
425
|
-
if (
|
|
426
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 =
|
|
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(
|
|
886
|
-
|
|
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) {
|