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/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(addLocalTx, addLocalTy, addLocalTz);
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAUN,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;IA2IlB,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"}
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"}
@@ -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] = { name, parent: parentIndex, x, y, z, appendParent, appendRatio, appendRotate, appendMove };
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
- if (a.parent >= 0 && a.parent < count) {
364
- const p = abs[a.parent];
365
- bones.push({
366
- name: a.name,
367
- parentIndex: a.parent,
368
- bindTranslation: [a.x - p.x, a.y - p.y, a.z - p.z],
369
- children: [], // Will be populated later when building skeleton
370
- appendParentIndex: a.appendParent,
371
- appendRatio: a.appendRatio,
372
- appendRotate: a.appendRotate,
373
- appendMove: a.appendMove,
374
- });
375
- }
376
- else {
377
- bones.push({
378
- name: a.name,
379
- parentIndex: a.parent,
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
  }
@@ -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;
@@ -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;AAE7B,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,EAAE,CAAA;CACxB;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;IA8Db,OAAO,CAAC,aAAa;IA+CrB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,IAAI;CAMb"}
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"}
@@ -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 (assuming 30 FPS like the Rust code)
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
- for (const { time, boneFrame } of allBoneFrames) {
49
- if (Math.abs(time - currentTime) > 0.001) {
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
- currentBoneFrames.push(boneFrame);
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
- // Skip position (12 bytes: 3 x f32, little endian)
99
- this.skip(12);
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
- // Skip interpolation parameters (64 bytes)
107
- this.skip(64);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.2.19",
3
+ "version": "0.3.0",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
+ }