@forgeax/engine-render 0.1.33 → 0.1.34

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 (518) hide show
  1. package/README.md +1010 -124
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  11. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  12. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  14. package/dist/assembly/factory.d.ts +1 -1
  15. package/dist/assembly/factory.d.ts.map +1 -1
  16. package/dist/assembly/host-contract.d.ts +3 -3
  17. package/dist/assembly/host-contract.d.ts.map +1 -1
  18. package/dist/assembly/material/assembly.d.ts.map +1 -1
  19. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  20. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  21. package/dist/assembly/material-shader-policy.d.ts +16 -2
  22. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  23. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  25. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  27. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  28. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  32. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-inspection.d.ts +11 -0
  35. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  36. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  37. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  38. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  39. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +1 -1
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  42. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  43. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  44. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  45. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  46. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  47. package/dist/authoring.mjs +1 -1
  48. package/dist/barrel-distortion.d.ts +47 -0
  49. package/dist/barrel-distortion.d.ts.map +1 -0
  50. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  51. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  52. package/dist/{chunk-ZDQWGQW7.mjs → chunk-HEB564WF.mjs} +4 -4
  53. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-HEB564WF.mjs.map} +1 -1
  54. package/dist/{chunk-VOZWDM3L.mjs → chunk-MKDIT2EM.mjs} +1307 -188
  55. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  56. package/dist/{chunk-LEAXOMYJ.mjs → chunk-OCDLEI6I.mjs} +3 -3
  57. package/dist/{chunk-LEAXOMYJ.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  58. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  59. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  60. package/dist/{chunk-3RTCJTH4.mjs → chunk-SHFUFJ5N.mjs} +184 -13
  61. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  62. package/dist/{chunk-OO3DRYK7.mjs → chunk-UI5SFODX.mjs} +176 -96
  63. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  64. package/dist/{chunk-PLAXOOX5.mjs → chunk-UINHLCZA.mjs} +561 -79
  65. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  66. package/dist/{chunk-NB2S7ZRX.mjs → chunk-WEX45HKH.mjs} +127 -386
  67. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  68. package/dist/cloud/density.d.ts +55 -0
  69. package/dist/cloud/density.d.ts.map +1 -0
  70. package/dist/cloud/extract.d.ts +25 -0
  71. package/dist/cloud/extract.d.ts.map +1 -0
  72. package/dist/cloud/feature.d.ts +57 -0
  73. package/dist/cloud/feature.d.ts.map +1 -0
  74. package/dist/cloud/frame-context.d.ts +8 -0
  75. package/dist/cloud/frame-context.d.ts.map +1 -0
  76. package/dist/cloud/inspection.d.ts +59 -0
  77. package/dist/cloud/inspection.d.ts.map +1 -0
  78. package/dist/cloud/optics.d.ts +56 -0
  79. package/dist/cloud/optics.d.ts.map +1 -0
  80. package/dist/cloud/parameters.d.ts +73 -0
  81. package/dist/cloud/parameters.d.ts.map +1 -0
  82. package/dist/cloud/resources.d.ts +60 -0
  83. package/dist/cloud/resources.d.ts.map +1 -0
  84. package/dist/cloud/shadow.d.ts +42 -0
  85. package/dist/cloud/shadow.d.ts.map +1 -0
  86. package/dist/cloud/temporal.d.ts +74 -0
  87. package/dist/cloud/temporal.d.ts.map +1 -0
  88. package/dist/components/barrel-distortion.d.ts +13 -0
  89. package/dist/components/barrel-distortion.d.ts.map +1 -0
  90. package/dist/components/cloud-layer.d.ts +29 -0
  91. package/dist/components/cloud-layer.d.ts.map +1 -0
  92. package/dist/components/index.d.ts +2 -0
  93. package/dist/components/index.d.ts.map +1 -1
  94. package/dist/components/instances.d.ts +20 -6
  95. package/dist/components/instances.d.ts.map +1 -1
  96. package/dist/construct-renderer.d.ts +1 -1
  97. package/dist/construct-renderer.d.ts.map +1 -1
  98. package/dist/construct-renderer.mjs +6759 -2875
  99. package/dist/construct-renderer.mjs.map +1 -1
  100. package/dist/dynamic-geometry.d.ts +5 -1
  101. package/dist/dynamic-geometry.d.ts.map +1 -1
  102. package/dist/errors/cloud.d.ts +59 -0
  103. package/dist/errors/cloud.d.ts.map +1 -0
  104. package/dist/errors/render.d.ts +18 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  107. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  108. package/dist/extract/camera.d.ts.map +1 -1
  109. package/dist/extract/contracts.d.ts +5 -1
  110. package/dist/extract/contracts.d.ts.map +1 -1
  111. package/dist/features/barrel-distortion.d.ts +29 -0
  112. package/dist/features/barrel-distortion.d.ts.map +1 -0
  113. package/dist/features/error-reporting.d.ts +6 -2
  114. package/dist/features/error-reporting.d.ts.map +1 -1
  115. package/dist/features/host.d.ts +10 -0
  116. package/dist/features/host.d.ts.map +1 -1
  117. package/dist/features/plan.d.ts +8 -0
  118. package/dist/features/plan.d.ts.map +1 -1
  119. package/dist/features/render-graph-contribution.d.ts +5 -0
  120. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  121. package/dist/features/render-graph-raster.d.ts +1 -0
  122. package/dist/features/render-graph-raster.d.ts.map +1 -1
  123. package/dist/features/targets.d.ts +2 -0
  124. package/dist/features/targets.d.ts.map +1 -1
  125. package/dist/features/types.d.ts +44 -0
  126. package/dist/features/types.d.ts.map +1 -1
  127. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  128. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  129. package/dist/gpu-dirty-ranges.d.ts +11 -0
  130. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  131. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  132. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  133. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  134. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  135. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  136. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  137. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  138. package/dist/gpu-driven/production-raster.d.ts +47 -3
  139. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  140. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  141. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  142. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  143. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  144. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  145. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  146. package/dist/gpu-scene.d.ts +5 -0
  147. package/dist/gpu-scene.d.ts.map +1 -1
  148. package/dist/hdrp-buffers.d.ts +2 -2
  149. package/dist/hdrp-buffers.d.ts.map +1 -1
  150. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +27 -7
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +258 -8
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +135 -1
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instance-buffer-cache.d.ts +4 -4
  159. package/dist/instance-buffer-cache.d.ts.map +1 -1
  160. package/dist/instances.d.ts +48 -100
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.mjs +7 -7
  163. package/dist/pbr-pipeline.d.ts +19 -1
  164. package/dist/pbr-pipeline.d.ts.map +1 -1
  165. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  166. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  167. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  168. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  169. package/dist/pipeline/standard-output/types.d.ts +4 -2
  170. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  171. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  172. package/dist/pipeline/standard-post.d.ts.map +1 -1
  173. package/dist/pipeline-builder.d.ts +1 -1
  174. package/dist/pipeline-builder.d.ts.map +1 -1
  175. package/dist/plugin.d.ts.map +1 -1
  176. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  177. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  178. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  179. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  180. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  181. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  182. package/dist/record/fold-buckets.d.ts +95 -0
  183. package/dist/record/fold-buckets.d.ts.map +1 -0
  184. package/dist/record/fold-instance-buffer.d.ts +6 -0
  185. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  186. package/dist/record/frame-lighting.d.ts +1 -0
  187. package/dist/record/frame-lighting.d.ts.map +1 -1
  188. package/dist/record/frame-snapshot.d.ts +37 -3
  189. package/dist/record/frame-snapshot.d.ts.map +1 -1
  190. package/dist/record/frame.d.ts +10 -61
  191. package/dist/record/frame.d.ts.map +1 -1
  192. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  193. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  194. package/dist/record/gpu-timing.d.ts +8 -0
  195. package/dist/record/gpu-timing.d.ts.map +1 -1
  196. package/dist/record/main-pass-geometry.d.ts +21 -5
  197. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  198. package/dist/record/main-pass-material.d.ts +7 -1
  199. package/dist/record/main-pass-material.d.ts.map +1 -1
  200. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  201. package/dist/record/main-pass.d.ts +13 -2
  202. package/dist/record/main-pass.d.ts.map +1 -1
  203. package/dist/record/motion-blur-receipt.d.ts +11 -0
  204. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  205. package/dist/record/probe-blend-buffer.d.ts +16 -0
  206. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  207. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  208. package/dist/record/render-context.d.ts +22 -3
  209. package/dist/record/render-context.d.ts.map +1 -1
  210. package/dist/record/shadow-pass.d.ts +2 -0
  211. package/dist/record/shadow-pass.d.ts.map +1 -1
  212. package/dist/record/skybox-post-pass.d.ts +1 -1
  213. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  214. package/dist/record/typed-frame-graph.d.ts +22 -2
  215. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  216. package/dist/record/view-ubo.d.ts +11 -3
  217. package/dist/record/view-ubo.d.ts.map +1 -1
  218. package/dist/record/volume-params.d.ts +9 -0
  219. package/dist/record/volume-params.d.ts.map +1 -0
  220. package/dist/recovery/render-system-candidate.d.ts +10 -0
  221. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  222. package/dist/reflection/record-owner.d.ts.map +1 -1
  223. package/dist/render-contract.d.ts +42 -4
  224. package/dist/render-contract.d.ts.map +1 -1
  225. package/dist/render-graph-primitives.d.ts.map +1 -1
  226. package/dist/render-pipeline.d.ts +103 -2
  227. package/dist/render-pipeline.d.ts.map +1 -1
  228. package/dist/render-system-extract-tail.d.ts +3 -2
  229. package/dist/render-system-extract-tail.d.ts.map +1 -1
  230. package/dist/render-system-extract.d.ts +16 -9
  231. package/dist/render-system-extract.d.ts.map +1 -1
  232. package/dist/render-system-presentation.d.ts +23 -0
  233. package/dist/render-system-presentation.d.ts.map +1 -0
  234. package/dist/render-system-producer-roots.d.ts +10 -0
  235. package/dist/render-system-producer-roots.d.ts.map +1 -0
  236. package/dist/render-system-residency.d.ts +12 -0
  237. package/dist/render-system-residency.d.ts.map +1 -0
  238. package/dist/render-system.d.ts +19 -14
  239. package/dist/render-system.d.ts.map +1 -1
  240. package/dist/scene/probe-blend.d.ts +27 -0
  241. package/dist/scene/probe-blend.d.ts.map +1 -1
  242. package/dist/scene/render-scene.d.ts +12 -5
  243. package/dist/scene/render-scene.d.ts.map +1 -1
  244. package/dist/ssao-buffers.d.ts +9 -0
  245. package/dist/ssao-buffers.d.ts.map +1 -1
  246. package/dist/surface/dynamic-input.d.ts +133 -0
  247. package/dist/surface/dynamic-input.d.ts.map +1 -0
  248. package/dist/surface/gpu-driven.d.ts +41 -0
  249. package/dist/surface/gpu-driven.d.ts.map +1 -0
  250. package/dist/surface/submission-observation.d.ts +136 -0
  251. package/dist/surface/submission-observation.d.ts.map +1 -0
  252. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  253. package/dist/temporal/gpu.d.ts +13 -1
  254. package/dist/temporal/gpu.d.ts.map +1 -1
  255. package/dist/temporal/index.mjs +5 -5
  256. package/dist/temporal/inspection.d.ts.map +1 -1
  257. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  258. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  259. package/dist/transmission/single-layer-medium.d.ts +89 -0
  260. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  261. package/dist/typed-render-graph-primitives.d.ts +14 -2
  262. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  263. package/dist/volume/component.d.ts +9 -4
  264. package/dist/volume/component.d.ts.map +1 -1
  265. package/dist/volume/extract.d.ts +1 -1
  266. package/dist/volume/extract.d.ts.map +1 -1
  267. package/dist/volume/inspection.d.ts +2 -0
  268. package/dist/volume/inspection.d.ts.map +1 -1
  269. package/dist/volume/passes.d.ts +4 -1
  270. package/dist/volume/passes.d.ts.map +1 -1
  271. package/dist/volume/resources.d.ts.map +1 -1
  272. package/package.json +22 -21
  273. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  274. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  275. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  276. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  277. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  278. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  279. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  280. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  281. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  282. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  283. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  284. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  285. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  286. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  287. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  288. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  289. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  290. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  291. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  292. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  293. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  294. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  295. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  296. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  297. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  298. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  299. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  300. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  301. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  302. package/src/__tests__/instances-store.unit.test.ts +97 -305
  303. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  304. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  305. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  306. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  307. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  308. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  309. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  310. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  311. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  312. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  313. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  314. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  315. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  316. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  317. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  318. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  319. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  320. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  321. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  322. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  323. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  324. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  325. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  326. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  327. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  328. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  329. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  330. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  331. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  332. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  333. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  334. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  335. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  336. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  337. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  338. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  339. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  340. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  341. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  342. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  343. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  344. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  345. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  346. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  347. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  348. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  349. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  350. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  351. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  352. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  353. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  354. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  355. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  356. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  357. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  358. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  359. package/src/assembly/backend-contract.ts +30 -0
  360. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  361. package/src/assembly/canvas-draw-guard.ts +11 -0
  362. package/src/assembly/cloud-shadow-entry.ts +22 -0
  363. package/src/assembly/dynamic-geometry-host.ts +65 -37
  364. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  365. package/src/assembly/factory.ts +2 -30
  366. package/src/assembly/host-contract.ts +3 -3
  367. package/src/assembly/material/assembly.ts +25 -13
  368. package/src/assembly/material/pipeline-helpers.ts +53 -12
  369. package/src/assembly/material-shader-policy.ts +55 -0
  370. package/src/assembly/material-shader-prewarm.ts +21 -10
  371. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  372. package/src/assembly/recovery/renderer-recover.ts +41 -18
  373. package/src/assembly/render-feature-post-processes.ts +54 -0
  374. package/src/assembly/renderer-facade.ts +4 -1
  375. package/src/assembly/renderer-feature-inspection.ts +8 -2
  376. package/src/assembly/renderer-frame-transaction.ts +23 -0
  377. package/src/assembly/renderer-inspection.ts +28 -0
  378. package/src/assembly/skin-palette-owner.ts +1 -1
  379. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  380. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  381. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  382. package/src/assembly/webgpu-ready.ts +115 -61
  383. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  384. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  385. package/src/assembly/webgpu-renderer.ts +214 -252
  386. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  387. package/src/barrel-distortion.ts +247 -0
  388. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  389. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  390. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  391. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  392. package/src/cloud/density.ts +600 -0
  393. package/src/cloud/extract.ts +110 -0
  394. package/src/cloud/feature.ts +1810 -0
  395. package/src/cloud/frame-context.ts +42 -0
  396. package/src/cloud/inspection.ts +130 -0
  397. package/src/cloud/optics.ts +285 -0
  398. package/src/cloud/parameters.ts +255 -0
  399. package/src/cloud/resources.ts +122 -0
  400. package/src/cloud/shadow.ts +167 -0
  401. package/src/cloud/temporal.ts +226 -0
  402. package/src/components/barrel-distortion.ts +46 -0
  403. package/src/components/cloud-layer.ts +47 -0
  404. package/src/components/index.ts +2 -0
  405. package/src/components/instances.ts +18 -45
  406. package/src/components/sprite-instances.ts +1 -1
  407. package/src/construct-renderer.ts +1 -1
  408. package/src/dynamic-geometry.ts +12 -1
  409. package/src/errors/cloud.ts +103 -0
  410. package/src/errors/render.ts +44 -7
  411. package/src/examples/single-layer-medium-surface.ts +59 -0
  412. package/src/extract/camera.ts +15 -1
  413. package/src/extract/contracts.ts +5 -0
  414. package/src/features/barrel-distortion.ts +172 -0
  415. package/src/features/error-reporting.ts +26 -6
  416. package/src/features/host.ts +115 -14
  417. package/src/features/plan.ts +8 -0
  418. package/src/features/render-graph-contribution.ts +9 -0
  419. package/src/features/render-graph-raster.ts +45 -3
  420. package/src/features/targets.ts +66 -0
  421. package/src/features/types.ts +47 -0
  422. package/src/fullscreen-post-process-pass.ts +41 -9
  423. package/src/gpu-dirty-ranges.ts +48 -0
  424. package/src/gpu-driven/batch-topology.ts +53 -0
  425. package/src/gpu-driven/material-artifacts.ts +33 -25
  426. package/src/gpu-driven/prepared-draw.ts +12 -4
  427. package/src/gpu-driven/production-raster-material.ts +1894 -0
  428. package/src/gpu-driven/production-raster.ts +825 -784
  429. package/src/gpu-driven/resource-allocation.ts +142 -0
  430. package/src/gpu-driven/shadow-ownership.ts +71 -0
  431. package/src/gpu-driven/view-gpu.ts +164 -10
  432. package/src/gpu-scene.ts +75 -84
  433. package/src/hdrp-buffers.ts +4 -3
  434. package/src/ibl/skylight-bind-group.ts +49 -7
  435. package/src/index.ts +196 -9
  436. package/src/inspection-types.ts +162 -0
  437. package/src/instance-buffer-cache.ts +4 -4
  438. package/src/instances.ts +149 -569
  439. package/src/pbr-pipeline.ts +219 -13
  440. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  441. package/src/pipeline/standard-forward-lane.ts +313 -57
  442. package/src/pipeline/standard-output/graph.ts +8 -2
  443. package/src/pipeline/standard-output/types.ts +12 -1
  444. package/src/pipeline/standard-pipeline.ts +291 -18
  445. package/src/pipeline/standard-post.ts +90 -24
  446. package/src/pipeline-builder.ts +1 -1
  447. package/src/plugin.ts +4 -0
  448. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  449. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  450. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  451. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  452. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  453. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  454. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  455. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  456. package/src/record/barrel-distortion-frame.ts +110 -0
  457. package/src/record/dynamic-geometry-consumption.ts +39 -9
  458. package/src/record/fold-buckets.ts +852 -0
  459. package/src/record/fold-instance-buffer.ts +51 -0
  460. package/src/record/frame-lighting.ts +36 -2
  461. package/src/record/frame-snapshot.ts +39 -3
  462. package/src/record/frame.ts +217 -1084
  463. package/src/record/gpu-draw-receipts.ts +38 -0
  464. package/src/record/gpu-timing.ts +48 -5
  465. package/src/record/main-pass-geometry.ts +251 -82
  466. package/src/record/main-pass-material.ts +66 -4
  467. package/src/record/main-pass-sprite-draws.ts +7 -65
  468. package/src/record/main-pass.ts +156 -20
  469. package/src/record/motion-blur-receipt.ts +91 -0
  470. package/src/record/probe-blend-buffer.ts +180 -0
  471. package/src/record/recovery-pipeline.ts +35 -3
  472. package/src/record/render-context.ts +39 -3
  473. package/src/record/shadow-pass.ts +30 -1
  474. package/src/record/skybox-post-pass.ts +28 -4
  475. package/src/record/typed-frame-graph.ts +770 -134
  476. package/src/record/view-ubo.ts +26 -3
  477. package/src/record/volume-params.ts +225 -0
  478. package/src/recovery/render-system-candidate.ts +235 -42
  479. package/src/reflection/record-owner.ts +7 -3
  480. package/src/render-contract.ts +48 -4
  481. package/src/render-graph-primitives.ts +7 -1
  482. package/src/render-pipeline.ts +130 -0
  483. package/src/render-system-extract-tail.ts +57 -61
  484. package/src/render-system-extract.ts +74 -76
  485. package/src/render-system-presentation.ts +56 -0
  486. package/src/render-system-producer-roots.ts +96 -0
  487. package/src/render-system-residency.ts +159 -0
  488. package/src/render-system.ts +347 -363
  489. package/src/scene/probe-blend.ts +96 -4
  490. package/src/scene/render-scene.ts +349 -280
  491. package/src/ssao-buffers.ts +95 -2
  492. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  493. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  494. package/src/surface/dynamic-input.ts +658 -0
  495. package/src/surface/gpu-driven.ts +147 -0
  496. package/src/surface/submission-observation.ts +517 -0
  497. package/src/systems/skin-palette-allocator.ts +4 -1
  498. package/src/temporal/gpu.ts +101 -17
  499. package/src/temporal/inspection.ts +7 -1
  500. package/src/temporal/standard-scene-data.ts +5 -0
  501. package/src/temporal/target-coverage-attachment.ts +4 -0
  502. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  503. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  504. package/src/transmission/single-layer-medium.ts +322 -0
  505. package/src/typed-render-graph-primitives.ts +70 -2
  506. package/src/volume/component.ts +10 -9
  507. package/src/volume/extract.ts +11 -5
  508. package/src/volume/inspection.ts +3 -0
  509. package/src/volume/passes.ts +87 -4
  510. package/src/volume/resources.ts +2 -1
  511. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  512. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  513. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  514. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  515. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  516. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  517. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  518. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
@@ -0,0 +1,1810 @@
1
+ import { ok, type Result } from '@forgeax/engine-types';
2
+ import type { RenderError } from '../errors/render';
3
+ import type { RenderFeaturePlan, RenderFeaturePlanContext } from '../features/plan';
4
+ import type { RenderFeature, RenderFeatureExtractContext } from '../features/types';
5
+ import { buildCloudDensityCache, CLOUD_VERTICAL_CELLS, type CloudDensityCache } from './density';
6
+ import {
7
+ type CloudLayerInspection,
8
+ cloudCapabilitiesFromRhi,
9
+ inspectCloudLayer,
10
+ } from './inspection';
11
+ import {
12
+ CLOUD_EXTINCTION_COEFFICIENT,
13
+ CLOUD_QUALITY_PROFILES,
14
+ cloudLayerFormationKey,
15
+ cloudShadowResolutionForQuality,
16
+ cloudViewDistanceForQuality,
17
+ type ValidatedCloudLayer,
18
+ } from './parameters';
19
+ import { inspectCloudLayerResources } from './resources';
20
+ import { type CloudShadowProjection, createCloudShadowProjection } from './shadow';
21
+ import {
22
+ CloudHistoryStore,
23
+ type CloudTemporalFrame,
24
+ type CloudTemporalSignature,
25
+ createCloudHistory,
26
+ } from './temporal';
27
+
28
+ export const CLOUD_LAYER_FEATURE_IDENTITY = 'forgeax.cloud-layer';
29
+ export const CLOUD_LAYER_DENSITY_PROGRAM = 'cloud-layer-density';
30
+ export const CLOUD_LAYER_DENSITY_BINDINGS = 'cloud-layer-density-bindings';
31
+ export const CLOUD_LAYER_DENSITY_CACHE = 'cloud-layer-density-cache';
32
+ export const CLOUD_LAYER_DENSITY_OUTPUT = 'cloud-layer-density-output';
33
+
34
+ const COMPUTE_STAGE = 0x4;
35
+ const CLOUD_WORKGROUP_SIZE = 64;
36
+ // 24 vec4/u32 lanes: authored field, sun, integration mode and the
37
+ // renderer-owned stable light-space shadow projection. The same POD is used by
38
+ // the display and shadow entry points so they cannot drift in their density or
39
+ // cache addressing contract.
40
+ // The final lane carries temporal validity/reset/blend. Keep the payload
41
+ // explicitly 16-byte aligned so the public schema, prepared UBO and WGSL stay
42
+ // one contract.
43
+ export const CLOUD_VIEW_PARAMS_BYTES = 176;
44
+
45
+ /** R8 cache bytes are packed into little-endian u32 storage elements. */
46
+ function packCloudDensityCache(cache: CloudDensityCache): Uint32Array {
47
+ const packed = new Uint32Array(Math.max(1, Math.ceil(cache.formationData.byteLength / 4)));
48
+ for (let index = 0; index < cache.formationData.byteLength; index += 1) {
49
+ const wordIndex = index >>> 2;
50
+ packed[wordIndex] =
51
+ (packed[wordIndex] ?? 0) | ((cache.formationData[index] ?? 0) << ((index & 3) * 8));
52
+ }
53
+ return packed;
54
+ }
55
+
56
+ /** Minimal copy compute; one invocation owns one packed u32 cache element. */
57
+ export const CLOUD_DENSITY_COMPUTE_WGSL = /* wgsl */ `
58
+ struct CloudCacheParams { count: u32, }
59
+ @group(0) @binding(0) var<storage, read> sourceDensity: array<u32>;
60
+ @group(0) @binding(1) var<storage, read_write> cachedDensity: array<u32>;
61
+ @group(0) @binding(2) var<uniform> params: CloudCacheParams;
62
+ @compute @workgroup_size(64)
63
+ fn cloud_density_cache(@builtin(global_invocation_id) id: vec3<u32>) {
64
+ if (id.x >= params.count) { return; }
65
+ cachedDensity[id.x] = sourceDensity[id.x];
66
+ }
67
+ `;
68
+
69
+ /**
70
+ * Cloud view transport. The View group is the renderer-owned camera/light
71
+ * source of truth; group(1) carries frame-local cloud parameters, depth and
72
+ * the bounded packed 3D cache. Camera, solar-column and cloud-interior
73
+ * integration all call cloud_density(), so wind only changes the sample
74
+ * coordinate and never the authored field.
75
+ */
76
+ export const CLOUD_VIEW_FULLSCREEN_WGSL = /* wgsl */ `
77
+ const CLOUD_VERTICAL_CELLS: f32 = ${CLOUD_VERTICAL_CELLS};
78
+ const CLOUD_VERTICAL_NOISE_CELLS: f32 = 2.0;
79
+
80
+ struct CloudCameraView {
81
+ worldViewProj: mat4x4<f32>,
82
+ lightDir: vec3<f32>,
83
+ lightColor: vec3<f32>,
84
+ cameraPos: vec3<f32>,
85
+ lightViewProj_A: mat4x4<f32>,
86
+ inverseViewProj: mat4x4<f32>,
87
+ lightViewProj_B: mat4x4<f32>,
88
+ lightViewProj_C: mat4x4<f32>,
89
+ lightViewProj_D: mat4x4<f32>,
90
+ splitPlanes: array<vec4<f32>, 4>,
91
+ cascadeCount: f32,
92
+ cascadeBlend: f32,
93
+ depthBias: f32,
94
+ normalBias: f32,
95
+ directionalShadowFilter: vec4<f32>,
96
+ spotLightViewProj: array<mat4x4<f32>, 4>,
97
+ temporalCurrentViewProj: mat4x4<f32>,
98
+ temporalPreviousViewProj: mat4x4<f32>,
99
+ temporalProjection: vec4<f32>,
100
+ temporalPreviousCameraPos: vec4<f32>,
101
+ ssrParams: vec4<f32>,
102
+ };
103
+
104
+ struct CloudViewParams {
105
+ layer: vec4<f32>, // baseHeight, thickness, scale, density
106
+ field: vec4<f32>, // coverage, timeSeconds, maxDistance, seed
107
+ wind: vec4<f32>, // xyz m/s
108
+ sunDirection: vec4<f32>,
109
+ sunRadiance: vec4<f32>,
110
+ integration: vec4<u32>, // cacheResolution, viewSteps, solarSteps, reserved
111
+ shadowOrigin: vec4<f32>,
112
+ shadowRight: vec4<f32>,
113
+ shadowUp: vec4<f32>,
114
+ shadowProjection: vec4<f32>, // range, valid, lowSun, texelSize
115
+ temporal: vec4<f32>, // valid, reset, historyWeight, reserved
116
+ };
117
+
118
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
119
+ @group(1) @binding(0) var sceneColor: texture_2d<f32>;
120
+ @group(1) @binding(1) var sceneSampler: sampler;
121
+ @group(1) @binding(2) var<uniform> cloud: CloudViewParams;
122
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
123
+ @group(1) @binding(4) var depthSampler: sampler;
124
+ @group(1) @binding(5) var previousRadiance: texture_2d<f32>;
125
+ @group(1) @binding(6) var previousTransmittance: texture_2d<f32>;
126
+ @group(1) @binding(7) var previousDepth: texture_2d<f32>;
127
+ @group(1) @binding(8) var<storage, read> densityCache: array<u32>;
128
+
129
+ struct CloudViewOut {
130
+ @builtin(position) position: vec4<f32>,
131
+ @location(0) uv: vec2<f32>,
132
+ };
133
+
134
+ fn cloud_hash3(cell: vec3<i32>, seed: u32) -> f32 {
135
+ var h = bitcast<u32>(cell.x) * 0x45d9f3bu;
136
+ h = (h ^ (bitcast<u32>(cell.y) * 0x119de1f3u)) * 0x45d9f3bu;
137
+ h = (h ^ (bitcast<u32>(cell.z) * 0x3449fu)) * 0x45d9f3bu;
138
+ h = h ^ seed;
139
+ h = (h ^ (h >> 16u)) * 0x45d9f3bu;
140
+ h = (h ^ (h >> 13u)) * 0x27d4eb2du;
141
+ return f32(h ^ (h >> 16u)) / 4294967296.0;
142
+ }
143
+
144
+ fn cloud_wrap_cell(value: i32, period: i32) -> i32 {
145
+ let wrapped = value % period;
146
+ return select(wrapped, wrapped + period, wrapped < 0);
147
+ }
148
+
149
+ fn cloud_hash3_periodic(
150
+ cell: vec3<i32>,
151
+ seed: u32,
152
+ periodX: i32,
153
+ periodY: i32,
154
+ periodZ: i32,
155
+ ) -> f32 {
156
+ return cloud_hash3(
157
+ vec3<i32>(
158
+ cloud_wrap_cell(cell.x, periodX),
159
+ cloud_wrap_cell(cell.y, periodY),
160
+ cloud_wrap_cell(cell.z, periodZ),
161
+ ),
162
+ seed,
163
+ );
164
+ }
165
+
166
+ fn cloud_value_noise(
167
+ position: vec3<f32>,
168
+ seed: u32,
169
+ periodX: i32,
170
+ periodY: i32,
171
+ periodZ: i32,
172
+ ) -> f32 {
173
+ let cell = vec3<i32>(floor(position));
174
+ let fraction = fract(position);
175
+ let smoothFraction = fraction * fraction * (3.0 - 2.0 * fraction);
176
+ let c000 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 0), seed, periodX, periodY, periodZ);
177
+ let c100 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 0), seed, periodX, periodY, periodZ);
178
+ let c010 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 0), seed, periodX, periodY, periodZ);
179
+ let c110 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 0), seed, periodX, periodY, periodZ);
180
+ let c001 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 1), seed, periodX, periodY, periodZ);
181
+ let c101 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 1), seed, periodX, periodY, periodZ);
182
+ let c011 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 1), seed, periodX, periodY, periodZ);
183
+ let c111 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 1), seed, periodX, periodY, periodZ);
184
+ let x00 = mix(c000, c100, smoothFraction.x);
185
+ let x10 = mix(c010, c110, smoothFraction.x);
186
+ let x01 = mix(c001, c101, smoothFraction.x);
187
+ let x11 = mix(c011, c111, smoothFraction.x);
188
+ return mix(mix(x00, x10, smoothFraction.y), mix(x01, x11, smoothFraction.y), smoothFraction.z);
189
+ }
190
+
191
+ fn cloud_cellular_noise(position: vec3<f32>, seed: u32, periodXZ: i32, periodY: i32) -> f32 {
192
+ let cell = vec3<i32>(floor(position));
193
+ let fraction = fract(position);
194
+ var nearest = 1e9;
195
+ var dz: i32 = -1;
196
+ loop {
197
+ var dy: i32 = -1;
198
+ loop {
199
+ var dx: i32 = -1;
200
+ loop {
201
+ let random = cloud_hash3_periodic(
202
+ cell + vec3<i32>(dx, dy, dz),
203
+ seed ^ 0x9e3779b9u,
204
+ periodXZ,
205
+ periodY,
206
+ periodXZ,
207
+ );
208
+ let point = vec3<f32>(
209
+ f32(dx) + fract(random * 17.0) - fraction.x,
210
+ f32(dy) + fract(random * 31.0) - fraction.y,
211
+ f32(dz) + fract(random * 47.0) - fraction.z,
212
+ );
213
+ nearest = min(nearest, dot(point, point));
214
+ if (dx >= 1) { break; }
215
+ dx = dx + 1;
216
+ }
217
+ if (dy >= 1) { break; }
218
+ dy = dy + 1;
219
+ }
220
+ if (dz >= 1) { break; }
221
+ dz = dz + 1;
222
+ }
223
+ return 1.0 - clamp(sqrt(nearest) * 1.25, 0.0, 1.0);
224
+ }
225
+
226
+ fn cloud_weather_field(position: vec3<f32>, seed: u32) -> f32 {
227
+ return smoothstep(
228
+ 0.34,
229
+ 0.66,
230
+ cloud_value_noise(
231
+ vec3<f32>(position.x * 2.0, 0.37, position.z * 2.0),
232
+ seed + 17041u,
233
+ 2,
234
+ 1,
235
+ 2,
236
+ ),
237
+ );
238
+ }
239
+
240
+ fn cloud_formation_field(position: vec3<f32>, seed: u32) -> vec3<f32> {
241
+ let macroWarpX = cloud_value_noise(
242
+ vec3<f32>(position.x * 2.0, position.y * CLOUD_VERTICAL_NOISE_CELLS + 3.0, position.z * 2.0),
243
+ seed + 41023u,
244
+ 2,
245
+ 2,
246
+ 2,
247
+ ) - 0.5;
248
+ let macroWarpZ = cloud_value_noise(
249
+ vec3<f32>(position.x * 2.0 + 5.0, position.y * CLOUD_VERTICAL_NOISE_CELLS - 7.0, position.z * 2.0),
250
+ seed + 41023u,
251
+ 2,
252
+ 2,
253
+ 2,
254
+ ) - 0.5;
255
+ let bodyX = position.x + macroWarpX * 0.3;
256
+ let bodyZ = position.z + macroWarpZ * 0.3;
257
+ var noise = 0.0;
258
+ var weight = 0.0;
259
+ var amplitude = 0.5;
260
+ var frequency = 1.0;
261
+ for (var octave = 0u; octave < 4u; octave = octave + 1u) {
262
+ let tilePeriod = 4.0 * frequency;
263
+ noise = noise + cloud_value_noise(
264
+ vec3<f32>(bodyX * tilePeriod, position.y * frequency * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * tilePeriod),
265
+ seed + octave * 1013u,
266
+ i32(tilePeriod),
267
+ i32(frequency * CLOUD_VERTICAL_NOISE_CELLS),
268
+ i32(tilePeriod),
269
+ ) * amplitude;
270
+ weight = weight + amplitude;
271
+ amplitude = amplitude * 0.4;
272
+ frequency = frequency * 2.0;
273
+ }
274
+ let broad = noise / max(0.0001, weight);
275
+ let cells = cloud_cellular_noise(
276
+ vec3<f32>(bodyX * 4.0, position.y * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * 4.0),
277
+ seed + 5011u,
278
+ 4,
279
+ i32(CLOUD_VERTICAL_NOISE_CELLS),
280
+ );
281
+ // Keep the broad field responsible for the cloud silhouette and use the
282
+ // cellular term as bounded breakup. A broad-dominant mix reads as fewer,
283
+ // wider formations instead of evenly spaced foam balls.
284
+ let base = clamp(broad * 0.85 + cells * 0.15 + 0.15, 0.0, 1.0);
285
+ let weather = cloud_weather_field(position, seed);
286
+ let detailPosition = position * 3.0;
287
+ let warpX = cloud_value_noise(
288
+ vec3<f32>(detailPosition.x * 2.0, detailPosition.y * 2.0 + 11.0, detailPosition.z * 2.0),
289
+ seed + 29011u,
290
+ 2,
291
+ 2,
292
+ 2,
293
+ ) - 0.5;
294
+ let warpZ = cloud_value_noise(
295
+ vec3<f32>(detailPosition.x * 2.0 + 7.0, detailPosition.y * 2.0 - 5.0, detailPosition.z * 2.0),
296
+ seed + 29011u,
297
+ 2,
298
+ 2,
299
+ 2,
300
+ ) - 0.5;
301
+ let erosion = cloud_cellular_noise(
302
+ vec3<f32>(
303
+ (detailPosition.x + warpX * 0.22) * 12.0,
304
+ detailPosition.y * 3.0,
305
+ (detailPosition.z + warpZ * 0.22) * 12.0,
306
+ ),
307
+ seed + 7919u,
308
+ 12,
309
+ 3,
310
+ );
311
+ return vec3<f32>(weather, base, erosion);
312
+ }
313
+
314
+ fn cloud_height_profile(height: f32, body: f32, weather: f32) -> f32 {
315
+ // A body-dependent crown breaks the fixed horizontal slab silhouette while
316
+ // keeping the profile smooth enough for the bounded ray step budget.
317
+ let lowerEdge = 0.05 + (1.0 - body) * 0.1;
318
+ let crownEdge = min(0.96, 0.6 + body * 0.3 + weather * 0.08);
319
+ let lower = smoothstep(lowerEdge, min(1.0, lowerEdge + 0.12), height);
320
+ let upper = 1.0 - smoothstep(crownEdge, min(1.0, crownEdge + 0.14), height);
321
+ return lower * upper;
322
+ }
323
+
324
+ fn cloud_compose_density(height: f32, formation: vec3<f32>, coverage: f32) -> f32 {
325
+ let threshold = 1.0 - coverage * (0.45 + formation.x * 0.55);
326
+ let covered = smoothstep(0.0, 0.4, (formation.y - threshold) / max(0.001, 1.0 - threshold));
327
+ let shaped = covered * cloud_height_profile(height, formation.y, formation.x);
328
+ let erosion = (1.0 - formation.z) * 0.18;
329
+ return clamp((shaped - erosion) / (1.0 - erosion), 0.0, 1.0);
330
+ }
331
+
332
+ fn cloud_analytic_density(position: vec3<f32>) -> f32 {
333
+ let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
334
+ if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
335
+ let advected = position + cloud.wind.xyz * cloud.field.y;
336
+ let advectedHeight = height +
337
+ cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
338
+ let p = vec3<f32>(
339
+ advected.x * cloud.layer.z,
340
+ fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
341
+ advected.z * cloud.layer.z,
342
+ );
343
+ let formation = cloud_formation_field(p, u32(round(cloud.field.w)));
344
+ return cloud_compose_density(height, formation, cloud.field.x) * cloud.layer.w;
345
+ }
346
+
347
+ fn cloud_cache_byte(index: u32) -> f32 {
348
+ let word = densityCache[index >> 2u];
349
+ let shift = (index & 3u) * 8u;
350
+ return f32((word >> shift) & 255u) / 255.0;
351
+ }
352
+
353
+ fn cloud_cache_component(plane: u32, index: u32, stride: u32) -> f32 {
354
+ return cloud_cache_byte(plane * stride + index);
355
+ }
356
+
357
+ fn cloud_cache_trilinear(
358
+ plane: u32,
359
+ resolution: u32,
360
+ x0: u32,
361
+ x1: u32,
362
+ y0: u32,
363
+ y1: u32,
364
+ z0: u32,
365
+ z1: u32,
366
+ fx: f32,
367
+ fy: f32,
368
+ fz: f32,
369
+ ) -> f32 {
370
+ let stride = resolution * resolution * resolution;
371
+ let layer = resolution * resolution;
372
+ let c000 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x0, stride);
373
+ let c100 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x1, stride);
374
+ let c010 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x0, stride);
375
+ let c110 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x1, stride);
376
+ let c001 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x0, stride);
377
+ let c101 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x1, stride);
378
+ let c011 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x0, stride);
379
+ let c111 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x1, stride);
380
+ let x00 = mix(c000, c100, fx);
381
+ let x10 = mix(c010, c110, fx);
382
+ let x01 = mix(c001, c101, fx);
383
+ let x11 = mix(c011, c111, fx);
384
+ return mix(mix(x00, x10, fy), mix(x01, x11, fy), fz);
385
+ }
386
+
387
+ fn cloud_density(position: vec3<f32>) -> f32 {
388
+ let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
389
+ if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
390
+ let resolution = cloud.integration.x;
391
+ if (resolution < 4u) { return max(cloud_analytic_density(position), 0.0); }
392
+ let periodCoordinate = fract((position.xz + cloud.wind.xz * cloud.field.y) * cloud.layer.z);
393
+ let x = fract(periodCoordinate.x - 0.5 / f32(resolution)) * f32(resolution);
394
+ let z = fract(periodCoordinate.y - 0.5 / f32(resolution)) * f32(resolution);
395
+ let advectedHeight = height +
396
+ cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
397
+ let y = fract(advectedHeight - 0.5 / f32(resolution)) * f32(resolution);
398
+ let x0 = u32(floor(x));
399
+ let y0 = u32(floor(y));
400
+ let z0 = u32(floor(z));
401
+ let x1 = (x0 + 1u) % resolution;
402
+ let y1 = (y0 + 1u) % resolution;
403
+ let z1 = (z0 + 1u) % resolution;
404
+ let fx = x - f32(x0);
405
+ let fy = y - f32(y0);
406
+ let fz = z - f32(z0);
407
+ let weather = cloud_cache_trilinear(0u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
408
+ let body = cloud_cache_trilinear(1u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
409
+ // Repeat only the independent erosion plane: fine boundary structure with
410
+ // the same eight packed reads and the same cache allocation.
411
+ let detail = fract(vec3<f32>(periodCoordinate.x, advectedHeight, periodCoordinate.y) * 3.0 - vec3<f32>(0.5 / f32(resolution))) * f32(resolution);
412
+ let d0 = vec3<u32>(floor(detail));
413
+ let d1 = (d0 + vec3<u32>(1u)) % vec3<u32>(resolution);
414
+ let df = fract(detail);
415
+ let erosion = cloud_cache_trilinear(2u, resolution, d0.x, d1.x, d0.y, d1.y, d0.z, d1.z, df.x, df.y, df.z);
416
+ return cloud_compose_density(height, vec3<f32>(weather, body, erosion), cloud.field.x) * cloud.layer.w;
417
+ }
418
+
419
+ fn cloud_layer_interval(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> vec2<f32> {
420
+ if (abs(direction.y) < 0.000001) {
421
+ if (origin.y < cloud.layer.x || origin.y > cloud.layer.x + cloud.layer.y) {
422
+ return vec2<f32>(1.0, 0.0);
423
+ }
424
+ return vec2<f32>(0.0, maxDistance);
425
+ }
426
+ let lower = (cloud.layer.x - origin.y) / direction.y;
427
+ let upper = (cloud.layer.x + cloud.layer.y - origin.y) / direction.y;
428
+ return vec2<f32>(
429
+ max(0.0, min(lower, upper)),
430
+ min(maxDistance, max(lower, upper)),
431
+ );
432
+ }
433
+
434
+ fn cloud_solar_transmittance(position: vec3<f32>) -> f32 {
435
+ let sunDirection = normalize(-view.lightDir);
436
+ let solarDistance = select(cloud.field.z, cloud.shadowProjection.x, cloud.shadowProjection.x > 0.0);
437
+ let interval = cloud_layer_interval(position, sunDirection, solarDistance);
438
+ if (interval.y <= interval.x) { return 1.0; }
439
+ let steps = min(cloud.integration.z, 64u);
440
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
441
+ var opticalDepth = 0.0;
442
+ let solarJitter = cloud_solar_jitter(position);
443
+ for (var index = 0u; index < 64u; index = index + 1u) {
444
+ if (index >= steps) { break; }
445
+ let samplePosition = position + sunDirection * (interval.x + (f32(index) + solarJitter) * stepLength);
446
+ opticalDepth = opticalDepth + cloud_density(samplePosition) * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
447
+ }
448
+ return exp(-max(opticalDepth, 0.0));
449
+ }
450
+
451
+ fn cloud_henyey_greenstein(cosTheta: f32, g: f32) -> f32 {
452
+ let denominator = max(0.0001, 1.0 + g * g - 2.0 * g * cosTheta);
453
+ return (1.0 - g * g) / (denominator * sqrt(denominator)) * 0.0795774715;
454
+ }
455
+
456
+ fn cloud_phase(cosTheta: f32) -> f32 {
457
+ // A bounded dual lobe gives a readable forward highlight and a small
458
+ // back-scatter rim without another solar march.
459
+ return cloud_henyey_greenstein(cosTheta, 0.55) * 0.8 +
460
+ cloud_henyey_greenstein(cosTheta, -0.2) * 0.2;
461
+ }
462
+
463
+ // Reuse the same remaining-path optical depth for two bounded scattering
464
+ // orders. This adds arithmetic only; no extra density or light-ray samples.
465
+ fn cloud_incident_light(solarT: f32, cosTheta: f32, height: f32) -> vec3<f32> {
466
+ let direct = solarT * cloud_phase(cosTheta);
467
+ let multiple = (0.5 * pow(solarT, 0.5) + 0.25 * pow(solarT, 0.25)) * 0.0795774715;
468
+ let skyFill = mix(0.025, 0.075, smoothstep(0.1, 0.85, height));
469
+ // Match volume-integrate: the shared DirectionalLight value uses the
470
+ // unnormalized phase convention. Convert the normalized HG basis once.
471
+ return cloud.sunRadiance.xyz *
472
+ (vec3<f32>((direct + multiple) * 12.5663706144) + vec3<f32>(0.72, 0.84, 1.0) * skyFill);
473
+ }
474
+
475
+ fn cloud_ray_jitter(pixel: vec2<f32>) -> f32 {
476
+ // A stable per-pixel offset removes view-step contour bands without adding
477
+ // a frame-varying noise source that would fight temporal reprojection.
478
+ return fract(sin(dot(pixel, vec2<f32>(12.9898, 78.233))) * 43758.5453);
479
+ }
480
+
481
+ fn cloud_solar_jitter(position: vec3<f32>) -> f32 {
482
+ // Dither the light-column samples in world space so low shadow-step
483
+ // profiles do not paint parallel contour bands across cloud bases.
484
+ return fract(sin(dot(position.xz, vec2<f32>(19.193, 47.117))) * 15731.743);
485
+ }
486
+
487
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
488
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
489
+ let world = view.inverseViewProj * ndc;
490
+ return world.xyz / max(abs(world.w), 0.00001);
491
+ }
492
+
493
+ @vertex
494
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudViewOut {
495
+ var positions = array<vec2<f32>, 3>(
496
+ vec2<f32>(-1.0, -1.0),
497
+ vec2<f32>(3.0, -1.0),
498
+ vec2<f32>(-1.0, 3.0),
499
+ );
500
+ var output: CloudViewOut;
501
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
502
+ // WebGPU clip-space Y grows upward while sampled framebuffer rows and the
503
+ // depth texture use a top-left origin. Keep this one top-left UV for color,
504
+ // depth and inverse-projection reconstruction; flipping only the final
505
+ // color would leave the camera ray and depth cut mirrored.
506
+ output.uv = vec2<f32>(
507
+ positions[index].x * 0.5 + 0.5,
508
+ 1.0 - (positions[index].y * 0.5 + 0.5),
509
+ );
510
+ return output;
511
+ }
512
+
513
+ @fragment
514
+ fn fs_main(input: CloudViewOut) -> @location(0) vec4<f32> {
515
+ // The shadow producer uses this same production density/cache contract but
516
+ // rasterizes a camera-independent, texel-snapped light-space map. It does
517
+ // not consume scene colour; the generic fullscreen host still supplies the
518
+ // input binding to keep the feature binding ABI single and validated.
519
+ if (cloud.integration.w == 1u) {
520
+ let projection = cloud.shadowProjection;
521
+ if (projection.y < 0.5 || projection.z > 0.5 || projection.x <= 0.0) {
522
+ return vec4<f32>(1.0);
523
+ }
524
+ let offset = (input.uv - vec2<f32>(0.5)) * projection.x;
525
+ let receiver = cloud.shadowOrigin.xyz +
526
+ cloud.shadowRight.xyz * offset.x + cloud.shadowUp.xyz * offset.y;
527
+ let sunDirection = normalize(-view.lightDir);
528
+ let interval = cloud_layer_interval(receiver, sunDirection, projection.x);
529
+ if (interval.y <= interval.x) { return vec4<f32>(1.0); }
530
+ let steps = min(cloud.integration.z, 64u);
531
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
532
+ var opticalDepth = 0.0;
533
+ var firstHeight = 1.0;
534
+ var lastHeight = 0.0;
535
+ var occupied = false;
536
+ let solarJitter = cloud_solar_jitter(receiver);
537
+ for (var index = 0u; index < 64u; index = index + 1u) {
538
+ if (index >= steps) { break; }
539
+ let samplePosition = receiver + sunDirection *
540
+ (interval.x + (f32(index) + solarJitter) * stepLength);
541
+ let localDensity = cloud_density(samplePosition);
542
+ opticalDepth = opticalDepth + localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
543
+ if (localDensity > 0.0001) {
544
+ let sampleHeight = clamp(
545
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
546
+ 0.0,
547
+ 1.0,
548
+ );
549
+ if (!occupied) { firstHeight = sampleHeight; }
550
+ lastHeight = sampleHeight;
551
+ occupied = true;
552
+ }
553
+ }
554
+ let transmittance = exp(-max(opticalDepth, 0.0));
555
+ // R stores full-column transmittance. G/B preserve the first/last
556
+ // occupied normalized layer heights so interior samples can estimate the
557
+ // remaining segment without pretending optical depth is uniform through
558
+ // the whole layer. A stores the integrated optical depth and distinguishes
559
+ // an empty projected column from a valid, nearly-clear one.
560
+ return vec4<f32>(
561
+ transmittance,
562
+ select(1.0, firstHeight, occupied),
563
+ select(0.0, lastHeight, occupied),
564
+ select(0.0, opticalDepth, occupied),
565
+ );
566
+ }
567
+ // The shadow producer branch above is per-fragment (its light-space
568
+ // interval depends on the rasterized receiver), so derivative-based
569
+ // sampling here would violate WGSL uniform-control-flow rules. The cloud
570
+ // scene path already owns an explicit level-0 sampler contract.
571
+ // Keep point semantics for the current full-resolution scene path. The
572
+ // half-resolution transport contract is introduced with its own resolve
573
+ // shader so this path retains the established depth convention.
574
+ let depthPixel = vec2<i32>(input.position.xy);
575
+ let depth = textureLoad(sceneDepth, depthPixel, 0);
576
+ let farWorld = reconstruct_world(input.uv, 1.0);
577
+ let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
578
+ let ray = normalize(farWorld - view.cameraPos);
579
+ let rayJitter = cloud_ray_jitter(input.position.xy);
580
+ let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
581
+ let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
582
+
583
+ // The depth producer shares the same camera ray, interval and density
584
+ // contract as the display path. It writes a cloud representative world
585
+ // position and a validity bit so history remains comparable after camera
586
+ // translation or rotation and never uses an opaque scene distance as a
587
+ // proxy for a participating medium.
588
+ if (cloud.integration.w == 2u) {
589
+ if (interval.y <= interval.x) { return vec4<f32>(sceneWorld, 0.0); }
590
+ let depthSteps = min(cloud.integration.y, 64u);
591
+ let depthStepLength = (interval.y - interval.x) / max(1.0, f32(depthSteps));
592
+ var depthTransmittance = 1.0;
593
+ var weightedDistance = 0.0;
594
+ var opticalWeight = 0.0;
595
+ for (var depthIndex = 0u; depthIndex < 64u; depthIndex = depthIndex + 1u) {
596
+ if (depthIndex >= depthSteps) { break; }
597
+ let sampleDistance = interval.x + (f32(depthIndex) + rayJitter) * depthStepLength;
598
+ let samplePosition = view.cameraPos + ray * sampleDistance;
599
+ let localDensity = cloud_density(samplePosition);
600
+ let localTransmittance = exp(-max(localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * depthStepLength, 0.0));
601
+ let weight = depthTransmittance * (1.0 - localTransmittance);
602
+ weightedDistance = weightedDistance + sampleDistance * weight;
603
+ opticalWeight = opticalWeight + weight;
604
+ depthTransmittance = depthTransmittance * localTransmittance;
605
+ }
606
+ let hasCloud = opticalWeight > 0.00001;
607
+ let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
608
+ let representativeWorld = view.cameraPos + ray * representativeDistance;
609
+ return vec4<f32>(representativeWorld, select(0.0, 1.0, hasCloud));
610
+ }
611
+
612
+ let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
613
+ if (interval.y <= interval.x) { return scene; }
614
+ let steps = min(cloud.integration.y, 64u);
615
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
616
+ var transmittance = 1.0;
617
+ var scattering = vec3<f32>(0.0);
618
+ var weightedDistance = 0.0;
619
+ var opticalWeight = 0.0;
620
+ let sunDirection = normalize(-view.lightDir);
621
+ for (var index = 0u; index < 64u; index = index + 1u) {
622
+ if (index >= steps) { break; }
623
+ let distance = interval.x + (f32(index) + rayJitter) * stepLength;
624
+ let samplePosition = view.cameraPos + ray * distance;
625
+ let localDensity = cloud_density(samplePosition);
626
+ let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
627
+ let localTransmittance = exp(-max(extinction, 0.0));
628
+ let depthWeight = transmittance * (1.0 - localTransmittance);
629
+ weightedDistance = weightedDistance + distance * depthWeight;
630
+ opticalWeight = opticalWeight + depthWeight;
631
+ // Empty samples are common in the separated-cloud field. Avoid a nested
632
+ // solar march for them and stop once the camera path is effectively opaque.
633
+ if (localDensity > 0.0001 && transmittance > 0.01) {
634
+ let solarTransmittance = cloud_solar_transmittance(samplePosition);
635
+ let cloudHeight = clamp(
636
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
637
+ );
638
+ scattering = scattering + cloud_incident_light(
639
+ solarTransmittance, dot(ray, sunDirection), cloudHeight,
640
+ ) * depthWeight;
641
+ }
642
+ transmittance = transmittance * localTransmittance;
643
+ if (transmittance < 0.01) { break; }
644
+ }
645
+ let hasCloud = opticalWeight > 0.00001;
646
+ let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
647
+ let representativeWorld = view.cameraPos + ray * representativeDistance;
648
+ // Wind is an advective coordinate change. Reproject the cloud point into
649
+ // the previous frame's world before applying the previous camera matrix;
650
+ // authored field values remain immutable.
651
+ let windDelta = cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w);
652
+ // cloud_density evaluates the authored field at position + wind*time.
653
+ // Keeping the same field sample in the previous frame therefore advances
654
+ // the previous world point by the elapsed wind delta as well.
655
+ let previousCloudWorld = representativeWorld + windDelta;
656
+ let previousCloudClip = view.temporalPreviousViewProj * vec4<f32>(previousCloudWorld, 1.0);
657
+ let previousCloudNdc = previousCloudClip.xyz / max(abs(previousCloudClip.w), 0.00001);
658
+ let previousCloudUv = vec2<f32>(
659
+ previousCloudNdc.x * 0.5 + 0.5,
660
+ 1.0 - (previousCloudNdc.y * 0.5 + 0.5),
661
+ );
662
+ let cloudInBounds = all(previousCloudUv >= vec2<f32>(0.0)) && all(previousCloudUv <= vec2<f32>(1.0));
663
+ let cloudHistoryUv = clamp(previousCloudUv, vec2<f32>(0.0), vec2<f32>(1.0));
664
+ let cloudHistoryDepthSample = textureSampleLevel(previousDepth, depthSampler, cloudHistoryUv, 0.0);
665
+ let cloudHistoryWorld = cloudHistoryDepthSample.xyz;
666
+ let previous = textureSampleLevel(previousRadiance, sceneSampler, cloudHistoryUv, 0.0);
667
+ let previousTransport = textureSampleLevel(
668
+ previousTransmittance,
669
+ sceneSampler,
670
+ cloudHistoryUv,
671
+ 0.0,
672
+ );
673
+ let worldTolerance = max(0.05, length(previousCloudWorld - view.cameraPos) * 0.02);
674
+ let depthAccepted = cloudHistoryDepthSample.a > 0.5 &&
675
+ distance(cloudHistoryWorld, previousCloudWorld) <= worldTolerance;
676
+ let accepted =
677
+ hasCloud && cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && cloudInBounds &&
678
+ depthAccepted && previousTransport.a > 0.0;
679
+ let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.95), accepted);
680
+ let cloudRadiance = mix(scattering, previous.rgb, historyWeight);
681
+ // Preserve scene-linear HDR output for the downstream material lane. The
682
+ // history owner receives cloudRadiance separately through the history pass.
683
+ return vec4<f32>(scene.rgb * transmittance + cloudRadiance, transmittance);
684
+ }
685
+
686
+ // The light-space shadow producer already paid for a bounded column integral.
687
+ // Reuse that texel for camera samples instead of starting a full solar march
688
+ // for every occupied camera step. Outside the occupied interval the cached
689
+ // full-column transmittance is exact for the projected column. Inside the
690
+ // interval, the cache supplies the stable projection/bounds and a short
691
+ // residual march resolves the non-uniform density that a linear optical-depth
692
+ // interpolation would lose. Invalid/out-of-range projections retain the
693
+ // analytic path as an explicit fallback.
694
+ fn cloud_solar_cached_transmittance(position: vec3<f32>) -> f32 {
695
+ let projection = cloud.shadowProjection;
696
+ if (projection.y < 0.5 || projection.x <= 0.0) {
697
+ return cloud_solar_transmittance(position);
698
+ }
699
+ let relative = position - cloud.shadowOrigin.xyz;
700
+ let uv = vec2<f32>(
701
+ dot(relative, cloud.shadowRight.xyz) / projection.x + 0.5,
702
+ dot(relative, cloud.shadowUp.xyz) / projection.x + 0.5,
703
+ );
704
+ if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
705
+ return cloud_solar_transmittance(position);
706
+ }
707
+ let cachedShadow = textureSampleLevel(previousRadiance, sceneSampler, uv, 0.0);
708
+ let cachedOpticalDepth = max(cachedShadow.a, 0.0);
709
+ let layerHeight = clamp(
710
+ (position.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
711
+ 0.0,
712
+ 1.0,
713
+ );
714
+ if (cachedOpticalDepth <= 0.0001) { return 1.0; }
715
+ let firstHeight = clamp(cachedShadow.g, 0.0, 1.0);
716
+ let lastHeight = clamp(cachedShadow.b, 0.0, 1.0);
717
+ let occupiedMin = min(firstHeight, lastHeight);
718
+ let occupiedMax = max(firstHeight, lastHeight);
719
+ let sunDirection = normalize(-view.lightDir);
720
+ // G/B are ordered along the light ray, not necessarily bottom-to-top. Only
721
+ // use the cached full/empty fast paths when the light has enough vertical
722
+ // component to make that ordering meaningful; grazing light stays on the
723
+ // residual path so a low-sun column cannot invert the cache bounds.
724
+ if (abs(sunDirection.y) > 0.2) {
725
+ if (sunDirection.y > 0.0) {
726
+ if (layerHeight <= occupiedMin) { return exp(-cachedOpticalDepth); }
727
+ if (layerHeight >= occupiedMax) { return 1.0; }
728
+ } else {
729
+ if (layerHeight >= occupiedMax) { return exp(-cachedOpticalDepth); }
730
+ if (layerHeight <= occupiedMin) { return 1.0; }
731
+ }
732
+ }
733
+
734
+ // A short residual march is only paid for samples inside the projected
735
+ // cloud span. Vertical light uses twelve taps, oblique light sixteen, and
736
+ // grazing light up to thirty-two because the same layer projects across a
737
+ // much longer solar segment. This removes the large error caused by
738
+ // assuming optical depth is uniform between the first and last occupied
739
+ // heights while keeping the normal camera path far below a second full
740
+ // solar integration.
741
+ let solarDistance = select(cloud.field.z, projection.x, projection.x > 0.0);
742
+ let interval = cloud_layer_interval(position, sunDirection, solarDistance);
743
+ if (interval.y <= interval.x) { return 1.0; }
744
+ let absLightY = abs(sunDirection.y);
745
+ let directionBudget = select(
746
+ 32u,
747
+ select(16u, 12u, absLightY > 0.5),
748
+ absLightY > 0.2,
749
+ );
750
+ let residualSteps = min(directionBudget, max(8u, cloud.integration.z * 3u));
751
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(residualSteps));
752
+ var opticalDepth = 0.0;
753
+ let solarJitter = cloud_solar_jitter(position);
754
+ for (var index = 0u; index < 32u; index = index + 1u) {
755
+ if (index >= residualSteps) { break; }
756
+ let samplePosition = position + sunDirection *
757
+ (interval.x + (f32(index) + solarJitter) * stepLength);
758
+ opticalDepth = opticalDepth + cloud_density(samplePosition) *
759
+ ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
760
+ }
761
+ return exp(-max(opticalDepth, 0.0));
762
+ }
763
+
764
+ struct CloudTransportOutput {
765
+ @location(0) radiance: vec4<f32>,
766
+ @location(1) transmittance: vec4<f32>,
767
+ @location(2) depth: vec4<f32>,
768
+ };
769
+
770
+ // One half-resolution transport pass writes all three reusable cloud fields.
771
+ // The full-resolution resolve below reconstructs colour and applies temporal
772
+ // history; no second full raymarch exists solely to obtain cloud depth.
773
+ @fragment
774
+ fn fs_transport(input: CloudViewOut) -> CloudTransportOutput {
775
+ let depth = textureSampleLevel(sceneDepth, depthSampler, input.uv, 0u);
776
+ let farWorld = reconstruct_world(input.uv, 1.0);
777
+ let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
778
+ let ray = normalize(farWorld - view.cameraPos);
779
+ let rayJitter = cloud_ray_jitter(input.position.xy);
780
+ let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
781
+ let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
782
+ var output: CloudTransportOutput;
783
+ output.radiance = vec4<f32>(0.0, 0.0, 0.0, 0.0);
784
+ output.transmittance = vec4<f32>(1.0, 1.0, 1.0, 1.0);
785
+ output.depth = vec4<f32>(sceneWorld, 0.0);
786
+ if (interval.y <= interval.x) { return output; }
787
+
788
+ let steps = min(cloud.integration.y, 64u);
789
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
790
+ var transmittance = 1.0;
791
+ var scattering = vec3<f32>(0.0);
792
+ var weightedDistance = 0.0;
793
+ var opticalWeight = 0.0;
794
+ let sunDirection = normalize(-view.lightDir);
795
+ for (var index = 0u; index < 64u; index = index + 1u) {
796
+ if (index >= steps) { break; }
797
+ let distance = interval.x + (f32(index) + rayJitter) * stepLength;
798
+ let samplePosition = view.cameraPos + ray * distance;
799
+ let localDensity = cloud_density(samplePosition);
800
+ let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
801
+ let localTransmittance = exp(-max(extinction, 0.0));
802
+ let depthWeight = transmittance * (1.0 - localTransmittance);
803
+ weightedDistance = weightedDistance + distance * depthWeight;
804
+ opticalWeight = opticalWeight + depthWeight;
805
+ if (localDensity > 0.0001 && transmittance > 0.01) {
806
+ let solarTransmittance = cloud_solar_cached_transmittance(samplePosition);
807
+ let cloudHeight = clamp(
808
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
809
+ );
810
+ scattering = scattering + cloud_incident_light(
811
+ solarTransmittance, dot(ray, sunDirection), cloudHeight,
812
+ ) * depthWeight;
813
+ }
814
+ transmittance = transmittance * localTransmittance;
815
+ if (transmittance < 0.01) { break; }
816
+ }
817
+ let hasCloud = opticalWeight > 0.00001;
818
+ let representativeDistance = select(sceneDistance, weightedDistance / max(opticalWeight, 0.00001), hasCloud);
819
+ output.radiance = vec4<f32>(scattering, select(0.0, 1.0, hasCloud));
820
+ output.transmittance = vec4<f32>(transmittance, transmittance, transmittance, 1.0);
821
+ output.depth = vec4<f32>(view.cameraPos + ray * representativeDistance, select(0.0, 1.0, hasCloud));
822
+ return output;
823
+ }
824
+ `;
825
+
826
+ /** Shared cloud transport module; `fs_transport` is selected by the MRT PSO. */
827
+ export const CLOUD_TRANSPORT_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL;
828
+ /** Analytic fallback for adapters without a projected cloud-shadow target. */
829
+ export const CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL.replace(
830
+ 'cloud_solar_cached_transmittance(samplePosition)',
831
+ 'cloud_solar_transmittance(samplePosition)',
832
+ );
833
+
834
+ /**
835
+ * History writer. It is a separate MRT pass so the cloud composite remains a
836
+ * normal scene-linear color producer while the renderer owns the ping-pong
837
+ * radiance/transmittance/depth attachments and advances them only on submit.
838
+ */
839
+ export const CLOUD_HISTORY_FULLSCREEN_WGSL = /* wgsl */ `
840
+ struct CloudCameraView {
841
+ worldViewProj: mat4x4<f32>,
842
+ lightDir: vec3<f32>,
843
+ lightColor: vec3<f32>,
844
+ cameraPos: vec3<f32>,
845
+ lightViewProj_A: mat4x4<f32>,
846
+ inverseViewProj: mat4x4<f32>,
847
+ lightViewProj_B: mat4x4<f32>,
848
+ lightViewProj_C: mat4x4<f32>,
849
+ lightViewProj_D: mat4x4<f32>,
850
+ splitPlanes: array<vec4<f32>, 4>,
851
+ cascadeCount: f32,
852
+ cascadeBlend: f32,
853
+ depthBias: f32,
854
+ normalBias: f32,
855
+ directionalShadowFilter: vec4<f32>,
856
+ spotLightViewProj: array<mat4x4<f32>, 4>,
857
+ temporalCurrentViewProj: mat4x4<f32>,
858
+ temporalPreviousViewProj: mat4x4<f32>,
859
+ temporalProjection: vec4<f32>,
860
+ temporalPreviousCameraPos: vec4<f32>,
861
+ ssrParams: vec4<f32>,
862
+ };
863
+
864
+ struct CloudHistoryParams {
865
+ temporal: vec4<f32>, // valid, reset, historyWeight, reserved
866
+ windTime: vec4<f32>, // wind.xyz, elapsed seconds since previous submit
867
+ };
868
+
869
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
870
+ @group(1) @binding(0) var currentRadiance: texture_2d<f32>;
871
+ @group(1) @binding(1) var sceneSampler: sampler;
872
+ @group(1) @binding(2) var<uniform> params: CloudHistoryParams;
873
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
874
+ @group(1) @binding(4) var depthSampler: sampler;
875
+ @group(1) @binding(5) var currentCloudDepth: texture_2d<f32>;
876
+ @group(1) @binding(6) var sceneBackground: texture_2d<f32>;
877
+
878
+ struct CloudHistoryInput {
879
+ @builtin(position) position: vec4<f32>,
880
+ @location(0) uv: vec2<f32>,
881
+ };
882
+
883
+ struct CloudHistoryOutput {
884
+ @location(0) radiance: vec4<f32>,
885
+ @location(1) transmittance: vec4<f32>,
886
+ };
887
+
888
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
889
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
890
+ let world = view.inverseViewProj * ndc;
891
+ return world.xyz / max(abs(world.w), 0.00001);
892
+ }
893
+
894
+ @vertex
895
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudHistoryInput {
896
+ var positions = array<vec2<f32>, 3>(
897
+ vec2<f32>(-1.0, -1.0),
898
+ vec2<f32>(3.0, -1.0),
899
+ vec2<f32>(-1.0, 3.0),
900
+ );
901
+ var output: CloudHistoryInput;
902
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
903
+ output.uv = vec2<f32>(
904
+ positions[index].x * 0.5 + 0.5,
905
+ 1.0 - (positions[index].y * 0.5 + 0.5),
906
+ );
907
+ return output;
908
+ }
909
+
910
+ @fragment
911
+ fn fs_main(input: CloudHistoryInput) -> CloudHistoryOutput {
912
+ let current = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
913
+ let background = textureSampleLevel(sceneBackground, sceneSampler, input.uv, 0.0);
914
+ let cloudDepth = textureSampleLevel(currentCloudDepth, depthSampler, input.uv, 0.0);
915
+ let currentValid = cloudDepth.a > 0.5;
916
+ // currentRadiance is the scene-linear cloud composite. Remove the
917
+ // unchanged scene background before writing history so the persistent owner
918
+ // stores raw cloud radiance only. The view pass is the sole temporal blend
919
+ // owner; blending here as well would accumulate history twice per frame.
920
+ let currentCloudRadiance = max(current.rgb - background.rgb * current.a, vec3<f32>(0.0));
921
+ let currentTransmittance = vec4<f32>(current.a, current.a, current.a, 1.0);
922
+ var output: CloudHistoryOutput;
923
+ output.radiance = vec4<f32>(currentCloudRadiance, select(0.0, 1.0, currentValid));
924
+ output.transmittance = currentTransmittance;
925
+ return output;
926
+ }
927
+ `;
928
+
929
+ /** Full-resolution resolve for the half-resolution transport MRT. */
930
+ export const CLOUD_RESOLVE_FULLSCREEN_WGSL = /* wgsl */ `
931
+ struct CloudCameraView {
932
+ worldViewProj: mat4x4<f32>,
933
+ lightDir: vec3<f32>,
934
+ lightColor: vec3<f32>,
935
+ cameraPos: vec3<f32>,
936
+ lightViewProj_A: mat4x4<f32>,
937
+ inverseViewProj: mat4x4<f32>,
938
+ lightViewProj_B: mat4x4<f32>,
939
+ lightViewProj_C: mat4x4<f32>,
940
+ lightViewProj_D: mat4x4<f32>,
941
+ splitPlanes: array<vec4<f32>, 4>,
942
+ cascadeCount: f32,
943
+ cascadeBlend: f32,
944
+ depthBias: f32,
945
+ normalBias: f32,
946
+ directionalShadowFilter: vec4<f32>,
947
+ spotLightViewProj: array<mat4x4<f32>, 4>,
948
+ temporalCurrentViewProj: mat4x4<f32>,
949
+ temporalPreviousViewProj: mat4x4<f32>,
950
+ temporalProjection: vec4<f32>,
951
+ temporalPreviousCameraPos: vec4<f32>,
952
+ ssrParams: vec4<f32>,
953
+ };
954
+
955
+ struct CloudResolveParams {
956
+ layer: vec4<f32>,
957
+ field: vec4<f32>,
958
+ wind: vec4<f32>,
959
+ sunDirection: vec4<f32>,
960
+ sunRadiance: vec4<f32>,
961
+ integration: vec4<u32>,
962
+ shadowOrigin: vec4<f32>,
963
+ shadowRight: vec4<f32>,
964
+ shadowUp: vec4<f32>,
965
+ shadowProjection: vec4<f32>,
966
+ temporal: vec4<f32>,
967
+ };
968
+
969
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
970
+ @group(1) @binding(0) var sceneColor: texture_2d<f32>;
971
+ @group(1) @binding(1) var sceneSampler: sampler;
972
+ @group(1) @binding(2) var<uniform> cloud: CloudResolveParams;
973
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
974
+ @group(1) @binding(4) var depthSampler: sampler;
975
+ @group(1) @binding(5) var currentRadiance: texture_2d<f32>;
976
+ @group(1) @binding(6) var currentTransmittance: texture_2d<f32>;
977
+ @group(1) @binding(7) var currentDepth: texture_2d<f32>;
978
+ @group(1) @binding(8) var previousRadiance: texture_2d<f32>;
979
+ @group(1) @binding(9) var previousTransmittance: texture_2d<f32>;
980
+ @group(1) @binding(10) var previousDepth: texture_2d<f32>;
981
+
982
+ struct CloudResolveInput {
983
+ @builtin(position) position: vec4<f32>,
984
+ @location(0) uv: vec2<f32>,
985
+ };
986
+
987
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
988
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
989
+ let world = view.inverseViewProj * ndc;
990
+ return world.xyz / max(abs(world.w), 0.00001);
991
+ }
992
+
993
+ @vertex
994
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudResolveInput {
995
+ var positions = array<vec2<f32>, 3>(
996
+ vec2<f32>(-1.0, -1.0),
997
+ vec2<f32>(3.0, -1.0),
998
+ vec2<f32>(-1.0, 3.0),
999
+ );
1000
+ var output: CloudResolveInput;
1001
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
1002
+ output.uv = vec2<f32>(
1003
+ positions[index].x * 0.5 + 0.5,
1004
+ 1.0 - (positions[index].y * 0.5 + 0.5),
1005
+ );
1006
+ return output;
1007
+ }
1008
+
1009
+ @fragment
1010
+ fn fs_main(input: CloudResolveInput) -> @location(0) vec4<f32> {
1011
+ let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
1012
+ // The resolve owns the full-resolution foreground test. Reject the current
1013
+ // low-resolution transport before any history or radiance is composited;
1014
+ // gating history alone still lets a newly sampled cloud bleed over geometry.
1015
+ let sceneDepthValue = textureLoad(sceneDepth, vec2<i32>(input.position.xy), 0);
1016
+ let sceneWorld = reconstruct_world(input.uv, clamp(sceneDepthValue, 0.0, 1.0));
1017
+ let sceneDistance = length(sceneWorld - view.cameraPos);
1018
+ let transport = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
1019
+ let transportT = textureSampleLevel(currentTransmittance, sceneSampler, input.uv, 0.0);
1020
+ // The transport depth is an rgba16float color attachment, not the scene's
1021
+ // depth texture. Sample it with the filtering color sampler; binding the
1022
+ // non-filtering scene-depth sampler here makes the shader contract depend on
1023
+ // an incompatible sampler/texture pair on strict WebGPU implementations.
1024
+ let cloudDepth = textureSampleLevel(currentDepth, sceneSampler, input.uv, 0.0);
1025
+ let currentValid = transport.a > 0.5 && cloudDepth.a > 0.5;
1026
+ let cloudDistance = length(cloudDepth.xyz - view.cameraPos);
1027
+ let foregroundAccepted = sceneDepthValue >= 0.999999 ||
1028
+ cloudDistance <= sceneDistance + max(0.05, sceneDistance * 0.01);
1029
+ if (!currentValid || !foregroundAccepted) { return scene; }
1030
+
1031
+ let previousClip = view.temporalPreviousViewProj * vec4<f32>(
1032
+ cloudDepth.xyz + cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w),
1033
+ 1.0,
1034
+ );
1035
+ let previousNdc = previousClip.xyz / max(abs(previousClip.w), 0.00001);
1036
+ let previousUv = vec2<f32>(
1037
+ previousNdc.x * 0.5 + 0.5,
1038
+ 1.0 - (previousNdc.y * 0.5 + 0.5),
1039
+ );
1040
+ let inBounds = all(previousUv >= vec2<f32>(0.0)) && all(previousUv <= vec2<f32>(1.0));
1041
+ let historyUv = clamp(previousUv, vec2<f32>(0.0), vec2<f32>(1.0));
1042
+ let historyDepth = textureSampleLevel(previousDepth, sceneSampler, historyUv, 0.0);
1043
+ let previous = textureSampleLevel(previousRadiance, sceneSampler, historyUv, 0.0);
1044
+ let previousTransport = textureSampleLevel(previousTransmittance, sceneSampler, historyUv, 0.0);
1045
+ let dims = vec2<f32>(textureDimensions(previousRadiance));
1046
+ let texel = 1.0 / max(dims, vec2<f32>(1.0));
1047
+ // Clamp reprojected history against the current low-resolution transport
1048
+ // neighborhood. Using only previous-frame neighbors (and including the
1049
+ // previous center in both extrema) is a no-op and lets stale bright cloud
1050
+ // pixels leak across a newly exposed edge.
1051
+ let c0 = transport.rgb;
1052
+ let c1 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
1053
+ let c2 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
1054
+ let c3 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
1055
+ let c4 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
1056
+ let minRadiance = min(c0, min(min(c1, c2), min(c3, c4)));
1057
+ let maxRadiance = max(c0, max(max(c1, c2), max(c3, c4)));
1058
+ let t1 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
1059
+ let t2 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
1060
+ let t3 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
1061
+ let t4 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
1062
+ let minTransport = min(transportT.r, min(min(t1, t2), min(t3, t4)));
1063
+ let maxTransport = max(transportT.r, max(max(t1, t2), max(t3, t4)));
1064
+ let worldTolerance = max(0.05, distance(cloudDepth.xyz, view.cameraPos) * 0.02);
1065
+ let depthAccepted = historyDepth.a > 0.5 && distance(historyDepth.xyz, cloudDepth.xyz) <= worldTolerance;
1066
+ let accepted = cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && inBounds && depthAccepted;
1067
+ let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.92), accepted);
1068
+ let stableHistory = clamp(previous.rgb, minRadiance, maxRadiance);
1069
+ let stableTransport = clamp(previousTransport.r, minTransport, maxTransport);
1070
+ let radiance = mix(transport.rgb, stableHistory, historyWeight);
1071
+ let transmittance = mix(transportT.r, stableTransport, historyWeight);
1072
+ return vec4<f32>(scene.rgb * transmittance + radiance, transmittance);
1073
+ }
1074
+ `;
1075
+
1076
+ export interface CloudLayerFeatureFrame {
1077
+ readonly params: ValidatedCloudLayer | undefined;
1078
+ readonly cache: CloudDensityCache | undefined;
1079
+ readonly sourceKey: string | undefined;
1080
+ readonly timeSeconds: number;
1081
+ readonly sunDirection: readonly [number, number, number] | undefined;
1082
+ readonly sunRadiance: readonly [number, number, number] | undefined;
1083
+ readonly shadowProjection: CloudShadowProjection | undefined;
1084
+ readonly temporal: CloudTemporalFrame | undefined;
1085
+ readonly generation: number;
1086
+ readonly inspection: CloudLayerInspection;
1087
+ }
1088
+
1089
+ export interface CloudLayerFeatureOptions {
1090
+ /** Disable the renderer-owned CloudLayer projection while retaining the component. */
1091
+ readonly enabled?: boolean;
1092
+ }
1093
+
1094
+ function makeCloudViewParams(
1095
+ frame: CloudLayerFeatureFrame,
1096
+ cache: CloudDensityCache,
1097
+ mode: 0 | 1 | 2 = 0,
1098
+ ): Uint8Array {
1099
+ const params = new ArrayBuffer(CLOUD_VIEW_PARAMS_BYTES);
1100
+ const floats = new Float32Array(params);
1101
+ const integers = new Uint32Array(params);
1102
+ const profile = CLOUD_QUALITY_PROFILES[frame.params?.quality ?? 'medium'];
1103
+ const authored = frame.params;
1104
+ floats[0] = authored?.baseHeight ?? 0;
1105
+ floats[1] = authored?.thickness ?? 1;
1106
+ floats[2] = authored?.scale ?? 1;
1107
+ floats[3] = authored?.density ?? 0;
1108
+ floats[4] = authored?.coverage ?? 1;
1109
+ floats[5] = frame.timeSeconds;
1110
+ floats[6] =
1111
+ mode === 1
1112
+ ? (authored?.shadowRange ?? 0)
1113
+ : cloudViewDistanceForQuality(authored?.quality ?? 'medium');
1114
+ floats[7] = authored?.seed ?? 0;
1115
+ floats[8] = authored?.wind[0] ?? 0;
1116
+ floats[9] = authored?.wind[1] ?? 0;
1117
+ floats[10] = authored?.wind[2] ?? 0;
1118
+ floats[12] = frame.sunDirection?.[0] ?? 0;
1119
+ floats[13] = frame.sunDirection?.[1] ?? -1;
1120
+ floats[14] = frame.sunDirection?.[2] ?? 0;
1121
+ floats[15] = 0.2;
1122
+ floats[16] = frame.sunRadiance?.[0] ?? 0;
1123
+ floats[17] = frame.sunRadiance?.[1] ?? 0;
1124
+ floats[18] = frame.sunRadiance?.[2] ?? 0;
1125
+ integers[20] = cache.resolution;
1126
+ integers[21] = Math.max(1, Math.min(64, Math.floor(profile.viewSteps)));
1127
+ integers[22] = Math.max(1, Math.min(64, Math.floor(profile.shadowSteps)));
1128
+ integers[23] = mode;
1129
+ const projection = frame.shadowProjection;
1130
+ if (projection !== undefined) {
1131
+ floats[24] = projection.origin[0];
1132
+ floats[25] = projection.origin[1];
1133
+ floats[26] = projection.origin[2];
1134
+ floats[28] = projection.right[0];
1135
+ floats[29] = projection.right[1];
1136
+ floats[30] = projection.right[2];
1137
+ floats[32] = projection.up[0];
1138
+ floats[33] = projection.up[1];
1139
+ floats[34] = projection.up[2];
1140
+ floats[36] = projection.range;
1141
+ floats[37] = projection.lowSun ? 0 : 1;
1142
+ floats[38] = projection.lowSun ? 1 : 0;
1143
+ floats[39] = projection.texelSize;
1144
+ }
1145
+ const temporal = frame.temporal;
1146
+ floats[40] = temporal !== undefined && temporal.reset === false ? 1 : 0;
1147
+ floats[41] = temporal?.reset === true ? 1 : 0;
1148
+ floats[42] = temporal === undefined ? 0 : profile.historyWeight;
1149
+ // The history entry is the last successfully submitted signature. Carry its
1150
+ // timestamp so the shader can advect the representative cloud point by the
1151
+ // exact elapsed time during reprojection.
1152
+ floats[43] = temporal?.history?.temporal.timeSeconds ?? frame.timeSeconds;
1153
+ return new Uint8Array(params);
1154
+ }
1155
+
1156
+ function planCloudDensity(
1157
+ frame: CloudLayerFeatureFrame,
1158
+ packedCache: Uint32Array,
1159
+ context: RenderFeaturePlanContext,
1160
+ ): RenderFeaturePlan {
1161
+ const cacheCount = packedCache.length;
1162
+ const params = new Uint32Array([cacheCount]);
1163
+ const inputTarget = context.targets.find((target) => target.name === 'motion-input');
1164
+ const outputTarget = context.targets.find((target) => target.name === 'motion-output');
1165
+ const historyRadiancePrevious = context.targets.find(
1166
+ (target) => target.name === 'cloud-history-radiance-previous',
1167
+ );
1168
+ const historyTransmittancePrevious = context.targets.find(
1169
+ (target) => target.name === 'cloud-history-transmittance-previous',
1170
+ );
1171
+ const historyDepthPrevious = context.targets.find(
1172
+ (target) => target.name === 'cloud-history-depth-previous',
1173
+ );
1174
+ const historyRadianceCurrent = context.targets.find(
1175
+ (target) => target.name === 'cloud-history-radiance-current',
1176
+ );
1177
+ const historyTransmittanceCurrent = context.targets.find(
1178
+ (target) => target.name === 'cloud-history-transmittance-current',
1179
+ );
1180
+ const historyDepthCurrent = context.targets.find(
1181
+ (target) => target.name === 'cloud-history-depth-current',
1182
+ );
1183
+ const hasHistoryTargets =
1184
+ historyRadiancePrevious !== undefined &&
1185
+ historyTransmittancePrevious !== undefined &&
1186
+ historyDepthPrevious !== undefined &&
1187
+ historyRadianceCurrent !== undefined &&
1188
+ historyTransmittanceCurrent !== undefined &&
1189
+ historyDepthCurrent !== undefined;
1190
+ const shadowTarget = context.targets.find((target) => target.name === 'cloud-shadow');
1191
+ const canShadow =
1192
+ shadowTarget !== undefined &&
1193
+ shadowTarget.sampleCount === 1 &&
1194
+ frame.shadowProjection !== undefined &&
1195
+ hasHistoryTargets;
1196
+ const canCompose =
1197
+ inputTarget !== undefined &&
1198
+ outputTarget !== undefined &&
1199
+ inputTarget.sampleCount === 1 &&
1200
+ outputTarget.sampleCount === 1 &&
1201
+ hasHistoryTargets;
1202
+ const transportReads = ['motion-input', { key: 'depth', sampleType: 'depth' as const }] as const;
1203
+ const transportCacheReads = [
1204
+ 'motion-input',
1205
+ 'cloud-shadow',
1206
+ { key: 'depth', sampleType: 'depth' as const },
1207
+ ] as const;
1208
+ const historyReads = [
1209
+ 'motion-input',
1210
+ 'cloud-history-radiance-previous',
1211
+ 'cloud-history-transmittance-previous',
1212
+ 'cloud-history-depth-previous',
1213
+ { key: 'depth', sampleType: 'depth' as const },
1214
+ ] as const;
1215
+ const historyAdditionalTextures = [
1216
+ 'cloud-history-radiance-previous',
1217
+ 'cloud-history-transmittance-previous',
1218
+ 'cloud-history-depth-previous',
1219
+ ] as const;
1220
+ const viewParams = makeCloudViewParams(frame, frame.cache as CloudDensityCache);
1221
+ const shadowParams = canShadow
1222
+ ? makeCloudViewParams(frame, frame.cache as CloudDensityCache, 1)
1223
+ : undefined;
1224
+ const transportUsesShadowCache = canCompose && canShadow;
1225
+ return {
1226
+ resources: [
1227
+ {
1228
+ kind: 'compute-program',
1229
+ name: CLOUD_LAYER_DENSITY_PROGRAM,
1230
+ program: {
1231
+ wgsl: CLOUD_DENSITY_COMPUTE_WGSL,
1232
+ entryPoints: ['cloud_density_cache'],
1233
+ bindings: [
1234
+ {
1235
+ label: 'forgeax.cloud-layer.cache',
1236
+ entries: [
1237
+ { binding: 0, visibility: COMPUTE_STAGE, buffer: { type: 'read-only-storage' } },
1238
+ { binding: 1, visibility: COMPUTE_STAGE, buffer: { type: 'storage' } },
1239
+ { binding: 2, visibility: COMPUTE_STAGE, buffer: { type: 'uniform' } },
1240
+ ],
1241
+ },
1242
+ ],
1243
+ },
1244
+ },
1245
+ {
1246
+ kind: 'buffer',
1247
+ name: CLOUD_LAYER_DENSITY_CACHE,
1248
+ size: packedCache.byteLength,
1249
+ usage: ['storage', 'copy-src'],
1250
+ data: packedCache,
1251
+ },
1252
+ {
1253
+ kind: 'buffer',
1254
+ name: CLOUD_LAYER_DENSITY_OUTPUT,
1255
+ size: packedCache.byteLength,
1256
+ usage: ['storage', 'copy-src'],
1257
+ },
1258
+ {
1259
+ kind: 'buffer',
1260
+ name: 'cloud-layer-density-params',
1261
+ size: params.byteLength,
1262
+ usage: ['uniform'],
1263
+ data: params,
1264
+ },
1265
+ {
1266
+ kind: 'compute-bindings',
1267
+ name: CLOUD_LAYER_DENSITY_BINDINGS,
1268
+ program: CLOUD_LAYER_DENSITY_PROGRAM,
1269
+ entries: [
1270
+ { binding: 0, resource: CLOUD_LAYER_DENSITY_CACHE },
1271
+ { binding: 1, resource: CLOUD_LAYER_DENSITY_OUTPUT },
1272
+ { binding: 2, resource: 'cloud-layer-density-params' },
1273
+ ],
1274
+ },
1275
+ {
1276
+ kind: 'fullscreen-program',
1277
+ name: 'cloud-layer-view',
1278
+ source: CLOUD_VIEW_FULLSCREEN_WGSL,
1279
+ usesView: true,
1280
+ storageBindings: [8],
1281
+ reads: historyReads,
1282
+ params: {
1283
+ byteSize: viewParams.byteLength,
1284
+ defaultValue: viewParams,
1285
+ },
1286
+ },
1287
+ ...(shadowParams === undefined
1288
+ ? []
1289
+ : [
1290
+ {
1291
+ kind: 'fullscreen-program' as const,
1292
+ name: 'cloud-layer-shadow',
1293
+ source: CLOUD_VIEW_FULLSCREEN_WGSL,
1294
+ usesView: true,
1295
+ storageBindings: [8],
1296
+ reads: historyReads,
1297
+ params: {
1298
+ byteSize: shadowParams.byteLength,
1299
+ defaultValue: shadowParams,
1300
+ },
1301
+ },
1302
+ ]),
1303
+ ...(canCompose
1304
+ ? [
1305
+ {
1306
+ kind: 'fullscreen-program' as const,
1307
+ name: 'cloud-layer-transport',
1308
+ source: transportUsesShadowCache
1309
+ ? CLOUD_TRANSPORT_FULLSCREEN_WGSL
1310
+ : CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL,
1311
+ fragmentEntryPoint: 'fs_transport',
1312
+ usesView: true,
1313
+ storageBindings: [8],
1314
+ reads: transportUsesShadowCache ? transportCacheReads : transportReads,
1315
+ params: {
1316
+ byteSize: viewParams.byteLength,
1317
+ defaultValue: viewParams,
1318
+ },
1319
+ },
1320
+ {
1321
+ kind: 'fullscreen-program' as const,
1322
+ name: 'cloud-layer-resolve',
1323
+ source: CLOUD_RESOLVE_FULLSCREEN_WGSL,
1324
+ usesView: true,
1325
+ reads: [
1326
+ 'motion-input',
1327
+ 'cloud-history-radiance-current',
1328
+ 'cloud-history-transmittance-current',
1329
+ 'cloud-history-depth-current',
1330
+ 'cloud-history-radiance-previous',
1331
+ 'cloud-history-transmittance-previous',
1332
+ 'cloud-history-depth-previous',
1333
+ { key: 'depth', sampleType: 'depth' as const },
1334
+ ],
1335
+ params: {
1336
+ byteSize: viewParams.byteLength,
1337
+ defaultValue: viewParams,
1338
+ },
1339
+ },
1340
+ ]
1341
+ : []),
1342
+ ...(canCompose
1343
+ ? [
1344
+ {
1345
+ kind: 'graphics-program' as const,
1346
+ name: 'cloud-layer-transport-pipeline',
1347
+ program: {
1348
+ shader: 'cloud-layer-transport',
1349
+ vertexLayout: 'none',
1350
+ colorFormats: [
1351
+ historyRadianceCurrent.format,
1352
+ historyTransmittanceCurrent.format,
1353
+ historyDepthCurrent.format,
1354
+ ],
1355
+ sampleCount: 1 as const,
1356
+ renderState: {
1357
+ depthWriteEnabled: false,
1358
+ depthCompare: 'always' as const,
1359
+ },
1360
+ },
1361
+ },
1362
+ {
1363
+ kind: 'graphics-bindings' as const,
1364
+ name: 'cloud-layer-transport-view-bindings',
1365
+ program: 'cloud-layer-transport-pipeline',
1366
+ values: { group: 0, view: true },
1367
+ },
1368
+ {
1369
+ kind: 'graphics-bindings' as const,
1370
+ name: 'cloud-layer-transport-bindings',
1371
+ program: 'cloud-layer-transport-pipeline',
1372
+ values: {
1373
+ group: 1,
1374
+ fullscreen: true,
1375
+ shader: 'cloud-layer-transport',
1376
+ input: 'motion-input',
1377
+ depth: 'depth',
1378
+ additionalTextures: transportUsesShadowCache ? ['cloud-shadow'] : [],
1379
+ storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT],
1380
+ },
1381
+ logicalTargets: { input: 'motion-input' },
1382
+ },
1383
+ {
1384
+ kind: 'graphics-program' as const,
1385
+ name: 'cloud-layer-resolve-pipeline',
1386
+ program: {
1387
+ shader: 'cloud-layer-resolve',
1388
+ vertexLayout: 'none',
1389
+ colorFormats: [outputTarget.format],
1390
+ sampleCount: 1 as const,
1391
+ renderState: {
1392
+ depthWriteEnabled: false,
1393
+ depthCompare: 'always' as const,
1394
+ },
1395
+ },
1396
+ },
1397
+ {
1398
+ kind: 'graphics-bindings' as const,
1399
+ name: 'cloud-layer-resolve-view-bindings',
1400
+ program: 'cloud-layer-resolve-pipeline',
1401
+ values: { group: 0, view: true },
1402
+ },
1403
+ {
1404
+ kind: 'graphics-bindings' as const,
1405
+ name: 'cloud-layer-resolve-bindings',
1406
+ program: 'cloud-layer-resolve-pipeline',
1407
+ values: {
1408
+ group: 1,
1409
+ fullscreen: true,
1410
+ shader: 'cloud-layer-resolve',
1411
+ input: 'motion-input',
1412
+ depth: 'depth',
1413
+ additionalTextures: [
1414
+ 'cloud-history-radiance-current',
1415
+ 'cloud-history-transmittance-current',
1416
+ 'cloud-history-depth-current',
1417
+ 'cloud-history-radiance-previous',
1418
+ 'cloud-history-transmittance-previous',
1419
+ 'cloud-history-depth-previous',
1420
+ ],
1421
+ },
1422
+ },
1423
+ ]
1424
+ : []),
1425
+ ...(canShadow
1426
+ ? [
1427
+ {
1428
+ kind: 'graphics-program' as const,
1429
+ name: 'cloud-layer-shadow-pipeline',
1430
+ program: {
1431
+ shader: 'cloud-layer-shadow',
1432
+ vertexLayout: 'none',
1433
+ colorFormats: [shadowTarget.format],
1434
+ sampleCount: 1 as const,
1435
+ renderState: {
1436
+ depthWriteEnabled: false,
1437
+ depthCompare: 'always' as const,
1438
+ blend: {
1439
+ color: {
1440
+ srcFactor: 'one' as const,
1441
+ dstFactor: 'zero' as const,
1442
+ operation: 'add' as const,
1443
+ },
1444
+ alpha: {
1445
+ srcFactor: 'one' as const,
1446
+ dstFactor: 'zero' as const,
1447
+ operation: 'add' as const,
1448
+ },
1449
+ },
1450
+ },
1451
+ },
1452
+ },
1453
+ {
1454
+ kind: 'graphics-bindings' as const,
1455
+ name: 'cloud-layer-shadow-view-bindings',
1456
+ program: 'cloud-layer-shadow-pipeline',
1457
+ values: { group: 0, view: true },
1458
+ },
1459
+ {
1460
+ kind: 'graphics-bindings' as const,
1461
+ name: 'cloud-layer-shadow-bindings',
1462
+ program: 'cloud-layer-shadow-pipeline',
1463
+ values: {
1464
+ group: 1,
1465
+ fullscreen: true,
1466
+ shader: 'cloud-layer-shadow',
1467
+ input: false,
1468
+ depth: 'depth',
1469
+ // The shadow branch's WGSL keeps the shared depth binding
1470
+ // shape, but its light-space path never samples scene depth.
1471
+ // Bind the renderer-owned far-depth fallback instead of
1472
+ // introducing a read-before-main graph dependency.
1473
+ depthFallback: true,
1474
+ additionalTextures: historyAdditionalTextures,
1475
+ storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT],
1476
+ },
1477
+ },
1478
+ ]
1479
+ : []),
1480
+ ],
1481
+ passes:
1482
+ context.caps.compute && context.caps.storageBuffer
1483
+ ? [
1484
+ {
1485
+ kind: 'compute' as const,
1486
+ name: 'cloud-layer-density-cache',
1487
+ program: CLOUD_LAYER_DENSITY_PROGRAM,
1488
+ bindings: CLOUD_LAYER_DENSITY_BINDINGS,
1489
+ dispatches: [
1490
+ {
1491
+ kind: 'direct' as const,
1492
+ entryPoint: 'cloud_density_cache',
1493
+ workgroups: [Math.max(1, Math.ceil(cacheCount / CLOUD_WORKGROUP_SIZE))],
1494
+ },
1495
+ ],
1496
+ },
1497
+ ...(canShadow
1498
+ ? [
1499
+ {
1500
+ kind: 'raster' as const,
1501
+ name: 'cloud-layer-shadow',
1502
+ colorAttachments: [
1503
+ {
1504
+ target: 'cloud-shadow',
1505
+ loadOp: 'clear' as const,
1506
+ storeOp: 'store' as const,
1507
+ },
1508
+ ],
1509
+ sampledTargets: [
1510
+ 'cloud-history-radiance-previous',
1511
+ 'cloud-history-transmittance-previous',
1512
+ 'cloud-history-depth-previous',
1513
+ ],
1514
+ draws: [
1515
+ {
1516
+ program: 'cloud-layer-shadow-pipeline',
1517
+ bindings: [
1518
+ 'cloud-layer-shadow-view-bindings',
1519
+ 'cloud-layer-shadow-bindings',
1520
+ ],
1521
+ vertexData: [],
1522
+ vertexLayout: 'none' as const,
1523
+ draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
1524
+ },
1525
+ ],
1526
+ },
1527
+ ]
1528
+ : []),
1529
+ ...(canCompose
1530
+ ? [
1531
+ {
1532
+ kind: 'raster' as const,
1533
+ name: 'cloud-layer-transport',
1534
+ colorAttachments: [
1535
+ {
1536
+ target: 'cloud-history-radiance-current',
1537
+ loadOp: 'clear' as const,
1538
+ storeOp: 'store' as const,
1539
+ },
1540
+ {
1541
+ target: 'cloud-history-transmittance-current',
1542
+ loadOp: 'clear' as const,
1543
+ storeOp: 'store' as const,
1544
+ },
1545
+ {
1546
+ target: 'cloud-history-depth-current',
1547
+ loadOp: 'clear' as const,
1548
+ storeOp: 'store' as const,
1549
+ },
1550
+ ],
1551
+ sampledTargets: [
1552
+ 'motion-input',
1553
+ 'depth',
1554
+ ...(transportUsesShadowCache ? ['cloud-shadow'] : []),
1555
+ ],
1556
+ draws: [
1557
+ {
1558
+ program: 'cloud-layer-transport-pipeline',
1559
+ bindings: [
1560
+ 'cloud-layer-transport-view-bindings',
1561
+ 'cloud-layer-transport-bindings',
1562
+ ],
1563
+ vertexData: [],
1564
+ vertexLayout: 'none' as const,
1565
+ draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
1566
+ },
1567
+ ],
1568
+ },
1569
+ {
1570
+ kind: 'raster' as const,
1571
+ name: 'cloud-layer-resolve',
1572
+ colorAttachments: [
1573
+ {
1574
+ target: 'motion-output',
1575
+ loadOp: 'clear' as const,
1576
+ storeOp: 'store' as const,
1577
+ },
1578
+ ],
1579
+ sampledTargets: [
1580
+ 'motion-input',
1581
+ 'depth',
1582
+ 'cloud-history-radiance-current',
1583
+ 'cloud-history-transmittance-current',
1584
+ 'cloud-history-depth-current',
1585
+ 'cloud-history-radiance-previous',
1586
+ 'cloud-history-transmittance-previous',
1587
+ 'cloud-history-depth-previous',
1588
+ ],
1589
+ draws: [
1590
+ {
1591
+ program: 'cloud-layer-resolve-pipeline',
1592
+ bindings: [
1593
+ 'cloud-layer-resolve-view-bindings',
1594
+ 'cloud-layer-resolve-bindings',
1595
+ ],
1596
+ vertexData: [],
1597
+ vertexLayout: 'none' as const,
1598
+ draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
1599
+ },
1600
+ ],
1601
+ },
1602
+ ]
1603
+ : []),
1604
+ ]
1605
+ : [],
1606
+ };
1607
+ }
1608
+
1609
+ /**
1610
+ * Renderer-installed cloud producer. Authoring is read once from World at
1611
+ * extraction; resource preparation, graph ordering and retirement stay with
1612
+ * RenderFeatureHost.
1613
+ */
1614
+ export function createCloudLayerFeature(
1615
+ options: CloudLayerFeatureOptions = {},
1616
+ ): RenderFeature<CloudLayerFeatureFrame> {
1617
+ let cachedSourceKey: string | undefined;
1618
+ let cached: CloudDensityCache | undefined;
1619
+ let packedCache: Uint32Array | undefined;
1620
+ let generation = 0;
1621
+ const histories = new CloudHistoryStore();
1622
+ let temporalResetCount = 0;
1623
+ let lastTemporalReset: CloudTemporalFrame['reasons'][number] | undefined;
1624
+ return Object.freeze({
1625
+ identity: CLOUD_LAYER_FEATURE_IDENTITY,
1626
+ placement: 'scene' as const,
1627
+ requiredCapabilities: ['compute', 'storageBuffer', 'rgba16floatRenderable'] as const,
1628
+ // Cloud programs are generated by the feature and have no user-authored
1629
+ // manifest entry to await. Use the immediate module path so first-frame
1630
+ // feature admission does not publish a transient pending-shader failure;
1631
+ // pipeline creation remains the validation boundary for this WGSL.
1632
+ shaderModuleMode: 'immediate' as const,
1633
+ extract: (
1634
+ context: RenderFeatureExtractContext,
1635
+ ): Result<CloudLayerFeatureFrame, RenderError> => {
1636
+ if (options.enabled === false) {
1637
+ histories.reset();
1638
+ return ok({
1639
+ params: undefined,
1640
+ cache: undefined,
1641
+ sourceKey: undefined,
1642
+ timeSeconds: 0,
1643
+ sunDirection: undefined,
1644
+ sunRadiance: undefined,
1645
+ shadowProjection: undefined,
1646
+ temporal: undefined,
1647
+ generation,
1648
+ inspection: inspectCloudLayer({ authored: false }),
1649
+ });
1650
+ }
1651
+ const world = context.worlds[context.owner];
1652
+ if (world === undefined) {
1653
+ histories.reset();
1654
+ return ok({
1655
+ params: undefined,
1656
+ cache: undefined,
1657
+ sourceKey: undefined,
1658
+ timeSeconds: 0,
1659
+ sunDirection: undefined,
1660
+ sunRadiance: undefined,
1661
+ shadowProjection: undefined,
1662
+ temporal: undefined,
1663
+ generation,
1664
+ inspection: inspectCloudLayer({ authored: false }),
1665
+ });
1666
+ }
1667
+ // The normal RenderSystem extract owns World reads and selected-sun
1668
+ // resolution. Direct feature probes must provide the same frame facts;
1669
+ // this keeps the feature from growing a detached authoring callback or
1670
+ // a second per-feature ECS registry.
1671
+ const cloud = context.frame?.cloudLayer;
1672
+ const capability =
1673
+ context.caps === undefined ? undefined : cloudCapabilitiesFromRhi(context.caps);
1674
+ if (cloud === undefined) {
1675
+ histories.reset();
1676
+ return ok({
1677
+ params: undefined,
1678
+ cache: undefined,
1679
+ sourceKey: undefined,
1680
+ timeSeconds: 0,
1681
+ sunDirection: undefined,
1682
+ sunRadiance: undefined,
1683
+ shadowProjection: undefined,
1684
+ temporal: undefined,
1685
+ generation,
1686
+ inspection: inspectCloudLayer({
1687
+ authored: false,
1688
+ ...(capability === undefined ? {} : { capability }),
1689
+ }),
1690
+ });
1691
+ }
1692
+ const formationKey = cloudLayerFormationKey(cloud.params);
1693
+ if (cached === undefined || cachedSourceKey !== formationKey) {
1694
+ cached = buildCloudDensityCache(cloud.params);
1695
+ packedCache = packCloudDensityCache(cached);
1696
+ cachedSourceKey = formationKey;
1697
+ generation += 1;
1698
+ }
1699
+ const view = context.frame?.view;
1700
+ const shadowProjection =
1701
+ view === undefined || cloud.sunDirection === undefined
1702
+ ? undefined
1703
+ : createCloudShadowProjection({
1704
+ center: view.shadowAnchor ?? view.cameraPosition,
1705
+ sunDirection: cloud.sunDirection,
1706
+ range: cloud.params.shadowRange,
1707
+ resolution: cloudShadowResolutionForQuality(cloud.params.quality),
1708
+ });
1709
+ const temporalSignature: CloudTemporalSignature | undefined =
1710
+ view === undefined
1711
+ ? undefined
1712
+ : {
1713
+ sourceKey: cloud.sourceKey,
1714
+ viewId: view.identity,
1715
+ authoringGeneration: generation,
1716
+ cloudShadowRevision: shadowProjection?.revision ?? 0,
1717
+ cameraRevision: view.cameraRevision,
1718
+ deviceGeneration: view.deviceGeneration,
1719
+ width: view.width,
1720
+ height: view.height,
1721
+ timeSeconds: cloud.worldTimeSeconds,
1722
+ quality: cloud.params.quality,
1723
+ ...(view.sceneDepthVersion === undefined
1724
+ ? {}
1725
+ : { sceneDepthVersion: view.sceneDepthVersion }),
1726
+ };
1727
+ const temporal =
1728
+ temporalSignature === undefined
1729
+ ? undefined
1730
+ : (() => {
1731
+ const decision = histories.begin(temporalSignature, {
1732
+ ...(view?.cameraCut === undefined ? {} : { cameraCut: view.cameraCut }),
1733
+ ...(view?.recovery === undefined ? {} : { recovery: view.recovery }),
1734
+ });
1735
+ const history =
1736
+ decision.history ?? createCloudHistory(temporalSignature, generation, false);
1737
+ return Object.freeze({
1738
+ signature: temporalSignature,
1739
+ history,
1740
+ reset: decision.reset,
1741
+ reasons: decision.reasons,
1742
+ });
1743
+ })();
1744
+ const resourceFacts = inspectCloudLayerResources({
1745
+ generation,
1746
+ cache: cached,
1747
+ ...(shadowProjection === undefined ? {} : { shadow: shadowProjection }),
1748
+ ...(temporal?.history === undefined ? {} : { history: temporal.history }),
1749
+ });
1750
+ const temporalReset = temporal?.reasons.at(-1);
1751
+ if (temporal?.reset === true) temporalResetCount += 1;
1752
+ if (temporalReset !== undefined) lastTemporalReset = temporalReset;
1753
+ return ok({
1754
+ params: cloud.params,
1755
+ cache: cached,
1756
+ sourceKey: cloud.sourceKey,
1757
+ timeSeconds: cloud.worldTimeSeconds,
1758
+ sunDirection: cloud.sunDirection,
1759
+ sunRadiance: cloud.sunRadiance,
1760
+ shadowProjection,
1761
+ temporal,
1762
+ generation,
1763
+ inspection: inspectCloudLayer({
1764
+ authored: true,
1765
+ sourceKey: cloud.sourceKey,
1766
+ generation,
1767
+ resourceFacts,
1768
+ ...(shadowProjection === undefined ? {} : { shadowRevision: shadowProjection.revision }),
1769
+ ...(temporal === undefined
1770
+ ? {}
1771
+ : {
1772
+ temporalResets: temporalResetCount,
1773
+ ...(lastTemporalReset === undefined ? {} : { lastTemporalReset }),
1774
+ }),
1775
+ ...(capability === undefined ? {} : { capability }),
1776
+ budget: {
1777
+ quality: cloud.params.quality,
1778
+ reason:
1779
+ 'physical GPU timing and residency receipts are unavailable until a prepared adapter submits them',
1780
+ },
1781
+ }),
1782
+ });
1783
+ },
1784
+ onFrameSubmitted: (frame: CloudLayerFeatureFrame) => {
1785
+ if (frame.temporal?.history !== undefined) {
1786
+ // Advance the metadata timestamp only after submit. The next frame's
1787
+ // GPU reprojection then receives the elapsed time since this exact
1788
+ // submitted signature, while an aborted frame keeps the old entry.
1789
+ histories.commit(
1790
+ frame.temporal.signature,
1791
+ createCloudHistory(frame.temporal.signature, frame.generation, true),
1792
+ );
1793
+ }
1794
+ },
1795
+ onFrameAborted: (frame: CloudLayerFeatureFrame) => {
1796
+ // A reset candidate is committed only after queue submission. The store
1797
+ // still contains the previous entry while this frame is rejected.
1798
+ void frame;
1799
+ },
1800
+ plan: (
1801
+ frame: CloudLayerFeatureFrame,
1802
+ context: RenderFeaturePlanContext,
1803
+ ): Result<RenderFeaturePlan, RenderError> => {
1804
+ if (frame.cache === undefined || packedCache === undefined || frame.params === undefined) {
1805
+ return ok({ resources: [], passes: [] });
1806
+ }
1807
+ return ok(planCloudDensity(frame, packedCache, context));
1808
+ },
1809
+ });
1810
+ }