reze-engine 0.10.0 → 0.10.1

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A minimal-dependency WebGPU engine for real-time MMD/PMX rendering. Only external dependency is Ammo.js for physics.
4
4
 
5
+ ![screenshot](./screenshot.png)
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
@@ -14,80 +16,113 @@ npm install reze-engine
14
16
  - VMD animation with IK solver and Bullet physics
15
17
  - Orbit camera with bone-follow mode
16
18
  - GPU picking (double-click/tap)
17
- - Ground plane with PCF shadow mapping, grid lines, and frosted texture
19
+ - Ground plane with PCF shadow mapping
18
20
  - Multi-model support
19
21
 
20
- ## Quick Start
22
+ ## Usage
21
23
 
22
24
  ```javascript
23
- import { Engine, Vec3 } from "reze-engine"
25
+ import { Engine, Vec3 } from "reze-engine";
24
26
 
25
27
  const engine = new Engine(canvas, {
26
28
  ambientColor: new Vec3(0.88, 0.92, 0.99),
27
29
  cameraDistance: 31.5, // MMD units (1 unit = 8 cm)
28
- })
29
- await engine.init()
30
-
31
- const model = await engine.loadModel("hero", "/models/hero/hero.pmx")
32
- await model.loadAnimation("idle", "/animations/idle.vmd")
33
- model.show("idle")
34
- model.play()
35
-
36
- engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0))
37
- engine.addGround({ width: 160, height: 160 })
38
- engine.runRenderLoop()
30
+ cameraTarget: new Vec3(0, 11.5, 0),
31
+ });
32
+ await engine.init();
33
+
34
+ const model = await engine.loadModel("hero", "/models/hero/hero.pmx");
35
+ await model.loadVmd("idle", "/animations/idle.vmd");
36
+ model.show("idle");
37
+ model.play();
38
+
39
+ engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0));
40
+ engine.addGround({ width: 160, height: 160 });
41
+ engine.runRenderLoop();
39
42
  ```
40
43
 
41
44
  ## API
42
45
 
46
+ One WebGPU **Engine** per page (singleton after `init()`). Load models via `engine.loadModel(path)` or `engine.loadModel(name, path)`.
47
+
43
48
  ### Engine
44
49
 
45
- | Method | Description |
46
- |--------|-------------|
47
- | `new Engine(canvas, options?)` | Create engine with optional config |
48
- | `engine.init()` | Initialize WebGPU device and context |
49
- | `engine.loadModel(path)` | Load PMX model (auto-named) |
50
- | `engine.loadModel(name, path)` | Load PMX model with name |
51
- | `engine.getModel(name)` | Get model by name |
52
- | `engine.getModelNames()` | List all model names |
53
- | `engine.removeModel(name)` | Remove model |
54
- | `engine.setMaterialVisible(model, mat, visible)` | Show/hide material |
55
- | `engine.toggleMaterialVisible(model, mat)` | Toggle material visibility |
56
- | `engine.setIKEnabled(enabled)` | Enable/disable IK globally |
57
- | `engine.setPhysicsEnabled(enabled)` | Enable/disable physics globally |
58
- | `engine.setCameraFollow(model, bone?, offset?)` | Orbit center tracks a bone |
59
- | `engine.setCameraTarget(vec3)` | Static camera target |
60
- | `engine.setCameraDistance(d)` | Set orbit radius |
61
- | `engine.setCameraAlpha(a)` | Set horizontal orbit angle |
62
- | `engine.setCameraBeta(b)` | Set vertical orbit angle |
63
- | `engine.addGround(options?)` | Add ground plane with shadows |
64
- | `engine.runRenderLoop(callback?)` | Start render loop |
65
- | `engine.stopRenderLoop()` | Stop render loop |
66
- | `engine.getStats()` | Returns `{ fps, frameTime }` |
67
- | `engine.dispose()` | Clean up all resources |
50
+ ```javascript
51
+ engine.init()
52
+ engine.loadModel(name, path)
53
+ engine.getModel(name)
54
+ engine.getModelNames()
55
+ engine.removeModel(name)
56
+
57
+ engine.setMaterialVisible(name, material, visible)
58
+ engine.toggleMaterialVisible(name, material)
59
+ engine.isMaterialVisible(name, material)
60
+
61
+ engine.setIKEnabled(enabled)
62
+ engine.setPhysicsEnabled(enabled)
63
+
64
+ engine.setCameraFollow(model, bone?, offset?)
65
+ engine.setCameraFollow(null)
66
+ engine.setCameraTarget(vec3)
67
+ engine.setCameraDistance(d)
68
+ engine.setCameraAlpha(a)
69
+ engine.setCameraBeta(b)
70
+
71
+ engine.addGround(options?)
72
+ engine.runRenderLoop(callback?)
73
+ engine.stopRenderLoop()
74
+ engine.getStats()
75
+ engine.dispose()
76
+ ```
68
77
 
