@babylonjs/loaders 9.12.1 → 9.14.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 (489) hide show
  1. package/BVH/bvhFileLoader.d.ts +4 -67
  2. package/BVH/bvhFileLoader.js +6 -130
  3. package/BVH/bvhFileLoader.js.map +1 -1
  4. package/BVH/bvhFileLoader.pure.d.ts +75 -0
  5. package/BVH/bvhFileLoader.pure.js +154 -0
  6. package/BVH/bvhFileLoader.pure.js.map +1 -0
  7. package/BVH/bvhFileLoader.types.d.ts +11 -0
  8. package/BVH/bvhFileLoader.types.js +2 -0
  9. package/BVH/bvhFileLoader.types.js.map +1 -0
  10. package/BVH/pure.d.ts +3 -0
  11. package/BVH/pure.js +5 -0
  12. package/BVH/pure.js.map +1 -0
  13. package/FBX/fbxFileLoader.d.ts +4 -192
  14. package/FBX/fbxFileLoader.js +6 -2437
  15. package/FBX/fbxFileLoader.js.map +1 -1
  16. package/FBX/fbxFileLoader.pure.d.ts +190 -0
  17. package/FBX/fbxFileLoader.pure.js +2451 -0
  18. package/FBX/fbxFileLoader.pure.js.map +1 -0
  19. package/FBX/fbxFileLoader.types.d.ts +11 -0
  20. package/FBX/fbxFileLoader.types.js +2 -0
  21. package/FBX/fbxFileLoader.types.js.map +1 -0
  22. package/FBX/pure.d.ts +3 -0
  23. package/FBX/pure.js +4 -0
  24. package/FBX/pure.js.map +1 -0
  25. package/OBJ/objFileLoader.d.ts +4 -134
  26. package/OBJ/objFileLoader.js +6 -315
  27. package/OBJ/objFileLoader.js.map +1 -1
  28. package/OBJ/objFileLoader.pure.d.ts +132 -0
  29. package/OBJ/objFileLoader.pure.js +329 -0
  30. package/OBJ/objFileLoader.pure.js.map +1 -0
  31. package/OBJ/objFileLoader.types.d.ts +11 -0
  32. package/OBJ/objFileLoader.types.js +2 -0
  33. package/OBJ/objFileLoader.types.js.map +1 -0
  34. package/OBJ/pure.d.ts +5 -0
  35. package/OBJ/pure.js +6 -0
  36. package/OBJ/pure.js.map +1 -0
  37. package/OBJ/solidParser.js +4 -3
  38. package/OBJ/solidParser.js.map +1 -1
  39. package/SPLAT/gaussianSplattingBlockAllocator.d.ts +137 -0
  40. package/SPLAT/gaussianSplattingBlockAllocator.js +555 -0
  41. package/SPLAT/gaussianSplattingBlockAllocator.js.map +1 -0
  42. package/SPLAT/gaussianSplattingDownloadManager.d.ts +109 -0
  43. package/SPLAT/gaussianSplattingDownloadManager.js +241 -0
  44. package/SPLAT/gaussianSplattingDownloadManager.js.map +1 -0
  45. package/SPLAT/gaussianSplattingResidencyController.d.ts +133 -0
  46. package/SPLAT/gaussianSplattingResidencyController.js +222 -0
  47. package/SPLAT/gaussianSplattingResidencyController.js.map +1 -0
  48. package/SPLAT/gaussianSplattingStream.d.ts +190 -9
  49. package/SPLAT/gaussianSplattingStream.js +623 -69
  50. package/SPLAT/gaussianSplattingStream.js.map +1 -1
  51. package/SPLAT/gaussianSplattingWorkBuffer.d.ts +56 -0
  52. package/SPLAT/gaussianSplattingWorkBuffer.js +215 -13
  53. package/SPLAT/gaussianSplattingWorkBuffer.js.map +1 -1
  54. package/SPLAT/gaussianSplattingWorkBufferShaders.d.ts +15 -0
  55. package/SPLAT/gaussianSplattingWorkBufferShaders.js +82 -0
  56. package/SPLAT/gaussianSplattingWorkBufferShaders.js.map +1 -1
  57. package/SPLAT/pure.d.ts +5 -0
  58. package/SPLAT/pure.js +7 -0
  59. package/SPLAT/pure.js.map +1 -0
  60. package/SPLAT/sog.d.ts +2 -93
  61. package/SPLAT/sog.js +4 -420
  62. package/SPLAT/sog.js.map +1 -1
  63. package/SPLAT/sog.pure.d.ts +102 -0
  64. package/SPLAT/sog.pure.js +514 -0
  65. package/SPLAT/sog.pure.js.map +1 -0
  66. package/SPLAT/splatFileLoader.d.ts +5 -106
  67. package/SPLAT/splatFileLoader.js +9 -636
  68. package/SPLAT/splatFileLoader.js.map +1 -1
  69. package/SPLAT/splatFileLoader.pure.d.ts +104 -0
  70. package/SPLAT/splatFileLoader.pure.js +650 -0
  71. package/SPLAT/splatFileLoader.pure.js.map +1 -0
  72. package/SPLAT/splatFileLoader.types.d.ts +11 -0
  73. package/SPLAT/splatFileLoader.types.js +2 -0
  74. package/SPLAT/splatFileLoader.types.js.map +1 -0
  75. package/STL/pure.d.ts +2 -0
  76. package/STL/pure.js +3 -0
  77. package/STL/pure.js.map +1 -0
  78. package/STL/stlFileLoader.d.ts +5 -76
  79. package/STL/stlFileLoader.js +7 -236
  80. package/STL/stlFileLoader.js.map +1 -1
  81. package/STL/stlFileLoader.pure.d.ts +73 -0
  82. package/STL/stlFileLoader.pure.js +249 -0
  83. package/STL/stlFileLoader.pure.js.map +1 -0
  84. package/STL/stlFileLoader.types.d.ts +10 -0
  85. package/STL/stlFileLoader.types.js +2 -0
  86. package/STL/stlFileLoader.types.js.map +1 -0
  87. package/glTF/1.0/pure.d.ts +3 -0
  88. package/glTF/1.0/pure.js +4 -0
  89. package/glTF/1.0/pure.js.map +1 -0
  90. package/glTF/2.0/Extensions/EXT_lights_area.d.ts +4 -39
  91. package/glTF/2.0/Extensions/EXT_lights_area.js +6 -91
  92. package/glTF/2.0/Extensions/EXT_lights_area.js.map +1 -1
  93. package/glTF/2.0/Extensions/EXT_lights_area.pure.d.ts +38 -0
  94. package/glTF/2.0/Extensions/EXT_lights_area.pure.js +106 -0
  95. package/glTF/2.0/Extensions/EXT_lights_area.pure.js.map +1 -0
  96. package/glTF/2.0/Extensions/EXT_lights_area.types.d.ts +9 -0
  97. package/glTF/2.0/Extensions/EXT_lights_area.types.js +2 -0
  98. package/glTF/2.0/Extensions/EXT_lights_area.types.js.map +1 -0
  99. package/glTF/2.0/Extensions/EXT_lights_ies.d.ts +4 -39
  100. package/glTF/2.0/Extensions/EXT_lights_ies.js +6 -81
  101. package/glTF/2.0/Extensions/EXT_lights_ies.js.map +1 -1
  102. package/glTF/2.0/Extensions/EXT_lights_ies.pure.d.ts +38 -0
  103. package/glTF/2.0/Extensions/EXT_lights_ies.pure.js +96 -0
  104. package/glTF/2.0/Extensions/EXT_lights_ies.pure.js.map +1 -0
  105. package/glTF/2.0/Extensions/EXT_lights_ies.types.d.ts +9 -0
  106. package/glTF/2.0/Extensions/EXT_lights_ies.types.js +2 -0
  107. package/glTF/2.0/Extensions/EXT_lights_ies.types.js.map +1 -0
  108. package/glTF/2.0/Extensions/EXT_lights_image_based.d.ts +4 -46
  109. package/glTF/2.0/Extensions/EXT_lights_image_based.js +6 -113
  110. package/glTF/2.0/Extensions/EXT_lights_image_based.js.map +1 -1
  111. package/glTF/2.0/Extensions/EXT_lights_image_based.pure.d.ts +37 -0
  112. package/glTF/2.0/Extensions/EXT_lights_image_based.pure.js +128 -0
  113. package/glTF/2.0/Extensions/EXT_lights_image_based.pure.js.map +1 -0
  114. package/glTF/2.0/Extensions/EXT_lights_image_based.types.d.ts +17 -0
  115. package/glTF/2.0/Extensions/EXT_lights_image_based.types.js +2 -0
  116. package/glTF/2.0/Extensions/EXT_lights_image_based.types.js.map +1 -0
  117. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.d.ts +5 -37
  118. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.js +7 -92
  119. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.js.map +1 -1
  120. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.pure.d.ts +35 -0
  121. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.pure.js +106 -0
  122. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.pure.js.map +1 -0
  123. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.types.d.ts +9 -0
  124. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.types.js +2 -0
  125. package/glTF/2.0/Extensions/EXT_mesh_gpu_instancing.types.js.map +1 -0
  126. package/glTF/2.0/Extensions/EXT_meshopt_compression.d.ts +4 -37
  127. package/glTF/2.0/Extensions/EXT_meshopt_compression.js +6 -48
  128. package/glTF/2.0/Extensions/EXT_meshopt_compression.js.map +1 -1
  129. package/glTF/2.0/Extensions/EXT_meshopt_compression.pure.d.ts +36 -0
  130. package/glTF/2.0/Extensions/EXT_meshopt_compression.pure.js +63 -0
  131. package/glTF/2.0/Extensions/EXT_meshopt_compression.pure.js.map +1 -0
  132. package/glTF/2.0/Extensions/EXT_meshopt_compression.types.d.ts +9 -0
  133. package/glTF/2.0/Extensions/EXT_meshopt_compression.types.js +2 -0
  134. package/glTF/2.0/Extensions/EXT_meshopt_compression.types.js.map +1 -0
  135. package/glTF/2.0/Extensions/EXT_texture_avif.d.ts +4 -32
  136. package/glTF/2.0/Extensions/EXT_texture_avif.js +6 -36
  137. package/glTF/2.0/Extensions/EXT_texture_avif.js.map +1 -1
  138. package/glTF/2.0/Extensions/EXT_texture_avif.pure.d.ts +31 -0
  139. package/glTF/2.0/Extensions/EXT_texture_avif.pure.js +51 -0
  140. package/glTF/2.0/Extensions/EXT_texture_avif.pure.js.map +1 -0
  141. package/glTF/2.0/Extensions/EXT_texture_avif.types.d.ts +9 -0
  142. package/glTF/2.0/Extensions/EXT_texture_avif.types.js +2 -0
  143. package/glTF/2.0/Extensions/EXT_texture_avif.types.js.map +1 -0
  144. package/glTF/2.0/Extensions/EXT_texture_webp.d.ts +4 -31
  145. package/glTF/2.0/Extensions/EXT_texture_webp.js +6 -35
  146. package/glTF/2.0/Extensions/EXT_texture_webp.js.map +1 -1
  147. package/glTF/2.0/Extensions/EXT_texture_webp.pure.d.ts +30 -0
  148. package/glTF/2.0/Extensions/EXT_texture_webp.pure.js +50 -0
  149. package/glTF/2.0/Extensions/EXT_texture_webp.pure.js.map +1 -0
  150. package/glTF/2.0/Extensions/EXT_texture_webp.types.d.ts +9 -0
  151. package/glTF/2.0/Extensions/EXT_texture_webp.types.js +2 -0
  152. package/glTF/2.0/Extensions/EXT_texture_webp.types.js.map +1 -0
  153. package/glTF/2.0/Extensions/ExtrasAsMetadata.d.ts +4 -51
  154. package/glTF/2.0/Extensions/ExtrasAsMetadata.js +6 -71
  155. package/glTF/2.0/Extensions/ExtrasAsMetadata.js.map +1 -1
  156. package/glTF/2.0/Extensions/ExtrasAsMetadata.pure.d.ts +50 -0
  157. package/glTF/2.0/Extensions/ExtrasAsMetadata.pure.js +85 -0
  158. package/glTF/2.0/Extensions/ExtrasAsMetadata.pure.js.map +1 -0
  159. package/glTF/2.0/Extensions/ExtrasAsMetadata.types.d.ts +9 -0
  160. package/glTF/2.0/Extensions/ExtrasAsMetadata.types.js +2 -0
  161. package/glTF/2.0/Extensions/ExtrasAsMetadata.types.js.map +1 -0
  162. package/glTF/2.0/Extensions/KHR_animation_pointer.d.ts +5 -45
  163. package/glTF/2.0/Extensions/KHR_animation_pointer.js +7 -75
  164. package/glTF/2.0/Extensions/KHR_animation_pointer.js.map +1 -1
  165. package/glTF/2.0/Extensions/KHR_animation_pointer.pure.d.ts +43 -0
  166. package/glTF/2.0/Extensions/KHR_animation_pointer.pure.js +89 -0
  167. package/glTF/2.0/Extensions/KHR_animation_pointer.pure.js.map +1 -0
  168. package/glTF/2.0/Extensions/KHR_animation_pointer.types.d.ts +9 -0
  169. package/glTF/2.0/Extensions/KHR_animation_pointer.types.js +2 -0
  170. package/glTF/2.0/Extensions/KHR_animation_pointer.types.js.map +1 -0
  171. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.d.ts +4 -45
  172. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.js +6 -90
  173. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.js.map +1 -1
  174. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.pure.d.ts +44 -0
  175. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.pure.js +105 -0
  176. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.pure.js.map +1 -0
  177. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.types.d.ts +9 -0
  178. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.types.js +2 -0
  179. package/glTF/2.0/Extensions/KHR_draco_mesh_compression.types.js.map +1 -0
  180. package/glTF/2.0/Extensions/KHR_interactivity/pure.d.ts +4 -0
  181. package/glTF/2.0/Extensions/KHR_interactivity/pure.js +5 -0
  182. package/glTF/2.0/Extensions/KHR_interactivity/pure.js.map +1 -0
  183. package/glTF/2.0/Extensions/KHR_interactivity.d.ts +4 -36
  184. package/glTF/2.0/Extensions/KHR_interactivity.js +6 -148
  185. package/glTF/2.0/Extensions/KHR_interactivity.js.map +1 -1
  186. package/glTF/2.0/Extensions/KHR_interactivity.pure.d.ts +35 -0
  187. package/glTF/2.0/Extensions/KHR_interactivity.pure.js +163 -0
  188. package/glTF/2.0/Extensions/KHR_interactivity.pure.js.map +1 -0
  189. package/glTF/2.0/Extensions/KHR_interactivity.types.d.ts +9 -0
  190. package/glTF/2.0/Extensions/KHR_interactivity.types.js +2 -0
  191. package/glTF/2.0/Extensions/KHR_interactivity.types.js.map +1 -0
  192. package/glTF/2.0/Extensions/KHR_lights_punctual.d.ts +4 -39
  193. package/glTF/2.0/Extensions/KHR_lights_punctual.js +6 -89
  194. package/glTF/2.0/Extensions/KHR_lights_punctual.js.map +1 -1
  195. package/glTF/2.0/Extensions/KHR_lights_punctual.pure.d.ts +38 -0
  196. package/glTF/2.0/Extensions/KHR_lights_punctual.pure.js +104 -0
  197. package/glTF/2.0/Extensions/KHR_lights_punctual.pure.js.map +1 -0
  198. package/glTF/2.0/Extensions/KHR_lights_punctual.types.d.ts +9 -0
  199. package/glTF/2.0/Extensions/KHR_lights_punctual.types.js +2 -0
  200. package/glTF/2.0/Extensions/KHR_lights_punctual.types.js.map +1 -0
  201. package/glTF/2.0/Extensions/KHR_materials_anisotropy.d.ts +4 -40
  202. package/glTF/2.0/Extensions/KHR_materials_anisotropy.js +6 -63
  203. package/glTF/2.0/Extensions/KHR_materials_anisotropy.js.map +1 -1
  204. package/glTF/2.0/Extensions/KHR_materials_anisotropy.pure.d.ts +39 -0
  205. package/glTF/2.0/Extensions/KHR_materials_anisotropy.pure.js +78 -0
  206. package/glTF/2.0/Extensions/KHR_materials_anisotropy.pure.js.map +1 -0
  207. package/glTF/2.0/Extensions/KHR_materials_anisotropy.types.d.ts +9 -0
  208. package/glTF/2.0/Extensions/KHR_materials_anisotropy.types.js +2 -0
  209. package/glTF/2.0/Extensions/KHR_materials_anisotropy.types.js.map +1 -0
  210. package/glTF/2.0/Extensions/KHR_materials_clearcoat.d.ts +4 -41
  211. package/glTF/2.0/Extensions/KHR_materials_clearcoat.js +6 -77
  212. package/glTF/2.0/Extensions/KHR_materials_clearcoat.js.map +1 -1
  213. package/glTF/2.0/Extensions/KHR_materials_clearcoat.pure.d.ts +40 -0
  214. package/glTF/2.0/Extensions/KHR_materials_clearcoat.pure.js +92 -0
  215. package/glTF/2.0/Extensions/KHR_materials_clearcoat.pure.js.map +1 -0
  216. package/glTF/2.0/Extensions/KHR_materials_clearcoat.types.d.ts +9 -0
  217. package/glTF/2.0/Extensions/KHR_materials_clearcoat.types.js +2 -0
  218. package/glTF/2.0/Extensions/KHR_materials_clearcoat.types.js.map +1 -0
  219. package/glTF/2.0/Extensions/KHR_materials_coat.d.ts +4 -47
  220. package/glTF/2.0/Extensions/KHR_materials_coat.js +6 -117
  221. package/glTF/2.0/Extensions/KHR_materials_coat.js.map +1 -1
  222. package/glTF/2.0/Extensions/KHR_materials_coat.pure.d.ts +46 -0
  223. package/glTF/2.0/Extensions/KHR_materials_coat.pure.js +132 -0
  224. package/glTF/2.0/Extensions/KHR_materials_coat.pure.js.map +1 -0
  225. package/glTF/2.0/Extensions/KHR_materials_coat.types.d.ts +9 -0
  226. package/glTF/2.0/Extensions/KHR_materials_coat.types.js +2 -0
  227. package/glTF/2.0/Extensions/KHR_materials_coat.types.js.map +1 -0
  228. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.d.ts +4 -41
  229. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.js +6 -56
  230. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.js.map +1 -1
  231. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.pure.d.ts +40 -0
  232. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.pure.js +71 -0
  233. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.pure.js.map +1 -0
  234. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.types.d.ts +9 -0
  235. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.types.js +2 -0
  236. package/glTF/2.0/Extensions/KHR_materials_diffuse_roughness.types.js.map +1 -0
  237. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.d.ts +4 -41
  238. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.js +6 -71
  239. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.js.map +1 -1
  240. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.pure.d.ts +40 -0
  241. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.pure.js +86 -0
  242. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.pure.js.map +1 -0
  243. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.types.d.ts +9 -0
  244. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.types.js +2 -0
  245. package/glTF/2.0/Extensions/KHR_materials_diffuse_transmission.types.js.map +1 -0
  246. package/glTF/2.0/Extensions/KHR_materials_dispersion.d.ts +4 -41
  247. package/glTF/2.0/Extensions/KHR_materials_dispersion.js +6 -54
  248. package/glTF/2.0/Extensions/KHR_materials_dispersion.js.map +1 -1
  249. package/glTF/2.0/Extensions/KHR_materials_dispersion.pure.d.ts +40 -0
  250. package/glTF/2.0/Extensions/KHR_materials_dispersion.pure.js +69 -0
  251. package/glTF/2.0/Extensions/KHR_materials_dispersion.pure.js.map +1 -0
  252. package/glTF/2.0/Extensions/KHR_materials_dispersion.types.d.ts +9 -0
  253. package/glTF/2.0/Extensions/KHR_materials_dispersion.types.js +2 -0
  254. package/glTF/2.0/Extensions/KHR_materials_dispersion.types.js.map +1 -0
  255. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.d.ts +4 -40
  256. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.js +6 -45
  257. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.js.map +1 -1
  258. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.pure.d.ts +39 -0
  259. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.pure.js +60 -0
  260. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.pure.js.map +1 -0
  261. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.types.d.ts +9 -0
  262. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.types.js +2 -0
  263. package/glTF/2.0/Extensions/KHR_materials_emissive_strength.types.js.map +1 -0
  264. package/glTF/2.0/Extensions/KHR_materials_fuzz.d.ts +4 -41
  265. package/glTF/2.0/Extensions/KHR_materials_fuzz.js +6 -75
  266. package/glTF/2.0/Extensions/KHR_materials_fuzz.js.map +1 -1
  267. package/glTF/2.0/Extensions/KHR_materials_fuzz.pure.d.ts +40 -0
  268. package/glTF/2.0/Extensions/KHR_materials_fuzz.pure.js +90 -0
  269. package/glTF/2.0/Extensions/KHR_materials_fuzz.pure.js.map +1 -0
  270. package/glTF/2.0/Extensions/KHR_materials_fuzz.types.d.ts +9 -0
  271. package/glTF/2.0/Extensions/KHR_materials_fuzz.types.js +2 -0
  272. package/glTF/2.0/Extensions/KHR_materials_fuzz.types.js.map +1 -0
  273. package/glTF/2.0/Extensions/KHR_materials_ior.d.ts +4 -44
  274. package/glTF/2.0/Extensions/KHR_materials_ior.js +6 -52
  275. package/glTF/2.0/Extensions/KHR_materials_ior.js.map +1 -1
  276. package/glTF/2.0/Extensions/KHR_materials_ior.pure.d.ts +43 -0
  277. package/glTF/2.0/Extensions/KHR_materials_ior.pure.js +67 -0
  278. package/glTF/2.0/Extensions/KHR_materials_ior.pure.js.map +1 -0
  279. package/glTF/2.0/Extensions/KHR_materials_ior.types.d.ts +9 -0
  280. package/glTF/2.0/Extensions/KHR_materials_ior.types.js +2 -0
  281. package/glTF/2.0/Extensions/KHR_materials_ior.types.js.map +1 -0
  282. package/glTF/2.0/Extensions/KHR_materials_iridescence.d.ts +4 -40
  283. package/glTF/2.0/Extensions/KHR_materials_iridescence.js +6 -66
  284. package/glTF/2.0/Extensions/KHR_materials_iridescence.js.map +1 -1
  285. package/glTF/2.0/Extensions/KHR_materials_iridescence.pure.d.ts +39 -0
  286. package/glTF/2.0/Extensions/KHR_materials_iridescence.pure.js +81 -0
  287. package/glTF/2.0/Extensions/KHR_materials_iridescence.pure.js.map +1 -0
  288. package/glTF/2.0/Extensions/KHR_materials_iridescence.types.d.ts +9 -0
  289. package/glTF/2.0/Extensions/KHR_materials_iridescence.types.js +2 -0
  290. package/glTF/2.0/Extensions/KHR_materials_iridescence.types.js.map +1 -0
  291. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.d.ts +4 -40
  292. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.js +6 -72
  293. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.js.map +1 -1
  294. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.pure.d.ts +39 -0
  295. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.pure.js +87 -0
  296. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.pure.js.map +1 -0
  297. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.types.d.ts +9 -0
  298. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.types.js +2 -0
  299. package/glTF/2.0/Extensions/KHR_materials_pbrSpecularGlossiness.types.js.map +1 -0
  300. package/glTF/2.0/Extensions/KHR_materials_sheen.d.ts +4 -41
  301. package/glTF/2.0/Extensions/KHR_materials_sheen.js +6 -71
  302. package/glTF/2.0/Extensions/KHR_materials_sheen.js.map +1 -1
  303. package/glTF/2.0/Extensions/KHR_materials_sheen.pure.d.ts +40 -0
  304. package/glTF/2.0/Extensions/KHR_materials_sheen.pure.js +86 -0
  305. package/glTF/2.0/Extensions/KHR_materials_sheen.pure.js.map +1 -0
  306. package/glTF/2.0/Extensions/KHR_materials_sheen.types.d.ts +9 -0
  307. package/glTF/2.0/Extensions/KHR_materials_sheen.types.js +2 -0
  308. package/glTF/2.0/Extensions/KHR_materials_sheen.types.js.map +1 -0
  309. package/glTF/2.0/Extensions/KHR_materials_specular.d.ts +4 -40
  310. package/glTF/2.0/Extensions/KHR_materials_specular.js +6 -74
  311. package/glTF/2.0/Extensions/KHR_materials_specular.js.map +1 -1
  312. package/glTF/2.0/Extensions/KHR_materials_specular.pure.d.ts +39 -0
  313. package/glTF/2.0/Extensions/KHR_materials_specular.pure.js +89 -0
  314. package/glTF/2.0/Extensions/KHR_materials_specular.pure.js.map +1 -0
  315. package/glTF/2.0/Extensions/KHR_materials_specular.types.d.ts +9 -0
  316. package/glTF/2.0/Extensions/KHR_materials_specular.types.js +2 -0
  317. package/glTF/2.0/Extensions/KHR_materials_specular.types.js.map +1 -0
  318. package/glTF/2.0/Extensions/KHR_materials_transmission.d.ts +4 -40
  319. package/glTF/2.0/Extensions/KHR_materials_transmission.js +6 -70
  320. package/glTF/2.0/Extensions/KHR_materials_transmission.js.map +1 -1
  321. package/glTF/2.0/Extensions/KHR_materials_transmission.pure.d.ts +39 -0
  322. package/glTF/2.0/Extensions/KHR_materials_transmission.pure.js +85 -0
  323. package/glTF/2.0/Extensions/KHR_materials_transmission.pure.js.map +1 -0
  324. package/glTF/2.0/Extensions/KHR_materials_transmission.types.d.ts +9 -0
  325. package/glTF/2.0/Extensions/KHR_materials_transmission.types.js +2 -0
  326. package/glTF/2.0/Extensions/KHR_materials_transmission.types.js.map +1 -0
  327. package/glTF/2.0/Extensions/KHR_materials_unlit.d.ts +4 -40
  328. package/glTF/2.0/Extensions/KHR_materials_unlit.js +6 -64
  329. package/glTF/2.0/Extensions/KHR_materials_unlit.js.map +1 -1
  330. package/glTF/2.0/Extensions/KHR_materials_unlit.pure.d.ts +39 -0
  331. package/glTF/2.0/Extensions/KHR_materials_unlit.pure.js +79 -0
  332. package/glTF/2.0/Extensions/KHR_materials_unlit.pure.js.map +1 -0
  333. package/glTF/2.0/Extensions/KHR_materials_unlit.types.d.ts +9 -0
  334. package/glTF/2.0/Extensions/KHR_materials_unlit.types.js +2 -0
  335. package/glTF/2.0/Extensions/KHR_materials_unlit.types.js.map +1 -0
  336. package/glTF/2.0/Extensions/KHR_materials_variants.d.ts +4 -110
  337. package/glTF/2.0/Extensions/KHR_materials_variants.js +6 -260
  338. package/glTF/2.0/Extensions/KHR_materials_variants.js.map +1 -1
  339. package/glTF/2.0/Extensions/KHR_materials_variants.pure.d.ts +97 -0
  340. package/glTF/2.0/Extensions/KHR_materials_variants.pure.js +275 -0
  341. package/glTF/2.0/Extensions/KHR_materials_variants.pure.js.map +1 -0
  342. package/glTF/2.0/Extensions/KHR_materials_variants.types.d.ts +29 -0
  343. package/glTF/2.0/Extensions/KHR_materials_variants.types.js +2 -0
  344. package/glTF/2.0/Extensions/KHR_materials_variants.types.js.map +1 -0
  345. package/glTF/2.0/Extensions/KHR_materials_volume.d.ts +4 -41
  346. package/glTF/2.0/Extensions/KHR_materials_volume.js +6 -77
  347. package/glTF/2.0/Extensions/KHR_materials_volume.js.map +1 -1
  348. package/glTF/2.0/Extensions/KHR_materials_volume.pure.d.ts +40 -0
  349. package/glTF/2.0/Extensions/KHR_materials_volume.pure.js +92 -0
  350. package/glTF/2.0/Extensions/KHR_materials_volume.pure.js.map +1 -0
  351. package/glTF/2.0/Extensions/KHR_materials_volume.types.d.ts +9 -0
  352. package/glTF/2.0/Extensions/KHR_materials_volume.types.js +2 -0
  353. package/glTF/2.0/Extensions/KHR_materials_volume.types.js.map +1 -0
  354. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.d.ts +4 -43
  355. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.js +6 -113
  356. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.js.map +1 -1
  357. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.pure.d.ts +42 -0
  358. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.pure.js +128 -0
  359. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.pure.js.map +1 -0
  360. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.types.d.ts +9 -0
  361. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.types.js +2 -0
  362. package/glTF/2.0/Extensions/KHR_materials_volume_scatter.types.js.map +1 -0
  363. package/glTF/2.0/Extensions/KHR_mesh_quantization.d.ts +4 -27
  364. package/glTF/2.0/Extensions/KHR_mesh_quantization.js +6 -20
  365. package/glTF/2.0/Extensions/KHR_mesh_quantization.js.map +1 -1
  366. package/glTF/2.0/Extensions/KHR_mesh_quantization.pure.d.ts +26 -0
  367. package/glTF/2.0/Extensions/KHR_mesh_quantization.pure.js +35 -0
  368. package/glTF/2.0/Extensions/KHR_mesh_quantization.pure.js.map +1 -0
  369. package/glTF/2.0/Extensions/KHR_mesh_quantization.types.d.ts +9 -0
  370. package/glTF/2.0/Extensions/KHR_mesh_quantization.types.js +2 -0
  371. package/glTF/2.0/Extensions/KHR_mesh_quantization.types.js.map +1 -0
  372. package/glTF/2.0/Extensions/KHR_node_hoverability.d.ts +4 -29
  373. package/glTF/2.0/Extensions/KHR_node_hoverability.js +6 -187
  374. package/glTF/2.0/Extensions/KHR_node_hoverability.js.map +1 -1
  375. package/glTF/2.0/Extensions/KHR_node_hoverability.pure.d.ts +28 -0
  376. package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js +202 -0
  377. package/glTF/2.0/Extensions/KHR_node_hoverability.pure.js.map +1 -0
  378. package/glTF/2.0/Extensions/KHR_node_hoverability.types.d.ts +9 -0
  379. package/glTF/2.0/Extensions/KHR_node_hoverability.types.js +2 -0
  380. package/glTF/2.0/Extensions/KHR_node_hoverability.types.js.map +1 -0
  381. package/glTF/2.0/Extensions/KHR_node_selectability.d.ts +4 -28
  382. package/glTF/2.0/Extensions/KHR_node_selectability.js +6 -121
  383. package/glTF/2.0/Extensions/KHR_node_selectability.js.map +1 -1
  384. package/glTF/2.0/Extensions/KHR_node_selectability.pure.d.ts +27 -0
  385. package/glTF/2.0/Extensions/KHR_node_selectability.pure.js +136 -0
  386. package/glTF/2.0/Extensions/KHR_node_selectability.pure.js.map +1 -0
  387. package/glTF/2.0/Extensions/KHR_node_selectability.types.d.ts +9 -0
  388. package/glTF/2.0/Extensions/KHR_node_selectability.types.js +2 -0
  389. package/glTF/2.0/Extensions/KHR_node_selectability.types.js.map +1 -0
  390. package/glTF/2.0/Extensions/KHR_node_visibility.d.ts +4 -28
  391. package/glTF/2.0/Extensions/KHR_node_visibility.js +6 -64
  392. package/glTF/2.0/Extensions/KHR_node_visibility.js.map +1 -1
  393. package/glTF/2.0/Extensions/KHR_node_visibility.pure.d.ts +27 -0
  394. package/glTF/2.0/Extensions/KHR_node_visibility.pure.js +79 -0
  395. package/glTF/2.0/Extensions/KHR_node_visibility.pure.js.map +1 -0
  396. package/glTF/2.0/Extensions/KHR_node_visibility.types.d.ts +9 -0
  397. package/glTF/2.0/Extensions/KHR_node_visibility.types.js +2 -0
  398. package/glTF/2.0/Extensions/KHR_node_visibility.types.js.map +1 -0
  399. package/glTF/2.0/Extensions/KHR_texture_basisu.d.ts +4 -31
  400. package/glTF/2.0/Extensions/KHR_texture_basisu.js +6 -35
  401. package/glTF/2.0/Extensions/KHR_texture_basisu.js.map +1 -1
  402. package/glTF/2.0/Extensions/KHR_texture_basisu.pure.d.ts +30 -0
  403. package/glTF/2.0/Extensions/KHR_texture_basisu.pure.js +50 -0
  404. package/glTF/2.0/Extensions/KHR_texture_basisu.pure.js.map +1 -0
  405. package/glTF/2.0/Extensions/KHR_texture_basisu.types.d.ts +9 -0
  406. package/glTF/2.0/Extensions/KHR_texture_basisu.types.js +2 -0
  407. package/glTF/2.0/Extensions/KHR_texture_basisu.types.js.map +1 -0
  408. package/glTF/2.0/Extensions/KHR_texture_transform.d.ts +4 -35
  409. package/glTF/2.0/Extensions/KHR_texture_transform.js +6 -56
  410. package/glTF/2.0/Extensions/KHR_texture_transform.js.map +1 -1
  411. package/glTF/2.0/Extensions/KHR_texture_transform.pure.d.ts +34 -0
  412. package/glTF/2.0/Extensions/KHR_texture_transform.pure.js +71 -0
  413. package/glTF/2.0/Extensions/KHR_texture_transform.pure.js.map +1 -0
  414. package/glTF/2.0/Extensions/KHR_texture_transform.types.d.ts +9 -0
  415. package/glTF/2.0/Extensions/KHR_texture_transform.types.js +2 -0
  416. package/glTF/2.0/Extensions/KHR_texture_transform.types.js.map +1 -0
  417. package/glTF/2.0/Extensions/KHR_xmp_json_ld.d.ts +4 -37
  418. package/glTF/2.0/Extensions/KHR_xmp_json_ld.js +6 -45
  419. package/glTF/2.0/Extensions/KHR_xmp_json_ld.js.map +1 -1
  420. package/glTF/2.0/Extensions/KHR_xmp_json_ld.pure.d.ts +36 -0
  421. package/glTF/2.0/Extensions/KHR_xmp_json_ld.pure.js +60 -0
  422. package/glTF/2.0/Extensions/KHR_xmp_json_ld.pure.js.map +1 -0
  423. package/glTF/2.0/Extensions/KHR_xmp_json_ld.types.d.ts +9 -0
  424. package/glTF/2.0/Extensions/KHR_xmp_json_ld.types.js +2 -0
  425. package/glTF/2.0/Extensions/KHR_xmp_json_ld.types.js.map +1 -0
  426. package/glTF/2.0/Extensions/MSFT_audio_emitter.d.ts +5 -54
  427. package/glTF/2.0/Extensions/MSFT_audio_emitter.js +7 -226
  428. package/glTF/2.0/Extensions/MSFT_audio_emitter.js.map +1 -1
  429. package/glTF/2.0/Extensions/MSFT_audio_emitter.pure.d.ts +52 -0
  430. package/glTF/2.0/Extensions/MSFT_audio_emitter.pure.js +240 -0
  431. package/glTF/2.0/Extensions/MSFT_audio_emitter.pure.js.map +1 -0
  432. package/glTF/2.0/Extensions/MSFT_audio_emitter.types.d.ts +9 -0
  433. package/glTF/2.0/Extensions/MSFT_audio_emitter.types.js +2 -0
  434. package/glTF/2.0/Extensions/MSFT_audio_emitter.types.js.map +1 -0
  435. package/glTF/2.0/Extensions/MSFT_lod.d.ts +4 -102
  436. package/glTF/2.0/Extensions/MSFT_lod.js +6 -335
  437. package/glTF/2.0/Extensions/MSFT_lod.js.map +1 -1
  438. package/glTF/2.0/Extensions/MSFT_lod.pure.d.ts +96 -0
  439. package/glTF/2.0/Extensions/MSFT_lod.pure.js +350 -0
  440. package/glTF/2.0/Extensions/MSFT_lod.pure.js.map +1 -0
  441. package/glTF/2.0/Extensions/MSFT_lod.types.d.ts +14 -0
  442. package/glTF/2.0/Extensions/MSFT_lod.types.js +2 -0
  443. package/glTF/2.0/Extensions/MSFT_lod.types.js.map +1 -0
  444. package/glTF/2.0/Extensions/MSFT_minecraftMesh.d.ts +6 -27
  445. package/glTF/2.0/Extensions/MSFT_minecraftMesh.js +8 -40
  446. package/glTF/2.0/Extensions/MSFT_minecraftMesh.js.map +1 -1
  447. package/glTF/2.0/Extensions/MSFT_minecraftMesh.pure.d.ts +24 -0
  448. package/glTF/2.0/Extensions/MSFT_minecraftMesh.pure.js +53 -0
  449. package/glTF/2.0/Extensions/MSFT_minecraftMesh.pure.js.map +1 -0
  450. package/glTF/2.0/Extensions/MSFT_minecraftMesh.types.d.ts +9 -0
  451. package/glTF/2.0/Extensions/MSFT_minecraftMesh.types.js +2 -0
  452. package/glTF/2.0/Extensions/MSFT_minecraftMesh.types.js.map +1 -0
  453. package/glTF/2.0/Extensions/MSFT_sRGBFactors.d.ts +6 -27
  454. package/glTF/2.0/Extensions/MSFT_sRGBFactors.js +8 -38
  455. package/glTF/2.0/Extensions/MSFT_sRGBFactors.js.map +1 -1
  456. package/glTF/2.0/Extensions/MSFT_sRGBFactors.pure.d.ts +24 -0
  457. package/glTF/2.0/Extensions/MSFT_sRGBFactors.pure.js +51 -0
  458. package/glTF/2.0/Extensions/MSFT_sRGBFactors.pure.js.map +1 -0
  459. package/glTF/2.0/Extensions/MSFT_sRGBFactors.types.d.ts +9 -0
  460. package/glTF/2.0/Extensions/MSFT_sRGBFactors.types.js +2 -0
  461. package/glTF/2.0/Extensions/MSFT_sRGBFactors.types.js.map +1 -0
  462. package/glTF/2.0/Extensions/pure.d.ts +45 -0
  463. package/glTF/2.0/Extensions/pure.js +47 -0
  464. package/glTF/2.0/Extensions/pure.js.map +1 -0
  465. package/glTF/2.0/glTFLoader.d.ts +1 -471
  466. package/glTF/2.0/glTFLoader.js +3 -2539
  467. package/glTF/2.0/glTFLoader.js.map +1 -1
  468. package/glTF/2.0/glTFLoader.pure.d.ts +476 -0
  469. package/glTF/2.0/glTFLoader.pure.js +2563 -0
  470. package/glTF/2.0/glTFLoader.pure.js.map +1 -0
  471. package/glTF/2.0/pure.d.ts +8 -0
  472. package/glTF/2.0/pure.js +10 -0
  473. package/glTF/2.0/pure.js.map +1 -0
  474. package/glTF/glTFFileLoader.d.ts +5 -551
  475. package/glTF/glTFFileLoader.js +6 -996
  476. package/glTF/glTFFileLoader.js.map +1 -1
  477. package/glTF/glTFFileLoader.pure.d.ts +562 -0
  478. package/glTF/glTFFileLoader.pure.js +1024 -0
  479. package/glTF/glTFFileLoader.pure.js.map +1 -0
  480. package/glTF/glTFFileLoader.types.d.ts +11 -0
  481. package/glTF/glTFFileLoader.types.js +2 -0
  482. package/glTF/glTFFileLoader.types.js.map +1 -0
  483. package/glTF/pure.d.ts +6 -0
  484. package/glTF/pure.js +8 -0
  485. package/glTF/pure.js.map +1 -0
  486. package/package.json +56 -4
  487. package/pure.d.ts +8 -0
  488. package/pure.js +10 -0
  489. package/pure.js.map +1 -0
