@forgeax/engine-shader 0.1.21 → 0.1.24

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 (83) hide show
  1. package/README.md +14 -5
  2. package/dist/ShaderRegistry.d.ts.map +1 -1
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +33042 -55
  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 +36 -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 +0 -17
  21. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +9 -3
  22. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +67 -122
  23. package/src/__tests__/deferred-lighting-ssao.test.ts +3 -3
  24. package/src/__tests__/direct-light-layout.unit.test.ts +40 -0
  25. package/src/__tests__/ibl-irradiance.unit.test.ts +16 -4
  26. package/src/__tests__/ibl-sampling.unit.test.ts +3 -4
  27. package/src/__tests__/lighting-directional.unit.test.ts +36 -0
  28. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -2
  29. package/src/__tests__/lighting-punctual.unit.test.ts +40 -5
  30. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +1 -1
  31. package/src/__tests__/ltc-provenance.unit.test.ts +33 -0
  32. package/src/__tests__/manifest.fixture.json +33 -0
  33. package/src/__tests__/material-builtins.unit.test.ts +21 -3
  34. package/src/__tests__/material-contract.unit.test.ts +188 -14
  35. package/src/__tests__/physical-clearcoat.unit.test.ts +65 -0
  36. package/src/__tests__/probe-lighting-composition.unit.test.ts +39 -0
  37. package/src/__tests__/rect-area-brdf.unit.test.ts +36 -0
  38. package/src/__tests__/reflection-probe-sampling.unit.test.ts +24 -2
  39. package/src/__tests__/shader-manifest-transmission.unit.test.ts +8 -10
  40. package/src/__tests__/shader.unit.test.ts +54 -85
  41. package/src/__tests__/spot-cookie-composition.unit.test.ts +58 -0
  42. package/src/__tests__/spot-modifier-composition.unit.test.ts +19 -0
  43. package/src/__tests__/sprite-variants.unit.test.ts +4 -1
  44. package/src/__tests__/standard-webgl2-varying-budget.unit.test.ts +17 -0
  45. package/src/__tests__/surface-v1-negative.unit.test.ts +37 -0
  46. package/src/__tests__/surface-v1.unit.test.ts +83 -0
  47. package/src/__tests__/transmission-thickness-scale.unit.test.ts +2 -6
  48. package/src/__tests__/vertex-color-variant.unit.test.ts +16 -5
  49. package/src/brdf.wgsl +115 -1
  50. package/src/common.wgsl +53 -79
  51. package/src/default-standard-pbr-skin.wgsl +687 -144
  52. package/src/default-standard-pbr.wgsl +600 -247
  53. package/src/default_standard_surface.wgsl +88 -0
  54. package/src/hdrp-cluster-forward.wgsl +225 -0
  55. package/src/ibl-irradiance.wgsl +47 -9
  56. package/src/ibl-prefilter.wgsl +13 -3
  57. package/src/ibl-sampling.wgsl +2 -2
  58. package/src/ibl-shared.wgsl +9 -2
  59. package/src/index.ts +23 -0
  60. package/src/lighting-directional.wgsl +9 -117
  61. package/src/lighting-probe.wgsl +51 -0
  62. package/src/lighting-punctual.wgsl +21 -11
  63. package/src/lighting-rect-area.wgsl +162 -0
  64. package/src/lighting-spot-modifiers.wgsl +100 -0
  65. package/src/lighting-spot-projector.wgsl +31 -0
  66. package/src/ltc/provenance.ts +19 -0
  67. package/src/ltc/tables.ts +2831 -0
  68. package/src/material/artifact-types.ts +137 -0
  69. package/src/material/physical/anisotropy.wgsl +26 -0
  70. package/src/material/physical/clearcoat.wgsl +24 -0
  71. package/src/material/physical/iridescence.wgsl +23 -0
  72. package/src/material/physical/sheen.wgsl +17 -0
  73. package/src/material-schemas.ts +77 -30
  74. package/src/register-default-sprite-lit.ts +3 -6
  75. package/src/register-default-standard-pbr-skin.ts +2 -2
  76. package/src/sprite-lit.wgsl +3 -3
  77. package/src/sprite.wgsl +1 -1
  78. package/src/standard-cluster.wgsl +80 -78
  79. package/src/surface_v1.wgsl +26 -0
  80. package/src/types.ts +2 -1
  81. package/src/volume/volume-inject.wgsl +34 -36
  82. package/src/volume/volume-integrate.wgsl +48 -38
  83. package/dist/.tsbuildinfo +0 -1
