@forgeax/engine-render 0.1.19 → 0.1.20

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 (174) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assembly/factory.d.ts +16 -5
  3. package/dist/assembly/factory.d.ts.map +1 -1
  4. package/dist/{chunk-RJRUS55L.mjs → chunk-4XINDLAY.mjs} +267 -186
  5. package/dist/chunk-4XINDLAY.mjs.map +1 -0
  6. package/dist/{chunk-YTQ5C7YQ.mjs → chunk-CDTWTHZR.mjs} +176 -8
  7. package/dist/chunk-CDTWTHZR.mjs.map +1 -0
  8. package/dist/{chunk-IPXCABEP.mjs → chunk-M2MP4ZOU.mjs} +2 -2
  9. package/dist/chunk-M2MP4ZOU.mjs.map +1 -0
  10. package/dist/{chunk-LOSO4CTV.mjs → chunk-TBQJOK2L.mjs} +67 -3
  11. package/dist/chunk-TBQJOK2L.mjs.map +1 -0
  12. package/dist/components/light-helpers.d.ts.map +1 -1
  13. package/dist/components/spot-light.d.ts +12 -0
  14. package/dist/components/spot-light.d.ts.map +1 -1
  15. package/dist/construct-renderer.mjs +2281 -164
  16. package/dist/construct-renderer.mjs.map +1 -1
  17. package/dist/device/gpu-residency.d.ts +19 -2
  18. package/dist/device/gpu-residency.d.ts.map +1 -1
  19. package/dist/errors/render.d.ts +69 -1
  20. package/dist/errors/render.d.ts.map +1 -1
  21. package/dist/gpu-usage.d.ts +2 -0
  22. package/dist/gpu-usage.d.ts.map +1 -1
  23. package/dist/index.d.ts +7 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +3 -3
  26. package/dist/internal.mjs +1 -1
  27. package/dist/light-buffer-layout.d.ts +20 -12
  28. package/dist/light-buffer-layout.d.ts.map +1 -1
  29. package/dist/pbr-pipeline.d.ts +6 -3
  30. package/dist/pbr-pipeline.d.ts.map +1 -1
  31. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  32. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  33. package/dist/pipeline/standard-profile.d.ts +13 -0
  34. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  35. package/dist/record/frame-lighting.d.ts +2 -0
  36. package/dist/record/frame-lighting.d.ts.map +1 -1
  37. package/dist/record/frame-snapshot.d.ts +37 -1
  38. package/dist/record/frame-snapshot.d.ts.map +1 -1
  39. package/dist/record/frame-targets.d.ts.map +1 -1
  40. package/dist/record/frame.d.ts +3 -2
  41. package/dist/record/frame.d.ts.map +1 -1
  42. package/dist/record/gpu-timing.d.ts +32 -0
  43. package/dist/record/gpu-timing.d.ts.map +1 -0
  44. package/dist/record/main-pass-material.d.ts +1 -1
  45. package/dist/record/main-pass-material.d.ts.map +1 -1
  46. package/dist/record/render-context.d.ts +5 -1
  47. package/dist/record/render-context.d.ts.map +1 -1
  48. package/dist/record/shadow-pass.d.ts.map +1 -1
  49. package/dist/record/typed-frame-graph.d.ts +11 -3
  50. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  51. package/dist/render-contract.d.ts +66 -1
  52. package/dist/render-contract.d.ts.map +1 -1
  53. package/dist/render-data.d.ts +10 -6
  54. package/dist/render-data.d.ts.map +1 -1
  55. package/dist/render-pipeline.d.ts +36 -0
  56. package/dist/render-pipeline.d.ts.map +1 -1
  57. package/dist/render-system-extract.d.ts +55 -3
  58. package/dist/render-system-extract.d.ts.map +1 -1
  59. package/dist/render-system.d.ts +7 -1
  60. package/dist/render-system.d.ts.map +1 -1
  61. package/dist/renderer-inspect.d.ts +29 -0
  62. package/dist/renderer-inspect.d.ts.map +1 -0
  63. package/dist/scene/render-scene.d.ts.map +1 -1
  64. package/dist/temporal/frame.d.ts +44 -0
  65. package/dist/temporal/frame.d.ts.map +1 -0
  66. package/dist/temporal/index.mjs +3 -3
  67. package/dist/typed-render-graph-primitives.d.ts +6 -1
  68. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  69. package/dist/typed-shadow-atlas.d.ts +10 -0
  70. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  71. package/dist/typed-shadow-passes.d.ts.map +1 -1
  72. package/dist/volume/capability.d.ts +57 -0
  73. package/dist/volume/capability.d.ts.map +1 -0
  74. package/dist/volume/component.d.ts +61 -0
  75. package/dist/volume/component.d.ts.map +1 -0
  76. package/dist/volume/extract.d.ts +11 -0
  77. package/dist/volume/extract.d.ts.map +1 -0
  78. package/dist/volume/inspection.d.ts +38 -0
  79. package/dist/volume/inspection.d.ts.map +1 -0
  80. package/dist/volume/passes.d.ts +38 -0
  81. package/dist/volume/passes.d.ts.map +1 -0
  82. package/dist/volume/recovery.d.ts +38 -0
  83. package/dist/volume/recovery.d.ts.map +1 -0
  84. package/dist/volume/resources.d.ts +57 -0
  85. package/dist/volume/resources.d.ts.map +1 -0
  86. package/dist/volume/temporal.d.ts +35 -0
  87. package/dist/volume/temporal.d.ts.map +1 -0
  88. package/package.json +20 -20
  89. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  90. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  91. package/src/__tests__/render-error-code-owner.test-d.ts +5 -0
  92. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +28 -0
  93. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  94. package/src/__tests__/skinned-shadow-caster.test.ts +21 -0
  95. package/src/__tests__/standard-profile-oracle.unit.test.ts +7 -5
  96. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  97. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  98. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  99. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  100. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  101. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  102. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  103. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  104. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  105. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  106. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  107. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  108. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  109. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  110. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  111. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  112. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  113. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  114. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  115. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  116. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  117. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  118. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  119. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  120. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  121. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  122. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  123. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  124. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  125. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  126. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  127. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  128. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  129. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  130. package/src/assembly/factory.ts +271 -15
  131. package/src/components/light-helpers.ts +4 -0
  132. package/src/components/spot-light.ts +22 -3
  133. package/src/device/gpu-residency.ts +168 -58
  134. package/src/errors/render.ts +119 -1
  135. package/src/gpu-usage.ts +2 -0
  136. package/src/index.ts +38 -1
  137. package/src/light-buffer-layout.ts +59 -26
  138. package/src/pbr-pipeline.ts +33 -10
  139. package/src/pipeline/standard-forward-lane.ts +20 -0
  140. package/src/pipeline/standard-pipeline.ts +20 -0
  141. package/src/pipeline/standard-profile.ts +23 -0
  142. package/src/record/frame-lighting.ts +103 -89
  143. package/src/record/frame-snapshot.ts +41 -1
  144. package/src/record/frame-targets.ts +6 -2
  145. package/src/record/frame.ts +908 -5
  146. package/src/record/gpu-timing.ts +224 -0
  147. package/src/record/main-pass-material.ts +6 -2
  148. package/src/record/render-context.ts +5 -0
  149. package/src/record/shadow-pass.ts +87 -38
  150. package/src/record/typed-frame-graph.ts +143 -8
  151. package/src/render-contract.ts +72 -0
  152. package/src/render-data.ts +50 -22
  153. package/src/render-pipeline.ts +44 -0
  154. package/src/render-system-extract.ts +329 -3
  155. package/src/render-system.ts +79 -0
  156. package/src/renderer-inspect.ts +66 -0
  157. package/src/scene/render-scene.ts +2 -0
  158. package/src/temporal/frame.ts +128 -0
  159. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +2 -3
  160. package/src/typed-render-graph-primitives.ts +32 -2
  161. package/src/typed-shadow-atlas.ts +17 -0
  162. package/src/typed-shadow-passes.ts +6 -5
  163. package/src/volume/capability.ts +173 -0
  164. package/src/volume/component.ts +110 -0
  165. package/src/volume/extract.ts +21 -0
  166. package/src/volume/inspection.ts +78 -0
  167. package/src/volume/passes.ts +680 -0
  168. package/src/volume/recovery.ts +86 -0
  169. package/src/volume/resources.ts +236 -0
  170. package/src/volume/temporal.ts +89 -0
  171. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  172. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  173. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  174. package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
