@forgeax/engine-shader 0.1.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.
Files changed (162) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/ShaderRegistry.d.ts +208 -0
  5. package/dist/ShaderRegistry.d.ts.map +1 -0
  6. package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
  7. package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
  8. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
  9. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
  10. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
  11. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
  12. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
  13. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
  14. package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
  15. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
  16. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
  17. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
  18. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
  19. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
  21. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
  22. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
  23. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
  25. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
  26. package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
  27. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
  29. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
  31. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
  33. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
  34. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
  35. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
  37. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
  39. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
  41. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
  43. package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  49. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
  51. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
  53. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/shader.unit.test.d.ts +2 -0
  55. package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
  57. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
  58. package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
  59. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
  61. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
  63. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
  65. package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
  66. package/dist/errors.d.ts +96 -0
  67. package/dist/errors.d.ts.map +1 -0
  68. package/dist/index.d.ts +44 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.mjs +560 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/material/artifact-registry.d.ts +31 -0
  73. package/dist/material/artifact-registry.d.ts.map +1 -0
  74. package/dist/material/artifact-types.d.ts +13 -0
  75. package/dist/material/artifact-types.d.ts.map +1 -0
  76. package/dist/material-schemas.d.ts +8 -0
  77. package/dist/material-schemas.d.ts.map +1 -0
  78. package/dist/register-default-sprite-lit.d.ts +40 -0
  79. package/dist/register-default-sprite-lit.d.ts.map +1 -0
  80. package/dist/register-default-standard-pbr-skin.d.ts +34 -0
  81. package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
  82. package/dist/tonemap.d.ts +13 -0
  83. package/dist/tonemap.d.ts.map +1 -0
  84. package/dist/types.d.ts +47 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/package.json +58 -0
  87. package/src/ShaderRegistry.ts +519 -0
  88. package/src/__tests__/atmosphere-background.test.ts +33 -0
  89. package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
  90. package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
  91. package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
  92. package/src/__tests__/atmosphere-preetham.test.ts +132 -0
  93. package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
  94. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
  95. package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
  96. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
  97. package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
  98. package/src/__tests__/fog-oracle.unit.test.ts +132 -0
  99. package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
  100. package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
  101. package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
  102. package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
  103. package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
  104. package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
  105. package/src/__tests__/manifest.fixture.json +22 -0
  106. package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
  107. package/src/__tests__/material-builtins.unit.test.ts +111 -0
  108. package/src/__tests__/material-contract.unit.test.ts +79 -0
  109. package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
  110. package/src/__tests__/public-surface.unit.test.ts +9 -0
  111. package/src/__tests__/scene-temporal.unit.test.ts +32 -0
  112. package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
  113. package/src/__tests__/shader.unit.test.ts +1909 -0
  114. package/src/__tests__/sprite-lit-shader.test.ts +240 -0
  115. package/src/__tests__/sprite-variants.unit.test.ts +209 -0
  116. package/src/__tests__/taa-resolve.unit.test.ts +32 -0
  117. package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
  118. package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
  119. package/src/atmosphere-background.wgsl +80 -0
  120. package/src/atmosphere-cubemap.wgsl +72 -0
  121. package/src/atmosphere-ibl.wgsl +127 -0
  122. package/src/atmosphere-preetham.wgsl +214 -0
  123. package/src/bloom-blur.wgsl +89 -0
  124. package/src/bloom-bright.wgsl +50 -0
  125. package/src/bloom-composite.wgsl +54 -0
  126. package/src/brdf.wgsl +25 -0
  127. package/src/common.wgsl +523 -0
  128. package/src/default-standard-pbr-skin.wgsl +538 -0
  129. package/src/default-standard-pbr.wgsl +794 -0
  130. package/src/errors.ts +169 -0
  131. package/src/fog.wgsl +27 -0
  132. package/src/fxaa.wgsl +259 -0
  133. package/src/hdrp-cluster-forward.wgsl +186 -0
  134. package/src/hdrp-deferred-lighting.wgsl +69 -0
  135. package/src/hdrp-ssao.wgsl +174 -0
  136. package/src/ibl-brdf-lut.wgsl +92 -0
  137. package/src/ibl-equirect-to-cube.wgsl +56 -0
  138. package/src/ibl-irradiance.wgsl +89 -0
  139. package/src/ibl-prefilter.wgsl +101 -0
  140. package/src/ibl-sampling.wgsl +58 -0
  141. package/src/ibl-shared.wgsl +118 -0
  142. package/src/index.ts +166 -0
  143. package/src/lighting-directional.wgsl +471 -0
  144. package/src/lighting-punctual.wgsl +274 -0
  145. package/src/material/artifact-registry.ts +78 -0
  146. package/src/material/artifact-types.ts +26 -0
  147. package/src/material-schemas.ts +48 -0
  148. package/src/msdf-text.wgsl +208 -0
  149. package/src/pbr-temporal.wgsl +73 -0
  150. package/src/register-default-sprite-lit.ts +87 -0
  151. package/src/register-default-standard-pbr-skin.ts +74 -0
  152. package/src/shadow-pcf.wgsl +120 -0
  153. package/src/shadow_caster.wgsl +54 -0
  154. package/src/skybox.wgsl +75 -0
  155. package/src/sprite-lit.wgsl +307 -0
  156. package/src/sprite.wgsl +365 -0
  157. package/src/taa-resolve.wgsl +129 -0
  158. package/src/tbn.wgsl +54 -0
  159. package/src/tonemap.ts +13 -0
  160. package/src/tonemap.wgsl +177 -0
  161. package/src/types.ts +59 -0
  162. package/src/unlit.wgsl +186 -0
