@forgeax/engine-shader 0.1.20 → 0.1.21

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 (38) hide show
  1. package/README.md +10 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/ShaderRegistry.d.ts.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.mjs +52 -1
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +4 -4
  9. package/src/ShaderRegistry.ts +65 -0
  10. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +30 -4
  11. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +7 -0
  12. package/src/__tests__/default-standard-pbr-transmission.unit.test.ts +69 -24
  13. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +8 -0
  14. package/src/__tests__/lighting-punctual.unit.test.ts +32 -6
  15. package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
  16. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -0
  17. package/src/__tests__/scene-temporal.unit.test.ts +20 -0
  18. package/src/__tests__/shader.unit.test.ts +34 -13
  19. package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
  20. package/src/__tests__/sprite-lit-shader.test.ts +24 -11
  21. package/src/__tests__/sprite-variants.unit.test.ts +1 -0
  22. package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
  23. package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
  24. package/src/common.wgsl +42 -12
  25. package/src/default-standard-pbr-skin.wgsl +30 -71
  26. package/src/default-standard-pbr.wgsl +92 -163
  27. package/src/fxaa.wgsl +27 -6
  28. package/src/ibl-sampling.wgsl +54 -0
  29. package/src/index.ts +6 -0
  30. package/src/lighting-directional.wgsl +157 -18
  31. package/src/lighting-punctual.wgsl +51 -21
  32. package/src/scene-temporal.wgsl +14 -8
  33. package/src/shadow-pcf.wgsl +44 -1
  34. package/src/sprite-lit.wgsl +36 -52
  35. package/src/{hdrp-cluster-forward.wgsl → standard-cluster.wgsl} +73 -60
  36. package/src/tonemap.wgsl +8 -3
  37. package/src/volume/volume-inject.wgsl +14 -2
  38. package/src/volume/volume-integrate.wgsl +1 -6
@@ -1,8 +1,12 @@
1
1
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
2
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
2
3
  #define_import_path forgeax_material::sprite-lit
3
4
 
4
- #import forgeax_view::common::{View, Mesh, InstanceData, PointLight, SpotLight, view, meshes, instances, pointLightsBuffer, spotLightsBuffer, sampleMaterialTexture}
5
- #import forgeax_scene_temporal::{packSceneTemporalV1}
5
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
6
+ #import forgeax_scene_temporal::{packSceneTemporalV1, sceneViewZ}
7
+ #ifdef CLUSTER_FORWARD_AVAILABLE
8
+ #import forgeax_standard::cluster::{evaluateStandardClusterLights}
9
+ #endif
6
10
 
7
11
  // @forgeax/engine-shader - sprite-lit.wgsl
8
12
  // (tweak-20260701-sprite-lit-flat-default-drop-ndotl-for-2d).
@@ -26,7 +30,8 @@
26
30
  // - no shadow receiver / caster
27
31
  // - no Godot-style light_mode blends
28
32
  // - no IBL ambient
29
- // - no HDRP cluster-forward path
33
+ // - Standard clustered punctual lights are enabled by the
34
+ // CLUSTER_FORWARD_AVAILABLE variant.
30
35
  //
31
36
  // >>> AI user error self-recovery hint:
32
37
  // sprite-lit needs at least one light in scene; for unlit sprites use
@@ -38,8 +43,7 @@
38
43
  // pipelineState.defaultSampler + .defaultWhiteTextureView on the host side):
39
44
  //
40
45
  // @group(0) @binding(0) view uniform
41
- // @group(0) @binding(1) pointLightsBuffer storage / uniform (variant)
42
- // @group(0) @binding(2) spotLightsBuffer storage / uniform (variant)
46
+ // @group(0) @binding(1..2) Standard Cluster storage (selected variant)
43
47
  // @group(1) @binding(0) material uniform (sprite layout
44
48
  // colorTint /
45
49
  // region /
@@ -80,16 +84,17 @@ struct VsIn {
80
84
  @location(3) tangent : vec4<f32>,
81
85
  };
