reze-engine 0.1.11 → 0.1.13

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/engine.js CHANGED
@@ -11,7 +11,7 @@ export class Engine {
11
11
  this.sampleCount = 4; // MSAA 4x
12
12
  // Bloom settings
13
13
  this.bloomThreshold = 0.3;
14
- this.bloomIntensity = 0.13;
14
+ this.bloomIntensity = 0.1;
15
15
  // Rim light settings
16
16
  this.rimLightIntensity = 0.35;
17
17
  this.rimLightPower = 2.0;
@@ -81,239 +81,120 @@ export class Engine {
81
81
  });
82
82
  const shaderModule = this.device.createShaderModule({
83
83
  label: "model shaders",
84
- code: /* wgsl */ `
85
- struct CameraUniforms {
86
- view: mat4x4f,
87
- projection: mat4x4f,
88
- viewPos: vec3f,
89
- _padding: f32,
90
- };
91
-
92
- struct Light {
93
- direction: vec3f,
94
- _padding1: f32,
95
- color: vec3f,
96
- intensity: f32,
97
- };
98
-
99
- struct LightUniforms {
100
- ambient: f32,
101
- lightCount: f32,
102
- _padding1: f32,
103
- _padding2: f32,
104
- lights: array<Light, 4>,
105
- };
106
-
107
- struct MaterialUniforms {
108
- alpha: f32,
109
- rimIntensity: f32,
110
- rimPower: f32,
111
- _padding1: f32,
112
- rimColor: vec3f,
113
- _padding2: f32,
114
- };
115
-
116
- struct VertexOutput {
117
- @builtin(position) position: vec4f,
118
- @location(0) normal: vec3f,
119
- @location(1) uv: vec2f,
120
- @location(2) worldPos: vec3f,
121
- };
122
-
123
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
124
- @group(0) @binding(1) var<uniform> light: LightUniforms;
125
- @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
126
- @group(0) @binding(3) var diffuseSampler: sampler;
127
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
128
- @group(0) @binding(5) var toonTexture: texture_2d<f32>;
129
- @group(0) @binding(6) var toonSampler: sampler;
130
- @group(0) @binding(7) var<uniform> material: MaterialUniforms;
131
-
132
- @vertex fn vs(
133
- @location(0) position: vec3f,
134
- @location(1) normal: vec3f,
135
- @location(2) uv: vec2f,
136
- @location(3) joints0: vec4<u32>,
137
- @location(4) weights0: vec4<f32>
138
- ) -> VertexOutput {
139
- var output: VertexOutput;
140
- let pos4 = vec4f(position, 1.0);
141
-
142
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
143
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
144
- var normalizedWeights: vec4f;
145
- if (weightSum > 0.0001) {
146
- normalizedWeights = weights0 / weightSum;
147
- } else {
148
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
149
- }
150
-
151
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
152
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
153
- for (var i = 0u; i < 4u; i++) {
154
- let j = joints0[i];
155
- let w = normalizedWeights[i];
156
- let m = skinMats[j];
157
- skinnedPos += (m * pos4) * w;
158
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
159
- skinnedNrm += (r3 * normal) * w;
160
- }
161
- let worldPos = skinnedPos.xyz;
162
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
163
- output.normal = normalize(skinnedNrm);
164
- output.uv = uv;
165
- output.worldPos = worldPos;
166
- return output;
167
- }
168
-
169
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
170
- let n = normalize(input.normal);
171
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
172
-
173
- var lightAccum = vec3f(light.ambient);
174
- let numLights = u32(light.lightCount);
175
- for (var i = 0u; i < numLights; i++) {
176
- let l = -light.lights[i].direction;
177
- let nDotL = max(dot(n, l), 0.0);
178
- let toonUV = vec2f(nDotL, 0.5);
179
- let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
180
- let radiance = light.lights[i].color * light.lights[i].intensity;
181
- lightAccum += toonFactor * radiance * nDotL;
182
- }
183
-
184
- // Rim light calculation
185
- let viewDir = normalize(camera.viewPos - input.worldPos);
186
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
187
- rimFactor = pow(rimFactor, material.rimPower);
188
- let rimLight = material.rimColor * material.rimIntensity * rimFactor;
189
-
190
- let color = albedo * lightAccum + rimLight;
191
- let finalAlpha = material.alpha;
192
- if (finalAlpha < 0.001) {
193
- discard;
194
- }
195
-
196
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
197
- }
198
- `,
199
- });
200
- // Unified hair shader that can handle both over-eyes and over-non-eyes cases
201
- // Uses material.alpha multiplier to control opacity (0.5 for over-eyes, 1.0 for over-non-eyes)
202
- const hairShaderModule = this.device.createShaderModule({
203
- label: "unified hair shaders",
204
- code: /* wgsl */ `
205
- struct CameraUniforms {
206
- view: mat4x4f,
207
- projection: mat4x4f,
208
- viewPos: vec3f,
209
- _padding: f32,
210
- };
211
-
212
- struct Light {
213
- direction: vec3f,
214
- _padding1: f32,
215
- color: vec3f,
216
- intensity: f32,
217
- };
218
-
219
- struct LightUniforms {
220
- ambient: f32,
221
- lightCount: f32,
222
- _padding1: f32,
223
- _padding2: f32,
224
- lights: array<Light, 4>,
225
- };
226
-
227
- struct MaterialUniforms {
228
- alpha: f32,
229
- alphaMultiplier: f32, // New: multiplier for alpha (0.5 for over-eyes, 1.0 for over-non-eyes)
230
- rimIntensity: f32,
231
- rimPower: f32,
232
- rimColor: vec3f,
233
- _padding1: f32,
234
- };
235
-
236
- struct VertexOutput {
237
- @builtin(position) position: vec4f,
238
- @location(0) normal: vec3f,
239
- @location(1) uv: vec2f,
240
- @location(2) worldPos: vec3f,
241
- };
242
-
243
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
244
- @group(0) @binding(1) var<uniform> light: LightUniforms;
245
- @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
246
- @group(0) @binding(3) var diffuseSampler: sampler;
247
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
248
- @group(0) @binding(5) var toonTexture: texture_2d<f32>;
249
- @group(0) @binding(6) var toonSampler: sampler;
250
- @group(0) @binding(7) var<uniform> material: MaterialUniforms;
251
-
252
- @vertex fn vs(
253
- @location(0) position: vec3f,
254
- @location(1) normal: vec3f,
255
- @location(2) uv: vec2f,
256
- @location(3) joints0: vec4<u32>,
257
- @location(4) weights0: vec4<f32>
258
- ) -> VertexOutput {
259
- var output: VertexOutput;
260
- let pos4 = vec4f(position, 1.0);
261
-
262
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
263
- var normalizedWeights: vec4f;
264
- if (weightSum > 0.0001) {
265
- normalizedWeights = weights0 / weightSum;
266
- } else {
267
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
268
- }
269
-
270
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
271
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
272
- for (var i = 0u; i < 4u; i++) {
273
- let j = joints0[i];
274
- let w = normalizedWeights[i];
275
- let m = skinMats[j];
276
- skinnedPos += (m * pos4) * w;
277
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
278
- skinnedNrm += (r3 * normal) * w;
279
- }
280
- let worldPos = skinnedPos.xyz;
281
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
282
- output.normal = normalize(skinnedNrm);
283
- output.uv = uv;
284
- output.worldPos = worldPos;
285
- return output;
286
- }
287
-
288
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
289
- let n = normalize(input.normal);
290
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
291
-
292
- var lightAccum = vec3f(light.ambient);
293
- let numLights = u32(light.lightCount);
294
- for (var i = 0u; i < numLights; i++) {
295
- let l = -light.lights[i].direction;
296
- let nDotL = max(dot(n, l), 0.0);
297
- let toonUV = vec2f(nDotL, 0.5);
298
- let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
299
- let radiance = light.lights[i].color * light.lights[i].intensity;
300
- lightAccum += toonFactor * radiance * nDotL;
301
- }
302
-
303
- // Rim light calculation
304
- let viewDir = normalize(camera.viewPos - input.worldPos);
305
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
306
- rimFactor = pow(rimFactor, material.rimPower);
307
- let rimLight = material.rimColor * material.rimIntensity * rimFactor;
308
-
309
- let color = albedo * lightAccum + rimLight;
310
- let finalAlpha = material.alpha * material.alphaMultiplier;
311
- if (finalAlpha < 0.001) {
312
- discard;
313
- }
314
-
315
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
316
- }
84
+ code: /* wgsl */ `
85
+ struct CameraUniforms {
86
+ view: mat4x4f,
87
+ projection: mat4x4f,
88
+ viewPos: vec3f,
89
+ _padding: f32,
90
+ };
91
+
92
+ struct Light {
93
+ direction: vec3f,
94
+ _padding1: f32,
95
+ color: vec3f,
96
+ intensity: f32,
97
+ };
98
+
99
+ struct LightUniforms {
100
+ ambient: f32,
101
+ lightCount: f32,
102
+ _padding1: f32,
103
+ _padding2: f32,
104
+ lights: array<Light, 4>,
105
+ };
106
+
107
+ struct MaterialUniforms {
108
+ alpha: f32,
109
+ alphaMultiplier: f32,
110
+ rimIntensity: f32,
111
+ rimPower: f32,
112
+ rimColor: vec3f,
113
+ _padding1: f32,
114
+ };
115
+
116
+ struct VertexOutput {
117
+ @builtin(position) position: vec4f,
118
+ @location(0) normal: vec3f,
119
+ @location(1) uv: vec2f,
120
+ @location(2) worldPos: vec3f,
121
+ };
122
+
123
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
124
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
125
+ @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
126
+ @group(0) @binding(3) var diffuseSampler: sampler;
127
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
128
+ @group(0) @binding(5) var toonTexture: texture_2d<f32>;
129
+ @group(0) @binding(6) var toonSampler: sampler;
130
+ @group(0) @binding(7) var<uniform> material: MaterialUniforms;
131
+
132
+ @vertex fn vs(
133
+ @location(0) position: vec3f,
134
+ @location(1) normal: vec3f,
135
+ @location(2) uv: vec2f,
136
+ @location(3) joints0: vec4<u32>,
137
+ @location(4) weights0: vec4<f32>
138
+ ) -> VertexOutput {
139
+ var output: VertexOutput;
140
+ let pos4 = vec4f(position, 1.0);
141
+
142
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
143
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
144
+ var normalizedWeights: vec4f;
145
+ if (weightSum > 0.0001) {
146
+ normalizedWeights = weights0 / weightSum;
147
+ } else {
148
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
149
+ }
150
+
151
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
152
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
153
+ for (var i = 0u; i < 4u; i++) {
154
+ let j = joints0[i];
155
+ let w = normalizedWeights[i];
156
+ let m = skinMats[j];
157
+ skinnedPos += (m * pos4) * w;
158
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
159
+ skinnedNrm += (r3 * normal) * w;
160
+ }
161
+ let worldPos = skinnedPos.xyz;
162
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
163
+ output.normal = normalize(skinnedNrm);
164
+ output.uv = uv;
165
+ output.worldPos = worldPos;
166
+ return output;
167
+ }
168
+
169
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
170
+ let n = normalize(input.normal);
171
+ let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
172
+
173
+ var lightAccum = vec3f(light.ambient);
174
+ let numLights = u32(light.lightCount);
175
+ for (var i = 0u; i < numLights; i++) {
176
+ let l = -light.lights[i].direction;
177
+ let nDotL = max(dot(n, l), 0.0);
178
+ let toonUV = vec2f(nDotL, 0.5);
179
+ let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
180
+ let radiance = light.lights[i].color * light.lights[i].intensity;
181
+ lightAccum += toonFactor * radiance * nDotL;
182
+ }
183
+
184
+ // Rim light calculation
185
+ let viewDir = normalize(camera.viewPos - input.worldPos);
186
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
187
+ rimFactor = pow(rimFactor, material.rimPower);
188
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
189
+
190
+ let color = albedo * lightAccum + rimLight;
191
+ let finalAlpha = material.alpha * material.alphaMultiplier;
192
+ if (finalAlpha < 0.001) {
193
+ discard;
194
+ }
195
+
196
+ return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
197
+ }
317
198
  `,
318
199
  });
319
200
  // Create explicit bind group layout for all pipelines using the main shader
@@ -405,72 +286,71 @@ export class Engine {
405
286
  });
406
287
  const outlineShaderModule = this.device.createShaderModule({
407
288
  label: "outline shaders",
408
- code: /* wgsl */ `
409
- struct CameraUniforms {
410
- view: mat4x4f,
411
- projection: mat4x4f,
412
- viewPos: vec3f,
413
- _padding: f32,
414
- };
415
-
416
- struct MaterialUniforms {
417
- edgeColor: vec4f,
418
- edgeSize: f32,
419
- _padding1: f32,
420
- _padding2: f32,
421
- _padding3: f32,
422
- };
423
-
424
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
425
- @group(0) @binding(1) var<uniform> material: MaterialUniforms;
426
- @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
427
-
428
- struct VertexOutput {
429
- @builtin(position) position: vec4f,
430
- };
431
-
432
- @vertex fn vs(
433
- @location(0) position: vec3f,
434
- @location(1) normal: vec3f,
435
- @location(2) uv: vec2f,
436
- @location(3) joints0: vec4<u32>,
437
- @location(4) weights0: vec4<f32>
438
- ) -> VertexOutput {
439
- var output: VertexOutput;
440
- let pos4 = vec4f(position, 1.0);
441
-
442
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
443
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
444
- var normalizedWeights: vec4f;
445
- if (weightSum > 0.0001) {
446
- normalizedWeights = weights0 / weightSum;
447
- } else {
448
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
449
- }
450
-
451
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
452
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
453
- for (var i = 0u; i < 4u; i++) {
454
- let j = joints0[i];
455
- let w = normalizedWeights[i];
456
- let m = skinMats[j];
457
- skinnedPos += (m * pos4) * w;
458
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
459
- skinnedNrm += (r3 * normal) * w;
460
- }
461
- let worldPos = skinnedPos.xyz;
462
- let worldNormal = normalize(skinnedNrm);
463
-
464
- // MMD invert hull: expand vertices outward along normals
465
- let scaleFactor = 0.01;
466
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
467
- output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
468
- return output;
469
- }
470
-
471
- @fragment fn fs() -> @location(0) vec4f {
472
- return material.edgeColor;
473
- }
289
+ code: /* wgsl */ `
290
+ struct CameraUniforms {
291
+ view: mat4x4f,
292
+ projection: mat4x4f,
293
+ viewPos: vec3f,
294
+ _padding: f32,
295
+ };
296
+
297
+ struct MaterialUniforms {
298
+ edgeColor: vec4f,
299
+ edgeSize: f32,
300
+ _padding1: f32,
301
+ _padding2: f32,
302
+ _padding3: f32,
303
+ };
304
+
305
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
306
+ @group(0) @binding(1) var<uniform> material: MaterialUniforms;
307
+ @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
308
+
309
+ struct VertexOutput {
310
+ @builtin(position) position: vec4f,
311
+ };
312
+
313
+ @vertex fn vs(
314
+ @location(0) position: vec3f,
315
+ @location(1) normal: vec3f,
316
+ @location(3) joints0: vec4<u32>,
317
+ @location(4) weights0: vec4<f32>
318
+ ) -> VertexOutput {
319
+ var output: VertexOutput;
320
+ let pos4 = vec4f(position, 1.0);
321
+
322
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
323
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
324
+ var normalizedWeights: vec4f;
325
+ if (weightSum > 0.0001) {
326
+ normalizedWeights = weights0 / weightSum;
327
+ } else {
328
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
329
+ }
330
+
331
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
332
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
333
+ for (var i = 0u; i < 4u; i++) {
334
+ let j = joints0[i];
335
+ let w = normalizedWeights[i];
336
+ let m = skinMats[j];
337
+ skinnedPos += (m * pos4) * w;
338
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
339
+ skinnedNrm += (r3 * normal) * w;
340
+ }
341
+ let worldPos = skinnedPos.xyz;
342
+ let worldNormal = normalize(skinnedNrm);
343
+
344
+ // MMD invert hull: expand vertices outward along normals
345
+ let scaleFactor = 0.01;
346
+ let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
347
+ output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
348
+ return output;
349
+ }
350
+
351
+ @fragment fn fs() -> @location(0) vec4f {
352
+ return material.edgeColor;
353
+ }
474
354
  `,
475
355
  });
