@driftengine/core 3.63.0 → 4.1.0

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 (428) hide show
  1. package/CHANGELOG.json +122 -0
  2. package/NOTICE +20 -0
  3. package/README.md +1 -1
  4. package/dist/core/loop.js +18 -2
  5. package/dist/core/storage.js +5 -0
  6. package/dist/geometry/meshBuilder.d.ts +1 -1
  7. package/dist/geometry/meshBuilder.js +6 -2
  8. package/dist/index.d.ts +69 -4
  9. package/dist/index.js +112 -2
  10. package/dist/input/input.js +6 -1
  11. package/dist/math/exact.d.ts +13 -3
  12. package/dist/math/exact.js +68 -3
  13. package/dist/math/frustum.d.ts +17 -2
  14. package/dist/math/frustum.js +24 -0
  15. package/dist/render/backend/api.d.ts +34 -1
  16. package/dist/render/backend/api.js +22 -1
  17. package/dist/render/backend/budget.d.ts +7 -6
  18. package/dist/render/backend/budget.js +7 -6
  19. package/dist/render/backend/createRenderer.d.ts +57 -0
  20. package/dist/render/backend/createRenderer.js +51 -2
  21. package/dist/render/backend/materialChanges.d.ts +41 -0
  22. package/dist/render/backend/materialChanges.js +38 -0
  23. package/dist/render/backend/select.js +8 -0
  24. package/dist/render/backend/webgl2/renderer.d.ts +80 -1
  25. package/dist/render/backend/webgl2/renderer.js +208 -10
  26. package/dist/render/backend/webgpu/buffers.d.ts +20 -1
  27. package/dist/render/backend/webgpu/buffers.js +116 -58
  28. package/dist/render/backend/webgpu/fieldCompose.d.ts +156 -0
  29. package/dist/render/backend/webgpu/fieldCompose.js +396 -0
  30. package/dist/render/backend/webgpu/giFieldPass.d.ts +66 -0
  31. package/dist/render/backend/webgpu/giFieldPass.js +137 -0
  32. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +728 -0
  33. package/dist/render/backend/webgpu/gpuDrivenPass.js +2473 -0
  34. package/dist/render/backend/webgpu/linePass.d.ts +4 -14
  35. package/dist/render/backend/webgpu/linePass.js +4 -33
  36. package/dist/render/backend/webgpu/probeBake.d.ts +148 -0
  37. package/dist/render/backend/webgpu/probeBake.js +493 -0
  38. package/dist/render/backend/webgpu/renderer.d.ts +379 -24
  39. package/dist/render/backend/webgpu/renderer.js +1369 -76
  40. package/dist/render/backend/webgpu/scatterPass.js +15 -1
  41. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +19 -9
  42. package/dist/render/backend/webgpu/surfaceTexturePass.js +58 -32
  43. package/dist/render/causticsRenderer.d.ts +3 -1
  44. package/dist/render/causticsRenderer.js +4 -1
  45. package/dist/render/depthConvention.d.ts +12 -0
  46. package/dist/render/depthConvention.js +14 -0
  47. package/dist/render/deviceFeatures.d.ts +32 -0
  48. package/dist/render/deviceFeatures.js +41 -0
  49. package/dist/render/fog.d.ts +79 -0
  50. package/dist/render/fog.js +138 -0
  51. package/dist/render/frame/alias.d.ts +30 -0
  52. package/dist/render/frame/alias.js +61 -0
  53. package/dist/render/frame/deps.d.ts +37 -0
  54. package/dist/render/frame/deps.js +122 -0
  55. package/dist/render/frame/flushGraph.d.ts +43 -0
  56. package/dist/render/frame/flushGraph.js +84 -0
  57. package/dist/render/frame/graphSchedule.d.ts +49 -0
  58. package/dist/render/frame/graphSchedule.js +120 -0
  59. package/dist/render/frame/index.d.ts +15 -0
  60. package/dist/render/frame/index.js +8 -0
  61. package/dist/render/frame/lifetime.d.ts +24 -0
  62. package/dist/render/frame/lifetime.js +36 -0
  63. package/dist/render/frame/passTimings.d.ts +42 -0
  64. package/dist/render/frame/passTimings.js +38 -0
  65. package/dist/render/frame/validate.d.ts +15 -0
  66. package/dist/render/frame/validate.js +22 -0
  67. package/dist/render/frame/virtual.d.ts +44 -0
  68. package/dist/render/frame/virtual.js +61 -0
  69. package/dist/render/gi/chain.d.ts +82 -0
  70. package/dist/render/gi/chain.js +135 -0
  71. package/dist/render/gi/denoise.d.ts +114 -0
  72. package/dist/render/gi/denoise.js +169 -0
  73. package/dist/render/gi/fieldScene.d.ts +125 -0
  74. package/dist/render/gi/fieldScene.js +219 -0
  75. package/dist/render/gi/globalField.d.ts +157 -0
  76. package/dist/render/gi/globalField.js +373 -0
  77. package/dist/render/gi/probeGridFit.d.ts +24 -0
  78. package/dist/render/gi/probeGridFit.js +71 -0
  79. package/dist/render/gi/probeTrace.d.ts +82 -0
  80. package/dist/render/gi/probeTrace.js +128 -0
  81. package/dist/render/gi/probeVolume.d.ts +136 -0
  82. package/dist/render/gi/probeVolume.js +191 -0
  83. package/dist/render/gi/reflection.d.ts +73 -0
  84. package/dist/render/gi/reflection.js +109 -0
  85. package/dist/render/gi/traceField.d.ts +91 -0
  86. package/dist/render/gi/traceField.js +129 -0
  87. package/dist/render/gi/traceScreen.d.ts +68 -0
  88. package/dist/render/gi/traceScreen.js +82 -0
  89. package/dist/render/gpuScene.d.ts +38 -0
  90. package/dist/render/gpuScene.js +41 -0
  91. package/dist/render/gpudriven/alphaTest.d.ts +28 -0
  92. package/dist/render/gpudriven/alphaTest.js +32 -0
  93. package/dist/render/gpudriven/clusterUpload.d.ts +59 -0
  94. package/dist/render/gpudriven/clusterUpload.js +75 -0
  95. package/dist/render/gpudriven/clusterWorld.d.ts +57 -0
  96. package/dist/render/gpudriven/clusterWorld.js +155 -0
  97. package/dist/render/gpudriven/compact.d.ts +51 -0
  98. package/dist/render/gpudriven/compact.js +68 -0
  99. package/dist/render/gpudriven/cullClusters.d.ts +129 -0
  100. package/dist/render/gpudriven/cullClusters.js +237 -0
  101. package/dist/render/gpudriven/decodeTables.d.ts +64 -0
  102. package/dist/render/gpudriven/decodeTables.js +219 -0
  103. package/dist/render/gpudriven/frustum.d.ts +25 -0
  104. package/dist/render/gpudriven/frustum.js +59 -0
  105. package/dist/render/gpudriven/geometryHarness.d.ts +15 -0
  106. package/dist/render/gpudriven/geometryHarness.js +31 -0
  107. package/dist/render/gpudriven/hzb.d.ts +60 -0
  108. package/dist/render/gpudriven/hzb.js +97 -0
  109. package/dist/render/gpudriven/ibl.d.ts +85 -0
  110. package/dist/render/gpudriven/ibl.js +129 -0
  111. package/dist/render/gpudriven/indirect.d.ts +67 -0
  112. package/dist/render/gpudriven/indirect.js +77 -0
  113. package/dist/render/gpudriven/instances.d.ts +43 -0
  114. package/dist/render/gpudriven/instances.js +172 -0
  115. package/dist/render/gpudriven/lit.d.ts +165 -0
  116. package/dist/render/gpudriven/lit.js +165 -0
  117. package/dist/render/gpudriven/lodCut.d.ts +28 -0
  118. package/dist/render/gpudriven/lodCut.js +38 -0
  119. package/dist/render/gpudriven/materialBin.d.ts +60 -0
  120. package/dist/render/gpudriven/materialBin.js +108 -0
  121. package/dist/render/gpudriven/materialTable.d.ts +88 -0
  122. package/dist/render/gpudriven/materialTable.js +115 -0
  123. package/dist/render/gpudriven/pipeline.d.ts +135 -0
  124. package/dist/render/gpudriven/pipeline.js +237 -0
  125. package/dist/render/gpudriven/rangeAlloc.d.ts +50 -0
  126. package/dist/render/gpudriven/rangeAlloc.js +112 -0
  127. package/dist/render/gpudriven/sceneUpload.d.ts +96 -0
  128. package/dist/render/gpudriven/sceneUpload.js +115 -0
  129. package/dist/render/gpudriven/shadeBins.d.ts +92 -0
  130. package/dist/render/gpudriven/shadeBins.js +182 -0
  131. package/dist/render/gpudriven/shadow.d.ts +136 -0
  132. package/dist/render/gpudriven/shadow.js +224 -0
  133. package/dist/render/gpudriven/shadowCamera.d.ts +87 -0
  134. package/dist/render/gpudriven/shadowCamera.js +345 -0
  135. package/dist/render/gpudriven/streamScene.d.ts +194 -0
  136. package/dist/render/gpudriven/streamScene.js +459 -0
  137. package/dist/render/gpudriven/surfaceFrame.d.ts +25 -0
  138. package/dist/render/gpudriven/surfaceFrame.js +51 -0
  139. package/dist/render/gpudriven/twoPhase.d.ts +122 -0
  140. package/dist/render/gpudriven/twoPhase.js +160 -0
  141. package/dist/render/gpudriven/visbuffer.d.ts +25 -0
  142. package/dist/render/gpudriven/visbuffer.js +37 -0
  143. package/dist/render/halfFloat.d.ts +10 -0
  144. package/dist/render/halfFloat.js +23 -4
  145. package/dist/render/inference/attentionTiled.d.ts +27 -0
  146. package/dist/render/inference/attentionTiled.js +117 -0
  147. package/dist/render/inference/denseKernels.d.ts +29 -0
  148. package/dist/render/inference/denseKernels.js +312 -0
  149. package/dist/render/inference/device.d.ts +21 -0
  150. package/dist/render/inference/device.js +32 -0
  151. package/dist/render/inference/deviceGraph.d.ts +34 -0
  152. package/dist/render/inference/deviceGraph.js +17 -0
  153. package/dist/render/inference/fuse.d.ts +15 -0
  154. package/dist/render/inference/fuse.js +37 -0
  155. package/dist/render/inference/kernelKit.d.ts +52 -0
  156. package/dist/render/inference/kernelKit.js +70 -0
  157. package/dist/render/inference/kernels.d.ts +3 -0
  158. package/dist/render/inference/kernels.js +11 -0
  159. package/dist/render/inference/plan.d.ts +13 -0
  160. package/dist/render/inference/plan.js +18 -0
  161. package/dist/render/inference/reuse.d.ts +39 -0
  162. package/dist/render/inference/reuse.js +65 -0
  163. package/dist/render/inference/runner.d.ts +31 -0
  164. package/dist/render/inference/runner.js +175 -0
  165. package/dist/render/inference/schedule.d.ts +39 -0
  166. package/dist/render/inference/schedule.js +59 -0
  167. package/dist/render/inference/shapeKernels.d.ts +10 -0
  168. package/dist/render/inference/shapeKernels.js +80 -0
  169. package/dist/render/inference/spatialKernels.d.ts +21 -0
  170. package/dist/render/inference/spatialKernels.js +179 -0
  171. package/dist/render/inference/splatRaster.wgsl.d.ts +75 -0
  172. package/dist/render/inference/splatRaster.wgsl.js +337 -0
  173. package/dist/render/inference/splatTiles.d.ts +79 -0
  174. package/dist/render/inference/splatTiles.js +161 -0
  175. package/dist/render/lineBatch.d.ts +2 -5
  176. package/dist/render/lineBatch.js +4 -24
  177. package/dist/render/mesh.d.ts +4 -0
  178. package/dist/render/mesh.js +45 -1
  179. package/dist/render/pass.d.ts +145 -8
  180. package/dist/render/prefilterEnvMap.d.ts +22 -0
  181. package/dist/render/prefilterEnvMap.js +31 -0
  182. package/dist/render/readback.d.ts +30 -0
  183. package/dist/render/readback.js +47 -0
  184. package/dist/render/recon/clamp.d.ts +36 -0
  185. package/dist/render/recon/clamp.js +92 -0
  186. package/dist/render/recon/disocclusion.d.ts +60 -0
  187. package/dist/render/recon/disocclusion.js +109 -0
  188. package/dist/render/recon/frameGen.d.ts +81 -0
  189. package/dist/render/recon/frameGen.js +150 -0
  190. package/dist/render/recon/frameSizes.d.ts +34 -0
  191. package/dist/render/recon/frameSizes.js +58 -0
  192. package/dist/render/recon/jitter.d.ts +60 -0
  193. package/dist/render/recon/jitter.js +118 -0
  194. package/dist/render/recon/motionVectors.d.ts +55 -0
  195. package/dist/render/recon/motionVectors.js +107 -0
  196. package/dist/render/recon/reproject.d.ts +41 -0
  197. package/dist/render/recon/reproject.js +131 -0
  198. package/dist/render/recon/resolve.d.ts +90 -0
  199. package/dist/render/recon/resolve.js +313 -0
  200. package/dist/render/recon/tier.d.ts +24 -0
  201. package/dist/render/recon/tier.js +8 -0
  202. package/dist/render/reflectionProbe.d.ts +26 -0
  203. package/dist/render/reflectionProbe.js +33 -0
  204. package/dist/render/renderQuality.d.ts +69 -0
  205. package/dist/render/renderQuality.js +22 -0
  206. package/dist/render/screenSpaceReflection.d.ts +23 -0
  207. package/dist/render/screenSpaceReflection.js +22 -3
  208. package/dist/render/sdfFont.js +2 -0
  209. package/dist/render/segmentQuads.d.ts +13 -0
  210. package/dist/render/segmentQuads.js +32 -0
  211. package/dist/render/shaders/flat/lobes.d.ts +27 -0
  212. package/dist/render/shaders/flat/lobes.js +33 -3
  213. package/dist/render/shaders/flat/preamble.d.ts +7 -0
  214. package/dist/render/shaders/flat/preamble.js +24 -1
  215. package/dist/render/shaders/flat/probeGrid.d.ts +1 -1
  216. package/dist/render/shaders/flat/probeGrid.js +71 -2
  217. package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -8
  218. package/dist/render/shaders/generated/flat.wgsl.js +64 -24
  219. package/dist/render/shaders/gi/composeField.wgsl.d.ts +56 -0
  220. package/dist/render/shaders/gi/composeField.wgsl.js +231 -0
  221. package/dist/render/shaders/gi/marchField.wgsl.d.ts +30 -0
  222. package/dist/render/shaders/gi/marchField.wgsl.js +141 -0
  223. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +113 -0
  224. package/dist/render/shaders/gi/probeBake.wgsl.js +635 -0
  225. package/dist/render/shaders/gi/probeTrace.wgsl.d.ts +72 -0
  226. package/dist/render/shaders/gi/probeTrace.wgsl.js +261 -0
  227. package/dist/render/shaders/gi/probeVolume.wgsl.d.ts +49 -0
  228. package/dist/render/shaders/gi/probeVolume.wgsl.js +242 -0
  229. package/dist/render/shaders/gi/sampleField.wgsl.d.ts +32 -0
  230. package/dist/render/shaders/gi/sampleField.wgsl.js +134 -0
  231. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +26 -0
  232. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +339 -0
  233. package/dist/render/shaders/gpudriven/compact.wgsl.d.ts +29 -0
  234. package/dist/render/shaders/gpudriven/compact.wgsl.js +58 -0
  235. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +90 -0
  236. package/dist/render/shaders/gpudriven/cull.wgsl.js +423 -0
  237. package/dist/render/shaders/gpudriven/decode.wgsl.d.ts +72 -0
  238. package/dist/render/shaders/gpudriven/decode.wgsl.js +349 -0
  239. package/dist/render/shaders/gpudriven/environment.wgsl.d.ts +44 -0
  240. package/dist/render/shaders/gpudriven/environment.wgsl.js +137 -0
  241. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +28 -0
  242. package/dist/render/shaders/gpudriven/fog.wgsl.js +107 -0
  243. package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +13 -0
  244. package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +62 -0
  245. package/dist/render/shaders/gpudriven/materialBin.wgsl.d.ts +51 -0
  246. package/dist/render/shaders/gpudriven/materialBin.wgsl.js +112 -0
  247. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +25 -0
  248. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +43 -0
  249. package/dist/render/shaders/gpudriven/meshTransform.wgsl.d.ts +18 -0
  250. package/dist/render/shaders/gpudriven/meshTransform.wgsl.js +37 -0
  251. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +43 -0
  252. package/dist/render/shaders/gpudriven/present.wgsl.js +127 -0
  253. package/dist/render/shaders/gpudriven/shade.wgsl.d.ts +127 -0
  254. package/dist/render/shaders/gpudriven/shade.wgsl.js +874 -0
  255. package/dist/render/shaders/gpudriven/shadow.wgsl.d.ts +44 -0
  256. package/dist/render/shaders/gpudriven/shadow.wgsl.js +308 -0
  257. package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.d.ts +28 -0
  258. package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.js +245 -0
  259. package/dist/render/shaders/network.wgsl.d.ts +101 -0
  260. package/dist/render/shaders/network.wgsl.js +206 -0
  261. package/dist/render/shaders/recon/motion.wgsl.d.ts +44 -0
  262. package/dist/render/shaders/recon/motion.wgsl.js +96 -0
  263. package/dist/render/shaders/recon/resolve.wgsl.d.ts +51 -0
  264. package/dist/render/shaders/recon/resolve.wgsl.js +733 -0
  265. package/dist/render/temporalAa.js +8 -33
  266. package/dist/render/textLayout.js +12 -6
  267. package/dist/render/textRenderer.d.ts +3 -1
  268. package/dist/render/textRenderer.js +4 -1
  269. package/dist/render/translucentQueue.d.ts +6 -0
  270. package/dist/render/translucentQueue.js +11 -0
  271. package/dist/render/windStreakRenderer.d.ts +2 -1
  272. package/dist/render/windStreakRenderer.js +3 -1
  273. package/dist/world/cell.d.ts +63 -0
  274. package/dist/world/cell.js +181 -0
  275. package/dist/world/cellStream.d.ts +103 -0
  276. package/dist/world/cellStream.js +198 -0
  277. package/dist/world/freeze.d.ts +81 -0
  278. package/dist/world/freeze.js +114 -0
  279. package/dist/world/rebase.d.ts +29 -0
  280. package/dist/world/rebase.js +41 -0
  281. package/package.json +8 -3
  282. package/scripts/frames.mjs +92 -0
  283. package/scripts/gpuCompute.mjs +323 -0
  284. package/scripts/png.d.mts +16 -0
  285. package/scripts/png.mjs +25 -0
  286. package/src/core/loop.ts +19 -2
  287. package/src/core/storage.ts +5 -0
  288. package/src/geometry/meshBuilder.ts +5 -1
  289. package/src/index.ts +246 -4
  290. package/src/input/input.ts +6 -1
  291. package/src/math/exact.ts +69 -3
  292. package/src/math/frustum.ts +52 -3
  293. package/src/render/backend/api.ts +42 -1
  294. package/src/render/backend/budget.ts +7 -6
  295. package/src/render/backend/createRenderer.ts +92 -3
  296. package/src/render/backend/materialChanges.ts +54 -0
  297. package/src/render/backend/select.ts +8 -0
  298. package/src/render/backend/webgl2/renderer.ts +211 -9
  299. package/src/render/backend/webgpu/buffers.ts +182 -69
  300. package/src/render/backend/webgpu/fieldCompose.ts +482 -0
  301. package/src/render/backend/webgpu/giFieldPass.ts +200 -0
  302. package/src/render/backend/webgpu/gpuDrivenPass.ts +2993 -0
  303. package/src/render/backend/webgpu/linePass.ts +4 -39
  304. package/src/render/backend/webgpu/probeBake.ts +588 -0
  305. package/src/render/backend/webgpu/renderer.ts +1481 -76
  306. package/src/render/backend/webgpu/scatterPass.ts +15 -1
  307. package/src/render/backend/webgpu/surfaceTexturePass.ts +63 -36
  308. package/src/render/causticsRenderer.ts +5 -2
  309. package/src/render/depthConvention.ts +15 -0
  310. package/src/render/deviceFeatures.ts +77 -0
  311. package/src/render/fog.ts +185 -0
  312. package/src/render/frame/alias.ts +97 -0
  313. package/src/render/frame/deps.ts +155 -0
  314. package/src/render/frame/flushGraph.ts +135 -0
  315. package/src/render/frame/graphSchedule.ts +170 -0
  316. package/src/render/frame/index.ts +52 -0
  317. package/src/render/frame/lifetime.ts +61 -0
  318. package/src/render/frame/passTimings.ts +69 -0
  319. package/src/render/frame/validate.ts +40 -0
  320. package/src/render/frame/virtual.ts +91 -0
  321. package/src/render/gi/chain.ts +225 -0
  322. package/src/render/gi/denoise.ts +276 -0
  323. package/src/render/gi/fieldScene.ts +264 -0
  324. package/src/render/gi/globalField.ts +543 -0
  325. package/src/render/gi/probeGridFit.ts +89 -0
  326. package/src/render/gi/probeTrace.ts +143 -0
  327. package/src/render/gi/probeVolume.ts +288 -0
  328. package/src/render/gi/reflection.ts +163 -0
  329. package/src/render/gi/traceField.ts +223 -0
  330. package/src/render/gi/traceScreen.ts +106 -0
  331. package/src/render/gpuScene.ts +80 -0
  332. package/src/render/gpudriven/alphaTest.ts +33 -0
  333. package/src/render/gpudriven/clusterUpload.ts +123 -0
  334. package/src/render/gpudriven/clusterWorld.ts +239 -0
  335. package/src/render/gpudriven/compact.ts +80 -0
  336. package/src/render/gpudriven/cullClusters.ts +333 -0
  337. package/src/render/gpudriven/decodeTables.ts +332 -0
  338. package/src/render/gpudriven/frustum.ts +70 -0
  339. package/src/render/gpudriven/geometryHarness.ts +51 -0
  340. package/src/render/gpudriven/hzb.ts +113 -0
  341. package/src/render/gpudriven/ibl.ts +147 -0
  342. package/src/render/gpudriven/indirect.ts +89 -0
  343. package/src/render/gpudriven/instances.ts +234 -0
  344. package/src/render/gpudriven/lit.ts +329 -0
  345. package/src/render/gpudriven/lodCut.ts +62 -0
  346. package/src/render/gpudriven/materialBin.ts +131 -0
  347. package/src/render/gpudriven/materialTable.ts +164 -0
  348. package/src/render/gpudriven/pipeline.ts +336 -0
  349. package/src/render/gpudriven/rangeAlloc.ts +122 -0
  350. package/src/render/gpudriven/sceneUpload.ts +173 -0
  351. package/src/render/gpudriven/shadeBins.ts +222 -0
  352. package/src/render/gpudriven/shadow.ts +285 -0
  353. package/src/render/gpudriven/shadowCamera.ts +406 -0
  354. package/src/render/gpudriven/streamScene.ts +575 -0
  355. package/src/render/gpudriven/surfaceFrame.ts +68 -0
  356. package/src/render/gpudriven/twoPhase.ts +214 -0
  357. package/src/render/gpudriven/visbuffer.ts +41 -0
  358. package/src/render/halfFloat.ts +22 -4
  359. package/src/render/inference/attentionTiled.ts +132 -0
  360. package/src/render/inference/denseKernels.ts +369 -0
  361. package/src/render/inference/device.ts +44 -0
  362. package/src/render/inference/deviceGraph.ts +39 -0
  363. package/src/render/inference/fuse.ts +47 -0
  364. package/src/render/inference/kernelKit.ts +120 -0
  365. package/src/render/inference/kernels.ts +18 -0
  366. package/src/render/inference/plan.ts +26 -0
  367. package/src/render/inference/reuse.ts +79 -0
  368. package/src/render/inference/runner.ts +204 -0
  369. package/src/render/inference/schedule.ts +80 -0
  370. package/src/render/inference/shapeKernels.ts +112 -0
  371. package/src/render/inference/spatialKernels.ts +220 -0
  372. package/src/render/inference/splatRaster.wgsl.ts +348 -0
  373. package/src/render/inference/splatTiles.ts +186 -0
  374. package/src/render/lineBatch.ts +4 -24
  375. package/src/render/mesh.ts +46 -1
  376. package/src/render/pass.ts +155 -10
  377. package/src/render/prefilterEnvMap.ts +33 -0
  378. package/src/render/readback.ts +73 -0
  379. package/src/render/recon/clamp.ts +113 -0
  380. package/src/render/recon/disocclusion.ts +143 -0
  381. package/src/render/recon/frameGen.ts +191 -0
  382. package/src/render/recon/frameSizes.ts +64 -0
  383. package/src/render/recon/jitter.ts +126 -0
  384. package/src/render/recon/motionVectors.ts +138 -0
  385. package/src/render/recon/reproject.ts +179 -0
  386. package/src/render/recon/resolve.ts +444 -0
  387. package/src/render/recon/tier.ts +31 -0
  388. package/src/render/reflectionProbe.ts +34 -0
  389. package/src/render/renderQuality.ts +87 -0
  390. package/src/render/screenSpaceReflection.ts +37 -3
  391. package/src/render/sdfFont.ts +2 -0
  392. package/src/render/segmentQuads.ts +39 -0
  393. package/src/render/shaders/flat/lobes.ts +35 -3
  394. package/src/render/shaders/flat/preamble.ts +25 -1
  395. package/src/render/shaders/flat/probeGrid.ts +77 -2
  396. package/src/render/shaders/generated/flat.wgsl.ts +64 -24
  397. package/src/render/shaders/gi/composeField.wgsl.ts +239 -0
  398. package/src/render/shaders/gi/marchField.wgsl.ts +146 -0
  399. package/src/render/shaders/gi/probeBake.wgsl.ts +649 -0
  400. package/src/render/shaders/gi/probeTrace.wgsl.ts +269 -0
  401. package/src/render/shaders/gi/probeVolume.wgsl.ts +246 -0
  402. package/src/render/shaders/gi/sampleField.wgsl.ts +140 -0
  403. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +343 -0
  404. package/src/render/shaders/gpudriven/compact.wgsl.ts +59 -0
  405. package/src/render/shaders/gpudriven/cull.wgsl.ts +433 -0
  406. package/src/render/shaders/gpudriven/decode.wgsl.ts +373 -0
  407. package/src/render/shaders/gpudriven/environment.wgsl.ts +141 -0
  408. package/src/render/shaders/gpudriven/fog.wgsl.ts +110 -0
  409. package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +62 -0
  410. package/src/render/shaders/gpudriven/materialBin.wgsl.ts +116 -0
  411. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +46 -0
  412. package/src/render/shaders/gpudriven/meshTransform.wgsl.ts +37 -0
  413. package/src/render/shaders/gpudriven/present.wgsl.ts +129 -0
  414. package/src/render/shaders/gpudriven/shade.wgsl.ts +884 -0
  415. package/src/render/shaders/gpudriven/shadow.wgsl.ts +322 -0
  416. package/src/render/shaders/gpudriven/visbufferRaster.wgsl.ts +251 -0
  417. package/src/render/shaders/network.wgsl.ts +238 -0
  418. package/src/render/shaders/recon/motion.wgsl.ts +101 -0
  419. package/src/render/shaders/recon/resolve.wgsl.ts +741 -0
  420. package/src/render/temporalAa.ts +9 -33
  421. package/src/render/textLayout.ts +12 -6
  422. package/src/render/textRenderer.ts +5 -2
  423. package/src/render/translucentQueue.ts +11 -0
  424. package/src/render/windStreakRenderer.ts +4 -2
  425. package/src/world/cell.ts +231 -0
  426. package/src/world/cellStream.ts +278 -0
  427. package/src/world/freeze.ts +157 -0
  428. package/src/world/rebase.ts +56 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A probe's own rays, on the device: `gi/chain.ts` minus its screen level.