82
86
 
83
- // VsOut carries the atlas UV plus the per-fragment world-space position.
84
- // The world position is emitted from the vertex stage (already computed
85
- // there for `out.clip`), so PointLight / SpotLight attenuation gets an
86
- // exact per-fragment worldPos through interpolation. This replaces the
87
- // earlier fragment-side reconstruction path, which collapsed under
88
- // multi-instance draws and could not handle the 9-slice quad layout.
87
+ // VsOut carries the atlas UV plus the per-fragment world-space position and
88
+ // the Standard cluster coordinates produced by the vertex stage. Fragment
89
+ // `@builtin(position)` is framebuffer-space after rasterization, so it is not
90
+ // a valid source for NDC cluster lookup; keep the perspective divide here and
91
+ // interpolate the result alongside worldPos.
89
92
  struct VsOut {
90
93
  @builtin(position) clip : vec4<f32>,
91
94
  @location(0) uv_atlas : vec2<f32>,
92
95
  @location(1) worldPos : vec3<f32>,
96
+ @location(2) ndc : vec3<f32>,
97
+ @location(3) viewZ : f32,
93
98
  };
94
99
 
95
100
  struct SpriteVertex {
@@ -155,6 +160,11 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32, @builtin(vertex_index)
155
160
  out.clip = view.worldViewProj * world;
156
161
  out.uv_atlas = vertex.uvAtlas;
157
162
  out.worldPos = world.xyz;
163
+ let clipPos = out.clip;
164
+ out.ndc = clipPos.xyz / clipPos.w;
165
+ // Keep the cluster depth exactly aligned with the CPU binner for both
166
+ // perspective and off-axis orthographic projections.
167
+ out.viewZ = sceneViewZ(clipPos, view.temporalProjection);
158
168
  return out;
159
169
  }
160
170
 
@@ -174,50 +184,24 @@ fn spriteLitDirectional(albedo : vec3<f32>) -> vec3<f32> {
174
184
  return albedo * view.lightColor;
175
185
  }
176
186
 
177
- // Flat point-light contribution. Applies the KHR-lights-punctual smooth-
178
- // window range attenuation only. `attenuation` is 0 outside the light's
179
- // range (`invRangeSquared`) and 1 at the light center; the reciprocal-square
180
- // term gives the physical falloff. URP cap = 4 point lights per pass.
181
- fn spriteLitPoint(p : PointLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
182
- let toLight = p.position - worldPos;
183
- let dSquared = max(dot(toLight, toLight), 1e-4);
184
- let factor = 1.0 - (dSquared * p.invRangeSquared) * (dSquared * p.invRangeSquared);
185
- let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
186
- return albedo * p.colorTimesIntensity * attenuation;
187
- }
188
-
189
- // Flat spot-light contribution. Same range attenuation as spriteLitPoint,
190
- // modulated by a smoothstep cone factor (host-side degree -> cosine
191
- // conversion). Spot direction only shapes the cone; it does not project
192
- // onto a surface normal. URP cap = 4 spot lights per pass.
193
- fn spriteLitSpot(s : SpotLight, worldPos : vec3<f32>, albedo : vec3<f32>) -> vec3<f32> {
194
- let toLight = s.position - worldPos;
195
- let dSquared = max(dot(toLight, toLight), 1e-4);
196
- let l = toLight / sqrt(dSquared);
197
- let factor = 1.0 - (dSquared * s.invRangeSquared) * (dSquared * s.invRangeSquared);
198
- let attenuation = max(min(factor, 1.0), 0.0) / dSquared;
199
- let cone = smoothstep(s.cosOuter, s.cosInner, dot(l, -s.direction));
200
- return albedo * s.colorTimesIntensity * attenuation * cone;
201
- }
202
-
203
187
  // Shared shading body used by both fs_main (LDR) and fs_main_hdr (HDR).
204
188
  // The two entry points differ only in their tail-end clamp + srgb encode
