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.
- package/README.md +66 -66
- package/dist/bezier-interpolate.d.ts +15 -0
- package/dist/bezier-interpolate.d.ts.map +1 -0
- package/dist/bezier-interpolate.js +40 -0
- package/dist/engine.d.ts +4 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -30
- package/dist/engine_r.d.ts +132 -0
- package/dist/engine_r.d.ts.map +1 -0
- package/dist/engine_r.js +1489 -0
- package/dist/engine_ts.d.ts +143 -0
- package/dist/engine_ts.d.ts.map +1 -0
- package/dist/engine_ts.js +1575 -0
- package/dist/player.d.ts +64 -0
- package/dist/player.d.ts.map +1 -0
- package/dist/player.js +220 -0
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +31 -28
- package/src/ik-solver.ts +411 -411
- package/src/math.ts +584 -584
- package/src/physics.ts +742 -742
- package/src/vmd-loader.ts +276 -276
- package/dist/audio.d.ts +0 -29
- package/dist/audio.d.ts.map +0 -1
- package/dist/audio.js +0 -116
- package/dist/particles.d.ts +0 -67
- package/dist/particles.d.ts.map +0 -1
- package/dist/particles.js +0 -266
package/dist/player.d.ts
ADDED
|
@@ -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
|
+
}
|