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/README.md +67 -66
- package/dist/bezier-interpolate.d.ts +15 -0
- package/dist/bezier-interpolate.d.ts.map +1 -0
- package/dist/bezier-interpolate.js +40 -0
- package/dist/engine.d.ts +7 -9
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -143
- package/dist/ik-solver.d.ts +26 -0
- package/dist/ik-solver.d.ts.map +1 -0
- package/dist/ik-solver.js +372 -0
- package/dist/math.d.ts +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +8 -0
- package/dist/model.d.ts +46 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +201 -3
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +57 -36
- package/dist/vmd-loader.d.ts +11 -1
- package/dist/vmd-loader.d.ts.map +1 -1
- package/dist/vmd-loader.js +91 -15
- package/package.json +1 -1
- package/src/bezier-interpolate.ts +47 -0
- package/src/camera.ts +358 -358
- package/src/engine.ts +275 -164
- package/src/ik-solver.ts +488 -0
- package/src/math.ts +555 -546
- package/src/model.ts +284 -3
- package/src/physics.ts +752 -752
- package/src/pmx-loader.ts +1173 -1145
- package/src/vmd-loader.ts +276 -179
package/dist/model.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import { Mat4, Quat, easeInOut } from "./math";
|
|
1
|
+
import { Mat4, Quat, Vec3, easeInOut } from "./math";
|
|
2
|
+
import { IKSolverSystem } from "./ik-solver";
|
|
2
3
|
const VERTEX_STRIDE = 8;
|
|
4
|
+
// Euler rotation order for angle constraints
|
|
5
|
+
export var EulerRotationOrder;
|
|
6
|
+
(function (EulerRotationOrder) {
|
|
7
|
+
EulerRotationOrder[EulerRotationOrder["YXZ"] = 0] = "YXZ";
|
|
8
|
+
EulerRotationOrder[EulerRotationOrder["ZYX"] = 1] = "ZYX";
|
|
9
|
+
EulerRotationOrder[EulerRotationOrder["XZY"] = 2] = "XZY";
|
|
10
|
+
})(EulerRotationOrder || (EulerRotationOrder = {}));
|
|
11
|
+
// Solve axis optimization
|
|
12
|
+
export var SolveAxis;
|
|
13
|
+
(function (SolveAxis) {
|
|
14
|
+
SolveAxis[SolveAxis["None"] = 0] = "None";
|
|
15
|
+
SolveAxis[SolveAxis["Fixed"] = 1] = "Fixed";
|
|
16
|
+
SolveAxis[SolveAxis["X"] = 2] = "X";
|
|
17
|
+
SolveAxis[SolveAxis["Y"] = 3] = "Y";
|
|
18
|
+
SolveAxis[SolveAxis["Z"] = 4] = "Z";
|
|
19
|
+
})(SolveAxis || (SolveAxis = {}));
|
|
3
20
|
export class Model {
|
|
4
21
|
constructor(vertexData, indexData, textures, materials, skeleton, skinning, morphing, rigidbodies = [], joints = []) {
|
|
5
22
|
this.textures = [];
|
|
@@ -27,6 +44,7 @@ export class Model {
|
|
|
27
44
|
}
|
|
28
45
|
this.initializeRuntimeSkeleton();
|
|
29
46
|
this.initializeRotTweenBuffers();
|
|
47
|
+
this.initializeTransTweenBuffers();
|
|
30
48
|
this.initializeRuntimeMorph();
|
|
31
49
|
this.initializeMorphTweenBuffers();
|
|
32
50
|
this.applyMorphs(); // Apply initial morphs (all weights are 0, so no change)
|
|
@@ -53,6 +71,51 @@ export class Model {
|
|
|
53
71
|
rotations[qi + 3] = 1;
|
|
54
72
|
}
|
|
55
73
|
}
|
|
74
|
+
// Initialize IK runtime state
|
|
75
|
+
this.initializeIKRuntime();
|
|
76
|
+
}
|
|
77
|
+
initializeIKRuntime() {
|
|
78
|
+
const boneCount = this.skeleton.bones.length;
|
|
79
|
+
const bones = this.skeleton.bones;
|
|
80
|
+
// Initialize IK chain info for all bones (will be populated for IK chain bones)
|
|
81
|
+
const ikChainInfo = new Array(boneCount);
|
|
82
|
+
for (let i = 0; i < boneCount; i++) {
|
|
83
|
+
ikChainInfo[i] = {
|
|
84
|
+
ikRotation: new Quat(0, 0, 0, 1),
|
|
85
|
+
localRotation: new Quat(0, 0, 0, 1),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Build IK solvers from bone data
|
|
89
|
+
const ikSolvers = [];
|
|
90
|
+
let solverIndex = 0;
|
|
91
|
+
for (let i = 0; i < boneCount; i++) {
|
|
92
|
+
const bone = bones[i];
|
|
93
|
+
if (bone.ikTargetIndex !== undefined && bone.ikLinks && bone.ikLinks.length > 0) {
|
|
94
|
+
// Check if all links are affected by physics (for optimization)
|
|
95
|
+
let canSkipWhenPhysicsEnabled = true;
|
|
96
|
+
for (const link of bone.ikLinks) {
|
|
97
|
+
// For now, assume no bones are physics-controlled (can be enhanced later)
|
|
98
|
+
// If a bone has a rigidbody attached, it's physics-controlled
|
|
99
|
+
const hasPhysics = this.rigidbodies.some((rb) => rb.boneIndex === link.boneIndex);
|
|
100
|
+
if (!hasPhysics) {
|
|
101
|
+
canSkipWhenPhysicsEnabled = false;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const solver = {
|
|
106
|
+
index: solverIndex++,
|
|
107
|
+
ikBoneIndex: i,
|
|
108
|
+
targetBoneIndex: bone.ikTargetIndex,
|
|
109
|
+
iterationCount: bone.ikIteration ?? 1,
|
|
110
|
+
limitAngle: bone.ikLimitAngle ?? Math.PI,
|
|
111
|
+
links: bone.ikLinks,
|
|
112
|
+
canSkipWhenPhysicsEnabled,
|
|
113
|
+
};
|
|
114
|
+
ikSolvers.push(solver);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.runtimeSkeleton.ikChainInfo = ikChainInfo;
|
|
118
|
+
this.runtimeSkeleton.ikSolvers = ikSolvers;
|
|
56
119
|
}
|
|
57
120
|
initializeRotTweenBuffers() {
|
|
58
121
|
const n = this.skeleton.bones.length;
|
|
@@ -64,6 +127,16 @@ export class Model {
|
|
|
64
127
|
durationMs: new Float32Array(n),
|
|
65
128
|
};
|
|
66
129
|
}
|
|
130
|
+
initializeTransTweenBuffers() {
|
|
131
|
+
const n = this.skeleton.bones.length;
|
|
132
|
+
this.transTweenState = {
|
|
133
|
+
active: new Uint8Array(n),
|
|
134
|
+
startVec: new Float32Array(n * 3),
|
|
135
|
+
targetVec: new Float32Array(n * 3),
|
|
136
|
+
startTimeMs: new Float32Array(n),
|
|
137
|
+
durationMs: new Float32Array(n),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
67
140
|
initializeMorphTweenBuffers() {
|
|
68
141
|
const n = this.morphing.morphs.length;
|
|
69
142
|
this.morphTweenState = {
|
|
@@ -108,6 +181,26 @@ export class Model {
|
|
|
108
181
|
state.active[i] = 0;
|
|
109
182
|
}
|
|
110
183
|
}
|
|
184
|
+
updateTranslationTweens() {
|
|
185
|
+
const state = this.transTweenState;
|
|
186
|
+
const now = performance.now();
|
|
187
|
+
const translations = this.runtimeSkeleton.localTranslations;
|
|
188
|
+
const boneCount = this.skeleton.bones.length;
|
|
189
|
+
for (let i = 0; i < boneCount; i++) {
|
|
190
|
+
if (state.active[i] !== 1)
|
|
191
|
+
continue;
|
|
192
|
+
const startMs = state.startTimeMs[i];
|
|
193
|
+
const durMs = Math.max(1, state.durationMs[i]);
|
|
194
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / durMs));
|
|
195
|
+
const e = easeInOut(t);
|
|
196
|
+
const ti = i * 3;
|
|
197
|
+
translations[ti] = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e;
|
|
198
|
+
translations[ti + 1] = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e;
|
|
199
|
+
translations[ti + 2] = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e;
|
|
200
|
+
if (t >= 1)
|
|
201
|
+
state.active[i] = 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
111
204
|
updateMorphWeightTweens() {
|
|
112
205
|
const state = this.morphTweenState;
|
|
113
206
|
const now = performance.now();
|
|
@@ -231,6 +324,91 @@ export class Model {
|
|
|
231
324
|
state.active[idx] = 1;
|
|
232
325
|
}
|
|
233
326
|
}
|
|
327
|
+
// Move bones using VMD-style relative translations (relative to bind pose world position)
|
|
328
|
+
// This is the default behavior for VMD animations
|
|
329
|
+
moveBones(names, relativeTranslations, durationMs) {
|
|
330
|
+
const state = this.transTweenState;
|
|
331
|
+
const now = performance.now();
|
|
332
|
+
const dur = durationMs && durationMs > 0 ? durationMs : 0;
|
|
333
|
+
const localRot = this.runtimeSkeleton.localRotations;
|
|
334
|
+
// Compute bind pose world positions for all bones
|
|
335
|
+
const skeleton = this.skeleton;
|
|
336
|
+
const computeBindPoseWorldPosition = (idx) => {
|
|
337
|
+
const bone = skeleton.bones[idx];
|
|
338
|
+
const bindPos = new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]);
|
|
339
|
+
if (bone.parentIndex >= 0 && bone.parentIndex < skeleton.bones.length) {
|
|
340
|
+
const parentWorldPos = computeBindPoseWorldPosition(bone.parentIndex);
|
|
341
|
+
return parentWorldPos.add(bindPos);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
return bindPos;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
for (let i = 0; i < names.length; i++) {
|
|
348
|
+
const name = names[i];
|
|
349
|
+
const idx = this.runtimeSkeleton.nameIndex[name] ?? -1;
|
|
350
|
+
if (idx < 0 || idx >= this.skeleton.bones.length)
|
|
351
|
+
continue;
|
|
352
|
+
const bone = this.skeleton.bones[idx];
|
|
353
|
+
const ti = idx * 3;
|
|
354
|
+
const qi = idx * 4;
|
|
355
|
+
const translations = this.runtimeSkeleton.localTranslations;
|
|
356
|
+
const vmdRelativeTranslation = relativeTranslations[i];
|
|
357
|
+
// VMD translation is relative to bind pose world position
|
|
358
|
+
// targetWorldPos = bindPoseWorldPos + vmdRelativeTranslation
|
|
359
|
+
const bindPoseWorldPos = computeBindPoseWorldPosition(idx);
|
|
360
|
+
const targetWorldPos = bindPoseWorldPos.add(vmdRelativeTranslation);
|
|
361
|
+
// Convert target world position to local translation
|
|
362
|
+
// We need parent's bind pose world position to transform to parent space
|
|
363
|
+
let parentBindPoseWorldPos;
|
|
364
|
+
if (bone.parentIndex >= 0) {
|
|
365
|
+
parentBindPoseWorldPos = computeBindPoseWorldPosition(bone.parentIndex);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
parentBindPoseWorldPos = new Vec3(0, 0, 0);
|
|
369
|
+
}
|
|
370
|
+
// Transform target world position to parent's local space
|
|
371
|
+
// In bind pose, parent's world matrix is just a translation
|
|
372
|
+
const parentSpacePos = targetWorldPos.subtract(parentBindPoseWorldPos);
|
|
373
|
+
// Subtract bindTranslation to get position after bind translation
|
|
374
|
+
const afterBindTranslation = parentSpacePos.subtract(new Vec3(bone.bindTranslation[0], bone.bindTranslation[1], bone.bindTranslation[2]));
|
|
375
|
+
// Apply inverse rotation to get local translation
|
|
376
|
+
const localRotation = new Quat(localRot[qi], localRot[qi + 1], localRot[qi + 2], localRot[qi + 3]);
|
|
377
|
+
const invRotation = localRotation.conjugate().normalize();
|
|
378
|
+
const rotationMat = Mat4.fromQuat(invRotation.x, invRotation.y, invRotation.z, invRotation.w);
|
|
379
|
+
const rm = rotationMat.values;
|
|
380
|
+
const localTranslation = new Vec3(rm[0] * afterBindTranslation.x + rm[4] * afterBindTranslation.y + rm[8] * afterBindTranslation.z, rm[1] * afterBindTranslation.x + rm[5] * afterBindTranslation.y + rm[9] * afterBindTranslation.z, rm[2] * afterBindTranslation.x + rm[6] * afterBindTranslation.y + rm[10] * afterBindTranslation.z);
|
|
381
|
+
const [tx, ty, tz] = [localTranslation.x, localTranslation.y, localTranslation.z];
|
|
382
|
+
if (dur === 0) {
|
|
383
|
+
translations[ti] = tx;
|
|
384
|
+
translations[ti + 1] = ty;
|
|
385
|
+
translations[ti + 2] = tz;
|
|
386
|
+
state.active[idx] = 0;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
let sx = translations[ti];
|
|
390
|
+
let sy = translations[ti + 1];
|
|
391
|
+
let sz = translations[ti + 2];
|
|
392
|
+
if (state.active[idx] === 1) {
|
|
393
|
+
const startMs = state.startTimeMs[idx];
|
|
394
|
+
const prevDur = Math.max(1, state.durationMs[idx]);
|
|
395
|
+
const t = Math.max(0, Math.min(1, (now - startMs) / prevDur));
|
|
396
|
+
const e = easeInOut(t);
|
|
397
|
+
sx = state.startVec[ti] + (state.targetVec[ti] - state.startVec[ti]) * e;
|
|
398
|
+
sy = state.startVec[ti + 1] + (state.targetVec[ti + 1] - state.startVec[ti + 1]) * e;
|
|
399
|
+
sz = state.startVec[ti + 2] + (state.targetVec[ti + 2] - state.startVec[ti + 2]) * e;
|
|
400
|
+
}
|
|
401
|
+
state.startVec[ti] = sx;
|
|
402
|
+
state.startVec[ti + 1] = sy;
|
|
403
|
+
state.startVec[ti + 2] = sz;
|
|
404
|
+
state.targetVec[ti] = tx;
|
|
405
|
+
state.targetVec[ti + 1] = ty;
|
|
406
|
+
state.targetVec[ti + 2] = tz;
|
|
407
|
+
state.startTimeMs[idx] = now;
|
|
408
|
+
state.durationMs[idx] = dur;
|
|
409
|
+
state.active[idx] = 1;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
234
412
|
getBoneWorldMatrices() {
|
|
235
413
|
return this.runtimeSkeleton.worldMatrices;
|
|
236
414
|
}
|
|
@@ -334,13 +512,29 @@ export class Model {
|
|
|
334
512
|
}
|
|
335
513
|
evaluatePose() {
|
|
336
514
|
this.updateRotationTweens();
|
|
515
|
+
this.updateTranslationTweens();
|
|
337
516
|
const hasActiveMorphTweens = this.updateMorphWeightTweens();
|
|
338
517
|
if (hasActiveMorphTweens) {
|
|
339
518
|
this.applyMorphs();
|
|
340
519
|
}
|
|
520
|
+
// Compute initial world matrices (needed for IK solving)
|
|
521
|
+
this.computeWorldMatrices();
|
|
522
|
+
// Solve IK chains (modifies localRotations)
|
|
523
|
+
this.solveIKChains();
|
|
524
|
+
// Recompute world matrices with IK rotations applied
|
|
341
525
|
this.computeWorldMatrices();
|
|
342
526
|
return hasActiveMorphTweens;
|
|
343
527
|
}
|
|
528
|
+
solveIKChains() {
|
|
529
|
+
const ikSolvers = this.runtimeSkeleton.ikSolvers;
|
|
530
|
+
if (!ikSolvers || ikSolvers.length === 0)
|
|
531
|
+
return;
|
|
532
|
+
const ikChainInfo = this.runtimeSkeleton.ikChainInfo;
|
|
533
|
+
if (!ikChainInfo)
|
|
534
|
+
return;
|
|
535
|
+
IKSolverSystem.solve(ikSolvers, this.skeleton.bones, this.runtimeSkeleton.localRotations, this.runtimeSkeleton.localTranslations, this.runtimeSkeleton.worldMatrices, ikChainInfo, false // usePhysics - can be enhanced later
|
|
536
|
+
);
|
|
537
|
+
}
|
|
344
538
|
computeWorldMatrices() {
|
|
345
539
|
const bones = this.skeleton.bones;
|
|
346
540
|
const localRot = this.runtimeSkeleton.localRotations;
|
|
@@ -397,11 +591,15 @@ export class Model {
|
|
|
397
591
|
}
|
|
398
592
|
}
|
|
399
593
|
}
|
|
400
|
-
// Build local matrix: identity + bind translation, then rotation, then append translation
|
|
594
|
+
// Build local matrix: identity + bind translation, then rotation, then local translation, then append translation
|
|
595
|
+
const ti = i * 3;
|
|
596
|
+
const localTx = localTrans[ti] + addLocalTx;
|
|
597
|
+
const localTy = localTrans[ti + 1] + addLocalTy;
|
|
598
|
+
const localTz = localTrans[ti + 2] + addLocalTz;
|
|
401
599
|
this.cachedIdentityMat1
|
|
402
600
|
.setIdentity()
|
|
403
601
|
.translateInPlace(b.bindTranslation[0], b.bindTranslation[1], b.bindTranslation[2]);
|
|
404
|
-
this.cachedIdentityMat2.setIdentity().translateInPlace(
|
|
602
|
+
this.cachedIdentityMat2.setIdentity().translateInPlace(localTx, localTy, localTz);
|
|
405
603
|
const localM = this.cachedIdentityMat1.multiply(rotateM).multiply(this.cachedIdentityMat2);
|
|
406
604
|
const worldOffset = i * 16;
|
|
407
605
|
if (b.parentIndex >= 0) {
|
package/dist/pmx-loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,
|
|
1
|
+
{"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAWN,MAAM,SAAS,CAAA;AAIhB,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C,OAAO,CAAC,KAAK;IAiBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAyFtB,OAAO,CAAC,UAAU;IAsKlB,OAAO,CAAC,WAAW;IA+InB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IAkLf,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
|
package/dist/pmx-loader.js
CHANGED
|
@@ -336,55 +336,76 @@ export class PmxLoader {
|
|
|
336
336
|
this.getInt32();
|
|
337
337
|
}
|
|
338
338
|
// IK block
|
|
339
|
+
let ikTargetIndex = undefined;
|
|
340
|
+
let ikIteration = undefined;
|
|
341
|
+
let ikLimitAngle = undefined;
|
|
342
|
+
let ikLinks = undefined;
|
|
339
343
|
if ((flags & FLAG_IK) !== 0) {
|
|
340
|
-
this.getNonVertexIndex(this.boneIndexSize); // target
|
|
341
|
-
this.getInt32(); // iteration
|
|
342
|
-
this.getFloat32(); // rotationConstraint
|
|
344
|
+
ikTargetIndex = this.getNonVertexIndex(this.boneIndexSize); // target
|
|
345
|
+
ikIteration = this.getInt32(); // iteration
|
|
346
|
+
ikLimitAngle = this.getFloat32(); // rotationConstraint
|
|
343
347
|
const linksCount = this.getInt32();
|
|
348
|
+
ikLinks = [];
|
|
344
349
|
for (let li = 0; li < linksCount; li++) {
|
|
345
|
-
this.getNonVertexIndex(this.boneIndexSize); // link target
|
|
350
|
+
const linkBoneIndex = this.getNonVertexIndex(this.boneIndexSize); // link target
|
|
346
351
|
const hasLimit = this.getUint8() === 1;
|
|
352
|
+
let minAngle = undefined;
|
|
353
|
+
let maxAngle = undefined;
|
|
347
354
|
if (hasLimit) {
|
|
348
355
|
// min and max angles (vec3 each)
|
|
349
|
-
this.getFloat32();
|
|
350
|
-
this.getFloat32();
|
|
351
|
-
this.getFloat32();
|
|
352
|
-
this.getFloat32();
|
|
353
|
-
this.getFloat32();
|
|
354
|
-
this.getFloat32();
|
|
356
|
+
const minX = this.getFloat32();
|
|
357
|
+
const minY = this.getFloat32();
|
|
358
|
+
const minZ = this.getFloat32();
|
|
359
|
+
const maxX = this.getFloat32();
|
|
360
|
+
const maxY = this.getFloat32();
|
|
361
|
+
const maxZ = this.getFloat32();
|
|
362
|
+
minAngle = new Vec3(minX, minY, minZ);
|
|
363
|
+
maxAngle = new Vec3(maxX, maxY, maxZ);
|
|
355
364
|
}
|
|
365
|
+
ikLinks.push({
|
|
366
|
+
boneIndex: linkBoneIndex,
|
|
367
|
+
hasLimit,
|
|
368
|
+
minAngle,
|
|
369
|
+
maxAngle,
|
|
370
|
+
});
|
|
356
371
|
}
|
|
357
372
|
}
|
|
358
373
|
// Stash minimal bone info; append data will be merged later
|
|
359
|
-
abs[i] = {
|
|
374
|
+
abs[i] = {
|
|
375
|
+
name,
|
|
376
|
+
parent: parentIndex,
|
|
377
|
+
x,
|
|
378
|
+
y,
|
|
379
|
+
z,
|
|
380
|
+
appendParent,
|
|
381
|
+
appendRatio,
|
|
382
|
+
appendRotate,
|
|
383
|
+
appendMove,
|
|
384
|
+
ikTargetIndex,
|
|
385
|
+
ikIteration,
|
|
386
|
+
ikLimitAngle,
|
|
387
|
+
ikLinks,
|
|
388
|
+
};
|
|
360
389
|
}
|
|
361
390
|
for (let i = 0; i < count; i++) {
|
|
362
391
|
const a = abs[i];
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
bindTranslation: [a.x, a.y, a.z],
|
|
381
|
-
children: [], // Will be populated later when building skeleton
|
|
382
|
-
appendParentIndex: a.appendParent,
|
|
383
|
-
appendRatio: a.appendRatio,
|
|
384
|
-
appendRotate: a.appendRotate,
|
|
385
|
-
appendMove: a.appendMove,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
392
|
+
const boneData = {
|
|
393
|
+
name: a.name,
|
|
394
|
+
parentIndex: a.parent,
|
|
395
|
+
bindTranslation: a.parent >= 0 && a.parent < count
|
|
396
|
+
? [a.x - abs[a.parent].x, a.y - abs[a.parent].y, a.z - abs[a.parent].z]
|
|
397
|
+
: [a.x, a.y, a.z],
|
|
398
|
+
children: [], // Will be populated later when building skeleton
|
|
399
|
+
appendParentIndex: a.appendParent,
|
|
400
|
+
appendRatio: a.appendRatio,
|
|
401
|
+
appendRotate: a.appendRotate,
|
|
402
|
+
appendMove: a.appendMove,
|
|
403
|
+
ikTargetIndex: a.ikTargetIndex,
|
|
404
|
+
ikIteration: a.ikIteration,
|
|
405
|
+
ikLimitAngle: a.ikLimitAngle,
|
|
406
|
+
ikLinks: a.ikLinks,
|
|
407
|
+
};
|
|
408
|
+
bones.push(boneData);
|
|
388
409
|
}
|
|
389
410
|
this.bones = bones;
|
|
390
411
|
}
|
package/dist/vmd-loader.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { Quat } from "./math";
|
|
1
|
+
import { Quat, Vec3 } from "./math";
|
|
2
2
|
export interface BoneFrame {
|
|
3
3
|
boneName: string;
|
|
4
4
|
frame: number;
|
|
5
5
|
rotation: Quat;
|
|
6
|
+
translation: Vec3;
|
|
7
|
+
interpolation: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
export interface MorphFrame {
|
|
10
|
+
morphName: string;
|
|
11
|
+
frame: number;
|
|
12
|
+
weight: number;
|
|
6
13
|
}
|
|
7
14
|
export interface VMDKeyFrame {
|
|
8
15
|
time: number;
|
|
9
16
|
boneFrames: BoneFrame[];
|
|
17
|
+
morphFrames: MorphFrame[];
|
|
10
18
|
}
|
|
11
19
|
export declare class VMDLoader {
|
|
12
20
|
private view;
|
|
@@ -17,6 +25,8 @@ export declare class VMDLoader {
|
|
|
17
25
|
static loadFromBuffer(buffer: ArrayBuffer): VMDKeyFrame[];
|
|
18
26
|
private parse;
|
|
19
27
|
private readBoneFrame;
|
|
28
|
+
private readMorphFrame;
|
|
29
|
+
private getUint8;
|
|
20
30
|
private getUint32;
|
|
21
31
|
private getFloat32;
|
|
22
32
|
private getString;
|
package/dist/vmd-loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"vmd-loader.d.ts","sourceRoot":"","sources":["../src/vmd-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEnC,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAa;IAE5B,OAAO;WAWM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAKtD,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE;IAKzD,OAAO,CAAC,KAAK;IAgGb,OAAO,CAAC,aAAa;IAuDrB,OAAO,CAAC,cAAc;IAqCtB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,IAAI;CAMb"}
|
package/dist/vmd-loader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Quat } from "./math";
|
|
1
|
+
import { Quat, Vec3 } from "./math";
|
|
2
2
|
export class VMDLoader {
|
|
3
3
|
constructor(buffer) {
|
|
4
4
|
this.offset = 0;
|
|
@@ -34,39 +34,67 @@ export class VMDLoader {
|
|
|
34
34
|
const allBoneFrames = [];
|
|
35
35
|
for (let i = 0; i < boneFrameCount; i++) {
|
|
36
36
|
const boneFrame = this.readBoneFrame();
|
|
37
|
-
// Convert frame number to time (
|
|
37
|
+
// Convert frame number to time (30 FPS)
|
|
38
38
|
const FRAME_RATE = 30.0;
|
|
39
39
|
const time = boneFrame.frame / FRAME_RATE;
|
|
40
40
|
allBoneFrames.push({ time, boneFrame });
|
|
41
41
|
}
|
|
42
|
+
// Read morph frame count (4 bytes, u32 little endian)
|
|
43
|
+
const morphFrameCount = this.getUint32();
|
|
44
|
+
// Read all morph frames
|
|
45
|
+
const allMorphFrames = [];
|
|
46
|
+
for (let i = 0; i < morphFrameCount; i++) {
|
|
47
|
+
const morphFrame = this.readMorphFrame();
|
|
48
|
+
// Convert frame number to time (30 FPS)
|
|
49
|
+
const FRAME_RATE = 30.0;
|
|
50
|
+
const time = morphFrame.frame / FRAME_RATE;
|
|
51
|
+
allMorphFrames.push({ time, morphFrame });
|
|
52
|
+
}
|
|
53
|
+
// Combine all frames and group by time
|
|
54
|
+
const allFrames = [];
|
|
55
|
+
for (const { time, boneFrame } of allBoneFrames) {
|
|
56
|
+
allFrames.push({ time, boneFrame });
|
|
57
|
+
}
|
|
58
|
+
for (const { time, morphFrame } of allMorphFrames) {
|
|
59
|
+
allFrames.push({ time, morphFrame });
|
|
60
|
+
}
|
|
61
|
+
// Sort by time
|
|
62
|
+
allFrames.sort((a, b) => a.time - b.time);
|
|
42
63
|
// Group by time and convert to VMDKeyFrame format
|
|
43
|
-
// Sort by time first
|
|
44
|
-
allBoneFrames.sort((a, b) => a.time - b.time);
|
|
45
64
|
const keyFrames = [];
|
|
46
65
|
let currentTime = -1.0;
|
|
47
66
|
let currentBoneFrames = [];
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
let currentMorphFrames = [];
|
|
68
|
+
for (const frame of allFrames) {
|
|
69
|
+
if (Math.abs(frame.time - currentTime) > 0.001) {
|
|
50
70
|
// New time frame
|
|
51
|
-
if (currentBoneFrames.length > 0) {
|
|
71
|
+
if (currentBoneFrames.length > 0 || currentMorphFrames.length > 0) {
|
|
52
72
|
keyFrames.push({
|
|
53
73
|
time: currentTime,
|
|
54
74
|
boneFrames: currentBoneFrames,
|
|
75
|
+
morphFrames: currentMorphFrames,
|
|
55
76
|
});
|
|
56
77
|
}
|
|
57
|
-
currentTime = time;
|
|
58
|
-
currentBoneFrames = [boneFrame];
|
|
78
|
+
currentTime = frame.time;
|
|
79
|
+
currentBoneFrames = frame.boneFrame ? [frame.boneFrame] : [];
|
|
80
|
+
currentMorphFrames = frame.morphFrame ? [frame.morphFrame] : [];
|
|
59
81
|
}
|
|
60
82
|
else {
|
|
61
83
|
// Same time frame
|
|
62
|
-
|
|
84
|
+
if (frame.boneFrame) {
|
|
85
|
+
currentBoneFrames.push(frame.boneFrame);
|
|
86
|
+
}
|
|
87
|
+
if (frame.morphFrame) {
|
|
88
|
+
currentMorphFrames.push(frame.morphFrame);
|
|
89
|
+
}
|
|
63
90
|
}
|
|
64
91
|
}
|
|
65
92
|
// Add the last frame
|
|
66
|
-
if (currentBoneFrames.length > 0) {
|
|
93
|
+
if (currentBoneFrames.length > 0 || currentMorphFrames.length > 0) {
|
|
67
94
|
keyFrames.push({
|
|
68
95
|
time: currentTime,
|
|
69
96
|
boneFrames: currentBoneFrames,
|
|
97
|
+
morphFrames: currentMorphFrames,
|
|
70
98
|
});
|
|
71
99
|
}
|
|
72
100
|
return keyFrames;
|
|
@@ -95,22 +123,70 @@ export class VMDLoader {
|
|
|
95
123
|
}
|
|
96
124
|
// Read frame number (4 bytes, little endian)
|
|
97
125
|
const frame = this.getUint32();
|
|
98
|
-
//
|
|
99
|
-
this.
|
|
126
|
+
// Read position/translation (12 bytes: 3 x f32, little endian)
|
|
127
|
+
const posX = this.getFloat32();
|
|
128
|
+
const posY = this.getFloat32();
|
|
129
|
+
const posZ = this.getFloat32();
|
|
130
|
+
const translation = new Vec3(posX, posY, posZ);
|
|
100
131
|
// Read rotation quaternion (16 bytes: 4 x f32, little endian)
|
|
101
132
|
const rotX = this.getFloat32();
|
|
102
133
|
const rotY = this.getFloat32();
|
|
103
134
|
const rotZ = this.getFloat32();
|
|
104
135
|
const rotW = this.getFloat32();
|
|
105
136
|
const rotation = new Quat(rotX, rotY, rotZ, rotW);
|
|
106
|
-
//
|
|
107
|
-
|
|
137
|
+
// Read interpolation parameters (64 bytes)
|
|
138
|
+
const interpolation = new Uint8Array(64);
|
|
139
|
+
for (let i = 0; i < 64; i++) {
|
|
140
|
+
interpolation[i] = this.getUint8();
|
|
141
|
+
}
|
|
108
142
|
return {
|
|
109
143
|
boneName,
|
|
110
144
|
frame,
|
|
111
145
|
rotation,
|
|
146
|
+
translation,
|
|
147
|
+
interpolation,
|
|
112
148
|
};
|
|
113
149
|
}
|
|
150
|
+
readMorphFrame() {
|
|
151
|
+
// Read morph name (15 bytes)
|
|
152
|
+
const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15);
|
|
153
|
+
this.offset += 15;
|
|
154
|
+
// Find the actual length of the morph name (stop at first null byte)
|
|
155
|
+
let nameLength = 15;
|
|
156
|
+
for (let i = 0; i < 15; i++) {
|
|
157
|
+
if (nameBuffer[i] === 0) {
|
|
158
|
+
nameLength = i;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Decode Shift-JIS morph name
|
|
163
|
+
let morphName;
|
|
164
|
+
try {
|
|
165
|
+
const nameSlice = nameBuffer.slice(0, nameLength);
|
|
166
|
+
morphName = this.decoder.decode(nameSlice);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Fallback to lossy decoding if there were encoding errors
|
|
170
|
+
morphName = String.fromCharCode(...nameBuffer.slice(0, nameLength));
|
|
171
|
+
}
|
|
172
|
+
// Read frame number (4 bytes, little endian)
|
|
173
|
+
const frame = this.getUint32();
|
|
174
|
+
// Read weight (4 bytes, f32, little endian)
|
|
175
|
+
const weight = this.getFloat32();
|
|
176
|
+
return {
|
|
177
|
+
morphName,
|
|
178
|
+
frame,
|
|
179
|
+
weight,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
getUint8() {
|
|
183
|
+
if (this.offset + 1 > this.view.buffer.byteLength) {
|
|
184
|
+
throw new RangeError(`Offset ${this.offset} + 1 exceeds buffer bounds ${this.view.buffer.byteLength}`);
|
|
185
|
+
}
|
|
186
|
+
const v = this.view.getUint8(this.offset);
|
|
187
|
+
this.offset += 1;
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
114
190
|
getUint32() {
|
|
115
191
|
if (this.offset + 4 > this.view.buffer.byteLength) {
|
|
116
192
|
throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bezier interpolation for VMD animations
|
|
3
|
+
* Based on the reference implementation from babylon-mmd
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bezier interpolation function
|
|
8
|
+
* @param x1 First control point X (0-127, normalized to 0-1)
|
|
9
|
+
* @param x2 Second control point X (0-127, normalized to 0-1)
|
|
10
|
+
* @param y1 First control point Y (0-127, normalized to 0-1)
|
|
11
|
+
* @param y2 Second control point Y (0-127, normalized to 0-1)
|
|
12
|
+
* @param t Interpolation parameter (0-1)
|
|
13
|
+
* @returns Interpolated value (0-1)
|
|
14
|
+
*/
|
|
15
|
+
export function bezierInterpolate(x1: number, x2: number, y1: number, y2: number, t: number): number {
|
|
16
|
+
// Clamp t to [0, 1]
|
|
17
|
+
t = Math.max(0, Math.min(1, t))
|
|
18
|
+
|
|
19
|
+
// Binary search for the t value that gives us the desired x
|
|
20
|
+
// 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
|
|
21
|
+
let start = 0
|
|
22
|
+
let end = 1
|
|
23
|
+
let mid = 0.5
|
|
24
|
+
|
|
25
|
+
// Iterate until we find the t value that gives us the desired x
|
|
26
|
+
for (let i = 0; i < 15; i++) {
|
|
27
|
+
// Evaluate Bezier curve at mid point
|
|
28
|
+
const x = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid
|
|
29
|
+
|
|
30
|
+
if (Math.abs(x - t) < 0.0001) {
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (x < t) {
|
|
35
|
+
start = mid
|
|
36
|
+
} else {
|
|
37
|
+
end = mid
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mid = (start + end) / 2
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Now evaluate the y value at this t
|
|
44
|
+
const y = 3 * (1 - mid) * (1 - mid) * mid * y1 + 3 * (1 - mid) * mid * mid * y2 + mid * mid * mid
|
|
45
|
+
|
|
46
|
+
return y
|
|
47
|
+
}
|