@forgeax/engine-shader 0.1.2

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 (162) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/ShaderRegistry.d.ts +208 -0
  5. package/dist/ShaderRegistry.d.ts.map +1 -0
  6. package/dist/__tests__/atmosphere-background.test.d.ts +2 -0
  7. package/dist/__tests__/atmosphere-background.test.d.ts.map +1 -0
  8. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts +2 -0
  9. package/dist/__tests__/atmosphere-cubemap-orientation.test.d.ts.map +1 -0
  10. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts +2 -0
  11. package/dist/__tests__/atmosphere-preetham-numeric.test.d.ts.map +1 -0
  12. package/dist/__tests__/atmosphere-preetham-red.test.d.ts +2 -0
  13. package/dist/__tests__/atmosphere-preetham-red.test.d.ts.map +1 -0
  14. package/dist/__tests__/atmosphere-preetham.test.d.ts +2 -0
  15. package/dist/__tests__/atmosphere-preetham.test.d.ts.map +1 -0
  16. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts +2 -0
  17. package/dist/__tests__/atmosphere-three-r184-reference.test.d.ts.map +1 -0
  18. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts +2 -0
  19. package/dist/__tests__/bloom-fxaa-tonemap.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts +2 -0
  21. package/dist/__tests__/builtin-texture-sampling-contract.test.d.ts.map +1 -0
  22. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts +2 -0
  23. package/dist/__tests__/default-standard-pbr-alpha.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/deferred-lighting-ssao.test.d.ts +2 -0
  25. package/dist/__tests__/deferred-lighting-ssao.test.d.ts.map +1 -0
  26. package/dist/__tests__/fog-oracle.unit.test.d.ts +2 -0
  27. package/dist/__tests__/fog-oracle.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts +2 -0
  29. package/dist/__tests__/fog-orthographic-ray.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts +2 -0
  31. package/dist/__tests__/fog-producer-matrix.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/hdrp-ssao-compile.test.d.ts +2 -0
  33. package/dist/__tests__/hdrp-ssao-compile.test.d.ts.map +1 -0
  34. package/dist/__tests__/ibl-irradiance.unit.test.d.ts +2 -0
  35. package/dist/__tests__/ibl-irradiance.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/ibl-sampling.unit.test.d.ts +2 -0
  37. package/dist/__tests__/ibl-sampling.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/lighting-punctual.unit.test.d.ts +2 -0
  39. package/dist/__tests__/lighting-punctual.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts +2 -0
  41. package/dist/__tests__/material-artifact-dedup.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/material-builtins.unit.test.d.ts +2 -0
  43. package/dist/__tests__/material-builtins.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/material-contract.unit.test.d.ts +2 -0
  45. package/dist/__tests__/material-contract.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/material-derived-builtins.integration.test.d.ts +2 -0
  47. package/dist/__tests__/material-derived-builtins.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
  49. package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/scene-temporal.unit.test.d.ts +2 -0
  51. package/dist/__tests__/scene-temporal.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts +2 -0
  53. package/dist/__tests__/shader-registry-transaction.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/shader.unit.test.d.ts +2 -0
  55. package/dist/__tests__/shader.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/sprite-lit-shader.test.d.ts +2 -0
  57. package/dist/__tests__/sprite-lit-shader.test.d.ts.map +1 -0
  58. package/dist/__tests__/sprite-variants.unit.test.d.ts +2 -0
  59. package/dist/__tests__/sprite-variants.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/taa-resolve.unit.test.d.ts +2 -0
  61. package/dist/__tests__/taa-resolve.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/transparent-pbr.unit.test.d.ts +2 -0
  63. package/dist/__tests__/transparent-pbr.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/vertex-color-variant.unit.test.d.ts +2 -0
  65. package/dist/__tests__/vertex-color-variant.unit.test.d.ts.map +1 -0
  66. package/dist/errors.d.ts +96 -0
  67. package/dist/errors.d.ts.map +1 -0
  68. package/dist/index.d.ts +44 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.mjs +560 -0
  71. package/dist/index.mjs.map +1 -0
  72. package/dist/material/artifact-registry.d.ts +31 -0
  73. package/dist/material/artifact-registry.d.ts.map +1 -0
  74. package/dist/material/artifact-types.d.ts +13 -0
  75. package/dist/material/artifact-types.d.ts.map +1 -0
  76. package/dist/material-schemas.d.ts +8 -0
  77. package/dist/material-schemas.d.ts.map +1 -0
  78. package/dist/register-default-sprite-lit.d.ts +40 -0
  79. package/dist/register-default-sprite-lit.d.ts.map +1 -0
  80. package/dist/register-default-standard-pbr-skin.d.ts +34 -0
  81. package/dist/register-default-standard-pbr-skin.d.ts.map +1 -0
  82. package/dist/tonemap.d.ts +13 -0
  83. package/dist/tonemap.d.ts.map +1 -0
  84. package/dist/types.d.ts +47 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/package.json +58 -0
  87. package/src/ShaderRegistry.ts +519 -0
  88. package/src/__tests__/atmosphere-background.test.ts +33 -0
  89. package/src/__tests__/atmosphere-cubemap-orientation.test.ts +68 -0
  90. package/src/__tests__/atmosphere-preetham-numeric.test.ts +122 -0
  91. package/src/__tests__/atmosphere-preetham-red.test.ts +52 -0
  92. package/src/__tests__/atmosphere-preetham.test.ts +132 -0
  93. package/src/__tests__/atmosphere-three-r184-reference.test.ts +122 -0
  94. package/src/__tests__/bloom-fxaa-tonemap.unit.test.ts +31 -0
  95. package/src/__tests__/builtin-texture-sampling-contract.test.ts +56 -0
  96. package/src/__tests__/default-standard-pbr-alpha.unit.test.ts +19 -0
  97. package/src/__tests__/deferred-lighting-ssao.test.ts +114 -0
  98. package/src/__tests__/fog-oracle.unit.test.ts +132 -0
  99. package/src/__tests__/fog-orthographic-ray.unit.test.ts +62 -0
  100. package/src/__tests__/fog-producer-matrix.integration.test.ts +77 -0
  101. package/src/__tests__/hdrp-ssao-compile.test.ts +185 -0
  102. package/src/__tests__/ibl-irradiance.unit.test.ts +43 -0
  103. package/src/__tests__/ibl-sampling.unit.test.ts +40 -0
  104. package/src/__tests__/lighting-punctual.unit.test.ts +66 -0
  105. package/src/__tests__/manifest.fixture.json +22 -0
  106. package/src/__tests__/material-artifact-dedup.unit.test.ts +21 -0
  107. package/src/__tests__/material-builtins.unit.test.ts +111 -0
  108. package/src/__tests__/material-contract.unit.test.ts +79 -0
  109. package/src/__tests__/material-derived-builtins.integration.test.ts +30 -0
  110. package/src/__tests__/public-surface.unit.test.ts +9 -0
  111. package/src/__tests__/scene-temporal.unit.test.ts +32 -0
  112. package/src/__tests__/shader-registry-transaction.unit.test.ts +116 -0
  113. package/src/__tests__/shader.unit.test.ts +1909 -0
  114. package/src/__tests__/sprite-lit-shader.test.ts +240 -0
  115. package/src/__tests__/sprite-variants.unit.test.ts +209 -0
  116. package/src/__tests__/taa-resolve.unit.test.ts +32 -0
  117. package/src/__tests__/transparent-pbr.unit.test.ts +21 -0
  118. package/src/__tests__/vertex-color-variant.unit.test.ts +52 -0
  119. package/src/atmosphere-background.wgsl +80 -0
  120. package/src/atmosphere-cubemap.wgsl +72 -0
  121. package/src/atmosphere-ibl.wgsl +127 -0
  122. package/src/atmosphere-preetham.wgsl +214 -0
  123. package/src/bloom-blur.wgsl +89 -0
  124. package/src/bloom-bright.wgsl +50 -0
  125. package/src/bloom-composite.wgsl +54 -0
  126. package/src/brdf.wgsl +25 -0
  127. package/src/common.wgsl +523 -0
  128. package/src/default-standard-pbr-skin.wgsl +538 -0
  129. package/src/default-standard-pbr.wgsl +794 -0
  130. package/src/errors.ts +169 -0
  131. package/src/fog.wgsl +27 -0
  132. package/src/fxaa.wgsl +259 -0
  133. package/src/hdrp-cluster-forward.wgsl +186 -0
  134. package/src/hdrp-deferred-lighting.wgsl +69 -0
  135. package/src/hdrp-ssao.wgsl +174 -0
  136. package/src/ibl-brdf-lut.wgsl +92 -0
  137. package/src/ibl-equirect-to-cube.wgsl +56 -0
  138. package/src/ibl-irradiance.wgsl +89 -0
  139. package/src/ibl-prefilter.wgsl +101 -0
  140. package/src/ibl-sampling.wgsl +58 -0
  141. package/src/ibl-shared.wgsl +118 -0
  142. package/src/index.ts +166 -0
  143. package/src/lighting-directional.wgsl +471 -0
  144. package/src/lighting-punctual.wgsl +274 -0
  145. package/src/material/artifact-registry.ts +78 -0
  146. package/src/material/artifact-types.ts +26 -0
  147. package/src/material-schemas.ts +48 -0
  148. package/src/msdf-text.wgsl +208 -0
  149. package/src/pbr-temporal.wgsl +73 -0
  150. package/src/register-default-sprite-lit.ts +87 -0
  151. package/src/register-default-standard-pbr-skin.ts +74 -0
  152. package/src/shadow-pcf.wgsl +120 -0
  153. package/src/shadow_caster.wgsl +54 -0
  154. package/src/skybox.wgsl +75 -0
  155. package/src/sprite-lit.wgsl +307 -0
  156. package/src/sprite.wgsl +365 -0
  157. package/src/taa-resolve.wgsl +129 -0
  158. package/src/tbn.wgsl +54 -0
  159. package/src/tonemap.ts +13 -0
  160. package/src/tonemap.wgsl +177 -0
  161. package/src/types.ts +59 -0
  162. package/src/unlit.wgsl +186 -0
