topazcube 0.1.33 → 0.1.35

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.
Files changed (34) hide show
  1. package/dist/Renderer.cjs +2925 -281
  2. package/dist/Renderer.cjs.map +1 -1
  3. package/dist/Renderer.js +2925 -281
  4. package/dist/Renderer.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/renderer/DebugUI.js +176 -45
  7. package/src/renderer/Material.js +3 -0
  8. package/src/renderer/Pipeline.js +3 -1
  9. package/src/renderer/Renderer.js +185 -13
  10. package/src/renderer/core/AssetManager.js +40 -5
  11. package/src/renderer/core/CullingSystem.js +1 -0
  12. package/src/renderer/gltf.js +17 -0
  13. package/src/renderer/rendering/RenderGraph.js +224 -30
  14. package/src/renderer/rendering/passes/BloomPass.js +5 -2
  15. package/src/renderer/rendering/passes/CRTPass.js +724 -0
  16. package/src/renderer/rendering/passes/FogPass.js +26 -0
  17. package/src/renderer/rendering/passes/GBufferPass.js +31 -7
  18. package/src/renderer/rendering/passes/HiZPass.js +30 -0
  19. package/src/renderer/rendering/passes/LightingPass.js +14 -0
  20. package/src/renderer/rendering/passes/ParticlePass.js +10 -4
  21. package/src/renderer/rendering/passes/PostProcessPass.js +127 -4
  22. package/src/renderer/rendering/passes/SSGIPass.js +3 -2
  23. package/src/renderer/rendering/passes/SSGITilePass.js +14 -5
  24. package/src/renderer/rendering/passes/ShadowPass.js +265 -15
  25. package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
  26. package/src/renderer/rendering/shaders/crt.wgsl +455 -0
  27. package/src/renderer/rendering/shaders/geometry.wgsl +36 -6
  28. package/src/renderer/rendering/shaders/particle_render.wgsl +153 -6
  29. package/src/renderer/rendering/shaders/postproc.wgsl +23 -2
  30. package/src/renderer/rendering/shaders/shadow.wgsl +42 -1
  31. package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
  32. package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
  33. package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
  34. package/src/renderer/utils/Raycaster.js +761 -0
@@ -85,7 +85,8 @@ struct ParticleUniforms {
85
85
  fogAlphas: vec3f, // [nearAlpha, midAlpha, farAlpha]
86
86
  fogPad1: f32,
87
87
  fogHeightFade: vec2f, // [bottomY, topY]
88
- fogPad2: vec2f,
88
+ fogDebug: f32, // 0 = off, 2 = show distance
89
+ fogPad2: f32,
89
90
  }
90
91
 
