@forgeax/engine-shader 0.1.20 → 0.1.23

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 (74) hide show
  1. package/README.md +10 -0
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +6 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +33070 -8
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/ltc/provenance.d.ts +17 -0
  8. package/dist/ltc/provenance.d.ts.map +1 -0
  9. package/dist/ltc/tables.d.ts +22 -0
  10. package/dist/ltc/tables.d.ts.map +1 -0
  11. package/dist/material/artifact-types.d.ts +52 -0
  12. package/dist/material/artifact-types.d.ts.map +1 -1
  13. package/dist/material-schemas.d.ts +5 -0
  14. package/dist/material-schemas.d.ts.map +1 -1
  15. package/dist/register-default-sprite-lit.d.ts +3 -6
  16. package/dist/register-default-sprite-lit.d.ts.map +1 -1
  17. package/dist/types.d.ts +2 -1
  18. package/dist/types.d.ts.map +1 -1
  19. package/package.json +4 -4
  20. package/src/ShaderRegistry.ts +65 -0
  21. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
  22. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
  23. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +138 -30
  24. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  25. package/src/__tests__/ibl-irradiance.unit.test.ts +7 -4
  26. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  27. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +16 -2
  28. package/src/__tests__/lighting-punctual.unit.test.ts +61 -8
  29. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  30. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  31. package/src/__tests__/manifest.fixture.json +33 -0
  32. package/src/__tests__/material-contract.unit.test.ts +61 -1
  33. package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
  34. package/src/__tests__/probe-lighting-composition.unit.test.ts +38 -0
  35. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  36. package/src/__tests__/reflection-probe-sampling.unit.test.ts +45 -0
  37. package/src/__tests__/scene-temporal.unit.test.ts +20 -0
  38. package/src/__tests__/shader.unit.test.ts +40 -15
  39. package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
  40. package/src/__tests__/spot-cookie-composition.unit.test.ts +57 -0
  41. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  42. package/src/__tests__/sprite-lit-shader.test.ts +24 -11
  43. package/src/__tests__/sprite-variants.unit.test.ts +5 -1
  44. package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
  45. package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
  46. package/src/brdf.wgsl +115 -1
  47. package/src/common.wgsl +95 -91
  48. package/src/default-standard-pbr-skin.wgsl +108 -92
  49. package/src/default-standard-pbr.wgsl +189 -191
  50. package/src/fxaa.wgsl +27 -6
  51. package/src/ibl-irradiance.wgsl +4 -1
  52. package/src/ibl-sampling.wgsl +56 -2
  53. package/src/index.ts +23 -0
  54. package/src/lighting-directional.wgsl +161 -132
  55. package/src/lighting-probe.wgsl +51 -0
  56. package/src/lighting-punctual.wgsl +69 -29
  57. package/src/lighting-rect-area.wgsl +162 -0
  58. package/src/lighting-spot-modifiers.wgsl +93 -0
  59. package/src/ltc/provenance.ts +19 -0
  60. package/src/ltc/tables.ts +2831 -0
  61. package/src/material/artifact-types.ts +137 -0
  62. package/src/material-schemas.ts +12 -0
  63. package/src/register-default-sprite-lit.ts +3 -6
  64. package/src/scene-temporal.wgsl +14 -8
  65. package/src/shadow-pcf.wgsl +44 -1
  66. package/src/sprite-lit.wgsl +37 -53
  67. package/src/sprite.wgsl +1 -1
  68. package/src/standard-cluster.wgsl +274 -0
  69. package/src/tonemap.wgsl +8 -3
  70. package/src/types.ts +2 -1
  71. package/src/volume/volume-inject.wgsl +48 -38
  72. package/src/volume/volume-integrate.wgsl +47 -42
  73. package/dist/.tsbuildinfo +0 -1
  74. package/src/hdrp-cluster-forward.wgsl +0 -224