@@ -3,14 +3,24 @@ import { Logger } from "@babylonjs/core/Misc/logger.js";
3
3
  import { Tools } from "@babylonjs/core/Misc/tools.js";
4
4
  import { Vector3, Matrix } from "@babylonjs/core/Maths/math.vector.js";
5
5
  import { Color4 } from "@babylonjs/core/Maths/math.color.js";
6
+ import { Frustum } from "@babylonjs/core/Maths/math.frustum.js";
7
+ import { Plane } from "@babylonjs/core/Maths/math.plane.js";
6
8
  import { Camera } from "@babylonjs/core/Cameras/camera.js";
7
9
  import { BoundingInfo } from "@babylonjs/core/Culling/boundingInfo.js";
8
10
  import { CreateLineSystem } from "@babylonjs/core/Meshes/Builders/linesBuilder.js";
9
11
  import { VertexBuffer } from "@babylonjs/core/Buffers/buffer.js";
10
- import { ParseSogMetaAsTextures } from "./sog.js";
12
+ import { ParseSogMetaAsTextures } from "./sog.pure.js";
11
13
  import { GaussianSplattingWorkBuffer } from "./gaussianSplattingWorkBuffer.js";
14
+ import { GaussianSplattingDownloadManager } from "./gaussianSplattingDownloadManager.js";
15
+ import { GaussianSplattingResidencyController } from "./gaussianSplattingResidencyController.js";
12
16
  // tan(22.5deg): reference half-FOV for a 45-degree vertical FOV, used for FOV compensation (matches PlayCanvas).
