@woosh/meep-engine 3.23.0 → 3.25.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 (812) hide show
  1. package/editor/entity_world_bounds.d.ts +9 -3
  2. package/editor/entity_world_bounds.d.ts.map +1 -1
  3. package/editor/entity_world_bounds.js +61 -4
  4. package/package.json +1 -1
  5. package/src/core/binary/allocator/OffsetAllocator.d.ts +40 -2
  6. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  7. package/src/core/binary/allocator/OffsetAllocator.js +124 -29
  8. package/src/core/collection/map/u32_index.d.ts +83 -0
  9. package/src/core/collection/map/u32_index.d.ts.map +1 -0
  10. package/src/core/collection/map/u32_index.js +247 -0
  11. package/src/engine/asset/loaders/GLTFSceneBundleAssetLoader.d.ts +2 -2
  12. package/src/engine/asset/loaders/GLTFSceneBundleAssetLoader.js +2 -2
  13. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.d.ts +1 -2
  14. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.d.ts.map +1 -1
  15. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.js +4 -12
  16. package/src/engine/ecs/gpu/GPUStateAuthority.d.ts +55 -0
  17. package/src/engine/ecs/gpu/GPUStateAuthority.d.ts.map +1 -0
  18. package/src/engine/ecs/gpu/GPUStateAuthority.js +72 -0
  19. package/src/engine/ecs/gpu/GPUStateAuthorityFlags.d.ts +40 -0
  20. package/src/engine/ecs/gpu/GPUStateAuthorityFlags.d.ts.map +1 -0
  21. package/src/engine/ecs/gpu/GPUStateAuthorityFlags.js +79 -0
  22. package/src/engine/ecs/gpu/gpu_authority_assert_cpu.d.ts +18 -0
  23. package/src/engine/ecs/gpu/gpu_authority_assert_cpu.d.ts.map +1 -0
  24. package/src/engine/ecs/gpu/gpu_authority_assert_cpu.js +27 -0
  25. package/src/engine/ecs/gpu/gpu_authority_declare.d.ts +23 -0
  26. package/src/engine/ecs/gpu/gpu_authority_declare.d.ts.map +1 -0
  27. package/src/engine/ecs/gpu/gpu_authority_declare.js +39 -0
  28. package/src/engine/ecs/gpu/gpu_authority_grant.d.ts +15 -0
  29. package/src/engine/ecs/gpu/gpu_authority_grant.d.ts.map +1 -0
  30. package/src/engine/ecs/gpu/gpu_authority_grant.js +38 -0
  31. package/src/engine/ecs/gpu/gpu_authority_has.d.ts +18 -0
  32. package/src/engine/ecs/gpu/gpu_authority_has.d.ts.map +1 -0
  33. package/src/engine/ecs/gpu/gpu_authority_has.js +28 -0
  34. package/src/engine/ecs/gpu/gpu_authority_revoke.d.ts +24 -0
  35. package/src/engine/ecs/gpu/gpu_authority_revoke.d.ts.map +1 -0
  36. package/src/engine/ecs/gpu/gpu_authority_revoke.js +49 -0
  37. package/src/engine/ecs/ik/IKProblem.d.ts +14 -0
  38. package/src/engine/ecs/ik/IKProblem.d.ts.map +1 -1
  39. package/src/engine/ecs/ik/IKProblem.js +16 -0
  40. package/src/engine/ecs/ik/InverseKinematicsSystem.d.ts.map +1 -1
  41. package/src/engine/ecs/ik/InverseKinematicsSystem.js +19 -0
  42. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +2 -2
  43. package/src/engine/ecs/ik/TwoBoneInverseKinematicsSolver.js +2 -2
  44. package/src/engine/ecs/ik/ik_bone_chain.d.ts +11 -2
  45. package/src/engine/ecs/ik/ik_bone_chain.d.ts.map +1 -1
  46. package/src/engine/ecs/ik/ik_bone_chain.js +47 -2
  47. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts.map +1 -1
  48. package/src/engine/ecs/ik/ik_write_local_rotation.js +6 -0
  49. package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
  50. package/src/engine/ecs/speaker/VoiceSystem.js +6 -0
  51. package/src/engine/ecs/transform/T64WorldCache.d.ts +50 -0
  52. package/src/engine/ecs/transform/T64WorldCache.d.ts.map +1 -0
  53. package/src/engine/ecs/transform/T64WorldCache.js +139 -0
  54. package/src/engine/ecs/transform/t64_evaluate_world.d.ts +31 -0
  55. package/src/engine/ecs/transform/t64_evaluate_world.d.ts.map +1 -0
  56. package/src/engine/ecs/transform/t64_evaluate_world.js +126 -0
  57. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts +8 -0
  58. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts.map +1 -1
  59. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js +59 -30
  60. package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.d.ts.map +1 -1
  61. package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.js +4 -11
  62. package/src/engine/graphics/ecs/mesh-v2/SHADED_GEOMETRY_EVENT_CHANGE.d.ts +14 -0
  63. package/src/engine/graphics/ecs/mesh-v2/SHADED_GEOMETRY_EVENT_CHANGE.d.ts.map +1 -0
  64. package/src/engine/graphics/ecs/mesh-v2/SHADED_GEOMETRY_EVENT_CHANGE.js +13 -0
  65. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.d.ts +2 -14
  66. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.d.ts.map +1 -1
  67. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +11 -21
  68. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.d.ts +1 -0
  69. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometryFlags.js +7 -0
  70. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.d.ts +5 -6
  71. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.d.ts.map +1 -1
  72. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.js +13 -11
  73. package/src/engine/graphics/ecs/mesh-v2/shaded_geometry_announce_change.d.ts +14 -0
  74. package/src/engine/graphics/ecs/mesh-v2/shaded_geometry_announce_change.d.ts.map +1 -0
  75. package/src/engine/graphics/ecs/mesh-v2/shaded_geometry_announce_change.js +21 -0
  76. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +1 -1
  77. package/src/engine/graphics/ecs/particles/ParticleEffect.js +1 -1
  78. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.d.ts +1 -1
  79. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.js +1 -1
  80. package/src/engine/graphics3/AnimationGraphSystem.js +3 -3
  81. package/src/engine/graphics3/AnimationSystem.js +2 -2
  82. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  83. package/src/engine/graphics3/DebugDrawSystem.js +2 -4
  84. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +34 -27
  85. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  86. package/src/engine/graphics3/GPUParticleEmitterSystem.js +185 -85
  87. package/src/engine/graphics3/HighlightOutlineSystem.d.ts +4 -3
  88. package/src/engine/graphics3/HighlightOutlineSystem.d.ts.map +1 -1
  89. package/src/engine/graphics3/HighlightOutlineSystem.js +7 -9
  90. package/src/engine/graphics3/LightSystem.d.ts +7 -5
  91. package/src/engine/graphics3/LightSystem.d.ts.map +1 -1
  92. package/src/engine/graphics3/LightSystem.js +36 -22
  93. package/src/engine/graphics3/MeshSystem.d.ts +43 -39
  94. package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
  95. package/src/engine/graphics3/MeshSystem.js +324 -331
  96. package/src/engine/graphics3/PathDisplaySystem.d.ts.map +1 -1
  97. package/src/engine/graphics3/PathDisplaySystem.js +2 -4
  98. package/src/engine/graphics3/PickingQuery.d.ts +15 -21
  99. package/src/engine/graphics3/PickingQuery.d.ts.map +1 -1
  100. package/src/engine/graphics3/PickingQuery.js +21 -25
  101. package/src/engine/graphics3/PickingSystem.d.ts +60 -31
  102. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  103. package/src/engine/graphics3/PickingSystem.js +413 -273
  104. package/src/engine/graphics3/ShadedGeometrySystem.d.ts +22 -12
  105. package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -1
  106. package/src/engine/graphics3/ShadedGeometrySystem.js +309 -174
  107. package/src/engine/graphics3/TerrainSystem.d.ts +19 -11
  108. package/src/engine/graphics3/TerrainSystem.d.ts.map +1 -1
  109. package/src/engine/graphics3/TerrainSystem.js +102 -53
  110. package/src/engine/graphics3/TooltipComponentSystem.d.ts.map +1 -1
  111. package/src/engine/graphics3/TooltipComponentSystem.js +2 -14
  112. package/src/engine/graphics3/Trail3DSystem.d.ts.map +1 -1
  113. package/src/engine/graphics3/Trail3DSystem.js +2 -4
  114. package/src/engine/graphics3/animation/ClipListPlayer.d.ts +4 -4
  115. package/src/engine/graphics3/animation/ClipListPlayer.d.ts.map +1 -1
  116. package/src/engine/graphics3/animation/ClipListPlayer.js +2 -2
  117. package/src/engine/graphics3/highlight/pack_highlight_table.d.ts +9 -11
  118. package/src/engine/graphics3/highlight/pack_highlight_table.d.ts.map +1 -1
  119. package/src/engine/graphics3/highlight/pack_highlight_table.js +11 -18
  120. package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts +4 -3
  121. package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts.map +1 -1
  122. package/src/engine/graphics3/pose/query_entity_node_world_pose.js +75 -74
  123. package/src/engine/graphics3/prefab/Prefab.d.ts +130 -0
  124. package/src/engine/graphics3/prefab/Prefab.d.ts.map +1 -0
  125. package/src/engine/graphics3/prefab/Prefab.js +162 -0
  126. package/src/engine/graphics3/prefab/PrefabInstance.d.ts +72 -0
  127. package/src/engine/graphics3/prefab/PrefabInstance.d.ts.map +1 -0
  128. package/src/engine/graphics3/prefab/PrefabInstance.js +80 -0
  129. package/src/engine/graphics3/prefab/prefab_compile.d.ts +23 -0
  130. package/src/engine/graphics3/prefab/prefab_compile.d.ts.map +1 -0
  131. package/src/engine/graphics3/prefab/prefab_compile.js +334 -0
  132. package/src/engine/graphics3/prefab/prefab_instantiate.d.ts +46 -0
  133. package/src/engine/graphics3/prefab/prefab_instantiate.d.ts.map +1 -0
  134. package/src/engine/graphics3/prefab/prefab_instantiate.js +230 -0
  135. package/src/engine/graphics3/prefab/prefab_normalize_skin_frames.d.ts +70 -0
  136. package/src/engine/graphics3/prefab/prefab_normalize_skin_frames.d.ts.map +1 -0
  137. package/src/engine/graphics3/prefab/prefab_normalize_skin_frames.js +210 -0
  138. package/src/engine/graphics3/prefab/prefab_place.d.ts +60 -0
  139. package/src/engine/graphics3/prefab/prefab_place.d.ts.map +1 -0
  140. package/src/engine/graphics3/prefab/prefab_place.js +130 -0
  141. package/src/engine/graphics3/prefab/prefab_skin_clip_scale_conflicts.d.ts +60 -0
  142. package/src/engine/graphics3/prefab/prefab_skin_clip_scale_conflicts.d.ts.map +1 -0
  143. package/src/engine/graphics3/prefab/prefab_skin_clip_scale_conflicts.js +128 -0
  144. package/src/engine/graphics3/prefab/prefab_skin_frame.d.ts +40 -0
  145. package/src/engine/graphics3/prefab/prefab_skin_frame.d.ts.map +1 -0
  146. package/src/engine/graphics3/prefab/prefab_skin_frame.js +116 -0
  147. package/src/engine/graphics3/preview/make_model_preview_scene.d.ts +16 -14
  148. package/src/engine/graphics3/preview/make_model_preview_scene.d.ts.map +1 -1
  149. package/src/engine/graphics3/preview/make_model_preview_scene.js +126 -154
  150. package/src/engine/graphics3/preview/make_model_thumbnail.js +1 -1
  151. package/src/engine/graphics3/register_instance_animation.d.ts +6 -6
  152. package/src/engine/graphics3/register_instance_animation.d.ts.map +1 -1
  153. package/src/engine/graphics3/register_instance_animation.js +4 -4
  154. package/src/engine/graphics3/shade_bundle_bounds.d.ts +2 -6
  155. package/src/engine/graphics3/shade_bundle_bounds.d.ts.map +1 -1
  156. package/src/engine/graphics3/shade_bundle_bounds.js +47 -55
  157. package/src/engine/graphics3/shader_picking_gather.d.ts +40 -0
  158. package/src/engine/graphics3/shader_picking_gather.d.ts.map +1 -0
  159. package/src/engine/graphics3/shader_picking_gather.js +120 -0
  160. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts +11 -18
  161. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts.map +1 -1
  162. package/src/engine/graphics3/terrain/pack_terrain_row_table.js +48 -53
  163. package/src/engine/physics/cloth/MEASUREMENTS.md +1050 -0
  164. package/src/engine/physics/cloth/PLAN.md +411 -49
  165. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts +17 -2
  166. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
  167. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +284 -256
  168. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts +52 -0
  169. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts.map +1 -0
  170. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.js +70 -0
  171. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts +12 -3
  172. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  173. package/src/engine/physics/cloth/ecs/ClothDynamics.js +340 -331
  174. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +49 -37
  175. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  176. package/src/engine/physics/cloth/ecs/ClothInstance.js +50 -37
  177. package/src/engine/physics/cloth/ecs/ClothRig.d.ts +7 -8
  178. package/src/engine/physics/cloth/ecs/ClothRig.d.ts.map +1 -1
  179. package/src/engine/physics/cloth/ecs/ClothRig.js +7 -8
  180. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts +84 -0
  181. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts.map +1 -0
  182. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.js +141 -0
  183. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +51 -81
  184. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  185. package/src/engine/physics/cloth/ecs/ClothSystem.js +81 -870
  186. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts +48 -0
  187. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts.map +1 -0
  188. package/src/engine/physics/cloth/ecs/ClothWorkerCore.js +107 -0
  189. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +286 -0
  190. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -0
  191. package/src/engine/physics/cloth/ecs/ClothWorld.js +1104 -0
  192. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +182 -0
  193. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -0
  194. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +562 -0
  195. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts +2 -0
  196. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts.map +1 -0
  197. package/src/engine/physics/cloth/ecs/cloth.worker.js +12 -0
  198. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  199. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +2 -1
  200. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +58 -0
  201. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  202. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +341 -271
  203. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +20 -2
  204. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  205. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +39 -15
  206. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -23
  207. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  208. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +201 -199
  209. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  210. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +8 -0
  211. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts +29 -0
  212. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts.map +1 -0
  213. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.js +78 -0
  214. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts +18 -34
  215. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts.map +1 -1
  216. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.js +58 -115
  217. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts +17 -0
  218. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts.map +1 -0
  219. package/src/engine/physics/cloth/ecs/makeClothWorker.js +18 -0
  220. package/src/engine/physics/cloth/playground/README.md +84 -13
  221. package/src/engine/physics/cloth/playground/garment_build.d.ts +0 -1
  222. package/src/engine/physics/cloth/playground/garment_build.d.ts.map +1 -1
  223. package/src/engine/physics/cloth/playground/garment_build.js +64 -58
  224. package/src/engine/physics/cloth/playground/garment_main.js +59 -33
  225. package/src/engine/physics/cloth/playground/wind.html +166 -0
  226. package/src/engine/physics/cloth/playground/wind_build.d.ts +81 -0
  227. package/src/engine/physics/cloth/playground/wind_build.d.ts.map +1 -0
  228. package/src/engine/physics/cloth/playground/wind_build.js +141 -0
  229. package/src/engine/physics/cloth/playground/wind_main.d.ts +2 -0
  230. package/src/engine/physics/cloth/playground/wind_main.d.ts.map +1 -0
  231. package/src/engine/physics/cloth/playground/wind_main.js +681 -0
  232. package/src/engine/physics/cloth/solver/ClothState.d.ts +134 -79
  233. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  234. package/src/engine/physics/cloth/solver/ClothState.js +273 -12
  235. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +33 -0
  236. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  237. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +34 -0
  238. package/src/engine/physics/cloth/solver/cloth_step.d.ts +21 -2
  239. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  240. package/src/engine/physics/cloth/solver/cloth_step.js +173 -4
  241. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +40 -0
  242. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -0
  243. package/src/engine/physics/cloth/solver/cloth_step_instance.js +284 -0
  244. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts +107 -0
  245. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts.map +1 -0
  246. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.js +263 -0
  247. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +88 -0
  248. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -0
  249. package/src/engine/physics/cloth/wind/AbstractClothWind.js +100 -0
  250. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +85 -0
  251. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -0
  252. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +182 -0
  253. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +79 -0
  254. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -0
  255. package/src/engine/physics/cloth/wind/ClothFluidWind.js +210 -0
  256. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +101 -0
  257. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -0
  258. package/src/engine/physics/cloth/wind/cloth_sample_air.js +290 -0
  259. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  260. package/src/shade/RENDERER_CONTRACT.md +85 -53
  261. package/src/shade/device/ShadeGPUCommandContext.d.ts +23 -0
  262. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  263. package/src/shade/device/ShadeGPUCommandContext.js +36 -0
  264. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  265. package/src/shade/device/mock/texture_copy.js +28 -0
  266. package/src/shade/playground/add_random_meshes.d.ts +4 -4
  267. package/src/shade/playground/add_random_meshes.d.ts.map +1 -1
  268. package/src/shade/playground/add_random_meshes.js +20 -16
  269. package/src/shade/playground/avboit/main.d.ts +1 -0
  270. package/src/shade/playground/avboit/main.d.ts.map +1 -1
  271. package/src/shade/playground/avboit/main.js +916 -875
  272. package/src/shade/playground/basis_textures/main.d.ts.map +1 -1
  273. package/src/shade/playground/basis_textures/main.js +22 -8
  274. package/src/shade/playground/basis_textures/texture_inventory.d.ts +2 -2
  275. package/src/shade/playground/basis_textures/texture_inventory.d.ts.map +1 -1
  276. package/src/shade/playground/basis_textures/texture_inventory.js +6 -7
  277. package/src/shade/playground/ground_seam/main.d.ts.map +1 -1
  278. package/src/shade/playground/ground_seam/main.js +13 -4
  279. package/src/shade/playground/ground_seam/make_ground.d.ts +13 -4
  280. package/src/shade/playground/ground_seam/make_ground.d.ts.map +1 -1
  281. package/src/shade/playground/ground_seam/make_ground.js +21 -16
  282. package/src/shade/playground/particle_ecs/bonfire_editor.js +3 -3
  283. package/src/shade/playground/particle_system/main.js +11 -10
  284. package/src/shade/playground/particle_system/particle_prototype.d.ts +15 -10
  285. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
  286. package/src/shade/playground/particle_system/particle_prototype.js +56 -15
  287. package/src/shade/playground/particle_system/particle_scene.d.ts +68 -28
  288. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  289. package/src/shade/playground/particle_system/particle_scene.js +149 -53
  290. package/src/shade/playground/readback_crossover/index.html +41 -0
  291. package/src/shade/playground/readback_crossover/main.d.ts +2 -0
  292. package/src/shade/playground/readback_crossover/main.d.ts.map +1 -0
  293. package/src/shade/playground/readback_crossover/main.js +372 -0
  294. package/src/shade/playground/skinned_blas_refit/README.md +11 -7
  295. package/src/shade/playground/skinned_blas_refit/add_floor.d.ts +13 -44
  296. package/src/shade/playground/skinned_blas_refit/add_floor.d.ts.map +1 -1
  297. package/src/shade/playground/skinned_blas_refit/add_floor.js +19 -59
  298. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  299. package/src/shade/playground/skinned_blas_refit/main.js +67 -58
  300. package/src/shade/playground/skinned_blas_refit/place_character.d.ts +40 -54
  301. package/src/shade/playground/skinned_blas_refit/place_character.d.ts.map +1 -1
  302. package/src/shade/playground/skinned_blas_refit/place_character.js +47 -74
  303. package/src/shade/playground/skinned_blas_refit/sweep_views.d.ts +3 -3
  304. package/src/shade/playground/skinned_blas_refit/sweep_views.d.ts.map +1 -1
  305. package/src/shade/playground/skinned_blas_refit/sweep_views.js +4 -3
  306. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.d.ts +3 -3
  307. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.d.ts.map +1 -1
  308. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.js +11 -4
  309. package/src/shade/playground/skinned_blas_refit/verify_instance_bounds.d.ts.map +1 -1
  310. package/src/shade/playground/skinned_blas_refit/verify_instance_bounds.js +38 -9
  311. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.d.ts +3 -3
  312. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.d.ts.map +1 -1
  313. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.js +9 -6
  314. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.d.ts +3 -3
  315. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.d.ts.map +1 -1
  316. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.js +3 -3
  317. package/src/shade/playground/skinned_mesh_soup/README.md +6 -6
  318. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
  319. package/src/shade/playground/skinned_mesh_soup/main.js +44 -102
  320. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +5 -5
  321. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  322. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +42 -33
  323. package/src/shade/playground/sponza_path_trace/main.d.ts +1 -0
  324. package/src/shade/playground/sponza_path_trace/main.d.ts.map +1 -1
  325. package/src/shade/playground/sponza_path_trace/main.js +393 -363
  326. package/src/shade/playground/ssr_reprojection/main.js +11 -9
  327. package/src/shade/playground/ssr_reprojection/scene.d.ts +15 -4
  328. package/src/shade/playground/ssr_reprojection/scene.d.ts.map +1 -1
  329. package/src/shade/playground/ssr_reprojection/scene.js +32 -11
  330. package/src/shade/playground/ssr_variance/main.js +1 -1
  331. package/src/shade/playground/ssr_variance/scene.d.ts +10 -4
  332. package/src/shade/playground/ssr_variance/scene.d.ts.map +1 -1
  333. package/src/shade/playground/ssr_variance/scene.js +39 -15
  334. package/src/shade/playground/vgeo_runtime/README.md +94 -0
  335. package/src/shade/playground/vgeo_runtime/index.html +62 -0
  336. package/src/shade/playground/vgeo_runtime/main.d.ts +2 -0
  337. package/src/shade/playground/vgeo_runtime/main.d.ts.map +1 -0
  338. package/src/shade/playground/vgeo_runtime/main.js +399 -0
  339. package/src/shade/playground/vgeo_viewer/main.js +6 -2
  340. package/src/shade/playground/vgeo_viewer/sample_asset.js +1 -1
  341. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  342. package/src/shade/playground/volumetrics_froxel/main.js +936 -922
  343. package/src/shade/renderer/GraphicsContext.d.ts +7 -0
  344. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  345. package/src/shade/renderer/GraphicsContext.js +11 -0
  346. package/src/shade/renderer/Renderer.d.ts +3 -2
  347. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  348. package/src/shade/renderer/Renderer.js +3 -2
  349. package/src/shade/renderer/animation/GPUAnimationManager.d.ts +39 -47
  350. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  351. package/src/shade/renderer/animation/GPUAnimationManager.js +319 -256
  352. package/src/shade/renderer/animation/ShadeAnimationChannel.d.ts +26 -22
  353. package/src/shade/renderer/animation/ShadeAnimationChannel.d.ts.map +1 -1
  354. package/src/shade/renderer/animation/ShadeAnimationChannel.js +28 -23
  355. package/src/shade/renderer/animation/ShadeAnimationClip.d.ts +14 -6
  356. package/src/shade/renderer/animation/ShadeAnimationClip.d.ts.map +1 -1
  357. package/src/shade/renderer/animation/ShadeAnimationClip.js +20 -8
  358. package/src/shade/renderer/animation/Skin.d.ts +19 -19
  359. package/src/shade/renderer/animation/Skin.d.ts.map +1 -1
  360. package/src/shade/renderer/animation/Skin.js +15 -13
  361. package/src/shade/renderer/animation/pose/pose_evaluate_local.d.ts +6 -5
  362. package/src/shade/renderer/animation/pose/pose_evaluate_local.d.ts.map +1 -1
  363. package/src/shade/renderer/animation/pose/pose_evaluate_local.js +9 -9
  364. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +5 -59
  365. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -1
  366. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +8 -132
  367. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts +65 -62
  368. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  369. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +148 -194
  370. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.d.ts.map +1 -1
  371. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.js +4 -7
  372. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +2 -2
  373. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +2 -2
  374. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.d.ts +74 -0
  375. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.d.ts.map +1 -0
  376. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.js +240 -0
  377. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts +14 -0
  378. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  379. package/src/shade/renderer/buffer/table/GPUDatabase.js +64 -6
  380. package/src/shade/renderer/buffer/table/GPUDatabaseReadback.d.ts +190 -0
  381. package/src/shade/renderer/buffer/table/GPUDatabaseReadback.d.ts.map +1 -0
  382. package/src/shade/renderer/buffer/table/GPUDatabaseReadback.js +1323 -0
  383. package/src/shade/renderer/buffer/table/GPUPageMetadata.d.ts +10 -0
  384. package/src/shade/renderer/buffer/table/GPUPageMetadata.d.ts.map +1 -1
  385. package/src/shade/renderer/buffer/table/GPUPageMetadata.js +12 -0
  386. package/src/shade/renderer/buffer/table/GPUReadbackStatus.d.ts +15 -0
  387. package/src/shade/renderer/buffer/table/GPUReadbackStatus.d.ts.map +1 -0
  388. package/src/shade/renderer/buffer/table/GPUReadbackStatus.js +34 -0
  389. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts +75 -7
  390. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  391. package/src/shade/renderer/buffer/table/GPUTypedTable.js +518 -135
  392. package/src/shade/renderer/buffer/table/GPU_TABLE_GATHER_DESCRIPTOR_WORDS.d.ts +19 -0
  393. package/src/shade/renderer/buffer/table/GPU_TABLE_GATHER_DESCRIPTOR_WORDS.d.ts.map +1 -0
  394. package/src/shade/renderer/buffer/table/GPU_TABLE_GATHER_DESCRIPTOR_WORDS.js +18 -0
  395. package/src/shade/renderer/buffer/table/WordViews.d.ts +39 -0
  396. package/src/shade/renderer/buffer/table/WordViews.d.ts.map +1 -0
  397. package/src/shade/renderer/buffer/table/WordViews.js +62 -0
  398. package/src/shade/renderer/buffer/table/gpu_table_gather_shader.d.ts +27 -0
  399. package/src/shade/renderer/buffer/table/gpu_table_gather_shader.d.ts.map +1 -0
  400. package/src/shade/renderer/buffer/table/gpu_table_gather_shader.js +117 -0
  401. package/src/shade/renderer/buffer/table/serde/generate_record_writer.d.ts +121 -0
  402. package/src/shade/renderer/buffer/table/serde/generate_record_writer.d.ts.map +1 -0
  403. package/src/shade/renderer/buffer/table/serde/generate_record_writer.js +395 -0
  404. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.d.ts.map +1 -1
  405. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.js +70 -20
  406. package/src/shade/renderer/camera/Camera.d.ts +14 -0
  407. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  408. package/src/shade/renderer/camera/Camera.js +7 -1
  409. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +1 -1
  410. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.d.ts.map +1 -1
  411. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.js +16 -1
  412. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  413. package/src/shade/renderer/geometry/GPUGeometryManager.js +29 -9
  414. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +10 -0
  415. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  416. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +13 -0
  417. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  418. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +6 -5
  419. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +3 -3
  420. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  421. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +15 -11
  422. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +46 -18
  423. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +70 -22
  424. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +97 -19
  425. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +12 -2
  426. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  427. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +14 -3
  428. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +2 -2
  429. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  430. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +124 -6
  431. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +22 -0
  432. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +1 -0
  433. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.js +53 -0
  434. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +8 -1
  435. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  436. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +8 -1
  437. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts +16 -0
  438. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts.map +1 -0
  439. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.js +15 -0
  440. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts +54 -0
  441. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts.map +1 -0
  442. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.js +53 -0
  443. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +13 -2
  444. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  445. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +14 -2
  446. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +100 -0
  447. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  448. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +113 -0
  449. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  450. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +5 -0
  451. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  452. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +108 -0
  453. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  454. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +23 -0
  455. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  456. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +40 -0
  457. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +228 -0
  458. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -0
  459. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +932 -0
  460. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +176 -0
  461. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -0
  462. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +425 -0
  463. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +241 -0
  464. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +574 -0
  465. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +120 -0
  466. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -0
  467. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +674 -0
  468. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +287 -0
  469. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -0
  470. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +976 -0
  471. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +42 -0
  472. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -0
  473. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +96 -0
  474. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts +16 -0
  475. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts.map +1 -0
  476. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.js +15 -0
  477. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts +19 -0
  478. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -0
  479. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +83 -0
  480. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts +9 -0
  481. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts.map +1 -0
  482. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.js +8 -0
  483. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +19 -0
  484. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -0
  485. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +160 -0
  486. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts +3 -0
  487. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -0
  488. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +255 -0
  489. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts +3 -0
  490. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -0
  491. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +80 -0
  492. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts +3 -0
  493. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -0
  494. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +144 -0
  495. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts +3 -0
  496. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -0
  497. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +530 -0
  498. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +39 -0
  499. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -0
  500. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +54 -0
  501. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +41 -0
  502. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -0
  503. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +56 -0
  504. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts +49 -0
  505. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -0
  506. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +328 -0
  507. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +10 -0
  508. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts.map +1 -0
  509. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +51 -0
  510. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +13 -0
  511. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -0
  512. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +12 -0
  513. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +17 -0
  514. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts.map +1 -0
  515. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +99 -0
  516. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts +9 -0
  517. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts.map +1 -0
  518. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +8 -0
  519. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +7 -0
  520. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts.map +1 -0
  521. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +27 -0
  522. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts +14 -0
  523. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts.map +1 -0
  524. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.js +13 -0
  525. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts +7 -0
  526. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts.map +1 -0
  527. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.js +35 -0
  528. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts +9 -0
  529. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts.map +1 -0
  530. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.js +8 -0
  531. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts +15 -0
  532. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts.map +1 -0
  533. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.js +14 -0
  534. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +7 -0
  535. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts.map +1 -0
  536. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +36 -0
  537. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +10 -0
  538. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -0
  539. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +9 -0
  540. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +11 -0
  541. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts.map +1 -0
  542. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +54 -0
  543. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts +9 -0
  544. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts.map +1 -0
  545. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.js +8 -0
  546. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts +11 -0
  547. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts.map +1 -0
  548. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.js +10 -0
  549. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts +7 -0
  550. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts.map +1 -0
  551. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.js +30 -0
  552. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts +9 -0
  553. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts.map +1 -0
  554. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.js +8 -0
  555. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts +13 -0
  556. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts.map +1 -0
  557. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.js +12 -0
  558. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts +14 -0
  559. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts.map +1 -0
  560. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.js +13 -0
  561. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +11 -0
  562. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts.map +1 -0
  563. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +61 -0
  564. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts +9 -0
  565. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -0
  566. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +8 -0
  567. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts +13 -0
  568. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts.map +1 -0
  569. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.js +12 -0
  570. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts +9 -0
  571. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts.map +1 -0
  572. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.js +8 -0
  573. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts +20 -0
  574. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts.map +1 -0
  575. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.js +19 -0
  576. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts +23 -0
  577. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -0
  578. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +70 -0
  579. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +18 -0
  580. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -0
  581. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +25 -0
  582. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +35 -0
  583. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -0
  584. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +41 -0
  585. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +32 -0
  586. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -0
  587. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +138 -0
  588. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_bake_for_scene.d.ts +1 -1
  589. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_bake_for_scene.js +4 -4
  590. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_generate_tree_from_scene.d.ts +1 -3
  591. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_generate_tree_from_scene.d.ts.map +1 -1
  592. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_generate_tree_from_scene.js +5 -4
  593. package/src/shade/renderer/global_illumination/brick4/gpu/bake/brick4_bake_basic.js +1 -1
  594. package/src/shade/renderer/global_illumination/lpv/bake/GPULightProbeVolumeRenderer.d.ts.map +1 -1
  595. package/src/shade/renderer/global_illumination/lpv/bake/GPULightProbeVolumeRenderer.js +1 -4
  596. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.d.ts +8 -3
  597. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.d.ts.map +1 -1
  598. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.js +19 -14
  599. package/src/shade/renderer/global_illumination/lpv/placement/graph_generate_probe_locations.d.ts.map +1 -1
  600. package/src/shade/renderer/global_illumination/lpv/placement/graph_generate_probe_locations.js +9 -1
  601. package/src/shade/renderer/global_illumination/lpv/placement/software/lpv_generate_probe_locations2.d.ts +3 -3
  602. package/src/shade/renderer/global_illumination/lpv/placement/software/lpv_generate_probe_locations2.d.ts.map +1 -1
  603. package/src/shade/renderer/global_illumination/lpv/placement/software/lpv_generate_probe_locations2.js +12 -5
  604. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +1 -1
  605. package/src/shade/renderer/light/GPULightCollection.d.ts +29 -0
  606. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  607. package/src/shade/renderer/light/GPULightCollection.js +444 -329
  608. package/src/shade/renderer/light/LIGHT_DATABASE_SPEC.d.ts.map +1 -1
  609. package/src/shade/renderer/light/LIGHT_DATABASE_SPEC.js +7 -1
  610. package/src/shade/renderer/light/LightCollection.d.ts +29 -1
  611. package/src/shade/renderer/light/LightCollection.d.ts.map +1 -1
  612. package/src/shade/renderer/light/LightCollection.js +50 -1
  613. package/src/shade/renderer/light/make_sunlight.js +2 -2
  614. package/src/shade/renderer/light/model/Light.d.ts +29 -2
  615. package/src/shade/renderer/light/model/Light.d.ts.map +1 -1
  616. package/src/shade/renderer/light/model/Light.js +30 -2
  617. package/src/shade/renderer/light/model/io/light_record_directional.d.ts +3 -1
  618. package/src/shade/renderer/light/model/io/light_record_directional.d.ts.map +1 -1
  619. package/src/shade/renderer/light/model/io/light_record_directional.js +8 -5
  620. package/src/shade/renderer/light/model/io/light_record_point.d.ts +3 -1
  621. package/src/shade/renderer/light/model/io/light_record_point.d.ts.map +1 -1
  622. package/src/shade/renderer/light/model/io/light_record_point.js +5 -2
  623. package/src/shade/renderer/light/model/io/light_record_spot.d.ts +4 -1
  624. package/src/shade/renderer/light/model/io/light_record_spot.d.ts.map +1 -1
  625. package/src/shade/renderer/light/model/io/light_record_spot.js +10 -6
  626. package/src/shade/renderer/light/place_light.d.ts +16 -0
  627. package/src/shade/renderer/light/place_light.d.ts.map +1 -0
  628. package/src/shade/renderer/light/place_light.js +36 -0
  629. package/src/shade/renderer/light/shader_refresh_light_poses.d.ts +44 -0
  630. package/src/shade/renderer/light/shader_refresh_light_poses.d.ts.map +1 -0
  631. package/src/shade/renderer/light/shader_refresh_light_poses.js +154 -0
  632. package/src/shade/renderer/lightmap/GPULightMap.js +1 -1
  633. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.d.ts.map +1 -1
  634. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.js +12 -14
  635. package/src/shade/renderer/loader/SceneBundle.d.ts +53 -28
  636. package/src/shade/renderer/loader/SceneBundle.d.ts.map +1 -1
  637. package/src/shade/renderer/loader/SceneBundle.js +111 -56
  638. package/src/shade/renderer/loader/SceneNode.d.ts +81 -0
  639. package/src/shade/renderer/loader/SceneNode.d.ts.map +1 -0
  640. package/src/shade/renderer/loader/SceneNode.js +114 -0
  641. package/src/shade/renderer/loader/gltf/load_gltf.d.ts +3 -2
  642. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  643. package/src/shade/renderer/loader/gltf/load_gltf.js +110 -142
  644. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  645. package/src/shade/renderer/loader/usd/UsdExtensionRegistry.d.ts +4 -4
  646. package/src/shade/renderer/loader/usd/UsdExtensionRegistry.d.ts.map +1 -1
  647. package/src/shade/renderer/loader/usd/UsdExtensionRegistry.js +2 -2
  648. package/src/shade/renderer/loader/usd/load_usd.d.ts +3 -2
  649. package/src/shade/renderer/loader/usd/load_usd.d.ts.map +1 -1
  650. package/src/shade/renderer/loader/usd/load_usd.js +4 -5
  651. package/src/shade/renderer/loader/usd/usd_build_scene.d.ts +3 -3
  652. package/src/shade/renderer/loader/usd/usd_build_scene.d.ts.map +1 -1
  653. package/src/shade/renderer/loader/usd/usd_build_scene.js +47 -55
  654. package/src/shade/renderer/material/GPUMaterialManager.js +1 -1
  655. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +5 -4
  656. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  657. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +8 -15
  658. package/src/shade/renderer/object_property/shader_refresh_all_mesh_bounds.d.ts +15 -13
  659. package/src/shade/renderer/object_property/shader_refresh_all_mesh_bounds.d.ts.map +1 -1
  660. package/src/shade/renderer/object_property/shader_refresh_all_mesh_bounds.js +40 -21
  661. package/src/shade/renderer/particles/DESIGN.md +53 -50
  662. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +31 -17
  663. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  664. package/src/shade/renderer/particles/GPUParticleSystem.js +54 -91
  665. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +12 -11
  666. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  667. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +12 -11
  668. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts +13 -19
  669. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts.map +1 -1
  670. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.js +19 -25
  671. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +34 -24
  672. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  673. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +53 -40
  674. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.js +1 -1
  675. package/src/shade/renderer/path_tracer/graph_path_trace_scene.js +1 -1
  676. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  677. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  678. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +9 -0
  679. package/src/shade/renderer/rasterize/expand/bucket/mesh/graph_meshes_classify_buckets.d.ts.map +1 -1
  680. package/src/shade/renderer/rasterize/expand/bucket/mesh/graph_meshes_classify_buckets.js +2 -1
  681. package/src/shade/renderer/rasterize/expand/bucket/mesh/graph_scene_meshes_cull_and_classify_buckets.d.ts.map +1 -1
  682. package/src/shade/renderer/rasterize/expand/bucket/mesh/graph_scene_meshes_cull_and_classify_buckets.js +5 -2
  683. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  684. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +13 -2
  685. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +1 -1
  686. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.js +1 -1
  687. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +1 -1
  688. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  689. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +12 -1
  690. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  691. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +10 -0
  692. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +1 -1
  693. package/src/shade/renderer/scene/DynamicMesh.d.ts +13 -4
  694. package/src/shade/renderer/scene/DynamicMesh.d.ts.map +1 -1
  695. package/src/shade/renderer/scene/DynamicMesh.js +14 -4
  696. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts +38 -18
  697. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts.map +1 -1
  698. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.js +112 -60
  699. package/src/shade/renderer/scene/GPUSceneContext.d.ts +69 -44
  700. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  701. package/src/shade/renderer/scene/GPUSceneContext.js +244 -525
  702. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +5 -5
  703. package/src/shade/renderer/scene/NODE3D_STRUCT.js +5 -5
  704. package/src/shade/renderer/scene/Scene.d.ts +18 -60
  705. package/src/shade/renderer/scene/Scene.d.ts.map +1 -1
  706. package/src/shade/renderer/scene/Scene.js +53 -146
  707. package/src/shade/renderer/scene/bvh/StaticSceneBVH.d.ts +24 -3
  708. package/src/shade/renderer/scene/bvh/StaticSceneBVH.d.ts.map +1 -1
  709. package/src/shade/renderer/scene/bvh/StaticSceneBVH.js +53 -13
  710. package/src/shade/renderer/scene/hierarchy/graph_scene_update_transform_hierarchy.d.ts +5 -6
  711. package/src/shade/renderer/scene/hierarchy/graph_scene_update_transform_hierarchy.d.ts.map +1 -1
  712. package/src/shade/renderer/scene/hierarchy/graph_scene_update_transform_hierarchy.js +5 -6
  713. package/src/shade/renderer/scene/optimization/scene_optimize.d.ts +10 -2
  714. package/src/shade/renderer/scene/optimization/scene_optimize.d.ts.map +1 -1
  715. package/src/shade/renderer/scene/optimization/scene_optimize.js +266 -230
  716. package/src/shade/renderer/scene/rows/GPUEntityBoundsReadback.d.ts +53 -0
  717. package/src/shade/renderer/scene/rows/GPUEntityBoundsReadback.d.ts.map +1 -0
  718. package/src/shade/renderer/scene/rows/GPUEntityBoundsReadback.js +144 -0
  719. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts +227 -0
  720. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -0
  721. package/src/shade/renderer/scene/rows/GPUSceneRows.js +1158 -0
  722. package/src/shade/renderer/scene/rows/place_primitive.d.ts +48 -0
  723. package/src/shade/renderer/scene/rows/place_primitive.d.ts.map +1 -0
  724. package/src/shade/renderer/scene/rows/place_primitive.js +98 -0
  725. package/src/shade/renderer/scene/serialization/deserialize_scene.d.ts +10 -8
  726. package/src/shade/renderer/scene/serialization/deserialize_scene.d.ts.map +1 -1
  727. package/src/shade/renderer/scene/serialization/deserialize_scene.js +24 -27
  728. package/src/shade/renderer/scene/serialization/serialize_scene.d.ts +9 -2
  729. package/src/shade/renderer/scene/serialization/serialize_scene.d.ts.map +1 -1
  730. package/src/shade/renderer/scene/serialization/serialize_scene.js +25 -4
  731. package/src/shade/renderer/scene/verify_gpu_tlas_build.js +2 -2
  732. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts +35 -0
  733. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts.map +1 -0
  734. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.js +91 -0
  735. package/src/shade/renderer/shader/chunk/light/DIRECTIONAL_LIGHT_STRUCT.d.ts.map +1 -1
  736. package/src/shade/renderer/shader/chunk/light/DIRECTIONAL_LIGHT_STRUCT.js +5 -0
  737. package/src/shade/renderer/shader/chunk/light/POINT_LIGHT_STRUCT.d.ts.map +1 -1
  738. package/src/shade/renderer/shader/chunk/light/POINT_LIGHT_STRUCT.js +5 -0
  739. package/src/shade/renderer/shader/chunk/light/SPOT_LIGHT_STRUCT.d.ts.map +1 -1
  740. package/src/shade/renderer/shader/chunk/light/SPOT_LIGHT_STRUCT.js +5 -0
  741. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.d.ts +39 -0
  742. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.d.ts.map +1 -1
  743. package/src/shade/renderer/shadow/map/DirectionalLightShadowmap.js +112 -64
  744. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts +7 -0
  745. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  746. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +1541 -1352
  747. package/src/shade/renderer/shadow/map/PointLightShadowmap.js +1 -1
  748. package/src/shade/renderer/shadow/map/SpotLightShadowmap.js +1 -1
  749. package/src/shade/renderer/shadow/map/TetrahedronPointLightShadowmap.js +1 -1
  750. package/src/shade/renderer/shadow/map/csm/CSM_CASTER_RANGE_STRUCT.d.ts +29 -0
  751. package/src/shade/renderer/shadow/map/csm/CSM_CASTER_RANGE_STRUCT.d.ts.map +1 -0
  752. package/src/shade/renderer/shadow/map/csm/CSM_CASTER_RANGE_STRUCT.js +43 -0
  753. package/src/shade/renderer/shadow/map/csm/SHADOWMAP_CSM_CASTER_RANGE_MARGIN_FRACTION.d.ts +13 -0
  754. package/src/shade/renderer/shadow/map/csm/SHADOWMAP_CSM_CASTER_RANGE_MARGIN_FRACTION.d.ts.map +1 -0
  755. package/src/shade/renderer/shadow/map/csm/SHADOWMAP_CSM_CASTER_RANGE_MARGIN_FRACTION.js +12 -0
  756. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.d.ts +49 -0
  757. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.d.ts.map +1 -0
  758. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.js +194 -0
  759. package/src/shade/renderer/shadow/map/csm_debug_draw_preview.js +1 -1
  760. package/src/shade/renderer/shadow/map/shadowmap_update_score.js +1 -1
  761. package/src/shade/renderer/shadow/ray/RTXShadows.js +1 -1
  762. package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.js +1 -1
  763. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.d.ts.map +1 -1
  764. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.js +3 -1
  765. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.js +8 -8
  766. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  767. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +4 -3
  768. package/src/engine/graphics3/instantiate_scene_bundle.d.ts +0 -54
  769. package/src/engine/graphics3/instantiate_scene_bundle.d.ts.map +0 -1
  770. package/src/engine/graphics3/instantiate_scene_bundle.js +0 -164
  771. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +0 -39
  772. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +0 -1
  773. package/src/engine/graphics3/shade_node_to_entity_composition.js +0 -88
  774. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts +0 -35
  775. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts.map +0 -1
  776. package/src/shade/renderer/animation/compute_skin_world_bounds.js +0 -146
  777. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts +0 -96
  778. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts.map +0 -1
  779. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.js +0 -531
  780. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts +0 -20
  781. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts.map +0 -1
  782. package/src/shade/renderer/animation/pose/pose_evaluate_world.js +0 -76
  783. package/src/shade/renderer/animation/skin_bind_transform.d.ts +0 -25
  784. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +0 -1
  785. package/src/shade/renderer/animation/skin_bind_transform.js +0 -33
  786. package/src/shade/renderer/animation/skin_frame_defect.d.ts +0 -34
  787. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +0 -1
  788. package/src/shade/renderer/animation/skin_frame_defect.js +0 -132
  789. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +0 -24
  790. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +0 -1
  791. package/src/shade/renderer/animation/skin_joint_matrix.js +0 -46
  792. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +0 -70
  793. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +0 -1
  794. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +0 -216
  795. package/src/shade/renderer/scene/InstanceBatch.d.ts +0 -92
  796. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +0 -1
  797. package/src/shade/renderer/scene/InstanceBatch.js +0 -235
  798. package/src/shade/renderer/scene/Mesh.d.ts +0 -55
  799. package/src/shade/renderer/scene/Mesh.d.ts.map +0 -1
  800. package/src/shade/renderer/scene/Mesh.js +0 -168
  801. package/src/shade/renderer/scene/Node3D.d.ts +0 -140
  802. package/src/shade/renderer/scene/Node3D.d.ts.map +0 -1
  803. package/src/shade/renderer/scene/Node3D.js +0 -263
  804. package/src/shade/renderer/scene/SceneNodeBinding.d.ts +0 -52
  805. package/src/shade/renderer/scene/SceneNodeBinding.d.ts.map +0 -1
  806. package/src/shade/renderer/scene/SceneNodeBinding.js +0 -56
  807. package/src/shade/renderer/scene/SkinnedMesh.d.ts +0 -110
  808. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +0 -1
  809. package/src/shade/renderer/scene/SkinnedMesh.js +0 -170
  810. package/src/shade/renderer/scene/TransformAuthority.d.ts +0 -19
  811. package/src/shade/renderer/scene/TransformAuthority.d.ts.map +0 -1
  812. package/src/shade/renderer/scene/TransformAuthority.js +0 -33