69
78
  ### Model
70
79
 
71
- | Method | Description |
72
- |--------|-------------|
73
- | `model.loadAnimation(name, url)` | Load VMD animation |
74
- | `model.loadAnimation(name, clip)` | Load/replace animation clip directly |
75
- | `model.show(name)` | Set pose at time 0 (resets bones and morphs first) |
76
- | `model.play(name?)` | Play animation (queued if busy; named play resets bones/morphs first) |
77
- | `model.play(name, { priority?, loop? })` | Priority-aware play; `loop` wraps at end (`0` default/lowest priority) |
78
- | `model.pause()` | Pause playback |
79
- | `model.stop()` | Stop playback |
80
- | `model.seek(time)` | Seek to time |
81
- | `model.getAnimationProgress()` | `{ current, duration, percentage, animationName, looping, playing, paused }` — `current`/`duration` are seconds |
82
- | `model.getAnimationClip(name)` | Get loaded clip by name |
83
- | `model.rotateBones(rotations, ms?)` | Tween bone rotations |
84
- | `model.moveBones(translations, ms?)` | Tween bone translations |
85
- | `model.setMorphWeight(name, weight, ms?)` | Tween morph weight |
86
- | `model.resetAllBones()` | Reset to bind pose |
87
- | `model.resetAllMorphs()` | Reset all morph weights |
88
- | `model.getBoneWorldPosition(name)` | World position of bone |
89
-
90
- `AnimationClip` is frame-based: `frameCount` is the last keyframe frame index, keyframes store `frame`. Engine playback uses fixed 30 FPS. Looping is controlled via `play(name, { loop: true })`, not on the clip.
80
+ ```javascript
81
+ await model.loadVmd(name, url)
82
+ model.loadClip(name, clip)
83
+ model.show(name)
84
+ model.play(name)
85
+ model.play(name, { priority: 8 }) // higher number = higher priority (0 default/lowest)
86
+ model.play(name, { loop: true }) // repeat until stop/pause or another play
87
+ model.pause()
88
+ model.stop()
89
+ model.seek(time)
90
+ model.getAnimationProgress()
91
+ model.getClip(name)
92
+ model.exportVmd(name) // returns ArrayBuffer
93
+
94
+ model.rotateBones({ 首: quat, 頭: quat }, ms?)
95
+ model.moveBones({ センター: vec3 }, ms?)
96
+ model.setMorphWeight(name, weight, ms?)
97
+ model.resetAllBones()
98
+ model.resetAllMorphs()
99
+ model.getBoneWorldPosition(name)
100
+ ```
101
+
102
+ #### Animation data
103
+
104
+ `AnimationClip` holds keyframes only: bone/morph tracks keyed by `frame`, and `frameCount` (last keyframe index). Time advances at fixed `FPS` (see package export `FPS`, default 30).
105
+
106
+ #### VMD Export
107
+
108
+ `model.exportVmd(name)` serialises a loaded clip back to the VMD binary format and returns an `ArrayBuffer`. Bone and morph names are Shift-JIS encoded for compatibility with standard MMD tools.
109
+
110
+ ```javascript
111
+ const buffer = model.exportVmd("idle")
112
+ const blob = new Blob([buffer], { type: "application/octet-stream" })
113
+ const link = document.createElement("a")
114
+ link.href = URL.createObjectURL(blob)
115
+ link.download = "idle.vmd"
116
+ link.click()
117
+ ```
118
+
119
+ #### Playback
120
+
121
+ Call `model.play(name, options?)` to start or switch motion. `loop: true` makes the playhead wrap at the end of the clip until you stop, pause, or call `play` with something else. `priority` chooses which request wins when several clips compete.
122
+
123
+ #### Progress
124
+
125
+ `getAnimationProgress()` reports `current` and `duration` in seconds, plus `playing`, `paused`, `looping`, and related fields.
91
126
 