13
17
  const RefTanHalfFov = Math.tan((22.5 * Math.PI) / 180);
18
+ // Sentinel "file" ids for the residency controller's pinned (never-evicted) allocations.
19
+ const PaddingFileId = -2;
20
+ const EnvironmentFileId = -1;
21
+ // Approximate bytes per resident splat used to convert a memory budget (MB) to a splat budget: the four
22
+ // work-buffer textures cost 16+16+16+4 = 52 bytes on the GPU, plus ~32 bytes of CPU position/sort data.
23
+ const BytesPerResidentSplat = 84;
14
24
  // Scratch objects reused by the per-frame optimal-LOD evaluation (avoids per-call allocations).
15
25
  const TmpInvWorld = new Matrix();
16
26
  const TmpLocalCamera = new Vector3();
@@ -98,20 +108,55 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
98
108
  this._lodUpdateInterval = 4;
99
109
  this._lodUpdateDistance = 0.5;
100
110
  this._maxDetailLod = 0;
111
+ // Frustum LOD bias: when enabled, nodes outside the camera frustum are rendered at their coarsest LOD.
112
+ this._frustumCulling = true;
113
+ // Reused world-space frustum planes and view-projection scratch matrix (avoids per-frame allocation).
114
+ this._frustumPlanes = [
115
+ new Plane(0, 0, 0, 0),
116
+ new Plane(0, 0, 0, 0),
117
+ new Plane(0, 0, 0, 0),
118
+ new Plane(0, 0, 0, 0),
119
+ new Plane(0, 0, 0, 0),
120
+ new Plane(0, 0, 0, 0),
121
+ ];
122
+ this._cullViewProj = new Matrix();
101
123
  // GPU work buffer holding all decoded splats; created once the total capacity is known.
