@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
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,8 +98,7 @@ 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
@@ -99,30 +128,28 @@ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
99
128
  // shadow_caster.wgsl indexes the 4 fields via `shadowCasterCascade.index`
100
129
  // (binding 5, written per shadow pass).
101
130
  //
102
- // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t3:
103
- // the merged DirectionalLight's shadow bias + PCF kernel width append at the
104
- // tail (depthBias / normalBias / pcfKernelSize at bytes 504/508/512, floats
105
- // 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
106
136
  // (charter P4); the prior 88 B host tail pad shrinks to 64 B, total stays
107
137
  // View slot remains within the existing 1024 B stride. lighting-directional.wgsl drives the
108
- // directional shadow bias (D-1: bias = max(normalBias*(1-N.L), depthBias)) and
109
- // 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.
110
140
  //
111
141
  // feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
112
142
  // the per-spot perspective `spotLightViewProj` matrices (4 lanes, fragment-read)
113
143
  // fold into the View UBO tail instead of a standalone @group(0) binding 9
114
144
  // uniform buffer. The standalone binding pushed the fragment-stage uniform
115
- // buffer count to 12 (view + pointLights + spotLights + shadowParams +
116
- // shadowCasterCascade + spotLightViewProj on the WebGL2 fallback path),
117
- // exceeding GLES 3.0's `max_uniform_buffers_per_shader_stage = 11` so the
118
- // WebGL2 fallback (mobile / WebKit this feat's compat target) crashed
119
- // pipeline-layout creation. Folding into the View UBO costs zero new bindings
120
- // (the count drops back to 11). D-1 rejected the View UBO for the CASTER vertex
121
- // channel (binding 7) because directional + spot casters both need a light-view
145
+ // buffer count to 12 before the local-light arrays were removed. The current
146
+ // view declares no point/spot light arrays; the spot matrices remain folded
147
+ // into the View UBO with no standalone binding. D-1 rejected the View UBO for
148
+ // the CASTER vertex channel (binding 7) because directional + spot casters both need a light-view
122
149
  // matrix in the same frame (same-frame write contention); the FRAGMENT read
123
150
  // channel has no such contention — the host writes all <=4 spot matrices once
124
151
  // per frame before the forward pass, so the fold is safe. mat4 array align=16:
125
- // the array lands at byte 528 (next 16 B-aligned offset after pcfKernelSize at
152
+ // the array lands at byte 528 (next 16 B-aligned offset after the filter carrier at
126
153
  // byte 512..516), spanning bytes 528..784. The host allocates VIEW_UBO_BYTES =
127
154
  // 960 (renderer-factory.ts) and writes the full 240 f32
128
155
  // payload (render-system-record.ts). Lane N = the spot with `shadowAtlasTile