91
92
  struct CascadeMatrices {
@@ -101,6 +102,7 @@ struct VertexOutput {
101
102
  @location(4) lighting: vec3f, // Pre-computed lighting from particle
102
103
  @location(5) @interpolate(flat) emitterIdx: u32, // For per-emitter settings
103
104
  @location(6) worldPos: vec3f, // For fog height fade
105
+ @location(7) @interpolate(flat) centerViewZ: f32, // View-space Z of particle center (for fog)
104
106
  }
105
107
 
106
108
  @group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;
@@ -174,6 +176,7 @@ fn vertexMain(
174
176
  output.linearDepth = 0.0;
175
177
  output.lighting = vec3f(0.0);
176
178
  output.worldPos = vec3f(0.0);
179
+ output.centerViewZ = 0.0;
177
180
  return output;
178
181
  }
179
182
 
@@ -181,6 +184,7 @@ fn vertexMain(
181
184
  // uniforms.blendMode: 1.0 = additive, 0.0 = alpha
182
185
  let particleIsAdditive = (particle.flags & 2u) != 0u;
183
186
  let renderingAdditive = uniforms.blendMode > 0.5;
187
+
184
188
  if (particleIsAdditive != renderingAdditive) {
185
189
  // Wrong blend mode for this pass - skip
186
190
  output.position = vec4f(0.0, 0.0, 0.0, 0.0);
@@ -190,6 +194,7 @@ fn vertexMain(
190
194
  output.linearDepth = 0.0;
191
195
  output.lighting = vec3f(0.0);
192
196
  output.worldPos = vec3f(0.0);
197
+ output.centerViewZ = 0.0;
193
198
  return output;
194
199
  }
195
200
 
@@ -236,9 +241,14 @@ fn vertexMain(
236
241
  // Pass through particle color
237
242
  output.color = particle.color;
238
243
 
239
- // Pass world position for fog
244
+ // Pass world position for fog height
240
245
  output.worldPos = particleWorldPos;
241
246
 
247
+ // Use the billboard's viewZ for fog distance
248
+ // This matches scene fog which uses view-space Z (linear depth)
249
+ // The billboard viewZ is already calculated correctly: -viewPos.z
250
+ output.centerViewZ = output.viewZ;
251
+
242
252
  return output;
243
253
  }
244
254
 
@@ -464,6 +474,56 @@ fn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {
464
474
  return saturate(depthDiff / uniforms.softness);
465
475
  }
466
476
 
477
+ // Calculate fog based on particle's world position (not scene depth)
478
+ // This allows particles to fog correctly even over sky
479
+ fn calcFog(cameraDistance: f32, worldPosY: f32) -> f32 {
480
+ if (uniforms.fogEnabled < 0.5) {
481
+ return 0.0;
482
+ }
483
+
484
+ // Distance fog - two gradients
485
+ var distanceFog: f32;
486
+ let d0 = uniforms.fogDistances.x;
487
+ let d1 = uniforms.fogDistances.y;
488
+ let d2 = uniforms.fogDistances.z;
489
+ let a0 = uniforms.fogAlphas.x;
490
+ let a1 = uniforms.fogAlphas.y;
491
+ let a2 = uniforms.fogAlphas.z;
492
+
493
+ if (cameraDistance <= d0) {
494
+ distanceFog = a0;
495
+ } else if (cameraDistance <= d1) {
496
+ let t = (cameraDistance - d0) / max(d1 - d0, 0.001);
497
+ distanceFog = mix(a0, a1, t);
498
+ } else if (cameraDistance <= d2) {
499
+ let t = (cameraDistance - d1) / max(d2 - d1, 0.001);
500
+ distanceFog = mix(a1, a2, t);
501
+ } else {
502
+ distanceFog = a2;
503
+ }
504
+
505
+ // Height fade - full fog at bottomY, zero fog at topY
506
+ let bottomY = uniforms.fogHeightFade.x;
507
+ let topY = uniforms.fogHeightFade.y;
508
+ var heightFactor = clamp((worldPosY - bottomY) / max(topY - bottomY, 0.001), 0.0, 1.0);
509
+ if (worldPosY < bottomY) {
510
+ heightFactor = 0.0;
511
+ }
512
+
513
+ return distanceFog * (1.0 - heightFactor);
514
+ }
515
+
516
+ // Apply fog to color (blend toward fog color)
517
+ // Particles use same fog calculation as scene
518
+ fn applyFog(color: vec3f, cameraDistance: f32, worldPosY: f32) -> vec3f {
519
+ let fogAlpha = calcFog(cameraDistance, worldPosY);
520
+ if (fogAlpha <= 0.0) {
521
+ return color;
522
+ }
523
+
524
+ return mix(color, uniforms.fogColor, fogAlpha);
525
+ }
526
+
467
527
  // Fragment for alpha blend mode
468
528
  @fragment
469
529
  fn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {
@@ -487,8 +547,51 @@ fn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {
487
547
  let baseColor = texColor.rgb * input.color.rgb;
488
548
  let litColor = applyLighting(baseColor, input.lighting);
489
549
 
490
- // Fog is applied as post-process to the combined HDR buffer
491
- output.color = vec4f(litColor, alpha);
550
+ // Use pre-computed center view-space Z from vertex shader (flat interpolated)
551
+ let centerViewZ = input.centerViewZ;
552
+
553
+ // Debug modes using centerViewZ (flat interpolated = same value across whole particle)
554
+ // Use full alpha to avoid blending artifacts in debug view
555
+ if (uniforms.fogDebug > 0.5) {
556
+ // Debug mode 1: show centerViewZ/100 as grayscale (should match scene fog)
557
+ if (uniforms.fogDebug < 1.5) {
558
+ let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);
559
+ output.color = vec4f(vec3f(dist), 1.0);
560
+ }
561
+ // Debug mode 2: show centerViewZ at 3 scales to find correct range
562
+ // R = /10, G = /100, B = /1000
563
+ else if (uniforms.fogDebug < 2.5) {
564
+ let r = clamp(centerViewZ / 10.0, 0.0, 1.0);
565
+ let g = clamp(centerViewZ / 100.0, 0.0, 1.0);
566
+ let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);
567
+ output.color = vec4f(r, g, b, 1.0);
568
+ }
569
+ // Debug mode 3: compare viewZ (interpolated) vs centerViewZ (flat)
570
+ // R = centerViewZ/100, G = viewZ/100, B = difference
571
+ else if (uniforms.fogDebug < 3.5) {
572
+ let center = clamp(centerViewZ / 100.0, 0.0, 1.0);
573
+ let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);
574
+ let diff = abs(center - vertex);
575
+ output.color = vec4f(center, vertex, diff * 10.0, 1.0);
576
+ }
577
+ // Debug mode 4: show worldPos
578
+ else if (uniforms.fogDebug < 4.5) {
579
+ let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);
580
+ let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);
581
+ let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);
582
+ output.color = vec4f(r, g, b, 1.0);
583
+ }
584
+ // Debug mode 5: show the actual fog color being used
585
+ else {
586
+ output.color = vec4f(uniforms.fogColor, 1.0);
587
+ }
588
+ output.depth = input.linearDepth;
589
+ return output;
590
+ }
591
+
592
+ let foggedColor = applyFog(litColor, centerViewZ, input.worldPos.y);
593
+
594
+ output.color = vec4f(foggedColor, alpha);
492
595
  output.depth = input.linearDepth;