102
124
  this._workBuffer = null;
103
- // Global splat offset where each source file begins in the work buffer (fixed for all files up front).
104
- this._fileBaseSplat = new Map();
125
+ // True once GPU position readback has been validated against a CPU decode (see _probeReadbackAsync). While
126
+ // false, positions are decoded on the CPU from the means images; once validated, every SOG image uses the
127
+ // fast direct upload and positions are read back from the work buffer (non-blocking).
128
+ this._useGpuPositionReadback = false;
129
+ // Whether the engine reports GPU readback support (candidate to validate on the first decode).
130
+ this._readbackCandidate = false;
131
+ // Set once the one-time readback validation has run (success or failure).
132
+ this._readbackProbed = false;
133
+ // Residency controller: owns the work-buffer slot allocator, per-file blocks, and eviction cooldowns.
134
+ this._residency = null;
105
135
  // Splat count of each source file (learned from its metadata before allocation).
106
136
  this._fileCounts = new Map();
107
137
  // Cached SOG metadata per file so on-demand decodes don't refetch the meta.json.
108
138
  this._fileMeta = new Map();
109
- // Files whose splats have been GPU-decoded into the work buffer.
139
+ // Files whose splats have been fully GPU-decoded into the work buffer (render-safe).
110
140
  this._decodedFiles = new Set();