92
127
  ### Engine Options
93
128
 
@@ -112,25 +147,6 @@ engine.runRenderLoop()
112
147
 
113
148
  `constraintSolverKeywords` — joints whose name contains any keyword use the Bullet 2.75 constraint solver; all others keep the stable Ammo 2.82+ default. See [babylon-mmd: Fix Constraint Behavior](https://noname0310.github.io/babylon-mmd/docs/reference/runtime/apply-physics-to-mmd-models/#fix-constraint-behavior) for details.
114
149
 
115
- ### Ground Options
116
-
117
- ```javascript
118
- engine.addGround({
119
- width: 100, // ground plane width
120
- height: 100, // ground plane depth
121
- diffuseColor: Vec3, // base color (default: 0.8, 0.1, 1.0)
122
- fadeStart: 5.0, // distance where edge fade begins
123
- fadeEnd: 60.0, // distance where ground fully fades out
124
- shadowMapSize: 4096, // shadow map resolution
125
- shadowStrength: 1.0, // shadow darkness
126
- gridSpacing: 5.0, // world-space distance between grid lines
127
- gridLineWidth: 0.012, // thickness of grid lines
128
- gridLineOpacity: 0.4, // grid line visibility (0–1)
129
- gridLineColor: Vec3, // grid line color (default: 0.8, 0.8, 0.8)
130
- noiseStrength: 0.08, // frosted/matte micro-texture intensity
131
- })
132
- ```
133
-
134
150
  ## Projects Using This Engine
135
151
 
136
152
  - **[MiKaPo](https://mikapo.vercel.app)** — Real-time motion capture for MMD
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export { Vec3, Quat, Mat4 } from "./math";
4
4
  export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
5
5
  export { FPS } from "./animation";
6
6
  export { Physics, type PhysicsOptions } from "./physics";
7
+ export { VMDWriter } from "./vmd-writer";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export { Model } from "./model";
3
3
  export { Vec3, Quat, Mat4 } from "./math";
4
4
  export { FPS } from "./animation";
5
5
  export { Physics } from "./physics";
6
+ export { VMDWriter } from "./vmd-writer";
package/dist/model.d.ts CHANGED
@@ -145,11 +145,12 @@ export declare class Model {
145
145
  setMorphWeight(name: string, weight: number, durationMs?: number): void;
146
146
  private applyMorphs;
147
147
  private buildClipFromVmdKeyFrames;
148
- loadAnimation(animationName: string, source: string): Promise<void>;
149
- loadAnimation(animationName: string, source: AnimationClip): void;
148
+ loadVmd(name: string, url: string): Promise<void>;
149
+ loadClip(name: string, clip: AnimationClip): void;
150
150
  resetAllBones(): void;
151
151
  resetAllMorphs(): void;
152
- getAnimationClip(name: string): AnimationClip | null;
152
+ getClip(name: string): AnimationClip | null;
153
+ exportVmd(name: string): ArrayBuffer;
153
154
  play(): void;
154
155
  play(name: string): boolean;
155
156
  play(name: string, options?: AnimationPlayOptions): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -2,6 +2,7 @@ import { Mat4, Quat, Vec3 } from "./math";
2
2
  import { Engine } from "./engine";
3
3
  import { IKSolverSystem } from "./ik-solver";
4
4
  import { VMDLoader } from "./vmd-loader";
5
+ import { VMDWriter } from "./vmd-writer";
5
6
  import { AnimationState, interpolateControlPoints, rawInterpolationToBoneInterpolation, } from "./animation";
6
7
  const VERTEX_STRIDE = 8;
7
8
  export class Model {
@@ -578,16 +579,15 @@ export class Model {
578
579
  }
579
580
  return { boneTracks, morphTracks, frameCount: maxFrame };
580
581
  }
581
- loadAnimation(animationName, source) {
582
- if (typeof source !== "string") {
583
- this.animationState.loadAnimation(animationName, source);
584
- return;
585
- }
586
- return VMDLoader.load(source).then((vmdKeyFrames) => {
582
+ loadVmd(name, url) {
583
+ return VMDLoader.load(url).then((vmdKeyFrames) => {
587
584
  const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames);
588
- this.animationState.loadAnimation(animationName, clip);
585
+ this.animationState.loadAnimation(name, clip);
589
586
  });
590
587
  }
588
+ loadClip(name, clip) {
589
+ this.animationState.loadAnimation(name, clip);
590
+ }
591
591
  resetAllBones() {
592
592
  for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
593
593
  const localRot = this.runtimeSkeleton.localRotations[boneIdx];
@@ -605,9 +605,15 @@ export class Model {
605
605
  this.morphsDirty = true;
606
606
  this.applyMorphs();
607
607
  }
608
- getAnimationClip(name) {
608
+ getClip(name) {
609
609
  return this.animationState.getAnimationClip(name);
610
610
  }
611
+ exportVmd(name) {
612
+ const clip = this.animationState.getAnimationClip(name);
613
+ if (!clip)
614
+ throw new Error(`Animation clip "${name}" not found`);
615
+ return new VMDWriter().write(clip);
616
+ }
611
617
  play(name, options) {
612
618
  if (name === undefined) {
613
619
  this.animationState.play();
@@ -0,0 +1,5 @@
1
+ import { AnimationClip } from "./animation";
2
+ export declare class VMDWriter {
3
+ write(clip: AnimationClip): ArrayBuffer;
4
+ }
5
+ //# sourceMappingURL=vmd-writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vmd-writer.d.ts","sourceRoot":"","sources":["../src/vmd-writer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAA;AA+C9D,qBAAa,SAAS;IACpB,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,WAAW;CAgFxC"}
@@ -0,0 +1,162 @@
1
+ const VMD_HEADER = "Vocaloid Motion Data 0002";
2
+ const HEADER_SIZE = 30;
3
+ const MODEL_NAME_SIZE = 20;
4
+ const BONE_NAME_SIZE = 15;
5
+ const MORPH_NAME_SIZE = 15;
6
+ const BONE_FRAME_SIZE = BONE_NAME_SIZE + 4 + 12 + 16 + 64; // 111 bytes
7
+ const MORPH_FRAME_SIZE = MORPH_NAME_SIZE + 4 + 4; // 23 bytes
8
+ // Build a Unicode-to-Shift-JIS lookup by inverting the TextDecoder mapping.
9
+ let shiftJISTable = null;
10
+ function getShiftJISTable() {
11
+ if (shiftJISTable)
12
+ return shiftJISTable;
13
+ const decoder = new TextDecoder("shift-jis");
14
+ const map = new Map();
15
+ // Single-byte range
16
+ for (let i = 0; i < 256; i++) {
17
+ const char = decoder.decode(new Uint8Array([i]));
18
+ if (char !== "\ufffd")
19
+ map.set(char, [i]);
20
+ }
21
+ // Two-byte range (JIS X 0208)
22
+ for (let hi = 0x81; hi <= 0xfc; hi++) {
23
+ if (hi >= 0xa0 && hi <= 0xdf)
24
+ continue;
25
+ for (let lo = 0x40; lo <= 0xfc; lo++) {
26
+ if (lo === 0x7f)
27
+ continue;
28
+ const char = decoder.decode(new Uint8Array([hi, lo]));
29
+ if (char !== "\ufffd" && !map.has(char)) {
30
+ map.set(char, [hi, lo]);
31
+ }
32
+ }
33
+ }
34
+ shiftJISTable = map;
35
+ return map;
36
+ }
37
+ function encodeShiftJIS(str) {
38
+ const table = getShiftJISTable();
39
+ const bytes = [];
40
+ for (const char of str) {
41
+ const b = table.get(char);
42
+ if (b)
43
+ bytes.push(...b);
44
+ }
45
+ return new Uint8Array(bytes);
46
+ }
47
+ export class VMDWriter {
48
+ write(clip) {
49
+ let totalBoneFrames = 0;
50
+ for (const frames of clip.boneTracks.values()) {
51
+ totalBoneFrames += frames.length;
52
+ }
53
+ let totalMorphFrames = 0;
54
+ for (const frames of clip.morphTracks.values()) {
55
+ totalMorphFrames += frames.length;
56
+ }
57
+ const size = HEADER_SIZE +
58
+ MODEL_NAME_SIZE +
59
+ 4 + totalBoneFrames * BONE_FRAME_SIZE +
60
+ 4 + totalMorphFrames * MORPH_FRAME_SIZE;
61
+ const buffer = new ArrayBuffer(size);
62
+ const view = new DataView(buffer);
63
+ let offset = 0;
64
+ // Header (30 bytes, ASCII)
65
+ offset = writeFixedString(buffer, offset, VMD_HEADER, HEADER_SIZE);
66
+ // Model name (20 bytes, zeroed)
67
+ offset += MODEL_NAME_SIZE;
68
+ // Bone frame count
69
+ view.setUint32(offset, totalBoneFrames, true);
70
+ offset += 4;
71
+ // Bone frames
72
+ for (const frames of clip.boneTracks.values()) {
73
+ for (const kf of frames) {
74
+ // Bone name (15 bytes, Shift-JIS)
75
+ offset = writeFixedShiftJIS(buffer, offset, kf.boneName, BONE_NAME_SIZE);
76
+ // Frame number (u32 LE)
77
+ view.setUint32(offset, kf.frame, true);
78
+ offset += 4;
79
+ // Translation (3 x f32 LE)
80
+ view.setFloat32(offset, kf.translation.x, true);
81
+ offset += 4;
82
+ view.setFloat32(offset, kf.translation.y, true);
83
+ offset += 4;
84
+ view.setFloat32(offset, kf.translation.z, true);
85
+ offset += 4;
86
+ // Rotation quaternion (4 x f32 LE)
87
+ view.setFloat32(offset, kf.rotation.x, true);
88
+ offset += 4;
89
+ view.setFloat32(offset, kf.rotation.y, true);
90
+ offset += 4;
91
+ view.setFloat32(offset, kf.rotation.z, true);
92
+ offset += 4;
93
+ view.setFloat32(offset, kf.rotation.w, true);
94
+ offset += 4;
95
+ // Interpolation (64 bytes)
96
+ const raw = boneInterpolationToRaw(kf.interpolation);
97
+ new Uint8Array(buffer, offset, 64).set(raw);
98
+ offset += 64;
99
+ }
100
+ }
101
+ // Morph frame count
102
+ view.setUint32(offset, totalMorphFrames, true);
103
+ offset += 4;
104
+ // Morph frames
105
+ for (const frames of clip.morphTracks.values()) {
106
+ for (const kf of frames) {
107
+ // Morph name (15 bytes, Shift-JIS)
108
+ offset = writeFixedShiftJIS(buffer, offset, kf.morphName, MORPH_NAME_SIZE);
109
+ // Frame number (u32 LE)
110
+ view.setUint32(offset, kf.frame, true);
111
+ offset += 4;
112
+ // Weight (f32 LE)
113
+ view.setFloat32(offset, kf.weight, true);
114
+ offset += 4;
115
+ }
116
+ }
117
+ return buffer;
118
+ }
119
+ }
120
+ function writeFixedString(buffer, offset, str, maxBytes) {
121
+ const bytes = new Uint8Array(buffer, offset, maxBytes);
122
+ bytes.fill(0);
123
+ for (let i = 0; i < str.length && i < maxBytes; i++) {
124
+ bytes[i] = str.charCodeAt(i) & 0xff;
125
+ }
126
+ return offset + maxBytes;
127
+ }
128
+ function writeFixedShiftJIS(buffer, offset, str, maxBytes) {
129
+ const target = new Uint8Array(buffer, offset, maxBytes);
130
+ target.fill(0);
131
+ const encoded = encodeShiftJIS(str);
132
+ target.set(encoded.subarray(0, maxBytes));
133
+ return offset + maxBytes;
134
+ }
135
+ /**
136
+ * Convert BoneInterpolation back to the 64-byte raw VMD interpolation table.
137
+ * Exact inverse of rawInterpolationToBoneInterpolation in animation.ts.
138
+ */
139
+ function boneInterpolationToRaw(interp) {
140
+ const raw = new Uint8Array(64);
141
+ // Rotation: [{x: raw[0], y: raw[2]}, {x: raw[1], y: raw[3]}]
142
+ raw[0] = interp.rotation[0].x;
143
+ raw[1] = interp.rotation[1].x;
144
+ raw[2] = interp.rotation[0].y;
145
+ raw[3] = interp.rotation[1].y;
146
+ // TranslationX: [{x: raw[0], y: raw[4]}, {x: raw[8], y: raw[12]}]
147
+ // raw[0] already set by rotation (shared byte)
148
+ raw[4] = interp.translationX[0].y;
149
+ raw[8] = interp.translationX[1].x;
150
+ raw[12] = interp.translationX[1].y;
151
+ // TranslationY: [{x: raw[16], y: raw[20]}, {x: raw[24], y: raw[28]}]
152
+ raw[16] = interp.translationY[0].x;
153
+ raw[20] = interp.translationY[0].y;
154
+ raw[24] = interp.translationY[1].x;
155
+ raw[28] = interp.translationY[1].y;
156
+ // TranslationZ: [{x: raw[32], y: raw[36]}, {x: raw[40], y: raw[44]}]
157
+ raw[32] = interp.translationZ[0].x;
158
+ raw[36] = interp.translationZ[0].y;
159
+ raw[40] = interp.translationZ[1].x;
160
+ raw[44] = interp.translationZ[1].y;
161
+ return raw;
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -36,10 +36,11 @@
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
38
  "@fred3d/ammo": "^1.0.0",
39
- "@webgpu/types": "^0.1.66"
39
+ "@webgpu/types": "^0.1.66",
40
+ "encoding-japanese": "^2.2.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^20",
43
44
  "typescript": "^5"
44
45
  }
45
- }
46
+ }
package/src/index.ts CHANGED
@@ -12,3 +12,4 @@ export type {
12
12
  } from "./animation"
13
13
  export { FPS } from "./animation"
14
14
  export { Physics, type PhysicsOptions } from "./physics"
15
+ export { VMDWriter } from "./vmd-writer"
package/src/model.ts CHANGED
@@ -3,6 +3,7 @@ import { Engine } from "./engine"
3
3
  import { Rigidbody, Joint } from "./physics"
4
4
  import { IKSolverSystem } from "./ik-solver"
5
5
  import { VMDLoader, type VMDKeyFrame } from "./vmd-loader"
6
+ import { VMDWriter } from "./vmd-writer"
6
7
  import {
7
8
  AnimationClip,
8
9
  AnimationPlayOptions,
@@ -856,19 +857,17 @@ export class Model {
856
857
  return { boneTracks, morphTracks, frameCount: maxFrame }
857
858
  }
858
859
 
859
- loadAnimation(animationName: string, source: string): Promise<void>
860
- loadAnimation(animationName: string, source: AnimationClip): void
861
- loadAnimation(animationName: string, source: string | AnimationClip): Promise<void> | void {
862
- if (typeof source !== "string") {
863
- this.animationState.loadAnimation(animationName, source)
864
- return
865
- }
866
- return VMDLoader.load(source).then((vmdKeyFrames) => {
860
+ loadVmd(name: string, url: string): Promise<void> {
861
+ return VMDLoader.load(url).then((vmdKeyFrames) => {
867
862
  const clip = this.buildClipFromVmdKeyFrames(vmdKeyFrames)
868
- this.animationState.loadAnimation(animationName, clip)
863
+ this.animationState.loadAnimation(name, clip)
869
864
  })
870
865
  }
871
866
 
867
+ loadClip(name: string, clip: AnimationClip): void {
868
+ this.animationState.loadAnimation(name, clip)
869
+ }
870
+
872
871
  resetAllBones(): void {
873
872
  for (let boneIdx = 0; boneIdx < this.skeleton.bones.length; boneIdx++) {
874
873
  const localRot = this.runtimeSkeleton.localRotations[boneIdx]
@@ -889,10 +888,16 @@ export class Model {
889
888
  this.applyMorphs()
890
889
  }
891
890
 
892
- getAnimationClip(name: string): AnimationClip | null {
891
+ getClip(name: string): AnimationClip | null {
893
892
  return this.animationState.getAnimationClip(name)
894
893
  }
895
894
 
895
+ exportVmd(name: string): ArrayBuffer {
896
+ const clip = this.animationState.getAnimationClip(name)
897
+ if (!clip) throw new Error(`Animation clip "${name}" not found`)
898
+ return new VMDWriter().write(clip)
899
+ }
900
+
896
901
  play(): void
897
902
  play(name: string): boolean
898
903
  play(name: string, options?: AnimationPlayOptions): boolean
@@ -0,0 +1,180 @@
1
+ import { AnimationClip, BoneInterpolation } from "./animation"
2
+
3
+ const VMD_HEADER = "Vocaloid Motion Data 0002"
4
+ const HEADER_SIZE = 30
5
+ const MODEL_NAME_SIZE = 20
6
+ const BONE_NAME_SIZE = 15
7
+ const MORPH_NAME_SIZE = 15
8
+ const BONE_FRAME_SIZE = BONE_NAME_SIZE + 4 + 12 + 16 + 64 // 111 bytes
9
+ const MORPH_FRAME_SIZE = MORPH_NAME_SIZE + 4 + 4 // 23 bytes
10
+
11
+ // Build a Unicode-to-Shift-JIS lookup by inverting the TextDecoder mapping.
12
+ let shiftJISTable: Map<string, number[]> | null = null
13
+
14
+ function getShiftJISTable(): Map<string, number[]> {
15
+ if (shiftJISTable) return shiftJISTable
16
+ const decoder = new TextDecoder("shift-jis")
17
+ const map = new Map<string, number[]>()
18
+ // Single-byte range
19
+ for (let i = 0; i < 256; i++) {
20
+ const char = decoder.decode(new Uint8Array([i]))
21
+ if (char !== "\ufffd") map.set(char, [i])
22
+ }
23
+ // Two-byte range (JIS X 0208)
24
+ for (let hi = 0x81; hi <= 0xfc; hi++) {
25
+ if (hi >= 0xa0 && hi <= 0xdf) continue
26
+ for (let lo = 0x40; lo <= 0xfc; lo++) {
27
+ if (lo === 0x7f) continue
28
+ const char = decoder.decode(new Uint8Array([hi, lo]))
29
+ if (char !== "\ufffd" && !map.has(char)) {
30
+ map.set(char, [hi, lo])
31
+ }
32
+ }
33
+ }
34
+ shiftJISTable = map
35
+ return map
36
+ }
37
+
38
+ function encodeShiftJIS(str: string): Uint8Array {
39
+ const table = getShiftJISTable()
40
+ const bytes: number[] = []
41
+ for (const char of str) {
42
+ const b = table.get(char)
43
+ if (b) bytes.push(...b)
44
+ }
45
+ return new Uint8Array(bytes)
46
+ }
47
+
48
+ export class VMDWriter {
49
+ write(clip: AnimationClip): ArrayBuffer {
50
+ let totalBoneFrames = 0
51
+ for (const frames of clip.boneTracks.values()) {
52
+ totalBoneFrames += frames.length
53
+ }
54
+ let totalMorphFrames = 0
55
+ for (const frames of clip.morphTracks.values()) {
56
+ totalMorphFrames += frames.length
57
+ }
58
+
59
+ const size =
60
+ HEADER_SIZE +
61
+ MODEL_NAME_SIZE +
62
+ 4 + totalBoneFrames * BONE_FRAME_SIZE +
63
+ 4 + totalMorphFrames * MORPH_FRAME_SIZE
64
+
65
+ const buffer = new ArrayBuffer(size)
66
+ const view = new DataView(buffer)
67
+ let offset = 0
68
+
69
+ // Header (30 bytes, ASCII)
70
+ offset = writeFixedString(buffer, offset, VMD_HEADER, HEADER_SIZE)
71
+
72
+ // Model name (20 bytes, zeroed)
73
+ offset += MODEL_NAME_SIZE
74
+
75
+ // Bone frame count
76
+ view.setUint32(offset, totalBoneFrames, true)
77
+ offset += 4
78
+
79
+ // Bone frames
80
+ for (const frames of clip.boneTracks.values()) {
81
+ for (const kf of frames) {
82
+ // Bone name (15 bytes, Shift-JIS)
83
+ offset = writeFixedShiftJIS(buffer, offset, kf.boneName, BONE_NAME_SIZE)
84
+
85
+ // Frame number (u32 LE)
86
+ view.setUint32(offset, kf.frame, true)
87
+ offset += 4
88
+
89
+ // Translation (3 x f32 LE)
90
+ view.setFloat32(offset, kf.translation.x, true); offset += 4
91
+ view.setFloat32(offset, kf.translation.y, true); offset += 4
92
+ view.setFloat32(offset, kf.translation.z, true); offset += 4
93
+
94
+ // Rotation quaternion (4 x f32 LE)
95
+ view.setFloat32(offset, kf.rotation.x, true); offset += 4
96
+ view.setFloat32(offset, kf.rotation.y, true); offset += 4
97
+ view.setFloat32(offset, kf.rotation.z, true); offset += 4
98
+ view.setFloat32(offset, kf.rotation.w, true); offset += 4
99
+
100
+ // Interpolation (64 bytes)
101
+ const raw = boneInterpolationToRaw(kf.interpolation)
102
+ new Uint8Array(buffer, offset, 64).set(raw)
103
+ offset += 64
104
+ }
105
+ }
106
+
107
+ // Morph frame count
108
+ view.setUint32(offset, totalMorphFrames, true)
109
+ offset += 4
110
+
111
+ // Morph frames
112
+ for (const frames of clip.morphTracks.values()) {
113
+ for (const kf of frames) {
114
+ // Morph name (15 bytes, Shift-JIS)
115
+ offset = writeFixedShiftJIS(buffer, offset, kf.morphName, MORPH_NAME_SIZE)
116
+
117
+ // Frame number (u32 LE)
118
+ view.setUint32(offset, kf.frame, true)
119
+ offset += 4
120
+
121
+ // Weight (f32 LE)
122
+ view.setFloat32(offset, kf.weight, true)
123
+ offset += 4
124
+ }
125
+ }
126
+
127
+ return buffer
128
+ }
129
+ }
130
+
131
+ function writeFixedString(buffer: ArrayBuffer, offset: number, str: string, maxBytes: number): number {
132
+ const bytes = new Uint8Array(buffer, offset, maxBytes)
133
+ bytes.fill(0)
134
+ for (let i = 0; i < str.length && i < maxBytes; i++) {
135
+ bytes[i] = str.charCodeAt(i) & 0xff
136
+ }
137
+ return offset + maxBytes
138
+ }
139
+
140
+ function writeFixedShiftJIS(buffer: ArrayBuffer, offset: number, str: string, maxBytes: number): number {
141
+ const target = new Uint8Array(buffer, offset, maxBytes)
142
+ target.fill(0)
143
+ const encoded = encodeShiftJIS(str)
144
+ target.set(encoded.subarray(0, maxBytes))
145
+ return offset + maxBytes
146
+ }
147
+
148
+ /**
149
+ * Convert BoneInterpolation back to the 64-byte raw VMD interpolation table.
150
+ * Exact inverse of rawInterpolationToBoneInterpolation in animation.ts.
151
+ */
152
+ function boneInterpolationToRaw(interp: BoneInterpolation): Uint8Array {
153
+ const raw = new Uint8Array(64)
154
+
155
+ // Rotation: [{x: raw[0], y: raw[2]}, {x: raw[1], y: raw[3]}]
156
+ raw[0] = interp.rotation[0].x
157
+ raw[1] = interp.rotation[1].x
158
+ raw[2] = interp.rotation[0].y
159
+ raw[3] = interp.rotation[1].y
160
+
161
+ // TranslationX: [{x: raw[0], y: raw[4]}, {x: raw[8], y: raw[12]}]
162
+ // raw[0] already set by rotation (shared byte)
163
+ raw[4] = interp.translationX[0].y
164
+ raw[8] = interp.translationX[1].x
165
+ raw[12] = interp.translationX[1].y
166
+
167
+ // TranslationY: [{x: raw[16], y: raw[20]}, {x: raw[24], y: raw[28]}]
168
+ raw[16] = interp.translationY[0].x
169
+ raw[20] = interp.translationY[0].y
170
+ raw[24] = interp.translationY[1].x
171
+ raw[28] = interp.translationY[1].y
172
+
173
+ // TranslationZ: [{x: raw[32], y: raw[36]}, {x: raw[40], y: raw[44]}]
174
+ raw[32] = interp.translationZ[0].x
175
+ raw[36] = interp.translationZ[0].y
176
+ raw[40] = interp.translationZ[1].x
177
+ raw[44] = interp.translationZ[1].y
178
+
179
+ return raw
180
+ }