@@ -3,50 +3,49 @@
3
3
  //
4
4
  // The cluster module owns membership and raw payload decoding only. Punctual
5
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.
6
8
 
7
9
  #define_import_path forgeax_standard::cluster
8
10
 
9
11
  #import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
10
- #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
12
+ #import forgeax_pbr::lighting_spot_modifiers::{spotModifierFactors}
13
+ #import forgeax_pbr::lighting_rect_area::{evalRectAreaLtcGgx}
11
14
  #import forgeax_view::common::{view}
12
15
  #ifdef PROJECTOR_AVAILABLE
13
- #import forgeax_view::common::{projectorTexture, projectorSampler}
16
+ #import forgeax_pbr::lighting_spot_projector::{sampleStandardSpotProjector}
14
17
  #endif
15
18
  #ifdef POINT_SHADOW_AVAILABLE
16
19
  #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
20
+ #import forgeax_view::common::{shadowParams}
17
21
  #endif
18
22
 
19
23
  #ifdef CLUSTER_FORWARD_AVAILABLE
20
24
 
21
25
  const KIND_POINT: u32 = 0u;
22
26
  const KIND_SPOT: u32 = 1u;
23
- // LightSlot.kind_and_shadow.y uses one high bit as the accepted projector
24
- // marker. The remaining bits are the shadow-atlas tile (0..3), with zero as
25
- // the explicit View-UBO lane for projector-only spots.
27
+ const KIND_RECT_AREA: u32 = 2u;
26
28
  const PROJECTOR_FLAG: i32 = 0x40000000;
27
29
  const TILE_MASK: i32 = 0x3fffffff;
28
30
 