111
141
  // Files whose decode is currently in flight (dedupes concurrent requests).
112
142
  this._loadingFiles = new Set();
113
143
  // FIFO of file ids waiting to be decoded (drained under a per-frame budget).
114
144
  this._decodeQueue = [];
145
+ // Per-file reference count: number of leaf nodes whose active LOD renders, or whose pending target points
146
+ // at, each file. At zero, a decoded file is scheduled for eviction and a still-downloading file is cancelled.
147
+ this._fileRefs = new Map();
148
+ // Files whose in-flight decode was cancelled; checked at decode checkpoints to bail out cooperatively.
149
+ this._cancelledDecodes = new Set();
150
+ // Eviction streaming config: enabled only when a budget smaller than the full dataset is configured.
151
+ this._evictionEnabled = false;
152
+ this._residentBudget = 0;
153
+ this._evictionCooldownFrames = 100;
154
+ // Serializes the allocate -> decode -> readback critical section so a defrag relayout (which runs inside it)
155
+ // never overlaps another file's decode writing the work buffer, which would corrupt the moved data.
156
+ this._decodeGate = Promise.resolve();
157
+ // Reusable scratch for the (rare) defrag relayout, to avoid per-relayout allocations during streaming.
158
+ this._relayoutOldOffsets = new Map();
159
+ this._relayoutSrcIndex = null;
115
160
  // Global range covered by the environment file (always rendered), or null until it loads.
116
161
  this._environmentRange = null;
117
162
  // Unzipped environment bundle contents, retained between count-gathering and decode.
@@ -173,9 +218,29 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
173
218
  if (options.maxDetailLod !== undefined) {
174
219
  this._maxDetailLod = Math.max(0, Math.floor(options.maxDetailLod));
175
220
  }
221
+ if (options.frustumCulling !== undefined) {
222
+ this._frustumCulling = options.frustumCulling;
223
+ }
176
224
  if (options.debugLodSource) {
177
225
  this._debugLodSource = options.debugLodSource;
178
226
  }
227
+ if (options.evictionCooldownFrames !== undefined) {
228
+ this._evictionCooldownFrames = Math.max(0, Math.floor(options.evictionCooldownFrames));
229
+ }
230
+ // Resolve the resident-splat budget from the splat-count and/or memory-size options (smaller wins).
231
+ let budget = 0;
232
+ if (options.maxResidentSplats !== undefined && options.maxResidentSplats > 0) {
233
+ budget = Math.floor(options.maxResidentSplats);
234
+ }
235
+ if (options.memoryBudgetMb !== undefined && options.memoryBudgetMb > 0) {
236
+ const fromMB = Math.floor((options.memoryBudgetMb * 1024 * 1024) / BytesPerResidentSplat);
237
+ budget = budget > 0 ? Math.min(budget, fromMB) : fromMB;
238
+ }
239
+ this._residentBudget = budget;
240
+ this._downloadManager = new GaussianSplattingDownloadManager({
241
+ maxConcurrent: options.maxConcurrentDownloads,
242
+ maxRetries: options.maxDownloadRetries,
243
+ });
179
244
  // PlayCanvas SOG data is authored with a flipped Y; match the standard SOG loader.
180
245
  this.scaling.y *= -1;
181
246
  // PlayCanvas SOG LOD scenes are authored Z-up; rotate into Babylon's Y-up convention.
@@ -193,6 +258,88 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
193
258
  getClassName() {
194
259
  return "GaussianSplattingStream";
195
260
  }
261
+ /**
262
+ * Resolves once the scene is fully streamed and displayed for the current camera: a LOD re-evaluation has
263
+ * run for the current point of view, every reachable LOD file has finished downloading and decoding (no
264
+ * downloads, decodes, or queued work remain), and the depth sort for the resulting splats has been applied
265
+ * and rendered. Intended for deterministic automated testing and screenshot/image comparison.
266
+ *
267
+ * Streaming and settling require rendered frames. If an external render loop is already running, this waits
268
+ * on it passively; otherwise (e.g. when awaited inside an async `createScene` before the host starts its
269
+ * render loop) it drives `scene.render()` itself until settled, so it never deadlocks.
270
+ *
271
+ * Note: the promise only resolves while the camera is still — if the camera keeps moving, the target LODs
272
+ * (and the depth sort) keep changing and the stream never settles. Position the camera, then await this.
273
+ * @param stableFrames number of consecutive settled frames to require before resolving (defaults to 3), so
274
+ * the final sorted frame is actually on screen
275
+ * @returns a promise that resolves when loading and rendering are complete for the current view
276
+ */
277
+ async whenSettledAsync(stableFrames = 3) {
278
+ if (this._disposed) {
279
+ return;
280
+ }
281
+ // Re-evaluate LODs immediately so the target levels reflect the current camera before we wait.
282
+ this._forceLodUpdate = true;
283
+ const required = Math.max(1, stableFrames);
284
+ const scene = this._scene;
285
+ let stable = 0;
286
+ const isSettled = () => {
287
+ if (this._isLoadingIdle() && this._isDepthSortSettled) {
288
+ return ++stable >= required;
289
+ }
290
+ stable = 0;
291
+ return false;
292
+ };
293
+ // An external render loop is already driving frames: observe it passively.
294
+ if (scene.getEngine().activeRenderLoops.length > 0) {
295
+ await new Promise((resolve) => {
296
+ let observer = null;
297
+ observer = scene.onAfterRenderObservable.add(() => {
298
+ if (this._disposed || isSettled()) {
299
+ if (observer) {
300
+ scene.onAfterRenderObservable.remove(observer);
301
+ observer = null;
302
+ }
303
+ resolve();
304
+ }
305
+ });
306
+ });
307
+ return;
308
+ }
309
+ // No render loop yet (e.g. awaited inside createScene): drive rendering ourselves so the streaming
310
+ // decodes and depth sort can progress, yielding between frames so async downloads/readbacks resolve.
311
+ // Wrap each render in beginFrame/endFrame exactly like the engine's own render loop: on WebGPU,
312
+ // endFrame submits the frame's command buffers and presents the swapchain, so a bare scene.render()
313
+ // would leave the acquired swapchain texture to be destroyed at the frame boundary before its command
314
+ // buffer is submitted ("destroyed texture used in a submit").
315
+ const engine = scene.getEngine();
316
+ const requestFrame = globalThis.requestAnimationFrame;
317
+ while (!this._disposed) {
318
+ engine.beginFrame();
319
+ scene.render();
320
+ engine.endFrame();
321
+ if (isSettled()) {
322
+ return;
323
+ }
324
+ // eslint-disable-next-line no-await-in-loop
325
+ await new Promise((resolve) => {
326
+ if (typeof requestFrame === "function") {
327
+ requestFrame(() => resolve());
328
+ }
329
+ else {
330
+ setTimeout(resolve, 16);
331
+ }
332
+ });
333
+ }
334
+ }
335
+ /**
336
+ * Whether the base layer is ready and there is no streaming work in flight (nothing queued for decode, no
337
+ * decode running, and no downloads pending).
338
+ * @returns true when no loading work remains
339
+ */
340
+ _isLoadingIdle() {
341
+ return this._baseLayerReady && this._decodeQueue.length === 0 && this._loadingFiles.size === 0 && this._downloadManager.isIdle;
342
+ }
196
343
  /**
197
344
  * Finest (most detailed) LOD level any node is allowed to render. `0` allows full detail (level 0);
198
345
  * `1` caps detail at the next-coarser level, and so on. Nodes already coarser than this cap (by
@@ -217,6 +364,23 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
217
364
  get maxLodLevel() {
218
365
  return Math.max(0, this._metadata.lodLevels - 1);
219
366
  }
367
+ /**
368
+ * When true (default), nodes whose bounding box is outside the camera frustum are biased to the coarsest
369
+ * LOD instead of being hidden. They stay in the sort/render set (their off-screen splats are clipped), so
370
+ * turning the camera toward them shows low detail immediately with no invisible frames, then refines.
371
+ * Changes take effect in real time.
372
+ */
373
+ get frustumCulling() {
374
+ return this._frustumCulling;
375
+ }
376
+ set frustumCulling(value) {
377
+ if (this._frustumCulling === value) {
378
+ return;
379
+ }
380
+ this._frustumCulling = value;
381
+ // Re-evaluate LODs next frame so the off-screen bias is applied/removed immediately.
382
+ this._forceLodUpdate = true;
383
+ }
220
384
  /**
221
385
  * When true, renders a wireframe box per LOD node, colored by the LOD level selected by {@link debugLodSource}.
222
386
  */
@@ -258,6 +422,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
258
422
  this._lodObserver = null;
259
423
  }
260
424
  this._clearDebugDisplay();
425
+ this._downloadManager.dispose();
426
+ this._residency?.dispose();
427
+ this._residency = null;
261
428
  this._workBuffer?.dispose();
262
429
  this._workBuffer = null;
263
430
  super.dispose(doNotRecurse);
@@ -287,7 +454,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
287
454
  const tanHalfH = tanHalfV * aspect;
288
455
  const fovScale = Math.min(tanHalfV, tanHalfH) / RefTanHalfFov;
289
456
  // Transform the camera into the mesh's local space (where the node bounds live).
290
- this.computeWorldMatrix(true).invertToRef(TmpInvWorld);
457
+ this.computeWorldMatrix(false).invertToRef(TmpInvWorld);
291
458
  const localCamera = Vector3.TransformCoordinatesToRef(camera.globalPosition, TmpInvWorld, TmpLocalCamera);
292
459
  const px = localCamera.x;
293
460
  const py = localCamera.y;
@@ -340,6 +507,13 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
340
507
  else if (optimalLod > rangeMax) {
341
508
  optimalLod = rangeMax;
342
509
  }
510
+ // Frustum-based LOD bias: nodes outside the camera frustum are pushed to the coarsest allowed
511
+ // level instead of being hidden. They stay in the render/sort set (their splats are off-screen
512
+ // and clipped anyway), so when the camera turns to include them they are already present at low
513
+ // detail with no invisible frames, then refine to the distance-optimal level.
514
+ if (this._frustumCulling && node.inFrustum === false) {
515
+ optimalLod = rangeMax;
516
+ }
343
517
  node.optimalLod = optimalLod;