@@ -1,1352 +1,1541 @@
1
- import { assert } from "../../../../core/assert.js";
2
- import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
3
- import { MaxRectanglesPacker } from "../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
4
- import { FrameGraph } from "../../../../engine/graphics/render/frame_graph/FrameGraph.js";
5
- import { TextureResourceDescriptor } from "../../../device/graph/TextureResourceDescriptor.js";
6
- import { CameraManager } from "../../camera/CameraManager.js";
7
- import { OrthographicCamera } from "../../camera/OrthographicCamera.js";
8
- import { PerspectiveCamera } from "../../camera/PerspectiveCamera.js";
9
- import { CullPolicy } from "../../rasterize/bucket/construct_primitive_state.js";
10
- import { graph_mesh_sphere_cull } from "../../rasterize/compute/graph_mesh_sphere_cull.js";
11
- import { execute_rasterization_jobs } from "../../rasterize/execute_rasterization_jobs.js";
12
- import {
13
- shadow_rasterization_alpha_tested_pass_descriptor
14
- } from "../../rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js";
15
- import {
16
- shadow_rasterization_pass_descriptor
17
- } from "../../rasterize/native/depth/shadow_rasterization_pass_descriptor.js";
18
- import { RasterizationJob, RasterizationPipelineType } from "../../rasterize/RasterizationJob.js";
19
- import { graph_rasterize_scene_fast_raw } from "../../rasterize/fast/graph_rasterize_scene_fast.js";
20
- import { graph_create_texture, graph_import_texture } from "../../shader/graph/graph_import_texture.js";
21
- import { GPUTextureContext } from "../../texture/GPUTextureContext.js";
22
- import { RenderTarget } from "../../texture/RenderTarget.js";
23
- import { View } from "../../view/View.js";
24
- import { ViewManager } from "../../view/ViewManager.js";
25
- import { SHADOWMAP_CSM_CASCADE_COUNT } from "./csm/SHADOWMAP_CSM_CASCADE_COUNT.js";
26
- import { DirectionalLightShadowmap } from "./DirectionalLightShadowmap.js";
27
- import { POINT_SHADOW_CUBE_COLS, POINT_SHADOW_CUBE_ROWS } from "./POINT_SHADOW_CUBE_LAYOUT.js";
28
- import {
29
- POINT_SHADOW_TETRA_COLS,
30
- POINT_SHADOW_TETRA_FACE_COUNT,
31
- POINT_SHADOW_TETRA_MAPPING_RESOLUTION_SCALE,
32
- POINT_SHADOW_TETRA_ROWS,
33
- } from "./POINT_SHADOW_TETRAHEDRON_LAYOUT.js";
34
- import { PointLightShadowmap } from "./PointLightShadowmap.js";
35
- import {
36
- shader_shadowmap_remap_cube_to_octahedral
37
- } from "./shader/shader_shadowmap_remap_cube_to_octahedral.js";
38
- import {
39
- POINT_SHADOW_TETRA_REMAP_SETTINGS,
40
- shader_shadowmap_remap_tetrahedron_to_octahedral
41
- } from "./shader/shader_shadowmap_remap_tetrahedron_to_octahedral.js";
42
- import { SHADOWMAP_ATLAS_BORDER } from "./SHADOWMAP_ATLAS_BORDER.js";
43
- import { TetrahedronPointLightShadowmap } from "./TetrahedronPointLightShadowmap.js";
44
- import { compute_light_projected_area_px } from "./shadowmap_update_score.js";
45
- import { ShadowmapDrawSelector } from "./ShadowmapDrawSelector.js";
46
- import { ShadowmapRegistry } from "./ShadowmapRegistry.js";
47
- import { ShadowmapResolutionPolicy } from "./ShadowmapResolutionPolicy.js";
48
- import { SpotLightShadowmap } from "./SpotLightShadowmap.js";
49
-
50
- /**
51
- * @typedef {import("../../../device/ShadeGPUCommandContext.js").ShadeGPUCommandContext} ShadeGPUCommandContext
52
- * @typedef {import("../../GraphicsContext.js").GraphicsContext} GraphicsContext
53
- * @typedef {import("../../camera/Camera.js").Camera} Camera
54
- * @typedef {import("../../light/GPULightCollection.js").GPULightCollection} GPULightCollection
55
- * @typedef {import("../../light/model/Light.js").Light} Light
56
- * @typedef {import("../../scene/GPUSceneContext.js").GPUSceneContext} GPUSceneContext
57
- * @typedef {import("./Shadowmap.js").Shadowmap} Shadowmap
58
- */
59
-
60
- const DEFAULT_SHADOWMAP_ATLAS_RESOLUTION = 4096 * 2;
61
-
62
- /**
63
- * Resolution by cascade.
64
- * If the number of cascades is greater than the array length, the last resolution is used.
65
- * @type {number[]}
66
- */
67
- const DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION = [
68
- 1740, // first cascade
69
- 1440,
70
- ];
71
-
72
- /**
73
- * Placeholder resolution for local-light shadowmaps (spot/point).
74
- * A proper implementation will project the light's bounding volume into screen space to derive this at runtime.
75
- * @type {number}
76
- */
77
- const DEFAULT_SHADOWMAP_LOCAL_RESOLUTION = 128;
78
-
79
- /**
80
- * Per-frame shadow refresh budget, measured in **views** (one cube face counts as one view,
81
- * one CSM cascade counts as one view, etc.). A point light costs 6 views, a spot light 1, a
82
- * directional SHADOWMAP_CSM_CASCADE_COUNT. A bigger light consumes more of the budget, which
83
- * keeps the cost of the refresh step roughly proportional to how much work we're asking the
84
- * GPU to do — a single point light running the meshlet rasterization pipeline 6 times is
85
- * equivalent work to 6 spot lights.
86
- *
87
- * Directional lights are always refreshed unconditionally (they drive the whole scene) and
88
- * their view cost is charged against the budget, so they effectively reduce the pool available
89
- * for local lights that frame.
90
- *
91
- * Unused budget is carried over between frames when demand exceeds supply — see
92
- * {@link GPUSceneShadowmapContext.#budget_carry_over}. This lets a light that's too expensive
93
- * to fit in a single frame (e.g. a point needing 6 views when only 2 are free) eventually get
94
- * its turn without permanently starving other lights.
95
- * @type {number}
96
- */
97
- const SHADOWMAP_UPDATE_BUDGET = 8;
98
-
99
- /**
100
- * Shadows for a single scene
101
- */
102
- export class GPUSceneShadowmapContext {
103
- atlas = new MaxRectanglesPacker(DEFAULT_SHADOWMAP_ATLAS_RESOLUTION, DEFAULT_SHADOWMAP_ATLAS_RESOLUTION);
104
-
105
- /**
106
- * Depth texture atlas used for shadowmaps.
107
- * @type {GPUTextureContext}
108
- */
109
- texture;
110
-
111
-
112
- /**
113
- * @type {GraphicsContext}
114
- */
115
- #graphics;
116
-
117
- /**
118
- * Dedicated ViewManager for the cube/cascade/perspective shadow views this context
119
- * creates and rasterizes into the atlas. Decoupled from the renderer-level ViewManager
120
- * so the ownership chain stays self-contained — the shadow context creates the views,
121
- * caches their GPU contexts, and disposes them with itself. The shared SceneManager
122
- * lookup is short-circuited with a tiny object that always returns our owning
123
- * GPUSceneContext (every shadow view here is for the same scene by construction).
124
- * @type {ViewManager}
125
- */
126
- shadow_view_manager;
127
-
128
- /**
129
- * Internal CameraManager backing {@link shadow_view_manager}. Shadow cameras are
130
- * created locally in {@link process_lights} and only referenced by this context, so
131
- * they don't need to share the renderer-level CameraManager.
132
- * @type {CameraManager}
133
- */
134
- #cameras;
135
-
136
- /**
137
- * @type {RenderTarget}
138
- */
139
- #render_target;
140
-
141
-
142
- /**
143
- * Every shadowmap this scene holds, plus the light -> map index and the reconciliation marks
144
- * that make {@link process_lights} O(lights + maps) instead of O(lights x maps).
145
- * @type {ShadowmapRegistry}
146
- */
147
- #registry = new ShadowmapRegistry();
148
-
149
- /**
150
- * Scores and picks the maps to refresh each frame. Holds the heap it selects with across
151
- * frames, so the per-frame scoring allocates nothing.
152
- * @type {ShadowmapDrawSelector}
153
- */
154
- #selector = new ShadowmapDrawSelector();
155
-
156
- /**
157
- * Scratch list of lights that need a shadowmap allocated, handed from pass 1 of
158
- * {@link process_lights} to pass 3. Held across frames rather than allocated per call — it is
159
- * empty on every frame where the light set didn't move, which is nearly all of them.
160
- * @type {Light[]}
161
- */
162
- #reconcile_newcomers = [];
163
-
164
- /**
165
- * Master kill switch for shadows on this scene. Defaults true; the renderer sets it from
166
- * its own `feature_shadows_enabled` flag before each frame's light update. When false,
167
- * {@link process_lights} evicts all maps so the next light-record build clears
168
- * CASTS_SHADOW_BIT on every light. {@link select_for_draw} and {@link draw} are still
169
- * gated externally by the renderer; this flag exists to keep the GPU light records in
170
- * sync with that gating without needing to thread the renderer's flag down further.
171
- * @type {boolean}
172
- */
173
- enabled = true;
174
-
175
- /**
176
- * Selects the tetrahedral point-light shadow generator (4 face frusta) over the default
177
- * cube generator (6 faces). The renderer pushes this from its
178
- * `feature_tetrahedron_point_shadows` flag before each light update, alongside {@link enabled}.
179
- *
180
- * Only affects how a point light's shadow is GENERATED — both paths remap into the same
181
- * octahedral atlas encoding, so the sampler is identical and this can be toggled at runtime.
182
- * A flip is picked up in {@link process_lights}: existing point maps whose generator no longer
183
- * matches this flag are evicted and rebuilt with the correct face count on the same pass.
184
- * @type {boolean}
185
- */
186
- use_tetrahedron_point_shadows = false;
187
-
188
- /**
189
- * Frame index of the most recent {@link select_for_draw} call. Passed to {@link draw} via
190
- * this field so we don't have to thread it through the call again — draw() stamps
191
- * `map.last_updated_frame_index` with this value after each successful rasterization.
192
- * @type {number}
193
- */
194
- #current_frame_index = -1;
195
-
196
- /**
197
- * View-budget surplus carried forward from prior frames. Accumulates whenever we had
198
- * non-directional candidates that couldn't fit in the per-frame budget (i.e. a point light
199
- * wanted 6 views but only 2 were free after directionals + earlier locals consumed their
200
- * share). Resets to 0 on any frame where the full demand was satisfied, so the carryover
201
- * doesn't grow unboundedly when the scene is quiet.
202
- * @type {number}
203
- */
204
- #budget_carry_over = 0;
205
-
206
- /**
207
- * Adjusts per-map atlas footprints each frame based on projected on-screen area. Lazily
208
- * tied to {@link atlas} in the constructor.
209
- * @type {ShadowmapResolutionPolicy}
210
- */
211
- #resolution_policy;
212
-
213
- /**
214
- * Number of shadowmaps actually rasterized by the most recent {@link draw} call. Reset at
215
- * the start of draw(), incremented once per map that went through the raster path. Exposed
216
- * for debugging scheduling behaviour — a persistently oscillating value (e.g. 36/20/15
217
- * repeating) indicates some maps are being invalidated every frame and forcing a redraw,
218
- * which usually means the resolution policy or selector is churning.
219
- * @type {number}
220
- */
221
- #debug_render_count = 0;
222
-
223
- /**
224
- * @returns {number} number of shadowmaps rasterized in the most recent draw pass
225
- */
226
- get debug_render_count() {
227
- return this.#debug_render_count;
228
- }
229
-
230
- /**
231
- * Most recently measured atlas occupancy (placed area / total area, including pending
232
- * reservations), in [0, 1]. Forwards from the resolution policy. Useful for the debug
233
- * overlay alongside {@link debug_drop_size_scale} — together they show whether the global
234
- * shrink/grow feedback has converged or is still chasing the target band.
235
- * @returns {number}
236
- */
237
- get debug_atlas_occupancy() {
238
- return this.#resolution_policy.last_occupancy;
239
- }
240
-
241
- /**
242
- * Current value of the global drop-size scale applied by the resolution policy. 1 means no
243
- * shrink; values below 1 mean atlas pressure has trimmed every light's target size
244
- * proportionally. Forwarded from the resolution policy for debug overlays.
245
- * @returns {number}
246
- */
247
- get debug_drop_size_scale() {
248
- return this.#resolution_policy.drop_size_scale;
249
- }
250
-
251
-
252
- /**
253
- * Per-type counts of shadow records written to the GPU database in the previous
254
- * {@link draw} pass. Used to identify trailing slots that must be zeroed when the
255
- * map count for a given type shrinks (e.g. a shadow-casting light is removed),
256
- * mirroring the bookkeeping in {@link GPULightCollection.#build}.
257
- */
258
- #prev_point_count = 0;
259
- #prev_spot_count = 0;
260
- #prev_directional_count = 0;
261
-
262
- /**
263
- * The Scene this context manages shadows for. Captured at construction so
264
- * {@link process_lights} doesn't need it threaded back through every frame.
265
- * @type {Scene}
266
- */
267
- #scene;
268
-
269
- /**
270
- * Owning light collection — gives us access to the GPU database where shadow
271
- * records live and to the source LightCollection. Set in the constructor.
272
- * @type {GPULightCollection}
273
- */
274
- #owner;
275
-
276
- /**
277
- *
278
- * @param {GraphicsContext} graphics
279
- * @param {GPUSceneContext} scene_ctx the GPU-side context for the Scene we manage
280
- * shadows for. Used as the (single) destination of {@link shadow_view_manager}'s
281
- * scene lookup — every shadow view is for this scene, so the lookup short-circuits.
282
- * @param {GPULightCollection} owner the parent light collection — gives us back-access
283
- * to the GPU database we publish records into during {@link draw}.
284
- */
285
- constructor(
286
- graphics,
287
- scene_ctx,
288
- owner,
289
- ) {
290
- assert.defined(graphics, 'graphics');
291
- assert.defined(scene_ctx, 'scene_ctx');
292
- assert.equal(scene_ctx.isGPUSceneContext, true, 'scene_ctx.isGPUSceneContext !== true');
293
- assert.defined(owner, 'owner');
294
-
295
-
296
- const initial_atlas_resolution = Math.min(
297
- graphics.device.limits.maxTextureDimension2D,
298
- DEFAULT_SHADOWMAP_ATLAS_RESOLUTION,
299
- );
300
-
301
- {
302
- const t = this.texture = new GPUTextureContext(graphics.device);
303
-
304
-
305
- const descriptor = t.descriptor;
306
-
307
- descriptor.format = "depth32float";
308
- descriptor.dimension = "2d";
309
- descriptor.label = "Shadowmap atlas";
310
- descriptor.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC;
311
-
312
- t.resize(
313
- initial_atlas_resolution,
314
- initial_atlas_resolution
315
- );
316
-
317
- // ensure atlas size matches
318
- this.atlas.resize(
319
- initial_atlas_resolution,
320
- initial_atlas_resolution
321
- );
322
- }
323
-
324
- this.#graphics = graphics;
325
- this.#scene = scene_ctx.scene;
326
- this.#owner = owner;
327
-
328
- // Local CameraManager + ViewManager dedicated to shadow rasterization. We don't
329
- // share with the renderer-level managers because the cameras and views here are
330
- // owned by this context and only referenced from inside it. The "scenes" lookup
331
- // is a stub that always returns the owning scene context — every shadow view is
332
- // for `this.#scene` by construction.
333
- this.#cameras = new CameraManager(graphics.device);
334
- this.shadow_view_manager = new ViewManager(graphics, this.#cameras, {
335
- obtain: () => scene_ctx,
336
- });
337
-
338
- this.#render_target = RenderTarget.from({
339
- depth: this.texture,
340
- });
341
-
342
- this.#resolution_policy = new ShadowmapResolutionPolicy(this.atlas);
343
- }
344
-
345
- /**
346
- *
347
- * @param {Light} light
348
- * @returns {Shadowmap|undefined}
349
- */
350
- get_map(light) {
351
- return this.#registry.get(light);
352
- }
353
-
354
- /**
355
- * Decide which shadowmaps should be rasterized this frame.
356
- *
357
- * Runs at the top of every frame, before {@link draw}. Responsibilities:
358
- *
359
- * 1. **Reset per-frame signals.** `should_draw` is cleared on every map so a stale "yes"
360
- * from a prior frame can never cause a spurious extra raster. Folded into the selector's
361
- * scoring pass, which walks every map anyway.
362
- * 2. **Measure on-screen presence.** `projected_area_px` is computed once per map and stored
363
- * on the map itself — consumed by the resolution policy, the scorer, and the selector,
364
- * and exposed for debug tooling.
365
- * 3. **Reserve pending atlas footprints.** {@link ShadowmapResolutionPolicy.adjust} may
366
- * attach a `pending_layout` to maps whose projected area has shifted enough to warrant a
367
- * resize. The committed `layout` is left untouched so sampling remains valid; the
368
- * pending is consumed later by {@link draw} when the scheduler naturally picks this map
369
- * up (no forced redraw).
370
- * 4. **Score + select.** Delegated to {@link ShadowmapDrawSelector}, which sets `should_draw`
371
- * on the winners and reports the view spend and whether any candidate went unmet:
372
- * - **Directional** → always selected (infinite score); view cost still charged.
373
- * - **Local, off-screen** (`projected_area_px <= 0`) → skipped entirely.
374
- * - **Local, in-view, invalid** → MUST draw. `is_invalid` means the committed layout
375
- * has never held valid depth (freshly created map); sampling it would display
376
- * garbage, so we bypass the budget. Resizes no longer invalidate under the dual-
377
- * layout model — this flag fires only on first-draw.
378
- * - **Local, in-view, valid** → compete for the remaining budget by score. Ordering
379
- * is descending score so the most important (largest-on-screen, stalest) maps win.
380
- * 5. **Fit the winners.** `map.update()` on each selected map, so its projection matrices
381
- * reflect the current frame. The `last_updated_frame_index` stamp is deferred to
382
- * {@link draw} — it records when the atlas contents become current, not when the
383
- * projection is fitted.
384
- *
385
- * Shadow records are NOT written here; they're written at the end of {@link draw} so the
386
- * shader reads slots that reflect any just-committed pending swaps within the same frame.
387
- *
388
- * See {@link compute_shadowmap_update_score} for the scoring formula and rationale, and
389
- * {@link ShadowmapDrawSelector} for why the ordering is a heap drain rather than a sort.
390
- *
391
- * @param {Camera} main_camera used both for rendering the final frame and for projected-area
392
- * computation and directional CSM fit; must be callable for `.update()`
393
- * @param {GPUSceneContext} scene
394
- * @param {number} frame_index monotonically increasing frame counter, used for staleness
395
- * @param {ArrayLike<number>} view_resolution [width, height] of the main view in pixels; used
396
- * to convert projected sphere extents into a pixel-area score
397
- */
398
- select_for_draw(main_camera, scene, frame_index, view_resolution) {
399
- assert.defined(main_camera, 'main_camera');
400
- assert.isObject(main_camera, 'main_camera');
401
- assert.equal(scene.isGPUSceneContext, true, 'scene.isGPUSceneContext !== true');
402
- assert.isNumber(frame_index, 'frame_index');
403
- assert.defined(view_resolution, 'view_resolution');
404
-
405
- // make sure the camera is updated — both our scoring (projected area) and any downstream
406
- // map.update() reads depend on view/projection matrices being current
407
- main_camera.update();
408
-
409
- this.#current_frame_index = frame_index;
410
-
411
- const shadowmaps = this.#registry.maps;
412
- const n = shadowmaps.length;
413
-
414
- // Step 1 (clearing the stale per-frame `should_draw`) happens inside the selector's own
415
- // scoring pass, so there is no separate walk for it here.
416
-
417
- // Step 2: recompute projected areas. Writing onto the map so the policy + scorer can
418
- // just read `map.projected_area_px` without threading a parallel array through.
419
- for (let i = 0; i < n; i++) {
420
- const m = shadowmaps[i];
421
- m.projected_area_px = compute_light_projected_area_px(m.light, main_camera, view_resolution);
422
- }
423
-
424
- // Step 3: policy reserves pending resize footprints alongside committed layouts. It
425
- // never mutates layout and never invalidates — sampling continues against the old
426
- // region until draw() commits the pending during a normal raster pass. Directionals and
427
- // off-screen locals are skipped (off-screen locals also get any stale pending released
428
- // so an idle reservation can't tie up atlas space).
429
- this.#resolution_policy.adjust(shadowmaps, frame_index);
430
-
431
- // Steps 4 and 5: score every map and greedy-fill the view budget in score order. See
432
- // ShadowmapDrawSelector for why this is a heap drain rather than a sort — the budget
433
- // consumes a handful of winners out of however many candidates the scene has.
434
- const budget_total = SHADOWMAP_UPDATE_BUDGET + this.#budget_carry_over;
435
- const selector = this.#selector;
436
-
437
- selector.select(shadowmaps, frame_index, budget_total);
438
-
439
- // Fit the winners' projections to the current frame. Kept out of the selector because it
440
- // needs the camera and the scene, and the selector is deliberately free of both.
441
- //
442
- // Maps that lost keep their prior metadata + atlas contents — a never-refreshed map
443
- // samples the initial depth-cleared rect, which reads as "fully lit" under the reverse-Z
444
- // greater compare. Acceptable behavior until the map eventually wins a slot.
445
- const selected = selector.selected;
446
- for (let i = 0; i < selected.length; i++) {
447
- selected[i].update(main_camera, scene);
448
- }
449
-
450
- // Carry over the unused pool only when there was demand we couldn't meet. When the whole
451
- // scene's demand was satisfied, reset to 0 so idle frames don't inflate the pool. If
452
- // must-draw bypasses over-spent the pool (views_spent > budget_total), clamp at zero —
453
- // we can't owe future frames for past over-spend, that way lies runaway starvation.
454
- if (selector.had_unfit_candidates) {
455
- this.#budget_carry_over = Math.max(0, budget_total - selector.views_spent);
456
- } else {
457
- this.#budget_carry_over = 0;
458
- }
459
- }
460
-
461
- /**
462
- * Publish per-shadowmap records into the scene's GPU database. Each map writes its record
463
- * into the type-specific shadow_* table at the slot identified by `light._gpu_shadowmap_id`
464
- * (which is itself the per-type index, set by {@link process_lights}). Trailing slots from
465
- * previous frames are zeroed when the map count for a type shrinks, so stale entries don't
466
- * linger.
467
- *
468
- * Only maps with `metadata_changed === true` are re-uploaded; the flag is cleared after
469
- * the slot is queued so unchanged maps don't pay for an upload they don't need. Trim writes
470
- * always run regardless of the flag — they zero slots that no longer correspond to any map.
471
- *
472
- * The type-bucket order here MUST match the per-type id assignment in {@link process_lights}
473
- * (Point → Spot → Directional) so the shadow_id stamped onto each light record points at the
474
- * slot we write here.
475
- *
476
- * Caller must invoke `database.update(cmd)` afterwards to upload the writes — that is done
477
- * by {@link draw} once for both the new records and any zeroed trailing slots.
478
- *
479
- * @param {GPUDatabase} database
480
- */
481
- #write_shadow_records(database) {
482
- const t_point = database.get('shadow_point');
483
- const t_spot = database.get('shadow_spot');
484
- const t_directional = database.get('shadow_directional');
485
-
486
- const maps = this.#registry.maps;
487
- let pc = 0;
488
- let sc = 0;
489
- let dc = 0;
490
-
491
- for (let i = 0; i < maps.length; i++) {
492
- const map = maps[i];
493
- const light = map.light;
494
-
495
- let table;
496
- let slot;
497
- if (light.isPointLight) {
498
- table = t_point;
499
- slot = pc++;
500
- } else if (light.isSpotLight) {
501
- table = t_spot;
502
- slot = sc++;
503
- } else if (light.isDirectionalLight) {
504
- table = t_directional;
505
- slot = dc++;
506
- } else {
507
- continue;
508
- }
509
-
510
- if (map.metadata_changed) {
511
- table.set(slot, map.make_record());
512
- map.metadata_changed = false;
513
- }
514
- }
515
-
516
- // trim trailing slots from previous frames so disused entries don't leak
517
- for (let i = pc; i < this.#prev_point_count; i++) {
518
- t_point.remove(i);
519
- }
520
- for (let i = sc; i < this.#prev_spot_count; i++) {
521
- t_spot.remove(i);
522
- }
523
- for (let i = dc; i < this.#prev_directional_count; i++) {
524
- t_directional.remove(i);
525
- }
526
-
527
- this.#prev_point_count = pc;
528
- this.#prev_spot_count = sc;
529
- this.#prev_directional_count = dc;
530
- }
531
-
532
- /**
533
- * Rasterize every shadowmap flagged by {@link select_for_draw} for a refresh this frame,
534
- * committing any pending resize reservations in the process, then publish per-shadow
535
- * metadata into the scene's GPU database.
536
- *
537
- * For each map with `should_draw` set:
538
- *
539
- * 1. If the map has a {@link Shadowmap.pending_layout} reservation, swap it into place
540
- * BEFORE rasterization so the draw targets the new region. The old committed rects
541
- * are remembered for step 3.
542
- * 2. Dispatch to the type-specific raster path:
543
- * - **Directional / spot**: direct raster into the atlas's sub-rects (one job per
544
- * cascade for directionals, one job per spot).
545
- * - **Point**: 6 cube-face depth passes into a shared intermediate texture, then
546
- * cube→octahedral remap into the atlas. Point lights are serialised so they can
547
- * reuse the intermediate texture.
548
- * 3. If a commit happened in step 1, release the old committed rects from the atlas now
549
- * that the new region holds valid depth.
550
- * 4. Clear per-frame flags (`should_draw`, `is_invalid`) and stamp
551
- * `last_updated_frame_index` with the current frame.
552
- *
553
- * After the loop, each map's metadata record is written into the corresponding
554
- * `shadow_point` / `shadow_spot` / `shadow_directional` table on the owning light
555
- * collection's database, then the database is uploaded — this is where the committed
556
- * swaps become visible to the lighting shader. `#debug_render_count` is reset at entry
557
- * and incremented per map to expose scheduling behaviour to the debug overlay.
558
- *
559
- * No external job list: every path batches its own work internally via
560
- * {@link execute_rasterization_jobs}. Callers just pass a command context.
561
- *
562
- * @param {ShadeGPUCommandContext} command_ctx
563
- */
564
- draw(command_ctx) {
565
- assert.defined(command_ctx, 'command_ctx');
566
-
567
-
568
- this.#debug_render_count = 0;
569
-
570
- const frame_index = this.#current_frame_index;
571
- const maps = this.#registry.maps;
572
-
573
- for (let i = 0; i < maps.length; i++) {
574
- const map = maps[i];
575
- if (!map.should_draw) {
576
- continue;
577
- }
578
-
579
- // Step 1: commit any pending-layout reservation before rasterizing. The draw helpers
580
- // read `map.layout` directly, so swapping here makes the raster target the new
581
- // region. We stash the old rect(s) locally for step 3's atlas release — once the
582
- // raster commands are enqueued, the old region's contents will no longer be sampled
583
- // (the next shadow-record write publishes the new layout), so returning that atlas
584
- // space to the packer is safe.
585
- let retired_rects = null;
586
- if (map.pending_layout !== null) {
587
- retired_rects = map.layout;
588
- map.layout = map.pending_layout;
589
- map.pending_layout = null;
590
- map.metadata_changed = true;
591
- }
592
-
593
- const light = map.light;
594
-
595
- if (light.isDirectionalLight || light.isSpotLight) {
596
- this.#draw_direct(map, command_ctx);
597
- } else if (light.isPointLight) {
598
- if (map.isTetrahedron) {
599
- this.#draw_point_tetrahedron(map, command_ctx);
600
- } else {
601
- this.#draw_point(map, command_ctx);
602
- }
603
- } else {
604
- throw new Error(`Unsupported light type ${light.constructor.name}`);
605
- }
606
-
607
- // Step 3: release the retired rects from the atlas. Must happen AFTER the raster
608
- // helpers finish reading map.layout (they may close over retired_rects as scratch
609
- // memory or view references in some paths; safest order is raster-first).
610
- if (retired_rects !== null) {
611
- for (let j = 0; j < retired_rects.length; j++) {
612
- this.atlas.remove(retired_rects[j]);
613
- }
614
- }
615
-
616
- // Step 4: atlas contents are now current. Clear per-frame flags and stamp the frame
617
- // index so the next frame's scorer computes staleness correctly.
618
- map.should_draw = false;
619
- map.is_invalid = false;
620
- map.last_updated_frame_index = frame_index;
621
- this.#debug_render_count++;
622
- }
623
-
624
-
625
- // Publish metadata AFTER all commits so the lighting shader sees atlas coordinates that
626
- // match the regions we just rasterised into. Never-drawn maps carry stale-but-valid
627
- // projection + layout; off-screen maps and skipped candidates publish their prior state.
628
- const database = this.#owner.database;
629
- this.#write_shadow_records(database);
630
- database.update(command_ctx);
631
-
632
-
633
- // finalize rendered views
634
- for (const map of this.#registry.maps) {
635
- if (map.last_updated_frame_index !== frame_index) {
636
- // skip
637
- continue;
638
- }
639
-
640
- for (const view of map.views) {
641
- const view_ctx = this.shadow_view_manager.obtain(view);
642
- view_ctx.finish_frame(command_ctx);
643
- }
644
- }
645
- }
646
-
647
- /**
648
- * Rasterize a directional or spot shadowmap directly into its atlas rects. One job per
649
- * `map.views[i]` / `map.layout[i]` pair — directionals have N cascades, spots have one.
650
- *
651
- * @param {Shadowmap} map
652
- * @param {ShadeGPUCommandContext} command_ctx
653
- */
654
- #draw_direct(map, command_ctx) {
655
- // TODO pre-cull mesh set by sphere-casting the light volume against the main View Frustum
656
- // see ref: https://www.reddit.com/r/Unity3D/comments/1foe8qi/added_a_way_to_perform_frustum_culling_and_take
657
-
658
- const graph = new FrameGraph("shadows");
659
-
660
- const shadow_views = map.views;
661
- const jobs = new Array(shadow_views.length);
662
-
663
- for (let i = 0; i < shadow_views.length; i++) {
664
- const view = shadow_views[i];
665
- const view_ctx = this.shadow_view_manager.obtain(view);
666
-
667
- view_ctx.update(this.#graphics);
668
-
669
- const atlas_aabb = map.layout[i];
670
- view_ctx.setViewportSize(atlas_aabb.width, atlas_aabb.height);
671
-
672
- const job = new RasterizationJob();
673
- job.opaque_pass = shadow_rasterization_pass_descriptor;
674
- job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
675
- job.cull_policy = CullPolicy.Reversed;
676
- job.render_target = this.#render_target;
677
- job.view_ctx = view_ctx;
678
- job.viewport = [atlas_aabb.x0, atlas_aabb.y0, atlas_aabb.width, atlas_aabb.height];
679
-
680
-
681
- jobs[i] = job;
682
-
683
- // TODO reproject current scene view into shadow map for better occlusion culling,
684
- // see Assassin's Creed Unity talk on GPU-driven rendering,
685
- // see "RE ENGINE Meshlet Rendering Pipeline" https://youtu.be/nWgPtCDXlPc?t=2055
686
-
687
- // TODO need to linearize depth(?)
688
- // can linearize together with reprojection (TAA)
689
- }
690
-
691
- execute_rasterization_jobs({
692
- jobs,
693
- limits: this.#graphics.collection_limits,
694
- command_ctx,
695
- graph,
696
- });
697
-
698
- command_ctx.encodeGraph(graph);
699
- }
700
-
701
- /**
702
- * Rasterize a point light's 6 cube faces into the shared intermediate texture, then run the
703
- * cube→octahedral remap shader to write the encoded depth into this light's atlas rect.
704
- *
705
- * Sequencing matters: each point light fully completes (6 cube rasters + remap) before the
706
- * next one starts, so the intermediate texture can be reused across point lights without
707
- * cross-contamination.
708
- *
709
- * @param {PointLightShadowmap} map
710
- * @param {ShadeGPUCommandContext} command_ctx
711
- */
712
- #draw_point(
713
- map,
714
- command_ctx,
715
- ) {
716
-
717
- const graph = new FrameGraph("shadows");
718
-
719
- const gr_atlas_texture = graph_import_texture(graph, this.texture);
720
-
721
- const atlas_rect = map.layout[0];
722
-
723
- const octahedral_resolution = atlas_rect.width;
724
-
725
- /**
726
- * Controls the relationship between octahedral resolution and the cube map used during rendering
727
- * If this is larger - we get better quality at the cost of rendering more pixels.
728
- *
729
- * Solving for solid angle coverage, using the formula `N=POW(1 - SQRT(E/3), 3/2)` we get:
730
- *
731
- * 0% error -> 1
732
- * 1% error -> 0.915
733
- * 5% error -> 0.81299
734
- * 10% error -> 0.739
735
- * 20% error -> 0.639
736
- * 30% error -> 0.565
737
- * 40% error -> 0.506
738
- * 50% error -> 0.455
739
- *
740
- * @type {number}
741
- */
742
- const CUBE_MAPPING_RESOLUTION_SCALE = 1;
743
-
744
- const cube_face_resolution = Math.ceil(octahedral_resolution * CUBE_MAPPING_RESOLUTION_SCALE);
745
-
746
- // reserve texture
747
- let cube_packed_texture = graph_create_texture(graph, TextureResourceDescriptor.from({
748
- resolution: [
749
- cube_face_resolution * POINT_SHADOW_CUBE_COLS,
750
- cube_face_resolution * POINT_SHADOW_CUBE_ROWS,
751
- ],
752
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
753
- format: 'depth32float',
754
- enableMips: false,
755
- }));
756
-
757
- /**
758
- *
759
- * @type {PointLight}
760
- */
761
- const light = map.light;
762
-
763
- const scene_ctx = this.shadow_view_manager.obtain(map.views[0]).scene;
764
-
765
- /**
766
- * Sphere-culled meshes from the scene.
767
- * We could do per-face culling, but most point lights are local, and there's already meshlet culling happening later in the pipeline.
768
- * Good enough.
769
- * @type {number}
770
- */
771
- const input_meshes = graph_mesh_sphere_cull({
772
- graph,
773
- sphere: [...light.position, light.distance],
774
- scene: scene_ctx,
775
- limits: this.#graphics.collection_limits,
776
- });
777
-
778
- for (let face = 0; face < 6; face++) {
779
- const view = map.views[face];
780
- const view_ctx = this.shadow_view_manager.obtain(view);
781
-
782
- view_ctx.update(this.#graphics);
783
- view_ctx.setViewportSize(cube_face_resolution, cube_face_resolution);
784
-
785
- const col = face % POINT_SHADOW_CUBE_COLS;
786
- const row = Math.floor(face / POINT_SHADOW_CUBE_COLS);
787
-
788
- const job = new RasterizationJob();
789
-
790
- job.pipeline_type = RasterizationPipelineType.Fast;
791
- job.opaque_pass = shadow_rasterization_pass_descriptor;
792
- job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
793
- job.cull_policy = CullPolicy.Reversed;
794
- job.render_target = null;
795
- job.view_ctx = view_ctx;
796
- job.viewport = [
797
- col * cube_face_resolution,
798
- row * cube_face_resolution,
799
- cube_face_resolution,
800
- cube_face_resolution,
801
- ];
802
-
803
- cube_packed_texture = graph_rasterize_scene_fast_raw({
804
- graph,
805
- meshes: input_meshes,
806
- limits: this.#graphics.collection_limits,
807
- target_depth: cube_packed_texture,
808
- job,
809
- }).target_depth;
810
- }
811
-
812
- /**
813
- *
814
- * @param data
815
- * @param {FramePassResources} resources
816
- * @param {GraphicsExecutionContext} context
817
- */
818
- function remap_to_atlas(data, resources, context) {
819
- const atlas = resources.get(data.atlas);
820
- const cube_texture = resources.get(data.cube_texture);
821
-
822
- const settings = {
823
- atlas_offset: [
824
- atlas_rect.x0 + SHADOWMAP_ATLAS_BORDER,
825
- atlas_rect.y0 + SHADOWMAP_ATLAS_BORDER,
826
- atlas_rect.width - 2 * SHADOWMAP_ATLAS_BORDER,
827
- atlas_rect.height - 2 * SHADOWMAP_ATLAS_BORDER,
828
- ],
829
- light_params: [
830
- map.light.distance,
831
- map.cube_near,
832
- 0,
833
- 0,
834
- ]
835
- };
836
-
837
- shader_shadowmap_remap_cube_to_octahedral.draw({
838
- encoder: context.encoder,
839
- bindings: {
840
- settings: settings,
841
- cube_faces: cube_texture.obtainView(),
842
- },
843
- depthStencilAttachment: {
844
- view: atlas.obtainView(),
845
- depthLoadOp: "load",
846
- depthStoreOp: "store",
847
- },
848
- viewport: [
849
- atlas_rect.x0,
850
- atlas_rect.y0,
851
- atlas_rect.width,
852
- atlas_rect.height,
853
- ]
854
- })
855
- }
856
-
857
- const remap_data = {};
858
-
859
- const remap_pass = graph.add("remap to atals", remap_data, remap_to_atlas);
860
-
861
- remap_data.atlas = remap_pass.write(gr_atlas_texture);
862
- remap_data.cube_texture = remap_pass.read(cube_packed_texture);
863
-
864
-
865
- command_ctx.encodeGraph(graph);
866
- }
867
-
868
- /**
869
- * Rasterize a point light's 4 tetrahedron faces into a shared intermediate texture, then run
870
- * the tetrahedron→octahedral remap to write the encoded depth into this light's atlas rect.
871
- * The tetrahedral parallel to {@link #draw_point} — same shape, 4 faces in a 2x2 grid instead
872
- * of 6 in a 3x2 grid, and the remap is handed each face's view-projection matrix so it can
873
- * select faces and project directions in exact agreement with how the depth was rasterized.
874
- *
875
- * @param {TetrahedronPointLightShadowmap} map
876
- * @param {ShadeGPUCommandContext} command_ctx
877
- */
878
- #draw_point_tetrahedron(
879
- map,
880
- command_ctx,
881
- ) {
882
-
883
- const graph = new FrameGraph("shadows");
884
-
885
- const gr_atlas_texture = graph_import_texture(graph, this.texture);
886
-
887
- const atlas_rect = map.layout[0];
888
-
889
- const octahedral_resolution = atlas_rect.width;
890
-
891
- // Wider face fov than the cube path (see POINT_SHADOW_TETRA_FOV_DEGREES) needs a larger
892
- // per-face resolution to hold comparable on-axis density — the analogue of the cube
893
- // path's CUBE_MAPPING_RESOLUTION_SCALE.
894
- const face_resolution = Math.ceil(octahedral_resolution * POINT_SHADOW_TETRA_MAPPING_RESOLUTION_SCALE);
895
-
896
- // reserve intermediate depth texture — 2x2 grid of faces
897
- let tetra_packed_texture = graph_create_texture(graph, TextureResourceDescriptor.from({
898
- resolution: [
899
- face_resolution * POINT_SHADOW_TETRA_COLS,
900
- face_resolution * POINT_SHADOW_TETRA_ROWS,
901
- ],
902
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
903
- format: 'depth32float',
904
- enableMips: false,
905
- }));
906
-
907
- /**
908
- *
909
- * @type {PointLight}
910
- */
911
- const light = map.light;
912
-
913
- const scene_ctx = this.shadow_view_manager.obtain(map.views[0]).scene;
914
-
915
- // One sphere cull for all 4 faces — most point lights are local and meshlet culling
916
- // refines later; per-face culling isn't worth it. Same trade as the cube path.
917
- const input_meshes = graph_mesh_sphere_cull({
918
- graph,
919
- sphere: [...light.position, light.distance],
920
- scene: scene_ctx,
921
- limits: this.#graphics.collection_limits,
922
- });
923
-
924
- for (let face = 0; face < POINT_SHADOW_TETRA_FACE_COUNT; face++) {
925
- const view = map.views[face];
926
- const view_ctx = this.shadow_view_manager.obtain(view);
927
-
928
- view_ctx.update(this.#graphics);
929
- view_ctx.setViewportSize(face_resolution, face_resolution);
930
-
931
- const col = face % POINT_SHADOW_TETRA_COLS;
932
- const row = Math.floor(face / POINT_SHADOW_TETRA_COLS);
933
-
934
- const job = new RasterizationJob();
935
-
936
- job.pipeline_type = RasterizationPipelineType.Fast;
937
- job.opaque_pass = shadow_rasterization_pass_descriptor;
938
- job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
939
- job.cull_policy = CullPolicy.Reversed;
940
- job.render_target = null;
941
- job.view_ctx = view_ctx;
942
- job.viewport = [
943
- col * face_resolution,
944
- row * face_resolution,
945
- face_resolution,
946
- face_resolution,
947
- ];
948
-
949
- tetra_packed_texture = graph_rasterize_scene_fast_raw({
950
- graph,
951
- meshes: input_meshes,
952
- limits: this.#graphics.collection_limits,
953
- target_depth: tetra_packed_texture,
954
- job,
955
- }).target_depth;
956
- }
957
-
958
- const views = map.views;
959
- const tetra_near = map.tetra_near;
960
- const light_max_distance = map.light.distance;
961
-
962
- /**
963
- *
964
- * @param data
965
- * @param {FramePassResources} resources
966
- * @param {GraphicsExecutionContext} context
967
- */
968
- function remap_to_atlas(data, resources, context) {
969
- const atlas = resources.get(data.atlas);
970
- const tetra_texture = resources.get(data.tetra_texture);
971
-
972
- // Read each face camera's view-projection at encode time — set in map.update() and
973
- // untouched since, and it is exactly the matrix the rasterizer used above.
974
- const face_view_proj = new Array(POINT_SHADOW_TETRA_FACE_COUNT);
975
- for (let f = 0; f < POINT_SHADOW_TETRA_FACE_COUNT; f++) {
976
- face_view_proj[f] = views[f].camera.view_projection_matrix;
977
- }
978
-
979
- const settings = context.encoder.allocateTransientValueBuffer(
980
- POINT_SHADOW_TETRA_REMAP_SETTINGS,
981
- {
982
- atlas_offset: [
983
- atlas_rect.x0 + SHADOWMAP_ATLAS_BORDER,
984
- atlas_rect.y0 + SHADOWMAP_ATLAS_BORDER,
985
- atlas_rect.width - 2 * SHADOWMAP_ATLAS_BORDER,
986
- atlas_rect.height - 2 * SHADOWMAP_ATLAS_BORDER,
987
- ],
988
- light_params: [
989
- light_max_distance,
990
- tetra_near,
991
- 0,
992
- 0,
993
- ],
994
- face_view_proj,
995
- }
996
- );
997
-
998
- shader_shadowmap_remap_tetrahedron_to_octahedral.draw({
999
- encoder: context.encoder,
1000
- bindings: {
1001
- settings: settings,
1002
- tetra_faces: tetra_texture.obtainView(),
1003
- },
1004
- depthStencilAttachment: {
1005
- view: atlas.obtainView(),
1006
- depthLoadOp: "load",
1007
- depthStoreOp: "store",
1008
- },
1009
- viewport: [
1010
- atlas_rect.x0,
1011
- atlas_rect.y0,
1012
- atlas_rect.width,
1013
- atlas_rect.height,
1014
- ]
1015
- })
1016
- }
1017
-
1018
- const remap_data = {};
1019
-
1020
- const remap_pass = graph.add("remap tetra to atlas", remap_data, remap_to_atlas);
1021
-
1022
- remap_data.atlas = remap_pass.write(gr_atlas_texture);
1023
- remap_data.tetra_texture = remap_pass.read(tetra_packed_texture);
1024
-
1025
-
1026
- command_ctx.encodeGraph(graph);
1027
- }
1028
-
1029
- /**
1030
- * Release the per-face GPU contexts (each view's uniform/HZB/volumetrics buffers + its camera
1031
- * uniform) backing a map. The dedicated {@link shadow_view_manager} / {@link #cameras} caches
1032
- * key on the View/Camera objects and have no GC path, so a map's contexts must be released
1033
- * explicitly here or their GPU buffers leak for the context's lifetime. Most visible when the
1034
- * point-shadow generator toggle rebuilds every point map (fresh Views each time), but it also
1035
- * closes the ordinary eviction leak (light removed / stops casting / shadows disabled).
1036
- *
1037
- * In this context Views and Cameras are 1:1 per map (each map creates its own), so releasing a
1038
- * view's camera alongside it can't strand a still-referenced camera.
1039
- *
1040
- * @param {Shadowmap} map
1041
- */
1042
- #release_map_gpu_contexts(map) {
1043
- const views = map.views;
1044
- for (let j = 0; j < views.length; j++) {
1045
- const view = views[j];
1046
- this.shadow_view_manager.release(view);
1047
- this.#cameras.release(view.camera);
1048
- }
1049
- }
1050
-
1051
- /**
1052
- * Release a map's atlas rects + GPU side-resources. Caller is responsible for splicing it
1053
- * out of {@link #registry}.
1054
- *
1055
- * @param {Shadowmap} map
1056
- */
1057
- #evict_map(map) {
1058
- for (let j = 0; j < map.layout.length; j++) {
1059
- this.atlas.remove(map.layout[j]);
1060
- }
1061
- if (map.pending_layout !== null) {
1062
- for (let j = 0; j < map.pending_layout.length; j++) {
1063
- this.atlas.remove(map.pending_layout[j]);
1064
- }
1065
- map.pending_layout = null;
1066
- }
1067
-
1068
- // free the per-view/-camera GPU contexts — without this an evicted map's buffers leak
1069
- this.#release_map_gpu_contexts(map);
1070
-
1071
- map.light._gpu_shadowmap_id = -1;
1072
- }
1073
-
1074
- /**
1075
- * Reconcile shadow allocations against the current scene state. Evicts maps for lights
1076
- * that no longer cast shadows or have left the scene, allocates maps for new shadow-
1077
- * casting lights, and reassigns per-type `_gpu_shadowmap_id` when the map ordering
1078
- * shifts.
1079
- *
1080
- * Called from {@link GPULightCollection.update} every frame, including frames where
1081
- * {@link LightCollection.version} hasn't moved — `light.casts_shadow` flips are not
1082
- * tracked by version and are detected here.
1083
- *
1084
- * Runs as three linear passes — mark, reclaim, allocate — over {@link #registry}, which owns
1085
- * the light -> map index and the marks. It used to scan the map array once per light to find
1086
- * out whether that light already had one, which made the pass quadratic: 148us per frame at
1087
- * 512 lights against 19us now, and 554us against 45us at 1024. See
1088
- * benchmark/reconcile.spec.js.
1089
- *
1090
- * @returns {boolean} true when at least one map was added/evicted or at least one light's
1091
- * per-type shadow_id moved. The caller uses this to decide whether the light
1092
- * records need to be re-published (their `shadow_id` and CASTS_SHADOW_BIT may
1093
- * have shifted).
1094
- */
1095
- process_lights() {
1096
- const registry = this.#registry;
1097
- const shadowmaps = registry.maps;
1098
-
1099
- // When the renderer has shadows disabled, treat every map as evictable so the next
1100
- // build clears CASTS_SHADOW_BIT on every light. Re-enabling reconstructs everything
1101
- // from scratch on the following frame — that's a one-frame catch-up cost, considered
1102
- // an acceptable trade for the simpler invariant ("disabled" = no maps held).
1103
- if (!this.enabled) {
1104
- const had_maps = shadowmaps.length > 0;
1105
- for (let i = shadowmaps.length - 1; i >= 0; i--) {
1106
- this.#evict_map(shadowmaps[i]);
1107
- }
1108
- registry.clear();
1109
- return had_maps;
1110
- }
1111
-
1112
- const scene = this.#scene;
1113
- const lights = scene.lights.elements;
1114
- let changed = false;
1115
-
1116
- const use_tetra = this.use_tetrahedron_point_shadows;
1117
- const newcomers = this.#reconcile_newcomers;
1118
-
1119
- registry.begin_reconcile();
1120
- newcomers.length = 0;
1121
-
1122
- // Pass 1 — mark. One index lookup per light both answers "does this light already have a
1123
- // map" and stamps that map as belonging to a light still in the scene, which is what lets
1124
- // the reclamation below run without building a set of live lights first.
1125
- //
1126
- // A light that stopped casting is simply not marked; its map falls out in pass 2 on the
1127
- // strength of the stale mark alone.
1128
- for (let i = 0; i < lights.length; i++) {
1129
- const light = lights[i];
1130
-
1131
- if (!light.casts_shadow) {
1132
- continue;
1133
- }
1134
-
1135
- const map = registry.mark_live(light);
1136
-
1137
- if (map === undefined) {
1138
- newcomers.push(light);
1139
- continue;
1140
- }
1141
-
1142
- // A point map built by the other generator (cube vs tetra) must be rebuilt when the
1143
- // flag flips — its face count and draw path no longer match. Pass 2 drops it on the
1144
- // same test; queueing the rebuild here lands it in this pass rather than the next.
1145
- if (light.isPointLight && Boolean(map.isTetrahedron) !== use_tetra) {
1146
- newcomers.push(light);
1147
- }
1148
- }
1149
-
1150
- // Pass 2 — reclaim. Anything not marked belongs to a light that left the scene or stopped
1151
- // casting, and its atlas rect would otherwise stay pinned forever. Walk in reverse so
1152
- // splice indices stay stable.
1153
- for (let i = shadowmaps.length - 1; i >= 0; i--) {
1154
- const map = shadowmaps[i];
1155
- const light = map.light;
1156
-
1157
- const generator_mismatch = light.isPointLight
1158
- && Boolean(map.isTetrahedron) !== use_tetra;
1159
-
1160
- if (registry.is_marked(map) && !generator_mismatch) {
1161
- continue;
1162
- }
1163
- this.#evict_map(map);
1164
- registry.remove_at(i);
1165
- changed = true;
1166
- }
1167
-
1168
- const add_directional = (light) => {
1169
- const map = new DirectionalLightShadowmap();
1170
- map.light = light;
1171
-
1172
- const bound = [];
1173
- try {
1174
- for (let j = 0; j < SHADOWMAP_CSM_CASCADE_COUNT; j++) {
1175
- const resolution_index = Math.min(j, DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION.length - 1);
1176
- const resolution = DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION[resolution_index];
1177
-
1178
- const rect = this.bind(resolution);
1179
- bound.push(rect);
1180
- map.layout[j] = rect;
1181
-
1182
- const camera = new OrthographicCamera();
1183
- camera.update();
1184
-
1185
- const view = new View(camera, scene);
1186
- view.label = `Shadow Cascade ${j}`;
1187
-
1188
- map.views[j] = view;
1189
- }
1190
- } catch (e) {
1191
- // Partial bind: roll back any cascade rects already inserted so the atlas isn't
1192
- // left holding orphans for a map that never got registered.
1193
- for (let j = 0; j < bound.length; j++) {
1194
- this.atlas.remove(bound[j]);
1195
- }
1196
- throw e;
1197
- }
1198
-
1199
- registry.add(map);
1200
- };
1201
-
1202
- const add_spot = (light) => {
1203
- const map = new SpotLightShadowmap();
1204
- map.light = light;
1205
-
1206
- const rect = this.bind(DEFAULT_SHADOWMAP_LOCAL_RESOLUTION);
1207
- map.layout[0] = rect;
1208
-
1209
- try {
1210
- const camera = new PerspectiveCamera();
1211
- camera.update();
1212
-
1213
- const view = new View(camera, scene);
1214
- view.label = `Spot Shadow`;
1215
-
1216
- map.views[0] = view;
1217
- } catch (e) {
1218
- this.atlas.remove(rect);
1219
- throw e;
1220
- }
1221
-
1222
- registry.add(map);
1223
- };
1224
-
1225
- const add_point = (light) => {
1226
-
1227
- // Generator picked by `use_tetra` above; both remap into one octahedral slot, they
1228
- // only differ in how many face frusta feed that remap (tetra 4, cube 6).
1229
- const map = use_tetra ? new TetrahedronPointLightShadowmap() : new PointLightShadowmap();
1230
- map.light = light;
1231
-
1232
- // single atlas rect holds the octahedral-encoded depth for all faces
1233
- const rect = this.bind(DEFAULT_SHADOWMAP_LOCAL_RESOLUTION);
1234
- map.layout[0] = rect;
1235
-
1236
- const face_count = use_tetra ? POINT_SHADOW_TETRA_FACE_COUNT : 6;
1237
-
1238
- for (let j = 0; j < face_count; j++) {
1239
- const camera = new PerspectiveCamera();
1240
- camera.update();
1241
-
1242
- const view = new View(camera, scene);
1243
- view.label = use_tetra ? `Point Shadow (tetra) face ${j}` : `Point Shadow face ${j}`;
1244
-
1245
- map.views[j] = view;
1246
- }
1247
-
1248
- registry.add(map);
1249
- };
1250
-
1251
- const add_light = (light) => {
1252
- // A light present twice in the scene's light array gets queued twice by pass 1, since
1253
- // neither occurrence finds a map. LightCollection.add doesn't dedupe, so re-check here
1254
- // rather than letting the registry's one-map-per-light assert fire every frame. Free
1255
- // in the normal case — this loop only runs over lights that actually need a map.
1256
- if (registry.get(light) !== undefined) {
1257
- return;
1258
- }
1259
-
1260
- if (light.isDirectionalLight) {
1261
- add_directional(light);
1262
- changed = true;
1263
- } else if (light.isSpotLight) {
1264
- add_spot(light);
1265
- changed = true;
1266
- } else if (light.isPointLight) {
1267
- add_point(light);
1268
- changed = true;
1269
- }
1270
- };
1271
-
1272
- // Pass 3 — allocate. Only the lights pass 1 flagged, so on the overwhelming majority of
1273
- // frames (nothing arrived, nothing left) this loop does not run at all. The old version
1274
- // walked every light here and re-tested casts_shadow on each; pass 1 already answered that.
1275
- for (let i = 0; i < newcomers.length; i++) {
1276
- try {
1277
- add_light(newcomers[i]);
1278
- } catch (e) {
1279
- console.error(e);
1280
- }
1281
- }
1282
-
1283
- // Drop the references rather than leaving them until the next call — a light that gets
1284
- // removed from the scene right after being added here would otherwise stay reachable from
1285
- // this scratch array for a frame.
1286
- newcomers.length = 0;
1287
-
1288
- // Publish per-type shadowmap ids back onto the lights. Each shadow_* table is indexed
1289
- // independently, so a point light's shadow_id points into shadow_point, a spot's into
1290
- // shadow_spot, etc. Type-bucket order MUST match {@link #write_shadow_records} so each
1291
- // light record's stored shadow_id lines up with the slot we'll write into the table.
1292
- // When a map's slot index actually changes (eviction shifted neighbours up the array),
1293
- // mark its record dirty so the next #write_shadow_records re-publishes it into the new
1294
- // slot, and report `changed` so the caller re-publishes the light records too.
1295
- let pc = 0;
1296
- let sc = 0;
1297
- let dc = 0;
1298
- for (let i = 0; i < shadowmaps.length; i++) {
1299
- const map = shadowmaps[i];
1300
- const light = map.light;
1301
-
1302
- let new_id;
1303
- if (light.isPointLight) {
1304
- new_id = pc++;
1305
- } else if (light.isSpotLight) {
1306
- new_id = sc++;
1307
- } else if (light.isDirectionalLight) {
1308
- new_id = dc++;
1309
- } else {
1310
- continue;
1311
- }
1312
-
1313
- if (light._gpu_shadowmap_id !== new_id) {
1314
- light._gpu_shadowmap_id = new_id;
1315
- map.metadata_changed = true;
1316
- changed = true;
1317
- }
1318
- }
1319
-
1320
- return changed;
1321
- }
1322
-
1323
- /**
1324
- *
1325
- * @param {number} resolution
1326
- * @returns {AABB2}
1327
- */
1328
- bind(resolution) {
1329
-
1330
- const rect = new AABB2(0, 0, resolution, resolution);
1331
-
1332
- if (!this.atlas.add(rect)) {
1333
- throw new Error(`Failed to add shadowmap to atlas`);
1334
- }
1335
-
1336
- return rect;
1337
- }
1338
-
1339
- destroy() {
1340
- this.texture.destroy();
1341
-
1342
- const maps = this.#registry.maps;
1343
- for (let i = 0; i < maps.length; i++) {
1344
- const map = maps[i];
1345
- // release the per-view/-camera GPU contexts too — map.destroy() alone doesn't (the
1346
- // buffers live in the view/camera manager caches, not on the map)
1347
- this.#release_map_gpu_contexts(map);
1348
- map.destroy();
1349
- }
1350
- this.#registry.clear();
1351
- }
1352
- }
1
+ import { assert } from "../../../../core/assert.js";
2
+ import AABB2 from "../../../../core/geom/2d/aabb/AABB2.js";
3
+ import { MaxRectanglesPacker } from "../../../../core/geom/packing/max-rect/MaxRectanglesPacker.js";
4
+ import { FrameGraph } from "../../../../engine/graphics/render/frame_graph/FrameGraph.js";
5
+ import { TextureResourceDescriptor } from "../../../device/graph/TextureResourceDescriptor.js";
6
+ import { CameraManager } from "../../camera/CameraManager.js";
7
+ import { OrthographicCamera } from "../../camera/OrthographicCamera.js";
8
+ import { PerspectiveCamera } from "../../camera/PerspectiveCamera.js";
9
+ import { CullPolicy } from "../../rasterize/bucket/construct_primitive_state.js";
10
+ import { graph_mesh_sphere_cull } from "../../rasterize/compute/graph_mesh_sphere_cull.js";
11
+ import { execute_rasterization_jobs } from "../../rasterize/execute_rasterization_jobs.js";
12
+ import {
13
+ shadow_rasterization_alpha_tested_pass_descriptor
14
+ } from "../../rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js";
15
+ import {
16
+ shadow_rasterization_pass_descriptor
17
+ } from "../../rasterize/native/depth/shadow_rasterization_pass_descriptor.js";
18
+ import { RasterizationJob, RasterizationPipelineType } from "../../rasterize/RasterizationJob.js";
19
+ import { graph_rasterize_scene_fast_raw } from "../../rasterize/fast/graph_rasterize_scene_fast.js";
20
+ import { graph_create_texture, graph_import_texture } from "../../shader/graph/graph_import_texture.js";
21
+ import { GPUTextureContext } from "../../texture/GPUTextureContext.js";
22
+ import { RenderTarget } from "../../texture/RenderTarget.js";
23
+ import { View } from "../../view/View.js";
24
+ import { ViewManager } from "../../view/ViewManager.js";
25
+ import { CSM_CASTER_RANGE_UNKNOWN, CSM_CASTER_RANGE_UNKNOWN_BITS } from "./csm/CSM_CASTER_RANGE_STRUCT.js";
26
+ import { SHADOWMAP_CSM_CASCADE_COUNT } from "./csm/SHADOWMAP_CSM_CASCADE_COUNT.js";
27
+ import {
28
+ CSM_CASTER_RANGE_SETTINGS_STRUCT,
29
+ CSM_CASTER_RANGE_WORKGROUP_SIZE,
30
+ shader_csm_caster_range_accumulate,
31
+ shader_csm_caster_range_publish,
32
+ } from "./csm/shader/shader_csm_caster_range.js";
33
+ import { DirectionalLightShadowmap } from "./DirectionalLightShadowmap.js";
34
+ import { POINT_SHADOW_CUBE_COLS, POINT_SHADOW_CUBE_ROWS } from "./POINT_SHADOW_CUBE_LAYOUT.js";
35
+ import {
36
+ POINT_SHADOW_TETRA_COLS,
37
+ POINT_SHADOW_TETRA_FACE_COUNT,
38
+ POINT_SHADOW_TETRA_MAPPING_RESOLUTION_SCALE,
39
+ POINT_SHADOW_TETRA_ROWS,
40
+ } from "./POINT_SHADOW_TETRAHEDRON_LAYOUT.js";
41
+ import { PointLightShadowmap } from "./PointLightShadowmap.js";
42
+ import {
43
+ shader_shadowmap_remap_cube_to_octahedral
44
+ } from "./shader/shader_shadowmap_remap_cube_to_octahedral.js";
45
+ import {
46
+ POINT_SHADOW_TETRA_REMAP_SETTINGS,
47
+ shader_shadowmap_remap_tetrahedron_to_octahedral
48
+ } from "./shader/shader_shadowmap_remap_tetrahedron_to_octahedral.js";
49
+ import { SHADOWMAP_ATLAS_BORDER } from "./SHADOWMAP_ATLAS_BORDER.js";
50
+ import { TetrahedronPointLightShadowmap } from "./TetrahedronPointLightShadowmap.js";
51
+ import { compute_light_projected_area_px } from "./shadowmap_update_score.js";
52
+ import { ShadowmapDrawSelector } from "./ShadowmapDrawSelector.js";
53
+ import { ShadowmapRegistry } from "./ShadowmapRegistry.js";
54
+ import { ShadowmapResolutionPolicy } from "./ShadowmapResolutionPolicy.js";
55
+ import { SpotLightShadowmap } from "./SpotLightShadowmap.js";
56
+
57
+ /**
58
+ * @typedef {import("../../../device/ShadeGPUCommandContext.js").ShadeGPUCommandContext} ShadeGPUCommandContext
59
+ * @typedef {import("../../GraphicsContext.js").GraphicsContext} GraphicsContext
60
+ * @typedef {import("../../camera/Camera.js").Camera} Camera
61
+ * @typedef {import("../../light/GPULightCollection.js").GPULightCollection} GPULightCollection
62
+ * @typedef {import("../../light/model/Light.js").Light} Light
63
+ * @typedef {import("../../scene/GPUSceneContext.js").GPUSceneContext} GPUSceneContext
64
+ * @typedef {import("./Shadowmap.js").Shadowmap} Shadowmap
65
+ */
66
+
67
+ const DEFAULT_SHADOWMAP_ATLAS_RESOLUTION = 4096 * 2;
68
+
69
+ /**
70
+ * Resolution by cascade.
71
+ * If the number of cascades is greater than the array length, the last resolution is used.
72
+ * @type {number[]}
73
+ */
74
+ const DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION = [
75
+ 1740, // first cascade
76
+ 1440,
77
+ ];
78
+
79
+ /**
80
+ * Placeholder resolution for local-light shadowmaps (spot/point).
81
+ * A proper implementation will project the light's bounding volume into screen space to derive this at runtime.
82
+ * @type {number}
83
+ */
84
+ const DEFAULT_SHADOWMAP_LOCAL_RESOLUTION = 128;
85
+
86
+ /**
87
+ * Per-frame shadow refresh budget, measured in **views** (one cube face counts as one view,
88
+ * one CSM cascade counts as one view, etc.). A point light costs 6 views, a spot light 1, a
89
+ * directional SHADOWMAP_CSM_CASCADE_COUNT. A bigger light consumes more of the budget, which
90
+ * keeps the cost of the refresh step roughly proportional to how much work we're asking the
91
+ * GPU to do — a single point light running the meshlet rasterization pipeline 6 times is
92
+ * equivalent work to 6 spot lights.
93
+ *
94
+ * Directional lights are always refreshed unconditionally (they drive the whole scene) and
95
+ * their view cost is charged against the budget, so they effectively reduce the pool available
96
+ * for local lights that frame.
97
+ *
98
+ * Unused budget is carried over between frames when demand exceeds supply — see
99
+ * {@link GPUSceneShadowmapContext.#budget_carry_over}. This lets a light that's too expensive
100
+ * to fit in a single frame (e.g. a point needing 6 views when only 2 are free) eventually get
101
+ * its turn without permanently starving other lights.
102
+ * @type {number}
103
+ */
104
+ const SHADOWMAP_UPDATE_BUDGET = 8;
105
+
106
+ /**
107
+ * Shadows for a single scene
108
+ */
109
+ export class GPUSceneShadowmapContext {
110
+ atlas = new MaxRectanglesPacker(DEFAULT_SHADOWMAP_ATLAS_RESOLUTION, DEFAULT_SHADOWMAP_ATLAS_RESOLUTION);
111
+
112
+ /**
113
+ * Depth texture atlas used for shadowmaps.
114
+ * @type {GPUTextureContext}
115
+ */
116
+ texture;
117
+
118
+
119
+ /**
120
+ * @type {GraphicsContext}
121
+ */
122
+ #graphics;
123
+
124
+ /**
125
+ * Dedicated ViewManager for the cube/cascade/perspective shadow views this context
126
+ * creates and rasterizes into the atlas. Decoupled from the renderer-level ViewManager
127
+ * so the ownership chain stays self-contained — the shadow context creates the views,
128
+ * caches their GPU contexts, and disposes them with itself. The shared SceneManager
129
+ * lookup is short-circuited with a tiny object that always returns our owning
130
+ * GPUSceneContext (every shadow view here is for the same scene by construction).
131
+ * @type {ViewManager}
132
+ */
133
+ shadow_view_manager;
134
+
135
+ /**
136
+ * Internal CameraManager backing {@link shadow_view_manager}. Shadow cameras are
137
+ * created locally in {@link process_lights} and only referenced by this context, so
138
+ * they don't need to share the renderer-level CameraManager.
139
+ * @type {CameraManager}
140
+ */
141
+ #cameras;
142
+
143
+ /**
144
+ * @type {RenderTarget}
145
+ */
146
+ #render_target;
147
+
148
+
149
+ /**
150
+ * Every shadowmap this scene holds, plus the light -> map index and the reconciliation marks
151
+ * that make {@link process_lights} O(lights + maps) instead of O(lights x maps).
152
+ * @type {ShadowmapRegistry}
153
+ */
154
+ #registry = new ShadowmapRegistry();
155
+
156
+ /**
157
+ * Scores and picks the maps to refresh each frame. Holds the heap it selects with across
158
+ * frames, so the per-frame scoring allocates nothing.
159
+ * @type {ShadowmapDrawSelector}
160
+ */
161
+ #selector = new ShadowmapDrawSelector();
162
+
163
+ /**
164
+ * Scratch list of lights that need a shadowmap allocated, handed from pass 1 of
165
+ * {@link process_lights} to pass 3. Held across frames rather than allocated per call — it is
166
+ * empty on every frame where the light set didn't move, which is nearly all of them.
167
+ * @type {Light[]}
168
+ */
169
+ #reconcile_newcomers = [];
170
+
171
+ /**
172
+ * Master kill switch for shadows on this scene. Defaults true; the renderer sets it from
173
+ * its own `feature_shadows_enabled` flag before each frame's light update. When false,
174
+ * {@link process_lights} evicts all maps so the next light-record build clears
175
+ * CASTS_SHADOW_BIT on every light. {@link select_for_draw} and {@link draw} are still
176
+ * gated externally by the renderer; this flag exists to keep the GPU light records in
177
+ * sync with that gating without needing to thread the renderer's flag down further.
178
+ * @type {boolean}
179
+ */
180
+ enabled = true;
181
+
182
+ /**
183
+ * Selects the tetrahedral point-light shadow generator (4 face frusta) over the default
184
+ * cube generator (6 faces). The renderer pushes this from its
185
+ * `feature_tetrahedron_point_shadows` flag before each light update, alongside {@link enabled}.
186
+ *
187
+ * Only affects how a point light's shadow is GENERATED — both paths remap into the same
188
+ * octahedral atlas encoding, so the sampler is identical and this can be toggled at runtime.
189
+ * A flip is picked up in {@link process_lights}: existing point maps whose generator no longer
190
+ * matches this flag are evicted and rebuilt with the correct face count on the same pass.
191
+ * @type {boolean}
192
+ */
193
+ use_tetrahedron_point_shadows = false;
194
+
195
+ /**
196
+ * Frame index of the most recent {@link select_for_draw} call. Passed to {@link draw} via
197
+ * this field so we don't have to thread it through the call again — draw() stamps
198
+ * `map.last_updated_frame_index` with this value after each successful rasterization.
199
+ * @type {number}
200
+ */
201
+ #current_frame_index = -1;
202
+
203
+ /**
204
+ * View-budget surplus carried forward from prior frames. Accumulates whenever we had
205
+ * non-directional candidates that couldn't fit in the per-frame budget (i.e. a point light
206
+ * wanted 6 views but only 2 were free after directionals + earlier locals consumed their
207
+ * share). Resets to 0 on any frame where the full demand was satisfied, so the carryover
208
+ * doesn't grow unboundedly when the scene is quiet.
209
+ * @type {number}
210
+ */
211
+ #budget_carry_over = 0;
212
+
213
+ /**
214
+ * Adjusts per-map atlas footprints each frame based on projected on-screen area. Lazily
215
+ * tied to {@link atlas} in the constructor.
216
+ * @type {ShadowmapResolutionPolicy}
217
+ */
218
+ #resolution_policy;
219
+
220
+ /**
221
+ * Number of shadowmaps actually rasterized by the most recent {@link draw} call. Reset at
222
+ * the start of draw(), incremented once per map that went through the raster path. Exposed
223
+ * for debugging scheduling behaviour — a persistently oscillating value (e.g. 36/20/15
224
+ * repeating) indicates some maps are being invalidated every frame and forcing a redraw,
225
+ * which usually means the resolution policy or selector is churning.
226
+ * @type {number}
227
+ */
228
+ #debug_render_count = 0;
229
+
230
+ /**
231
+ * The scene this context shadows.
232
+ * @type {GPUSceneContext}
233
+ */
234
+ #scene_ctx;
235
+
236
+ /**
237
+ * Rows of the light database's `shadow_csm_caster_range` table given back by evicted maps,
238
+ * to be claimed again before a new row is.
239
+ * @type {number[]}
240
+ */
241
+ #caster_rows_free = [];
242
+
243
+ /**
244
+ * Rows ever claimed; the next new row.
245
+ * @type {number}
246
+ */
247
+ #caster_row_count = 0;
248
+
249
+ /**
250
+ * One word per cascade per claimed row, the accumulate pass's scratch, holding plain f32 bit
251
+ * patterns seeded to {@link CSM_CASTER_RANGE_UNKNOWN} and reset to it by each publish.
252
+ * @type {GPUBuffer|null}
253
+ */
254
+ #caster_scratch = null;
255
+
256
+ /**
257
+ * Rows {@link #caster_scratch} has words for.
258
+ * @type {number}
259
+ */
260
+ #caster_scratch_rows = 0;
261
+
262
+ /**
263
+ * The readback service's handle for the caster range table, taken on first use.
264
+ * @type {number}
265
+ */
266
+ #caster_range_table = -1;
267
+
268
+ /**
269
+ * @returns {number} number of shadowmaps rasterized in the most recent draw pass
270
+ */
271
+ get debug_render_count() {
272
+ return this.#debug_render_count;
273
+ }
274
+
275
+ /**
276
+ * Most recently measured atlas occupancy (placed area / total area, including pending
277
+ * reservations), in [0, 1]. Forwards from the resolution policy. Useful for the debug
278
+ * overlay alongside {@link debug_drop_size_scale} — together they show whether the global
279
+ * shrink/grow feedback has converged or is still chasing the target band.
280
+ * @returns {number}
281
+ */
282
+ get debug_atlas_occupancy() {
283
+ return this.#resolution_policy.last_occupancy;
284
+ }
285
+
286
+ /**
287
+ * Current value of the global drop-size scale applied by the resolution policy. 1 means no
288
+ * shrink; values below 1 mean atlas pressure has trimmed every light's target size
289
+ * proportionally. Forwarded from the resolution policy for debug overlays.
290
+ * @returns {number}
291
+ */
292
+ get debug_drop_size_scale() {
293
+ return this.#resolution_policy.drop_size_scale;
294
+ }
295
+
296
+
297
+ /**
298
+ * Per-type counts of shadow records written to the GPU database in the previous
299
+ * {@link draw} pass. Used to identify trailing slots that must be zeroed when the
300
+ * map count for a given type shrinks (e.g. a shadow-casting light is removed),
301
+ * mirroring the bookkeeping in {@link GPULightCollection.#build}.
302
+ */
303
+ #prev_point_count = 0;
304
+ #prev_spot_count = 0;
305
+ #prev_directional_count = 0;
306
+
307
+ /**
308
+ * The Scene this context manages shadows for. Captured at construction so
309
+ * {@link process_lights} doesn't need it threaded back through every frame.
310
+ * @type {Scene}
311
+ */
312
+ #scene;
313
+
314
+ /**
315
+ * Owning light collection — gives us access to the GPU database where shadow
316
+ * records live and to the source LightCollection. Set in the constructor.
317
+ * @type {GPULightCollection}
318
+ */
319
+ #owner;
320
+
321
+ /**
322
+ *
323
+ * @param {GraphicsContext} graphics
324
+ * @param {GPUSceneContext} scene_ctx the GPU-side context for the Scene we manage
325
+ * shadows for. Used as the (single) destination of {@link shadow_view_manager}'s
326
+ * scene lookup — every shadow view is for this scene, so the lookup short-circuits.
327
+ * @param {GPULightCollection} owner the parent light collection — gives us back-access
328
+ * to the GPU database we publish records into during {@link draw}.
329
+ */
330
+ constructor(
331
+ graphics,
332
+ scene_ctx,
333
+ owner,
334
+ ) {
335
+ assert.defined(graphics, 'graphics');
336
+ assert.defined(scene_ctx, 'scene_ctx');
337
+ assert.equal(scene_ctx.isGPUSceneContext, true, 'scene_ctx.isGPUSceneContext !== true');
338
+ assert.defined(owner, 'owner');
339
+
340
+
341
+ const initial_atlas_resolution = Math.min(
342
+ graphics.device.limits.maxTextureDimension2D,
343
+ DEFAULT_SHADOWMAP_ATLAS_RESOLUTION,
344
+ );
345
+
346
+ {
347
+ const t = this.texture = new GPUTextureContext(graphics.device);
348
+
349
+
350
+ const descriptor = t.descriptor;
351
+
352
+ descriptor.format = "depth32float";
353
+ descriptor.dimension = "2d";
354
+ descriptor.label = "Shadowmap atlas";
355
+ descriptor.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC;
356
+
357
+ t.resize(
358
+ initial_atlas_resolution,
359
+ initial_atlas_resolution
360
+ );
361
+
362
+ // ensure atlas size matches
363
+ this.atlas.resize(
364
+ initial_atlas_resolution,
365
+ initial_atlas_resolution
366
+ );
367
+ }
368
+
369
+ this.#graphics = graphics;
370
+ this.#scene = scene_ctx.scene;
371
+ this.#owner = owner;
372
+
373
+ // Local CameraManager + ViewManager dedicated to shadow rasterization. We don't
374
+ // share with the renderer-level managers because the cameras and views here are
375
+ // owned by this context and only referenced from inside it. The "scenes" lookup
376
+ // is a stub that always returns the owning scene context — every shadow view is
377
+ // for `this.#scene` by construction.
378
+ this.#cameras = new CameraManager(graphics.device);
379
+ this.shadow_view_manager = new ViewManager(graphics, this.#cameras, {
380
+ obtain: () => scene_ctx,
381
+ });
382
+
383
+ this.#render_target = RenderTarget.from({
384
+ depth: this.texture,
385
+ });
386
+
387
+ this.#resolution_policy = new ShadowmapResolutionPolicy(this.atlas);
388
+
389
+ this.#scene_ctx = scene_ctx;
390
+ }
391
+
392
+ /**
393
+ * The readback service's handle for the light database's `shadow_csm_caster_range` table —
394
+ * what a directional map asks its range through.
395
+ *
396
+ * @returns {number}
397
+ */
398
+ get caster_range_table() {
399
+ if (this.#caster_range_table === -1) {
400
+ this.#caster_range_table = this.#owner.readback.table('shadow_csm_caster_range');
401
+ }
402
+
403
+ return this.#caster_range_table;
404
+ }
405
+
406
+ /**
407
+ * Give a directional map its row of the caster range table and its words of the scratch.
408
+ *
409
+ * @param {DirectionalLightShadowmap} map
410
+ */
411
+ #claim_caster_row(map) {
412
+ const free = this.#caster_rows_free;
413
+ const row = free.length > 0 ? free.pop() : this.#caster_row_count++;
414
+
415
+ // the CPU writes the row once, as unknown; the GPU writes it from here on
416
+ this.#owner.database.get('shadow_csm_caster_range').set(row, {
417
+ near: new Float32Array(SHADOWMAP_CSM_CASCADE_COUNT).fill(CSM_CASTER_RANGE_UNKNOWN),
418
+ });
419
+
420
+ map.caster_row = row;
421
+ map.caster_settings.base = row * SHADOWMAP_CSM_CASCADE_COUNT;
422
+ map.caster_range_valid = false;
423
+ map.caster_range.fill(NaN);
424
+
425
+ const rows_needed = row + 1;
426
+
427
+ if (rows_needed > this.#caster_scratch_rows) {
428
+ // Grown by replacement rather than copied: every claimed row's words are unknown
429
+ // between frames — the publish resets them — so a fresh seed loses nothing.
430
+ const rows = Math.max(rows_needed, this.#caster_scratch_rows * 2, 4);
431
+ const words = new Uint32Array(rows * SHADOWMAP_CSM_CASCADE_COUNT).fill(CSM_CASTER_RANGE_UNKNOWN_BITS);
432
+ const device = this.#graphics.device;
433
+
434
+ if (this.#caster_scratch !== null) {
435
+ this.#caster_scratch.destroy();
436
+ }
437
+
438
+ this.#caster_scratch = device.createBuffer({
439
+ label: 'shadows/csm caster range scratch',
440
+ size: words.byteLength,
441
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
442
+ });
443
+
444
+ device.queue.writeBuffer(this.#caster_scratch, 0, words);
445
+
446
+ this.#caster_scratch_rows = rows;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Take a directional map's caster range row back.
452
+ *
453
+ * @param {DirectionalLightShadowmap} map
454
+ */
455
+ #release_caster_row(map) {
456
+ const row = map.caster_row;
457
+
458
+ if (row === -1) {
459
+ return;
460
+ }
461
+
462
+ this.#owner.database.get('shadow_csm_caster_range').remove(row);
463
+ this.#caster_rows_free.push(row);
464
+
465
+ map.caster_row = -1;
466
+ map.caster_range_valid = false;
467
+ }
468
+
469
+ /**
470
+ * Measure this frame's casters against the map's cascades and publish the result to its row,
471
+ * for the readback to carry to the next frame's fit.
472
+ *
473
+ * @param {DirectionalLightShadowmap} map
474
+ * @param {ShadeGPUCommandContext} command_ctx
475
+ */
476
+ #measure_caster_range(map, command_ctx) {
477
+ const scene_ctx = this.#scene_ctx;
478
+ const meshes = scene_ctx.scene_database.get('meshes');
479
+ const group_count = meshes.dispatch_group_count(CSM_CASTER_RANGE_WORKGROUP_SIZE);
480
+
481
+ if (group_count === 0) {
482
+ // nothing to cast: the row keeps what it holds, which is unknown
483
+ return;
484
+ }
485
+
486
+ const ranges = this.#caster_scratch;
487
+
488
+ // transient: the frusta differ every frame, so there is nothing worth keeping between them
489
+ const settings = command_ctx.allocateTransientValueBuffer(CSM_CASTER_RANGE_SETTINGS_STRUCT, map.caster_settings);
490
+
491
+ shader_csm_caster_range_accumulate.dispatch({
492
+ encoder: command_ctx,
493
+ group_count_x: group_count,
494
+ bindings: {
495
+ scene_database: scene_ctx.scene_database_buffer,
496
+ settings,
497
+ ranges,
498
+ },
499
+ });
500
+
501
+ shader_csm_caster_range_publish.dispatch({
502
+ encoder: command_ctx,
503
+ group_count_x: 1,
504
+ bindings: {
505
+ ranges,
506
+ light_database: this.#owner.database.buffer,
507
+ publish: {
508
+ row: map.caster_row,
509
+ base: map.caster_settings.base,
510
+ cascade_count: map.caster_settings.cascade_count,
511
+ },
512
+ },
513
+ });
514
+ }
515
+
516
+ /**
517
+ *
518
+ * @param {Light} light
519
+ * @returns {Shadowmap|undefined}
520
+ */
521
+ get_map(light) {
522
+ return this.#registry.get(light);
523
+ }
524
+
525
+ /**
526
+ * Decide which shadowmaps should be rasterized this frame.
527
+ *
528
+ * Runs at the top of every frame, before {@link draw}. Responsibilities:
529
+ *
530
+ * 1. **Reset per-frame signals.** `should_draw` is cleared on every map so a stale "yes"
531
+ * from a prior frame can never cause a spurious extra raster. Folded into the selector's
532
+ * scoring pass, which walks every map anyway.
533
+ * 2. **Measure on-screen presence.** `projected_area_px` is computed once per map and stored
534
+ * on the map itself — consumed by the resolution policy, the scorer, and the selector,
535
+ * and exposed for debug tooling.
536
+ * 3. **Reserve pending atlas footprints.** {@link ShadowmapResolutionPolicy.adjust} may
537
+ * attach a `pending_layout` to maps whose projected area has shifted enough to warrant a
538
+ * resize. The committed `layout` is left untouched so sampling remains valid; the
539
+ * pending is consumed later by {@link draw} when the scheduler naturally picks this map
540
+ * up (no forced redraw).
541
+ * 4. **Score + select.** Delegated to {@link ShadowmapDrawSelector}, which sets `should_draw`
542
+ * on the winners and reports the view spend and whether any candidate went unmet:
543
+ * - **Directional** → always selected (infinite score); view cost still charged.
544
+ * - **Local, off-screen** (`projected_area_px <= 0`) → skipped entirely.
545
+ * - **Local, in-view, invalid** → MUST draw. `is_invalid` means the committed layout
546
+ * has never held valid depth (freshly created map); sampling it would display
547
+ * garbage, so we bypass the budget. Resizes no longer invalidate under the dual-
548
+ * layout model — this flag fires only on first-draw.
549
+ * - **Local, in-view, valid** → compete for the remaining budget by score. Ordering
550
+ * is descending score so the most important (largest-on-screen, stalest) maps win.
551
+ * 5. **Fit the winners.** `map.update()` on each selected map, so its projection matrices
552
+ * reflect the current frame. The `last_updated_frame_index` stamp is deferred to
553
+ * {@link draw} — it records when the atlas contents become current, not when the
554
+ * projection is fitted.
555
+ *
556
+ * Shadow records are NOT written here; they're written at the end of {@link draw} so the
557
+ * shader reads slots that reflect any just-committed pending swaps within the same frame.
558
+ *
559
+ * See {@link compute_shadowmap_update_score} for the scoring formula and rationale, and
560
+ * {@link ShadowmapDrawSelector} for why the ordering is a heap drain rather than a sort.
561
+ *
562
+ * @param {Camera} main_camera used both for rendering the final frame and for projected-area
563
+ * computation and directional CSM fit; must be callable for `.update()`
564
+ * @param {GPUSceneContext} scene
565
+ * @param {number} frame_index monotonically increasing frame counter, used for staleness
566
+ * @param {ArrayLike<number>} view_resolution [width, height] of the main view in pixels; used
567
+ * to convert projected sphere extents into a pixel-area score
568
+ */
569
+ select_for_draw(main_camera, scene, frame_index, view_resolution) {
570
+ assert.defined(main_camera, 'main_camera');
571
+ assert.isObject(main_camera, 'main_camera');
572
+ assert.equal(scene.isGPUSceneContext, true, 'scene.isGPUSceneContext !== true');
573
+ assert.isNumber(frame_index, 'frame_index');
574
+ assert.defined(view_resolution, 'view_resolution');
575
+
576
+ // make sure the camera is updated — both our scoring (projected area) and any downstream
577
+ // map.update() reads depend on view/projection matrices being current
578
+ main_camera.update();
579
+
580
+ this.#current_frame_index = frame_index;
581
+
582
+ const shadowmaps = this.#registry.maps;
583
+ const n = shadowmaps.length;
584
+
585
+ // Step 1 (clearing the stale per-frame `should_draw`) happens inside the selector's own
586
+ // scoring pass, so there is no separate walk for it here.
587
+
588
+ // Step 2: recompute projected areas. Writing onto the map so the policy + scorer can
589
+ // just read `map.projected_area_px` without threading a parallel array through.
590
+ for (let i = 0; i < n; i++) {
591
+ const m = shadowmaps[i];
592
+ m.projected_area_px = compute_light_projected_area_px(m.light, main_camera, view_resolution);
593
+ }
594
+
595
+ // Step 3: policy reserves pending resize footprints alongside committed layouts. It
596
+ // never mutates layout and never invalidates — sampling continues against the old
597
+ // region until draw() commits the pending during a normal raster pass. Directionals and
598
+ // off-screen locals are skipped (off-screen locals also get any stale pending released
599
+ // so an idle reservation can't tie up atlas space).
600
+ this.#resolution_policy.adjust(shadowmaps, frame_index);
601
+
602
+ // Steps 4 and 5: score every map and greedy-fill the view budget in score order. See
603
+ // ShadowmapDrawSelector for why this is a heap drain rather than a sort — the budget
604
+ // consumes a handful of winners out of however many candidates the scene has.
605
+ const budget_total = SHADOWMAP_UPDATE_BUDGET + this.#budget_carry_over;
606
+ const selector = this.#selector;
607
+
608
+ selector.select(shadowmaps, frame_index, budget_total);
609
+
610
+ // Fit the winners' projections to the current frame. Kept out of the selector because it
611
+ // needs the camera and the scene, and the selector is deliberately free of both.
612
+ //
613
+ // Maps that lost keep their prior metadata + atlas contents — a never-refreshed map
614
+ // samples the initial depth-cleared rect, which reads as "fully lit" under the reverse-Z
615
+ // greater compare. Acceptable behavior until the map eventually wins a slot.
616
+ const selected = selector.selected;
617
+ for (let i = 0; i < selected.length; i++) {
618
+ selected[i].update(main_camera, scene);
619
+ }
620
+
621
+ // Carry over the unused pool only when there was demand we couldn't meet. When the whole
622
+ // scene's demand was satisfied, reset to 0 so idle frames don't inflate the pool. If
623
+ // must-draw bypasses over-spent the pool (views_spent > budget_total), clamp at zero —
624
+ // we can't owe future frames for past over-spend, that way lies runaway starvation.
625
+ if (selector.had_unfit_candidates) {
626
+ this.#budget_carry_over = Math.max(0, budget_total - selector.views_spent);
627
+ } else {
628
+ this.#budget_carry_over = 0;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Publish per-shadowmap records into the scene's GPU database. Each map writes its record
634
+ * into the type-specific shadow_* table at the slot identified by `light._gpu_shadowmap_id`
635
+ * (which is itself the per-type index, set by {@link process_lights}). Trailing slots from
636
+ * previous frames are zeroed when the map count for a type shrinks, so stale entries don't
637
+ * linger.
638
+ *
639
+ * Only maps with `metadata_changed === true` are re-uploaded; the flag is cleared after
640
+ * the slot is queued so unchanged maps don't pay for an upload they don't need. Trim writes
641
+ * always run regardless of the flag — they zero slots that no longer correspond to any map.
642
+ *
643
+ * The type-bucket order here MUST match the per-type id assignment in {@link process_lights}
644
+ * (Point → Spot → Directional) so the shadow_id stamped onto each light record points at the
645
+ * slot we write here.
646
+ *
647
+ * Caller must invoke `database.update(cmd)` afterwards to upload the writes — that is done
648
+ * by {@link draw} once for both the new records and any zeroed trailing slots.
649
+ *
650
+ * @param {GPUDatabase} database
651
+ */
652
+ #write_shadow_records(database) {
653
+ const t_point = database.get('shadow_point');
654
+ const t_spot = database.get('shadow_spot');
655
+ const t_directional = database.get('shadow_directional');
656
+
657
+ const maps = this.#registry.maps;
658
+ let pc = 0;
659
+ let sc = 0;
660
+ let dc = 0;
661
+
662
+ for (let i = 0; i < maps.length; i++) {
663
+ const map = maps[i];
664
+ const light = map.light;
665
+
666
+ let table;
667
+ let slot;
668
+ if (light.isPointLight) {
669
+ table = t_point;
670
+ slot = pc++;
671
+ } else if (light.isSpotLight) {
672
+ table = t_spot;
673
+ slot = sc++;
674
+ } else if (light.isDirectionalLight) {
675
+ table = t_directional;
676
+ slot = dc++;
677
+ } else {
678
+ continue;
679
+ }
680
+
681
+ if (map.metadata_changed) {
682
+ table.set(slot, map.make_record());
683
+ map.metadata_changed = false;
684
+ }
685
+ }
686
+
687
+ // trim trailing slots from previous frames so disused entries don't leak
688
+ for (let i = pc; i < this.#prev_point_count; i++) {
689
+ t_point.remove(i);
690
+ }
691
+ for (let i = sc; i < this.#prev_spot_count; i++) {
692
+ t_spot.remove(i);
693
+ }
694
+ for (let i = dc; i < this.#prev_directional_count; i++) {
695
+ t_directional.remove(i);
696
+ }
697
+
698
+ this.#prev_point_count = pc;
699
+ this.#prev_spot_count = sc;
700
+ this.#prev_directional_count = dc;
701
+ }
702
+
703
+ /**
704
+ * Rasterize every shadowmap flagged by {@link select_for_draw} for a refresh this frame,
705
+ * committing any pending resize reservations in the process, then publish per-shadow
706
+ * metadata into the scene's GPU database.
707
+ *
708
+ * For each map with `should_draw` set:
709
+ *
710
+ * 1. If the map has a {@link Shadowmap.pending_layout} reservation, swap it into place
711
+ * BEFORE rasterization so the draw targets the new region. The old committed rects
712
+ * are remembered for step 3.
713
+ * 2. Dispatch to the type-specific raster path:
714
+ * - **Directional / spot**: direct raster into the atlas's sub-rects (one job per
715
+ * cascade for directionals, one job per spot).
716
+ * - **Point**: 6 cube-face depth passes into a shared intermediate texture, then
717
+ * cube→octahedral remap into the atlas. Point lights are serialised so they can
718
+ * reuse the intermediate texture.
719
+ * 3. If a commit happened in step 1, release the old committed rects from the atlas now
720
+ * that the new region holds valid depth.
721
+ * 4. Clear per-frame flags (`should_draw`, `is_invalid`) and stamp
722
+ * `last_updated_frame_index` with the current frame.
723
+ *
724
+ * After the loop, each map's metadata record is written into the corresponding
725
+ * `shadow_point` / `shadow_spot` / `shadow_directional` table on the owning light
726
+ * collection's database, then the database is uploaded — this is where the committed
727
+ * swaps become visible to the lighting shader. `#debug_render_count` is reset at entry
728
+ * and incremented per map to expose scheduling behaviour to the debug overlay.
729
+ *
730
+ * No external job list: every path batches its own work internally via
731
+ * {@link execute_rasterization_jobs}. Callers just pass a command context.
732
+ *
733
+ * @param {ShadeGPUCommandContext} command_ctx
734
+ */
735
+ draw(command_ctx) {
736
+ assert.defined(command_ctx, 'command_ctx');
737
+
738
+
739
+ this.#debug_render_count = 0;
740
+
741
+ const frame_index = this.#current_frame_index;
742
+ const maps = this.#registry.maps;
743
+
744
+ for (let i = 0; i < maps.length; i++) {
745
+ const map = maps[i];
746
+ if (!map.should_draw) {
747
+ continue;
748
+ }
749
+
750
+ // Step 1: commit any pending-layout reservation before rasterizing. The draw helpers
751
+ // read `map.layout` directly, so swapping here makes the raster target the new
752
+ // region. We stash the old rect(s) locally for step 3's atlas release — once the
753
+ // raster commands are enqueued, the old region's contents will no longer be sampled
754
+ // (the next shadow-record write publishes the new layout), so returning that atlas
755
+ // space to the packer is safe.
756
+ let retired_rects = null;
757
+ if (map.pending_layout !== null) {
758
+ retired_rects = map.layout;
759
+ map.layout = map.pending_layout;
760
+ map.pending_layout = null;
761
+ map.metadata_changed = true;
762
+ }
763
+
764
+ const light = map.light;
765
+
766
+ if (light.isDirectionalLight || light.isSpotLight) {
767
+ this.#draw_direct(map, command_ctx);
768
+ } else if (light.isPointLight) {
769
+ if (map.isTetrahedron) {
770
+ this.#draw_point_tetrahedron(map, command_ctx);
771
+ } else {
772
+ this.#draw_point(map, command_ctx);
773
+ }
774
+ } else {
775
+ throw new Error(`Unsupported light type ${light.constructor.name}`);
776
+ }
777
+
778
+ // Step 3: release the retired rects from the atlas. Must happen AFTER the raster
779
+ // helpers finish reading map.layout (they may close over retired_rects as scratch
780
+ // memory or view references in some paths; safest order is raster-first).
781
+ if (retired_rects !== null) {
782
+ for (let j = 0; j < retired_rects.length; j++) {
783
+ this.atlas.remove(retired_rects[j]);
784
+ }
785
+ }
786
+
787
+ // Step 4: atlas contents are now current. Clear per-frame flags and stamp the frame
788
+ // index so the next frame's scorer computes staleness correctly.
789
+ map.should_draw = false;
790
+ map.is_invalid = false;
791
+ map.last_updated_frame_index = frame_index;
792
+ this.#debug_render_count++;
793
+ }
794
+
795
+
796
+ // Publish metadata AFTER all commits so the lighting shader sees atlas coordinates that
797
+ // match the regions we just rasterised into. Never-drawn maps carry stale-but-valid
798
+ // projection + layout; off-screen maps and skipped candidates publish their prior state.
799
+ // The database's update encodes its readback at its tail, so what the maps asked for this
800
+ // frame — their caster ranges, as the passes above published them — is copied out here,
801
+ // after the passes, and delivered at the head of a later frame.
802
+ const database = this.#owner.database;
803
+ this.#write_shadow_records(database);
804
+ database.update(command_ctx);
805
+
806
+
807
+ // finalize rendered views
808
+ for (const map of this.#registry.maps) {
809
+ if (map.last_updated_frame_index !== frame_index) {
810
+ // skip
811
+ continue;
812
+ }
813
+
814
+ for (const view of map.views) {
815
+ const view_ctx = this.shadow_view_manager.obtain(view);
816
+ view_ctx.finish_frame(command_ctx);
817
+ }
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Rasterize a directional or spot shadowmap directly into its atlas rects. One job per
823
+ * `map.views[i]` / `map.layout[i]` pair — directionals have N cascades, spots have one.
824
+ *
825
+ * @param {Shadowmap} map
826
+ * @param {ShadeGPUCommandContext} command_ctx
827
+ */
828
+ #draw_direct(map, command_ctx) {
829
+ if (map.light.isDirectionalLight === true && map.caster_row !== -1) {
830
+ // this frame's casters against this frame's cascades, for next frame's fit
831
+ this.#measure_caster_range(map, command_ctx);
832
+ }
833
+
834
+ // TODO pre-cull mesh set by sphere-casting the light volume against the main View Frustum
835
+ // see ref: https://www.reddit.com/r/Unity3D/comments/1foe8qi/added_a_way_to_perform_frustum_culling_and_take
836
+
837
+ const graph = new FrameGraph("shadows");
838
+
839
+ const shadow_views = map.views;
840
+ const jobs = new Array(shadow_views.length);
841
+
842
+ for (let i = 0; i < shadow_views.length; i++) {
843
+ const view = shadow_views[i];
844
+ const view_ctx = this.shadow_view_manager.obtain(view);
845
+
846
+ view_ctx.update(this.#graphics);
847
+
848
+ const atlas_aabb = map.layout[i];
849
+ view_ctx.setViewportSize(atlas_aabb.width, atlas_aabb.height);
850
+
851
+ const job = new RasterizationJob();
852
+ job.opaque_pass = shadow_rasterization_pass_descriptor;
853
+ job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
854
+ job.cull_policy = CullPolicy.Reversed;
855
+ job.render_target = this.#render_target;
856
+ job.view_ctx = view_ctx;
857
+ job.viewport = [atlas_aabb.x0, atlas_aabb.y0, atlas_aabb.width, atlas_aabb.height];
858
+
859
+
860
+ jobs[i] = job;
861
+
862
+ // TODO reproject current scene view into shadow map for better occlusion culling,
863
+ // see Assassin's Creed Unity talk on GPU-driven rendering,
864
+ // see "RE ENGINE Meshlet Rendering Pipeline" https://youtu.be/nWgPtCDXlPc?t=2055
865
+
866
+ // TODO need to linearize depth(?)
867
+ // can linearize together with reprojection (TAA)
868
+ }
869
+
870
+ execute_rasterization_jobs({
871
+ jobs,
872
+ limits: this.#graphics.collection_limits,
873
+ command_ctx,
874
+ graph,
875
+ });
876
+
877
+ command_ctx.encodeGraph(graph);
878
+ }
879
+
880
+ /**
881
+ * Rasterize a point light's 6 cube faces into the shared intermediate texture, then run the
882
+ * cube→octahedral remap shader to write the encoded depth into this light's atlas rect.
883
+ *
884
+ * Sequencing matters: each point light fully completes (6 cube rasters + remap) before the
885
+ * next one starts, so the intermediate texture can be reused across point lights without
886
+ * cross-contamination.
887
+ *
888
+ * @param {PointLightShadowmap} map
889
+ * @param {ShadeGPUCommandContext} command_ctx
890
+ */
891
+ #draw_point(
892
+ map,
893
+ command_ctx,
894
+ ) {
895
+
896
+ const graph = new FrameGraph("shadows");
897
+
898
+ const gr_atlas_texture = graph_import_texture(graph, this.texture);
899
+
900
+ const atlas_rect = map.layout[0];
901
+
902
+ const octahedral_resolution = atlas_rect.width;
903
+
904
+ /**
905
+ * Controls the relationship between octahedral resolution and the cube map used during rendering
906
+ * If this is larger - we get better quality at the cost of rendering more pixels.
907
+ *
908
+ * Solving for solid angle coverage, using the formula `N=POW(1 - SQRT(E/3), 3/2)` we get:
909
+ *
910
+ * 0% error -> 1
911
+ * 1% error -> 0.915
912
+ * 5% error -> 0.81299
913
+ * 10% error -> 0.739
914
+ * 20% error -> 0.639
915
+ * 30% error -> 0.565
916
+ * 40% error -> 0.506
917
+ * 50% error -> 0.455
918
+ *
919
+ * @type {number}
920
+ */
921
+ const CUBE_MAPPING_RESOLUTION_SCALE = 1;
922
+
923
+ const cube_face_resolution = Math.ceil(octahedral_resolution * CUBE_MAPPING_RESOLUTION_SCALE);
924
+
925
+ // reserve texture
926
+ let cube_packed_texture = graph_create_texture(graph, TextureResourceDescriptor.from({
927
+ resolution: [
928
+ cube_face_resolution * POINT_SHADOW_CUBE_COLS,
929
+ cube_face_resolution * POINT_SHADOW_CUBE_ROWS,
930
+ ],
931
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
932
+ format: 'depth32float',
933
+ enableMips: false,
934
+ }));
935
+
936
+ /**
937
+ *
938
+ * @type {PointLight}
939
+ */
940
+ const light = map.light;
941
+
942
+ const scene_ctx = this.shadow_view_manager.obtain(map.views[0]).scene;
943
+
944
+ /**
945
+ * Sphere-culled meshes from the scene.
946
+ * We could do per-face culling, but most point lights are local, and there's already meshlet culling happening later in the pipeline.
947
+ * Good enough.
948
+ * @type {number}
949
+ */
950
+ const input_meshes = graph_mesh_sphere_cull({
951
+ graph,
952
+ sphere: [light.transform.translation_x, light.transform.translation_y, light.transform.translation_z, light.distance],
953
+ scene: scene_ctx,
954
+ limits: this.#graphics.collection_limits,
955
+ });
956
+
957
+ for (let face = 0; face < 6; face++) {
958
+ const view = map.views[face];
959
+ const view_ctx = this.shadow_view_manager.obtain(view);
960
+
961
+ view_ctx.update(this.#graphics);
962
+ view_ctx.setViewportSize(cube_face_resolution, cube_face_resolution);
963
+
964
+ const col = face % POINT_SHADOW_CUBE_COLS;
965
+ const row = Math.floor(face / POINT_SHADOW_CUBE_COLS);
966
+
967
+ const job = new RasterizationJob();
968
+
969
+ job.pipeline_type = RasterizationPipelineType.Fast;
970
+ job.opaque_pass = shadow_rasterization_pass_descriptor;
971
+ job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
972
+ job.cull_policy = CullPolicy.Reversed;
973
+ job.render_target = null;
974
+ job.view_ctx = view_ctx;
975
+ job.viewport = [
976
+ col * cube_face_resolution,
977
+ row * cube_face_resolution,
978
+ cube_face_resolution,
979
+ cube_face_resolution,
980
+ ];
981
+
982
+ cube_packed_texture = graph_rasterize_scene_fast_raw({
983
+ graph,
984
+ meshes: input_meshes,
985
+ limits: this.#graphics.collection_limits,
986
+ target_depth: cube_packed_texture,
987
+ job,
988
+ }).target_depth;
989
+ }
990
+
991
+ /**
992
+ *
993
+ * @param data
994
+ * @param {FramePassResources} resources
995
+ * @param {GraphicsExecutionContext} context
996
+ */
997
+ function remap_to_atlas(data, resources, context) {
998
+ const atlas = resources.get(data.atlas);
999
+ const cube_texture = resources.get(data.cube_texture);
1000
+
1001
+ const settings = {
1002
+ atlas_offset: [
1003
+ atlas_rect.x0 + SHADOWMAP_ATLAS_BORDER,
1004
+ atlas_rect.y0 + SHADOWMAP_ATLAS_BORDER,
1005
+ atlas_rect.width - 2 * SHADOWMAP_ATLAS_BORDER,
1006
+ atlas_rect.height - 2 * SHADOWMAP_ATLAS_BORDER,
1007
+ ],
1008
+ light_params: [
1009
+ map.light.distance,
1010
+ map.cube_near,
1011
+ 0,
1012
+ 0,
1013
+ ]
1014
+ };
1015
+
1016
+ shader_shadowmap_remap_cube_to_octahedral.draw({
1017
+ encoder: context.encoder,
1018
+ bindings: {
1019
+ settings: settings,
1020
+ cube_faces: cube_texture.obtainView(),
1021
+ },
1022
+ depthStencilAttachment: {
1023
+ view: atlas.obtainView(),
1024
+ depthLoadOp: "load",
1025
+ depthStoreOp: "store",
1026
+ },
1027
+ viewport: [
1028
+ atlas_rect.x0,
1029
+ atlas_rect.y0,
1030
+ atlas_rect.width,
1031
+ atlas_rect.height,
1032
+ ]
1033
+ })
1034
+ }
1035
+
1036
+ const remap_data = {};
1037
+
1038
+ const remap_pass = graph.add("remap to atals", remap_data, remap_to_atlas);
1039
+
1040
+ remap_data.atlas = remap_pass.write(gr_atlas_texture);
1041
+ remap_data.cube_texture = remap_pass.read(cube_packed_texture);
1042
+
1043
+
1044
+ command_ctx.encodeGraph(graph);
1045
+ }
1046
+
1047
+ /**
1048
+ * Rasterize a point light's 4 tetrahedron faces into a shared intermediate texture, then run
1049
+ * the tetrahedron→octahedral remap to write the encoded depth into this light's atlas rect.
1050
+ * The tetrahedral parallel to {@link #draw_point} — same shape, 4 faces in a 2x2 grid instead
1051
+ * of 6 in a 3x2 grid, and the remap is handed each face's view-projection matrix so it can
1052
+ * select faces and project directions in exact agreement with how the depth was rasterized.
1053
+ *
1054
+ * @param {TetrahedronPointLightShadowmap} map
1055
+ * @param {ShadeGPUCommandContext} command_ctx
1056
+ */
1057
+ #draw_point_tetrahedron(
1058
+ map,
1059
+ command_ctx,
1060
+ ) {
1061
+
1062
+ const graph = new FrameGraph("shadows");
1063
+
1064
+ const gr_atlas_texture = graph_import_texture(graph, this.texture);
1065
+
1066
+ const atlas_rect = map.layout[0];
1067
+
1068
+ const octahedral_resolution = atlas_rect.width;
1069
+
1070
+ // Wider face fov than the cube path (see POINT_SHADOW_TETRA_FOV_DEGREES) needs a larger
1071
+ // per-face resolution to hold comparable on-axis density — the analogue of the cube
1072
+ // path's CUBE_MAPPING_RESOLUTION_SCALE.
1073
+ const face_resolution = Math.ceil(octahedral_resolution * POINT_SHADOW_TETRA_MAPPING_RESOLUTION_SCALE);
1074
+
1075
+ // reserve intermediate depth texture — 2x2 grid of faces
1076
+ let tetra_packed_texture = graph_create_texture(graph, TextureResourceDescriptor.from({
1077
+ resolution: [
1078
+ face_resolution * POINT_SHADOW_TETRA_COLS,
1079
+ face_resolution * POINT_SHADOW_TETRA_ROWS,
1080
+ ],
1081
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1082
+ format: 'depth32float',
1083
+ enableMips: false,
1084
+ }));
1085
+
1086
+ /**
1087
+ *
1088
+ * @type {PointLight}
1089
+ */
1090
+ const light = map.light;
1091
+
1092
+ const scene_ctx = this.shadow_view_manager.obtain(map.views[0]).scene;
1093
+
1094
+ // One sphere cull for all 4 faces — most point lights are local and meshlet culling
1095
+ // refines later; per-face culling isn't worth it. Same trade as the cube path.
1096
+ const input_meshes = graph_mesh_sphere_cull({
1097
+ graph,
1098
+ sphere: [light.transform.translation_x, light.transform.translation_y, light.transform.translation_z, light.distance],
1099
+ scene: scene_ctx,
1100
+ limits: this.#graphics.collection_limits,
1101
+ });
1102
+
1103
+ for (let face = 0; face < POINT_SHADOW_TETRA_FACE_COUNT; face++) {
1104
+ const view = map.views[face];
1105
+ const view_ctx = this.shadow_view_manager.obtain(view);
1106
+
1107
+ view_ctx.update(this.#graphics);
1108
+ view_ctx.setViewportSize(face_resolution, face_resolution);
1109
+
1110
+ const col = face % POINT_SHADOW_TETRA_COLS;
1111
+ const row = Math.floor(face / POINT_SHADOW_TETRA_COLS);
1112
+
1113
+ const job = new RasterizationJob();
1114
+
1115
+ job.pipeline_type = RasterizationPipelineType.Fast;
1116
+ job.opaque_pass = shadow_rasterization_pass_descriptor;
1117
+ job.alpha_tested_pass = shadow_rasterization_alpha_tested_pass_descriptor;
1118
+ job.cull_policy = CullPolicy.Reversed;
1119
+ job.render_target = null;
1120
+ job.view_ctx = view_ctx;
1121
+ job.viewport = [
1122
+ col * face_resolution,
1123
+ row * face_resolution,
1124
+ face_resolution,
1125
+ face_resolution,
1126
+ ];
1127
+
1128
+ tetra_packed_texture = graph_rasterize_scene_fast_raw({
1129
+ graph,
1130
+ meshes: input_meshes,
1131
+ limits: this.#graphics.collection_limits,
1132
+ target_depth: tetra_packed_texture,
1133
+ job,
1134
+ }).target_depth;
1135
+ }
1136
+
1137
+ const views = map.views;
1138
+ const tetra_near = map.tetra_near;
1139
+ const light_max_distance = map.light.distance;
1140
+
1141
+ /**
1142
+ *
1143
+ * @param data
1144
+ * @param {FramePassResources} resources
1145
+ * @param {GraphicsExecutionContext} context
1146
+ */
1147
+ function remap_to_atlas(data, resources, context) {
1148
+ const atlas = resources.get(data.atlas);
1149
+ const tetra_texture = resources.get(data.tetra_texture);
1150
+
1151
+ // Read each face camera's view-projection at encode time — set in map.update() and
1152
+ // untouched since, and it is exactly the matrix the rasterizer used above.
1153
+ const face_view_proj = new Array(POINT_SHADOW_TETRA_FACE_COUNT);
1154
+ for (let f = 0; f < POINT_SHADOW_TETRA_FACE_COUNT; f++) {
1155
+ face_view_proj[f] = views[f].camera.view_projection_matrix;
1156
+ }
1157
+
1158
+ const settings = context.encoder.allocateTransientValueBuffer(
1159
+ POINT_SHADOW_TETRA_REMAP_SETTINGS,
1160
+ {
1161
+ atlas_offset: [
1162
+ atlas_rect.x0 + SHADOWMAP_ATLAS_BORDER,
1163
+ atlas_rect.y0 + SHADOWMAP_ATLAS_BORDER,
1164
+ atlas_rect.width - 2 * SHADOWMAP_ATLAS_BORDER,
1165
+ atlas_rect.height - 2 * SHADOWMAP_ATLAS_BORDER,
1166
+ ],
1167
+ light_params: [
1168
+ light_max_distance,
1169
+ tetra_near,
1170
+ 0,
1171
+ 0,
1172
+ ],
1173
+ face_view_proj,
1174
+ }
1175
+ );
1176
+
1177
+ shader_shadowmap_remap_tetrahedron_to_octahedral.draw({
1178
+ encoder: context.encoder,
1179
+ bindings: {
1180
+ settings: settings,
1181
+ tetra_faces: tetra_texture.obtainView(),
1182
+ },
1183
+ depthStencilAttachment: {
1184
+ view: atlas.obtainView(),
1185
+ depthLoadOp: "load",
1186
+ depthStoreOp: "store",
1187
+ },
1188
+ viewport: [
1189
+ atlas_rect.x0,
1190
+ atlas_rect.y0,
1191
+ atlas_rect.width,
1192
+ atlas_rect.height,
1193
+ ]
1194
+ })
1195
+ }
1196
+
1197
+ const remap_data = {};
1198
+
1199
+ const remap_pass = graph.add("remap tetra to atlas", remap_data, remap_to_atlas);
1200
+
1201
+ remap_data.atlas = remap_pass.write(gr_atlas_texture);
1202
+ remap_data.tetra_texture = remap_pass.read(tetra_packed_texture);
1203
+
1204
+
1205
+ command_ctx.encodeGraph(graph);
1206
+ }
1207
+
1208
+ /**
1209
+ * Release the per-face GPU contexts (each view's uniform/HZB/volumetrics buffers + its camera
1210
+ * uniform) backing a map. The dedicated {@link shadow_view_manager} / {@link #cameras} caches
1211
+ * key on the View/Camera objects and have no GC path, so a map's contexts must be released
1212
+ * explicitly here or their GPU buffers leak for the context's lifetime. Most visible when the
1213
+ * point-shadow generator toggle rebuilds every point map (fresh Views each time), but it also
1214
+ * closes the ordinary eviction leak (light removed / stops casting / shadows disabled).
1215
+ *
1216
+ * In this context Views and Cameras are 1:1 per map (each map creates its own), so releasing a
1217
+ * view's camera alongside it can't strand a still-referenced camera.
1218
+ *
1219
+ * @param {Shadowmap} map
1220
+ */
1221
+ #release_map_gpu_contexts(map) {
1222
+ const views = map.views;
1223
+ for (let j = 0; j < views.length; j++) {
1224
+ const view = views[j];
1225
+ this.shadow_view_manager.release(view);
1226
+ this.#cameras.release(view.camera);
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Release a map's atlas rects + GPU side-resources. Caller is responsible for splicing it
1232
+ * out of {@link #registry}.
1233
+ *
1234
+ * @param {Shadowmap} map
1235
+ */
1236
+ #evict_map(map) {
1237
+ for (let j = 0; j < map.layout.length; j++) {
1238
+ this.atlas.remove(map.layout[j]);
1239
+ }
1240
+ if (map.pending_layout !== null) {
1241
+ for (let j = 0; j < map.pending_layout.length; j++) {
1242
+ this.atlas.remove(map.pending_layout[j]);
1243
+ }
1244
+ map.pending_layout = null;
1245
+ }
1246
+
1247
+ // free the per-view/-camera GPU contexts — without this an evicted map's buffers leak
1248
+ this.#release_map_gpu_contexts(map);
1249
+
1250
+ if (map.light.isDirectionalLight === true) {
1251
+ this.#release_caster_row(map);
1252
+ }
1253
+
1254
+ map.light._gpu_shadowmap_id = -1;
1255
+ }
1256
+
1257
+ /**
1258
+ * Reconcile shadow allocations against the current scene state. Evicts maps for lights
1259
+ * that no longer cast shadows or have left the scene, allocates maps for new shadow-
1260
+ * casting lights, and reassigns per-type `_gpu_shadowmap_id` when the map ordering
1261
+ * shifts.
1262
+ *
1263
+ * Called from {@link GPULightCollection.update} every frame, including frames where
1264
+ * {@link LightCollection.version} hasn't moved — `light.casts_shadow` flips are not
1265
+ * tracked by version and are detected here.
1266
+ *
1267
+ * Runs as three linear passes — mark, reclaim, allocate — over {@link #registry}, which owns
1268
+ * the light -> map index and the marks. It used to scan the map array once per light to find
1269
+ * out whether that light already had one, which made the pass quadratic: 148us per frame at
1270
+ * 512 lights against 19us now, and 554us against 45us at 1024. See
1271
+ * benchmark/reconcile.spec.js.
1272
+ *
1273
+ * @returns {boolean} true when at least one map was added/evicted or at least one light's
1274
+ * per-type shadow_id moved. The caller uses this to decide whether the light
1275
+ * records need to be re-published (their `shadow_id` and CASTS_SHADOW_BIT may
1276
+ * have shifted).
1277
+ */
1278
+ process_lights() {
1279
+ const registry = this.#registry;
1280
+ const shadowmaps = registry.maps;
1281
+
1282
+ // When the renderer has shadows disabled, treat every map as evictable so the next
1283
+ // build clears CASTS_SHADOW_BIT on every light. Re-enabling reconstructs everything
1284
+ // from scratch on the following frame — that's a one-frame catch-up cost, considered
1285
+ // an acceptable trade for the simpler invariant ("disabled" = no maps held).
1286
+ if (!this.enabled) {
1287
+ const had_maps = shadowmaps.length > 0;
1288
+ for (let i = shadowmaps.length - 1; i >= 0; i--) {
1289
+ this.#evict_map(shadowmaps[i]);
1290
+ }
1291
+ registry.clear();
1292
+ return had_maps;
1293
+ }
1294
+
1295
+ const scene = this.#scene;
1296
+ const lights = scene.lights.elements;
1297
+ let changed = false;
1298
+
1299
+ const use_tetra = this.use_tetrahedron_point_shadows;
1300
+ const newcomers = this.#reconcile_newcomers;
1301
+
1302
+ registry.begin_reconcile();
1303
+ newcomers.length = 0;
1304
+
1305
+ // Pass 1 — mark. One index lookup per light both answers "does this light already have a
1306
+ // map" and stamps that map as belonging to a light still in the scene, which is what lets
1307
+ // the reclamation below run without building a set of live lights first.
1308
+ //
1309
+ // A light that stopped casting is simply not marked; its map falls out in pass 2 on the
1310
+ // strength of the stale mark alone.
1311
+ for (let i = 0; i < lights.length; i++) {
1312
+ const light = lights[i];
1313
+
1314
+ if (!light.casts_shadow) {
1315
+ continue;
1316
+ }
1317
+
1318
+ const map = registry.mark_live(light);
1319
+
1320
+ if (map === undefined) {
1321
+ newcomers.push(light);
1322
+ continue;
1323
+ }
1324
+
1325
+ // A point map built by the other generator (cube vs tetra) must be rebuilt when the
1326
+ // flag flips — its face count and draw path no longer match. Pass 2 drops it on the
1327
+ // same test; queueing the rebuild here lands it in this pass rather than the next.
1328
+ if (light.isPointLight && Boolean(map.isTetrahedron) !== use_tetra) {
1329
+ newcomers.push(light);
1330
+ }
1331
+ }
1332
+
1333
+ // Pass 2 — reclaim. Anything not marked belongs to a light that left the scene or stopped
1334
+ // casting, and its atlas rect would otherwise stay pinned forever. Walk in reverse so
1335
+ // splice indices stay stable.
1336
+ for (let i = shadowmaps.length - 1; i >= 0; i--) {
1337
+ const map = shadowmaps[i];
1338
+ const light = map.light;
1339
+
1340
+ const generator_mismatch = light.isPointLight
1341
+ && Boolean(map.isTetrahedron) !== use_tetra;
1342
+
1343
+ if (registry.is_marked(map) && !generator_mismatch) {
1344
+ continue;
1345
+ }
1346
+ this.#evict_map(map);
1347
+ registry.remove_at(i);
1348
+ changed = true;
1349
+ }
1350
+
1351
+ const add_directional = (light) => {
1352
+ const map = new DirectionalLightShadowmap();
1353
+ map.light = light;
1354
+
1355
+ const bound = [];
1356
+ try {
1357
+ for (let j = 0; j < SHADOWMAP_CSM_CASCADE_COUNT; j++) {
1358
+ const resolution_index = Math.min(j, DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION.length - 1);
1359
+ const resolution = DEFAULT_SHADOWMAP_DIRECTIONAL_RESOLUTION[resolution_index];
1360
+
1361
+ const rect = this.bind(resolution);
1362
+ bound.push(rect);
1363
+ map.layout[j] = rect;
1364
+
1365
+ const camera = new OrthographicCamera();
1366
+ camera.update();
1367
+
1368
+ const view = new View(camera, scene);
1369
+ view.label = `Shadow Cascade ${j}`;
1370
+
1371
+ map.views[j] = view;
1372
+ }
1373
+ } catch (e) {
1374
+ // Partial bind: roll back any cascade rects already inserted so the atlas isn't
1375
+ // left holding orphans for a map that never got registered.
1376
+ for (let j = 0; j < bound.length; j++) {
1377
+ this.atlas.remove(bound[j]);
1378
+ }
1379
+ throw e;
1380
+ }
1381
+
1382
+ this.#claim_caster_row(map);
1383
+ registry.add(map);
1384
+ };
1385
+
1386
+ const add_spot = (light) => {
1387
+ const map = new SpotLightShadowmap();
1388
+ map.light = light;
1389
+
1390
+ const rect = this.bind(DEFAULT_SHADOWMAP_LOCAL_RESOLUTION);
1391
+ map.layout[0] = rect;
1392
+
1393
+ try {
1394
+ const camera = new PerspectiveCamera();
1395
+ camera.update();
1396
+
1397
+ const view = new View(camera, scene);
1398
+ view.label = `Spot Shadow`;
1399
+
1400
+ map.views[0] = view;
1401
+ } catch (e) {
1402
+ this.atlas.remove(rect);
1403
+ throw e;
1404
+ }
1405
+
1406
+ registry.add(map);
1407
+ };
1408
+
1409
+ const add_point = (light) => {
1410
+
1411
+ // Generator picked by `use_tetra` above; both remap into one octahedral slot, they
1412
+ // only differ in how many face frusta feed that remap (tetra 4, cube 6).
1413
+ const map = use_tetra ? new TetrahedronPointLightShadowmap() : new PointLightShadowmap();
1414
+ map.light = light;
1415
+
1416
+ // single atlas rect holds the octahedral-encoded depth for all faces
1417
+ const rect = this.bind(DEFAULT_SHADOWMAP_LOCAL_RESOLUTION);
1418
+ map.layout[0] = rect;
1419
+
1420
+ const face_count = use_tetra ? POINT_SHADOW_TETRA_FACE_COUNT : 6;
1421
+
1422
+ for (let j = 0; j < face_count; j++) {
1423
+ const camera = new PerspectiveCamera();
1424
+ camera.update();
1425
+
1426
+ const view = new View(camera, scene);
1427
+ view.label = use_tetra ? `Point Shadow (tetra) face ${j}` : `Point Shadow face ${j}`;
1428
+
1429
+ map.views[j] = view;
1430
+ }
1431
+
1432
+ registry.add(map);
1433
+ };
1434
+
1435
+ const add_light = (light) => {
1436
+ // A light present twice in the scene's light array gets queued twice by pass 1, since
1437
+ // neither occurrence finds a map. LightCollection.add doesn't dedupe, so re-check here
1438
+ // rather than letting the registry's one-map-per-light assert fire every frame. Free
1439
+ // in the normal case — this loop only runs over lights that actually need a map.
1440
+ if (registry.get(light) !== undefined) {
1441
+ return;
1442
+ }
1443
+
1444
+ if (light.isDirectionalLight) {
1445
+ add_directional(light);
1446
+ changed = true;
1447
+ } else if (light.isSpotLight) {
1448
+ add_spot(light);
1449
+ changed = true;
1450
+ } else if (light.isPointLight) {
1451
+ add_point(light);
1452
+ changed = true;
1453
+ }
1454
+ };
1455
+
1456
+ // Pass 3 — allocate. Only the lights pass 1 flagged, so on the overwhelming majority of
1457
+ // frames (nothing arrived, nothing left) this loop does not run at all. The old version
1458
+ // walked every light here and re-tested casts_shadow on each; pass 1 already answered that.
1459
+ for (let i = 0; i < newcomers.length; i++) {
1460
+ try {
1461
+ add_light(newcomers[i]);
1462
+ } catch (e) {
1463
+ console.error(e);
1464
+ }
1465
+ }
1466
+
1467
+ // Drop the references rather than leaving them until the next call — a light that gets
1468
+ // removed from the scene right after being added here would otherwise stay reachable from
1469
+ // this scratch array for a frame.
1470
+ newcomers.length = 0;
1471
+
1472
+ // Publish per-type shadowmap ids back onto the lights. Each shadow_* table is indexed
1473
+ // independently, so a point light's shadow_id points into shadow_point, a spot's into
1474
+ // shadow_spot, etc. Type-bucket order MUST match {@link #write_shadow_records} so each
1475
+ // light record's stored shadow_id lines up with the slot we'll write into the table.
1476
+ // When a map's slot index actually changes (eviction shifted neighbours up the array),
1477
+ // mark its record dirty so the next #write_shadow_records re-publishes it into the new
1478
+ // slot, and report `changed` so the caller re-publishes the light records too.
1479
+ let pc = 0;
1480
+ let sc = 0;
1481
+ let dc = 0;
1482
+ for (let i = 0; i < shadowmaps.length; i++) {
1483
+ const map = shadowmaps[i];
1484
+ const light = map.light;
1485
+
1486
+ let new_id;
1487
+ if (light.isPointLight) {
1488
+ new_id = pc++;
1489
+ } else if (light.isSpotLight) {
1490
+ new_id = sc++;
1491
+ } else if (light.isDirectionalLight) {
1492
+ new_id = dc++;
1493
+ } else {
1494
+ continue;
1495
+ }
1496
+
1497
+ if (light._gpu_shadowmap_id !== new_id) {
1498
+ light._gpu_shadowmap_id = new_id;
1499
+ map.metadata_changed = true;
1500
+ changed = true;
1501
+ }
1502
+ }
1503
+
1504
+ return changed;
1505
+ }
1506
+
1507
+ /**
1508
+ *
1509
+ * @param {number} resolution
1510
+ * @returns {AABB2}
1511
+ */
1512
+ bind(resolution) {
1513
+
1514
+ const rect = new AABB2(0, 0, resolution, resolution);
1515
+
1516
+ if (!this.atlas.add(rect)) {
1517
+ throw new Error(`Failed to add shadowmap to atlas`);
1518
+ }
1519
+
1520
+ return rect;
1521
+ }
1522
+
1523
+ destroy() {
1524
+ if (this.#caster_scratch !== null) {
1525
+ this.#caster_scratch.destroy();
1526
+ this.#caster_scratch = null;
1527
+ }
1528
+
1529
+ this.texture.destroy();
1530
+
1531
+ const maps = this.#registry.maps;
1532
+ for (let i = 0; i < maps.length; i++) {
1533
+ const map = maps[i];
1534
+ // release the per-view/-camera GPU contexts too — map.destroy() alone doesn't (the
1535
+ // buffers live in the view/camera manager caches, not on the map)
1536
+ this.#release_map_gpu_contexts(map);
1537
+ map.destroy();
1538
+ }
1539
+ this.#registry.clear();
1540
+ }
1541
+ }