205
189
  // step (LDR clamp / HDR pass-through boundary).
206
- fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
190
+ fn spriteLitShadeAccum(
191
+ albedo : vec3<f32>,
192
+ worldPos : vec3<f32>,
193
+ ndc : vec3<f32>,
194
+ viewZ : f32,
195
+ ) -> vec3<f32> {
207
196
  // Directional contribution (1 light, View UBO).
208
197
  var lit = spriteLitDirectional(albedo);
209
- // Point-light contribution (URP cap = 4; PointLightsArray.count).
210
- let pointCount = pointLightsBuffer.count;
211
- for (var i : u32 = 0u; i < pointCount; i = i + 1u) {
212
- let p = pointLightsBuffer.slots[i];
213
- lit = lit + spriteLitPoint(p, worldPos, albedo);
214
- }
215
- // Spot-light contribution (URP cap = 4).
216
- let spotCount = spotLightsBuffer.count;
217
- for (var i : u32 = 0u; i < spotCount; i = i + 1u) {
218
- let s = spotLightsBuffer.slots[i];
219
- lit = lit + spriteLitSpot(s, worldPos, albedo);
220
- }
198
+ #ifdef CLUSTER_FORWARD_AVAILABLE
199
+ let viewDir = normalize(view.cameraPos - worldPos);
200
+ lit = lit + evaluateStandardClusterLights(
201
+ ndc, viewZ, worldPos, vec3<f32>(0.0, 0.0, 1.0),
202
+ viewDir, albedo, 0.0, 1.0, vec3<f32>(0.04), true,
203
+ );
204
+ #endif
221
205
  return lit;
222
206
  }
223
207
 