493
596
  return output;
494
597
  }
@@ -516,9 +619,53 @@ fn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {
516
619
  let baseColor = texColor.rgb * input.color.rgb;
517
620
  let litColor = applyLighting(baseColor, input.lighting);
518
621
 
519
- // Fog is applied as post-process to the combined HDR buffer
622
+ // Use pre-computed center view-space Z from vertex shader (flat interpolated)
623
+ let centerViewZ = input.centerViewZ;
624
+
625
+ // Debug modes using centerViewZ (flat interpolated = same value across whole particle)
626
+ // Use full alpha to avoid blending artifacts in debug view
627
+ if (uniforms.fogDebug > 0.5) {
628
+ // Debug mode 1: show centerViewZ/100 as grayscale (should match scene fog)
629
+ if (uniforms.fogDebug < 1.5) {
630
+ let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);
631
+ output.color = vec4f(vec3f(dist), 1.0);
632
+ }
633
+ // Debug mode 2: show centerViewZ at 3 scales
634
+ else if (uniforms.fogDebug < 2.5) {
635
+ let r = clamp(centerViewZ / 10.0, 0.0, 1.0);
636
+ let g = clamp(centerViewZ / 100.0, 0.0, 1.0);
637
+ let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);
638
+ output.color = vec4f(r, g, b, 1.0);
639
+ }
640
+ // Debug mode 3: compare viewZ vs centerViewZ
641
+ else if (uniforms.fogDebug < 3.5) {
642
+ let center = clamp(centerViewZ / 100.0, 0.0, 1.0);
643
+ let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);
644
+ let diff = abs(center - vertex);
645
+ output.color = vec4f(center, vertex, diff * 10.0, 1.0);
646
+ }
647
+ // Debug mode 4: show worldPos
648
+ else if (uniforms.fogDebug < 4.5) {
649
+ let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);
650
+ let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);
651
+ let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);
652
+ output.color = vec4f(r, g, b, 1.0);
653
+ }
654
+ // Debug mode 5: show the actual fog color being used
655
+ else {
656
+ output.color = vec4f(uniforms.fogColor, 1.0);
657
+ }
658
+ output.depth = input.linearDepth;
659
+ return output;
660
+ }
661
+
662
+ // For additive particles, fog should fade the contribution to zero
663
+ // (not mix with fog color, which would add fog color to the already-fogged background)
664
+ let fogAlpha = calcFog(centerViewZ, input.worldPos.y);
665
+ let fadedColor = litColor * (1.0 - fogAlpha);
666
+
520
667
  // Premultiply for additive blending