29
- // LightSlot is the byte-frozen 64B std430/std140 transport contract.
30
- // [0..2] position, [3] invRangeSquared
31
- // [4..6] colorTimesIntensity, [7] cosInner
32
- // [8..10] direction, [11] cosOuter
33
- // [12] raw kind u32, [13] shadow identity i32
34
- // [14..15] point near/far f32 bits; spot keeps these lanes zero.
35
- struct LightSlot {
36
- position : vec4<f32>,
37
- color : vec4<f32>,
38
- direction : vec4<f32>,
39
- kind_and_shadow : vec4<u32>,
31
+ // DirectLightSlot is the byte-frozen 80B std430/std140 transport contract.
32
+ // The named rows match the host packer; metadata carries kind and identities.
33
+ struct DirectLightSlot {
34
+ position : vec4<f32>,
35
+ colorTimesIntensity : vec4<f32>,
36
+ direction : vec4<f32>,
37
+ auxiliary : vec4<f32>,
38
+ metadata : vec4<u32>,
40
39
  };
41
40
 
42
- const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
41
+ const DIRECT_LIGHT_SLOT_BYTE_SIZE: u32 = 80u;
43
42
 
44
43
  struct ClusterUniform {
45
44
  grid : vec4<u32>,
46
45
  near_far_log : vec4<f32>,
47
46
  };
48
47
 
49
- @group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
48
+ @group(2) @binding(3) var<storage, read> light_data : array<DirectLightSlot, 256>;
50
49
  @group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
51
50
  @group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
52
51
  @group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
@@ -89,30 +88,10 @@ fn ndc_position_to_cluster(
89
88
  return vec3(cx, cy, cz);
90
89
  }
91
90
 
92
- // Surface SpotLight projector sampling is owned by the same clustered light
93
- // evaluator as the analytic cone/range contribution. The host marks the
94
- // selected projector in the LightSlot flag and writes its matrix to the
95
- // corresponding View lane (lane 0 for projector-only); no second
96
- // material-local sampling loop or resource owner is allowed.
97
- fn sampleStandardSpotProjector(
98
- lightViewProj : mat4x4<f32>,
99
- world_pos : vec3<f32>,
100
- ) -> vec3<f32> {
101
- #ifdef PROJECTOR_AVAILABLE
102
- let uv = projectSpotUv(lightViewProj, world_pos);
103
- if (!(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0)) {
104
- return vec3<f32>(1.0);
105
- }
106
- return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
107
- #else
108
- return vec3<f32>(1.0);
109
- #endif
110
- }
111
-
112
91
  // Decode one kind first, then delegate the complete evaluation to the shared
113
92
  // punctual owner. Unknown kinds are explicitly zero contribution.
114
93
  fn evaluate_cluster_light(
115
- light : LightSlot,
94
+ light : DirectLightSlot,
116
95
  world_pos : vec3<f32>,
117
96
  normal : vec3<f32>,
118
97
  view_dir : vec3<f32>,
@@ -122,78 +101,101 @@ fn evaluate_cluster_light(
122
101
  f0 : vec3<f32>,
123
102
  flat_2d : bool,
124
103
  ) -> vec3<f32> {
125
- let kind = light.kind_and_shadow.x;
104
+ let kind = light.metadata.x;
126
105
  if (kind == KIND_POINT) {
127
106
  if (flat_2d) {
128
107
  return evalPointFlat(
129
- light.position.xyz, light.color.xyz, light.position.w,
108
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
130
109
  world_pos, base_color,
131
110
  );
132
111
  }
133
112
  #ifdef POINT_SHADOW_AVAILABLE
134
- let layer = bitcast<i32>(light.kind_and_shadow.y);
113
+ let layer = bitcast<i32>(light.metadata.y);
135
114
  if (layer >= 0) {
115
+ let shadow = shadowParams[layer];
136
116
  return evalPointShadowed(
137
- light.position.xyz, light.color.xyz, light.position.w,
117
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
138
118
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
139
119
  layer,
140
- bitcast<f32>(light.kind_and_shadow.z),
141
- bitcast<f32>(light.kind_and_shadow.w),
120
+ shadow.x,
121
+ shadow.y,
142
122
  0.005, 0.05,
143
123
  );
144
124
  }
145
125
  #endif
146
126
  return evalPoint(
147
- light.position.xyz, light.color.xyz, light.position.w,
127
+ light.position.xyz, light.colorTimesIntensity.xyz, light.position.w,
148
128
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
149
129
  );
150
130
  }
151
131
  if (kind == KIND_SPOT) {
152
132
  if (flat_2d) {
153
133
  return evalSpotFlat(
154
- light.position.xyz, light.direction.xyz, light.color.xyz,
155
- light.color.w, light.direction.w, light.position.w,
134
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
135
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
156
136
  world_pos, base_color,
157
137
  );
158
138
  }
159
- let encoded_tile = bitcast<i32>(light.kind_and_shadow.y);
160
- // The unshadowed sentinel is -1. A signed bitwise test against the
161
- // projector flag would misclassify that sentinel because -1 has every bit
162
- // set, so admit the flag only from the non-negative encoded domain.
139
+ let modifierFactors = spotModifierFactors(
140
+ light.position.xyz,
141
+ light.direction.xyz,
142
+ light.direction.w,
143
+ world_pos,
144
+ light.auxiliary.w,
145
+ light.metadata,
146
+ );
147
+ let modifier = vec3<f32>(modifierFactors.x) * modifierFactors.yzw;
148
+ let encoded_tile = bitcast<i32>(light.metadata.y);
149
+ // The optional projector marker shares the non-negative shadow identity
150
+ // lane. The direct-light packer keeps the lower tile bits intact.
151
+ var projector_selected = false;
152
+ var tile : i32 = encoded_tile;
163
153
  if (encoded_tile >= 0 && (encoded_tile & PROJECTOR_FLAG) != 0) {
164
- let tile = encoded_tile & TILE_MASK;
165
- if (tile < 4) {
166
- let projector = sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos);
167
- return evalSpotShadowed(
168
- light.position.xyz, light.direction.xyz, light.color.xyz,
169
- // LightSlot packs cosInner in color.w and cosOuter in direction.w;
170
- // keep the evaluator's named cone order intact at the decode boundary.
171
- light.color.w, light.direction.w, light.position.w,
172
- world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
173
- view.spotLightViewProj[tile], tile, 0.005, 0.05,
174
- bitcast<f32>(light.kind_and_shadow.w),
175
- bitcast<f32>(light.kind_and_shadow.z),
176
- ) * projector;
177
- }
178
- return vec3<f32>(0.0);
154
+ projector_selected = true;
155
+ tile = encoded_tile & TILE_MASK;
179
156
  }
180
- if (encoded_tile >= 0 && encoded_tile < 4) {
181
- let tile = encoded_tile;
157
+ if (tile >= 0 && tile < 4) {
182
158
  return evalSpotShadowed(
183
- light.position.xyz, light.direction.xyz, light.color.xyz,
184
- // LightSlot packs cosInner in color.w and cosOuter in direction.w;
159
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
160
+ // DirectLightSlot packs cosInner in color.w and cosOuter in direction.w;
185
161
  // keep the evaluator's named cone order intact at the decode boundary.
186
- light.color.w, light.direction.w, light.position.w,
187
- world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
162
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
163
+ world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
188
164
  view.spotLightViewProj[tile], tile, 0.005, 0.05,
189
- bitcast<f32>(light.kind_and_shadow.w),
190
- bitcast<f32>(light.kind_and_shadow.z),
191
- );
165
+ 3.0,
166
+ 1.0,
167
+ ) * modifier
168
+ #ifdef PROJECTOR_AVAILABLE
169
+ * select(
170
+ vec3<f32>(1.0),
171
+ sampleStandardSpotProjector(view.spotLightViewProj[tile], world_pos, light.metadata),
172
+ projector_selected,
173
+ )
174
+ #endif
175
+ ;
192
176
  }
193
177
  return evalSpot(
194
- light.position.xyz, light.direction.xyz, light.color.xyz,
195
- light.color.w, light.direction.w, light.position.w,
178
+ light.position.xyz, light.direction.xyz, light.colorTimesIntensity.xyz,
179
+ light.colorTimesIntensity.w, light.direction.w, light.position.w,
196
180
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
181
+ ) * modifier;
182
+ }
183
+ if (kind == KIND_RECT_AREA) {
184
+ return evalRectAreaLtcGgx(
185
+ light.position.xyz,
186
+ light.colorTimesIntensity.xyz,
187
+ light.auxiliary.xyz,
188
+ light.direction.xyz,
189
+ light.colorTimesIntensity.w,
190
+ light.direction.w,
191
+ light.position.w,
192
+ world_pos,
193
+ normal,
194
+ view_dir,
195
+ base_color,
196
+ metallic,
197
+ alpha_sq,
198
+ f0,
197
199
  );
198
200
  }
199
201
  return vec3<f32>(0.0);
@@ -0,0 +1,26 @@
1
+ #define_import_path forgeax_material::surface_v1
2
+
3
+ // Engine-owned input ABI for authored Standard material surfaces.
4
+ struct SurfaceInput {
5
+ positionOS : vec3<f32>,
6
+ positionWS : vec3<f32>,
7
+ geometricNormalWS : vec3<f32>,
8
+ tangentWS : vec4<f32>,
9
+ viewDirectionWS : vec3<f32>,
10
+ uv0 : vec2<f32>,
11
+ uv1 : vec2<f32>,
12
+ vertexColor : vec4<f32>,
13
+ frontFacing : bool,
14
+ };
15
+
16
+ // Surface output is consumed by the Standard BRDF and pass family.
17
+ struct SurfaceData {
18
+ baseColor : vec3<f32>,
19
+ normalWS : vec3<f32>,
20
+ metallic : f32,
21
+ roughness : f32,
22
+ emissive : vec3<f32>,
23
+ occlusion : f32,
24
+ opacity : f32,
25
+ alphaClipThreshold : f32,
26
+ };
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.
@@ -29,41 +30,18 @@ struct VolumeParams {
29
30
  optics : vec4<f32>,
30
31
  };
31
32
 
32
- struct SpotLight {
33
- position : vec3<f32>,
34
- invRangeSquared : f32,
35
- colorTimesIntensity : vec3<f32>,
36
- cosInner : f32,
37
- direction : vec3<f32>,
38
- cosOuter : f32,
39
- depthBias : f32,
40
- normalBias : f32,
41
- pcfKernelSize : f32,
42
- shadowAtlasTile : i32,
43
- shadowIntensity : f32,
44
- };
45
-
46
- struct SpotLightsArray {
47
- count : u32,
48
- slots : array<SpotLight, 4>,
49
- };
50
-
51
- struct PointLight {
52
- position : vec3<f32>,
53
- invRangeSquared : f32,
54
- colorTimesIntensity : vec3<f32>,
55
- shadowAtlasLayer : i32,
56
- };
33
+ const DIRECT_LIGHT_METADATA_SENTINEL : u32 = 0xffffffffu;
34
+ const DIRECT_LIGHT_TILE_MASK : u32 = 0x3fffffffu;
57
35
 
58
- struct PointLightsArray {
59
- count : u32,
60
- slots : array<PointLight, 4>,
36
+ struct ClusterUniform {
37
+ grid : vec4<u32>,
38
+ near_far_log : vec4<f32>,
61
39
  };
62
40
 
63
41
  @group(0) @binding(5) var<uniform> volume_params : VolumeParams;
64
42
  @group(0) @binding(6) var volume_froxel : texture_storage_2d_array<rgba8unorm, write>;
65
- @group(0) @binding(7) var<storage, read> spotLightsBuffer : SpotLightsArray;
66
- @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;
67
45
  @group(0) @binding(0) var<uniform> view : View;
68
46
 
69
47
  fn reconstruct_world(uv : vec2<f32>, depth : f32) -> vec3<f32> {
@@ -161,12 +139,32 @@ fn volume_cascade(world_position : vec3<f32>, view_depth : f32) -> f32 {
161
139
  );
162
140
  }
163
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
+
164
161
  fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
165
- let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
166
- if (slot >= spotLightsBuffer.count) { return 0.0; }
167
- let light = spotLightsBuffer.slots[slot];
168
- if (light.shadowAtlasTile < 0) { return 1.0; }
169
- 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);
170
168
  let projectorUv = projectSpotUv(view.spotLightViewProj[tile], world_position);
171
169
  // The accepted projector tuple is sampled with textureSampleLevel by the
172
170
  // surface and volume owners when a projector texture is present.
@@ -186,7 +184,7 @@ fn spot_shadow_visibility_at(world_position : vec3<f32>) -> f32 {
186
184
  let shadow_size = vec2<f32>(textureDimensions(shadowMap));
187
185
  return sample_shadow_2d_kernel(
188
186
  shadowMap, shadowSampler, atlas_uv, 1.0 / shadow_size, projected.z,
189
- light.normalBias, light.depthBias, 1.0, light.pcfKernelSize,
187
+ light.auxiliary.y, light.auxiliary.x, 1.0, 3.0,
190
188
  );
191
189
  }
192
190
 
@@ -2,6 +2,7 @@
2
2
  // forgeax_pbr::lighting_punctual remains the surface contract; the isolated
3
3
  // attenuation module below provides its resource-free volume equivalents.
4
4
  #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
5
+ #import forgeax_view::common::{DirectLightSlot}
5
6
 
6
7
  struct VolumeView {
7
8
  _prefix : array<vec4<f32>, 6>,
@@ -11,20 +12,12 @@ struct VolumeView {
11
12
  _viewTail : array<vec4<f32>, 18>,
12
13
  spotLightViewProj : array<mat4x4<f32>, 4>,
13
14
  };
14
- struct SpotLight {
15
- position : vec3<f32>, invRangeSquared : f32,
16
- colorTimesIntensity : vec3<f32>, cosInner : f32,
17
- direction : vec3<f32>, cosOuter : f32,
18
- depthBias : f32, normalBias : f32, pcfKernelSize : f32,
19
- shadowAtlasTile : i32,
20
- 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>,
21
20
  };
22
- struct SpotLightsArray { count : u32, slots : array<SpotLight, 4>, };
23
- struct PointLight {
24
- position : vec3<f32>, invRangeSquared : f32,
25
- colorTimesIntensity : vec3<f32>, shadowAtlasLayer : i32,
26
- };
27
- struct PointLightsArray { count : u32, slots : array<PointLight, 4>, };
28
21
  struct VolumeParams {
29
22
  bounds_min : vec4<f32>, bounds_max : vec4<f32>, extinction : vec4<f32>, albedo : vec4<f32>,
30
23
  emission : vec4<f32>, light_direction : vec4<f32>, light_color : vec4<f32>, optics : vec4<f32>,
@@ -69,8 +62,8 @@ fn stratified32(seed : u32, frame_index : u32, odd_stride : u32) -> f32 {
69
62
  @group(0) @binding(7) var density_sampler : sampler;
70
63
  @group(0) @binding(8) var history_seed : texture_storage_2d<rgba16float, write>;
71
64
  @group(0) @binding(9) var temporal_seed : texture_storage_2d<rgba16float, write>;
72
- @group(0) @binding(10) var<storage, read> spotLightsBuffer : SpotLightsArray;
73
- @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;
74
67
  @group(0) @binding(12) var projectorTexture : texture_2d<f32>;
75
68
  @group(0) @binding(13) var projectorSampler : sampler;
76
69
 
@@ -107,26 +100,43 @@ fn evalSpotAttenuation(
107
100
  return distance * smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
108
101
  }
109
102
 
110
- fn spotProjectorMatrix(light : SpotLight) -> mat4x4<f32> {
111
- if (light.shadowAtlasTile >= 0) {
112
- return volume_view.spotLightViewProj[light.shadowAtlasTile];
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);
109
+ }
110
+
111
+ fn spotProjectorMatrix(light : DirectLightSlot) -> mat4x4<f32> {
112
+ let tile = shadowAtlasTile(light);
113
+ if (tile >= 0) {
114
+ return volume_view.spotLightViewProj[tile];
113
115
  }
114
116
  return volume_view.spotLightViewProj[0];
115
117
  }
116
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
+
117
127
  fn spot_attenuation_at(world_position : vec3<f32>) -> f32 {
118
- let slot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
119
- if (slot >= spotLightsBuffer.count) { return 0.0; }
120
- 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)];
121
131
  let projectorUv = projectSpotUv(spotProjectorMatrix(light), world_position);
122
132
  if (any(projectorUv < vec2<f32>(0.0)) || any(projectorUv > vec2<f32>(1.0))) { return 0.0; }
123
133
  return evalSpotAttenuation(
124
- light.position,
125
- light.direction,
134
+ light.position.xyz,
135
+ light.direction.xyz,
126
136
  world_position,
127
- light.cosInner,
128
- light.cosOuter,
129
- light.invRangeSquared,
137
+ light.colorTimesIntensity.w,
138
+ light.direction.w,
139
+ light.position.w,
130
140
  );
131
141
  }
132
142
 
@@ -151,21 +161,21 @@ fn evalVolumeSpot(
151
161
  }
152
162
 
153
163
  fn volume_point_radiance(world_position : vec3<f32>) -> vec3<f32> {
154
- let pointSlot = u32(clamp(round(volume_params.emission.w), 0.0, 3.0));
155
- if (pointSlot >= pointLightsBuffer.count) { return vec3<f32>(0.0); }
156
- 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)];
157
167
  return evalVolumePoint(
158
- point.position, point.colorTimesIntensity, point.invRangeSquared, world_position,
168
+ point.position.xyz, point.colorTimesIntensity.xyz, point.position.w, world_position,
159
169
  );
160
170
  }
161
171
 
162
172
  fn volume_spot_radiance(world_position : vec3<f32>) -> vec3<f32> {
163
- let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
164
- if (spotSlot >= spotLightsBuffer.count) { return vec3<f32>(0.0); }
165
- 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)];
166
176
  let radiance = evalVolumeSpot(
167
- spot.position, spot.direction, spot.colorTimesIntensity,
168
- 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,
169
179
  );
170
180
  let projectorUv = projectSpotUv(spotProjectorMatrix(spot), world_position);
171
181
  if (!(projectorUv.x >= 0.0 && projectorUv.x <= 1.0 && projectorUv.y >= 0.0 && projectorUv.y <= 1.0)) {
@@ -178,9 +188,9 @@ fn volume_spot_radiance(world_position : vec3<f32>) -> vec3<f32> {
178
188
  }
179
189
 
180
190
  fn volume_spot_shadow_intensity() -> f32 {
181
- let spotSlot = u32(clamp(round(volume_params.light_color.w), 0.0, 3.0));
182
- if (spotSlot >= spotLightsBuffer.count) { return 1.0; }
183
- 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);
184
194
  }
185
195
 
186
196
  fn volume_light_radiance(world_position : vec3<f32>) -> vec3<f32> {