@woosh/meep-engine 3.14.4 → 3.14.6

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 (717) hide show
  1. package/README.md +6 -0
  2. package/editor/Editor.d.ts +162 -0
  3. package/editor/Editor.d.ts.map +1 -0
  4. package/editor/EditorCameraNavigation.d.ts +22 -0
  5. package/editor/EditorCameraNavigation.d.ts.map +1 -0
  6. package/editor/EditorKeyMap.d.ts +28 -0
  7. package/editor/EditorKeyMap.d.ts.map +1 -0
  8. package/editor/SelectionVisualizer.d.ts +37 -0
  9. package/editor/SelectionVisualizer.d.ts.map +1 -0
  10. package/editor/actions/concrete/ActionInvalidateSampler.d.ts +18 -0
  11. package/editor/actions/concrete/ActionInvalidateSampler.d.ts.map +1 -0
  12. package/editor/actions/concrete/ActionUpdateTexture.d.ts +12 -0
  13. package/editor/actions/concrete/ActionUpdateTexture.d.ts.map +1 -0
  14. package/editor/actions/concrete/ActionUpdateTexture.js +1 -1
  15. package/editor/actions/concrete/ArrayCopyAction.d.ts +20 -0
  16. package/editor/actions/concrete/ArrayCopyAction.d.ts.map +1 -0
  17. package/editor/actions/concrete/ComponentAddAction.d.ts +15 -0
  18. package/editor/actions/concrete/ComponentAddAction.d.ts.map +1 -0
  19. package/editor/actions/concrete/ComponentRemoveAction.d.ts +11 -0
  20. package/editor/actions/concrete/ComponentRemoveAction.d.ts.map +1 -0
  21. package/editor/actions/concrete/EntityCreateAction.d.ts +18 -0
  22. package/editor/actions/concrete/EntityCreateAction.d.ts.map +1 -0
  23. package/editor/actions/concrete/EntityRemoveAction.d.ts +11 -0
  24. package/editor/actions/concrete/EntityRemoveAction.d.ts.map +1 -0
  25. package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts +47 -0
  26. package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts.map +1 -0
  27. package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts +38 -0
  28. package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts.map +1 -0
  29. package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts +23 -0
  30. package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts.map +1 -0
  31. package/editor/actions/concrete/PatchTerrainHeightAction.d.ts +32 -0
  32. package/editor/actions/concrete/PatchTerrainHeightAction.d.ts.map +1 -0
  33. package/editor/actions/concrete/SelectionAddAction.d.ts +19 -0
  34. package/editor/actions/concrete/SelectionAddAction.d.ts.map +1 -0
  35. package/editor/actions/concrete/SelectionClearAction.d.ts +9 -0
  36. package/editor/actions/concrete/SelectionClearAction.d.ts.map +1 -0
  37. package/editor/actions/concrete/SelectionRemoveAction.d.ts +10 -0
  38. package/editor/actions/concrete/SelectionRemoveAction.d.ts.map +1 -0
  39. package/editor/actions/concrete/TransformModifyAction.d.ts +17 -0
  40. package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -0
  41. package/editor/actions/concrete/WriteGridValueAction.d.ts +14 -0
  42. package/editor/actions/concrete/WriteGridValueAction.d.ts.map +1 -0
  43. package/editor/camera/camera_framing.d.ts +33 -0
  44. package/editor/camera/camera_framing.d.ts.map +1 -0
  45. package/editor/clipboard.d.ts +19 -0
  46. package/editor/clipboard.d.ts.map +1 -0
  47. package/editor/ecs/EditorEntity.d.ts +16 -0
  48. package/editor/ecs/EditorEntity.d.ts.map +1 -0
  49. package/editor/ecs/EditorEntitySystem.d.ts +8 -0
  50. package/editor/ecs/EditorEntitySystem.d.ts.map +1 -0
  51. package/editor/ecs/component/TypeEditor.d.ts +7 -1
  52. package/editor/ecs/component/TypeEditor.d.ts.map +1 -1
  53. package/editor/ecs/component/TypeEditor.js +5 -0
  54. package/editor/ecs/component/editors/ColorEditor.d.ts +7 -0
  55. package/editor/ecs/component/editors/ColorEditor.d.ts.map +1 -0
  56. package/editor/ecs/component/editors/HTMLElementEditor.d.ts +7 -0
  57. package/editor/ecs/component/editors/HTMLElementEditor.d.ts.map +1 -0
  58. package/editor/ecs/component/editors/ImagePathEditor.d.ts +7 -0
  59. package/editor/ecs/component/editors/ImagePathEditor.d.ts.map +1 -0
  60. package/editor/ecs/component/editors/LargeStringEditor.d.ts +16 -0
  61. package/editor/ecs/component/editors/LargeStringEditor.d.ts.map +1 -0
  62. package/editor/ecs/component/editors/NumericIntervalEditor.d.ts +7 -0
  63. package/editor/ecs/component/editors/NumericIntervalEditor.d.ts.map +1 -0
  64. package/editor/ecs/component/editors/ObservedBooleanEditor.d.ts +7 -0
  65. package/editor/ecs/component/editors/ObservedBooleanEditor.d.ts.map +1 -0
  66. package/editor/ecs/component/editors/ObservedEnumEditor.d.ts +7 -0
  67. package/editor/ecs/component/editors/ObservedEnumEditor.d.ts.map +1 -0
  68. package/editor/ecs/component/editors/ObservedIntegerEditor.d.ts +7 -0
  69. package/editor/ecs/component/editors/ObservedIntegerEditor.d.ts.map +1 -0
  70. package/editor/ecs/component/editors/ObservedStringEditor.d.ts +7 -0
  71. package/editor/ecs/component/editors/ObservedStringEditor.d.ts.map +1 -0
  72. package/editor/ecs/component/editors/Sampler2DEditor.d.ts +7 -0
  73. package/editor/ecs/component/editors/Sampler2DEditor.d.ts.map +1 -0
  74. package/editor/ecs/component/editors/collection/ListEditor.d.ts +8 -0
  75. package/editor/ecs/component/editors/collection/ListEditor.d.ts.map +1 -0
  76. package/editor/ecs/component/editors/common/BitFlagsEditor.d.ts +12 -0
  77. package/editor/ecs/component/editors/common/BitFlagsEditor.d.ts.map +1 -0
  78. package/editor/ecs/component/editors/common/EnumEditor.d.ts +8 -0
  79. package/editor/ecs/component/editors/common/EnumEditor.d.ts.map +1 -0
  80. package/editor/ecs/component/editors/common/makeV3_editor.d.ts +15 -0
  81. package/editor/ecs/component/editors/common/makeV3_editor.d.ts.map +1 -0
  82. package/editor/ecs/component/editors/common/noEditor.d.ts +3 -0
  83. package/editor/ecs/component/editors/common/noEditor.d.ts.map +1 -0
  84. package/editor/ecs/component/editors/common/two_way_sync.d.ts +22 -0
  85. package/editor/ecs/component/editors/common/two_way_sync.d.ts.map +1 -0
  86. package/editor/ecs/component/editors/ecs/GridObstacleEditor.d.ts +7 -0
  87. package/editor/ecs/component/editors/ecs/GridObstacleEditor.d.ts.map +1 -0
  88. package/editor/ecs/component/editors/ecs/MinimapMarkerEditor.d.ts +14 -0
  89. package/editor/ecs/component/editors/ecs/MinimapMarkerEditor.d.ts.map +1 -0
  90. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.d.ts +20 -0
  91. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.d.ts.map +1 -0
  92. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.d.ts +12 -0
  93. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.d.ts.map +1 -0
  94. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.d.ts +14 -0
  95. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.d.ts.map +1 -0
  96. package/editor/ecs/component/editors/ecs/ParticleEmitterEditor.d.ts +52 -0
  97. package/editor/ecs/component/editors/ecs/ParticleEmitterEditor.d.ts.map +1 -0
  98. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.d.ts +47 -0
  99. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.d.ts.map +1 -0
  100. package/editor/ecs/component/editors/ecs/SGMeshEditor.d.ts +21 -0
  101. package/editor/ecs/component/editors/ecs/SGMeshEditor.d.ts.map +1 -0
  102. package/editor/ecs/component/editors/ecs/ShadedGeometryEditor.d.ts +26 -0
  103. package/editor/ecs/component/editors/ecs/ShadedGeometryEditor.d.ts.map +1 -0
  104. package/editor/ecs/component/editors/ecs/SimulationStepDefinitionEditor.d.ts +16 -0
  105. package/editor/ecs/component/editors/ecs/SimulationStepDefinitionEditor.d.ts.map +1 -0
  106. package/editor/ecs/component/editors/ecs/Trail2DEditor.d.ts +15 -0
  107. package/editor/ecs/component/editors/ecs/Trail2DEditor.d.ts.map +1 -0
  108. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts +42 -0
  109. package/editor/ecs/component/editors/ecs/Trail3DEditor.d.ts.map +1 -0
  110. package/editor/ecs/component/editors/ecs/TransformEditor.d.ts +20 -0
  111. package/editor/ecs/component/editors/ecs/TransformEditor.d.ts.map +1 -0
  112. package/editor/ecs/component/editors/ecs/buildGridObstaclePreview.d.ts +8 -0
  113. package/editor/ecs/component/editors/ecs/buildGridObstaclePreview.d.ts.map +1 -0
  114. package/editor/ecs/component/editors/ecs/terrain/SplatMappingEditor.d.ts +19 -0
  115. package/editor/ecs/component/editors/ecs/terrain/SplatMappingEditor.d.ts.map +1 -0
  116. package/editor/ecs/component/editors/ecs/terrain/TerrainEditor.d.ts +27 -0
  117. package/editor/ecs/component/editors/ecs/terrain/TerrainEditor.d.ts.map +1 -0
  118. package/editor/ecs/component/editors/ecs/terrain/TerrainLayerEditor.d.ts +16 -0
  119. package/editor/ecs/component/editors/ecs/terrain/TerrainLayerEditor.d.ts.map +1 -0
  120. package/editor/ecs/component/editors/ecs/terrain/TerrainLayersEditor.d.ts +19 -0
  121. package/editor/ecs/component/editors/ecs/terrain/TerrainLayersEditor.d.ts.map +1 -0
  122. package/editor/ecs/component/editors/ecs/terrain/TerrainOverlayEditor.d.ts +18 -0
  123. package/editor/ecs/component/editors/ecs/terrain/TerrainOverlayEditor.d.ts.map +1 -0
  124. package/editor/ecs/component/editors/geom/QuaternionEditor.d.ts +6 -0
  125. package/editor/ecs/component/editors/geom/QuaternionEditor.d.ts.map +1 -0
  126. package/editor/ecs/component/editors/geom/Vector1Editor.d.ts +7 -0
  127. package/editor/ecs/component/editors/geom/Vector1Editor.d.ts.map +1 -0
  128. package/editor/ecs/component/editors/geom/Vector2Editor.d.ts +6 -0
  129. package/editor/ecs/component/editors/geom/Vector2Editor.d.ts.map +1 -0
  130. package/editor/ecs/component/editors/geom/Vector3Editor.d.ts +6 -0
  131. package/editor/ecs/component/editors/geom/Vector3Editor.d.ts.map +1 -0
  132. package/editor/ecs/component/editors/geom/Vector4Editor.d.ts +6 -0
  133. package/editor/ecs/component/editors/geom/Vector4Editor.d.ts.map +1 -0
  134. package/editor/ecs/component/editors/primitive/ArrayEditor.d.ts +8 -0
  135. package/editor/ecs/component/editors/primitive/ArrayEditor.d.ts.map +1 -0
  136. package/editor/ecs/component/editors/primitive/BooleanEditor.d.ts +7 -0
  137. package/editor/ecs/component/editors/primitive/BooleanEditor.d.ts.map +1 -0
  138. package/editor/ecs/component/editors/primitive/FunctionEditor.d.ts +7 -0
  139. package/editor/ecs/component/editors/primitive/FunctionEditor.d.ts.map +1 -0
  140. package/editor/ecs/component/editors/primitive/NumberEditor.d.ts +8 -0
  141. package/editor/ecs/component/editors/primitive/NumberEditor.d.ts.map +1 -0
  142. package/editor/ecs/component/editors/primitive/StringEditor.d.ts +7 -0
  143. package/editor/ecs/component/editors/primitive/StringEditor.d.ts.map +1 -0
  144. package/editor/ecs/component/editors/shade/MeshletGeometryEditor.d.ts +28 -0
  145. package/editor/ecs/component/editors/shade/MeshletGeometryEditor.d.ts.map +1 -0
  146. package/editor/ecs/component/editors/shade/ShadeImageEditor.d.ts +29 -0
  147. package/editor/ecs/component/editors/shade/ShadeImageEditor.d.ts.map +1 -0
  148. package/editor/ecs/component/editors/shade/ShadeMaterialEditor.d.ts +28 -0
  149. package/editor/ecs/component/editors/shade/ShadeMaterialEditor.d.ts.map +1 -0
  150. package/editor/ecs/component/editors/shade/ShadeTextureEditor.d.ts +41 -0
  151. package/editor/ecs/component/editors/shade/ShadeTextureEditor.d.ts.map +1 -0
  152. package/editor/ecs/component/editors/shade/StandardShadeMaterialEditor.d.ts +31 -0
  153. package/editor/ecs/component/editors/shade/StandardShadeMaterialEditor.d.ts.map +1 -0
  154. package/editor/ecs/component/prototypeObjectEditor.d.ts +2 -0
  155. package/editor/ecs/component/prototypeObjectEditor.d.ts.map +1 -0
  156. package/editor/ecs/component/registerBasicTypeEditors.d.ts +11 -0
  157. package/editor/ecs/component/registerBasicTypeEditors.d.ts.map +1 -0
  158. package/editor/ecs/component/registerEngineComponentEditors.d.ts +8 -0
  159. package/editor/ecs/component/registerEngineComponentEditors.d.ts.map +1 -0
  160. package/editor/enableEditor.d.ts +14 -0
  161. package/editor/enableEditor.d.ts.map +1 -0
  162. package/editor/entity_world_bounds.d.ts +17 -0
  163. package/editor/entity_world_bounds.d.ts.map +1 -0
  164. package/editor/library/MeshLibrary.d.ts +13 -0
  165. package/editor/library/MeshLibrary.d.ts.map +1 -0
  166. package/editor/particles/lut_ops.d.ts +34 -0
  167. package/editor/particles/lut_ops.d.ts.map +1 -0
  168. package/editor/particles/particleEditEvents.d.ts +11 -0
  169. package/editor/particles/particleEditEvents.d.ts.map +1 -0
  170. package/editor/particles/rebuildParticleEmitter.d.ts +11 -0
  171. package/editor/particles/rebuildParticleEmitter.d.ts.map +1 -0
  172. package/editor/persistence/EditorLayoutStore.d.ts +30 -0
  173. package/editor/persistence/EditorLayoutStore.d.ts.map +1 -0
  174. package/editor/persistence/SceneMetadata.d.ts +38 -0
  175. package/editor/persistence/SceneMetadata.d.ts.map +1 -0
  176. package/editor/persistence/SceneSlotManager.d.ts +40 -0
  177. package/editor/persistence/SceneSlotManager.d.ts.map +1 -0
  178. package/editor/persistence/buildNewSceneDataset.d.ts +10 -0
  179. package/editor/persistence/buildNewSceneDataset.d.ts.map +1 -0
  180. package/editor/persistence/files.d.ts +24 -0
  181. package/editor/persistence/files.d.ts.map +1 -0
  182. package/editor/persistence/scene_import.d.ts +45 -0
  183. package/editor/persistence/scene_import.d.ts.map +1 -0
  184. package/editor/persistence/scene_serialization.d.ts +36 -0
  185. package/editor/persistence/scene_serialization.d.ts.map +1 -0
  186. package/editor/persistence/string_buffer_codec.d.ts +20 -0
  187. package/editor/persistence/string_buffer_codec.d.ts.map +1 -0
  188. package/editor/process/DisableGameUIProcess.d.ts +8 -0
  189. package/editor/process/DisableGameUIProcess.d.ts.map +1 -0
  190. package/editor/process/EditorProcess.d.ts +19 -0
  191. package/editor/process/EditorProcess.d.ts.map +1 -0
  192. package/editor/process/GridDisplayProcess.d.ts +35 -0
  193. package/editor/process/GridDisplayProcess.d.ts.map +1 -0
  194. package/editor/process/ObstacleGridDisplayProcess.d.ts +13 -0
  195. package/editor/process/ObstacleGridDisplayProcess.d.ts.map +1 -0
  196. package/editor/process/ParticleEmitterRebuildProcess.d.ts +66 -0
  197. package/editor/process/ParticleEmitterRebuildProcess.d.ts.map +1 -0
  198. package/editor/process/ProcessEngine.d.ts +30 -0
  199. package/editor/process/ProcessEngine.d.ts.map +1 -0
  200. package/editor/process/SymbolicDisplayProcess.d.ts +23 -0
  201. package/editor/process/SymbolicDisplayProcess.d.ts.map +1 -0
  202. package/editor/process/symbolic/AudioEmitterSymbolicDisplay.d.ts +17 -0
  203. package/editor/process/symbolic/AudioEmitterSymbolicDisplay.d.ts.map +1 -0
  204. package/editor/process/symbolic/CameraSymbolicDisplay.d.ts +25 -0
  205. package/editor/process/symbolic/CameraSymbolicDisplay.d.ts.map +1 -0
  206. package/editor/process/symbolic/GridPositionSymbolicDisplay.d.ts +19 -0
  207. package/editor/process/symbolic/GridPositionSymbolicDisplay.d.ts.map +1 -0
  208. package/editor/process/symbolic/LightSymbolicDisplay.d.ts +19 -0
  209. package/editor/process/symbolic/LightSymbolicDisplay.d.ts.map +1 -0
  210. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.d.ts +18 -0
  211. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.d.ts.map +1 -0
  212. package/editor/process/symbolic/PathSymbolicDisplay.d.ts +20 -0
  213. package/editor/process/symbolic/PathSymbolicDisplay.d.ts.map +1 -0
  214. package/editor/process/symbolic/PositionedMarkerSymbolicDisplay.d.ts +25 -0
  215. package/editor/process/symbolic/PositionedMarkerSymbolicDisplay.d.ts.map +1 -0
  216. package/editor/process/symbolic/SymbolicDisplay.d.ts +48 -0
  217. package/editor/process/symbolic/SymbolicDisplay.d.ts.map +1 -0
  218. package/editor/process/symbolic/gizmo_draw_wire_box_oriented.d.ts +13 -0
  219. package/editor/process/symbolic/gizmo_draw_wire_box_oriented.d.ts.map +1 -0
  220. package/editor/process/symbolic/gizmo_draw_wire_circle.d.ts +18 -0
  221. package/editor/process/symbolic/gizmo_draw_wire_circle.d.ts.map +1 -0
  222. package/editor/prototypeEditorShell.d.ts +13 -0
  223. package/editor/prototypeEditorShell.d.ts.map +1 -0
  224. package/editor/selection/editor_pick.d.ts +43 -0
  225. package/editor/selection/editor_pick.d.ts.map +1 -0
  226. package/editor/selection/screen_rect_frustum.d.ts +32 -0
  227. package/editor/selection/screen_rect_frustum.d.ts.map +1 -0
  228. package/editor/templates/entity_templates.d.ts +19 -0
  229. package/editor/templates/entity_templates.d.ts.map +1 -0
  230. package/editor/tools/GridPaintTool.d.ts +18 -0
  231. package/editor/tools/GridPaintTool.d.ts.map +1 -0
  232. package/editor/tools/SelectionTool.d.ts +38 -0
  233. package/editor/tools/SelectionTool.d.ts.map +1 -0
  234. package/editor/tools/SelectionTool.js +2 -2
  235. package/editor/tools/TransformToolV2.d.ts +70 -0
  236. package/editor/tools/TransformToolV2.d.ts.map +1 -0
  237. package/editor/tools/engine/Tool.d.ts +88 -0
  238. package/editor/tools/engine/Tool.d.ts.map +1 -0
  239. package/editor/tools/engine/ToolEngine.d.ts +22 -0
  240. package/editor/tools/engine/ToolEngine.d.ts.map +1 -0
  241. package/editor/tools/engine/ToolState.d.ts +7 -0
  242. package/editor/tools/engine/ToolState.d.ts.map +1 -0
  243. package/editor/tools/paint/TerrainHeightPaintTool.d.ts +18 -0
  244. package/editor/tools/paint/TerrainHeightPaintTool.d.ts.map +1 -0
  245. package/editor/tools/paint/TerrainPaintTool.d.ts +82 -0
  246. package/editor/tools/paint/TerrainPaintTool.d.ts.map +1 -0
  247. package/editor/tools/paint/TerrainTexturePaintTool.d.ts +19 -0
  248. package/editor/tools/paint/TerrainTexturePaintTool.d.ts.map +1 -0
  249. package/editor/tools/v2/BlenderCameraOrientationGizmo.d.ts +139 -0
  250. package/editor/tools/v2/BlenderCameraOrientationGizmo.d.ts.map +1 -0
  251. package/editor/tools/v2/GizmoNode.d.ts +21 -0
  252. package/editor/tools/v2/GizmoNode.d.ts.map +1 -0
  253. package/editor/tools/v2/TransformControls.d.ts +219 -0
  254. package/editor/tools/v2/TransformControls.d.ts.map +1 -0
  255. package/editor/tools/v2/TransformMode.d.ts +7 -0
  256. package/editor/tools/v2/TransformMode.d.ts.map +1 -0
  257. package/editor/tools/v2/handle/gizmo_drag_plane.d.ts +37 -0
  258. package/editor/tools/v2/handle/gizmo_drag_plane.d.ts.map +1 -0
  259. package/editor/tools/v2/handle/gizmo_draw.d.ts +14 -0
  260. package/editor/tools/v2/handle/gizmo_draw.d.ts.map +1 -0
  261. package/editor/tools/v2/handle/gizmo_handles.d.ts +160 -0
  262. package/editor/tools/v2/handle/gizmo_handles.d.ts.map +1 -0
  263. package/editor/tools/v2/handle/gizmo_ray_shapes.d.ts +103 -0
  264. package/editor/tools/v2/handle/gizmo_ray_shapes.d.ts.map +1 -0
  265. package/editor/view/GridPickCoordinateView.d.ts +22 -0
  266. package/editor/view/GridPickCoordinateView.d.ts.map +1 -0
  267. package/editor/view/ecs/ComponentControlView.d.ts +24 -0
  268. package/editor/view/ecs/ComponentControlView.d.ts.map +1 -0
  269. package/editor/view/ecs/EntityEditor.d.ts +25 -0
  270. package/editor/view/ecs/EntityEditor.d.ts.map +1 -0
  271. package/editor/view/ecs/EntityList.d.ts +22 -0
  272. package/editor/view/ecs/EntityList.d.ts.map +1 -0
  273. package/editor/view/ecs/HierarchicalEntityListView.d.ts +25 -0
  274. package/editor/view/ecs/HierarchicalEntityListView.d.ts.map +1 -0
  275. package/editor/view/ecs/components/DatGuiController.d.ts +2 -0
  276. package/editor/view/ecs/components/DatGuiController.d.ts.map +1 -0
  277. package/editor/view/ecs/components/color/GaugeView.d.ts +9 -5
  278. package/editor/view/ecs/components/color/GaugeView.d.ts.map +1 -1
  279. package/editor/view/ecs/components/color/GaugeView.js +4 -4
  280. package/editor/view/ecs/components/common/NumberController.d.ts +9 -5
  281. package/editor/view/ecs/components/common/NumberController.d.ts.map +1 -1
  282. package/editor/view/ecs/components/common/NumberController.js +4 -4
  283. package/editor/view/ecs/components/common/NumericIntervalController.d.ts +10 -0
  284. package/editor/view/ecs/components/common/NumericIntervalController.d.ts.map +1 -0
  285. package/editor/view/ecs/components/common/TextController.d.ts.map +1 -1
  286. package/editor/view/library/MeshLibraryView.d.ts +16 -0
  287. package/editor/view/library/MeshLibraryView.d.ts.map +1 -0
  288. package/editor/view/library/mesh_library_drag.d.ts +21 -0
  289. package/editor/view/library/mesh_library_drag.d.ts.map +1 -0
  290. package/editor/view/library/model_thumbnail_source.d.ts +12 -0
  291. package/editor/view/library/model_thumbnail_source.d.ts.map +1 -0
  292. package/editor/view/makeEntityDecorators.d.ts +6 -0
  293. package/editor/view/makeEntityDecorators.d.ts.map +1 -0
  294. package/editor/view/node-graph/NodeGraphCamera.d.ts.map +1 -1
  295. package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
  296. package/editor/view/node-graph/NodeGraphSelection.d.ts.map +1 -1
  297. package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
  298. package/editor/view/node-graph/NodeView.d.ts.map +1 -1
  299. package/editor/view/node-graph/PortView.d.ts.map +1 -1
  300. package/editor/view/node-graph/actions/ConnectionCreateAction.d.ts.map +1 -1
  301. package/editor/view/node-graph/actions/ConnectionDeleteAction.d.ts.map +1 -1
  302. package/editor/view/node-graph/actions/NodeDeleteAction.d.ts.map +1 -1
  303. package/editor/view/node-graph/actions/NodeParameterSetAction.d.ts.map +1 -1
  304. package/editor/view/node-graph/actions/NodesMoveAction.d.ts.map +1 -1
  305. package/editor/view/node-graph/actions/SelectionSetAction.d.ts.map +1 -1
  306. package/editor/view/node-graph/connection_wire_geometry.d.ts.map +1 -1
  307. package/editor/view/particles/ColorLutGradientView.d.ts +19 -0
  308. package/editor/view/particles/ColorLutGradientView.d.ts.map +1 -0
  309. package/editor/view/particles/ScalarLutCurveView.d.ts +19 -0
  310. package/editor/view/particles/ScalarLutCurveView.d.ts.map +1 -0
  311. package/editor/view/prepareMeshLibrary.d.ts +7 -0
  312. package/editor/view/prepareMeshLibrary.d.ts.map +1 -0
  313. package/editor/view/process/ProcessView.d.ts +14 -0
  314. package/editor/view/process/ProcessView.d.ts.map +1 -0
  315. package/editor/view/shell/EditorShellView.d.ts +18 -0
  316. package/editor/view/shell/EditorShellView.d.ts.map +1 -0
  317. package/editor/view/shell/SceneTreePanelView.d.ts +17 -0
  318. package/editor/view/shell/SceneTreePanelView.d.ts.map +1 -0
  319. package/editor/view/shell/ViewportPanelView.d.ts +18 -0
  320. package/editor/view/shell/ViewportPanelView.d.ts.map +1 -0
  321. package/editor/view/tools/ToolSettingsView.d.ts +22 -0
  322. package/editor/view/tools/ToolSettingsView.d.ts.map +1 -0
  323. package/editor/view/tools/ToolView.d.ts +13 -0
  324. package/editor/view/tools/ToolView.d.ts.map +1 -0
  325. package/editor/view/v2/SplitView.d.ts +2 -0
  326. package/editor/view/v2/SplitView.d.ts.map +1 -0
  327. package/package.json +9 -11
  328. package/samples/engine/README.md +113 -0
  329. package/samples/engine/first-scene/index.html +32 -0
  330. package/samples/engine/first-scene/main.js +152 -0
  331. package/src/core/assert.d.ts +2 -2
  332. package/src/core/binary/BitSet.d.ts +2 -2
  333. package/src/core/binary/BitSet.d.ts.map +1 -1
  334. package/src/core/binary/BitSet.js +1 -1
  335. package/src/core/bvh2/bvh3/BVH.d.ts +4 -0
  336. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  337. package/src/core/bvh2/bvh3/BVH.js +4 -0
  338. package/src/core/bvh2/bvh3/BvhClient.d.ts +4 -0
  339. package/src/core/bvh2/bvh3/BvhClient.d.ts.map +1 -1
  340. package/src/core/bvh2/bvh3/BvhClient.js +4 -0
  341. package/src/core/collection/array/array_sort_quick.d.ts +1 -1
  342. package/src/core/collection/list/List.d.ts +7 -3
  343. package/src/core/collection/list/List.d.ts.map +1 -1
  344. package/src/core/collection/list/List.js +6 -2
  345. package/src/core/collection/table/RowFirstTableSpec.d.ts.map +1 -1
  346. package/src/core/collection/table/RowFirstTableSpec.js +0 -4
  347. package/src/core/color/Color.d.ts +1 -0
  348. package/src/core/color/Color.d.ts.map +1 -1
  349. package/src/core/color/Color.js +4 -0
  350. package/src/core/events/signal/SignalBinding.d.ts +4 -0
  351. package/src/core/events/signal/SignalBinding.d.ts.map +1 -1
  352. package/src/core/events/signal/SignalBinding.js +4 -0
  353. package/src/core/geom/2d/quad-tree/QuadTreeDatum.d.ts +5 -1
  354. package/src/core/geom/2d/quad-tree/QuadTreeDatum.d.ts.map +1 -1
  355. package/src/core/geom/2d/quad-tree/QuadTreeDatum.js +4 -0
  356. package/src/core/geom/3d/SurfacePoint3.d.ts +2 -2
  357. package/src/core/geom/3d/SurfacePoint3.d.ts.map +1 -1
  358. package/src/core/geom/3d/SurfacePoint3.js +1 -1
  359. package/src/core/geom/3d/aabb/AABB3.d.ts +63 -7
  360. package/src/core/geom/3d/aabb/AABB3.d.ts.map +1 -1
  361. package/src/core/geom/3d/aabb/AABB3.js +17 -3
  362. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
  363. package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
  364. package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
  365. package/src/core/geom/3d/plane/plane3_compute_intersection_objects.js +2 -2
  366. package/src/core/geom/3d/ray/Ray3.d.ts +4 -4
  367. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +10 -3
  368. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  369. package/src/core/geom/3d/shape/AbstractShape3D.js +9 -2
  370. package/src/core/geom/3d/shape/TransformedShape3D.d.ts +2 -2
  371. package/src/core/geom/3d/sphere/harmonics/sh3_rotate.d.ts +1 -1
  372. package/src/core/geom/Vector1.d.ts +4 -0
  373. package/src/core/geom/Vector1.d.ts.map +1 -1
  374. package/src/core/geom/Vector1.js +4 -0
  375. package/src/core/geom/Vector2.d.ts +4 -0
  376. package/src/core/geom/Vector2.d.ts.map +1 -1
  377. package/src/core/geom/Vector2.js +4 -0
  378. package/src/core/geom/Vector3.d.ts +4 -0
  379. package/src/core/geom/Vector3.d.ts.map +1 -1
  380. package/src/core/geom/Vector3.js +4 -0
  381. package/src/core/geom/Vector4.d.ts +4 -0
  382. package/src/core/geom/Vector4.d.ts.map +1 -1
  383. package/src/core/geom/Vector4.js +4 -0
  384. package/src/core/geom/packing/max-rect/MaxRectanglesPacker.d.ts +6 -2
  385. package/src/core/geom/packing/max-rect/MaxRectanglesPacker.d.ts.map +1 -1
  386. package/src/core/geom/packing/max-rect/MaxRectanglesPacker.js +4 -0
  387. package/src/core/geom/packing/miniball/Subspan.d.ts +4 -4
  388. package/src/core/geom/packing/miniball/Subspan.d.ts.map +1 -1
  389. package/src/core/geom/packing/miniball/Subspan.js +2 -2
  390. package/src/core/math/interval/NumericInterval.d.ts +4 -0
  391. package/src/core/math/interval/NumericInterval.d.ts.map +1 -1
  392. package/src/core/math/interval/NumericInterval.js +4 -0
  393. package/src/core/math/random/roundFair.d.ts +2 -2
  394. package/src/core/math/random/roundFair.d.ts.map +1 -1
  395. package/src/core/math/random/roundFair.js +1 -1
  396. package/src/core/model/BoundedValue.d.ts +4 -0
  397. package/src/core/model/BoundedValue.d.ts.map +1 -1
  398. package/src/core/model/BoundedValue.js +4 -0
  399. package/src/core/model/ObservedBoolean.d.ts +4 -0
  400. package/src/core/model/ObservedBoolean.d.ts.map +1 -1
  401. package/src/core/model/ObservedBoolean.js +4 -0
  402. package/src/core/model/ObservedInteger.d.ts +4 -0
  403. package/src/core/model/ObservedInteger.d.ts.map +1 -1
  404. package/src/core/model/ObservedInteger.js +4 -0
  405. package/src/core/model/ObservedString.d.ts +4 -0
  406. package/src/core/model/ObservedString.d.ts.map +1 -1
  407. package/src/core/model/ObservedString.js +4 -0
  408. package/src/core/model/stat/Stat.d.ts +7 -3
  409. package/src/core/model/stat/Stat.d.ts.map +1 -1
  410. package/src/core/model/stat/Stat.js +4 -0
  411. package/src/engine/Engine.d.ts.map +1 -1
  412. package/src/engine/Engine.js +65 -0
  413. package/src/engine/EngineHarness.d.ts +8 -1
  414. package/src/engine/EngineHarness.d.ts.map +1 -1
  415. package/src/engine/EngineHarness.js +10 -14
  416. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts +2 -2
  417. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  418. package/src/engine/animation/clip/ecd_bind_animation_curve.js +4 -4
  419. package/src/engine/asset/loaders/async_traverse_three_object.d.ts +2 -2
  420. package/src/engine/asset/loaders/async_traverse_three_object.d.ts.map +1 -1
  421. package/src/engine/asset/loaders/async_traverse_three_object.js +1 -1
  422. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts +7 -3
  423. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts.map +1 -1
  424. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.js +33 -3
  425. package/src/engine/control/first-person/DESIGN.md +20 -2
  426. package/src/engine/control/first-person/DESIGN_COLLISION.md +17 -0
  427. package/src/engine/control/first-person/collision/KinematicMover.d.ts +58 -11
  428. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  429. package/src/engine/control/first-person/collision/KinematicMover.js +97 -20
  430. package/src/engine/ecs/Entity.js +1 -1
  431. package/src/engine/ecs/EntityComponentDataset.d.ts +2 -2
  432. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  433. package/src/engine/ecs/EntityComponentDataset.js +1 -1
  434. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  435. package/src/engine/ecs/EntityManager.js +27 -3
  436. package/src/engine/ecs/animation/Animation.d.ts +17 -3
  437. package/src/engine/ecs/animation/Animation.d.ts.map +1 -1
  438. package/src/engine/ecs/animation/Animation.js +26 -6
  439. package/src/engine/ecs/terrain/TerrainPreview.d.ts +4 -0
  440. package/src/engine/ecs/terrain/TerrainPreview.d.ts.map +1 -1
  441. package/src/engine/ecs/terrain/TerrainPreview.js +4 -0
  442. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts +4 -4
  443. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  444. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +2 -2
  445. package/src/engine/ecs/terrain/ecs/makeTerrainWorkerProxy.d.ts +3 -2
  446. package/src/engine/ecs/terrain/ecs/makeTerrainWorkerProxy.d.ts.map +1 -1
  447. package/src/engine/ecs/terrain/ecs/makeTerrainWorkerProxy.js +25 -2
  448. package/src/engine/ecs/transform/Transform64.d.ts +4 -4
  449. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  450. package/src/engine/ecs/transform/Transform64.js +2 -2
  451. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts +26 -0
  452. package/src/engine/graphics/ecs/decal/v2/Decal.d.ts.map +1 -1
  453. package/src/engine/graphics/ecs/decal/v2/Decal.js +26 -0
  454. package/src/engine/graphics/ecs/light/Light.d.ts +38 -1
  455. package/src/engine/graphics/ecs/light/Light.d.ts.map +1 -1
  456. package/src/engine/graphics/ecs/light/Light.js +51 -1
  457. package/src/engine/graphics/ecs/mesh/copyToVector3.d.ts +3 -3
  458. package/src/engine/graphics/ecs/mesh/copyToVector3.d.ts.map +1 -1
  459. package/src/engine/graphics/ecs/mesh/copyToVector3.js +2 -2
  460. package/src/engine/graphics/ecs/mesh/setMesh.d.ts +2 -2
  461. package/src/engine/graphics/ecs/mesh/setMesh.d.ts.map +1 -1
  462. package/src/engine/graphics/ecs/mesh/setMesh.js +1 -1
  463. package/src/engine/graphics/geometry/buffered/computeBufferAttributeHash.d.ts +2 -2
  464. package/src/engine/graphics/geometry/buffered/computeBufferAttributeHash.d.ts.map +1 -1
  465. package/src/engine/graphics/geometry/buffered/computeBufferAttributeHash.js +1 -1
  466. package/src/engine/graphics/geometry/buffered/computeGeometryEquality.d.ts +3 -3
  467. package/src/engine/graphics/geometry/buffered/computeGeometryEquality.d.ts.map +1 -1
  468. package/src/engine/graphics/geometry/buffered/computeGeometryEquality.js +2 -2
  469. package/src/engine/graphics/geometry/buffered/computeGeometryHash.d.ts +2 -2
  470. package/src/engine/graphics/geometry/buffered/computeGeometryHash.d.ts.map +1 -1
  471. package/src/engine/graphics/geometry/buffered/computeGeometryHash.js +1 -1
  472. package/src/engine/graphics/geometry/buffered/compute_buffer_geometry_byte_size.d.ts +2 -2
  473. package/src/engine/graphics/geometry/buffered/compute_buffer_geometry_byte_size.d.ts.map +1 -1
  474. package/src/engine/graphics/geometry/buffered/compute_buffer_geometry_byte_size.js +1 -1
  475. package/src/engine/graphics/geometry/compute_geometry_polycount.d.ts +2 -2
  476. package/src/engine/graphics/geometry/compute_geometry_polycount.d.ts.map +1 -1
  477. package/src/engine/graphics/geometry/compute_geometry_polycount.js +1 -1
  478. package/src/engine/graphics/geometry/ribbon/copyAttributeV3.d.ts +3 -3
  479. package/src/engine/graphics/geometry/ribbon/copyAttributeV3.d.ts.map +1 -1
  480. package/src/engine/graphics/geometry/ribbon/copyAttributeV3.js +2 -2
  481. package/src/engine/graphics/geometry/ribbon/copyAttributeValue.d.ts +4 -4
  482. package/src/engine/graphics/geometry/ribbon/copyAttributeValue.d.ts.map +1 -1
  483. package/src/engine/graphics/geometry/ribbon/copyAttributeValue.js +3 -3
  484. package/src/engine/graphics/geometry/ribbon/equalAttributeV3.d.ts +3 -3
  485. package/src/engine/graphics/geometry/ribbon/equalAttributeV3.d.ts.map +1 -1
  486. package/src/engine/graphics/geometry/ribbon/equalAttributeV3.js +2 -2
  487. package/src/engine/graphics/geometry/ribbon/equalAttributeValue.d.ts +4 -4
  488. package/src/engine/graphics/geometry/ribbon/equalAttributeValue.d.ts.map +1 -1
  489. package/src/engine/graphics/geometry/ribbon/equalAttributeValue.js +3 -3
  490. package/src/engine/graphics/geometry/scaleGeometryToBox.d.ts +2 -2
  491. package/src/engine/graphics/geometry/scaleGeometryToBox.d.ts.map +1 -1
  492. package/src/engine/graphics/geometry/scaleGeometryToBox.js +1 -1
  493. package/src/engine/graphics/impostors/card_cluster/FacePlaneAssignment.d.ts +2 -2
  494. package/src/engine/graphics/impostors/card_cluster/FacePlaneAssignment.d.ts.map +1 -1
  495. package/src/engine/graphics/impostors/card_cluster/FacePlaneAssignment.js +1 -1
  496. package/src/engine/graphics/impostors/octahedral/ImpostorDescription.d.ts +2 -2
  497. package/src/engine/graphics/impostors/octahedral/ImpostorDescription.d.ts.map +1 -1
  498. package/src/engine/graphics/impostors/octahedral/ImpostorDescription.js +1 -1
  499. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts +4 -12
  500. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts.map +1 -1
  501. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +2 -2
  502. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.d.ts +5 -20
  503. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.d.ts.map +1 -1
  504. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.d.ts +24 -24
  505. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.d.ts.map +1 -1
  506. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.js +16 -16
  507. package/src/engine/graphics/render/layers/RenderLayer.d.ts +4 -4
  508. package/src/engine/graphics/render/layers/RenderLayer.d.ts.map +1 -1
  509. package/src/engine/graphics/render/layers/RenderLayer.js +2 -2
  510. package/src/engine/graphics/render/make_bvh_visibility_builder.js +1 -1
  511. package/src/engine/graphics/render/visibility/VisibilityComputer.d.ts +1 -1
  512. package/src/engine/graphics/render/visibility/VisibilityComputer.js +1 -1
  513. package/src/engine/graphics/texture/computeThreeTextureInternalFormatFromDataType.d.ts +2 -2
  514. package/src/engine/graphics/texture/computeThreeTextureInternalFormatFromDataType.d.ts.map +1 -1
  515. package/src/engine/graphics/texture/computeThreeTextureInternalFormatFromDataType.js +1 -1
  516. package/src/engine/graphics3/DecalSystem.d.ts +8 -0
  517. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  518. package/src/engine/graphics3/DecalSystem.js +8 -0
  519. package/src/engine/graphics3/GraphicsEngine.d.ts +19 -8
  520. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  521. package/src/engine/graphics3/GraphicsEngine.js +22 -11
  522. package/src/engine/graphics3/LightSystem.d.ts.map +1 -1
  523. package/src/engine/graphics3/LightSystem.js +11 -1
  524. package/src/engine/input/devices/gamepad/GamepadStickCalibration.d.ts +4 -0
  525. package/src/engine/input/devices/gamepad/GamepadStickCalibration.d.ts.map +1 -1
  526. package/src/engine/input/devices/gamepad/GamepadStickCalibration.js +4 -0
  527. package/src/engine/navigation/mesh/NavigationMesh.d.ts +90 -9
  528. package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
  529. package/src/engine/navigation/mesh/NavigationMesh.js +80 -8
  530. package/src/engine/network/NetworkSession.d.ts +60 -0
  531. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  532. package/src/engine/network/NetworkSession.js +141 -1
  533. package/src/engine/network/orchestrator/NetworkPeer.d.ts +56 -8
  534. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  535. package/src/engine/network/orchestrator/NetworkPeer.js +263 -67
  536. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
  537. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  538. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
  539. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +13 -7
  540. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  541. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +103 -138
  542. package/src/engine/network/replication/Replicator.d.ts +47 -6
  543. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  544. package/src/engine/network/replication/Replicator.js +362 -107
  545. package/src/engine/network/sim/ActionLog.d.ts +2 -0
  546. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  547. package/src/engine/network/sim/ActionLog.js +2 -0
  548. package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
  549. package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
  550. package/src/engine/network/sim/ActionRecordCursor.js +120 -0
  551. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  552. package/src/engine/network/sim/RewindEngine.js +13 -29
  553. package/src/engine/network/sim/SimActionExecutor.d.ts +2 -3
  554. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  555. package/src/engine/network/sim/SimActionExecutor.js +10 -11
  556. package/src/engine/physics/ecs/PhysicsSystem.d.ts +27 -0
  557. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  558. package/src/engine/physics/ecs/PhysicsSystem.js +27 -0
  559. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  560. package/src/engine/sound/SoundEngine.d.ts +24 -0
  561. package/src/engine/sound/SoundEngine.d.ts.map +1 -1
  562. package/src/engine/sound/SoundEngine.js +29 -11
  563. package/src/engine/sound/simulation/configureAcousticSimulation.d.ts +12 -4
  564. package/src/engine/sound/simulation/configureAcousticSimulation.d.ts.map +1 -1
  565. package/src/engine/sound/simulation/configureAcousticSimulation.js +10 -4
  566. package/src/engine/sound/simulation/ecs/AcousticProbeFieldSystem.d.ts +3 -1
  567. package/src/engine/sound/simulation/ecs/AcousticProbeFieldSystem.d.ts.map +1 -1
  568. package/src/engine/sound/simulation/ecs/AcousticProbeFieldSystem.js +27 -1
  569. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts +34 -0
  570. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts.map +1 -1
  571. package/src/engine/sound/simulation/render/ProbeReverbRenderer.js +61 -2
  572. package/src/engine/sound/simulation/render/reverbImpulseResponse.d.ts.map +1 -1
  573. package/src/engine/sound/simulation/render/reverbImpulseResponse.js +9 -0
  574. package/src/shade/playground/particle_system/README.md +14 -3
  575. package/src/shade/playground/particle_system/index.html +1 -0
  576. package/src/shade/playground/particle_system/main.js +15 -2
  577. package/src/shade/playground/particle_system/particle_scene.d.ts +11 -2
  578. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  579. package/src/shade/playground/particle_system/particle_scene.js +164 -46
  580. package/src/shade/playground/skinned_mesh_soup/README.md +39 -18
  581. package/src/shade/playground/skinned_mesh_soup/index.html +9 -6
  582. package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
  583. package/src/shade/playground/skinned_mesh_soup/main.js +16 -13
  584. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +8 -2
  585. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
  586. package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +23 -4
  587. package/src/shade/renderer/GraphicsContext.d.ts +1 -0
  588. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  589. package/src/shade/renderer/GraphicsContext.js +4 -0
  590. package/src/shade/renderer/Renderer.d.ts +1 -1
  591. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  592. package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
  593. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
  594. package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
  595. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
  596. package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
  597. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
  598. package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
  599. package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
  600. package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
  601. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
  602. package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
  603. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
  604. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
  605. package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
  606. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
  607. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
  608. package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
  609. package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
  610. package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
  611. package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
  612. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
  613. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
  614. package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
  615. package/src/shade/renderer/geometry/MeshletGeometrySerializationAdapter.d.ts +13 -0
  616. package/src/shade/renderer/geometry/MeshletGeometrySerializationAdapter.d.ts.map +1 -1
  617. package/src/shade/renderer/geometry/MeshletGeometrySerializationAdapter.js +13 -0
  618. package/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.d.ts +18 -0
  619. package/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.d.ts.map +1 -1
  620. package/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js +18 -0
  621. package/src/shade/renderer/global_illumination/lpv/LightProbeVolume.d.ts +4 -0
  622. package/src/shade/renderer/global_illumination/lpv/LightProbeVolume.d.ts.map +1 -1
  623. package/src/shade/renderer/global_illumination/lpv/LightProbeVolume.js +4 -0
  624. package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.d.ts +12 -0
  625. package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.d.ts.map +1 -1
  626. package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.js +13 -0
  627. package/src/shade/renderer/light/environment/rgbe/parse_rgbe.d.ts +1 -1
  628. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  629. package/src/shade/renderer/particles/DESIGN.md +71 -20
  630. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +29 -5
  631. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  632. package/src/shade/renderer/particles/GPUParticleSystem.js +61 -6
  633. package/src/shade/renderer/particles/ParticleConstants.d.ts +31 -0
  634. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  635. package/src/shade/renderer/particles/ParticleConstants.js +33 -0
  636. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts +61 -0
  637. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts.map +1 -0
  638. package/src/shade/renderer/particles/coherence/graph_particle_bucket.js +179 -0
  639. package/src/shade/renderer/particles/coherence/particle_program_buckets.d.ts +63 -0
  640. package/src/shade/renderer/particles/coherence/particle_program_buckets.d.ts.map +1 -0
  641. package/src/shade/renderer/particles/coherence/particle_program_buckets.js +116 -0
  642. package/src/shade/renderer/particles/coherence/shader_particle_bucket.d.ts +59 -0
  643. package/src/shade/renderer/particles/coherence/shader_particle_bucket.d.ts.map +1 -0
  644. package/src/shade/renderer/particles/coherence/shader_particle_bucket.js +344 -0
  645. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.d.ts +27 -5
  646. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.d.ts.map +1 -1
  647. package/src/shade/renderer/particles/data/PARTICLE_COUNTERS.js +27 -5
  648. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts +13 -1
  649. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts.map +1 -1
  650. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.js +15 -1
  651. package/src/shade/renderer/particles/data/chunk_particle_emitter.d.ts.map +1 -1
  652. package/src/shade/renderer/particles/data/chunk_particle_emitter.js +3 -0
  653. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
  654. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
  655. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
  656. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
  657. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  658. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
  659. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
  660. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  661. package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
  662. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
  663. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
  664. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
  665. package/src/shade/renderer/particles/graph_particles.d.ts +39 -11
  666. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  667. package/src/shade/renderer/particles/graph_particles.js +60 -20
  668. package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
  669. package/src/shade/renderer/particles/particle_prototype.js +181 -180
  670. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +8 -1
  671. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  672. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +8 -1
  673. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +22 -2
  674. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts.map +1 -1
  675. package/src/shade/renderer/particles/runtime/ProgramArena.js +54 -3
  676. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
  677. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  678. package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
  679. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  680. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +32 -14
  681. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts +2 -23
  682. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  683. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +40 -11
  684. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts +6 -17
  685. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts.map +1 -1
  686. package/src/shade/renderer/particles/sort/shader_particle_sort.js +60 -9
  687. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
  688. package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
  689. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
  690. package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
  691. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
  692. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
  693. package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
  694. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +1 -1
  695. package/src/view/DOM.d.ts +69 -0
  696. package/src/view/DOM.d.ts.map +1 -0
  697. package/src/view/DOM.js +2 -1
  698. package/src/view/common/LocalizedLabelView.d.ts +50 -0
  699. package/src/view/common/LocalizedLabelView.d.ts.map +1 -0
  700. package/src/view/controller/controls/GuiControl.d.ts +19 -0
  701. package/src/view/controller/controls/GuiControl.d.ts.map +1 -0
  702. package/src/view/elements/FrameRateView.d.ts +45 -0
  703. package/src/view/elements/FrameRateView.d.ts.map +1 -0
  704. package/src/view/elements/FrameRateView.js +131 -0
  705. package/src/view/elements/SliderView.d.ts +37 -0
  706. package/src/view/elements/SliderView.d.ts.map +1 -0
  707. package/src/view/elements/SliderView.js +71 -0
  708. package/src/view/elements/navigation/ViewStack.d.ts +1 -1
  709. package/src/view/elements/navigation/ViewStack.js +1 -1
  710. package/src/view/elements/notify/NotificationView.d.ts +22 -0
  711. package/src/view/elements/notify/NotificationView.d.ts.map +1 -0
  712. package/src/view/tooltip/TooltipView.d.ts +5 -1
  713. package/src/view/tooltip/TooltipView.d.ts.map +1 -1
  714. package/src/view/tooltip/TooltipView.js +4 -0
  715. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
  716. package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
  717. package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
