reze-engine 0.1.16 → 0.2.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/engine.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Camera } from "./camera";
2
- import { Quat, Vec3 } from "./math";
2
+ import { Quat } from "./math";
3
3
  export interface EngineStats {
4
4
  fps: number;
5
5
  frameTime: number;
@@ -77,6 +77,8 @@ export declare class Engine {
77
77
  private stats;
78
78
  private animationFrameId;
79
79
  private renderLoopCallback;
80
+ private animationFrames;
81
+ private animationTimeouts;
80
82
  constructor(canvas: HTMLCanvasElement);
81
83
  init(): Promise<void>;
82
84
  private createPipelines;
@@ -88,8 +90,11 @@ export declare class Engine {
88
90
  private handleResize;
89
91
  private setupCamera;
90
92
  private setupLighting;
91
- addLight(direction: Vec3, color: Vec3, intensity?: number): boolean;
92
- setAmbient(intensity: number): void;
93
+ private addLight;
94
+ private setAmbient;
95
+ loadAnimation(url: string): Promise<void>;
96
+ playAnimation(): void;
97
+ stopAnimation(): void;
93
98
  getStats(): EngineStats;
94
99
  runRenderLoop(callback?: () => void): void;
95
100
  stopRenderLoop(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Camera } from "./camera";
2
- import { Vec3 } from "./math";
2
+ import { Quat, Vec3 } from "./math";
3
3
  import { PmxLoader } from "./pmx-loader";
4
4
  import { Physics } from "./physics";
5
+ import { VMDLoader } from "./vmd-loader";
5
6
  export class Engine {
6
7
  constructor(canvas) {
7
8
  this.cameraMatrixData = new Float32Array(36);
@@ -34,6 +35,8 @@ export class Engine {
34
35
  };
35
36
  this.animationFrameId = null;
36
37
  this.renderLoopCallback = null;
38
+ this.animationFrames = [];
39
+ this.animationTimeouts = [];
37
40
  this.opaqueNonEyeNonHairDraws = [];
38
41
  this.eyeDraws = [];
39
42
  this.hairDrawsOverEyes = [];
@@ -1258,6 +1261,107 @@ export class Engine {
1258
1261
  setAmbient(intensity) {
1259
1262
  this.lightData[0] = intensity;
1260
1263
  }
1264
+ async loadAnimation(url) {
1265
+ const frames = await VMDLoader.load(url);
1266
+ this.animationFrames = frames;
1267
+ console.log(this.animationFrames);
1268
+ }
1269
+ playAnimation() {
1270
+ if (this.animationFrames.length === 0)
1271
+ return;
1272
+ this.stopAnimation();
1273
+ const allBoneKeyFrames = [];
1274
+ for (const keyFrame of this.animationFrames) {
1275
+ for (const boneFrame of keyFrame.boneFrames) {
1276
+ allBoneKeyFrames.push({
1277
+ boneName: boneFrame.boneName,
1278
+ time: keyFrame.time,
1279
+ rotation: boneFrame.rotation,
1280
+ });
1281
+ }
1282
+ }
1283
+ const boneKeyFramesByBone = new Map();
1284
+ for (const boneKeyFrame of allBoneKeyFrames) {
1285
+ if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
1286
+ boneKeyFramesByBone.set(boneKeyFrame.boneName, []);
1287
+ }
1288
+ boneKeyFramesByBone.get(boneKeyFrame.boneName).push(boneKeyFrame);
1289
+ }
1290
+ for (const keyFrames of boneKeyFramesByBone.values()) {
1291
+ keyFrames.sort((a, b) => a.time - b.time);
1292
+ }
1293
+ const time0Rotations = [];
1294
+ const bonesWithTime0 = new Set();
1295
+ for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
1296
+ if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1297
+ time0Rotations.push({
1298
+ boneName: boneName,
1299
+ rotation: keyFrames[0].rotation,
1300
+ });
1301
+ bonesWithTime0.add(boneName);
1302
+ }
1303
+ }
1304
+ if (this.currentModel) {
1305
+ if (time0Rotations.length > 0) {
1306
+ const boneNames = time0Rotations.map((r) => r.boneName);
1307
+ const rotations = time0Rotations.map((r) => r.rotation);
1308
+ this.rotateBones(boneNames, rotations, 0);
1309
+ }
1310
+ const skeleton = this.currentModel.getSkeleton();
1311
+ const bonesToReset = [];
1312
+ for (const bone of skeleton.bones) {
1313
+ if (!bonesWithTime0.has(bone.name)) {
1314
+ bonesToReset.push(bone.name);
1315
+ }
1316
+ }
1317
+ if (bonesToReset.length > 0) {
1318
+ const identityQuat = new Quat(0, 0, 0, 1);
1319
+ const identityQuats = new Array(bonesToReset.length).fill(identityQuat);
1320
+ this.rotateBones(bonesToReset, identityQuats, 0);
1321
+ }
1322
+ this.currentModel.evaluatePose();
1323
+ // Reset physics immediately and upload matrices to prevent A-pose flash
1324
+ if (this.physics) {
1325
+ const worldMats = this.currentModel.getBoneWorldMatrices();
1326
+ this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices());
1327
+ // Upload matrices immediately so next frame shows correct pose
1328
+ this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
1329
+ this.computeSkinMatrices();
1330
+ }
1331
+ }
1332
+ for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
1333
+ for (let i = 0; i < keyFrames.length; i++) {
1334
+ const boneKeyFrame = keyFrames[i];
1335
+ const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null;
1336
+ if (boneKeyFrame.time === 0)
1337
+ continue;
1338
+ let durationMs = 0;
1339
+ if (i === 0) {
1340
+ durationMs = boneKeyFrame.time * 1000;
1341
+ }
1342
+ else if (previousBoneKeyFrame) {
1343
+ durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000;
1344
+ }
1345
+ const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0;
1346
+ const delayMs = scheduleTime * 1000;
1347
+ if (delayMs <= 0) {
1348
+ this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1349
+ }
1350
+ else {
1351
+ const timeoutId = window.setTimeout(() => {
1352
+ this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs);
1353
+ }, delayMs);
1354
+ this.animationTimeouts.push(timeoutId);
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+ stopAnimation() {
1360
+ for (const timeoutId of this.animationTimeouts) {
1361
+ clearTimeout(timeoutId);
1362
+ }
1363
+ this.animationTimeouts = [];
1364
+ }
1261
1365
  getStats() {
1262
1366
  return { ...this.stats };
1263
1367
  }
@@ -1281,6 +1385,7 @@ export class Engine {
1281
1385
  }
1282
1386
  dispose() {
1283
1387
  this.stopRenderLoop();
1388
+ this.stopAnimation();
1284
1389
  if (this.camera)
1285
1390
  this.camera.detachControl();
1286
1391
  if (this.resizeObserver) {
@@ -1899,19 +2004,13 @@ export class Engine {
1899
2004
  colorAttachment.view = this.sceneRenderTextureView;
1900
2005
  }
1901
2006
  }
1902
- // Update model pose and physics
1903
2007
  updateModelPose(deltaTime) {
1904
- // Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
1905
2008
  this.currentModel.evaluatePose();
1906
- // Step 2: Get world matrices (still in CPU memory)
1907
2009
  const worldMats = this.currentModel.getBoneWorldMatrices();
1908
- // Step 3: Physics modifies matrices in-place
1909
2010
  if (this.physics) {
1910
2011
  this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
1911
2012
  }
1912
- // Step 4: Upload ONCE with final result (animation + physics)
1913
2013
  this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
1914
- // Step 5: GPU skinning
1915
2014
  this.computeSkinMatrices();
1916
2015
  }
1917
2016
  // Compute skin matrices on GPU
package/dist/physics.d.ts CHANGED
@@ -72,6 +72,7 @@ export declare class Physics {
72
72
  private createAmmoRigidbodies;
73
73
  private createAmmoJoints;
74
74
  private normalizeAngle;
75
+ reset(boneWorldMatrices: Float32Array, boneInverseBindMatrices: Float32Array): void;
75
76
  step(dt: number, boneWorldMatrices: Float32Array, boneInverseBindMatrices: Float32Array): void;
76
77
  private computeBodyOffsets;
77
78
  private positionBodiesFromBones;
@@ -1 +1 @@
1
- {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAIzC,oBAAY,cAAc;IACxB,MAAM,IAAI;IACV,GAAG,IAAI;IACP,OAAO,IAAI;CACZ;AAED,oBAAY,aAAa;IACvB,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,cAAc,CAAA;IACrB,IAAI,EAAE,IAAI,CAAA;IACV,aAAa,EAAE,IAAI,CAAA;IACnB,aAAa,EAAE,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,aAAa,CAAA;IACnB,uBAAuB,EAAE,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,QAAQ,EAAE,IAAI,CAAA;IACd,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,cAAc,EAAE,IAAI,CAAA;IACpB,cAAc,EAAE,IAAI,CAAA;CACrB;AAED,qBAAa,OAAO;IAClB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,WAAW,CAAqC;IACxD,OAAO,CAAC,IAAI,CAA4B;IAExC,OAAO,CAAC,aAAa,CAAY;IAEjC,OAAO,CAAC,eAAe,CAAY;IAEnC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,sBAAsB,CAAQ;IACtC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,UAAU,CAAO;IACzB,OAAO,CAAC,oCAAoC,CAAO;IAEnD,OAAO,CAAC,UAAU,CAAY;gBAElB,WAAW,EAAE,SAAS,EAAE,EAAE,MAAM,GAAE,KAAK,EAAO;YAM5C,QAAQ;IAatB,UAAU,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI;IAU/B,UAAU,IAAI,IAAI;IAIlB,cAAc,IAAI,SAAS,EAAE;IAI7B,SAAS,IAAI,KAAK,EAAE;IAIpB,sBAAsB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC;IA6CnE,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,qBAAqB;IA+F7B,OAAO,CAAC,gBAAgB;IA0KxB,OAAO,CAAC,cAAc;IActB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,YAAY,EAAE,uBAAuB,EAAE,YAAY,GAAG,IAAI;IAsC9F,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,uBAAuB;IAkD/B,OAAO,CAAC,aAAa;IAwDrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,2BAA2B;CAqCpC"}
1
+ {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"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"}
package/dist/physics.js CHANGED
@@ -329,6 +329,64 @@ export class Physics {
329
329
  }
330
330
  return angle;
331
331
  }
332
+ // Reset physics state (reposition bodies, clear velocities)
333
+ // Following babylon-mmd pattern: initialize all rigid body positions from current bone poses
334
+ // Call this when starting a new animation to prevent physics instability from sudden pose changes
335
+ reset(boneWorldMatrices, boneInverseBindMatrices) {
336
+ if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
337
+ return;
338
+ }
339
+ const boneCount = boneWorldMatrices.length / 16;
340
+ const Ammo = this.ammo;
341
+ // Ensure body offsets are computed
342
+ if (!this.rigidbodiesInitialized) {
343
+ this.computeBodyOffsets(boneInverseBindMatrices, boneCount);
344
+ this.rigidbodiesInitialized = true;
345
+ }
346
+ // Reposition ALL rigid bodies from current bone poses (like babylon-mmd initialize)
347
+ // This ensures all bodies are correctly positioned before physics starts
348
+ for (let i = 0; i < this.rigidbodies.length; i++) {
349
+ const rb = this.rigidbodies[i];
350
+ const ammoBody = this.ammoRigidbodies[i];
351
+ if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount)
352
+ continue;
353
+ const boneIdx = rb.boneIndex;
354
+ const worldMatIdx = boneIdx * 16;
355
+ // Get bone world matrix
356
+ const boneWorldMat = new Mat4(boneWorldMatrices.subarray(worldMatIdx, worldMatIdx + 16));
357
+ // Compute body world matrix: bodyWorld = boneWorld × bodyOffsetMatrix
358
+ // (like babylon-mmd: bodyWorldMatrix = bodyOffsetMatrix.multiplyToRef(bodyWorldMatrix))
359
+ const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
360
+ const bodyWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
361
+ const worldPos = bodyWorldMatrix.getPosition();
362
+ const worldRot = bodyWorldMatrix.toQuat();
363
+ // Set transform matrix
364
+ const transform = new Ammo.btTransform();
365
+ const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
366
+ const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
367
+ transform.setOrigin(pos);
368
+ transform.setRotation(quat);
369
+ ammoBody.setWorldTransform(transform);
370
+ ammoBody.getMotionState().setWorldTransform(transform);
371
+ // Clear velocities for all rigidbodies
372
+ if (!this.zeroVector) {
373
+ this.zeroVector = new Ammo.btVector3(0, 0, 0);
374
+ }
375
+ ammoBody.setLinearVelocity(this.zeroVector);
376
+ ammoBody.setAngularVelocity(this.zeroVector);
377
+ // Explicitly activate dynamic rigidbodies after reset (wake them up)
378
+ // This is critical for dress pieces and other dynamic bodies to prevent teleporting
379
+ if (rb.type === RigidbodyType.Dynamic) {
380
+ ammoBody.activate(true); // Wake up the body
381
+ }
382
+ Ammo.destroy(pos);
383
+ Ammo.destroy(quat);
384
+ }
385
+ // Step simulation once to stabilize (like babylon-mmd)
386
+ if (this.dynamicsWorld.stepSimulation) {
387
+ this.dynamicsWorld.stepSimulation(0, 0, 0);
388
+ }
389
+ }
332
390
  // Syncs bones to rigidbodies, simulates dynamics, solves constraints
333
391
  // Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
334
392
  step(dt, boneWorldMatrices, boneInverseBindMatrices) {
@@ -0,0 +1,25 @@
1
+ import { Quat } from "./math";
2
+ export interface BoneFrame {
3
+ boneName: string;
4
+ frame: number;
5
+ rotation: Quat;
6
+ }
7
+ export interface VMDKeyFrame {
8
+ time: number;
9
+ boneFrames: BoneFrame[];
10
+ }
11
+ export declare class VMDLoader {
12
+ private view;
13
+ private offset;
14
+ private decoder;
15
+ private constructor();
16
+ static load(url: string): Promise<VMDKeyFrame[]>;
17
+ static loadFromBuffer(buffer: ArrayBuffer): VMDKeyFrame[];
18
+ private parse;
19
+ private readBoneFrame;
20
+ private getUint32;
21
+ private getFloat32;
22
+ private getString;
23
+ private skip;
24
+ }
25
+ //# sourceMappingURL=vmd-loader.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,141 @@
1
+ import { Quat } from "./math";
2
+ export class VMDLoader {
3
+ constructor(buffer) {
4
+ this.offset = 0;
5
+ this.view = new DataView(buffer);
6
+ // Try to use Shift-JIS decoder, fallback to UTF-8 if not available
7
+ try {
8
+ this.decoder = new TextDecoder("shift-jis");
9
+ }
10
+ catch {
11
+ // Fallback to UTF-8 if Shift-JIS is not supported
12
+ this.decoder = new TextDecoder("utf-8");
13
+ }
14
+ }
15
+ static async load(url) {
16
+ const loader = new VMDLoader(await fetch(url).then((r) => r.arrayBuffer()));
17
+ return loader.parse();
18
+ }
19
+ static loadFromBuffer(buffer) {
20
+ const loader = new VMDLoader(buffer);
21
+ return loader.parse();
22
+ }
23
+ parse() {
24
+ // Read header (30 bytes)
25
+ const header = this.getString(30);
26
+ if (!header.startsWith("Vocaloid Motion Data")) {
27
+ throw new Error("Invalid VMD file header");
28
+ }
29
+ // Skip model name (20 bytes)
30
+ this.skip(20);
31
+ // Read bone frame count (4 bytes, u32 little endian)
32
+ const boneFrameCount = this.getUint32();
33
+ // Read all bone frames
34
+ const allBoneFrames = [];
35
+ for (let i = 0; i < boneFrameCount; i++) {
36
+ const boneFrame = this.readBoneFrame();
37
+ // Convert frame number to time (assuming 30 FPS like the Rust code)
38
+ const FRAME_RATE = 30.0;
39
+ const time = boneFrame.frame / FRAME_RATE;
40
+ allBoneFrames.push({ time, boneFrame });
41
+ }
42
+ // Group by time and convert to VMDKeyFrame format
43
+ // Sort by time first
44
+ allBoneFrames.sort((a, b) => a.time - b.time);
45
+ const keyFrames = [];
46
+ let currentTime = -1.0;
47
+ let currentBoneFrames = [];
48
+ for (const { time, boneFrame } of allBoneFrames) {
49
+ if (Math.abs(time - currentTime) > 0.001) {
50
+ // New time frame
51
+ if (currentBoneFrames.length > 0) {
52
+ keyFrames.push({
53
+ time: currentTime,
54
+ boneFrames: currentBoneFrames,
55
+ });
56
+ }
57
+ currentTime = time;
58
+ currentBoneFrames = [boneFrame];
59
+ }
60
+ else {
61
+ // Same time frame
62
+ currentBoneFrames.push(boneFrame);
63
+ }
64
+ }
65
+ // Add the last frame
66
+ if (currentBoneFrames.length > 0) {
67
+ keyFrames.push({
68
+ time: currentTime,
69
+ boneFrames: currentBoneFrames,
70
+ });
71
+ }
72
+ return keyFrames;
73
+ }
74
+ readBoneFrame() {
75
+ // Read bone name (15 bytes)
76
+ const nameBuffer = new Uint8Array(this.view.buffer, this.offset, 15);
77
+ this.offset += 15;
78
+ // Find the actual length of the bone name (stop at first null byte)
79
+ let nameLength = 15;
80
+ for (let i = 0; i < 15; i++) {
81
+ if (nameBuffer[i] === 0) {
82
+ nameLength = i;
83
+ break;
84
+ }
85
+ }
86
+ // Decode Shift-JIS bone name
87
+ let boneName;
88
+ try {
89
+ const nameSlice = nameBuffer.slice(0, nameLength);
90
+ boneName = this.decoder.decode(nameSlice);
91
+ }
92
+ catch {
93
+ // Fallback to lossy decoding if there were encoding errors
94
+ boneName = String.fromCharCode(...nameBuffer.slice(0, nameLength));
95
+ }
96
+ // Read frame number (4 bytes, little endian)
97
+ const frame = this.getUint32();
98
+ // Skip position (12 bytes: 3 x f32, little endian)
99
+ this.skip(12);
100
+ // Read rotation quaternion (16 bytes: 4 x f32, little endian)
101
+ const rotX = this.getFloat32();
102
+ const rotY = this.getFloat32();
103
+ const rotZ = this.getFloat32();
104
+ const rotW = this.getFloat32();
105
+ const rotation = new Quat(rotX, rotY, rotZ, rotW);
106
+ // Skip interpolation parameters (64 bytes)
107
+ this.skip(64);
108
+ return {
109
+ boneName,
110
+ frame,
111
+ rotation,
112
+ };
113
+ }
114
+ getUint32() {
115
+ if (this.offset + 4 > this.view.buffer.byteLength) {
116
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
117
+ }
118
+ const v = this.view.getUint32(this.offset, true); // true = little endian
119
+ this.offset += 4;
120
+ return v;
121
+ }
122
+ getFloat32() {
123
+ if (this.offset + 4 > this.view.buffer.byteLength) {
124
+ throw new RangeError(`Offset ${this.offset} + 4 exceeds buffer bounds ${this.view.buffer.byteLength}`);
125
+ }
126
+ const v = this.view.getFloat32(this.offset, true); // true = little endian
127
+ this.offset += 4;
128
+ return v;
129
+ }
130
+ getString(len) {
131
+ const bytes = new Uint8Array(this.view.buffer, this.offset, len);
132
+ this.offset += len;
133
+ return String.fromCharCode(...bytes);
134
+ }
135
+ skip(bytes) {
136
+ if (this.offset + bytes > this.view.buffer.byteLength) {
137
+ throw new RangeError(`Offset ${this.offset} + ${bytes} exceeds buffer bounds ${this.view.buffer.byteLength}`);
138
+ }
139
+ this.offset += bytes;
140
+ }
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.1.16",
3
+ "version": "0.2.0",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/engine.ts CHANGED
@@ -3,6 +3,7 @@ import { Quat, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
4
  import { PmxLoader } from "./pmx-loader"
5
5
  import { Physics } from "./physics"
6
+ import { VMDKeyFrame, VMDLoader } from "./vmd-loader"
6
7
 
7
8
  export interface EngineStats {
8
9
  fps: number
@@ -10,6 +11,13 @@ export interface EngineStats {
10
11
  gpuMemory: number // MB (estimated total GPU memory)
11
12
  }
12
13
 
14
+ // Internal type for organizing bone keyframes during animation playback
15
+ type BoneKeyFrame = {
16
+ boneName: string
17
+ time: number
18
+ rotation: Quat
19
+ }
20
+
13
21
  export class Engine {
14
22
  private canvas: HTMLCanvasElement
15
23
  private device!: GPUDevice
@@ -94,6 +102,9 @@ export class Engine {
94
102
  private animationFrameId: number | null = null
95
103
  private renderLoopCallback: (() => void) | null = null
96
104
 
105
+ private animationFrames: VMDKeyFrame[] = []
106
+ private animationTimeouts: number[] = []
107
+
97
108
  constructor(canvas: HTMLCanvasElement) {
98
109
  this.canvas = canvas
99
110
  }
@@ -1352,7 +1363,7 @@ export class Engine {
1352
1363
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData)
1353
1364
  }
1354
1365
 
1355
- public addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
1366
+ private addLight(direction: Vec3, color: Vec3, intensity: number = 1.0): boolean {
1356
1367
  if (this.lightCount >= 4) return false
1357
1368
 
1358
1369
  const normalized = direction.normalize()
@@ -1371,10 +1382,131 @@ export class Engine {
1371
1382
  return true
1372
1383
  }
1373
1384
 
1374
- public setAmbient(intensity: number) {
1385
+ private setAmbient(intensity: number) {
1375
1386
  this.lightData[0] = intensity
1376
1387
  }
1377
1388
 
1389
+ public async loadAnimation(url: string) {
1390
+ const frames = await VMDLoader.load(url)
1391
+ this.animationFrames = frames
1392
+ console.log(this.animationFrames)
1393
+ }
1394
+
1395
+ public playAnimation() {
1396
+ if (this.animationFrames.length === 0) return
1397
+
1398
+ this.stopAnimation()
1399
+
1400
+ const allBoneKeyFrames: BoneKeyFrame[] = []
1401
+ for (const keyFrame of this.animationFrames) {
1402
+ for (const boneFrame of keyFrame.boneFrames) {
1403
+ allBoneKeyFrames.push({
1404
+ boneName: boneFrame.boneName,
1405
+ time: keyFrame.time,
1406
+ rotation: boneFrame.rotation,
1407
+ })
1408
+ }
1409
+ }
1410
+
1411
+ const boneKeyFramesByBone = new Map<string, BoneKeyFrame[]>()
1412
+ for (const boneKeyFrame of allBoneKeyFrames) {
1413
+ if (!boneKeyFramesByBone.has(boneKeyFrame.boneName)) {
1414
+ boneKeyFramesByBone.set(boneKeyFrame.boneName, [])
1415
+ }
1416
+ boneKeyFramesByBone.get(boneKeyFrame.boneName)!.push(boneKeyFrame)
1417
+ }
1418
+
1419
+ for (const keyFrames of boneKeyFramesByBone.values()) {
1420
+ keyFrames.sort((a, b) => a.time - b.time)
1421
+ }
1422
+
1423
+ const time0Rotations: Array<{ boneName: string; rotation: Quat }> = []
1424
+ const bonesWithTime0 = new Set<string>()
1425
+ for (const [boneName, keyFrames] of boneKeyFramesByBone.entries()) {
1426
+ if (keyFrames.length > 0 && keyFrames[0].time === 0) {
1427
+ time0Rotations.push({
1428
+ boneName: boneName,
1429
+ rotation: keyFrames[0].rotation,
1430
+ })
1431
+ bonesWithTime0.add(boneName)
1432
+ }
1433
+ }
1434
+
1435
+ if (this.currentModel) {
1436
+ if (time0Rotations.length > 0) {
1437
+ const boneNames = time0Rotations.map((r) => r.boneName)
1438
+ const rotations = time0Rotations.map((r) => r.rotation)
1439
+ this.rotateBones(boneNames, rotations, 0)
1440
+ }
1441
+
1442
+ const skeleton = this.currentModel.getSkeleton()
1443
+ const bonesToReset: string[] = []
1444
+ for (const bone of skeleton.bones) {
1445
+ if (!bonesWithTime0.has(bone.name)) {
1446
+ bonesToReset.push(bone.name)
1447
+ }
1448
+ }
1449
+
1450
+ if (bonesToReset.length > 0) {
1451
+ const identityQuat = new Quat(0, 0, 0, 1)
1452
+ const identityQuats = new Array(bonesToReset.length).fill(identityQuat)
1453
+ this.rotateBones(bonesToReset, identityQuats, 0)
1454
+ }
1455
+
1456
+ this.currentModel.evaluatePose()
1457
+
1458
+ // Reset physics immediately and upload matrices to prevent A-pose flash
1459
+ if (this.physics) {
1460
+ const worldMats = this.currentModel.getBoneWorldMatrices()
1461
+ this.physics.reset(worldMats, this.currentModel.getBoneInverseBindMatrices())
1462
+
1463
+ // Upload matrices immediately so next frame shows correct pose
1464
+ this.device.queue.writeBuffer(
1465
+ this.worldMatrixBuffer!,
1466
+ 0,
1467
+ worldMats.buffer,
1468
+ worldMats.byteOffset,
1469
+ worldMats.byteLength
1470
+ )
1471
+ this.computeSkinMatrices()
1472
+ }
1473
+ }
1474
+ for (const [_, keyFrames] of boneKeyFramesByBone.entries()) {
1475
+ for (let i = 0; i < keyFrames.length; i++) {
1476
+ const boneKeyFrame = keyFrames[i]
1477
+ const previousBoneKeyFrame = i > 0 ? keyFrames[i - 1] : null
1478
+
1479
+ if (boneKeyFrame.time === 0) continue
1480
+
1481
+ let durationMs = 0
1482
+ if (i === 0) {
1483
+ durationMs = boneKeyFrame.time * 1000
1484
+ } else if (previousBoneKeyFrame) {
1485
+ durationMs = (boneKeyFrame.time - previousBoneKeyFrame.time) * 1000
1486
+ }
1487
+
1488
+ const scheduleTime = i > 0 && previousBoneKeyFrame ? previousBoneKeyFrame.time : 0
1489
+ const delayMs = scheduleTime * 1000
1490
+
1491
+ if (delayMs <= 0) {
1492
+ this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
1493
+ } else {
1494
+ const timeoutId = window.setTimeout(() => {
1495
+ this.rotateBones([boneKeyFrame.boneName], [boneKeyFrame.rotation], durationMs)
1496
+ }, delayMs)
1497
+ this.animationTimeouts.push(timeoutId)
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+
1503
+ public stopAnimation() {
1504
+ for (const timeoutId of this.animationTimeouts) {
1505
+ clearTimeout(timeoutId)
1506
+ }
1507
+ this.animationTimeouts = []
1508
+ }
1509
+
1378
1510
  public getStats(): EngineStats {
1379
1511
  return { ...this.stats }
1380
1512
  }
@@ -1405,6 +1537,7 @@ export class Engine {
1405
1537
 
1406
1538
  public dispose() {
1407
1539
  this.stopRenderLoop()
1540
+ this.stopAnimation()
1408
1541
  if (this.camera) this.camera.detachControl()
1409
1542
  if (this.resizeObserver) {
1410
1543
  this.resizeObserver.disconnect()
@@ -2153,20 +2286,14 @@ export class Engine {
2153
2286
  }
2154
2287
  }
2155
2288
 
2156
- // Update model pose and physics
2157
2289
  private updateModelPose(deltaTime: number) {
2158
- // Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
2159
2290
  this.currentModel!.evaluatePose()
2160
-
2161
- // Step 2: Get world matrices (still in CPU memory)
2162
2291
  const worldMats = this.currentModel!.getBoneWorldMatrices()
2163
2292
 
2164
- // Step 3: Physics modifies matrices in-place
2165
2293
  if (this.physics) {
2166
2294
  this.physics.step(deltaTime, worldMats, this.currentModel!.getBoneInverseBindMatrices())
2167
2295
  }
2168
2296
 
2169
- // Step 4: Upload ONCE with final result (animation + physics)
2170
2297
  this.device.queue.writeBuffer(
2171
2298
  this.worldMatrixBuffer!,
2172
2299
  0,
@@ -2174,8 +2301,6 @@ export class Engine {
2174
2301
  worldMats.byteOffset,
2175
2302
  worldMats.byteLength
2176
2303
  )
2177
-
2178
- // Step 5: GPU skinning
2179
2304
  this.computeSkinMatrices()
2180
2305
  }
2181
2306