@@ -0,0 +1,471 @@
1
+ #define_import_path forgeax_pbr::lighting_directional
2
+
3
+ // @forgeax/engine-shader - lighting-directional.wgsl
4
+ // (feat-20260523-shader-template-instance-split M5 / T02;
5
+ // feat-20260613-csm-cascaded-shadow-maps-unique-shadow-path M5 / w18 rewrite).
6
+ //
7
+ // Directional-light evaluator extracted from pbr.wgsl. Cook-Torrance
8
+ // (D_GGX + V_Smith + F_Schlick) microfacet specular + Lambertian diffuse,
9
+ // modulated by a slope-scaled-bias 3x3 PCF shadow lookup against the
10
+ // host's CSM shadow atlas (LearnOpenGL 3.1.3 PCF model + Bevy-style
11
+ // cascaded shadow maps).
12
+ //
13
+ // Cascade selection (feat-20260613 AC-03 / AC-06 / AC-10):
14
+ // 1. Pick layer in 0..cascadeCount-1 based on viewZ vs splitPlanes[i].
15
+ // The same code path covers a 1-layer config (single tile) and a
16
+ // 4-layer config (Bevy default) -- AC-03 forbids any single-cascade
17
+ // fallback branch.
18
+ // 2. Project worldPos through the matching lightViewProj. The host emits
19
+ // pure clip-space matrices (orthoProj * lightView with no tile-UV
20
+ // pre-bake); this helper performs atlas tile UV placement on the
21
+ // fragment side so shadow_caster can keep gl_Position in clip space
22
+ // (w28 split-of-roles -- writer and reader share one matrix shape).
23
+ // 3. textureSampleCompareLevel against shadowMap (the atlas) with a
24
+ // slope-scaled bias (LO 3.1.3) and 3x3 PCF tap kernel.
25
+ // 4. When cascadeBlend > 0 and the fragment lies near the cascade
26
+ // boundary, mix(shadow_curr, shadow_next, t) where t walks 0->1
27
+ // across a band of width splitPlanes[layer] * cascadeBlend.
28
+ //
29
+ // feat-20260612-point-light-shadows-urp-hdrp M2 / T-M2-2 (plan-strategy D-4):
30
+ // 9-tap PCF taps come from shared sample_shadow_2d in forgeax_pbr::shadow_pcf.
31
+ // Bias formula and 9-tap kernel are byte-equivalent to the prior inline
32
+ // version (research L1.5 lines 47-81); the shared core is the single SSOT
33
+ // for directional + point-light PCF.
34
+ //
35
+ // Pulls View + shadowMap from forgeax_view::common -- the helper inherits
36
+ // the group(0) binding namespace from the host material shader (every
37
+ // consumer already imports forgeax_view::common). Shadow PCF taps use
38
+ // textureSampleCompareLevel against the comparison sampler so the path
39
+ // is portable across WebGPU + GLES.
40
+ //
41
+ // Exports:
42
+ // - evalDirectional(...) -> vec3<f32> (Cook-Torrance + CSM shadow mod)
43
+ // - evalDirectionalNoShadow(...) -> vec3<f32> (Cook-Torrance, no shadow;
44
+ // sprite-lit M1' / w3 D-1; also the inner brdf body of evalDirectional)
45
+ // - evalDirectionalShadowFactor(...) -> f32 (one reusable CSM sample for
46
+ // layered BRDFs such as base + clearcoat)
47
+
48
+ #import forgeax_view::common::{view, shadowMap, shadowSampler}
49
+ #import forgeax_pbr::brdf::{f_schlick, v_smith, d_ggx}
50
+ #import forgeax_pbr::shadow_pcf::{sample_shadow_2d}
51
+
52
+ // feat-20260621-learn-render-5-3-production-shadow-demos M0 / AC-14:
53
+ // compile-time upper bound on the PCF half-extent so the WGSL tap loops keep
54
+ // a constant trip count (no dynamic loop bounds / shader variants). half=2
55
+ // covers pcfKernelSize in {1,3,5} -> {1,9,25} taps; view.pcfKernelSize selects
56
+ // the runtime radius via a per-iteration clip (plan-strategy D-1).
57
+ const MAX_PCF_HALF : u32 = 2u;
58
+
59
+ // Source: Three.js r184 src/nodes/functions/BSDF/DFGLUT.js. The full 16x16
60
+ // RG16F table is decoded to exact f32 constants here; the lookup below mirrors
61
+ // the source DataTexture's linear filtering and clamp-to-edge sampling without
62
+ // adding a host binding or coupling direct light to the engine IBL LUT.
63
+ const THREE_R184_DFG_LUT_SIZE : u32 = 16u;
64
+ const THREE_R184_DFG_LUT : array<vec2<f32>, 256> = array<vec2<f32>, 256>(
65
+ vec2<f32>(0.1470947265625, 0.85205078125), vec2<f32>(0.16552734375, 0.78759765625), vec2<f32>(0.244384765625, 0.638671875), vec2<f32>(0.370849609375, 0.51953125),
66
+ vec2<f32>(0.496826171875, 0.41552734375), vec2<f32>(0.60205078125, 0.326416015625), vec2<f32>(0.68408203125, 0.25390625), vec2<f32>(0.74609375, 0.197509765625),
67
+ vec2<f32>(0.79052734375, 0.154296875), vec2<f32>(0.822265625, 0.12164306640625), vec2<f32>(0.84326171875, 0.09698486328125), vec2<f32>(0.8564453125, 0.07843017578125),
68
+ vec2<f32>(0.86328125, 0.06439208984375), vec2<f32>(0.86572265625, 0.0537109375), vec2<f32>(0.8642578125, 0.045440673828125), vec2<f32>(0.85986328125, 0.039031982421875),
69
+ vec2<f32>(0.388671875, 0.611328125), vec2<f32>(0.39306640625, 0.6005859375), vec2<f32>(0.412353515625, 0.5458984375), vec2<f32>(0.45654296875, 0.4482421875),
70
+ vec2<f32>(0.52783203125, 0.3525390625), vec2<f32>(0.607421875, 0.27392578125), vec2<f32>(0.6787109375, 0.21142578125), vec2<f32>(0.7333984375, 0.16259765625),
71
+ vec2<f32>(0.77099609375, 0.1253662109375), vec2<f32>(0.79345703125, 0.0972900390625), vec2<f32>(0.80322265625, 0.07623291015625), vec2<f32>(0.8037109375, 0.06036376953125),
72
+ vec2<f32>(0.79638671875, 0.048431396484375), vec2<f32>(0.78564453125, 0.03936767578125), vec2<f32>(0.77197265625, 0.03240966796875), vec2<f32>(0.7548828125, 0.0269775390625),
73
+ vec2<f32>(0.572265625, 0.427490234375), vec2<f32>(0.57373046875, 0.424072265625), vec2<f32>(0.57958984375, 0.403564453125), vec2<f32>(0.591796875, 0.3544921875),
74
+ vec2<f32>(0.6162109375, 0.2880859375), vec2<f32>(0.6552734375, 0.224853515625), vec2<f32>(0.69873046875, 0.172607421875), vec2<f32>(0.7353515625, 0.1318359375),
75
+ vec2<f32>(0.75927734375, 0.10089111328125), vec2<f32>(0.77001953125, 0.07745361328125), vec2<f32>(0.77197265625, 0.0599365234375), vec2<f32>(0.76611328125, 0.046844482421875),
76
+ vec2<f32>(0.751953125, 0.0369873046875), vec2<f32>(0.732421875, 0.029541015625), vec2<f32>(0.70947265625, 0.023834228515625), vec2<f32>(0.68359375, 0.019439697265625),
77
+ vec2<f32>(0.708984375, 0.291015625), vec2<f32>(0.708984375, 0.289794921875), vec2<f32>(0.7099609375, 0.28125), vec2<f32>(0.7099609375, 0.258544921875),
78
+ vec2<f32>(0.71142578125, 0.220458984375), vec2<f32>(0.7197265625, 0.1768798828125), vec2<f32>(0.734375, 0.1370849609375), vec2<f32>(0.748046875, 0.10479736328125),
79
+ vec2<f32>(0.755859375, 0.07989501953125), vec2<f32>(0.759765625, 0.061004638671875), vec2<f32>(0.75341796875, 0.046844482421875), vec2<f32>(0.73876953125, 0.036224365234375),
80
+ vec2<f32>(0.7177734375, 0.02825927734375), vec2<f32>(0.69189453125, 0.0222625732421875), vec2<f32>(0.6611328125, 0.0177001953125), vec2<f32>(0.62841796875, 0.01419830322265625),
81
+ vec2<f32>(0.80810546875, 0.1917724609375), vec2<f32>(0.8076171875, 0.1912841796875), vec2<f32>(0.80615234375, 0.18798828125), vec2<f32>(0.8017578125, 0.1781005859375),
82
+ vec2<f32>(0.79296875, 0.15869140625), vec2<f32>(0.78369140625, 0.1322021484375), vec2<f32>(0.775390625, 0.1048583984375), vec2<f32>(0.7685546875, 0.08111572265625),
83
+ vec2<f32>(0.7646484375, 0.061981201171875), vec2<f32>(0.75537109375, 0.047271728515625), vec2<f32>(0.740234375, 0.036163330078125), vec2<f32>(0.71875, 0.0277557373046875),
84
+ vec2<f32>(0.69091796875, 0.021484375), vec2<f32>(0.658203125, 0.0167388916015625), vec2<f32>(0.6220703125, 0.01316070556640625), vec2<f32>(0.583984375, 0.01041412353515625),
85
+ vec2<f32>(0.87841796875, 0.1217041015625), vec2<f32>(0.8779296875, 0.1217041015625), vec2<f32>(0.875, 0.12060546875), vec2<f32>(0.869140625, 0.116943359375),
86
+ vec2<f32>(0.85693359375, 0.1080322265625), vec2<f32>(0.837890625, 0.09375), vec2<f32>(0.81494140625, 0.076904296875), vec2<f32>(0.7958984375, 0.060699462890625),
87
+ vec2<f32>(0.7763671875, 0.046875), vec2<f32>(0.75634765625, 0.035919189453125), vec2<f32>(0.732421875, 0.0274505615234375), vec2<f32>(0.703125, 0.021026611328125),
88
+ vec2<f32>(0.6689453125, 0.01617431640625), vec2<f32>(0.63037109375, 0.01251220703125), vec2<f32>(0.58935546875, 0.00974273681640625), vec2<f32>(0.54638671875, 0.007633209228515625),
89
+ vec2<f32>(0.92626953125, 0.07379150390625), vec2<f32>(0.92578125, 0.07391357421875), vec2<f32>(0.9228515625, 0.07391357421875), vec2<f32>(0.9169921875, 0.0731201171875),
90
+ vec2<f32>(0.90380859375, 0.06982421875), vec2<f32>(0.88134765625, 0.0631103515625), vec2<f32>(0.85107421875, 0.053802490234375), vec2<f32>(0.8212890625, 0.043701171875),
91
+ vec2<f32>(0.791015625, 0.034393310546875), vec2<f32>(0.76123046875, 0.026611328125), vec2<f32>(0.72802734375, 0.02044677734375), vec2<f32>(0.69189453125, 0.015655517578125),
92
+ vec2<f32>(0.65087890625, 0.0120086669921875), vec2<f32>(0.607421875, 0.00925445556640625), vec2<f32>(0.56103515625, 0.0071563720703125), vec2<f32>(0.51416015625, 0.005565643310546875),
93
+ vec2<f32>(0.95751953125, 0.042327880859375), vec2<f32>(0.95703125, 0.042449951171875), vec2<f32>(0.95458984375, 0.042816162109375), vec2<f32>(0.94921875, 0.043182373046875),
94
+ vec2<f32>(0.93701171875, 0.04266357421875), vec2<f32>(0.9130859375, 0.040252685546875), vec2<f32>(0.8818359375, 0.035797119140625), vec2<f32>(0.8447265625, 0.0301361083984375),
95
+ vec2<f32>(0.80615234375, 0.0243377685546875), vec2<f32>(0.76708984375, 0.0191497802734375), vec2<f32>(0.7265625, 0.01485443115234375), vec2<f32>(0.6826171875, 0.01142120361328125),
96
+ vec2<f32>(0.63623046875, 0.0087738037109375), vec2<f32>(0.5869140625, 0.006740570068359375), vec2<f32>(0.53662109375, 0.005199432373046875), vec2<f32>(0.486083984375, 0.00402069091796875),
97
+ vec2<f32>(0.9775390625, 0.0226287841796875), vec2<f32>(0.97705078125, 0.0227508544921875), vec2<f32>(0.97509765625, 0.023193359375), vec2<f32>(0.9697265625, 0.0239105224609375),
98
+ vec2<f32>(0.95947265625, 0.0244903564453125), vec2<f32>(0.93603515625, 0.0241241455078125), vec2<f32>(0.9052734375, 0.02252197265625), vec2<f32>(0.865234375, 0.0197601318359375),
99
+ vec2<f32>(0.82177734375, 0.016510009765625), vec2<f32>(0.7744140625, 0.0132904052734375), vec2<f32>(0.7265625, 0.01045989990234375), vec2<f32>(0.67626953125, 0.0081329345703125),
100
+ vec2<f32>(0.6240234375, 0.00627899169921875), vec2<f32>(0.56982421875, 0.004825592041015625), vec2<f32>(0.51513671875, 0.0037136077880859375), vec2<f32>(0.46142578125, 0.0028629302978515625),
101
+ vec2<f32>(0.98876953125, 0.01107025146484375), vec2<f32>(0.98876953125, 0.01116180419921875), vec2<f32>(0.98681640625, 0.01151275634765625), vec2<f32>(0.98291015625, 0.01221466064453125),
102
+ vec2<f32>(0.97314453125, 0.01299285888671875), vec2<f32>(0.953125, 0.013519287109375), vec2<f32>(0.9228515625, 0.01328277587890625), vec2<f32>(0.88232421875, 0.01226043701171875),
103
+ vec2<f32>(0.8349609375, 0.01065826416015625), vec2<f32>(0.783203125, 0.00885009765625), vec2<f32>(0.728515625, 0.007114410400390625), vec2<f32>(0.671875, 0.00560760498046875),
104
+ vec2<f32>(0.61376953125, 0.004360198974609375), vec2<f32>(0.5546875, 0.0033721923828125), vec2<f32>(0.496337890625, 0.0025997161865234375), vec2<f32>(0.439453125, 0.0020046234130859375),
105
+ vec2<f32>(0.9951171875, 0.00479888916015625), vec2<f32>(0.9951171875, 0.004863739013671875), vec2<f32>(0.99365234375, 0.005096435546875), vec2<f32>(0.990234375, 0.005603790283203125),
106
+ vec2<f32>(0.9814453125, 0.0063323974609375), vec2<f32>(0.96435546875, 0.006977081298828125), vec2<f32>(0.93603515625, 0.007297515869140625), vec2<f32>(0.896484375, 0.0071258544921875),
107
+ vec2<f32>(0.8466796875, 0.00649261474609375), vec2<f32>(0.79150390625, 0.005596160888671875), vec2<f32>(0.7314453125, 0.004627227783203125), vec2<f32>(0.6689453125, 0.0037174224853515625),
108
+ vec2<f32>(0.60546875, 0.0029296875), vec2<f32>(0.54150390625, 0.00228118896484375), vec2<f32>(0.479248046875, 0.001766204833984375), vec2<f32>(0.419677734375, 0.00136566162109375),
109
+ vec2<f32>(0.998046875, 0.0017604827880859375), vec2<f32>(0.998046875, 0.0017938613891601562), vec2<f32>(0.99658203125, 0.0019292831420898438), vec2<f32>(0.994140625, 0.002239227294921875),
110
+ vec2<f32>(0.986328125, 0.0027294158935546875), vec2<f32>(0.9716796875, 0.0032501220703125), vec2<f32>(0.94580078125, 0.00366973876953125), vec2<f32>(0.90771484375, 0.003818511962890625),
111
+ vec2<f32>(0.8583984375, 0.003681182861328125), vec2<f32>(0.79931640625, 0.0033206939697265625), vec2<f32>(0.7353515625, 0.0028438568115234375), vec2<f32>(0.66748046875, 0.0023441314697265625),
112
+ vec2<f32>(0.5986328125, 0.0018796920776367188), vec2<f32>(0.5302734375, 0.0014829635620117188), vec2<f32>(0.464111328125, 0.0011587142944335938), vec2<f32>(0.402099609375, 0.0008993148803710938),
113
+ vec2<f32>(0.99951171875, 0.0005011558532714844), vec2<f32>(0.99951171875, 0.0005154609680175781), vec2<f32>(0.998046875, 0.000583648681640625), vec2<f32>(0.99658203125, 0.0007505416870117188),
114
+ vec2<f32>(0.9892578125, 0.00101470947265625), vec2<f32>(0.97607421875, 0.001346588134765625), vec2<f32>(0.95263671875, 0.0016527175903320312), vec2<f32>(0.916015625, 0.0018558502197265625),
115
+ vec2<f32>(0.8671875, 0.0019054412841796875), vec2<f32>(0.8076171875, 0.0018167495727539062), vec2<f32>(0.7392578125, 0.001621246337890625), vec2<f32>(0.6669921875, 0.0013799667358398438),
116
+ vec2<f32>(0.59326171875, 0.0011358261108398438), vec2<f32>(0.52001953125, 0.0009121894836425781), vec2<f32>(0.450439453125, 0.0007219314575195312), vec2<f32>(0.385986328125, 0.0005650520324707031),
117
+ vec2<f32>(1.0, 0.00009316205978393555), vec2<f32>(1.0, 0.0000978708267211914), vec2<f32>(0.9990234375, 0.00012540817260742188), vec2<f32>(0.9970703125, 0.00019216537475585938),
118
+ vec2<f32>(0.99072265625, 0.0003104209899902344), vec2<f32>(0.97900390625, 0.0004699230194091797), vec2<f32>(0.95751953125, 0.0006470680236816406), vec2<f32>(0.92333984375, 0.0007915496826171875),
119
+ vec2<f32>(0.87548828125, 0.0008769035339355469), vec2<f32>(0.81494140625, 0.0008869171142578125), vec2<f32>(0.744140625, 0.0008325576782226562), vec2<f32>(0.66748046875, 0.0007386207580566406),
120
+ vec2<f32>(0.58837890625, 0.0006265640258789062), vec2<f32>(0.51123046875, 0.0005168914794921875), vec2<f32>(0.438232421875, 0.0004165172576904297), vec2<f32>(0.37109375, 0.0003311634063720703),
121
+ vec2<f32>(1.0, 0.000007271766662597656), vec2<f32>(1.0, 0.000008165836334228516), vec2<f32>(0.9990234375, 0.000016987323760986328), vec2<f32>(0.99755859375, 0.00003790855407714844),
122
+ vec2<f32>(0.9921875, 0.00007593631744384766), vec2<f32>(0.9814453125, 0.00013744831085205078), vec2<f32>(0.96142578125, 0.00020754337310791016), vec2<f32>(0.92919921875, 0.00028014183044433594),
123
+ vec2<f32>(0.8828125, 0.0003345012664794922), vec2<f32>(0.82177734375, 0.00036263465881347656), vec2<f32>(0.7490234375, 0.00036215782165527344), vec2<f32>(0.66845703125, 0.0003380775451660156),
124
+ vec2<f32>(0.58544921875, 0.00029969215393066406), vec2<f32>(0.50390625, 0.00025582313537597656), vec2<f32>(0.427001953125, 0.0002117156982421875), vec2<f32>(0.357666015625, 0.00017201900482177734),
125
+ vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 5.960464477539063e-8), vec2<f32>(0.99951171875, 0.0000012516975402832031), vec2<f32>(0.99755859375, 0.000005304813385009766),
126
+ vec2<f32>(0.9931640625, 0.000015079975128173828), vec2<f32>(0.98291015625, 0.00002855062484741211), vec2<f32>(0.96435546875, 0.00004744529724121094), vec2<f32>(0.93408203125, 0.00006842613220214844),
127
+ vec2<f32>(0.88916015625, 0.00008893013000488281), vec2<f32>(0.828125, 0.0001042485237121582), vec2<f32>(0.75390625, 0.00011217594146728516), vec2<f32>(0.67041015625, 0.00011241436004638672),
128
+ vec2<f32>(0.5830078125, 0.00010627508163452148), vec2<f32>(0.4970703125, 0.00009584426879882812), vec2<f32>(0.4169921875, 0.0000833272933959961), vec2<f32>(0.34521484375, 0.00007051229476928711),
129
+ );
130
+
131
+ fn _sampleThreeR184DfgLut(roughness : f32, dotNV : f32) -> vec2<f32> {
132
+ let uv = clamp(vec2<f32>(roughness, dotNV), vec2<f32>(0.0), vec2<f32>(1.0));
133
+ let samplePosition = uv * f32(THREE_R184_DFG_LUT_SIZE) - vec2<f32>(0.5);
134
+ let base = vec2<i32>(floor(samplePosition));
135
+ let weight = fract(samplePosition);
136
+ let last = i32(THREE_R184_DFG_LUT_SIZE - 1u);
137
+ let lo = clamp(base, vec2<i32>(0), vec2<i32>(last));
138
+ let hi = clamp(base + vec2<i32>(1), vec2<i32>(0), vec2<i32>(last));
139
+ let rowLo = mix(
140
+ THREE_R184_DFG_LUT[u32(lo.y) * THREE_R184_DFG_LUT_SIZE + u32(lo.x)],
141
+ THREE_R184_DFG_LUT[u32(lo.y) * THREE_R184_DFG_LUT_SIZE + u32(hi.x)],
142
+ weight.x,
143
+ );
144
+ let rowHi = mix(
145
+ THREE_R184_DFG_LUT[u32(hi.y) * THREE_R184_DFG_LUT_SIZE + u32(lo.x)],
146
+ THREE_R184_DFG_LUT[u32(hi.y) * THREE_R184_DFG_LUT_SIZE + u32(hi.x)],
147
+ weight.x,
148
+ );
149
+ return mix(rowLo, rowHi, weight.y);
150
+ }
151
+
152
+ fn _threeR184DirectMultiScatter(
153
+ roughness : f32,
154
+ nDotV : f32,
155
+ nDotL : f32,
156
+ F0 : vec3<f32>,
157
+ ) -> vec3<f32> {
158
+ let dfgV = _sampleThreeR184DfgLut(roughness, nDotV);
159
+ let dfgL = _sampleThreeR184DfgLut(roughness, nDotL);
160
+ let fssEssV = F0 * dfgV.x + vec3<f32>(dfgV.y);
161
+ let fssEssL = F0 * dfgL.x + vec3<f32>(dfgL.y);
162
+ let emsV = 1.0 - dfgV.x - dfgV.y;
163
+ let emsL = 1.0 - dfgL.x - dfgL.y;
164
+ let favg = F0 + (vec3<f32>(1.0) - F0) * 0.047619;
165
+ let energyLoss = emsV * emsL;
166
+ let fms = fssEssV * fssEssL * favg
167
+ / (vec3<f32>(1.0) - energyLoss * favg * favg + vec3<f32>(1e-6));
168
+ return fms * energyLoss;
169
+ }
170
+
171
+ // Pick the cascade layer for a positive view-space depth -- walks
172
+ // splitPlanes in order, returns the first split the depth falls below. Last
173
+ // layer (count - 1) catches everything beyond splits[count-2]. cascadeCount=1
174
+ // returns 0 unconditionally without a special branch (count - 1u == 0
175
+ // short-circuits the loop trip count).
176
+ //
177
+ // NOTE the sign: the vertex stage emits `viewZ = -clipPos.w` (NEGATIVE in
178
+ // front of the camera -- the deliberate convention the cluster Z-slice path
179
+ // also relies on), but `pssmSplit` host-side produces POSITIVE view-space
180
+ // split depths. The caller therefore passes `viewDepth = -viewZ` so this
181
+ // comparison is positive-vs-positive. Comparing the raw negative viewZ against
182
+ // positive splits collapsed every visible fragment to layer 0 (its near slab),
183
+ // projecting far geometry out of the tile -> shadowFactor always 1.0 (no
184
+ // occlusion). (downstream template integration #1.)
185
+ fn _pickCascadeLayer(viewDepth : f32, count : u32) -> u32 {
186
+ var layer : u32 = count - 1u;
187
+ for (var i : u32 = 0u; i < count - 1u; i = i + 1u) {
188
+ let sp = view.splitPlanes[i].x;
189
+ if (viewDepth < sp) {
190
+ layer = i;
191
+ break;
192
+ }
193
+ }
194
+ return layer;
195
+ }
196
+
197
+ // Look up the lightViewProj matrix for layer index. View UBO carries 4
198
+ // distinct fields (lightViewProj_A..D); WGSL has no addressable mat4
199
+ // array on a uniform, so a manual switch keeps the path uniform.
200
+ fn _cascadeLightViewProj(layer : u32) -> mat4x4<f32> {
201
+ switch (layer) {
202
+ case 0u: { return view.lightViewProj_A; }
203
+ case 1u: { return view.lightViewProj_B; }
204
+ case 2u: { return view.lightViewProj_C; }
205
+ default: { return view.lightViewProj_D; }
206
+ }
207
+ }
208
+
209
+ // Match the host's compact atlas exactly: 1 -> 1x1, 2 -> 2x1, 3/4 -> 2x2.
210
+ // A square-only scale halves Y for the common two-cascade case even though
211
+ // each raster viewport spans the atlas's full height, so receivers sample a
212
+ // different region than casters wrote.
213
+ fn _atlasTileGrid(count : u32) -> vec2<u32> {
214
+ let columns : u32 = select(2u, 1u, count <= 1u);
215
+ let rows : u32 = (count + columns - 1u) / columns;
216
+ return vec2<u32>(columns, rows);
217
+ }
218
+
219
+ fn _atlasTileScale(count : u32) -> vec2<f32> {
220
+ return vec2<f32>(1.0) / vec2<f32>(_atlasTileGrid(count));
221
+ }
222
+
223
+ fn _atlasTileOrigin(layer : u32, count : u32) -> vec2<f32> {
224
+ let grid = _atlasTileGrid(count);
225
+ let tile = vec2<u32>(layer % grid.x, layer / grid.x);
226
+ return vec2<f32>(tile) / vec2<f32>(grid);
227
+ }
228
+
229
+ // Sample the shadow atlas with the LO 3.1.3 slope-scaled bias + dynamic PCF
230
+ // kernel (driven by view.pcfKernelSize, MAX_PCF_HALF=2),
231
+ // against the lightViewProj for the chosen cascade. The shader maps NDC
232
+ // xy to that cascade's atlas tile in fragment space (matrix carries
233
+ // clip-space; tile placement happens here so shadow_caster.gl_Position
234
+ // stays in the WGSL clip-space contract).
235
+ fn _sampleShadowForCascade(
236
+ worldPos : vec3<f32>,
237
+ layer : u32,
238
+ count : u32,
239
+ normal : vec3<f32>,
240
+ l : vec3<f32>,
241
+ ) -> f32 {
242
+ let lvp = _cascadeLightViewProj(layer);
243
+ let lightClip = lvp * vec4<f32>(worldPos, 1.0);
244
+ let projCoords = lightClip.xyz / lightClip.w;
245
+ let tileScale = _atlasTileScale(count);
246
+ let tileOrigin = _atlasTileOrigin(layer, count);
247
+ // NDC [-1,1] -> tile-local UV -> compact atlas UV.
248
+ let tileUv = vec2<f32>(projCoords.x * 0.5 + 0.5, -projCoords.y * 0.5 + 0.5);
249
+ let uv = tileUv * tileScale + tileOrigin;
250
+ let currentDepth = projCoords.z;
251
+ // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t4
252
+ // (D-1): the slope-scaled bias is driven by the merged DirectionalLight's
253
+ // shadow fields carried in the View UBO -- normalBias scales the
254
+ // (1 - N.L) slope term, depthBias is the constant floor. Replaces the prior
255
+ // hardcoded max(0.05*(1-N.L), 0.005).
256
+ let bias = max(view.normalBias * (1.0 - dot(normal, l)), view.depthBias);
257
+ let adjustedDepth = currentDepth - bias;
258
+ // NaN-safe bounds: relational < and > do not reject NaN (NaN < 0 is
259
+ // false), so a zero / degenerate lightViewProj matrix that produces
260
+ // 0/0 = NaN would slip through. Use x >= 0 && x <= 1 instead -- NaN
261
+ // makes the conjunction false and the early-return fires (shadow=1.0).
262
+ // Matches the main-line pattern that survived the transform-hierarchy
263
+ // dawn regression test (AC-08 parent-move pixel-diff on main).
264
+ if (!(tileUv.x >= 0.0 && tileUv.x <= 1.0 && tileUv.y >= 0.0 && tileUv.y <= 1.0 && currentDepth <= 1.0)) {
265
+ return 1.0;
266
+ }
267
+ let texelDims = vec2<f32>(textureDimensions(shadowMap, 0));
268
+ let texel = vec2<f32>(1.0 / texelDims.x, 1.0 / texelDims.y);
269
+ // AC-07 (bug-20260619): the OOB guard above is in tile-local space, but the
270
+ // PCF tap offset is applied in atlas space. For count>1 (inv<1) a fragment
271
+ // within one texel of a tile edge would sample into a NEIGHBOURING cascade's
272
+ // tile, reading the wrong depth and producing a 1-texel seam at cascade
273
+ // boundaries. Clamp every tap to this cascade's tile rect
274
+ // [tileOrigin, tileOrigin+tileScale) (one texel inset) so taps stay in-tile.
275
+ let tileLo = tileOrigin + texel;
276
+ let tileHi = tileOrigin + tileScale - texel;
277
+ // Variable-width PCF kernel driven by view.pcfKernelSize (feat-20260621
278
+ // 5.3-production-shadow-demos AC-14 merged with the DirectionalLightShadow
279
+ // merge). Constant trip count to MAX_PCF_HALF with a per-iteration clip to the
280
+ // runtime radius keeps the shader variant-free (no dynamic loop bound, legal
281
+ // for textureSampleCompareLevel uniform control flow). Host clamps
282
+ // view.pcfKernelSize to {1,3,5}; divisor = actual tap count, so pcfKernelSize=3
283
+ // -> half=1 -> 9 taps / 9.0 (result-identical to the prior hard-coded 3x3);
284
+ // pcfKernelSize=1 -> half=0 -> single centre tap (hard edge); pcfKernelSize=5
285
+ // -> half=2 -> 25-tap soft penumbra.
286
+ let requestedKernel = clamp(u32(round(view.pcfKernelSize)), 1u, 2u * MAX_PCF_HALF + 1u);
287
+ // Preserve the previous radius mapping for malformed even values:
288
+ // 1 -> 1x1, 2/3/4 -> 3x3, 5 -> 5x5. The authored component accepts only
289
+ // {1,3,5}, but keeping this normalization makes the refactor output-stable
290
+ // for raw UBO callers too.
291
+ let kernel = select(select(5u, 3u, requestedKernel <= 4u), 1u, requestedKernel == 1u);
292
+ if (kernel == 1u) {
293
+ let lit = textureSampleCompareLevel(shadowMap, shadowSampler, clamp(uv, tileLo, tileHi), adjustedDepth);
294
+ return lit;
295
+ }
296
+
297
+ var blocked = 0.0;
298
+ if (kernel == 3u) {
299
+ // A linear comparison sample is already the bilinear average of four
300
+ // depth comparisons. Three adjacent samples on one axis therefore have
301
+ // the separable texel weights [1-f, 1, 1, f]. Pairing those four weights
302
+ // into two bilinear samples is exact, reducing the 3x3 path from 9 samples
303
+ // to 4. Keep the original 9-sample form at cascade-tile edges where the
304
+ // per-tap clamp intentionally duplicates edge samples.
305
+ let interior = all(uv >= tileLo + texel) && all(uv <= tileHi - texel);
306
+ if (interior) {
307
+ let pcfFraction = fract(uv / texel - vec2<f32>(0.5));
308
+ let loWeight = vec2<f32>(2.0) - pcfFraction;
309
+ let hiWeight = vec2<f32>(1.0) + pcfFraction;
310
+ let loOffset = vec2<f32>(-1.0) - pcfFraction + vec2<f32>(1.0) / loWeight;
311
+ let hiOffset = vec2<f32>(1.0) - pcfFraction + pcfFraction / hiWeight;
312
+ let litLoLo = textureSampleCompareLevel(
313
+ shadowMap, shadowSampler, uv + vec2<f32>(loOffset.x, loOffset.y) * texel, adjustedDepth,
314
+ );
315
+ let litHiLo = textureSampleCompareLevel(
316
+ shadowMap, shadowSampler, uv + vec2<f32>(hiOffset.x, loOffset.y) * texel, adjustedDepth,
317
+ );
318
+ let litLoHi = textureSampleCompareLevel(
319
+ shadowMap, shadowSampler, uv + vec2<f32>(loOffset.x, hiOffset.y) * texel, adjustedDepth,
320
+ );
321
+ let litHiHi = textureSampleCompareLevel(
322
+ shadowMap, shadowSampler, uv + vec2<f32>(hiOffset.x, hiOffset.y) * texel, adjustedDepth,
323
+ );
324
+ return (
325
+ litLoLo * loWeight.x * loWeight.y +
326
+ litHiLo * hiWeight.x * loWeight.y +
327
+ litLoHi * loWeight.x * hiWeight.y +
328
+ litHiHi * hiWeight.x * hiWeight.y
329
+ ) / 9.0;
330
+ }
331
+ for (var x = -1; x <= 1; x++) {
332
+ for (var y = -1; y <= 1; y++) {
333
+ let offsetUv = clamp(uv + vec2<f32>(f32(x), f32(y)) * texel, tileLo, tileHi);
334
+ let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
335
+ blocked = blocked + (1.0 - lit);
336
+ }
337
+ }
338
+ return 1.0 - blocked / 9.0;
339
+ }
340
+
341
+ for (var x = -i32(MAX_PCF_HALF); x <= i32(MAX_PCF_HALF); x++) {
342
+ for (var y = -i32(MAX_PCF_HALF); y <= i32(MAX_PCF_HALF); y++) {
343
+ let offsetUv = clamp(uv + vec2<f32>(f32(x), f32(y)) * texel, tileLo, tileHi);
344
+ let lit = textureSampleCompareLevel(shadowMap, shadowSampler, offsetUv, adjustedDepth);
345
+ blocked = blocked + (1.0 - lit);
346
+ }
347
+ }
348
+ return 1.0 - blocked / 25.0;
349
+ }
350
+
351
+ // `evalDirectionalNoShadow` evaluates the GGX direct-lighting term for the
352
+ // single directional light carried in `view.lightDir / view.lightColor`
353
+ // WITHOUT applying any shadow factor — pure Cook-Torrance (D_GGX + V_Smith
354
+ // + F_Schlick) microfacet specular + Lambertian diffuse, returned scaled
355
+ // by `lightColor * nDotL`.
356
+ //
357
+ // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / w3 (D-1):
358
+ // extracted from evalDirectional so the brdf body matches the
359
+ // `evalPoint` / `evalSpot` pattern (no shadow tap inside the brdf
360
+ // function — caller multiplies the shadow factor afterwards). Industry
361
+ // alignment: Bevy / glTF Sample Renderer / Three.js all keep their
362
+ // directional brdf shadow-free; forgeax was the outlier with
363
+ // `_sampleShadowForCascade` hard-coded inside the body. Sprite-lit and
364
+ // future per-light variants get a clean shadow-free reuse target while
365
+ // mesh PBR keeps calling the wrapping `evalDirectional` (mathematically
366
+ // equivalent — see plan-strategy R-3D-mesh-PBR-output-shift).
367
+ //
368
+ // @internal — exported for sprite-lit re-use inside the engine; external
369
+ // material shaders should keep calling `evalDirectional` for backward
370
+ // compat with the cascaded-shadow pipeline.
371
+ fn evalDirectionalNoShadow(
372
+ normal : vec3<f32>,
373
+ viewDir : vec3<f32>,
374
+ baseColor : vec3<f32>,
375
+ metallic : f32,
376
+ alphaSq : f32,
377
+ F0 : vec3<f32>,
378
+ ) -> vec3<f32> {
379
+ let l = normalize(-view.lightDir);
380
+ let h = normalize(viewDir + l);
381
+ let nDotL = max(dot(normal, l), 0.0);
382
+ let nDotV = max(dot(normal, viewDir), 1e-5);
383
+ let nDotH = max(dot(normal, h), 0.0);
384
+ let vDotH = max(dot(viewDir, h), 0.0);
385
+ let fresnel = exp2((-5.55473 * vDotH - 6.98316) * vDotH);
386
+ let f = F0 * (vec3<f32>(1.0) - fresnel) + vec3<f32>(fresnel);
387
+ let roughness = sqrt(max(alphaSq, 0.0));
388
+ let multiScatter = _threeR184DirectMultiScatter(roughness, nDotV, nDotL, F0);
389
+ let specular = d_ggx(nDotH, alphaSq) * v_smith(nDotV, nDotL, alphaSq) * f + multiScatter;
390
+ let diffuse = (1.0 - metallic) * baseColor / 3.14159265;
391
+ return (diffuse + specular) * view.lightColor * nDotL;
392
+ }
393
+
394
+ fn evalDirectionalShadowFactor(
395
+ normal : vec3<f32>,
396
+ worldPos : vec3<f32>,
397
+ viewZ : f32,
398
+ ) -> f32 {
399
+ // `cascadeCount == 0` is the host-side sentinel for DirectionalLight
400
+ // `castShadow:false`. A non-shadow-casting light must stay fully lit: the
401
+ // fallback depth view and zeroed light matrices only satisfy the bind-group
402
+ // shape and are not a valid CSM sample. Do this before the count clamp so
403
+ // the disabled path cannot accidentally project through cascade zero.
404
+ if (view.cascadeCount < 1.0) {
405
+ return 1.0;
406
+ }
407
+ let l = normalize(-view.lightDir);
408
+ let count = u32(max(view.cascadeCount, 1.0));
409
+ let viewDepth = -viewZ;
410
+ // `pssmSplit` stores the authored shadowDistance in the last active split.
411
+ // Geometry beyond that distance is intentionally unshadowed; reject it
412
+ // before cascade projection, atlas dimension queries, and the PCF loop.
413
+ if (viewDepth > view.splitPlanes[count - 1u].x) {
414
+ return 1.0;
415
+ }
416
+ let layer = _pickCascadeLayer(viewDepth, count);
417
+ let shadowCurr = _sampleShadowForCascade(worldPos, layer, count, normal, l);
418
+
419
+ var shadow = shadowCurr;
420
+ if (view.cascadeBlend > 0.0 && layer + 1u < count) {
421
+ let spCurr = view.splitPlanes[layer].x;
422
+ let blendWidth = spCurr * view.cascadeBlend;
423
+ if (blendWidth > 0.0) {
424
+ let dist = spCurr - viewDepth;
425
+ let t = clamp(1.0 - dist / blendWidth, 0.0, 1.0);
426
+ if (t > 0.0) {
427
+ let shadowNext = _sampleShadowForCascade(worldPos, layer + 1u, count, normal, l);
428
+ shadow = mix(shadowCurr, shadowNext, t);
429
+ }
430
+ }
431
+ }
432
+ return shadow;
433
+ }
434
+
435
+ // `evalDirectional` evaluates the GGX direct-lighting term for the single
436
+ // directional light carried in `view.lightDir / view.lightColor`. CSM
437
+ // pathway: pick cascade layer from viewZ + splitPlanes, sample the atlas
438
+ // tile via the matching lightViewProj, optionally blend with the next
439
+ // cascade across a `cascadeBlend`-wide boundary band.
440
+ //
441
+ // feat-20260624 M1' / w3 (D-1): body now delegates the brdf math to
442
+ // `evalDirectionalNoShadow` and multiplies the cascade shadow factor at
443
+ // the call site (mirrors `evalPoint` / `evalSpot` shape). Output is
444
+ // mathematically equivalent to the pre-refactor inline form — mesh PBR
445
+ // pixel-parity bench is the regression guard (w8).
446
+ fn evalDirectional(
447
+ normal : vec3<f32>,
448
+ viewDir : vec3<f32>,
449
+ baseColor : vec3<f32>,
450
+ metallic : f32,
451
+ alphaSq : f32,
452
+ F0 : vec3<f32>,
453
+ worldPos : vec3<f32>,
454
+ viewZ : f32,
455
+ ) -> vec3<f32> {
456
+ // No-shadow brdf body (factored out — same expression as the prior
457
+ // inline form, multiplied by shadow factor below). Reuse keeps the
458
+ // mesh PBR output byte-equivalent and gives sprite-lit a shared
459
+ // shadow-free entry point.
460
+ let lit = evalDirectionalNoShadow(normal, viewDir, baseColor, metallic, alphaSq, F0);
461
+
462
+ // Cascade selection + atlas sampling (AC-03 / AC-05 / AC-06 / AC-10).
463
+ // feat-20260613-csm-cascaded-shadow-maps M5 / w18: uses inline 9-tap PCF
464
+ // inside `_sampleShadowForCascade` rather than the shared sample_shadow_2d
465
+ // (forgeax_pbr::shadow_pcf used by point-light) — the cascade dispatch
466
+ // wraps the kernel per-tile so the shared core's `(uv, currentDepth)`
467
+ // entry shape doesn't fit (it expects pre-projected light-space coords;
468
+ // CSM derives them per-cascade after dispatch). F-J-1 future-tracks the
469
+ // dedup once `forgeax_view::cascade` lands as its own module (post-#387).
470
+ return lit * evalDirectionalShadowFactor(normal, worldPos, viewZ);
471
+ }