@forgeax/engine-shader 0.1.19 → 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 (51) 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 +71 -22
  13. package/src/__tests__/fog-oracle.unit.test.ts +110 -114
  14. package/src/__tests__/fog-orthographic-ray.unit.test.ts +12 -37
  15. package/src/__tests__/fog-producer-matrix.integration.test.ts +16 -51
  16. package/src/__tests__/lighting-point-spot-volume.unit.test.ts +56 -0
  17. package/src/__tests__/lighting-punctual.unit.test.ts +32 -6
  18. package/src/__tests__/lighting-spot-volume-attenuation.unit.test.ts +26 -0
  19. package/src/__tests__/lighting-spot-volume.unit.test.ts +25 -0
  20. package/src/__tests__/material-derived-builtins.integration.test.ts +27 -0
  21. package/src/__tests__/reflection-probe-sampling.unit.test.ts +23 -0
  22. package/src/__tests__/scene-temporal.unit.test.ts +20 -0
  23. package/src/__tests__/shader.unit.test.ts +35 -13
  24. package/src/__tests__/shadow-pcss.unit.test.ts +131 -0
  25. package/src/__tests__/sprite-lit-shader.test.ts +24 -11
  26. package/src/__tests__/sprite-variants.unit.test.ts +2 -0
  27. package/src/__tests__/transmission-thickness-scale.unit.test.ts +7 -1
  28. package/src/__tests__/transparent-pbr.unit.test.ts +7 -0
  29. package/src/common.wgsl +72 -73
  30. package/src/default-standard-pbr-skin.wgsl +31 -89
  31. package/src/default-standard-pbr.wgsl +94 -146
  32. package/src/fxaa.wgsl +27 -6
  33. package/src/hdrp-deferred-lighting.wgsl +3 -27
  34. package/src/ibl-sampling.wgsl +54 -0
  35. package/src/index.ts +6 -0
  36. package/src/lighting-attenuation.wgsl +58 -0
  37. package/src/lighting-directional.wgsl +188 -21
  38. package/src/lighting-punctual.wgsl +94 -19
  39. package/src/msdf-text.wgsl +4 -23
  40. package/src/scene-temporal.wgsl +14 -8
  41. package/src/shadow-pcf.wgsl +75 -11
  42. package/src/skybox.wgsl +2 -7
  43. package/src/sprite-lit.wgsl +38 -73
  44. package/src/sprite.wgsl +3 -22
  45. package/src/{hdrp-cluster-forward.wgsl → standard-cluster.wgsl} +74 -23
  46. package/src/tonemap.wgsl +8 -3
  47. package/src/unlit.wgsl +2 -24
  48. package/src/volume/volume-composite.wgsl +113 -0
  49. package/src/volume/volume-inject.wgsl +261 -0
  50. package/src/volume/volume-integrate.wgsl +337 -0
  51. package/src/volume/volume-temporal.wgsl +70 -0
package/src/common.wgsl CHANGED
@@ -36,6 +36,36 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
36
36
  return select(high, low, safe <= vec3<f32>(0.0031308));
37
37
  }
38
38
 
39
+ // Shared final-surface dither. The output transform and FXAA are both
40
+ // display-encoded fullscreen writers, so they must use one owner-level
41
+ // algorithm rather than carrying subtly different copies in each pass.
42
+ // The bounded shift is ±0.5/255 per channel, matching Three.js's optional
43
+ // material dithering amplitude while the screen-space hash remains stable for
44
+ // a still frame. Callers decide whether this writer is the final 8-bit
45
+ // boundary; intermediate float targets must leave it disabled.
46
+ fn hash32(value : u32) -> u32 {
47
+ var hash = value;
48
+ hash = (hash ^ 61u) ^ (hash >> 16u);
49
+ hash = hash + (hash << 3u);
50
+ hash = hash ^ (hash >> 4u);
51
+ hash = hash * 668265261u;
52
+ hash = hash ^ (hash >> 15u);
53
+ return hash;
54
+ }
55
+
56
+ fn ditherNoise(pixelPosition : vec2<f32>) -> f32 {
57
+ let pixel = vec2<u32>(pixelPosition);
58
+ let seed = (pixel.x * 1973u) ^ (pixel.y * 9277u) ^ 89173u;
59
+ return f32(hash32(seed) & 1023u) / 1023.0;
60
+ }
61
+
62
+ fn ditherUnorm8(value : vec3<f32>, pixelPosition : vec2<f32>) -> vec3<f32> {
63
+ let noise = ditherNoise(pixelPosition);
64
+ var shift = vec3<f32>(0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0);
65
+ shift = mix(2.0 * shift, -2.0 * shift, noise);
66
+ return clamp(value + shift, vec3<f32>(0.0), vec3<f32>(1.0));
67
+ }
68
+
39
69
  // @forgeax/engine-shader - common.wgsl (M5 T-18 feat-20260512-naga-oil-composition-hmr).
