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 +92 -76
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/model.d.ts +4 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +14 -8
- package/dist/vmd-writer.d.ts +5 -0
- package/dist/vmd-writer.d.ts.map +1 -0
- package/dist/vmd-writer.js +162 -0
- package/package.json +4 -3
- package/src/index.ts +1 -0
- package/src/model.ts +15 -10
- package/src/vmd-writer.ts +180 -0
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
|
+

|
|
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
|
|
19
|
+
- Ground plane with PCF shadow mapping
|
|
18
20
|
- Multi-model support
|
|
19
21
|
|
|
20
|
-
##
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await
|
|
33
|
-
model.
|
|
34
|
-
model.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
engine.
|
|
38
|
-
engine.
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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
|
-
|
|
149
|
-
|
|
148
|
+
loadVmd(name: string, url: string): Promise<void>;
|
|
149
|
+
loadClip(name: string, clip: AnimationClip): void;
|
|
150
150
|
resetAllBones(): void;
|
|
151
151
|
resetAllMorphs(): void;
|
|
152
|
-
|
|
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;
|
package/dist/model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEzC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;
|
|
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
|
-
|
|
582
|
-
|
|
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(
|
|
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
|
-
|
|
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 @@
|
|
|
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.
|
|
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
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
|
-
|
|
860
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|