reze-engine 0.3.4 → 0.3.6
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/ammo-loader.d.ts +0 -1
- package/dist/ammo-loader.d.ts.map +1 -1
- package/dist/ammo-loader.js +0 -3
- package/dist/engine.d.ts +2 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +44 -100
- package/dist/ik-solver.d.ts +7 -2
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/ik-solver.js +110 -137
- package/dist/math.d.ts +10 -6
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +36 -62
- package/dist/model.d.ts +1 -19
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +24 -93
- package/dist/player.d.ts +6 -20
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +89 -193
- package/package.json +1 -1
- package/src/ammo-loader.ts +0 -4
- package/src/engine.ts +54 -101
- package/src/ik-solver.ts +121 -178
- package/src/math.ts +43 -82
- package/src/model.ts +26 -101
- package/src/player.ts +116 -212
- package/src/bezier-interpolate.ts +0 -47
- package/src/ik.ts +0 -449
package/dist/player.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { bezierInterpolate } from "./
|
|
2
|
-
import { Quat, Vec3 } from "./math";
|
|
1
|
+
import { Quat, Vec3, bezierInterpolate } from "./math";
|
|
3
2
|
import { VMDLoader } from "./vmd-loader";
|
|
4
3
|
export class Player {
|
|
5
4
|
constructor() {
|
|
@@ -7,15 +6,13 @@ export class Player {
|
|
|
7
6
|
this.frames = [];
|
|
8
7
|
this.boneTracks = new Map();
|
|
9
8
|
this.morphTracks = new Map();
|
|
10
|
-
this.
|
|
9
|
+
this._duration = 0;
|
|
11
10
|
// Playback state
|
|
12
11
|
this.isPlaying = false;
|
|
13
12
|
this.isPaused = false;
|
|
14
|
-
this.
|
|
13
|
+
this._currentTime = 0;
|
|
15
14
|
// Timing
|
|
16
15
|
this.startTime = 0; // Real-time when playback started
|
|
17
|
-
this.pausedTime = 0; // Accumulated paused duration
|
|
18
|
-
this.pauseStartTime = 0;
|
|
19
16
|
}
|
|
20
17
|
/**
|
|
21
18
|
* Load VMD animation file
|
|
@@ -29,68 +26,45 @@ export class Player {
|
|
|
29
26
|
* Process frames into tracks
|
|
30
27
|
*/
|
|
31
28
|
processFrames() {
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
for (const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
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 });
|
|
40
36
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const { boneFrame, time } of allBoneKeyFrames) {
|
|
44
|
-
if (!boneKeyFramesByBone.has(boneFrame.boneName)) {
|
|
45
|
-
boneKeyFramesByBone.set(boneFrame.boneName, []);
|
|
37
|
+
for (const keyFrames of tracks.values()) {
|
|
38
|
+
keyFrames.sort((a, b) => a.time - b.time);
|
|
46
39
|
}
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Process morph frames
|
|
53
|
-
const allMorphKeyFrames = [];
|
|
40
|
+
return tracks;
|
|
41
|
+
};
|
|
42
|
+
// Collect all bone and morph frames
|
|
43
|
+
const boneItems = [];
|
|
44
|
+
const morphItems = [];
|
|
54
45
|
for (const keyFrame of this.frames) {
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
morphFrame,
|
|
58
|
-
time: keyFrame.time,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const morphKeyFramesByMorph = new Map();
|
|
63
|
-
for (const { morphFrame, time } of allMorphKeyFrames) {
|
|
64
|
-
if (!morphKeyFramesByMorph.has(morphFrame.morphName)) {
|
|
65
|
-
morphKeyFramesByMorph.set(morphFrame.morphName, []);
|
|
46
|
+
for (const boneFrame of keyFrame.boneFrames) {
|
|
47
|
+
boneItems.push({ item: boneFrame, name: boneFrame.boneName, time: keyFrame.time });
|
|
66
48
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const keyFrames of morphKeyFramesByMorph.values()) {
|
|
70
|
-
keyFrames.sort((a, b) => a.time - b.time);
|
|
71
|
-
}
|
|
72
|
-
// Store tracks
|
|
73
|
-
this.boneTracks = boneKeyFramesByBone;
|
|
74
|
-
this.morphTracks = morphKeyFramesByMorph;
|
|
75
|
-
// Calculate animation duration from max frame time
|
|
76
|
-
let maxFrameTime = 0;
|
|
77
|
-
for (const keyFrames of this.boneTracks.values()) {
|
|
78
|
-
if (keyFrames.length > 0) {
|
|
79
|
-
const lastTime = keyFrames[keyFrames.length - 1].time;
|
|
80
|
-
if (lastTime > maxFrameTime) {
|
|
81
|
-
maxFrameTime = lastTime;
|
|
82
|
-
}
|
|
49
|
+
for (const morphFrame of keyFrame.morphFrames) {
|
|
50
|
+
morphItems.push({ item: morphFrame, name: morphFrame.morphName, time: keyFrame.time });
|
|
83
51
|
}
|
|
84
52
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
maxFrameTime = lastTime;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
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 })));
|
|
92
57
|
}
|
|
93
|
-
this.
|
|
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);
|
|
94
68
|
}
|
|
95
69
|
/**
|
|
96
70
|
* Start or resume playback
|
|
@@ -99,17 +73,8 @@ export class Player {
|
|
|
99
73
|
play() {
|
|
100
74
|
if (this.frames.length === 0)
|
|
101
75
|
return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.isPaused = false;
|
|
105
|
-
// Adjust start time so current time calculation continues smoothly
|
|
106
|
-
this.startTime = performance.now() - this.currentTime * 1000;
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
// Start from beginning or current seek position
|
|
110
|
-
this.startTime = performance.now() - this.currentTime * 1000;
|
|
111
|
-
this.pausedTime = 0;
|
|
112
|
-
}
|
|
76
|
+
this.isPaused = false;
|
|
77
|
+
this.startTime = performance.now() - this._currentTime * 1000;
|
|
113
78
|
this.isPlaying = true;
|
|
114
79
|
}
|
|
115
80
|
/**
|
|
@@ -119,7 +84,6 @@ export class Player {
|
|
|
119
84
|
if (!this.isPlaying || this.isPaused)
|
|
120
85
|
return;
|
|
121
86
|
this.isPaused = true;
|
|
122
|
-
this.pauseStartTime = performance.now();
|
|
123
87
|
}
|
|
124
88
|
/**
|
|
125
89
|
* Stop playback and reset to beginning
|
|
@@ -127,20 +91,17 @@ export class Player {
|
|
|
127
91
|
stop() {
|
|
128
92
|
this.isPlaying = false;
|
|
129
93
|
this.isPaused = false;
|
|
130
|
-
this.
|
|
94
|
+
this._currentTime = 0;
|
|
131
95
|
this.startTime = 0;
|
|
132
|
-
this.pausedTime = 0;
|
|
133
96
|
}
|
|
134
97
|
/**
|
|
135
98
|
* Seek to specific time
|
|
136
99
|
*/
|
|
137
100
|
seek(time) {
|
|
138
|
-
const clampedTime = Math.max(0, Math.min(time, this.
|
|
139
|
-
this.
|
|
140
|
-
// Adjust start time if playing
|
|
101
|
+
const clampedTime = Math.max(0, Math.min(time, this._duration));
|
|
102
|
+
this._currentTime = clampedTime;
|
|
141
103
|
if (this.isPlaying && !this.isPaused) {
|
|
142
104
|
this.startTime = performance.now() - clampedTime * 1000;
|
|
143
|
-
this.pausedTime = 0;
|
|
144
105
|
}
|
|
145
106
|
}
|
|
146
107
|
/**
|
|
@@ -153,18 +114,18 @@ export class Player {
|
|
|
153
114
|
}
|
|
154
115
|
// If paused, return current pose at paused time (no time update)
|
|
155
116
|
if (this.isPaused) {
|
|
156
|
-
return this.getPoseAtTime(this.
|
|
117
|
+
return this.getPoseAtTime(this._currentTime);
|
|
157
118
|
}
|
|
158
119
|
// Calculate current animation time
|
|
159
120
|
const elapsedSeconds = (currentRealTime - this.startTime) / 1000;
|
|
160
|
-
this.
|
|
121
|
+
this._currentTime = elapsedSeconds;
|
|
161
122
|
// Check if animation ended
|
|
162
|
-
if (this.
|
|
163
|
-
this.
|
|
123
|
+
if (this._currentTime >= this._duration) {
|
|
124
|
+
this._currentTime = this._duration;
|
|
164
125
|
this.pause(); // Auto-pause at end
|
|
165
|
-
return this.getPoseAtTime(this.
|
|
126
|
+
return this.getPoseAtTime(this._currentTime);
|
|
166
127
|
}
|
|
167
|
-
return this.getPoseAtTime(this.
|
|
128
|
+
return this.getPoseAtTime(this._currentTime);
|
|
168
129
|
}
|
|
169
130
|
/**
|
|
170
131
|
* Get pose at specific time (pure function)
|
|
@@ -175,114 +136,61 @@ export class Player {
|
|
|
175
136
|
boneTranslations: new Map(),
|
|
176
137
|
morphWeights: new Map(),
|
|
177
138
|
};
|
|
178
|
-
//
|
|
179
|
-
const
|
|
180
|
-
let left = 0;
|
|
181
|
-
let right = keyFrames.length;
|
|
139
|
+
// Generic binary search for upper bound
|
|
140
|
+
const upperBound = (time, keyFrames) => {
|
|
141
|
+
let left = 0, right = keyFrames.length;
|
|
182
142
|
while (left < right) {
|
|
183
143
|
const mid = Math.floor((left + right) / 2);
|
|
184
|
-
if (keyFrames[mid].time <= time)
|
|
144
|
+
if (keyFrames[mid].time <= time)
|
|
185
145
|
left = mid + 1;
|
|
186
|
-
|
|
187
|
-
else {
|
|
146
|
+
else
|
|
188
147
|
right = mid;
|
|
189
|
-
}
|
|
190
148
|
}
|
|
191
149
|
return left;
|
|
192
150
|
};
|
|
193
|
-
// Process
|
|
151
|
+
// Process bone tracks
|
|
194
152
|
for (const [boneName, keyFrames] of this.boneTracks.entries()) {
|
|
195
153
|
if (keyFrames.length === 0)
|
|
196
154
|
continue;
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time));
|
|
201
|
-
const upperBoundIndex = upperBoundFrameIndex(clampedFrameTime, keyFrames);
|
|
202
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1;
|
|
203
|
-
if (upperBoundIndexMinusOne < 0)
|
|
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)
|
|
204
158
|
continue;
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
pose.
|
|
210
|
-
pose.boneTranslations.set(boneName, boneFrameA.translation);
|
|
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);
|
|
211
164
|
}
|
|
212
165
|
else {
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
interp[2] / 127, // y1
|
|
222
|
-
interp[3] / 127, // y2
|
|
223
|
-
gradient);
|
|
224
|
-
const interpolatedRotation = Quat.slerp(boneFrameA.rotation, boneFrameB.rotation, rotWeight);
|
|
225
|
-
// Interpolate translation using Bezier (separate curves for X, Y, Z)
|
|
226
|
-
const xWeight = bezierInterpolate(interp[0] / 127, // X_x1
|
|
227
|
-
interp[8] / 127, // X_x2
|
|
228
|
-
interp[4] / 127, // X_y1
|
|
229
|
-
interp[12] / 127, // X_y2
|
|
230
|
-
gradient);
|
|
231
|
-
const yWeight = bezierInterpolate(interp[16] / 127, // Y_x1
|
|
232
|
-
interp[24] / 127, // Y_x2
|
|
233
|
-
interp[20] / 127, // Y_y1
|
|
234
|
-
interp[28] / 127, // Y_y2
|
|
235
|
-
gradient);
|
|
236
|
-
const zWeight = bezierInterpolate(interp[32] / 127, // Z_x1
|
|
237
|
-
interp[40] / 127, // Z_x2
|
|
238
|
-
interp[36] / 127, // Z_y1
|
|
239
|
-
interp[44] / 127, // Z_y2
|
|
240
|
-
gradient);
|
|
241
|
-
const interpolatedTranslation = new Vec3(boneFrameA.translation.x + (boneFrameB.translation.x - boneFrameA.translation.x) * xWeight, boneFrameA.translation.y + (boneFrameB.translation.y - boneFrameA.translation.y) * yWeight, boneFrameA.translation.z + (boneFrameB.translation.z - boneFrameA.translation.z) * zWeight);
|
|
242
|
-
pose.boneRotations.set(boneName, interpolatedRotation);
|
|
243
|
-
pose.boneTranslations.set(boneName, interpolatedTranslation);
|
|
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))));
|
|
244
174
|
}
|
|
245
175
|
}
|
|
246
|
-
//
|
|
247
|
-
const upperBoundMorphIndex = (time, keyFrames) => {
|
|
248
|
-
let left = 0;
|
|
249
|
-
let right = keyFrames.length;
|
|
250
|
-
while (left < right) {
|
|
251
|
-
const mid = Math.floor((left + right) / 2);
|
|
252
|
-
if (keyFrames[mid].time <= time) {
|
|
253
|
-
left = mid + 1;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
right = mid;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return left;
|
|
260
|
-
};
|
|
261
|
-
// Process each morph track
|
|
176
|
+
// Process morph tracks
|
|
262
177
|
for (const [morphName, keyFrames] of this.morphTracks.entries()) {
|
|
263
178
|
if (keyFrames.length === 0)
|
|
264
179
|
continue;
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
const clampedFrameTime = Math.max(startTime, Math.min(endTime, time));
|
|
269
|
-
const upperBoundIndex = upperBoundMorphIndex(clampedFrameTime, keyFrames);
|
|
270
|
-
const upperBoundIndexMinusOne = upperBoundIndex - 1;
|
|
271
|
-
if (upperBoundIndexMinusOne < 0)
|
|
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)
|
|
272
183
|
continue;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
pose.morphWeights.set(morphName, morphFrameA.weight);
|
|
184
|
+
const frameA = keyFrames[idx].morphFrame;
|
|
185
|
+
const frameB = keyFrames[idx + 1]?.morphFrame;
|
|
186
|
+
if (!frameB) {
|
|
187
|
+
pose.morphWeights.set(morphName, frameA.weight);
|
|
278
188
|
}
|
|
279
189
|
else {
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
const interpolatedWeight = morphFrameA.weight + (morphFrameB.weight - morphFrameA.weight) * gradient;
|
|
285
|
-
pose.morphWeights.set(morphName, interpolatedWeight);
|
|
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);
|
|
286
194
|
}
|
|
287
195
|
}
|
|
288
196
|
return pose;
|
|
@@ -292,33 +200,21 @@ export class Player {
|
|
|
292
200
|
*/
|
|
293
201
|
getProgress() {
|
|
294
202
|
return {
|
|
295
|
-
current: this.
|
|
296
|
-
duration: this.
|
|
297
|
-
percentage: this.
|
|
203
|
+
current: this._currentTime,
|
|
204
|
+
duration: this._duration,
|
|
205
|
+
percentage: this._duration > 0 ? (this._currentTime / this._duration) * 100 : 0,
|
|
298
206
|
};
|
|
299
207
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
*/
|
|
303
|
-
getCurrentTime() {
|
|
304
|
-
return this.currentTime;
|
|
208
|
+
get currentTime() {
|
|
209
|
+
return this._currentTime;
|
|
305
210
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
*/
|
|
309
|
-
getDuration() {
|
|
310
|
-
return this.duration;
|
|
211
|
+
get duration() {
|
|
212
|
+
return this._duration;
|
|
311
213
|
}
|
|
312
|
-
|
|
313
|
-
* Check if playing
|
|
314
|
-
*/
|
|
315
|
-
isPlayingState() {
|
|
214
|
+
get isPlayingState() {
|
|
316
215
|
return this.isPlaying && !this.isPaused;
|
|
317
216
|
}
|
|
318
|
-
|
|
319
|
-
* Check if paused
|
|
320
|
-
*/
|
|
321
|
-
isPausedState() {
|
|
217
|
+
get isPausedState() {
|
|
322
218
|
return this.isPaused;
|
|
323
219
|
}
|
|
324
220
|
}
|
package/package.json
CHANGED
package/src/ammo-loader.ts
CHANGED
package/src/engine.ts
CHANGED
|
@@ -135,7 +135,6 @@ export class Engine {
|
|
|
135
135
|
|
|
136
136
|
private player: Player = new Player()
|
|
137
137
|
private hasAnimation = false // Set to true when loadAnimation is called
|
|
138
|
-
private animationStartTime: number = 0 // Track when animation first started (for A-pose prevention)
|
|
139
138
|
|
|
140
139
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
141
140
|
this.canvas = canvas
|
|
@@ -1317,8 +1316,8 @@ export class Engine {
|
|
|
1317
1316
|
public playAnimation() {
|
|
1318
1317
|
if (!this.hasAnimation || !this.currentModel) return
|
|
1319
1318
|
|
|
1320
|
-
const wasPaused = this.player.isPausedState
|
|
1321
|
-
const wasPlaying = this.player.isPlayingState
|
|
1319
|
+
const wasPaused = this.player.isPausedState
|
|
1320
|
+
const wasPlaying = this.player.isPlayingState
|
|
1322
1321
|
|
|
1323
1322
|
// Only reset pose and physics if starting from beginning (not resuming)
|
|
1324
1323
|
if (!wasPlaying && !wasPaused) {
|
|
@@ -1365,9 +1364,6 @@ export class Engine {
|
|
|
1365
1364
|
|
|
1366
1365
|
// Start playback (or resume if paused)
|
|
1367
1366
|
this.player.play()
|
|
1368
|
-
if (this.animationStartTime === 0) {
|
|
1369
|
-
this.animationStartTime = performance.now()
|
|
1370
|
-
}
|
|
1371
1367
|
}
|
|
1372
1368
|
|
|
1373
1369
|
public stopAnimation() {
|
|
@@ -1659,23 +1655,7 @@ export class Engine {
|
|
|
1659
1655
|
const materialAlpha = mat.diffuse[3]
|
|
1660
1656
|
const isTransparent = materialAlpha < 1.0 - Engine.TRANSPARENCY_EPSILON
|
|
1661
1657
|
|
|
1662
|
-
|
|
1663
|
-
const materialUniformData = new Float32Array(8)
|
|
1664
|
-
materialUniformData[0] = materialAlpha
|
|
1665
|
-
materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
|
|
1666
|
-
materialUniformData[2] = this.rimLightIntensity
|
|
1667
|
-
materialUniformData[3] = 0.0 // _padding1
|
|
1668
|
-
materialUniformData[4] = 1.0 // rimColor.r
|
|
1669
|
-
materialUniformData[5] = 1.0 // rimColor.g
|
|
1670
|
-
materialUniformData[6] = 1.0 // rimColor.b
|
|
1671
|
-
materialUniformData[7] = 0.0 // isOverEyes
|
|
1672
|
-
|
|
1673
|
-
const materialUniformBuffer = this.device.createBuffer({
|
|
1674
|
-
label: `material uniform: ${mat.name}`,
|
|
1675
|
-
size: materialUniformData.byteLength,
|
|
1676
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1677
|
-
})
|
|
1678
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1658
|
+
const materialUniformBuffer = this.createMaterialUniformBuffer(mat.name, materialAlpha, 0.0)
|
|
1679
1659
|
|
|
1680
1660
|
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1681
1661
|
const bindGroup = this.device.createBindGroup({
|
|
@@ -1691,33 +1671,22 @@ export class Engine {
|
|
|
1691
1671
|
],
|
|
1692
1672
|
})
|
|
1693
1673
|
|
|
1694
|
-
|
|
1674
|
+
const addDrawCall = (draws: DrawCall[]) => {
|
|
1695
1675
|
if (indexCount > 0) {
|
|
1696
|
-
|
|
1697
|
-
count: indexCount,
|
|
1698
|
-
firstIndex: currentIndexOffset,
|
|
1699
|
-
bindGroup,
|
|
1700
|
-
})
|
|
1676
|
+
draws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup })
|
|
1701
1677
|
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (mat.isEye) {
|
|
1681
|
+
addDrawCall(this.eyeDraws)
|
|
1702
1682
|
} else if (mat.isHair) {
|
|
1703
1683
|
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1704
1684
|
const createHairBindGroup = (isOverEyes: boolean) => {
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
uniformData[4] = 1.0 // rimColor.rgb
|
|
1711
|
-
uniformData[5] = 1.0
|
|
1712
|
-
uniformData[6] = 1.0
|
|
1713
|
-
uniformData[7] = isOverEyes ? 1.0 : 0.0 // isOverEyes
|
|
1714
|
-
|
|
1715
|
-
const buffer = this.device.createBuffer({
|
|
1716
|
-
label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1717
|
-
size: uniformData.byteLength,
|
|
1718
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1719
|
-
})
|
|
1720
|
-
this.device.queue.writeBuffer(buffer, 0, uniformData)
|
|
1685
|
+
const buffer = this.createMaterialUniformBuffer(
|
|
1686
|
+
`${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1687
|
+
materialAlpha,
|
|
1688
|
+
isOverEyes ? 1.0 : 0.0
|
|
1689
|
+
)
|
|
1721
1690
|
|
|
1722
1691
|
return this.device.createBindGroup({
|
|
1723
1692
|
label: `material bind group (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
@@ -1742,7 +1711,6 @@ export class Engine {
|
|
|
1742
1711
|
firstIndex: currentIndexOffset,
|
|
1743
1712
|
bindGroup: bindGroupOverEyes,
|
|
1744
1713
|
})
|
|
1745
|
-
|
|
1746
1714
|
this.hairDrawsOverNonEyes.push({
|
|
1747
1715
|
count: indexCount,
|
|
1748
1716
|
firstIndex: currentIndexOffset,
|
|
@@ -1750,41 +1718,27 @@ export class Engine {
|
|
|
1750
1718
|
})
|
|
1751
1719
|
}
|
|
1752
1720
|
} else if (isTransparent) {
|
|
1753
|
-
|
|
1754
|
-
this.transparentDraws.push({
|
|
1755
|
-
count: indexCount,
|
|
1756
|
-
firstIndex: currentIndexOffset,
|
|
1757
|
-
bindGroup,
|
|
1758
|
-
})
|
|
1759
|
-
}
|
|
1721
|
+
addDrawCall(this.transparentDraws)
|
|
1760
1722
|
} else {
|
|
1761
|
-
|
|
1762
|
-
this.opaqueDraws.push({
|
|
1763
|
-
count: indexCount,
|
|
1764
|
-
firstIndex: currentIndexOffset,
|
|
1765
|
-
bindGroup,
|
|
1766
|
-
})
|
|
1767
|
-
}
|
|
1723
|
+
addDrawCall(this.opaqueDraws)
|
|
1768
1724
|
}
|
|
1769
1725
|
|
|
1770
1726
|
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1771
1727
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1772
|
-
const materialUniformData = new Float32Array(
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const materialUniformBuffer = this.
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
})
|
|
1787
|
-
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1728
|
+
const materialUniformData = new Float32Array([
|
|
1729
|
+
mat.edgeColor[0],
|
|
1730
|
+
mat.edgeColor[1],
|
|
1731
|
+
mat.edgeColor[2],
|
|
1732
|
+
mat.edgeColor[3],
|
|
1733
|
+
mat.edgeSize,
|
|
1734
|
+
0,
|
|
1735
|
+
0,
|
|
1736
|
+
0,
|
|
1737
|
+
])
|
|
1738
|
+
const materialUniformBuffer = this.createUniformBuffer(
|
|
1739
|
+
`outline material uniform: ${mat.name}`,
|
|
1740
|
+
materialUniformData
|
|
1741
|
+
)
|
|
1788
1742
|
|
|
1789
1743
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1790
1744
|
label: `outline bind group: ${mat.name}`,
|
|
@@ -1797,31 +1751,14 @@ export class Engine {
|
|
|
1797
1751
|
})
|
|
1798
1752
|
|
|
1799
1753
|
if (indexCount > 0) {
|
|
1800
|
-
|
|
1801
|
-
this.eyeOutlineDraws
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
count: indexCount,
|
|
1809
|
-
firstIndex: currentIndexOffset,
|
|
1810
|
-
bindGroup: outlineBindGroup,
|
|
1811
|
-
})
|
|
1812
|
-
} else if (isTransparent) {
|
|
1813
|
-
this.transparentOutlineDraws.push({
|
|
1814
|
-
count: indexCount,
|
|
1815
|
-
firstIndex: currentIndexOffset,
|
|
1816
|
-
bindGroup: outlineBindGroup,
|
|
1817
|
-
})
|
|
1818
|
-
} else {
|
|
1819
|
-
this.opaqueOutlineDraws.push({
|
|
1820
|
-
count: indexCount,
|
|
1821
|
-
firstIndex: currentIndexOffset,
|
|
1822
|
-
bindGroup: outlineBindGroup,
|
|
1823
|
-
})
|
|
1824
|
-
}
|
|
1754
|
+
const outlineDraws = mat.isEye
|
|
1755
|
+
? this.eyeOutlineDraws
|
|
1756
|
+
: mat.isHair
|
|
1757
|
+
? this.hairOutlineDraws
|
|
1758
|
+
: isTransparent
|
|
1759
|
+
? this.transparentOutlineDraws
|
|
1760
|
+
: this.opaqueOutlineDraws
|
|
1761
|
+
outlineDraws.push({ count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup })
|
|
1825
1762
|
}
|
|
1826
1763
|
}
|
|
1827
1764
|
|
|
@@ -1829,6 +1766,22 @@ export class Engine {
|
|
|
1829
1766
|
}
|
|
1830
1767
|
}
|
|
1831
1768
|
|
|
1769
|
+
private createMaterialUniformBuffer(label: string, alpha: number, isOverEyes: number): GPUBuffer {
|
|
1770
|
+
const data = new Float32Array(8)
|
|
1771
|
+
data.set([alpha, 1.0, this.rimLightIntensity, 0.0, 1.0, 1.0, 1.0, isOverEyes])
|
|
1772
|
+
return this.createUniformBuffer(`material uniform: ${label}`, data)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
private createUniformBuffer(label: string, data: Float32Array | Uint32Array): GPUBuffer {
|
|
1776
|
+
const buffer = this.device.createBuffer({
|
|
1777
|
+
label,
|
|
1778
|
+
size: data.byteLength,
|
|
1779
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1780
|
+
})
|
|
1781
|
+
this.device.queue.writeBuffer(buffer, 0, data as ArrayBufferView<ArrayBuffer>)
|
|
1782
|
+
return buffer
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1832
1785
|
private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
|
|
1833
1786
|
const cached = this.textureCache.get(path)
|
|
1834
1787
|
if (cached) {
|