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/player.js CHANGED
@@ -1,5 +1,4 @@
1
- import { bezierInterpolate } from "./bezier-interpolate";
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.duration = 0;
9
+ this._duration = 0;
11
10
  // Playback state
12
11
  this.isPlaying = false;
13
12
  this.isPaused = false;
14
- this.currentTime = 0;
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
- // Process bone frames
33
- const allBoneKeyFrames = [];
34
- for (const keyFrame of this.frames) {
35
- for (const boneFrame of keyFrame.boneFrames) {
36
- allBoneKeyFrames.push({
37
- boneFrame,
38
- time: keyFrame.time,
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
- const boneKeyFramesByBone = new Map();
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
- boneKeyFramesByBone.get(boneFrame.boneName).push({ boneFrame, time });
48
- }
49
- for (const keyFrames of boneKeyFramesByBone.values()) {
50
- keyFrames.sort((a, b) => a.time - b.time);
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 morphFrame of keyFrame.morphFrames) {
56
- allMorphKeyFrames.push({
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
- morphKeyFramesByMorph.get(morphFrame.morphName).push({ morphFrame, time });
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
- for (const keyFrames of this.morphTracks.values()) {
86
- if (keyFrames.length > 0) {
87
- const lastTime = keyFrames[keyFrames.length - 1].time;
88
- if (lastTime > maxFrameTime) {
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.duration = maxFrameTime > 0 ? maxFrameTime : 0;
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
- if (this.isPaused) {
103
- // Resume from paused position - don't adjust time, just continue from where we paused
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.currentTime = 0;
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.duration));
139
- this.currentTime = clampedTime;
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.currentTime);
117
+ return this.getPoseAtTime(this._currentTime);
157
118
  }
158
119
  // Calculate current animation time
159
120
  const elapsedSeconds = (currentRealTime - this.startTime) / 1000;
160
- this.currentTime = elapsedSeconds;
121
+ this._currentTime = elapsedSeconds;
161
122
  // Check if animation ended
162
- if (this.currentTime >= this.duration) {
163
- this.currentTime = this.duration;
123
+ if (this._currentTime >= this._duration) {
124
+ this._currentTime = this._duration;
164
125
  this.pause(); // Auto-pause at end
165
- return this.getPoseAtTime(this.currentTime);
126
+ return this.getPoseAtTime(this._currentTime);
166
127
  }
167
- return this.getPoseAtTime(this.currentTime);
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
- // Helper to find upper bound index (binary search)
179
- const upperBoundFrameIndex = (time, keyFrames) => {
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 each bone track
151
+ // Process bone tracks
194
152
  for (const [boneName, keyFrames] of this.boneTracks.entries()) {
195
153
  if (keyFrames.length === 0)
196
154
  continue;
197
- // Clamp frame time to track range
198
- const startTime = keyFrames[0].time;
199
- const endTime = keyFrames[keyFrames.length - 1].time;
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 timeB = keyFrames[upperBoundIndex]?.time;
206
- const boneFrameA = keyFrames[upperBoundIndexMinusOne].boneFrame;
207
- if (timeB === undefined) {
208
- // Last keyframe or beyond - use the last keyframe value
209
- pose.boneRotations.set(boneName, boneFrameA.rotation);
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
- // Interpolate between two keyframes
214
- const timeA = keyFrames[upperBoundIndexMinusOne].time;
215
- const boneFrameB = keyFrames[upperBoundIndex].boneFrame;
216
- const gradient = (clampedFrameTime - timeA) / (timeB - timeA);
217
- // Interpolate rotation using Bezier
218
- const interp = boneFrameB.interpolation;
219
- const rotWeight = bezierInterpolate(interp[0] / 127, // x1
220
- interp[1] / 127, // x2
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
- // Helper to find upper bound index for morph frames
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
- // Clamp frame time to track range
266
- const startTime = keyFrames[0].time;
267
- const endTime = keyFrames[keyFrames.length - 1].time;
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 timeB = keyFrames[upperBoundIndex]?.time;
274
- const morphFrameA = keyFrames[upperBoundIndexMinusOne].morphFrame;
275
- if (timeB === undefined) {
276
- // Last keyframe or beyond - use the last keyframe value
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
- // Linear interpolation between two keyframes
281
- const timeA = keyFrames[upperBoundIndexMinusOne].time;
282
- const morphFrameB = keyFrames[upperBoundIndex].morphFrame;
283
- const gradient = (clampedFrameTime - timeA) / (timeB - timeA);
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.currentTime,
296
- duration: this.duration,
297
- percentage: this.duration > 0 ? (this.currentTime / this.duration) * 100 : 0,
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
- * Get current time
302
- */
303
- getCurrentTime() {
304
- return this.currentTime;
208
+ get currentTime() {
209
+ return this._currentTime;
305
210
  }
306
- /**
307
- * Get animation duration
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -29,7 +29,3 @@ export async function loadAmmo(): Promise<AmmoInstance> {
29
29
 
30
30
  return ammoPromise
31
31
  }
32
-
33
- export function getAmmoInstance(): AmmoInstance | null {
34
- return ammoInstance
35
- }
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
- // Create material uniform data
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
- if (mat.isEye) {
1674
+ const addDrawCall = (draws: DrawCall[]) => {
1695
1675
  if (indexCount > 0) {
1696
- this.eyeDraws.push({
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 uniformData = new Float32Array(8)
1706
- uniformData[0] = materialAlpha
1707
- uniformData[1] = 1.0 // alphaMultiplier (shader adjusts based on isOverEyes)
1708
- uniformData[2] = this.rimLightIntensity
1709
- uniformData[3] = 0.0 // _padding1
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
- if (indexCount > 0) {
1754
- this.transparentDraws.push({
1755
- count: indexCount,
1756
- firstIndex: currentIndexOffset,
1757
- bindGroup,
1758
- })
1759
- }
1721
+ addDrawCall(this.transparentDraws)
1760
1722
  } else {
1761
- if (indexCount > 0) {
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(8)
1773
- materialUniformData[0] = mat.edgeColor[0] // edgeColor.r
1774
- materialUniformData[1] = mat.edgeColor[1] // edgeColor.g
1775
- materialUniformData[2] = mat.edgeColor[2] // edgeColor.b
1776
- materialUniformData[3] = mat.edgeColor[3] // edgeColor.a
1777
- materialUniformData[4] = mat.edgeSize
1778
- materialUniformData[5] = 0.0 // isOverEyes
1779
- materialUniformData[6] = 0.0
1780
- materialUniformData[7] = 0.0
1781
-
1782
- const materialUniformBuffer = this.device.createBuffer({
1783
- label: `outline material uniform: ${mat.name}`,
1784
- size: materialUniformData.byteLength,
1785
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
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
- if (mat.isEye) {
1801
- this.eyeOutlineDraws.push({
1802
- count: indexCount,
1803
- firstIndex: currentIndexOffset,
1804
- bindGroup: outlineBindGroup,
1805
- })
1806
- } else if (mat.isHair) {
1807
- this.hairOutlineDraws.push({
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) {