344
518
  }
345
519
  }
@@ -510,6 +684,9 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
510
684
  node.baseLod = levels[levels.length - 1];
511
685
  node.activeLod = undefined;
512
686
  node.lodCooldown = 0;
687
+ node.inFrustum = true;
688
+ // Local-space bounds for the per-node frustum test; the mesh world matrix is applied per evaluation.
689
+ node.cullBounds = new BoundingInfo(Vector3.FromArray(node.bound.min), Vector3.FromArray(node.bound.max));
513
690
  this._leafNodes.push(node);
514
691
  }
515
692
  /**
@@ -524,27 +701,44 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
524
701
  if (this._disposed) {
525
702
  return;
526
703
  }
527
- // Phase 2: assign fixed work-buffer offsets (environment first, then every file) and allocate.
704
+ // Phase 2: learn the full dataset size (padding + environment + every LOD file). The work buffer is
705
+ // sized to this unless a smaller budget enables eviction-based streaming.
528
706
  // Index 0 is reserved as a never-decoded padding splat: the sort worker and index buffer pad unused
529
707
  // slots with index 0, and leaving that slot zeroed (center.w = 0 => zero covariance, alpha 0) makes
530
708
  // the padding invisible instead of ghosting a copy of the first real splat.
531
- let capacity = 1;
709
+ let fullCapacity = 1;
532
710
  if (envCount > 0) {
533
- this._environmentRange = { offset: capacity, count: envCount };
534
- capacity += envCount;
711
+ fullCapacity += envCount;
535
712
  }
536
713
  for (const fileId of fileIds) {
537
714
  const count = this._fileCounts.get(fileId);
538
- if (count === undefined || count <= 0) {
539
- continue;
715
+ if (count !== undefined && count > 0) {
716
+ fullCapacity += count;
540
717
  }
541
- this._fileBaseSplat.set(fileId, capacity);
542
- capacity += count;
543
718
  }
544
- if (capacity <= 1) {
719
+ if (fullCapacity <= 1) {
545
720
  return;
546
721
  }
722
+ // Eviction streams the dataset through a fixed budget; only enabled when that budget is below the full set.
723
+ this._evictionEnabled = this._residentBudget > 0 && this._residentBudget < fullCapacity;
724
+ const capacity = this._evictionEnabled ? Math.max(this._residentBudget, 1) : fullCapacity;
725
+ this._residency = new GaussianSplattingResidencyController(capacity, this._evictionCooldownFrames, (file) => this._onFileEvicted(file));
726
+ // Pin splat 0 as the invisible padding splat, then the environment (always rendered) — neither is evicted.
727
+ this._residency.pin(PaddingFileId, 1);
728
+ if (envCount > 0) {
729
+ const envOffset = this._residency.pin(EnvironmentFileId, envCount);
730
+ if (envOffset !== null) {
731
+ this._environmentRange = { offset: envOffset, count: envCount };
732
+ }
733
+ else {
734
+ Logger.Warn("GaussianSplattingStream: environment does not fit the memory budget; skipping it.");
735
+ this._environmentFiles = null;
736
+ }
737
+ }
547
738
  this._workBuffer = new GaussianSplattingWorkBuffer(this._scene, capacity);
739
+ // GPU readback is only enabled after it is validated against a CPU decode on the first file (see
740
+ // _probeReadbackAsync); until then positions are decoded on the CPU so there is always a correct result.
741
+ this._readbackCandidate = this._workBuffer.supportsAsyncCentersReadback;
548
742
  const splatPositions = new Float32Array(capacity * 4);
549
743
  const textures = this._workBuffer.textures;
550
744
  this._setExternalWorkBuffer(textures[0], textures[1], textures[2], textures[3], splatPositions, capacity);
@@ -559,7 +753,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
559
753
  const baseFiles = new Set();
560
754
  for (const node of this._leafNodes) {
561
755
  const entry = node.lods[String(node.baseLod)];
562
- if (entry && this._fileBaseSplat.has(entry.file)) {
756
+ if (entry && this._fileCounts.has(entry.file)) {
563
757
  baseFiles.add(entry.file);
564
758
  }
565
759
  }
@@ -606,7 +800,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
606
800
  if (this._metadata.environment) {
607
801
  try {
608
802
  const url = this._rootUrl + this._metadata.environment;
609
- const buffer = (await Tools.LoadFileAsync(url, true));
803
+ const buffer = await this._downloadManager.loadFileAsync(url);
610
804
  const files = await this._unzipAsync(new Uint8Array(buffer));
611
805
  const metaBytes = files.get("meta.json");
612
806
  if (metaBytes) {
@@ -629,8 +823,8 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
629
823
  try {
630
824
  const metaUrl = this._rootUrl + relativePath;
631
825
  const subRootUrl = metaUrl.substring(0, metaUrl.lastIndexOf("/") + 1);
632
- const metaText = (await Tools.LoadFileAsync(metaUrl, false));
633
- const sogData = JSON.parse(metaText);
826
+ const metaBuffer = await this._downloadManager.loadFileAsync(metaUrl);
827
+ const sogData = JSON.parse(new TextDecoder().decode(new Uint8Array(metaBuffer)));
634
828
  this._fileCounts.set(fileId, GaussianSplattingStream._GetSplatCount(sogData));
635
829
  this._fileMeta.set(fileId, { sogData, subRootUrl });
636
830
  }
@@ -670,6 +864,95 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
670
864
  });
671
865
  }
672
866
  }
867
+ /**
868
+ * Writes a decoded splat range's positions into the shared buffer, expands the bounds, and incrementally
869
+ * patches the sort worker.
870
+ * @param positions stride-4 positions for the range
871
+ * @param base first splat index of the range in the work buffer
872
+ * @param count number of splats in the range
873
+ */
874
+ _applyPositions(positions, base, count) {
875
+ this._splatPositions.set(positions, base * 4);
876
+ this._updateBounds(positions, count);
877
+ // Incrementally patch only this range in the sort worker (avoids the full position-buffer re-copy).
878
+ this._postWorkerPositionsRange(base, count);
879
+ }
880
+ /**
881
+ * One-time validation of GPU position readback: reads a sample of the just-decoded range back from the work
882
+ * buffer and compares it to the CPU-decoded positions. Enables {@link _useGpuPositionReadback} only on an
883
+ * exact (within float tolerance) match, so an unsupported or incorrect readback (e.g. a backend without the
884
+ * required texture usage, or an orientation mismatch) safely keeps the CPU decode path.
885
+ * @param base first splat index of the validated range
886
+ * @param count number of splats in the range
887
+ * @param cpuPositions the CPU-decoded stride-4 positions for the range (ground truth)
888
+ */
889
+ async _probeReadbackAsync(base, count, cpuPositions) {
890
+ this._readbackProbed = true;
891
+ if (!this._workBuffer) {
892
+ return;
893
+ }
894
+ const sampleCount = Math.min(count, 1024);
895
+ let ok = false;
896
+ try {
897
+ const gpu = await this._workBuffer.readCentersRangeAsync(base, sampleCount);
898
+ if (this._disposed) {
899
+ return;
900
+ }
901
+ if (gpu && gpu.length >= sampleCount * 4) {
902
+ ok = true;
903
+ for (let i = 0; i < sampleCount && ok; i++) {
904
+ for (let j = 0; j < 3; j++) {
905
+ const a = gpu[i * 4 + j];
906
+ const b = cpuPositions[i * 4 + j];
907
+ if (Math.abs(a - b) > 1e-2 * (1 + Math.abs(b))) {
908
+ ok = false;
909
+ break;
910
+ }
911
+ }
912
+ }
913
+ }
914
+ }
915
+ catch {
916
+ ok = false;
917
+ }
918
+ this._useGpuPositionReadback = ok;
919
+ Logger.Log(ok
920
+ ? "GaussianSplattingStream: GPU position readback validated; streamed LOD positions are read back from the GPU."
921
+ : "GaussianSplattingStream: GPU position readback unavailable; decoding LOD positions on the CPU.");
922
+ }
923
+ /**
924
+ * Resolves the decoded positions for a splat range and applies them. Once GPU readback has been validated,
925
+ * positions are read back from the work buffer (non-blocking) and `pack.positions` is empty; otherwise the
926
+ * CPU-decoded `pack.positions` are used, and — on the first such decode — the GPU readback is validated
927
+ * against them so subsequent decodes can use the fast path.
928
+ * @param pack the parsed SOG pack (its `positions` is populated only on the CPU path)
929
+ * @param base first splat index of the range in the work buffer
930
+ * @param count number of splats in the range
931
+ * @returns whether positions were applied
932
+ */
933
+ async _applyDecodedPositionsAsync(pack, base, count) {
934
+ if (this._useGpuPositionReadback && this._workBuffer) {
935
+ const positions = await this._workBuffer.readCentersRangeAsync(base, count);
936
+ if (this._disposed) {
937
+ return false;
938
+ }
939
+ if (positions && this._splatPositions) {
940
+ this._applyPositions(positions, base, count);
941
+ return true;
942
+ }
943
+ // Validated readback unexpectedly returned nothing; fall through to the (likely empty) CPU positions.
944
+ }
945
+ const cpu = pack.positions.length >= count * 4 ? pack.positions.subarray(0, count * 4) : null;
946
+ if (!cpu || !this._splatPositions) {
947
+ return false;
948
+ }
949
+ this._applyPositions(cpu, base, count);
950
+ // First CPU decode while readback is a candidate: validate it so later decodes can use the fast path.
951
+ if (!this._readbackProbed && this._readbackCandidate) {
952
+ await this._probeReadbackAsync(base, count, cpu);
953
+ }
954
+ return true;
955
+ }
673
956
  /**
674
957
  * Decodes the always-on environment bundle into its work-buffer block and activates its range.
675
958
  */
@@ -679,7 +962,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
679
962
  }
680
963
  const range = this._environmentRange;
