reze-engine 0.3.12 → 0.3.14

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.
@@ -0,0 +1,64 @@
1
+ import { Quat, Vec3 } from "./math";
2
+ export interface AnimationPose {
3
+ boneRotations: Map<string, Quat>;
4
+ boneTranslations: Map<string, Vec3>;
5
+ morphWeights: Map<string, number>;
6
+ }
7
+ export interface AnimationProgress {
8
+ current: number;
9
+ duration: number;
10
+ percentage: number;
11
+ }
12
+ export declare class Player {
13
+ private frames;
14
+ private boneTracks;
15
+ private morphTracks;
16
+ private _duration;
17
+ private isPlaying;
18
+ private isPaused;
19
+ private _currentTime;
20
+ private startTime;
21
+ /**
22
+ * Load VMD animation file
23
+ */
24
+ loadVmd(vmdUrl: string): Promise<void>;
25
+ /**
26
+ * Process frames into tracks
27
+ */
28
+ private processFrames;
29
+ /**
30
+ * Start or resume playback
31
+ * Note: For iOS, this should be called synchronously from a user interaction event
32
+ */
33
+ play(): void;
34
+ /**
35
+ * Pause playback
36
+ */
37
+ pause(): void;
38
+ /**
39
+ * Stop playback and reset to beginning
40
+ */
41
+ stop(): void;
42
+ /**
43
+ * Seek to specific time
44
+ */
45
+ seek(time: number): void;
46
+ /**
47
+ * Update playback and return current pose
48
+ * Returns null if not playing, but returns current pose if paused
49
+ */
50
+ update(currentRealTime: number): AnimationPose | null;
51
+ /**
52
+ * Get pose at specific time (pure function)
53
+ */
54
+ getPoseAtTime(time: number): AnimationPose;
55
+ /**
56
+ * Get current playback progress
57
+ */
58
+ getProgress(): AnimationProgress;
59
+ get currentTime(): number;
60
+ get duration(): number;
61
+ get isPlayingState(): boolean;
62
+ get isPausedState(): boolean;
63
+ }
64
+ //# sourceMappingURL=player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../src/player.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAqB,MAAM,QAAQ,CAAA;AAGtD,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAChC,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IACnC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,MAAM;IAEjB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAwE;IAC1F,OAAO,CAAC,WAAW,CAA0E;IAC7F,OAAO,CAAC,SAAS,CAAY;IAG7B,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,YAAY,CAAY;IAGhC,OAAO,CAAC,SAAS,CAAY;IAE7B;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5C;;OAEG;IACH,OAAO,CAAC,aAAa;IAsDrB;;;OAGG;IACH,IAAI,IAAI,IAAI;IASZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAKb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASxB;;;OAGG;IACH,MAAM,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAwBrD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IA6F1C;;OAEG;IACH,WAAW,IAAI,iBAAiB;IAQhC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;CACF"}
package/dist/player.js ADDED
@@ -0,0 +1,220 @@
1
+ import { Quat, Vec3, bezierInterpolate } from "./math";
2
+ import { VMDLoader } from "./vmd-loader";
3
+ export class Player {
4
+ constructor() {
5
+ // Animation data
6
+ this.frames = [];
7
+ this.boneTracks = new Map();
8
+ this.morphTracks = new Map();
9
+ this._duration = 0;
10
+ // Playback state
11
+ this.isPlaying = false;
12
+ this.isPaused = false;
13
+ this._currentTime = 0;
14
+ // Timing
15
+ this.startTime = 0; // Real-time when playback started
16
+ }
17
+ /**
18
+ * Load VMD animation file
19
+ */
20
+ async loadVmd(vmdUrl) {
21
+ // Load animation
22
+ this.frames = await VMDLoader.load(vmdUrl);
23
+ this.processFrames();
24
+ }
25
+ /**
26
+ * Process frames into tracks
27
+ */
28
+ processFrames() {
29
+ // Helper to group frames by name and sort by time
30
+ const groupFrames = (items) => {
31
+ const tracks = new Map();
32
+ for (const { item, name, time } of items) {
33
+ if (!tracks.has(name))
34
+ tracks.set(name, []);
35
+ tracks.get(name).push({ item, time });
36
+ }
37
+ for (const keyFrames of tracks.values()) {
38
+ keyFrames.sort((a, b) => a.time - b.time);
39
+ }
40
+ return tracks;
41
+ };
42
+ // Collect all bone and morph frames
43
+ const boneItems = [];
44
+ const morphItems = [];
45
+ for (const keyFrame of this.frames) {
46
+ for (const boneFrame of keyFrame.boneFrames) {
47
+ boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time });
48
+ }
49
+ for (const morphFrame of keyFrame.morphFrames) {
50
+ morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time });
51
+ }
52
+ }
53
+ // Transform to expected format
54
+ this.boneTracks = new Map();
55
+ for (const [name, frames] of groupFrames(boneItems).entries()) {
56
+ this.boneTracks.set(name, frames.map((f) => ({ boneFrame: f.item, time: f.time })));
57
+ }
58
+ this.morphTracks = new Map();
59
+ for (const [name, frames] of groupFrames(morphItems).entries()) {
60
+ this.morphTracks.set(name, frames.map((f) => ({ morphFrame: f.item, time: f.time })));
61
+ }
62
+ // Calculate duration from all tracks
63
+ const allTracks = [...this.boneTracks.values(), ...this.morphTracks.values()];
64
+ this._duration = allTracks.reduce((max, keyFrames) => {
65
+ const lastTime = keyFrames[keyFrames.length - 1]?.time ?? 0;
66
+ return Math.max(max, lastTime);
67
+ }, 0);
68
+ }
69
+ /**
70
+ * Start or resume playback
71
+ * Note: For iOS, this should be called synchronously from a user interaction event
72
+ */
73
+ play() {
74
+ if (this.frames.length === 0)
75
+ return;
76
+ this.isPaused = false;
77
+ this.startTime = performance.now() - this._currentTime * 1000;
78
+ this.isPlaying = true;
79
+ }
80
+ /**
81
+ * Pause playback
82
+ */
83
+ pause() {
84
+ if (!this.isPlaying || this.isPaused)
85
+ return;
86
+ this.isPaused = true;
87
+ }
88
+ /**
89
+ * Stop playback and reset to beginning
90
+ */
91
+ stop() {
92
+ this.isPlaying = false;
93
+ this.isPaused = false;
94
+ this._currentTime = 0;
95
+ this.startTime = 0;
96
+ }
97
+ /**
98
+ * Seek to specific time
99
+ */
100
+ seek(time) {
101
+ const clampedTime = Math.max(0, Math.min(time, this._duration));
102
+ this._currentTime = clampedTime;
103
+ if (this.isPlaying && !this.isPaused) {
104
+ this.startTime = performance.now() - clampedTime * 1000;
105
+ }
106
+ }
107
+ /**
108
+ * Update playback and return current pose
109
+ * Returns null if not playing, but returns current pose if paused
110
+ */
111
+ update(currentRealTime) {
112
+ if (!this.isPlaying || this.frames.length === 0) {
113
+ return null;
114
+ }
115
+ // If paused, return current pose at paused time (no time update)
116
+ if (this.isPaused) {
117
+ return this.getPoseAtTime(this._currentTime);
118
+ }
119
+ // Calculate current animation time
120
+ const elapsedSeconds = (currentRealTime - this.startTime) / 1000;
121
+ this._currentTime = elapsedSeconds;
122
+ // Check if animation ended
123
+ if (this._currentTime >= this._duration) {
124
+ this._currentTime = this._duration;
125
+ this.pause(); // Auto-pause at end
126
+ return this.getPoseAtTime(this._currentTime);
127
+ }
128
+ return this.getPoseAtTime(this._currentTime);
129
+ }
130
+ /**
131
+ * Get pose at specific time (pure function)
132
+ */
133
+ getPoseAtTime(time) {
134
+ const pose = {
135
+ boneRotations: new Map(),
136
+ boneTranslations: new Map(),
137
+ morphWeights: new Map(),
138
+ };
139
+ // Generic binary search for upper bound
140
+ const upperBound = (time, keyFrames) => {
141
+ let left = 0, right = keyFrames.length;
142
+ while (left < right) {
143
+ const mid = Math.floor((left + right) / 2);
144
+ if (keyFrames[mid].time <= time)
145
+ left = mid + 1;
146
+ else
147
+ right = mid;
148
+ }
149
+ return left;
150
+ };
151
+ // Process bone tracks
152
+ for (const [boneName, keyFrames] of this.boneTracks.entries()) {
153
+ if (keyFrames.length === 0)
154
+ continue;
155
+ const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
156
+ const idx = upperBound(clampedTime, keyFrames) - 1;
157
+ if (idx < 0)
158
+ continue;
159
+ const frameA = keyFrames[idx].boneFrame;
160
+ const frameB = keyFrames[idx + 1]?.boneFrame;
161
+ if (!frameB) {
162
+ pose.boneRotations.set(boneName, frameA.rotation);
163
+ pose.boneTranslations.set(boneName, frameA.translation);
164
+ }
165
+ else {
166
+ const timeA = keyFrames[idx].time;
167
+ const timeB = keyFrames[idx + 1].time;
168
+ const gradient = (clampedTime - timeA) / (timeB - timeA);
169
+ const interp = frameB.interpolation;
170
+ pose.boneRotations.set(boneName, Quat.slerp(frameA.rotation, frameB.rotation, bezierInterpolate(interp[0] / 127, interp[1] / 127, interp[2] / 127, interp[3] / 127, gradient)));
171
+ const lerp = (a, b, w) => a + (b - a) * w;
172
+ const getWeight = (offset) => bezierInterpolate(interp[offset] / 127, interp[offset + 8] / 127, interp[offset + 4] / 127, interp[offset + 12] / 127, gradient);
173
+ pose.boneTranslations.set(boneName, new Vec3(lerp(frameA.translation.x, frameB.translation.x, getWeight(0)), lerp(frameA.translation.y, frameB.translation.y, getWeight(16)), lerp(frameA.translation.z, frameB.translation.z, getWeight(32))));
174
+ }
175
+ }
176
+ // Process morph tracks
177
+ for (const [morphName, keyFrames] of this.morphTracks.entries()) {
178
+ if (keyFrames.length === 0)
179
+ continue;
180
+ const clampedTime = Math.max(keyFrames[0].time, Math.min(keyFrames[keyFrames.length - 1].time, time));
181
+ const idx = upperBound(clampedTime, keyFrames) - 1;
182
+ if (idx < 0)
183
+ continue;
184
+ const frameA = keyFrames[idx].morphFrame;
185
+ const frameB = keyFrames[idx + 1]?.morphFrame;
186
+ if (!frameB) {
187
+ pose.morphWeights.set(morphName, frameA.weight);
188
+ }
189
+ else {
190
+ const timeA = keyFrames[idx].time;
191
+ const timeB = keyFrames[idx + 1].time;
192
+ const gradient = (clampedTime - timeA) / (timeB - timeA);
193
+ pose.morphWeights.set(morphName, frameA.weight + (frameB.weight - frameA.weight) * gradient);
194
+ }
195
+ }
196
+ return pose;
197
+ }
198
+ /**
199
+ * Get current playback progress
200
+ */
201
+ getProgress() {
202
+ return {
203
+ current: this._currentTime,
204
+ duration: this._duration,
205
+ percentage: this._duration > 0 ? (this._currentTime / this._duration) * 100 : 0,
206
+ };
207
+ }
208
+ get currentTime() {
209
+ return this._currentTime;
210
+ }
211
+ get duration() {
212
+ return this._duration;
213
+ }
214
+ get isPlayingState() {
215
+ return this.isPlaying && !this.isPaused;
216
+ }
217
+ get isPausedState() {
218
+ return this.isPaused;
219
+ }
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",