40
70
  //
41
71
  // Shared view + mesh structs extracted from pbr.wgsl + unlit.wgsl via
@@ -50,7 +80,7 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
50
80
  // aligned with Bevy's bevy_view::common and matches charter proposition 5
51
81
  // (consistent abstraction - one View struct everywhere).
52
82
 
53
- // View UBO byte layout (592 B std140):
83
+ // View UBO byte layout (960 B payload, 1024 B slot):
54
84
  // [ 0.. 64) worldViewProj mat4x4<f32> (align 16, size 64)
55
85
  // [ 64.. 80) lightDir vec3<f32> (align 16, 12+4 pad)
56
86
  // [ 80.. 96) lightColor vec3<f32> (align 16, 12+4 pad)
@@ -68,16 +98,14 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
68
98
  // [500..504) cascadeBlend f32 (align 4, size 4)
69
99
  // [504..508) depthBias f32 (align 4, size 4)
70
100
  // [508..512) normalBias f32 (align 4, size 4)
71
- // [512..516) pcfKernelSize f32 (align 4, size 4)
72
- // [516..528) _align_pad — (mat4 array align=16, 12 B)
101
+ // [512..528) directional shadow carrier f32[4] (profile, radius, penumbra, reserved)
73
102
  // [528..784) spotLightViewProj array<mat4x4<f32>,4> (align 16, size 256)
74
103
  // WGSL prefix = 784 B. Host UBO = VIEW_UBO_BYTES = 960 B (renderer-factory.ts);
75
104
  // view-ubo.ts writes the full 240 f32 payload. The spot matrix
76
105
  // array fold (feat-20260625 w25) removes the standalone @group(0) binding 9
77
106
  // uniform buffer so the WebGL2 fallback fragment uniform-buffer count returns
78
107
  // to 11 (GLES 3.0 max).
79
- // temporal projection facts and Fog payload append after the spot matrix;
80
- // total = 960 B.
108
+ // temporal projection facts append after the spot matrix; total = 960 B.
81
109
  //
82
110
  // Field order must stay byte-for-byte identical to every prior release
83
111
  // (charter P4 consistent abstraction); new fields append at the tail.
@@ -100,14 +128,15 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
100
128
  // shadow_caster.wgsl indexes the 4 fields via `shadowCasterCascade.index`
101
129
  // (binding 5, written per shadow pass).
102
130
  //
103
- // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t3:
104
- // the merged DirectionalLight's shadow bias + PCF kernel width append at the
105
- // tail (depthBias / normalBias / pcfKernelSize at bytes 504/508/512, floats
106
- // 126/127/128). Tail-append only -- field order is byte-for-byte stable
131
+ // feat-20260827-directional-csm-pcss-quality M1:
132
+ // the merged DirectionalLight's shadow bias + accepted filter carrier occupy
133
+ // the existing tail (depthBias / normalBias at bytes 504/508, carrier at
134
+ // bytes 512..528, floats 126/127/128..131). Tail reuse only -- field order is
135
+ // byte-for-byte stable
107
136
  // (charter P4); the prior 88 B host tail pad shrinks to 64 B, total stays
108
137
  // View slot remains within the existing 1024 B stride. lighting-directional.wgsl drives the
109
- // directional shadow bias (D-1: bias = max(normalBias*(1-N.L), depthBias)) and
110
- // a pcfKernelSize-wide PCF loop from these fields.
138
+ // directional shadow bias (D-1: bias = max(normalBias*(1-N.L), depthBias));
139
+ // directional shadow carrier is the receiver-facing profile/radius/penumbra POD.
111
140
  //