@@ -0,0 +1,69 @@
1
+ #define_import_path forgeax_view::hdrp_deferred_lighting
2
+
3
+ #import forgeax_view::common::FullscreenOutput
4
+ #import forgeax_view::common::{View, FogViewParams, FogRay}
5
+ #import forgeax_view::common::fullscreen_triangle
6
+ #import forgeax_view::fog::{apply_fog}
7
+
8
+ // HDRP deferred lighting producer. The lighting pass consumes the material
9
+ // G-buffer and current-frame depth, reconstructs the fragment world position,
10
+ // then applies the shared analytic Fog exactly once to linear radiance.
11
+
12
+ @group(0) @binding(0) var<uniform> view : View;
13
+
14
+ @group(1) @binding(0) var gbufferAlbedo : texture_2d<f32>;
15
+ @group(1) @binding(1) var gbufferSampler : sampler;
16
+ @group(1) @binding(2) var<uniform> deferredParams : vec4<f32>;
17
+ @group(1) @binding(3) var sceneDepth : texture_depth_2d;
18
+ @group(1) @binding(4) var depthSampler : sampler;
19
+ @group(1) @binding(5) var gbufferNormal : texture_2d<f32>;
20
+ @group(1) @binding(6) var gbufferEmissive : texture_2d<f32>;
21
+
22
+ @vertex
23
+ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
24
+ return fullscreen_triangle(vertex_index);
25
+ }
26
+
27
+ fn reconstructWorldPosition(uv : vec2<f32>, depth : f32) -> vec3<f32> {
28
+ let clip = vec4<f32>(uv * 2.0 - vec2<f32>(1.0), depth, 1.0);
29
+ let worldH = view.inverseViewProj * clip;
30
+ return worldH.xyz / worldH.w;
31
+ }
32
+
33
+ fn fogDeferredRadiance(color : vec4<f32>, worldPos : vec3<f32>) -> vec4<f32> {
34
+ var origin = view.cameraPos;
35
+ var direction = normalize(worldPos - origin);
36
+ var rayDistance = length(worldPos - origin);
37
+ if (view.temporalProjection.z >= 0.5) {
38
+ let nearH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
39
+ let farH = view.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
40
+ let nearPoint = nearH.xyz / nearH.w;
41
+ let farPoint = farH.xyz / farH.w;
42
+ direction = normalize(farPoint - nearPoint);
43
+ origin = worldPos - direction * dot(worldPos - view.cameraPos, direction);
44
+ rayDistance = max(dot(worldPos - origin, direction), 0.0);
45
+ }
46
+ return apply_fog(view.fog, FogRay(origin, direction, rayDistance), color);
47
+ }
48
+
49
+ @fragment
50
+ fn fs_lighting(in : FullscreenOutput) -> @location(0) vec4<f32> {
51
+ // The lighting pass runs before the forward lane and shares its cleared
52
+ // scene target. A far-depth sample is an empty G-buffer pixel; discard it
53
+ // so deferred lighting preserves the existing clear/background contract.
54
+ // Writing a synthetic material value here would cover forward-only scenes
55
+ // (and change the HDRP baseline even when no deferred material is present).
56
+ let depth = textureSampleLevel(sceneDepth, depthSampler, in.uv, 0);
57
+ if (depth >= 0.999999) {
58
+ discard;
59
+ }
60
+ let material = textureSampleLevel(gbufferAlbedo, gbufferSampler, in.uv, 0);
61
+ let normalSample = textureSampleLevel(gbufferNormal, gbufferSampler, in.uv, 0);
62
+ let emissiveSample = textureSampleLevel(gbufferEmissive, gbufferSampler, in.uv, 0);
63
+ let normal = normalize(normalSample.xyz * 2.0 - vec3<f32>(1.0));
64
+ let light = max(dot(normal, normalize(-view.lightDir)), 0.0);
65
+ let direct = material.rgb * view.lightColor * light * clamp(emissiveSample.a, 0.0, 1.0);
66
+ let radiance = vec4<f32>(direct + emissiveSample.rgb, material.a);
67
+ let worldPos = reconstructWorldPosition(in.uv, depth);
68
+ return fogDeferredRadiance(radiance, worldPos);
69
+ }
@@ -0,0 +1,174 @@
1
+ // hdrp-ssao.wgsl — HDRP Screen-Space Ambient Occlusion.
2
+ // feat-20260612-hdrp-ssao M2 / w8.
3
+ //
4
+ // Two fullscreen fragment passes following LO 5.9:
5
+ // fs_ssao_calc — 64-sample hemisphere SSAO with range check, writes R8 scalar.
6
+ // fs_ssao_blur — 4x4 box blur (16 tap), reads ssaoRaw, writes R8 scalar.
7
+ //
8
+ // plan-strategy D-1: SSAO owns its uniform group (@group(2)) with view +
9
+ // projection + inverseProjection (3 mat4, 192 B UBO). Does NOT read View UBO.
10
+ // plan-strategy D-2: exactly 2 pass, no H/V split.
11
+ // requirements OOS-3: 4x4 box blur only; no depth-aware/bilateral.
12
+ // requirements OOS-4: no depth-reconstruction path for normals; reads normal from
13
+ // g-buffer RT0 (packed world-space normal, 0.5*n+0.5 in rgba16f).
14
+ //
15
+ // BGL layout (@group(0), SSAO-dedicated; w37 expansion for D-A/D-D + dawn fix):
16
+ // @binding(0) var<uniform> ssao_uniform : SsaoUniform (256 B UBO)
17
+ // @binding(1) var<uniform> ssao_kernel : array<vec4<f32>,64> (1024 B UBO)
18
+ // @binding(2) var ssao_noise_texture : texture_2d<f32> (4x4 rgba32float)
19
+ // @binding(3) var ssao_noise_sampler : sampler (filtering, for noise tile)
20
+ // @binding(4) var gbuffer_normal : texture_2d<f32> (RT0, packed world normal)
21
+ // @binding(5) var hdr_depth : texture_depth_2d (hardware depth)
22
+ // @binding(6) var ssao_depth_sampler : sampler (non-filtering, for hdr_depth)
23
+ // @binding(7) var ssaoRaw : texture_2d<f32> (half-res calc output, blur input)
24
+ // @binding(8) var ssaoSampler : sampler (filtering, for ssaoRaw)
25
+ //
26
+ // Sampler split (w37 dawn-blocker fix): WebGPU validation requires depth
27
+ // textures (sampleType=depth) to be sampled with non-filtering or comparison
28
+ // samplers, not filtering. Pre-w37 the BGL had a single `filtering` sampler at
29
+ // binding 3 paired with hdr_depth at binding 5, which crashed every HDRP PSO
30
+ // build on dawn. ssao_depth_sampler at binding 6 is the non-filtering sampler
31
+ // dedicated to hdr_depth; ssao_noise_sampler at binding 3 stays filtering for
32
+ // the float noise/normal textures.
33
+ //
34
+ // fs_ssao_calc: reads g-buffer normal + hdrDepth, writes half-res R8.
35
+ // fs_ssao_blur: reads ssaoRaw (half-res R8), writes half-res R8 (D-D fix:
36
+ // pre-w37 blur erroneously read gbuffer_normal — copy-paste typo from
37
+ // fs_ssao_calc).
38
+ //
39
+ // Vertex stage: both passes use fullscreen_triangle from common.wgsl.
40
+ //
41
+ // Research F1-F6 (LO 5.9 GLSL -> WGSL translation), KB ref:
42
+ // .forgeax-harness/knowledge-base/references/repos/learnopengl/src/5.advanced_lighting/9.ssao/
43
+
44
+ #define_import_path forgeax_hdrp::ssao
45
+
46
+ #import forgeax_view::common::{fullscreen_triangle, FullscreenOutput}
47
+
48
+ // ── SSAO uniform (3 mat4 + vec4 intensityPad, 256 B, D-1 + D-C) ─────────────
49
+ //
50
+ // intensityPad carries the lighting intensity plus the calc radius/bias.
51
+ // The lighting shader reads intensity from `cluster_uniform.near_far_log.w`
52
+ // (HDRP unified BGL @group(2) @binding(6)); the calc shader reads radius and
53
+ // bias here. Keeping the values in this existing vec4 preserves the one-shot
54
+ // 256 B UBO write and avoids a second parameter binding.
55
+
56
+ struct SsaoUniform {
57
+ view : mat4x4<f32>, // world -> view
58
+ projection : mat4x4<f32>, // view -> clip
59
+ inverseProjection : mat4x4<f32>, // NDC -> view
60
+ intensityPad : vec4<f32>, // x = intensity, y = radius, z = bias
61
+ };
62
+
63
+ // ── SSAO binding declarations (@group(2)) ───────────────────────────────────
64
+
65
+ @group(0) @binding(0) var<uniform> ssao_uniform : SsaoUniform;
66
+ @group(0) @binding(1) var<uniform> ssao_kernel : array<vec4<f32>, 64>;
67
+ @group(0) @binding(2) var ssao_noise_texture : texture_2d<f32>;
68
+ @group(0) @binding(3) var ssao_noise_sampler : sampler;
69
+ @group(0) @binding(4) var gbuffer_normal : texture_2d<f32>;
70
+ @group(0) @binding(5) var hdr_depth : texture_depth_2d;
71
+ @group(0) @binding(6) var ssao_depth_sampler : sampler;
72
+ @group(0) @binding(7) var ssaoRaw : texture_2d<f32>;
73
+ @group(0) @binding(8) var ssaoSampler : sampler;
74
+
75
+ // ── vertex: fullscreen triangle (SSOT in common.wgsl) ───────────────────────
76
+
77
+ struct SsaoVsOut {
78
+ @builtin(position) position : vec4<f32>,
79
+ @location(0) uv : vec2<f32>,
80
+ };
81
+
82
+ @vertex
83
+ fn vs_ssao(@builtin(vertex_index) vertex_index : u32) -> SsaoVsOut {
84
+ let ft = fullscreen_triangle(vertex_index);
85
+ var out : SsaoVsOut;
86
+ out.position = ft.position;
87
+ out.uv = ft.uv;
88
+ return out;
89
+ }
90
+
91
+ // ── fs_ssao_calc: 64-sample hemisphere SSAO (LO 5.9) ────────────────────────
92
+
93
+ @fragment
94
+ fn fs_ssao_calc(in : SsaoVsOut) -> @location(0) f32 {
95
+ // Reconstruct view-space position from depth + NDC.
96
+ let depth = textureSampleLevel(hdr_depth, ssao_depth_sampler, in.uv, 0);
97
+ // NDC reconstruction: screen-space xy in [-1,1], depth in [0,1].
98
+ let ndc = vec4<f32>(in.uv * 2.0 - 1.0, depth, 1.0);
99
+ // Transform NDC -> view via inverse projection.
100
+ var viewPosH = ssao_uniform.inverseProjection * ndc;
101
+ viewPosH = viewPosH / viewPosH.w;
102
+ let viewPos = viewPosH.xyz;
103
+
104
+ // Read and unpack world-space normal from g-buffer RT0.
105
+ let packedNormal = textureSample(gbuffer_normal, ssao_noise_sampler, in.uv).rgb;
106
+ let worldNormal = normalize(packedNormal * 2.0 - 1.0);
107
+
108
+ // Rotate world normal to view-space.
109
+ let viewNormal = normalize((ssao_uniform.view * vec4<f32>(worldNormal, 0.0)).xyz);
110
+
111
+ // Read noise for per-pixel TBN rotation.
112
+ let noiseScale = vec2<f32>(textureDimensions(ssao_noise_texture, 0)) / 4.0;
113
+ // Actually: scale factor is screenDim / noiseDim. Use a fixed factor for half-res.
114
+ // The noise texture is 4x4 and tiled via REPEAT sampling.
115
+ let screenDim = vec2<f32>(textureDimensions(gbuffer_normal, 0));
116
+ let noiseUV = in.uv * screenDim / 4.0;
117
+ let randomVec = normalize(textureSample(ssao_noise_texture, ssao_noise_sampler, noiseUV).xyz);
118
+
119
+ // TBN: Gram-Schmidt orthonormalization (tangent-space -> view-space).
120
+ let tangent = normalize(randomVec - viewNormal * dot(randomVec, viewNormal));
121
+ let bitangent = cross(viewNormal, tangent);
122
+ let TBN = mat3x3<f32>(tangent, bitangent, viewNormal);
123
+
124
+ // SSAO parameters are install-time config values projected by the host.
125
+ let radius = ssao_uniform.intensityPad.y;
126
+ let bias = ssao_uniform.intensityPad.z;
127
+
128
+ var occlusion = 0.0;
129
+ for (var i = 0u; i < 64u; i = i + 1u) {
130
+ // Tangent-space sample -> view-space via TBN.
131
+ let sampleTangent = ssao_kernel[i].xyz;
132
+ var sampleView = TBN * sampleTangent;
133
+ sampleView = viewPos + sampleView * radius;
134
+
135
+ // Project sample to screen.
136
+ var offset = ssao_uniform.projection * vec4<f32>(sampleView, 1.0);
137
+ offset = offset / offset.w;
138
+ offset = offset * 0.5 + 0.5;
139
+
140
+ // Sample depth at the projected screen location.
141
+ let sampleDepth = textureSampleLevel(hdr_depth, ssao_depth_sampler, offset.xy, 0);
142
+ // Reconstruct view-space z of the sampled fragment (at offset.xy).
143
+ var sampledViewPosH = ssao_uniform.inverseProjection * vec4<f32>(offset.xy * 2.0 - 1.0, sampleDepth, 1.0);
144
+ sampledViewPosH = sampledViewPosH / sampledViewPosH.w;
145
+ let sampledViewZ = sampledViewPosH.z;
146
+
147
+ // Range check: smoothstep based on distance along view-z axis.
148
+ let rangeCheck = smoothstep(0.0, 1.0, radius / abs(viewPos.z - sampledViewZ));
149
+ let sampleContrib = select(0.0, 1.0, sampledViewZ >= sampleView.z + bias);
150
+ occlusion += sampleContrib * rangeCheck;
151
+ }
152
+
153
+ occlusion = 1.0 - (occlusion / 64.0);
154
+ return occlusion;
155
+ }
156
+
157
+ // ── fs_ssao_blur: 4x4 box blur (LO 5.9, OOS-3) ─────────────────────────────
158
+
159
+ @fragment
160
+ fn fs_ssao_blur(in : SsaoVsOut) -> @location(0) f32 {
161
+ // D-D fix: blur reads ssaoRaw (half-res calc output), not gbuffer_normal.
162
+ // Pre-w37 the dimensions + sample texture both incorrectly named
163
+ // gbuffer_normal — copy-paste typo from fs_ssao_calc that produced no
164
+ // occlusion at all (the blur ran a 4x4 average over packed world normals).
165
+ let texelSize = 1.0 / vec2<f32>(textureDimensions(ssaoRaw, 0));
166
+ var result = 0.0;
167
+ for (var x = -2; x < 2; x = x + 1) {
168
+ for (var y = -2; y < 2; y = y + 1) {
169
+ let offset = vec2<f32>(f32(x), f32(y)) * texelSize;
170
+ result += textureSample(ssaoRaw, ssaoSampler, in.uv + offset).r;
171
+ }
172
+ }
173
+ return result / 16.0;
174
+ }
@@ -0,0 +1,92 @@
1
+ #define_import_path forgeax_pbr::ibl_brdf_lut
2
+
3
+ // @forgeax/engine-shader - ibl-brdf-lut.wgsl
4
+ // (feat-20260520-skylight-ibl-cubemap M3 / t46).
5
+ //
6
+ // BRDF integration LUT bake (Karis 2013 split-sum, second factor).
7
+ // One-shot fullscreen pass into a 256x256 RG16F target. Output channels:
8
+ // R = sum (1 - Fc) * G * (V . H) / (N . H * N . V)
9
+ // G = sum Fc * G * (V . H) / (N . H * N . V)
10
+ // where Fc = (1 - V . H)^5. This LUT is consumed at runtime by
11
+ // sampleIblSpecular() in ibl-sampling.wgsl.
12
+ //
13
+ // Zero @group(1) bindings -- the LUT is computed entirely from screen-space
14
+ // position + Hammersley sequence + shared math. AC-06 / plan D-10 fixes
15
+ // BRDF_LUT_SIZE = 256u as the SSOT constant; the host (IblPipelineCache)
16
+ // reads the same constant via a TS mirror.
17
+ //
18
+ // Entries: fullscreen_vs + brdfLutBake_fs.
19
+
20
+ #import forgeax_pbr::ibl_shared::{hammersley, importanceSampleGGX, iblGeometrySmith}
21
+
22
+ struct VsOut {
23
+ @builtin(position) clip: vec4<f32>,
24
+ @location(0) worldPos: vec3<f32>,
25
+ };
26
+
27
+ // AC-06 / plan D-10: BRDF LUT resolution. Both the bake fragment and the
28
+ // caller (IblPipelineCache.runIblPrecompute) align on this SSOT constant.
29
+ const BRDF_LUT_SIZE: u32 = 256u;
30
+ const BRDF_LUT_SAMPLE_COUNT: u32 = 1024u;
31
+
32
+ @vertex
33
+ fn fullscreen_vs(@builtin(vertex_index) vi: u32) -> VsOut {
34
+ // Fullscreen triangle covering NDC [-1,1]^2.
35
+ var x: f32 = -1.0;
36
+ var y: f32 = -1.0;
37
+ if (vi == 1u) {
38
+ x = 3.0;
39
+ }
40
+ if (vi == 2u) {
41
+ y = 3.0;
42
+ }
43
+ var out: VsOut;
44
+ out.clip = vec4<f32>(x, y, 0.0, 1.0);
45
+ out.worldPos = vec3<f32>(0.0); // unused
46
+ return out;
47
+ }
48
+
49
+ @fragment
50
+ fn brdfLutBake_fs(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
51
+ // Screen-space framebuffer position maps to (NdotV, roughness) in [0, 1]^2.
52
+ // Caller renders a fullscreen quad at BRDF_LUT_SIZE x BRDF_LUT_SIZE viewport
53
+ // (256x256 per plan D-10 / AC-06). pos.x / pos.y are framebuffer pixel
54
+ // coordinates in [0.5, BRDF_LUT_SIZE - 0.5].
55
+ let denom = f32(BRDF_LUT_SIZE - 1u);
56
+ let NdotV = clamp(pos.x / denom, 0.0, 1.0);
57
+ let roughness = clamp(pos.y / denom, 0.0, 1.0);
58
+
59
+ // Reconstruct V from NdotV in canonical frame: N = (0, 0, 1).
60
+ let V = vec3<f32>(
61
+ sqrt(clamp(1.0 - NdotV * NdotV, 0.0, 1.0)),
62
+ 0.0,
63
+ NdotV,
64
+ );
65
+ let N = vec3<f32>(0.0, 0.0, 1.0);
66
+
67
+ var A: f32 = 0.0;
68
+ var B: f32 = 0.0;
69
+
70
+ for (var i: u32 = 0u; i < BRDF_LUT_SAMPLE_COUNT; i = i + 1u) {
71
+ let Xi = hammersley(i, BRDF_LUT_SAMPLE_COUNT);
72
+ let H = importanceSampleGGX(Xi, N, roughness);
73
+ let L = normalize(2.0 * dot(V, H) * H - V);
74
+
75
+ let NdotL = max(L.z, 0.0);
76
+ if (NdotL > 0.0) {
77
+ let NdotH = max(H.z, 0.0);
78
+ let VdotH = max(dot(V, H), 0.0);
79
+
80
+ let G = iblGeometrySmith(NdotV, NdotL, roughness);
81
+ let GVis = (G * VdotH) / max(NdotH * NdotV, 1e-5);
82
+ let Fc = pow(max(1.0 - VdotH, 0.0), 5.0);
83
+
84
+ A += (1.0 - Fc) * GVis;
85
+ B += Fc * GVis;
86
+ }
87
+ }
88
+ A = A / f32(BRDF_LUT_SAMPLE_COUNT);
89
+ B = B / f32(BRDF_LUT_SAMPLE_COUNT);
90
+
91
+ return vec4<f32>(A, B, 0.0, 1.0);
92
+ }
@@ -0,0 +1,56 @@
1
+ #define_import_path forgeax_pbr::ibl_equirect_to_cube
2
+
3
+ // @forgeax/engine-shader - ibl-equirect-to-cube.wgsl
4
+ // (feat-20260520-skylight-ibl-cubemap M3 / t43).
5
+ //
6
+ // Equirectangular HDRI -> cubemap projection pass.
7
+ // Per LearnOpenGL §6.2.1: render a unit cube from each of the 6 face views,
8
+ // fragment shader maps the world-space direction to equirect UV and samples
9
+ // the input HDRI texture.
10
+ //
11
+ // @group(0) = per-face viewProj uniform.
12
+ // @group(1) = equirect texture_2d + sampler. NOTE: this is the ONLY ibl-*
13
+ // module that binds a texture_2d at @group(1); the irradiance /
14
+ // prefilter modules bind texture_cube at the same slot, which
15
+ // is why the round-1 single-file design collided.
16
+ //
17
+ // Entries:
18
+ // - cubemap_vs (shared VS used by irradiance + prefilter as well;
19
+ // each module redeclares its own copy because WGSL
20
+ // modules cannot cross-import entry points)
21
+ // - equirectToCube_fs
22
+
23
+ #import forgeax_pbr::ibl_shared::{sampleSphericalMap}
24
+
25
+ struct CubemapVsIn {
26
+ @location(0) pos: vec3<f32>,
27
+ };
28
+ struct CubemapVsOut {
29
+ @builtin(position) clip: vec4<f32>,
30
+ @location(0) worldPos: vec3<f32>,
31
+ };
32
+
33
+ struct CubemapFaceUniforms {
34
+ viewProj: mat4x4<f32>,
35
+ };
36
+
37
+ @group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
38
+
39
+ @group(1) @binding(0) var equirectTexture: texture_2d<f32>;
40
+ @group(1) @binding(1) var equirectSamplerS: sampler;
41
+
42
+ @vertex
43
+ fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
44
+ var out: CubemapVsOut;
45
+ out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
46
+ out.worldPos = in0.pos;
47
+ return out;
48
+ }
49
+
50
+ @fragment
51
+ fn equirectToCube_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
52
+ let dir = normalize(in0.worldPos);
53
+ let uv = sampleSphericalMap(dir);
54
+ let color = textureSample(equirectTexture, equirectSamplerS, uv);
55
+ return vec4<f32>(color.rgb, 1.0);
56
+ }
@@ -0,0 +1,89 @@
1
+ #define_import_path forgeax_pbr::ibl_irradiance
2
+
3
+ // @forgeax/engine-shader - ibl-irradiance.wgsl
4
+ // (feat-20260520-skylight-ibl-cubemap M3 / t44).
5
+ //
6
+ // Diffuse irradiance convolution. Per LearnOpenGL §6.2.2: hemisphere
7
+ // Riemann sum (sampleDelta = 0.025) integrates the env cubemap to produce
8
+ // the convolved irradiance cubemap consumed by sampleIblDiffuse() at
9
+ // runtime.
10
+ //
11
+ // @group(0) = per-face viewProj uniform.
12
+ // @group(1) = env cubemap (texture_cube<f32>) + sampler. This is the same
13
+ // slot the prefilter module uses for its env cube, BUT because
14
+ // round-2 keeps each ibl-* module physically separate, the WGSL
15
+ // (group, binding) global-uniqueness rule applies per module
16
+ // rather than across the family.
17
+ //
18
+ // Entries: cubemap_vs + irradianceConvolve_fs.
19
+
20
+ #import forgeax_pbr::ibl_shared::{PI}
21
+
22
+ struct CubemapVsIn {
23
+ @location(0) pos: vec3<f32>,
24
+ };
25
+ struct CubemapVsOut {
26
+ @builtin(position) clip: vec4<f32>,
27
+ @location(0) worldPos: vec3<f32>,
28
+ };
29
+
30
+ struct CubemapFaceUniforms {
31
+ viewProj: mat4x4<f32>,
32
+ };
33
+
34
+ @group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
35
+
36
+ @group(1) @binding(0) var envCube: texture_cube<f32>;
37
+ @group(1) @binding(1) var envSamplerS: sampler;
38
+
39
+ const IRRADIANCE_SAMPLE_DELTA: f32 = 0.025;
40
+
41
+ @vertex
42
+ fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
43
+ var out: CubemapVsOut;
44
+ out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
45
+ out.worldPos = in0.pos;
46
+ return out;
47
+ }
48
+
49
+ @fragment
50
+ fn irradianceConvolve_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
51
+ let N = normalize(in0.worldPos);
52
+ let up0 = select(
53
+ vec3<f32>(0.0, 1.0, 0.0),
54
+ vec3<f32>(1.0, 0.0, 0.0),
55
+ abs(N.y) > 0.999,
56
+ );
57
+ let right = normalize(cross(up0, N));
58
+ let up = normalize(cross(N, right));
59
+
60
+ var irradiance = vec3<f32>(0.0);
61
+ var nrSamples: f32 = 0.0;
62
+
63
+ var phi: f32 = 0.0;
64
+ while (phi < 2.0 * PI) {
65
+ var theta: f32 = 0.0;
66
+ while (theta < 0.5 * PI) {
67
+ let tangentSample = vec3<f32>(
68
+ sin(theta) * cos(phi),
69
+ sin(theta) * sin(phi),
70
+ cos(theta),
71
+ );
72
+ let sampleVec = tangentSample.x * right +
73
+ tangentSample.y * up +
74
+ tangentSample.z * N;
75
+
76
+ let sampleColor = textureSampleLevel(
77
+ envCube, envSamplerS, sampleVec, 0.0,
78
+ ).rgb;
79
+ irradiance += sampleColor * cos(theta) * sin(theta);
80
+ nrSamples += 1.0;
81
+ theta += IRRADIANCE_SAMPLE_DELTA;
82
+ }
83
+ phi += IRRADIANCE_SAMPLE_DELTA;
84
+ }
85
+
86
+ // IRRADIANCE_PAYLOAD_E_TIMES_PI: the stored payload is E * pi.
87
+ irradiance = PI * irradiance / max(nrSamples, 1.0);
88
+ return vec4<f32>(irradiance, 1.0);
89
+ }
@@ -0,0 +1,101 @@
1
+ #define_import_path forgeax_pbr::ibl_prefilter
2
+
3
+ // @forgeax/engine-shader - ibl-prefilter.wgsl
4
+ // (feat-20260520-skylight-ibl-cubemap M3 / t45).
5
+ //
6
+ // GGX specular prefilter pass (Karis 2013 split-sum). For each roughness
7
+ // mip level, importance-sample the environment cubemap with SAMPLE_COUNT
8
+ // Hammersley samples; the result is the prefiltered radiance cube
9
+ // consumed by sampleIblSpecular() at runtime.
10
+ //
11
+ // @group(0) layout (dual uniform -- the (b3) compose test in the round-2
12
+ // dawn suite exercises this ordering):
13
+ // binding(0) = CubemapFaceUniforms { viewProj } (per-face)
14
+ // binding(1) = PrefilterUniforms { roughness, faceSize, _pad0, _pad1 }
15
+ // @group(1) = env cubemap + sampler. Same shape as ibl-irradiance, but
16
+ // physically isolated -- both bind @binding(0) to a texture_cube
17
+ // inside *their own* module without colliding because round-2
18
+ // ships each module as a separate file.
19
+ //
20
+ // Entries: cubemap_vs + prefilterEnv_fs.
21
+
22
+ #import forgeax_pbr::ibl_shared::{PI, hammersley, importanceSampleGGX, iblDGGX}
23
+
24
+ struct CubemapVsIn {
25
+ @location(0) pos: vec3<f32>,
26
+ };
27
+ struct CubemapVsOut {
28
+ @builtin(position) clip: vec4<f32>,
29
+ @location(0) worldPos: vec3<f32>,
30
+ };
31
+
32
+ struct CubemapFaceUniforms {
33
+ viewProj: mat4x4<f32>,
34
+ };
35
+
36
+ struct PrefilterUniforms {
37
+ roughness: f32,
38
+ faceSize: f32,
39
+ // Keep the uniform block 16-byte aligned on WebGL2 downlevel backends.
40
+ _pad0: f32,
41
+ _pad1: f32,
42
+ };
43
+
44
+ @group(0) @binding(0) var<uniform> faceUniforms: CubemapFaceUniforms;
45
+ @group(0) @binding(1) var<uniform> prefUniforms: PrefilterUniforms;
46
+
47
+ @group(1) @binding(0) var envCube: texture_cube<f32>;
48
+ @group(1) @binding(1) var envSamplerS: sampler;
49
+
50
+ const PREFILTER_SAMPLE_COUNT: u32 = 1024u;
51
+
52
+ @vertex
53
+ fn cubemap_vs(in0: CubemapVsIn) -> CubemapVsOut {
54
+ var out: CubemapVsOut;
55
+ out.clip = faceUniforms.viewProj * vec4<f32>(in0.pos, 1.0);
56
+ out.worldPos = in0.pos;
57
+ return out;
58
+ }
59
+
60
+ @fragment
61
+ fn prefilterEnv_fs(in0: CubemapVsOut) -> @location(0) vec4<f32> {
62
+ let roughness = prefUniforms.roughness;
63
+ // V = R = N (cubemap capture from origin assumption).
64
+ let N = normalize(in0.worldPos);
65
+ let V = N;
66
+
67
+ var prefilteredColor = vec3<f32>(0.0);
68
+ var totalWeight: f32 = 0.0;
69
+
70
+ for (var i: u32 = 0u; i < PREFILTER_SAMPLE_COUNT; i = i + 1u) {
71
+ let Xi = hammersley(i, PREFILTER_SAMPLE_COUNT);
72
+ let H = importanceSampleGGX(Xi, N, roughness);
73
+ let L = normalize(2.0 * dot(V, H) * H - V);
74
+
75
+ let NdotL = max(dot(N, L), 0.0);
76
+ if (NdotL > 0.0) {
77
+ // Mip level from solid-angle PDF ratio.
78
+ let D0 = iblDGGX(max(dot(N, H), 0.0), roughness);
79
+ let NdotH0 = max(dot(N, H), 0.0);
80
+ let HdotV = max(dot(H, V), 0.0);
81
+ let pdf = D0 * NdotH0 / (4.0 * HdotV) + 0.0001;
82
+
83
+ let resolution: f32 = 512.0;
84
+ let saTexel = 4.0 * PI / (6.0 * resolution * resolution);
85
+ let saSample = 1.0 / (f32(PREFILTER_SAMPLE_COUNT) * pdf + 0.0001);
86
+
87
+ let mipLevel = select(
88
+ 0.5 * log2(saSample / saTexel),
89
+ 0.0,
90
+ roughness == 0.0,
91
+ );
92
+
93
+ prefilteredColor += textureSampleLevel(
94
+ envCube, envSamplerS, L, mipLevel,
95
+ ).rgb * NdotL;
96
+ totalWeight += NdotL;
97
+ }
98
+ }
99
+ prefilteredColor = prefilteredColor / max(totalWeight, 0.001);
100
+ return vec4<f32>(prefilteredColor, 1.0);
101
+ }
@@ -0,0 +1,58 @@
1
+ #define_import_path forgeax_pbr::ibl_sampling
2
+
3
+ // @forgeax/engine-shader - ibl-sampling.wgsl
4
+ // (feat-20260520-skylight-ibl-cubemap M3 / t47).
5
+ //
6
+ // Runtime IBL sampling helpers consumed by pbr.wgsl. Each helper takes the
7
+ // texture + sampler as function arguments rather than declaring its own
8
+ // @group/@binding, so the host (pbr.wgsl's @group(1) material BGL,
9
+ // Skylight resources at @binding(7..13) per D-5 round-4) owns the
10
+ // binding layout and this module composes cleanly anywhere.
11
+ //
12
+ // Zero @group/@binding declarations -- this is the symmetric counterpart
13
+ // to ibl-shared.wgsl for runtime sampling code.
14
+ //
15
+ // Exports:
16
+ // - sampleIblDiffuse(N, irradianceMap, irradianceSampler)
17
+ // - sampleIblSpecular(N, V, roughness, F0, prefilterMap, prefilterSampler,
18
+ // brdfLut, brdfLutSampler)
19
+
20
+ #import forgeax_pbr::ibl_shared::{PI, fresnelSchlickRoughness, inverseRotateEnvironment}
21
+
22
+ // Sample pre-convolved irradiance from the irradiance cubemap.
23
+ // Y is negated to compensate for WebGPU's top-left texture origin vs the
24
+ // OpenGL convention used during equirect-to-cube render passes.
25
+ fn sampleIblDiffuse(
26
+ normal: vec3<f32>,
27
+ rotation: vec4<f32>,
28
+ irradianceMap: texture_cube<f32>,
29
+ irradianceSampler: sampler,
30
+ ) -> vec3<f32> {
31
+ let rotated = inverseRotateEnvironment(normal, rotation);
32
+ let dir = vec3<f32>(rotated.x, -rotated.y, rotated.z);
33
+ let irradianceEOverPi = textureSample(irradianceMap, irradianceSampler, dir).rgb;
34
+ return irradianceEOverPi / PI;
35
+ }
36
+
37
+ // Split-sum specular IBL: prefiltered env * (F0 * scale + bias).
38
+ fn sampleIblSpecular(
39
+ normal: vec3<f32>,
40
+ view: vec3<f32>,
41
+ roughness: f32,
42
+ F0: vec3<f32>,
43
+ rotation: vec4<f32>,
44
+ prefilterMap: texture_cube<f32>,
45
+ prefilterSampler: sampler,
46
+ brdfLut: texture_2d<f32>,
47
+ brdfLutSampler: sampler,
48
+ ) -> vec3<f32> {
49
+ let NdotV = max(dot(normal, view), 0.001);
50
+ let R = reflect(-view, normal);
51
+ let rotated = inverseRotateEnvironment(R, rotation);
52
+ let Rflip = vec3<f32>(rotated.x, -rotated.y, rotated.z);
53
+ let mip = roughness * 4.0;
54
+ let prefilteredColor = textureSampleLevel(prefilterMap, prefilterSampler, Rflip, mip).rgb;
55
+ let envBRDF = textureSample(brdfLut, brdfLutSampler, vec2<f32>(NdotV, roughness)).rg;
56
+ let F = fresnelSchlickRoughness(NdotV, F0, roughness);
57
+ return prefilteredColor * (F * envBRDF.r + envBRDF.g);
58
+ }