@@ -0,0 +1,274 @@
1
+ // standard-cluster.wgsl — Standard clustered punctual light evaluation.
2
+ // feat-20260608-cluster-lighting M4 / w17.
3
+ //
4
+ // The cluster module owns membership and raw payload decoding only. Punctual
5
+ // BRDF, range, cone, PCF, and fail-open behavior live in lighting-punctual.
6
+ // ProbeBlendRecord is intentionally absent: probes are not DirectLightSlot
7
+ // members and this cluster owner never performs an all-probe loop.
8
+
9
+ #define_import_path forgeax_standard::cluster
10
+
11
+ #import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
12
+ #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
13
+ #import forgeax_pbr::lighting_spot_modifiers::{spotModifierFactors}
14
+ #import forgeax_pbr::lighting_rect_area::{evalRectAreaLtcGgx}
15
+ #import forgeax_view::common::{view}
16
+ #ifdef PROJECTOR_AVAILABLE
17
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
18
+ #import forgeax_view::common::{spotModifierSampler, cookieTexture}
19
+ #else
20
+ #ifndef EXTENDED_LIGHTING_AVAILABLE
21
+ #import forgeax_view::common::{projectorTexture, projectorSampler}
22
+ #endif
23
+ #endif
24
+ #endif
25
+ #ifdef POINT_SHADOW_AVAILABLE
26
+ #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
27
+ #import forgeax_view::common::{shadowParams}
28
+ #endif
29
+
30
+ #ifdef CLUSTER_FORWARD_AVAILABLE
31
+
32
+ const KIND_POINT: u32 = 0u;
33
+ const KIND_SPOT: u32 = 1u;
34
+ const KIND_RECT_AREA: u32 = 2u;
35
+ const PROJECTOR_FLAG: i32 = 0x40000000;
36
+ const TILE_MASK: i32 = 0x3fffffff;
37
+
38
+ // DirectLightSlot is the byte-frozen 80B std430/std140 transport contract.
39
+ // The named rows match the host packer; metadata carries kind and identities.
40
+ struct DirectLightSlot {
41
+ position : vec4<f32>,
42
+ colorTimesIntensity : vec4<f32>,
43
+ direction : vec4<f32>,
44
+ auxiliary : vec4<f32>,
45
+ metadata : vec4<u32>,
46
+ };
47
+
48
+ const DIRECT_LIGHT_SLOT_BYTE_SIZE: u32 = 80u;
49
+
50
+ struct ClusterUniform {
51
+ grid : vec4<u32>,
52
+ near_far_log : vec4<f32>,
53
+ };
54
+
55
+ @group(2) @binding(3) var<storage, read> light_data : array<DirectLightSlot, 256>;
56
+ @group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
57
+ @group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
58
+ @group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
59
+
60
+ fn get_ssao_intensity() -> f32 {
61
+ return cluster_uniform.near_far_log.w;
62
+ }
63
+
64
+ fn view_z_to_z_slice(
65
+ view_z : f32,
66
+ grid_z : u32,
67
+ near : f32,
68
+ far : f32,
69
+ log_far_over_near : f32,
70
+ ) -> u32 {
71
+ if (view_z >= -near) {
72
+ return 0u;
73
+ }
74
+ let slice = floor(log(-view_z / near) / log_far_over_near * f32(grid_z));
75
+ let u_slice = u32(slice);
76
+ if (u_slice >= grid_z) {
77
+ return grid_z - 1u;
78
+ }
79
+ return u_slice;
80
+ }
81
+
82
+ fn ndc_position_to_cluster(
83
+ ndc : vec3<f32>,
84
+ view_z : f32,
85
+ grid_x : u32,
86
+ grid_y : u32,
87
+ grid_z : u32,
88
+ near : f32,
89
+ far : f32,
90
+ log_far : f32,
91
+ ) -> vec3<u32> {
92
+ let cx = clamp(u32(floor((ndc.x * 0.5 + 0.5) * f32(grid_x))), 0u, grid_x - 1u);
93
+ let cy = clamp(u32(floor((ndc.y * 0.5 + 0.5) * f32(grid_y))), 0u, grid_y - 1u);
94
+ let cz = view_z_to_z_slice(view_z, grid_z, near, far, log_far);
95
+ return vec3(cx, cy, cz);
96
+ }
97
+
98
+ // Surface SpotLight projector sampling is owned by the same clustered light
99
+ // evaluator as the analytic cone/range contribution. The host marks the
100
+ // selected projector in the DirectLightSlot flag and writes its matrix to the
101
+ // corresponding View lane (lane 0 for projector-only); no second
102
+ // material-local sampling loop or resource owner is allowed.
103
+ fn sampleStandardSpotProjector(
104
+ lightViewProj : mat4x4<f32>,
105
+ world_pos : vec3<f32>,
106
+ metadata : vec4<u32>,
107
+ ) -> vec3<f32> {
108
+ #ifdef PROJECTOR_AVAILABLE
109
+ let uv = projectSpotUv(lightViewProj, world_pos);
110
+ if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
111
+ return vec3<f32>(1.0);
112
+ }
113
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
114
+ if (metadata.w == 0xffffffffu) {
115
+ return vec3<f32>(1.0);
116
+ }
117
+ return textureSampleLevel(cookieTexture, spotModifierSampler, uv, metadata.w, 0.0).rgb;
118
+ #else
119
+ return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
120
+ #endif
121
+ #else
122
+ return vec3<f32>(1.0);
123
+ #endif
124
+ }
125
+
126
+ // Decode one kind first, then delegate the complete evaluation to the shared
127
+ // punctual owner. Unknown kinds are explicitly zero contribution.
128
+ fn evaluate_cluster_light(
129
+ light : DirectLightSlot,
130
+ world_pos : vec3<f32>,
131
+ normal : vec3<f32>,
132
+ view_dir : vec3<f32>,
133
+ base_color : vec3<f32>,
134
+ metallic : f32,
135
+ alpha_sq : f32,
136
+ f0 : vec3<f32>,
137
+ flat_2d : bool,
138
+ ) -> vec3<f32> {
139
+ let kind = light.metadata.x;
140
+ if (kind == KIND_POINT) {
141
+ if (flat_2d) {
142
+ return evalPointFlat(
143
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
144
+ world_pos, base_color,
145
+ );
146
+ }
147
+ #ifdef POINT_SHADOW_AVAILABLE
148
+ let layer = bitcast<i32>(light.metadata.y);
149
+ if (layer >= 0) {
150
+ let shadow = shadowParams[layer];
151
+ return evalPointShadowed(
152
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
153
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
154
+ layer,
155
+ shadow.x,
156
+ shadow.y,
157
+ 0.005, 0.05,
158
+ );
159
+ }
160
+ #endif
161
+ return evalPoint(
162
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
163
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
164
+ );
165
+ }
166
+ if (kind == KIND_SPOT) {
167
+ if (flat_2d) {
168
+ return evalSpotFlat(
169
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
170
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
171
+ world_pos, base_color,
172
+ );
173
+ }
174
+ let modifierFactors = spotModifierFactors(
175
+ light.position.xyz,
176
+ light.direction.xyz,
177
+ light.direction.w,
178
+ world_pos,
179
+ light.auxiliary.w,
180
+ light.metadata,
181
+ );
182
+ let modifier = vec3<f32>(modifierFactors.x) * modifierFactors.yzw;
183
+ let encoded_tile = bitcast<i32>(light.metadata.y);
184
+ // The optional projector marker shares the non-negative shadow identity
185
+ // lane. The direct-light packer keeps the lower tile bits intact.
186
+ var projector_selected = false;
187
+ var tile : i32 = encoded_tile;
188
+ if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0) {
189
+ projector_selected = true;
190
+ tile = encoded_tile & TILE_MASK;
191
+ }
192
+ if (tile >= 0 && tile < 4) {
193
+ return evalSpotShadowed(
194
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
195
+ // DirectLightSlot packs cosInner in color.w and cosOuter in direction.w;
196
+ // keep the evaluator's named cone order intact at the decode boundary.
197
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
198
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
199
+ view.spotLightViewProj[tile], tile, 0.005, 0.05,
200
+ 3.0,
201
+ 1.0,
202
+ ) * modifier
203
+ #ifdef PROJECTOR_AVAILABLE
204
+ * select(
205
+ vec3<f32>(1.0),
206
+ sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata),
207
+ projector_selected,
208
+ )
209
+ #endif
210
+ ;
211
+ }
212
+ return evalSpot(
213
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
214
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
215
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
216
+ ) * modifier;
217
+ }
218
+ if (kind == KIND_RECT_AREA) {
219
+ return evalRectAreaLtcGgx(
220
+ light.position.xyz,
221
+ light.colorTimesIntensity.xyz,
222
+ light.auxiliary.xyz,
223
+ light.direction.xyz,
224
+ light.colorTimesIntensity.w,
225
+ light.direction.w,
226
+ light.position.w,
227
+ world_pos,
228
+ normal,
229
+ view_dir,
230
+ base_color,
231
+ metallic,
232
+ alpha_sq,
233
+ f0,
234
+ );
235
+ }
236
+ return vec3<f32>(0.0);
237
+ }
238
+
239
+ fn evaluateStandardClusterLights(
240
+ ndc : vec3<f32>,
241
+ view_z : f32,
242
+ world_pos : vec3<f32>,
243
+ normal : vec3<f32>,
244
+ view_dir : vec3<f32>,
245
+ base_color : vec3<f32>,
246
+ metallic : f32,
247
+ alpha_sq : f32,
248
+ f0 : vec3<f32>,
249
+ flat_2d : bool,
250
+ ) -> vec3<f32> {
251
+ let gx = cluster_uniform.grid.x;
252
+ let gy = cluster_uniform.grid.y;
253
+ let gz = cluster_uniform.grid.z;
254
+ let near = cluster_uniform.near_far_log.x;
255
+ let far = cluster_uniform.near_far_log.y;
256
+ let log_far = cluster_uniform.near_far_log.z;
257
+ var total_radiance = vec3<f32>(0.0);
258
+
259
+ let cluster_idx = ndc_position_to_cluster(ndc, view_z, gx, gy, gz, near, far, log_far);
260
+ let cluster_linear = cluster_idx.z * gy * gx + cluster_idx.y * gx + cluster_idx.x;
261
+ let grid_offset = cluster_linear * 2u;
262
+ let list_offset = cluster_grid[grid_offset];
263
+ let list_count = cluster_grid[grid_offset + 1u];
264
+ for (var i = 0u; i < list_count; i = i + 1u) {
265
+ let light = light_data[light_index_list[list_offset + i]];
266
+ total_radiance += evaluate_cluster_light(
267
+ light, world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
268
+ flat_2d,
269
+ );
270
+ }
271
+ return total_radiance;
272
+ }
273
+
274
+ #endif // CLUSTER_FORWARD_AVAILABLE
package/src/tonemap.wgsl CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  #import forgeax_view::common::FullscreenOutput
11
11
  #import forgeax_view::common::fullscreen_triangle
