reze-engine 0.2.14 → 0.2.16

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
@@ -8,8 +8,7 @@ export class Engine {
8
8
  this.cameraMatrixData = new Float32Array(36);
9
9
  this.cameraDistance = 26.6;
10
10
  this.cameraTarget = new Vec3(0, 12.5, 0);
11
- this.lightData = new Float32Array(64);
12
- this.lightCount = 0;
11
+ this.lightData = new Float32Array(4);
13
12
  this.resizeObserver = null;
14
13
  this.sampleCount = 4;
15
14
  // Constants
@@ -17,9 +16,9 @@ export class Engine {
17
16
  this.COMPUTE_WORKGROUP_SIZE = 64;
18
17
  this.BLOOM_DOWNSCALE_FACTOR = 2;
19
18
  // Ambient light settings
20
- this.ambient = 1.0;
19
+ this.ambientColor = new Vec3(1.0, 1.0, 1.0);
21
20
  // Bloom settings
22
- this.bloomThreshold = 0.3;
21
+ this.bloomThreshold = 0.01;
23
22
  this.bloomIntensity = 0.12;
24
23
  // Rim light settings
25
24
  this.rimLightIntensity = 0.45;
@@ -59,7 +58,7 @@ export class Engine {
59
58
  this.breathingBaseRotations = new Map();
60
59
  this.canvas = canvas;
61
60
  if (options) {
62
- this.ambient = options.ambient ?? 1.0;
61
+ this.ambientColor = options.ambientColor ?? new Vec3(1.0, 1.0, 1.0);
63
62
  this.bloomIntensity = options.bloomIntensity ?? 0.12;
64
63
  this.rimLightIntensity = options.rimLightIntensity ?? 0.45;
65
64
  this.cameraDistance = options.cameraDistance ?? 26.6;
@@ -101,121 +100,101 @@ export class Engine {
101
100
  });
102
101
  const shaderModule = this.device.createShaderModule({
103
102
  label: "model shaders",
104
- code: /* wgsl */ `
105
- struct CameraUniforms {
106
- view: mat4x4f,
107
- projection: mat4x4f,
108
- viewPos: vec3f,
109
- _padding: f32,
110
- };
111
-
112
- struct Light {
113
- direction: vec3f,
114
- _padding1: f32,
115
- color: vec3f,
116
- intensity: f32,
117
- };
118
-
119
- struct LightUniforms {
120
- ambient: f32,
121
- lightCount: f32,
122
- _padding1: f32,
123
- _padding2: f32,
124
- lights: array<Light, 4>,
125
- };
126
-
127
- struct MaterialUniforms {
128
- alpha: f32,
129
- alphaMultiplier: f32,
130
- rimIntensity: f32,
131
- _padding1: f32,
132
- rimColor: vec3f,
133
- isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
134
- };
135
-
136
- struct VertexOutput {
137
- @builtin(position) position: vec4f,
138
- @location(0) normal: vec3f,
139
- @location(1) uv: vec2f,
140
- @location(2) worldPos: vec3f,
141
- };
142
-
143
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
144
- @group(0) @binding(1) var<uniform> light: LightUniforms;
145
- @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
146
- @group(0) @binding(3) var diffuseSampler: sampler;
147
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
148
- @group(0) @binding(5) var toonTexture: texture_2d<f32>;
149
- @group(0) @binding(6) var toonSampler: sampler;
150
- @group(0) @binding(7) var<uniform> material: MaterialUniforms;
151
-
152
- @vertex fn vs(
153
- @location(0) position: vec3f,
154
- @location(1) normal: vec3f,
155
- @location(2) uv: vec2f,
156
- @location(3) joints0: vec4<u32>,
157
- @location(4) weights0: vec4<f32>
158
- ) -> VertexOutput {
159
- var output: VertexOutput;
160
- let pos4 = vec4f(position, 1.0);
161
-
162
- // Branchless weight normalization (avoids GPU branch divergence)
163
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
164
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
165
- let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
166
-
167
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
168
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
169
- for (var i = 0u; i < 4u; i++) {
170
- let j = joints0[i];
171
- let w = normalizedWeights[i];
172
- let m = skinMats[j];
173
- skinnedPos += (m * pos4) * w;
174
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
175
- skinnedNrm += (r3 * normal) * w;
176
- }
177
- let worldPos = skinnedPos.xyz;
178
- output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
179
- output.normal = normalize(skinnedNrm);
180
- output.uv = uv;
181
- output.worldPos = worldPos;
182
- return output;
183
- }
184
-
185
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
186
- // Early alpha test - discard before expensive calculations
187
- var finalAlpha = material.alpha * material.alphaMultiplier;
188
- if (material.isOverEyes > 0.5) {
189
- finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
190
- }
191
- if (finalAlpha < 0.001) {
192
- discard;
193
- }
194
-
195
- let n = normalize(input.normal);
196
- let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
197
-
198
- var lightAccum = vec3f(light.ambient);
199
- let numLights = u32(light.lightCount);
200
- for (var i = 0u; i < numLights; i++) {
201
- let l = -light.lights[i].direction;
202
- let nDotL = max(dot(n, l), 0.0);
203
- let toonUV = vec2f(nDotL, 0.5);
204
- let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
205
- let radiance = light.lights[i].color * light.lights[i].intensity;
206
- lightAccum += toonFactor * radiance * nDotL;
207
- }
208
-
209
- // Rim light calculation
210
- let viewDir = normalize(camera.viewPos - input.worldPos);
211
- var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
212
- rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
213
- let rimLight = material.rimColor * material.rimIntensity * rimFactor;
214
-
215
- let color = albedo * lightAccum + rimLight;
216
-
217
- return vec4f(color, finalAlpha);
218
- }
103
+ code: /* wgsl */ `
104
+ struct CameraUniforms {
105
+ view: mat4x4f,
106
+ projection: mat4x4f,
107
+ viewPos: vec3f,
108
+ _padding: f32,
109
+ };
110
+
111
+ struct LightUniforms {
112
+ ambientColor: vec3f,
113
+ };
114
+
115
+ struct MaterialUniforms {
116
+ alpha: f32,
117
+ alphaMultiplier: f32,
118
+ rimIntensity: f32,
119
+ _padding1: f32,
120
+ rimColor: vec3f,
121
+ isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
122
+ };
123
+
124
+ struct VertexOutput {
125
+ @builtin(position) position: vec4f,
126
+ @location(0) normal: vec3f,
127
+ @location(1) uv: vec2f,
128
+ @location(2) worldPos: vec3f,
129
+ };
130
+
131
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
132
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
133
+ @group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
134
+ @group(0) @binding(3) var diffuseSampler: sampler;
135
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
136
+ @group(0) @binding(5) var toonTexture: texture_2d<f32>;
137
+ @group(0) @binding(6) var toonSampler: sampler;
138
+ @group(0) @binding(7) var<uniform> material: MaterialUniforms;
139
+
140
+ @vertex fn vs(
141
+ @location(0) position: vec3f,
142
+ @location(1) normal: vec3f,
143
+ @location(2) uv: vec2f,
144
+ @location(3) joints0: vec4<u32>,
145
+ @location(4) weights0: vec4<f32>
146
+ ) -> VertexOutput {
147
+ var output: VertexOutput;
148
+ let pos4 = vec4f(position, 1.0);
149
+
150
+ // Branchless weight normalization (avoids GPU branch divergence)
151
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
152
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
153
+ let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
154
+
155
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
156
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
157
+ for (var i = 0u; i < 4u; i++) {
158
+ let j = joints0[i];
159
+ let w = normalizedWeights[i];
160
+ let m = skinMats[j];
161
+ skinnedPos += (m * pos4) * w;
162
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
163
+ skinnedNrm += (r3 * normal) * w;
164
+ }
165
+ let worldPos = skinnedPos.xyz;
166
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
167
+ output.normal = normalize(skinnedNrm);
168
+ output.uv = uv;
169
+ output.worldPos = worldPos;
170
+ return output;
171
+ }
172
+
173
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
174
+ // Early alpha test - discard before expensive calculations
175
+ var finalAlpha = material.alpha * material.alphaMultiplier;
176
+ if (material.isOverEyes > 0.5) {
177
+ finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
178
+ }
179
+ if (finalAlpha < 0.001) {
180
+ discard;
181
+ }
182
+
183
+ let n = normalize(input.normal);
184
+ let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
185
+
186
+ let lightAccum = light.ambientColor;
187
+
188
+ // Rim light calculation
189
+ let viewDir = normalize(camera.viewPos - input.worldPos);
190
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
191
+ rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
192
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
193
+
194
+ let color = albedo * lightAccum + rimLight;
195
+
196
+ return vec4f(color, finalAlpha);
197
+ }
219
198
  `,
220
199
  });
221
200
  // Create explicit bind group layout for all pipelines using the main shader
@@ -305,73 +284,73 @@ export class Engine {
305
284
  });
306
285
  const outlineShaderModule = this.device.createShaderModule({
307
286
  label: "outline shaders",
308
- code: /* wgsl */ `
309
- struct CameraUniforms {
310
- view: mat4x4f,
311
- projection: mat4x4f,
312
- viewPos: vec3f,
313
- _padding: f32,
314
- };
315
-
316
- struct MaterialUniforms {
317
- edgeColor: vec4f,
318
- edgeSize: f32,
319
- isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
320
- _padding1: f32,
321
- _padding2: f32,
322
- };
323
-
324
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
325
- @group(0) @binding(1) var<uniform> material: MaterialUniforms;
326
- @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
327
-
328
- struct VertexOutput {
329
- @builtin(position) position: vec4f,
330
- };
331
-
332
- @vertex fn vs(
333
- @location(0) position: vec3f,
334
- @location(1) normal: vec3f,
335
- @location(3) joints0: vec4<u32>,
336
- @location(4) weights0: vec4<f32>
337
- ) -> VertexOutput {
338
- var output: VertexOutput;
339
- let pos4 = vec4f(position, 1.0);
340
-
341
- // Branchless weight normalization (avoids GPU branch divergence)
342
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
343
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
344
- let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
345
-
346
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
347
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
348
- for (var i = 0u; i < 4u; i++) {
349
- let j = joints0[i];
350
- let w = normalizedWeights[i];
351
- let m = skinMats[j];
352
- skinnedPos += (m * pos4) * w;
353
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
354
- skinnedNrm += (r3 * normal) * w;
355
- }
356
- let worldPos = skinnedPos.xyz;
357
- let worldNormal = normalize(skinnedNrm);
358
-
359
- // MMD invert hull: expand vertices outward along normals
360
- let scaleFactor = 0.01;
361
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
362
- output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
363
- return output;
364
- }
365
-
366
- @fragment fn fs() -> @location(0) vec4f {
367
- var color = material.edgeColor;
368
-
369
- if (material.isOverEyes > 0.5) {
370
- color.a *= 0.5; // Hair outlines over eyes get 50% alpha
371
- }
372
-
373
- return color;
374
- }
287
+ code: /* wgsl */ `
288
+ struct CameraUniforms {
289
+ view: mat4x4f,
290
+ projection: mat4x4f,
291
+ viewPos: vec3f,
292
+ _padding: f32,
293
+ };
294
+
295
+ struct MaterialUniforms {
296
+ edgeColor: vec4f,
297
+ edgeSize: f32,
298
+ isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
299
+ _padding1: f32,
300
+ _padding2: f32,
301
+ };
302
+
303
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
304
+ @group(0) @binding(1) var<uniform> material: MaterialUniforms;
305
+ @group(0) @binding(2) var<storage, read> skinMats: array<mat4x4f>;
306
+
307
+ struct VertexOutput {
308
+ @builtin(position) position: vec4f,
309
+ };
310
+
311
+ @vertex fn vs(
312
+ @location(0) position: vec3f,
313
+ @location(1) normal: vec3f,
314
+ @location(3) joints0: vec4<u32>,
315
+ @location(4) weights0: vec4<f32>
316
+ ) -> VertexOutput {
317
+ var output: VertexOutput;
318
+ let pos4 = vec4f(position, 1.0);
319
+
320
+ // Branchless weight normalization (avoids GPU branch divergence)
321
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
322
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
323
+ let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
324
+
325
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
326
+ var skinnedNrm = vec3f(0.0, 0.0, 0.0);
327
+ for (var i = 0u; i < 4u; i++) {
328
+ let j = joints0[i];
329
+ let w = normalizedWeights[i];
330
+ let m = skinMats[j];
331
+ skinnedPos += (m * pos4) * w;
332
+ let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
333
+ skinnedNrm += (r3 * normal) * w;
334
+ }
335
+ let worldPos = skinnedPos.xyz;
336
+ let worldNormal = normalize(skinnedNrm);
337
+
338
+ // MMD invert hull: expand vertices outward along normals
339
+ let scaleFactor = 0.01;
340
+ let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
341
+ output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
342
+ return output;
343
+ }
344
+
345
+ @fragment fn fs() -> @location(0) vec4f {
346
+ var color = material.edgeColor;
347
+
348
+ if (material.isOverEyes > 0.5) {
349
+ color.a *= 0.5; // Hair outlines over eyes get 50% alpha
350
+ }
351
+
352
+ return color;
353
+ }
375
354
  `,
376
355
  });
377
356
  this.outlinePipeline = this.device.createRenderPipeline({
@@ -575,45 +554,45 @@ export class Engine {
575
554
  // Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
576
555
  const depthOnlyShaderModule = this.device.createShaderModule({
577
556
  label: "depth only shader",
578
- code: /* wgsl */ `
579
- struct CameraUniforms {
580
- view: mat4x4f,
581
- projection: mat4x4f,
582
- viewPos: vec3f,
583
- _padding: f32,
584
- };
585
-
586
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
587
- @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
588
-
589
- @vertex fn vs(
590
- @location(0) position: vec3f,
591
- @location(1) normal: vec3f,
592
- @location(3) joints0: vec4<u32>,
593
- @location(4) weights0: vec4<f32>
594
- ) -> @builtin(position) vec4f {
595
- let pos4 = vec4f(position, 1.0);
596
-
597
- // Branchless weight normalization (avoids GPU branch divergence)
598
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
599
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
600
- let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
601
-
602
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
603
- for (var i = 0u; i < 4u; i++) {
604
- let j = joints0[i];
605
- let w = normalizedWeights[i];
606
- let m = skinMats[j];
607
- skinnedPos += (m * pos4) * w;
608
- }
609
- let worldPos = skinnedPos.xyz;
610
- let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
611
- return clipPos;
612
- }
613
-
614
- @fragment fn fs() -> @location(0) vec4f {
615
- return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
616
- }
557
+ code: /* wgsl */ `
558
+ struct CameraUniforms {
559
+ view: mat4x4f,
560
+ projection: mat4x4f,
561
+ viewPos: vec3f,
562
+ _padding: f32,
563
+ };
564
+
565
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
566
+ @group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
567
+
568
+ @vertex fn vs(
569
+ @location(0) position: vec3f,
570
+ @location(1) normal: vec3f,
571
+ @location(3) joints0: vec4<u32>,
572
+ @location(4) weights0: vec4<f32>
573
+ ) -> @builtin(position) vec4f {
574
+ let pos4 = vec4f(position, 1.0);
575
+
576
+ // Branchless weight normalization (avoids GPU branch divergence)
577
+ let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
578
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
579
+ let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
580
+
581
+ var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
582
+ for (var i = 0u; i < 4u; i++) {
583
+ let j = joints0[i];
584
+ let w = normalizedWeights[i];
585
+ let m = skinMats[j];
586
+ skinnedPos += (m * pos4) * w;
587
+ }
588
+ let worldPos = skinnedPos.xyz;
589
+ let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
590
+ return clipPos;
591
+ }
592
+
593
+ @fragment fn fs() -> @location(0) vec4f {
594
+ return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
595
+ }
617
596
  `,
618
597
  });
619
598
  // Hair depth pre-pass pipeline: depth-only with color writes disabled to eliminate overdraw
@@ -796,30 +775,30 @@ export class Engine {
796
775
  createSkinMatrixComputePipeline() {
797
776
  const computeShader = this.device.createShaderModule({
798
777
  label: "skin matrix compute",
799
- code: /* wgsl */ `
800
- struct BoneCountUniform {
801
- count: u32,
802
- _padding1: u32,
803
- _padding2: u32,
804
- _padding3: u32,
805
- _padding4: vec4<u32>,
806
- };
807
-
808
- @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
809
- @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
810
- @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
811
- @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
812
-
813
- @compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
814
- fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
815
- let boneIndex = globalId.x;
816
- if (boneIndex >= boneCount.count) {
817
- return;
818
- }
819
- let worldMat = worldMatrices[boneIndex];
820
- let invBindMat = inverseBindMatrices[boneIndex];
821
- skinMatrices[boneIndex] = worldMat * invBindMat;
822
- }
778
+ code: /* wgsl */ `
779
+ struct BoneCountUniform {
780
+ count: u32,
781
+ _padding1: u32,
782
+ _padding2: u32,
783
+ _padding3: u32,
784
+ _padding4: vec4<u32>,
785
+ };
786
+
787
+ @group(0) @binding(0) var<uniform> boneCount: BoneCountUniform;
788
+ @group(0) @binding(1) var<storage, read> worldMatrices: array<mat4x4f>;
789
+ @group(0) @binding(2) var<storage, read> inverseBindMatrices: array<mat4x4f>;
790
+ @group(0) @binding(3) var<storage, read_write> skinMatrices: array<mat4x4f>;
791
+
792
+ @compute @workgroup_size(64) // Must match COMPUTE_WORKGROUP_SIZE
793
+ fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
794
+ let boneIndex = globalId.x;
795
+ if (boneIndex >= boneCount.count) {
796
+ return;
797
+ }
798
+ let worldMat = worldMatrices[boneIndex];
799
+ let invBindMat = inverseBindMatrices[boneIndex];
800
+ skinMatrices[boneIndex] = worldMat * invBindMat;
801
+ }
823
802
  `,
824
803
  });
825
804
  this.skinMatrixComputePipeline = this.device.createComputePipeline({
@@ -873,140 +852,140 @@ export class Engine {
873
852
  // Bloom extraction shader (extracts bright areas)
874
853
  const bloomExtractShader = this.device.createShaderModule({
875
854
  label: "bloom extract",
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
- }
855
+ code: /* wgsl */ `
856
+ struct VertexOutput {
857
+ @builtin(position) position: vec4f,
858
+ @location(0) uv: vec2f,
859
+ };
860
+
861
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
862
+ var output: VertexOutput;
863
+ // Generate fullscreen quad from vertex index
864
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
865
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
866
+ output.position = vec4f(x, y, 0.0, 1.0);
867
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
868
+ return output;
869
+ }
870
+
871
+ struct BloomExtractUniforms {
872
+ threshold: f32,
873
+ _padding1: f32,
874
+ _padding2: f32,
875
+ _padding3: f32,
876
+ _padding4: f32,
877
+ _padding5: f32,
878
+ _padding6: f32,
879
+ _padding7: f32,
880
+ };
881
+
882
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
883
+ @group(0) @binding(1) var inputSampler: sampler;
884
+ @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
885
+
886
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
887
+ let color = textureSample(inputTexture, inputSampler, input.uv);
888
+ // Extract bright areas above threshold
889
+ let threshold = extractUniforms.threshold;
890
+ let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
891
+ return vec4f(bloom, color.a);
892
+ }
914
893
  `,
915
894
  });
916
895
  // Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
917
896
  const bloomBlurShader = this.device.createShaderModule({
918
897
  label: "bloom blur",
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
- // 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
949
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
950
- let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
951
-
952
- // Bilinear optimization: leverage hardware filtering to sample between pixels
953
- // Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
954
- // Optimized 3-tap: combine adjacent samples using weighted offsets
955
- let weight0 = 0.38774; // Center sample
956
- let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
957
- let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
958
-
959
- var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
960
- let offsetVec = offset1 * texelSize * blurUniforms.direction;
961
- result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
962
- result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
963
-
964
- return result;
965
- }
898
+ code: /* wgsl */ `
899
+ struct VertexOutput {
900
+ @builtin(position) position: vec4f,
901
+ @location(0) uv: vec2f,
902
+ };
903
+
904
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
905
+ var output: VertexOutput;
906
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
907
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
908
+ output.position = vec4f(x, y, 0.0, 1.0);
909
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
910
+ return output;
911
+ }
912
+
913
+ struct BlurUniforms {
914
+ direction: vec2f,
915
+ _padding1: f32,
916
+ _padding2: f32,
917
+ _padding3: f32,
918
+ _padding4: f32,
919
+ _padding5: f32,
920
+ _padding6: f32,
921
+ };
922
+
923
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
924
+ @group(0) @binding(1) var inputSampler: sampler;
925
+ @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
926
+
927
+ // 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
928
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
929
+ let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
930
+
931
+ // Bilinear optimization: leverage hardware filtering to sample between pixels
932
+ // Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
933
+ // Optimized 3-tap: combine adjacent samples using weighted offsets
934
+ let weight0 = 0.38774; // Center sample
935
+ let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
936
+ let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
937
+
938
+ var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
939
+ let offsetVec = offset1 * texelSize * blurUniforms.direction;
940
+ result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
941
+ result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
942
+
943
+ return result;
944
+ }
966
945
  `,
967
946
  });
968
947
  // Bloom composition shader (combines original scene with bloom)
969
948
  const bloomComposeShader = this.device.createShaderModule({
970
949
  label: "bloom compose",
971
- code: /* wgsl */ `
972
- struct VertexOutput {
973
- @builtin(position) position: vec4f,
974
- @location(0) uv: vec2f,
975
- };
976
-
977
- @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
978
- var output: VertexOutput;
979
- let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
980
- let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
981
- output.position = vec4f(x, y, 0.0, 1.0);
982
- output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
983
- return output;
984
- }
985
-
986
- struct BloomComposeUniforms {
987
- intensity: f32,
988
- _padding1: f32,
989
- _padding2: f32,
990
- _padding3: f32,
991
- _padding4: f32,
992
- _padding5: f32,
993
- _padding6: f32,
994
- _padding7: f32,
995
- };
996
-
997
- @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
998
- @group(0) @binding(1) var sceneSampler: sampler;
999
- @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
1000
- @group(0) @binding(3) var bloomSampler: sampler;
1001
- @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
1002
-
1003
- @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1004
- let scene = textureSample(sceneTexture, sceneSampler, input.uv);
1005
- let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
1006
- // Additive blending with intensity control
1007
- let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
1008
- return vec4f(result, scene.a);
1009
- }
950
+ code: /* wgsl */ `
951
+ struct VertexOutput {
952
+ @builtin(position) position: vec4f,
953
+ @location(0) uv: vec2f,
954
+ };
955
+
956
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
957
+ var output: VertexOutput;
958
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
959
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
960
+ output.position = vec4f(x, y, 0.0, 1.0);
961
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
962
+ return output;
963
+ }
964
+
965
+ struct BloomComposeUniforms {
966
+ intensity: f32,
967
+ _padding1: f32,
968
+ _padding2: f32,
969
+ _padding3: f32,
970
+ _padding4: f32,
971
+ _padding5: f32,
972
+ _padding6: f32,
973
+ _padding7: f32,
974
+ };
975
+
976
+ @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
977
+ @group(0) @binding(1) var sceneSampler: sampler;
978
+ @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
979
+ @group(0) @binding(3) var bloomSampler: sampler;
980
+ @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
981
+
982
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
983
+ let scene = textureSample(sceneTexture, sceneSampler, input.uv);
984
+ let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
985
+ // Additive blending with intensity control
986
+ let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
987
+ return vec4f(result, scene.a);
988
+ }
1010
989
  `,
1011
990
  });
1012
991
  // Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
@@ -1235,35 +1214,18 @@ export class Engine {
1235
1214
  setupLighting() {
1236
1215
  this.lightUniformBuffer = this.device.createBuffer({
1237
1216
  label: "light uniforms",
1238
- size: 64 * 4,
1217
+ size: 4 * 4, // 4 floats: ambientColor vec3f (3) + padding (1)
1239
1218
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1240
1219
  });
1241
- this.lightCount = 0;
1242
- this.setAmbient(this.ambient);
1243
- this.addLight(new Vec3(-0.5, -0.8, 0.5).normalize(), new Vec3(1.0, 0.95, 0.9), 0.02);
1244
- this.addLight(new Vec3(0.7, -0.5, 0.3).normalize(), new Vec3(0.8, 0.85, 1.0), 0.015);
1245
- this.addLight(new Vec3(0.3, -0.5, -1.0).normalize(), new Vec3(0.9, 0.9, 1.0), 0.01);
1220
+ this.setAmbientColor(this.ambientColor);
1246
1221
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
1247
1222
  }
1248
- addLight(direction, color, intensity = 1.0) {
1249
- if (this.lightCount >= 4)
1250
- return false;
1251
- const normalized = direction.normalize();
1252
- const baseIndex = 4 + this.lightCount * 8;
1253
- this.lightData[baseIndex] = normalized.x;
1254
- this.lightData[baseIndex + 1] = normalized.y;
1255
- this.lightData[baseIndex + 2] = normalized.z;
1256
- this.lightData[baseIndex + 3] = 0;
1257
- this.lightData[baseIndex + 4] = color.x;
1258
- this.lightData[baseIndex + 5] = color.y;
1259
- this.lightData[baseIndex + 6] = color.z;
1260
- this.lightData[baseIndex + 7] = intensity;
1261
- this.lightCount++;
1262
- this.lightData[1] = this.lightCount;
1263
- return true;
1264
- }
1265
- setAmbient(intensity) {
1266
- this.lightData[0] = intensity;
1223
+ setAmbientColor(color) {
1224
+ // Layout: ambientColor (0-2), padding (3)
1225
+ this.lightData[0] = color.x;
1226
+ this.lightData[1] = color.y;
1227
+ this.lightData[2] = color.z;
1228
+ this.lightData[3] = 0.0; // Padding for vec3f alignment
1267
1229
  }
1268
1230
  async loadAnimation(url) {
1269
1231
  const frames = await VMDLoader.load(url);