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.
- package/dist/Renderer.cjs +2925 -281
- package/dist/Renderer.cjs.map +1 -1
- package/dist/Renderer.js +2925 -281
- package/dist/Renderer.js.map +1 -1
- package/package.json +1 -1
- package/src/renderer/DebugUI.js +176 -45
- package/src/renderer/Material.js +3 -0
- package/src/renderer/Pipeline.js +3 -1
- package/src/renderer/Renderer.js +185 -13
- package/src/renderer/core/AssetManager.js +40 -5
- package/src/renderer/core/CullingSystem.js +1 -0
- package/src/renderer/gltf.js +17 -0
- package/src/renderer/rendering/RenderGraph.js +224 -30
- package/src/renderer/rendering/passes/BloomPass.js +5 -2
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +26 -0
- package/src/renderer/rendering/passes/GBufferPass.js +31 -7
- package/src/renderer/rendering/passes/HiZPass.js +30 -0
- package/src/renderer/rendering/passes/LightingPass.js +14 -0
- package/src/renderer/rendering/passes/ParticlePass.js +10 -4
- package/src/renderer/rendering/passes/PostProcessPass.js +127 -4
- package/src/renderer/rendering/passes/SSGIPass.js +3 -2
- package/src/renderer/rendering/passes/SSGITilePass.js +14 -5
- package/src/renderer/rendering/passes/ShadowPass.js +265 -15
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +36 -6
- package/src/renderer/rendering/shaders/particle_render.wgsl +153 -6
- package/src/renderer/rendering/shaders/postproc.wgsl +23 -2
- package/src/renderer/rendering/shaders/shadow.wgsl +42 -1
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- 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
|
-
|
|
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
|
-
//
|
|
491
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|