@forgeax/engine-render 0.1.31 → 0.1.33

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 (293) hide show
  1. package/README.md +447 -34
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/fxaa-registration.d.ts +4 -0
  5. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  6. package/dist/assembly/lod-observation.d.ts +4 -0
  7. package/dist/assembly/lod-observation.d.ts.map +1 -0
  8. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  9. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  10. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  11. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  12. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  13. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  14. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  15. package/dist/assembly/webgpu-ready.d.ts +3 -2
  16. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  17. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  18. package/dist/bloom-admission.d.ts +13 -0
  19. package/dist/bloom-admission.d.ts.map +1 -0
  20. package/dist/{chunk-FUUO4UND.mjs → chunk-3RTCJTH4.mjs} +12 -9
  21. package/dist/chunk-3RTCJTH4.mjs.map +1 -0
  22. package/dist/{chunk-NO4WZCAH.mjs → chunk-LEAXOMYJ.mjs} +3 -3
  23. package/dist/{chunk-NO4WZCAH.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
  24. package/dist/{chunk-JRBDO2KB.mjs → chunk-NB2S7ZRX.mjs} +166 -10
  25. package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
  26. package/dist/{chunk-7MQ3XBSP.mjs → chunk-NNQL7CB3.mjs} +4 -4
  27. package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
  28. package/dist/{chunk-C3DDSXYK.mjs → chunk-OO3DRYK7.mjs} +171 -230
  29. package/dist/chunk-OO3DRYK7.mjs.map +1 -0
  30. package/dist/{chunk-S5UE2T5P.mjs → chunk-PLAXOOX5.mjs} +608 -7
  31. package/dist/chunk-PLAXOOX5.mjs.map +1 -0
  32. package/dist/{chunk-PSEQ6QPH.mjs → chunk-VOZWDM3L.mjs} +295 -198
  33. package/dist/chunk-VOZWDM3L.mjs.map +1 -0
  34. package/dist/{chunk-MDQJPY3T.mjs → chunk-ZDQWGQW7.mjs} +10 -4
  35. package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
  36. package/dist/components/atmosphere.d.ts +2 -0
  37. package/dist/components/atmosphere.d.ts.map +1 -1
  38. package/dist/components/camera.d.ts +20 -4
  39. package/dist/components/camera.d.ts.map +1 -1
  40. package/dist/components/depth-of-field.d.ts +28 -0
  41. package/dist/components/depth-of-field.d.ts.map +1 -0
  42. package/dist/components/directional-light.d.ts +2 -1
  43. package/dist/components/directional-light.d.ts.map +1 -1
  44. package/dist/components/index.d.ts +1 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/components/mesh-filter.d.ts +6 -5
  47. package/dist/components/mesh-filter.d.ts.map +1 -1
  48. package/dist/components/motion-blur.d.ts +1 -0
  49. package/dist/components/motion-blur.d.ts.map +1 -1
  50. package/dist/construct-renderer.mjs +3746 -837
  51. package/dist/construct-renderer.mjs.map +1 -1
  52. package/dist/device/gpu-residency.d.ts +6 -0
  53. package/dist/device/gpu-residency.d.ts.map +1 -1
  54. package/dist/environment/analytic-fog.d.ts +6 -0
  55. package/dist/environment/analytic-fog.d.ts.map +1 -0
  56. package/dist/errors/render.d.ts +6 -2
  57. package/dist/errors/render.d.ts.map +1 -1
  58. package/dist/extract/camera.d.ts.map +1 -1
  59. package/dist/extract/contracts.d.ts +7 -0
  60. package/dist/extract/contracts.d.ts.map +1 -1
  61. package/dist/extract/environment.d.ts +2 -0
  62. package/dist/extract/environment.d.ts.map +1 -1
  63. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
  64. package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
  65. package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
  66. package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
  67. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
  68. package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
  69. package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
  70. package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
  71. package/dist/features/error-reporting.d.ts +13 -0
  72. package/dist/features/error-reporting.d.ts.map +1 -0
  73. package/dist/features/host.d.ts +5 -0
  74. package/dist/features/host.d.ts.map +1 -1
  75. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  76. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  77. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  78. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  79. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  80. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  81. package/dist/features/plan.d.ts +7 -4
  82. package/dist/features/plan.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +9 -5
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/render-graph-compute.d.ts.map +1 -1
  86. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  87. package/dist/features/types.d.ts +19 -1
  88. package/dist/features/types.d.ts.map +1 -1
  89. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  90. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  91. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  92. package/dist/gpu-driven/production-raster.d.ts +3 -1
  93. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  94. package/dist/gpu-driven/view-gpu.d.ts +9 -0
  95. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +6 -1
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/index.d.ts +8 -3
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.mjs +6 -5
  101. package/dist/index.mjs.map +1 -1
  102. package/dist/inspection-types.d.ts +78 -2
  103. package/dist/inspection-types.d.ts.map +1 -1
  104. package/dist/instances.d.ts +15 -1
  105. package/dist/instances.d.ts.map +1 -1
  106. package/dist/internal.mjs +6 -6
  107. package/dist/mesh-material-bindings.d.ts +3 -0
  108. package/dist/mesh-material-bindings.d.ts.map +1 -1
  109. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  110. package/dist/pipeline/standard-output/types.d.ts +4 -2
  111. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts +28 -19
  113. package/dist/pipeline/standard-post.d.ts.map +1 -1
  114. package/dist/pipeline-spec.d.ts +2 -2
  115. package/dist/plugin.d.ts.map +1 -1
  116. package/dist/record/frame-snapshot.d.ts +24 -1
  117. package/dist/record/frame-snapshot.d.ts.map +1 -1
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +5 -2
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +6 -5
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/render-context.d.ts +12 -28
  126. package/dist/record/render-context.d.ts.map +1 -1
  127. package/dist/record/shadow-pass.d.ts.map +1 -1
  128. package/dist/record/skybox-post-pass.d.ts +5 -3
  129. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  130. package/dist/record/typed-frame-graph.d.ts +8 -1
  131. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  133. package/dist/render-contract.d.ts +32 -5
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-graph-primitives.d.ts +2 -0
  136. package/dist/render-graph-primitives.d.ts.map +1 -1
  137. package/dist/render-pipeline.d.ts +15 -1
  138. package/dist/render-pipeline.d.ts.map +1 -1
  139. package/dist/render-system-extract-tail.d.ts.map +1 -1
  140. package/dist/render-system-extract.d.ts +26 -5
  141. package/dist/render-system-extract.d.ts.map +1 -1
  142. package/dist/render-system.d.ts +9 -2
  143. package/dist/render-system.d.ts.map +1 -1
  144. package/dist/scene/render-scene.d.ts +2 -0
  145. package/dist/scene/render-scene.d.ts.map +1 -1
  146. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  147. package/dist/temporal/frame.d.ts +7 -0
  148. package/dist/temporal/frame.d.ts.map +1 -1
  149. package/dist/temporal/index.mjs +5 -5
  150. package/dist/temporal/view.d.ts +6 -1
  151. package/dist/temporal/view.d.ts.map +1 -1
  152. package/dist/typed-render-graph-primitives.d.ts +10 -7
  153. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  154. package/dist/volume/component.d.ts +0 -1
  155. package/dist/volume/component.d.ts.map +1 -1
  156. package/dist/volume/passes.d.ts.map +1 -1
  157. package/dist/volume/temporal.d.ts +3 -1
  158. package/dist/volume/temporal.d.ts.map +1 -1
  159. package/package.json +21 -21
  160. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  161. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  162. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  163. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  164. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  165. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  166. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  167. package/src/__tests__/factory-contract.integration.test.ts +331 -68
  168. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  169. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  170. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
  171. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  172. package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
  173. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
  174. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  175. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
  176. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  177. package/src/__tests__/gpu-scene.unit.test.ts +91 -0
  178. package/src/__tests__/instances-store.unit.test.ts +90 -0
  179. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  180. package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
  181. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +49 -0
  182. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  183. package/src/__tests__/public-surface.test-d.ts +8 -0
  184. package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
  185. package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
  186. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  187. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
  188. package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
  189. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  190. package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
  191. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  192. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  193. package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
  194. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  195. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  196. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  197. package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
  198. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
  199. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
  200. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  201. package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
  202. package/src/assembly/analytic-fog-registration.ts +14 -0
  203. package/src/assembly/fxaa-registration.ts +17 -0
  204. package/src/assembly/lod-observation.ts +12 -0
  205. package/src/assembly/motion-blur-registration.ts +17 -0
  206. package/src/assembly/renderer-facade.ts +2 -0
  207. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  208. package/src/assembly/webgpu-ready-contract.ts +8 -3
  209. package/src/assembly/webgpu-ready.ts +186 -250
  210. package/src/assembly/webgpu-renderer.ts +30 -45
  211. package/src/bloom-admission.ts +21 -0
  212. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  213. package/src/components/atmosphere.ts +4 -0
  214. package/src/components/camera.ts +66 -10
  215. package/src/components/depth-of-field.ts +45 -0
  216. package/src/components/directional-light.ts +2 -1
  217. package/src/components/index.ts +1 -0
  218. package/src/components/mesh-filter.ts +11 -9
  219. package/src/components/motion-blur.ts +1 -0
  220. package/src/device/gpu-residency.ts +56 -12
  221. package/src/environment/analytic-fog.ts +44 -0
  222. package/src/environment/background.ts +2 -2
  223. package/src/errors/render.ts +14 -1
  224. package/src/extract/camera.ts +42 -9
  225. package/src/extract/contracts.ts +7 -0
  226. package/src/extract/environment.ts +4 -0
  227. package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
  228. package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
  229. package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
  230. package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
  231. package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
  232. package/src/features/error-reporting.ts +78 -0
  233. package/src/features/host.ts +26 -13
  234. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  235. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  236. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  237. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  238. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  239. package/src/features/plan.ts +25 -8
  240. package/src/features/prepared-gpu-work.ts +86 -35
  241. package/src/features/render-graph-compute.ts +13 -5
  242. package/src/features/render-graph-contribution.ts +2 -0
  243. package/src/features/types.ts +18 -1
  244. package/src/gpu-driven/material-artifacts.ts +2 -7
  245. package/src/gpu-driven/prepared-draw.ts +15 -10
  246. package/src/gpu-driven/production-raster.ts +83 -7
  247. package/src/gpu-driven/view-gpu.ts +17 -0
  248. package/src/gpu-scene.ts +188 -21
  249. package/src/index.ts +64 -0
  250. package/src/inspection-types.ts +98 -2
  251. package/src/instances.ts +93 -0
  252. package/src/mesh-material-bindings.ts +4 -0
  253. package/src/pipeline/standard-output/graph.ts +3 -0
  254. package/src/pipeline/standard-output/types.ts +4 -1
  255. package/src/pipeline/standard-post.ts +324 -76
  256. package/src/pipeline-spec.ts +5 -5
  257. package/src/plugin.ts +2 -0
  258. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  259. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
  260. package/src/record/frame-snapshot.ts +31 -2
  261. package/src/record/frame-targets.ts +10 -6
  262. package/src/record/frame.ts +202 -28
  263. package/src/record/main-pass-geometry.ts +30 -1
  264. package/src/record/main-pass-sprite-draws.ts +6 -1
  265. package/src/record/main-pass.ts +1 -5
  266. package/src/record/mesh-ssbo.ts +43 -6
  267. package/src/record/render-context.ts +12 -28
  268. package/src/record/shadow-pass.ts +6 -1
  269. package/src/record/skybox-post-pass.ts +122 -196
  270. package/src/record/typed-frame-graph.ts +259 -15
  271. package/src/recovery/render-system-candidate.ts +28 -10
  272. package/src/render-contract.ts +34 -4
  273. package/src/render-graph-primitives.ts +8 -1
  274. package/src/render-pipeline.ts +22 -1
  275. package/src/render-system-extract-tail.ts +103 -13
  276. package/src/render-system-extract.ts +148 -38
  277. package/src/render-system.ts +332 -119
  278. package/src/scene/render-scene.ts +85 -4
  279. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  280. package/src/temporal/frame-coordinator.ts +8 -1
  281. package/src/temporal/frame.ts +20 -0
  282. package/src/temporal/view.ts +13 -2
  283. package/src/typed-render-graph-primitives.ts +124 -48
  284. package/src/volume/component.ts +0 -1
  285. package/src/volume/passes.ts +8 -4
  286. package/src/volume/resources.ts +1 -1
  287. package/src/volume/temporal.ts +11 -0
  288. package/dist/chunk-C3DDSXYK.mjs.map +0 -1
  289. package/dist/chunk-FUUO4UND.mjs.map +0 -1
  290. package/dist/chunk-JRBDO2KB.mjs.map +0 -1
  291. package/dist/chunk-MDQJPY3T.mjs.map +0 -1
  292. package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
  293. package/dist/chunk-S5UE2T5P.mjs.map +0 -1
@@ -37,6 +37,7 @@ import type {
37
37
  } from '@forgeax/engine-rhi';
38
38
  import { ok, type Result } from '@forgeax/engine-types';
39
39
  import type { Tonemap } from './components/camera';
40
+ import type { DepthOfFieldSide } from './components/depth-of-field';
40
41
  import type { DirectionalShadowFilterLabel } from './components/directional-shadow-filter';
41
42
  import type { RenderError } from './errors/render';
42
43
  import type { RenderFeatureTargetKind } from './features/targets';
@@ -223,6 +224,8 @@ export interface RenderPipelineTopology {
223
224
  readonly clearOnly?: boolean | undefined;
224
225
  /** Selected atmosphere requires the graph-owned sky producer and background. */
225
226
  readonly atmosphere?: boolean | undefined;
227
+ /** Finite-depth distance / exponential-height fog; no volume history. */
228
+ readonly analyticFog?: boolean | undefined;
226
229
  /** Whether Standard main draws carry the producer-owned fallback MRT. */
227
230
  readonly reflectionFallback?: { readonly enabled: boolean } | undefined;
228
231
  readonly surface: {
@@ -234,7 +237,19 @@ export interface RenderPipelineTopology {
234
237
  };
235
238
  /** Renderer-owned immutable dimensions for Standard graph consumers. */
236
239
  readonly extent?: RenderExtent;
237
- readonly camera: Pick<RenderPipelineContext['camera'], 'tonemap' | 'antialias' | 'bloom'>;
240
+ readonly camera: Pick<
241
+ RenderPipelineContext['camera'],
242
+ 'tonemap' | 'antialias' | 'bloom' | 'bloomIntensity'
243
+ > & {
244
+ /** Shape-only DoF admission; optical values remain per-frame UBO data. */
245
+ readonly depthOfField?:
246
+ | {
247
+ readonly blurSide: DepthOfFieldSide;
248
+ readonly useNear: boolean;
249
+ readonly useFar: boolean;
250
+ }
251
+ | undefined;
252
+ };
238
253
  /** Frame-stable Standard output policy; no raw RHI handles enter topology. */
239
254
  readonly output?: {
240
255
  readonly autoExposure: boolean;
@@ -475,6 +490,12 @@ export interface GpuDrivenStandardPbrFrameResources {
475
490
 
476
491
  export interface RenderPipelineBuildContext<FrameCtx extends RenderPipelineFrame> {
477
492
  readonly graph: RenderGraphBuilder<FrameCtx>;
493
+ /** Per-frame camera values consumed by optical fullscreen features. */
494
+ readonly camera?: {
495
+ readonly depthOfField?:
496
+ | import('./features/depth-of-field/depth-of-field-params').DepthOfFieldParams
497
+ | undefined;
498
+ };
478
499
  /** Optional Standard-only output-domain coverage producer input. */
479
500
  readonly targetCoverage?: import('./temporal/target-coverage-attachment').TargetCoverageAttachment;
480
501
  /** Explicit receipt-bound color domains requested for this frame. */
@@ -86,6 +86,7 @@ import {
86
86
  gpuDrivenSourceDrawItemIndex,
87
87
  } from './extract/gpu-driven';
88
88
  import type { VisibilitySnapshot } from './extract/visibility';
89
+ import { MotionBlurValidationError } from './features/motion-blur/motion-blur-params';
89
90
  import type {
90
91
  RenderFeatureHiddenEntityReport,
91
92
  RenderFeatureWorldVisibilitySnapshot,
@@ -120,6 +121,7 @@ import type {
120
121
  ExtractPipelineSurface,
121
122
  InstancesSnapshot,
122
123
  LightProbeSnapshot,
124
+ MaterialProgramSelection,
123
125
  MaterialSnapshot,
124
126
  MaterialSnapshotCache,
125
127
  MaterialSnapshotCachesByWorld,
@@ -161,7 +163,7 @@ import {
161
163
  materialColorParameterSchema,
162
164
  materialNormalScale,
163
165
  materialParamSchemaForMaterial,
164
- materialProgramKeysForMaterial,
166
+ materialProgramSelectionsForMaterial,
165
167
  materialSceneIndexProgramKeysForMaterial,
166
168
  materialStandardTextureMask,
167
169
  materialTextureFields,
@@ -191,6 +193,25 @@ function skinPaletteIdentity(world: World, entity: number): string {
191
193
  return `skin:${world.identity}:${entity}`;
192
194
  }
193
195
 
196
+ function materialProgramSelectionRequiresSkin(selection: {
197
+ readonly abi?: {
198
+ readonly skinPaletteAddress?: unknown;
199
+ readonly vertexInputs?: readonly { readonly semantic?: unknown }[];
200
+ };
201
+ }): boolean {
202
+ if (selection.abi?.skinPaletteAddress !== undefined) return true;
203
+ const semantics = new Set((selection.abi?.vertexInputs ?? []).map((input) => input.semantic));
204
+ return semantics.has('skinIndex') && semantics.has('skinWeight');
205
+ }
206
+
207
+ function isMissingMaterialProgram(error: unknown): boolean {
208
+ return (
209
+ error !== null &&
210
+ typeof error === 'object' &&
211
+ (error as { readonly code?: unknown }).code === 'material-specialization-not-cooked'
212
+ );
213
+ }
214
+
194
215
  export function extractFrames(
195
216
  worlds: readonly World[],
196
217
  owner: number | ExtractFramesOwner,
@@ -330,6 +351,11 @@ export function extractFrames(
330
351
 
331
352
  framesByWorld.set(wi, frame);
332
353
  } catch (err) {
354
+ // Camera effect parameters are part of the public render contract. Keep
355
+ // their typed validation error on the renderer path so draw() can return
356
+ // a RenderError and publish the invalid inspection state; ordinary
357
+ // per-world extraction failures retain the existing world-local route.
358
+ if (err instanceof MotionBlurValidationError) throw err;
333
359
  // Per-world failure: route to world's own error handler, skip contribution.
334
360
  try {
335
361
  createWorldInternalView(world)._routeError(err, {
@@ -482,8 +508,8 @@ export function extractFrames(
482
508
  // bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
483
509
  // against the surfaced camera. In a single-world app the per-world extract
484
510
  // already produced correct matrices (light+camera share the world), and this
485
- // recompute reproduces them byte-identically (same config, same camera). In
486
- // the editor super-composite the directional light's world has NO camera, so
511
+ // recompute uses the same config and camera. In the editor super-composite
512
+ // the directional light's world has NO camera, so
487
513
  // its per-world `lightViewProj` is undefined/degenerate; the surfaced camera
488
514
  // lives in the cameraOwner world. Pairing them here is the ONLY place both
489
515
  // are visible. Point/spot shadows are camera-independent (light-space only)
@@ -1226,9 +1252,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1226
1252
  // bug-20260710-editor-cross-world-shadow: CSM matrices are computed by the
1227
1253
  // shared pure {@link computeDirectionalCsm}, called here per-world with THIS
1228
1254
  // world's own `cameras[0]`. In a single-world app the light and camera share
1229
- // the world, so this per-world result is final (byte-identical to the prior
1230
- // inline block). In the editor super-composite the light's world may have no
1231
- // camera → this yields no matrices; {@link extractFrames} then RECOMPUTES at
1255
+ // the world, so this per-world result is final. In the editor
1256
+ // super-composite the light's world may have no camera → this yields no
1257
+ // matrices; {@link extractFrames} then RECOMPUTES at
1232
1258
  // the merge layer using the surfaced (cameraOwner) camera + the raw config
1233
1259
  // carried on ExtractedLights. The raw config + direction are surfaced
1234
1260
  // unconditionally so the merge layer can re-run the builder.
@@ -1551,6 +1577,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1551
1577
  mieCoefficient: value.mieCoefficient,
1552
1578
  mieDirectionalG: value.mieDirectionalG,
1553
1579
  sunAngularRadius: value.sunAngularRadius,
1580
+ circumsolarStrength: value.circumsolarStrength,
1581
+ circumsolarWidth: value.circumsolarWidth,
1554
1582
  } as const;
1555
1583
  environmentCandidates.push({
1556
1584
  kind: 'atmosphere',
@@ -1861,6 +1889,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
1861
1889
  if (!authorVisible && !retainHidden) {
1862
1890
  continue;
1863
1891
  }
1892
+ // A scalar shared<T> field uses slot 0 as the schema-level unbound
1893
+ // sentinel. An entity carrying an unbound MeshFilter is not a
1894
+ // renderable yet; keep it out of frame validation. Non-zero handles
1895
+ // still follow the normal resolve/error path below.
1896
+ if (isRenderable && fAssetHandle === 0) {
1897
+ continue;
1898
+ }
1864
1899
  const layerVal = fLayerValue ?? 0;
1865
1900
  // bug-20260709-builtin-quad-withoutaabb-disables-sprite-frustum-cu M2.5
1866
1901
  // (carries PR #598 feat-20260703 D-7): dispatch entries for this entity
@@ -2030,6 +2065,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2030
2065
  : undefined;
2031
2066
 
2032
2067
  let materialSnap: MaterialSnapshot;
2068
+ let materialProgramSelections: readonly MaterialProgramSelection[] | undefined;
2033
2069
 
2034
2070
  if (cachedMaterial !== undefined) {
2035
2071
  materialSnap = cachedMaterial.snapshot;
@@ -2143,11 +2179,59 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2143
2179
  ...context.materialContext,
2144
2180
  geometry: hasSkin ? ('skinned' as const) : context.materialContext.geometry,
2145
2181
  };
2146
- const materialProgramKeys = materialProgramKeysForMaterial(
2147
- asset,
2148
- assets,
2149
- materialContext,
2150
- );
2182
+ try {
2183
+ materialProgramSelections = materialProgramSelectionsForMaterial(
2184
+ asset,
2185
+ assets,
2186
+ materialContext,
2187
+ );
2188
+ } catch (error) {
2189
+ // A standalone mesh can only satisfy a cooked skinned program
2190
+ // through a Skin-bearing mesh. Probe that same exact owner
2191
+ // selection only when the resolved mesh really lacks the
2192
+ // advertised attributes; otherwise retain the producer's exact
2193
+ // publication error instead of guessing from module names/order.
2194
+ if (isMissingMaterialProgram(error) && materialContext?.geometry === 'mesh') {
2195
+ try {
2196
+ const skinnedSelections = materialProgramSelectionsForMaterial(asset, assets, {
2197
+ ...materialContext,
2198
+ geometry: 'skinned',
2199
+ });
2200
+ if (
2201
+ skinnedSelections?.some(materialProgramSelectionRequiresSkin) === true &&
2202
+ gpuDrivenMesh !== undefined
2203
+ ) {
2204
+ const hasSkinIndex = gpuDrivenMesh.attributes.skinIndex !== undefined;
2205
+ const hasSkinWeight = gpuDrivenMesh.attributes.skinWeight !== undefined;
2206
+ if (!hasSkinIndex || !hasSkinWeight) {
2207
+ const missing =
2208
+ !hasSkinIndex && !hasSkinWeight
2209
+ ? 'both'
2210
+ : !hasSkinIndex
2211
+ ? 'skinIndex'
2212
+ : 'skinWeight';
2213
+ worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
2214
+ severity: Severity.Error,
2215
+ systemName: 'RenderSystem.extract (material-skin-attr-missing)',
2216
+ });
2217
+ continue;
2218
+ }
2219
+ }
2220
+ } catch {
2221
+ // Preserve the original exact-context publication error.
2222
+ }
2223
+ }
2224
+ throw error;
2225
+ }
2226
+ const materialProgramKeys =
2227
+ materialProgramSelections === undefined
2228
+ ? undefined
2229
+ : Object.fromEntries(
2230
+ materialProgramSelections.map(({ pass, specializationKey }) => [
2231
+ pass,
2232
+ specializationKey,
2233
+ ]),
2234
+ );
2151
2235
  const materialSceneIndexProgramKeys = materialSceneIndexProgramKeysForMaterial(
2152
2236
  asset,
2153
2237
  assets,
@@ -2208,7 +2292,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2208
2292
  skinSkeletonView !== undefined &&
2209
2293
  skinSkeletonView !== undefined &&
2210
2294
  skinSkeletonView !== 0;
2211
- const isPbrSkinMaterial = isStandardPbrSkinMaterialShader(firstPassShader);
2295
+ const isPbrSkinMaterial =
2296
+ materialProgramSelections?.some(materialProgramSelectionRequiresSkin) === true ||
2297
+ isStandardPbrSkinMaterialShader(firstPassShader);
2212
2298
  if (hasSkinSkel && !isPbrSkinMaterial) {
2213
2299
  worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
2214
2300
  severity: Severity.Error,
@@ -2759,7 +2845,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2759
2845
  // reinterpret a skinned vertex stream through an ordinary mesh group.
2760
2846
  if (skinSlice === undefined && hasSkin) {
2761
2847
  releaseSkinIdentity();
2762
- if (materialSnap.materialShaderId === 'forgeax::pbr-skin') {
2848
+ if (
2849
+ materialSnap.materialShaderId === 'forgeax::pbr-skin' ||
2850
+ materialProgramSelections?.some(materialProgramSelectionRequiresSkin) === true
2851
+ ) {
2763
2852
  pendingDispatch.length = 0;
2764
2853
  continue;
2765
2854
  }
@@ -3017,6 +3106,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3017
3106
  }
3018
3107
  instancesSnap = {
3019
3108
  transforms: collectionResult.value.transforms,
3109
+ generations: collectionResult.value.generations,
3020
3110
  instanceCount: collectionResult.value.count,
3021
3111
  collectionId: collectionResult.value.collectionId,
3022
3112
  revision: collectionResult.value.revision,
@@ -27,7 +27,7 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
27
27
  // fire; no renderable; no dispatch.
28
28
  // - case B (material === 0) : missing-spec sentinel; defaultMaterial
29
29
  // Snapshot fills the slot. No fire.
30
- // - case C (handle unresolved): assets.get(handle).err with the entity
30
+ // - case C (non-zero handle unresolved): assets.get(handle).err with the entity
31
31
  // carrying the full T+MF+MR renderable
32
32
  // archetype -> structured RhiError(
33
33
  // `asset-not-registered`) routed through
@@ -61,8 +61,8 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
61
61
  // material; hint literal: `'pass
62
62
  // undefined or omit field to request
63
63
  // default material'` (AC-10).
64
- // - case C dangling-ref : `assets.get(handle).err` (strict path,
65
- // isRenderable === true) ->
64
+ // - case C dangling-ref : `assets.get(handle).err` for a non-zero
65
+ // handle (strict path, isRenderable === true) ->
66
66
  // `RhiError({ code: 'asset-not-
67
67
  // registered', detail: { assetHandle },
68
68
  // hint: 'register material via
@@ -73,7 +73,9 @@ import type { MaterialCookProgramContext } from '@forgeax/engine-pack/material-c
73
73
  // entity skipped from
74
74
  // RenderableSnapshot[] +
75
75
  // MaterialDispatchSnapshot[]; mirrors
76
- // `MeshFilter.assetHandle` dangling path
76
+ // `MeshFilter.assetHandle` dangling path. A
77
+ // scalar shared<T> slot 0 is the unbound,
78
+ // non-renderable state and is skipped
77
79
  // (charter proposition 5; AC-11).
78
80
  //
79
81
  // Renderer-owned Large Instances:
@@ -96,7 +98,14 @@ import {
96
98
  selectMaterialPassProgram,
97
99
  walkMaterialPassesOverSharedRefs,
98
100
  } from '@forgeax/engine-assets-runtime';
99
- import type { Component, ComponentSchema, EntityHandle, Query, World } from '@forgeax/engine-ecs';
101
+ import {
102
+ type Component,
103
+ type ComponentSchema,
104
+ type EntityHandle,
105
+ type Query,
106
+ Time,
107
+ type World,
108
+ } from '@forgeax/engine-ecs';
100
109
  import { readRenderArrayView, routeWorldError } from '@forgeax/engine-ecs/projection';
101
110
  import { type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
102
111
  import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
@@ -116,6 +125,7 @@ import type {
116
125
  MaterialColorParameterSchema,
117
126
  MaterialParameter,
118
127
  MaterialPass,
128
+ MaterialProgramAbi,
119
129
  MaterialRenderState,
120
130
  MaterialTextureCoordinates,
121
131
  MaterialTextureValue,
@@ -132,7 +142,6 @@ import {
132
142
  import { buildCubeCameraFaceViews } from './capture/cube-views';
133
143
  import {
134
144
  antialiasFromF32,
135
- bloomEnabledFromF32,
136
145
  CAMERA_EXPOSURE_MODE_MANUAL,
137
146
  Camera,
138
147
  CubeCamera,
@@ -147,8 +156,10 @@ import {
147
156
  ScreenSpaceReflection,
148
157
  tonemapFromF32,
149
158
  tonemapToU32,
159
+ validateCameraBloom,
150
160
  validateDynamicResolutionCamera,
151
161
  } from './components';
162
+ import { DepthOfField } from './components/depth-of-field';
152
163
  import type { DirectionalShadowQuality } from './components/directional-shadow-filter';
153
164
  import type { LightValidationError } from './components/light-helpers';
154
165
  import { selectEnvironment } from './environment/frame';
@@ -165,6 +176,11 @@ import type {
165
176
  } from './extract/environment';
166
177
  import type { GpuDrivenDrawSnapshot } from './extract/gpu-driven';
167
178
  import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
179
+ import {
180
+ depthOfFieldRequestFailure,
181
+ resolveDepthOfFieldParams,
182
+ } from './features/depth-of-field/depth-of-field-params';
183
+ import { resolveMotionBlurParams } from './features/motion-blur/motion-blur-params';
168
184
  import type {
169
185
  RenderFeatureHiddenEntityReport,
170
186
  RenderFeatureWorldVisibilitySnapshot,
@@ -659,6 +675,8 @@ export interface ExtractedVolumetricFog {
659
675
  readonly generation?: number;
660
676
  readonly digest?: string;
661
677
  readonly worldId?: number;
678
+ /** Canonical simulation time projected from this World for authored medium motion. */
679
+ readonly worldTimeSeconds?: number;
662
680
  /** Required same-World light selected by the authoring component. */
663
681
  readonly lightEntity?: EntityHandle;
664
682
  readonly lightKind?: 'directional' | 'point' | 'spot';
@@ -810,6 +828,9 @@ export type RenderableReactiveReason =
810
828
  export interface RenderableTemporalSnapshot {
811
829
  readonly previousEpoch: number | undefined;
812
830
  readonly previousSource: RenderablePreviousSource;
831
+ /** True only when current and previous geometry belong to one stable source identity. */
832
+ readonly motionValid: boolean;
833
+ /** Color-history invalidation is independent from motion validity. */
813
834
  readonly reactive: boolean;
814
835
  readonly reactiveReasons: readonly RenderableReactiveReason[];
815
836
  readonly previousTransform: TransformSnapshot;
@@ -830,6 +851,8 @@ export interface InstancesSnapshot {
830
851
  readonly collectionId?: InstanceCollectionId;
831
852
  /** Packed column-major mat4 transforms (16 f32 per instance). */
832
853
  readonly transforms: Float32Array;
854
+ /** Monotonic per-ordinal identity generations, when the collection owns them. */
855
+ readonly generations?: Uint32Array;
833
856
  /** Number of instances (transforms.length / 16). */
834
857
  readonly instanceCount: number;
835
858
  /** Collection revision captured by this detached frame snapshot. */
@@ -1543,6 +1566,53 @@ export function materialProgramKeysForMaterial(
1543
1566
  return materialProgramKeysForAddress(material, assets, context, 'direct');
1544
1567
  }
1545
1568
 
1569
+ /**
1570
+ * Return the exact direct/scene-index programs selected for one renderer
1571
+ * context. The selection result is kept next to the key projection so
1572
+ * consumers that validate a mesh contract can inspect the producer ABI
1573
+ * without rebuilding pass/context matching from payload order.
1574
+ */
1575
+ export interface MaterialProgramSelection {
1576
+ readonly pass: string;
1577
+ readonly context: MaterialCookProgramContext;
1578
+ readonly specializationKey: string;
1579
+ readonly abi?: MaterialProgramAbi;
1580
+ }
1581
+
1582
+ export function materialProgramSelectionsForMaterial(
1583
+ material: MaterialAsset,
1584
+ assets: AssetRegistry,
1585
+ context: MaterialCookProgramContext | undefined,
1586
+ address: 'direct' | 'scene-index' = 'direct',
1587
+ ): readonly MaterialProgramSelection[] | undefined {
1588
+ const projection = assets.getMaterialProjectionForPayload(material);
1589
+ if (projection === undefined) return undefined;
1590
+ if (context === undefined)
1591
+ throw new Error('Published material selection requires renderer-owned compiler context');
1592
+ return projection.passes.map((pass) => {
1593
+ const mode = String(
1594
+ (pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
1595
+ );
1596
+ const passKind = /shadow/i.test(mode)
1597
+ ? ('shadow' as const)
1598
+ : /depth/i.test(mode)
1599
+ ? ('depth' as const)
1600
+ : ('forward' as const);
1601
+ const selectedContext: MaterialCookProgramContext = {
1602
+ ...context,
1603
+ pass: passKind,
1604
+ pipeline: /deferred|gbuffer/i.test(mode) ? 'deferred' : context.pipeline,
1605
+ };
1606
+ const selected = selectMaterialPassProgram(projection, pass.name, selectedContext, address);
1607
+ return {
1608
+ pass: pass.name,
1609
+ context: selectedContext,
1610
+ specializationKey: selected.specializationKey,
1611
+ ...(selected.abi === undefined ? {} : { abi: selected.abi }),
1612
+ };
1613
+ });
1614
+ }
1615
+
1546
1616
  /** Resolve the producer's scene-index program key for each cooked Pass. */
1547
1617
  export function materialSceneIndexProgramKeysForMaterial(
1548
1618
  material: MaterialAsset,
@@ -1597,26 +1667,12 @@ function materialProgramKeysForAddress(
1597
1667
  context: MaterialCookProgramContext | undefined,
1598
1668
  address: 'direct' | 'scene-index',
1599
1669
  ): Readonly<Record<string, string>> | undefined {
1600
- const projection = assets.getMaterialProjectionForPayload(material);
1601
- if (projection === undefined) return undefined;
1602
- if (context === undefined)
1603
- throw new Error('Published material selection requires renderer-owned compiler context');
1604
- const entries: [string, string][] = [];
1605
- for (const pass of projection.passes) {
1606
- const mode = String(
1607
- (pass.renderState?.tags as Record<string, unknown> | undefined)?.LightMode ?? pass.name,
1608
- );
1609
- const selectedContext: MaterialCookProgramContext = {
1610
- ...context,
1611
- pass: /shadow/i.test(mode) ? 'shadow' : /depth/i.test(mode) ? 'depth' : 'forward',
1612
- pipeline: /deferred|gbuffer/i.test(mode) ? 'deferred' : context.pipeline,
1613
- };
1614
- entries.push([
1615
- pass.name,
1616
- selectMaterialPassProgram(projection, pass.name, selectedContext, address).specializationKey,
1617
- ]);
1618
- }
1619
- return entries.length === 0 ? undefined : Object.fromEntries(entries);
1670
+ const selections = materialProgramSelectionsForMaterial(material, assets, context, address);
1671
+ return selections === undefined
1672
+ ? undefined
1673
+ : Object.fromEntries(
1674
+ selections.map(({ pass, specializationKey }) => [pass, specializationKey]),
1675
+ );
1620
1676
  }
1621
1677
 
1622
1678
  export function runtimeMaterialShaderIdForMaterial(
@@ -2712,9 +2768,10 @@ export interface DirectionalCsmResult {
2712
2768
  * the caller leaves lightViewProj undefined rather than emitting zero matrices
2713
2769
  * that the WGSL reader would sample as NaN → "fully lit").
2714
2770
  *
2715
- * Byte-identical to the prior inline computation for the single-world case
2716
- * (same PSSM lambda, same toward-light Z reach RC-2 fix, same clip-space
2717
- * matrix / tile-placement-in-shader split).
2771
+ * Keeps the prior PSSM lambda, toward-light Z reach RC-2 fix, and clip-space
2772
+ * matrix / tile-placement-in-shader split. The XY fit is additionally snapped
2773
+ * to the cascade's shadow-texel grid so camera motion cannot continuously
2774
+ * shimmer a receiver across raster texel boundaries.
2718
2775
  */
2719
2776
  export function computeDirectionalCsm(
2720
2777
  direction: Vec3,
@@ -2820,6 +2877,29 @@ export function computeDirectionalCsm(
2820
2877
  if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
2821
2878
  if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
2822
2879
  }
2880
+ // Stabilize the orthographic receiver grid against sub-texel camera
2881
+ // motion. The raster pass writes each cascade into a square `mapSize`
2882
+ // viewport, so the light-space texel pitch is the fitted extent divided
2883
+ // by that viewport size. Snap the fitted center to that grid and retain a
2884
+ // half-texel guard on each side; caster and receiver projections then move
2885
+ // together in whole shadow texels without clipping a frustum corner when
2886
+ // the rounded center shifts by half a texel.
2887
+ const texelX = (maxX - minX) / config.mapSize;
2888
+ const texelY = (maxY - minY) / config.mapSize;
2889
+ if (Number.isFinite(texelX) && texelX > 0) {
2890
+ const centerX = (minX + maxX) * 0.5;
2891
+ const snappedCenterX = Math.round(centerX / texelX) * texelX;
2892
+ const halfWidth = (maxX - minX) * 0.5 + texelX * 0.5;
2893
+ minX = snappedCenterX - halfWidth;
2894
+ maxX = snappedCenterX + halfWidth;
2895
+ }
2896
+ if (Number.isFinite(texelY) && texelY > 0) {
2897
+ const centerY = (minY + maxY) * 0.5;
2898
+ const snappedCenterY = Math.round(centerY / texelY) * texelY;
2899
+ const halfHeight = (maxY - minY) * 0.5 + texelY * 0.5;
2900
+ minY = snappedCenterY - halfHeight;
2901
+ maxY = snappedCenterY + halfHeight;
2902
+ }
2823
2903
  // Orthographic projection from light-space AABB. RC-2: whole-frustum
2824
2904
  // toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
2825
2905
  const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
@@ -2924,15 +3004,29 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2924
3004
  const cameraQuery = world
2925
3005
  .query({
2926
3006
  read: [Camera],
2927
- optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection],
3007
+ optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection, DepthOfField],
2928
3008
  with: [Transform, GlobalTransform],
2929
3009
  })
2930
3010
  .unwrap();
2931
3011
  for (const row of cameraQuery) {
2932
3012
  const cam = row.get(Camera);
2933
3013
  const motionBlur = row.get(MotionBlur);
3014
+ const motionBlurResult = resolveMotionBlurParams(motionBlur);
3015
+ if (!motionBlurResult.ok) throw motionBlurResult.error;
3016
+ const motionBlurParams = motionBlurResult.value;
3017
+ const depthOfField = row.get(DepthOfField);
2934
3018
  const dynamicResolution = row.get(DynamicResolution);
2935
3019
  const screenSpaceReflection = row.get(ScreenSpaceReflection);
3020
+ const projection = cameraProjectionFromF32(cam.projection);
3021
+ const depthOfFieldResult = resolveDepthOfFieldParams(depthOfField, {
3022
+ projection,
3023
+ fov: cam.fov,
3024
+ near: cam.near,
3025
+ far: cam.far,
3026
+ });
3027
+ const depthOfFieldError = depthOfFieldResult.ok
3028
+ ? undefined
3029
+ : depthOfFieldRequestFailure(depthOfFieldResult.error);
2936
3030
  const entity = row.entity;
2937
3031
  const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
2938
3032
  if (view === undefined) continue;
@@ -2967,7 +3061,7 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2967
3061
  aspect: cam.aspect,
2968
3062
  near: cam.near,
2969
3063
  far: cam.far,
2970
- projection: cameraProjectionFromF32(cam.projection),
3064
+ projection,
2971
3065
  orthoLeft: cam.left,
2972
3066
  orthoRight: cam.right,
2973
3067
  orthoBottom: cam.bottom,
@@ -2980,23 +3074,35 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2980
3074
  ...(dynamicResolutionResult.value === undefined
2981
3075
  ? {}
2982
3076
  : { dynamicResolution: dynamicResolutionResult.value }),
2983
- bloom: bloomEnabledFromF32(cam.bloom),
3077
+ bloom: validateCameraBloom(
3078
+ cam.bloom,
3079
+ cam.bloomThreshold,
3080
+ cam.bloomIntensity,
3081
+ cam.bloomSoftKnee,
3082
+ cam.bloomScatter,
3083
+ ),
2984
3084
  bloomThreshold: cam.bloomThreshold,
2985
3085
  bloomIntensity: cam.bloomIntensity,
2986
- bloomBlurRadius: cam.bloomBlurRadius,
3086
+ bloomSoftKnee: cam.bloomSoftKnee,
3087
+ bloomScatter: cam.bloomScatter,
2987
3088
  clearColor: [
2988
3089
  cam.clearColor[0] ?? 0,
2989
3090
  cam.clearColor[1] ?? 0,
2990
3091
  cam.clearColor[2] ?? 0,
2991
3092
  cam.clearColor[3] ?? 1,
2992
3093
  ],
2993
- ...(motionBlur === undefined
3094
+ ...(depthOfFieldResult.ok && depthOfFieldResult.value !== undefined
3095
+ ? { depthOfField: depthOfFieldResult.value }
3096
+ : {}),
3097
+ ...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
3098
+ ...(motionBlurParams === undefined
2994
3099
  ? {}
2995
3100
  : {
2996
3101
  motionBlur: {
2997
- shutterAngle: motionBlur.shutterAngle,
2998
- maxRadiusPixels: motionBlur.maxRadiusPixels,
2999
- sampleCount: motionBlur.sampleCount,
3102
+ shutterAngle: motionBlurParams.shutterAngle,
3103
+ maxRadiusPixels: motionBlurParams.maxRadiusPixels,
3104
+ sampleCount: motionBlurParams.sampleCount,
3105
+ targetFps: motionBlurParams.targetFps,
3000
3106
  },
3001
3107
  }),
3002
3108
  ...(screenSpaceReflection === undefined
@@ -3254,6 +3360,8 @@ export function extractVolumeSnapshot(
3254
3360
  assets: AssetRegistry | null | undefined,
3255
3361
  worldInternal: WorldInternalView,
3256
3362
  ): ExtractedVolumetricFog | undefined {
3363
+ const elapsed = world.getResource(Time).elapsed;
3364
+ const worldTimeSeconds = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
3257
3365
  const query = world.query({ read: [VolumetricFog] }).unwrap();
3258
3366
  const authorings: VolumetricFogAuthoring[] = [];
3259
3367
  let selectedLight:
@@ -3299,7 +3407,7 @@ export function extractVolumeSnapshot(
3299
3407
  : resolved.error,
3300
3408
  { severity: Severity.Error, systemName: 'RenderSystem.extract (volumetric-density)' },
3301
3409
  );
3302
- return { status: 'degraded' };
3410
+ return { status: 'degraded', worldTimeSeconds };
3303
3411
  }
3304
3412
  const densityAsset = resolved.value;
3305
3413
  const guid = assets?.guidOf(densityAsset) ?? `handle:${Number(densityHandle)}`;
@@ -3363,6 +3471,7 @@ export function extractVolumeSnapshot(
3363
3471
  });
3364
3472
  return {
3365
3473
  status: 'degraded',
3474
+ worldTimeSeconds,
3366
3475
  ...(first === undefined
3367
3476
  ? {}
3368
3477
  : {
@@ -3402,6 +3511,7 @@ export function extractVolumeSnapshot(
3402
3511
  };
3403
3512
  return {
3404
3513
  status: 'available',
3514
+ worldTimeSeconds,
3405
3515
  fog: extracted.value.fog,
3406
3516
  densityHandle: first.densityHandle,
3407
3517
  densityAsset: first.densityAsset,