476
356
  this.outlinePipeline = this.device.createRenderPipeline({
@@ -492,11 +372,6 @@ export class Engine {
492
372
  offset: 3 * 4,
493
373
  format: "float32x3",
494
374
  },
495
- {
496
- shaderLocation: 2,
497
- offset: 6 * 4,
498
- format: "float32x2",
499
- },
500
375
  ],
501
376
  },
502
377
  {
@@ -561,11 +436,6 @@ export class Engine {
561
436
  offset: 3 * 4,
562
437
  format: "float32x3",
563
438
  },
564
- {
565
- shaderLocation: 2,
566
- offset: 6 * 4,
567
- format: "float32x2",
568
- },
569
439
  ],
570
440
  },
571
441
  {
@@ -642,11 +512,6 @@ export class Engine {
642
512
  offset: 3 * 4,
643
513
  format: "float32x3",
644
514
  },
645
- {
646
- shaderLocation: 2,
647
- offset: 6 * 4,
648
- format: "float32x2",
649
- },
650
515
  ],
651
516
  },
652
517
  {
@@ -712,7 +577,7 @@ export class Engine {
712
577
  label: "hair pipeline (over eyes)",
713
578
  layout: sharedPipelineLayout,
714
579
  vertex: {
715
- module: hairShaderModule,
580
+ module: shaderModule,
716
581
  buffers: [
717
582
  {
718
583
  arrayStride: 8 * 4,
@@ -733,7 +598,7 @@ export class Engine {
733
598
  ],
734
599
  },
735
600
  fragment: {
736
- module: hairShaderModule,
601
+ module: shaderModule,
737
602
  targets: [
738
603
  {
739
604
  format: this.presentationFormat,
@@ -777,7 +642,7 @@ export class Engine {
777
642
  label: "hair pipeline (over non-eyes)",
778
643
  layout: sharedPipelineLayout,
779
644
  vertex: {
780
- module: hairShaderModule,
645
+ module: shaderModule,
781
646
  buffers: [
782
647
  {
783
648
  arrayStride: 8 * 4,
@@ -798,7 +663,7 @@ export class Engine {
798
663
  ],
799
664
  },
800
665
  fragment: {
801
- module: hairShaderModule,
666
+ module: shaderModule,
802
667
  targets: [
803
668
  {
804
669
  format: this.presentationFormat,
@@ -907,31 +772,31 @@ export class Engine {
907
772
  createSkinMatrixComputePipeline() {
908
773
  const computeShader = this.device.createShaderModule({
909
774
  label: "skin matrix compute",
910
- code: /* wgsl */ `
911
- struct BoneCountUniform {
912
- count: u32,
913
- _padding1: u32,
914
- _padding2: u32,
915
- _padding3: u32,
916
- _padding4: vec4<u32>,
917
- };
918
-
919
- @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
920
- @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
921
- @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
922
- @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
923
-
924
- @compute @workgroup_size(64)
925
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
926
- let boneIndex = globalId.x;
927
- // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
928
- if (boneIndex >= boneCount.count) {
929
- return;
930
- }
931
- let worldMat = worldMatrices[boneIndex];
932
- let invBindMat = inverseBindMatrices[boneIndex];
933
- skinMatrices[boneIndex] = worldMat * invBindMat;
934
- }
775
+ code: /* wgsl */ `
776
+ struct BoneCountUniform {
777
+ count: u32,
778
+ _padding1: u32,
779
+ _padding2: u32,
780
+ _padding3: u32,
781
+ _padding4: vec4<u32>,
782
+ };
783
+
784
+ @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
785
+ @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
786
+ @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
787
+ @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
788
+
789
+ @compute @workgroup_size(64)
790
+ fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
791
+ let boneIndex = globalId.x;
792
+ // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
793
+ if (boneIndex >= boneCount.count) {
794
+ return;
795
+ }
796
+ let worldMat = worldMatrices[boneIndex];
797
+ let invBindMat = inverseBindMatrices[boneIndex];
798
+ skinMatrices[boneIndex] = worldMat * invBindMat;
799
+ }
935
800
  `,
936
801
  });
937
802
  this.skinMatrixComputePipeline = this.device.createComputePipeline({
@@ -985,143 +850,143 @@ export class Engine {
985
850
  // Bloom extraction shader (extracts bright areas)
986
851
  const bloomExtractShader = this.device.createShaderModule({
987
852
  label: "bloom extract",
988
- code: /* wgsl */ `
989
- struct VertexOutput {
990
- @builtin(position) position: vec4f,
991
- @location(0) uv: vec2f,
992
- };
993
-
994
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
995
- var output: VertexOutput;
996
- // Generate fullscreen quad from vertex index
997
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
998
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
999
- output.position = vec4f(x, y, 0.0, 1.0);
1000
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
1001
- return output;
1002
- }
1003
-
1004
- struct BloomExtractUniforms {
1005
- threshold: f32,
1006
- _padding1: f32,
1007
- _padding2: f32,
1008
- _padding3: f32,
1009
- _padding4: f32,
1010
- _padding5: f32,
1011
- _padding6: f32,
1012
- _padding7: f32,
1013
- };
1014
-
1015
- @group(0) @binding(0) var inputTexture: texture_2d<f32>;
1016
- @group(0) @binding(1) var inputSampler: sampler;
1017
- @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
1018
-
1019
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1020
- let color = textureSample(inputTexture, inputSampler, input.uv);
1021
- // Extract bright areas above threshold
1022
- let threshold = extractUniforms.threshold;
1023
- let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
1024
- return vec4f(bloom, color.a);
1025
- }
853
+ code: /* wgsl */ `
854
+ struct VertexOutput {
855
+ @builtin(position) position: vec4f,
856
+ @location(0) uv: vec2f,
857
+ };
858
+
859
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
860
+ var output: VertexOutput;
861
+ // Generate fullscreen quad from vertex index
862
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
863
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
864
+ output.position = vec4f(x, y, 0.0, 1.0);
865
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
866
+ return output;
867
+ }
868
+
869
+ struct BloomExtractUniforms {
870
+ threshold: f32,
871
+ _padding1: f32,
872
+ _padding2: f32,
873
+ _padding3: f32,
874
+ _padding4: f32,
875
+ _padding5: f32,
876
+ _padding6: f32,
877
+ _padding7: f32,
878
+ };
879
+
880
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
881
+ @group(0) @binding(1) var inputSampler: sampler;
882
+ @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
883
+
884
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
885
+ let color = textureSample(inputTexture, inputSampler, input.uv);
886
+ // Extract bright areas above threshold
887
+ let threshold = extractUniforms.threshold;
888
+ let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
889
+ return vec4f(bloom, color.a);
890
+ }
1026
891
  `,
1027
892
  });
1028
893
  // Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
1029
894
  const bloomBlurShader = this.device.createShaderModule({
1030
895
  label: "bloom blur",
1031
- code: /* wgsl */ `
1032
- struct VertexOutput {
1033
- @builtin(position) position: vec4f,
1034
- @location(0) uv: vec2f,
1035
- };
1036
-
1037
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
1038
- var output: VertexOutput;
1039
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
1040
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
1041
- output.position = vec4f(x, y, 0.0, 1.0);
1042
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
1043
- return output;
1044
- }
1045
-
1046
- struct BlurUniforms {
1047
- direction: vec2f,
1048
- _padding1: f32,
1049
- _padding2: f32,
1050
- _padding3: f32,
1051
- _padding4: f32,
1052
- _padding5: f32,
1053
- _padding6: f32,
1054
- };
1055
-
1056
- @group(0) @binding(0) var inputTexture: texture_2d<f32>;
1057
- @group(0) @binding(1) var inputSampler: sampler;
1058
- @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
1059
-
1060
- // 9-tap gaussian blur
1061
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1062
- let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
1063
- var result = vec4f(0.0);
1064
-
1065
- // Gaussian weights for 9-tap filter
1066
- let weights = array<f32, 9>(
1067
- 0.01621622, 0.05405405, 0.12162162,
1068
- 0.19459459, 0.22702703,
1069
- 0.19459459, 0.12162162, 0.05405405, 0.01621622
1070
- );
1071
-
1072
- let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
1073
-
1074
- for (var i = 0u; i < 9u; i++) {
1075
- let offset = offsets[i] * texelSize * blurUniforms.direction;
1076
- result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
1077
- }
1078
-
1079
- return result;
1080
- }
896
+ code: /* wgsl */ `
897
+ struct VertexOutput {
898
+ @builtin(position) position: vec4f,
899
+ @location(0) uv: vec2f,
900
+ };
901
+
902
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
903
+ var output: VertexOutput;
904
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
905
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
906
+ output.position = vec4f(x, y, 0.0, 1.0);
907
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
908
+ return output;
909
+ }
910
+
911
+ struct BlurUniforms {
912
+ direction: vec2f,
913
+ _padding1: f32,
914
+ _padding2: f32,
915
+ _padding3: f32,
916
+ _padding4: f32,
917
+ _padding5: f32,
918
+ _padding6: f32,
919
+ };
920
+
921
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
922
+ @group(0) @binding(1) var inputSampler: sampler;
923
+ @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
924
+
925
+ // 9-tap gaussian blur
926
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
927
+ let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
928
+ var result = vec4f(0.0);
929
+
930
+ // Gaussian weights for 9-tap filter
931
+ let weights = array<f32, 9>(
932
+ 0.01621622, 0.05405405, 0.12162162,
933
+ 0.19459459, 0.22702703,
934
+ 0.19459459, 0.12162162, 0.05405405, 0.01621622
935
+ );
936
+
937
+ let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
938
+
939
+ for (var i = 0u; i < 9u; i++) {
940
+ let offset = offsets[i] * texelSize * blurUniforms.direction;
941
+ result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
942
+ }
943
+
944
+ return result;
945
+ }
1081
946
  `,
1082
947
  });
1083
948
  // Bloom composition shader (combines original scene with bloom)
1084
949
  const bloomComposeShader = this.device.createShaderModule({
1085
950
  label: "bloom compose",
1086
- code: /* wgsl */ `
1087
- struct VertexOutput {
1088
- @builtin(position) position: vec4f,
1089
- @location(0) uv: vec2f,
1090
- };
1091
-
1092
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
1093
- var output: VertexOutput;
1094
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
1095
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
1096
- output.position = vec4f(x, y, 0.0, 1.0);
1097
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
1098
- return output;
1099
- }
1100
-
1101
- struct BloomComposeUniforms {
1102
- intensity: f32,
1103
- _padding1: f32,
1104
- _padding2: f32,
1105
- _padding3: f32,
1106
- _padding4: f32,
1107
- _padding5: f32,
1108
- _padding6: f32,
1109
- _padding7: f32,
1110
- };
1111
-
1112
- @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
1113
- @group(0) @binding(1) var sceneSampler: sampler;
1114
- @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
1115
- @group(0) @binding(3) var bloomSampler: sampler;
1116
- @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
1117
-
1118
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1119
- let scene = textureSample(sceneTexture, sceneSampler, input.uv);
1120
- let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
1121
- // Additive blending with intensity control
1122
- let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
1123
- return vec4f(result, scene.a);
1124
- }
951
+ code: /* wgsl */ `
952
+ struct VertexOutput {
953
+ @builtin(position) position: vec4f,
954
+ @location(0) uv: vec2f,
955
+ };
956
+
957
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
958
+ var output: VertexOutput;
959
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
960
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
961
+ output.position = vec4f(x, y, 0.0, 1.0);
962
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
963
+ return output;
964
+ }
965
+
966
+ struct BloomComposeUniforms {
967
+ intensity: f32,
968
+ _padding1: f32,
969
+ _padding2: f32,
970
+ _padding3: f32,
971
+ _padding4: f32,
972
+ _padding5: f32,
973
+ _padding6: f32,
974
+ _padding7: f32,
975
+ };
976
+
977
+ @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
978
+ @group(0) @binding(1) var sceneSampler: sampler;
979
+ @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
980
+ @group(0) @binding(3) var bloomSampler: sampler;
981
+ @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
982
+
983
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
984
+ let scene = textureSample(sceneTexture, sceneSampler, input.uv);
985
+ let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
986
+ // Additive blending with intensity control
987
+ let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
988
+ return vec4f(result, scene.a);
989
+ }
1125
990
  `,
1126
991
  });
1127
992
  // Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
@@ -1520,17 +1385,16 @@ export class Engine {
1520
1385
  const materialAlpha = mat.diffuse[3];
1521
1386
  const EPSILON = 0.001;
1522
1387
  const isTransparent = materialAlpha < 1.0 - EPSILON;
1523
- // Create material uniform data - for hair materials, we'll create two versions
1524
- // MaterialUniforms struct: alpha, rimIntensity, rimPower, _padding1, rimColor (vec3), _padding2
1388
+ // Create material uniform data
1525
1389
  const materialUniformData = new Float32Array(8);
1526
1390
  materialUniformData[0] = materialAlpha;
1527
- materialUniformData[1] = this.rimLightIntensity;
1528
- materialUniformData[2] = this.rimLightPower;
1529
- materialUniformData[3] = 0.0; // _padding1
1391
+ materialUniformData[1] = 1.0; // alphaMultiplier: 1.0 for non-hair materials
1392
+ materialUniformData[2] = this.rimLightIntensity;
1393
+ materialUniformData[3] = this.rimLightPower;
1530
1394
  materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
1531
1395
  materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
1532
1396
  materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
1533
- materialUniformData[7] = 0.0; // _padding2
1397
+ materialUniformData[7] = 0.0; // _padding1
1534
1398
  const materialUniformBuffer = this.device.createBuffer({
1535
1399
  label: `material uniform: ${mat.name}`,
1536
1400
  size: materialUniformData.byteLength,
@@ -1563,7 +1427,6 @@ export class Engine {
1563
1427
  }
1564
1428
  else if (mat.isHair) {
1565
1429
  // For hair materials, create two bind groups: one for over-eyes (alphaMultiplier = 0.5) and one for over-non-eyes (alphaMultiplier = 1.0)
1566
- // Hair MaterialUniforms struct: alpha, alphaMultiplier, rimIntensity, rimPower, rimColor (vec3), _padding1
1567
1430
  const materialUniformDataOverEyes = new Float32Array(8);
1568
1431
  materialUniformDataOverEyes[0] = materialAlpha;
1569
1432
  materialUniformDataOverEyes[1] = 0.5; // alphaMultiplier: 0.5 for over-eyes
@@ -2010,16 +1873,17 @@ export class Engine {
2010
1873
  }
2011
1874
  // Update model pose and physics
2012
1875
  updateModelPose(deltaTime) {
1876
+ // Step 1: Animation evaluation (computes matrices to CPU memory, no upload yet)
2013
1877
  this.currentModel.evaluatePose();
2014
- // Upload world matrices to GPU
1878
+ // Step 2: Get world matrices (still in CPU memory)
2015
1879
  const worldMats = this.currentModel.getBoneWorldMatrices();
2016
- this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
1880
+ // Step 3: Physics modifies matrices in-place
2017
1881
  if (this.physics) {
2018
1882
  this.physics.step(deltaTime, worldMats, this.currentModel.getBoneInverseBindMatrices());
2019
- // Re-upload world matrices after physics (physics may have updated bones)
2020
- this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
2021
1883
  }
2022
- // Compute skin matrices on GPU
1884
+ // Step 4: Upload ONCE with final result (animation + physics)
1885
+ this.device.queue.writeBuffer(this.worldMatrixBuffer, 0, worldMats.buffer, worldMats.byteOffset, worldMats.byteLength);
1886
+ // Step 5: GPU skinning
2023
1887
  this.computeSkinMatrices();
2024
1888
  }
2025
1889
  // Compute skin matrices on GPU
@@ -2123,20 +1987,56 @@ export class Engine {
2123
1987
  if (skeleton)
2124
1988
  bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
2125
1989
  }
1990
+ if (this.worldMatrixBuffer) {
1991
+ const skeleton = this.currentModel?.getSkeleton();
1992
+ if (skeleton)
1993
+ bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
1994
+ }
1995
+ if (this.inverseBindMatrixBuffer) {
1996
+ const skeleton = this.currentModel?.getSkeleton();
1997
+ if (skeleton)
1998
+ bufferMemoryBytes += Math.max(256, skeleton.bones.length * 16 * 4);
1999
+ }
2126
2000
  bufferMemoryBytes += 40 * 4; // cameraUniformBuffer
2127
2001
  bufferMemoryBytes += 64 * 4; // lightUniformBuffer
2002
+ bufferMemoryBytes += 32; // boneCountBuffer
2003
+ bufferMemoryBytes += 32; // blurDirectionBuffer
2004
+ bufferMemoryBytes += 32; // bloomIntensityBuffer
2005
+ bufferMemoryBytes += 32; // bloomThresholdBuffer
2006
+ if (this.fullscreenQuadBuffer) {
2007
+ bufferMemoryBytes += 24 * 4; // fullscreenQuadBuffer (6 vertices * 4 floats)
2008
+ }
2009
+ // Material uniform buffers: Float32Array(8) = 32 bytes each
2128
2010
  const totalMaterialDraws = this.opaqueNonEyeNonHairDraws.length +
2129
2011
  this.eyeDraws.length +
2130
2012
  this.hairDrawsOverEyes.length +
2131
2013
  this.hairDrawsOverNonEyes.length +
2132
2014
  this.transparentNonEyeNonHairDraws.length;
2133
- bufferMemoryBytes += totalMaterialDraws * 4; // Material uniform buffers
2015
+ bufferMemoryBytes += totalMaterialDraws * 32; // Material uniform buffers (8 floats = 32 bytes)
2016
+ // Outline material uniform buffers: Float32Array(8) = 32 bytes each
2017
+ const totalOutlineDraws = this.opaqueNonEyeNonHairOutlineDraws.length +
2018
+ this.eyeOutlineDraws.length +
2019
+ this.hairOutlineDraws.length +
2020
+ this.transparentNonEyeNonHairOutlineDraws.length;
2021
+ bufferMemoryBytes += totalOutlineDraws * 32; // Outline material uniform buffers
2134
2022
  let renderTargetMemoryBytes = 0;
2135
2023
  if (this.multisampleTexture) {
2136
2024
  const width = this.canvas.width;
2137
2025
  const height = this.canvas.height;
2138
2026
  renderTargetMemoryBytes += width * height * 4 * this.sampleCount; // multisample color
2139
- renderTargetMemoryBytes += width * height * 4; // depth
2027
+ renderTargetMemoryBytes += width * height * 4; // depth (depth24plus-stencil8 = 4 bytes)
2028
+ }
2029
+ if (this.sceneRenderTexture) {
2030
+ const width = this.canvas.width;
2031
+ const height = this.canvas.height;
2032
+ renderTargetMemoryBytes += width * height * 4; // sceneRenderTexture (non-multisampled)
2033
+ }
2034
+ if (this.bloomExtractTexture) {
2035
+ const width = Math.floor(this.canvas.width / 2);
2036
+ const height = Math.floor(this.canvas.height / 2);
2037
+ renderTargetMemoryBytes += width * height * 4; // bloomExtractTexture
2038
+ renderTargetMemoryBytes += width * height * 4; // bloomBlurTexture1
2039
+ renderTargetMemoryBytes += width * height * 4; // bloomBlurTexture2
2140
2040
  }
2141
2041
  const totalGPUMemoryBytes = textureMemoryBytes + bufferMemoryBytes + renderTargetMemoryBytes;
2142
2042
  this.stats.gpuMemory = Math.round((totalGPUMemoryBytes / 1024 / 1024) * 100) / 100;