3
+ *
4
+ * **A probe has no use for screen space, and including it would be a defect rather than a cost.**
5
+ * The screen level is an accelerator for rays cast from the camera's own view, where the frame
6
+ * already holds the answer. A probe stands in the world and its rays go everywhere, so most of them
7
+ * leave the frame — and the few that do not would make a *cached* probe depend on where the camera
8
+ * happens to be, so a probe would change its contents as the viewer turned and every surface it
9
+ * lights would shimmer. So the chain is entered with a projector that answers "off screen" for
10
+ * every point, which makes level 0 contribute exactly nothing and levels 1 and 2 take the whole
11
+ * ray. The reference stays one function; this is that function with one input pinned.
12
+ *
13
+ * **What a field hit is worth comes from the probes**, which is the maintainer's answer of
14
+ * 2026-09-17 and the reason `GiResources.fieldRadiance` is a function rather than a texture: a
15
+ * probe traced against radiance that is itself probe radiance converges on several bounces over a
16
+ * few refreshes. The direction it is read along is back down the ray — the light leaving that
17
+ * surface *towards* the probe is what the probe is measuring — which the chain's own signature
18
+ * leaves to the caller because it takes no direction.
19
+ *
20
+ * **One core, two modules**, as `shaders/recon/resolve.wgsl.ts` and `network.wgsl.ts` are. The
21
+ * arithmetic reads the field and the probes through functions the including module defines, so the
22
+ * production module can read the renderer's own buffers and the parity check can read plain ones of
23
+ * the same numbers.
24
+ *
25
+ * The accessors an including module must define, before including the core:
26
+ *
27
+ * ```wgsl
28
+ * fn probeFieldAt(p: vec3<f32>) -> f32 // the composed field's distance at a world point
29
+ * fn probeFieldInside(p: vec3<f32>) -> f32 // how far inside the outermost cascade, negative outside
30
+ * fn probeSurfaceRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32>
31
+ * fn probeDistantRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32>
32
+ * ```
33
+ *
34
+ * **Two, and it used to be one.** A ray that struck a wall and a ray that left the world need
35
+ * different answers and the single accessor could not tell them apart: what leaves a wall is its
36
+ * albedo times the light reaching it, and what a ray finds having left is whatever the probe
37
+ * volume coarsely says is out there. Measured with one accessor, a red wall and a white wall
38
+ * returned the same colour — `demo/dev/bounce.html`, and `plans/2026-09-17-indirect-light.md`
39
+ * Task 8.
40
+ *
41
+ * and `march: ProbeMarch` as a uniform.
42
+ */
43
+ /**
44
+ * Floats in the march block, which is sixteen because of how a uniform aligns rather than how many
45
+ * numbers it holds: four counts, four lengths, and two `vec3`s that each take a whole four-float row.
46
+ */
47
+ export declare const PROBE_MARCH_FLOATS = 16;
48
+ /** The workgroup a trace dispatches over: one invocation a direction. */
49
+ export declare const PROBE_TRACE_WORKGROUP = 64;
50
+ export declare const PROBE_MARCH_WGSL = "\nstruct ProbeMarch {\n /* Steps a ray may take, the cascades it may read, and how many samples a cascade is a side. */\n steps: u32,\n levels: u32,\n side: u32,\n rayCount: u32,\n /* How far a ray may travel, how close to a surface counts as arriving, the finest voxel. */\n reachM: f32,\n hitEpsilonM: f32,\n finestStep: f32,\n coneAngle: f32,\n /* The outermost cascade's corners, for the confidence the last level is faded by. */\n outerLow: vec3<f32>,\n outerHigh: vec3<f32>,\n}\n";
51
+ /**
52
+ * The march and the two levels, as text.
53
+ *
54
+ * Defines `probeIndirect(point, normal, direction, coneAngle) -> vec3<f32>`, which is what one of a
55
+ * probe's rays is worth.
56
+ */
57
+ export declare const PROBE_TRACE_CORE_WGSL: string;
58
+ /** `FIELD_MARCH`'s own numbers, so a caller fills the block from one place. */
59
+ export declare const PROBE_MARCH_DEFAULTS: import("../../gi/traceField.ts").FieldMarch;
60
+ /**
61
+ * The parity module: the core over plain buffers, with a radiance that is a function of position.
62
+ *
63
+ * **The radiance is positional on purpose.** A march that lands a voxel away from where the
64
+ * reference landed would return almost the right answer from a smooth field and exactly the right
65
+ * one from a constant, so a constant radiance would check the *blend* and nothing about the march.
66
+ * A steep function of the hit's position turns a wrong hit into a wrong colour.
67
+ *
68
+ * `probeRadianceAt` is an accessor by design, so this check is of the march and the level blend
69
+ * alone; what a probe volume answers is the production module's business and `probeVolume.test.ts`
70
+ * is what holds that.
71
+ */
72
+ export declare function probeTraceParityWgsl(): string;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * A probe's own rays, on the device: `gi/chain.ts` minus its screen level.
3
+ *
4
+ * **A probe has no use for screen space, and including it would be a defect rather than a cost.**
5
+ * The screen level is an accelerator for rays cast from the camera's own view, where the frame
6
+ * already holds the answer. A probe stands in the world and its rays go everywhere, so most of them
7
+ * leave the frame — and the few that do not would make a *cached* probe depend on where the camera
8
+ * happens to be, so a probe would change its contents as the viewer turned and every surface it
9
+ * lights would shimmer. So the chain is entered with a projector that answers "off screen" for
10
+ * every point, which makes level 0 contribute exactly nothing and levels 1 and 2 take the whole
11
+ * ray. The reference stays one function; this is that function with one input pinned.
12
+ *
13
+ * **What a field hit is worth comes from the probes**, which is the maintainer's answer of
14
+ * 2026-09-17 and the reason `GiResources.fieldRadiance` is a function rather than a texture: a
15
+ * probe traced against radiance that is itself probe radiance converges on several bounces over a
16
+ * few refreshes. The direction it is read along is back down the ray — the light leaving that
17
+ * surface *towards* the probe is what the probe is measuring — which the chain's own signature
18
+ * leaves to the caller because it takes no direction.
19
+ *
20
+ * **One core, two modules**, as `shaders/recon/resolve.wgsl.ts` and `network.wgsl.ts` are. The
21
+ * arithmetic reads the field and the probes through functions the including module defines, so the
22
+ * production module can read the renderer's own buffers and the parity check can read plain ones of
23
+ * the same numbers.
24
+ *
25
+ * The accessors an including module must define, before including the core:
26
+ *
27
+ * ```wgsl
28
+ * fn probeFieldAt(p: vec3<f32>) -> f32 // the composed field's distance at a world point
29
+ * fn probeFieldInside(p: vec3<f32>) -> f32 // how far inside the outermost cascade, negative outside
30
+ * fn probeSurfaceRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32>
31
+ * fn probeDistantRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32>
32
+ * ```
33
+ *
34
+ * **Two, and it used to be one.** A ray that struck a wall and a ray that left the world need
35
+ * different answers and the single accessor could not tell them apart: what leaves a wall is its
36
+ * albedo times the light reaching it, and what a ray finds having left is whatever the probe
37
+ * volume coarsely says is out there. Measured with one accessor, a red wall and a white wall
38
+ * returned the same colour — `demo/dev/bounce.html`, and `plans/2026-09-17-indirect-light.md`
39
+ * Task 8.
40
+ *
41
+ * and `march: ProbeMarch` as a uniform.
42
+ */
43
+ import { GLOBAL_FIELD_BLEND } from '../../gi/globalField.js';
44
+ import { FIELD_MARCH, FIELD_START_VOXELS } from '../../gi/traceField.js';
45
+ import { SAMPLE_FIELD_WGSL } from './sampleField.wgsl.js';
46
+ /**
47
+ * Floats in the march block, which is sixteen because of how a uniform aligns rather than how many
48
+ * numbers it holds: four counts, four lengths, and two `vec3`s that each take a whole four-float row.
49
+ */
50
+ export const PROBE_MARCH_FLOATS = 16;
51
+ /** The workgroup a trace dispatches over: one invocation a direction. */
52
+ export const PROBE_TRACE_WORKGROUP = 64;
53
+ /** A number as WGSL always reads it as a float, so an integer constant is not an `i32`. */
54
+ function f(value) {
55
+ return Number.isInteger(value) ? value.toFixed(1) : String(value);
56
+ }
57
+ export const PROBE_MARCH_WGSL = /* wgsl */ `
58
+ struct ProbeMarch {
59
+ /* Steps a ray may take, the cascades it may read, and how many samples a cascade is a side. */
60
+ steps: u32,
61
+ levels: u32,
62
+ side: u32,
63
+ rayCount: u32,
64
+ /* How far a ray may travel, how close to a surface counts as arriving, the finest voxel. */
65
+ reachM: f32,
66
+ hitEpsilonM: f32,
67
+ finestStep: f32,
68
+ coneAngle: f32,
69
+ /* The outermost cascade's corners, for the confidence the last level is faded by. */
70
+ outerLow: vec3<f32>,
71
+ outerHigh: vec3<f32>,
72
+ }
73
+ `;
74
+ /**
75
+ * The march and the two levels, as text.
76
+ *
77
+ * Defines `probeIndirect(point, normal, direction, coneAngle) -> vec3<f32>`, which is what one of a
78
+ * probe's rays is worth.
79
+ */
80
+ export const PROBE_TRACE_CORE_WGSL = /* wgsl */ `
81
+ const PROBE_FIELD_START_VOXELS: f32 = ${f(FIELD_START_VOXELS)};
82
+ const PROBE_FIELD_BLEND: f32 = ${f(GLOBAL_FIELD_BLEND)};
83
+
84
+ struct ProbeFieldHit {
85
+ hit: bool,
86
+ point: vec3<f32>,
87
+ }
88
+
89
+ /* A cone's radius at a distance. Zero angle is a ray, which is what a probe's rays are. */
90
+ fn probeConeRadius(distanceM: f32, coneAngle: f32) -> f32 {
91
+ return select(0.0, distanceM * tan(coneAngle), coneAngle > 0.0);
92
+ }
93
+
94
+ /*
95
+ * March one ray through the composed world field.
96
+ *
97
+ * **Lifted along the normal and only along the normal.** Pushing the start along the ray as well
98
+ * double-counts: a ray leaving along its own normal would begin two offsets out and every distance
99
+ * it reported would carry the second one. And it does not help the case that needs help — a tangent
100
+ * ray pushed along itself is still on the surface, because a surface is what tangent means.
101
+ */
102
+ fn probeTraceField(
103
+ point: vec3<f32>,
104
+ normal: vec3<f32>,
105
+ direction: vec3<f32>,
106
+ coneAngle: f32,
107
+ ) -> ProbeFieldHit {
108
+ var out: ProbeFieldHit;
109
+ out.hit = false;
110
+ out.point = point;
111
+
112
+ /* Not named "length": a local of that name shadows the built-in and the next call to it fails. */
113
+ let reach = length(direction);
114
+ if (!(reach > 0.0)) { return out; }
115
+ let unit = direction / reach;
116
+
117
+ let start = PROBE_FIELD_START_VOXELS * march.finestStep;
118
+ let normalLength = length(normal);
119
+ let lifted = point + select(vec3<f32>(0.0), normal * (start / normalLength), normalLength > 0.0);
120
+
121
+ var travelled = 0.0;
122
+ for (var step = 0u; step < march.steps; step = step + 1u) {
123
+ let at = lifted + unit * travelled;
124
+ /*
125
+ * **Outside the outermost cascade the march stops rather than sampling.** There is no
126
+ * information out there: the sampler clamps to the edge, which is right for a sample and wrong
127
+ * for a march — stepping on clamped values converges on the cascade's boundary and reports a
128
+ * surface made of nothing.
129
+ */
130
+ if (probeFieldInside(at) <= 0.0) { return out; }
131
+
132
+ let distance = probeFieldAt(at);
133
+ if (distance <= probeConeRadius(travelled, coneAngle) + march.hitEpsilonM) {
134
+ out.hit = true;
135
+ out.point = at;
136
+ return out;
137
+ }
138
+ /*
139
+ * **Step by the distance, and by at least a quarter of a voxel.** A field whose value is
140
+ * genuinely tiny beside a surface the cone is not wide enough to accept would otherwise take
141
+ * ever-smaller steps and spend the whole budget arriving nowhere, which reads as a miss and
142
+ * costs a fallback anyway. The floor makes that case terminate quickly instead.
143
+ */
144
+ travelled = travelled + max(distance, march.finestStep * 0.25);
145
+ if (travelled > march.reachM) { return out; }
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /*
151
+ * How much of a field hit is trusted, faded across the outer tenth of the outermost cascade.
152
+ *
153
+ * Every boundary in this chain moves with the camera — the cascades are centred on it — and a hard
154
+ * switch at a boundary that moves is a seam sweeping through the picture whenever the camera turns.
155
+ */
156
+ fn probeFieldConfidence(p: vec3<f32>) -> f32 {
157
+ let band = PROBE_FIELD_BLEND * (march.outerHigh.x - march.outerLow.x);
158
+ if (band <= 0.0) { return 1.0; }
159
+ let gap = min(p - march.outerLow, march.outerHigh - p);
160
+ return clamp(min(gap.x, min(gap.y, gap.z)) / band, 0.0, 1.0);
161
+ }
162
+
163
+ /*
164
+ * One of a probe's rays, as radiance.
165
+ *
166
+ * Level 1 answers as much as its distance from the outermost cascade's face allows; level 2 takes
167
+ * whatever is left without asking, which is what "never wrong and only ever coarse" buys. There is
168
+ * no case where the probes decline, and no fourth level to fall to.
169
+ */
170
+ fn probeIndirect(
171
+ point: vec3<f32>,
172
+ normal: vec3<f32>,
173
+ direction: vec3<f32>,
174
+ coneAngle: f32,
175
+ ) -> vec3<f32> {
176
+ var radiance = vec3<f32>(0.0);
177
+ var fieldShare = 0.0;
178
+
179
+ let hit = probeTraceField(point, normal, direction, coneAngle);
180
+ if (hit.hit) {
181
+ fieldShare = probeFieldConfidence(hit.point);
182
+ if (fieldShare > 0.0) {
183
+ /* Back down the ray: what this surface sends *towards* the probe is what the probe measures. */
184
+ radiance = radiance + probeSurfaceRadiance(hit.point, -direction) * fieldShare;
185
+ }
186
+ }
187
+
188
+ let probes = 1.0 - fieldShare;
189
+ if (probes > 0.0) {
190
+ radiance = radiance + probeDistantRadiance(point + direction * march.reachM, -direction) * probes;
191
+ }
192
+ return radiance;
193
+ }
194
+ `;
195
+ /** `FIELD_MARCH`'s own numbers, so a caller fills the block from one place. */
196
+ export const PROBE_MARCH_DEFAULTS = FIELD_MARCH;
197
+ /**
198
+ * The parity module: the core over plain buffers, with a radiance that is a function of position.
199
+ *
200
+ * **The radiance is positional on purpose.** A march that lands a voxel away from where the
201
+ * reference landed would return almost the right answer from a smooth field and exactly the right
202
+ * one from a constant, so a constant radiance would check the *blend* and nothing about the march.
203
+ * A steep function of the hit's position turns a wrong hit into a wrong colour.
204
+ *
205
+ * `probeRadianceAt` is an accessor by design, so this check is of the march and the level blend
206
+ * alone; what a probe volume answers is the production module's business and `probeVolume.test.ts`
207
+ * is what holds that.
208
+ */
209
+ export function probeTraceParityWgsl() {
210
+ return /* wgsl */ `
211
+ @group(0) @binding(0) var<storage, read> cascades: array<f32>;
212
+ @group(0) @binding(1) var<storage, read> fields: array<f32>;
213
+ ${PROBE_MARCH_WGSL}
214
+ @group(0) @binding(2) var<uniform> march: ProbeMarch;
215
+ /* Seven floats a ray: the point, the normal, the direction. The cone angle is the march's. */
216
+ @group(0) @binding(3) var<storage, read> rays: array<f32>;
217
+ @group(0) @binding(4) var<storage, read_write> radiance: array<f32>;
218
+ ${SAMPLE_FIELD_WGSL}
219
+
220
+ fn probeFieldAt(p: vec3<f32>) -> f32 {
221
+ return sampleField(march.levels, march.side, p);
222
+ }
223
+
224
+ fn probeFieldInside(p: vec3<f32>) -> f32 {
225
+ return insideBy(march.levels - 1u, p);
226
+ }
227
+
228
+ /* The same expression the harness evaluates, and the reason it is a sine of position. */
229
+ fn probeParityRadiance(p: vec3<f32>) -> vec3<f32> {
230
+ return vec3<f32>(
231
+ 0.5 + 0.4 * sin(p.x * 1.3),
232
+ 0.5 + 0.4 * sin(p.y * 1.7 + 1.0),
233
+ 0.5 + 0.4 * sin(p.z * 2.3 + 2.0),
234
+ );
235
+ }
236
+
237
+ /* One expression for both, so this module keeps checking the march and the blend and nothing else. */
238
+ fn probeSurfaceRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32> {
239
+ return probeParityRadiance(p);
240
+ }
241
+
242
+ fn probeDistantRadiance(p: vec3<f32>, towards: vec3<f32>) -> vec3<f32> {
243
+ return probeParityRadiance(p);
244
+ }
245
+ ${PROBE_TRACE_CORE_WGSL}
246
+
247
+ @compute @workgroup_size(${PROBE_TRACE_WORKGROUP})
248
+ fn traceMain(@builtin(global_invocation_id) id: vec3<u32>) {
249
+ if (id.x >= march.rayCount) { return; }
250
+ let at = id.x * 9u;
251
+ let point = vec3<f32>(rays[at], rays[at + 1u], rays[at + 2u]);
252
+ let normal = vec3<f32>(rays[at + 3u], rays[at + 4u], rays[at + 5u]);
253
+ let direction = vec3<f32>(rays[at + 6u], rays[at + 7u], rays[at + 8u]);
254
+ let value = probeIndirect(point, normal, direction, march.coneAngle);
255
+ let out = id.x * 3u;
256
+ radiance[out] = value.x;
257
+ radiance[out + 1u] = value.y;
258
+ radiance[out + 2u] = value.z;
259
+ }
260
+ `;
261
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What the probe grid answers at a world point, on the device.
3
+ *
4
+ * **This is the fourth spelling of one idea and the file says so rather than hoping.**
5
+ * `probeGrid.ts` holds the eight-probe trilinear blend in TypeScript, `octahedral.ts` holds the
6
+ * mapping in TypeScript and again in GLSL, and `shaders/flat/probeGrid.ts` holds both together in
7
+ * the GLSL the lit pass uses. This is WGSL, because a hand-written compute shader cannot include a
8
+ * generated one — and `scripts/probe-bake-parity.mjs` is what holds it to the others rather than
9
+ * a reader's confidence that four copies of an expression say the same thing.
10
+ *
11
+ * **Why a probe reads the grid at all**: the maintainer's answer of 2026-09-17 is that what a field
12
+ * hit is worth comes from the probes, so a probe traced against radiance that is itself probe
13
+ * radiance converges on several bounces over a few refreshes. That is what makes this the multi-
14
+ * bounce half of Wave 4A rather than a single-bounce approximation of it.
15
+ *
16
+ * **No visibility term here, deliberately.** `visibleProbes` exists to stop light reaching a
17
+ * *surface* through a wall it is behind, and it is what `gridIrradiance` applies when shading. A
18
+ * probe asking "what is the radiance out there" is a different question with a different failure:
19
+ * crushing a blind probe's weight would make the bounce darker than the room, and the reference
20
+ * this is checked against — `indirect-parity.mjs` — enters the chain with `visibility: null` for
21
+ * exactly this reason.
22
+ *
23
+ * The accessor an including module must define, before including the core:
24
+ *
25
+ * ```wgsl
26
+ * fn probeLayerSample(layer: u32, uv: vec2<f32>) -> vec3<f32> // at the radiance level
27
+ * fn probeLayerIrradiance(layer: u32, uv: vec2<f32>) -> vec3<f32> // at the irradiance level
28
+ * ```
29
+ *
30
+ * and `volume: ProbeVolume` as a uniform. Production samples a texture; the parity module reads a
31
+ * plain buffer of the same numbers.
32
+ */
33
+ /**
34
+ * Floats in the volume block, and where each field starts.
35
+ *
36
+ * Named offsets for the reason `probeBake.wgsl.ts` gives and paid for: an `f32` after a `vec3<f32>`
37
+ * fills the vec3's padding rather than starting a new row, so `edge` is float **11**.
38
+ */
39
+ export declare const VOLUME_ORIGIN = 0;
40
+ export declare const VOLUME_INV_SPACING = 4;
41
+ export declare const VOLUME_COUNTS = 8;
42
+ export declare const VOLUME_EDGE = 11;
43
+ export declare const VOLUME_LEVEL = 12;
44
+ export declare const VOLUME_RADIANCE_EDGE = 13;
45
+ export declare const VOLUME_RADIANCE_LEVEL = 14;
46
+ /** Fifty-two bytes of fields rounded up to the struct's own sixteen-byte alignment. */
47
+ export declare const PROBE_VOLUME_FLOATS = 16;
48
+ export declare const PROBE_VOLUME_STRUCT_WGSL = "\nstruct ProbeVolume {\n /* Where the grid's first probe stands, and one over the step between them. */\n origin: vec3<f32>,\n invSpacing: vec3<f32>,\n /* How many probes along each axis. Whole numbers carried as floats. */\n counts: vec3<f32>,\n /* Texels across the level being sampled, gutter included. */\n edge: f32,\n /* Which mip of the probe array that level is. */\n level: f32,\n /* And the level that holds *radiance* rather than irradiance, with its own edge. */\n radianceEdge: f32,\n radianceLevel: f32,\n padA: f32,\n}\n";
49
+ export declare const PROBE_VOLUME_CORE_WGSL = "\n/* The octahedral encode, matching \"octEncode\" in \"shaders/octahedral.ts\". */\nfn probeOctEncode(d: vec3<f32>) -> vec2<f32> {\n let n = d / (abs(d.x) + abs(d.y) + abs(d.z));\n var p = n.xy;\n if (n.z < 0.0) {\n p = (1.0 - abs(vec2<f32>(n.y, n.x))) *\n vec2<f32>(select(-1.0, 1.0, n.x >= 0.0), select(-1.0, 1.0, n.y >= 0.0));\n }\n return p * 0.5 + 0.5;\n}\n\n/*\n * Where to sample a direction in a map whose outer ring is a gutter.\n *\n * The inner region is \"edge - 2\" texels offset by one, so a direction at the very border of the\n * octahedral square lands on the last real texel and the tap that interpolates past it reads the\n * gutter \u2014 which the bake filled with the folded direction.\n */\nfn probeOctInsetUv(d: vec3<f32>, edge: f32) -> vec2<f32> {\n return (probeOctEncode(d) * (edge - 2.0) + 1.0) / edge;\n}\n\n/*\n * One axis of the lookup: which probe is below the point, and how far past it the point is.\n *\n * **The index is integer arithmetic and the fraction is not**, which is the distinction row 139 of\n * the resume file is about. A world position divided by a spacing is a genuine float division and\n * its floor is a genuine floor; what must never be written in floats is a *lattice* \u2014 an index\n * decomposed out of a layer number \u2014 because a device may implement that division as a\n * multiplication by a reciprocal and answer 3 for 3.0 % 3.0.\n *\n * The lower index stops one short of the last probe so the pair \"index\" and \"index + 1\" is always\n * inside the grid, and a single-probe axis has no pair at all \u2014 both corners are probe zero and\n * the fraction is zero, so that axis multiplies every weight by one.\n */\nstruct ProbeCell {\n index: u32,\n fraction: f32,\n}\n\nfn probeCell(value: f32, origin: f32, invSpacing: f32, count: u32) -> ProbeCell {\n var out: ProbeCell;\n out.index = 0u;\n out.fraction = 0.0;\n if (count <= 1u) { return out; }\n let local = (value - origin) * invSpacing;\n let span = f32(count - 1u);\n let clamped = clamp(local, 0.0, span);\n let index = min(u32(floor(clamped)), count - 2u);\n out.index = index;\n out.fraction = clamped - f32(index);\n return out;\n}\n\n/*\n * The layer a probe at whole lattice coordinates occupies. x fastest, then y, then z.\n *\n * **Not clamped, and \"ProbeGrid.layerAt\" is** \u2014 the difference is who may call them. \"layerAt\" is a\n * public method whose contract is that it accepts anything, so it clamps; this is reached from one\n * place, under two guarantees that together make the clamp unreachable. \"probeCell\" returns an\n * index no greater than \"count - 2\", so \"index + 1\" is inside; and on an axis only one probe wide\n * it returns index 0 with fraction 0, so the far corner's weight is zero and the caller skips it\n * before asking. A clamp here would be a line no perturbation can make fail, which this session\n * has already had to remove twice.\n */\nfn probeLayerAt(ix: u32, iy: u32, iz: u32, counts: vec3<u32>) -> u32 {\n return ix + counts.x * (iy + counts.y * iz);\n}\n\n/*\n * What has already bounced onto a surface facing the normal at a point, from the level this writes.\n *\n * **One shared direction, and that is right here where a per-corner one is right for radiance.**\n * An irradiance map is indexed by the *normal of the surface being lit*, not by where the light\n * came from, so every probe is asked the same question: what reaches a surface facing this way.\n * Reading it at the level the bake writes is what closes the loop \u2014 the second bounce is the first\n * one read back.\n */\nfn probeVolumeIrradianceAt(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {\n let counts = vec3<u32>(\n max(1u, u32(volume.counts.x)),\n max(1u, u32(volume.counts.y)),\n max(1u, u32(volume.counts.z)),\n );\n let cx = probeCell(p.x, volume.origin.x, volume.invSpacing.x, counts.x);\n let cy = probeCell(p.y, volume.origin.y, volume.invSpacing.y, counts.y);\n let cz = probeCell(p.z, volume.origin.z, volume.invSpacing.z, counts.z);\n let uv = probeOctInsetUv(n, volume.edge);\n\n var sum = vec3<f32>(0.0);\n for (var oz = 0u; oz < 2u; oz = oz + 1u) {\n let wz = select(1.0 - cz.fraction, cz.fraction, oz == 1u);\n for (var oy = 0u; oy < 2u; oy = oy + 1u) {\n let wy = select(1.0 - cy.fraction, cy.fraction, oy == 1u);\n for (var ox = 0u; ox < 2u; ox = ox + 1u) {\n let wx = select(1.0 - cx.fraction, cx.fraction, ox == 1u);\n let weight = wx * wy * wz;\n if (weight <= 0.0) { continue; }\n let layer = probeLayerAt(cx.index + ox, cy.index + oy, cz.index + oz, counts);\n sum = sum + probeLayerIrradiance(layer, uv) * weight;\n }\n }\n }\n return sum;\n}\n\n/* Where a layer's probe stands, which a parallax read needs and the lattice above decides. */\nfn probeVolumeProbeAt(layer: u32, counts: vec3<u32>) -> vec3<f32> {\n let x = layer % counts.x;\n let y = (layer / counts.x) % counts.y;\n let z = layer / (counts.x * counts.y);\n let spacing = vec3<f32>(\n select(0.0, 1.0 / volume.invSpacing.x, volume.invSpacing.x != 0.0),\n select(0.0, 1.0 / volume.invSpacing.y, volume.invSpacing.y != 0.0),\n select(0.0, 1.0 / volume.invSpacing.z, volume.invSpacing.z != 0.0),\n );\n return volume.origin + vec3<f32>(f32(x), f32(y), f32(z)) * spacing;\n}\n\n/*\n * The radiance leaving a world point, as the grid saw it.\n *\n * **A ray needs radiance and the first version read irradiance**, which is what\n * \"demo/dev/bounce.html\" measured as a bounce dimmer and greyer than the rasterised grid it was\n * meant to replace. Irradiance is light *arriving* at a point, already cosine-convolved over a\n * hemisphere; what a ray that struck a wall needs is the light *leaving* that wall towards it. The\n * two differ by the surface's albedo \u2014 so a red wall and a white wall at the same irradiance\n * returned the same colour, and the red never travelled.\n *\n * **Level 0 of the probe array is that radiance**, and it needs no albedo because it is a\n * rasterised capture of the shaded room: the red wall is red in it. \u00A76.4 of the design names\n * \"envProbeArray.ts\" as this track's starting point and leaves how much of it to reuse to\n * measurement, which is this.\n *\n * **Each corner is read along its own direction to the point**, which is what makes this a\n * position lookup rather than a direction one. Eight probes sampled along one shared direction\n * answer \"what is over there from roughly here\"; eight probes each sampled towards \"p\" answer\n * \"what is at \"p\"\", and the difference is the parallax that a marched hit exists to provide. It is\n * also why no direction is passed in: deriving it removes the reversed-direction mistake that\n * \"probe-bake-parity.mjs\" records being unable to see.\n */\nfn probeVolumeRadianceAt(p: vec3<f32>) -> vec3<f32> {\n let counts = vec3<u32>(\n max(1u, u32(volume.counts.x)),\n max(1u, u32(volume.counts.y)),\n max(1u, u32(volume.counts.z)),\n );\n let cx = probeCell(p.x, volume.origin.x, volume.invSpacing.x, counts.x);\n let cy = probeCell(p.y, volume.origin.y, volume.invSpacing.y, counts.y);\n let cz = probeCell(p.z, volume.origin.z, volume.invSpacing.z, counts.z);\n\n var sum = vec3<f32>(0.0);\n for (var oz = 0u; oz < 2u; oz = oz + 1u) {\n let wz = select(1.0 - cz.fraction, cz.fraction, oz == 1u);\n for (var oy = 0u; oy < 2u; oy = oy + 1u) {\n let wy = select(1.0 - cy.fraction, cy.fraction, oy == 1u);\n for (var ox = 0u; ox < 2u; ox = ox + 1u) {\n let wx = select(1.0 - cx.fraction, cx.fraction, ox == 1u);\n let weight = wx * wy * wz;\n if (weight <= 0.0) { continue; }\n let layer = probeLayerAt(cx.index + ox, cy.index + oy, cz.index + oz, counts);\n /*\n * From that probe towards the point, which is the parallax. A probe standing exactly on\n * the point has no direction to offer and takes its own zeroth texel, which is as good an\n * answer as exists for a point inside a probe.\n */\n let offset = p - probeVolumeProbeAt(layer, counts);\n let reach = length(offset);\n let towards = select(vec3<f32>(0.0, 0.0, 1.0), offset / reach, reach > 0.0);\n let uv = probeOctInsetUv(towards, volume.radianceEdge);\n sum = sum + probeLayerSample(layer, uv) * weight;\n }\n }\n }\n return sum;\n}\n";
@@ -0,0 +1,242 @@
1
+ /**
2
+ * What the probe grid answers at a world point, on the device.
3
+ *
4
+ * **This is the fourth spelling of one idea and the file says so rather than hoping.**
5
+ * `probeGrid.ts` holds the eight-probe trilinear blend in TypeScript, `octahedral.ts` holds the
6
+ * mapping in TypeScript and again in GLSL, and `shaders/flat/probeGrid.ts` holds both together in
7
+ * the GLSL the lit pass uses. This is WGSL, because a hand-written compute shader cannot include a
8
+ * generated one — and `scripts/probe-bake-parity.mjs` is what holds it to the others rather than
9
+ * a reader's confidence that four copies of an expression say the same thing.
10
+ *
11
+ * **Why a probe reads the grid at all**: the maintainer's answer of 2026-09-17 is that what a field
12
+ * hit is worth comes from the probes, so a probe traced against radiance that is itself probe
13
+ * radiance converges on several bounces over a few refreshes. That is what makes this the multi-
14
+ * bounce half of Wave 4A rather than a single-bounce approximation of it.
15
+ *
16
+ * **No visibility term here, deliberately.** `visibleProbes` exists to stop light reaching a
17
+ * *surface* through a wall it is behind, and it is what `gridIrradiance` applies when shading. A
18
+ * probe asking "what is the radiance out there" is a different question with a different failure:
19
+ * crushing a blind probe's weight would make the bounce darker than the room, and the reference
20
+ * this is checked against — `indirect-parity.mjs` — enters the chain with `visibility: null` for
21
+ * exactly this reason.
22
+ *
23
+ * The accessor an including module must define, before including the core:
24
+ *
25
+ * ```wgsl
26
+ * fn probeLayerSample(layer: u32, uv: vec2<f32>) -> vec3<f32> // at the radiance level
27
+ * fn probeLayerIrradiance(layer: u32, uv: vec2<f32>) -> vec3<f32> // at the irradiance level
28
+ * ```
29
+ *
30
+ * and `volume: ProbeVolume` as a uniform. Production samples a texture; the parity module reads a
31
+ * plain buffer of the same numbers.
32
+ */
33
+ /**
34
+ * Floats in the volume block, and where each field starts.
35
+ *
36
+ * Named offsets for the reason `probeBake.wgsl.ts` gives and paid for: an `f32` after a `vec3<f32>`
37
+ * fills the vec3's padding rather than starting a new row, so `edge` is float **11**.
38
+ */
39
+ export const VOLUME_ORIGIN = 0;
40
+ export const VOLUME_INV_SPACING = 4;
41
+ export const VOLUME_COUNTS = 8;
42
+ export const VOLUME_EDGE = 11;
43
+ export const VOLUME_LEVEL = 12;
44
+ export const VOLUME_RADIANCE_EDGE = 13;
45
+ export const VOLUME_RADIANCE_LEVEL = 14;
46
+ /** Fifty-two bytes of fields rounded up to the struct's own sixteen-byte alignment. */
47
+ export const PROBE_VOLUME_FLOATS = 16;
48
+ export const PROBE_VOLUME_STRUCT_WGSL = /* wgsl */ `
49
+ struct ProbeVolume {
50
+ /* Where the grid's first probe stands, and one over the step between them. */
51
+ origin: vec3<f32>,
52
+ invSpacing: vec3<f32>,
53
+ /* How many probes along each axis. Whole numbers carried as floats. */
54
+ counts: vec3<f32>,
55
+ /* Texels across the level being sampled, gutter included. */
56
+ edge: f32,
57
+ /* Which mip of the probe array that level is. */
58
+ level: f32,
59
+ /* And the level that holds *radiance* rather than irradiance, with its own edge. */
60
+ radianceEdge: f32,
61
+ radianceLevel: f32,
62
+ padA: f32,
63
+ }
64
+ `;
65
+ export const PROBE_VOLUME_CORE_WGSL = /* wgsl */ `
66
+ /* The octahedral encode, matching "octEncode" in "shaders/octahedral.ts". */
67
+ fn probeOctEncode(d: vec3<f32>) -> vec2<f32> {
68
+ let n = d / (abs(d.x) + abs(d.y) + abs(d.z));
69
+ var p = n.xy;
70
+ if (n.z < 0.0) {
71
+ p = (1.0 - abs(vec2<f32>(n.y, n.x))) *
72
+ vec2<f32>(select(-1.0, 1.0, n.x >= 0.0), select(-1.0, 1.0, n.y >= 0.0));
73
+ }
74
+ return p * 0.5 + 0.5;
75
+ }
76
+
77
+ /*
78
+ * Where to sample a direction in a map whose outer ring is a gutter.
79
+ *
80
+ * The inner region is "edge - 2" texels offset by one, so a direction at the very border of the
81
+ * octahedral square lands on the last real texel and the tap that interpolates past it reads the
82
+ * gutter — which the bake filled with the folded direction.
83
+ */
84
+ fn probeOctInsetUv(d: vec3<f32>, edge: f32) -> vec2<f32> {
85
+ return (probeOctEncode(d) * (edge - 2.0) + 1.0) / edge;
86
+ }
87
+
88
+ /*
89
+ * One axis of the lookup: which probe is below the point, and how far past it the point is.
90
+ *
91
+ * **The index is integer arithmetic and the fraction is not**, which is the distinction row 139 of
92
+ * the resume file is about. A world position divided by a spacing is a genuine float division and
93
+ * its floor is a genuine floor; what must never be written in floats is a *lattice* — an index
94
+ * decomposed out of a layer number — because a device may implement that division as a
95
+ * multiplication by a reciprocal and answer 3 for 3.0 % 3.0.
96
+ *
97
+ * The lower index stops one short of the last probe so the pair "index" and "index + 1" is always
98
+ * inside the grid, and a single-probe axis has no pair at all — both corners are probe zero and
99
+ * the fraction is zero, so that axis multiplies every weight by one.
100
+ */
101
+ struct ProbeCell {
102
+ index: u32,
103
+ fraction: f32,
104
+ }
105
+
106
+ fn probeCell(value: f32, origin: f32, invSpacing: f32, count: u32) -> ProbeCell {
107
+ var out: ProbeCell;
108
+ out.index = 0u;
109
+ out.fraction = 0.0;
110
+ if (count <= 1u) { return out; }
111
+ let local = (value - origin) * invSpacing;
112
+ let span = f32(count - 1u);
113
+ let clamped = clamp(local, 0.0, span);
114
+ let index = min(u32(floor(clamped)), count - 2u);
115
+ out.index = index;
116
+ out.fraction = clamped - f32(index);
117
+ return out;
118
+ }
119
+
120
+ /*
121
+ * The layer a probe at whole lattice coordinates occupies. x fastest, then y, then z.
122
+ *
123
+ * **Not clamped, and "ProbeGrid.layerAt" is** — the difference is who may call them. "layerAt" is a
124
+ * public method whose contract is that it accepts anything, so it clamps; this is reached from one
125
+ * place, under two guarantees that together make the clamp unreachable. "probeCell" returns an
126
+ * index no greater than "count - 2", so "index + 1" is inside; and on an axis only one probe wide
127
+ * it returns index 0 with fraction 0, so the far corner's weight is zero and the caller skips it
128
+ * before asking. A clamp here would be a line no perturbation can make fail, which this session
129
+ * has already had to remove twice.
130
+ */
131
+ fn probeLayerAt(ix: u32, iy: u32, iz: u32, counts: vec3<u32>) -> u32 {
132
+ return ix + counts.x * (iy + counts.y * iz);
133
+ }
134
+
135
+ /*
136
+ * What has already bounced onto a surface facing the normal at a point, from the level this writes.
137
+ *
138
+ * **One shared direction, and that is right here where a per-corner one is right for radiance.**
139
+ * An irradiance map is indexed by the *normal of the surface being lit*, not by where the light
140
+ * came from, so every probe is asked the same question: what reaches a surface facing this way.
141
+ * Reading it at the level the bake writes is what closes the loop — the second bounce is the first
142
+ * one read back.
143
+ */
144
+ fn probeVolumeIrradianceAt(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
145
+ let counts = vec3<u32>(
146
+ max(1u, u32(volume.counts.x)),
147
+ max(1u, u32(volume.counts.y)),
148
+ max(1u, u32(volume.counts.z)),
149
+ );
150
+ let cx = probeCell(p.x, volume.origin.x, volume.invSpacing.x, counts.x);
151
+ let cy = probeCell(p.y, volume.origin.y, volume.invSpacing.y, counts.y);
152
+ let cz = probeCell(p.z, volume.origin.z, volume.invSpacing.z, counts.z);
153
+ let uv = probeOctInsetUv(n, volume.edge);
154
+
155
+ var sum = vec3<f32>(0.0);
156
+ for (var oz = 0u; oz < 2u; oz = oz + 1u) {
157
+ let wz = select(1.0 - cz.fraction, cz.fraction, oz == 1u);
158
+ for (var oy = 0u; oy < 2u; oy = oy + 1u) {
159
+ let wy = select(1.0 - cy.fraction, cy.fraction, oy == 1u);
160
+ for (var ox = 0u; ox < 2u; ox = ox + 1u) {
161
+ let wx = select(1.0 - cx.fraction, cx.fraction, ox == 1u);
162
+ let weight = wx * wy * wz;
163
+ if (weight <= 0.0) { continue; }
164
+ let layer = probeLayerAt(cx.index + ox, cy.index + oy, cz.index + oz, counts);
165
+ sum = sum + probeLayerIrradiance(layer, uv) * weight;
166
+ }
167
+ }
168
+ }
169
+ return sum;
170
+ }
171
+
172
+ /* Where a layer's probe stands, which a parallax read needs and the lattice above decides. */
173
+ fn probeVolumeProbeAt(layer: u32, counts: vec3<u32>) -> vec3<f32> {
174
+ let x = layer % counts.x;
175
+ let y = (layer / counts.x) % counts.y;
176
+ let z = layer / (counts.x * counts.y);
177
+ let spacing = vec3<f32>(
178
+ select(0.0, 1.0 / volume.invSpacing.x, volume.invSpacing.x != 0.0),
179
+ select(0.0, 1.0 / volume.invSpacing.y, volume.invSpacing.y != 0.0),
180
+ select(0.0, 1.0 / volume.invSpacing.z, volume.invSpacing.z != 0.0),
181
+ );
182
+ return volume.origin + vec3<f32>(f32(x), f32(y), f32(z)) * spacing;
183
+ }
184
+
185
+ /*
186
+ * The radiance leaving a world point, as the grid saw it.
187
+ *
188
+ * **A ray needs radiance and the first version read irradiance**, which is what
189
+ * "demo/dev/bounce.html" measured as a bounce dimmer and greyer than the rasterised grid it was
190
+ * meant to replace. Irradiance is light *arriving* at a point, already cosine-convolved over a
191
+ * hemisphere; what a ray that struck a wall needs is the light *leaving* that wall towards it. The
192
+ * two differ by the surface's albedo — so a red wall and a white wall at the same irradiance
193
+ * returned the same colour, and the red never travelled.
194
+ *
195
+ * **Level 0 of the probe array is that radiance**, and it needs no albedo because it is a
196
+ * rasterised capture of the shaded room: the red wall is red in it. §6.4 of the design names
197
+ * "envProbeArray.ts" as this track's starting point and leaves how much of it to reuse to
198
+ * measurement, which is this.
199
+ *
200
+ * **Each corner is read along its own direction to the point**, which is what makes this a
201
+ * position lookup rather than a direction one. Eight probes sampled along one shared direction
202
+ * answer "what is over there from roughly here"; eight probes each sampled towards "p" answer
203
+ * "what is at "p"", and the difference is the parallax that a marched hit exists to provide. It is
204
+ * also why no direction is passed in: deriving it removes the reversed-direction mistake that
205
+ * "probe-bake-parity.mjs" records being unable to see.
206
+ */
207
+ fn probeVolumeRadianceAt(p: vec3<f32>) -> vec3<f32> {
208
+ let counts = vec3<u32>(
209
+ max(1u, u32(volume.counts.x)),
210
+ max(1u, u32(volume.counts.y)),
211
+ max(1u, u32(volume.counts.z)),
212
+ );
213
+ let cx = probeCell(p.x, volume.origin.x, volume.invSpacing.x, counts.x);
214
+ let cy = probeCell(p.y, volume.origin.y, volume.invSpacing.y, counts.y);
215
+ let cz = probeCell(p.z, volume.origin.z, volume.invSpacing.z, counts.z);
216
+
217
+ var sum = vec3<f32>(0.0);
218
+ for (var oz = 0u; oz < 2u; oz = oz + 1u) {
219
+ let wz = select(1.0 - cz.fraction, cz.fraction, oz == 1u);
220
+ for (var oy = 0u; oy < 2u; oy = oy + 1u) {
221
+ let wy = select(1.0 - cy.fraction, cy.fraction, oy == 1u);
222
+ for (var ox = 0u; ox < 2u; ox = ox + 1u) {
223
+ let wx = select(1.0 - cx.fraction, cx.fraction, ox == 1u);
224
+ let weight = wx * wy * wz;
225
+ if (weight <= 0.0) { continue; }
226
+ let layer = probeLayerAt(cx.index + ox, cy.index + oy, cz.index + oz, counts);
227
+ /*
228
+ * From that probe towards the point, which is the parallax. A probe standing exactly on
229
+ * the point has no direction to offer and takes its own zeroth texel, which is as good an
230
+ * answer as exists for a point inside a probe.
231
+ */
232
+ let offset = p - probeVolumeProbeAt(layer, counts);
233
+ let reach = length(offset);
234
+ let towards = select(vec3<f32>(0.0, 0.0, 1.0), offset / reach, reach > 0.0);
235
+ let uv = probeOctInsetUv(towards, volume.radianceEdge);
236
+ sum = sum + probeLayerSample(layer, uv) * weight;
237
+ }
238
+ }
239
+ }
240
+ return sum;
241
+ }
242
+ `;