521
- let rgb = litColor * alpha;
668
+ let rgb = fadedColor * alpha;
522
669
  output.color = vec4f(rgb, alpha);
523
670
  output.depth = input.linearDepth;
524
671
  return output;
@@ -6,7 +6,7 @@ struct VertexOutput {
6
6
  struct Uniforms {
7
7
  canvasSize: vec2f,
8
8
  noiseParams: vec4f, // x = size, y = offsetX, z = offsetY, w = fxaaEnabled
9
- ditherParams: vec4f, // x = enabled, y = colorLevels (32 = 5-bit PS1 style), z = unused, w = unused
9
+ ditherParams: vec4f, // x = enabled, y = colorLevels (32 = 5-bit PS1 style), z = tonemapMode (0=ACES, 1=Reinhard, 2=None/Linear), w = unused
10
10
  bloomParams: vec4f, // x = enabled, y = intensity, z = radius (mip levels to sample), w = mipCount
11
11
  }
12
12
 
@@ -222,6 +222,26 @@ fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {
222
222
  return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));
223
223
  }
224
224
 
225
+ // Reinhard tone mapping
226
+ fn reinhard_tone_map(hdr: vec3<f32>) -> vec3<f32> {
227
+ return hdr / (hdr + vec3(1.0));
228
+ }
229
+
230
+ // No tone mapping - just gamma correction and clamp
231
+ fn linear_tone_map(hdr: vec3<f32>) -> vec3<f32> {
232
+ return clamp(hdr, vec3(0.0), vec3(1.0));
233
+ }
234
+
235
+ // Apply selected tone mapping
236
+ fn apply_tone_map(hdr: vec3<f32>, mode: i32) -> vec3<f32> {
237
+ if (mode == 1) {
238
+ return reinhard_tone_map(hdr);
239
+ } else if (mode == 2) {
240
+ return linear_tone_map(hdr);
241
+ }
242
+ return aces_tone_map(hdr); // Default: ACES (mode 0)
243
+ }
244
+
225
245
  @vertex
226
246
  fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
227
247
  var output : VertexOutput;
@@ -248,7 +268,8 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
248
268
  color += bloom * uniforms.bloomParams.y; // y = intensity
249
269
  }
250
270
 
251
- var sdr = aces_tone_map(color);
271
+ let tonemapMode = i32(uniforms.ditherParams.z);
272
+ var sdr = apply_tone_map(color, tonemapMode);
252
273
 
253
274
  // Blend GUI overlay (after tone mapping, before dithering)
254
275
  // GUI is premultiplied alpha blending
@@ -6,6 +6,9 @@ struct Uniforms {
6
6
  lightType: f32, // 0 = directional, 1 = point, 2 = spot
7
7
  lightDirection: vec3f, // Light direction (for surface bias)
8
8
  surfaceBias: f32, // Expand triangles along normals (meters)
9
+ skinEnabled: f32, // 1.0 if skinning enabled, 0.0 otherwise
10
+ numJoints: f32, // Number of joints in the skin
11
+ _pad: vec2f,
9
12
  }
10
13
 