@@ -15,23 +15,24 @@
15
15
  // PointLight in common.wgsl. -1 (0xFFFFFFFF) is the no-shadow
16
16
  // sentinel — shader path samples shadow only when layer >= 0.
17
17
  //
18
- // SpotLight (64 B / 16 floats):
18
+ // SpotLight (80 B / 20 floats):
19
19
  // [ 0..2 ] position vec3<f32>
20
20
  // [ 3 ] invRangeSquared f32
21
21
  // [ 4..6 ] color vec3<f32> (host-pre-multiplied)
22
22
  // [ 7 ] cosInner f32
23
23
  // [ 8..10] direction vec3<f32> (raw outgoing vector)
24
24
  // [ 11 ] cosOuter f32
25
- // [12..14] pad f32 = 0 (std430 vec4 stride alignment)
25
+ // [12] depthBias, [13] normalBias, [14] pcfKernelSize
26
26
  // [ 15 ] shadowAtlasTile i32 (sentinel -1 = unassigned/clipped; 0..3 = tile)
27
27
  // — feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4):
28
- // SpotLight GPU struct had no free pad lane (8 lanes full), so the
29
- // clip signal extends the struct to a 4th vec4 column. The lane is
30
- // written i32 via an Int32Array view of the same backing
28
+ // SpotLight receiver controls occupy the 4th vec4 column. The tile
29
+ // lane is written i32 via an Int32Array view of the same backing
31
30
  // ArrayBuffer; the shader reads `shadowAtlasTile: i32` from byte
32
31
  // 60..64 of struct SpotLight (common.wgsl). -1 (0xFFFFFFFF) is the