@@ -142,10 +169,10 @@ struct View {
142
169
  cascadeBlend : f32,
143
170
  depthBias : f32,
144
171
  normalBias : f32,
145
- pcfKernelSize : f32,
172
+ directionalShadowFilter : vec4<f32>,
146
173
  // feat-20260625-spot-light-shadow-mapping w25: per-spot perspective
147
174
  // light-view-projection matrices, fragment-read. Auto-aligned to byte 528
148
- // (mat4 array align=16, after pcfKernelSize at byte 512); spans bytes
175
+ // (mat4 array align=16, after the four directional carrier lanes at byte 512); spans bytes
149
176
  // 528..784. Lane N = spot with shadowAtlasTile === N (cap = 4). Zeroed lanes
150
177
  // are safe (sample gated on shadowAtlasTile >= 0 in default-standard-pbr.wgsl).
151
178
  spotLightViewProj : array<mat4x4<f32>, 4>,
@@ -177,12 +204,22 @@ struct Mesh {
177
204
  #endif
178
205
  };
179
206
 
207
+ // Retained CPU ProbeBlendRecord ABI: 4 scalar lanes plus 9 vec4 lanes = 160 B.
208
+ // The last lane carries padding for the 27 f32 SH coefficients. Sky residual
209
+ // is derived in lighting-probe.wgsl, never stored as a second fraction.
210
+ struct ProbeBlendRecord {
211
+ objectKey : f32,
212
+ generation : f32,
213
+ localBlendFraction : f32,
214
+ flags : f32,
215
+ shPreblend : array<vec4<f32>, 9>,
216
+ };
217
+
180
218
  // feat-20260519-light-casters-point-spot-pbr M4 / w21 (D-S1 + D-S2 +
181
219
  // AC-04 b/c + AC-05 binding declaration). Punctual-light std430 storage
182
- // types byte-for-byte mirror packPointLight / packSpotLight in
183
- // `packages/runtime/src/light-buffer-layout.ts`:
220
+ // types use the shared five-row host ABI in `light-buffer-layout.ts`:
184
221
  //
185
- // PointLight (32 B / 8 floats):
222
+ // Point/Spot/Rect direct slot (80 B / 20 u32-or-float lanes):
186
223
  // [ 0..2 ] position vec3<f32>
187
224
  // [ 3 ] invRangeSquared f32 (Bevy color_inverse_square_range.w; 0
188
225
  // collapses range falloff to a pure 1/d^2 inverse-square law)
@@ -190,7 +227,7 @@ struct Mesh {
190
227
  // color * intensity so the shader avoids the per-fragment mul)
191
228
  // [ 7 ] pad f32 = 0
192
229
  //
193
- // SpotLight (80 B / 20 floats):
230
+ // The metadata row carries kind, shadow, IES, and Cookie identities.
194
231
  // [ 0..2 ] position vec3<f32>
195
232
  // [ 3 ] invRangeSquared f32
196
233
  // [ 4..6 ] colorTimesIntensity vec3<f32>
@@ -199,81 +236,31 @@ struct Mesh {
199
236
  // [ 8..10] direction vec3<f32> (raw outgoing vector; shader reads
200
237
  // via dot(L, -direction) for cone angle test)
201
238
  // [ 11 ] cosOuter f32
202
- // [12] depthBias f32, [13] normalBias f32, [14] pcfKernelSize f32
203
- // [ 15 ] shadowAtlasTile i32 (sentinel -1 = unassigned/clipped,
204
- // 0..3 = spot atlas tile index; feat-20260625 M2/M3 D-4)
205
- // [ 16 ] shadowIntensity f32 (fraction of sampled shadow to apply)
206
- // [17..19] alignment tail padding
239
+ // Spot row 3: [12] depthBias, [13] normalBias, [14] shadowIntensity,
240
+ // [15] rollDeg. The unified slot has no separate pcf lane; the current
241
+ // surface and volume receivers use the stable PCF3 profile.
242
+ // Rect row 1/2/3: halfWidth, axisY + halfHeight, axisX + zero pad. The
243
+ // final metadata row carries the kind,
244
+ // shadow, IES, and Cookie identities.
207
245
  //
208
246
  // WGSL std430 alignment audit: vec3<f32> alignof=16 / sizeof=12. The
209
247
  // f32 lane wedged immediately after each vec3 fills the 4 B remainder
210
248
  // (WGSL struct member rule: next.offset = roundUp(prev.offset +
211
249
  // prev.size, this.alignof) - for f32 alignof=4 the round-up is a
212
250
  // no-op, so position[3] / color[3] / direction[3] sit at byte
213
- // offsets 12 / 28 / 44 with zero internal padding). PointLight stays
214
- // 32 B; SpotLight grows from 64 B (feat-20260625 M2/M3 D-4) for the three
215
- // receiver controls plus `shadowAtlasTile: i32`, then to 80 B for the
216
- // parity-only `shadowIntensity` lane at byte 64. These struct sizes match the
217
- // host packers (packPointLight 32 B / packSpotLight 80 B) exactly.
251
+ // offsets 12 / 28 / 44 with zero internal padding). Point, Spot, and Rect all
252
+ // use the unified 80 B host packer; this shared slot is the sole local-light
253
+ // carrier.
218
254
  //
219
- // Header `count: u32` lives in a wrapper struct with the trailing
220
- // runtime-sized array. Array-of-vec4-aligned-struct alignof = 16, so
221
- // `slots` starts at byte 16 (count u32 occupies 0..4; bytes 4..16 are
222
- // implicit padding). This matches `packLightArrayHeader`'s 16 B
223
- // (count u32 + 12 B pad) layout in light-buffer-layout.ts.
224
-
225
- struct PointLight {
226
- position : vec3<f32>,
227
- invRangeSquared : f32,
228
- colorTimesIntensity : vec3<f32>,
229
- // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-8 (plan-strategy §D-2):
230
- // Repurposes the prior `pointPadW` (f32 zero-pad) lane as `shadowAtlasLayer`
231
- // (i32 sentinel, -1 = no shadow, 0..3 = cube_array atlas layer index for
232
- // shadow-casting point lights). Cap = 4 enforced by the PointLightShadow
233
- // ECS component cardinality. The shader path samples the cube_array atlas
234
- // (binding 5) only when `shadowAtlasLayer >= 0`. Field name is unique to
235
- // this struct (no `pad0`/`pad1` collision with the pbr Material struct
236
- // under naga_oil writeback substitution).
237
- shadowAtlasLayer : i32,
238
- };
239
-
240
- struct SpotLight {
241
- position : vec3<f32>,
242
- invRangeSquared : f32,
243
- colorTimesIntensity : vec3<f32>,
244
- cosInner : f32,
245
- direction : vec3<f32>,
246
- cosOuter : f32,
247
- // feat-20260625-spot-light-shadow-mapping M3 / w13 (plan-strategy D-4):
248
- // The trailing vec4 transports receiver controls and the tile identity.
249
- // Host packSpotLight writes slots 12..14 from the same snapshot; slot 15 is
250
- // the i32 shadow tile sentinel and slot 16 carries shadowIntensity.
251
- depthBias : f32,
252
- normalBias : f32,
253
- pcfKernelSize : f32,
254
- shadowAtlasTile : i32,
255
- shadowIntensity : f32,
256
- };
257
-
258
- struct PointLightsArray {
259
- count : u32,
260
- slots : array<PointLight, 4>,
261
- };
262
-
263
- struct SpotLightsArray {
264
- count : u32,
265
- slots : array<SpotLight, 4>,
255
+ struct DirectLightSlot {
256
+ position : vec4<f32>, // row0: position + inverse range
257
+ colorTimesIntensity : vec4<f32>, // row1: color + first angular fact
258
+ direction : vec4<f32>, // row2: primary axis + second angular fact
259
+ auxiliary : vec4<f32>, // row3: auxiliary Rect axis
260
+ metadata : vec4<u32>,
266
261
  };
267
262
 
268
263
  @group(0) @binding(0) var<uniform> view : View;
269
-
270
- #if STORAGE_BUFFER_AVAILABLE == true
271
- @group(0) @binding(1) var<storage, read> pointLightsBuffer : PointLightsArray;
272
- @group(0) @binding(2) var<storage, read> spotLightsBuffer : SpotLightsArray;
273
- #else
274
- @group(0) @binding(1) var<uniform> pointLightsBuffer : PointLightsArray;
275
- @group(0) @binding(2) var<uniform> spotLightsBuffer : SpotLightsArray;
276
- #endif
277
264
  // feat-20260520-directional-light-shadow-mapping M3 / w16 (D-1 / plan-strategy §8.1):
278
265
  // shadowMap + shadowSampler consume @group(0) bindings 3/4 (smallest unused
279
266
  // slots adjacent to view UBO at 0). M3 uses 3x3 PCF with textureLoad on
@@ -284,7 +271,7 @@ struct SpotLightsArray {
284
271
  // layers=4, depth32float; one cube per shadow-casting point light, cap=4).
285
272
  // Binding 6 declares the per-light shadow params buffer (URP only — proj
286
273
  // constants for cube depth-ref reconstruction); HDRP rides the same constants
287
- // on LightSlot pad lanes (.kind_and_pad.wyz) so binding 6 stays unbound on
274
+ // on direct-light metadata so binding 6 stays unbound on
288
275
  // the HDRP path. Sample-time gating by PointLight.shadowAtlasLayer >= 0.
289
276
  @group(0) @binding(3) var shadowMap : texture_depth_2d;
290
277
  @group(0) @binding(4) var shadowSampler : sampler_comparison;
@@ -307,7 +294,7 @@ struct SpotLightsArray {
307
294
  // depth-ref reconstruction (research L0.5 + L1.13). Each lane stores the
308
295
  // shader-side reconstruction constants for one shadow-casting point light;
309
296
  // slot N matches `PointLight.shadowAtlasLayer = N`. HDRP rides the same
310
- // constants on `LightSlot.kind_and_pad.wyz` so binding 6 stays unbound on
297
+ // constants on direct-light metadata so binding 6 stays unbound on
311
298
  // the HDRP path.
312
299
  #ifdef POINT_SHADOW_AVAILABLE
313
300
  @group(0) @binding(5) var shadowAtlas : texture_depth_cube_array;
@@ -380,6 +367,23 @@ struct ShadowCasterCascade {
380
367
  // — folded there in feat-20260625 w25 (scope-amend) to keep the WebGL2 fallback
381
368
  // fragment uniform-buffer count <= 11; binding 8 is the last view-BG binding.
382
369
  @group(0) @binding(8) var spotShadowMap : texture_depth_2d;
370
+ // Extended-lighting resources are a real optional view topology. The
371
+ // renderer selects the matching material variant and BGL together; keeping
372
+ // these declarations behind the same axis prevents a no-extension variant
373
+ // from consuming texture budget or requiring absent bindings.
374
+ #ifdef EXTENDED_LIGHTING_AVAILABLE
375
+ @group(0) @binding(9) var spotModifierSampler : sampler;
376
+ @group(0) @binding(11) var iesProfileTexture : texture_2d_array<f32>;
377
+ @group(0) @binding(12) var cookieTexture : texture_2d_array<f32>;
378
+ @group(0) @binding(13) var ltcLambertTexture : texture_2d<f32>;
379
+ @group(0) @binding(14) var ltcGgxTexture : texture_2d<f32>;
380
+ // One aspect-correction matrix per Cookie array slice. The matrix is kept in
381
+ // the shared view group so the same projected Cookie semantics are available
382
+ // to both the ordinary and clustered Standard PBR consumers.
383
+ @group(0) @binding(15) var<uniform> cookieMatrices : array<mat4x4<f32>, 32>;
384
+ #endif
385
+ #ifdef PROJECTOR_AVAILABLE
386
+ #ifndef EXTENDED_LIGHTING_AVAILABLE
383
387
  // Optional authored SpotLight projector. Surface and volume bind the same
384
388
  // accepted TextureAsset view and linear-clamp sampler; an unprojected spot
385
389
  // receives the renderer-owned white fallback. The declaration is capability
@@ -387,10 +391,10 @@ struct ShadowCasterCascade {
387
391
  // minimum of 16 sampled textures without this optional cookie. Devices that
388
392
  // cannot expose the 17th sampled texture select the matching white-projector
389
393
  // shader variant and omit these resources from the view BGL.
390
- #ifdef PROJECTOR_AVAILABLE
391
394
  @group(0) @binding(11) var projectorTexture : texture_2d<f32>;
392
395
  @group(0) @binding(12) var projectorSampler : sampler;
393
396
  #endif
397
+ #endif
394
398
  #if STORAGE_BUFFER_AVAILABLE == true
395
399
  @group(2) @binding(0) var<storage, read> meshes : array<Mesh>;
396
400
  #else
@@ -1,18 +1,23 @@
1
- #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, PointLight, SpotLight, pointLightsBuffer, spotLightsBuffer, shadowMap, shadowSampler, sampleMaterialTexture}
1
+ #import forgeax_view::common::{View, Mesh, InstanceData, view, meshes, instances, shadowMap, shadowSampler, 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}
6
+ #import forgeax_pbr::lighting_probe::{evaluateProbeDiffuse}
5
7
  #import forgeax_pbr::tbn::{decodeTangentSpaceNormalRg, scaleTangentSpaceNormal, applyTBN}
6
8
  #import forgeax_pbr::lighting_directional::{evalDirectionalNoShadow, evalDirectionalShadowFactor}
7
- #import forgeax_pbr::lighting_punctual::{evalPoint, evalSpot}
8
- #ifdef POINT_SHADOW_AVAILABLE
9
- #import forgeax_pbr::lighting_punctual::{evalPointShadowed}
10
- #import forgeax_view::common::{shadowParams}
9
+ #ifdef CLUSTER_FORWARD_AVAILABLE
10
+ #import forgeax_standard::cluster::{evaluateStandardClusterLights}
11
11
  #endif
12
12
 
13
13
  #define_import_path forgeax_material::pbr-skin
14
14
  #pragma variant_axis STORAGE_BUFFER_AVAILABLE
15
+ #pragma variant_axis CLUSTER_FORWARD_AVAILABLE
15
16
  #pragma variant_axis VERTEX_COLOR_AVAILABLE
17
+ #pragma variant_axis PROBE_BLEND_AVAILABLE
18
+ #pragma variant_axis EXTENDED_LIGHTING_AVAILABLE
19
+
20
+ #pragma variant_axis DIRECTIONAL_PCSS_AVAILABLE
16
21
 
17
22
  // @forgeax/engine-shader - default-standard-pbr-skin.wgsl
18
23
  // (feat-20260523-skin-skeleton-animation M3 / T-29).
@@ -170,13 +175,35 @@ struct SkylightUniforms {
170
175
  @group(1) @binding(22) var brdfLutSampler : sampler;
171
176
  @group(1) @binding(23) var<uniform> skylight : SkylightUniforms;
172
177
 
173
- #if STORAGE_BUFFER_AVAILABLE == true
174
- @group(2) @binding(1) var<storage, read> palette : array<mat4x4<f32>>;
175
- @group(2) @binding(2) var<storage, read> previousPalette : array<mat4x4<f32>>;
178
+ #ifdef PROBE_BLEND_AVAILABLE
179
+ // Optional consumer lane. The host must provide the retained 160B record
180
+ // buffer and matching BGL before enabling this capability.
181
+ @group(3) @binding(1) var<storage, read> probeBlendRecords : array<vec4<f32>>;
182
+ #endif
183
+
184
+ #ifdef STORAGE_BUFFER_AVAILABLE
185
+ @group(2) @binding(1) var<storage, read> paletteStorage : array<mat4x4<f32>>;
186
+ @group(2) @binding(2) var<storage, read> previousPaletteStorage : array<mat4x4<f32>>;
187
+ #else
188
+ @group(2) @binding(1) var<uniform> paletteUniform : array<mat4x4<f32>, 255>;
189
+ @group(2) @binding(2) var<uniform> previousPaletteUniform : array<mat4x4<f32>, 255>;
190
+ #endif
191
+
192
+ fn skinPalette(index : u32) -> mat4x4<f32> {
193
+ #ifdef STORAGE_BUFFER_AVAILABLE
194
+ return paletteStorage[index];
176
195
  #else
177
- @group(2) @binding(1) var<uniform> palette : array<mat4x4<f32>, 255>;
178
- @group(2) @binding(2) var<uniform> previousPalette : array<mat4x4<f32>, 255>;
196
+ return paletteUniform[index];
179
197
  #endif
198
+ }
199
+
200
+ fn previousSkinPalette(index : u32) -> mat4x4<f32> {
201
+ #ifdef STORAGE_BUFFER_AVAILABLE
202
+ return previousPaletteStorage[index];
203
+ #else
204
+ return previousPaletteUniform[index];
205
+ #endif
206
+ }
180
207
 
181
208
  struct VsIn {
182
209
  @location(0) pos : vec3<f32>,
@@ -204,7 +231,6 @@ struct VsOut {
204
231
  @location(1) worldNormal : vec3<f32>,
205
232
  @location(2) uv : vec2<f32>,
206
233
  @location(3) worldTangent : vec4<f32>,
207
- @location(4) @interpolate(flat) instanceIdx : u32,
208
234
  // feat-city-glb multi-UV tiling: second UV set varying at location 5
209
235
  // (parity with default-standard-pbr.wgsl).
210
236
  @location(5) uv1 : vec2<f32>,
@@ -217,6 +243,9 @@ struct VsOut {
217
243
  #ifdef VERTEX_COLOR_AVAILABLE
218
244
  @location(14) color : vec4<f32>,
219
245
  #endif
246
+ // Vertex-produced Standard cluster coordinates. Fragment builtin position
247
+ // is framebuffer-space and cannot recover the original clip-space NDC.
248
+ @location(6) ndc : vec3<f32>,
220
249
  @location(7) viewZ : f32,
221
250
  };
222
251
 
@@ -229,8 +258,7 @@ fn pick_channel(rgba : vec4<f32>, channelIndex : u32) -> f32 {
229
258
  }
230
259
  }
231
260
 
232
- @vertex
233
- fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
261
+ fn vs_main_impl(in : VsIn, idx : u32) -> VsOut {
234
262
  // 4-bone weighted skinning (plan-strategy D-3 / D-3a).
235
263
  // The host pre-computes each joint matrix as worldFromJoint * inverseBindMatrix
236
264
  // (CPU-side pre-multiplication, per D-4) and writes them into the palette
@@ -242,10 +270,10 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
242
270
  // The host sets the BindGroup dynamic offset to (byteOffset / 64) so the
243
271
  // first palette entry visible to this draw is palette[0]. The shader
244
272
  // indexes into palette directly using the per-vertex skinIndex values.
245
- let skinMatrix = palette[in.skinIndex.x] * in.skinWeight.x +
246
- palette[in.skinIndex.y] * in.skinWeight.y +
247
- palette[in.skinIndex.z] * in.skinWeight.z +
248
- palette[in.skinIndex.w] * in.skinWeight.w;
273
+ let skinMatrix = skinPalette(in.skinIndex.x) * in.skinWeight.x +
274
+ skinPalette(in.skinIndex.y) * in.skinWeight.y +
275
+ skinPalette(in.skinIndex.z) * in.skinWeight.z +
276
+ skinPalette(in.skinIndex.w) * in.skinWeight.w;
249
277
 
250
278
  // glTF 2.0 sec.Skins Implementation Note: when a mesh node has a skin
251
279
  // property, the joint matrices already encode the global transform of each
@@ -257,7 +285,7 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
257
285
  // skinnedLocal IS the world position -- no additional left-multiply by
258
286
  // meshes[0].worldFromLocal or instanceLocal is needed.
259
287
  //
260
- // This removes the implicit contract "Skin entity Transform.world must be
288
+ // This removes the implicit contract "Skin entity GlobalTransform.world must be
261
289
  // identity" -- a skin entity can be parented under any Transform chain and
262
290
  // the skinned mesh will rigidly follow via joint propagation alone.
263
291
  let skinnedLocal = skinMatrix * vec4<f32>(in.pos, 1.0);
@@ -298,14 +326,27 @@ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
298
326
  #ifdef VERTEX_COLOR_AVAILABLE
299
327
  out.color = in.color;
300
328
  #endif
301
- out.instanceIdx = idx;
302
329
  // feat-20260613-csm-cascaded-shadow-maps M5 / w19: viewZ replaces the
303
330
  // prior light-space-position varying; evalDirectional picks the cascade
304
331
  // matrix per fragment from viewZ + worldPos.
305
- out.viewZ = -out.clip.w;
332
+ let clipPos = out.clip;
333
+ out.ndc = vec3(clipPos.xy / clipPos.w, clipPos.z / clipPos.w);
334
+ // Keep the cluster depth exactly aligned with the CPU binner for both
335
+ // perspective and off-axis orthographic projections.
336
+ out.viewZ = sceneViewZ(out.clip, view.temporalProjection);
306
337
  return out;
307
338
  }
308
339
 
340
+ @vertex
341
+ fn vs_main(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
342
+ return vs_main_impl(in, idx);
343
+ }
344
+
345
+ @vertex
346
+ fn vs_scene_index(in : VsIn, @builtin(instance_index) idx : u32) -> VsOut {
347
+ return vs_main_impl(in, idx);
348
+ }
349
+
309
350
  fn transformedMaterialUv(transform : vec4<f32>, metadata : vec4<f32>, in : VsOut) -> vec2<f32> {
310
351
  var source = in.uv;
311
352
  if (metadata.x >= 1.0) { source = in.uv1; }
@@ -377,16 +418,34 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
377
418
  prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
378
419
  );
379
420
  let skyColor = vec3<f32>(skylight.colorR, skylight.colorG, skylight.colorB);
421
+ let skyFactor = skyColor * skylight.intensity;
380
422
  var ambient = (kD * irradiance * albedo + specularIbl) * (vec3<f32>(1.0) - coatF);
423
+ #ifdef PROBE_BLEND_AVAILABLE
424
+ // group(3) is the per-object bind group; record zero is the object selected
425
+ // by this draw, independent of instance count.
426
+ let probeShPreblend = array<vec4<f32>, 9>(
427
+ probeBlendRecords[1], probeBlendRecords[2], probeBlendRecords[3],
428
+ probeBlendRecords[4], probeBlendRecords[5], probeBlendRecords[6],
429
+ probeBlendRecords[7], probeBlendRecords[8], probeBlendRecords[9],
430
+ );
431
+ let probeLocalBlendFraction = probeBlendRecords[0].z;
432
+ let probeDiffuseK = kD / max(vec3<f32>(1.0 - metallic), vec3<f32>(0.0001));
433
+ let probeDiffuse = evaluateProbeDiffuse(probeShPreblend, probeLocalBlendFraction, n, irradiance * skyFactor, probeDiffuseK, albedo, metallic);
434
+ ambient = (probeDiffuse + specularIbl * skyFactor) * (vec3<f32>(1.0) - coatF);
435
+ #endif
381
436
  if (material.clearcoat != 0.0) {
382
437
  let clearcoatIbl = sampleIblSpecular(
383
438
  n, v, coatRoughness, vec3<f32>(0.04),
384
439
  skylight.rotation,
385
440
  prefilterMap, prefilterSampler, brdfLut, brdfLutSampler,
386
441
  );
387
- ambient = ambient + clearcoatIbl * material.clearcoat;
442
+ ambient = ambient + clearcoatIbl * material.clearcoat * skyFactor;
388
443
  }
389
- ambient = ambient * skyColor * skylight.intensity;
444
+ #ifdef PROBE_BLEND_AVAILABLE
445
+ ambient = ambient;
446
+ #else
447
+ ambient = ambient * skyFactor;
448
+ #endif
390
449
  var color = ambient;
391
450
  let directionalShadow = evalDirectionalShadowFactor(n, in.worldPos, in.viewZ);
392
451
  let directionalBase = evalDirectionalNoShadow(n, v, albedo, metallic, a, f0);
@@ -397,65 +456,17 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
397
456
  );
398
457
  color = color + directionalShadow * material.clearcoat * directionalClearcoat;
399
458
  }
400
- let pointCount = pointLightsBuffer.count;
401
- for (var i: u32 = 0u; i < pointCount; i = i + 1u) {
402
- let p = pointLightsBuffer.slots[i];
403
- #ifdef POINT_SHADOW_AVAILABLE
404
- if (p.shadowAtlasLayer >= 0) {
405
- let lane = shadowParams[p.shadowAtlasLayer];
406
- color = color + evalPointShadowed(
407
- p.position, p.colorTimesIntensity, p.invRangeSquared,
408
- in.worldPos, n, v, albedo, metallic, a, f0,
409
- p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
410
- );
411
- if (material.clearcoat != 0.0) {
412
- color = color + material.clearcoat * evalPointShadowed(
413
- p.position, p.colorTimesIntensity, p.invRangeSquared,
414
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
415
- p.shadowAtlasLayer, lane.x, lane.y, 0.005, 0.05,
416
- );
417
- }
418
- } else {
419
- color = color + evalPoint(
420
- p.position, p.colorTimesIntensity, p.invRangeSquared,
421
- in.worldPos, n, v, albedo, metallic, a, f0,
422
- );
423
- if (material.clearcoat != 0.0) {
424
- color = color + material.clearcoat * evalPoint(
425
- p.position, p.colorTimesIntensity, p.invRangeSquared,
426
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
427
- );
428
- }
429
- }
430
- #else
431
- color = color + evalPoint(
432
- p.position, p.colorTimesIntensity, p.invRangeSquared,
433
- in.worldPos, n, v, albedo, metallic, a, f0,
434
- );
435
- if (material.clearcoat != 0.0) {
436
- color = color + material.clearcoat * evalPoint(
437
- p.position, p.colorTimesIntensity, p.invRangeSquared,
438
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
439
- );
440
- }
441
- #endif
442
- }
443
- let spotCount = spotLightsBuffer.count;
444
- for (var i: u32 = 0u; i < spotCount; i = i + 1u) {
445
- let s = spotLightsBuffer.slots[i];
446
- color = color + evalSpot(
447
- s.position, s.direction, s.colorTimesIntensity,
448
- s.cosInner, s.cosOuter, s.invRangeSquared,
449
- in.worldPos, n, v, albedo, metallic, a, f0,
459
+ #ifdef CLUSTER_FORWARD_AVAILABLE
460
+ color = color + evaluateStandardClusterLights(
461
+ in.ndc, in.viewZ, in.worldPos, n, v, albedo, metallic, a, f0, false,
462
+ );
463
+ if (material.clearcoat != 0.0) {
464
+ color = color + material.clearcoat * evaluateStandardClusterLights(
465
+ in.ndc, in.viewZ, in.worldPos, n, v,
466
+ vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04), false,
450
467
  );
451
- if (material.clearcoat != 0.0) {
452
- color = color + material.clearcoat * evalSpot(
453
- s.position, s.direction, s.colorTimesIntensity,
454
- s.cosInner, s.cosOuter, s.invRangeSquared,
455
- in.worldPos, n, v, vec3<f32>(0.0), 1.0, coatAlpha, vec3<f32>(0.04),
456
- );
457
- }
458
468
  }
469
+ #endif
459
470
  return vec4<f32>(color, alpha);
460
471
  }
461
472
 
@@ -471,6 +482,7 @@ struct TemporalVsOut {
471
482
  @location(7) uv7 : vec2<f32>,
472
483
  @location(8) @interpolate(perspective) currentClip : vec4<f32>,
473
484
  @location(9) @interpolate(perspective) previousClip : vec4<f32>,
485
+ @location(10) @interpolate(flat) reactive : f32,
474
486
  #ifdef VERTEX_COLOR_AVAILABLE
475
487
  @location(14) color : vec4<f32>,
476
488
  #endif
@@ -478,20 +490,25 @@ struct TemporalVsOut {
478
490
 
479
491
  @vertex
480
492
  fn vs_temporal(in : VsIn, @builtin(instance_index) idx : u32) -> TemporalVsOut {
481
- let currentSkin = palette[in.skinIndex.x] * in.skinWeight.x +
482
- palette[in.skinIndex.y] * in.skinWeight.y +
483
- palette[in.skinIndex.z] * in.skinWeight.z +
484
- palette[in.skinIndex.w] * in.skinWeight.w;
485
- let previousSkin = previousPalette[in.skinIndex.x] * in.skinWeight.x +
486
- previousPalette[in.skinIndex.y] * in.skinWeight.y +
487
- previousPalette[in.skinIndex.z] * in.skinWeight.z +
488
- previousPalette[in.skinIndex.w] * in.skinWeight.w;
493
+ let currentSkin = skinPalette(in.skinIndex.x) * in.skinWeight.x +
494
+ skinPalette(in.skinIndex.y) * in.skinWeight.y +
495
+ skinPalette(in.skinIndex.z) * in.skinWeight.z +
496
+ skinPalette(in.skinIndex.w) * in.skinWeight.w;
497
+ let previousSkin = previousSkinPalette(in.skinIndex.x) * in.skinWeight.x +
498
+ previousSkinPalette(in.skinIndex.y) * in.skinWeight.y +
499
+ previousSkinPalette(in.skinIndex.z) * in.skinWeight.z +
500
+ previousSkinPalette(in.skinIndex.w) * in.skinWeight.w;
489
501
  let currentWorld = currentSkin * vec4<f32>(in.pos, 1.0);
490
502
  let previousWorld = previousSkin * vec4<f32>(in.pos, 1.0);
491
503
  var out : TemporalVsOut;
492
504
  out.currentClip = view.temporalCurrentViewProj * currentWorld;
493
505
  out.clip = out.currentClip;
494
506
  out.previousClip = view.temporalPreviousViewProj * previousWorld;
507
+ #if STORAGE_BUFFER_AVAILABLE == true
508
+ out.reactive = meshes[0].temporal.x;
509
+ #else
510
+ out.reactive = 1.0;
511
+ #endif
495
512
  out.uv = in.uv;
496
513
  out.uv1 = in.uv1;
497
514
  out.uv2 = in.uv2;
@@ -517,10 +534,9 @@ fn temporalVertexAlpha(in : TemporalVsOut) -> f32 {
517
534
 
518
535
  @fragment
519
536
  fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
520
- #if STORAGE_BUFFER_AVAILABLE == true
521
- let reactive = meshes[0].temporal.x;
522
- #else
523
- let reactive = 1.0;
537
+ var reactive = 0.0;
538
+ #ifdef STORAGE_BUFFER_AVAILABLE
539
+ reactive = meshes[0].temporal.x;
524
540
  #endif
525
541
  return projectPbrSceneTemporal(
526
542
  material.baseColor.a * temporalVertexAlpha(in),
@@ -532,7 +548,7 @@ fn fs_temporal(in : TemporalVsOut) -> @location(0) vec4<f32> {
532
548
  in.currentClip,
533
549
  in.previousClip,
534
550
  view.temporalProjection,
535
- reactive,
551
+ in.reactive,
536
552
  in.uv, in.uv1, in.uv2, in.uv3,
537
553
  in.uv4, in.uv5, in.uv6, in.uv7,
538
554
  );