@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,523 @@
1
+ #define_import_path forgeax_view::common
2
+
3
+ // The texture view owns the texture transfer: an sRGB view decodes once during
4
+ // textureSample, while a linear view is returned unchanged. Authored scalar
5
+ // values are decoded by the material extraction path before the uniform upload.
6
+ fn sampleMaterialTextureLinear(
7
+ texture : texture_2d<f32>,
8
+ textureSampler : sampler,
9
+ uv : vec2<f32>,
10
+ uvScale : vec2<f32>,
11
+ ) -> vec4<f32> {
12
+ return textureSample(texture, textureSampler, uv * uvScale);
13
+ }
14
+
15
+ // Keep the established helper for the other material paths; the explicit
16
+ // linear name is the owner-facing entry point for the unlit parity path.
17
+ fn sampleMaterialTexture(
18
+ texture : texture_2d<f32>,
19
+ textureSampler : sampler,
20
+ uv : vec2<f32>,
21
+ uvScale : vec2<f32>,
22
+ ) -> vec4<f32> {
23
+ return sampleMaterialTextureLinear(texture, textureSampler, uv, uvScale);
24
+ }
25
+
26
+ // Shared linear-LDR -> display-sRGB OETF for raw swap-chain storage writes.
27
+ // sRGB texture views perform this conversion on attachment writes, but the
28
+ // native output and FXAA passes intentionally target the non-sRGB storage view.
29
+ fn linearToSrgbOetf(color : vec3<f32>) -> vec3<f32> {
30
+ let safe = max(color, vec3<f32>(0.0));
31
+ let high = pow(safe, vec3<f32>(0.41666)) * vec3<f32>(1.055) - vec3<f32>(0.055);
32
+ let low = safe * vec3<f32>(12.92);
33
+ return select(high, low, safe <= vec3<f32>(0.0031308));
34
+ }
35
+
36
+ // @forgeax/engine-shader - common.wgsl (M5 T-18 feat-20260512-naga-oil-composition-hmr).
37
+ //
38
+ // Shared view + mesh structs extracted from pbr.wgsl + unlit.wgsl via
39
+ // naga_oil #define_import_path / #import (D-04 moduleId convention). Both
40
+ // material shaders pull View + Mesh from here so the RenderSystem can
41
+ // uniform-upload view + mesh storage once per frame for all pipelines.
42
+ //
43
+ // `View` carries the full PBR-superset field set (worldViewProj + light +
44
+ // cameraPos). Unlit only reads `worldViewProj`; the extra fields stay bound
45
+ // but unused in the unlit path (zero perf cost because the fragment shader
46
+ // never references them). This is the canonical "superset struct" pattern
47
+ // aligned with Bevy's bevy_view::common and matches charter proposition 5
48
+ // (consistent abstraction - one View struct everywhere).
49
+
50
+ // View UBO byte layout (592 B std140):
51
+ // [ 0.. 64) worldViewProj mat4x4<f32> (align 16, size 64)
52
+ // [ 64.. 80) lightDir vec3<f32> (align 16, 12+4 pad)
53
+ // [ 80.. 96) lightColor vec3<f32> (align 16, 12+4 pad)
54
+ // [ 96..112) cameraPos vec3<f32> (align 16, 12+4 pad)
55
+ // [112..176) lightViewProj_A mat4x4<f32> (align 16, size 64)
56
+ // [176..240) inverseViewProj mat4x4<f32> (align 16, size 64)
57
+ // [240..304) lightViewProj_B mat4x4<f32> (align 16, size 64)
58
+ // [304..368) lightViewProj_C mat4x4<f32> (align 16, size 64)
59
+ // [368..432) lightViewProj_D mat4x4<f32> (align 16, size 64)
60
+ // [432..448) splitPlanes[0] vec4<f32> (align 16, 16; .x=depth)
61
+ // [448..464) splitPlanes[1] vec4<f32> (align 16, 16)
62
+ // [464..480) splitPlanes[2] vec4<f32> (align 16, 16)
63
+ // [480..496) splitPlanes[3] vec4<f32> (align 16, 16)
64
+ // [496..500) cascadeCount f32 (align 4, size 4)
65
+ // [500..504) cascadeBlend f32 (align 4, size 4)
66
+ // [504..508) depthBias f32 (align 4, size 4)
67
+ // [508..512) normalBias f32 (align 4, size 4)
68
+ // [512..516) pcfKernelSize f32 (align 4, size 4)
69
+ // [516..528) _align_pad — (mat4 array align=16, 12 B)
70
+ // [528..784) spotLightViewProj array<mat4x4<f32>,4> (align 16, size 256)
71
+ // WGSL prefix = 784 B. Host UBO = VIEW_UBO_BYTES = 960 B (renderer-factory.ts);
72
+ // view-ubo.ts writes the full 240 f32 payload. The spot matrix
73
+ // array fold (feat-20260625 w25) removes the standalone @group(0) binding 9
74
+ // uniform buffer so the WebGL2 fallback fragment uniform-buffer count returns
75
+ // to 11 (GLES 3.0 max).
76
+ // temporal projection facts and Fog payload append after the spot matrix;
77
+ // total = 960 B.
78
+ //
79
+ // Field order must stay byte-for-byte identical to every prior release
80
+ // (charter P4 consistent abstraction); new fields append at the tail.
81
+ // Host write in view-ubo.ts builds the 240-float payload and the renderer
82
+ // allocates VIEW_UBO_BYTES = 960.
83
+ //
84
+ // feat-20260531-skybox-env-background M2 / w3: inverseViewProj appended
85
+ // at the tail (64 B mat4 at byte offset 176). The host pre-computes
86
+ // mat4.invert(inverseViewProj, worldViewProj) so the skybox fragment
87
+ // shader can reconstruct world-space view direction without per-pixel
88
+ // matrix inversion.
89
+ //
90
+ // feat-20260613-csm-cascaded-shadow-maps M4 / w16 + M5 / w28:
91
+ // the legacy single-cascade light-space matrix is replaced by a 4-cascade
92
+ // `lightViewProj_A..D` array at offset 112 (inverseViewProj stays at 176;
93
+ // `lightViewProj_B..D` + `splitPlanes` + cascadeCount/cascadeBlend follow
94
+ // inverseViewProj). WGSL struct is 512 B (auto-padded tail); the host
95
+ // allocates the larger View slot and writes trailing zero lanes to satisfy
96
+ // the AC-08 fixed-UBO-size invariant (the extra bytes are never read).
97
+ // shadow_caster.wgsl indexes the 4 fields via `shadowCasterCascade.index`
98
+ // (binding 5, written per shadow pass).
99
+ //
100
+ // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t3:
101
+ // the merged DirectionalLight's shadow bias + PCF kernel width append at the
102
+ // tail (depthBias / normalBias / pcfKernelSize at bytes 504/508/512, floats
103
+ // 126/127/128). Tail-append only -- field order is byte-for-byte stable
104
+ // (charter P4); the prior 88 B host tail pad shrinks to 64 B, total stays
105
+ // View slot remains within the existing 1024 B stride. lighting-directional.wgsl drives the
106
+ // directional shadow bias (D-1: bias = max(normalBias*(1-N.L), depthBias)) and
107
+ // a pcfKernelSize-wide PCF loop from these fields.
108
+ //
109
+ // feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
110
+ // the per-spot perspective `spotLightViewProj` matrices (4 lanes, fragment-read)
111
+ // fold into the View UBO tail instead of a standalone @group(0) binding 9
112
+ // uniform buffer. The standalone binding pushed the fragment-stage uniform
113
+ // buffer count to 12 (view + pointLights + spotLights + shadowParams +
114
+ // shadowCasterCascade + spotLightViewProj on the WebGL2 fallback path),
115
+ // exceeding GLES 3.0's `max_uniform_buffers_per_shader_stage = 11` so the
116
+ // WebGL2 fallback (mobile / WebKit — this feat's compat target) crashed
117
+ // pipeline-layout creation. Folding into the View UBO costs zero new bindings
118
+ // (the count drops back to 11). D-1 rejected the View UBO for the CASTER vertex
119
+ // channel (binding 7) because directional + spot casters both need a light-view
120
+ // matrix in the same frame (same-frame write contention); the FRAGMENT read
121
+ // channel has no such contention — the host writes all <=4 spot matrices once
122
+ // per frame before the forward pass, so the fold is safe. mat4 array align=16:
123
+ // the array lands at byte 528 (next 16 B-aligned offset after pcfKernelSize at
124
+ // byte 512..516), spanning bytes 528..784. The host allocates VIEW_UBO_BYTES =
125
+ // 960 (renderer-factory.ts) and writes the full 240 f32
126
+ // payload (render-system-record.ts). Lane N = the spot with `shadowAtlasTile
127
+ // === N` (cap = 4); `evalSpotShadowed` reads `view.spotLightViewProj[tile]`.
128
+ struct FogViewParams {
129
+ color : vec3<f32>,
130
+ density : f32,
131
+ heightFalloff : f32,
132
+ maxOpacity : f32,
133
+ };
134
+
135
+ struct FogRay {
136
+ origin : vec3<f32>,
137
+ direction : vec3<f32>,
138
+ distance : f32,
139
+ };
140
+
141
+ struct View {
142
+ worldViewProj : mat4x4<f32>,
143
+ lightDir : vec3<f32>,
144
+ lightColor : vec3<f32>,
145
+ cameraPos : vec3<f32>,
146
+ lightViewProj_A : mat4x4<f32>,
147
+ inverseViewProj : mat4x4<f32>,
148
+ lightViewProj_B : mat4x4<f32>,
149
+ lightViewProj_C : mat4x4<f32>,
150
+ lightViewProj_D : mat4x4<f32>,
151
+ splitPlanes : array<vec4<f32>,4>,
152
+ cascadeCount : f32,
153
+ cascadeBlend : f32,
154
+ depthBias : f32,
155
+ normalBias : f32,
156
+ pcfKernelSize : f32,
157
+ // feat-20260625-spot-light-shadow-mapping w25: per-spot perspective
158
+ // light-view-projection matrices, fragment-read. Auto-aligned to byte 528
159
+ // (mat4 array align=16, after pcfKernelSize at byte 512); spans bytes
160
+ // 528..784. Lane N = spot with shadowAtlasTile === N (cap = 4). Zeroed lanes
161
+ // are safe (sample gated on shadowAtlasTile >= 0 in default-standard-pbr.wgsl).
162
+ spotLightViewProj : array<mat4x4<f32>, 4>,
163
+ temporalCurrentViewProj : mat4x4<f32>,
164
+ temporalPreviousViewProj : mat4x4<f32>,
165
+ // x = near, y = far, z = 1 for orthographic and 0 for perspective.
166
+ temporalProjection : vec4<f32>,
167
+ fog : FogViewParams,
168
+ };
169
+
170
+ // Per-instance mesh slot (feat-20260518-pbr-direct-lighting-mvp M2 / w8.5,
171
+ // plan-strategy D-5 + AC-08). `normalMatrix` is the host-precomputed
172
+ // transpose(inverse(mat3(worldFromLocal))) so the fragment stage avoids the
173
+ // 9-mul shader-side inverse path; per-frame per-entity CPU computation
174
+ // (single mat4 -> mat3 -> invert+transpose) is one-shot and feeds the
175
+ // world-space normal mapping path in pbr.wgsl. Storage layout: mat4 in
176
+ // [0, 64), mat3 columns in [64, 112) padded as three vec4 (16 B each = 48 B
177
+ // total), trailing slack inside the PER_ENTITY_STRIDE = 256 B slot.
178
+ struct Mesh {
179
+ worldFromLocal : mat4x4<f32>,
180
+ normalMatrix : mat3x3<f32>,
181
+ #if STORAGE_BUFFER_AVAILABLE == true
182
+ previousWorldFromLocal : mat4x4<f32>,
183
+ // x = conservative reactive mask. The remaining lanes are reserved and
184
+ // zero so one vec4 remains the sole host/shader metadata ABI.
185
+ temporal : vec4<f32>,
186
+ #endif
187
+ };
188
+
189
+ // feat-20260519-light-casters-point-spot-pbr M4 / w21 (D-S1 + D-S2 +
190
+ // AC-04 b/c + AC-05 binding declaration). Punctual-light std430 storage
191
+ // types byte-for-byte mirror packPointLight / packSpotLight in
192
+ // `packages/runtime/src/light-buffer-layout.ts`:
193
+ //
194
+ // PointLight (32 B / 8 floats):
195
+ // [ 0..2 ] position vec3<f32>
196
+ // [ 3 ] invRangeSquared f32 (Bevy color_inverse_square_range.w; 0
197
+ // collapses range falloff to a pure 1/d^2 inverse-square law)
198
+ // [ 4..6 ] colorTimesIntensity vec3<f32> (host pre-multiplied
199
+ // color * intensity so the shader avoids the per-fragment mul)
200
+ // [ 7 ] pad f32 = 0
201
+ //
202
+ // SpotLight (64 B / 16 floats):
203
+ // [ 0..2 ] position vec3<f32>
204
+ // [ 3 ] invRangeSquared f32
205
+ // [ 4..6 ] colorTimesIntensity vec3<f32>
206
+ // [ 7 ] cosInner f32 (cos of half-angle inner cone; KHR
207
+ // smoothstep falloff anchor)
208
+ // [ 8..10] direction vec3<f32> (raw outgoing vector; shader reads
209
+ // via dot(L, -direction) for cone angle test)
210
+ // [ 11 ] cosOuter f32
211
+ // [12..14] spotPad vec3<f32> = 0 (std430 vec4-alignment padding)
212
+ // [ 15 ] shadowAtlasTile i32 (sentinel -1 = unassigned/clipped,
213
+ // 0..3 = spot atlas tile index; feat-20260625 M2/M3 D-4)
214
+ //
215
+ // WGSL std430 alignment audit: vec3<f32> alignof=16 / sizeof=12. The
216
+ // f32 lane wedged immediately after each vec3 fills the 4 B remainder
217
+ // (WGSL struct member rule: next.offset = roundUp(prev.offset +
218
+ // prev.size, this.alignof) - for f32 alignof=4 the round-up is a
219
+ // no-op, so position[3] / color[3] / direction[3] sit at byte
220
+ // offsets 12 / 28 / 44 with zero internal padding). PointLight stays
221
+ // 32 B; SpotLight grows to 64 B (feat-20260625 M2/M3 D-4): the trailing
222
+ // `spotPad: vec3<f32>` + `shadowAtlasTile: i32` form one vec4-aligned block
223
+ // at bytes 48..64, with `shadowAtlasTile` at byte 60. These struct sizes
224
+ // match the host packers (packPointLight 32 B / packSpotLight 64 B) exactly.
225
+ //
226
+ // Header `count: u32` lives in a wrapper struct with the trailing
227
+ // runtime-sized array. Array-of-vec4-aligned-struct alignof = 16, so
228
+ // `slots` starts at byte 16 (count u32 occupies 0..4; bytes 4..16 are
229
+ // implicit padding). This matches `packLightArrayHeader`'s 16 B
230
+ // (count u32 + 12 B pad) layout in light-buffer-layout.ts.
231
+
232
+ struct PointLight {
233
+ position : vec3<f32>,
234
+ invRangeSquared : f32,
235
+ colorTimesIntensity : vec3<f32>,
236
+ // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-8 (plan-strategy §D-2):
237
+ // Repurposes the prior `pointPadW` (f32 zero-pad) lane as `shadowAtlasLayer`
238
+ // (i32 sentinel, -1 = no shadow, 0..3 = cube_array atlas layer index for
239
+ // shadow-casting point lights). Cap = 4 enforced by the PointLightShadow
240
+ // ECS component cardinality. The shader path samples the cube_array atlas
241
+ // (binding 5) only when `shadowAtlasLayer >= 0`. Field name is unique to
242
+ // this struct (no `pad0`/`pad1` collision with the pbr Material struct
243
+ // under naga_oil writeback substitution).
244
+ shadowAtlasLayer : i32,
245
+ };
246
+
247
+ struct SpotLight {
248
+ position : vec3<f32>,
249
+ invRangeSquared : f32,
250
+ colorTimesIntensity : vec3<f32>,
251
+ cosInner : f32,
252
+ direction : vec3<f32>,
253
+ cosOuter : f32,
254
+ // feat-20260625-spot-light-shadow-mapping M3 / w13 (plan-strategy D-4):
255
+ // The prior 8-lane (48 B) SpotLight had no spare pad lane (unlike PointLight
256
+ // which repurposed `pointPadW`), so the clip-signal field needs a fresh
257
+ // vec4-aligned block. `spotPad` fills bytes 48..60 (host packSpotLight slots
258
+ // 12..14 stay zero); `shadowAtlasTile` (i32 sentinel, -1 = no shadow /
259
+ // clipped, 0..3 = spot atlas tile) lands at byte 60 (host slot 15, written
260
+ // through an Int32Array view). The fragment path gates on
261
+ // `shadowAtlasTile >= 0` (evalSpotShadowed vs evalSpot). Field names are
262
+ // unique across the composed module surface so naga_oil writeback
263
+ // substitution does not collide with prior `pad0` members. (naga_oil
264
+ // reserves the `__` identifier prefix, so the pad lane is `spotPad`, not
265
+ // `__pad`.)
266
+ spotPad : vec3<f32>,
267
+ shadowAtlasTile : i32,
268
+ };
269
+
270
+ struct PointLightsArray {
271
+ count : u32,
272
+ slots : array<PointLight, 4>,
273
+ };
274
+
275
+ struct SpotLightsArray {
276
+ count : u32,
277
+ slots : array<SpotLight, 4>,
278
+ };
279
+
280
+ @group(0) @binding(0) var<uniform> view : View;
281
+ #if STORAGE_BUFFER_AVAILABLE == true
282
+ @group(0) @binding(1) var<storage, read> pointLightsBuffer : PointLightsArray;
283
+ @group(0) @binding(2) var<storage, read> spotLightsBuffer : SpotLightsArray;
284
+ #else
285
+ @group(0) @binding(1) var<uniform> pointLightsBuffer : PointLightsArray;
286
+ @group(0) @binding(2) var<uniform> spotLightsBuffer : SpotLightsArray;
287
+ #endif
288
+ // feat-20260520-directional-light-shadow-mapping M3 / w16 (D-1 / plan-strategy §8.1):
289
+ // shadowMap + shadowSampler consume @group(0) bindings 3/4 (smallest unused
290
+ // slots adjacent to view UBO at 0). M3 uses 3x3 PCF with textureLoad on
291
+ // shadowMap for 9-tap depth sampling; shadowSampler retained for GPU probe.
292
+ //
293
+ // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-8 (plan-strategy §D-1 + §D-8):
294
+ // Binding 5 declares the cube_array shadow atlas (texture_depth_cube_array,
295
+ // layers=4, depth32float; one cube per shadow-casting point light, cap=4).
296
+ // Binding 6 declares the per-light shadow params buffer (URP only — proj
297
+ // constants for cube depth-ref reconstruction); HDRP rides the same constants
298
+ // on LightSlot pad lanes (.kind_and_pad.wyz) so binding 6 stays unbound on
299
+ // the HDRP path. Sample-time gating by PointLight.shadowAtlasLayer >= 0.
300
+ @group(0) @binding(3) var shadowMap : texture_depth_2d;
301
+ @group(0) @binding(4) var shadowSampler : sampler_comparison;
302
+ // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-7 (plan-strategy §D-1 + §D-8):
303
+ // Bindings 5 + 6 carry the cube_array shadow atlas + per-light shadow params
304
+ // buffer. URP forward consumers (this file's `forgeax_view::common`) declare
305
+ // them only when the `POINT_SHADOW_AVAILABLE` naga_oil define is true so
306
+ // material shaders that share the URP BGL but do not yet require shadows
307
+ // keep validating with the lean BGL shape (binding 0..4) — the runtime
308
+ // `viewBindGroupLayout` in createRenderer.ts is the matching SSOT.
309
+ //
310
+ // Cube_array depth atlas (binding 5):
311
+ // `texture_depth_cube_array` with `layers = 4` (= PointLightShadow
312
+ // cardinality cap). Sampled via `sample_shadow_cube_hw2x2`
313
+ // (forgeax_pbr::shadow_pcf) in lighting-punctual.wgsl when
314
+ // `PointLight.shadowAtlasLayer >= 0`.
315
+ //
316
+ // Per-light shadow params (binding 6):
317
+ // `array<vec4<f32>, 4>` carrying the URP-side proj constants for cube
318
+ // depth-ref reconstruction (research L0.5 + L1.13). Each lane stores the
319
+ // shader-side reconstruction constants for one shadow-casting point light;
320
+ // slot N matches `PointLight.shadowAtlasLayer = N`. HDRP rides the same
321
+ // constants on `LightSlot.kind_and_pad.wyz` so binding 6 stays unbound on
322
+ // the HDRP path.
323
+ #ifdef POINT_SHADOW_AVAILABLE
324
+ @group(0) @binding(5) var shadowAtlas : texture_depth_cube_array;
325
+ @group(0) @binding(6) var<uniform> shadowParams : array<vec4<f32>, 4>;
326
+ #endif
327
+
328
+ // feat-20260613-csm-cascaded-shadow-maps M5 / w28: shadowCasterCascade is
329
+ // a per-pass uniform consumed exclusively by `shadow_caster.wgsl`. The
330
+ // host writes `index = i` (in 0..3) before each cascade's shadow pass so
331
+ // the shadow_caster vertex shader picks the matching `view.lightViewProj_X`
332
+ // for that pass. Forward material shaders declare the binding (it lives
333
+ // in the shared view BGL) but never reference it -- WGSL accepts unused
334
+ // bindings. The `index` is a u32; the three trailing u32 lanes keep the
335
+ // struct at the 16-byte uniform-buffer alignment WebGL2 requires.
336
+ //
337
+ // Binding 7 (not 5) on 2026-06-13: bindings 5/6 went to point-shadow
338
+ // (cube_array atlas + params UBO; FRAGMENT-only) so the cascade UBO —
339
+ // which needs VERTEX|FRAGMENT visibility — moved to the next free slot.
340
+ // `pbr-pipeline.ts buildPbrViewBglEntries` is the matching SSOT.
341
+ // feat-20260625-spot-light-shadow-mapping M2 / w10 (D-1): the cascade UBO
342
+ // gains a spot-specific perspective light-view-projection matrix + an `isSpot`
343
+ // discriminant. The spot shadow pass writes its own perspective matrix into
344
+ // `spotLightViewProj` + sets `isSpot = 1u` before each tile pass; the
345
+ // shadow_caster vertex shader then routes through the spot matrix instead of
346
+ // the directional `view.lightViewProj_A..D` cascade slots. This keeps the spot
347
+ // matrix OUT of the directional View UBO (no same-frame write contention —
348
+ // directional and spot both need light-view-proj matrices in one frame). The
349
+ // `index` lane is unused on the spot path (spot tile order is tracked
350
+ // host-side via depthLoadOp clear/load counting, decoupled from directional
351
+ // cascadeIndex per D-2).
352
+ struct ShadowCasterCascade {
353
+ index : u32,
354
+ // feat-20260625 M2 / w10 (D-1): spot-path discriminant (0 = directional
355
+ // cascade, 1 = spot perspective). Repurposes the former `shadowCasterPadA`
356
+ // lane. The shadow_caster vertex shader branches on this.
357
+ isSpot : u32,
358
+ // Two trailing u32 pad lanes (zero-initialised host-side) keep `index`/
359
+ // `isSpot` in the first 16 B vec4 so the `mat4x4<f32>` below starts at a
360
+ // 16 B-aligned offset (WebGL2 GLES 3.0 requirement). Field names are unique
361
+ // across the composed module surface so naga_oil's writeback substitution
362
+ // does not collide with prior `pad0` / `pad1` members in other shared
363
+ // structs (Material, SkylightUniforms).
364
+ shadowCasterPadB : u32,
365
+ shadowCasterPadC : u32,
366
+ // feat-20260625 M2 / w10 (D-1): spot perspective light-view-projection
367
+ // matrix (perspective(outerCone*2) x lookAt). Written per spot tile pass.
368
+ // Ignored on the directional path (isSpot = 0).
369
+ spotLightViewProj : mat4x4<f32>,
370
+ };
371
+ @group(0) @binding(7) var<uniform> shadowCasterCascade : ShadowCasterCascade;
372
+
373
+ // feat-20260625-spot-light-shadow-mapping M3 / w13 (plan-strategy D-5):
374
+ // Spot shadow atlas (single 2D depth texture; the host packs up to 4 spot
375
+ // shadows into a 2x2 tile grid — see urp-pipeline.ts spotShadowDepth). Bound
376
+ // at @group(0) binding 8 (smallest free slot after the cascade UBO at 7).
377
+ //
378
+ // ALWAYS-ON, no #ifdef gate (unlike the point cube_array atlas at binding 5,
379
+ // which rides POINT_SHADOW_AVAILABLE because cube_array is unavailable in the
380
+ // WebGPU compat profile). Spot shadows are `texture_depth_2d` only, which is
381
+ // compat-safe everywhere, so the binding is unconditionally declared — keeping
382
+ // the BGL <-> WGSL alignment surface minimal (D-5: a define axis here would be
383
+ // pure burden). The matching runtime SSOT is `buildPbrViewBglEntries`
384
+ // (pbr-pipeline.ts) which declares binding 8 unconditionally.
385
+ //
386
+ // The comparison sampler is REUSED from binding 4 (`shadowSampler`): spot
387
+ // perspective-depth sampling needs the exact same descriptor (clamp-to-edge +
388
+ // compare less) as directional/point, so no binding 9 sampler is introduced.
389
+ // The per-spot perspective lightViewProj matrices that the forward (fragment)
390
+ // shadow sample needs live in the View UBO (binding 0, `view.spotLightViewProj`)
391
+ // — folded there in feat-20260625 w25 (scope-amend) to keep the WebGL2 fallback
392
+ // fragment uniform-buffer count <= 11; binding 8 is the last view-BG binding.
393
+ @group(0) @binding(8) var spotShadowMap : texture_depth_2d;
394
+
395
+ #if STORAGE_BUFFER_AVAILABLE == true
396
+ @group(2) @binding(0) var<storage, read> meshes : array<Mesh>;
397
+ #else
398
+ @group(2) @binding(0) var<uniform> meshes : array<Mesh, 128>;
399
+ #endif
400
+
401
+ // feat-20260604-instances-per-instance-transform-shader-group3-bin M1 / w3:
402
+ // Per-instance local transform (column-major mat4, 64 B per entry).
403
+ // Byte-for-byte isomorphic to the record-stage packed mat4 write in
404
+ // render-system-record.ts (inst.transforms, stride-16 row-major 16-float
405
+ // per-instance) and the identity 16-float mat4 in createRenderer.ts.
406
+ // @group(3) is the per-instance storage buffer — already uploaded by
407
+ // the record stage at render-system-record.ts:2958, bound via
408
+ // setBindGroup(3, instancesBg) at :2985, and drawn with
409
+ // drawIndexed(indexCount, instanceCount, 0, 0) at :2898.
410
+ //
411
+ // Reuses the STORAGE_BUFFER_AVAILABLE axis (D-2) — the uniform fallback
412
+ // array<InstanceData, 128> caps at MAX_UNIFORM_INSTANCES=128, same as
413
+ // the meshes array. No new INSTANCE_STORAGE_AVAILABLE axis.
414
+ //
415
+ // feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M2 / w7
416
+ // (plan-strategy §2 D-1 + §2 D-4 + research §Q-R-4.1-4.4): the
417
+ // PER_INSTANCE_REGION axis is declared in sprite.wgsl ONLY (D-4 keeps
418
+ // pbr / unlit at their existing 2-variant count). When the sprite
419
+ // pipeline composes this module with `PER_INSTANCE_REGION = true`, the
420
+ // InstanceData struct grows from 64 B (mat4) to 80 B (mat4 + vec4) so
421
+ // the record stage can interleave per-instance UV regions into the same
422
+ // single GPU buffer that already carries the per-instance mat4 (single
423
+ // binding slot, BGL zero-modification per D-R-4). The conditional sits
424
+ // at the tail of the struct so the pre-feat 64 B layout stays
425
+ // byte-identical when PER_INSTANCE_REGION is undefined (sprite-atlas
426
+ // + every non-sprite material).
427
+ //
428
+ // Uniform-fallback safety (R-6): worst case is
429
+ // 80 B × MAX_UNIFORM_INSTANCES (128) = 10240 B
430
+ // which is below the WebGL2 UBO floor of 16384 B; the 128 cap survives
431
+ // the stride bump.
432
+ struct InstanceData {
433
+ localFromInstance : mat4x4<f32>,
434
+ #if STORAGE_BUFFER_AVAILABLE == true
435
+ previousLocalFromInstance : mat4x4<f32>,
436
+ #endif
437
+ #if PER_INSTANCE_REGION == true
438
+ // Per-instance atlas region: .xy = (uMin, vMin), .zw = (uW, vH). Pairs
439
+ // with the legacy `material.region` UBO field (sprite.wgsl Material
440
+ // struct, 16 B at byte offset 16). Sprite vs_main picks between the
441
+ // material-level fallback (legacy, single-material-per-draw) and the
442
+ // per-instance value via the same `#if PER_INSTANCE_REGION == true`
443
+ // gate so a single sprite.wgsl source supports both shapes.
444
+ region : vec4<f32>,
445
+ #endif
446
+ };
447
+
448
+ fn sceneTemporalUv(clip : vec4<f32>) -> vec2<f32> {
449
+ let safeW = select(1e-6, clip.w, abs(clip.w) >= 1e-6);
450
+ let ndc = clip.xy / safeW;
451
+ return vec2<f32>(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
452
+ }
453
+
454
+ fn sceneTemporalViewDepth(clip : vec4<f32>) -> f32 {
455
+ let perspectiveDepth = max(clip.w, 0.0);
456
+ let ndcDepth = clip.z / max(abs(clip.w), 1e-6);
457
+ let orthographicDepth = view.temporalProjection.x +
458
+ ndcDepth * (view.temporalProjection.y - view.temporalProjection.x);
459
+ let viewDepth = select(
460
+ perspectiveDepth,
461
+ max(orthographicDepth, 0.0),
462
+ view.temporalProjection.z >= 0.5,
463
+ );
464
+ return log2(1.0 + viewDepth);
465
+ }
466
+
467
+ fn packSceneTemporal(
468
+ currentClip : vec4<f32>,
469
+ previousClip : vec4<f32>,
470
+ reactive : f32,
471
+ ) -> vec4<f32> {
472
+ return vec4<f32>(
473
+ sceneTemporalUv(currentClip) - sceneTemporalUv(previousClip),
474
+ sceneTemporalViewDepth(currentClip),
475
+ clamp(reactive, 0.0, 1.0),
476
+ );
477
+ }
478
+ #if STORAGE_BUFFER_AVAILABLE == true
479
+ @group(3) @binding(0) var<storage, read> instances : array<InstanceData>;
480
+ #else
481
+ @group(3) @binding(0) var<uniform> instances : array<InstanceData, 128>;
482
+ #endif
483
+
484
+ // Fullscreen large-triangle SSOT (feat-20260519-tonemap-reinhard-mvp / T-M2.1,
485
+ // research F3 section 2.3). 3 vertices in clip-space:
486
+ // index 0 -> (-1, -1) bottom-left
487
+ // index 1 -> ( 3, -1) beyond right edge
488
+ // index 2 -> (-1, 3) beyond top edge
489
+ // The triangle fully covers [-1, 1]^2 NDC; the rasterizer clips the
490
+ // out-of-range portion. UV is derived from xy with Y flipped so the
491
+ // downstream textureSample returns the right-side-up image (WebGPU
492
+ // convention: UV (0, 0) is top-left of the texture).
493
+ //
494
+ // Consumers (tonemap.wgsl and any future post-process pass) call this
495
+ // in their @vertex stage:
496
+ // @vertex fn vs(@builtin(vertex_index) i : u32) -> FullscreenOutput {
497
+ // return fullscreen_triangle(i);
498
+ // }
499
+ //
500
+ // Keep the TS port in
501
+ // `packages/shader/src/__tests__/fullscreen-triangle.test.ts` in lockstep.
502
+
503
+ struct FullscreenOutput {
504
+ @builtin(position) position : vec4<f32>,
505
+ @location(0) uv : vec2<f32>,
506
+ };
507
+
508
+ fn fullscreen_triangle(vertex_index : u32) -> FullscreenOutput {
509
+ var x : f32 = -1.0;
510
+ var y : f32 = -1.0;
511
+ if (vertex_index == 1u) {
512
+ x = 3.0;
513
+ }
514
+ if (vertex_index == 2u) {
515
+ y = 3.0;
516
+ }
517
+ let u : f32 = (x + 1.0) * 0.5;
518
+ let v : f32 = 1.0 - (y + 1.0) * 0.5;
519
+ var out : FullscreenOutput;
520
+ out.position = vec4<f32>(x, y, 0.0, 1.0);
521
+ out.uv = vec2<f32>(u, v);
522
+ return out;
523
+ }