@volter/game-editor 0.5.82 → 0.5.83

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 (332) hide show
  1. package/BUNDLED_NOTICES +22 -22
  2. package/catalog/entries/blender.json +2 -2
  3. package/catalog/entries/music.json +2 -2
  4. package/catalog/entries/reflections.json +1 -1
  5. package/catalog/project-source/src/lib/reflections/ReflectionProbe.tsx +34 -5
  6. package/catalog/project-source/src/lib/reflections/index.ts +1 -0
  7. package/catalog/project-source/src/lib/reflections/probe-controller.ts +44 -3
  8. package/catalog/project-source/src/lib/reflections/reflection-probe-registry.ts +32 -7
  9. package/catalog/project-source/src/lib/reflections/volume-reflection-material.ts +638 -160
  10. package/dist/.vite/manifest.json +2206 -2188
  11. package/dist/assets/{AssetBudgetPanel-Iip49rxI.js → AssetBudgetPanel-SrgQ5Lx5.js} +3 -3
  12. package/dist/assets/{AudioViewer-DmkOvskF.js → AudioViewer-BqEwLSZN.js} +1 -1
  13. package/dist/assets/{CanvasBoardDocument-DCS8uzQo.js → CanvasBoardDocument-BfzWZdLX.js} +1 -1
  14. package/dist/assets/{CanvasSceneViewport-CfOuQiQ0.js → CanvasSceneViewport-CQ7ewAlO.js} +1 -1
  15. package/dist/assets/{ColorInput-7CPpl8MI.js → ColorInput-DQ6YyPD_.js} +1 -1
  16. package/dist/assets/EntityModelDocument-By-BM9dS.js +1 -0
  17. package/dist/assets/{EnvironmentAssetDocument-DlS6KFJu.js → EnvironmentAssetDocument-CrD37BI0.js} +1 -1
  18. package/dist/assets/{GameplaySessionTimeline-C3zH6edi.js → GameplaySessionTimeline-DYtnW0Si.js} +1 -1
  19. package/dist/assets/{ImageViewer-C0lQ6YO0.js → ImageViewer-CZNpdUEA.js} +1 -1
  20. package/dist/assets/InspectorObjectPreview-gWOVV6Ll.js +1 -0
  21. package/dist/assets/{JsonAssetDocument-DjmH9CQH.js → JsonAssetDocument-Bg74MDXy.js} +1 -1
  22. package/dist/assets/{LiveModuleDocument-Hiot8Q5L.js → LiveModuleDocument-CSqzNMjn.js} +1 -1
  23. package/dist/assets/{LutAssetDocument-6bBwNMOn.js → LutAssetDocument-CsboXa8x.js} +1 -1
  24. package/dist/assets/ModelAssetDocument-DDVoKZpQ.js +1 -0
  25. package/dist/assets/{NonThreeAuthoringBootstrap-DpieYAMC.js → NonThreeAuthoringBootstrap-DcqD6_34.js} +1 -1
  26. package/dist/assets/{Object3DDocumentToolbar-BtZkDxKa.js → Object3DDocumentToolbar-p2pd2TuS.js} +1 -1
  27. package/dist/assets/Object3DDocumentViewport-BvNfSHi_.js +2 -0
  28. package/dist/assets/{Object3DPreview-DdrYVJUZ.js → Object3DPreview-Bv0fZcdS.js} +1 -1
  29. package/dist/assets/{OnlineAssetDetail-D06eTe3y.js → OnlineAssetDetail-CPDafN4x.js} +1 -1
  30. package/dist/assets/{PlayBar-BbzjgLFJ.js → PlayBar-CBD4rcED.js} +1 -1
  31. package/dist/assets/{QuarksAssetDocument-wGHNt4ov.js → QuarksAssetDocument-DUUhKUxZ.js} +1 -1
  32. package/dist/assets/{ReactCanvasControls-BL2tOUj6.js → ReactCanvasControls-3ual87uP.js} +1 -1
  33. package/dist/assets/{RootSelectionOverlay-C6Wu8nIs.js → RootSelectionOverlay-DtX8yWUQ.js} +1 -1
  34. package/dist/assets/{ShaderAssetDocument-BDi8iNLA.js → ShaderAssetDocument-rkhRdPRk.js} +3 -3
  35. package/dist/assets/{ShadowEditor-C3L0UWSH.js → ShadowEditor-B-3FlftY.js} +1 -1
  36. package/dist/assets/{SourceAssetViewer-D832bARo.js → SourceAssetViewer-CsMPohqK.js} +1 -1
  37. package/dist/assets/StageHost-BOaImrmu.js +2 -0
  38. package/dist/assets/{StoryPreviewMount-CmOEFzZY.js → StoryPreviewMount-BI-ANJix.js} +1 -1
  39. package/dist/assets/{ThreeBoardDocument-BtBMGtiQ.js → ThreeBoardDocument-CoBjs8BP.js} +2 -2
  40. package/dist/assets/{ThreeIsolationSceneContent-BEHNSEsi.js → ThreeIsolationSceneContent-uShwSK1O.js} +1 -1
  41. package/dist/assets/{ToolObject3DPreview-76LMkfin.js → ToolObject3DPreview-CBZdWA_7.js} +1 -1
  42. package/dist/assets/{Toolbar-BEBQdxRz.js → Toolbar-DJK7o_be.js} +1 -1
  43. package/dist/assets/{TransientHint-CzsdY8W6.js → TransientHint-Dgu6u7U5.js} +1 -1
  44. package/dist/assets/{VideoViewer-Ckrawyuq.js → VideoViewer-BwTcASO2.js} +1 -1
  45. package/dist/assets/{ViewportShadingMenu-BofQt6_4.js → ViewportShadingMenu-DUROUZdC.js} +1 -1
  46. package/dist/assets/{active-ingest-D_ryI_R4.js → active-ingest-Ra07ruDo.js} +3 -3
  47. package/dist/assets/{animation-clock-QjtJDIsR.js → animation-clock-CKeqYCt9.js} +1 -1
  48. package/dist/assets/{animation-mixers.service-BInUs1US.js → animation-mixers.service-BNiEU2dA.js} +1 -1
  49. package/dist/assets/{animation-timeline.utility-DUrfNSwn.js → animation-timeline.utility-DQqMh1lF.js} +1 -1
  50. package/dist/assets/{asset-budget-asset.menu-DVQEksFp.js → asset-budget-asset.menu-DFFih9b4.js} +1 -1
  51. package/dist/assets/{asset-budget.action-C-ENAa52.js → asset-budget.action-C3SMMeSX.js} +1 -1
  52. package/dist/assets/{asset-budget.document-Bb1PyL3H.js → asset-budget.document-B5NTy5dU.js} +1 -1
  53. package/dist/assets/{asset-budget.menu-CPi2yjBA.js → asset-budget.menu-Cclz2utV.js} +1 -1
  54. package/dist/assets/{asset-editor-persistence-Duoq7o22.js → asset-editor-persistence-DsIjTddX.js} +1 -1
  55. package/dist/assets/{asset-loaders-BftJ2m7-.js → asset-loaders-CQiLyapK.js} +1 -1
  56. package/dist/assets/{asset-preview-BS71mXsZ.js → asset-preview-BkFAKkZH.js} +1 -1
  57. package/dist/assets/{audio-unlock.service-C3ygvDjU.js → audio-unlock.service-CupzdUuC.js} +1 -1
  58. package/dist/assets/{audio.utility-DF55sgHW.js → audio.utility-0il96T5s.js} +1 -1
  59. package/dist/assets/autoplay.service-SQBFaKvM.js +1 -0
  60. package/dist/assets/{babylon-authoring-adapter-CmeehOBt.js → babylon-authoring-adapter-HZ_SbAet.js} +1 -1
  61. package/dist/assets/{binding-resolver-DUz8C8Fs.js → binding-resolver-BoecCqTq.js} +2 -2
  62. package/dist/assets/blender-emscripten-engine-Dk-rq4bm.js +2 -0
  63. package/dist/assets/{blender-node-editor.document-B8S2TVMM.js → blender-node-editor.document-B_MGPd1D.js} +1 -1
  64. package/dist/assets/{blender-properties-tab-y-_BeZDx.js → blender-properties-tab-ITmWT_gZ.js} +1 -1
  65. package/dist/assets/{blender-runtime-host-CUH6peXj.js → blender-runtime-host-CACzxt1b.js} +1 -1
  66. package/dist/assets/{blender-runtime-skin-B1tYITn2.js → blender-runtime-skin-CwFW2Hj7.js} +13 -13
  67. package/dist/assets/blender-runtime.document-CozOB5TX.js +13 -0
  68. package/dist/assets/blender-screenshot.menu-BFR44H0L.js +1 -0
  69. package/dist/assets/{blender-timeline.document-CeCylyXB.js → blender-timeline.document-C0Zzi8Vo.js} +1 -1
  70. package/dist/assets/{blender-uv-editor.document-DEllMw3a.js → blender-uv-editor.document-BktbYoor.js} +1 -1
  71. package/dist/assets/{blender-version.status-CrT9NqK-.js → blender-version.status-CLPjDPFV.js} +1 -1
  72. package/dist/assets/blender-wali-engine-CUWSQ_vI.js +2 -0
  73. package/dist/assets/{blender.command-GIjMG1-m.js → blender.command-DdHEHM62.js} +1 -1
  74. package/dist/assets/{board-guides-B6TEPOPi.js → board-guides-DhwRYHGT.js} +1 -1
  75. package/dist/assets/bridge.command-D4O9w4ft.js +3 -0
  76. package/dist/assets/{browserAll-k35XRFil.js → browserAll-BhQBgoJL.js} +1 -1
  77. package/dist/assets/{build-player.document-CHxj4P8q.js → build-player.document-DFr-d97N.js} +1 -1
  78. package/dist/assets/{build-profiles.document-CZCk1HYA.js → build-profiles.document-C-64YHSV.js} +2 -2
  79. package/dist/assets/{build-progress.status-BNwBxP2a.js → build-progress.status-DVm95Lmt.js} +1 -1
  80. package/dist/assets/{build-session-CW2UyYFF.js → build-session-C_SD2MNJ.js} +1 -1
  81. package/dist/assets/{build.action-EmCcMu47.js → build.action-CPee4b_9.js} +1 -1
  82. package/dist/assets/{build.command-CzEckGcR.js → build.command-CrZ5sHYt.js} +1 -1
  83. package/dist/assets/{build.header-CsYLpZbh.js → build.header-DDbhXD6t.js} +1 -1
  84. package/dist/assets/{build.menu-CaDkDQuy.js → build.menu-Ci-W_A1E.js} +1 -1
  85. package/dist/assets/{build.service-BcKtbU0-.js → build.service-CpUtTLP4.js} +1 -1
  86. package/dist/assets/{camera-BwWZUyKt.js → camera-BZM67LhS.js} +1 -1
  87. package/dist/assets/{camera-runtime.inspector-CJyi3zmp.js → camera-runtime.inspector-BLgPGd97.js} +1 -1
  88. package/dist/assets/{canvas-design-mount-B_uwZkEU.js → canvas-design-mount-DPAhHuIQ.js} +4 -4
  89. package/dist/assets/{canvas-entry-runtime-DskaohHK.js → canvas-entry-runtime-DeyvFKWS.js} +2 -2
  90. package/dist/assets/{canvas-preview-frames-BzLeTumX.js → canvas-preview-frames-Cm0GJ-40.js} +1 -1
  91. package/dist/assets/{canvas-story-capture.service-C80CsS8r.js → canvas-story-capture.service-Ziv9XNot.js} +2 -2
  92. package/dist/assets/{capability-coverage-wd6bAhv3.js → capability-coverage-39yK3DGt.js} +3 -3
  93. package/dist/assets/collaboration-presence-MR8LW4mZ.js +1 -0
  94. package/dist/assets/{collider-Dw2iWV3F.js → collider-4Sgmy-qt.js} +1 -1
  95. package/dist/assets/component-board.service-BdcOkwgn.js +2 -0
  96. package/dist/assets/{component-board.service-CH99sgj0.js → component-board.service-DO_oHdnS.js} +2 -2
  97. package/dist/assets/{component-board.service-XAxyLCnM.js → component-board.service-DRJcETCR.js} +2 -2
  98. package/dist/assets/{component-contracts.service-DerT0BP-.js → component-contracts.service-BYQTO1HK.js} +1 -1
  99. package/dist/assets/{component-instance-root-Cw_-ajBX.js → component-instance-root-CzRnRMe6.js} +1 -1
  100. package/dist/assets/{component-verbs.command-BHK9M2Rk.js → component-verbs.command-CFv5y_pR.js} +1 -1
  101. package/dist/assets/component-verbs.service-BMBAwqjA.js +1 -0
  102. package/dist/assets/{config-1rCvm02z.js → config-C6iAsozB.js} +3 -3
  103. package/dist/assets/{configurations-CRTYw0r9.js → configurations-Dua_x6Wx.js} +1 -1
  104. package/dist/assets/{connection.status-D4fjnH24.js → connection.status-DQJvEgdT.js} +1 -1
  105. package/dist/assets/{constraints.inspector-BgnafOLf.js → constraints.inspector-AHeP-LBc.js} +1 -1
  106. package/dist/assets/{content-bounds-D5pXeHJN.js → content-bounds-tMCAobKq.js} +1 -1
  107. package/dist/assets/{contract-system-adapters-rVGdworX.js → contract-system-adapters-D6ion7SF.js} +1 -1
  108. package/dist/assets/{coverage.service-BNme5Agb.js → coverage.service-Dm_H5o74.js} +2 -2
  109. package/dist/assets/{create-runtime-CcuQTSfy.js → create-runtime-C82CmCbc.js} +2 -2
  110. package/dist/assets/{curve-DorFv33R.js → curve-C7Gg05DV.js} +1 -1
  111. package/dist/assets/{data-asset-CAE35Emh.js → data-asset-BquhdFX2.js} +1 -1
  112. package/dist/assets/{data-ref-DXqRnZsJ.js → data-ref-Dcq9KJIG.js} +1 -1
  113. package/dist/assets/{debug-bridge-DdCJSEni.js → debug-bridge-DODnLefz.js} +1 -1
  114. package/dist/assets/{debug-registry-xQJcVubm.js → debug-registry-CwYeaZ9J.js} +1 -1
  115. package/dist/assets/{deferred-ingest-play-CEnsC3uN.js → deferred-ingest-play-7EJo_GNB.js} +2 -2
  116. package/dist/assets/{design-time-canvas-mount-CPRrCPFA.js → design-time-canvas-mount-DCINR1Hg.js} +2 -2
  117. package/dist/assets/{design-time-layers-hmGqBp06.js → design-time-layers-i1N-JjPz.js} +1 -1
  118. package/dist/assets/{design-time-mount.service-CLL-1Za4.js → design-time-mount.service-CtnijZJJ.js} +2 -2
  119. package/dist/assets/{design-time-mount.service-CEmJjRIJ.js → design-time-mount.service-DrUVDx2-.js} +2 -2
  120. package/dist/assets/{design-time-react-mount-DmXi3GBy.js → design-time-react-mount-CpgoPMGr.js} +1 -1
  121. package/dist/assets/{document-preview-three-B4j_zNef.js → document-preview-three-CuBUMP4o.js} +1 -1
  122. package/dist/assets/{dom-authoring-adapter-BklRbjZB.js → dom-authoring-adapter-DSM-2JgU.js} +1 -1
  123. package/dist/assets/{draco3d-CFWI1XDT.js → draco3d--d91Ay95.js} +1 -1
  124. package/dist/assets/{edit-mode-audio.service-SUzv0nHs.js → edit-mode-audio.service-C9_299G0.js} +1 -1
  125. package/dist/assets/{edit-mode-authoring-CSt3zzmS.js → edit-mode-authoring-DgvzXpiM.js} +1 -1
  126. package/dist/assets/{edit-mode-networking.service-D6UohrCr.js → edit-mode-networking.service-B6l9ESo-.js} +1 -1
  127. package/dist/assets/editor-api-D9NJWRVZ.js +1 -0
  128. package/dist/assets/{editor-layers-CjeYrUlm.js → editor-layers-D7HXdvWY.js} +1 -1
  129. package/dist/assets/{editor-shell-store-DckLm6m8.js → editor-shell-store-CGWO4Ldz.js} +1 -1
  130. package/dist/assets/{entity-object-V13bGmay.js → entity-object-Dl04-FE8.js} +1 -1
  131. package/dist/assets/{extend-BQeRA-aV.js → extend-GXzJ7XU_.js} +1 -1
  132. package/dist/assets/fork-menu-DO1KASKk.js +1 -0
  133. package/dist/assets/{frame-debugger-store-DDxWlDPK.js → frame-debugger-store-DOewah-x.js} +1 -1
  134. package/dist/assets/{game-contract-seams-DrYyE5PX.js → game-contract-seams-B5a-Ev-f.js} +1 -1
  135. package/dist/assets/{game-css-scope-transform-D5hQWaci.js → game-css-scope-transform-CQhuDfMR.js} +1 -1
  136. package/dist/assets/{game-document.service-cJ1yKc2V.js → game-document.service-CMc6SfH9.js} +1 -1
  137. package/dist/assets/{game-eval.command-CKo-W17_.js → game-eval.command-MY5YxNL-.js} +1 -1
  138. package/dist/assets/{game-module-access-DuptGLGx.js → game-module-access-BafPamOR.js} +1 -1
  139. package/dist/assets/{game-network-CAH_Jsh6.js → game-network-COQRxS_4.js} +3 -3
  140. package/dist/assets/{gameplay-export-B-JE4uSD.js → gameplay-export-i_fnSpRp.js} +1 -1
  141. package/dist/assets/gameplay-replay-BM_frdpr.js +1 -0
  142. package/dist/assets/{gameplay.command-D4cY3nOq.js → gameplay.command-Dkp2IG9Q.js} +1 -1
  143. package/dist/assets/{generation-jobs-CcCrhKLM.js → generation-jobs-B6T_j2D_.js} +1 -1
  144. package/dist/assets/{generation.service-CQwmLAOQ.js → generation.service-BXJTa6dr.js} +1 -1
  145. package/dist/assets/{generations-qfhCRH8C.js → generations-DqNhoKz8.js} +1 -1
  146. package/dist/assets/{generations.status-CcbR2OfF.js → generations.status-Dt8TW7Lk.js} +1 -1
  147. package/dist/assets/{hierarchy-marks-B1jSqxSv.js → hierarchy-marks-fSNwXIFe.js} +1 -1
  148. package/dist/assets/{hosted-asset-materialization-BZf-8JA1.js → hosted-asset-materialization-76jdNP28.js} +2 -2
  149. package/dist/assets/ibl-override-material-C09OAhCc.js +6 -0
  150. package/dist/assets/{index-V4J7xfRg.js → index-C5tasxXb.js} +294 -294
  151. package/dist/assets/{index-DXtfej3G.js → index-CKznhnFr.js} +1 -1
  152. package/dist/assets/index-CO6HFW9e.js +1 -0
  153. package/dist/assets/{index-aREcGExk.js → index-Cf-uQvPt.js} +1 -1
  154. package/dist/assets/{index-D_ViOFCz.js → index-Cub6ESrb.js} +2 -2
  155. package/dist/assets/{index-DrEVdQvN.js → index-D5pDWxZH.js} +1 -1
  156. package/dist/assets/{index-Cj_b0Ygc.js → index-c6fd_bqy.js} +1 -1
  157. package/dist/assets/{index-CHa43s2p.js → index-cGaEzVz5.js} +1 -1
  158. package/dist/assets/{index-D-RMHSzb.js → index-cNdu8TlA.js} +1 -1
  159. package/dist/assets/{inference-diagnostics-DoZQ1mpg.js → inference-diagnostics-D7uYs70A.js} +1 -1
  160. package/dist/assets/{ingest-boot-viewport-DWdOtGGE.js → ingest-boot-viewport-CjtFKkMD.js} +1 -1
  161. package/dist/assets/{ingest-siblings-DyvasVDH.js → ingest-siblings-Dlx0quan.js} +1 -1
  162. package/dist/assets/{ingest-source-persistence-CQOJeGhj.js → ingest-source-persistence-SAe0xIy-.js} +1 -1
  163. package/dist/assets/{ingest.service-CqIcydem.js → ingest.service-BXkZ1vXa.js} +1 -1
  164. package/dist/assets/{inspect-DiSjB-N1.js → inspect-BqIL-41D.js} +1 -1
  165. package/dist/assets/instances.command-wpQVWPb9.js +1 -0
  166. package/dist/assets/{json-history-resource-CH2nzMZh.js → json-history-resource-BUJgH5iK.js} +1 -1
  167. package/dist/assets/layered-pick-RElCrqXb.js +1 -0
  168. package/dist/assets/{light-CxiGIJma.js → light-DLADuHH2.js} +1 -1
  169. package/dist/assets/{live-project-verbs-CnknlUlT.js → live-project-verbs-Cc1GXtG5.js} +1 -1
  170. package/dist/assets/{machine.document-D7jMrzXt.js → machine.document-ER7OD-37.js} +1 -1
  171. package/dist/assets/{machines.finder-Dor3O0uf.js → machines.finder-CZ2ZxPFR.js} +1 -1
  172. package/dist/assets/material-CspuVO3Q.js +1 -0
  173. package/dist/assets/{material-factory-BN3Lax5C.js → material-factory-KV4mqSzl.js} +1 -1
  174. package/dist/assets/{mesh-Cj-CYroI.js → mesh-CrIkl8_H.js} +1 -1
  175. package/dist/assets/{model-asset-sections.service-mC4WJFTA.js → model-asset-sections.service-Cyom1FRs.js} +1 -1
  176. package/dist/assets/{model-inspection-8o57YSZW.js → model-inspection-BLOiPoW4.js} +1 -1
  177. package/dist/assets/{model-thumbnail-chePRlH7.js → model-thumbnail-B7DVMYtQ.js} +1 -1
  178. package/dist/assets/{models.finder-CKkT5ZdU.js → models.finder-At2wuWE3.js} +1 -1
  179. package/dist/assets/{module-mode-BMiMhHI3.js → module-mode-CHqvCb69.js} +2 -2
  180. package/dist/assets/{mount-canvas-ingest-root-B1swVIUJ.js → mount-canvas-ingest-root-uISMj3zn.js} +2 -2
  181. package/dist/assets/{mount-dom-ingest-root-B8hO5mht.js → mount-dom-ingest-root-C8PsgrCd.js} +2 -2
  182. package/dist/assets/{mount-ingest-root-Cbnh-mSw.js → mount-ingest-root-Dmb_D_oY.js} +2 -2
  183. package/dist/assets/{mount-manifest-B7oL1Abg.js → mount-manifest-CGwCOZO6.js} +1 -1
  184. package/dist/assets/{mount-three-ingest-root-BdUJx6ix.js → mount-three-ingest-root-BGdd6UoG.js} +1 -1
  185. package/dist/assets/{mounted-authoring-C47L5H5t.js → mounted-authoring-BS53gpIm.js} +1 -1
  186. package/dist/assets/{mounted-story-viewport-source-CyEK_vaq.js → mounted-story-viewport-source-CL9Ra-_N.js} +1 -1
  187. package/dist/assets/{native-debug-module-JJOFtIVz.js → native-debug-module-DRmkIrIW.js} +1 -1
  188. package/dist/assets/{navigation-ROJM2w84.js → navigation-DOuNvaV6.js} +1 -1
  189. package/dist/assets/{navmesh.menu-DjefqtkI.js → navmesh.menu-9g4nO2dp.js} +1 -1
  190. package/dist/assets/{navmesh.service-BPqjEQiq.js → navmesh.service-B7MllQj9.js} +1 -1
  191. package/dist/assets/network-observer.service-DY_39EdG.js +1 -0
  192. package/dist/assets/{network.utility-6Y5u53n8.js → network.utility-BgzPLUS5.js} +1 -1
  193. package/dist/assets/{object3d-document-session-registry-DNCOGA3i.js → object3d-document-session-registry-Bk-I9jZH.js} +2 -2
  194. package/dist/assets/{oid-transform-DQzN_Gl_.js → oid-transform-CHeHFNUu.js} +1 -1
  195. package/dist/assets/{particles-DGdUmqOr.js → particles-RD4cI69F.js} +1 -1
  196. package/dist/assets/{pixi-authoring-adapter-BRktdcIz.js → pixi-authoring-adapter-BuP3Qc7X.js} +1 -1
  197. package/dist/assets/{pixi-live-write-target-Dg0mdLaa.js → pixi-live-write-target-DzyXhmiw.js} +1 -1
  198. package/dist/assets/{play.command-4IDAqR5W.js → play.command-C7iZbm26.js} +2 -2
  199. package/dist/assets/{presentation-8m-RDKIS.js → presentation-43UAzXCV.js} +1 -1
  200. package/dist/assets/{product-t6tauO7R.js → product-BfaoEtK8.js} +1 -1
  201. package/dist/assets/{profiler.action-CXA4IkQB.js → profiler.action-DIp4OPNA.js} +1 -1
  202. package/dist/assets/{profiler.menu-BgIBv4-I.js → profiler.menu-tHZqii7c.js} +1 -1
  203. package/dist/assets/{profiler.utility-BYoRbfIK.js → profiler.utility-CSg5WNN8.js} +1 -1
  204. package/dist/assets/{project-authoring-session-DkekCxNv.js → project-authoring-session-BbwFHEzZ.js} +2 -2
  205. package/dist/assets/{properties-bone-constraints.inspector-DXpiCtf9.js → properties-bone-constraints.inspector-ooGWoGiI.js} +1 -1
  206. package/dist/assets/{properties-bone.inspector-wIDsP4c5.js → properties-bone.inspector-C_UdJjDe.js} +1 -1
  207. package/dist/assets/{properties-collection.inspector-5SRPbqfM.js → properties-collection.inspector-Dm4MNZQK.js} +1 -1
  208. package/dist/assets/{properties-constraints.inspector-DIjKz692.js → properties-constraints.inspector-DlCHF3Nd.js} +1 -1
  209. package/dist/assets/{properties-data.inspector-DE6i8biZ.js → properties-data.inspector-DTfv2ILq.js} +1 -1
  210. package/dist/assets/{properties-material.inspector-Cu1UCPY4.js → properties-material.inspector-C_-GsOvF.js} +1 -1
  211. package/dist/assets/{properties-modifiers.inspector-DsQzA638.js → properties-modifiers.inspector-BELGiG49.js} +1 -1
  212. package/dist/assets/{properties-object.inspector-C57X0GJq.js → properties-object.inspector-COLlzAo4.js} +1 -1
  213. package/dist/assets/{properties-output.inspector-DzrcFuRM.js → properties-output.inspector-DLpqLRCp.js} +1 -1
  214. package/dist/assets/{properties-particles.inspector-DGqSnelN.js → properties-particles.inspector-C8ZhuG-X.js} +1 -1
  215. package/dist/assets/{properties-physics.inspector-DIjhtiLe.js → properties-physics.inspector-DQ3fUdPx.js} +1 -1
  216. package/dist/assets/{properties-render.inspector-CX0orwn9.js → properties-render.inspector-DlPuoXQ-.js} +1 -1
  217. package/dist/assets/{properties-scene.inspector-DaHNL_sq.js → properties-scene.inspector-itnV8qGu.js} +1 -1
  218. package/dist/assets/{properties-texture.inspector-B5hmxPRy.js → properties-texture.inspector-BZbBAQ_O.js} +1 -1
  219. package/dist/assets/{properties-view-layer.inspector-C7hdJgei.js → properties-view-layer.inspector-CNOevOy3.js} +1 -1
  220. package/dist/assets/{properties-world.inspector-EWTC6jNw.js → properties-world.inspector-C7u-dAkO.js} +1 -1
  221. package/dist/assets/{r3f-design-session-CXlGV9hp.js → r3f-design-session-DbXiRez3.js} +2 -2
  222. package/dist/assets/{r3f-physics-binding-DZSIB0d_.js → r3f-physics-binding-B8Je7bNy.js} +1 -1
  223. package/dist/assets/{r3f-source-authoring-adapter-GR97EmII.js → r3f-source-authoring-adapter-CarxaAwC.js} +1 -1
  224. package/dist/assets/{react-canvas-navigation-L_mOnybL.js → react-canvas-navigation-BTVCmK_d.js} +1 -1
  225. package/dist/assets/{react-inspector.service-B4P3UnSY.js → react-inspector.service-BV9ZVls7.js} +1 -1
  226. package/dist/assets/{react-story-board-CT1_3sk_.js → react-story-board-CETbMKNG.js} +1 -1
  227. package/dist/assets/{react-three-rapier.esm-DyMNYoA_.js → react-three-rapier.esm-BQ3IBcaP.js} +1 -1
  228. package/dist/assets/{react-world-authoring-adapter-OR1SW2jI.js → react-world-authoring-adapter-DsywMgco.js} +1 -1
  229. package/dist/assets/{realm-services-u0mnjXU9.js → realm-services-DoN23Ol2.js} +1 -1
  230. package/dist/assets/{reflection-probe-capture.inspector-CTNlSaGW.js → reflection-probe-capture.inspector-D0grUDdX.js} +1 -1
  231. package/dist/assets/{registry-wqakYaDW.js → registry-CZxNkOhN.js} +1 -1
  232. package/dist/assets/{relative-import-specifier-DXWVQf0o.js → relative-import-specifier-DMppRnNH.js} +1 -1
  233. package/dist/assets/{render-env-CN09NtDf.js → render-env-BAO3Rc6G.js} +1 -1
  234. package/dist/assets/{render-features-HCEupUJ6.js → render-features-CBm-s0QE.js} +1 -1
  235. package/dist/assets/{render-settings-Clt6d05o.js → render-settings-DCZaLK-j.js} +1 -1
  236. package/dist/assets/{resolve-three-CLzKVMx2.js → resolve-three-Bwj7rkZB.js} +3 -3
  237. package/dist/assets/{root-seam-contract-D2uQHWF9.js → root-seam-contract-DMUsCBFW.js} +1 -1
  238. package/dist/assets/{run-selection-AyelFc9T.js → run-selection-DRkjJF-w.js} +1 -1
  239. package/dist/assets/{scene-capture-WbPcLfJl.js → scene-capture-DVXdql_1.js} +1 -1
  240. package/dist/assets/{scene-document.service-4U6rDhCp.js → scene-document.service-CxGQ1BN-.js} +2 -2
  241. package/dist/assets/{scene-framing-DzSL2Cld.js → scene-framing-DUd4lKtW.js} +1 -1
  242. package/dist/assets/{seeded-random-tpdJjdqy.js → seeded-random-DzZFJTfh.js} +1 -1
  243. package/dist/assets/{setup-renderer-B5vZmCyu.js → setup-renderer-DF2zFw-E.js} +1 -1
  244. package/dist/assets/{source-object3d-authoring-adapter-BQnQ3Qzg.js → source-object3d-authoring-adapter-RimvAMDG.js} +1 -1
  245. package/dist/assets/{source-persistence-backend-uyd6NiWD.js → source-persistence-backend-BRcy23H9.js} +1 -1
  246. package/dist/assets/{stage-keyboard-B2yrk2zu.js → stage-keyboard-JRZ1CNbL.js} +1 -1
  247. package/dist/assets/{stage-overlay-set-Cqtz3s06.js → stage-overlay-set-DHN11xPU.js} +1 -1
  248. package/dist/assets/{stage-presence-markers-BgLs0Aj5.js → stage-presence-markers-AQuGIbnu.js} +1 -1
  249. package/dist/assets/stage-transform-tools-B8u8n8gJ.js +1 -0
  250. package/dist/assets/{standard-viewport-dressing-uhv_3JvQ.js → standard-viewport-dressing-NHKaiGVg.js} +3 -3
  251. package/dist/assets/{state-watch.action-BJ08HgcO.js → state-watch.action-B_conRk5.js} +1 -1
  252. package/dist/assets/{state-watch.menu-rAULwijr.js → state-watch.menu-B6GNrnYH.js} +1 -1
  253. package/dist/assets/{state-watch.utility-eEbQcj8t.js → state-watch.utility-CxuUOPhl.js} +1 -1
  254. package/dist/assets/story-capture-DYXQlnlZ.js +1 -0
  255. package/dist/assets/{story-documents-Cm4jCkW5.js → story-documents-D_Tg0idt.js} +2 -2
  256. package/dist/assets/{story-documents.service-Cgtg6rXn.js → story-documents.service-DQh0SkkQ.js} +1 -1
  257. package/dist/assets/{story-documents.service-BDvQCslK.js → story-documents.service-ES8nxeNu.js} +1 -1
  258. package/dist/assets/story-media-presence-D4w-waFJ.js +1 -0
  259. package/dist/assets/{story-pixi-preview-D62Q_vIW.js → story-pixi-preview-B6GFGUQG.js} +1 -1
  260. package/dist/assets/{story-presentation-CofUdEU7.js → story-presentation-6qhUDkv4.js} +1 -1
  261. package/dist/assets/{story-three-preview-B9O1wapP.js → story-three-preview-BgLSJIlN.js} +2 -2
  262. package/dist/assets/{studio-environment-BIhqrtFc.js → studio-environment-BGU84AOG.js} +1 -1
  263. package/dist/assets/{surface-state-DrM3wsrr.js → surface-state-BtNEZzCd.js} +1 -1
  264. package/dist/assets/{syntactic-prop-specs-BGz0N7Dg.js → syntactic-prop-specs-B309atpv.js} +1 -1
  265. package/dist/assets/{team-playtest.service-CiE8UJAe.js → team-playtest.service-B8leDEE-.js} +1 -1
  266. package/dist/assets/{three-BQfhEwUz.js → three-BRZq2Px5.js} +1 -1
  267. package/dist/assets/{three-authoring-adapter-COBwREQO.js → three-authoring-adapter-DkfGlaNQ.js} +1 -1
  268. package/dist/assets/three-authoring.service-sOchR4o8.js +2 -0
  269. package/dist/assets/{three-ingest-runtime-BnRF_nJN.js → three-ingest-runtime-D9JoNRjk.js} +1 -1
  270. package/dist/assets/three-integration-D3zBmOoj.js +2 -0
  271. package/dist/assets/three-integration.service-CE8aNHob.js +1 -0
  272. package/dist/assets/{three-projection-core-PQm-CO61.js → three-projection-core-Dt5ARGlM.js} +1 -1
  273. package/dist/assets/{three-state-C7khRIQE.js → three-state-3qPk0Sq8.js} +1 -1
  274. package/dist/assets/{three-story-capture.service-B0oiGIfp.js → three-story-capture.service-B3U2L4qG.js} +2 -2
  275. package/dist/assets/three-story-model-C96x-E0V.js +1 -0
  276. package/dist/assets/{three-viewport-presentation-B_tBudM-.js → three-viewport-presentation-DGctcUkE.js} +1 -1
  277. package/dist/assets/{transform-mode-request-DWM6BdRA.js → transform-mode-request-C0cdwtJf.js} +1 -1
  278. package/dist/assets/{transport-strip-vgXGDgkI.js → transport-strip-8OsPiJIm.js} +1 -1
  279. package/dist/assets/transport.header-D1Otpit3.js +1 -0
  280. package/dist/assets/{tuples-BnYrlQqS.js → tuples-CaBWqclR.js} +1 -1
  281. package/dist/assets/ui-board-document-BMTAhmJP.js +2 -0
  282. package/dist/assets/{user-data-CifDDvSr.js → user-data-4Ls0MsfF.js} +1 -1
  283. package/dist/assets/{utility-view-state-BPl8Asup.js → utility-view-state-gIzv2Oo_.js} +1 -1
  284. package/dist/assets/{viewport-door-BCXEYmon.js → viewport-door-C4Fu0zS3.js} +1 -1
  285. package/dist/assets/{viewport-root-presentation-DJLpUuf0.js → viewport-root-presentation-DMMIH4rt.js} +1 -1
  286. package/dist/assets/{viewport-surface-status-jwLhIK_M.js → viewport-surface-status-CdI6sQko.js} +1 -1
  287. package/dist/assets/{webworkerAll-P6jePa6X.js → webworkerAll-BINEvomm.js} +1 -1
  288. package/dist/assets/worker-Cu9kIzIf.js +2 -0
  289. package/dist/assets/{workspace-arrangement-B_OcbQbj.js → workspace-arrangement-w-T2xUGF.js} +1 -1
  290. package/dist/assets/{world-documents-Bx3j867R.js → world-documents-feEUCgVE.js} +1 -1
  291. package/dist/assets/{world-root-stage-binding-LEhjWDkn.js → world-root-stage-binding-BDb6wt_C.js} +1 -1
  292. package/dist/assets/{xstate-animation-binding-Dionc_fC.js → xstate-animation-binding-Bhz1Uhhw.js} +1 -1
  293. package/dist/assets/{xstate-animation-meta-XEmD_qc_.js → xstate-animation-meta-CvDlYgrt.js} +1 -1
  294. package/dist-node/cli.js +21 -18
  295. package/package.json +16 -16
  296. package/template/src/lib/reflections/ReflectionProbe.tsx +34 -5
  297. package/template/src/lib/reflections/index.ts +1 -0
  298. package/template/src/lib/reflections/probe-controller.ts +44 -3
  299. package/template/src/lib/reflections/reflection-probe-registry.ts +32 -7
  300. package/template/src/lib/reflections/volume-reflection-material.ts +638 -160
  301. package/dist/assets/EntityModelDocument-CplmAib0.js +0 -1
  302. package/dist/assets/InspectorObjectPreview-Bjc1Tn-I.js +0 -1
  303. package/dist/assets/ModelAssetDocument-G6ajiV51.js +0 -1
  304. package/dist/assets/Object3DDocumentViewport-BhH8afDi.js +0 -2
  305. package/dist/assets/StageHost-Bor1eHEO.js +0 -2
  306. package/dist/assets/autoplay.service-DaonKf7O.js +0 -1
  307. package/dist/assets/blender-emscripten-engine-DQljvfR7.js +0 -2
  308. package/dist/assets/blender-runtime.document-DnSl0twI.js +0 -12
  309. package/dist/assets/blender-wali-engine-CO-jG_ik.js +0 -2
  310. package/dist/assets/bridge.command-D6qTcgje.js +0 -3
  311. package/dist/assets/collaboration-presence-DcPhahtF.js +0 -1
  312. package/dist/assets/component-board.service-Cd1c9oSN.js +0 -2
  313. package/dist/assets/component-verbs.service-DsxhVcvk.js +0 -1
  314. package/dist/assets/editor-api-WeHVNlEo.js +0 -1
  315. package/dist/assets/fork-menu-Bthg9viY.js +0 -1
  316. package/dist/assets/gameplay-replay-WyopKGyB.js +0 -1
  317. package/dist/assets/ibl-override-material-CMo_a49U.js +0 -6
  318. package/dist/assets/index-yXSK_qPW.js +0 -1
  319. package/dist/assets/instances.command-COw2QpbN.js +0 -1
  320. package/dist/assets/layered-pick-BUr1U8lU.js +0 -1
  321. package/dist/assets/material-B6cH1Oqw.js +0 -1
  322. package/dist/assets/network-observer.service-GFzXuBMb.js +0 -1
  323. package/dist/assets/stage-transform-tools-BHcz53td.js +0 -1
  324. package/dist/assets/story-capture-B8wfSf5s.js +0 -1
  325. package/dist/assets/story-media-presence-SGlp_afL.js +0 -1
  326. package/dist/assets/three-authoring.service-C0azvoq8.js +0 -2
  327. package/dist/assets/three-integration-DeWYFf1x.js +0 -2
  328. package/dist/assets/three-integration.service-CoWCBTmg.js +0 -1
  329. package/dist/assets/three-story-model-D7g2pqfJ.js +0 -1
  330. package/dist/assets/transport.header-CfVHXjrT.js +0 -1
  331. package/dist/assets/ui-board-document-B_-K9luq.js +0 -2
  332. package/dist/assets/worker-CJIsrsP6.js +0 -2