33
32
  // unassigned/clipped sentinel — shader samples shadow only when
34
33
  // tile >= 0. Mirrors point's slot-7 shadowAtlasLayer pattern.
34
+ // [ 16 ] shadowIntensity f32 (default 1; [0,1] residual visibility)
35
+ // [17..19] std430 tail pad (the struct rounds to the next 16 B boundary)
35
36
  //
36
37
  // Array header (16 B, std430 stride alignment):
37
38
  // [ 0 ] count u32 (number of valid slots in the trailing array)
@@ -69,13 +70,16 @@ import type { PointLightSnapshot, SpotLightSnapshot } from './render-system-extr
69
70
  // [ 11 ] cosOuter f32 (point: 0.0)
70
71
  // [ 12 ] kind u32 (POINT = 0, SPOT = 1)
71
72
  // [ 13 ] shadow identity i32 (point layer / spot tile, -1 sentinel)
72
- // [14..15] point depth f32x2 (point near/far; spot zero)
73
+ // [14] point near / spot shadowIntensity f32
74
+ // [15] point far / spot pcfKernelSize f32
73
75
  //
74
76
  // Design: research Finding 1 (Bevy GpuClusteredLight 152B -> forgeax 64B),
75
77
  // plan-strategy D-light-slot (kind bit-tag discriminant).
76
78
  //
77
- // The existing URP PointLight (32B) / SpotLight (48B) pack functions remain
78
- // untouched; packLightSlot is the new HDRP-only entry.
79
+ // The existing URP PointLight (32B) pack function remains unchanged; the URP
80
+ // SpotLight grows from 64B to 80B to carry the independent shadow visibility
81
+ // factor without stealing the tile or PCF lanes. packLightSlot remains the
82
+ // compact 64B HDRP owner and uses a spot-only pad lane for the same value.
79
83
 
80
84
  /** Byte size of one HDRP LightSlot in std430 (16 x f32). AC-11 absolute-value lock. */
81
85
  export const BYTES_PER_LIGHT_SLOT = 64;
@@ -99,9 +103,11 @@ export type LightSlotKind = (typeof LightSlotKind)[keyof typeof LightSlotKind];
99
103
  * feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
100
104
  * the prior u32x3 pad lanes at byte 52..64 carry shadow-side data on the HDRP
101
105
  * path. `kind_and_pad.x = kind` (u32), `.y = shadow identity` (i32, sentinel
102
- * -1 = no shadow), `.z = near` (f32 via bitcast), `.w = far`
103
- * (f32 via bitcast). The shader reads near/far through `bitcast<f32>(...)`
104
- * to keep the WGSL struct as `vec4<u32>` (alignment + naga_oil unchanged).
106
+ * -1 = no shadow). For point lights `.z/.w` carry near/far (f32 via bitcast);
107
+ * for spot lights `.z` carries shadowIntensity and `.w` carries the PCF
108
+ * kernel width (f32 via bitcast). The shader reads these lanes through
109
+ * `bitcast<f32>(...)` to
110
+ * keep the WGSL struct as `vec4<u32>` (alignment + naga_oil unchanged).
105
111
  */