12
- #import forgeax_view::common::linearToSrgbOetf
12
+ #import forgeax_view::common::{linearToSrgbOetf, ditherUnorm8}
13
13
 
14
14
  const TONEMAP_LUMINANCE_EPSILON : f32 = 1e-5;
15
15
 
@@ -17,7 +17,7 @@ struct TonemapParams {
17
17
  exposure : f32,
18
18
  whitePoint : f32,
19
19
  mode : u32,
20
- pad1 : f32,
20
+ ditherEnabled : f32,
21
21
  };
22
22
 
23
23
  @group(1) @binding(0) var hdr : texture_2d<f32>;
@@ -171,5 +171,10 @@ fn fs_main(in : FullscreenOutput) -> @location(0) vec4<f32> {
171
171
  }
172
172
  // Every public mode reaches exactly one shared sRGB OETF.
173
173
  let encoded = linearToSrgbOetf(mapped);
174
- return vec4<f32>(encoded, source.a);
174
+ let output = select(
175
+ encoded,
176
+ ditherUnorm8(encoded, in.position.xy),
177
+ params.ditherEnabled > 0.5,
178
+ );
179
+ return vec4<f32>(output, source.a);
175
180
  }
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  * no longer carry a bindingLayout sidecar (§Change stance: no v1/v2
18
18
  * dual-path, no incremental drop window).