681
964
  try {
682
- const parsed = await ParseSogMetaAsTextures(this._environmentFiles, "", this._scene);
965
+ const parsed = await ParseSogMetaAsTextures(this._environmentFiles, "", this._scene, !this._useGpuPositionReadback, this._downloadManager);
683
966
  const pack = parsed.sogTextures;
684
967
  if (!pack) {
685
968
  return;
@@ -692,9 +975,10 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
692
975
  if (this._disposed) {
693
976
  return;
694
977
  }
695
- this._splatPositions.set(pack.positions.subarray(0, range.count * 4), range.offset * 4);
696
- this._updateBounds(pack.positions, range.count);
697
- this._notifyWorkerNewData();
978
+ await this._applyDecodedPositionsAsync(pack, range.offset, range.count);
979
+ if (this._disposed) {
980
+ return;
981
+ }
698
982
  this._refreshActiveRanges();
699
983
  }
700
984
  finally {
@@ -709,52 +993,197 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
709
993
  /**
710
994
  * Loads one LOD source file as GPU textures, decodes it into its fixed work-buffer block, records its
711
995
  * CPU centers for sorting, frees the source textures, then promotes any nodes that were waiting for it.
712
- * Concurrent or repeat requests for the same file are ignored.
996
+ * Concurrent or repeat requests for the same file are ignored. If the file is cancelled mid-flight
997
+ * (because every node that wanted it retargeted), the decode bails cooperatively at the next checkpoint.
713
998
  * @param fileId file index to decode
714
999
  */
715
1000
  async _decodeFileAsync(fileId) {
716
- if (this._decodedFiles.has(fileId) || this._loadingFiles.has(fileId)) {
1001
+ if (this._decodedFiles.has(fileId) || this._loadingFiles.has(fileId) || !this._residency) {
717
1002
  return;
718
1003
  }
719
1004
  const meta = this._fileMeta.get(fileId);
720
- const base = this._fileBaseSplat.get(fileId);
721
1005
  const count = this._fileCounts.get(fileId);
722
- if (!meta || base === undefined || count === undefined) {
1006
+ if (!meta || count === undefined) {
723
1007
  return;
724
1008
  }
725
1009
  this._loadingFiles.add(fileId);
1010
+ this._cancelledDecodes.delete(fileId);
1011
+ let allocated = false;
726
1012
  try {
727
- const parsed = await ParseSogMetaAsTextures(meta.sogData, meta.subRootUrl, this._scene);
1013
+ const parsed = await ParseSogMetaAsTextures(meta.sogData, meta.subRootUrl, this._scene, !this._useGpuPositionReadback, this._downloadManager, fileId);
728
1014
  const pack = parsed.sogTextures;
729
1015
  if (!pack) {
730
1016
  return;
731
1017
  }
1018
+ // Serialize the allocate -> decode -> readback section: a relayout runs only inside it (see
1019
+ // _relayoutAndAllocateAsync), so it never moves a file whose decode has not finished writing.
1020
+ const release = await this._acquireDecodeGateAsync();
732
1021
  try {
733
- if (this._disposed || !this._workBuffer) {
1022
+ if (this._disposed || !this._workBuffer || this._cancelledDecodes.has(fileId)) {
1023
+ return;
1024
+ }
1025
+ let base = this._residency.allocate(fileId, count);
1026
+ if (base === null) {
1027
+ // Defragment the work buffer to reclaim fragmented free space, then retry.
1028
+ base = await this._relayoutAndAllocateAsync(fileId, count);
1029
+ }
1030
+ if (base === null) {
1031
+ // No room even after evicting and compacting: refuse and keep nodes on their current LOD.
1032
+ // A file cancelled mid-flight isn't a budget problem, so don't warn for it.
1033
+ if (!this._cancelledDecodes.has(fileId)) {
1034
+ Logger.Warn(`GaussianSplattingStream: resident memory budget full; skipping LOD file ${fileId}.`);
1035
+ }
1036
+ return;
1037
+ }
1038
+ allocated = true;
1039
+ if (this._disposed || !this._workBuffer || this._cancelledDecodes.has(fileId)) {
734
1040
  return;
735
1041
  }
736
1042
  await this._workBuffer.decodeAsync(pack, base);
1043
+ if (this._disposed || this._cancelledDecodes.has(fileId)) {
1044
+ return;
1045
+ }
1046
+ await this._applyDecodedPositionsAsync(pack, base, count);
737
1047
  if (this._disposed) {
738
1048
  return;
739
1049
  }
740
- this._splatPositions.set(pack.positions.subarray(0, count * 4), base * 4);
741
- this._updateBounds(pack.positions, count);
742
1050
  this._decodedFiles.add(fileId);
743
- this._notifyWorkerNewData();
744
1051
  // Promote any nodes that can now reach their desired LOD via this newly decoded file.
745
1052
  if (this._applyDesiredLods()) {
746
1053
  this._refreshActiveRanges();
747
1054
  }
748
1055
  }
749
1056
  finally {
750
- // Always release the GPU source textures (the decode pass is the only consumer).
751
1057
  GaussianSplattingStream._DisposePack(pack);
1058
+ release();
1059
+ }
1060
+ }
1061
+ catch (e) {
1062
+ // A cancelled file rejects its downloads on purpose — swallow that; re-throw genuine failures.
1063
+ if (!this._cancelledDecodes.has(fileId)) {
1064
+ throw e;
752
1065
  }
753
1066
  }
754
1067
  finally {
1068
+ // If a slot was allocated but the decode did not complete (cancelled/disposed), release it.
1069
+ if (allocated && !this._decodedFiles.has(fileId)) {
1070
+ this._residency.free(fileId);
1071
+ }
755
1072
  this._loadingFiles.delete(fileId);
1073
+ this._cancelledDecodes.delete(fileId);
756
1074
  }
757
1075
  }
1076
+ /**
1077
+ * Acquires the decode gate (a simple async mutex). Resolves once any prior holder releases, returning a
1078
+ * release function the caller must invoke in a `finally`.
1079
+ * @returns the release function
1080
+ */
1081
+ async _acquireDecodeGateAsync() {
1082
+ const previous = this._decodeGate;
1083
+ let release;
1084
+ this._decodeGate = new Promise((resolve) => {
1085
+ release = resolve;
1086
+ });
1087
+ await previous;
1088
+ return release;
1089
+ }
1090
+ /**
1091
+ * Defragments the work buffer to make room for a file that did not fit, then allocates its slot. Runs the
1092
+ * compaction + GPU relayout atomically inside a single `onBeforeRender` so no inconsistent CPU/GPU layout
1093
+ * is ever rendered. Returns the new slot offset, or null if even compaction cannot free enough contiguous
1094
+ * space (the caller refuses the upgrade).
1095
+ * @param fileId file to allocate after compaction
1096
+ * @param count splats the file needs
1097
+ * @returns the allocated offset, or null
1098
+ */
1099
+ async _relayoutAndAllocateAsync(fileId, count) {
1100
+ if (!this._residency || !this._workBuffer) {
1101
+ return null;
1102
+ }
1103
+ // Even a perfect compaction cannot help if the total free space is below what is needed.
1104
+ if (this._residency.freeSize < count) {
1105
+ return null;
1106
+ }
1107
+ return await new Promise((resolve) => {
1108
+ const attempt = () => {
1109
+ // Bail out (no relayout) if the file was cancelled while we waited for the shader to be ready,
1110
+ // so rapidly-changing targets don't trigger an expensive compaction for a file no longer needed.
1111
+ if (this._disposed || !this._residency || !this._workBuffer || this._cancelledDecodes.has(fileId)) {
1112
+ resolve(null);
1113
+ return;
1114
+ }
1115
+ if (!this._workBuffer.isRelayoutReady()) {
1116
+ this._scene.onBeforeRenderObservable.addOnce(attempt);
1117
+ return;
1118
+ }
1119
+ this._performRelayout();
1120
+ resolve(this._residency.allocate(fileId, count));
1121
+ };
1122
+ this._scene.onBeforeRenderObservable.addOnce(attempt);
1123
+ });
1124
+ }
1125
+ /**
1126
+ * Compacts the resident set and relocates the corresponding GPU textures and CPU positions to the new
1127
+ * layout. Must run at a frame-safe point with the work buffer's relayout shader ready.
1128
+ */
1129
+ _performRelayout() {
1130
+ if (!this._residency || !this._workBuffer || !this._splatPositions) {
1131
+ return;
1132
+ }
1133
+ const oldOffsets = this._relayoutOldOffsets;
1134
+ oldOffsets.clear();
1135
+ for (const block of this._residency.getResidentBlocks()) {
1136
+ oldOffsets.set(block.file, block.offset);
1137
+ }
1138
+ const moves = this._residency.compact();
1139
+ if (moves.length === 0) {
1140
+ return;
1141
+ }
1142
+ const capacity = this._residency.capacity;
1143
+ if (!this._relayoutSrcIndex || this._relayoutSrcIndex.length !== capacity) {
1144
+ this._relayoutSrcIndex = new Float32Array(capacity);
1145
+ }
1146
+ const srcIndexByDst = this._relayoutSrcIndex;
1147
+ srcIndexByDst.fill(-1);
1148
+ const resident = this._residency.getResidentBlocks();
1149
+ // Destination->source splat index map for the GPU relayout pass.
1150
+ for (const block of resident) {
1151
+ const oldOffset = oldOffsets.get(block.file);
1152
+ for (let k = 0; k < block.count; k++) {
1153
+ srcIndexByDst[block.offset + k] = oldOffset + k;
1154
+ }
1155
+ }
1156
+ // GPU: relocate the decoded textures in place (same texture instances).
1157
+ this._workBuffer.relayoutSync(srcIndexByDst);
1158
+ // CPU positions: compaction only ever moves a block to a lower offset, so copying in place in ascending
1159
+ // new-offset order is safe (a block's source is never overwritten by an earlier move). This avoids a
1160
+ // full capacity*4 scratch buffer.
1161
+ const positions = this._splatPositions;
1162
+ resident.sort((a, b) => a.offset - b.offset);
1163
+ for (const block of resident) {
1164
+ const oldOffset = oldOffsets.get(block.file);
1165
+ if (oldOffset !== block.offset) {
1166
+ positions.copyWithin(block.offset * 4, oldOffset * 4, (oldOffset + block.count) * 4);
1167
+ }
1168
+ }
1169
+ // Update the environment offset (it may have moved), re-post to the sort worker, and refresh ranges.
1170
+ if (this._environmentRange) {
1171
+ const envOffset = this._residency.offset(EnvironmentFileId);
1172
+ if (envOffset !== undefined) {
1173
+ this._environmentRange.offset = envOffset;
1174
+ }
1175
+ }
1176
+ this._notifyWorkerNewData();
1177
+ this._refreshActiveRanges();
1178
+ }
1179
+ /**
1180
+ * Drops a file evicted by the residency controller from the decoded set so it will be re-decoded on demand.
1181
+ * The file had no remaining references, so no node was rendering or downloading it.
1182
+ * @param fileId evicted file index
1183
+ */
1184
+ _onFileEvicted(fileId) {
1185
+ this._decodedFiles.delete(fileId);
1186
+ }
758
1187
  /**
759
1188
  * Snaps a desired LOD level to the nearest level the node provides, while never selecting a level finer
760
1189
  * than {@link maxDetailLod} (i.e. with an index below the cap). Ties prefer the finer allowed level. If
@@ -793,41 +1222,125 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
793
1222
  }
794
1223
  /**
795
1224
  * Applies each node's {@link ISOGLODNode.targetLevel}: switches a node to its target level when that
796
- * level's file is already decoded, otherwise queues the file and leaves the node on its current LOD (so
797
- * nothing ever disappears). Nodes within their post-switch cooldown are left untouched to damp oscillation.
1225
+ * level's file is already decoded, otherwise records a pending download request for the file and leaves
1226
+ * the node on its current LOD (so nothing ever disappears). Nodes within their post-switch cooldown are
1227
+ * left untouched to damp oscillation (and keep their existing pending request).
1228
+ *
1229
+ * Each node tracks the single file it currently needs but lacks ({@link ISOGLODNode.pendingFile}). When a
1230
+ * node's target changes before that file finished downloading, the old file's reference is released; if no
1231
+ * other node still needs it, its queued/in-flight download is cancelled (see {@link _releaseFileRef}).
798
1232
  * @returns true when at least one node changed LOD (callers should refresh the active ranges)
799
1233
  */
800
1234
  _applyDesiredLods() {
801
1235
  let dirty = false;
802
1236
  for (const node of this._leafNodes) {
1237
+ // Nodes in cooldown keep their current LOD and their existing pending request untouched.
803
1238
  if (node.lodCooldown && node.lodCooldown > 0) {
804
1239
  continue;
805
1240
  }
806
1241
  const desired = node.targetLevel ?? node.baseLod;
807
- if (desired === node.activeLod) {
808
- continue;
809
- }
810
- const entry = node.lods[String(desired)];
811
- if (!entry) {
812
- continue;
813
- }
814
- if (this._decodedFiles.has(entry.file)) {
815
- node.activeLod = desired;
816
- node.lodCooldown = this._lodCooldownFrames;
817
- dirty = true;
1242
+ let newPending;
1243
+ if (desired !== node.activeLod) {
1244
+ const entry = node.lods[String(desired)];
1245
+ if (entry) {
1246
+ if (this._decodedFiles.has(entry.file)) {
1247
+ this._switchActiveFile(node, entry.file);
1248
+ node.activeLod = desired;
1249
+ node.lodCooldown = this._lodCooldownFrames;
1250
+ dirty = true;
1251
+ }
1252
+ else {
1253
+ newPending = entry.file;
1254
+ }
1255
+ }
818
1256
  }
819
- else {
820
- this._enqueueDecode(entry.file);
1257
+ // Reconcile this node's pending-download reference against its (possibly changed) target.
1258
+ if (node.pendingFile !== newPending) {
1259
+ if (node.pendingFile !== undefined) {
1260
+ this._releaseFileRef(node.pendingFile);
1261
+ }
1262
+ if (newPending !== undefined) {
1263
+ this._acquirePendingFile(newPending);
1264
+ }
1265
+ node.pendingFile = newPending;
821
1266
  }
822
1267
  }
823
1268
  return dirty;
824
1269
  }
825
1270
  /**
826
- * Per-frame LOD streaming loop. Ticks cooldowns and pumps the decode queue every frame, but throttles
827
- * the expensive LOD re-evaluation (optimal-LOD computation, budget balancing, desired-LOD application
828
- * and interval rebuild) to run at most every {@link _lodUpdateInterval} frames and only after the camera
829
- * has moved far enough, so continuous camera motion no longer rebuilds the interval set every frame. A
830
- * budget change forces a single immediate update regardless of the throttle.
1271
+ * Moves a node's resident reference from its previous active file to the one it now renders, so the file
1272
+ * count that keeps a block in the work buffer stays accurate (and cancels any pending eviction of the new
1273
+ * file). The new file is already decoded.
1274
+ * @param node leaf node switching its rendered file
1275
+ * @param file the file the node now renders from
1276
+ */
1277
+ _switchActiveFile(node, file) {
1278
+ if (node.activeFile === file) {
1279
+ return;
1280
+ }
1281
+ if (node.activeFile !== undefined) {
1282
+ this._releaseFileRef(node.activeFile);
1283
+ }
1284
+ this._acquireFileRef(file);
1285
+ node.activeFile = file;
1286
+ }
1287
+ /**
1288
+ * Adds a reference to a file (active render or pending download), cancelling any scheduled eviction.
1289
+ * @param fileId file index
1290
+ */
1291
+ _acquireFileRef(fileId) {
1292
+ const refs = (this._fileRefs.get(fileId) ?? 0) + 1;
1293
+ this._fileRefs.set(fileId, refs);
1294
+ if (refs === 1) {
1295
+ // Referenced again before its eviction cooldown elapsed: keep it resident.
1296
+ this._residency?.cancelEviction(fileId);
1297
+ }
1298
+ }
1299
+ /**
1300
+ * Records that a node needs a not-yet-decoded file, bumping its reference count and queueing the decode.
1301
+ * @param fileId file index the node now targets
1302
+ */
1303
+ _acquirePendingFile(fileId) {
1304
+ this._acquireFileRef(fileId);
1305
+ this._enqueueDecode(fileId);
1306
+ }
1307
+ /**
1308
+ * Releases a node's reference to a file. When the last reference is dropped: a decoded file is scheduled
1309
+ * for eviction (when streaming under a budget), and a still-downloading file has its queued decode dropped
1310
+ * and any in-flight download cancelled.
1311
+ * @param fileId file index the node no longer references
1312
+ */
1313
+ _releaseFileRef(fileId) {
1314
+ const refs = (this._fileRefs.get(fileId) ?? 0) - 1;
1315
+ if (refs > 0) {
1316
+ this._fileRefs.set(fileId, refs);
1317
+ return;
1318
+ }
1319
+ this._fileRefs.delete(fileId);
1320
+ if (this._decodedFiles.has(fileId)) {
1321
+ // No node renders it anymore: schedule eviction (only when streaming under a budget).
1322
+ if (this._evictionEnabled) {
1323
+ this._residency?.scheduleEviction(fileId);
1324
+ }
1325
+ return;
1326
+ }
1327
+ // Still downloading/queued: drop the queued decode and cancel any in-flight download.
1328
+ const queueIndex = this._decodeQueue.indexOf(fileId);
1329
+ if (queueIndex !== -1) {
1330
+ this._decodeQueue.splice(queueIndex, 1);
1331
+ }
1332
+ if (this._loadingFiles.has(fileId)) {
1333
+ // Flag the in-flight decode to bail at its next checkpoint and cancel its image downloads.
1334
+ this._cancelledDecodes.add(fileId);
1335
+ this._downloadManager.cancelGroup(fileId);
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Per-frame LOD streaming loop. Ticks cooldowns and pumps the decode queue every frame, and runs the
1340
+ * cheap per-node frustum test every frame so the off-screen LOD bias tracks camera rotation. The LOD
1341
+ * re-evaluation is throttled to at most every {@link _lodUpdateInterval} frames once the camera has
1342
+ * translated far enough, but also runs immediately whenever a node enters/leaves the frustum (so its
1343
+ * detail upgrades/downgrades promptly) or a cap change forces it. Active ranges rebuild on any LOD change.
831
1344
  */
832
1345
  _onLodFrame() {
833
1346
  if (this._disposed || !this._baseLayerReady) {
@@ -838,29 +1351,70 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
838
1351
  node.lodCooldown--;
839
1352
  }
840
1353
  }
1354
+ // Tick eviction cooldowns: unreferenced files are freed once their cooldown elapses (budgeted streaming).
1355
+ if (this._evictionEnabled) {
1356
+ this._residency?.tick();
1357
+ }
841
1358
  // In-flight/queued decodes still progress every frame.
842
1359
  this._pumpDecodeQueue();
843
- const forced = this._forceLodUpdate;
844
- if (!forced) {
845
- if (++this._framesSinceLodUpdate < this._lodUpdateInterval) {
846
- return;
847
- }
1360
+ // Per-node frustum test runs every frame (cheap) so the off-screen LOD bias tracks camera rotation,
1361
+ // not just the translation that gates the throttled LOD re-evaluation below.
1362
+ const frustumChanged = this._updateNodeFrustum();
1363
+ let runLodEval = this._forceLodUpdate || frustumChanged;
1364
+ if (!runLodEval && ++this._framesSinceLodUpdate >= this._lodUpdateInterval) {
848
1365
  const camera = this._scene.activeCamera;
849
- if (camera) {
850
- const threshold = this._lodUpdateDistance;
851
- if (Vector3.DistanceSquared(camera.globalPosition, this._lastLodCamPos) < threshold * threshold) {
852
- return;
1366
+ const threshold = this._lodUpdateDistance;
1367
+ if (!camera || Vector3.DistanceSquared(camera.globalPosition, this._lastLodCamPos) >= threshold * threshold) {
1368
+ if (camera) {
1369
+ this._lastLodCamPos.copyFrom(camera.globalPosition);
853
1370
  }
854
- this._lastLodCamPos.copyFrom(camera.globalPosition);
1371
+ runLodEval = true;
855
1372
  }
856
1373
  }
857
- this._forceLodUpdate = false;
858
- this._framesSinceLodUpdate = 0;
859
- this.evaluateOptimalLods(this._scene.activeCamera);
860
- this._computeTargetLevels();
861
- if (this._applyDesiredLods()) {
862
- this._refreshActiveRanges();
1374
+ if (runLodEval) {
1375
+ this._forceLodUpdate = false;
1376
+ this._framesSinceLodUpdate = 0;
1377
+ this.evaluateOptimalLods(this._scene.activeCamera);
1378
+ this._computeTargetLevels();
1379
+ if (this._applyDesiredLods()) {
1380
+ this._refreshActiveRanges();
1381
+ }
1382
+ }
1383
+ }
1384
+ /**
1385
+ * Updates each leaf node's {@link ISOGLODNode.inFrustum} flag from a per-node frustum test against the
1386
+ * active camera. When {@link frustumCulling} is disabled (or there is no camera) every node is marked
1387
+ * in-frustum. Bounds are static (from the LOD tree), so flags are valid for all nodes regardless of
1388
+ * decode state. Returns true when any node's in-frustum state changed (so the LOD bias must be re-applied).
1389
+ * @returns whether any node's in-frustum state changed
1390
+ */
1391
+ _updateNodeFrustum() {
1392
+ const camera = this._scene.activeCamera;
1393
+ let changed = false;
1394
+ if (!this._frustumCulling || !camera) {
1395
+ for (const node of this._leafNodes) {
1396
+ if (node.inFrustum === false) {
1397
+ node.inFrustum = true;
1398
+ changed = true;
1399
+ }
1400
+ }
1401
+ return changed;
1402
+ }
1403
+ // World-space frustum planes from the current view-projection, tested against each node's world AABB.
1404
+ // force=false uses the renderId/sync fast-path (still recomputes when the transform actually changed),
1405
+ // avoiding a full world-matrix recompute every frame for the per-node frustum test.
1406
+ const world = this.computeWorldMatrix(false);
1407
+ camera.getViewMatrix().multiplyToRef(camera.getProjectionMatrix(), this._cullViewProj);
1408
+ Frustum.GetPlanesToRef(this._cullViewProj, this._frustumPlanes);
1409
+ for (const node of this._leafNodes) {
1410
+ node.cullBounds.update(world);
1411
+ const inFrustum = node.cullBounds.isInFrustum(this._frustumPlanes);
1412
+ if (inFrustum !== node.inFrustum) {
1413
+ node.inFrustum = inFrustum;
1414
+ changed = true;
1415
+ }
863
1416
  }
1417
+ return changed;
864
1418
  }
865
1419
  /**
866
1420
  * Reads the splat count from SOG metadata.
@@ -919,7 +1473,7 @@ export class GaussianSplattingStream extends GaussianSplattingMesh {
919
1473
  if (!entry) {
920
1474
  continue;
921
1475
  }
922
- const base = this._fileBaseSplat.get(entry.file);
1476
+ const base = this._residency?.offset(entry.file);
923
1477
  if (base === undefined) {
924
1478
  continue;
925
1479
  }