106
112
  export const LIGHTSLOT_LAYOUT = {
107
113
  byteSize: BYTES_PER_LIGHT_SLOT,
@@ -117,6 +123,7 @@ export const LIGHTSLOT_LAYOUT = {
117
123
  shadowAtlasTileOffset: 52,
118
124
  shadowNearOffset: 56,
119
125
  shadowFarOffset: 60,
126
+ spotPcfKernelSizeOffset: 60,
120
127
  floatCount: 16,
121
128
  vec4Count: 4,
122
129
  } as const;
@@ -137,16 +144,18 @@ export const STORAGE_BUFFER_MIN_REQUIRED = 4;
137
144
  /** Byte size of one PointLight std430 slot (vec4 * 2). */
138
145
  export const POINT_LIGHT_STD430_BYTES = 32;
139
146
  /**
140
- * Byte size of one SpotLight std430 slot (vec4 * 4).
147
+ * Byte size of one SpotLight std430 slot (five 16-byte columns).
141
148
  *
142
- * feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4): grew 48 -> 64 to make
143
- * room for the `shadowAtlasTile: i32` clip-signal lane (slot 15). Slots 0..11
144
- * keep the prior layout; slots 12..14 are vec4-alignment padding.
149
+ * feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4) grew 48 -> 64 for
150
+ * `shadowAtlasTile`; the visual-parity contract then adds a fifth column for
151
+ * `shadowIntensity` rather than overloading the tile/PCF lanes.
145
152
  */
146
- export const SPOT_LIGHT_STD430_BYTES = 64;
153
+ export const SPOT_LIGHT_STD430_BYTES = 80;
147
154
 
148
155
  /** Sentinel value for SpotLight slot[15] meaning "no shadow / clipped". */
149
156
  export const SPOT_LIGHT_SHADOW_TILE_SENTINEL = -1;
157
+ /** Sentinel for a projector-only SpotLight with no shadow atlas tile. */
158
+ export const SPOT_LIGHT_PROJECTOR_ONLY_SENTINEL = -2;
150
159
  /** Byte size of the array header (count u32 + 12 B pad to 16 B stride). */
151
160
  export const LIGHT_ARRAY_HEADER_BYTES = 16;
152
161
  /** Maximum number of slots packed in the first-slice cap layout (D-S3). */
@@ -182,7 +191,7 @@ export function packPointLight(snap: PointLightSnapshot): Float32Array {
182
191
  }
183
192
 
184
193
  /**
185
- * Pack one SpotLightSnapshot into 16 floats (64 B byte-for-byte std430).
194
+ * Pack one SpotLightSnapshot into 20 floats (80 B byte-for-byte std430).
186
195
  *
187
196
  * cosInner / cosOuter ride the otherwise-padding `.w` lanes of the
188
197
  * `color` and `direction` vec4 columns (AC-04 c + plan D-S2 16 B alignment
@@ -190,10 +199,13 @@ export function packPointLight(snap: PointLightSnapshot): Float32Array {
190
199
  * (sentinel -1 = unassigned/clipped, 0..3 = atlas tile) written through an
191
200
  * Int32Array view so the shader's `i32` read at byte 60..64 of struct SpotLight
192
201
  * (common.wgsl) sees the correct two's-complement bits (-1 = 0xFFFFFFFF).
193
- * Slots 12..14 stay zero (vec4-alignment padding). feat-20260625 M2 / w8 (D-4).
202
+ * Slots 12..14 carry the shared receiver controls; slot 15 remains the i32
203
+ * shadow tile sentinel; slot 16 carries shadowIntensity and slots 17..19 are
204
+ * alignment tail padding. Surface and volume consumers therefore share one
205
+ * extracted shadow configuration.
194
206
  */
195
207
  export function packSpotLight(snap: SpotLightSnapshot): Float32Array {
196
- const out = new Float32Array(16);
208
+ const out = new Float32Array(20);
197
209
  out[0] = snap.position[0] ?? 0;
198
210
  out[1] = snap.position[1] ?? 0;
199
211
  out[2] = snap.position[2] ?? 0;
@@ -206,10 +218,20 @@ export function packSpotLight(snap: SpotLightSnapshot): Float32Array {
206
218
  out[9] = snap.direction[1] ?? 0;
207
219
  out[10] = snap.direction[2] ?? 0;
208
220
  out[11] = snap.cosOuter;
209
- // Slots 12..14 stay zero (vec4-alignment padding).
221
+ out[12] = snap.depthBias ?? 0.005;
222
+ out[13] = snap.normalBias ?? 0.05;
223
+ out[14] = snap.pcfKernelSize ?? 3;
210
224
  // Slot 15: shadowAtlasTile i32 via Int32Array view (sentinel -1 default).
211
225
  const i32 = new Int32Array(out.buffer);
212
- i32[15] = snap.shadowAtlasTile ?? SPOT_LIGHT_SHADOW_TILE_SENTINEL;
226
+ i32[15] =
227
+ snap.shadowAtlasTile >= 0
228
+ ? snap.shadowAtlasTile
229
+ : snap.projectorAsset !== undefined && snap.lightViewProj !== undefined
230
+ ? SPOT_LIGHT_PROJECTOR_ONLY_SENTINEL
231
+ : SPOT_LIGHT_SHADOW_TILE_SENTINEL;
232
+ // Slot 16 begins the fifth vec4 column. The remaining tail lanes stay zero
233
+ // so the std430 array stride is the explicit 80-byte contract.
234
+ out[16] = snap.shadowIntensity ?? 1;
213
235
  return out;
214
236
  }
215
237
 
@@ -275,8 +297,8 @@ export interface PointShadowSlotInfo {
275
297
  * feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
276
298
  * the optional point `shadow` info threads through to bytes 52..64 as
277
299
  * `(shadowAtlasLayer i32, near f32, far f32)`. Spot snapshots write their
278
- * `shadowAtlasTile` directly into the same identity lane and keep near/far 0.
279
- * The shader reads `near`/`far`
300
+ * `shadowAtlasTile` directly into the same identity lane, put shadowIntensity
301
+ * in the `.z` lane, and the PCF kernel width in `.w`. The shader reads `near`/`far`
280
302
  * through `bitcast<f32>(kind_and_pad.zw)` so the WGSL struct stays
281
303
  * `kind_and_pad: vec4<u32>` (alignment + naga_oil compose unchanged). When
282
304
  * `shadow` is undefined (no PointLightShadow) the lanes default to
@@ -309,15 +331,26 @@ export function packLightSlot(
309
331
  // Float32Array: SPOT=1 must be 0x00000001, not f32(1.0) bits.
310
332
  const u32 = new Uint32Array(out.buffer);
311
333
  u32[12] = snap.kind === 'point' ? LightSlotKind.POINT : LightSlotKind.SPOT;
312
- // Lanes 13..15 carry one kind-discriminated shadow identity. Point keeps
313
- // layer/near/far; spot writes its snapshot tile and leaves near/far zero.
334
+ // Lanes 13..15 carry one kind-discriminated shadow payload. Point keeps
335
+ // layer/near/far; spot writes its snapshot tile, shadowIntensity, and PCF
336
+ // kernel width.
314
337
  const i32 = new Int32Array(out.buffer);
315
338
  if (snap.kind === 'point' && shadow !== undefined) {
316
339
  i32[13] = shadow.shadowAtlasLayer;
317
340
  out[14] = shadow.near;
318
341
  out[15] = shadow.far;
319
342
  } else if (snap.kind === 'spot') {
320
- i32[13] = snap.shadowAtlasTile;
343
+ i32[13] =
344
+ snap.shadowAtlasTile >= 0
345
+ ? snap.shadowAtlasTile
346
+ : snap.projectorAsset !== undefined && snap.lightViewProj !== undefined
347
+ ? SPOT_LIGHT_PROJECTOR_ONLY_SENTINEL
348
+ : SPOT_LIGHT_SHADOW_TILE_SENTINEL;
349
+ out[14] = snap.shadowIntensity ?? 1;
350
+ // HDRP has no separate 80-byte SpotLight payload. Reuse the otherwise
351
+ // unused spot `.w` lane so the cluster path consumes the same authored PCF
352
+ // width as the URP and volume paths.
353
+ out[15] = snap.pcfKernelSize ?? 3;
321
354
  } else {
322
355
  i32[13] = POINT_LIGHT_SHADOW_LAYER_SENTINEL;
323
356
  // out[14] / out[15] remain zero (Float32Array zero-init).
@@ -140,7 +140,7 @@ export function buildPbrMaterialUserRegionEntries(
140
140
  // material-UBO contract. derive() emits binding 0 as the first numeric run's
141
141
  // merged UBO; an empty schema has no binding-0 UBO and needs no patch.
142
142
  const ubo = entries[0];
143
- if (ubo !== undefined && ubo.binding === 0 && ubo.buffer !== undefined) {
143
+ if (ubo !== undefined && ubo.binding === 0 && ubo.buffer?.type === 'uniform') {
144
144
  entries[0] = {
145
145
  binding: 0,
146
146
  visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
@@ -325,6 +325,8 @@ export const INJECTION_KIND_LENGTHS = {
325
325
  */
326
326
  export interface PbrCaps {
327
327
  readonly storageBuffer: boolean;
328
+ /** Whether the device can carry the optional SpotLight projector texture. */
329
+ readonly projectorAvailable?: boolean;
328
330
  }
329
331
 
330
332
  /**
@@ -376,7 +378,7 @@ export interface PbrCaps {
376
378
  */
377
379
  export function buildPbrViewBglEntries(caps: PbrCaps): GPUBindGroupLayoutEntry[] {
378
380
  const lightBufType: GPUBufferBindingType = caps.storageBuffer ? 'read-only-storage' : 'uniform';
379
- return [
381
+ const entries: GPUBindGroupLayoutEntry[] = [
380
382
  {
381
383
  binding: 0,
382
384
  visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
@@ -469,12 +471,32 @@ export function buildPbrViewBglEntries(caps: PbrCaps): GPUBindGroupLayoutEntry[]
469
471
  visibility: GPU_SHADER_STAGE_FRAGMENT,
470
472
  texture: { sampleType: 'depth', viewDimension: '2d' },
471
473
  },
472
- {
473
- binding: 10,
474
- visibility: GPU_SHADER_STAGE_VERTEX,
475
- buffer: { type: 'uniform', hasDynamicOffset: true },
476
- },
477
474
  ];
475
+ // The projector is optional, but when present it is a sampled texture in
476
+ // the fragment stage. Keep it out of the low-limit view layout so the
477
+ // remaining HDRP PBR ABI stays within WebGPU's minimum 16 sampled textures.
478
+ // The corresponding PROJECTOR_AVAILABLE shader variant is selected from
479
+ // the same device capability bit and returns a white modulation.
480
+ if (caps.projectorAvailable !== false) {
481
+ entries.push(
482
+ {
483
+ binding: 11,
484
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
485
+ texture: { sampleType: 'float', viewDimension: '2d' },
486
+ },
487
+ {
488
+ binding: 12,
489
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
490
+ sampler: { type: 'filtering' },
491
+ },
492
+ );
493
+ }
494
+ entries.push({
495
+ binding: 10,
496
+ visibility: GPU_SHADER_STAGE_VERTEX,
497
+ buffer: { type: 'uniform', hasDynamicOffset: true },
498
+ });
499
+ return entries;
478
500
  }
479
501
 
480
502
  // ─── PBR pipeline layout factory ────────────────────────────────────────────
@@ -784,9 +806,10 @@ export const SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET = '';
784
806
  * - `'unlit-material'` — 7 entries: base PBR material only (no inject)
785
807
  * - `'hdrp-7-slot'` — 7 entries (binding 0 + 3..8): HDRP cluster + SSAO group(2) BGL
786
808
  * 2. Caps-driven literal shapes (no shader):
787
- * - `'pbr-view'` — 10 entries: view UBO + lights + 6 shadow bindings
788
- * (directional 3/4, point 5/6, cascade 7, spot atlas 8, spot
789
- * lightViewProj matrices 9 feat-20260625)
809
+ * - `'pbr-view'` — 12 entries: view UBO + lights + shadow bindings,
810
+ * projector texture/sampler (11/12), and the Points/Lines UBO (10)
811
+ * (directional 3/4, point 5/6, cascade 7, spot atlas 8; the former
812
+ * spot lightViewProj matrices moved into the View UBO)
790
813
  * - `'pbr-mesh-array'` — 1 entry: per-entity mesh SSBO (dynamic-offset)
791
814
  * - `'pbr-instances'` — 1 entry: per-instance SSBO (no dynamic-offset)
792
815
  * - `'pbr-skin-mesh-array'` — 2 entries: meshes + skin palette
@@ -34,6 +34,7 @@ import {
34
34
  addTypedSkyboxPass,
35
35
  } from '../typed-render-graph-primitives';
36
36
  import { addTypedShadowPasses } from '../typed-shadow-passes';
37
+ import { addAuthoredVolumetricFogPasses } from '../volume/passes';
37
38
  import { addStandardPost } from './standard-post';
38
39
 
39
40
  function target(
@@ -245,8 +246,27 @@ function buildUrp(
245
246
  if (!temporal.ok) return temporal;
246
247
  }
247
248
 
249
+ if (topology.volumetricFog?.enabled === true) {
250
+ const depthSample = graph.view(depth.value.texture, {
251
+ label: 'scene-depth.sample',
252
+ dimension: '2d',
253
+ aspect: 'depth-only',
254
+ });
255
+ if (!depthSample.ok) return depthSample;
256
+ const volume = addAuthoredVolumetricFogPasses(
257
+ graph,
258
+ topology,
259
+ sceneResolved.value,
260
+ depthSample.value,
261
+ shadows.value.directional.view,
262
+ shadows.value.spot.view,
263
+ );
264
+ if (!volume.ok) return volume;
265
+ }
266
+
248
267
  const features = context.contributeFeatures([
249
268
  {
269
+ name: 'linear-hdr',
250
270
  kind: 'scene-color',
251
271
  texture: scene.value.texture,
252
272
  view: scene.value.view,
@@ -47,6 +47,7 @@ import {
47
47
  typedFrameClearColor,
48
48
  } from '../typed-render-graph-primitives';
49
49
  import { addTypedShadowPasses } from '../typed-shadow-passes';
50
+ import { addAuthoredVolumetricFogPasses } from '../volume/passes';
50
51
 
51
52
  import { buildStandardForwardLane } from './standard-forward-lane';
52
53
  import { addStandardPost } from './standard-post';
@@ -516,9 +517,28 @@ function buildHdrp(
516
517
  const temporal = addTransmissionBackdropTemporalPass(graph, scene.value);
517
518
  if (!temporal.ok) return temporal;
518
519
  }
520
+ if (topology.volumetricFog?.enabled === true) {
521
+ const depthSample = graph.view(depth.value.texture, {
522
+ label: 'scene-depth.sample',
523
+ dimension: '2d',
524
+ aspect: 'depth-only',
525
+ });
526
+ if (!depthSample.ok) return depthSample;
527
+ const volume = addAuthoredVolumetricFogPasses(
528
+ graph,
529
+ topology,
530
+ scene.value,
531
+ depthSample.value,
532
+ shadows.value.directional.view,
533
+ shadows.value.spot.view,
534
+ );
535
+ if (!volume.ok) return volume;
536
+ }
537
+ // The volume-composite pass remains in linear HDR before tone mapping.
519
538
 
520
539
  const features = context.contributeFeatures([
521
540
  {
541
+ name: 'linear-hdr',
522
542
  kind: 'scene-color',
523
543
  texture: scene.value.texture,
524
544
  view: scene.value.view,
@@ -29,6 +29,13 @@ export const STANDARD_POST_STAGE_NAMES = [
29
29
  'present',
30
30
  ] as const;
31
31
  export type StandardPostStage = (typeof STANDARD_POST_STAGE_NAMES)[number];
32
+ export type StandardVolumetricFogQuality = 'low' | 'high';
33
+
34
+ export interface StandardVolumetricFogProfile {
35
+ readonly quality: StandardVolumetricFogQuality;
36
+ readonly depth: 48 | 64;
37
+ readonly tileSize: 4 | 16;
38
+ }
32
39
 
33
40
  export interface StandardProfile {
34
41
  readonly pipelineId: typeof STANDARD_PIPELINE_ID;
@@ -38,6 +45,12 @@ export interface StandardProfile {
38
45
  readonly pbr: boolean;
39
46
  readonly ibl: boolean;
40
47
  readonly ssao: boolean;
48
+ readonly bloom: boolean;
49
+ readonly tone: StandardTone;
50
+ readonly antialias: StandardAntialias;
51
+ readonly sky: boolean;
52
+ /** Renderer-owned 1080p volume profile; tile width/height remain surface-derived. */
53
+ readonly volumetricFog?: StandardVolumetricFogProfile | undefined;
41
54
  readonly fallback: StandardFallbackLane;
42
55
  readonly postStages: typeof STANDARD_POST_STAGE_NAMES;
43
56
  }
@@ -50,10 +63,20 @@ export const DEFAULT_STANDARD_PROFILE: StandardProfile = Object.freeze({
50
63
  pbr: true,
51
64
  ibl: true,
52
65
  ssao: false,
66
+ bloom: true,
67
+ tone: 'aces-filmic',
68
+ antialias: 'fxaa',
69
+ sky: true,
53
70
  fallback: 'native',
54
71
  postStages: STANDARD_POST_STAGE_NAMES,
55
72
  });
56
73
 
74
+ export function resolveVolumetricFogProfile(
75
+ profile: Pick<StandardProfile, 'volumetricFog'>,
76
+ ): StandardVolumetricFogProfile {
77
+ return profile.volumetricFog ?? { quality: 'high', depth: 64, tileSize: 4 };
78
+ }
79
+
57
80
  export type StandardLane = StandardLightingLane | 'cpu-webgl2';
58
81
 
59
82
  export function resolveStandardLane(
@@ -5,6 +5,7 @@
5
5
  import { mat4, type Vec3, vec3 } from '@forgeax/engine-math';
6
6
  import {
7
7
  type BindGroup,
8
+ type BindGroupEntry,
8
9
  type BindGroupLayout,
9
10
  RhiError,
10
11
  type TextureView,
@@ -87,6 +88,8 @@ export function buildPerFrameBindGroups(
87
88
  graphTargets?: {
88
89
  readonly directionalShadow?: TextureView | undefined;
89
90
  readonly spotShadow?: TextureView | undefined;
91
+ readonly projector?: TextureView | undefined;
92
+ readonly projectorSampler?: import('@forgeax/engine-rhi').Sampler | undefined;
90
93
  },
91
94
  includeView = true,
92
95
  ): {
@@ -150,6 +153,9 @@ export function buildPerFrameBindGroups(
150
153
  graphSpotShadowView !== undefined
151
154
  ? graphSpotShadowView
152
155
  : pipelineState.shadowFallbackTextureView;
156
+ const projectorAvailable = pipelineState.projectorAvailable !== false;
157
+ const b11View = graphTargets?.projector ?? pipelineState.defaultWhiteTextureView;
158
+ const b12Sampler = graphTargets?.projectorSampler ?? pipelineState.defaultSampler;
153
159
  viewBindGroup = getOrCreateFromChain(
154
160
  frameState.viewBindGroupCache,
155
161
  [
@@ -161,110 +167,118 @@ export function buildPerFrameBindGroups(
161
167
  b5View,
162
168
  pipelineState.shadowParamsBuffer,
163
169
  b8View,
170
+ ...(projectorAvailable ? [b11View, b12Sampler] : []),
164
171
  pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
165
172
  ],
166
173
  'view-main',
167
174
  () => {
168
- const viewBindGroupResult = internals.device.createBindGroup({
169
- label: 'pbr-view-bg',
170
- layout: pipelineState.viewBindGroupLayout,
171
- entries: [
172
- {
173
- binding: 0,
174
- resource: {
175
- kind: 'buffer',
176
- value: { buffer: pipelineState.viewUniformBuffer, size: VIEW_UNIFORM_BYTES },
177
- },
175
+ const entries: BindGroupEntry[] = [
176
+ {
177
+ binding: 0,
178
+ resource: {
179
+ kind: 'buffer',
180
+ value: { buffer: pipelineState.viewUniformBuffer, size: VIEW_UNIFORM_BYTES },
178
181
  },
179
- {
180
- binding: 1,
181
- resource: {
182
- kind: 'buffer',
183
- value: { buffer: pipelineState.pointLightsBuffer },
184
- },
182
+ },
183
+ {
184
+ binding: 1,
185
+ resource: {
186
+ kind: 'buffer',
187
+ value: { buffer: pipelineState.pointLightsBuffer },
185
188
  },
186
- {
187
- binding: 2,
188
- resource: {
189
- kind: 'buffer',
190
- value: { buffer: pipelineState.spotLightsBuffer },
191
- },
189
+ },
190
+ {
191
+ binding: 2,
192
+ resource: {
193
+ kind: 'buffer',
194
+ value: { buffer: pipelineState.spotLightsBuffer },
192
195
  },
193
- {
194
- binding: 3,
195
- resource: {
196
- kind: 'textureView',
197
- value: b3View,
198
- },
196
+ },
197
+ {
198
+ binding: 3,
199
+ resource: {
200
+ kind: 'textureView',
201
+ value: b3View,
199
202
  },
200
- {
201
- binding: 4,
202
- resource: {
203
- kind: 'sampler',
204
- value: shadowSampler,
205
- },
203
+ },
204
+ {
205
+ binding: 4,
206
+ resource: {
207
+ kind: 'sampler',
208
+ value: shadowSampler,
206
209
  },
207
- // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
208
- // cube_array shadow atlas view (real ShadowAtlas when point
209
- // shadows are active; else 1x1x6 fallback).
210
- {
211
- binding: 5,
212
- resource: {
213
- kind: 'textureView',
214
- value: b5View,
215
- },
210
+ },
211
+ // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
212
+ // cube_array shadow atlas view (real ShadowAtlas when point
213
+ // shadows are active; else 1x1x6 fallback).
214
+ {
215
+ binding: 5,
216
+ resource: {
217
+ kind: 'textureView',
218
+ value: b5View,
216
219
  },
217
- // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
218
- // shadowParams UBO (`array<vec4<f32>, 4>` = 64 B). Lane N
219
- // stores `(near, far, 1/(far-near), 0)` for the point light
220
- // with shadowAtlasLayer === N. Updated per frame from
221
- // `pointShadowSnapshots` below.
222
- {
223
- binding: 6,
224
- resource: {
225
- kind: 'buffer',
226
- value: { buffer: pipelineState.shadowParamsBuffer },
227
- },
220
+ },
221
+ // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
222
+ // shadowParams UBO (`array<vec4<f32>, 4>` = 64 B). Lane N
223
+ // stores `(near, far, 1/(far-near), 0)` for the point light
224
+ // with shadowAtlasLayer === N. Updated per frame from
225
+ // `pointShadowSnapshots` below.
226
+ {
227
+ binding: 6,
228
+ resource: {
229
+ kind: 'buffer',
230
+ value: { buffer: pipelineState.shadowParamsBuffer },
228
231
  },
229
- // feat-20260613-csm-cascaded-shadow-maps M5 / w28 (rebased to
230
- // binding 7 on 2026-06-13 to make room for point-shadow 5/6):
231
- // forward shaders declare binding 7 in common.wgsl (shared
232
- // view BGL) but never reference it; only shadow_caster.wgsl
233
- // reads it. Host writes a stable singleton buffer so every
234
- // forward bind group entry stays populated.
235
- {
236
- binding: 7,
237
- resource: {
238
- kind: 'buffer',
239
- value: { buffer: pipelineState.shadowCasterCascadeBuffer },
240
- },
232
+ },
233
+ // feat-20260613-csm-cascaded-shadow-maps M5 / w28 (rebased to
234
+ // binding 7 on 2026-06-13 to make room for point-shadow 5/6):
235
+ // forward shaders declare binding 7 in common.wgsl (shared
236
+ // view BGL) but never reference it; only shadow_caster.wgsl
237
+ // reads it. Host writes a stable singleton buffer so every
238
+ // forward bind group entry stays populated.
239
+ {
240
+ binding: 7,
241
+ resource: {
242
+ kind: 'buffer',
243
+ value: { buffer: pipelineState.shadowCasterCascadeBuffer },
241
244
  },
242
- // feat-20260625-spot-light-shadow-mapping M3 / w21 (D-5):
243
- // spot shadow 2D atlas (real spotShadowDepth view when spot
244
- // shadows run this frame, else the 1x1 depth fallback). Always-on.
245
- {
246
- binding: 8,
247
- resource: {
248
- kind: 'textureView',
249
- value: b8View,
250
- },
245
+ },
246
+ // feat-20260625-spot-light-shadow-mapping M3 / w21 (D-5):
247
+ // spot shadow 2D atlas (real spotShadowDepth view when spot
248
+ // shadows run this frame, else the 1x1 depth fallback). Always-on.
249
+ {
250
+ binding: 8,
251
+ resource: {
252
+ kind: 'textureView',
253
+ value: b8View,
251
254
  },
252
- {
253
- binding: 10,
254
- resource: {
255
- kind: 'buffer',
256
- value: {
257
- buffer: pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
258
- size: POINTS_LINES_VIEW_BYTES,
259
- },
255
+ },
256
+ {
257
+ binding: 10,
258
+ resource: {
259
+ kind: 'buffer',
260
+ value: {
261
+ buffer: pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
262
+ size: POINTS_LINES_VIEW_BYTES,
260
263
  },
261
264
  },
262
- // feat-20260625-spot-light-shadow-mapping w25: the per-spot
263
- // fragment-read lightViewProj matrices fold into the View UBO
264
- // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
265
- // (WebGL2 fragment uniform-buffer budget). binding 10 is the
266
- // dedicated vertex-only Points/Lines viewport UBO.
267
- ],
265
+ },
266
+ ...(projectorAvailable
267
+ ? [
268
+ { binding: 11, resource: { kind: 'textureView' as const, value: b11View } },
269
+ { binding: 12, resource: { kind: 'sampler' as const, value: b12Sampler } },
270
+ ]
271
+ : []),
272
+ // feat-20260625-spot-light-shadow-mapping w25: the per-spot
273
+ // fragment-read lightViewProj matrices fold into the View UBO
274
+ // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
275
+ // (WebGL2 fragment uniform-buffer budget). binding 10 is the
276
+ // dedicated vertex-only Points/Lines viewport UBO.
277
+ ];
278
+ const viewBindGroupResult = internals.device.createBindGroup({
279
+ label: 'pbr-view-bg',
280
+ layout: pipelineState.viewBindGroupLayout,
281
+ entries,
268
282
  });
269
283
  if (!viewBindGroupResult.ok) throw viewBindGroupResult.error;
270
284
  return viewBindGroupResult.value;