@@ -225,7 +209,7 @@ fn spriteLitShadeAccum(albedo : vec3<f32>, worldPos : vec3<f32>) -> vec3<f32> {
225
209
  fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
226
210
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
227
211
  let albedo4 = texel * material.colorTint;
228
- let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
212
+ let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
229
213
  // Strict clamp 0..1 before premultiplied alpha multiply keeps the LDR
230
214
  // path bounded even when a scene stacks many lights.
231
215
  let lit_rgba = clamp(vec4<f32>(lit, albedo4.a), vec4<f32>(0.0), vec4<f32>(1.0));
@@ -243,7 +227,7 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
243
227
  fn fs_main_hdr(in : VsOut) -> @location(0) vec4<f32> {
244
228
  let texel = sampleMaterialTexture(baseColorTexture, baseColorSampler, in.uv_atlas, material.baseColorTextureCoordinatesMetadata.zw);
245
229
  let albedo4 = texel * material.colorTint;
246
- let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos);
230
+ let lit = spriteLitShadeAccum(albedo4.rgb, in.worldPos, in.ndc, in.viewZ);
247
231
  // HDR variant: do NOT clamp the lit output to [0, 1]; let the tonemap
248
232
  // pass absorb HDR values > 1. Alpha stays clamped because the premult
249
233
  // math requires alpha in [0, 1].
@@ -1,12 +1,13 @@
1
- // hdrp-cluster-forward.wgsl — HDRP cluster-forward punctual light evaluation.
1
+ // standard-cluster.wgsl — Standard clustered punctual light evaluation.
2
2
  // feat-20260608-cluster-lighting M4 / w17.
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
6
 
7
- #define_import_path forgeax_hdrp::cluster_forward
7
+ #define_import_path forgeax_standard::cluster
8
8
 
9
- #import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot, evalSpotShadowed, projectSpotUv}
9
+ #import forgeax_pbr::lighting_punctual::{evalPoint, evalPointFlat, evalSpot, evalSpotFlat, evalSpotShadowed}
10
+ #import forgeax_pbr::lighting_attenuation::{projectSpotUv}
10
11
  #import forgeax_view::common::{view}
11
12
  #ifdef PROJECTOR_AVAILABLE
12
13
  #import forgeax_view::common::{projectorTexture, projectorSampler}
@@ -19,15 +20,18 @@
19
20
 
20
21
  const KIND_POINT: u32 = 0u;
21
22
  const KIND_SPOT: u32 = 1u;
22
- const PROJECTOR_ONLY_TILE: i32 = -2;
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.
26
+ const PROJECTOR_FLAG: i32 = 0x40000000;
27
+ const TILE_MASK: i32 = 0x3fffffff;
23
28
 
24
29
  // LightSlot is the byte-frozen 64B std430/std140 transport contract.
25
30
  // [0..2] position, [3] invRangeSquared
26
31
  // [4..6] colorTimesIntensity, [7] cosInner
27
32
  // [8..10] direction, [11] cosOuter
28
33
  // [12] raw kind u32, [13] shadow identity i32
29
- // [14] point near / spot shadow intensity f32 bits
30
- // [15] point far / spot PCF kernel width f32 bits
34
+ // [14..15] point near/far f32 bits; spot keeps these lanes zero.
31
35
  struct LightSlot {
32
36
  position : vec4<f32>,
33
37
  color : vec4<f32>,
@@ -37,35 +41,14 @@ struct LightSlot {
37
41
 
38
42
  const LIGHTSLOT_BYTE_SIZE: u32 = 64u;
39
43
 
40
- fn sampleClusterSpotProjector(lightViewProj : mat4x4<f32>, worldPos : vec3<f32>) -> vec3<f32> {
41
- #ifdef PROJECTOR_AVAILABLE
42
- let clip = lightViewProj * vec4<f32>(worldPos, 1.0);
43
- // The projector is a cookie, not an extra cone. A fragment that cannot be
44
- // projected (or lies outside the tile) keeps the analytic Spot contribution
45
- // intact, matching Three's SpotLightNode fail-open map gate.
46
- if (abs(clip.w) < 1e-6) { return vec3<f32>(1.0); }
47
- let uv = projectSpotUv(lightViewProj, worldPos);
48
- if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
49
- return vec3<f32>(1.0);
50
- }
51
- return textureSampleLevel(projectorTexture, projectorSampler, uv, 0.0).rgb;
52
- #else
53
- return vec3<f32>(1.0);
54
- #endif
55
- }
56
-
57
44
  struct ClusterUniform {
58
45
  grid : vec4<u32>,
59
46
  near_far_log : vec4<f32>,
60
47
  };
61
48
 
62
- #if STORAGE_BUFFER_AVAILABLE == true
63
49
  @group(2) @binding(3) var<storage, read> light_data : array<LightSlot, 256>;
64
50
  @group(2) @binding(4) var<storage, read> cluster_grid : array<u32>;
65
51
  @group(2) @binding(5) var<storage, read> light_index_list : array<u32>;
66
- #else
67
- @group(2) @binding(3) var<uniform> light_data_uniform : array<LightSlot, 128>;
68
- #endif
69
52
  @group(2) @binding(6) var<uniform> cluster_uniform : ClusterUniform;
70
53
 
71
54
  fn get_ssao_intensity() -> f32 {
@@ -106,6 +89,26 @@ fn ndc_position_to_cluster(
106
89
  return vec3(cx, cy, cz);
107
90
  }
108
91
 
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
+
109
112
  // Decode one kind first, then delegate the complete evaluation to the shared
110
113
  // punctual owner. Unknown kinds are explicitly zero contribution.
111
114
  fn evaluate_cluster_light(
@@ -117,9 +120,16 @@ fn evaluate_cluster_light(
117
120
  metallic : f32,
118
121
  alpha_sq : f32,
119
122
  f0 : vec3<f32>,
123
+ flat_2d : bool,
120
124
  ) -> vec3<f32> {
121
125
  let kind = light.kind_and_shadow.x;
122
126
  if (kind == KIND_POINT) {
127
+ if (flat_2d) {
128
+ return evalPointFlat(
129
+ light.position.xyz, light.color.xyz, light.position.w,
130
+ world_pos, base_color,
131
+ );
132
+ }
123
133
  #ifdef POINT_SHADOW_AVAILABLE
124
134
  let layer = bitcast<i32>(light.kind_and_shadow.y);
125
135
  if (layer >= 0) {
@@ -139,9 +149,36 @@ fn evaluate_cluster_light(
139
149
  );
140
150
  }
141
151
  if (kind == KIND_SPOT) {
142
- let tile = bitcast<i32>(light.kind_and_shadow.y);
143
- if (tile >= 0) {
144
- let projector = sampleClusterSpotProjector(view.spotLightViewProj[tile], world_pos);
152
+ if (flat_2d) {
153
+ return evalSpotFlat(
154
+ light.position.xyz, light.direction.xyz, light.color.xyz,
155
+ light.color.w, light.direction.w, light.position.w,
156
+ world_pos, base_color,
157
+ );
158
+ }
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.
163
+ 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);
179
+ }
180
+ if (encoded_tile >= 0 && encoded_tile < 4) {
181
+ let tile = encoded_tile;
145
182
  return evalSpotShadowed(
146
183
  light.position.xyz, light.direction.xyz, light.color.xyz,
147
184
  // LightSlot packs cosInner in color.w and cosOuter in direction.w;
@@ -149,22 +186,9 @@ fn evaluate_cluster_light(
149
186
  light.color.w, light.direction.w, light.position.w,
150
187
  world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
151
188
  view.spotLightViewProj[tile], tile, 0.005, 0.05,
152
- clamp(
153
- round(select(3.0, bitcast<f32>(light.kind_and_shadow.w),
154
- bitcast<f32>(light.kind_and_shadow.w) > 0.5)),
155
- 1.0,
156
- 5.0,
157
- ),
158
- clamp(bitcast<f32>(light.kind_and_shadow.z), 0.0, 1.0),
159
- ) * projector;
160
- }
161
- if (tile == PROJECTOR_ONLY_TILE) {
162
- let projector = sampleClusterSpotProjector(view.spotLightViewProj[0], world_pos);
163
- return evalSpot(
164
- light.position.xyz, light.direction.xyz, light.color.xyz,
165
- light.color.w, light.direction.w, light.position.w,
166
- world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
167
- ) * projector;
189
+ bitcast<f32>(light.kind_and_shadow.w),
190
+ bitcast<f32>(light.kind_and_shadow.z),
191
+ );
168
192
  }
169
193
  return evalSpot(
170
194
  light.position.xyz, light.direction.xyz, light.color.xyz,
@@ -175,7 +199,7 @@ fn evaluate_cluster_light(
175
199
  return vec3<f32>(0.0);
176
200
  }
177
201
 
178
- fn evaluate_cluster_lights(
202
+ fn evaluateStandardClusterLights(
179
203
  ndc : vec3<f32>,
180
204
  view_z : f32,
181
205
  world_pos : vec3<f32>,
@@ -185,6 +209,7 @@ fn evaluate_cluster_lights(
185
209
  metallic : f32,
186
210
  alpha_sq : f32,
187
211
  f0 : vec3<f32>,
212
+ flat_2d : bool,
188
213
  ) -> vec3<f32> {
189
214
  let gx = cluster_uniform.grid.x;
190
215
  let gy = cluster_uniform.grid.y;
@@ -194,7 +219,6 @@ fn evaluate_cluster_lights(
194
219
  let log_far = cluster_uniform.near_far_log.z;
195
220
  var total_radiance = vec3<f32>(0.0);
196
221
 
197
- #if STORAGE_BUFFER_AVAILABLE == true
198
222
  let cluster_idx = ndc_position_to_cluster(ndc, view_z, gx, gy, gz, near, far, log_far);
199
223
  let cluster_linear = cluster_idx.z * gy * gx + cluster_idx.y * gx + cluster_idx.x;
200
224
  let grid_offset = cluster_linear * 2u;
@@ -204,20 +228,9 @@ fn evaluate_cluster_lights(
204
228
  let light = light_data[light_index_list[list_offset + i]];
205
229
  total_radiance += evaluate_cluster_light(
206
230
  light, world_pos, normal, view_dir, base_color, metallic, alpha_sq, f0,
231
+ flat_2d,
207
232
  );
208
233
  }
209
- #else
210
- let light_count = min(cluster_uniform.grid.w, 128u);
211
- for (var i = 0u; i < 128u; i = i + 1u) {
212
- if (i >= light_count) {
213
- break;
214
- }
215
- total_radiance += evaluate_cluster_light(
216
- light_data_uniform[i], world_pos, normal, view_dir,
217
- base_color, metallic, alpha_sq, f0,
218
- );
219
- }
220
- #endif
221
234
  return total_radiance;
222
235
  }
223
236
 
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
  }
@@ -9,7 +9,8 @@ struct View {
9
9
  worldViewProj : mat4x4<f32>, lightDir : vec3<f32>, lightColor : vec3<f32>, cameraPos : vec3<f32>,
10
10
  lightViewProj_A : mat4x4<f32>, inverseViewProj : mat4x4<f32>, lightViewProj_B : mat4x4<f32>,
11
11
  lightViewProj_C : mat4x4<f32>, lightViewProj_D : mat4x4<f32>, splitPlanes : array<vec4<f32>,4>,
12
- cascadeCount : f32, cascadeBlend : f32, depthBias : f32, normalBias : f32, pcfKernelSize : f32,
12
+ cascadeCount : f32, cascadeBlend : f32, depthBias : f32, normalBias : f32,
13
+ directionalShadowFilter : vec4<f32>,
13
14
  spotLightViewProj : array<mat4x4<f32>,4>, temporalCurrentViewProj : mat4x4<f32>,
14
15
  temporalPreviousViewProj : mat4x4<f32>, temporalProjection : vec4<f32>, temporalPreviousCameraPos : vec4<f32>,
15
16
  };
@@ -143,9 +144,20 @@ fn volume_cascade(world_position : vec3<f32>, view_depth : f32) -> f32 {
143
144
  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
145
  let uv = tile_uv * tile_scale + vec2<f32>(tile) * tile_scale;
145
146
  let shadow_size = vec2<f32>(textureDimensions(shadowMap));
147
+ let filter_profile = clamp(u32(round(view.directionalShadowFilter.x)), 1u, 5u);
148
+ // Volumetric injection currently owns a depth-only PCF receiver. Directional
149
+ // PCSS profiles remain an explicit capability fallback here: the surface
150
+ // directional owner performs blocker search, while volume keeps the same
151
+ // accepted filter carrier and uses the stable PCF3 receiver instead of
152
+ // misreading profiles 4/5 as a kernel width.
153
+ let volume_kernel = select(
154
+ select(select(3.0, 5.0, filter_profile == 3u), 1.0, filter_profile == 1u),
155
+ 3.0,
156
+ filter_profile >= 4u,
157
+ );
146
158
  return sample_shadow_2d_kernel(
147
159
  shadowMap, shadowSampler, uv, 1.0 / shadow_size, projected.z,
148
- 0.0, view.depthBias, 1.0, view.pcfKernelSize,
160
+ 0.0, view.depthBias, 1.0, volume_kernel,
149
161
  );
150
162
  }
151
163
 
@@ -1,6 +1,7 @@
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}
4
5
 
5
6
  struct VolumeView {
6
7
  _prefix : array<vec4<f32>, 6>,
@@ -106,12 +107,6 @@ fn evalSpotAttenuation(
106
107
  return distance * smoothstep(cosOuter, cosInner, dot(direction, -normalize(lightDir)));
107
108
  }
108
109
 
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);
113
- }
114
-
115
110
  fn spotProjectorMatrix(light : SpotLight) -> mat4x4<f32> {
116
111
  if (light.shadowAtlasTile >= 0) {
117
112
  return volume_view.spotLightViewProj[light.shadowAtlasTile];