11
14
  struct VertexInput {
@@ -29,6 +32,35 @@ struct VertexOutput {
29
32
  }
30
33
 
31
34
  @group(0) @binding(0) var<uniform> uniforms: Uniforms;
35
+ @group(0) @binding(1) var jointTexture: texture_2d<f32>;
36
+ @group(0) @binding(2) var jointSampler: sampler;
37
+
38
+ // Get a 4x4 matrix from the joint texture
39
+ fn getJointMatrix(jointIndex: u32) -> mat4x4f {
40
+ let row = i32(jointIndex);
41
+ let col0 = textureLoad(jointTexture, vec2i(0, row), 0);
42
+ let col1 = textureLoad(jointTexture, vec2i(1, row), 0);
43
+ let col2 = textureLoad(jointTexture, vec2i(2, row), 0);
44
+ let col3 = textureLoad(jointTexture, vec2i(3, row), 0);
45
+ return mat4x4f(col0, col1, col2, col3);
46
+ }
47
+
48
+ // Apply skinning to a position
49
+ fn applySkinning(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {
50
+ var skinnedPos = vec3f(0.0);
51
+
52
+ let m0 = getJointMatrix(joints.x);
53
+ let m1 = getJointMatrix(joints.y);
54
+ let m2 = getJointMatrix(joints.z);
55
+ let m3 = getJointMatrix(joints.w);
56
+
57
+ skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;
58
+ skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;
59
+ skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;
60
+ skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;
61
+
62
+ return skinnedPos;
63
+ }
32
64
 
33
65
  @vertex
34
66
  fn vertexMain(input: VertexInput) -> VertexOutput {
@@ -42,8 +74,17 @@ fn vertexMain(input: VertexInput) -> VertexOutput {
42
74
  input.model3
43
75
  );
44
76
 
77
+ // Apply skinning if enabled
78
+ var localPos = input.position;
79
+ if (uniforms.skinEnabled > 0.5) {
80
+ let weightSum = input.weights.x + input.weights.y + input.weights.z + input.weights.w;
81
+ if (weightSum > 0.001) {
82
+ localPos = applySkinning(input.position, input.joints, input.weights);
83
+ }
84
+ }
85
+
45
86
  // Transform to world space
46
- let worldPos = modelMatrix * vec4f(input.position, 1.0);
87
+ let worldPos = modelMatrix * vec4f(localPos, 1.0);
47
88
 
48
89
  // Transform to light clip space
49
90
  var clipPos = uniforms.lightViewProjection * worldPos;
@@ -0,0 +1,80 @@
1
+ // Volumetric Fog - Gaussian Blur
2
+ // Blurs the ray-marched fog for softer edges
3
+
4
+ struct Uniforms {
5
+ direction: vec2f,
6
+ texelSize: vec2f,
7
+ radius: f32,
8
+ _pad0: f32,
9
+ _pad1: f32,
10
+ _pad2: f32,
11
+ }
12
+
13
+ struct VertexOutput {
14
+ @builtin(position) position: vec4f,
15
+ @location(0) uv: vec2f,
16
+ }
17
+
18
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
19
+ @group(0) @binding(1) var inputTexture: texture_2d<f32>;
20
+ @group(0) @binding(2) var inputSampler: sampler;
21
+
22
+ // Full-screen triangle vertex shader
23
+ @vertex
24
+ fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
25
+ var output: VertexOutput;
26
+
27
+ let x = f32(vertexIndex & 1u) * 4.0 - 1.0;
28
+ let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;
29
+
30
+ output.position = vec4f(x, y, 0.0, 1.0);
31
+ output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
32
+
33
+ return output;
34
+ }
35
+
36
+ // Gaussian weight function
37
+ fn gaussian(x: f32, sigma: f32) -> f32 {
38
+ return exp(-(x * x) / (2.0 * sigma * sigma));
39
+ }
40
+
41
+ @fragment
42
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
43
+ let uv = input.uv;
44
+ let radius = uniforms.radius;
45
+ let sigma = radius / 3.0; // 99.7% of Gaussian within 3 sigma
46
+
47
+ // Sample center pixel
48
+ var color = textureSample(inputTexture, inputSampler, uv);
49
+ var totalWeight = 1.0;
50
+
51
+ // Step size (sample every 1.5 texels for efficiency)
52
+ let stepSize = 1.5;
53
+ let numSamples = i32(ceil(radius / stepSize));
54
+
55
+ // Blur direction (scaled by texel size)
56
+ let dir = uniforms.direction * uniforms.texelSize;
57
+
58
+ // Bidirectional sampling
59
+ for (var i = 1; i <= numSamples; i++) {
60
+ let offset = f32(i) * stepSize;
61
+ let weight = gaussian(offset, sigma);
62
+
63
+ // Positive direction
64
+ let uvPos = uv + dir * offset;
65
+ let samplePos = textureSample(inputTexture, inputSampler, uvPos);
66
+ color += samplePos * weight;
67
+
68
+ // Negative direction
69
+ let uvNeg = uv - dir * offset;
70
+ let sampleNeg = textureSample(inputTexture, inputSampler, uvNeg);
71
+ color += sampleNeg * weight;
72
+
73
+ totalWeight += weight * 2.0;
74
+ }
75
+
76
+ // Normalize
77
+ color /= totalWeight;
78
+
79
+ return color;
80
+ }
@@ -0,0 +1,80 @@
1
+ // Volumetric Fog - Composite
2
+ // Blends the blurred fog into the scene (additive)
3
+
4
+ struct Uniforms {
5
+ canvasSize: vec2f,
6
+ renderSize: vec2f,
7
+ texelSize: vec2f,
8
+ // Brightness-based fog attenuation (like bloom)
9
+ brightnessThreshold: f32, // Scene luminance where fog starts fading
10
+ minVisibility: f32, // Minimum fog visibility over bright surfaces (0-1)
11
+ skyBrightness: f32, // Virtual brightness for sky pixels (depth at far plane)
12
+ _pad: f32,
13
+ }
14
+
15
+ struct VertexOutput {
16
+ @builtin(position) position: vec4f,
17
+ @location(0) uv: vec2f,
18
+ }
19
+
20
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
21
+ @group(0) @binding(1) var sceneTexture: texture_2d<f32>;
22
+ @group(0) @binding(2) var fogTexture: texture_2d<f32>;
23
+ @group(0) @binding(3) var linearSampler: sampler;
24
+ @group(0) @binding(4) var depthTexture: texture_depth_2d;
25
+
26
+ // Full-screen triangle vertex shader
27
+ @vertex
28
+ fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
29
+ var output: VertexOutput;
30
+
31
+ let x = f32(vertexIndex & 1u) * 4.0 - 1.0;
32
+ let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;
33
+
34
+ output.position = vec4f(x, y, 0.0, 1.0);
35
+ output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
36
+
37
+ return output;
38
+ }
39
+
40
+ @fragment
41
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
42
+ let uv = input.uv;
43
+
44
+ // Sample scene color at full resolution
45
+ let sceneColor = textureSample(sceneTexture, linearSampler, uv).rgb;
46
+
47
+ // Sample fog at reduced resolution (will be upsampled by linear filtering)
48
+ let fog = textureSample(fogTexture, linearSampler, uv);
49
+
50
+ // Sample depth to detect sky (depth near 1.0 = far plane = sky)
51
+ let depthCoord = vec2i(input.position.xy);
52
+ let depth = textureLoad(depthTexture, depthCoord, 0);
53
+
54
+ // Calculate scene luminance for brightness-based attenuation
55
+ var luminance = dot(sceneColor, vec3f(0.299, 0.587, 0.114));
56
+
57
+ // Sky detection: if depth is very close to 1.0 (far plane), treat as bright
58
+ // This ensures fog is less visible over sky even if sky color isn't bright
59
+ let isSky = depth > 0.9999;
60
+ if (isSky) {
61
+ luminance = max(luminance, uniforms.skyBrightness);
62
+ }
63
+
64
+ // Fog visibility decreases over bright surfaces (like bloom behavior)
65
+ // Full visibility at dark, reduced visibility at bright, but never zero
66
+ let threshold = uniforms.brightnessThreshold;
67
+ let minVis = uniforms.minVisibility;
68
+
69
+ // Soft falloff using inverse relationship
70
+ // At luminance=0: visibility=1
71
+ // At luminance=threshold: visibility≈0.5
72
+ // At luminance>>threshold: visibility→minVis
73
+ let falloff = 1.0 / (1.0 + luminance / max(threshold, 0.01));
74
+ let fogVisibility = mix(minVis, 1.0, falloff);
75
+
76
+ // Additive blend with brightness attenuation
77
+ let finalColor = sceneColor + fog.rgb * fogVisibility;
78
+
79
+ return vec4f(finalColor, 1.0);
80
+ }