112
141
  // feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
113
142
  // the per-spot perspective `spotLightViewProj` matrices (4 lanes, fragment-read)
@@ -123,24 +152,11 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
123
152
  // matrix in the same frame (same-frame write contention); the FRAGMENT read
124
153
  // channel has no such contention — the host writes all <=4 spot matrices once
125
154
  // per frame before the forward pass, so the fold is safe. mat4 array align=16:
126
- // the array lands at byte 528 (next 16 B-aligned offset after pcfKernelSize at
155
+ // the array lands at byte 528 (next 16 B-aligned offset after the filter carrier at
127
156
  // byte 512..516), spanning bytes 528..784. The host allocates VIEW_UBO_BYTES =
128
157
  // 960 (renderer-factory.ts) and writes the full 240 f32
129
158
  // payload (render-system-record.ts). Lane N = the spot with `shadowAtlasTile
130
159
  // === N` (cap = 4); `evalSpotShadowed` reads `view.spotLightViewProj[tile]`.
131
- struct FogViewParams {
132
- color : vec3<f32>,
133
- density : f32,
134
- heightFalloff : f32,
135
- maxOpacity : f32,
136
- };
137
-
138
- struct FogRay {
139
- origin : vec3<f32>,
140
- direction : vec3<f32>,
141
- distance : f32,
142
- };
143
-
144
160
  struct View {
145
161
  worldViewProj : mat4x4<f32>,
146
162
  lightDir : vec3<f32>,
@@ -156,10 +172,10 @@ struct View {
156
172
  cascadeBlend : f32,
157
173
  depthBias : f32,
158
174
  normalBias : f32,
159
- pcfKernelSize : f32,
175
+ directionalShadowFilter : vec4<f32>,
160
176
  // feat-20260625-spot-light-shadow-mapping w25: per-spot perspective
161
177
  // light-view-projection matrices, fragment-read. Auto-aligned to byte 528
162
- // (mat4 array align=16, after pcfKernelSize at byte 512); spans bytes
178
+ // (mat4 array align=16, after the four directional carrier lanes at byte 512); spans bytes
163
179
  // 528..784. Lane N = spot with shadowAtlasTile === N (cap = 4). Zeroed lanes
164
180
  // are safe (sample gated on shadowAtlasTile >= 0 in default-standard-pbr.wgsl).
165
181
  spotLightViewProj : array<mat4x4<f32>, 4>,
@@ -167,7 +183,9 @@ struct View {
167
183
  temporalPreviousViewProj : mat4x4<f32>,
168
184
  // x = near, y = far, z = 1 for orthographic and 0 for perspective.
169
185
  temporalProjection : vec4<f32>,
170
- fog : FogViewParams,
186
+ // Accepted camera origin at the previous submitted frame. This occupies
187
+ // the existing tail padding and keeps temporal reprojection on one SSOT.
188
+ temporalPreviousCameraPos : vec4<f32>,
171
189
  };
172
190
 
173
191
  // Per-instance mesh slot (feat-20260518-pbr-direct-lighting-mvp M2 / w8.5,
@@ -202,7 +220,7 @@ struct Mesh {
202
220
  // color * intensity so the shader avoids the per-fragment mul)
203
221
  // [ 7 ] pad f32 = 0
204
222
  //
205
- // SpotLight (64 B / 16 floats):
223
+ // SpotLight (80 B / 20 floats):
206
224
  // [ 0..2 ] position vec3<f32>
207
225
  // [ 3 ] invRangeSquared f32
208
226
  // [ 4..6 ] colorTimesIntensity vec3<f32>
@@ -211,9 +229,11 @@ struct Mesh {
211
229
  // [ 8..10] direction vec3<f32> (raw outgoing vector; shader reads
212
230
  // via dot(L, -direction) for cone angle test)
213
231
  // [ 11 ] cosOuter f32
214
- // [12..14] spotPad vec3<f32> = 0 (std430 vec4-alignment padding)
232
+ // [12] depthBias f32, [13] normalBias f32, [14] pcfKernelSize f32
215
233
  // [ 15 ] shadowAtlasTile i32 (sentinel -1 = unassigned/clipped,
216
234
  // 0..3 = spot atlas tile index; feat-20260625 M2/M3 D-4)
235
+ // [ 16 ] shadowIntensity f32 (fraction of sampled shadow to apply)
236
+ // [17..19] alignment tail padding
217
237
  //
218
238
  // WGSL std430 alignment audit: vec3<f32> alignof=16 / sizeof=12. The
219
239
  // f32 lane wedged immediately after each vec3 fills the 4 B remainder
@@ -221,10 +241,10 @@ struct Mesh {
221
241
  // prev.size, this.alignof) - for f32 alignof=4 the round-up is a
222
242
  // no-op, so position[3] / color[3] / direction[3] sit at byte
223
243
  // offsets 12 / 28 / 44 with zero internal padding). PointLight stays
224
- // 32 B; SpotLight grows to 64 B (feat-20260625 M2/M3 D-4): the trailing
225
- // `spotPad: vec3<f32>` + `shadowAtlasTile: i32` form one vec4-aligned block
226
- // at bytes 48..64, with `shadowAtlasTile` at byte 60. These struct sizes
227
- // match the host packers (packPointLight 32 B / packSpotLight 64 B) exactly.
244
+ // 32 B; SpotLight grows from 64 B (feat-20260625 M2/M3 D-4) for the three
245
+ // receiver controls plus `shadowAtlasTile: i32`, then to 80 B for the
246
+ // parity-only `shadowIntensity` lane at byte 64. These struct sizes match the
247
+ // host packers (packPointLight 32 B / packSpotLight 80 B) exactly.
228
248
  //
229
249
  // Header `count: u32` lives in a wrapper struct with the trailing
230
250
  // runtime-sized array. Array-of-vec4-aligned-struct alignof = 16, so
@@ -255,19 +275,14 @@ struct SpotLight {
255
275
  direction : vec3<f32>,
256
276
  cosOuter : f32,
257
277
  // feat-20260625-spot-light-shadow-mapping M3 / w13 (plan-strategy D-4):
258
- // The prior 8-lane (48 B) SpotLight had no spare pad lane (unlike PointLight
259
- // which repurposed `pointPadW`), so the clip-signal field needs a fresh
260
- // vec4-aligned block. `spotPad` fills bytes 48..60 (host packSpotLight slots
261
- // 12..14 stay zero); `shadowAtlasTile` (i32 sentinel, -1 = no shadow /
262
- // clipped, 0..3 = spot atlas tile) lands at byte 60 (host slot 15, written
263
- // through an Int32Array view). The fragment path gates on
264
- // `shadowAtlasTile >= 0` (evalSpotShadowed vs evalSpot). Field names are
265
- // unique across the composed module surface so naga_oil writeback
266
- // substitution does not collide with prior `pad0` members. (naga_oil
267
- // reserves the `__` identifier prefix, so the pad lane is `spotPad`, not
268
- // `__pad`.)
269
- spotPad : vec3<f32>,
278
+ // The trailing vec4 transports receiver controls and the tile identity.
279
+ // Host packSpotLight writes slots 12..14 from the same snapshot; slot 15 is
280
+ // the i32 shadow tile sentinel and slot 16 carries shadowIntensity.
281
+ depthBias : f32,
282
+ normalBias : f32,
283
+ pcfKernelSize : f32,
270
284
  shadowAtlasTile : i32,
285
+ shadowIntensity : f32,
271
286
  };
272
287
 
273
288
  struct PointLightsArray {
@@ -282,32 +297,6 @@ struct SpotLightsArray {
282
297
 
283
298
  @group(0) @binding(0) var<uniform> view : View;
284
299
 
285
- // Shared screen-space ray reconstruction for scene Fog producers. Keeping the
286
- // inverse-view and projection branch here prevents each producer from growing
287
- // a second Fog formula or drifting from the View ABI.
288
- fn fogWorldPoint(ndc : vec3<f32>) -> vec3<f32> {
289
- let homogeneous = view.inverseViewProj * vec4<f32>(ndc, 1.0);
290
- let divisor = select(1.0, homogeneous.w, abs(homogeneous.w) > 0.000001);
291
- return homogeneous.xyz / divisor;
292
- }
293
-
294
- fn fogRayFromNdc(ndc : vec3<f32>) -> FogRay {
295
- let worldPosition = fogWorldPoint(ndc);
296
- let nearPosition = fogWorldPoint(vec3<f32>(ndc.xy, 0.0));
297
- let farPosition = fogWorldPoint(vec3<f32>(ndc.xy, 1.0));
298
- let perspective = view.temporalProjection.z < 0.5;
299
- let perspectiveVector = worldPosition - view.cameraPos;
300
- let orthographicVector = farPosition - nearPosition;
301
- let direction = normalize(select(orthographicVector, perspectiveVector, perspective));
302
- let origin = select(nearPosition, view.cameraPos, perspective);
303
- let rayDistance = select(
304
- max(dot(worldPosition - nearPosition, direction), 0.0),
305
- length(perspectiveVector),
306
- perspective,
307
- );
308
- return FogRay(origin, direction, rayDistance);
309
- }
310
-
311
300
  #if STORAGE_BUFFER_AVAILABLE == true
312
301
  @group(0) @binding(1) var<storage, read> pointLightsBuffer : PointLightsArray;
313
302
  @group(0) @binding(2) var<storage, read> spotLightsBuffer : SpotLightsArray;
@@ -421,7 +410,17 @@ struct ShadowCasterCascade {
421
410
  // — folded there in feat-20260625 w25 (scope-amend) to keep the WebGL2 fallback
422
411
  // fragment uniform-buffer count <= 11; binding 8 is the last view-BG binding.
423
412
  @group(0) @binding(8) var spotShadowMap : texture_depth_2d;
424
-
413
+ // Optional authored SpotLight projector. Surface and volume bind the same
414
+ // accepted TextureAsset view and linear-clamp sampler; an unprojected spot
415
+ // receives the renderer-owned white fallback. The declaration is capability
416
+ // gated because the complete HDRP PBR view/material ABI reaches the WebGPU
417
+ // minimum of 16 sampled textures without this optional cookie. Devices that
418
+ // cannot expose the 17th sampled texture select the matching white-projector
419
+ // shader variant and omit these resources from the view BGL.
420
+ #ifdef PROJECTOR_AVAILABLE
421
+ @group(0) @binding(11) var projectorTexture : texture_2d<f32>;
422
+ @group(0) @binding(12) var projectorSampler : sampler;
423
+ #endif
425
424
  #if STORAGE_BUFFER_AVAILABLE == true
426
425
  @group(2) @binding(0) var<storage, read> meshes : array<Mesh>;
427
426
  #else
@@ -1,19 +1,19 @@
1
- #import forgeax_view::common::{View, FogViewParams, FogRay, Mesh, InstanceData, view, meshes, instances, PointLight, SpotLight, pointLightsBuffer, spotLightsBuffer, shadowMap, shadowSampler, sampleMaterialTexture}
1
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, sampleMaterialTexture}
2
+ #import forgeax_scene_temporal::{sceneViewZ}
2
3
  #import forgeax_pbr::temporal::{projectPbrSceneTemporal}
3
4
  #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
4
5
  #import forgeax_pbr::ibl_sampling::{sampleIblDiffuse, sampleIblSpecular}
5
6
  #import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
6
7
  #import forgeax_pbr::lighting_directional::{evalDirectionalNoShadow, evalDirectionalShadowFactor}
7
- #import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot}
8
- #import forgeax_view::fog::{apply_fog}
9
- #ifdef POINT_SHADOW_AVAILABLE
10
- #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
11
- #import forgeax_view::common::{shadowParams}
8
+ #ifdef CLUSTER_FORWARD_AVAILABLE
9
+ #import forgeax_standard::cluster::{evaluateStandardClusterLights}
12
10
  #endif
13
11
 
14
12
  #define_import_path forgeax_material::pbr-skin
15
13
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
14
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
16
15
  #pragma variant_axis VERTEX_COLOR_AVAILABLE
16
+ #pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
17
17
 
18
18
  // @forgeax/engine-shader - default-standard-pbr-skin.wgsl
19
19
  // (feat-20260523-skin-skeleton-animation M3 / T-29).
@@ -205,7 +205,6 @@ struct VsOut {
205
205
  @location(1) worldNormal : vec3<f32>,
206
206
  @location(2) uv : vec2<f32>,
207
207
  @location(3) worldTangent : vec4<f32>,
208
- @location(4) @interpolate(flat) instanceIdx : u32,
209
208
  // feat-city-glb multi-UV tiling: second UV set varying at location 5
210
209
  // (parity with default-standard-pbr.wgsl).
211
210
  @location(5) uv1 : vec2<f32>,
@@ -218,6 +217,9 @@ struct VsOut {
218
217
  #ifdef VERTEX_COLOR_AVAILABLE
219
218
  @location(14) color : vec4<f32>,
220
219
  #endif
220
+ // Vertex-produced Standard cluster coordinates. Fragment builtin position
221
+ // is framebuffer-space and cannot recover the original clip-space NDC.
222
+ @location(6) ndc : vec3<f32>,
221
223
  @location(7) viewZ : f32,
222
224
  };
223
225
 
@@ -230,22 +232,6 @@ fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
230
232
  }
231
233
  }
232
234
 
233
- fn applySceneFog(viewParams : View, color : vec3<f32>, alpha : f32, worldPos : vec3<f32>) -> vec4<f32> {
234
- var origin = viewParams.cameraPos;
235
- var direction = normalize(worldPos - origin);
236
- var rayDistance = length(worldPos - origin);
237
- if (viewParams.temporalProjection.z >= 0.5) {
238
- let nearH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 0.0, 1.0);
239
- let farH = viewParams.inverseViewProj * vec4<f32>(0.0, 0.0, 1.0, 1.0);
240
- let nearPoint = nearH.xyz / nearH.w;
241
- let farPoint = farH.xyz / farH.w;
242
- direction = normalize(farPoint - nearPoint);
243
- origin = worldPos - direction * dot(worldPos - viewParams.cameraPos, direction);
244
- rayDistance = max(dot(worldPos - origin, direction), 0.0);
245
- }
246
- return apply_fog(viewParams.fog, FogRay(origin, direction, rayDistance), vec4<f32>(color, alpha));
247
- }
248
-
249
235
  @vertex
250
236
  fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
251
237
  // 4-bone weighted skinning (plan-strategy D-3 / D-3a).
@@ -315,11 +301,14 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
315
301
  #ifdef VERTEX_COLOR_AVAILABLE
316
302
  out.color = in.color;
317
303
  #endif
318
- out.instanceIdx = idx;
319
304
  // feat-20260613-csm-cascaded-shadow-maps M5 / w19: viewZ replaces the
320
305
  // prior light-space-position varying; evalDirectional picks the cascade
321
306
  // matrix per fragment from viewZ + worldPos.
322
- out.viewZ = -out.clip.w;
307
+ let clipPos = out.clip;
308
+ out.ndc = vec3(clipPos.xy / clipPos.w, clipPos.z / clipPos.w);
309
+ // Keep the cluster depth exactly aligned with the CPU binner for both
310
+ // perspective and off-axis orthographic projections.
311
+ out.viewZ = sceneViewZ(out.clip, view.temporalProjection);
323
312
  return out;
324
313
  }
325
314
 
@@ -414,66 +403,18 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
414
403
  );
415
404
  color = color + directionalShadow * material.clearcoat * directionalClearcoat;
416
405
  }
417
- let pointCount = pointLightsBuffer.count;
418
- for (var i: u32 = 0u; i < pointCount; i = i + 1u) {
419
- let p = pointLightsBuffer.slots[i];
420
- #ifdef POINT_SHADOW_AVAILABLE
421
- if (p.shadowAtlasLayer >= 0) {
422
- let lane = shadowParams[p.shadowAtlasLayer];
423
- color = color + evalPointShadowed(
424
- p.position, p.colorTimesIntensity, p.invRangeSquared,
425
- in.worldPos, n, v, albedo, metallic, a, f0,
426
- p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
427
- );
428
- if (material.clearcoat != 0.0) {
429
- color = color + material.clearcoat * evalPointShadowed(
430
- p.position, p.colorTimesIntensity, p.invRangeSquared,
431
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
432
- p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
433
- );
434
- }
435
- } else {
436
- color = color + evalPoint(
437
- p.position, p.colorTimesIntensity, p.invRangeSquared,
438
- in.worldPos, n, v, albedo, metallic, a, f0,
439
- );
440
- if (material.clearcoat != 0.0) {
441
- color = color + material.clearcoat * evalPoint(
442
- p.position, p.colorTimesIntensity, p.invRangeSquared,
443
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
444
- );
445
- }
446
- }
447
- #else
448
- color = color + evalPoint(
449
- p.position, p.colorTimesIntensity, p.invRangeSquared,
450
- in.worldPos, n, v, albedo, metallic, a, f0,
451
- );
452
- if (material.clearcoat != 0.0) {
453
- color = color + material.clearcoat * evalPoint(
454
- p.position, p.colorTimesIntensity, p.invRangeSquared,
455
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
456
- );
457
- }
458
- #endif
459
- }
460
- let spotCount = spotLightsBuffer.count;
461
- for (var i: u32 = 0u; i < spotCount; i = i + 1u) {
462
- let s = spotLightsBuffer.slots[i];
463
- color = color + evalSpot(
464
- s.position, s.direction, s.colorTimesIntensity,
465
- s.cosInner, s.cosOuter, s.invRangeSquared,
466
- in.worldPos, n, v, albedo, metallic, a, f0,
406
+ #ifdef CLUSTER_FORWARD_AVAILABLE
407
+ color = color + evaluateStandardClusterLights(
408
+ in.ndc, in.viewZ, in.worldPos, n, v, albedo, metallic, a, f0, false,
409
+ );
410
+ if (material.clearcoat != 0.0) {
411
+ color = color + material.clearcoat * evaluateStandardClusterLights(
412
+ in.ndc, in.viewZ, in.worldPos, n, v,
413
+ vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04), false,
467
414
  );
468
- if (material.clearcoat != 0.0) {
469
- color = color + material.clearcoat * evalSpot(
470
- s.position, s.direction, s.colorTimesIntensity,
471
- s.cosInner, s.cosOuter, s.invRangeSquared,
472
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
473
- );
474
- }
475
415
  }
476
- return applySceneFog(view, color, alpha, in.worldPos);
416
+ #endif
417
+ return vec4<f32>(color, alpha);
477
418
  }
478
419
 
479
420
  struct TemporalVsOut {
@@ -488,6 +429,7 @@ struct TemporalVsOut {
488
429
  @location(7) uv7 : vec2<f32>,
489
430
  @location(8) @interpolate(perspective) currentClip : vec4<f32>,
490
431
  @location(9) @interpolate(perspective) previousClip : vec4<f32>,
432
+ @location(10) @interpolate(flat) reactive : f32,
491
433
  #ifdef VERTEX_COLOR_AVAILABLE
492
434
  @location(14) color : vec4<f32>,
493
435
  #endif
@@ -509,6 +451,11 @@ fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
509
451
  out.currentClip = view.temporalCurrentViewProj * currentWorld;
510
452
  out.clip = out.currentClip;
511
453
  out.previousClip = view.temporalPreviousViewProj * previousWorld;
454
+ #if STORAGE_BUFFER_AVAILABLE == true
455
+ out.reactive = meshes[0].temporal.x;
456
+ #else
457
+ out.reactive = 1.0;
458
+ #endif
512
459
  out.uv = in.uv;
513
460
  out.uv1 = in.uv1;
514
461
  out.uv2 = in.uv2;
@@ -534,11 +481,6 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
534
481
 
535
482
  @fragment
536
483
  fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
537
- #if STORAGE_BUFFER_AVAILABLE == true
538
- let reactive = meshes[0].temporal.x;
539
- #else
540
- let reactive = 1.0;
541
- #endif
542
484
  return projectPbrSceneTemporal(
543
485
  material.baseColor.a * temporalVertexAlpha(in),
544
486
  material.alphaCutoff,
@@ -549,7 +491,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
549
491
  in.currentClip,
550
492
  in.previousClip,
551
493
  view.temporalProjection,
552
- reactive,
494
+ in.reactive,
553
495
  in.uv, in.uv1, in.uv2, in.uv3,
554
496
  in.uv4, in.uv5, in.uv6, in.uv7,
555
497
  );