@@ -32,11 +32,12 @@ import {
32
32
  baseIblFunctions,
33
33
  createIblSentinelTexture,
34
34
  IBL_WORLD_POSITION,
35
- type IblOverride,
36
- type IblOverrideShader,
37
35
  overrideMaterialIbl,
38
36
  } from '@volter/threejs-runtime/render/ibl-override-material';
39
37
  import {
38
+ type Camera,
39
+ type Material,
40
+ Matrix4,
40
41
  MeshPhysicalMaterial,
41
42
  type MeshPhysicalMaterialParameters,
42
43
  MeshStandardMaterial,
@@ -47,8 +48,11 @@ import {
47
48
  Vector3,
48
49
  type WebGLRenderer,
49
50
  } from 'three';
51
+ import type { ReflectionProbeDiffuse } from './probe-controller';
50
52
  import {
51
53
  acquireReflectionProbeRegistry,
54
+ type ReflectionProbeObserver,
55
+ type ReflectionProbeRegistry,
52
56
  type ReflectionProbeRegistryLease,
53
57
  type ReflectionProbeRuntime,
54
58
  } from './reflection-probe-registry';
@@ -66,7 +70,11 @@ export interface VolumeReflectionMaterialHandle<
66
70
  > {
67
71
  /** The material this factory created. Use it like any `MeshStandardMaterial`. */
68
72
  readonly material: T;
69
- /** Releases the registry lease and the material's own GPU resources. */
73
+ /**
74
+ * Releases the registry leases and the material's own GPU resources: the
75
+ * same as `material.dispose()`, which lets go of everything this handle
76
+ * holds too.
77
+ */
70
78
  dispose(): void;
71
79
  }
72
80
 
@@ -91,15 +99,15 @@ function cubeUvSamplingFunctions(): string {
91
99
  }
92
100
 
93
101
  /**
94
- * `(texelWidth, texelHeight, maxMip)` for one PMREM atlas — three's own
95
- * `generateCubeUVSize`, which reads the installed envMap's image height, asked
96
- * per texture instead.
102
+ * `(texelWidth, texelHeight, maxMip)` for one PMREM atlas, written into
103
+ * `target` — three's own `generateCubeUVSize`, which reads the installed
104
+ * envMap's image height, asked per texture instead.
97
105
  */
98
- function atlasSizeOf(texture: Texture | null): Vector3 {
106
+ function atlasSizeInto(texture: Texture | null, target: Vector3): Vector3 {
99
107
  const height = (texture?.image as { height?: number } | undefined)?.height ?? 0;
100
- if (height <= 0) return new Vector3(0, 0, 0);
108
+ if (height <= 0) return target.set(0, 0, 0);
101
109
  const maxMip = Math.log2(height) - 2;
102
- return new Vector3(1 / (3 * Math.max(2 ** maxMip, 7 * 16)), 1 / height, maxMip);
110
+ return target.set(1 / (3 * Math.max(2 ** maxMip, 7 * 16)), 1 / height, maxMip);
103
111
  }
104
112
 
105
113
  function probeWeight(index: number): string {
@@ -124,16 +132,32 @@ function probeWeight(index: number): string {
124
132
  }`;
125
133
  }
126
134
 
127
- function probeWeightBlock(index: number): string {
135
+ /** Whether probe `index` takes part in the reflected (`radiance`) or the diffuse (`irradiance`) term. */
136
+ function probeTakesPart(index: number, term: 'radiance' | 'irradiance'): string {
137
+ return term === 'radiance'
138
+ ? `volterProbeReady[${index}] > 0.5`
139
+ : `volterProbeReady[${index}] > 0.5 && volterProbeDiffuseMode[${index}] > 0.5`;
140
+ }
141
+
142
+ function probeWeightBlock(index: number, term: 'radiance' | 'irradiance'): string {
128
143
  return `
129
- if (volterProbeReady[${index}] > 0.5) {${probeWeight(index)}
144
+ if (${probeTakesPart(index, term)}) {${probeWeight(index)}
130
145
  if (weight > 0.0) bestPriority = max(bestPriority, volterProbePriority[${index}]);
131
146
  }`;
132
147
  }
133
148
 
134
- function probeSampleBlock(index: number, roughness: string, direction: string): string {
149
+ function probeSampleBlock(index: number, term: 'radiance' | 'irradiance'): string {
150
+ const [roughness, direction] = term === 'radiance' ? ['roughness', 'worldReflect'] : ['1.0', 'worldNormal'];
151
+ const sampled = `volterTextureCubeUV(
152
+ volterProbeMap${index}, probeDirection, ${roughness}, volterProbeAtlasSize[${index}]
153
+ ).rgb`;
154
+ // The diffuse term is the capture's, or the probe's constant colour (mode 2), with its own weight.
155
+ const light =
156
+ term === 'radiance'
157
+ ? `${sampled} * volterProbeIntensity[${index}]`
158
+ : `(volterProbeDiffuseMode[${index}] > 1.5 ? volterProbeDiffuseColor[${index}] : ${sampled}) * volterProbeDiffuseIntensity[${index}]`;
135
159
  return `
136
- if (volterProbeReady[${index}] > 0.5 && abs(volterProbePriority[${index}] - bestPriority) < 0.001) {${probeWeight(index)}
160
+ if (${probeTakesPart(index, term)} && abs(volterProbePriority[${index}] - bestPriority) < 0.001) {${probeWeight(index)}
137
161
  if (weight > 0.0) {
138
162
  vec3 probeDirection = (volterProbeWorldToLocal[${index}] * vec4(${direction}, 0.0)).xyz;
139
163
  if (volterProbeParallax[${index}] > 0.5) {
@@ -146,29 +170,38 @@ function probeSampleBlock(index: number, roughness: string, direction: string):
146
170
  probeDirection = boxPos + safeDirection * nearestFace
147
171
  + volterProbeParallaxOffset[${index}] - volterProbeCaptureOffset[${index}];
148
172
  }
149
- accumulated += volterTextureCubeUV(
150
- volterProbeMap${index}, probeDirection, ${roughness}, volterProbeAtlasSize[${index}]
151
- ).rgb * volterProbeIntensity[${index}] * weight;
173
+ accumulated += ${light} * weight;
152
174
  totalWeight += weight;
153
175
  }
154
176
  }`;
155
177
  }
156
178
 
157
- function volumeReflectionShader(probeCount: number): string {
158
- const samplers = Array.from(
159
- { length: probeCount },
160
- (_, index) => `uniform sampler2D volterProbeMap${index};`,
161
- ).join('\n');
162
- const weights = Array.from({ length: probeCount }, (_, index) => probeWeightBlock(index)).join(
163
- '\n',
179
+ /**
180
+ * Three's anisotropic radiance (`getIBLAnisotropyRadiance`, which an
181
+ * anisotropic physical material calls instead of `getIBLRadiance`) bends the
182
+ * normal and samples the radiance; in the renamed base chunk it would sample
183
+ * the environment alone, so it samples the probes' radiance instead (declared
184
+ * ahead of it, defined below).
185
+ */
186
+ function anisotropyOverProbes(base: string): string {
187
+ return base.replace(
188
+ /(vec3 getIBLAnisotropyRadiance\([\s\S]*?)getBaseIBLRadiance\(/,
189
+ '$1getIBLRadiance(',
164
190
  );
165
- const radiance = Array.from({ length: probeCount }, (_, index) =>
166
- probeSampleBlock(index, 'roughness', 'worldReflect'),
167
- ).join('\n');
168
- const irradiance = Array.from({ length: probeCount }, (_, index) =>
169
- probeSampleBlock(index, '1.0', 'worldNormal'),
170
- ).join('\n');
171
- return `${baseIblFunctions()}
191
+ }
192
+
193
+ /**
194
+ * The IBL chunk for `probeCount` probes, of which the first `sampled` have a
195
+ * sampler (the rest are declared in the arrays, whose length is the program's
196
+ * cache key, and never read).
197
+ */
198
+ function volumeReflectionShader(probeCount: number, sampled: number): string {
199
+ const each = (block: (index: number) => string) =>
200
+ Array.from({ length: sampled }, (_, index) => block(index)).join('\n');
201
+ return `#ifdef USE_ENVMAP
202
+ vec3 getIBLRadiance(const in vec3 viewDir, const in vec3 normal, const in float roughness);
203
+ #endif
204
+ ${anisotropyOverProbes(baseIblFunctions())}
172
205
  ${cubeUvSamplingFunctions()}
173
206
  #ifdef USE_ENVMAP
174
207
  uniform float volterProbeReady[${probeCount}];
@@ -183,19 +216,22 @@ uniform float volterProbeRadius[${probeCount}];
183
216
  uniform float volterProbeBlendDistance[${probeCount}];
184
217
  uniform float volterProbePriority[${probeCount}];
185
218
  uniform float volterProbeIntensity[${probeCount}];
219
+ uniform float volterProbeDiffuseMode[${probeCount}];
220
+ uniform float volterProbeDiffuseIntensity[${probeCount}];
221
+ uniform vec3 volterProbeDiffuseColor[${probeCount}];
186
222
  uniform float volterProbeParallax[${probeCount}];
187
223
  uniform float volterBaseEnvironment;
188
- ${samplers}
224
+ ${each((index) => `uniform sampler2D volterProbeMap${index};`)}
189
225
 
190
226
  vec3 getIBLIrradiance(const in vec3 normal) {
191
227
  #ifdef ENVMAP_TYPE_CUBE_UV
192
228
  vec3 baseIrradiance = getBaseIBLIrradiance(normal) * volterBaseEnvironment;
193
229
  vec3 worldNormal = inverseTransformDirection(normal, viewMatrix);
194
230
  float bestPriority = -1000000.0;
195
- ${weights}
231
+ ${each((index) => probeWeightBlock(index, 'irradiance'))}
196
232
  vec3 accumulated = vec3(0.0);
197
233
  float totalWeight = 0.0;
198
- ${irradiance}
234
+ ${each((index) => probeSampleBlock(index, 'irradiance'))}
199
235
  return totalWeight > 0.0
200
236
  ? PI * accumulated / totalWeight * envMapIntensity
201
237
  : baseIrradiance;
@@ -211,10 +247,10 @@ vec3 getIBLRadiance(const in vec3 viewDir, const in vec3 normal, const in float
211
247
  reflected = normalize(mix(reflected, normal, roughness * roughness));
212
248
  vec3 worldReflect = inverseTransformDirection(reflected, viewMatrix);
213
249
  float bestPriority = -1000000.0;
214
- ${weights}
250
+ ${each((index) => probeWeightBlock(index, 'radiance'))}
215
251
  vec3 accumulated = vec3(0.0);
216
252
  float totalWeight = 0.0;
217
- ${radiance}
253
+ ${each((index) => probeSampleBlock(index, 'radiance'))}
218
254
  return totalWeight > 0.0
219
255
  ? accumulated / totalWeight * envMapIntensity
220
256
  : baseRadiance;
@@ -225,72 +261,194 @@ ${radiance}
225
261
  #endif`;
226
262
  }
227
263
 
228
- function writeProbeUniforms(
229
- shader: IblOverrideShader | null,
264
+ /** Every uniform the chunk declares, by name: one object each, for the material's whole life. */
265
+ type ProbeUniforms = Readonly<Record<string, { value: unknown }>>;
266
+
267
+ const FLOAT_UNIFORMS = [
268
+ 'volterProbeReady',
269
+ 'volterProbeShape',
270
+ 'volterProbeRadius',
271
+ 'volterProbeBlendDistance',
272
+ 'volterProbePriority',
273
+ 'volterProbeIntensity',
274
+ 'volterProbeDiffuseMode',
275
+ 'volterProbeDiffuseIntensity',
276
+ 'volterProbeParallax',
277
+ ] as const;
278
+
279
+ const VECTOR_UNIFORMS = [
280
+ 'volterProbeExtents',
281
+ 'volterProbeParallaxExtents',
282
+ 'volterProbeParallaxOffset',
283
+ 'volterProbeCaptureOffset',
284
+ 'volterProbeAtlasSize',
285
+ 'volterProbeDiffuseColor',
286
+ ] as const;
287
+
288
+ function createProbeUniforms(): ProbeUniforms {
289
+ const uniforms: Record<string, { value: unknown }> = {
290
+ volterBaseEnvironment: { value: 0 },
291
+ volterProbeWorldToLocal: { value: null },
292
+ };
293
+ for (const name of [...FLOAT_UNIFORMS, ...VECTOR_UNIFORMS]) uniforms[name] = { value: null };
294
+ for (let index = 0; index < MAX_PROBES; index += 1) uniforms[`volterProbeMap${index}`] = { value: null };
295
+ return uniforms;
296
+ }
297
+
298
+ /**
299
+ * One binding's probe values, allocated once and rewritten in place. Every
300
+ * array is {@link MAX_PROBES} long whatever the program declares, the entries
301
+ * past the selected probes neutral (not ready, identity, zero), so a program
302
+ * compiled for more probes than are selected still uploads in full and
303
+ * lights nothing from the extra ones.
304
+ */
305
+ interface ProbeValues {
306
+ readonly floats: Readonly<Record<(typeof FLOAT_UNIFORMS)[number], Float32Array>>;
307
+ readonly vectors: Readonly<Record<(typeof VECTOR_UNIFORMS)[number], readonly Vector3[]>>;
308
+ readonly worldToLocal: readonly Matrix4[];
309
+ readonly maps: (Texture | null)[];
310
+ }
311
+
312
+ function createProbeValues(): ProbeValues {
313
+ const floats = {} as Record<(typeof FLOAT_UNIFORMS)[number], Float32Array>;
314
+ for (const name of FLOAT_UNIFORMS) floats[name] = new Float32Array(MAX_PROBES);
315
+ const vectors = {} as Record<(typeof VECTOR_UNIFORMS)[number], readonly Vector3[]>;
316
+ for (const name of VECTOR_UNIFORMS) vectors[name] = Array.from({ length: MAX_PROBES }, () => new Vector3());
317
+ return {
318
+ floats,
319
+ vectors,
320
+ worldToLocal: Array.from({ length: MAX_PROBES }, () => new Matrix4()),
321
+ maps: Array.from({ length: MAX_PROBES }, () => null),
322
+ };
323
+ }
324
+
325
+ /** The probe's diffuse term: 0 none, 1 its capture, 2 its constant colour. */
326
+ function diffuseModeOf(config: ReflectionProbeDiffuse): number {
327
+ if (config.diffuse === 'none') return 0;
328
+ return config.diffuse === 'color' ? 2 : 1;
329
+ }
330
+
331
+ /** Rewrite `values` in place for `probes`, padding the rest with neutral entries. */
332
+ function fillProbeValues(
333
+ values: ProbeValues,
230
334
  probes: readonly ReflectionProbeRuntime[],
231
- baseEnvironment: boolean,
232
335
  capturing: boolean,
233
336
  ): void {
234
- if (!shader) return;
235
- const values = <T>(name: string, value: T) => {
236
- shader.uniforms[name] = { value };
337
+ const { floats, vectors } = values;
338
+ for (let index = 0; index < MAX_PROBES; index += 1) {
339
+ const probe = probes[index];
340
+ if (!probe) {
341
+ for (const name of FLOAT_UNIFORMS) floats[name][index] = 0;
342
+ for (const name of VECTOR_UNIFORMS) vectors[name][index]!.set(0, 0, 0);
343
+ values.worldToLocal[index]!.identity();
344
+ values.maps[index] = null;
345
+ continue;
346
+ }
347
+ const config = probe.mark.config as typeof probe.mark.config & ReflectionProbeDiffuse;
348
+ floats.volterProbeReady[index] = Number(probe.ready && !capturing);
349
+ floats.volterProbeShape[index] = Number(config.shape === 'sphere');
350
+ floats.volterProbeRadius[index] = Math.max(0.01, config.radius);
351
+ floats.volterProbeBlendDistance[index] = Math.max(0, config.blendDistance);
352
+ floats.volterProbePriority[index] = config.priority;
353
+ floats.volterProbeIntensity[index] = Math.max(0, config.intensity);
354
+ floats.volterProbeDiffuseMode[index] = diffuseModeOf(config);
355
+ floats.volterProbeDiffuseIntensity[index] = Math.max(0, config.diffuseIntensity ?? config.intensity);
356
+ floats.volterProbeParallax[index] = Number(config.parallaxProjection);
357
+ vectors.volterProbeExtents[index]!.copy(probe.extents);
358
+ vectors.volterProbeParallaxExtents[index]!.copy(probe.parallaxExtents);
359
+ vectors.volterProbeParallaxOffset[index]!.copy(probe.parallaxOffset);
360
+ vectors.volterProbeCaptureOffset[index]!.copy(probe.captureOffset);
361
+ atlasSizeInto(probe.texture, vectors.volterProbeAtlasSize[index]!);
362
+ const color = config.diffuseColor;
363
+ vectors.volterProbeDiffuseColor[index]!.set(color?.[0] ?? 0, color?.[1] ?? 0, color?.[2] ?? 0);
364
+ values.worldToLocal[index]!.copy(probe.worldToLocal);
365
+ values.maps[index] = probe.texture;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Point the material's uniform objects at `values`. Only `.value` changes:
371
+ * every program compiled for the material, in any renderer, holds these same
372
+ * objects, so the next draw uploads what they point at.
373
+ */
374
+ function pointProbeUniforms(uniforms: ProbeUniforms, values: ProbeValues): void {
375
+ const set = (name: string, value: unknown) => {
376
+ (uniforms[name] as { value: unknown }).value = value;
237
377
  };
238
- values(
239
- 'volterProbeReady',
240
- probes.map((probe) => Number(probe.ready && !capturing)),
241
- );
242
- values(
243
- 'volterProbeWorldToLocal',
244
- probes.map((probe) => probe.worldToLocal),
245
- );
246
- values(
247
- 'volterProbeExtents',
248
- probes.map((probe) => probe.extents),
249
- );
250
- values(
251
- 'volterProbeParallaxExtents',
252
- probes.map((probe) => probe.parallaxExtents),
253
- );
254
- values(
255
- 'volterProbeParallaxOffset',
256
- probes.map((probe) => probe.parallaxOffset),
257
- );
258
- values(
259
- 'volterProbeCaptureOffset',
260
- probes.map((probe) => probe.captureOffset),
261
- );
262
- values(
263
- 'volterProbeAtlasSize',
264
- probes.map((probe) => atlasSizeOf(probe.texture)),
265
- );
266
- values(
267
- 'volterProbeShape',
268
- probes.map((probe) => Number(probe.mark.config.shape === 'sphere')),
269
- );
270
- values(
271
- 'volterProbeRadius',
272
- probes.map((probe) => Math.max(0.01, probe.mark.config.radius)),
273
- );
274
- values(
275
- 'volterProbeBlendDistance',
276
- probes.map((probe) => Math.max(0, probe.mark.config.blendDistance)),
277
- );
278
- values(
279
- 'volterProbePriority',
280
- probes.map((probe) => probe.mark.config.priority),
281
- );
282
- values(
283
- 'volterProbeIntensity',
284
- probes.map((probe) => Math.max(0, probe.mark.config.intensity)),
285
- );
286
- values(
287
- 'volterProbeParallax',
288
- probes.map((probe) => Number(probe.mark.config.parallaxProjection)),
378
+ for (const name of FLOAT_UNIFORMS) set(name, values.floats[name]);
379
+ for (const name of VECTOR_UNIFORMS) set(name, values.vectors[name]);
380
+ set('volterProbeWorldToLocal', values.worldToLocal);
381
+ for (let index = 0; index < MAX_PROBES; index += 1) set(`volterProbeMap${index}`, values.maps[index]);
382
+ }
383
+
384
+ /** Texture units a program three compiles with `parameters` binds before any probe's. */
385
+ const PARAMETER_TEXTURES = [
386
+ 'map',
387
+ 'matcap',
388
+ 'envMap',
389
+ 'lightMap',
390
+ 'aoMap',
391
+ 'bumpMap',
392
+ 'normalMap',
393
+ 'displacementMap',
394
+ 'emissiveMap',
395
+ 'metalnessMap',
396
+ 'roughnessMap',
397
+ 'anisotropyMap',
398
+ 'clearcoatMap',
399
+ 'clearcoatNormalMap',
400
+ 'clearcoatRoughnessMap',
401
+ 'iridescenceMap',
402
+ 'iridescenceThicknessMap',
403
+ 'sheenColorMap',
404
+ 'sheenRoughnessMap',
405
+ 'specularMap',
406
+ 'specularColorMap',
407
+ 'specularIntensityMap',
408
+ 'transmission',
409
+ 'transmissionMap',
410
+ 'thicknessMap',
411
+ 'gradientMap',
412
+ 'alphaMap',
413
+ 'skinning',
414
+ 'instancingMorph',
415
+ 'batchingColor',
416
+ ] as const;
417
+
418
+ function texturesBeforeProbes(parameters: Readonly<Record<string, unknown>>): number {
419
+ const count = (name: string) => {
420
+ const value = parameters[name];
421
+ return typeof value === 'number' ? value : 0;
422
+ };
423
+ return (
424
+ PARAMETER_TEXTURES.filter((name) => Boolean(parameters[name])).length +
425
+ // A batched mesh's matrices and ids, a geometry's morph targets.
426
+ (parameters['batching'] ? 2 : 0) +
427
+ (count('morphTargetsCount') > 0 ? 1 : 0) +
428
+ // A rect area light's two LTC lookup textures.
429
+ (count('numRectAreaLights') > 0 ? 2 : 0) +
430
+ count('numDirLightShadows') +
431
+ count('numPointLightShadows') +
432
+ count('numSpotLightShadows') +
433
+ count('numSpotLightMaps')
289
434
  );
290
- values('volterBaseEnvironment', baseEnvironment ? 1 : 0);
291
- probes.forEach((probe, index) => {
292
- values(`volterProbeMap${index}`, probe.texture);
293
- });
435
+ }
436
+
437
+ /** Warned once each: a registry with more probes than a material blends, a renderer out of units. */
438
+ const warnedRegistries = new WeakSet<ReflectionProbeRegistry>();
439
+ const warnedRenderers = new WeakSet<WebGLRenderer>();
440
+
441
+ /** The parameters and, when given, the renderer and scene of either factory's two forms. */
442
+ function splitArguments<O>(
443
+ first: WebGLRenderer | O | undefined,
444
+ scene: Scene | undefined,
445
+ options: O | undefined,
446
+ ): { readonly bound: Binding | null; readonly options: O | undefined } {
447
+ const renderer = first as WebGLRenderer | undefined;
448
+ if (typeof renderer?.render === 'function' && scene !== undefined) {
449
+ return { bound: { renderer, scene }, options };
450
+ }
451
+ return { bound: null, options: first as O | undefined };
294
452
  }
295
453
 
296
454
  /**
@@ -300,21 +458,66 @@ function writeProbeUniforms(
300
458
  * The material is this factory's: no existing material is mutated, and only
301
459
  * meshes the author assigns it to are affected. Assigning `envMap` later works
302
460
  * as usual and takes over the fallback term — the black texture that keeps
303
- * three's IBL entry points compiled when there is no environment at all is
304
- * installed behind that property and never replaces an assigned map.
461
+ * three's IBL entry points compiled when probes apply and there is no
462
+ * environment at all is installed behind that property and never replaces an
463
+ * assigned map. Where the scene has no probes the material draws as three's
464
+ * own.
465
+ *
466
+ * `onBeforeCompile`, `customProgramCacheKey` and `onBeforeRender` are the
467
+ * material's own hooks, and compose as three's hooks do: reading one returns
468
+ * exactly what was last assigned (the material's own before that), and a hook
469
+ * that wraps the value it read and calls it keeps the volume lighting, however
470
+ * many wrap it in turn. The probe chunk is spliced once per program, where the
471
+ * chain reaches the material's own hook; with no probes to light nothing is
472
+ * spliced, so a hook that takes three's IBL chunk for itself still finds it.
473
+ * A hook assigned without calling the one it replaces replaces the volume
474
+ * lighting too, as on any three material.
475
+ *
476
+ * Given no `renderer` and `scene` — a material made before either exists, as
477
+ * a module-level resource is — the material reads the probes of whichever
478
+ * renderer and scene draw it, from their first draw (three's
479
+ * `Material.onBeforeRender`, which runs before that draw compiles its
480
+ * program). It keeps one registry lease per renderer and scene that has drawn
481
+ * it, so two renderers drawing it (an editor viewport and a game view, a
482
+ * throwaway preview renderer) each keep their own probes and never evict each
483
+ * other's; before each draw the material's uniform values are those of the
484
+ * drawing renderer's probes. Given a `renderer` and `scene`, it reads that
485
+ * pair's probes wherever it is drawn. It lets go of every lease when it is
486
+ * disposed (`material.dispose()` or the handle's), taking them up again if it
487
+ * is drawn after that, or when it is garbage collected: whoever holds it owns
488
+ * it as they own any three material.
489
+ *
490
+ * With more probes in the scene than `maxProbes` (at most eight), the ones
491
+ * nearest the camera are blended, reselected as it moves, and the registry
492
+ * warns once. A program blends no more probes than the texture units the
493
+ * renderer has left once the material's maps, environment, shadow maps and
494
+ * area-light tables are bound (`capabilities.maxTextures`); the farthest are
495
+ * dropped from that program, and the renderer warns once.
496
+ *
497
+ * A program compiled without a draw (`renderer.compile`) takes the probe count
498
+ * of the binding that drew last; the renderer's next draw recompiles it if its
499
+ * own probes differ, and until then the program's extra probe slots light
500
+ * nothing.
501
+ *
502
+ * `clone()` and `toJSON()` see the author's `envMap` (null if none), never the
503
+ * black texture that stands in for it while probes apply.
305
504
  */
505
+ export function createVolumeReflectionMaterial(
506
+ options?: VolumeReflectionMaterialOptions,
507
+ ): VolumeReflectionMaterialHandle;
306
508
  export function createVolumeReflectionMaterial(
307
509
  renderer: WebGLRenderer,
308
510
  scene: Scene,
309
- options: VolumeReflectionMaterialOptions = {},
511
+ options?: VolumeReflectionMaterialOptions,
512
+ ): VolumeReflectionMaterialHandle;
513
+ export function createVolumeReflectionMaterial(
514
+ first?: WebGLRenderer | VolumeReflectionMaterialOptions,
515
+ scene?: Scene,
516
+ options?: VolumeReflectionMaterialOptions,
310
517
  ): VolumeReflectionMaterialHandle {
311
- const { maxProbes, ...parameters } = options;
312
- return ownVolumeReflectionMaterial(
313
- renderer,
314
- scene,
315
- new MeshStandardMaterial(parameters),
316
- maxProbes,
317
- );
518
+ const split = splitArguments(first, scene, options);
519
+ const { maxProbes, ...parameters } = split.options ?? {};
520
+ return ownVolumeReflectionMaterial(new MeshStandardMaterial(parameters), maxProbes, split.bound);
318
521
  }
319
522
 
320
523
  export interface VolumeReflectionPhysicalMaterialOptions extends MeshPhysicalMaterialParameters {
@@ -322,95 +525,370 @@ export interface VolumeReflectionPhysicalMaterialOptions extends MeshPhysicalMat
322
525
  }
323
526
 
324
527
  /** The same explicit volume effect with Three's physical material controls. */
528
+ export function createVolumeReflectionPhysicalMaterial(
529
+ options?: VolumeReflectionPhysicalMaterialOptions,
530
+ ): VolumeReflectionMaterialHandle<MeshPhysicalMaterial>;
325
531
  export function createVolumeReflectionPhysicalMaterial(
326
532
  renderer: WebGLRenderer,
327
533
  scene: Scene,
328
- options: VolumeReflectionPhysicalMaterialOptions = {},
534
+ options?: VolumeReflectionPhysicalMaterialOptions,
535
+ ): VolumeReflectionMaterialHandle<MeshPhysicalMaterial>;
536
+ export function createVolumeReflectionPhysicalMaterial(
537
+ first?: WebGLRenderer | VolumeReflectionPhysicalMaterialOptions,
538
+ scene?: Scene,
539
+ options?: VolumeReflectionPhysicalMaterialOptions,
329
540
  ): VolumeReflectionMaterialHandle<MeshPhysicalMaterial> {
330
- const { maxProbes, ...parameters } = options;
331
- return ownVolumeReflectionMaterial(
332
- renderer,
333
- scene,
334
- new MeshPhysicalMaterial(parameters),
335
- maxProbes,
336
- );
541
+ const split = splitArguments(first, scene, options);
542
+ const { maxProbes, ...parameters } = split.options ?? {};
543
+ return ownVolumeReflectionMaterial(new MeshPhysicalMaterial(parameters), maxProbes, split.bound);
544
+ }
545
+
546
+ interface Binding {
547
+ readonly renderer: WebGLRenderer;
548
+ readonly scene: Scene;
549
+ }
550
+
551
+ /** One renderer and scene's probes, as this material reads them. */
552
+ interface ProbeBinding extends Binding {
553
+ readonly lease: ReflectionProbeRegistryLease;
554
+ /** Held here, so it lives exactly as long as the binding; the registry reaches it weakly. */
555
+ readonly observer: ReflectionProbeObserver;
556
+ stop: () => void;
557
+ probes: readonly ReflectionProbeRuntime[];
558
+ capturing: boolean;
559
+ /** The probes blended, nearest the camera first; its length is the program's probe count. */
560
+ readonly selected: ReflectionProbeRuntime[];
561
+ /** The registry reported, or the selection changed, since `values` were last filled. */
562
+ dirty: boolean;
563
+ readonly camera: Vector3;
564
+ /** This binding's uniform values, rewritten in place. */
565
+ readonly values: ProbeValues;
566
+ /** Scratch for selection: the nearest probes so far and their distances. */
567
+ readonly nearest: (ReflectionProbeRuntime | undefined)[];
568
+ readonly nearestDistance: Float64Array;
569
+ }
570
+
571
+ /**
572
+ * Observe `registry` through a weak reference to `observer`: once the
573
+ * binding that holds the observer is collected (with the material, or with
574
+ * its renderer or scene), the next update stops observing and releases the
575
+ * lease. Module-level on purpose — a closure made inside the material's own
576
+ * scope would hold that scope, and the material with it, from the registry.
577
+ */
578
+ function observeWeakly(
579
+ lease: ReflectionProbeRegistryLease,
580
+ observer: WeakRef<ReflectionProbeObserver>,
581
+ ): () => void {
582
+ let stop = (): void => undefined;
583
+ stop = lease.registry.observe((probes, capturing) => {
584
+ const live = observer.deref();
585
+ if (live) {
586
+ live(probes, capturing);
587
+ return;
588
+ }
589
+ stop();
590
+ lease.release();
591
+ });
592
+ return stop;
593
+ }
594
+
595
+ /** How far the camera is from a probe's volume: 0 inside it. */
596
+ function distanceToProbe(probe: ReflectionProbeRuntime, camera: Vector3, local: Vector3): number {
597
+ local.copy(camera).applyMatrix4(probe.worldToLocal);
598
+ if (probe.mark.config.shape === 'sphere') return Math.max(0, local.length() - probe.mark.config.radius);
599
+ const dx = Math.max(0, Math.abs(local.x) - probe.extents.x);
600
+ const dy = Math.max(0, Math.abs(local.y) - probe.extents.y);
601
+ const dz = Math.max(0, Math.abs(local.z) - probe.extents.z);
602
+ return Math.hypot(dx, dy, dz);
603
+ }
604
+
605
+ /** The chunks the probe splice reads from three's program, or the first missing. */
606
+ function missingChunk(shader: { vertexShader: string; fragmentShader: string }): string | null {
607
+ if (!shader.vertexShader.includes('#include <worldpos_vertex>')) return '#include <worldpos_vertex>';
608
+ if (!shader.fragmentShader.includes('#include <envmap_physical_pars_fragment>')) {
609
+ return '#include <envmap_physical_pars_fragment>';
610
+ }
611
+ return null;
612
+ }
613
+
614
+ type CompileHook = Material['onBeforeCompile'];
615
+ type RenderHook = Material['onBeforeRender'];
616
+
617
+ /**
618
+ * Make `name` an accessor that reads back exactly what was last assigned, or
619
+ * `own` before anything was (or once `own` is assigned back). Three calls
620
+ * what it reads, so a hook that wraps the value it read chains to `own`.
621
+ */
622
+ function ownHook<F>(material: Material, name: string, own: F): void {
623
+ let assigned: F | null = null;
624
+ Object.defineProperty(material, name, {
625
+ configurable: true,
626
+ enumerable: true,
627
+ get: () => assigned ?? own,
628
+ set: (value: F) => {
629
+ assigned = value === own ? null : value;
630
+ },
631
+ });
337
632
  }
338
633
 
339
634
  function ownVolumeReflectionMaterial<T extends MeshStandardMaterial>(
340
- renderer: WebGLRenderer,
341
- scene: Scene,
342
635
  material: T,
343
636
  maxProbes: number | undefined,
637
+ bound: Binding | null,
344
638
  ): VolumeReflectionMaterialHandle<T> {
345
639
  const limit = Math.min(MAX_PROBES, Math.max(1, Math.round(maxProbes ?? MAX_PROBES)));
346
640
  const sentinel = createIblSentinelTexture();
641
+ const uniforms = createProbeUniforms();
347
642
  let authoredEnvMap = material.envMap;
348
- let override: IblOverride | null = null;
349
- let installedCount = 0;
350
- let warnedOverflow = false;
351
- let eligible: readonly ReflectionProbeRuntime[] = [];
352
- let isCapturing = false;
353
- let lease: ReflectionProbeRegistryLease | null = acquireReflectionProbeRegistry(renderer, scene);
643
+ let bindings = new WeakMap<WebGLRenderer, WeakMap<Scene, ProbeBinding>>();
644
+ // Every binding made, weakly, so disposing can release their leases.
645
+ let made = new Set<WeakRef<ProbeBinding>>();
646
+ let pinned: ProbeBinding | null = null;
647
+ /** The binding of the draw in progress: the program three compiles next is its. */
648
+ let current: ProbeBinding | null = null;
649
+ /** The binding whose values the uniform objects point at. */
650
+ let written: ProbeBinding | null = null;
651
+ /** Per renderer, the probe count of the program it last resolved for this material. */
652
+ let resolved = new WeakMap<WebGLRenderer, number>();
653
+ /** The samplers the program being compiled declares (its texture units allowing). */
654
+ let sampled = 0;
655
+ /** Inside `clone()`/`toJSON()`, which see the author's envMap. */
656
+ let plain = 0;
657
+ let warnedChunk = false;
658
+ const cameraPosition = new Vector3();
659
+ const scratch = new Vector3();
660
+
661
+ const probeCount = () => current?.selected.length ?? 0;
354
662
 
355
663
  // Three deletes both IBL entry points from a program with no environment at
356
- // all, so a probe-only material needs one native input present to keep them.
357
- // The author's own value always wins, and `scene.environment` is left to
358
- // three whenever it exists.
359
- const hasBaseEnvironment = () => Boolean(authoredEnvMap ?? scene.environment);
664
+ // all, so a material that probes apply to needs one native input present to
665
+ // keep them. The author's own value always wins, and `scene.environment` is
666
+ // left to three whenever it exists.
360
667
  Object.defineProperty(material, 'envMap', {
361
668
  configurable: true,
362
669
  enumerable: true,
363
- get: () => authoredEnvMap ?? (scene.environment ? null : sentinel),
670
+ get: () =>
671
+ authoredEnvMap ??
672
+ (plain > 0 || probeCount() === 0 || current?.scene.environment ? null : sentinel),
364
673
  set: (value: Texture | null) => {
365
674
  authoredEnvMap = value;
366
675
  },
367
676
  });
677
+ const prototype = Object.getPrototypeOf(material) as T;
678
+ const plainly = <R>(run: () => R): R => {
679
+ plain += 1;
680
+ try {
681
+ return run();
682
+ } finally {
683
+ plain -= 1;
684
+ }
685
+ };
686
+ Object.defineProperty(material, 'clone', {
687
+ configurable: true,
688
+ writable: true,
689
+ value: () => plainly(() => prototype.clone.call(material)),
690
+ });
691
+ Object.defineProperty(material, 'toJSON', {
692
+ configurable: true,
693
+ writable: true,
694
+ value: (meta?: Parameters<T['toJSON']>[0]) => plainly(() => prototype.toJSON.call(material, meta)),
695
+ });
368
696
 
369
- const uninstall = () => {
370
- override?.restore();
371
- override = null;
372
- installedCount = 0;
697
+ const bind = (renderer: WebGLRenderer, scene: Scene): ProbeBinding => {
698
+ const lease = acquireReflectionProbeRegistry(renderer, scene);
699
+ const binding: ProbeBinding = {
700
+ renderer,
701
+ scene,
702
+ lease,
703
+ observer: (probes, capturing) => {
704
+ binding.probes = probes;
705
+ binding.capturing = capturing;
706
+ binding.dirty = true;
707
+ if (probes.length > limit && !warnedRegistries.has(lease.registry)) {
708
+ warnedRegistries.add(lease.registry);
709
+ // biome-ignore lint/suspicious/noConsole: the uniform arrays have a fixed ceiling; say that probes fell off it.
710
+ console.warn(
711
+ `[reflections] a volume reflection material blends at most ${limit} probes; this scene has ${probes.length}, so the ${limit} nearest the camera are blended.`,
712
+ );
713
+ }
714
+ },
715
+ stop: () => undefined,
716
+ probes: [],
717
+ capturing: false,
718
+ selected: [],
719
+ dirty: true,
720
+ camera: new Vector3(Number.NaN, 0, 0),
721
+ values: createProbeValues(),
722
+ nearest: Array.from({ length: MAX_PROBES }, () => undefined),
723
+ nearestDistance: new Float64Array(MAX_PROBES),
724
+ };
725
+ binding.stop = observeWeakly(lease, new WeakRef(binding.observer));
726
+ for (const reference of made) if (!reference.deref()) made.delete(reference);
727
+ made.add(new WeakRef(binding));
728
+ return binding;
373
729
  };
374
730
 
375
- const install = (count: number) => {
376
- uninstall();
377
- installedCount = count;
378
- override = overrideMaterialIbl(material, {
379
- fragment: () => volumeReflectionShader(count),
380
- cacheKey: () => `volter-volume-reflections:${count}`,
381
- onCompile: (shader) =>
382
- writeProbeUniforms(shader, eligible, hasBaseEnvironment(), isCapturing),
383
- });
731
+ const bindingFor = (renderer: WebGLRenderer, scene: Scene): ProbeBinding => {
732
+ // Made at construction, and again at a draw after a dispose.
733
+ if (bound) {
734
+ pinned ??= bind(bound.renderer, bound.scene);
735
+ return pinned;
736
+ }
737
+ let scenes = bindings.get(renderer);
738
+ if (!scenes) {
739
+ scenes = new WeakMap();
740
+ bindings.set(renderer, scenes);
741
+ }
742
+ let binding = scenes.get(scene);
743
+ if (!binding) {
744
+ binding = bind(renderer, scene);
745
+ scenes.set(scene, binding);
746
+ }
747
+ return binding;
748
+ };
749
+
750
+ /**
751
+ * The `limit` probes nearest the camera, nearest first (a program short of
752
+ * texture units samples the first ones), chosen without allocating: again
753
+ * whenever the registry reports, and as the camera moves.
754
+ */
755
+ const select = (binding: ProbeBinding, camera: Camera) => {
756
+ // A capture window zeroes every probe: nothing to choose between.
757
+ if (binding.capturing) return;
758
+ cameraPosition.setFromMatrixPosition(camera.matrixWorld);
759
+ const moved = !cameraPosition.equals(binding.camera);
760
+ if (!binding.dirty && !(moved && binding.probes.length > 1)) return;
761
+ binding.camera.copy(cameraPosition);
762
+ const { nearest, nearestDistance: distance } = binding;
763
+ let count = 0;
764
+ for (const probe of binding.probes) {
765
+ const d = binding.probes.length === 1 ? 0 : distanceToProbe(probe, cameraPosition, scratch);
766
+ if (count === limit && d >= distance[count - 1]!) continue;
767
+ let slot = count < limit ? count++ : count - 1;
768
+ for (; slot > 0 && distance[slot - 1]! > d; slot -= 1) {
769
+ distance[slot] = distance[slot - 1]!;
770
+ nearest[slot] = nearest[slot - 1];
771
+ }
772
+ distance[slot] = d;
773
+ nearest[slot] = probe;
774
+ }
775
+ const selected = binding.selected;
776
+ let same = count === selected.length;
777
+ for (let index = 0; same && index < count; index += 1) same = nearest[index] === selected[index];
778
+ if (!same) {
779
+ selected.length = count;
780
+ for (let index = 0; index < count; index += 1) selected[index] = nearest[index]!;
781
+ binding.dirty = true;
782
+ }
783
+ nearest.fill(undefined);
784
+ };
785
+
786
+ // Before every draw, in whichever renderer: its probes are the ones the
787
+ // uniform objects point at for this draw. Three uploads a material's
788
+ // uniforms whenever its program or the material drawn changes, and at the
789
+ // start of every render, so a value written here reaches this draw.
790
+ const render: RenderHook = (renderer, scene, camera) => {
791
+ const binding = bindingFor(renderer, scene);
792
+ current = binding;
793
+ select(binding, camera);
794
+ // The program this renderer resolved last may be for another binding's
795
+ // count (one compiled by `renderer.compile`, which draws nothing and so
796
+ // never comes here first): have three resolve it again.
797
+ const count = resolved.get(renderer);
798
+ if (count !== undefined && count !== binding.selected.length) material.needsUpdate = true;
799
+ if (binding.dirty) {
800
+ fillProbeValues(binding.values, binding.selected, binding.capturing);
801
+ binding.dirty = false;
802
+ written = null;
803
+ }
804
+ if (written !== binding) {
805
+ pointProbeUniforms(uniforms, binding.values);
806
+ written = binding;
807
+ }
808
+ (uniforms['volterBaseEnvironment'] as { value: unknown }).value = (authoredEnvMap ?? scene.environment) ? 1 : 0;
384
809
  };
385
810
 
386
- const unobserve = lease.registry.observe(
387
- (probes: readonly ReflectionProbeRuntime[], capturing: boolean) => {
388
- isCapturing = capturing;
389
- eligible = probes.slice(0, limit);
390
- if (probes.length > limit && !warnedOverflow) {
391
- warnedOverflow = true;
392
- // biome-ignore lint/suspicious/noConsole: the uniform arrays have a fixed ceiling; say which probes fell off it.
811
+ // The IBL override goes on ONCE, for the material's whole life: its
812
+ // fragment and cache key are read at compile time from the drawing
813
+ // binding, and every program it compiles holds the same uniform objects.
814
+ overrideMaterialIbl(material, {
815
+ fragment: () =>
816
+ probeCount() === 0
817
+ ? ShaderChunk.envmap_physical_pars_fragment
818
+ : volumeReflectionShader(probeCount(), sampled),
819
+ cacheKey: () => `volter-volume-reflections:${probeCount()}`,
820
+ onCompile: (shader) => {
821
+ Object.assign(shader.uniforms, uniforms);
822
+ },
823
+ });
824
+ const volumeCompile: CompileHook = material.onBeforeCompile;
825
+ const volumeKey = material.customProgramCacheKey;
826
+
827
+ // Where a chain of hooks reaches the material's own: the probe splice, once
828
+ // per program three compiles (a wrapper that calls it twice splices once).
829
+ const spliced = new WeakSet<object>();
830
+ const compile: CompileHook = (shader, renderer) => {
831
+ if (spliced.has(shader)) return;
832
+ spliced.add(shader);
833
+ const count = probeCount();
834
+ resolved.set(renderer, count);
835
+ // No probes: three's own program, whose chunks a later hook may still take.
836
+ if (count === 0) return;
837
+ const missing = missingChunk(shader);
838
+ if (missing !== null) {
839
+ if (!warnedChunk) {
840
+ warnedChunk = true;
841
+ // biome-ignore lint/suspicious/noConsole: a hook took a chunk the probe splice needs; say why no probe lights this material.
393
842
  console.warn(
394
- `[reflections] "${material.name || 'volume reflection material'}" blends at most ${limit} probes; ${probes.length - limit} more are in the scene and are not sampled.`,
843
+ `[reflections] volume reflection material "${material.name}" draws without its probes: an onBeforeCompile hook replaced ${missing}, which the probe lighting is spliced into.`,
395
844
  );
396
845
  }
397
- if (eligible.length !== installedCount) {
398
- if (eligible.length === 0) uninstall();
399
- else install(eligible.length);
400
- }
401
- writeProbeUniforms(override?.shader ?? null, eligible, hasBaseEnvironment(), capturing);
402
- },
403
- );
846
+ return;
847
+ }
848
+ const free = renderer.capabilities.maxTextures - texturesBeforeProbes(shader as unknown as Record<string, unknown>);
849
+ sampled = Math.max(0, Math.min(count, free));
850
+ if (sampled < count && !warnedRenderers.has(renderer)) {
851
+ warnedRenderers.add(renderer);
852
+ // biome-ignore lint/suspicious/noConsole: a GPU's texture units bound how many probes one program can sample.
853
+ console.warn(
854
+ `[reflections] a volume reflection material samples ${sampled} of its ${count} probes: this GPU has ${renderer.capabilities.maxTextures} texture units and the material's maps and shadow maps use the rest. The probes farthest from the camera are left out.`,
855
+ );
856
+ }
857
+ volumeCompile.call(material, shader, renderer);
858
+ };
859
+ // Three asks for the key each time it resolves a program: during a draw,
860
+ // for the drawing renderer.
861
+ const cacheKey = (): string => {
862
+ if (current) resolved.set(current.renderer, probeCount());
863
+ return volumeKey.call(material);
864
+ };
865
+ ownHook(material, 'onBeforeCompile', compile);
866
+ ownHook(material, 'customProgramCacheKey', cacheKey);
867
+ ownHook(material, 'onBeforeRender', render);
868
+
869
+ const release = () => {
870
+ for (const reference of made) {
871
+ const binding = reference.deref();
872
+ if (!binding) continue;
873
+ binding.stop();
874
+ binding.lease.release();
875
+ }
876
+ made = new Set();
877
+ bindings = new WeakMap();
878
+ resolved = new WeakMap();
879
+ pinned = null;
880
+ current = null;
881
+ written = null;
882
+ sentinel.dispose();
883
+ };
884
+ material.addEventListener('dispose', release);
885
+
886
+ if (bound) pinned = bind(bound.renderer, bound.scene);
404
887
 
405
888
  return {
406
889
  material,
407
890
  dispose() {
408
- unobserve();
409
- uninstall();
410
- sentinel.dispose();
411
891
  material.dispose();
412
- lease?.release();
413
- lease = null;
414
892
  },
415
893
  };
416
894
  }