19
19
  */
20
+ import type { MaterialShaderArtifactReceipt } from './material/artifact-types.js';
20
21
  export interface MaterialShaderManifestVariant {
21
22
  readonly definesKey: string;
22
23
  readonly defines: Record<string, boolean>;
@@ -33,7 +34,7 @@ export interface MaterialShaderManifestVariant {
33
34
  *
34
35
  * For Engine.create() internal use only; AI users never see this type.
35
36
  */
36
- export interface MaterialShaderManifestEntry {
37
+ export interface MaterialShaderManifestEntry extends Partial<MaterialShaderArtifactReceipt> {
37
38
  readonly identifier: string;
38
39
  readonly sourcePath: string;
39
40
  readonly composedWgsl: string;
@@ -1,6 +1,7 @@
1
1
  #define_import_path forgeax_view::volume_inject
2
2
  #import forgeax_pbr::shadow_pcf::{sample_shadow_2d_kernel}
3
3
  #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
4
+ #import forgeax_view::common::{DirectLightSlot}
4
5
  // forgeax_pbr::lighting_punctual exports the surface-equivalent
5
6
  // evalVolumePoint and evalVolumeSpot contract; this stage consumes their
6
7
  // shared projector and shadow visibility facts without a second light owner.
@@ -9,7 +10,8 @@ struct View {
9
10
  worldViewProj : mat4x4<f32>, lightDir : vec3<f32>, lightColor : vec3<f32>, cameraPos : vec3<f32>,
10
11
  lightViewProj_A : mat4x4<f32>, inverseViewProj : mat4x4<f32>, lightViewProj_B : mat4x4<f32>,
11
12
  lightViewProj_C : mat4x4<f32>, lightViewProj_D : mat4x4<f32>, splitPlanes : array<vec4<f32>,4>,
12
- cascadeCount : f32, cascadeBlend : f32, depthBias : f32, normalBias : f32, pcfKernelSize : f32,
13
+ cascadeCount : f32, cascadeBlend : f32, depthBias : f32, normalBias : f32,
14
+ directionalShadowFilter : vec4<f32>,
13
15
  spotLightViewProj : array<mat4x4<f32>,4>, temporalCurrentViewProj : mat4x4<f32>,
14
16
  temporalPreviousViewProj : mat4x4<f32>, temporalProjection : vec4<f32>, temporalPreviousCameraPos : vec4<f32>,
15
17
  };
@@ -28,41 +30,18 @@ struct VolumeParams {
28
30
  optics : vec4<f32>,
29
31
  };
30
32
 
31
- struct SpotLight {
32
- position : vec3<f32>,
33
- invRangeSquared : f32,
34
- colorTimesIntensity : vec3<f32>,
35
- cosInner : f32,
36
- direction : vec3<f32>,
37
- cosOuter : f32,
38
- depthBias : f32,
39
- normalBias : f32,
40
- pcfKernelSize : f32,
41
- shadowAtlasTile : i32,
42
- shadowIntensity : f32,
43
- };
44
-
45
- struct SpotLightsArray {
46
- count : u32,
47
- slots : array<SpotLight, 4>,
48
- };
49
-
50
- struct PointLight {
51
- position : vec3<f32>,
52
- invRangeSquared : f32,
53
- colorTimesIntensity : vec3<f32>,
54
- shadowAtlasLayer : i32,
55
- };
33
+ const DIRECT_LIGHT_METADATA_SENTINEL : u32 = 0xffffffffu;
34
+ const DIRECT_LIGHT_TILE_MASK : u32 = 0x3fffffffu;
56
35
 
57
- struct PointLightsArray {
58
- count : u32,
59
- slots : array<PointLight, 4>,
36
+ struct ClusterUniform {
37
+ grid : vec4<u32>,
38
+ near_far_log : vec4<f32>,
60
39
  };
61
40
 
62
41
  @group(0) @binding(5) var<uniform> volume_params : VolumeParams;
63
42
  @group(0) @binding(6) var volume_froxel : texture_storage_2d_array<rgba8unorm, write>;
64
- @group(0) @binding(7) var<storage, read> spotLightsBuffer : SpotLightsArray;
65
- @group(0) @binding(8) var<storage, read> pointLightsBuffer : PointLightsArray;
43
+ @group(0) @binding(7) var<storage, read> light_data : array<DirectLightSlot, 256>;
44
+ @group(0) @binding(8) var<uniform> cluster_uniform : ClusterUniform;
66
45
  @group(0) @binding(0) var<uniform> view : View;
67
46
 
68
47
  fn reconstruct_world(uv : vec2<f32>, depth : f32) -> vec3<f32> {
@@ -143,18 +122,49 @@ fn volume_cascade(world_position : vec3<f32>, view_depth : f32) -> f32 {
143
122
  if (!(tile_uv.x >= 0.0 && tile_uv.x <= 1.0 && tile_uv.y >= 0.0 && tile_uv.y <= 1.0 && projected.z <= 1.0)) { return 1.0; }
144
123
  let uv = tile_uv * tile_scale + vec2<f32>(tile) * tile_scale;
145
124
  let shadow_size = vec2<f32>(textureDimensions(shadowMap));
125
+ let filter_profile = clamp(u32(round(view.directionalShadowFilter.x)), 1u, 5u);
126
+ // Volumetric injection currently owns a depth-only PCF receiver. Directional
127
+ // PCSS profiles remain an explicit capability fallback here: the surface
128
+ // directional owner performs blocker search, while volume keeps the same
129
+ // accepted filter carrier and uses the stable PCF3 receiver instead of
130
+ // misreading profiles 4/5 as a kernel width.
131
+ let volume_kernel = select(
132
+ select(select(3.0, 5.0, filter_profile == 3u), 1.0, filter_profile == 1u),
133
+ 3.0,
134
+ filter_profile >= 4u,
135
+ );
146
136
  return sample_shadow_2d_kernel(
147
137
  shadowMap, shadowSampler, uv, 1.0 / shadow_size, projected.z,
148
- 0.0, view.depthBias, 1.0, view.pcfKernelSize,
138
+ 0.0, view.depthBias, 1.0, volume_kernel,
149
139
  );
150
140
  }
151
141
 
142
+ fn shadowAtlasTile(light : DirectLightSlot) -> i32 {
143
+ let encoded = light.metadata.y;
144
+ if (encoded == DIRECT_LIGHT_METADATA_SENTINEL) { return -1; }
145
+ // Projector-selected spots carry the marker in bit 30 and retain the
146
+ // selected shadow tile in the lower 30 bits. A projector-only spot uses
147
+ // tile zero, matching the surface projector contract.
148
+ let tile = encoded & DIRECT_LIGHT_TILE_MASK;
149
+ if (tile >= 4u) { return -1; }
150
+ return i32(tile);
151
+ }
152
+
153
+ fn selected_cluster_light_slot(value : f32) -> i32 {
154
+ let slot = i32(round(value));
155
+ if (slot < 0) { return -1; }
156
+ let index = u32(slot);
157
+ if (index >= cluster_uniform.grid.w || index >= 256u) { return -1; }
158
+ return slot;
159
+ }
160
+
152
161
  fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
153
- let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
154
- if (slot >= spotLightsBuffer.count) { return 0.0; }
155
- let light = spotLightsBuffer.slots[slot];
156
- if (light.shadowAtlasTile < 0) { return 1.0; }
157
- let tile = u32(light.shadowAtlasTile);
162
+ let slot = selected_cluster_light_slot(volume_params.light_color.w);
163
+ if (slot < 0) { return 0.0; }
164
+ let light = light_data[u32(slot)];
165
+ let encodedTile = shadowAtlasTile(light);
166
+ if (encodedTile < 0) { return 1.0; }
167
+ let tile = u32(encodedTile);
158
168
  let projectorUv = projectSpotUv(view.spotLightViewProj[tile], world_position);
159
169
  // The accepted projector tuple is sampled with textureSampleLevel by the
160
170
  // surface and volume owners when a projector texture is present.
@@ -174,7 +184,7 @@ fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
174
184
  let shadow_size = vec2<f32>(textureDimensions(shadowMap));
175
185
  return sample_shadow_2d_kernel(
176
186
  shadowMap, shadowSampler, atlas_uv, 1.0 / shadow_size, projected.z,
177
- light.normalBias, light.depthBias, 1.0, light.pcfKernelSize,
187
+ light.auxiliary.y, light.auxiliary.x, 1.0, 3.0,
178
188
  );
179
189
  }
180
190
 
@@ -1,6 +1,8 @@
1
1
  #define_import_path forgeax_view::volume_integrate
2
2
  // forgeax_pbr::lighting_punctual remains the surface contract; the isolated
3
3
  // attenuation module below provides its resource-free volume equivalents.
4
+ #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
5
+ #import forgeax_view::common::{DirectLightSlot}
4
6
 
5
7
  struct VolumeView {
6
8
  _prefix : array<vec4<f32>, 6>,
@@ -10,20 +12,12 @@ struct VolumeView {
10
12
  _viewTail : array<vec4<f32>, 18>,
11
13
  spotLightViewProj : array<mat4x4<f32>, 4>,
12
14
  };
13
- struct SpotLight {
14
- position : vec3<f32>, invRangeSquared : f32,
15
- colorTimesIntensity : vec3<f32>, cosInner : f32,
16
- direction : vec3<f32>, cosOuter : f32,
17
- depthBias : f32, normalBias : f32, pcfKernelSize : f32,
18
- shadowAtlasTile : i32,
19
- shadowIntensity : f32,
15
+ // Volume reads the same full light_data payload as Standard surface Cluster;
16
+ // it has no secondary light mirror.
17
+ struct ClusterUniform {
18
+ grid : vec4<u32>,
19
+ near_far_log : vec4<f32>,
20
20
  };
21
- struct SpotLightsArray { count : u32, slots : array<SpotLight, 4>, };
22
- struct PointLight {
23
- position : vec3<f32>, invRangeSquared : f32,
24
- colorTimesIntensity : vec3<f32>, shadowAtlasLayer : i32,
25
- };
26
- struct PointLightsArray { count : u32, slots : array<PointLight, 4>, };
27
21
  struct VolumeParams {
28
22
  bounds_min : vec4<f32>, bounds_max : vec4<f32>, extinction : vec4<f32>, albedo : vec4<f32>,
29
23
  emission : vec4<f32>, light_direction : vec4<f32>, light_color : vec4<f32>, optics : vec4<f32>,
@@ -68,8 +62,8 @@ fn stratified32(seed : u32, frame_index : u32, odd_stride : u32) -> f32 {
68
62
  @group(0) @binding(7) var density_sampler : sampler;
69
63
  @group(0) @binding(8) var history_seed : texture_storage_2d<rgba16float, write>;
70
64
  @group(0) @binding(9) var temporal_seed : texture_storage_2d<rgba16float, write>;
71
- @group(0) @binding(10) var<storage, read> spotLightsBuffer : SpotLightsArray;
72
- @group(0) @binding(11) var<storage, read> pointLightsBuffer : PointLightsArray;
65
+ @group(0) @binding(10) var<storage, read> light_data : array<DirectLightSlot, 256>;
66
+ @group(0) @binding(11) var<uniform> cluster_uniform : ClusterUniform;
73
67
  @group(0) @binding(12) var projectorTexture : texture_2d<f32>;
74
68
  @group(0) @binding(13) var projectorSampler : sampler;
75
69
 
@@ -106,32 +100,43 @@ fn evalSpotAttenuation(
106
100
  return distance * smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
107
101
  }
108
102
 
109
- fn projectSpotUv(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec2<f32> {
110
- let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
111
- let invW = select(1.0 / clip.w, 0.0, abs(clip.w) < 1e-6);
112
- return vec2<f32>(clip.x * invW * 0.5 + 0.5, clip.y * invW * -0.5 + 0.5);
103
+ fn shadowAtlasTile(light : DirectLightSlot) -> i32 {
104
+ let encoded = light.metadata.y;
105
+ if (encoded == 0xffffffffu) { return -1; }
106
+ let tile = encoded & 0x3fffffffu;
107
+ if (tile >= 4u) { return -1; }
108
+ return i32(tile);
113
109
  }
114
110
 
115
- fn spotProjectorMatrix(light : SpotLight) -> mat4x4<f32> {
116
- if (light.shadowAtlasTile >= 0) {
117
- return volume_view.spotLightViewProj[light.shadowAtlasTile];
111
+ fn spotProjectorMatrix(light : DirectLightSlot) -> mat4x4<f32> {
112
+ let tile = shadowAtlasTile(light);
113
+ if (tile >= 0) {
114
+ return volume_view.spotLightViewProj[tile];
118
115
  }
119
116
  return volume_view.spotLightViewProj[0];
120
117
  }
121
118
 
119
+ fn selected_cluster_light_slot(value : f32) -> i32 {
120
+ let slot = i32(round(value));
121
+ if (slot < 0) { return -1; }
122
+ let index = u32(slot);
123
+ if (index >= cluster_uniform.grid.w || index >= 256u) { return -1; }
124
+ return slot;
125
+ }
126
+
122
127
  fn spot_attenuation_at(world_position : vec3<f32>) -> f32 {
123
- let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
124
- if (slot >= spotLightsBuffer.count) { return 0.0; }
125
- let light = spotLightsBuffer.slots[slot];
128
+ let slot = selected_cluster_light_slot(volume_params.light_color.w);
129
+ if (slot < 0) { return 0.0; }
130
+ let light = light_data[u32(slot)];
126
131
  let projectorUv = projectSpotUv(spotProjectorMatrix(light), world_position);
127
132
  if (any(projectorUv < vec2<f32>(0.0)) || any(projectorUv > vec2<f32>(1.0))) { return 0.0; }
128
133
  return evalSpotAttenuation(
129
- light.position,
130
- light.direction,
134
+ light.position.xyz,
135
+ light.direction.xyz,
131
136
  world_position,
132
- light.cosInner,
133
- light.cosOuter,
134
- light.invRangeSquared,
137
+ light.colorTimesIntensity.w,
138
+ light.direction.w,
139
+ light.position.w,
135
140
  );
136
141
  }
137
142
 
@@ -156,21 +161,21 @@ fn evalVolumeSpot(
156
161
  }
157
162
 
158
163
  fn volume_point_radiance(world_position : vec3<f32>) -> vec3<f32> {
159
- let pointSlot = u32(clamp(round(volume_params.emission.w), 0.0, 3.0));
160
- if (pointSlot >= pointLightsBuffer.count) { return vec3<f32>(0.0); }
161
- let point = pointLightsBuffer.slots[pointSlot];
164
+ let pointSlot = selected_cluster_light_slot(volume_params.emission.w);
165
+ if (pointSlot < 0) { return vec3<f32>(0.0); }
166
+ let point = light_data[u32(pointSlot)];
162
167
  return evalVolumePoint(
163
- point.position, point.colorTimesIntensity, point.invRangeSquared, world_position,
168
+ point.position.xyz, point.colorTimesIntensity.xyz, point.position.w, world_position,
164
169
  );
165
170
  }
166
171
 
167
172
  fn volume_spot_radiance(world_position : vec3<f32>) -> vec3<f32> {
168
- let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
169
- if (spotSlot >= spotLightsBuffer.count) { return vec3<f32>(0.0); }
170
- let spot = spotLightsBuffer.slots[spotSlot];
173
+ let spotSlot = selected_cluster_light_slot(volume_params.light_color.w);
174
+ if (spotSlot < 0) { return vec3<f32>(0.0); }
175
+ let spot = light_data[u32(spotSlot)];
171
176
  let radiance = evalVolumeSpot(
172
- spot.position, spot.direction, spot.colorTimesIntensity,
173
- spot.cosInner, spot.cosOuter, spot.invRangeSquared, world_position,
177
+ spot.position.xyz, spot.direction.xyz, spot.colorTimesIntensity.xyz,
178
+ spot.colorTimesIntensity.w, spot.direction.w, spot.position.w, world_position,
174
179
  );
175
180
  let projectorUv = projectSpotUv(spotProjectorMatrix(spot), world_position);
176
181
  if (!(projectorUv.x >= 0.0 && projectorUv.x <= 1.0 && projectorUv.y >= 0.0 && projectorUv.y <= 1.0)) {
@@ -183,9 +188,9 @@ fn volume_spot_radiance(world_position : vec3<f32>) -> vec3<f32> {
183
188
  }
184
189
 
185
190
  fn volume_spot_shadow_intensity() -> f32 {
186
- let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
187
- if (spotSlot >= spotLightsBuffer.count) { return 1.0; }
188
- return clamp(spotLightsBuffer.slots[spotSlot].shadowIntensity, 0.0, 1.0);
191
+ let spotSlot = selected_cluster_light_slot(volume_params.light_color.w);
192
+ if (spotSlot < 0) { return 1.0; }
193
+ return clamp(light_data[u32(spotSlot)].auxiliary.z, 0.0, 1.0);
189
194
  }
190
195
 
191
196
  fn volume_light_radiance(world_position : vec3<f32>) -> vec3<f32> {