@@ -1,1796 +1,1958 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { FrameGraph } from "../../../engine/graphics/render/frame_graph/FrameGraph.js";
3
- import { GPUDatabase } from "../buffer/table/GPUDatabase.js";
4
- import {
5
- graph_scene_update_transform_hierarchy
6
- } from "../scene/hierarchy/graph_scene_update_transform_hierarchy.js";
7
- import { graph_import_buffer } from "../shader/graph/graph_import_buffer.js";
8
- import {
9
- ANIMATION_BOUND_TRACK_GROUP_NULL,
10
- TRACKS_PER_GROUP_NODE,
11
- } from "./ANIMATION_BOUND_TRACK_GROUP_STRUCT.js";
12
- import { ANIMATION_DATABASE_SPEC } from "./ANIMATION_DATABASE_SPEC.js";
13
- import { AnimationClipFlags } from "./AnimationClipFlags.js";
14
- import { KEYFRAMES_PER_BLOCK } from "./ANIMATION_KEYFRAME_BLOCK_STRUCT.js";
15
- import {
16
- ANIMATION_SKIN_JOINT_BLOCK_NULL,
17
- JOINTS_PER_SKIN_BLOCK,
18
- } from "./ANIMATION_SKIN_JOINT_BLOCK_STRUCT.js";
19
- import { POSE_ACCUMULATOR_SLOT_BYTES } from "./POSE_ACCUMULATOR_LAYOUT.js";
20
- import {
21
- shader_animation_apply,
22
- WORKGROUP_SIZE as ANIMATION_APPLY_WORKGROUP_SIZE,
23
- } from "./shader_animation_apply.js";
24
- import {
25
- shader_animation_pose_resolve,
26
- WORKGROUP_SIZE as ANIMATION_POSE_RESOLVE_WORKGROUP_SIZE,
27
- } from "./shader_animation_pose_resolve.js";
28
- import {
29
- shader_animation_tick,
30
- WORKGROUP_SIZE as ANIMATION_TICK_WORKGROUP_SIZE,
31
- } from "./shader_animation_tick.js";
32
- import {
33
- shader_skin_matrix_prep,
34
- WORKGROUP_SIZE as SKIN_MATRIX_PREP_WORKGROUP_SIZE,
35
- } from "./skinning/shader_skin_matrix_prep.js";
36
-
37
- /**
38
- * Tail-slot placeholders for the fixed-size inline arrays in chain
39
- * nodes. The shader's `count` guard means these bytes are never read,
40
- * but the upload buffer doesn't zero unwritten ranges and
41
- * `write_struct_value` skips undefined fields — so explicit zeros are
42
- * the only way to keep tail bytes deterministic. Frozen so a stray
43
- * mutation can't bleed into multiple slots that share the reference.
44
- */
45
- const ZERO_KEYFRAME = Object.freeze({ time: 0, value: 0, inTangent: 0, outTangent: 0 });
46
- const ZERO_BINDING = Object.freeze({ track: 0, instance: 0, property: 0 });
47
- // Object.freeze on a typed array with elements throws under V8
48
- // ("Cannot freeze array buffer views with elements"), so we leave the
49
- // Float32Array unfrozen — the surrounding placeholder object is still
50
- // frozen to keep accidental field-level mutation out of bounds.
51
- const ZERO_INVERSE_BIND = new Float32Array(16);
52
- const ZERO_SKIN_JOINT = Object.freeze({ node: 0, inverse_bind: ZERO_INVERSE_BIND });
53
-
54
- /**
55
- * One skinning matrix is a `mat4x4f` 16 f32s, 64 bytes. The
56
- * `skin_matrices` scratch buffer holds one entry per (skin, joint) pair
57
- * across every registered skin.
58
- */
59
- const SKIN_MATRIX_BYTES = 64;
60
-
61
- /**
62
- * Initial capacity of the `skin_matrices` scratch buffer, in matrices.
63
- * 256 × 64 = 16 KB — fits a handful of small skeletons before the first
64
- * grow. Picked to be wasteful neither at "no skins" nor "one character"
65
- * scale; growth doubles, so a few realloc cycles cover anything larger.
66
- */
67
- const SKIN_MATRICES_INITIAL_CAPACITY = 256;
68
-
69
- /**
70
- * Initial capacity of the {@link #pose_accumulator_buffer} in rows
71
- * (= transforms-table slots). 256 × 64 = 16 KB; growth doubles to
72
- * fit larger scenes. Resolve always zeroes the slot after draining,
73
- * so a fresh buffer (createBuffer is zero-initialised) and a "no
74
- * contribution this frame" state are observationally identical.
75
- */
76
- const POSE_ACCUMULATOR_INITIAL_CAPACITY = 256;
77
-
78
- /**
79
- * Per-frame `dt` cap pushed into the tick shader. Without it, a tab
80
- * backgrounded for several seconds (or a paused debugger) would
81
- * leak that whole interval into the next tick — either rocketing a
82
- * looping clip through dozens of cycles in one step or, for
83
- * non-looping clips, clamping them past the meaningful range. The
84
- * cap trades a few frames of CSS-smooth animation after a stall
85
- * for a deterministic clip state.
86
- */
87
- const MAX_DT_SECONDS = 1 / 15;
88
-
89
- /**
90
- * Allocate a singly-linked chain of fixed-capacity nodes into `table`,
91
- * one node per `items_per_chunk`-sized window of `items`. Builds
92
- * tail-first so each node's successor id is known at `add()` time —
93
- * the alternative (forward + back-patch) would mean a second `set()`
94
- * per node.
95
- *
96
- * `build_node(start, count, next_id)` constructs the record to add: it
97
- * populates the inline array from `items[start..start+count]`, pads
98
- * tail slots, and assembles any extra payload the table expects
99
- * (including embedding `next_id` as the node's `next` field).
100
- *
101
- * Returns the head id (the last `add()`'s slot id), or `0xFFFFFFFF` if
102
- * `items` is empty.
103
- *
104
- * @template T
105
- * @template R
106
- * @param {{ add: (record: R) => number }} table
107
- * @param {T[]} items
108
- * @param {number} items_per_chunk
109
- * @param {(start: number, count: number, next_id: number) => R} build_node
110
- * @returns {number}
111
- */
112
- function add_linked_chain(table, items, items_per_chunk, build_node) {
113
- const node_count = Math.ceil(items.length / items_per_chunk);
114
- let next_id = 0xFFFFFFFF;
115
-
116
- for (let n = node_count - 1; n >= 0; n--) {
117
- const start = n * items_per_chunk;
118
- const this_count = Math.min(items_per_chunk, items.length - start);
119
- next_id = table.add(build_node(start, this_count, next_id));
120
- }
121
-
122
- return next_id;
123
- }
124
-
125
- /**
126
- * Top-level coordinator for GPU-resident animation.
127
- *
128
- * Owns a single {@link GPUDatabase} keyed by {@link ANIMATION_DATABASE_SPEC}
129
- * with five tables: `animation_curves`, `animation_keyframe_blocks`,
130
- * `animation_tracks`, `animation_clips`, `animation_bound_track_groups`.
131
- * Every record including individual keyframes (inlined inside block
132
- * nodes) and individual bindings (inlined inside group nodes) — lives in
133
- * the database, so the dispatch shader needs only one storage binding.
134
- *
135
- * Animation *data* (curves, keyframes, tracks) is independent from
136
- * *bound clips* (each carrying a `time` and a head pointer to its
137
- * bound-track group chain). Bindings are no longer their own table:
138
- * each chain node inlines up to {@link TRACKS_PER_GROUP_NODE} bindings
139
- * and back-references its owning clip.
140
- *
141
- * Usage:
142
- * 1. (Once at setup) Either low-level (`add_curve`, `add_track`,
143
- * `add_skin`, `add_clip`) or high-level (`register_skin`,
144
- * `register_clip` against the CPU data classes {@link Skin} /
145
- * {@link ShadeAnimationClip}).
146
- * 2. (Once) {@link start} per clip to begin GPU-driven playback —
147
- * the {@link shader_animation_tick} pass advances `time` each
148
- * frame thereafter. CPU-driven prototypes can instead call
149
- * {@link set_time} per frame and skip the Playing flag.
150
- * 3. (Per frame) {@link update} flushes pending writes for the
151
- * database. The scene context already does this on its own
152
- * update path.
153
- */
154
- export class GPUAnimationManager {
155
-
156
- /** @type {GPUDevice} */
157
- #device;
158
-
159
- /** @type {string} */
160
- #label;
161
-
162
- /** @type {GPUDatabase} */
163
- #database;
164
-
165
- /**
166
- * Ping-pong pair of per-frame skinning-matrix buffers — one holds
167
- * this frame's matrices (target of the prep dispatch, source for
168
- * skinning's current-position write), the other last frame's
169
- * (source for skinning's `position_prev` write that feeds the
170
- * velocity post-pass).
171
- *
172
- * Ping-pong rather than a single buffer because the prev-frame
173
- * matrices need to survive next frame's prep dispatch — and the
174
- * cheapest way to preserve them is to write the new matrices into
175
- * the OTHER buffer instead of overwriting the only one we have.
176
- *
177
- * {@link #skin_matrices_current_index} selects which entry of this
178
- * array is "current" for the frame about to be dispatched;
179
- * {@link dispatch_skin_matrix_prep} flips it before each frame's
180
- * prep so the now-stale slot gets recycled.
181
- *
182
- * Lazily allocated on first {@link add_skin}; both grow together
183
- * by doubling. After a grow both buffers are zero-initialised and
184
- * {@link #first_frame_skin_matrices} is set so the next prep
185
- * dispatch seeds the "previous" buffer with a copy of the freshly
186
- * written "current" — without this, the first velocity readback
187
- * after a grow would interpolate against zero matrices.
188
- *
189
- * @type {Array<GPUBuffer|null>}
190
- */
191
- #skin_matrices_buffers = [null, null];
192
-
193
- /**
194
- * Which entry of {@link #skin_matrices_buffers} is "current" for
195
- * the next prep dispatch. Flipped at the start of every prep call.
196
- * @type {number}
197
- */
198
- #skin_matrices_current_index = 0;
199
-
200
- /**
201
- * When true, the next prep dispatch will copy the freshly-written
202
- * "current" buffer into the "previous" buffer at the end of the
203
- * pass — used on first allocation and after every grow so the
204
- * very first velocity computation has matching prev/current
205
- * matrices (zero velocity).
206
- * @type {boolean}
207
- */
208
- #first_frame_skin_matrices = true;
209
-
210
- /**
211
- * Capacity of each of {@link #skin_matrices_buffers} in matrices
212
- * (not bytes). Both buffers always have the same capacity.
213
- * @type {number}
214
- */
215
- #skin_matrices_capacity = 0;
216
-
217
- /**
218
- * Next free matrix index. Each {@link add_skin} reserves
219
- * `joint_count` consecutive entries starting here, then advances.
220
- * Monotonic — no freeing in the MVP.
221
- * @type {number}
222
- */
223
- #next_skin_matrix_offset = 0;
224
-
225
- /**
226
- * CPU-side `skin_id → skin_matrix_offset` map. Each registered
227
- * skin's base address in the shared `skin_matrices` scratch
228
- * buffer; the per-block absolute offset (the prep shader's hot
229
- * path) is derived as `base + chain_position *
230
- * JOINTS_PER_SKIN_BLOCK` at registration. Storing the base on
231
- * GPU would duplicate it the block records already carry the
232
- * per-block absolute address, and only the CPU (the skinning-
233
- * binding upload path) needs the skin's base value.
234
- *
235
- * @type {Map<number, number>}
236
- */
237
- #skin_matrix_offsets = new Map();
238
-
239
- /**
240
- * Per-frame pose-accumulator scratch buffer. One slot per
241
- * `transforms_table` row (slot size = `POSE_ACCUMULATOR_SLOT_BYTES`).
242
- * The apply pass atomic-adds weighted TRS contributions into each
243
- * dirty row; the resolve pass drains it into `transforms_table.local_*`
244
- * and zeros the slot for next frame.
245
- *
246
- * Lazily allocated on first {@link ensure_pose_accumulator_capacity};
247
- * grows by doubling. Grow destroys the old buffer since resolve
248
- * always leaves it zero, no copy is needed on grow.
249
- *
250
- * @type {GPUBuffer|null}
251
- */
252
- #pose_accumulator_buffer = null;
253
-
254
- /**
255
- * Capacity of {@link #pose_accumulator_buffer} in rows (slot
256
- * indices), not bytes.
257
- * @type {number}
258
- */
259
- #pose_accumulator_capacity = 0;
260
-
261
- /**
262
- * Back-reference to the owning scene context. Lets the high-level
263
- * `register_*` methods resolve `Node3D.id` → transforms-table row
264
- * via {@link GPUSceneContext#id_mapping} and bind skinning meshes
265
- * via {@link GPUSceneContext#skinning}, without callers having to
266
- * thread the context through every call.
267
- *
268
- * The manager doesn't touch this at construction (the scene
269
- * context is itself only partially constructed when it builds the
270
- * manager) only inside `register_*` calls, which happen after
271
- * scene.build has populated `id_mapping`.
272
- *
273
- * @type {GPUSceneContext}
274
- */
275
- #scene_context;
276
-
277
- /**
278
- * Identity-keyed cache of curve-id-on-GPU for every
279
- * {@link AnimationCurve} that has been registered via
280
- * {@link register_clip}. Lets multiple clips whose channels share
281
- * the same curve instance (e.g. one base clip retargeted across
282
- * many character instances) reuse the same GPU rows for the
283
- * curve header + keyframe blocks rather than uploading
284
- * duplicates. At grid-of-dancers scale duplication would balloon
285
- * the keyframe-blocks table past its page-table limits.
286
- *
287
- * Low-level `add_curve` deliberately stays uncached every call
288
- * uploads so existing callers that pre-build curves with
289
- * distinct IDs (e.g. a pre-built track pool)
290
- * keep their behavior.
291
- *
292
- * @type {WeakMap<AnimationCurve, number>}
293
- */
294
- #curve_id_cache = new WeakMap();
295
-
296
- /**
297
- * The curves uploaded so far, by what they are rather than which instance they were: the
298
- * `hash()` of a curve's keys, to every uploaded curve with that hash and its GPU id.
299
- *
300
- * An instance cache alone shares nothing between two parses of one file, and a character
301
- * instanced a few hundred times is a few hundred parses — each with its own instances of the
302
- * same keys, filling the keyframe-block table with copies of one dance until its page limit
303
- * refuses the next dancer. Curves are read-only once uploaded and nothing here releases them,
304
- * so equal content can safely mean one upload.
305
- *
306
- * A hash bucket is a list because a hash is not an identity: a curve is only shared with one
307
- * whose every key `equals` its own, tangents included.
308
- *
309
- * @type {Map<number, {curve: AnimationCurve, id: number}[]>}
310
- */
311
- #curve_id_by_content = new Map();
312
-
313
- /**
314
- * Cache of track-id-on-GPU keyed by `(curve_id_x, curve_id_y,
315
- * curve_id_z, curve_id_w, mask)`. With curve dedup already
316
- * sharing curve IDs across clips, two channels in different
317
- * clips that bind to the same (target_property, curves) end up
318
- * with identical track keys — and we can share the track row
319
- * too. Each curve_id slot is `-1` when the corresponding channel
320
- * is absent.
321
- *
322
- * Keys are joined into a string for Map lookup; the join cost is
323
- * negligible compared to the GPU upload it avoids.
324
- *
325
- * @type {Map<string, number>}
326
- */
327
- #track_id_cache = new Map();
328
-
329
- /**
330
- * Skins registered via {@link register_skin} whose joint-block
331
- * chain hasn't been built yet because one or more joint Node3Ds
332
- * lacks an `id_mapping` row. The skin's GPU table row exists
333
- * (placeholder `joint_block_head = NULL`, no joints driving the
334
- * prep pass yet), and state mutators / unregister already work
335
- * but the deformation only kicks in once {@link update}
336
- * successfully drains the entry.
337
- *
338
- * Each entry: `skin_id`, the CPU {@link Skin} (whose `joints`
339
- * array drives the chain build), the `skin_matrix_offset`
340
- * reserved at register time, and the `inverse_bind_matrices`
341
- * typed array.
342
- *
343
- * @type {Array<{
344
- * skin_id: number,
345
- * skin: Skin,
346
- * skin_matrix_offset: number,
347
- * inverse_bind_matrices: Float32Array,
348
- * }>}
349
- */
350
- #pending_skins = [];
351
-
352
- /**
353
- * Clips registered via {@link register_clip} whose bindings
354
- * chain hasn't been built yet because one or more channel
355
- * targets lacks an `id_mapping` row. The clip's GPU table row
356
- * exists (placeholder `bound_track_group_head = NULL`, no
357
- * tracks driving the apply pass yet); time/state mutators and
358
- * unregister already work.
359
- *
360
- * Each entry: `clip_id` plus the per-channel `(track_id,
361
- * target, property)` triples — the curves and tracks were
362
- * interned at register time so only the (target → instance row)
363
- * resolution is deferred.
364
- *
365
- * @type {Array<{
366
- * clip_id: number,
367
- * channels: Array<{ track: number, target: Node3D, property: number }>,
368
- * }>}
369
- */
370
- #pending_clips = [];
371
-
372
- /**
373
- * @param {GPUDevice} device
374
- * @param {string} label
375
- * @param {GPUSceneContext} scene_context
376
- */
377
- constructor(device, label, scene_context) {
378
- assert.defined(device, 'device');
379
- assert.defined(scene_context, 'scene_context');
380
-
381
- this.#device = device;
382
- this.#label = label;
383
- this.#scene_context = scene_context;
384
-
385
- this.#database = new GPUDatabase({
386
- device,
387
- definition: ANIMATION_DATABASE_SPEC,
388
- label: `${label}/Database`,
389
- });
390
- }
391
-
392
- /**
393
- * @returns {GPUDatabase}
394
- */
395
- get database() {
396
- return this.#database;
397
- }
398
-
399
- /**
400
- * Per-frame skinning-matrix scratch buffer for the *current*
401
- * frame. `null` until the first skin is registered. The
402
- * skin-matrix-prep pass writes to it; the skinning compute pass
403
- * reads from it to deform current-frame positions. Address `j`
404
- * of skin `s` is at matrix index `s.skin_matrix_offset + j`.
405
- *
406
- * The buffer reference is invalidated on grow AND flips role
407
- * every frame as part of the ping-pong with
408
- * {@link prev_skin_matrices_buffer} — re-fetch each frame.
409
- *
410
- * @returns {GPUBuffer|null}
411
- */
412
- get skin_matrices_buffer() {
413
- return this.#skin_matrices_buffers[this.#skin_matrices_current_index];
414
- }
415
-
416
- /**
417
- * Previous-frame counterpart of {@link skin_matrices_buffer}
418
- * holds the matrices the previous frame's prep dispatch wrote.
419
- * Read by the skinning compute pass to produce per-vertex
420
- * `position_prev` data for the velocity post-pass.
421
- *
422
- * `null` until the first skin is registered. Flips role with
423
- * {@link skin_matrices_buffer} every frame.
424
- *
425
- * @returns {GPUBuffer|null}
426
- */
427
- get prev_skin_matrices_buffer() {
428
- return this.#skin_matrices_buffers[1 - this.#skin_matrices_current_index];
429
- }
430
-
431
- /**
432
- * Number of matrices currently in use (= sum of `joint_count` over
433
- * registered skins). Independent of buffer capacity, which is at
434
- * least this big.
435
- *
436
- * @returns {number}
437
- */
438
- get skin_matrix_count() {
439
- return this.#next_skin_matrix_offset;
440
- }
441
-
442
- /**
443
- * Pose-accumulator scratch buffer (per-Node3D-row weighted
444
- * contribution slots). `null` until {@link ensure_pose_accumulator_capacity}
445
- * is called for the first time.
446
- *
447
- * The buffer reference is invalidated on grow — re-fetch each
448
- * frame after {@link ensure_pose_accumulator_capacity} rather
449
- * than caching across frames.
450
- *
451
- * @returns {GPUBuffer|null}
452
- */
453
- get pose_accumulator_buffer() {
454
- return this.#pose_accumulator_buffer;
455
- }
456
-
457
- /**
458
- * Ensure the pose-accumulator scratch buffer can address at least
459
- * `row_count` `transforms_table` rows. Grows by doubling from the
460
- * initial capacity; idempotent when already big enough. Called
461
- * by the per-frame dispatch driver after `scene.build` has
462
- * stabilised the transforms-table size for the frame.
463
- *
464
- * Grow destroys the previous GPU buffer; resolve always leaves the
465
- * accumulator zeroed at the end of each frame, so there's nothing
466
- * to copy across the realloc.
467
- *
468
- * @param {number} row_count
469
- */
470
- ensure_pose_accumulator_capacity(row_count) {
471
- assert.isNonNegativeInteger(row_count, 'row_count');
472
-
473
- // Existing buffer fits the request → nothing to do. The null
474
- // check guarantees the very first call always allocates,
475
- // even when `row_count == 0` (e.g. before any scene content
476
- // has been built): downstream binding paths assume the
477
- // buffer reference is non-null whenever the apply pass runs.
478
- if (this.#pose_accumulator_buffer !== null && row_count <= this.#pose_accumulator_capacity) {
479
- return;
480
- }
481
-
482
- let new_capacity = Math.max(this.#pose_accumulator_capacity, POSE_ACCUMULATOR_INITIAL_CAPACITY);
483
- while (new_capacity < row_count) {
484
- new_capacity *= 2;
485
- }
486
-
487
- if (this.#pose_accumulator_buffer !== null) {
488
- this.#pose_accumulator_buffer.destroy();
489
- }
490
-
491
- this.#pose_accumulator_buffer = this.#device.createBuffer({
492
- label: `${this.#label}/PoseAccumulator`,
493
- size: new_capacity * POSE_ACCUMULATOR_SLOT_BYTES,
494
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
495
- });
496
- this.#pose_accumulator_capacity = new_capacity;
497
- }
498
-
499
- /**
500
- * Read back a skin's record from the database's CPU shadow.
501
- *
502
- * @param {number} skin_id
503
- * @returns {{joint_block_head: number, joint_count: number}}
504
- */
505
- get_skin(skin_id) {
506
- assert.isNonNegativeInteger(skin_id, 'skin_id');
507
- return this.#database.get('animation_skins').get(skin_id);
508
- }
509
-
510
- /**
511
- * Base address of the skin's matrix range in the shared
512
- * `skin_matrices` scratch buffer — matrix `j` of the skin lives
513
- * at `skin_matrices[get_skin_matrix_offset(skin_id) + j]`.
514
- *
515
- * Tracked CPU-side rather than on the skin record because the
516
- * value is only ever consumed by the skinning-binding upload
517
- * path; the prep shader uses the per-block absolute offset
518
- * (cheaper than a per-thread skin lookup for that hot path).
519
- *
520
- * @param {number} skin_id
521
- * @returns {number}
522
- */
523
- get_skin_matrix_offset(skin_id) {
524
- assert.isNonNegativeInteger(skin_id, 'skin_id');
525
- const offset = this.#skin_matrix_offsets.get(skin_id);
526
- assert.defined(offset, `unknown skin_id ${skin_id}`);
527
- return offset;
528
- }
529
-
530
- /**
531
- * Add an animation curve. Keyframes are split into linked-list blocks
532
- * of up to {@link KEYFRAMES_PER_BLOCK} entries each; the curve header
533
- * points at the head block, and each block's `next` field chains
534
- * forward.
535
- *
536
- * The curve is a cubic Hermite spline step, linear, and arbitrary
537
- * cubic shapes are all expressible by choosing tangents on each keyframe.
538
- *
539
- * @param {AnimationCurve} curve
540
- * @returns {number} curve id (index into the `animation_curves` table)
541
- */
542
- add_curve(curve) {
543
- assert.defined(curve, 'curve');
544
- assert.equal(curve.isAnimationCurve, true, 'curve.isAnimationCurve !== true');
545
- assert.greaterThanOrEqual(curve.length, 1, 'curve must have at least one keyframe');
546
-
547
- const keys = curve.keys;
548
- const blocks_table = this.#database.get('animation_keyframe_blocks');
549
-
550
- const head_block_id = add_linked_chain(
551
- blocks_table,
552
- keys,
553
- KEYFRAMES_PER_BLOCK,
554
- (start, count, next) => {
555
- const keyframes = new Array(KEYFRAMES_PER_BLOCK);
556
- for (let j = 0; j < KEYFRAMES_PER_BLOCK; j++) {
557
- keyframes[j] = j < count ? keys[start + j] : ZERO_KEYFRAME;
558
- }
559
- return { keyframes, count, next };
560
- }
561
- );
562
-
563
- const curves_table = this.#database.get('animation_curves');
564
- return curves_table.add({
565
- kind: 0,
566
- keyframe_block_head: head_block_id,
567
- keyframe_count: keys.length,
568
- time_min: curve.start_time,
569
- time_max: curve.end_time,
570
- });
571
- }
572
-
573
- /**
574
- * Add a track. Channels are addressed by name (`x`, `y`, `z`, `w`) — the
575
- * provided curves go into the matching slots of the track's `vec4f`
576
- * output. Omitted channels are masked off and read as 0.
577
- *
578
- * The slot meaning is property-specific: for `MeshTranslation`, x/y/z
579
- * are the translation components; for `MeshRotation` they're the
580
- * quaternion's x/y/z/w. The track's mask travels with the evaluated
581
- * value to the apply step, which uses it to decide which destinations
582
- * to overwrite.
583
- *
584
- * @param {{ x?: number, y?: number, z?: number, w?: number }} channels
585
- * curve ids per channel; pass at least one
586
- * @returns {number} track id (index into the `animation_tracks` table)
587
- */
588
- add_track(channels) {
589
- assert.defined(channels, 'channels');
590
-
591
- const cx = channels.x;
592
- const cy = channels.y;
593
- const cz = channels.z;
594
- const cw = channels.w;
595
-
596
- let mask = 0;
597
- if (cx !== undefined) mask |= 0x1;
598
- if (cy !== undefined) mask |= 0x2;
599
- if (cz !== undefined) mask |= 0x4;
600
- if (cw !== undefined) mask |= 0x8;
601
-
602
- assert.notEqual(mask, 0, 'track must have at least one channel');
603
-
604
- const curves = [
605
- cx ?? 0,
606
- cy ?? 0,
607
- cz ?? 0,
608
- cw ?? 0,
609
- ];
610
-
611
- const table = this.#database.get('animation_tracks');
612
- return table.add({ mask, curves });
613
- }
614
-
615
- /**
616
- * Create a bound clip — a group of bound tracks that share a single
617
- * `time`. Bindings are split into linked-list group nodes of up to
618
- * {@link TRACKS_PER_GROUP_NODE} each; the clip points at the head
619
- * node and each node back-references the clip so the apply shader
620
- * (one thread per group node) can fetch the clip without walking the
621
- * chain from the clip end.
622
- *
623
- * Tracks may be authored on a clock that doesn't align with
624
- * `[time_start, time_end]` their curves are clamped to their own
625
- * `[time_min, time_max]` during evaluation.
626
- *
627
- * Defaults: `time = 0`, `time_start = 0`, `time_end = 0`,
628
- * `playback_rate = 1`, `playback_weight = 1`, `flags = 0` (not
629
- * playing, not looping). The clip is inert at creation — call
630
- * {@link start} to begin GPU-side time advancement.
631
- *
632
- * @param {{
633
- * time?: number,
634
- * time_start?: number,
635
- * time_end?: number,
636
- * bindings: Array<{ track: number, instance: number, property: number }>,
637
- * }} clip
638
- * @returns {number} clip id (index into the `animation_clips` table)
639
- */
640
- add_clip(clip) {
641
- assert.defined(clip, 'clip');
642
- assert.isArray(clip.bindings, 'clip.bindings');
643
- assert.greaterThanOrEqual(clip.bindings.length, 1, 'clip must have at least one binding');
644
-
645
- const time = clip.time ?? 0;
646
- const time_start = clip.time_start ?? 0;
647
- const time_end = clip.time_end ?? 0;
648
- const bindings = clip.bindings;
649
-
650
- const clips_table = this.#database.get('animation_clips');
651
- const groups_table = this.#database.get('animation_bound_track_groups');
652
-
653
- // Reserve the clip slot up front so group nodes can back-reference
654
- // it via `clip`. The head pointer is patched in once the chain is
655
- // built (we don't know its first id until we've allocated every
656
- // group node).
657
- const clip_id = clips_table.add({
658
- time,
659
- time_start,
660
- time_end,
661
- bound_track_group_head: ANIMATION_BOUND_TRACK_GROUP_NULL,
662
- playback_rate: 1,
663
- flags: 0,
664
- playback_weight: 1,
665
- });
666
-
667
- const head_group_id = add_linked_chain(
668
- groups_table,
669
- bindings,
670
- TRACKS_PER_GROUP_NODE,
671
- (start, count, next) => {
672
- const inline_bindings = new Array(TRACKS_PER_GROUP_NODE);
673
- for (let j = 0; j < TRACKS_PER_GROUP_NODE; j++) {
674
- if (j < count) {
675
- const b = bindings[start + j];
676
-
677
- assert.isNonNegativeInteger(b.track, `clip.bindings[${start + j}].track`);
678
- assert.isNonNegativeInteger(b.instance, `clip.bindings[${start + j}].instance`);
679
- assert.isNonNegativeInteger(b.property, `clip.bindings[${start + j}].property`);
680
-
681
- inline_bindings[j] = b;
682
- } else {
683
- inline_bindings[j] = ZERO_BINDING;
684
- }
685
- }
686
- return { bindings: inline_bindings, count, next, clip: clip_id };
687
- }
688
- );
689
-
690
- // Patch the head pointer now that the chain exists. Re-`set` the
691
- // whole record because the table API doesn't expose per-field
692
- // updates.
693
- clips_table.set(clip_id, {
694
- time,
695
- time_start,
696
- time_end,
697
- bound_track_group_head: head_group_id,
698
- playback_rate: 1,
699
- flags: 0,
700
- playback_weight: 1,
701
- });
702
-
703
- return clip_id;
704
- }
705
-
706
- /**
707
- * Overwrite a single field of a clip's record by reading the current
708
- * record back from the CPU shadow and re-`set`ting with one field
709
- * changed. The table API doesn't expose per-field updates, so we
710
- * round-trip through a full record; the upload-buffer dedupe
711
- * collapses the resulting per-frame write into a single GPU touch
712
- * when nothing else changed.
713
- *
714
- * @param {number} clip_id
715
- * @param {string} field
716
- * @param {number} value
717
- */
718
- #patch_clip_field(clip_id, field, value) {
719
- assert.isNonNegativeInteger(clip_id, 'clip_id');
720
-
721
- const clips_table = this.#database.get('animation_clips');
722
- const current = clips_table.get(clip_id);
723
-
724
- clips_table.set(clip_id, {
725
- time: current.time,
726
- time_start: current.time_start,
727
- time_end: current.time_end,
728
- bound_track_group_head: current.bound_track_group_head,
729
- playback_rate: current.playback_rate,
730
- flags: current.flags,
731
- playback_weight: current.playback_weight,
732
- [field]: value,
733
- });
734
- }
735
-
736
- /**
737
- * Direct override of a clip's `time`. Bypasses {@link start} /
738
- * {@link stop} — `time` is written even if the clip is paused, and
739
- * the {@link AnimationClipFlags.Playing} bit is left untouched.
740
- * The host typically uses this to scrub or rewind; GPU-driven
741
- * playback advances time on its own via the tick shader.
742
- *
743
- * TODO: race between `start()` and `set_time()` is non-atomic — if
744
- * a caller does `start(id); set_time(id, phase)` and the next GPU
745
- * tick fires before the page upload from `set_time` lands, the
746
- * tick advances from the old `time` for one frame. RIS would be
747
- * an atomic `start_at(id, time, ...)` that fuses both field
748
- * writes into one record set. For phase offsets the one-frame
749
- * skew is invisible; for scrub-and-resume UIs it can show.
750
- *
751
- * @param {number} clip_id
752
- * @param {number} time
753
- */
754
- set_time(clip_id, time) {
755
- assert.isNumber(time, 'time');
756
- this.#patch_clip_field(clip_id, 'time', time);
757
- }
758
-
759
- /**
760
- * Direct override of a clip's per-frame advance scalar. `1.0` =
761
- * realtime; `0.0` effectively pauses (independent of the
762
- * {@link AnimationClipFlags.Playing} bit); negative values play
763
- * in reverse.
764
- *
765
- * @param {number} clip_id
766
- * @param {number} rate
767
- */
768
- set_playback_rate(clip_id, rate) {
769
- assert.isNumber(rate, 'rate');
770
- this.#patch_clip_field(clip_id, 'playback_rate', rate);
771
- }
772
-
773
- /**
774
- * Override a clip's contribution weight into the pose accumulator.
775
- * `1.0` is full strength; `0.0` silences the clip without removing
776
- * its bindings; values in between blend it against other clips
777
- * targeting the same nodes. The apply shader pre-multiplies every
778
- * curve-evaluated value by this scalar before atomic-adding into
779
- * the accumulator.
780
- *
781
- * Normalized blends expect weights across all clips driving a
782
- * node to sum to `1`; additive layers use weights that don't sum
783
- * to `1`. The system doesn't enforce either — host's call.
784
- *
785
- * @param {number} clip_id
786
- * @param {number} weight
787
- */
788
- set_playback_weight(clip_id, weight) {
789
- assert.isNumber(weight, 'weight');
790
- this.#patch_clip_field(clip_id, 'playback_weight', weight);
791
- }
792
-
793
- /**
794
- * Add (OR) one or more flag bits onto a clip's bitfield. Bits
795
- * already set stay set; other bits are untouched. Values come
796
- * from {@link AnimationClipFlags} and can be combined with `|`:
797
- * `set_flags(id, AnimationClipFlags.Loop | AnimationClipFlags.Playing)`.
798
- *
799
- * Counterpart {@link clear_flags} ANDs the bits out. Together
800
- * they cover the common cases without the overwrite footgun
801
- * the original version had (where setting Loop would silently
802
- * clear Playing).
803
- *
804
- * For toggling just the playing bit, {@link start} / {@link stop}
805
- * are equivalent shortcuts.
806
- *
807
- * @param {number} clip_id
808
- * @param {number} flags bits to set
809
- */
810
- set_flags(clip_id, flags) {
811
- assert.isNonNegativeInteger(flags, 'flags');
812
-
813
- const clips_table = this.#database.get('animation_clips');
814
- const current = clips_table.get(clip_id);
815
-
816
- this.#patch_clip_field(clip_id, 'flags', current.flags | flags);
817
- }
818
-
819
- /**
820
- * Clear (AND-NOT) one or more flag bits from a clip's bitfield.
821
- * Bits not present in `flags` are left untouched. Symmetric
822
- * with {@link set_flags}.
823
- *
824
- * @param {number} clip_id
825
- * @param {number} flags bits to clear
826
- */
827
- clear_flags(clip_id, flags) {
828
- assert.isNonNegativeInteger(flags, 'flags');
829
-
830
- const clips_table = this.#database.get('animation_clips');
831
- const current = clips_table.get(clip_id);
832
-
833
- this.#patch_clip_field(clip_id, 'flags', current.flags & ~flags);
834
- }
835
-
836
- /**
837
- * Begin GPU-side playback. Sets the {@link AnimationClipFlags.Playing}
838
- * bit; preserves every other flag bit, the current `time`, the
839
- * `playback_rate`, and so on. Calling {@link stop} and then `start`
840
- * back-to-back should feel like a no-op modulo at most one tick's
841
- * worth of advance.
842
- *
843
- * Idempotent: starting an already-playing clip is a no-op (the
844
- * upload-buffer dedupe collapses the write).
845
- *
846
- * @param {number} clip_id
847
- */
848
- start(clip_id) {
849
- assert.isNonNegativeInteger(clip_id, 'clip_id');
850
-
851
- const clips_table = this.#database.get('animation_clips');
852
- const current = clips_table.get(clip_id);
853
-
854
- this.#patch_clip_field(clip_id, 'flags', current.flags | AnimationClipFlags.Playing);
855
- }
856
-
857
- /**
858
- * Pause GPU-side playback. Clears the {@link AnimationClipFlags.Playing}
859
- * bit; preserves every other flag bit and the current `time`.
860
- * Symmetric with {@link start} — a stop/start pair leaves the clip
861
- * where it was (give or take one tick of GPU time loss).
862
- *
863
- * Idempotent: stopping an already-stopped clip is a no-op.
864
- *
865
- * @param {number} clip_id
866
- */
867
- stop(clip_id) {
868
- assert.isNonNegativeInteger(clip_id, 'clip_id');
869
-
870
- const clips_table = this.#database.get('animation_clips');
871
- const current = clips_table.get(clip_id);
872
-
873
- this.#patch_clip_field(clip_id, 'flags', current.flags & ~AnimationClipFlags.Playing);
874
- }
875
-
876
- /**
877
- * Register a skin: an ordered list of joints, each pinning a
878
- * Node3D row that drives the joint plus the inverse-bind matrix
879
- * that takes vertex positions from mesh space into the joint's
880
- * bind-pose local space. The skin reserves a contiguous range of
881
- * `joints.length` matrices in the {@link #skin_matrices_buffer}
882
- * scratch buffer; the prep pass populates that range each frame
883
- * with `node.global × inverse_bind` for every joint.
884
- *
885
- * The skin record carries no mesh-world factor. The rasterizer
886
- * applies `mesh.transform_global` to the skinned clone vertex
887
- * via the standard mesh transform path
888
- * ({@link chunk_mesh_resolve_vertex_position_world}), so adding
889
- * an `inverse(mesh.global)` factor in the skin matrix would
890
- * double-cancel against that re-application and shrink the rig
891
- * back to mesh-local coordinates rendered as world.
892
- *
893
- * Joints are split into linked-list blocks of up to
894
- * {@link JOINTS_PER_SKIN_BLOCK} each — same chain pattern used for
895
- * bound-track groups and keyframe blocks. Each block records the
896
- * absolute write offset for its slots, so the prep shader's
897
- * per-thread work is one block load + a bounded inner loop with no
898
- * indirection back through the skin record.
899
- *
900
- * @param {{
901
- * joints: Array<{ node: number, inverse_bind: Float32Array | number[] }>,
902
- * }} skin
903
- * @returns {number} skin id (index into the `animation_skins` table)
904
- */
905
- add_skin(skin) {
906
- assert.defined(skin, 'skin');
907
- assert.isArray(skin.joints, 'skin.joints');
908
- assert.greaterThanOrEqual(skin.joints.length, 1, 'skin must have at least one joint');
909
-
910
- const joints = skin.joints;
911
- const joint_count = joints.length;
912
-
913
- // Reserve the matrix range. Each block bakes in its absolute
914
- // slot offset (skin_base + chain_position * JOINTS_PER_SKIN_BLOCK)
915
- // so the prep shader writes directly without dereferencing
916
- // the skin row for the offset; the base is also stashed on
917
- // the CPU map so the skinning-binding upload can look it up
918
- // without reading the GPU back.
919
- const skin_matrix_offset = this.#next_skin_matrix_offset;
920
- this.#next_skin_matrix_offset += joint_count;
921
- this.#ensure_skin_matrices_capacity(this.#next_skin_matrix_offset);
922
-
923
- const skins_table = this.#database.get('animation_skins');
924
- const blocks_table = this.#database.get('animation_skin_joint_blocks');
925
-
926
- // Reserve the skin row up front. The head pointer is patched
927
- // in once the chain exists. NULL placeholder 0 is a valid
928
- // block row id (the first allocation in an empty table).
929
- const skin_id = skins_table.add({
930
- joint_block_head: ANIMATION_SKIN_JOINT_BLOCK_NULL,
931
- joint_count,
932
- });
933
- this.#skin_matrix_offsets.set(skin_id, skin_matrix_offset);
934
-
935
- const head_block_id = add_linked_chain(
936
- blocks_table,
937
- joints,
938
- JOINTS_PER_SKIN_BLOCK,
939
- (start, count, next) => {
940
- const inline_joints = new Array(JOINTS_PER_SKIN_BLOCK);
941
- for (let j = 0; j < JOINTS_PER_SKIN_BLOCK; j++) {
942
- if (j < count) {
943
- const joint = joints[start + j];
944
-
945
- assert.isNonNegativeInteger(joint.node, `skin.joints[${start + j}].node`);
946
- assert.defined(joint.inverse_bind, `skin.joints[${start + j}].inverse_bind`);
947
-
948
- inline_joints[j] = joint;
949
- } else {
950
- inline_joints[j] = ZERO_SKIN_JOINT;
951
- }
952
- }
953
- return {
954
- joints: inline_joints,
955
- count,
956
- next,
957
- skin_matrix_offset: skin_matrix_offset + start,
958
- };
959
- }
960
- );
961
-
962
- // Patch the head pointer now that the chain exists. Re-`set`
963
- // the whole record because the table API doesn't expose
964
- // per-field updates.
965
- skins_table.set(skin_id, {
966
- joint_block_head: head_block_id,
967
- joint_count,
968
- });
969
-
970
- return skin_id;
971
- }
972
-
973
- /**
974
- * High-level skin registration: consumes a {@link Skin} (CPU data
975
- * with Node3D joint references and a flat inverse-bind matrix
976
- * array), allocates a GPU skin record, and binds every entry in
977
- * `skin.meshes` through the skinning context.
978
- *
979
- * **Eventual consistency.** The `skin_id` is allocated and
980
- * returned synchronously — callers can immediately pass it to
981
- * state mutators or {@link unregister_skin}. If any joint's
982
- * Node3D hasn't been added to a scene yet (no `id_mapping` row),
983
- * the joint-block chain that drives the deformation is queued
984
- * and built on the next {@link update} that sees all joints
985
- * resolved. The skin's table row uses a placeholder
986
- * `joint_block_head` until then observationally indistinguishable
987
- * from "skin with no joints driving it" (no deformation runs).
988
- *
989
- * Mesh→skin bindings register immediately via
990
- * {@link GPUMeshSkinningContext#bind}; the skinning context
991
- * already tolerates pre-scene-build registration. Each mesh also
992
- * gets its {@link SkinnedMesh#skin} reference and a fresh set of
993
- * bounds off it, synchronously — that part does not wait for
994
- * `id_mapping`, because it reads the joints' `transform_global`
995
- * rather than any GPU row.
996
- *
997
- * @param {Skin} skin
998
- * @returns {number} skin id (index into the `animation_skins` table)
999
- */
1000
- register_skin(skin) {
1001
- assert.defined(skin, 'skin');
1002
- assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
1003
- assert.greaterThanOrEqual(skin.meshes.length, 1, 'skin.meshes must contain at least one mesh');
1004
-
1005
- const joint_count = skin.joints.length;
1006
- const inverse_bind_matrices = skin.inverse_bind_matrices;
1007
-
1008
- // Reserve the matrix range up front block records carry
1009
- // their absolute slot offset, so the prep shader writes
1010
- // directly. The base also lands on #skin_matrix_offsets so
1011
- // skinning-binding upload can look it up.
1012
- const skin_matrix_offset = this.#next_skin_matrix_offset;
1013
- this.#next_skin_matrix_offset += joint_count;
1014
- this.#ensure_skin_matrices_capacity(this.#next_skin_matrix_offset);
1015
-
1016
- // Reserve the skin row immediately with a placeholder head
1017
- // pointer. State mutators and the public id are usable from
1018
- // this point on; the joint chain gets patched in once
1019
- // id_mapping has every joint (possibly synchronously below,
1020
- // possibly deferred via the pending queue).
1021
- const skins_table = this.#database.get('animation_skins');
1022
- const skin_id = skins_table.add({
1023
- // NULL placeholder — 0 is a valid block row id
1024
- joint_block_head: ANIMATION_SKIN_JOINT_BLOCK_NULL,
1025
- joint_count,
1026
- });
1027
- this.#skin_matrix_offsets.set(skin_id, skin_matrix_offset);
1028
-
1029
- // Bind every mesh into the skinning context. Idempotent and
1030
- // pre-build tolerant — the binding upload defers until
1031
- // id_mapping is ready.
1032
- //
1033
- // Each mesh also takes a reference to the CPU-side skin and
1034
- // re-derives its bounds off it. From here on the skin's
1035
- // formula is what places the mesh's vertices, and `Mesh`'s
1036
- // composition rest-pose geometry under `transform_global` —
1037
- // is the wrong one, because the skinning pass cancels that
1038
- // transform back out (see {@link SkinnedMesh#updateBoundsBasic}).
1039
- // Nothing else would notice: bounds are refreshed by
1040
- // `updateMatrices`, and a character does not move again just
1041
- // because it finished loading.
1042
- const skinning = this.#scene_context.skinning;
1043
- const meshes = skin.meshes;
1044
- for (let i = 0; i < meshes.length; i++) {
1045
- const mesh = meshes[i];
1046
-
1047
- skinning.bind(mesh, skin_id);
1048
-
1049
- mesh.skin = skin;
1050
- mesh.updateBoundsBasic();
1051
-
1052
- // The row the scene context published for this mesh holds the pre-skin bounds, and
1053
- // nothing about binding a skin looks like movement — `updateBoundsBasic` is not
1054
- // `updateMatrices` and does not touch the version the sync compares against. Without
1055
- // this the corrected box sits on the CPU object and never reaches the table.
1056
- mesh.needsUpdate = true;
1057
- }
1058
-
1059
- // Try to complete now. If id_mapping is ready for every
1060
- // joint, the chain gets built and patched in. Otherwise the
1061
- // skin lands on #pending_skins and the next update() drain
1062
- // retries.
1063
- const pending = {
1064
- skin_id,
1065
- skin,
1066
- skin_matrix_offset,
1067
- inverse_bind_matrices,
1068
- };
1069
- if (!this.#try_complete_skin(pending)) {
1070
- this.#pending_skins.push(pending);
1071
- }
1072
-
1073
- return skin_id;
1074
- }
1075
-
1076
- /**
1077
- * Attempt to build the joint-block chain for a pending skin and
1078
- * patch its head pointer onto the skin row. Returns `true` when
1079
- * every joint Node3D has resolved to a transforms-table row
1080
- * (and the chain was built); `false` when at least one joint is
1081
- * still missing.
1082
- *
1083
- * @param {{
1084
- * skin_id: number,
1085
- * skin: Skin,
1086
- * skin_matrix_offset: number,
1087
- * inverse_bind_matrices: Float32Array,
1088
- * }} pending
1089
- * @returns {boolean}
1090
- */
1091
- #try_complete_skin(pending) {
1092
- const id_mapping = this.#scene_context.id_mapping;
1093
- const joint_nodes = pending.skin.joints;
1094
- const joint_count = joint_nodes.length;
1095
- const inverse_bind_matrices = pending.inverse_bind_matrices;
1096
- const skin_matrix_offset = pending.skin_matrix_offset;
1097
- const skin_id = pending.skin_id;
1098
-
1099
- // All-or-nothing: a single missing row leaves the chain
1100
- // unbuilt — partial chains aren't valid (the prep shader
1101
- // walks every block).
1102
- const joints = new Array(joint_count);
1103
- for (let i = 0; i < joint_count; i++) {
1104
- const node = joint_nodes[i];
1105
- const row = id_mapping.get(node.id);
1106
- if (row === undefined) return false;
1107
-
1108
- joints[i] = {
1109
- node: row,
1110
- // Per-joint slice of the flat matrix array. The
1111
- // marshaller copies these bytes into the skin-joint
1112
- // block record; sub-array views are fine — they
1113
- // share the underlying buffer but write_struct_value
1114
- // reads by length, not by offset.
1115
- inverse_bind: inverse_bind_matrices.subarray(i * 16, (i + 1) * 16),
1116
- };
1117
- }
1118
-
1119
- const blocks_table = this.#database.get('animation_skin_joint_blocks');
1120
- const head_block_id = add_linked_chain(
1121
- blocks_table,
1122
- joints,
1123
- JOINTS_PER_SKIN_BLOCK,
1124
- (start, count, next) => {
1125
- const inline_joints = new Array(JOINTS_PER_SKIN_BLOCK);
1126
- for (let j = 0; j < JOINTS_PER_SKIN_BLOCK; j++) {
1127
- inline_joints[j] = j < count ? joints[start + j] : ZERO_SKIN_JOINT;
1128
- }
1129
- return {
1130
- joints: inline_joints,
1131
- count,
1132
- next,
1133
- skin_matrix_offset: skin_matrix_offset + start,
1134
- };
1135
- }
1136
- );
1137
-
1138
- // Patch the head pointer onto the already-allocated skin
1139
- // row. Read-modify-write so any other fields the user might
1140
- // have changed via mutators (none today, but future-proof)
1141
- // are preserved.
1142
- const skins_table = this.#database.get('animation_skins');
1143
- const current = skins_table.get(skin_id);
1144
- skins_table.set(skin_id, {
1145
- joint_block_head: head_block_id,
1146
- joint_count: current.joint_count,
1147
- });
1148
-
1149
- return true;
1150
- }
1151
-
1152
- /**
1153
- * High-level clip registration: consumes a {@link ShadeAnimationClip}
1154
- * (CPU data with Node3D targets and per-component curves), uploads
1155
- * curves and tracks, and creates the GPU clip record.
1156
- *
1157
- * **Eventual consistency.** The `clip_id` is allocated and
1158
- * returned synchronously — callers can immediately call
1159
- * {@link start} / {@link stop} / {@link set_time} /
1160
- * {@link set_playback_rate} / {@link set_flags} / {@link clear_flags}
1161
- * / {@link set_playback_weight} / {@link unregister_clip}. If
1162
- * any channel target's Node3D hasn't been added to a scene yet
1163
- * (no `id_mapping` row), the bindings chain that drives the
1164
- * apply pass is queued and built on the next {@link update}
1165
- * that sees all targets resolved. The clip's table row uses a
1166
- * placeholder `bound_track_group_head` until then apply
1167
- * produces no contributions, so the clip is observationally
1168
- * inert (still subject to time advancement via the tick shader
1169
- * if `Playing` is set).
1170
- *
1171
- * Curves and tracks are interned synchronously since they're
1172
- * pure data no `id_mapping` dependency.
1173
- *
1174
- * @param {ShadeAnimationClip} clip
1175
- * @returns {number} clip id
1176
- */
1177
- register_clip(clip) {
1178
- assert.defined(clip, 'clip');
1179
- assert.equal(clip.isShadeAnimationClip, true, 'clip.isShadeAnimationClip !== true');
1180
-
1181
- const channels = clip.channels;
1182
-
1183
- // Intern every channel's curves + track now. None of this
1184
- // depends on id_mapping pure data uploads.
1185
- //
1186
- // Each curve goes through #intern_curve so two channels
1187
- // referencing the same AnimationCurve instance share one
1188
- // GPU upload. With 324 dancers retargeted at the same base
1189
- // clip this turns 324 × 195 channel uploads into 1 (the
1190
- // curves are read-only animation data identical bytes
1191
- // wherever they're referenced).
1192
- const pending_channels = new Array(channels.length);
1193
- for (let i = 0; i < channels.length; i++) {
1194
- const channel = channels[i];
1195
- const curves = channel.curves;
1196
- const track_channels = {};
1197
- if (curves.x !== undefined) track_channels.x = this.#intern_curve(curves.x);
1198
- if (curves.y !== undefined) track_channels.y = this.#intern_curve(curves.y);
1199
- if (curves.z !== undefined) track_channels.z = this.#intern_curve(curves.z);
1200
- if (curves.w !== undefined) track_channels.w = this.#intern_curve(curves.w);
1201
-
1202
- pending_channels[i] = {
1203
- track: this.#intern_track(track_channels),
1204
- target: channel.target,
1205
- property: channel.property,
1206
- };
1207
- }
1208
-
1209
- // Reserve the clip row immediately with a placeholder head
1210
- // pointer. State mutators and the public id are usable from
1211
- // this point on; the bindings chain gets patched in once
1212
- // id_mapping has every target. `time` defaults to 0 (the
1213
- // playback offset relative to `time_start`; the apply
1214
- // shader does `effective_time = clip.time + clip.time_start`).
1215
- const clips_table = this.#database.get('animation_clips');
1216
- const clip_id = clips_table.add({
1217
- time: 0,
1218
- time_start: clip.start_time,
1219
- time_end: clip.end_time,
1220
- bound_track_group_head: ANIMATION_BOUND_TRACK_GROUP_NULL,
1221
- playback_rate: 1,
1222
- flags: 0,
1223
- playback_weight: 1,
1224
- });
1225
-
1226
- const pending = { clip_id, channels: pending_channels };
1227
- if (!this.#try_complete_clip(pending)) {
1228
- this.#pending_clips.push(pending);
1229
- }
1230
-
1231
- return clip_id;
1232
- }
1233
-
1234
- /**
1235
- * Attempt to build the bindings chain for a pending clip and
1236
- * patch its head pointer onto the clip row. Returns `true` when
1237
- * every channel target has resolved to a transforms-table row
1238
- * (and the chain was built); `false` when at least one is still
1239
- * missing.
1240
- *
1241
- * @param {{
1242
- * clip_id: number,
1243
- * channels: Array<{ track: number, target: Node3D, property: number }>,
1244
- * }} pending
1245
- * @returns {boolean}
1246
- */
1247
- #try_complete_clip(pending) {
1248
- const id_mapping = this.#scene_context.id_mapping;
1249
- const channels = pending.channels;
1250
- const clip_id = pending.clip_id;
1251
-
1252
- const bindings = new Array(channels.length);
1253
- for (let i = 0; i < channels.length; i++) {
1254
- const ch = channels[i];
1255
- const instance = id_mapping.get(ch.target.id);
1256
- if (instance === undefined) return false;
1257
-
1258
- bindings[i] = {
1259
- track: ch.track,
1260
- instance,
1261
- property: ch.property,
1262
- };
1263
- }
1264
-
1265
- const groups_table = this.#database.get('animation_bound_track_groups');
1266
- const head_group_id = add_linked_chain(
1267
- groups_table,
1268
- bindings,
1269
- TRACKS_PER_GROUP_NODE,
1270
- (start, count, next) => {
1271
- const inline_bindings = new Array(TRACKS_PER_GROUP_NODE);
1272
- for (let j = 0; j < TRACKS_PER_GROUP_NODE; j++) {
1273
- inline_bindings[j] = j < count ? bindings[start + j] : ZERO_BINDING;
1274
- }
1275
- return { bindings: inline_bindings, count, next, clip: clip_id };
1276
- }
1277
- );
1278
-
1279
- // Patch the head pointer onto the already-allocated clip
1280
- // row. Read-modify-write preserves any user-driven state
1281
- // changes (set_time, set_flags, etc.) made between
1282
- // register_clip and the drain.
1283
- this.#patch_clip_field(clip_id, 'bound_track_group_head', head_group_id);
1284
-
1285
- return true;
1286
- }
1287
-
1288
- /**
1289
- * Drop a clip from the manager. Walks the bindings chain and
1290
- * removes every group-node + the clip row. Tolerates the clip
1291
- * still being pending (drains it from the queue without GPU
1292
- * touches besides the original row removal).
1293
- *
1294
- * Curves and tracks the clip referenced stay alive — they're
1295
- * shared via {@link #intern_curve} / {@link #intern_track} and
1296
- * may be in use by other clips. Removing them would need
1297
- * reference-counting the manager doesn't carry.
1298
- *
1299
- * Safe to call repeatedly on the same id (subsequent calls are
1300
- * no-ops once the row is gone).
1301
- *
1302
- * @param {number} clip_id
1303
- */
1304
- unregister_clip(clip_id) {
1305
- assert.isNonNegativeInteger(clip_id, 'clip_id');
1306
-
1307
- // Drop from pending queue if present (skip-and-keep-others
1308
- // pattern is cheaper than splice for the rare case of
1309
- // dozens-of-pending; if pending depth grows this can be
1310
- // revisited).
1311
- for (let i = this.#pending_clips.length - 1; i >= 0; i--) {
1312
- if (this.#pending_clips[i].clip_id === clip_id) {
1313
- this.#pending_clips.splice(i, 1);
1314
- }
1315
- }
1316
-
1317
- const clips_table = this.#database.get('animation_clips');
1318
- const groups_table = this.#database.get('animation_bound_track_groups');
1319
-
1320
- const clip_record = clips_table.get(clip_id);
1321
- if (clip_record === undefined) return;
1322
-
1323
- // Walk the bindings chain and remove every node. The chain
1324
- // may be empty (placeholder head NULL) if the clip is still
1325
- // pending or has been registered then drained but never
1326
- // completed; both cases skip the loop.
1327
- let block_id = clip_record.bound_track_group_head;
1328
- while (block_id !== ANIMATION_BOUND_TRACK_GROUP_NULL) {
1329
- const block = groups_table.get(block_id);
1330
- const next_id = block !== undefined ? block.next : ANIMATION_BOUND_TRACK_GROUP_NULL;
1331
- groups_table.remove(block_id);
1332
- block_id = next_id;
1333
- }
1334
-
1335
- clips_table.remove(clip_id);
1336
- }
1337
-
1338
- /**
1339
- * Drop a skin from the manager. Walks the joint-block chain and
1340
- * removes every block + the skin row, and tears down every
1341
- * skinning binding pointing at this skin (so the skinning
1342
- * context doesn't try to read the now-freed skin's matrix
1343
- * offset on its next upload).
1344
- *
1345
- * The skin's reserved matrix range stays allocated — compaction
1346
- * isn't supported in the MVP. Re-registering after an
1347
- * `unregister_skin` therefore costs new matrix slots; long-
1348
- * running spawn/despawn loops accumulate wasted slots.
1349
- *
1350
- * Tolerates the skin still being pending. Safe to call
1351
- * repeatedly on the same id.
1352
- *
1353
- * @param {number} skin_id
1354
- */
1355
- unregister_skin(skin_id) {
1356
- assert.isNonNegativeInteger(skin_id, 'skin_id');
1357
-
1358
- for (let i = this.#pending_skins.length - 1; i >= 0; i--) {
1359
- if (this.#pending_skins[i].skin_id === skin_id) {
1360
- this.#pending_skins.splice(i, 1);
1361
- }
1362
- }
1363
-
1364
- // Tear down skinning bindings first so the skinning
1365
- // context's next upload doesn't reference a freed skin row
1366
- // (its upload path looks up skin_matrix_offset via
1367
- // get_skin_matrix_offset, which would assert post-removal).
1368
- this.#scene_context.skinning.unbind_all_by_skin(skin_id);
1369
-
1370
- const skins_table = this.#database.get('animation_skins');
1371
- const blocks_table = this.#database.get('animation_skin_joint_blocks');
1372
-
1373
- const skin_record = skins_table.get(skin_id);
1374
- if (skin_record === undefined) {
1375
- this.#skin_matrix_offsets.delete(skin_id);
1376
- return;
1377
- }
1378
-
1379
- // Walk the joint-block chain, terminating on the NULL
1380
- // sentinel (also the placeholder for a not-yet-patched head).
1381
- // Note: 0 is a VALID block row id — the first single-block
1382
- // skin registered into an empty table gets head 0, and
1383
- // treating it as a placeholder would leak its block, leaving
1384
- // the prep shader processing it against a recycled
1385
- // transforms-table row every frame.
1386
- let block_id = skin_record.joint_block_head;
1387
- while (block_id !== ANIMATION_SKIN_JOINT_BLOCK_NULL) {
1388
- const block = blocks_table.get(block_id);
1389
- const next_id = block !== undefined ? block.next : ANIMATION_SKIN_JOINT_BLOCK_NULL;
1390
- blocks_table.remove(block_id);
1391
- block_id = next_id;
1392
- }
1393
-
1394
- skins_table.remove(skin_id);
1395
- this.#skin_matrix_offsets.delete(skin_id);
1396
- }
1397
-
1398
- /**
1399
- * Cached wrapper around {@link add_curve}: one upload per distinct curve, where distinct means
1400
- * distinct *content*.
1401
- *
1402
- * The instance cache answers first — the common case inside one clip, and free. Otherwise the
1403
- * curve's keys are hashed and held against every uploaded curve with that hash; a match on
1404
- * every key, tangents included, reuses that upload, and only a curve nobody has uploaded before
1405
- * goes through the low-level path. Either way the instance is remembered, so the next look-up
1406
- * of it is the free one.
1407
- *
1408
- * @param {AnimationCurve} curve
1409
- * @returns {number} curve id (index into the `animation_curves` table)
1410
- */
1411
- #intern_curve(curve) {
1412
- const cached = this.#curve_id_cache.get(curve);
1413
-
1414
- if (cached !== undefined) {
1415
- return cached;
1416
- }
1417
-
1418
- const hash = curve.hash();
1419
-
1420
- let bucket = this.#curve_id_by_content.get(hash);
1421
-
1422
- if (bucket === undefined) {
1423
- bucket = [];
1424
-
1425
- this.#curve_id_by_content.set(hash, bucket);
1426
- } else {
1427
- for (const entry of bucket) {
1428
- if (entry.curve.equals(curve)) {
1429
- this.#curve_id_cache.set(curve, entry.id);
1430
-
1431
- return entry.id;
1432
- }
1433
- }
1434
- }
1435
-
1436
- const id = this.add_curve(curve);
1437
-
1438
- this.#curve_id_cache.set(curve, id);
1439
-
1440
- bucket.push({ curve, id });
1441
-
1442
- return id;
1443
- }
1444
-
1445
- /**
1446
- * Cache-keyed wrapper around {@link add_track}. Two channels that
1447
- * end up with the same (x, y, z, w, mask) — typical when their
1448
- * source curves are shared via {@link #intern_curve} collapse
1449
- * to one track row instead of one row per channel.
1450
- *
1451
- * @param {{ x?: number, y?: number, z?: number, w?: number }} channels
1452
- * @returns {number} track id
1453
- */
1454
- #intern_track(channels) {
1455
- // Pack into a deterministic string key. -1 sentinel for
1456
- // missing channels keeps the (x=undefined) and (x=0) cases
1457
- // distinct.
1458
- const cx = channels.x ?? -1;
1459
- const cy = channels.y ?? -1;
1460
- const cz = channels.z ?? -1;
1461
- const cw = channels.w ?? -1;
1462
- const key = `${cx},${cy},${cz},${cw}`;
1463
-
1464
- const cached = this.#track_id_cache.get(key);
1465
- if (cached !== undefined) {
1466
- return cached;
1467
- }
1468
- const id = this.add_track(channels);
1469
- this.#track_id_cache.set(key, id);
1470
- return id;
1471
- }
1472
-
1473
- /**
1474
- * Grow the skinning-matrix scratch buffer if `required_count`
1475
- * exceeds current capacity. Doubles on grow until the requirement
1476
- * fits. The buffer's contents are scratch (rewritten every frame by
1477
- * the prep pass), so growth doesn't need to copy old data — but it
1478
- * does invalidate the previous {@link #skin_matrices_buffer}
1479
- * reference, which is why callers re-fetch through the getter.
1480
- *
1481
- * @param {number} required_count number of matrices that must fit
1482
- */
1483
- #ensure_skin_matrices_capacity(required_count) {
1484
- if (required_count <= this.#skin_matrices_capacity) {
1485
- return;
1486
- }
1487
-
1488
- let new_capacity = Math.max(this.#skin_matrices_capacity, SKIN_MATRICES_INITIAL_CAPACITY);
1489
- while (new_capacity < required_count) {
1490
- new_capacity *= 2;
1491
- }
1492
-
1493
- // Grow both ping-pong buffers in lockstep — they always share
1494
- // capacity. After grow both are zero-initialised, so the next
1495
- // prep dispatch needs to seed "previous" from "current"
1496
- // (otherwise velocity would interpolate against zero
1497
- // matrices). The flag below tells dispatch_skin_matrix_prep
1498
- // to issue that copy.
1499
- for (let i = 0; i < 2; i++) {
1500
- if (this.#skin_matrices_buffers[i] !== null) {
1501
- this.#skin_matrices_buffers[i].destroy();
1502
- }
1503
- this.#skin_matrices_buffers[i] = this.#device.createBuffer({
1504
- label: `${this.#label}/SkinMatrices/${i}`,
1505
- size: new_capacity * SKIN_MATRIX_BYTES,
1506
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
1507
- });
1508
- }
1509
- this.#skin_matrices_capacity = new_capacity;
1510
- this.#first_frame_skin_matrices = true;
1511
- }
1512
-
1513
- /**
1514
- * Flush all pending uploads (curves, keyframe blocks, tracks, clips,
1515
- * bound-track groups, skins, skin joint blocks) into the GPU
1516
- * database. Call once per frame, before any shader that reads from
1517
- * `database.buffer`.
1518
- *
1519
- * Also drains the pending-registration queues: skins and clips
1520
- * whose dependencies (joint Node3D rows, channel-target rows)
1521
- * weren't ready at register time get one shot per `update` to
1522
- * complete. Once `id_mapping` has the missing rows, the
1523
- * deferred chain build runs and the registration becomes
1524
- * observationally complete — the next-frame `database.update`
1525
- * picks up the patched head pointer.
1526
- *
1527
- * Does not touch the skin-matrices ping-pong buffers those
1528
- * are scratch populated each frame by
1529
- * {@link dispatch_skin_matrix_prep}, not host data.
1530
- *
1531
- * @param {ShadeGPUCommandContext} cmd_ctx
1532
- */
1533
- update(cmd_ctx) {
1534
- this.#drain_pending_skins();
1535
- this.#drain_pending_clips();
1536
- this.#database.update(cmd_ctx);
1537
- }
1538
-
1539
- #drain_pending_skins() {
1540
- // Walk back-to-front so successful completions can splice
1541
- // without disturbing the iteration's remaining indices.
1542
- for (let i = this.#pending_skins.length - 1; i >= 0; i--) {
1543
- if (this.#try_complete_skin(this.#pending_skins[i])) {
1544
- this.#pending_skins.splice(i, 1);
1545
- }
1546
- }
1547
- }
1548
-
1549
- #drain_pending_clips() {
1550
- for (let i = this.#pending_clips.length - 1; i >= 0; i--) {
1551
- if (this.#try_complete_clip(this.#pending_clips[i])) {
1552
- this.#pending_clips.splice(i, 1);
1553
- }
1554
- }
1555
- }
1556
-
1557
- /**
1558
- * Run the per-frame skin-matrix-prep dispatch and advance the
1559
- * ping-pong. Replaces the manual `shader_skin_matrix_prep.dispatch`
1560
- * the prototypes used to call — wrapping it here is how we get
1561
- * correct ping-pong behaviour across frames without leaking the
1562
- * buffer-role bookkeeping into every caller.
1563
- *
1564
- * No-op when no skins are registered (`group_count_x === 0`).
1565
- *
1566
- * Per-frame ordering:
1567
- * 1. Flip {@link #skin_matrices_current_index} the buffer that
1568
- * was last frame's "current" is about to be overwritten with
1569
- * this frame's matrices; the other buffer (last frame's prev)
1570
- * becomes this frame's "previous" implicit data source for
1571
- * {@link prev_skin_matrices_buffer}.
1572
- * 2. Wait — clarify: we want last frame's "current" to BECOME
1573
- * this frame's "previous". So flipping current_index moves
1574
- * the role tag the correct way: what was current[i] becomes
1575
- * previous (= buffers[1 - i]), and current[1 - i] is the
1576
- * write target.
1577
- * 3. Dispatch the prep shader into the new current.
1578
- * 4. On the very first frame (or after a grow), copy current →
1579
- * previous so the first velocity reads see matching matrices
1580
- * and produce zero velocity.
1581
- *
1582
- * @param {ShadeGPUCommandContext} cmd_ctx
1583
- */
1584
- dispatch_skin_matrix_prep(cmd_ctx) {
1585
- const groups = this.#database
1586
- .get('animation_skin_joint_blocks')
1587
- .dispatch_group_count(SKIN_MATRIX_PREP_WORKGROUP_SIZE);
1588
- if (groups === 0) {
1589
- return;
1590
- }
1591
-
1592
- // Flip the ping-pong: what was "current" last frame is no
1593
- // longer current, but it still holds valid matrices — and
1594
- // through the role flip it becomes this frame's
1595
- // `prev_skin_matrices_buffer`. The buffer we're flipping TO
1596
- // (the new "current") held two-frames-old matrices, about to
1597
- // be overwritten.
1598
- this.#skin_matrices_current_index = 1 - this.#skin_matrices_current_index;
1599
-
1600
- const current = this.#skin_matrices_buffers[this.#skin_matrices_current_index];
1601
- const previous = this.#skin_matrices_buffers[1 - this.#skin_matrices_current_index];
1602
-
1603
- shader_skin_matrix_prep.dispatch({
1604
- encoder: cmd_ctx,
1605
- group_count_x: groups,
1606
- bindings: {
1607
- animation_database: this.#database.buffer,
1608
- scene_database: this.#scene_context.scene_database_buffer,
1609
- skin_matrices: current,
1610
- },
1611
- });
1612
-
1613
- // First-frame init (or post-grow): "previous" buffer is
1614
- // zero-initialised, which would have the velocity pass
1615
- // interpolate against the origin and produce huge nonsense
1616
- // on the first frame the skin appears. Seed prev from
1617
- // current first velocity reads then yield zero, which is
1618
- // the right answer (no motion has happened yet).
1619
- if (this.#first_frame_skin_matrices) {
1620
- cmd_ctx.copyBufferToBuffer(
1621
- current, 0,
1622
- previous, 0,
1623
- current.size,
1624
- );
1625
- this.#first_frame_skin_matrices = false;
1626
- }
1627
- }
1628
-
1629
- /**
1630
- * Per-frame orchestrator: drains pending registrations and the
1631
- * database queue, runs the GPU tick (advances `clip.time`),
1632
- * applies tracks into the pose accumulator, resolves the
1633
- * accumulator into node TRS, propagates the transform
1634
- * hierarchy, preps skinning matrices, and runs the skinning
1635
- * compute pass that deforms cloned vertex regions in place.
1636
- *
1637
- * One call replaces the ~80-line per-frame dispatch block every
1638
- * consumer would otherwise duplicate; the shader identities,
1639
- * workgroup sizes, pose-accumulator sizing, and pass ordering
1640
- * are all internal.
1641
- *
1642
- * `dt_seconds` is wall-clock seconds since the last call (the
1643
- * caller measures via `performance.now()` or equivalent). It's
1644
- * clamped to {@link MAX_DT_SECONDS} to keep a stall from
1645
- * leaking multiple seconds into the next tick.
1646
- *
1647
- * Early-outs: the clip passes (tick/apply/resolve/hierarchy)
1648
- * gate on the `animation_bound_track_groups` table being
1649
- * non-empty; skin matrix prep + skinning gate on registered
1650
- * skins instead a skin whose joints are host-driven (no
1651
- * clips bound) still gets fresh skinning matrices every frame.
1652
- * With nothing registered no GPU work is encoded beyond the
1653
- * database flush — safe to call every frame regardless of
1654
- * registration state.
1655
- *
1656
- * @param {ShadeGPUCommandContext} cmd_ctx
1657
- * @param {number} dt_seconds wall-clock seconds since last call
1658
- */
1659
- tick(cmd_ctx, dt_seconds) {
1660
- assert.defined(cmd_ctx, 'cmd_ctx');
1661
- assert.isNumber(dt_seconds, 'dt_seconds');
1662
-
1663
- // Cap dt so a long stall doesn't fly the clip through dozens
1664
- // of loops or clamp it past the meaningful range.
1665
- const dt = Math.min(dt_seconds, MAX_DT_SECONDS);
1666
-
1667
- // Flush database + drain pending registrations.
1668
- this.update(cmd_ctx);
1669
-
1670
- // Clip-pass gate: no bound tracks → apply has nothing to
1671
- // do, and resolve + hierarchy propagation cascade from
1672
- // apply. Skipping them keeps the per-frame cost at "one
1673
- // database update" when the scene has no clip animation.
1674
- const apply_groups = this.#database
1675
- .get('animation_bound_track_groups')
1676
- .dispatch_group_count(ANIMATION_APPLY_WORKGROUP_SIZE);
1677
-
1678
- if (apply_groups > 0) {
1679
- this.#encode_clip_passes(cmd_ctx, dt, apply_groups);
1680
- }
1681
-
1682
- // Skin matrix prep + skinning deformation. Both no-op when
1683
- // no skins are registered and both run independent of the
1684
- // bound-track gate above: a registered skin whose joints
1685
- // are host-driven (no clips bound) must still be deformed
1686
- // each frame, otherwise it renders bind pose forever.
1687
- this.dispatch_skin_matrix_prep(cmd_ctx);
1688
- this.#scene_context.skinning.update(cmd_ctx);
1689
- }
1690
-
1691
- /**
1692
- * Encode the clip-driven passes: GPU tick (advances
1693
- * `clip.time`), track apply into the pose accumulator, pose
1694
- * resolve into node TRS, and transform-hierarchy propagation.
1695
- * Split out of {@link tick} so the skin passes there run
1696
- * independently of the bound-track early-out.
1697
- *
1698
- * @param {ShadeGPUCommandContext} cmd_ctx
1699
- * @param {number} dt clamped delta time, seconds
1700
- * @param {number} apply_groups dispatch group count for the
1701
- * apply pass — caller guarantees `> 0`
1702
- */
1703
- #encode_clip_passes(cmd_ctx, dt, apply_groups) {
1704
- // Tick: advance clip.time on the GPU.
1705
- const tick_groups = this.#database
1706
- .get('animation_clips')
1707
- .dispatch_group_count(ANIMATION_TICK_WORKGROUP_SIZE);
1708
- if (tick_groups > 0) {
1709
- shader_animation_tick.dispatch({
1710
- encoder: cmd_ctx,
1711
- group_count_x: tick_groups,
1712
- bindings: {
1713
- dt,
1714
- animation_database: this.#database.buffer,
1715
- },
1716
- });
1717
- }
1718
-
1719
- // Size the pose accumulator to cover every allocated row of
1720
- // the transforms table apply writes by absolute row index,
1721
- // resolve iterates the table via page iterator.
1722
- const transforms_table = this.#scene_context.scene_database.get('transforms');
1723
- const resolve_groups = transforms_table.dispatch_group_count(
1724
- ANIMATION_POSE_RESOLVE_WORKGROUP_SIZE
1725
- );
1726
- this.ensure_pose_accumulator_capacity(transforms_table.element_capacity);
1727
-
1728
- // ── Frame graph: apply → pose resolve → hierarchy propagate
1729
- const graph = new FrameGraph("GPUAnimationManager/Tick");
1730
- const gr_animation_database = graph_import_buffer(
1731
- graph, this.#database.buffer, "animation_database"
1732
- );
1733
- const gr_scene_database = graph_import_buffer(
1734
- graph, this.#scene_context.scene_database_buffer, "scene_database"
1735
- );
1736
- const gr_materials = graph_import_buffer(
1737
- graph, this.#scene_context.materials.metadata_table.buffer, "materials"
1738
- );
1739
- const gr_pose_accumulator = graph_import_buffer(
1740
- graph, this.#pose_accumulator_buffer, "pose_accumulator"
1741
- );
1742
-
1743
- const apply_data = {};
1744
- const apply_pass = graph.add("animation/apply", apply_data, (data, resources, ctx) => {
1745
- shader_animation_apply.dispatch({
1746
- encoder: ctx.encoder,
1747
- group_count_x: apply_groups,
1748
- bindings: {
1749
- animation_database: resources.get(data.animation_database),
1750
- pose_accumulator: resources.get(data.pose_accumulator),
1751
- materials: resources.get(data.materials),
1752
- },
1753
- });
1754
- });
1755
- apply_data.animation_database = apply_pass.read(gr_animation_database);
1756
- apply_data.materials = apply_pass.write(apply_pass.read(gr_materials));
1757
- apply_data.pose_accumulator = apply_pass.write(apply_pass.read(gr_pose_accumulator));
1758
-
1759
- const resolve_data = {};
1760
- const resolve_pass = graph.add("animation/pose_resolve", resolve_data, (data, resources, ctx) => {
1761
- if (resolve_groups === 0) return;
1762
- shader_animation_pose_resolve.dispatch({
1763
- encoder: ctx.encoder,
1764
- group_count_x: resolve_groups,
1765
- bindings: {
1766
- scene_database: resources.get(data.scene_database),
1767
- pose_accumulator: resources.get(data.pose_accumulator),
1768
- },
1769
- });
1770
- });
1771
- resolve_data.scene_database = resolve_pass.write(resolve_pass.read(gr_scene_database));
1772
- resolve_data.pose_accumulator = resolve_pass.write(resolve_pass.read(apply_data.pose_accumulator));
1773
-
1774
- graph_scene_update_transform_hierarchy({
1775
- graph,
1776
- scene: this.#scene_context,
1777
- scene_database: resolve_data.scene_database,
1778
- });
1779
-
1780
- cmd_ctx.encodeGraph(graph);
1781
- }
1782
-
1783
- destroy() {
1784
- this.#database.destroy();
1785
- for (let i = 0; i < 2; i++) {
1786
- if (this.#skin_matrices_buffers[i] !== null) {
1787
- this.#skin_matrices_buffers[i].destroy();
1788
- this.#skin_matrices_buffers[i] = null;
1789
- }
1790
- }
1791
- if (this.#pose_accumulator_buffer !== null) {
1792
- this.#pose_accumulator_buffer.destroy();
1793
- this.#pose_accumulator_buffer = null;
1794
- }
1795
- }
1796
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { FrameGraph } from "../../../engine/graphics/render/frame_graph/FrameGraph.js";
3
+ import { GPUDatabase } from "../buffer/table/GPUDatabase.js";
4
+ import {
5
+ graph_scene_update_transform_hierarchy
6
+ } from "../scene/hierarchy/graph_scene_update_transform_hierarchy.js";
7
+ import { graph_import_buffer } from "../shader/graph/graph_import_buffer.js";
8
+ import {
9
+ ANIMATION_BOUND_TRACK_GROUP_NULL,
10
+ TRACKS_PER_GROUP_NODE,
11
+ } from "./ANIMATION_BOUND_TRACK_GROUP_STRUCT.js";
12
+ import { ANIMATION_DATABASE_SPEC } from "./ANIMATION_DATABASE_SPEC.js";
13
+ import { AnimationClipFlags } from "./AnimationClipFlags.js";
14
+ import { KEYFRAMES_PER_BLOCK } from "./ANIMATION_KEYFRAME_BLOCK_STRUCT.js";
15
+ import {
16
+ ANIMATION_SKIN_JOINT_BLOCK_NULL,
17
+ JOINTS_PER_SKIN_BLOCK,
18
+ } from "./ANIMATION_SKIN_JOINT_BLOCK_STRUCT.js";
19
+ import { POSE_ACCUMULATOR_SLOT_BYTES } from "./POSE_ACCUMULATOR_LAYOUT.js";
20
+ import {
21
+ clip_scale_channels,
22
+ skin_clip_scale_conflicts
23
+ } from "./skin_clip_scale_conflicts.js";
24
+ import { skin_normalize_mesh_frame } from "./skin_normalize_mesh_frame.js";
25
+ import {
26
+ shader_animation_apply,
27
+ WORKGROUP_SIZE as ANIMATION_APPLY_WORKGROUP_SIZE,
28
+ } from "./shader_animation_apply.js";
29
+ import {
30
+ shader_animation_pose_resolve,
31
+ WORKGROUP_SIZE as ANIMATION_POSE_RESOLVE_WORKGROUP_SIZE,
32
+ } from "./shader_animation_pose_resolve.js";
33
+ import {
34
+ shader_animation_tick,
35
+ WORKGROUP_SIZE as ANIMATION_TICK_WORKGROUP_SIZE,
36
+ } from "./shader_animation_tick.js";
37
+ import {
38
+ shader_skin_matrix_prep,
39
+ WORKGROUP_SIZE as SKIN_MATRIX_PREP_WORKGROUP_SIZE,
40
+ } from "./skinning/shader_skin_matrix_prep.js";
41
+
42
+ /**
43
+ * Tail-slot placeholders for the fixed-size inline arrays in chain
44
+ * nodes. The shader's `count` guard means these bytes are never read,
45
+ * but the upload buffer doesn't zero unwritten ranges and
46
+ * `write_struct_value` skips undefined fields so explicit zeros are
47
+ * the only way to keep tail bytes deterministic. Frozen so a stray
48
+ * mutation can't bleed into multiple slots that share the reference.
49
+ */
50
+ const ZERO_KEYFRAME = Object.freeze({ time: 0, value: 0, inTangent: 0, outTangent: 0 });
51
+ const ZERO_BINDING = Object.freeze({ track: 0, instance: 0, property: 0 });
52
+ // Object.freeze on a typed array with elements throws under V8
53
+ // ("Cannot freeze array buffer views with elements"), so we leave the
54
+ // Float32Array unfrozen — the surrounding placeholder object is still
55
+ // frozen to keep accidental field-level mutation out of bounds.
56
+ const ZERO_INVERSE_BIND = new Float32Array(16);
57
+ const ZERO_SKIN_JOINT = Object.freeze({ node: 0, inverse_bind: ZERO_INVERSE_BIND });
58
+
59
+ /**
60
+ * One skinning matrix is a `mat4x4f` — 16 f32s, 64 bytes. The
61
+ * `skin_matrices` scratch buffer holds one entry per (skin, joint) pair
62
+ * across every registered skin.
63
+ */
64
+ const SKIN_MATRIX_BYTES = 64;
65
+
66
+ /**
67
+ * Initial capacity of the `skin_matrices` scratch buffer, in matrices.
68
+ * 256 × 64 = 16 KB — fits a handful of small skeletons before the first
69
+ * grow. Picked to be wasteful neither at "no skins" nor "one character"
70
+ * scale; growth doubles, so a few realloc cycles cover anything larger.
71
+ */
72
+ const SKIN_MATRICES_INITIAL_CAPACITY = 256;
73
+
74
+ /**
75
+ * Initial capacity of the {@link #pose_accumulator_buffer} in rows
76
+ * (= transforms-table slots). 256 × 64 = 16 KB; growth doubles to
77
+ * fit larger scenes. Resolve always zeroes the slot after draining,
78
+ * so a fresh buffer (createBuffer is zero-initialised) and a "no
79
+ * contribution this frame" state are observationally identical.
80
+ */
81
+ const POSE_ACCUMULATOR_INITIAL_CAPACITY = 256;
82
+
83
+ /**
84
+ * Per-frame `dt` cap pushed into the tick shader. Without it, a tab
85
+ * backgrounded for several seconds (or a paused debugger) would
86
+ * leak that whole interval into the next tick — either rocketing a
87
+ * looping clip through dozens of cycles in one step or, for
88
+ * non-looping clips, clamping them past the meaningful range. The
89
+ * cap trades a few frames of CSS-smooth animation after a stall
90
+ * for a deterministic clip state.
91
+ */
92
+ const MAX_DT_SECONDS = 1 / 15;
93
+
94
+ /**
95
+ * Allocate a singly-linked chain of fixed-capacity nodes into `table`,
96
+ * one node per `items_per_chunk`-sized window of `items`. Builds
97
+ * tail-first so each node's successor id is known at `add()` time —
98
+ * the alternative (forward + back-patch) would mean a second `set()`
99
+ * per node.
100
+ *
101
+ * `build_node(start, count, next_id)` constructs the record to add: it
102
+ * populates the inline array from `items[start..start+count]`, pads
103
+ * tail slots, and assembles any extra payload the table expects
104
+ * (including embedding `next_id` as the node's `next` field).
105
+ *
106
+ * Returns the head id (the last `add()`'s slot id), or `0xFFFFFFFF` if
107
+ * `items` is empty.
108
+ *
109
+ * @template T
110
+ * @template R
111
+ * @param {{ add: (record: R) => number }} table
112
+ * @param {T[]} items
113
+ * @param {number} items_per_chunk
114
+ * @param {(start: number, count: number, next_id: number) => R} build_node
115
+ * @returns {number}
116
+ */
117
+ function add_linked_chain(table, items, items_per_chunk, build_node) {
118
+ const node_count = Math.ceil(items.length / items_per_chunk);
119
+ let next_id = 0xFFFFFFFF;
120
+
121
+ for (let n = node_count - 1; n >= 0; n--) {
122
+ const start = n * items_per_chunk;
123
+ const this_count = Math.min(items_per_chunk, items.length - start);
124
+ next_id = table.add(build_node(start, this_count, next_id));
125
+ }
126
+
127
+ return next_id;
128
+ }
129
+
130
+ /**
131
+ * Top-level coordinator for GPU-resident animation.
132
+ *
133
+ * Owns a single {@link GPUDatabase} keyed by {@link ANIMATION_DATABASE_SPEC}
134
+ * with five tables: `animation_curves`, `animation_keyframe_blocks`,
135
+ * `animation_tracks`, `animation_clips`, `animation_bound_track_groups`.
136
+ * Every record including individual keyframes (inlined inside block
137
+ * nodes) and individual bindings (inlined inside group nodes) lives in
138
+ * the database, so the dispatch shader needs only one storage binding.
139
+ *
140
+ * Animation *data* (curves, keyframes, tracks) is independent from
141
+ * *bound clips* (each carrying a `time` and a head pointer to its
142
+ * bound-track group chain). Bindings are no longer their own table:
143
+ * each chain node inlines up to {@link TRACKS_PER_GROUP_NODE} bindings
144
+ * and back-references its owning clip.
145
+ *
146
+ * Usage:
147
+ * 1. (Once at setup) Either low-level (`add_curve`, `add_track`,
148
+ * `add_skin`, `add_clip`) or high-level (`register_skin`,
149
+ * `register_clip` against the CPU data classes {@link Skin} /
150
+ * {@link ShadeAnimationClip}).
151
+ * 2. (Once) {@link start} per clip to begin GPU-driven playback —
152
+ * the {@link shader_animation_tick} pass advances `time` each
153
+ * frame thereafter. CPU-driven prototypes can instead call
154
+ * {@link set_time} per frame and skip the Playing flag.
155
+ * 3. (Per frame) {@link update} flushes pending writes for the
156
+ * database. The scene context already does this on its own
157
+ * update path.
158
+ */
159
+ export class GPUAnimationManager {
160
+
161
+ /** @type {GPUDevice} */
162
+ #device;
163
+
164
+ /** @type {string} */
165
+ #label;
166
+
167
+ /** @type {GPUDatabase} */
168
+ #database;
169
+
170
+ /**
171
+ * Ping-pong pair of per-frame skinning-matrix buffers — one holds
172
+ * this frame's matrices (target of the prep dispatch, source for
173
+ * skinning's current-position write), the other last frame's
174
+ * (source for skinning's `position_prev` write that feeds the
175
+ * velocity post-pass).
176
+ *
177
+ * Ping-pong rather than a single buffer because the prev-frame
178
+ * matrices need to survive next frame's prep dispatch and the
179
+ * cheapest way to preserve them is to write the new matrices into
180
+ * the OTHER buffer instead of overwriting the only one we have.
181
+ *
182
+ * {@link #skin_matrices_current_index} selects which entry of this
183
+ * array is "current" for the frame about to be dispatched;
184
+ * {@link dispatch_skin_matrix_prep} flips it before each frame's
185
+ * prep so the now-stale slot gets recycled.
186
+ *
187
+ * Lazily allocated on first {@link add_skin}; both grow together
188
+ * by doubling. After a grow both buffers are zero-initialised and
189
+ * {@link #first_frame_skin_matrices} is set so the next prep
190
+ * dispatch seeds the "previous" buffer with a copy of the freshly
191
+ * written "current" — without this, the first velocity readback
192
+ * after a grow would interpolate against zero matrices.
193
+ *
194
+ * @type {Array<GPUBuffer|null>}
195
+ */
196
+ #skin_matrices_buffers = [null, null];
197
+
198
+ /**
199
+ * Which entry of {@link #skin_matrices_buffers} is "current" for
200
+ * the next prep dispatch. Flipped at the start of every prep call.
201
+ * @type {number}
202
+ */
203
+ #skin_matrices_current_index = 0;
204
+
205
+ /**
206
+ * When true, the next prep dispatch will copy the freshly-written
207
+ * "current" buffer into the "previous" buffer at the end of the
208
+ * pass — used on first allocation and after every grow so the
209
+ * very first velocity computation has matching prev/current
210
+ * matrices (zero velocity).
211
+ * @type {boolean}
212
+ */
213
+ #first_frame_skin_matrices = true;
214
+
215
+ /**
216
+ * Capacity of each of {@link #skin_matrices_buffers} in matrices
217
+ * (not bytes). Both buffers always have the same capacity.
218
+ * @type {number}
219
+ */
220
+ #skin_matrices_capacity = 0;
221
+
222
+ /**
223
+ * Next free matrix index. Each {@link add_skin} reserves
224
+ * `joint_count` consecutive entries starting here, then advances.
225
+ * Monotonic — no freeing in the MVP.
226
+ * @type {number}
227
+ */
228
+ #next_skin_matrix_offset = 0;
229
+
230
+ /**
231
+ * CPU-side `skin_id skin_matrix_offset` map. Each registered
232
+ * skin's base address in the shared `skin_matrices` scratch
233
+ * buffer; the per-block absolute offset (the prep shader's hot
234
+ * path) is derived as `base + chain_position *
235
+ * JOINTS_PER_SKIN_BLOCK` at registration. Storing the base on
236
+ * GPU would duplicate it — the block records already carry the
237
+ * per-block absolute address, and only the CPU (the skinning-
238
+ * binding upload path) needs the skin's base value.
239
+ *
240
+ * @type {Map<number, number>}
241
+ */
242
+ #skin_matrix_offsets = new Map();
243
+
244
+ /**
245
+ * Registered skins by id, kept for one reason: a clip registered *after* a skin has to be held
246
+ * against it, and a clip registered before one has to be held against the skin when it arrives.
247
+ * The frame a skinned mesh is deformed in is fixed once, at registration, and a clip that scales
248
+ * a joint is the one thing that can undo it afterwards — see {@link skin_clip_scale_conflicts}
249
+ * and `SKINNING_FRAME_PLAN.md` §4.
250
+ *
251
+ * @type {Map<number, Skin>}
252
+ */
253
+ #skins_by_id = new Map();
254
+
255
+ /**
256
+ * Registered clips that scale *something*, by id. Almost always empty — a character's clips
257
+ * rotate and translate — which is what keeps the pairwise check free in the ordinary case.
258
+ *
259
+ * @type {Map<number, ShadeAnimationClip>}
260
+ */
261
+ #scaling_clips = new Map();
262
+
263
+ /**
264
+ * Per-frame pose-accumulator scratch buffer. One slot per
265
+ * `transforms_table` row (slot size = `POSE_ACCUMULATOR_SLOT_BYTES`).
266
+ * The apply pass atomic-adds weighted TRS contributions into each
267
+ * dirty row; the resolve pass drains it into `transforms_table.local_*`
268
+ * and zeros the slot for next frame.
269
+ *
270
+ * Lazily allocated on first {@link ensure_pose_accumulator_capacity};
271
+ * grows by doubling. Grow destroys the old buffer — since resolve
272
+ * always leaves it zero, no copy is needed on grow.
273
+ *
274
+ * @type {GPUBuffer|null}
275
+ */
276
+ #pose_accumulator_buffer = null;
277
+
278
+ /**
279
+ * Capacity of {@link #pose_accumulator_buffer} in rows (slot
280
+ * indices), not bytes.
281
+ * @type {number}
282
+ */
283
+ #pose_accumulator_capacity = 0;
284
+
285
+ /**
286
+ * Back-reference to the owning scene context. Lets the high-level
287
+ * `register_*` methods resolve `Node3D.id` transforms-table row
288
+ * via {@link GPUSceneContext#id_mapping} and bind skinning meshes
289
+ * via {@link GPUSceneContext#skinning}, without callers having to
290
+ * thread the context through every call.
291
+ *
292
+ * The manager doesn't touch this at construction (the scene
293
+ * context is itself only partially constructed when it builds the
294
+ * manager) only inside `register_*` calls, which happen after
295
+ * scene.build has populated `id_mapping`.
296
+ *
297
+ * @type {GPUSceneContext}
298
+ */
299
+ #scene_context;
300
+
301
+ /**
302
+ * Identity-keyed cache of curve-id-on-GPU for every
303
+ * {@link AnimationCurve} that has been registered via
304
+ * {@link register_clip}. Lets multiple clips whose channels share
305
+ * the same curve instance (e.g. one base clip retargeted across
306
+ * many character instances) reuse the same GPU rows for the
307
+ * curve header + keyframe blocks rather than uploading
308
+ * duplicates. At grid-of-dancers scale duplication would balloon
309
+ * the keyframe-blocks table past its page-table limits.
310
+ *
311
+ * Low-level `add_curve` deliberately stays uncached — every call
312
+ * uploads — so existing callers that pre-build curves with
313
+ * distinct IDs (e.g. a pre-built track pool)
314
+ * keep their behavior.
315
+ *
316
+ * @type {WeakMap<AnimationCurve, number>}
317
+ */
318
+ #curve_id_cache = new WeakMap();
319
+
320
+ /**
321
+ * The curves uploaded so far, by what they are rather than which instance they were: the
322
+ * `hash()` of a curve's keys, to every uploaded curve with that hash and its GPU id.
323
+ *
324
+ * An instance cache alone shares nothing between two parses of one file, and a character
325
+ * instanced a few hundred times is a few hundred parses — each with its own instances of the
326
+ * same keys, filling the keyframe-block table with copies of one dance until its page limit
327
+ * refuses the next dancer. Curves are read-only once uploaded and nothing here releases them,
328
+ * so equal content can safely mean one upload.
329
+ *
330
+ * A hash bucket is a list because a hash is not an identity: a curve is only shared with one
331
+ * whose every key `equals` its own, tangents included.
332
+ *
333
+ * @type {Map<number, {curve: AnimationCurve, id: number}[]>}
334
+ */
335
+ #curve_id_by_content = new Map();
336
+
337
+ /**
338
+ * Cache of track-id-on-GPU keyed by `(curve_id_x, curve_id_y,
339
+ * curve_id_z, curve_id_w, mask)`. With curve dedup already
340
+ * sharing curve IDs across clips, two channels in different
341
+ * clips that bind to the same (target_property, curves) end up
342
+ * with identical track keys — and we can share the track row
343
+ * too. Each curve_id slot is `-1` when the corresponding channel
344
+ * is absent.
345
+ *
346
+ * Keys are joined into a string for Map lookup; the join cost is
347
+ * negligible compared to the GPU upload it avoids.
348
+ *
349
+ * @type {Map<string, number>}
350
+ */
351
+ #track_id_cache = new Map();
352
+
353
+ /**
354
+ * Skins registered via {@link register_skin} whose joint-block
355
+ * chain hasn't been built yet because one or more joint Node3Ds
356
+ * lacks an `id_mapping` row. The skin's GPU table row exists
357
+ * (placeholder `joint_block_head = NULL`, no joints driving the
358
+ * prep pass yet), and state mutators / unregister already work
359
+ * — but the deformation only kicks in once {@link update}
360
+ * successfully drains the entry.
361
+ *
362
+ * Each entry: `skin_id`, the CPU {@link Skin} (whose `joints`
363
+ * array drives the chain build), the `skin_matrix_offset`
364
+ * reserved at register time, and the `inverse_bind_matrices`
365
+ * typed array.
366
+ *
367
+ * @type {Array<{
368
+ * skin_id: number,
369
+ * skin: Skin,
370
+ * skin_matrix_offset: number,
371
+ * inverse_bind_matrices: Float32Array,
372
+ * }>}
373
+ */
374
+ #pending_skins = [];
375
+
376
+ /**
377
+ * Clips registered via {@link register_clip} whose bindings
378
+ * chain hasn't been built yet because one or more channel
379
+ * targets lacks an `id_mapping` row. The clip's GPU table row
380
+ * exists (placeholder `bound_track_group_head = NULL`, no
381
+ * tracks driving the apply pass yet); time/state mutators and
382
+ * unregister already work.
383
+ *
384
+ * Each entry: `clip_id` plus the per-channel `(track_id,
385
+ * target, property)` triples — the curves and tracks were
386
+ * interned at register time so only the (target → instance row)
387
+ * resolution is deferred.
388
+ *
389
+ * @type {Array<{
390
+ * clip_id: number,
391
+ * channels: Array<{ track: number, target: Node3D, property: number }>,
392
+ * }>}
393
+ */
394
+ #pending_clips = [];
395
+
396
+ /**
397
+ * @param {GPUDevice} device
398
+ * @param {string} label
399
+ * @param {GPUSceneContext} scene_context
400
+ */
401
+ constructor(device, label, scene_context) {
402
+ assert.defined(device, 'device');
403
+ assert.defined(scene_context, 'scene_context');
404
+
405
+ this.#device = device;
406
+ this.#label = label;
407
+ this.#scene_context = scene_context;
408
+
409
+ this.#database = new GPUDatabase({
410
+ device,
411
+ definition: ANIMATION_DATABASE_SPEC,
412
+ label: `${label}/Database`,
413
+ });
414
+ }
415
+
416
+ /**
417
+ * @returns {GPUDatabase}
418
+ */
419
+ get database() {
420
+ return this.#database;
421
+ }
422
+
423
+ /**
424
+ * Per-frame skinning-matrix scratch buffer for the *current*
425
+ * frame. `null` until the first skin is registered. The
426
+ * skin-matrix-prep pass writes to it; the skinning compute pass
427
+ * reads from it to deform current-frame positions. Address `j`
428
+ * of skin `s` is at matrix index `s.skin_matrix_offset + j`.
429
+ *
430
+ * The buffer reference is invalidated on grow AND flips role
431
+ * every frame as part of the ping-pong with
432
+ * {@link prev_skin_matrices_buffer} re-fetch each frame.
433
+ *
434
+ * @returns {GPUBuffer|null}
435
+ */
436
+ get skin_matrices_buffer() {
437
+ return this.#skin_matrices_buffers[this.#skin_matrices_current_index];
438
+ }
439
+
440
+ /**
441
+ * Previous-frame counterpart of {@link skin_matrices_buffer} —
442
+ * holds the matrices the previous frame's prep dispatch wrote.
443
+ * Read by the skinning compute pass to produce per-vertex
444
+ * `position_prev` data for the velocity post-pass.
445
+ *
446
+ * `null` until the first skin is registered. Flips role with
447
+ * {@link skin_matrices_buffer} every frame.
448
+ *
449
+ * @returns {GPUBuffer|null}
450
+ */
451
+ get prev_skin_matrices_buffer() {
452
+ return this.#skin_matrices_buffers[1 - this.#skin_matrices_current_index];
453
+ }
454
+
455
+ /**
456
+ * Number of matrices currently in use (= sum of `joint_count` over
457
+ * registered skins). Independent of buffer capacity, which is at
458
+ * least this big.
459
+ *
460
+ * @returns {number}
461
+ */
462
+ get skin_matrix_count() {
463
+ return this.#next_skin_matrix_offset;
464
+ }
465
+
466
+ /**
467
+ * Pose-accumulator scratch buffer (per-Node3D-row weighted
468
+ * contribution slots). `null` until {@link ensure_pose_accumulator_capacity}
469
+ * is called for the first time.
470
+ *
471
+ * The buffer reference is invalidated on grow — re-fetch each
472
+ * frame after {@link ensure_pose_accumulator_capacity} rather
473
+ * than caching across frames.
474
+ *
475
+ * @returns {GPUBuffer|null}
476
+ */
477
+ get pose_accumulator_buffer() {
478
+ return this.#pose_accumulator_buffer;
479
+ }
480
+
481
+ /**
482
+ * Ensure the pose-accumulator scratch buffer can address at least
483
+ * `row_count` `transforms_table` rows. Grows by doubling from the
484
+ * initial capacity; idempotent when already big enough. Called
485
+ * by the per-frame dispatch driver after `scene.build` has
486
+ * stabilised the transforms-table size for the frame.
487
+ *
488
+ * Grow destroys the previous GPU buffer; resolve always leaves the
489
+ * accumulator zeroed at the end of each frame, so there's nothing
490
+ * to copy across the realloc.
491
+ *
492
+ * @param {number} row_count
493
+ */
494
+ ensure_pose_accumulator_capacity(row_count) {
495
+ assert.isNonNegativeInteger(row_count, 'row_count');
496
+
497
+ // Existing buffer fits the request → nothing to do. The null
498
+ // check guarantees the very first call always allocates,
499
+ // even when `row_count == 0` (e.g. before any scene content
500
+ // has been built): downstream binding paths assume the
501
+ // buffer reference is non-null whenever the apply pass runs.
502
+ if (this.#pose_accumulator_buffer !== null && row_count <= this.#pose_accumulator_capacity) {
503
+ return;
504
+ }
505
+
506
+ let new_capacity = Math.max(this.#pose_accumulator_capacity, POSE_ACCUMULATOR_INITIAL_CAPACITY);
507
+ while (new_capacity < row_count) {
508
+ new_capacity *= 2;
509
+ }
510
+
511
+ if (this.#pose_accumulator_buffer !== null) {
512
+ this.#pose_accumulator_buffer.destroy();
513
+ }
514
+
515
+ this.#pose_accumulator_buffer = this.#device.createBuffer({
516
+ label: `${this.#label}/PoseAccumulator`,
517
+ size: new_capacity * POSE_ACCUMULATOR_SLOT_BYTES,
518
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
519
+ });
520
+ this.#pose_accumulator_capacity = new_capacity;
521
+ }
522
+
523
+ /**
524
+ * Read back a skin's record from the database's CPU shadow.
525
+ *
526
+ * @param {number} skin_id
527
+ * @returns {{joint_block_head: number, joint_count: number}}
528
+ */
529
+ get_skin(skin_id) {
530
+ assert.isNonNegativeInteger(skin_id, 'skin_id');
531
+ return this.#database.get('animation_skins').get(skin_id);
532
+ }
533
+
534
+ /**
535
+ * Base address of the skin's matrix range in the shared
536
+ * `skin_matrices` scratch buffer matrix `j` of the skin lives
537
+ * at `skin_matrices[get_skin_matrix_offset(skin_id) + j]`.
538
+ *
539
+ * Tracked CPU-side rather than on the skin record because the
540
+ * value is only ever consumed by the skinning-binding upload
541
+ * path; the prep shader uses the per-block absolute offset
542
+ * (cheaper than a per-thread skin lookup for that hot path).
543
+ *
544
+ * @param {number} skin_id
545
+ * @returns {number}
546
+ */
547
+ get_skin_matrix_offset(skin_id) {
548
+ assert.isNonNegativeInteger(skin_id, 'skin_id');
549
+ const offset = this.#skin_matrix_offsets.get(skin_id);
550
+ assert.defined(offset, `unknown skin_id ${skin_id}`);
551
+ return offset;
552
+ }
553
+
554
+ /**
555
+ * Add an animation curve. Keyframes are split into linked-list blocks
556
+ * of up to {@link KEYFRAMES_PER_BLOCK} entries each; the curve header
557
+ * points at the head block, and each block's `next` field chains
558
+ * forward.
559
+ *
560
+ * The curve is a cubic Hermite spline — step, linear, and arbitrary
561
+ * cubic shapes are all expressible by choosing tangents on each keyframe.
562
+ *
563
+ * @param {AnimationCurve} curve
564
+ * @returns {number} curve id (index into the `animation_curves` table)
565
+ */
566
+ add_curve(curve) {
567
+ assert.defined(curve, 'curve');
568
+ assert.equal(curve.isAnimationCurve, true, 'curve.isAnimationCurve !== true');
569
+ assert.greaterThanOrEqual(curve.length, 1, 'curve must have at least one keyframe');
570
+
571
+ const keys = curve.keys;
572
+ const blocks_table = this.#database.get('animation_keyframe_blocks');
573
+
574
+ const head_block_id = add_linked_chain(
575
+ blocks_table,
576
+ keys,
577
+ KEYFRAMES_PER_BLOCK,
578
+ (start, count, next) => {
579
+ const keyframes = new Array(KEYFRAMES_PER_BLOCK);
580
+ for (let j = 0; j < KEYFRAMES_PER_BLOCK; j++) {
581
+ keyframes[j] = j < count ? keys[start + j] : ZERO_KEYFRAME;
582
+ }
583
+ return { keyframes, count, next };
584
+ }
585
+ );
586
+
587
+ const curves_table = this.#database.get('animation_curves');
588
+ return curves_table.add({
589
+ kind: 0,
590
+ keyframe_block_head: head_block_id,
591
+ keyframe_count: keys.length,
592
+ time_min: curve.start_time,
593
+ time_max: curve.end_time,
594
+ });
595
+ }
596
+
597
+ /**
598
+ * Add a track. Channels are addressed by name (`x`, `y`, `z`, `w`) the
599
+ * provided curves go into the matching slots of the track's `vec4f`
600
+ * output. Omitted channels are masked off and read as 0.
601
+ *
602
+ * The slot meaning is property-specific: for `MeshTranslation`, x/y/z
603
+ * are the translation components; for `MeshRotation` they're the
604
+ * quaternion's x/y/z/w. The track's mask travels with the evaluated
605
+ * value to the apply step, which uses it to decide which destinations
606
+ * to overwrite.
607
+ *
608
+ * @param {{ x?: number, y?: number, z?: number, w?: number }} channels
609
+ * curve ids per channel; pass at least one
610
+ * @returns {number} track id (index into the `animation_tracks` table)
611
+ */
612
+ add_track(channels) {
613
+ assert.defined(channels, 'channels');
614
+
615
+ const cx = channels.x;
616
+ const cy = channels.y;
617
+ const cz = channels.z;
618
+ const cw = channels.w;
619
+
620
+ let mask = 0;
621
+ if (cx !== undefined) mask |= 0x1;
622
+ if (cy !== undefined) mask |= 0x2;
623
+ if (cz !== undefined) mask |= 0x4;
624
+ if (cw !== undefined) mask |= 0x8;
625
+
626
+ assert.notEqual(mask, 0, 'track must have at least one channel');
627
+
628
+ const curves = [
629
+ cx ?? 0,
630
+ cy ?? 0,
631
+ cz ?? 0,
632
+ cw ?? 0,
633
+ ];
634
+
635
+ const table = this.#database.get('animation_tracks');
636
+ return table.add({ mask, curves });
637
+ }
638
+
639
+ /**
640
+ * Create a bound clip — a group of bound tracks that share a single
641
+ * `time`. Bindings are split into linked-list group nodes of up to
642
+ * {@link TRACKS_PER_GROUP_NODE} each; the clip points at the head
643
+ * node and each node back-references the clip so the apply shader
644
+ * (one thread per group node) can fetch the clip without walking the
645
+ * chain from the clip end.
646
+ *
647
+ * Tracks may be authored on a clock that doesn't align with
648
+ * `[time_start, time_end]` — their curves are clamped to their own
649
+ * `[time_min, time_max]` during evaluation.
650
+ *
651
+ * Defaults: `time = 0`, `time_start = 0`, `time_end = 0`,
652
+ * `playback_rate = 1`, `playback_weight = 1`, `flags = 0` (not
653
+ * playing, not looping). The clip is inert at creation call
654
+ * {@link start} to begin GPU-side time advancement.
655
+ *
656
+ * @param {{
657
+ * time?: number,
658
+ * time_start?: number,
659
+ * time_end?: number,
660
+ * bindings: Array<{ track: number, instance: number, property: number }>,
661
+ * }} clip
662
+ * @returns {number} clip id (index into the `animation_clips` table)
663
+ */
664
+ add_clip(clip) {
665
+ assert.defined(clip, 'clip');
666
+ assert.isArray(clip.bindings, 'clip.bindings');
667
+ assert.greaterThanOrEqual(clip.bindings.length, 1, 'clip must have at least one binding');
668
+
669
+ const time = clip.time ?? 0;
670
+ const time_start = clip.time_start ?? 0;
671
+ const time_end = clip.time_end ?? 0;
672
+ const bindings = clip.bindings;
673
+
674
+ const clips_table = this.#database.get('animation_clips');
675
+ const groups_table = this.#database.get('animation_bound_track_groups');
676
+
677
+ // Reserve the clip slot up front so group nodes can back-reference
678
+ // it via `clip`. The head pointer is patched in once the chain is
679
+ // built (we don't know its first id until we've allocated every
680
+ // group node).
681
+ const clip_id = clips_table.add({
682
+ time,
683
+ time_start,
684
+ time_end,
685
+ bound_track_group_head: ANIMATION_BOUND_TRACK_GROUP_NULL,
686
+ playback_rate: 1,
687
+ flags: 0,
688
+ playback_weight: 1,
689
+ });
690
+
691
+ const head_group_id = add_linked_chain(
692
+ groups_table,
693
+ bindings,
694
+ TRACKS_PER_GROUP_NODE,
695
+ (start, count, next) => {
696
+ const inline_bindings = new Array(TRACKS_PER_GROUP_NODE);
697
+ for (let j = 0; j < TRACKS_PER_GROUP_NODE; j++) {
698
+ if (j < count) {
699
+ const b = bindings[start + j];
700
+
701
+ assert.isNonNegativeInteger(b.track, `clip.bindings[${start + j}].track`);
702
+ assert.isNonNegativeInteger(b.instance, `clip.bindings[${start + j}].instance`);
703
+ assert.isNonNegativeInteger(b.property, `clip.bindings[${start + j}].property`);
704
+
705
+ inline_bindings[j] = b;
706
+ } else {
707
+ inline_bindings[j] = ZERO_BINDING;
708
+ }
709
+ }
710
+ return { bindings: inline_bindings, count, next, clip: clip_id };
711
+ }
712
+ );
713
+
714
+ // Patch the head pointer now that the chain exists. Re-`set` the
715
+ // whole record because the table API doesn't expose per-field
716
+ // updates.
717
+ clips_table.set(clip_id, {
718
+ time,
719
+ time_start,
720
+ time_end,
721
+ bound_track_group_head: head_group_id,
722
+ playback_rate: 1,
723
+ flags: 0,
724
+ playback_weight: 1,
725
+ });
726
+
727
+ return clip_id;
728
+ }
729
+
730
+ /**
731
+ * Overwrite a single field of a clip's record by reading the current
732
+ * record back from the CPU shadow and re-`set`ting with one field
733
+ * changed. The table API doesn't expose per-field updates, so we
734
+ * round-trip through a full record; the upload-buffer dedupe
735
+ * collapses the resulting per-frame write into a single GPU touch
736
+ * when nothing else changed.
737
+ *
738
+ * @param {number} clip_id
739
+ * @param {string} field
740
+ * @param {number} value
741
+ */
742
+ #patch_clip_field(clip_id, field, value) {
743
+ assert.isNonNegativeInteger(clip_id, 'clip_id');
744
+
745
+ const clips_table = this.#database.get('animation_clips');
746
+ const current = clips_table.get(clip_id);
747
+
748
+ clips_table.set(clip_id, {
749
+ time: current.time,
750
+ time_start: current.time_start,
751
+ time_end: current.time_end,
752
+ bound_track_group_head: current.bound_track_group_head,
753
+ playback_rate: current.playback_rate,
754
+ flags: current.flags,
755
+ playback_weight: current.playback_weight,
756
+ [field]: value,
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Direct override of a clip's `time`. Bypasses {@link start} /
762
+ * {@link stop} `time` is written even if the clip is paused, and
763
+ * the {@link AnimationClipFlags.Playing} bit is left untouched.
764
+ * The host typically uses this to scrub or rewind; GPU-driven
765
+ * playback advances time on its own via the tick shader.
766
+ *
767
+ * TODO: race between `start()` and `set_time()` is non-atomic — if
768
+ * a caller does `start(id); set_time(id, phase)` and the next GPU
769
+ * tick fires before the page upload from `set_time` lands, the
770
+ * tick advances from the old `time` for one frame. RIS would be
771
+ * an atomic `start_at(id, time, ...)` that fuses both field
772
+ * writes into one record set. For phase offsets the one-frame
773
+ * skew is invisible; for scrub-and-resume UIs it can show.
774
+ *
775
+ * @param {number} clip_id
776
+ * @param {number} time
777
+ */
778
+ set_time(clip_id, time) {
779
+ assert.isNumber(time, 'time');
780
+ this.#patch_clip_field(clip_id, 'time', time);
781
+ }
782
+
783
+ /**
784
+ * Direct override of a clip's per-frame advance scalar. `1.0` =
785
+ * realtime; `0.0` effectively pauses (independent of the
786
+ * {@link AnimationClipFlags.Playing} bit); negative values play
787
+ * in reverse.
788
+ *
789
+ * @param {number} clip_id
790
+ * @param {number} rate
791
+ */
792
+ set_playback_rate(clip_id, rate) {
793
+ assert.isNumber(rate, 'rate');
794
+ this.#patch_clip_field(clip_id, 'playback_rate', rate);
795
+ }
796
+
797
+ /**
798
+ * Override a clip's contribution weight into the pose accumulator.
799
+ * `1.0` is full strength; `0.0` silences the clip without removing
800
+ * its bindings; values in between blend it against other clips
801
+ * targeting the same nodes. The apply shader pre-multiplies every
802
+ * curve-evaluated value by this scalar before atomic-adding into
803
+ * the accumulator.
804
+ *
805
+ * Normalized blends expect weights across all clips driving a
806
+ * node to sum to `1`; additive layers use weights that don't sum
807
+ * to `1`. The system doesn't enforce either — host's call.
808
+ *
809
+ * @param {number} clip_id
810
+ * @param {number} weight
811
+ */
812
+ set_playback_weight(clip_id, weight) {
813
+ assert.isNumber(weight, 'weight');
814
+ this.#patch_clip_field(clip_id, 'playback_weight', weight);
815
+ }
816
+
817
+ /**
818
+ * Add (OR) one or more flag bits onto a clip's bitfield. Bits
819
+ * already set stay set; other bits are untouched. Values come
820
+ * from {@link AnimationClipFlags} and can be combined with `|`:
821
+ * `set_flags(id, AnimationClipFlags.Loop | AnimationClipFlags.Playing)`.
822
+ *
823
+ * Counterpart {@link clear_flags} ANDs the bits out. Together
824
+ * they cover the common cases without the overwrite footgun
825
+ * the original version had (where setting Loop would silently
826
+ * clear Playing).
827
+ *
828
+ * For toggling just the playing bit, {@link start} / {@link stop}
829
+ * are equivalent shortcuts.
830
+ *
831
+ * @param {number} clip_id
832
+ * @param {number} flags bits to set
833
+ */
834
+ set_flags(clip_id, flags) {
835
+ assert.isNonNegativeInteger(flags, 'flags');
836
+
837
+ const clips_table = this.#database.get('animation_clips');
838
+ const current = clips_table.get(clip_id);
839
+
840
+ this.#patch_clip_field(clip_id, 'flags', current.flags | flags);
841
+ }
842
+
843
+ /**
844
+ * Clear (AND-NOT) one or more flag bits from a clip's bitfield.
845
+ * Bits not present in `flags` are left untouched. Symmetric
846
+ * with {@link set_flags}.
847
+ *
848
+ * @param {number} clip_id
849
+ * @param {number} flags bits to clear
850
+ */
851
+ clear_flags(clip_id, flags) {
852
+ assert.isNonNegativeInteger(flags, 'flags');
853
+
854
+ const clips_table = this.#database.get('animation_clips');
855
+ const current = clips_table.get(clip_id);
856
+
857
+ this.#patch_clip_field(clip_id, 'flags', current.flags & ~flags);
858
+ }
859
+
860
+ /**
861
+ * Begin GPU-side playback. Sets the {@link AnimationClipFlags.Playing}
862
+ * bit; preserves every other flag bit, the current `time`, the
863
+ * `playback_rate`, and so on. Calling {@link stop} and then `start`
864
+ * back-to-back should feel like a no-op modulo at most one tick's
865
+ * worth of advance.
866
+ *
867
+ * Idempotent: starting an already-playing clip is a no-op (the
868
+ * upload-buffer dedupe collapses the write).
869
+ *
870
+ * @param {number} clip_id
871
+ */
872
+ start(clip_id) {
873
+ assert.isNonNegativeInteger(clip_id, 'clip_id');
874
+
875
+ const clips_table = this.#database.get('animation_clips');
876
+ const current = clips_table.get(clip_id);
877
+
878
+ this.#patch_clip_field(clip_id, 'flags', current.flags | AnimationClipFlags.Playing);
879
+ }
880
+
881
+ /**
882
+ * Pause GPU-side playback. Clears the {@link AnimationClipFlags.Playing}
883
+ * bit; preserves every other flag bit and the current `time`.
884
+ * Symmetric with {@link start} — a stop/start pair leaves the clip
885
+ * where it was (give or take one tick of GPU time loss).
886
+ *
887
+ * Idempotent: stopping an already-stopped clip is a no-op.
888
+ *
889
+ * @param {number} clip_id
890
+ */
891
+ stop(clip_id) {
892
+ assert.isNonNegativeInteger(clip_id, 'clip_id');
893
+
894
+ const clips_table = this.#database.get('animation_clips');
895
+ const current = clips_table.get(clip_id);
896
+
897
+ this.#patch_clip_field(clip_id, 'flags', current.flags & ~AnimationClipFlags.Playing);
898
+ }
899
+
900
+ /**
901
+ * Register a skin: an ordered list of joints, each pinning a
902
+ * Node3D row that drives the joint plus the inverse-bind matrix
903
+ * that takes vertex positions from mesh space into the joint's
904
+ * bind-pose local space. The skin reserves a contiguous range of
905
+ * `joints.length` matrices in the {@link #skin_matrices_buffer}
906
+ * scratch buffer; the prep pass populates that range each frame
907
+ * with `node.global × inverse_bind` for every joint.
908
+ *
909
+ * The skin record carries no mesh-world factor. The rasterizer
910
+ * applies `mesh.transform_global` to the skinned clone vertex
911
+ * via the standard mesh transform path
912
+ * ({@link chunk_mesh_resolve_vertex_position_world}), so adding
913
+ * an `inverse(mesh.global)` factor in the skin matrix would
914
+ * double-cancel against that re-application and shrink the rig
915
+ * back to mesh-local coordinates rendered as world.
916
+ *
917
+ * Joints are split into linked-list blocks of up to
918
+ * {@link JOINTS_PER_SKIN_BLOCK} each same chain pattern used for
919
+ * bound-track groups and keyframe blocks. Each block records the
920
+ * absolute write offset for its slots, so the prep shader's
921
+ * per-thread work is one block load + a bounded inner loop with no
922
+ * indirection back through the skin record.
923
+ *
924
+ * @param {{
925
+ * joints: Array<{ node: number, inverse_bind: Float32Array | number[] }>,
926
+ * }} skin
927
+ * @returns {number} skin id (index into the `animation_skins` table)
928
+ */
929
+ add_skin(skin) {
930
+ assert.defined(skin, 'skin');
931
+ assert.isArray(skin.joints, 'skin.joints');
932
+ assert.greaterThanOrEqual(skin.joints.length, 1, 'skin must have at least one joint');
933
+
934
+ const joints = skin.joints;
935
+ const joint_count = joints.length;
936
+
937
+ // Reserve the matrix range. Each block bakes in its absolute
938
+ // slot offset (skin_base + chain_position * JOINTS_PER_SKIN_BLOCK)
939
+ // so the prep shader writes directly without dereferencing
940
+ // the skin row for the offset; the base is also stashed on
941
+ // the CPU map so the skinning-binding upload can look it up
942
+ // without reading the GPU back.
943
+ const skin_matrix_offset = this.#next_skin_matrix_offset;
944
+ this.#next_skin_matrix_offset += joint_count;
945
+ this.#ensure_skin_matrices_capacity(this.#next_skin_matrix_offset);
946
+
947
+ const skins_table = this.#database.get('animation_skins');
948
+ const blocks_table = this.#database.get('animation_skin_joint_blocks');
949
+
950
+ // Reserve the skin row up front. The head pointer is patched
951
+ // in once the chain exists. NULL placeholder — 0 is a valid
952
+ // block row id (the first allocation in an empty table).
953
+ const skin_id = skins_table.add({
954
+ joint_block_head: ANIMATION_SKIN_JOINT_BLOCK_NULL,
955
+ joint_count,
956
+ });
957
+ this.#skin_matrix_offsets.set(skin_id, skin_matrix_offset);
958
+
959
+ const head_block_id = add_linked_chain(
960
+ blocks_table,
961
+ joints,
962
+ JOINTS_PER_SKIN_BLOCK,
963
+ (start, count, next) => {
964
+ const inline_joints = new Array(JOINTS_PER_SKIN_BLOCK);
965
+ for (let j = 0; j < JOINTS_PER_SKIN_BLOCK; j++) {
966
+ if (j < count) {
967
+ const joint = joints[start + j];
968
+
969
+ assert.isNonNegativeInteger(joint.node, `skin.joints[${start + j}].node`);
970
+ assert.defined(joint.inverse_bind, `skin.joints[${start + j}].inverse_bind`);
971
+
972
+ inline_joints[j] = joint;
973
+ } else {
974
+ inline_joints[j] = ZERO_SKIN_JOINT;
975
+ }
976
+ }
977
+ return {
978
+ joints: inline_joints,
979
+ count,
980
+ next,
981
+ skin_matrix_offset: skin_matrix_offset + start,
982
+ };
983
+ }
984
+ );
985
+
986
+ // Patch the head pointer now that the chain exists. Re-`set`
987
+ // the whole record because the table API doesn't expose
988
+ // per-field updates.
989
+ skins_table.set(skin_id, {
990
+ joint_block_head: head_block_id,
991
+ joint_count,
992
+ });
993
+
994
+ return skin_id;
995
+ }
996
+
997
+ /**
998
+ * High-level skin registration: consumes a {@link Skin} (CPU data
999
+ * with Node3D joint references and a flat inverse-bind matrix
1000
+ * array), allocates a GPU skin record, and binds every entry in
1001
+ * `skin.meshes` through the skinning context.
1002
+ *
1003
+ * **Eventual consistency.** The `skin_id` is allocated and
1004
+ * returned synchronously — callers can immediately pass it to
1005
+ * state mutators or {@link unregister_skin}. If any joint's
1006
+ * Node3D hasn't been added to a scene yet (no `id_mapping` row),
1007
+ * the joint-block chain that drives the deformation is queued
1008
+ * and built on the next {@link update} that sees all joints
1009
+ * resolved. The skin's table row uses a placeholder
1010
+ * `joint_block_head` until then observationally indistinguishable
1011
+ * from "skin with no joints driving it" (no deformation runs).
1012
+ *
1013
+ * Mesh→skin bindings register immediately via
1014
+ * {@link GPUMeshSkinningContext#bind}; the skinning context
1015
+ * already tolerates pre-scene-build registration. Each mesh also
1016
+ * gets its {@link SkinnedMesh#skin} reference and a fresh set of
1017
+ * bounds off it, synchronously that part does not wait for
1018
+ * `id_mapping`, because it reads the joints' `transform_global`
1019
+ * rather than any GPU row.
1020
+ *
1021
+ * @param {Skin} skin
1022
+ * @returns {number} skin id (index into the `animation_skins` table)
1023
+ */
1024
+ register_skin(skin) {
1025
+ assert.defined(skin, 'skin');
1026
+ assert.equal(skin.isSkin, true, 'skin.isSkin !== true');
1027
+ assert.greaterThanOrEqual(skin.meshes.length, 1, 'skin.meshes must contain at least one mesh');
1028
+
1029
+ const joint_count = skin.joints.length;
1030
+ const inverse_bind_matrices = skin.inverse_bind_matrices;
1031
+
1032
+ // Reserve the matrix range up front — block records carry
1033
+ // their absolute slot offset, so the prep shader writes
1034
+ // directly. The base also lands on #skin_matrix_offsets so
1035
+ // skinning-binding upload can look it up.
1036
+ const skin_matrix_offset = this.#next_skin_matrix_offset;
1037
+ this.#next_skin_matrix_offset += joint_count;
1038
+ this.#ensure_skin_matrices_capacity(this.#next_skin_matrix_offset);
1039
+
1040
+ // Reserve the skin row immediately with a placeholder head
1041
+ // pointer. State mutators and the public id are usable from
1042
+ // this point on; the joint chain gets patched in once
1043
+ // id_mapping has every joint (possibly synchronously below,
1044
+ // possibly deferred via the pending queue).
1045
+ const skins_table = this.#database.get('animation_skins');
1046
+ const skin_id = skins_table.add({
1047
+ // NULL placeholder — 0 is a valid block row id
1048
+ joint_block_head: ANIMATION_SKIN_JOINT_BLOCK_NULL,
1049
+ joint_count,
1050
+ });
1051
+ this.#skin_matrix_offsets.set(skin_id, skin_matrix_offset);
1052
+
1053
+ // Bind every mesh into the skinning context. Idempotent and
1054
+ // pre-build tolerant the binding upload defers until
1055
+ // id_mapping is ready.
1056
+ //
1057
+ // Each mesh also takes a reference to the CPU-side skin and
1058
+ // re-derives its bounds off it. From here on the skin's
1059
+ // formula is what places the mesh's vertices, and `Mesh`'s
1060
+ // composition rest-pose geometry under `transform_global`
1061
+ // is the wrong one, because the skinning pass cancels that
1062
+ // transform back out (see {@link SkinnedMesh#updateBoundsBasic}).
1063
+ // Nothing else would notice: bounds are refreshed by
1064
+ // `updateMatrices`, and a character does not move again just
1065
+ // because it finished loading.
1066
+ // Before any of that: put the meshes into the frame this skin actually deforms in. The
1067
+ // skinning pass writes each clone's vertices in `inverse(mesh.global)` space, so
1068
+ // `mesh.global` is that frame — and glTF says a skinned mesh's node transform is ignored,
1069
+ // which makes the frame the engine's to choose rather than the exporter's to get right.
1070
+ // Ahead of the bind loop because it moves the mesh, and the bounds published below have to
1071
+ // be the ones that survive. See `SKINNING_FRAME_PLAN.md`.
1072
+ this.#normalize_skin_frame(skin);
1073
+
1074
+ this.#skins_by_id.set(skin_id, skin);
1075
+
1076
+ for (const clip of this.#scaling_clips.values()) {
1077
+ this.#report_scale_conflicts(skin, clip);
1078
+ }
1079
+
1080
+ const skinning = this.#scene_context.skinning;
1081
+ const meshes = skin.meshes;
1082
+ for (let i = 0; i < meshes.length; i++) {
1083
+ const mesh = meshes[i];
1084
+
1085
+ skinning.bind(mesh, skin_id);
1086
+
1087
+ mesh.skin = skin;
1088
+ mesh.updateBoundsBasic();
1089
+
1090
+ // The row the scene context published for this mesh holds the pre-skin bounds, and
1091
+ // nothing about binding a skin looks like movement — `updateBoundsBasic` is not
1092
+ // `updateMatrices` and does not touch the version the sync compares against. Without
1093
+ // this the corrected box sits on the CPU object and never reaches the table.
1094
+ mesh.needsUpdate = true;
1095
+ }
1096
+
1097
+ // Try to complete now. If id_mapping is ready for every
1098
+ // joint, the chain gets built and patched in. Otherwise the
1099
+ // skin lands on #pending_skins and the next update() drain
1100
+ // retries.
1101
+ const pending = {
1102
+ skin_id,
1103
+ skin,
1104
+ skin_matrix_offset,
1105
+ inverse_bind_matrices,
1106
+ };
1107
+ if (!this.#try_complete_skin(pending)) {
1108
+ this.#pending_skins.push(pending);
1109
+ }
1110
+
1111
+ return skin_id;
1112
+ }
1113
+
1114
+ /**
1115
+ * Put a skin's meshes into the frame it deforms in, and say so when they cannot be.
1116
+ *
1117
+ * The correction itself is {@link skin_normalize_mesh_frame}, which is where the argument lives.
1118
+ * This is the reporting: an asset that renders wrong should say which asset and by how much, on
1119
+ * the frame it is registered, rather than reaching a person as a screenshot of soup.
1120
+ *
1121
+ * @param {Skin} skin
1122
+ */
1123
+ #normalize_skin_frame(skin) {
1124
+ const report = skin_normalize_mesh_frame(skin);
1125
+
1126
+ // a glTF skin's `name` is optional and often absent; the mesh it deforms is what a person
1127
+ // would recognise, and is what the asset's own hierarchy calls it
1128
+ const label = skin.name !== '' ? skin.name : (skin.meshes[0]?.name ?? '<unnamed>');
1129
+
1130
+ if (report.unwalked.length > 0) {
1131
+ console.error(
1132
+ `skin '${label}': ${report.unwalked.length} of its nodes have never been walked — `
1133
+ + `'${report.unwalked[0].name}' among them — so their world transforms are the `
1134
+ + `identity by default rather than by derivation. Call updateMatrices on the scene `
1135
+ + `before registering. Nothing was moved; the deformation will be wrong.`
1136
+ );
1137
+
1138
+ return;
1139
+ }
1140
+
1141
+ if (report.frame_defect > 0.001) {
1142
+ console.error(
1143
+ `skin '${label}': no frame can deform it — `
1144
+ + `inverse(frame) × joint.global × inverse_bind is ${report.frame_defect.toFixed(4)} `
1145
+ + `away from rigid at its own bind pose, and a dual quaternion carries a rotation and `
1146
+ + `a translation and nothing else. The rest pose has a non-uniform scale or a shear `
1147
+ + `in it. Rendering will be wrong; see SKINNING_FRAME_PLAN.md §4.`
1148
+ );
1149
+
1150
+ return;
1151
+ }
1152
+
1153
+ if (report.residual > 0.001) {
1154
+ console.error(
1155
+ `skin '${label}': its mesh frame did not land ${report.residual.toFixed(6)} `
1156
+ + `left over. A correction carrying shear does not survive a Transform64's TRS `
1157
+ + `components. Rendering will be wrong; see SKINNING_FRAME_PLAN.md §3.`
1158
+ );
1159
+
1160
+ return;
1161
+ }
1162
+
1163
+ for (const mesh of report.obstructed) {
1164
+ console.warn(
1165
+ `skin '${label}': mesh node '${mesh.name}' carries the skin's frame now, and its `
1166
+ + `${mesh.children.length} child node(s) inherit it. Sockets belong on bones.`
1167
+ );
1168
+ }
1169
+
1170
+ if (report.corrected > 0) {
1171
+ // verbose rather than a warning: this is a legal asset shape, and a crowd of them would
1172
+ // otherwise be a crowd of log lines
1173
+ console.debug(
1174
+ `skin '${label}': ${report.corrected} mesh frame(s) moved onto the skin's bind `
1175
+ + `transform, absorbing ${report.absorbed.toFixed(4)} of non-rigidity the skinning `
1176
+ + `pass could not have carried.`
1177
+ );
1178
+ }
1179
+ }
1180
+
1181
+ /**
1182
+ * Say so when a clip scales a skin's joints out from under the frame it is deformed in.
1183
+ *
1184
+ * The frame {@link skin_normalize_mesh_frame} chooses is fixed at registration, and it is sound
1185
+ * because the joints and the mesh share their scaling ancestors. A scale that reaches only some
1186
+ * of them breaks that every frame it plays, on the GPU, where nothing can see it —
1187
+ * {@link skin_clip_scale_conflicts} is that question asked off the clip's own curves instead.
1188
+ *
1189
+ * Nothing is refused: the clip is legal animation and the skin is a legal skin, and it is their
1190
+ * combination the skinning pass cannot carry. Refusing either would be picking one to blame.
1191
+ *
1192
+ * @param {Skin} skin
1193
+ * @param {ShadeAnimationClip} clip
1194
+ */
1195
+ #report_scale_conflicts(skin, clip) {
1196
+ const conflicts = skin_clip_scale_conflicts(skin, clip);
1197
+
1198
+ if (conflicts.length === 0) {
1199
+ return;
1200
+ }
1201
+
1202
+ const skin_label = skin.name !== '' ? skin.name : (skin.meshes[0]?.name ?? '<unnamed>');
1203
+
1204
+ const nodes = conflicts.map(conflict => conflict.node.name || '<unnamed>').join(', ');
1205
+
1206
+ const uniform = conflicts.every(conflict => conflict.uniform);
1207
+
1208
+ console.error(
1209
+ `clip '${clip.name}' scales ${conflicts.length} node(s) of skin '${skin_label}' `
1210
+ + `${nodes} which reach its joints but not its mesh, so the scale enters the joint `
1211
+ + `matrices and not the frame they are folded against. A dual quaternion cannot carry `
1212
+ + `it and no choice of frame fixes it: the deformation will be wrong while this clip `
1213
+ + `plays. `
1214
+ + (uniform
1215
+ ? `The scale is uniform, which SKINNING_FRAME_PLAN.md §4 has a design for.`
1216
+ : `The scale is non-uniform, which no dual quaternion holds — §4's linear-blend `
1217
+ + `path or an import-time refusal.`)
1218
+ );
1219
+ }
1220
+
1221
+ /**
1222
+ * Attempt to build the joint-block chain for a pending skin and
1223
+ * patch its head pointer onto the skin row. Returns `true` when
1224
+ * every joint Node3D has resolved to a transforms-table row
1225
+ * (and the chain was built); `false` when at least one joint is
1226
+ * still missing.
1227
+ *
1228
+ * @param {{
1229
+ * skin_id: number,
1230
+ * skin: Skin,
1231
+ * skin_matrix_offset: number,
1232
+ * inverse_bind_matrices: Float32Array,
1233
+ * }} pending
1234
+ * @returns {boolean}
1235
+ */
1236
+ #try_complete_skin(pending) {
1237
+ const id_mapping = this.#scene_context.id_mapping;
1238
+ const joint_nodes = pending.skin.joints;
1239
+ const joint_count = joint_nodes.length;
1240
+ const inverse_bind_matrices = pending.inverse_bind_matrices;
1241
+ const skin_matrix_offset = pending.skin_matrix_offset;
1242
+ const skin_id = pending.skin_id;
1243
+
1244
+ // All-or-nothing: a single missing row leaves the chain
1245
+ // unbuilt — partial chains aren't valid (the prep shader
1246
+ // walks every block).
1247
+ const joints = new Array(joint_count);
1248
+ for (let i = 0; i < joint_count; i++) {
1249
+ const node = joint_nodes[i];
1250
+ const row = id_mapping.get(node.id);
1251
+ if (row === undefined) return false;
1252
+
1253
+ joints[i] = {
1254
+ node: row,
1255
+ // Per-joint slice of the flat matrix array. The
1256
+ // marshaller copies these bytes into the skin-joint
1257
+ // block record; sub-array views are fine — they
1258
+ // share the underlying buffer but write_struct_value
1259
+ // reads by length, not by offset.
1260
+ inverse_bind: inverse_bind_matrices.subarray(i * 16, (i + 1) * 16),
1261
+ };
1262
+ }
1263
+
1264
+ const blocks_table = this.#database.get('animation_skin_joint_blocks');
1265
+ const head_block_id = add_linked_chain(
1266
+ blocks_table,
1267
+ joints,
1268
+ JOINTS_PER_SKIN_BLOCK,
1269
+ (start, count, next) => {
1270
+ const inline_joints = new Array(JOINTS_PER_SKIN_BLOCK);
1271
+ for (let j = 0; j < JOINTS_PER_SKIN_BLOCK; j++) {
1272
+ inline_joints[j] = j < count ? joints[start + j] : ZERO_SKIN_JOINT;
1273
+ }
1274
+ return {
1275
+ joints: inline_joints,
1276
+ count,
1277
+ next,
1278
+ skin_matrix_offset: skin_matrix_offset + start,
1279
+ };
1280
+ }
1281
+ );
1282
+
1283
+ // Patch the head pointer onto the already-allocated skin
1284
+ // row. Read-modify-write so any other fields the user might
1285
+ // have changed via mutators (none today, but future-proof)
1286
+ // are preserved.
1287
+ const skins_table = this.#database.get('animation_skins');
1288
+ const current = skins_table.get(skin_id);
1289
+ skins_table.set(skin_id, {
1290
+ joint_block_head: head_block_id,
1291
+ joint_count: current.joint_count,
1292
+ });
1293
+
1294
+ return true;
1295
+ }
1296
+
1297
+ /**
1298
+ * High-level clip registration: consumes a {@link ShadeAnimationClip}
1299
+ * (CPU data with Node3D targets and per-component curves), uploads
1300
+ * curves and tracks, and creates the GPU clip record.
1301
+ *
1302
+ * **Eventual consistency.** The `clip_id` is allocated and
1303
+ * returned synchronously — callers can immediately call
1304
+ * {@link start} / {@link stop} / {@link set_time} /
1305
+ * {@link set_playback_rate} / {@link set_flags} / {@link clear_flags}
1306
+ * / {@link set_playback_weight} / {@link unregister_clip}. If
1307
+ * any channel target's Node3D hasn't been added to a scene yet
1308
+ * (no `id_mapping` row), the bindings chain that drives the
1309
+ * apply pass is queued and built on the next {@link update}
1310
+ * that sees all targets resolved. The clip's table row uses a
1311
+ * placeholder `bound_track_group_head` until then apply
1312
+ * produces no contributions, so the clip is observationally
1313
+ * inert (still subject to time advancement via the tick shader
1314
+ * if `Playing` is set).
1315
+ *
1316
+ * Curves and tracks are interned synchronously since they're
1317
+ * pure data — no `id_mapping` dependency.
1318
+ *
1319
+ * @param {ShadeAnimationClip} clip
1320
+ * @returns {number} clip id
1321
+ */
1322
+ register_clip(clip) {
1323
+ assert.defined(clip, 'clip');
1324
+ assert.equal(clip.isShadeAnimationClip, true, 'clip.isShadeAnimationClip !== true');
1325
+
1326
+ const channels = clip.channels;
1327
+
1328
+ // Intern every channel's curves + track now. None of this
1329
+ // depends on id_mapping — pure data uploads.
1330
+ //
1331
+ // Each curve goes through #intern_curve so two channels
1332
+ // referencing the same AnimationCurve instance share one
1333
+ // GPU upload. With 324 dancers retargeted at the same base
1334
+ // clip this turns 324 × 195 channel uploads into 1 (the
1335
+ // curves are read-only animation data — identical bytes
1336
+ // wherever they're referenced).
1337
+ const pending_channels = new Array(channels.length);
1338
+ for (let i = 0; i < channels.length; i++) {
1339
+ const channel = channels[i];
1340
+ const curves = channel.curves;
1341
+ const track_channels = {};
1342
+ if (curves.x !== undefined) track_channels.x = this.#intern_curve(curves.x);
1343
+ if (curves.y !== undefined) track_channels.y = this.#intern_curve(curves.y);
1344
+ if (curves.z !== undefined) track_channels.z = this.#intern_curve(curves.z);
1345
+ if (curves.w !== undefined) track_channels.w = this.#intern_curve(curves.w);
1346
+
1347
+ pending_channels[i] = {
1348
+ track: this.#intern_track(track_channels),
1349
+ target: channel.target,
1350
+ property: channel.property,
1351
+ };
1352
+ }
1353
+
1354
+ // Reserve the clip row immediately with a placeholder head
1355
+ // pointer. State mutators and the public id are usable from
1356
+ // this point on; the bindings chain gets patched in once
1357
+ // id_mapping has every target. `time` defaults to 0 (the
1358
+ // playback offset relative to `time_start`; the apply
1359
+ // shader does `effective_time = clip.time + clip.time_start`).
1360
+ const clips_table = this.#database.get('animation_clips');
1361
+ const clip_id = clips_table.add({
1362
+ time: 0,
1363
+ time_start: clip.start_time,
1364
+ time_end: clip.end_time,
1365
+ bound_track_group_head: ANIMATION_BOUND_TRACK_GROUP_NULL,
1366
+ playback_rate: 1,
1367
+ flags: 0,
1368
+ playback_weight: 1,
1369
+ });
1370
+
1371
+ const pending = { clip_id, channels: pending_channels };
1372
+ if (!this.#try_complete_clip(pending)) {
1373
+ this.#pending_clips.push(pending);
1374
+ }
1375
+
1376
+ /*
1377
+ Asked cheaply first: a clip with no scale channel at all — which is every clip a character
1378
+ normally carries — is dismissed in one pass over its channels and remembered by nothing. Only
1379
+ a clip that scales something is worth holding against the skins, now and as more arrive.
1380
+ */
1381
+ if (clip_scale_channels(clip).length > 0) {
1382
+ this.#scaling_clips.set(clip_id, clip);
1383
+
1384
+ for (const skin of this.#skins_by_id.values()) {
1385
+ this.#report_scale_conflicts(skin, clip);
1386
+ }
1387
+ }
1388
+
1389
+ return clip_id;
1390
+ }
1391
+
1392
+ /**
1393
+ * Attempt to build the bindings chain for a pending clip and
1394
+ * patch its head pointer onto the clip row. Returns `true` when
1395
+ * every channel target has resolved to a transforms-table row
1396
+ * (and the chain was built); `false` when at least one is still
1397
+ * missing.
1398
+ *
1399
+ * @param {{
1400
+ * clip_id: number,
1401
+ * channels: Array<{ track: number, target: Node3D, property: number }>,
1402
+ * }} pending
1403
+ * @returns {boolean}
1404
+ */
1405
+ #try_complete_clip(pending) {
1406
+ const id_mapping = this.#scene_context.id_mapping;
1407
+ const channels = pending.channels;
1408
+ const clip_id = pending.clip_id;
1409
+
1410
+ const bindings = new Array(channels.length);
1411
+ for (let i = 0; i < channels.length; i++) {
1412
+ const ch = channels[i];
1413
+ const instance = id_mapping.get(ch.target.id);
1414
+ if (instance === undefined) return false;
1415
+
1416
+ bindings[i] = {
1417
+ track: ch.track,
1418
+ instance,
1419
+ property: ch.property,
1420
+ };
1421
+ }
1422
+
1423
+ const groups_table = this.#database.get('animation_bound_track_groups');
1424
+ const head_group_id = add_linked_chain(
1425
+ groups_table,
1426
+ bindings,
1427
+ TRACKS_PER_GROUP_NODE,
1428
+ (start, count, next) => {
1429
+ const inline_bindings = new Array(TRACKS_PER_GROUP_NODE);
1430
+ for (let j = 0; j < TRACKS_PER_GROUP_NODE; j++) {
1431
+ inline_bindings[j] = j < count ? bindings[start + j] : ZERO_BINDING;
1432
+ }
1433
+ return { bindings: inline_bindings, count, next, clip: clip_id };
1434
+ }
1435
+ );
1436
+
1437
+ // Patch the head pointer onto the already-allocated clip
1438
+ // row. Read-modify-write preserves any user-driven state
1439
+ // changes (set_time, set_flags, etc.) made between
1440
+ // register_clip and the drain.
1441
+ this.#patch_clip_field(clip_id, 'bound_track_group_head', head_group_id);
1442
+
1443
+ return true;
1444
+ }
1445
+
1446
+ /**
1447
+ * Drop a clip from the manager. Walks the bindings chain and
1448
+ * removes every group-node + the clip row. Tolerates the clip
1449
+ * still being pending (drains it from the queue without GPU
1450
+ * touches besides the original row removal).
1451
+ *
1452
+ * Curves and tracks the clip referenced stay alive — they're
1453
+ * shared via {@link #intern_curve} / {@link #intern_track} and
1454
+ * may be in use by other clips. Removing them would need
1455
+ * reference-counting the manager doesn't carry.
1456
+ *
1457
+ * Safe to call repeatedly on the same id (subsequent calls are
1458
+ * no-ops once the row is gone).
1459
+ *
1460
+ * @param {number} clip_id
1461
+ */
1462
+ unregister_clip(clip_id) {
1463
+ assert.isNonNegativeInteger(clip_id, 'clip_id');
1464
+
1465
+ // Drop from pending queue if present (skip-and-keep-others
1466
+ // pattern is cheaper than splice for the rare case of
1467
+ // dozens-of-pending; if pending depth grows this can be
1468
+ // revisited).
1469
+ for (let i = this.#pending_clips.length - 1; i >= 0; i--) {
1470
+ if (this.#pending_clips[i].clip_id === clip_id) {
1471
+ this.#pending_clips.splice(i, 1);
1472
+ }
1473
+ }
1474
+
1475
+ const clips_table = this.#database.get('animation_clips');
1476
+ const groups_table = this.#database.get('animation_bound_track_groups');
1477
+
1478
+ const clip_record = clips_table.get(clip_id);
1479
+ if (clip_record === undefined) return;
1480
+
1481
+ // Walk the bindings chain and remove every node. The chain
1482
+ // may be empty (placeholder head NULL) if the clip is still
1483
+ // pending or has been registered then drained but never
1484
+ // completed; both cases skip the loop.
1485
+ let block_id = clip_record.bound_track_group_head;
1486
+ while (block_id !== ANIMATION_BOUND_TRACK_GROUP_NULL) {
1487
+ const block = groups_table.get(block_id);
1488
+ const next_id = block !== undefined ? block.next : ANIMATION_BOUND_TRACK_GROUP_NULL;
1489
+ groups_table.remove(block_id);
1490
+ block_id = next_id;
1491
+ }
1492
+
1493
+ clips_table.remove(clip_id);
1494
+
1495
+ this.#scaling_clips.delete(clip_id);
1496
+ }
1497
+
1498
+ /**
1499
+ * Drop a skin from the manager. Walks the joint-block chain and
1500
+ * removes every block + the skin row, and tears down every
1501
+ * skinning binding pointing at this skin (so the skinning
1502
+ * context doesn't try to read the now-freed skin's matrix
1503
+ * offset on its next upload).
1504
+ *
1505
+ * The skin's reserved matrix range stays allocated — compaction
1506
+ * isn't supported in the MVP. Re-registering after an
1507
+ * `unregister_skin` therefore costs new matrix slots; long-
1508
+ * running spawn/despawn loops accumulate wasted slots.
1509
+ *
1510
+ * Tolerates the skin still being pending. Safe to call
1511
+ * repeatedly on the same id.
1512
+ *
1513
+ * @param {number} skin_id
1514
+ */
1515
+ unregister_skin(skin_id) {
1516
+ assert.isNonNegativeInteger(skin_id, 'skin_id');
1517
+
1518
+ this.#skins_by_id.delete(skin_id);
1519
+
1520
+ for (let i = this.#pending_skins.length - 1; i >= 0; i--) {
1521
+ if (this.#pending_skins[i].skin_id === skin_id) {
1522
+ this.#pending_skins.splice(i, 1);
1523
+ }
1524
+ }
1525
+
1526
+ // Tear down skinning bindings first so the skinning
1527
+ // context's next upload doesn't reference a freed skin row
1528
+ // (its upload path looks up skin_matrix_offset via
1529
+ // get_skin_matrix_offset, which would assert post-removal).
1530
+ this.#scene_context.skinning.unbind_all_by_skin(skin_id);
1531
+
1532
+ const skins_table = this.#database.get('animation_skins');
1533
+ const blocks_table = this.#database.get('animation_skin_joint_blocks');
1534
+
1535
+ const skin_record = skins_table.get(skin_id);
1536
+ if (skin_record === undefined) {
1537
+ this.#skin_matrix_offsets.delete(skin_id);
1538
+ return;
1539
+ }
1540
+
1541
+ // Walk the joint-block chain, terminating on the NULL
1542
+ // sentinel (also the placeholder for a not-yet-patched head).
1543
+ // Note: 0 is a VALID block row id — the first single-block
1544
+ // skin registered into an empty table gets head 0, and
1545
+ // treating it as a placeholder would leak its block, leaving
1546
+ // the prep shader processing it against a recycled
1547
+ // transforms-table row every frame.
1548
+ let block_id = skin_record.joint_block_head;
1549
+ while (block_id !== ANIMATION_SKIN_JOINT_BLOCK_NULL) {
1550
+ const block = blocks_table.get(block_id);
1551
+ const next_id = block !== undefined ? block.next : ANIMATION_SKIN_JOINT_BLOCK_NULL;
1552
+ blocks_table.remove(block_id);
1553
+ block_id = next_id;
1554
+ }
1555
+
1556
+ skins_table.remove(skin_id);
1557
+ this.#skin_matrix_offsets.delete(skin_id);
1558
+ }
1559
+
1560
+ /**
1561
+ * Cached wrapper around {@link add_curve}: one upload per distinct curve, where distinct means
1562
+ * distinct *content*.
1563
+ *
1564
+ * The instance cache answers first the common case inside one clip, and free. Otherwise the
1565
+ * curve's keys are hashed and held against every uploaded curve with that hash; a match on
1566
+ * every key, tangents included, reuses that upload, and only a curve nobody has uploaded before
1567
+ * goes through the low-level path. Either way the instance is remembered, so the next look-up
1568
+ * of it is the free one.
1569
+ *
1570
+ * @param {AnimationCurve} curve
1571
+ * @returns {number} curve id (index into the `animation_curves` table)
1572
+ */
1573
+ #intern_curve(curve) {
1574
+ const cached = this.#curve_id_cache.get(curve);
1575
+
1576
+ if (cached !== undefined) {
1577
+ return cached;
1578
+ }
1579
+
1580
+ const hash = curve.hash();
1581
+
1582
+ let bucket = this.#curve_id_by_content.get(hash);
1583
+
1584
+ if (bucket === undefined) {
1585
+ bucket = [];
1586
+
1587
+ this.#curve_id_by_content.set(hash, bucket);
1588
+ } else {
1589
+ for (const entry of bucket) {
1590
+ if (entry.curve.equals(curve)) {
1591
+ this.#curve_id_cache.set(curve, entry.id);
1592
+
1593
+ return entry.id;
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ const id = this.add_curve(curve);
1599
+
1600
+ this.#curve_id_cache.set(curve, id);
1601
+
1602
+ bucket.push({ curve, id });
1603
+
1604
+ return id;
1605
+ }
1606
+
1607
+ /**
1608
+ * Cache-keyed wrapper around {@link add_track}. Two channels that
1609
+ * end up with the same (x, y, z, w, mask) — typical when their
1610
+ * source curves are shared via {@link #intern_curve} — collapse
1611
+ * to one track row instead of one row per channel.
1612
+ *
1613
+ * @param {{ x?: number, y?: number, z?: number, w?: number }} channels
1614
+ * @returns {number} track id
1615
+ */
1616
+ #intern_track(channels) {
1617
+ // Pack into a deterministic string key. -1 sentinel for
1618
+ // missing channels keeps the (x=undefined) and (x=0) cases
1619
+ // distinct.
1620
+ const cx = channels.x ?? -1;
1621
+ const cy = channels.y ?? -1;
1622
+ const cz = channels.z ?? -1;
1623
+ const cw = channels.w ?? -1;
1624
+ const key = `${cx},${cy},${cz},${cw}`;
1625
+
1626
+ const cached = this.#track_id_cache.get(key);
1627
+ if (cached !== undefined) {
1628
+ return cached;
1629
+ }
1630
+ const id = this.add_track(channels);
1631
+ this.#track_id_cache.set(key, id);
1632
+ return id;
1633
+ }
1634
+
1635
+ /**
1636
+ * Grow the skinning-matrix scratch buffer if `required_count`
1637
+ * exceeds current capacity. Doubles on grow until the requirement
1638
+ * fits. The buffer's contents are scratch (rewritten every frame by
1639
+ * the prep pass), so growth doesn't need to copy old data — but it
1640
+ * does invalidate the previous {@link #skin_matrices_buffer}
1641
+ * reference, which is why callers re-fetch through the getter.
1642
+ *
1643
+ * @param {number} required_count number of matrices that must fit
1644
+ */
1645
+ #ensure_skin_matrices_capacity(required_count) {
1646
+ if (required_count <= this.#skin_matrices_capacity) {
1647
+ return;
1648
+ }
1649
+
1650
+ let new_capacity = Math.max(this.#skin_matrices_capacity, SKIN_MATRICES_INITIAL_CAPACITY);
1651
+ while (new_capacity < required_count) {
1652
+ new_capacity *= 2;
1653
+ }
1654
+
1655
+ // Grow both ping-pong buffers in lockstep — they always share
1656
+ // capacity. After grow both are zero-initialised, so the next
1657
+ // prep dispatch needs to seed "previous" from "current"
1658
+ // (otherwise velocity would interpolate against zero
1659
+ // matrices). The flag below tells dispatch_skin_matrix_prep
1660
+ // to issue that copy.
1661
+ for (let i = 0; i < 2; i++) {
1662
+ if (this.#skin_matrices_buffers[i] !== null) {
1663
+ this.#skin_matrices_buffers[i].destroy();
1664
+ }
1665
+ this.#skin_matrices_buffers[i] = this.#device.createBuffer({
1666
+ label: `${this.#label}/SkinMatrices/${i}`,
1667
+ size: new_capacity * SKIN_MATRIX_BYTES,
1668
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
1669
+ });
1670
+ }
1671
+ this.#skin_matrices_capacity = new_capacity;
1672
+ this.#first_frame_skin_matrices = true;
1673
+ }
1674
+
1675
+ /**
1676
+ * Flush all pending uploads (curves, keyframe blocks, tracks, clips,
1677
+ * bound-track groups, skins, skin joint blocks) into the GPU
1678
+ * database. Call once per frame, before any shader that reads from
1679
+ * `database.buffer`.
1680
+ *
1681
+ * Also drains the pending-registration queues: skins and clips
1682
+ * whose dependencies (joint Node3D rows, channel-target rows)
1683
+ * weren't ready at register time get one shot per `update` to
1684
+ * complete. Once `id_mapping` has the missing rows, the
1685
+ * deferred chain build runs and the registration becomes
1686
+ * observationally complete the next-frame `database.update`
1687
+ * picks up the patched head pointer.
1688
+ *
1689
+ * Does not touch the skin-matrices ping-pong buffers — those
1690
+ * are scratch populated each frame by
1691
+ * {@link dispatch_skin_matrix_prep}, not host data.
1692
+ *
1693
+ * @param {ShadeGPUCommandContext} cmd_ctx
1694
+ */
1695
+ update(cmd_ctx) {
1696
+ this.#drain_pending_skins();
1697
+ this.#drain_pending_clips();
1698
+ this.#database.update(cmd_ctx);
1699
+ }
1700
+
1701
+ #drain_pending_skins() {
1702
+ // Walk back-to-front so successful completions can splice
1703
+ // without disturbing the iteration's remaining indices.
1704
+ for (let i = this.#pending_skins.length - 1; i >= 0; i--) {
1705
+ if (this.#try_complete_skin(this.#pending_skins[i])) {
1706
+ this.#pending_skins.splice(i, 1);
1707
+ }
1708
+ }
1709
+ }
1710
+
1711
+ #drain_pending_clips() {
1712
+ for (let i = this.#pending_clips.length - 1; i >= 0; i--) {
1713
+ if (this.#try_complete_clip(this.#pending_clips[i])) {
1714
+ this.#pending_clips.splice(i, 1);
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ /**
1720
+ * Run the per-frame skin-matrix-prep dispatch and advance the
1721
+ * ping-pong. Replaces the manual `shader_skin_matrix_prep.dispatch`
1722
+ * the prototypes used to call — wrapping it here is how we get
1723
+ * correct ping-pong behaviour across frames without leaking the
1724
+ * buffer-role bookkeeping into every caller.
1725
+ *
1726
+ * No-op when no skins are registered (`group_count_x === 0`).
1727
+ *
1728
+ * Per-frame ordering:
1729
+ * 1. Flip {@link #skin_matrices_current_index} — the buffer that
1730
+ * was last frame's "current" is about to be overwritten with
1731
+ * this frame's matrices; the other buffer (last frame's prev)
1732
+ * becomes this frame's "previous" implicit data source for
1733
+ * {@link prev_skin_matrices_buffer}.
1734
+ * 2. Wait — clarify: we want last frame's "current" to BECOME
1735
+ * this frame's "previous". So flipping current_index moves
1736
+ * the role tag the correct way: what was current[i] becomes
1737
+ * previous (= buffers[1 - i]), and current[1 - i] is the
1738
+ * write target.
1739
+ * 3. Dispatch the prep shader into the new current.
1740
+ * 4. On the very first frame (or after a grow), copy current →
1741
+ * previous so the first velocity reads see matching matrices
1742
+ * and produce zero velocity.
1743
+ *
1744
+ * @param {ShadeGPUCommandContext} cmd_ctx
1745
+ */
1746
+ dispatch_skin_matrix_prep(cmd_ctx) {
1747
+ const groups = this.#database
1748
+ .get('animation_skin_joint_blocks')
1749
+ .dispatch_group_count(SKIN_MATRIX_PREP_WORKGROUP_SIZE);
1750
+ if (groups === 0) {
1751
+ return;
1752
+ }
1753
+
1754
+ // Flip the ping-pong: what was "current" last frame is no
1755
+ // longer current, but it still holds valid matrices — and
1756
+ // through the role flip it becomes this frame's
1757
+ // `prev_skin_matrices_buffer`. The buffer we're flipping TO
1758
+ // (the new "current") held two-frames-old matrices, about to
1759
+ // be overwritten.
1760
+ this.#skin_matrices_current_index = 1 - this.#skin_matrices_current_index;
1761
+
1762
+ const current = this.#skin_matrices_buffers[this.#skin_matrices_current_index];
1763
+ const previous = this.#skin_matrices_buffers[1 - this.#skin_matrices_current_index];
1764
+
1765
+ shader_skin_matrix_prep.dispatch({
1766
+ encoder: cmd_ctx,
1767
+ group_count_x: groups,
1768
+ bindings: {
1769
+ animation_database: this.#database.buffer,
1770
+ scene_database: this.#scene_context.scene_database_buffer,
1771
+ skin_matrices: current,
1772
+ },
1773
+ });
1774
+
1775
+ // First-frame init (or post-grow): "previous" buffer is
1776
+ // zero-initialised, which would have the velocity pass
1777
+ // interpolate against the origin and produce huge nonsense
1778
+ // on the first frame the skin appears. Seed prev from
1779
+ // current — first velocity reads then yield zero, which is
1780
+ // the right answer (no motion has happened yet).
1781
+ if (this.#first_frame_skin_matrices) {
1782
+ cmd_ctx.copyBufferToBuffer(
1783
+ current, 0,
1784
+ previous, 0,
1785
+ current.size,
1786
+ );
1787
+ this.#first_frame_skin_matrices = false;
1788
+ }
1789
+ }
1790
+
1791
+ /**
1792
+ * Per-frame orchestrator: drains pending registrations and the
1793
+ * database queue, runs the GPU tick (advances `clip.time`),
1794
+ * applies tracks into the pose accumulator, resolves the
1795
+ * accumulator into node TRS, propagates the transform
1796
+ * hierarchy, preps skinning matrices, and runs the skinning
1797
+ * compute pass that deforms cloned vertex regions in place.
1798
+ *
1799
+ * One call replaces the ~80-line per-frame dispatch block every
1800
+ * consumer would otherwise duplicate; the shader identities,
1801
+ * workgroup sizes, pose-accumulator sizing, and pass ordering
1802
+ * are all internal.
1803
+ *
1804
+ * `dt_seconds` is wall-clock seconds since the last call (the
1805
+ * caller measures via `performance.now()` or equivalent). It's
1806
+ * clamped to {@link MAX_DT_SECONDS} to keep a stall from
1807
+ * leaking multiple seconds into the next tick.
1808
+ *
1809
+ * Early-outs: the clip passes (tick/apply/resolve/hierarchy)
1810
+ * gate on the `animation_bound_track_groups` table being
1811
+ * non-empty; skin matrix prep + skinning gate on registered
1812
+ * skins instead — a skin whose joints are host-driven (no
1813
+ * clips bound) still gets fresh skinning matrices every frame.
1814
+ * With nothing registered no GPU work is encoded beyond the
1815
+ * database flush — safe to call every frame regardless of
1816
+ * registration state.
1817
+ *
1818
+ * @param {ShadeGPUCommandContext} cmd_ctx
1819
+ * @param {number} dt_seconds wall-clock seconds since last call
1820
+ */
1821
+ tick(cmd_ctx, dt_seconds) {
1822
+ assert.defined(cmd_ctx, 'cmd_ctx');
1823
+ assert.isNumber(dt_seconds, 'dt_seconds');
1824
+
1825
+ // Cap dt so a long stall doesn't fly the clip through dozens
1826
+ // of loops or clamp it past the meaningful range.
1827
+ const dt = Math.min(dt_seconds, MAX_DT_SECONDS);
1828
+
1829
+ // Flush database + drain pending registrations.
1830
+ this.update(cmd_ctx);
1831
+
1832
+ // Clip-pass gate: no bound tracks → apply has nothing to
1833
+ // do, and resolve + hierarchy propagation cascade from
1834
+ // apply. Skipping them keeps the per-frame cost at "one
1835
+ // database update" when the scene has no clip animation.
1836
+ const apply_groups = this.#database
1837
+ .get('animation_bound_track_groups')
1838
+ .dispatch_group_count(ANIMATION_APPLY_WORKGROUP_SIZE);
1839
+
1840
+ if (apply_groups > 0) {
1841
+ this.#encode_clip_passes(cmd_ctx, dt, apply_groups);
1842
+ }
1843
+
1844
+ // Skin matrix prep + skinning deformation. Both no-op when
1845
+ // no skins are registered — and both run independent of the
1846
+ // bound-track gate above: a registered skin whose joints
1847
+ // are host-driven (no clips bound) must still be deformed
1848
+ // each frame, otherwise it renders bind pose forever.
1849
+ this.dispatch_skin_matrix_prep(cmd_ctx);
1850
+ this.#scene_context.skinning.update(cmd_ctx);
1851
+ }
1852
+
1853
+ /**
1854
+ * Encode the clip-driven passes: GPU tick (advances
1855
+ * `clip.time`), track apply into the pose accumulator, pose
1856
+ * resolve into node TRS, and transform-hierarchy propagation.
1857
+ * Split out of {@link tick} so the skin passes there run
1858
+ * independently of the bound-track early-out.
1859
+ *
1860
+ * @param {ShadeGPUCommandContext} cmd_ctx
1861
+ * @param {number} dt clamped delta time, seconds
1862
+ * @param {number} apply_groups dispatch group count for the
1863
+ * apply pass — caller guarantees `> 0`
1864
+ */
1865
+ #encode_clip_passes(cmd_ctx, dt, apply_groups) {
1866
+ // Tick: advance clip.time on the GPU.
1867
+ const tick_groups = this.#database
1868
+ .get('animation_clips')
1869
+ .dispatch_group_count(ANIMATION_TICK_WORKGROUP_SIZE);
1870
+ if (tick_groups > 0) {
1871
+ shader_animation_tick.dispatch({
1872
+ encoder: cmd_ctx,
1873
+ group_count_x: tick_groups,
1874
+ bindings: {
1875
+ dt,
1876
+ animation_database: this.#database.buffer,
1877
+ },
1878
+ });
1879
+ }
1880
+
1881
+ // Size the pose accumulator to cover every allocated row of
1882
+ // the transforms table — apply writes by absolute row index,
1883
+ // resolve iterates the table via page iterator.
1884
+ const transforms_table = this.#scene_context.scene_database.get('transforms');
1885
+ const resolve_groups = transforms_table.dispatch_group_count(
1886
+ ANIMATION_POSE_RESOLVE_WORKGROUP_SIZE
1887
+ );
1888
+ this.ensure_pose_accumulator_capacity(transforms_table.element_capacity);
1889
+
1890
+ // ── Frame graph: apply → pose resolve → hierarchy propagate
1891
+ const graph = new FrameGraph("GPUAnimationManager/Tick");
1892
+ const gr_animation_database = graph_import_buffer(
1893
+ graph, this.#database.buffer, "animation_database"
1894
+ );
1895
+ const gr_scene_database = graph_import_buffer(
1896
+ graph, this.#scene_context.scene_database_buffer, "scene_database"
1897
+ );
1898
+ const gr_materials = graph_import_buffer(
1899
+ graph, this.#scene_context.materials.metadata_table.buffer, "materials"
1900
+ );
1901
+ const gr_pose_accumulator = graph_import_buffer(
1902
+ graph, this.#pose_accumulator_buffer, "pose_accumulator"
1903
+ );
1904
+
1905
+ const apply_data = {};
1906
+ const apply_pass = graph.add("animation/apply", apply_data, (data, resources, ctx) => {
1907
+ shader_animation_apply.dispatch({
1908
+ encoder: ctx.encoder,
1909
+ group_count_x: apply_groups,
1910
+ bindings: {
1911
+ animation_database: resources.get(data.animation_database),
1912
+ pose_accumulator: resources.get(data.pose_accumulator),
1913
+ materials: resources.get(data.materials),
1914
+ },
1915
+ });
1916
+ });
1917
+ apply_data.animation_database = apply_pass.read(gr_animation_database);
1918
+ apply_data.materials = apply_pass.write(apply_pass.read(gr_materials));
1919
+ apply_data.pose_accumulator = apply_pass.write(apply_pass.read(gr_pose_accumulator));
1920
+
1921
+ const resolve_data = {};
1922
+ const resolve_pass = graph.add("animation/pose_resolve", resolve_data, (data, resources, ctx) => {
1923
+ if (resolve_groups === 0) return;
1924
+ shader_animation_pose_resolve.dispatch({
1925
+ encoder: ctx.encoder,
1926
+ group_count_x: resolve_groups,
1927
+ bindings: {
1928
+ scene_database: resources.get(data.scene_database),
1929
+ pose_accumulator: resources.get(data.pose_accumulator),
1930
+ },
1931
+ });
1932
+ });
1933
+ resolve_data.scene_database = resolve_pass.write(resolve_pass.read(gr_scene_database));
1934
+ resolve_data.pose_accumulator = resolve_pass.write(resolve_pass.read(apply_data.pose_accumulator));
1935
+
1936
+ graph_scene_update_transform_hierarchy({
1937
+ graph,
1938
+ scene: this.#scene_context,
1939
+ scene_database: resolve_data.scene_database,
1940
+ });
1941
+
1942
+ cmd_ctx.encodeGraph(graph);
1943
+ }
1944
+
1945
+ destroy() {
1946
+ this.#database.destroy();
1947
+ for (let i = 0; i < 2; i++) {
1948
+ if (this.#skin_matrices_buffers[i] !== null) {
1949
+ this.#skin_matrices_buffers[i].destroy();
1950
+ this.#skin_matrices_buffers[i] = null;
1951
+ }
1952
+ }
1953
+ if (this.#pose_accumulator_buffer !== null) {
1954
+ this.#pose_accumulator_buffer.destroy();
1955
+ this.#pose_accumulator_buffer = null;
1956
+ }
1957
+ }
1958
+ }