reze-engine 0.2.0 → 0.2.2

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
@@ -4,17 +4,19 @@ import { PmxLoader } from "./pmx-loader";
4
4
  import { Physics } from "./physics";
5
5
  import { VMDLoader } from "./vmd-loader";
6
6
  export class Engine {
7
- constructor(canvas) {
7
+ constructor(canvas, options) {
8
8
  this.cameraMatrixData = new Float32Array(36);
9
9
  this.lightData = new Float32Array(64);
10
10
  this.lightCount = 0;
11
11
  this.resizeObserver = null;
12
12
  this.sampleCount = 4; // MSAA 4x
13
+ // Ambient light settings
14
+ this.ambient = 1.0;
13
15
  // Bloom settings
14
16
  this.bloomThreshold = 0.3;
15
- this.bloomIntensity = 0.1;
17
+ this.bloomIntensity = 0.12;
16
18
  // Rim light settings
17
- this.rimLightIntensity = 0.35;
19
+ this.rimLightIntensity = 0.45;
18
20
  this.rimLightPower = 2.0;
19
21
  this.rimLightColor = [1.0, 1.0, 1.0];
20
22
  this.currentModel = null;
@@ -47,6 +49,13 @@ export class Engine {
47
49
  this.hairOutlineDraws = [];
48
50
  this.transparentNonEyeNonHairOutlineDraws = [];
49
51
  this.canvas = canvas;
52
+ if (options) {
53
+ this.ambient = options.ambient;
54
+ this.bloomThreshold = options.bloomThreshold;
55
+ this.bloomIntensity = options.bloomIntensity;
56
+ this.rimLightIntensity = options.rimLightIntensity;
57
+ this.rimLightPower = options.rimLightPower;
58
+ }
50
59
  }
51
60
  // Step 1: Get WebGPU device and context
52
61
  async init() {
@@ -84,125 +93,125 @@ export class Engine {
84
93
  });
85
94
  const shaderModule = this.device.createShaderModule({
86
95
  label: "model shaders",
87
- code: /* wgsl */ `
88
- struct CameraUniforms {
89
- view: mat4x4f,
90
- projection: mat4x4f,
91
- viewPos: vec3f,
92
- _padding: f32,
93
- };
94
-
95
- struct Light {
96
- direction: vec3f,
97
- _padding1: f32,
98
- color: vec3f,
99
- intensity: f32,
100
- };
101
-
102
- struct LightUniforms {
103
- ambient: f32,
104
- lightCount: f32,
105
- _padding1: f32,
106
- _padding2: f32,
107
- lights: array<Light, 4>,
108
- };
109
-
110
- struct MaterialUniforms {
111
- alpha: f32,
112
- alphaMultiplier: f32,
113
- rimIntensity: f32,
114
- rimPower: f32,
115
- rimColor: vec3f,
116
- isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
117
- };
118
-
119
- struct VertexOutput {
120
- @builtin(position) position: vec4f,
121
- @location(0) normal: vec3f,
122
- @location(1) uv: vec2f,
123
- @location(2) worldPos: vec3f,
124
- };
125
-
126
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
127
- @group(0) @binding(1) var<uniform> light: LightUniforms;
128
- @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
129
- @group(0) @binding(3) var diffuseSampler: sampler;
130
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
131
- @group(0) @binding(5) var toonTexture: texture_2d<f32>;
132
- @group(0) @binding(6) var toonSampler: sampler;
133
- @group(0) @binding(7) var<uniform> material: MaterialUniforms;
134
-
135
- @vertex fn vs(
136
- @location(0) position: vec3f,
137
- @location(1) normal: vec3f,
138
- @location(2) uv: vec2f,
139
- @location(3) joints0: vec4<u32>,
140
- @location(4) weights0: vec4<f32>
141
- ) -> VertexOutput {
142
- var output: VertexOutput;
143
- let pos4 = vec4f(position, 1.0);
144
-
145
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
146
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
147
- var normalizedWeights: vec4f;
148
- if (weightSum > 0.0001) {
149
- normalizedWeights = weights0 / weightSum;
150
- } else {
151
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
152
- }
153
-
154
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
155
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
156
- for (var i = 0u; i < 4u; i++) {
157
- let j = joints0[i];
158
- let w = normalizedWeights[i];
159
- let m = skinMats[j];
160
- skinnedPos += (m * pos4) * w;
161
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
162
- skinnedNrm += (r3 * normal) * w;
163
- }
164
- let worldPos = skinnedPos.xyz;
165
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
166
- output.normal = normalize(skinnedNrm);
167
- output.uv = uv;
168
- output.worldPos = worldPos;
169
- return output;
170
- }
171
-
172
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
173
- let n = normalize(input.normal);
174
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
175
-
176
- var lightAccum = vec3f(light.ambient);
177
- let numLights = u32(light.lightCount);
178
- for (var i = 0u; i < numLights; i++) {
179
- let l = -light.lights[i].direction;
180
- let nDotL = max(dot(n, l), 0.0);
181
- let toonUV = vec2f(nDotL, 0.5);
182
- let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
183
- let radiance = light.lights[i].color * light.lights[i].intensity;
184
- lightAccum += toonFactor * radiance * nDotL;
185
- }
186
-
187
- // Rim light calculation
188
- let viewDir = normalize(camera.viewPos - input.worldPos);
189
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
190
- rimFactor = pow(rimFactor, material.rimPower);
191
- let rimLight = material.rimColor * material.rimIntensity * rimFactor;
192
-
193
- let color = albedo * lightAccum + rimLight;
194
-
195
- var finalAlpha = material.alpha * material.alphaMultiplier;
196
- if (material.isOverEyes > 0.5) {
197
- finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
198
- }
199
-
200
- if (finalAlpha < 0.001) {
201
- discard;
202
- }
203
-
204
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
205
- }
96
+ code: /* wgsl */ `
97
+ struct CameraUniforms {
98
+ view: mat4x4f,
99
+ projection: mat4x4f,
100
+ viewPos: vec3f,
101
+ _padding: f32,
102
+ };
103
+
104
+ struct Light {
105
+ direction: vec3f,
106
+ _padding1: f32,
107
+ color: vec3f,
108
+ intensity: f32,
109
+ };
110
+
111
+ struct LightUniforms {
112
+ ambient: f32,
113
+ lightCount: f32,
114
+ _padding1: f32,
115
+ _padding2: f32,
116
+ lights: array<Light, 4>,
117
+ };
118
+
119
+ struct MaterialUniforms {
120
+ alpha: f32,
121
+ alphaMultiplier: f32,
122
+ rimIntensity: f32,
123
+ rimPower: f32,
124
+ rimColor: vec3f,
125
+ isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
126
+ };
127
+
128
+ struct VertexOutput {
129
+ @builtin(position) position: vec4f,
130
+ @location(0) normal: vec3f,
131
+ @location(1) uv: vec2f,
132
+ @location(2) worldPos: vec3f,
133
+ };
134
+
135
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
136
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
137
+ @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
138
+ @group(0) @binding(3) var diffuseSampler: sampler;
139
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
140
+ @group(0) @binding(5) var toonTexture: texture_2d<f32>;
141
+ @group(0) @binding(6) var toonSampler: sampler;
142
+ @group(0) @binding(7) var<uniform> material: MaterialUniforms;
143
+
144
+ @vertex fn vs(
145
+ @location(0) position: vec3f,
146
+ @location(1) normal: vec3f,
147
+ @location(2) uv: vec2f,
148
+ @location(3) joints0: vec4<u32>,
149
+ @location(4) weights0: vec4<f32>
150
+ ) -> VertexOutput {
151
+ var output: VertexOutput;
152
+ let pos4 = vec4f(position, 1.0);
153
+
154
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
155
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
156
+ var normalizedWeights: vec4f;
157
+ if (weightSum > 0.0001) {
158
+ normalizedWeights = weights0 / weightSum;
159
+ } else {
160
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
161
+ }
162
+
163
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
164
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
165
+ for (var i = 0u; i < 4u; i++) {
166
+ let j = joints0[i];
167
+ let w = normalizedWeights[i];
168
+ let m = skinMats[j];
169
+ skinnedPos += (m * pos4) * w;
170
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
171
+ skinnedNrm += (r3 * normal) * w;
172
+ }
173
+ let worldPos = skinnedPos.xyz;
174
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
175
+ output.normal = normalize(skinnedNrm);
176
+ output.uv = uv;
177
+ output.worldPos = worldPos;
178
+ return output;
179
+ }
180
+
181
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
182
+ let n = normalize(input.normal);
183
+ let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
184
+
185
+ var lightAccum = vec3f(light.ambient);
186
+ let numLights = u32(light.lightCount);
187
+ for (var i = 0u; i < numLights; i++) {
188
+ let l = -light.lights[i].direction;
189
+ let nDotL = max(dot(n, l), 0.0);
190
+ let toonUV = vec2f(nDotL, 0.5);
191
+ let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
192
+ let radiance = light.lights[i].color * light.lights[i].intensity;
193
+ lightAccum += toonFactor * radiance * nDotL;
194
+ }
195
+
196
+ // Rim light calculation
197
+ let viewDir = normalize(camera.viewPos - input.worldPos);
198
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
199
+ rimFactor = pow(rimFactor, material.rimPower);
200
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
201
+
202
+ let color = albedo * lightAccum + rimLight;
203
+
204
+ var finalAlpha = material.alpha * material.alphaMultiplier;
205
+ if (material.isOverEyes > 0.5) {
206
+ finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
207
+ }
208
+
209
+ if (finalAlpha < 0.001) {
210
+ discard;
211
+ }
212
+
213
+ return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
214
+ }
206
215
  `,
207
216
  });
208
217
  // Create explicit bind group layout for all pipelines using the main shader
@@ -293,77 +302,77 @@ export class Engine {
293
302
  });
294
303
  const outlineShaderModule = this.device.createShaderModule({
295
304
  label: "outline shaders",
296
- code: /* wgsl */ `
297
- struct CameraUniforms {
298
- view: mat4x4f,
299
- projection: mat4x4f,
300
- viewPos: vec3f,
301
- _padding: f32,
302
- };
303
-
304
- struct MaterialUniforms {
305
- edgeColor: vec4f,
306
- edgeSize: f32,
307
- isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
308
- _padding1: f32,
309
- _padding2: f32,
310
- };
311
-
312
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
313
- @group(0) @binding(1) var<uniform> material: MaterialUniforms;
314
- @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
315
-
316
- struct VertexOutput {
317
- @builtin(position) position: vec4f,
318
- };
319
-
320
- @vertex fn vs(
321
- @location(0) position: vec3f,
322
- @location(1) normal: vec3f,
323
- @location(3) joints0: vec4<u32>,
324
- @location(4) weights0: vec4<f32>
325
- ) -> VertexOutput {
326
- var output: VertexOutput;
327
- let pos4 = vec4f(position, 1.0);
328
-
329
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
330
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
331
- var normalizedWeights: vec4f;
332
- if (weightSum > 0.0001) {
333
- normalizedWeights = weights0 / weightSum;
334
- } else {
335
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
336
- }
337
-
338
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
339
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
340
- for (var i = 0u; i < 4u; i++) {
341
- let j = joints0[i];
342
- let w = normalizedWeights[i];
343
- let m = skinMats[j];
344
- skinnedPos += (m * pos4) * w;
345
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
346
- skinnedNrm += (r3 * normal) * w;
347
- }
348
- let worldPos = skinnedPos.xyz;
349
- let worldNormal = normalize(skinnedNrm);
350
-
351
- // MMD invert hull: expand vertices outward along normals
352
- let scaleFactor = 0.01;
353
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
354
- output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
355
- return output;
356
- }
357
-
358
- @fragment fn fs() -> @location(0) vec4f {
359
- var color = material.edgeColor;
360
-
361
- if (material.isOverEyes > 0.5) {
362
- color.a *= 0.5; // Hair outlines over eyes get 50% alpha
363
- }
364
-
365
- return color;
366
- }
305
+ code: /* wgsl */ `
306
+ struct CameraUniforms {
307
+ view: mat4x4f,
308
+ projection: mat4x4f,
309
+ viewPos: vec3f,
310
+ _padding: f32,
311
+ };
312
+
313
+ struct MaterialUniforms {
314
+ edgeColor: vec4f,
315
+ edgeSize: f32,
316
+ isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
317
+ _padding1: f32,
318
+ _padding2: f32,
319
+ };
320
+
321
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
322
+ @group(0) @binding(1) var<uniform> material: MaterialUniforms;
323
+ @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
324
+
325
+ struct VertexOutput {
326
+ @builtin(position) position: vec4f,
327
+ };
328
+
329
+ @vertex fn vs(
330
+ @location(0) position: vec3f,
331
+ @location(1) normal: vec3f,
332
+ @location(3) joints0: vec4<u32>,
333
+ @location(4) weights0: vec4<f32>
334
+ ) -> VertexOutput {
335
+ var output: VertexOutput;
336
+ let pos4 = vec4f(position, 1.0);
337
+
338
+ // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
339
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
340
+ var normalizedWeights: vec4f;
341
+ if (weightSum > 0.0001) {
342
+ normalizedWeights = weights0 / weightSum;
343
+ } else {
344
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
345
+ }
346
+
347
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
348
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
349
+ for (var i = 0u; i < 4u; i++) {
350
+ let j = joints0[i];
351
+ let w = normalizedWeights[i];
352
+ let m = skinMats[j];
353
+ skinnedPos += (m * pos4) * w;
354
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
355
+ skinnedNrm += (r3 * normal) * w;
356
+ }
357
+ let worldPos = skinnedPos.xyz;
358
+ let worldNormal = normalize(skinnedNrm);
359
+
360
+ // MMD invert hull: expand vertices outward along normals
361
+ let scaleFactor = 0.01;
362
+ let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
363
+ output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
364
+ return output;
365
+ }
366
+
367
+ @fragment fn fs() -> @location(0) vec4f {
368
+ var color = material.edgeColor;
369
+
370
+ if (material.isOverEyes > 0.5) {
371
+ color.a *= 0.5; // Hair outlines over eyes get 50% alpha
372
+ }
373
+
374
+ return color;
375
+ }
367
376
  `,
368
377
  });
369
378
  this.outlinePipeline = this.device.createRenderPipeline({
@@ -564,49 +573,49 @@ export class Engine {
564
573
  // Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
565
574
  const depthOnlyShaderModule = this.device.createShaderModule({
566
575
  label: "depth only shader",
567
- code: /* wgsl */ `
568
- struct CameraUniforms {
569
- view: mat4x4f,
570
- projection: mat4x4f,
571
- viewPos: vec3f,
572
- _padding: f32,
573
- };
574
-
575
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
576
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
577
-
578
- @vertex fn vs(
579
- @location(0) position: vec3f,
580
- @location(1) normal: vec3f,
581
- @location(3) joints0: vec4<u32>,
582
- @location(4) weights0: vec4<f32>
583
- ) -> @builtin(position) vec4f {
584
- let pos4 = vec4f(position, 1.0);
585
-
586
- // Normalize weights
587
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
588
- var normalizedWeights: vec4f;
589
- if (weightSum > 0.0001) {
590
- normalizedWeights = weights0 / weightSum;
591
- } else {
592
- normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
593
- }
594
-
595
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
596
- for (var i = 0u; i < 4u; i++) {
597
- let j = joints0[i];
598
- let w = normalizedWeights[i];
599
- let m = skinMats[j];
600
- skinnedPos += (m * pos4) * w;
601
- }
602
- let worldPos = skinnedPos.xyz;
603
- let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
604
- return clipPos;
605
- }
606
-
607
- @fragment fn fs() -> @location(0) vec4f {
608
- return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
609
- }
576
+ code: /* wgsl */ `
577
+ struct CameraUniforms {
578
+ view: mat4x4f,
579
+ projection: mat4x4f,
580
+ viewPos: vec3f,
581
+ _padding: f32,
582
+ };
583
+
584
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
585
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
586
+
587
+ @vertex fn vs(
588
+ @location(0) position: vec3f,
589
+ @location(1) normal: vec3f,
590
+ @location(3) joints0: vec4<u32>,
591
+ @location(4) weights0: vec4<f32>
592
+ ) -> @builtin(position) vec4f {
593
+ let pos4 = vec4f(position, 1.0);
594
+
595
+ // Normalize weights
596
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
597
+ var normalizedWeights: vec4f;
598
+ if (weightSum > 0.0001) {
599
+ normalizedWeights = weights0 / weightSum;
600
+ } else {
601
+ normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
602
+ }
603
+
604
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
605
+ for (var i = 0u; i < 4u; i++) {
606
+ let j = joints0[i];
607
+ let w = normalizedWeights[i];
608
+ let m = skinMats[j];
609
+ skinnedPos += (m * pos4) * w;
610
+ }
611
+ let worldPos = skinnedPos.xyz;
612
+ let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
613
+ return clipPos;
614
+ }
615
+
616
+ @fragment fn fs() -> @location(0) vec4f {
617
+ return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
618
+ }
610
619
  `,
611
620
  });
612
621
  // Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
@@ -786,31 +795,31 @@ export class Engine {
786
795
  createSkinMatrixComputePipeline() {
787
796
  const computeShader = this.device.createShaderModule({
788
797
  label: "skin matrix compute",
789
- code: /* wgsl */ `
790
- struct BoneCountUniform {
791
- count: u32,
792
- _padding1: u32,
793
- _padding2: u32,
794
- _padding3: u32,
795
- _padding4: vec4<u32>,
796
- };
797
-
798
- @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
799
- @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
800
- @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
801
- @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
802
-
803
- @compute @workgroup_size(64)
804
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
805
- let boneIndex = globalId.x;
806
- // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
807
- if (boneIndex >= boneCount.count) {
808
- return;
809
- }
810
- let worldMat = worldMatrices[boneIndex];
811
- let invBindMat = inverseBindMatrices[boneIndex];
812
- skinMatrices[boneIndex] = worldMat * invBindMat;
813
- }
798
+ code: /* wgsl */ `
799
+ struct BoneCountUniform {
800
+ count: u32,
801
+ _padding1: u32,
802
+ _padding2: u32,
803
+ _padding3: u32,
804
+ _padding4: vec4<u32>,
805
+ };
806
+
807
+ @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
808
+ @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
809
+ @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
810
+ @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
811
+
812
+ @compute @workgroup_size(64)
813
+ fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
814
+ let boneIndex = globalId.x;
815
+ // Bounds check: we dispatch workgroups (64 threads each), so some threads may be out of range
816
+ if (boneIndex >= boneCount.count) {
817
+ return;
818
+ }
819
+ let worldMat = worldMatrices[boneIndex];
820
+ let invBindMat = inverseBindMatrices[boneIndex];
821
+ skinMatrices[boneIndex] = worldMat * invBindMat;
822
+ }
814
823
  `,
815
824
  });
816
825
  this.skinMatrixComputePipeline = this.device.createComputePipeline({
@@ -864,143 +873,143 @@ export class Engine {
864
873
  // Bloom extraction shader (extracts bright areas)
865
874
  const bloomExtractShader = this.device.createShaderModule({
866
875
  label: "bloom extract",
867
- code: /* wgsl */ `
868
- struct VertexOutput {
869
- @builtin(position) position: vec4f,
870
- @location(0) uv: vec2f,
871
- };
872
-
873
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
874
- var output: VertexOutput;
875
- // Generate fullscreen quad from vertex index
876
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
877
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
878
- output.position = vec4f(x, y, 0.0, 1.0);
879
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
880
- return output;
881
- }
882
-
883
- struct BloomExtractUniforms {
884
- threshold: f32,
885
- _padding1: f32,
886
- _padding2: f32,
887
- _padding3: f32,
888
- _padding4: f32,
889
- _padding5: f32,
890
- _padding6: f32,
891
- _padding7: f32,
892
- };
893
-
894
- @group(0) @binding(0) var inputTexture: texture_2d<f32>;
895
- @group(0) @binding(1) var inputSampler: sampler;
896
- @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
897
-
898
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
899
- let color = textureSample(inputTexture, inputSampler, input.uv);
900
- // Extract bright areas above threshold
901
- let threshold = extractUniforms.threshold;
902
- let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
903
- return vec4f(bloom, color.a);
904
- }
876
+ code: /* wgsl */ `
877
+ struct VertexOutput {
878
+ @builtin(position) position: vec4f,
879
+ @location(0) uv: vec2f,
880
+ };
881
+
882
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
883
+ var output: VertexOutput;
884
+ // Generate fullscreen quad from vertex index
885
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
886
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
887
+ output.position = vec4f(x, y, 0.0, 1.0);
888
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
889
+ return output;
890
+ }
891
+
892
+ struct BloomExtractUniforms {
893
+ threshold: f32,
894
+ _padding1: f32,
895
+ _padding2: f32,
896
+ _padding3: f32,
897
+ _padding4: f32,
898
+ _padding5: f32,
899
+ _padding6: f32,
900
+ _padding7: f32,
901
+ };
902
+
903
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
904
+ @group(0) @binding(1) var inputSampler: sampler;
905
+ @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
906
+
907
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
908
+ let color = textureSample(inputTexture, inputSampler, input.uv);
909
+ // Extract bright areas above threshold
910
+ let threshold = extractUniforms.threshold;
911
+ let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
912
+ return vec4f(bloom, color.a);
913
+ }
905
914
  `,
906
915
  });
907
916
  // Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
908
917
  const bloomBlurShader = this.device.createShaderModule({
909
918
  label: "bloom blur",
910
- code: /* wgsl */ `
911
- struct VertexOutput {
912
- @builtin(position) position: vec4f,
913
- @location(0) uv: vec2f,
914
- };
915
-
916
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
917
- var output: VertexOutput;
918
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
919
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
920
- output.position = vec4f(x, y, 0.0, 1.0);
921
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
922
- return output;
923
- }
924
-
925
- struct BlurUniforms {
926
- direction: vec2f,
927
- _padding1: f32,
928
- _padding2: f32,
929
- _padding3: f32,
930
- _padding4: f32,
931
- _padding5: f32,
932
- _padding6: f32,
933
- };
934
-
935
- @group(0) @binding(0) var inputTexture: texture_2d<f32>;
936
- @group(0) @binding(1) var inputSampler: sampler;
937
- @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
938
-
939
- // 9-tap gaussian blur
940
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
941
- let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
942
- var result = vec4f(0.0);
943
-
944
- // Gaussian weights for 9-tap filter
945
- let weights = array<f32, 9>(
946
- 0.01621622, 0.05405405, 0.12162162,
947
- 0.19459459, 0.22702703,
948
- 0.19459459, 0.12162162, 0.05405405, 0.01621622
949
- );
950
-
951
- let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
952
-
953
- for (var i = 0u; i < 9u; i++) {
954
- let offset = offsets[i] * texelSize * blurUniforms.direction;
955
- result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
956
- }
957
-
958
- return result;
959
- }
919
+ code: /* wgsl */ `
920
+ struct VertexOutput {
921
+ @builtin(position) position: vec4f,
922
+ @location(0) uv: vec2f,
923
+ };
924
+
925
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
926
+ var output: VertexOutput;
927
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
928
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
929
+ output.position = vec4f(x, y, 0.0, 1.0);
930
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
931
+ return output;
932
+ }
933
+
934
+ struct BlurUniforms {
935
+ direction: vec2f,
936
+ _padding1: f32,
937
+ _padding2: f32,
938
+ _padding3: f32,
939
+ _padding4: f32,
940
+ _padding5: f32,
941
+ _padding6: f32,
942
+ };
943
+
944
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
945
+ @group(0) @binding(1) var inputSampler: sampler;
946
+ @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
947
+
948
+ // 9-tap gaussian blur
949
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
950
+ let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
951
+ var result = vec4f(0.0);
952
+
953
+ // Gaussian weights for 9-tap filter
954
+ let weights = array<f32, 9>(
955
+ 0.01621622, 0.05405405, 0.12162162,
956
+ 0.19459459, 0.22702703,
957
+ 0.19459459, 0.12162162, 0.05405405, 0.01621622
958
+ );
959
+
960
+ let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
961
+
962
+ for (var i = 0u; i < 9u; i++) {
963
+ let offset = offsets[i] * texelSize * blurUniforms.direction;
964
+ result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
965
+ }
966
+
967
+ return result;
968
+ }
960
969
  `,
961
970
  });
962
971
  // Bloom composition shader (combines original scene with bloom)
963
972
  const bloomComposeShader = this.device.createShaderModule({
964
973
  label: "bloom compose",
965
- code: /* wgsl */ `
966
- struct VertexOutput {
967
- @builtin(position) position: vec4f,
968
- @location(0) uv: vec2f,
969
- };
970
-
971
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
972
- var output: VertexOutput;
973
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
974
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
975
- output.position = vec4f(x, y, 0.0, 1.0);
976
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
977
- return output;
978
- }
979
-
980
- struct BloomComposeUniforms {
981
- intensity: f32,
982
- _padding1: f32,
983
- _padding2: f32,
984
- _padding3: f32,
985
- _padding4: f32,
986
- _padding5: f32,
987
- _padding6: f32,
988
- _padding7: f32,
989
- };
990
-
991
- @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
992
- @group(0) @binding(1) var sceneSampler: sampler;
993
- @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
994
- @group(0) @binding(3) var bloomSampler: sampler;
995
- @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
996
-
997
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
998
- let scene = textureSample(sceneTexture, sceneSampler, input.uv);
999
- let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
1000
- // Additive blending with intensity control
1001
- let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
1002
- return vec4f(result, scene.a);
1003
- }
974
+ code: /* wgsl */ `
975
+ struct VertexOutput {
976
+ @builtin(position) position: vec4f,
977
+ @location(0) uv: vec2f,
978
+ };
979
+
980
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
981
+ var output: VertexOutput;
982
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
983
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
984
+ output.position = vec4f(x, y, 0.0, 1.0);
985
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
986
+ return output;
987
+ }
988
+
989
+ struct BloomComposeUniforms {
990
+ intensity: f32,
991
+ _padding1: f32,
992
+ _padding2: f32,
993
+ _padding3: f32,
994
+ _padding4: f32,
995
+ _padding5: f32,
996
+ _padding6: f32,
997
+ _padding7: f32,
998
+ };
999
+
1000
+ @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
1001
+ @group(0) @binding(1) var sceneSampler: sampler;
1002
+ @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
1003
+ @group(0) @binding(3) var bloomSampler: sampler;
1004
+ @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
1005
+
1006
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1007
+ let scene = textureSample(sceneTexture, sceneSampler, input.uv);
1008
+ let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
1009
+ // Additive blending with intensity control
1010
+ let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
1011
+ return vec4f(result, scene.a);
1012
+ }
1004
1013
  `,
1005
1014
  });
1006
1015
  // Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
@@ -1235,10 +1244,10 @@ export class Engine {
1235
1244
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1236
1245
  });
1237
1246
  this.lightCount = 0;
1238
- this.setAmbient(0.96);
1239
- this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.12);
1240
- this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.1);
1241
- this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.08);
1247
+ this.setAmbient(this.ambient);
1248
+ this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.02);
1249
+ this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.015);
1250
+ this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.01);
1242
1251
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
1243
1252
  }
1244
1253
  addLight(direction, color, intensity = 1.0) {