@woosh/meep-engine 3.60.4 → 3.60.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 (419) hide show
  1. package/CHANGELOG.md +2621 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts +11 -0
  63. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  64. package/src/shade/renderer/Renderer.js +50 -6
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  67. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  68. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  69. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  70. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  71. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  72. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
  74. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  75. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
  77. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  80. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  82. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  83. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  85. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  86. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  88. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  89. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  91. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  92. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  93. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  94. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  96. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  98. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
  99. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  100. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
  101. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  102. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  103. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  104. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  106. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  107. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  108. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  109. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  110. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  111. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  112. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  113. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  114. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  115. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  116. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  117. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  118. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  119. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  120. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  121. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  122. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  123. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  124. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  125. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  126. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  127. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  128. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  129. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  130. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  131. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  136. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  137. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  138. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  139. package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
  140. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  141. package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
  142. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  143. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  144. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  145. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  146. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  147. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  149. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  150. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  151. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  152. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  153. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  154. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  155. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  156. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  158. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  159. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  160. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  161. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  162. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  163. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  164. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  165. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  166. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  167. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  168. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  169. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  170. package/src/shade/renderer/water/shader_water.js +22 -13
  171. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  172. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  173. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  174. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  175. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  176. package/editor/REWORK_PLAN_2026_08.md +0 -692
  177. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  178. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  179. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  180. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  181. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  182. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  183. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  184. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  185. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  186. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  187. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  188. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  189. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  190. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  191. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  192. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  193. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  194. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  195. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  196. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  197. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  198. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  199. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  200. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  201. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  202. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  203. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  204. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  205. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  206. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  207. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  208. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  209. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  210. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  211. package/src/REVIEW_2026_08_06/generation.md +0 -176
  212. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  213. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  214. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  215. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  216. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  217. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  218. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  219. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  220. package/src/REVIEW_2026_08_06/view.md +0 -667
  221. package/src/REVIEW_2026_08_06.md +0 -610
  222. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  223. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  224. package/src/core/bvh8/NOTES.md +0 -63
  225. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  226. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  227. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  228. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  229. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  230. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  231. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  232. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  233. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  234. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  235. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  236. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  237. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  238. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  239. package/src/engine/REVIEW_2026_07_03.md +0 -211
  240. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  241. package/src/engine/control/first-person/DESIGN.md +0 -658
  242. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  244. package/src/engine/control/first-person/TODO.md +0 -160
  245. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  246. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  247. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  248. package/src/engine/ecs/ik/PLAN.md +0 -179
  249. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  250. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  251. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  252. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  253. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  254. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  255. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  256. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  257. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  258. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  259. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  260. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  261. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  262. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  263. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  264. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  265. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  266. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  267. package/src/engine/physics/cloth/PLAN.md +0 -3056
  268. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  269. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  270. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  271. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  272. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  273. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  274. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  275. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  276. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  277. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  278. package/src/format/image/avif/DECISIONS.md +0 -545
  279. package/src/format/image/jpeg/NOTES.md +0 -3
  280. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  281. package/src/generation/COORDINATES.md +0 -54
  282. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  283. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  284. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  285. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  286. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  287. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  288. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  289. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  290. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  291. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  292. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  293. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  294. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  295. package/src/shade/RENDERER_CONTRACT.md +0 -697
  296. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  297. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  298. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  299. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  300. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  301. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  302. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  303. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  304. package/src/shade/renderer/NOTES.md +0 -78
  305. package/src/shade/renderer/PLAN.md +0 -8
  306. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  307. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  308. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  309. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  310. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  311. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  312. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  313. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  314. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  315. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  316. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  317. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  318. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  319. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  320. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  321. package/src/shade/renderer/camera/orbital/README.md +0 -81
  322. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  323. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  324. package/src/shade/renderer/camera/orbital/index.js +0 -9
  325. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  326. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  327. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  328. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  329. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  330. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  331. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  332. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  333. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  334. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  335. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  336. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  337. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  338. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  339. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  340. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  341. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  342. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  343. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  344. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  345. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  346. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  347. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  348. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  349. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  350. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  351. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  352. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  353. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  354. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
  355. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  356. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  357. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  358. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  359. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  360. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  361. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  362. package/src/shade/renderer/hiz/NOTES.md +0 -3
  363. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  364. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  365. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  366. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  367. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  368. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  369. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  370. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  371. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  372. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  373. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  374. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  375. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  376. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  377. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  378. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  379. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  380. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  381. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  382. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  383. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  384. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  385. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  386. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  387. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  388. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  389. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  390. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  391. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  392. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  393. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  394. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  395. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  396. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  397. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  398. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  399. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  400. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  401. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  402. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  403. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  404. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  405. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  406. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  407. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  408. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  409. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  410. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  411. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  412. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  413. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  414. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  415. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  416. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  417. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  418. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  419. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,3056 +0,0 @@
1
- # Cloth — Design
2
-
3
- > Design document. Written 2026-09-10; **M0 through M5 are implemented** — the solver, chains,
4
- > collision, the rigged path, self-collision and the worker — and every place they contradicted it has
5
- > been corrected in place with the measurement named beside it.
6
- > §17 says what is done and what is not; `MEASUREMENTS.md` is the evidence.
7
- >
8
- > Scope: a cloth and soft-chain simulator that lives inside `engine/physics`, collides against the
9
- > `Collider` components the rest of the engine already authors, runs on a worker, and is safe to ship
10
- > with its defaults untouched.
11
-
12
- ---
13
-
14
- ## 1. What this is
15
-
16
- One solver over particles, driven by components:
17
-
18
- - **`Cloth`** — the thing that simulates. Put it on an entity and that entity's transform subtree
19
- becomes a simulated chain: a rope, a chain, a hanging sign, a mooring line.
20
- - **`ClothRig`** — add it alongside `Cloth` and the particles are the **joints of a skinned model**
21
- rather than the entity subtree. This is what makes a cape, a skirt, a banner on a character — and,
22
- built from triangles by `cloth_proxy_from_mesh`, a sheet: a flag, a curtain, a cloth thrown over a
23
- table (§19.5).
24
- - **`ClothCollider`** — the marker that opts an existing `(Collider, Transform64)` entity into being
25
- something cloth collides with. This is the `RigidBody`-analogue: it says *this collider participates*,
26
- and carries only what cloth needs that `Collider` does not already say.
27
- - **`ClothPin`** — *M7.* Put it on any entity and that entity holds the particles it names. A cloth
28
- has as many holders as name it — a rope its own top, a flag its pole, a cape both shoulders — and
29
- none is a cloth that falls. §19.
30
-
31
- That last component follows a pattern the engine already has twice. `AcousticBody` opts a collider
32
- into the acoustic simulation and carries an `AcousticMaterial`; `FluidObstacle` opts a collider into
33
- the fluid simulation and carries an inflation radius. `ClothCollider` is the third instance of the
34
- same idea, and it should read like a sibling of those two, not like a new invention.
35
-
36
- The design commitments, in priority order, because they conflict and the ordering is the design:
37
-
38
- 1. **It does not explode.** Not at 5 fps, not at 500, not when a parameter is absurd, not when a
39
- character teleports, not when a collider is spawned inside the cloth.
40
- 2. **It does not get stuck.** The set of configurations the simulation can reach is bounded around
41
- the animated pose, and error cannot accumulate across steps.
42
- 3. **It works untouched.** Adding a `Cloth` component with default values to a correctly authored
43
- garment produces shippable motion. Tuning is an optimisation, never a prerequisite.
44
- 4. **It is cheap.** A CPU worker budget measured in low single-digit milliseconds per character.
45
- 5. **It is accurate.** Last, deliberately. Cloth that is 10% too stiff and never misbehaves beats
46
- cloth that is correct and occasionally detonates.
47
-
48
- ---
49
-
50
- ## 2. Non-goals
51
-
52
- - **Garment authoring.** No pattern cutting, no sewing, no pressure/inflation. Import a mesh.
53
- - **Woven anisotropy.** No warp/weft/shear tensor. That needs UV-aligned quad topology and a
54
- material model this design does not carry. Cloth here is isotropic with a bend modulus.
55
- - **Penetration-free guarantees.** IPC-class methods (`C-IPC`, `GIPC`) guarantee no intersection ever,
56
- at a cost per step this budget cannot pay. We guarantee bounded, self-correcting penetration.
57
- - **Cloth pushing rigid bodies.** Coupling is one-way: colliders move cloth. Two-way coupling into
58
- `PhysicsSystem`'s velocity solver is a later question and the constraint model leaves room for it
59
- (§6), but it is not in scope.
60
- - **Tearing, plasticity, burning.**
61
- - **GPU simulation.** The engine has a mature WebGPU path and a particle VM that could host this, but
62
- the requirement is a worker, and a worker is the cheaper, more portable, more debuggable answer.
63
- Nothing in the data layout forecloses a GPU port; §12 keeps that door open.
64
-
65
- ---
66
-
67
- ## 3. Author-facing API
68
-
69
- ```js
70
- // An entity hierarchy — rope, chain, hanging sign, a prop built out of entities.
71
- // Cloth simulates the transform subtree of the entity it is on, that entity included.
72
- // What holds it is a ClothPin: here the top holds its own particle, 0.
73
- rope_top.add(Cloth.from(ClothDynamics.ROPE));
74
- rope_top.add(ClothPin.from({ cloth: rope_top, particles: [0] }));
75
-
76
- // A skinned model's garment — cape, skirt, banner, hair.
77
- // Cloth simulates the JOINTS of the model instance this entity owns.
78
- character.add(Cloth.from(ClothDynamics.SILK));
79
- character.add(ClothRig.from(cape_proxy)); // cape_proxy: ClothProxy, built at import
80
- ```
81
-
82
- There is no mode enum and no discriminator field. **`Cloth` alone simulates the entity subtree;
83
- `Cloth` + `ClothRig` simulates a skinned model's joints** — component presence doing the job an enum
84
- would otherwise do badly. The solver is identical either way; only seeding and write-back differ.
85
-
86
- **M7 note: what holds a cloth is not part of it.** Every sentence below that says "the anchor" meant
87
- three things at once — the frame the cloth is simulated in, the particle it was pinned at, and what
88
- its tethers measured from — and only the first is the `Cloth` entity's. The fixed set is now whatever
89
- `ClothPin`s name (§19), any number of them, and the rest of the "anchor" sentences are read through
90
- §19.
91
-
92
- Those are two cases rather than one because **the engine has two hierarchies, and that is a decision
93
- rather than an accident**. `shade_node_to_entity_composition` expands a loaded model into entities and
94
- gives a `ShadedGeometry` to every node that draws — except a skinned one, which "is not expanded, and
95
- that is a boundary rather than an omission": it contributes its transform and its name and no
96
- geometry, because its vertices deform against a skeleton the renderer owns and the GPU poses, and
97
- making those joints entities is a design this program does not do (D47). A rope prop is entities all
98
- the way down. A character's cape is not, and never will be. §11 is where that lands.
99
-
100
- ### 3.1 `Cloth`
101
-
102
- ```js
103
- export class Cloth {
104
- dynamics = new ClothDynamics(); // how it behaves — §3.3
105
- blend = 1; // 0 = fully animated pose, 1 = fully simulated
106
- layer = 1; // ClothCollider layer/mask filtering, RigidBody semantics
107
- mask = 0xFFFFFFFF;
108
- flags = ClothFlags.None;
109
- }
110
- ```
111
-
112
- **The anchor is the entity the component is on**, and nothing names anything. Without a `ClothRig`
113
- the particle set is the entity's own transform subtree, walked through
114
- `TransformAttachmentSystem#findChildrenOf`/`#countChildrenOf` — the parent-to-children index the
115
- engine already maintains. Put `Cloth` on the top link of a rope and you get the rope; put it on a
116
- node with several chains under it and you get all of them in **one coupled solve**, which is what
117
- makes self-collision between them work.
118
-
119
- Two rules keep the walk unambiguous:
120
-
121
- - **A nested `Cloth` claims its own subtree.** The walk stops when it meets another `Cloth`, so
122
- independent hanging parts of one prop partition naturally.
123
- - **`ClothExclude`** is a tag component that prunes a subtree from the walk, for the rig that has a
124
- non-simulated attachment point in the middle of a chain.
125
-
126
- With a `ClothRig`, the entity is instead the one that owns the skinned model instance, and the
127
- subtree walk does not run at all — the particle set comes from the proxy's joint list (§3.2).
128
-
129
- **M7 corrected "the anchor is the entity the component is on".** The entity is the cloth's **frame**:
130
- what it is simulated in and what §7 P7's inertia is measured against. It is still particle 0 of the
131
- walk — links run from it — but nothing pins it: a `ClothPin` on it does, `ClothPin.from({ cloth: e,
132
- particles: [0] })`, and without one the rope falls while the entity, which cloth reads and never
133
- writes, stays where it is. That is the price of there being no structural pin, paid in one line per
134
- rope, and §19 has why it is the right one.
135
-
136
- `blend` is the escape hatch for cinematics, LOD and "turn it off for this one shot". It is a lerp
137
- between the animated pose and the simulated pose applied at write-back, not a solver parameter, so it
138
- is free and always safe. **M1 note:** writing it wakes a sleeping cloth, because sleep skips the
139
- write-back as well as the step (§7 P5) and a `blend` written to a sleeping cloth would otherwise take
140
- effect at whatever later moment something else happened to wake it.
141
-
142
- ### 3.2 `ClothRig`
143
-
144
- ```js
145
- export class ClothRig {
146
- proxy = null; // ClothProxy — shared, immutable, built at import
147
- skin = -1; // the entity whose MeshSkin the proxy's joint indices are into
148
- }
149
- ```
150
-
151
- A `ClothProxy` **object**, not a path. This is the convention the engine already uses for a component
152
- that points at a built, shared, immutable asset: `Collider#shape` holds an `AbstractShape3D` and is
153
- documented as *not* serialised because shapes are shared assets the pipeline wires up;
154
- `ParticleEffect` holds a `ParticleLayout` and a `ParticleProgram` the same way. `ClothRig` serialises
155
- the proxy's identity and the skin's entity, and the asset pipeline resolves the rest.
156
-
157
- **The skin is an entity's, named by entity** (2026-10-11). `skin` was an ordinal into the skins of
158
- whatever `MeshSystem.instance_of` answered for the cloth's entity, which made a rig reachable only
159
- through a loaded model. A skin is now a `MeshSkin` component on the mesh it deforms, and the rig
160
- names that entity; a mesh built in code is rigged the same way as a loaded one. -1 means not named
161
- yet, and waits like a model still loading.
162
-
163
- **The proxy addresses joints by index, never by name.** The binding is resolved **once, at import**:
164
- the builder turns authored bone names into integer indices into `Skin#joints`, and the runtime
165
- component holds integers. Reintroducing strings at runtime would be the wrong answer.
166
-
167
- **A joint is an entity, and this paragraph used to say it could not be.** It said "a joint is a
168
- `Node3D` inside a `SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible
169
- by construction", and the engine moved: a model is spawned by `prefab_instantiate`, its joints *are*
170
- entities, each carrying a `TransformAttachment` holding its offset from its parent joint, and that
171
- attachment is what `cloth_write_back_rig` writes. Names are resolved through `Prefab`/`PrefabInstance`
172
- rather than through `SceneBundleInstance#nodes`. The *conclusion* is unchanged and is now better
173
- supported — indices into `Skin#joints` are stable across respawns where entity ids are not, so the
174
- proxy still holds integers — but it is a choice rather than an impossibility. §11.2 and §13 carried
175
- the same description.
176
-
177
- ### 3.3 `ClothDynamics`
178
-
179
- The parameters live in one value object, and the library is a set of instances of it.
180
-
181
- ```js
182
- export class ClothDynamics {
183
- stretch = 0.7; // 0..1 normalised material coordinate
184
- bend = 0.2; // 0..1
185
- damping = 0.15; // 0..1
186
- slack = 0.5; // 0..1 — how far the cloth may leave the animated pose (§3.4)
187
- thickness = 0.02; // metres — particle radius, for collision offset and self-collision
188
- inertia = 1; // 0..1 — how much of the anchor's world motion the cloth feels
189
- drag = 0.1; // 0..1 — aerodynamic coupling to the air (§10)
190
- substeps = 2; // solver budget
191
- iterations = 4; // solver budget
192
- flags = ClothDynamicsFlags.StrainLimit; // | SelfCollision, ...
193
- }
194
- ```
195
-
196
- The library is `ClothDynamics.SILK`, `.COTTON`, `.DENIM`, `.LEATHER`, `.ROPE`, `.CHAIN`, `.HAIR` —
197
- frozen instances in `cloth_dynamics_library.js`. Assign one directly and it is shared, immutable and
198
- free; call `.clone()` and you own a copy to edit. There is no field that silently rewrites other
199
- fields, and no second "advanced" block: this *is* the parameter set, and the library is a set of
200
- points in it that are known to be good rather than a mechanism.
201
-
202
- The stiffnesses are normalised coordinates, not moduli in N/m — log-mapped internally so that `0.7`
203
- means something an author can predict and `0.9` is not four orders of magnitude away from it. They are
204
- independent, because the library carries the good combinations and that is protection enough; nothing
205
- needs to be collapsed onto a single magic axis to keep an author out of trouble.
206
-
207
- **M1 wrote that mapping (`cloth_dynamics_map.js`) and neither stiffness maps to newtons per metre.**
208
- `stretch` maps to a *strain allowance*, because §6's measurement makes the fabric row hard and leaves
209
- nothing for a stiffness to be. `bend` maps to a **ratio** of the row's own `m_eff/dt²` — at the fixed
210
- step rather than the substep — for two reasons that are both wanted: the fabric's look becomes
211
- invariant to how heavy the author made the particles, which is what a normalised knob should mean; and
212
- since `k/(m/h²) = ratio/substeps²`, a bend row expressed that way is inside M0's dissipative band at
213
- *any* substep count by construction, rather than by an author staying out of trouble. A stiffness in
214
- N/m has neither property. The cost, stated: **bending saturates** at the top of that band, so a fabric
215
- stiffer than `bend = 1` cannot be expressed as a soft row that also comes to rest.
216
-
217
- `substeps` and `iterations` are budget rather than material, and they are here because a chain wants
218
- more of both than a bedsheet does, so they are part of what "chain" means. Raising `iterations`
219
- converges further toward the same answer (§5.1); raising `substeps` also reduces implicit Euler's
220
- numerical damping, so it does change the drape slightly. Both are ordinary fields with ordinary
221
- documentation, not hidden couplings.
222
-
223
- `ClothDynamics` carries `equals` and `hash` like every other value type in the engine, so a component
224
- holding one compares and hashes cheaply.
225
-
226
- ### 3.4 Slack, and why there is no radius in metres
227
-
228
- `slack` is dimensionless. Each particle's leash radius is derived, not authored:
229
-
230
- ```
231
- r_i = 2 · slack · d_geo,i
232
- ```
233
-
234
- **M1 corrected the factor of two, and it is a derivation rather than a fudge.** This paragraph
235
- originally read `r_i = slack · d_geo,i`, which makes `slack = 1` a real and fairly tight constraint
236
- instead of the absence of one: a rope hanging straight down cannot swing through ninety degrees,
237
- because its tip has to travel `√2·d_geo` to get there and its leash is `d_geo`. The radius at which
238
- the row is *provably* inert is `2·d_geo` — the tether already holds every particle within `d_geo` of
239
- the anchor, the animated position it is leashed to is also within `d_geo` of the anchor since a chord
240
- is never longer than the arc it subtends, so by the triangle inequality the two are never further
241
- apart than that. With the factor of two in, `slack = 1` means exactly "no leash": the row cannot
242
- bind, so it is not built at all, and there is no sentinel and no discontinuity to code around.
243
- `MEASUREMENTS.md` §10.
244
-
245
- where `d_geo,i` is the particle's baked geodesic distance to the fixed set. At `slack = 0` every
246
- particle is welded to its animated position; at `slack = 1` a particle may be anywhere within its own
247
- reach of that position. Three things fall out of that and none of them need a knob:
248
-
249
- - **The profile is automatic.** A hem gets a long leash and a waistband gets a short one, because
250
- that is what their geodesic depth says, not because someone painted it.
251
- - **The fixed set falls out too.** A pinned particle has `d_geo = 0`, so `r_i = 0`, so the leash *is*
252
- the pin. There is no separate attach constraint — one row covers both (§6). *M7: not any more — a
253
- held particle is kinematic, inverse mass zero, carried by its holder (§19); `d_geo` is its distance
254
- to the nearest held particle along the fabric, and the animated position `s_i` is the rest pose hung
255
- from that particle. The factor of two still holds exactly: `|s_i − x_p| = |r_i − r_p| ≤ d_geo`.*
256
- - **There is no magic default.** The old formulation had a radius in metres with `0` meaning "derive
257
- it from the mesh extent", which is the kind of overloaded sentinel that gets copied into a save file
258
- and then means the wrong thing.
259
-
260
- Slack is not a stretch allowance and does not govern inextensibility — the tether row does that, and
261
- the two are different questions (§6). Slack asks how loosely the cloth is leashed to the animation;
262
- the tether asks how far the fabric may extend from its anchor. A cape wants both: a moderate leash so
263
- it follows the character, and a hard tether so it never becomes rubber.
264
-
265
- ### 3.5 `ClothCollider`
266
-
267
- ```js
268
- export class ClothCollider {
269
- inflation = 0; // extra thickness in metres, on top of ClothDynamics#thickness
270
- friction_scale = 1; // multiplies the entity's Collider#friction
271
- layer = 1; // RigidBody semantics, and see below
272
- mask = 0xFFFFFFFF;
273
- flags = 0; // ClothColliderFlags: none defined; bit 0 was Static, retired
274
- }
275
- ```
276
-
277
- Everything else is read from components the entity already has: geometry and friction from
278
- `Collider`, pose from `Transform64`.
279
-
280
- **M2 added the layer and the mask, because this paragraph originally said "layer filtering from the
281
- `Cloth` side" and that is not a thing that can happen.** `Cloth#mask` is a mask *of collider layers*,
282
- and §3.5 is also the paragraph that says a `ClothCollider` does not require a `RigidBody` — so there
283
- is nowhere else in the tuple for a collider's layer to live. The pair is here with `RigidBody`'s own
284
- semantics, symmetric: a cloth and a collider interact when each is in the other's mask.
285
-
286
- **A `ClothCollider` does not require a `RigidBody`.** This is a deliberate divergence from
287
- `FluidObstacle`, which does require one because it needs a wall velocity. Cloth's colliders are
288
- overwhelmingly character bones — ordinary entities in a `TransformAttachment` hierarchy — and forcing
289
- each of them to become a physics body to be seen by cloth would be a tax on the common case. Collider
290
- velocity is derived from the pose delta between fixed steps, which the system caches anyway for swept
291
- collision (§8.2). World props that *do* carry a `RigidBody` add a `ClothCollider` the same way and land
292
- in the same index; the two cases are not distinguished.
293
-
294
- ---
295
-
296
- ## 4. Components and systems
297
-
298
- ```
299
- engine/physics/cloth/
300
- ecs/
301
- Cloth.js component: the simulated thing; anchor is its own entity
302
- ClothFlags.js TeleportKeep, NeverSleep
303
- ClothExclude.js tag: prune a subtree from the walk
304
- ClothRig.js component: particles are a skinned model instance's joints
305
- ClothDynamics.js value object: how it behaves
306
- ClothDynamicsFlags.js SelfCollision, StrainLimit
307
- cloth_dynamics_library.js SILK, COTTON, DENIM, LEATHER, ROPE, CHAIN, HAIR — frozen instances
308
- cloth_dynamics_map.js normalised coordinates → row parameters; every constant measured
309
- ClothCollider.js component: the RigidBody-analogue for collision
310
- ClothColliderFlags.js
311
- ClothWorld.js every cloth in a scene, and everything that is not arithmetic
312
- ClothSystem.js main-thread system: a ClothWorld stepped inline
313
- ClothInstance.js one live cloth: the binding between a subtree and a ClothState
314
- cloth_seed_subtree.js §11.1's walk: entities → node_parent + animated_position
315
- cloth_seed_rig.js §11.2's proxy: joints → node_parent + animated_position
316
- cloth_build_rows.js everything after that, shared by both seeders
317
- cloth_reconstruct_pose.js particles → a pose, shared by both write-backs
318
- cloth_write_back.js that pose → Transform64 + TransformAttachment
319
- cloth_write_back_rig.js that pose → Node3D.transform_local
320
- cloth_gather_colliders.js the index's overlapping records, in the cloth's own frame
321
- WorkerClothSystem.js drop-in worker variant: the same ClothWorld, stepped over there
322
- ClothWorkerCore.js worker side: a ClothState view per registered cloth
323
- cloth.worker.js the entry point, a real module worker
324
- makeClothWorker.js the spawn expression, isolated because it needs import.meta
325
- cloth_worker_protocol.js the command region: a count, then a block per cloth
326
- ClothSerializationAdapter.js the component + its ClothDynamics, by value
327
- ClothPin.js component: this entity holds these particles of that cloth (§19)
328
- ClothPinSerializationAdapter.js the pin, with its cloth as a raw entity id
329
- ClothHold.js one pin as the cloth sees it: offsets, the holder's pose
330
- cloth_holds.js what holding decides: who is kinematic, tethers, the animated pose
331
- cloth_geodesic.js distance along the fabric from a set of particles
332
- ClothColliderSerializationAdapter.js five authored scalars, the same way
333
- ClothRigSerializationAdapter.js NOT BUILT — §3.2's asset identity, and the proxy codec
334
- collider/
335
- ClothColliderIndex.js the index of live cloth colliders + their swept AABBs
336
- ClothColliderRecord.js the packed layout: kind, params, both poses, bounds, interpolated pose
337
- ClothColliderKind.js sphere, capsule, box, cylinder, plane
338
- cloth_collider_pack.js AbstractShape3D → packed record
339
- cloth_collider_bounds.js the swept AABB, shared by the index and each cloth's own table
340
- cloth_collider_sdf.js pose interpolation, localisation, and the field dispatch
341
- wind/
342
- AbstractClothWind.js §10's one interface: sample the air, and say whether it varies
343
- ClothGPUFluidWind.js the GPU wind: lattices read back from GPUFluidSystem, the far field elsewhere
344
- ClothFluidWind.js every live FluidComponent, with the scene's far field underneath it
345
- cloth_sample_air.js the air into the cloth's own frame, on the main thread
346
- solver/
347
- ClothState.js SoA particle + topology + constraint storage over one buffer
348
- cloth_step_instance.js one cloth, one step, over nothing but memory — what crosses
349
- cloth_step.js the fixed step: predict, warm start, iterate, dual update, integrate
350
- cloth_solve_vertex.js the per-vertex 3x3 block update
351
- cloth_wind_accelerate.js §10's aerodynamic term, and the factor that makes a flag a flag
352
- constraint/ one module per row type (§6)
353
- cloth_color_particles.js graph colouring of the particle graph
354
- constraint/cloth_leash_row.js §6's slack row and its tether row, which are one row model
355
- build/
356
- ClothProxy.js the built artefact
357
- cloth_proxy_from_joints.js §11.3 steps 5–8, for a garment that is already rigged
358
- cloth_proxy_build.js unrigged: source mesh → proxy (weld, simplify, extract, validate)
359
- cloth_proxy_bind.js unrigged: render mesh → proxy binding weights
360
- ClothDiagnostics.js dev-build tripwires
361
- ```
362
-
363
- Two systems:
364
-
365
- - **`ClothColliderSystem`** observes `(Collider, Transform64, ClothCollider)` and maintains a
366
- `ClothColliderIndex` — a flat array of packed records plus swept world AABBs. This is the same shape
367
- as `AcousticOccluderIndex`/`OccluderBody` in the sound simulation, and for the same reason: the
368
- consumers need a compact, pose-cached, broadphase-able view of a set of colliders that is not the
369
- physics BVH. It cannot be the physics BVH, because `ColliderObserverSystem` deliberately skips
370
- colliders with no owning body, and bone colliders have no body.
371
-
372
- **M2 note: it has no `fixedUpdate`, and that is §4.1 applied a second time.** The index has to be
373
- refreshed from the world's transforms *before* any cloth reads it, and the scheduler cannot express
374
- that for exactly the reason §4.1 gives — a score is a sum over what a system references, so a system
375
- can only be moved earlier by touching more, and `ClothSystem` (which reads and writes `Transform64`
376
- and `TransformAttachment`) outscores one that only reads. So the answer is not a third mechanism, it
377
- is one call site: `ClothSystem#fixedUpdate` calls `refresh()` at the top of its own step, before any
378
- cloth is advanced. The collider poses are then read at exactly the moment the anchor poses are, which
379
- is what makes a contact consistent rather than merely fresh. With no `ClothSystem` in the manager the
380
- index is never refreshed, and nothing reads it either.
381
-
382
- **And the index is a linear scan rather than a BVH**, which is a divergence from
383
- `AcousticOccluderIndex` with two reasons. The query is one AABB per cloth per step against a set that
384
- is a character's bones — a `ClothCollider` is an opt-in marker, not every collider in the scene — and
385
- a tree over twenty leaves that all move every frame costs more to keep than to skip. And a
386
- `PlaneShape3D` has an **unbounded** AABB, which a BVH cannot hold: every refit would grow the root to
387
- infinity. If a scene ever wants thousands of cloth colliders, the shape of the fix is a BVH over the
388
- bounded ones with the planes in a short list beside it.
389
-
390
- - **`ClothSystem`** owns the simulation. Per fixed step: refresh the animated targets, gather the
391
- overlapping colliders from the index, step, write back.
392
-
393
- **M5 note: it owns it jointly with `WorkerClothSystem`, and "two systems" is now three.** The two
394
- are interchangeable — same components, same declared access, same position in the derived order —
395
- and they share a `ClothWorld`, which is the registry and every phase of a step that is not
396
- arithmetic. `ClothSystem` calls `cloth_step_instance` between the two halves; `WorkerClothSystem`
397
- puts a thread there. §12 has why that is what makes the bit-parity gate structural.
398
-
399
- Cloth runs at the fixed rate and its poses are **held** between ticks rather than interpolated, which
400
- on a display faster than the fixed step is visible and is not wrong. The first draft of this paragraph
401
- said cloth reuses `PhysicsSystem`'s producer/`__interp_restore` contract; M5 measured that it cannot —
402
- that contract is entity-keyed and a rigged garment's joints are not entities — and §13.1 and §18 carry
403
- what the cloth-shaped version would be and what it would cost. What keeps a cloth-driven bone from
404
- fighting an animation-driven one is not interpolation but `TransformAuthority`, which `cloth_seed_rig`
405
- claims (§11.2, `MEASUREMENTS.md` §25) — as a hold since §61, because the bit alone never stopped a
406
- clip on the GPU.
407
-
408
- ### 4.1 Ordering, and why the answer is not to be ordered
409
-
410
- Cloth wants to read the skeleton *after* animation has posed it for this step and *before* anything
411
- renders. The engine has exactly two ways to order systems: **registration order**, and what each
412
- system declares in `components_used`, from which writers are sorted before readers. There is
413
- deliberately no `runs_before`/`runs_after`, because naming other systems is the coupling that design
414
- exists to avoid.
415
-
416
- **Cloth both reads and writes `Transform64`**, which puts it on both sides of that component's own
417
- rule, and M1 established that neither mechanism can express the relation — for a structural reason
418
- rather than an arithmetic one. `updateExecutionOrder` scores a system as a **sum** over the components
419
- it references and sorts descending, so a score can only be *raised* by touching more components or
420
- touching them at a higher access level. The only way to sort **later** is therefore to touch strictly
421
- less than the system you have to follow, and a consumer of a posed hierarchy touches strictly more.
422
- Registration order cannot rescue it either: ties break by registration, and this is not a tie —
423
- `ClothSystem` outscores `TransformAttachmentSystem` and any plausible poser outright, registered first
424
- or last.
425
-
426
- **So the answer is not a third mechanism. The answer is that cloth must not care.** An earlier draft
427
- of M1 added an execution-phase number to `System`, which put the words "pose" and "animation" into a
428
- layer of the engine that has no such vocabulary, and bought an ordering the design should not have
429
- needed. What M1 ships instead is a write-back that is **idempotent under recomposition**: cloth writes
430
- each node's local `TransformAttachment#transform` alongside its world `Transform64`, so a hierarchy
431
- recomposition — which is what happens whenever anything announces on the anchor — reproduces the
432
- simulated pose instead of replacing it with the authored one. Measured at **1.7e-15 m** across a
433
- 16-link rope (`MEASUREMENTS.md` §15). With that, where cloth runs is a **quality** question.
434
-
435
- And a small one. Running before the poser means reading last step's anchor pose, so the cloth's
436
- *shape* is one fixed step stale; its *position* is not, because the attachment cascade carries the
437
- whole subtree rigidly with the anchor from cloth's own local transforms. Measured on a 1.6 m rope: a
438
- few centimetres under ordinary motion, up to a fifth of its own length at the sharpest velocity
439
- reversal of a ±9 m/s square wave, and **not accumulating** — the same figure after fifteen seconds as
440
- after two, because the leash and the tether bound the reachable set. `ClothSystem.spec.js` asserts the
441
- idempotency, records the order the scheduler actually produces, and bounds that divergence.
442
-
443
- What the declaration *does* buy is the half of the ordering that is real: cloth writes
444
- `TransformAttachment#transform` and `TransformAttachmentSystem` reads it, so writer-before-reader puts
445
- cloth first and the attachment system publishes its result. That falls out of `components_used` with
446
- nothing else asked for.
447
-
448
- **`InverseKinematicsSystem` is the same shape and reaches the same conclusion.** It reads world
449
- `Transform64`s "the hierarchy has already composed" and writes local ones back — which is exactly the
450
- arrangement above, and exactly why it is not fragile in the way its scoring suggests.
451
-
452
- ---
453
-
454
- ## 5. The solver
455
-
456
- **Augmented Vertex Block Descent** (Giles, Diaz & Yuksel, SIGGRAPH 2025) — VBD's per-vertex
457
- Gauss-Seidel block coordinate descent on the implicit-Euler variational energy, with an augmented
458
- Lagrangian in place of pure penalties.
459
-
460
- ### 5.1 Why this, and not XPBD
461
-
462
- XPBD with small substeps is the incumbent: MagicaCloth2, Chaos Cloth, Blender, most of the field.
463
- It works. Three properties push past it, and each maps onto a stated requirement:
464
-
465
- - **Unconditional stability at any iteration count.** Every per-vertex update is a descent step on an
466
- energy that is bounded below, so the update can be truncated at one iteration and the simulation is
467
- still stable — just softer. That turns requirement 1 into a property of the update rule rather than
468
- a property of tuning. XPBD is stable in practice, but its stability is a function of substep size
469
- and stiffness; ours is not.
470
- - **Parameters that keep their meaning.** AVBD targets the implicit-Euler solution, so a stiffness is
471
- a material modulus and adding iterations moves the result *toward* the same answer rather than
472
- changing which answer it is. Under XPBD, apparent stiffness and apparent damping both move when the
473
- iteration budget moves, which is why cloth packages ship dozens of interacting sliders and
474
- per-parameter animation curves to compensate. §3.3's small, independent parameter set is only
475
- honest on a solver whose knobs are orthogonal to its budget.
476
-
477
- This is iteration-count insensitivity, not substep insensitivity. Implicit Euler is numerically
478
- damped by an amount that depends on `h`, so more substeps still means less artificial damping and
479
- a livelier drape — which is why `substeps` is a documented field rather than a hidden constant.
480
- - **Hard constraints without stiff penalties.** The augmented Lagrangian reaches effectively infinite
481
- stiffness through an accumulating multiplier rather than a large `k`, so pins, tethers and slack
482
- bounds are *hard* — enforced to convergence — while cloth stays soft, in the same sweep, without
483
- the conditioning disaster a 10⁸ penalty would cause. VBD alone cannot do this; it is exactly the
484
- limitation AVBD was written to fix.
485
-
486
- Two smaller wins. Colouring the *particle* graph needs far fewer colours than colouring the
487
- *constraint* graph — the VBD paper reports 8 colours for a 3,891-vertex tetrahedral mesh against 76
488
- for its 14,802 tetrahedra — which matters for a future GPU port and for cache behaviour now. And the
489
- per-vertex form is a natural fit for SoA typed arrays, which is what a worker needs anyway.
490
-
491
- The cost is honest: more arithmetic per vertex than an XPBD projection, and a shorter track record
492
- for cloth specifically. §18 carries both as risks with a measurement plan.
493
-
494
- ### 5.2 The step
495
-
496
- Per substep, with `h` the substep length, `m_i` particle mass, `y_i` the inertial target:
497
-
498
- ```
499
- predict y_i = x_i + h·v_i + h²·a_ext,i
500
- x_i ← x_i + h·v_i + h²·ã_i (adaptive initialisation)
501
-
502
- warm start for each HARD constraint c: (soft rows: k_c = stiffness, λ_c = 0)
503
- λ_c ← λ_c · α · γ
504
- k_c ← clamp(k_c · γ, k_min·s_c, k_max·s_c) s_c = the row's own m_eff/h²
505
-
506
- iterate repeat `iterations` times:
507
- for each colour: (parallel within a colour)
508
- for each particle i in colour:
509
- f = -(m_i/h²)(x_i - y_i)
510
- H = (m_i/h²)·I
511
- for each constraint c on i:
512
- C = C_c(x); g = ∂C_c/∂x_i
513
- fu = k_c·C + λ_c
514
- fc = clamp(fu, fmin_c, fmax_c)
515
- f -= fc·g
516
- if fc > 0: (row in tension)
517
- H += (fc/L)·(I − g gᵀ) (geometric stiffness, PSD part)
518
- if fc == fu: (row active — not at a clamp)
519
- H += k_c·(g gᵀ)
520
- x_i += H⁻¹·f (3×3 LDLᵀ, capped at the shortest incident row)
521
-
522
- last iteration only, when the held particles lie on one line (axis a through o):
523
- for each free particle i: r_i = x_i − o, t_i = a × r_i
524
- ∇E_i, H_i = the vertex update's inertial term + leash, backstop, contact rows
525
- g = Σ t_i·∇E_i
526
- H = Σ t_iᵀ·H_i·t_i + max(0, Σ ∇E_i·(a(a·r_i) − r_i))
527
- turn every free particle about the line by θ = −g/H
528
-
529
- dual update, for each HARD constraint c:
530
- λ_c ← clamp(k_c·C_c + λ_c, fmin_c, fmax_c)
531
- k_c ← min(k_c + β·|C_c|, k_max·s_c)
532
- (a contact's normal row: k_c ← min(k_c + ρ·k_max·s_c·|C_c|/offset, k_max·s_c))
533
-
534
- integrate v_i = (x_i - x_i^prev)/h
535
- ```
536
-
537
- A contact's warm start differs in one line from the listing above: its two penalties decay by
538
- `γ_c = CLOTH_CONTACT_GAMMA` rather than `γ`, the multipliers by `α·γ` as every hard row's do. The
539
- correction below is why.
540
-
541
- The Hessian contribution is gated on the row being *active*. A clamped row's force is constant with
542
- respect to position, so its true second derivative is zero; adding `k·ggᵀ` anyway would make an
543
- inactive unilateral constraint resist motion it should not touch. It contributes its (constant) force
544
- to `f` and nothing to `H`.
545
-
546
- `α ∈ [0,1]` regularises the multiplier carried between steps, `γ ∈ [0,1)` decays warm-started duals so
547
- a resting configuration bleeds off accumulated force instead of ringing, and `β` is the penalty ramp.
548
- These are solver internals, global, and deliberately **not** on `ClothDynamics` — they are not a
549
- property of a fabric. The reference 2D implementation's defaults are a rigid-body tuning and will not
550
- transfer; §17 M0 makes picking cloth values a measured step with its own spec, not a copied constant.
551
-
552
- **M0 measured them, and four things about this pseudocode were wrong.** The sweep and its full grids
553
- are in `MEASUREMENTS.md`; the corrections are folded into the listing above and are, in short:
554
-
555
- 1. **The five constants only govern *hard* rows.** A row whose material stiffness is finite is
556
- representable as a plain penalty, and a plain penalty at the material stiffness *is* the material
557
- law — there is nothing for a multiplier to add. Carrying one anyway drives `C` to zero and makes
558
- every fabric inextensible regardless of what it was authored as: a 1 kN/m spring measured 100×
559
- stiffer than Hooke. So a soft row is a pure penalty at its material stiffness and reads dead flat
560
- against all five constants, which is what makes them honest as global internals. A row is **hard**
561
- exactly when its stiffness is `Infinity` — §6's table already says `hard` in that column, and this
562
- makes it literal rather than a threshold.
563
- 2. **`k_min` and `k_max` are ratios of `m/h²`, not stiffnesses in N/m.** Held at a fixed multiple of
564
- the row's own inertial scale, a 20-link hard chain produced the same residual column across five
565
- decades of particle mass; held at a fixed stiffness it produced a different answer for every mass.
566
- This paragraph's own reasoning already names `k_max/(m/h²)` as the quantity that matters — it just
567
- stopped one step short of making the ratio the parameter.
568
- 3. **`α` and `γ` act only through their product, and `α·γ ≥ 0.5` diverges.** At `α·γ ≤ 0.3` a 20-link
569
- hard chain settles to rest with kinetic energy at 1e-9 J; at 0.5 it jitters; at 0.6 it detonates.
570
- Residual improves monotonically towards that cliff, so the value is a margin decision:
571
- **α·γ = 0.25**, a factor of two clear.
572
- 4. **`β` could not be chosen from M0 data.** Above about 1e4 every M0 scenario gives an identical
573
- answer, and with `k_min = k_max` — which measured as good as any ramp for M0's persistent rows —
574
- it has no effect at all. The ramp exists for **transient** rows, a contact that should arrive soft
575
- rather than shock the mesh on its first frame, and M0 has none. M2 has to measure it.
576
-
577
- Chosen at M0: **α = 0.25, γ = 1, β = 1e5 (unexercised), k_min = k_max = 8·(m/h²)**. γ sits at 1
578
- because its second job — decaying the penalty — has nothing to do while the band is a single point, so
579
- the whole decay lives in α, whose only job is the multiplier.
580
-
581
- **M2 made the band real and the line above is out of date in three places.** `β` is **1e3**, not 1e5:
582
- M2's contacts are the first transient rows in the system and the ramp is finally live, and both ends
583
- of the range measured worse than the middle — no ramp at all settles at 1.95 J of residual kinetic
584
- energy against 0.12 J at 1e3, a fast ramp gives up most of what the low floor bought
585
- (`MEASUREMENTS.md` §17, and `cloth_solver_constants.js`'s own docblock carries the argument). The band
586
- is no longer a single point for a **contact**: `CLOTH_CONTACT_K_MIN_RATIO = 0.25` is its floor,
587
- against a ceiling of `8·(m/h²)` — a margin decision in the middle of a flat region, with two decades
588
- of ramp above it. And `γ` **is still 1, and was never re-measured**: this paragraph said it would be
589
- "when M2's contacts make the band real", M2 made the band real, and the sweep was not run. That is now
590
- a decision rather than an omission — nothing measured since has asked for a decaying penalty, and the
591
- one place it would help is recorded in §18 as a coupling rather than as a bug — but it is an
592
- **unmeasured** constant and the only one left in the set, so it is named here as one.
593
-
594
- **The cloth-skirt example corrected the contact's ramp and gave it a decay (`MEASUREMENTS.md` §67).**
595
- A thigh advancing 17 mm a substep into a hanging skirt showed through it in 12.7% of the dance's
596
- steps, up to 13 mm, and four times the iteration budget made that zero: the row was arriving soft.
597
- `β = 1e3 N/m²` against that garment's band of 72 to 2,300 N/m is **0.003 of the band per contact
598
- offset of violation**, so against a limb the penalty never left the floor and the multiplier, rebuilt
599
- from a quarter every substep, could not carry the push in four iterations. The ramp is a ratio now,
600
- `ρ = CLOTH_CONTACT_RAMP_RATIO = 0.1` of the band per offset of violation per iteration, which keeps the
601
- shape the M2 measurement picked in the units the contact is defined in: 12.7% → 0.1% of steps, M2's
602
- landing sheet 1.10 → 0.68 mm, M2 §6's swept capsule 17 → 1.8 mm, the tunnelling threshold unmoved.
603
- It ramps whether or not the row is active, as before: ramping active rows only, the reference
604
- implementation's form, leaves the thigh through ten times as often and takes the thrown particle's
605
- stopped speed from 6 to 4 m/s at two substeps, which is §18's one-sided finding again.
606
-
607
- What a ratio ramp does to a **resting** contact is what §18 predicted: every row violated by
608
- millimetres while a pile lands is at the ceiling for good, where §53's limit cycle is too large for
609
- silk to sleep — §64's ribbon pile never slept, the twirled skirt settled 17 mm from where it hung, the
610
- draped sheet was carried 4° less. The resolution is the one §18 named, a `γ < 1` for contacts:
611
- `γ_c = CLOTH_CONTACT_GAMMA = 0.8` decays a contact's two penalties per substep while the multiplier
612
- carries the load, and every scene sleeps as before (erratic at 0.7, slow at 0.5; decaying the normal
613
- penalty alone leaves the resting rope's sleep a coin toss, so both decay). Its cost is static
614
- friction's exactness: a sheet on a slope it should stick to creeps 0.2–0.3 mm in four seconds, and
615
- the marginal case, μ a tenth over the tangent, slides 0.55 m where the old ramp slid 0.19 — the ratio
616
- ramp without the decay made that case stick exactly, so the two together are a trade, not a free win.
617
-
618
- **Adaptive initialisation** is VBD's: seed the iteration at `x + h·v + h²·ã` where `ã` interpolates
619
- between zero and full external acceleration according to whether the previous step looked like free
620
- fall or like resting contact. Without it, a low iteration count over-stretches under gravity on the
621
- first iteration and the tethers spend the rest of the budget pulling it back.
622
-
623
- **Chebyshev acceleration** (`ω₁ = 1`, `ω₂ = 2/(2−ρ²)`, `ωₙ = 4/(4−ρ²ωₙ₋₁)`) is **not here**. M2
624
- built it as specified — including the gate the VBD paper requires, disabled for any particle in
625
- contact this step, since accelerating a vertex whose constraint set just changed is where the method's
626
- stability argument stops applying — and measured it against the first contacts the system has.
627
-
628
- It lost on every axis. On a sheet landing at 3 m/s it left the penetration inside the run-to-run
629
- spread and made the settled kinetic energy 1.5× worse; on a hanging cape it traded a third off the
630
- penetration for ten times the residual energy, which is a livelier cloth rather than a
631
- better-converged one; it costs 2–5%; and past `ρ = 0.9` it diverges, with `ρ = 0.99` reaching a
632
- non-positive-definite Hessian inside a second. A mechanism that costs, does not help, and detonates at
633
- the top of its own range is worse than its absence. `MEASUREMENTS.md` §21 keeps the grid so a future
634
- attempt starts from the numbers.
635
-
636
- **M7 added one block to the descent: the turn about a hold line** (`cloth_hold_line.js`). Every vertex
637
- update is a 3×3 block, and a cloth whose held particles lie on one line has a motion no such block can
638
- make — turning about the line as a rigid body. A vertex update moves one particle, so each adds
639
- `x_i × m·Δx_i` of angular momentum about the line, and the step stopped at a fabric's iteration count
640
- leaves a gradient along the turn. About a vertical line nothing restores it, and the warm start's
641
- `α·γ = 0.25` means a cloth at rest re-solves three quarters of its weight every substep and is never a
642
- fixed point: a flag on a pole turned for as long as it was run (§18).
643
-
644
- So the turn gets its own block, in the listing above: one Newton step of `E` along it, in the last
645
- iteration, after the sweep and before the dual update. Internal rows are invariant under the turn —
646
- their ends turn together, or one of them is on the axis — so only the inertial term and the unary rows
647
- enter. The last term of `H` is the path's own curvature, `d²x/dθ² = −r⊥`: near zero about a vertical
648
- axis, the pendulum's gravity stiffness about a horizontal one, and added only where positive so the step
649
- is never longer than the one without it. It is a descent step on the same energy, so §5.3 stands, and it
650
- changes no row, multiplier or constant. It runs when the held particles lie on one line at that substep,
651
- at two distinct points or more, to `2⁻¹⁶` of their largest coordinate. That is a runtime fact about the
652
- holds, and the predict, which carries every held particle, lists them on its way.
653
-
654
- What it does not cover: a single held point has three free turns and a cloth nothing holds has six, and
655
- neither is handled. On a line gravity does restore it has slowed some sleeps and kept one cloth awake
656
- (§18). Carrying the multiplier instead — AVBD's Eq. 18, or the reference code's post-stabilisation —
657
- stops the turn too, and fails six to ten of the cloth gates. `MEASUREMENTS.md` §66.
658
-
659
- ### 5.3 Why the update never explodes
660
-
661
- `H = (m/h²)·I + Σ_tension (fc/L)·(I − g gᵀ) + Σ_active k_c·(g gᵀ)`. The mass term is positive
662
- definite; every `k_c·g gᵀ` is a positive semidefinite rank-one term; every `(fc/L)·(I − g gᵀ)` with
663
- `fc > 0` is positive semidefinite too. So `H` is symmetric positive definite by construction — for any
664
- topology, any parameter values, any degenerate configuration, and regardless of how many rows are
665
- clamped out. `H⁻¹f` is always a descent direction.
666
-
667
- **Three things M0 measured changed this paragraph.** They are corrections to the mechanism, not to the
668
- goal; the full evidence is in `MEASUREMENTS.md`.
669
-
670
- **The geometric-stiffness term is kept where it is positive semidefinite, not dropped wholesale.**
671
- For a distance row `∂²C/∂x² = (I − g gᵀ)/L`, which is PSD exactly when the row is in tension, so
672
- `max(fc, 0)·∂²C/∂x²` can be added without touching the SPD argument — every term in `H` is still PSD
673
- on top of a PD mass term, and there is still no threshold. Dropping it *all* was measured to make the
674
- update overshoot sideways whenever `fc/L` passes `m/h²`, raising the vertex energy by 20% at a ratio
675
- of 4 and by a factor of 40 at a ratio of 7e4. This is not "adding the indefinite term back": the
676
- negative-semidefinite half, where the row is in compression, is still dropped, and dropping it there
677
- is conservative rather than dangerous. Note the term is added whether or not the row is at a clamp —
678
- a saturated row applies a constant-magnitude force that still swings as the row rotates.
679
-
680
- **The 3×3 solve is an LDLᵀ factorisation, not a cofactor inverse.** They agree in exact arithmetic
681
- and do not agree in `Float64`. A vertex carrying one very stiff row against a small mass term
682
- assembles an `H` with a condition number in the billions, and the cofactor determinant — a sum of
683
- triple products that has to cancel by that ratio squared — comes out **negative** on a matrix whose
684
- eigenvalues are all positive (measured: eigenvalues 0.12, 0.12, 5.4e8; determinant −4.8e8 against a
685
- true 7.75e6). The factorisation never forms those products; its pivots stayed positive throughout the
686
- fuzz, and their product is the determinant to full precision. The dev-build assertion checks the
687
- pivots, which *is* the positive-definiteness claim rather than a proxy for it.
688
-
689
- **Positive definiteness gives a descent direction, not a monotone step.** `Δ = H⁻¹f` satisfies
690
- `f·Δ > 0`, so the energy falls for a small enough step — and the update takes a full step of 1. Where
691
- the quadratic model stops resembling the energy, a full step overshoots and climbs: measured falling
692
- monotonically out to `t ≈ 0.001` of one step and then rising to 40× the starting energy at `t = 1`.
693
- The step is therefore capped at the **shortest row incident on the particle** — the distance over
694
- which "the direction to a neighbour" means anything, which is the mesh's own geometry rather than a
695
- tuned constant, and which never binds for cloth in ordinary motion. With all three corrections in,
696
- full-step energy increases on a realistic cape are 0.004% of updates and never exceed a fifth of the
697
- local energy scale; across deliberately absurd cloth they are 1.5% and never exceed 1.0. §7 P1 states
698
- the gate that follows from this.
699
-
700
- A dev-build tripwire mirrors `find_non_finite_physics_state.js`: a post-step sweep for non-finite
701
- particle state that names the first offender. It sits behind `assert.*` so it compiles out of consumer
702
- bundles — the engine ships raw source, and a check moved out of `assert.*` stops being strippable.
703
-
704
- ### 5.4 Colouring
705
-
706
- The particle graph is coloured so no constraint touches two particles of the same colour, using the
707
- greedy colouring in `core/graph/csr/csr_graph_color_greedy.js` and checked by
708
- `csr_graph_validate_coloring.js`, over the deduplicated particle-coupling CSR that
709
- `solver/cloth_particle_coupling.js` builds (the object `Graph` route was O(n²·degree): 134 ms → 3.5 ms
710
- on a 64×64 sheet, same colours). Bending stencils span four particles, so the graph is the mesh graph plus
711
- the bend diagonals — around 8–10 colours for typical garment topology. Colouring runs once, on that
712
- CSR, and is baked into the `ClothProxy` as colour ranges over a permuted particle order;
713
- for a chain it runs at link time on a graph of a few dozen nodes. Nothing colours per step and the
714
- runtime never sees a graph object.
715
-
716
- Self-collision rows appear and vanish per step and cannot be pre-coloured. They are accumulated into
717
- a per-particle scratch buffer and applied Jacobi-style within the sweep — the same mixed
718
- Gauss-Seidel/Jacobi treatment VBD uses for dynamic collisions.
719
-
720
- **M2 found that a *collider* contact needs none of that.** A contact against a collider is **unary**:
721
- one particle against a surface that is fixed for the substep. It couples no two particles, so there is
722
- no Gauss-Seidel versus Jacobi distinction to make and nothing to keep out of the colouring — it
723
- accumulates in the vertex update exactly like a leash row. What is true of a contact is that it *comes
724
- and goes*, which is a storage problem rather than an ordering one: M2's answer is a fixed four slots
725
- per particle inside `ClothState`'s own buffer, rebuilt each step and keyed on the collider's index
726
- handle so a multiplier survives the rebuild. Determinism comes from the *set* being built by one pass
727
- in a fixed order over a fixed table. The paragraph above stands unchanged for M4's rows, which really
728
- are pairs. `MEASUREMENTS.md` §18.
729
-
730
- ---
731
-
732
- ## 6. Everything is one constraint row
733
-
734
- Every feature in this system is a row in one table. A row is `(C(x), f_min, f_max, stiffness)`, and
735
- the solver in §5.2 knows nothing else about it.
736
-
737
- | Row | `C(x)` | `f_min` | `f_max` | Stiffness | Gives you |
738
- |---|---|---|---|---|---|
739
- | Stretch | `‖x_i − x_j‖ − L₀` | −∞ | +∞ | `k_stretch` | fabric that resists stretching |
740
- | Bend | quadratic stencil `Σ w_k·x_k` | −∞ | +∞ | `k_bend` | fabric that resists folding |
741
- | Slack | `‖x_i − s_i‖ − 2·slack·d_geo,i` | 0 | +∞ | hard | follows the animation; pins at `d_geo = 0` |
742
- | Tether (LRA) | `‖x_i − p_anchor‖ − d_geo,i` | 0 | +∞ | hard | no rubber-band stretch |
743
- | Backstop | `r_b − (x_i − b_i)·n_i` | 0 | +∞ | hard | never inside the body |
744
- | Collision | `(r_i + infl) − sdf_c(x_i)` | 0 | +∞ | hard | colliders push cloth |
745
- | Friction | tangential slip magnitude | 0 | `+μ·f_n` | hard | cloth grips |
746
- | Self-collision | `‖x_i − x_j‖ − (r_i + r_j)` | −∞ | 0 | `k_self` | cloth doesn't pass through itself |
747
- | Strain limit | `‖x_i − x_j‖ − L_max` | 0 | +∞ | hard | hard cap on elongation |
748
-
749
- Unilateral rows fall out of the clamp: **`[0, +∞)` admits only forces that reduce `C`, `(−∞, 0]` only
750
- forces that increase it**, and `(−∞, +∞)` is bilateral.
751
-
752
- **M1 corrected three of those clamps and the sentence above them.** An earlier draft glossed the rule
753
- as "`[0, +∞)` is push-only, `(−∞, 0]` is pull-only", which is true only relative to a particular sign
754
- of `C` — the collision row writes `C` as `r − sdf` and the distance rows write it as `‖·‖ − r`, so the
755
- same clamp means opposite things on them. Following the gloss rather than the rule gave Slack, Tether
756
- and Strain limit `(−∞, 0]`, which under the solver's own `f -= fc·g` is the force that pushes a
757
- distance row *apart*. Such a row does not bind weakly; it never binds at all, and it would drive a
758
- particle away from the leash centre it is supposed to hold it near. `cloth_leash_row.spec.js`
759
- demonstrates it directly and `MEASUREMENTS.md` §10 carries the derivation. **A row is hard exactly when its stiffness is `Infinity`** — the word in the table above,
760
- made literal. A hard row's penalty lives in `[k_min, k_max]·(m/h²)` and its multiplier carries the
761
- load; a soft row is a pure penalty at its material stiffness and carries no multiplier at all. M0
762
- measured why it has to be one or the other rather than a sliding scale (§5.2), and measured what the
763
- multiplier actually buys: not a smaller residual — at a fixed budget a stiff penalty is comparable —
764
- but **settling**. A hard 20-link chain comes to rest with 1e-14 J of kinetic energy where the same
765
- chain on a penalty of any stiffness sits in a limit cycle at 1e-4 J. That is §7 P5, and it is the
766
- augmented Lagrangian's real return.
767
-
768
- `C` for a bending row is a **vector** — the discrete mean-curvature vector — so that row carries three
769
- multipliers against one penalty, and its clamps are absent because a bending moment has no sign to be
770
- one-sided about. Every other row in the table is scalar.
771
-
772
- **M2 corrected the last two rows of that table, and both corrections come from rules the table already
773
- states.** The collision and friction rows carried named stiffnesses `k_contact` and `k_friction`, and
774
- by M0's own rule — a row is hard exactly when its stiffness is `Infinity` — that made both of them
775
- soft penalties. A soft row's entire force is `k·C` with `k` capped at `8·(m/h²)`, which is **4.6 N at
776
- one full thickness of penetration**; the hard row measured producing 11 N for a sheet arriving at
777
- 1 m/s and 376 N at 8 m/s. A soft contact holds a draped garment and fails every time one is put down.
778
- Both rows are hard.
779
-
780
- And friction's clamps were `±μ·f_n`, which is the *signed-slip* formulation — a row whose `C` is a
781
- displacement along a chosen tangent. This row's `C` is the **magnitude** of the slip in the contact's
782
- tangent plane, so `C ≥ 0` always and the negative half of that clamp names a force pulling the
783
- particle further from where it stuck. `[0, μ·f_n]` is the same physics, and writing it as a magnitude
784
- is what removes the tangent basis: a signed row needs two per contact, and any choice continuous frame
785
- to frame carries state, which is exactly what §7 P4(a) exists to keep out. Measured against Coulomb:
786
- the stick/slide transition sits on `tan θ` at 10°, 20°, 30°, 40° and 50°. `MEASUREMENTS.md` §18.
787
-
788
- **There is no separate attach row.** A pinned particle is a slack row with `d_geo = 0`: the constraint
789
- becomes `‖x_i − s_i‖ ≤ 0` under a pull-only clamp, which welds it. One row, one code path, and the
790
- distinction between "pinned" and "loosely following" is a number rather than a branch. (The gradient of
791
- `‖x − s‖` is undefined at `x = s`; the row is skipped below an epsilon, which is the standard guard and
792
- is also exactly when it has nothing to do.)
793
-
794
- **M7: and there still is none, but a pin is not a row either.** The anchor was pinned by inverse mass
795
- zero all along, and that is what a hold is: a kinematic particle carried to where its holder puts it,
796
- an equal share of the way at each substep (§19). Both leash rows now measure from a **particle** —
797
- `ClothState#leash_source` and an offset, re-centred every substep after the held particles move —
798
- because a centre written once a step sits a step behind a held particle that is moving, and a hard
799
- row measured from a stale point fights the hold. The tether's centre is the nearest held particle,
800
- not "the anchor"; a particle nothing holds has both rows at radius `Infinity`, skipped before any
801
- arithmetic.
802
-
803
- **Slack and tether are different questions, and a cape needs both.** Slack bounds distance to the
804
- particle's *animated position*; the tether bounds distance to the *anchor*. Slack alone implies a
805
- tether bound of `(1 + slack)·d_geo` — enough to stop a catastrophe, not enough to stop visible rubber —
806
- and for a garment with no meaningful animated pose to follow (a flag, a curtain, where `slack` is near
807
- 1 by necessity) the tether is doing all the inextensibility work on its own.
808
-
809
- This mirrors the doctrine already written into `physics/constraint/solve_constraints.js`, whose
810
- 6-DOF joints are "one row model, parameterised by mode", with LOCKED and LIMITED differing only in
811
- bias and clamp. Same idea, position level instead of velocity level. A reader who knows one knows the
812
- other, and a new cloth feature is a new row rather than a new pass.
813
-
814
- Two rows deserve a note.
815
-
816
- **Bend is the quadratic model**, not a dihedral angle. Its Hessian is constant for a fixed rest shape,
817
- so the stencil weights are precomputed at build time, `C` is linear in `x`, and `g` is a constant read
818
- from the proxy — bending costs almost nothing per iteration, and M0 measured it at an eighth of a
819
- stretch row per visit despite touching twice as many particles. The weights annihilate the rest shape
820
- and every affine image of it, which is every in-plane deformation to first order; they do *not*
821
- annihilate an arbitrary planar configuration, and that is the precise content of the near-isometric
822
- assumption below. More importantly, it has **no singularity
823
- at 0° or 180°**. Dihedral bending needs a normal, an angle and a sign, all of which degenerate when two
824
- triangles become coplanar or invert, and that degeneracy is a classic source of exactly the twitching
825
- this design exists to eliminate. The cost is that the quadratic model assumes a near-flat rest shape
826
- and near-isometric deformation; a garment with a strongly curved rest shape it wants to hold will
827
- under-resist. §18 carries that.
828
-
829
- **Stretch is per-edge.** Edge springs on a triangle mesh do not resist shear on their own; the bend
830
- rows and the slack bound between them keep the mesh from collapsing in practice, and a triangle-area
831
- row is available for the cases where they do not. Full in-plane anisotropy is a non-goal (§2).
832
-
833
- **A chain bends through a skip row, not through the quadratic stencil.** M1 found there is nothing to
834
- build a hinge on: the stencil is a discrete mean curvature over a *surface*, four particles across the
835
- diamond either side of a shared edge, and a rope has no diamond — `bt_edge_read_diamond` declines a
836
- wire edge for exactly that reason and is right to. What a chain gets instead is an ordinary stretch
837
- row between a node and its **grandparent**, at whatever distance the authored pose puts them. That is
838
- this section's own doctrine applied once more, it costs no new row kind, and it keeps both properties
839
- the quadratic model was chosen for: no normal, no angle and no sign, so nothing degenerates at 0° or
840
- 180°, and a fold is high energy rather than a singular configuration. It also does something the
841
- quadratic stencil explicitly cannot (§18) — because its rest length is read from the authored pose
842
- rather than assumed flat, **it holds a curved rest shape**. `MEASUREMENTS.md` §9.
843
-
844
- **And a stretch row has to be hard, which this table's `k_stretch` does not say.** M0 measured that a
845
- *penalty* stiffer than about `8·(m/h²)` makes the step add energy rather than dissipate it — the cloth
846
- sustains a limit cycle forever, and raising the iteration count shrinks it without removing it. For a
847
- half-gram particle at 60 Hz × 2 substeps that boundary sits near 60 N/m, which is a rubber band rather
848
- than a fabric, so every stiffness an author would reach for is on the wrong side of it. The augmented
849
- Lagrangian is therefore not a refinement over a stiff penalty for this row: it is the only formulation
850
- of it that comes to rest, because its penalty is capped at the top of the dissipative band and the
851
- multiplier carries the rest. M1's normalised `ClothDynamics#stretch` should map to `Infinity` with a
852
- strain-limit row governing extension, not to a large finite `k`. `MEASUREMENTS.md` §6.
853
-
854
- **M1 built that and measured what the strain-limit row is actually worth, and the answer is "a
855
- third".** It removes 34–38% of the peak over-stretch, consistently, at every budget — and it **never
856
- reaches its own allowance**: at `stretch = 1` it asks for 0.1% of strain and delivers 5.9% at one
857
- iteration per substep, 2.2% at 2×2 and 0.9% at 2×4. The reason is structural. A strain-limit row is a
858
- *second hard row on the same edge with the same gradient*, so it competes for the same trust region
859
- and the same penalty band as the fabric row it is backing up; it can add force in a direction force is
860
- already being added in, and nothing more. Below about `stretch = 0.35` its allowance is looser than
861
- what the fabric row does unaided and it is simply inert.
862
-
863
- So **`stretch` is the weakest field in §3.3's parameter set**, and the honest statement is that at low
864
- budgets the bound belongs to the budget rather than to the fabric. The row is worth its 13–15% of a
865
- garment frame where the residual is visible and is not worth it where the residual is already around
866
- one per cent, which is why it is a flag — `ClothDynamicsFlags.StrainLimit` — rather than always on.
867
-
868
- **And the flag defaults to *set*, which is the opposite of what §3.3 used to show.** `ClothDynamics`
869
- starts at `ClothDynamicsFlags.StrainLimit`, so a garment that says nothing gets the row. Of the four
870
- library entries at 2×4, three clear it — silk, cotton and hair, and they clear it by writing
871
- `flags: ClothDynamicsFlags.SelfCollision`, which *replaces* the default rather than adding to it — and
872
- **`CLOTH_ROPE` does not**: it sets no flags at all, so it keeps the default and runs the strain-limit
873
- row at 2×4. That is the right answer for a rope, whose `stretch` is its most characteristic property
874
- and whose forty particles make the row free (`cloth_dynamics_library.js` says exactly that), but "the
875
- library entries at 2×4 clear it" was never true of it. `MEASUREMENTS.md` §11.
876
-
877
- ---
878
-
879
- ## 7. Pathology budget
880
-
881
- The requirement was cloth that does not fold up, get stuck, or spaz out. Each failure mode below gets
882
- a named mechanism and a spec that fails without it. This table is the acceptance criterion for the
883
- whole system, and every row's spec is a standing gate, not a one-off.
884
-
885
- **P1 — Blow-up, NaN, unbounded velocity.**
886
- *Mechanism:* SPD Hessian by construction (§5.3), so every update is a descent direction; the step
887
- capped at the shortest incident row so a full Newton step cannot leave the region its model describes;
888
- a per-particle velocity clamp; the `assert.*` non-finite tripwire.
889
-
890
- **M1 landed the clamp and it is not derived from `thickness`.** This row asked for one "derived from
891
- `thickness` and `h`", and `thickness` turns out to be the wrong length scale: it is a *contact*
892
- radius, so a clamp built from it is a **tunnelling** bound, and a tunnelling bound sits below ordinary
893
- cloth speeds rather than above them. Silk is 4 mm thick, so "a particle may not cross its own radius
894
- in a step" is 0.24 m/s at 60 Hz — against the 29 m/s the same silk reaches behind a sprinting anchor.
895
- Any multiple large enough to stop the clamp being a *behaviour* for silk is large enough to be
896
- meaningless for leather, whose thickness is five times silk's and whose speeds are not.
897
-
898
- The scale that works is the solver's own **correction authority**,
899
- `shortest_row · substeps · iterations / dt` — the trust region caps a vertex update at the shortest
900
- row incident on the particle, so that product is the whole of what one fixed step can undo. M1
901
- measured a cloth losing its bounds past it (§7 P2 below), and the clamp is four times it: a bound on
902
- the solver being asked to do more than it can, scaling with the mesh and the budget rather than with a
903
- radius nothing reads until M2. The smallest headroom over it in the library is 5.7×. M2 still needs
904
- the tunnelling bound — it is a different number for a different job and it arrives with the contacts.
905
- `MEASUREMENTS.md` §13.
906
- *Spec:* fuzz over random topologies and random `ClothDynamics` values including absurd ones, 10,000
907
- steps, assert every particle finite. **M0 corrected the second half of this.** "The per-vertex energy
908
- never increases across an update" does not follow from an SPD Hessian and is not true — SPD buys a
909
- descent direction, and the update takes a full step (§5.3). What is asserted instead, and holds:
910
- every LDLᵀ pivot positive on every update (which *is* positive definiteness, checked where it is
911
- used); nothing non-finite across 10,000 steps of absurd cloth; and full-step energy increases bounded
912
- and rare — 0.004% of updates on a realistic cape, never above a fifth of the local energy scale, and
913
- 1.5% / 1.0 across the absurd fuzz. **The gate is those figures doubled, not those figures**: the spec
914
- holds 3% / 2 on the absurd fuzz and 0.01% / 0.5 on the cape, which is twice each measurement rather
915
- than the measurement itself. The last sentence of this paragraph used to claim otherwise. The margin
916
- is deliberate — a full step's energy behaviour is a distribution over a fuzz corpus and a gate pinned
917
- to its own sample is a gate that fails on an unrelated change — but "a change that makes it worse
918
- fails" is true only of a change that makes it *twice* as bad.
919
-
920
- **P2 — Over-stretch, the rubber cape.**
921
- *Mechanism:* long-range attachments (Kim et al. 2012). Each particle carries the geodesic distance to
922
- its nearest fixed particle, baked once, enforced as a hard unilateral row. Because it is hard rather
923
- than a penalty, one iteration enforces it almost exactly — this is the row that makes low iteration
924
- counts viable.
925
- *Spec:* a 30-link × 3-strand rig at one iteration per substep, dropped through ninety degrees; assert
926
- `max‖x − p_anchor‖ ≤ d_geo·(1+ε)` for every particle across 600 steps, and assert the same rig without
927
- the rows does not hold.
928
- *Measured, M1:* **1.0155 with the tether against 1.4650 without**, at 1×1. Four times the budget takes
929
- the untethered rig to 1.0199, which is still worse than one iteration *with* tethers — so the tether
930
- is not a cheaper way to buy convergence, it answers a question the per-edge rows cannot.
931
-
932
- *Limit, stated, and M1 found it:* **the bound is conditional on the solver's correction authority.**
933
- The trust region caps a vertex update at the shortest row incident on the particle (§5.3), so one
934
- fixed step can move a particle at most `shortest_row · substeps · iterations`, and a displacement
935
- larger than that is not one the tether has been given a chance to answer for. Measured: a 30-link
936
- chain kicked at 16 m/s against 3.0 m/s of authority reaches **6.9× its own reach**, and the same kick
937
- against 24 m/s of authority reaches 1.06×. Both recover — the excursion is transient, which is P4(c)
938
- holding — but "no particle is ever further from the anchor than `d_geo`" is true only while the
939
- velocity clamp of P1 keeps a particle inside what the solve can undo, and that is the reason the clamp
940
- is built out of the same quantity. `MEASUREMENTS.md` §10.
941
-
942
- **P3 — Tunnelling through the character.**
943
- *Mechanism:* three layers. (a) Collision rows evaluate against the collider pose *interpolated to the
944
- substep*, from the previous and current poses the index caches — cheap swept collision. (b) The slack
945
- bound stops a particle long before it can travel far enough to cross a limb. (c) Substepping shrinks
946
- per-step travel.
947
-
948
- **M2 found (a) is two mechanisms and only had one of them.** Interpolating the poses does nothing
949
- unless the contact **set** covers the sweep as well, and it did not: `cloth_contact_find` runs once
950
- per step against the *end-of-step* pose, so a collider that crossed the cloth within the step is
951
- already past it and no row is ever created. Measured on a capsule driven through a hanging sheet at
952
- 20 m/s: **zero contact rows for the whole pass**, with the sheet untouched — not tunnelled through so
953
- much as never noticed. The fix is one subtraction per record per step, folding the collider's own
954
- travel into the discovery reach, and it makes 2 to 40 m/s behave alike (94.8 rows a step at 20 m/s
955
- against 0.0 without). `MEASUREMENTS.md` §20.
956
-
957
- **And the interpolation is of a translation and a quaternion, not of two matrices.** §8.2 said
958
- matrices; a component-wise lerp of two rotation matrices is not a rotation, and halfway through a turn
959
- of `θ` it scales the shape it poses by `cos(θ/2)`. At the half-radian per step
960
- `CLOTH_ANCHOR_ROTATION_LIMIT` already calls a spin, a 25 cm collider loses 7.8 mm — twice a silk
961
- garment's thickness — halfway through every step. `MEASUREMENTS.md` §19.
962
-
963
- **Post-M6 found the mirror case, and M2 had argued it away rather than measured it.** (a) folds the
964
- *collider's* travel into the discovery reach and nothing folds in the *particle's*, so a **static**
965
- collider is only ever discovered within `2·(thickness + inflation)` — 16 mm for a garment, which is
966
- half a metre a second of approach. The docblock said anything faster was already bounded by §7 P1's
967
- velocity clamp; that clamp is `4 · shortest_row · substeps · iterations / dt`, about **154 m/s** for a
968
- garment, and everything in between was uncovered. Measured, one particle at 8 mm thrown at a static
969
- 3 cm capsule with no gravity at 2 × 4: **no row is ever created before the particle is already
970
- inside, at any speed**, and from **3 m/s** the row the next step finally makes is evaluated on the far
971
- side and pushes it the rest of the way out — which is P4(a)'s orientation-over-history choice
972
- collecting its bill when discovery is late. The reach now carries `|v_i|·dt` per particle as well.
973
- That is free at rest and 0.8% of a step for a cloth falling at 3 m/s, and `cloth_library_bench.mjs`
974
- cannot see it because the bench has no colliders. `MEASUREMENTS.md` §54.
975
-
976
- *Limit, stated:* all three are swept in translation first. A collider spinning fast enough that a
977
- point on its surface travels more than its own radius within one substep can still be missed, because
978
- the discovery margin is built from the *centre's* travel. Full CCD for cloth is out of scope.
979
-
980
- *And the second limit, which post-M6 measured rather than assumed:* **discovery being right is
981
- necessary and not sufficient.** A row that exists is still evaluated at substep positions, so a
982
- particle crossing a whole collider inside one substep has a row that never sees it touching. Swept,
983
- in half-metre steps from 1 to 12 m/s, the particle is stopped up to **6.0 m/s at two substeps and
984
- through the whole range at four** — a clean threshold, and it is `ClothDynamics#substeps` rather than
985
- the reach. This row has always said "more substeps in the affected `ClothDynamics`"; that is where it
986
- lands.
987
- *Spec:* a capsule driven through a hanging sheet at 2 to 40 m/s, swept against unswept; assert the
988
- swept pass creates rows at every speed and that no particle spends a step more than half a collider
989
- radius inside. The mirror — a particle thrown at a *static* capsule from 1 to 10 m/s, with a step
990
- length of zero standing in for the unswept side — is in `cloth_contact_row.spec.js`.
991
- `cloth_collision.spec.js` carries the ECS-level companion of both.
992
-
993
- **P4 — Folds, tangles, and stays stuck.** The named requirement, so four mechanisms:
994
-
995
- *(a) No history-dependent normals.* Self-collision and collision normals are derived from the
996
- proxy's own surface orientation and the collider's SDF gradient at the current position — never from
997
- "which side was this on last frame". A history-based scheme latches onto a wrong side and stays
998
- wrong indefinitely; an orientation-based one resolves a tangle as soon as the surrounding geometry
999
- permits. This is the "predefined surface orientation" branch of the untangling literature, chosen
1000
- specifically because it has no state that *can* get stuck.
1001
-
1002
- *(b) No degenerate fold state.* Quadratic bending (§6) means a fully inverted 180° fold is not a
1003
- singular configuration — it is simply high energy, and high energy relaxes.
1004
-
1005
- *(c) The slack bound bounds the reachable set.* Every particle is under a hard leash of radius
1006
- `slack·d_geo,i` around its animated position, so the set of configurations the simulation can occupy
1007
- is a bounded neighbourhood of the animation. **Error cannot accumulate** — however bad a step is, the
1008
- next step starts inside that same neighbourhood, and there is no drift mode that walks the cloth
1009
- somewhere it cannot return from. That is the guarantee, and note what it does *not* claim: the
1010
- animated pose itself can be imperfect. It is collision-free in the bind pose (the build validates
1011
- exactly that, §11.3, and fails loudly with the offending vertex and collider named), but an extreme
1012
- animation pose can skin a garment slightly into a thigh. When that happens the attractor is slightly
1013
- wrong and the cloth is slightly wrong with it — bounded and self-limiting, not divergent.
1014
-
1015
- This is also why `slack` is the one number worth reaching for when a garment misbehaves: it is
1016
- literally the size of the neighbourhood the simulation is allowed to explore. **M1 note:** with §3.4's
1017
- corrected factor of two, `slack = 1` makes that neighbourhood the whole of what the tether already
1018
- allows, so the guarantee at `slack = 1` is the tether's rather than the leash's — which is the right
1019
- answer for a rope, and is why every chain in the library sits there.
1020
-
1021
- **M2 note, and it is a limit rather than a gap.** The escape hatch below watches a *residual*, and
1022
- there is a configuration whose residual is zero and which is still stuck: a rope authored exactly
1023
- along an axis, compressed by a plane whose normal is exactly that axis, has no lateral component
1024
- anywhere in the system and folds into an accordion in which every link is at its rest length. That
1025
- is an exact equilibrium, so nothing has anything to act on. Measured: an 8-link rope on a floor
1026
- stops 18 cm above it and stays; **one part in a thousand of lateral lean** — a millimetre across a
1027
- 10 cm link — and it lies down. Recorded rather than fixed, because the fix is noise injected by a
1028
- solver whose whole design commitment is that it needs none, and no authored scene is that
1029
- symmetric. `MEASUREMENTS.md` §22.
1030
-
1031
- *(d) A bounded escape hatch. **NOT BUILT, and this row is the design for one rather than a
1032
- description of one.*** If a particle's constraint residual stays above a threshold for N consecutive
1033
- steps, it blends to its animated position over a short window. Local, bounded, and counted in
1034
- `ClothDiagnostics` so that a garment relying on it is visible rather than silently rescued.
1035
-
1036
- **It has been deferred at every milestone that could have built it** — M1 for want of a residual
1037
- monitor, M2 to M4 on the grounds that self-collision is where a particle gets *pinched* rather than
1038
- merely held, M4 onward as still open — and it is deferred deliberately rather than forgotten: M2
1039
- measured the one stuck configuration whose residual is **zero** (§7 P4's own note, `MEASUREMENTS.md`
1040
- §22), which is the quantity this row watches, so the mechanism would not have caught the case that
1041
- motivated looking for it. The sentence that used to stand here said "on by default with a
1042
- conservative threshold", which read as a shipped default and was never true of any build.
1043
- `ClothDiagnostics` is not built either; every reference to it in this document is a design for one.
1044
- What *is* shipped and does the job an author reaches for is `Cloth#blend` (§3.1), which is the same
1045
- blend applied deliberately instead of automatically.
1046
-
1047
- *Spec:* deliberately invert half a sheet through itself, run, assert the residual falls below threshold
1048
- and the mesh is self-intersection-free within N steps; separately assert the escape hatch is not
1049
- triggered by a correctly authored garment under normal animation.
1050
-
1051
- **P5 — Jitter at rest.**
1052
- *Mechanism:* the multiplier decay `α·γ < 1` bleeds accumulated force out of a resting configuration so
1053
- it cannot ring; per-cloth sleep when the maximum particle displacement over a window falls below a
1054
- threshold; wake on collider motion, anchor motion, wind change, a collider arriving or leaving, or a
1055
- `ClothDynamics` change.
1056
-
1057
- **Post-M6 note, and "wake on collider motion" was the wrong question.** Motion is what the test asked
1058
- and it is not what a sleeping cloth needs to know. `ClothColliderSystem#link` **places** a new
1059
- collider — previous pose equal to current, deliberately, so its first step sweeps nothing — so a
1060
- collider *spawned* inside a sleeping cloth has not moved and never woke it. Measured: a 6-link rope
1061
- settled and asleep, a sphere of radius 0.2 spawned around its lower half, still asleep 300 steps later
1062
- with **4 of its 6 links inside**. §1's first commitment names exactly this case. The test now also
1063
- compares the **set** of handles the broadphase returns with the set the cloth last stepped against —
1064
- one integer per record, against a pose comparison that was already there — so a collider appearing or
1065
- leaving wakes it whatever the poses say. Its own limit, stated: a `PlaneShape3D`'s AABB is unbounded,
1066
- so a plane is in every cloth's set the moment it exists and every sleeping cloth wakes once when one
1067
- appears anywhere in the scene. It finds nothing, and sleeps again thirty steps later.
1068
-
1069
- **Post-M6 note, and it is this row's own sentence coming true two milestones late.** "Maximum particle
1070
- displacement over a window" is what this row has said since M0 and it is not what M1 built: M1's test
1071
- is the largest particle **speed** the step produced, held under `CLOTH_SLEEP_SPEED_RATIO · reach` for
1072
- `CLOTH_SLEEP_STEPS`. On a free-hanging chain the two agree. On a cloth **resting on a collider** they
1073
- do not, and the gap is total: four of the seven library fabrics — cotton on a floor with or without
1074
- friction, leather and rope on a floor with it — **never sleep at all**, where the same rope with no
1075
- collider under it sleeps at step 283. The cause is not friction, which the frictionless column rules
1076
- out; it is that a contact's penalty ramps by `β·|C|` and `γ = 1` never brings it down, so what a
1077
- resting cloth settles into is a **bounded limit cycle** rather than a rest state: measured on cotton,
1078
- a tenth of a millimetre of travel a step — invisible — at a peak speed of 1.5e-2 m/s, eighteen times
1079
- the threshold. Displacement tells those apart and an instantaneous speed cannot. The sleep test now
1080
- reads displacement from a window anchor, at `CLOTH_SLEEP_DISPLACEMENT_RATIO = 5e-4` of reach, chosen
1081
- to be no *looser* than the speed test it replaces rather than the fastest value that works; every
1082
- entry on a floor and on a sphere then sleeps within **961 steps**, and the free-hanging column gets
1083
- uniformly faster rather than slower. The limit cycle itself is recorded and not fixed — `γ` is
1084
- unmeasured-by-decision (§5.2) and a tenth of a millimetre is not a rendering problem.
1085
- `MEASUREMENTS.md` §53.
1086
- *M0 found the gap, and it is wider than "nothing is damping it".* The mechanism reaches **hard** rows
1087
- only, because a soft row has no multiplier to decay — and a soft row whose penalty exceeds about
1088
- `8·(m/h²)` does not merely fail to dissipate, it makes the step **add** energy. Measured on a hanging
1089
- sheet: total mechanical energy climbing +6.8 J against a system whose entire gravitational potential
1090
- is 1.2 J, sustained indefinitely, and shrinking but never vanishing as iterations or substeps go up.
1091
- Below that ratio the same row is dissipative and settles. What stops it is `ClothDynamics#damping`,
1092
- which arrives with the component in M1 — so **damping is load-bearing for P5 rather than a taste
1093
- control**, and the M1 spec has to gate on a soft cloth actually settling. `MEASUREMENTS.md` §6.
1094
- *Spec:* a settled cloth reaches sleep within a bounded number of steps and its positions are bit-identical
1095
- across the following 300.
1096
- **M6 note: a cloth also wakes on the air changing, and the test has one sample in it.** §10's "sleep
1097
- wakes on a wind change" is the same shape as the collider test beside it — ask a question three orders
1098
- of magnitude cheaper than the step it decides whether to skip — and the question is the air at the
1099
- cloth's own centre against the air the last *applied* step saw. The last applied rather than the last
1100
- sampled, so a gust that builds slowly accumulates instead of being reset by every step that declined
1101
- to act on it. *Its limit, stated:* one point. A gust that reaches only the hem of a sleeping banner
1102
- and leaves its middle still will not wake it. The collider test has no such gap because a collider's
1103
- pose is seven exact numbers and a field is a continuum.
1104
-
1105
- *M1 landed both halves and measured them.* Every library fabric, on a 12-link chain set swinging at
1106
- 3 m/s, sleeps: **220 steps for denim, 770 for silk**, everything else between, and the order is the
1107
- damping order. (Those two figures are the speed test's; the post-M6 note above replaced it, and every
1108
- entry sleeps sooner under the displacement window than it did under the speed one.) Sleep stops the **write-back** as well as the step, which is what makes "bit-identical"
1109
- a fact rather than a hope — a pose that is not rewritten cannot drift, and one rewritten from unchanged
1110
- inputs is only *probably* identical. A cloth wakes on anchor motion, on a `ClothDynamics` change, on a
1111
- `blend` change, and on an explicit `ClothSystem#wake` for anything it cannot see.
1112
-
1113
- And the damping gate, on the configuration §6 measured the limit cycle in — a 16-link rope with its
1114
- stretch rows forced soft at 1e5 N/m, a ratio of about 350 — after 1,500 steps: **2.90e-3 J undamped,
1115
- 1.28e-6 J at the library's own `damping = 0.12`**, 1.82e-12 J at the top of the range. Three orders of
1116
- magnitude from a field the design first described as a taste control. Note what damping does *not* do:
1117
- it does not stop the step adding energy, it removes energy faster than the step adds it, which is why
1118
- the numbers keep falling to `damping = 1` instead of hitting a floor. `MEASUREMENTS.md` §12.
1119
-
1120
- **P6 — Teleport and cut.**
1121
- *Mechanism:* anchor motion above a threshold in one step is a teleport — `Reset` (re-seed at the
1122
- animated pose, zero velocity) or `Keep` (carry the shape across). Default `Reset`, followed by a short
1123
- stabilisation window in which the velocity limit ramps back from nothing, so the first steps after a
1124
- reset cannot produce a whip.
1125
- *Spec:* teleport 1 km mid-swing; assert bounded velocity every step and convergence to the animated pose.
1126
- *Measured, M1:* `Reset` peaks at **0.000 m/s** across the 240 steps after the jump and lands on the
1127
- animated pose; `Keep` peaks at 5.58 m/s, which is the swing it was already carrying, and shifts by
1128
- 0.078 m in local coordinates against 1.489 m for the reset. The threshold is `max(4·reach, 2 m)` per
1129
- step, and a 9 m/s sprint does not reach it for any library fabric.
1130
-
1131
- **M1 named the mode differently and found `Keep` is free.** There is no `ClothTeleportMode`: the choice
1132
- is binary, so it is `ClothFlags.TeleportKeep`, one bit on the component that already carries the
1133
- cloth's own switches — a mode enum for two values is the discriminator field §3.1 exists to avoid. And
1134
- `Keep` is *literally* free: MagicaCloth2 applies the anchor delta rigidly to every particle, but in a
1135
- frame anchored to the cloth's own entity the particles are already anchor-relative, so "apply the
1136
- delta" is doing nothing. The implementation is a `return`.
1137
-
1138
- **M7: a holder can jump too, and it is measured in the frame.** A rope tied to a ship that is moved
1139
- across the map has a frame that did not move and a holder that did. The test is now the frame's jump
1140
- in the world *or* any holder's jump in the frame, against the same threshold. `Reset` puts every
1141
- particle on the animated pose hung from where the holders now are, held particles on their targets so
1142
- the jump is taken between steps rather than dragged through one; `Keep` does nothing for the frame, as
1143
- before, and for a holder applies that holder's own jump to what hangs from it. Measured: a hand holding
1144
- a rope's top, moved a kilometre in one step, resets once and peaks under 5% of the velocity clamp
1145
- (`cloth_pin.spec.js`).
1146
-
1147
- **P7 — Fast locomotion: whipping, lag, centrifugal spray.**
1148
- *Mechanism:* the simulation runs in a **local frame anchored to the cloth's own entity**, with
1149
- `ClothDynamics#inertia` controlling how much of the anchor's world motion the cloth is allowed to
1150
- feel, plus anchor linear and angular speed limits. This is MagicaCloth2's `InertiaConstraint` idea and
1151
- it is the correct one: it makes "character sprints, cape behaves" a default rather than a tuning
1152
- exercise.
1153
- *Spec:* drive the anchor along a step-function velocity profile; assert bounded particle speed throughout.
1154
- *Measured, M1:* a square wave of ±9 m/s, one second a phase. Peak local particle speed runs 11.3 to
1155
- 29.3 m/s across the library against clamps of 96 to 288, so the smallest headroom is **5.7×**; nothing
1156
- teleports, and nothing leaves its own reach by more than 0.35%. The anchor limits are derived rather
1157
- than authored — half a reach of travel and half a radian of rotation per fixed step, both expressed
1158
- against the cloth's own geometry so a 3 cm pendant and a 30 m mooring line get the same treatment.
1159
- `inertia` measures monotone and exactly zero at zero: 0, 0.20, 0.37, 0.52, 0.65 m of tip lag across
1160
- its range, which is what makes it a dial rather than a switch with a meaningless middle.
1161
- `MEASUREMENTS.md` §13.
1162
-
1163
- **M7: the frame is the only motion this softens.** A holder moving *relative to the frame* moves its
1164
- particles in full — that is what holding means — so P7's limits protect a cloth whose holders ride its
1165
- frame, which is every garment and every rope hung from its own top. A cloth held by something that
1166
- moves independently — a banner between two characters — feels that motion unsoftened, and the frame
1167
- it should ride is the author's choice, as Chaos Cloth's reference bone is. What bounds such a cloth is
1168
- P1's clamp and P2's authority: measured, a hand dragging a rope at four times what one step can
1169
- correct stays under the clamp every step and is back inside 1% of its tether within ten seconds of
1170
- stopping (`cloth_pin.spec.js`).
1171
-
1172
- **P8 — Degenerate authoring.**
1173
- *Mechanism:* build-time validation, not runtime tolerance. The proxy builder welds duplicates, drops
1174
- zero-area triangles, and refuses non-manifold and inconsistently-wound configurations. The existing
1175
- `bt_edge_read_diamond` already declines every configuration a bending stencil cannot be built on —
1176
- wire edges, boundary edges, non-manifold edges, non-triangular neighbours, inconsistent winding, and a
1177
- "diamond" that is really one doubled triangle — so the degenerate cases are rejected by a function that
1178
- has already thought about them rather than by new code.
1179
- *Spec:* a corpus of deliberately broken meshes, each producing a specific diagnostic and no proxy.
1180
-
1181
- ---
1182
-
1183
- ## 8. Collision against the physics world
1184
-
1185
- ### 8.1 The index
1186
-
1187
- `ClothColliderSystem` maintains `ClothColliderIndex`: a flat array of packed records, each holding the
1188
- shape kind and its parameters, the world matrix, its cached inverse, the previous step's matrix, the
1189
- world AABB, friction and inflation. `OccluderBody` in the acoustic simulator is the same record and is
1190
- the shape to copy — including its precondition that the pose is rigid, so a distance in the shape's
1191
- local frame equals a distance in world space.
1192
-
1193
- Per step, `ClothSystem` queries the index with the cloth's own fattened AABB and gets the overlapping
1194
- records. That set is packed into shared memory for the worker; nothing on the far side of the worker
1195
- boundary is an object.
1196
-
1197
- ### 8.2 Evaluating a collision
1198
-
1199
- The collision row needs `sdf_c(x_i)` and its gradient. Bring the particle into the collider's local
1200
- frame, evaluate the closed-form SDF and gradient there, rotate the gradient back.
1201
-
1202
- **The pose is a translation and a quaternion, and there is no cached inverse.** A rigid pose inverts by
1203
- conjugating its quaternion and subtracting its translation, which costs less than storing an inverse
1204
- and is exact rather than cached — and, more importantly, it is the only form the sweep can
1205
- interpolate. This paragraph originally said the sweep comes from "interpolating between the previous
1206
- and current **matrices**", and a component-wise lerp of two rotation matrices is not a rotation: it
1207
- scales the shape it poses by `cos(θ/2)`, which at the half-radian per step the anchor limit already
1208
- calls a spin is 7.8 mm off a 25 cm collider — twice a silk garment's thickness, every step.
1209
- `MEASUREMENTS.md` §19.
1210
-
1211
- **And the sweep is two things, not one.** Interpolating the poses is useless unless the contact *set*
1212
- also covers the sweep; §7 P3 carries what that cost and what it now does.
1213
-
1214
- **Supported kinds are analytic primitives**: sphere, capsule, box, cylinder, and a plane. That
1215
- restriction is universal in shipping cloth systems — MagicaCloth2 offers sphere, capsule and plane;
1216
- Chaos Cloth adds tapered capsules and convex hulls — and it is what makes the inner loop a handful of
1217
- arithmetic instead of a BVH descent. A `Collider` carrying a mesh or heightmap shape is **skipped with
1218
- one warning naming the entity**, and the documented answer is to add primitive `ClothCollider` children,
1219
- which is what an author would do anyway for a character.
1220
-
1221
- The engine's shapes already carry the closed-form SDFs — `CapsuleShape3D#signed_distance_at_point`,
1222
- `BoxShape3D#signed_distance_at_point` and the rest are exact and inline. Their *gradients* are not:
1223
- `signed_distance_gradient_at_point` on capsule, box and cylinder all fall back to
1224
- `compute_signed_distance_gradient_by_sampling`, which is four SDF evaluations and a normalise. That is
1225
- fine for voxelising a fluid obstacle once per step. It is not fine in a per-particle, per-iteration
1226
- loop, so §14 makes the analytic gradients shared code that both sides use.
1227
-
1228
- ### 8.3 Friction
1229
-
1230
- Coulomb friction as a row on the same contact, with `μ` combining `Collider#friction` with
1231
- `ClothCollider#friction_scale`.
1232
-
1233
- **The combine rule is *not* `physics/contact/combine_material.js`, and this paragraph used to say it
1234
- was.** Nothing in `cloth/` imports that module. `combine_material` answers a different question — how
1235
- two *bodies'* materials combine into one contact coefficient, which is a `sqrt(μ₁·μ₂)`-shaped rule
1236
- over a symmetric pair — and cloth's question is not symmetric: one side is a collider with a material
1237
- and the other is a garment with an opt-in scale on top of it. `ClothColliderSystem` therefore packs
1238
- `Collider#friction · ClothCollider#friction_scale` into the index record, one multiply at link time,
1239
- and a `friction_scale` of 1 means "whatever this surface already is". The *claim* the old sentence was
1240
- making — that cloth and rigid bodies agree about what a surface feels like — is still true and is why
1241
- `Collider#friction` is the base: what is not true is that the agreement comes from sharing a function.
1242
- §13's reuse ledger carried the same wrong entry.
1243
-
1244
- **M2 made three of those words exact.** The clamp is `[0, μ·f_n]` rather than `±μ·f_n`, because the
1245
- row's `C` is the *magnitude* of the tangential slip — §6 carries the derivation and what writing it
1246
- that way buys. `f_n` is the collision row's force **this iteration**, not its converged multiplier
1247
- from this step: the multiplier is one iteration stale and therefore exactly zero on a contact's first
1248
- iteration, so at the one iteration per substep §5.1 says the solver must be usable at, friction would
1249
- never act at all. And the static/dynamic separation needs no smooth transition function of its own —
1250
- it falls out of the clamp, because a row under the cap is enforced to convergence and sticks, and one
1251
- at the cap applies exactly `μ·f_n` against the slip direction and slides.
1252
-
1253
- The row's anchor — where the particle stuck — is stored in the **collider's** own frame and read back
1254
- through the interpolated pose, which is what makes a sliding surface drag cloth with it instead of a
1255
- point in space holding it still.
1256
-
1257
- ---
1258
-
1259
- ## 9. Self-collision
1260
-
1261
- Off by default — `ClothDynamicsFlags.SelfCollision`. It is the most expensive part of every cloth
1262
- system that has it — MagicaCloth2's self-collision constraint is 2,564 lines against 606 for
1263
- distance — and the pathology guarantees in §7 do not depend on it.
1264
-
1265
- **M4 corrected which entries set it, and the axis the list was drawn along.** This paragraph used
1266
- to name them by what the garment *is* — set on skirts, long coats and hair, clear on capes,
1267
- ponytails and banners. What decides is **how far the fabric bends**. Measured on a folding sheet
1268
- at each entry's own `bend`: silk, hair and cotton pass through themselves 8 to 11 times a sample
1269
- and the row removes 71% to 81% of it; denim tangles a third as much; leather does not tangle at
1270
- all — 0.18 crossings a sample — and the row makes it *worse*, because the few pairs it finds are
1271
- noise it is adding to a fabric that was never going to fold that tightly. `CLOTH_SILK`,
1272
- `CLOTH_COTTON` and `CLOTH_HAIR` set the flag; `CLOTH_DENIM`, `CLOTH_LEATHER` and both chains do
1273
- not. `MEASUREMENTS.md` §33.
1274
-
1275
- Two tiers:
1276
-
1277
- **M4a — particle-particle repulsion. M4 built it.** A uniform hash grid over particles in the
1278
- cloth's local frame; pairs closer than `r_i + r_j` get the unilateral row from §6. Pairs sharing a
1279
- triangle or an edge are excluded, since they are already governed by stretch and bend rows and
1280
- would otherwise fight them — in practice the exclusion is *sharing any row at all*, which is the
1281
- same set plus a chain's skip rows.
1282
-
1283
- **That `r` is not the fabric's thickness, and M4 got that wrong before it measured its way out.**
1284
- It reads as the same `r` a collider contact uses, which is `ClothDynamics#thickness`; a fabric is
1285
- 4 to 20 mm thick and a proxy's particles are 2 to 8 cm apart, so at that radius the two layers of
1286
- a fold are almost never in contact and the row finds 1.6 to 3.7 pairs across a whole garment. The
1287
- radius is a property of **the proxy**: `ClothState#self_radius`, `max(thickness, shortest_row/2)`,
1288
- so the row's diameter is one particle spacing. At the fabric's own 6 mm the row removes 21% of a
1289
- sheet's self-intersections; at half the shortest row it removes 82%. `MEASUREMENTS.md` §31. And the
1290
- row is **the particle's own** (`ClothState#self_row`), a pair's reach the sum of its two radii: a circle
1291
- skirt's hem has rows five times the waistband's, and one radius per cloth gave it the waistband's, a
1292
- fifth of its spacing, with the flutes through each other at every sample. §67, §68.
1293
-
1294
- **It also needs a ceiling, and the ceiling is a measured length.** A diameter that reaches the
1295
- closest pair *no row governs* puts a cloth which is not folded at all in permanent contact with
1296
- itself — leather, 20 mm thick on a 20 mm proxy, found 1,398 pairs a step across 576 particles
1297
- standing still (§36). On a sheet that distance is two hops and could have been assumed; on a
1298
- rigged garment, whose proxy is strips with nothing joining one to the next, it is the gap the
1299
- artist left between strips and is half what the link length implies. So `cloth_build_rows` measures
1300
- it once at seed, in the bind pose, per particle into `ClothState#self_free_pair`, and the ceiling is
1301
- two fifths of it. `MEASUREMENTS.md` §37, §68.
1302
-
1303
- "O(n) with a small constant" is right about the shape and optimistic about the constant: the pair
1304
- search is linear in particles with no term in the pair count, and it is **12% of a frame** at the
1305
- §17 workload, and most of everything self-collision costs. The rows themselves are nearly free.
1306
- `MEASUREMENTS.md` §34. "The great majority of visible interpenetration" is 82% of it at the
1307
- stiffness M4 shipped, `8·(m_eff/h²)`, which §32 chose on a folding sheet's crossing count.
1308
- **Post-M7 corrected the stiffness to `2·(m_eff/h²)`.** §32's sheet showed no energy growth at 64×
1309
- because its contacts come and go. A fold resting on a fold is a pair that carries weight, a spring
1310
- that is on every step, and at 8 it never comes to rest (§18). 2 is the stiffest ratio that lets it,
1311
- and it removes 63% to 71% of the folding sheet's crossings rather than 73% to 79%.
1312
- `MEASUREMENTS.md` §32, §65.
1313
-
1314
- *This tier cannot see cloth passing between particles.* A sheet thinner than its particle spacing can
1315
- still slip through itself. Stated, not hidden.
1316
-
1317
- **M4b — point-triangle and edge-edge.** The full stencil set, which closes that gap, over the same
1318
- grid. Deferred behind measurement: it roughly triples the self-collision cost and the benefit is
1319
- garment-dependent.
1320
-
1321
- **M4 makes that cost estimate the first thing to re-measure.** The stencils would ride on a grid
1322
- that has already been built, and the grid is where nearly all the money goes — tripling the rows
1323
- triples something that is 5% to 20% of the feature. `MEASUREMENTS.md` §34.
1324
-
1325
- Both tiers derive their normals from the proxy's surface orientation, per P4(a).
1326
-
1327
- **M4a's normal is the line between the two particles, not a surface orientation**, because a
1328
- particle pair has no surface in it. What P4(a) is actually asking for — nothing history-dependent,
1329
- no "which side was this on last frame" to latch onto — the pair normal satisfies completely and
1330
- more simply: `(x_i − x_j)/‖x_i − x_j‖` at the current position, recomputed every iteration, with
1331
- no multiplier, no warm start and no identity across steps. The sentence above is right about M4b,
1332
- where a point-triangle stencil genuinely needs an orientation to choose a side.
1333
-
1334
- There is no 3D uniform hash grid in `core` today; there is a well-tested 2D one
1335
- (`core/geom/2d/hash-grid/SpatialHashGrid.js`) whose structure is the template. §14.
1336
-
1337
- **M4 built `core/geom/3d/hash-grid/PointHashGrid3.js` and it is not that structure.** The 2D grid
1338
- is persistent and incremental — a bounded cell array, elements that are AABBs and carry identity,
1339
- insert/remove/move as the primitives. Cloth particles are points with no identity across steps in
1340
- an unbounded domain, and *every one of them moves every step*, so the incremental primitive never
1341
- gets to be incremental. `PointHashGrid3` is a counting sort into a power-of-two hash and has no
1342
- insert, no remove and no element identity at all. The idea carried over; not one line of the
1343
- structure did. `MEASUREMENTS.md` §34.
1344
-
1345
- ---
1346
-
1347
- ## 10. Wind and aerodynamics
1348
-
1349
- Cloth that does not move with the air reads as dead, and this engine has something better than a wind
1350
- zone: a **fluid simulator**. `FluidField` exposes `sampleVelocity(out, x, y, z)` with trilinear
1351
- filtering over a MAC grid, `fluid_build_world_to_grid` builds the world→grid transform, and
1352
- `FluidObstacle` already voxelises physics colliders into the field so the air is genuinely disturbed by
1353
- the bodies in the scene.
1354
-
1355
- So the design is: **a cloth overlapping a `FluidComponent` samples its velocity field per particle and
1356
- applies aerodynamic drag against it**, scaled by `ClothDynamics#drag`. The term is an external
1357
- acceleration, `a_drag = response · facing · (u_air − v)`.
1358
-
1359
- The payoff is that a character running past a cape gets a wake, because the fluid solver already
1360
- produces one; a fire's convection lifts a banner; a door slamming pushes a curtain. None of that needs
1361
- cloth-specific authoring, and none of it needs a wind system that duplicates what the fluid system
1362
- already does.
1363
-
1364
- Fallback when there is no fluid field in range: a `GlobalFluidEffector`-shaped ambient term — a
1365
- constant `wind` vector plus a small band-limited noise for gusting — read from the same interface, so
1366
- the cloth code has one path and does not branch on whether a simulation is present.
1367
-
1368
- *Corrected by the GPU wind fluid's S5 (`../fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md` §12, G2):*
1369
- the fallback is no longer cloth's own. It is the scene's **far field** — its `GlobalFluidEffector`s,
1370
- gust included, folded by `FluidEffectorsSystem` once a tick — which is what the shape above was
1371
- always copying. `ClothAmbientWind` is retired; `ClothGPUFluidWind` reads the GPU wind and
1372
- `ClothFluidWind` the CPU fields, each with the far field underneath. `MEASUREMENTS.md` §69.
1373
- And cloth reads it **by default**: with nothing assigned, `ClothWorld#wind` is a `ClothGPUFluidWind`
1374
- while the scene has an enabled `GPUFluidComponent`, else a `ClothFluidWind` while the far field is not
1375
- zero, else `null` as before — decided again every tick from those facts, so a scene without wind gains
1376
- no drag. An assigned source, `null` included, always wins.
1377
-
1378
- Cost control: sampling is per particle per *step*, not per iteration, and a cloth outside every fluid
1379
- field's bounds skips it entirely. Sleep (P5) wakes on a wind change so a settled banner does not stay
1380
- asleep through a gust.
1381
-
1382
- **M6 built it and corrected this section in four places.**
1383
-
1384
- **The factor above is not `|n·û|`, because half of what this system simulates has no `n`.** This
1385
- paragraph originally wrote the term as `a_drag = drag · (u_air − v) · |n·û|`, "with the normal-facing
1386
- factor making a sheet catch the wind broadside and slice through it edge-on — the difference between a
1387
- flag and a rope". It is right about what it wants and wrong that a normal is available to want it
1388
- with. A chain's particles span a **line** — every direction perpendicular to it is equally a normal,
1389
- so picking one is the history-dependent fabrication §7 P4(a) exists to keep out — and a rope's
1390
- aerodynamics are the *complement* of a sheet's anyway: it catches the flow across it and slices along
1391
- it. Both are one quantity, **the part of the flow the particle's own incident rows cannot let past**:
1392
- take the span `T` of the directions to its stretch-row neighbours in the current configuration, and
1393
- `facing = ‖û − proj_T(û)‖`.
1394
-
1395
- | rank of `T` | what the particle is | the factor |
1396
- |---|---|---|
1397
- | 2 | a point on a surface | `\|û·n\|` with `n = b₁×b₂` — this paragraph's own expression |
1398
- | 1 | a link of a chain | `√(1 − (û·t)²)` — the cross-flow, which is the rope's answer |
1399
- | 0 | an isolated particle | `1` — a bead has no preferred direction |
1400
-
1401
- One expression, three topologies, no stored normal, no orientation, no sign, nothing carried between
1402
- steps. `MEASUREMENTS.md` §47.
1403
-
1404
- **And the factor's headline claim is about *laminar* air, which is a limit rather than a fault.** On
1405
- the playground's banner, turning a uniform wind through ninety degrees takes the mean factor from
1406
- 0.221 to **0.000** and the aerodynamic acceleration from 43.2 m/s² to 0.08 — a flag becoming a rope,
1407
- exactly as promised. Turning a **wake** through ninety degrees does nothing at all: 0.216 to 0.280. A
1408
- wake has velocity in every direction, so no orientation of a surface is edge-on to one, and the factor
1409
- is correctly reporting that there is nothing to slice through. That makes this section's argument for
1410
- reading a simulation *stronger* than this section knew. `MEASUREMENTS.md` §51.
1411
-
1412
- **`drag` is not what multiplies the flow, and the difference is a stability argument.** One external
1413
- acceleration is held for the whole fixed step, so an explicit `rate · (u_air − v)` overshoots the air
1414
- once `rate·dt > 1` and diverges past 2 — and §1's first commitment is "not at 5 fps", which is a `dt`
1415
- of 0.2 s and a rate of 10. What the step multiplies by is the **per-step response**
1416
- `(1 − e^(−rate·dt))/dt` (`cloth_drag_response`), the exact solution of the same linear ODE, which
1417
- closes a fraction of the gap that is strictly below one for every rate and every step length. It is
1418
- also the step `GlobalFluidEffector` already takes for the air itself, which is a symmetry rather than
1419
- a coincidence: both are relaxations toward a velocity. `MEASUREMENTS.md` §49.
1420
-
1421
- **The field answers in grid cells per second and cloth needs metres.** `FluidField#sampleVelocity`
1422
- documents that it deliberately does not rescale, "on the assumption that callers (vegetation sway,
1423
- dust drift) interpret the field as a direction × magnitude wind". Cloth is not such a caller: its row
1424
- is a relaxation toward a velocity, so without the multiply by `cell_size` the same wind voxelised at
1425
- 0.25 m instead of 1 m blows a garment four times as hard. `ClothFluidWind` converts.
1426
- `MEASUREMENTS.md` §46.
1427
-
1428
- **And this section never said which thread samples, which M5 made a question.** It cannot be the
1429
- worker's: a fluid field is a hundred thousand floats its own solver rewrites every step and may itself
1430
- live on another worker, while the *answer* is three floats a particle. So `cloth_sample_air` runs on
1431
- the **main thread** in both systems and writes into `ClothState#air_velocity` — a region of the
1432
- cloth's own `SharedArrayBuffer`, so nothing is copied — and everything built from it runs in
1433
- `cloth_step_instance` with the rest of the arithmetic, which is what keeps §12's parity structural.
1434
- Measured: 58–62 ns a particle for a field sample, a twenty-fifth of that for the ambient (one sample
1435
- for the whole cloth, which is what §10's "skips it entirely" is worth), and one per cent of a step for
1436
- the term itself. `MEASUREMENTS.md` §48, §49.
1437
-
1438
- **Where the air comes from is a scene's choice and there is no default.** `ClothWorld#wind` is `null`
1439
- until something assigns a source, exactly as the collider index is: a scene without one gets M5's
1440
- cloth rather than an error. Null rather than a still-air ambient, because a source answering zero
1441
- everywhere is not "no wind" — the row is a relaxation toward the air, so still air still resists a
1442
- *moving* cloth, anisotropically. That is better physics than `ClothDynamics#damping`'s isotropic decay
1443
- and it is not free, so it is a thing a scene asks for. What a cloth does in air that is not moving is
1444
- `damping`'s job (§7 P5, where it is load-bearing); what it does in air that *is* is this.
1445
-
1446
- ---
1447
-
1448
- ## 11. Geometry in, results out
1449
-
1450
- **Cloth moves joints. It does not produce vertices.** The mapping from a simulated particle to a
1451
- render vertex already exists in this engine and it is the **skin**: a `Skin` holds `joints: Node3D[]`
1452
- plus inverse-bind matrices, every render vertex carries a four-entry weight palette into that joint
1453
- list, and `node.global · inverse_bind` is what the GPU blends every frame. Cloth writes joint
1454
- transforms; the existing skinning pass does the rest — including bounds refresh and BLAS refit, which
1455
- a bespoke vertex-deformation path would have to reimplement.
1456
-
1457
- That is the whole relation, and it is why there is no cloth-owned render mesh anywhere in this design.
1458
-
1459
- ### 11.1 Entity hierarchies — `Cloth` alone
1460
-
1461
- One particle per entity in the transform subtree of the entity the component is on, walked through
1462
- `TransformAttachmentSystem#findChildrenOf`, stopping at a nested `Cloth` or a `ClothExclude`. Stretch
1463
- rows along parent-child links, bend rows along each chain, and the anchor entity itself is the fixed
1464
- set — so `d_geo` is hop distance from the anchor and every derived quantity in §3.4 works unchanged.
1465
-
1466
- Output is written back to the subtree entities' `Transform64` — position directly, rotation derived
1467
- from the direction to the child, interpolated between parent-driven and child-driven orientation, which
1468
- is what keeps a two-link chain from looking hinged.
1469
-
1470
- **M1 made three of those words exact.**
1471
-
1472
- *Both transforms are written: the world `Transform64` and the local
1473
- `TransformAttachment#transform`.* Writing only the world one is cheaper and is a trap — the attachment
1474
- system recomposes a child from its local whenever the parent announces, so any announcement arriving
1475
- after cloth had written would replace the simulated pose with the authored one, and where in the fixed
1476
- step cloth ran would be a *correctness* question. With the local written too, a recomposition
1477
- reproduces exactly what cloth just wrote (measured at 1.7e-15 m), and §4.1's ordering problem stops
1478
- being one. It is also the engine's own precedent: `InverseKinematicsSystem` reads world transforms the
1479
- hierarchy has composed and writes local ones back, and §11.2's rig path writes `Node3D.transform_local`
1480
- for the same reason — a local transform is what a joint angle *is*.
1481
-
1482
- The authored pose is not lost by being overwritten. It is captured once, when the cloth seeds, and
1483
- lives in the cloth from then on — which is what `blend = 0` returns to, what the leash centres are
1484
- measured from, and what a re-seed of a live cloth reads instead of the shape the solver left behind.
1485
-
1486
- **Post-M6 found both halves of that last clause to be false in the code**, and they are the same
1487
- mistake twice: the authored pose lives in the cloth, so the cloth is the only thing that can give it
1488
- back, and it did not. `ClothWorld#reseed` cleared `ClothInstance#seeded` *before* the walk, and the
1489
- capture was keyed on `seeded` — so the one path that exists to re-walk a live cloth was the one path
1490
- that could remember nothing, and `read_rest_pose` fell back to `TransformAttachment#transform`, which
1491
- this very paragraph says the write-back has been overwriting every step. Reproduced on a 6-link rope
1492
- authored horizontally and settled for 300 steps: a re-seed took the authored tip from **x 0.600,
1493
- y −0.012 to x −0.003, y −0.600**, and `blend = 0` then landed on the sagged pose. The capture is now
1494
- keyed on there *being* a previous capture. And the same for removal: `unlink` dropped the registry
1495
- entry and nothing put the authored local transforms back, so a `Cloth` removed left its subtree frozen
1496
- in the solver's last shape and a `Cloth` added back seeded *that* as the pose it returns to.
1497
- `cloth_release_subtree` is the other half of the seed, announced once on the anchor.
1498
- The consequence, stated: **in the entity path the animated pose is constant in the anchor's frame.**
1499
- That is a property of the case rather than a limitation of the code — a subtree's authored pose is
1500
- rigid relative to its anchor — but it does mean a subtree whose *links* are independently animated is
1501
- not an M1 case.
1502
-
1503
- **M3 corrected the sentence that used to end this paragraph**, which said that was the rig path and
1504
- that there the animated pose "comes from the skeleton and is live". For a **rigged** garment it is
1505
- not: cloth claims those joints (§11.2), so nothing else poses them, so their authored pose is the
1506
- proxy's baked bind pose — constant in the anchor's frame, exactly as a rope's is. What moves is the
1507
- anchor, and the anchor is a bone of the character's own animated skeleton. The live case is the
1508
- **unrigged** path, where the targets are mesh vertices skinned by
1509
- `mesh_apply_skeletal_vertex_skinning` and genuinely move under the animation.
1510
-
1511
- *The rotation is a **swing** on top of the animated orientation*, `q = swing(d_animated → d_simulated)
1512
- ⊗ q_animated`, using the minimal (twist-free) arc. Twist is inherited from the animation rather than
1513
- invented, because a chain of particles carries no twist degree of freedom and any twist this code
1514
- produced would be a fabrication — and the whole thing degenerates correctly, since at `blend = 0` the
1515
- simulated direction *is* the animated one and the swing is the identity. A node with both a parent and
1516
- children takes the half-way rotation between the two swings; a leaf takes its parent's. The shortest
1517
- arc had no primitive in `core` and now does (`quat3_shortest_arc`), which is §14's doctrine turning up
1518
- a fourth time.
1519
-
1520
- *Nothing is announced on a simulated node, and every pruned boundary is.* A `TRANSFORM64_EVENT_CHANGE`
1521
- on a simulated node would make the attachment system recompose its children from the authored pose —
1522
- undoing the write that was just made, and cascading, for a chain, quadratically. But the root of a
1523
- `ClothExclude` subtree, and a nested `Cloth`'s anchor, are *not* particles and still have to move: the
1524
- write-back poses each of them by hand against the link it hangs from and announces exactly there, which
1525
- reaches everything below the cut through the ordinary cascade and reaches nothing above it.
1526
-
1527
- This is the path for props that genuinely *are* entity hierarchies: a rope, a chain, a hanging sign, a
1528
- mooring line, a rigid model whose parts were expanded into entities and each drew as a
1529
- `ShadedGeometry`. It needs no new rendering code, because moving an entity's `Transform64` is already
1530
- how anything moves.
1531
-
1532
- ### 11.2 Skinned garments — `Cloth` + `ClothRig`
1533
-
1534
- A character's cape is not an entity hierarchy **of its own**: a skinned node contributes its transform
1535
- and its name and **no geometry**, because its vertices deform against a skeleton the renderer owns and
1536
- the GPU poses. The geometry is registered with `GPUAnimationManager`, and what cloth writes is a
1537
- joint's offset from its parent joint.
1538
-
1539
- **Where that offset lives is no longer a `Node3D`, and the three paragraphs that follow were written
1540
- when it was.** M3 wrote `Node3D.transform_local` on joints inside a `SceneBundleInstance`, and this
1541
- section still describes that, including a flat assertion that bone entities are "explicitly a design
1542
- this program does not do (D47)". The engine's Rows-Not-Nodes work deleted `Node3D`: a model is spawned
1543
- by `prefab_instantiate`, its joints **are** entities, and the writable handle is
1544
- `TransformAttachment#transform` on the joint entity — which is what `cloth_seed_rig` collects and
1545
- `cloth_write_back_rig` writes. Nothing about the design changes with it and one thing gets simpler:
1546
- the rig path and the entity path now write the *same component*, so §11's "the solver is identical
1547
- either way; only seeding and write-back differ" is now true of the write-back's data type as well as
1548
- of its shape. Read `Node3D.transform_local` below as `TransformAttachment#transform` on a joint
1549
- entity, and `SceneBundleInstance` as `PrefabInstance`.
1550
-
1551
- **And cloth has to *claim* those joints, which §11.2 originally read as though it did not.** The
1552
- sentence above says the handle is "the same handle the animation systems write", and they do not
1553
- write it: `TransformAuthority` is explicit that a node a clip drives is `GPU`, that "the CPU
1554
- allocates and seeds the row, then never writes to it again", and that "the node's own
1555
- `transform_local` stops describing it and should not be read as if it did" — and
1556
- `instantiate_scene_bundle` stamps `GPU` on every node a clip channel targets. So cloth writing
1557
- `transform_local` on a clip-driven joint would be writing into a field nothing reads.
1558
-
1559
- `cloth_seed_rig` therefore sets `TransformAuthority.CPU` on every joint it drives. The consequence,
1560
- stated: **a joint cloth simulates stops being animatable by a clip.** Two writers of one row is the
1561
- race the enum exists to prevent, and for a cape that is the point of simulating it; for a garment
1562
- whose joints are also keyed it is an authoring error, and the claim is where it surfaces.
1563
- `MEASUREMENTS.md` §25.
1564
-
1565
- **Post-M6: the claim is a loan, and it was written as a gift.** The consequence above is the right
1566
- trade only while the cloth is *there*. `cloth_seed_rig` revoked the GPU's authority over every joint
1567
- it drove and nothing ever re-granted it, so a garment removed left its joints owned by a CPU that had
1568
- stopped writing them — neither animated nor simulated, stuck in the pose the solver last produced, and
1569
- with no component left anywhere naming which joints those were. The seed now records exactly the
1570
- joints its revoke *changed*, and `cloth_release_rig` hands those back when the `Cloth` is removed or
1571
- the `ClothRig` is taken away; a joint that was already the CPU's before cloth arrived stays the CPU's.
1572
-
1573
- **2026-10-06: the claim never stopped a clip, and the loan was handed back to whoever asked last.**
1574
- No GPU pass reads `GPUStateAuthority`. A bound clip's bindings write a joint's row through the pose
1575
- accumulator every frame, after the CPU's flush, whatever the bit says, so clearing the bit only let
1576
- cloth's flush and the clip race for one row, and the clip won. And cloth and `GPUAnimationManager`
1577
- each flipped the same bit without the other: a clip that left during the loan was answered by
1578
- cloth's re-grant with a joint nothing wrote again, and a clip that arrived during it took the bit
1579
- from under cloth's write-back. The rule now is a **hold**. `GPUStateAuthority#held` records that a CPU
1580
- writer has the offset, the bit stays clear while it does, and the animation manager — the only
1581
- writer of the bit — mutes every binding that names a held joint and leaves its offset alone when the
1582
- last clip goes. Ending the hold grants nothing; a clip still bound takes the joint back. So **"a joint
1583
- cloth simulates stops being animatable by a clip" is true now, and a garment whose joints are also
1584
- keyed is not an authoring error**: exporters often bake every bone, and the cape simulates anyway.
1585
- `MEASUREMENTS.md` §61.
1586
-
1587
- **The anchor's world pose is computed, never read back.** A joint of an animated skeleton has no
1588
- CPU-side `transform_global` — nothing maintains it — so the rig path evaluates the anchor with
1589
- `pose_evaluate_world` over the clips the entity is playing, which is the engine's own sanctioned
1590
- answer to "where is this bone" and the one `query_entity_node_world_pose` gives gameplay. A cape and
1591
- a scabbard on the same shoulder are then placed by the same evaluator off the same clock.
1592
-
1593
- Cloth therefore drives joints, and the proxy names which ones. **Three resolutions, deliberately:**
1594
-
1595
- | tier | order of magnitude, skirt | what it is | who owns it |
1596
- |---|---|---|---|
1597
- | render mesh | ~5,000 vertices | `MeshletGeometry`, GPU-resident, never touched by cloth | the asset |
1598
- | sim proxy | ~400 particles | what the solver steps | `ClothProxy` |
1599
- | output joints | ~60 | what `Skin#joints` holds; the palette binds render vertices to these | `ClothProxy` |
1600
-
1601
- Decoupling the middle tier from the bottom one is the point. Simulation wants enough particles to
1602
- drape; skinning wants only enough joints to *describe* the drape, and a four-weight palette blends
1603
- smoothly between far fewer of them than the simulation needs. Each output joint takes its position and
1604
- its frame from a small neighbourhood of particles — a triangle fan or a fitted patch — so the joint
1605
- count is a build-time choice about deformation fidelity rather than a consequence of solver
1606
- resolution. That is also what disposes of the "one node per particle" cost worry: nothing requires
1607
- one node per particle.
1608
-
1609
- Two cases, and they differ only in where the joints and weights came from:
1610
-
1611
- - **The garment is already rigged** — an artist made 8–20 cape joints and skinned the cape to them.
1612
- This is the common case and by far the cheapest. The proxy is thin: joint indices, the topology
1613
- connecting them, rest lengths, `d_geo`. **There is no binding step and no generated geometry — the
1614
- artist's skin is the relation.** A cape, a ponytail, a coat tail, a loincloth: all of this.
1615
- - **The garment has no usable rig** and wants per-vertex drape — a full skirt. The importer generates
1616
- the joint set and generates a skin binding render vertices to it, then bakes both into the proxy.
1617
- At runtime this is *indistinguishable* from the rigged case: the GPU sees an ordinary skin, the
1618
- solver sees an ordinary particle set.
1619
-
1620
- Animated targets — the `s_i` that the slack row, blending and P4 all need — come from
1621
- `mesh_apply_skeletal_vertex_skinning`, which already takes flat positions, weights, indices and 4×3
1622
- bone matrices and writes flat output. Typed arrays in, typed arrays out, no objects: it crosses the
1623
- worker boundary as-is.
1624
-
1625
- **What was rejected.** A dedicated deformation pass — cloth writing vertex positions into a dynamic
1626
- geometry buffer, with render vertices bound barycentrically to proxy triangles — is the obvious
1627
- alternative and is genuinely simpler *as a deformation*: one frame per triangle rather than per joint,
1628
- no palette width to respect. It loses because `MeshletGeometry` is meshlet-clustered and BLAS-backed,
1629
- so a new path has to reimplement per-frame vertex upload, meshlet bounds refresh, normal
1630
- recomputation and BLAS refit — all of which the skinning path already does and has playgrounds for.
1631
- It stays written down as the fallback if M3's measurements go badly.
1632
-
1633
- ### 11.3 Building a `ClothProxy`
1634
-
1635
- At import, producing the object `ClothRig#proxy` points at. Almost entirely existing code.
1636
-
1637
- For a **rigged** garment, only steps 5–8 run, over the joint graph rather than a mesh:
1638
-
1639
- 1. Load the source garment into a `BinaryTopology` (`bt_mesh_from_indexed_geometry`).
1640
- 2. Weld and clean (`geometry_deduplicate_vertices`, drop degenerate faces).
1641
- 3. Decimate to the target particle count with `bt_mesh_simplify_by_error` — quadric error metric, via
1642
- `bt_mesh_compute_vertex_quadrics`. This is the standard distance-based reduction every cloth package
1643
- does, with a principled metric instead of a hand-rolled one.
1644
- 4. Extract stretch rows from edges, bend stencils from `bt_edge_read_diamond`, and the boundary from
1645
- `bt_collect_boundary_segments`.
1646
- 5. Derive the **fixed set automatically**: particles whose skin weights are dominated by joints
1647
- *outside* the garment's own joint set are pinned. This is what makes "works untouched" true — a
1648
- cape pinned at the shoulders needs no painting, because the skin data already says where the cape
1649
- stops being cape.
1650
-
1651
- **M3 found this is the unrigged rule and that the rigged path has a better one.** For a garment
1652
- that is already rigged there are no mesh vertices to read weights for — the particles *are*
1653
- joints — and the topology answers directly: a joint whose parent is outside the named set is a
1654
- joint the garment hangs from, and they all have to hang from the same bone. That bone is the
1655
- anchor and its `d_geo` is zero, so §3.4's derived quantities collapse onto it with nothing else
1656
- saying "this one is pinned". Several chains from one bone are one garment and are §3.1's coupled
1657
- solve; chains from *different* bones are refused, because there is then no single frame to
1658
- simulate in.
1659
-
1660
- **M7 lifted the refusal, because the frame and the fixed set came apart.** The bones a garment
1661
- hangs from are particles of the proxy that cloth reads and never writes (`ClothProxy#output`), with
1662
- the links to their roots the first links of the fabric; the frame is the nearest skin joint above
1663
- all of them (`ClothProxy#frame`); and the builder hands back one `ClothProxyGroup` per bone — the
1664
- pin to add. A cape across both shoulders is one proxy, two pins, riding the chest
1665
- (`cloth_rig.spec.js`).
1666
- 6. Bake `d_geo` — geodesic distance from every particle to the fixed set. This one array drives the
1667
- tether row, the slack profile, and the pinning, so the three cannot disagree.
1668
- 7. Colour the particle graph (`csr_graph_color_greedy`, checked by `csr_graph_validate_coloring`), and permute
1669
- the particle order so each colour is a contiguous range.
1670
- 8. Resolve joint **names to indices** into `Skin#joints`, using `SceneBundleInstance#nodes`. This is
1671
- the only place a name appears anywhere in the system.
1672
- 9. **Unrigged only:** choose the output joint set, fit each joint's neighbourhood and bind pose, and
1673
- generate the skin's weights and inverse-bind matrices for the render mesh.
1674
- 10. **Validate**: manifoldness, no zero-length edges, and — the P4(c) precondition — that the skinned
1675
- bind pose is collision-free against the character's `ClothCollider` set.
1676
-
1677
- Vertex painting (per-vertex stiffness or slack multiplier) — **not built.** The sentence that used to
1678
- stand here said it "is supported through an optional texture or vertex-colour channel"; nothing reads
1679
- a texture or a colour channel, and `ClothProxy` has no per-vertex override to read one into. Steps 5
1680
- and 6 are the whole of what a garment gets, which is the design's own claim that most garments never
1681
- need to override them, made by there being no override. The shape of one, if a garment ever needs it,
1682
- is a per-particle multiplier beside the geodesic — one array, read where `slack` and `stretch` are
1683
- read — and it is the kind of authored knob §3.4 spent a section removing, so it should arrive with a
1684
- garment that demonstrably needs it rather than ahead of one.
1685
-
1686
- ---
1687
-
1688
- ## 12. Data layout, threading, the worker
1689
-
1690
- **One `SharedArrayBuffer` per cloth**, and the command region names which cloths are live this step —
1691
- so sleeping (P5) and culled (§12.1) cloths cost nothing but their absence from a list. `WorkerSystem`'s
1692
- model is a single shared buffer, a single command region and a single atomic publishing one in-flight
1693
- step, and `WorkerFluidSystem` shows how a variable set of simulated things fits it.
1694
-
1695
- **M5 corrected "one buffer for the whole cloth world", and `reseed` is why.** A `ClothState` is sized
1696
- at seed from *that cloth's* particle and row counts, and `ClothSystem#reseed` — a chain gaining a link,
1697
- a `ClothDynamics` change the row tables depend on, a rigged garment whose model has just loaded —
1698
- builds a different one while other cloths have steps in flight. A world buffer would therefore need an
1699
- allocator with relocation, and a relocation invalidates every view of every region on both threads at
1700
- once. One buffer per cloth costs one cold-plane message per seed (`add_cloth` / `remove_cloth`, the
1701
- shape `WorkerFluidSystem` already has) and makes a re-seed a re-registration, which is the same code
1702
- path as a first seed. `MEASUREMENTS.md` §39.
1703
-
1704
- SoA throughout:
1705
-
1706
- | Region | Type | Contents |
1707
- |---|---|---|
1708
- | particle state | `Float32Array` | `x`, `x_prev`, `v`, `y`, inverse mass, radius |
1709
- | animated targets | `Float32Array` | `s`, backstop origin + normal |
1710
- | topology | `Int32Array` | edge pairs, bend stencils, colour ranges, tether anchors |
1711
- | rest data | `Float32Array` | rest lengths, bend weights, `d_geo` |
1712
- | duals | `Float32Array` | `λ` and `k` per persistent constraint |
1713
- | colliders | `Float32Array` | packed records for this step — `CLOTH_COLLIDER_SLOTS` of them |
1714
- | contacts | `Float32Array` | transient rows + their warm-start `λ`, keyed by (particle, collider slot) |
1715
-
1716
- The **step's parameters** reach the worker through the command region, written every step — fourteen
1717
- words into memory the worker already shares, cheaper than tracking dirtiness, and it keeps
1718
- `cloth.dynamics.slack = 0.2` working live, which is the point of a value object rather than a preset
1719
- that has to be re-applied. (Thirteen at M5; M6's §10 adds the drag response, and the air itself
1720
- travels as a region of the same buffer rather than as words — `ClothState#air_velocity`, written by
1721
- the main thread and read by the step, which is the one direction the buffer could always carry and
1722
- nothing had used.)
1723
-
1724
- **M5 corrected "`ClothDynamics` reaches the worker".** By the time the step can use them a cloth's
1725
- dynamics have been through `cloth_dynamics_map.js` — a damping retention, a velocity clamp, a
1726
- self-collision radius, gravity rotated into the local frame — and sending the raw component would mean
1727
- running that mapping a second time on the far side, with no way to prove the two copies agreed. What
1728
- crosses is `ClothStepParam`: `dt`, local gravity, substeps, iterations, retention, clamp, contact
1729
- radius, self radius, collider count, and whether self-collision is on. `Float64` throughout, so the
1730
- round trip is exact — an `f32` command region would round the clamp, and a clamp that rounds is a
1731
- velocity that rounds. `MEASUREMENTS.md` §40.
1732
-
1733
- The same block is what makes the parity gate below structural rather than aspirational:
1734
- `ClothSystem` fills the words of a private scratch array and calls `cloth_step_instance`;
1735
- `WorkerClothSystem` fills the same words of the shared command region and the worker calls the same
1736
- function. Neither system computes a step parameter of its own, so neither can compute one differently.
1737
-
1738
- **M6 found the one thing that is not a parameter and still differs: the clock.** A gust is a function
1739
- of time, and a worker cloth is not advanced until its registration is acknowledged — so the two
1740
- systems reach a cloth's *first step* at different tick counts, and a clock that counted ticks hands
1741
- that cloth a different gust on the same step. Measured with the clock put back on ticks, the gate
1742
- fails at step 1, byte 0, which is the step's own result word. `ClothWorld#time` therefore counts the
1743
- steps the world has taken rather than the ticks it has been asked about: it advances at the first
1744
- `advance` of a tick, so a tick in which nothing is advanced does not move the weather. That is also
1745
- the right semantic on its own terms — a paused simulation should not have the wind move.
1746
- `MEASUREMENTS.md` §50.
1747
-
1748
- **Post-M6: a world clock fixes the first cloth in a scene and no other.** The paragraph above is
1749
- right about *one* cloth and the gate it was measured against had one. With cloth A already running,
1750
- cloth B's registration takes k ticks during which A is still being advanced — so the world clock
1751
- moves, and B reaches its own first step at a different clock value under the two systems. Measured
1752
- with a two-tick delivery: **0.1000 s against 0.1333 s** at B's first step, and the gate fails at B's
1753
- step 1, byte 0, exactly as it did for the tick clock. A gust of zero gives zero differences, which is
1754
- what identifies the clock as the cause rather than the latency. So the gust reads **the asking
1755
- cloth's own age** — `ClothInstance#age`, seconds *this instance* has stepped — and `ClothWorld#time`
1756
- is left to do the one job it can do correctly, which is telling a source that reads the scene when to
1757
- walk it. The visible consequence is that two garments spawned a second apart are a second apart in
1758
- the gust rather than in phase with it; for a bounded sum of sinusoids that is indistinguishable from
1759
- weather, which is why this is free. `MEASUREMENTS.md` §57.
1760
-
1761
- **G2 (the GPU wind fluid's S5): the clock is the scene's, and the gate is restated.** The gust moved
1762
- onto `GlobalFluidEffector` and runs on the scene's tick, so every consumer of the wind reads one
1763
- weather — and a cloth steps in the weather of the tick it is advanced on. The worker advances a cloth
1764
- from the tick its registration is acknowledged, so the gate now reads: **worker and inline agree for
1765
- cloths registered on the same tick, and for any registration under a time-invariant wind.** Measured:
1766
- the staggered spawn under a 3 m/s gust differs at the second cloth's step 1, byte 0; registered on
1767
- the same tick, none in 180 steps. Under the GPU wind on a device, lattices land when the GPU answers,
1768
- so run-to-run repeatability is given up too (F4). `MEASUREMENTS.md` §69.
1769
-
1770
- **And post-M6 found the other one, which is not a clock: the two systems ran a tick's phases in
1771
- different orders.** `ClothSystem` ran advance, step and finish per cloth, in hierarchy-depth order, so
1772
- a **nested** cloth advanced against an anchor the cloth above it had already written *this* step;
1773
- `WorkerClothSystem` cannot do that — its `apply` writes step T−1 back at the top of a tick and its
1774
- `collect` then advances everything for step T. Measured on a 4-link rope hung from link 3 of a 6-link
1775
- rope, no wind, 120 steps: the parent identical, the child differing at **step 1, byte 24**. Both
1776
- systems now run three loops — advance every cloth, then step every cloth, then finish every cloth — so
1777
- a nested cloth reads its parent one fixed step stale on both sides, and `ClothWorld`'s depth sort,
1778
- which existed only to make the other answer true, is gone. That was necessary and not sufficient: the
1779
- write-back also composed each node's world pose against the **live** anchor rather than the anchor the
1780
- step ran on, which for a worker writing step T−1 during tick T is a different anchor. It now uses the
1781
- pose `advance` cached, which is the same value for `ClothSystem` and the honest one for both.
1782
- `MEASUREMENTS.md` §56.
1783
-
1784
- `Float32` for particle state, not `Float64`. The simulation runs in the cloth's **local frame**, so
1785
- coordinates are garment-sized rather than world-sized and f32 has ample precision; the payoff is half
1786
- the bandwidth in the loop that dominates. The local frame is not a memory optimisation that happens to
1787
- help — it is the same choice that makes P7's inertia handling natural, and the precision win follows.
1788
- Write-back converts local f32 to world `Transform64` at the boundary, once per particle per step.
1789
-
1790
- Transient contact rows need their multipliers warm-started across steps to be worth anything, so they
1791
- are keyed by `(particle index, collider handle)` and matched on the next step. A collider that leaves
1792
- the index drops its slot and its contacts start cold, which is correct.
1793
-
1794
- **M2 made "slot" exact and it matters.** The key is the collider's **handle in the index**, not its
1795
- record index in the cloth's own per-step table: that table is whatever this step's broadphase returned,
1796
- in whatever order, so a record index names a different collider from one step to the next. Storage is a
1797
- fixed `CLOTH_CONTACT_SLOTS` per particle inside `ClothState`'s single buffer — four, measured never to
1798
- saturate on a twenty-collider scene and to average a tenth of one — which keeps §12's one-allocation
1799
- property and bounds the worst case at the four *most violated* colliders, which is the four that matter.
1800
-
1801
- **And M5 moved the per-step collider table itself into that buffer**, at a fixed
1802
- `CLOTH_COLLIDER_SLOTS`, which is where the table above always put it. M2 kept it on `ClothInstance`
1803
- and grew it by doubling, on the correct grounds that its size is a property of the world rather than of
1804
- the cloth; what decides is that an array which can be *replaced* mid-run is memory the worker has to be
1805
- told about again before it can name it. So the size stays a property of the world and the region gets a
1806
- capacity. **Sixty-four**, because the measurement showed the bound is a *scene* rather than a garment —
1807
- a cloth's table holds every collider on every character standing inside its query box, which is ten for
1808
- one body and thirty for three — and because an overflow discards colliders in the index's own order
1809
- rather than in order of how much they matter. It warns once. `MEASUREMENTS.md` §41.
1810
-
1811
- **The worker.** `WorkerClothSystem` is a `WorkerSystem` subclass and follows `WorkerFluidSystem`
1812
- exactly, because that pattern is already proven in this repo: `collect` packs colliders and animated
1813
- targets into shared memory and writes the command words, a single `Atomics.store` publishes the step,
1814
- `apply` reads the result on the next tick, and the fixed-step gate guarantees the worker is idle during
1815
- every system's `fixedUpdate`. The split is the same shape too — arbitrary user-facing work (component
1816
- reads, `Transform64` sync, index queries) stays on the main thread; the pure numeric step crosses.
1817
-
1818
- The standing gate is the one the fluid system already holds itself to: **`ClothSystem` and
1819
- `WorkerClothSystem` must produce bit-identical results.** Identical code over identical data in an
1820
- identical order. That is what makes the worker a deployment choice rather than a behavioural fork, and
1821
- it is why the core is a pure function over typed arrays from milestone zero even though the worker does
1822
- not arrive until milestone five.
1823
-
1824
- **M5 measured it and the number is zero**, at every step of a run rather than at the end of one: the
1825
- whole `ClothState` buffer — particles, velocities, every dual and penalty, the collider table, the
1826
- warm-started contact multipliers, the self-collision pair list — compared byte for byte after each of
1827
- 420 steps across two scenes, one with a walking anchor and one with colliders and self-collision live.
1828
- What the scene sees differs by exactly one tick: the pose written at tick `T` is step `T − 1`'s. That
1829
- is §4.1's own sentence in a second setting — the cloth's *shape* is one fixed step stale and its
1830
- *position* is not, because the attachment cascade carries the subtree rigidly with the anchor.
1831
- `MEASUREMENTS.md` §40.
1832
-
1833
- Testing the bridge needs no worker: `SystemWorkerLoopback` gives a real `SystemWorkerHost`, a real
1834
- `SharedArrayBuffer` and real `Atomics` in-process, with message delivery and step execution as explicit
1835
- events a spec can order. Only the thread is faked.
1836
-
1837
- `SharedArrayBuffer` requires a cross-origin isolated page (`COOP`/`COEP`), which `WorkerFluidSystem`
1838
- already documents. The main-thread `ClothSystem` is the fallback where that is not available, and it is
1839
- not a degraded mode — it is the same code.
1840
-
1841
- ### 12.1 Culling
1842
-
1843
- Cloth outside the view frustum stops stepping, which for a crowd is the difference between a budget and
1844
- a problem. Resuming without a pop means blending from the animated pose back to the simulated one over
1845
- a short window rather than snapping — the same `blend` machinery from §3.1, driven by the system
1846
- instead of the author.
1847
-
1848
- ---
1849
-
1850
- ## 13. Reuse ledger
1851
-
1852
- What this design takes from the engine rather than building:
1853
-
1854
- | Need | Existing code |
1855
- |---|---|
1856
- | Vector and quaternion math | `core/geom/vec3/*` (95 functions), `core/geom/3d/quaternion/*` |
1857
- | Mesh topology, adjacency, bending stencils | `core/geom/3d/topology/struct/binary/` — `BinaryTopology`, `bt_edge_read_diamond`, `bt_edge_is_boundary`, `bt_collect_boundary_segments` |
1858
- | Proxy decimation | `bt_mesh_simplify_by_error`, `bt_mesh_compute_vertex_quadrics` |
1859
- | Mesh cleanup | `geometry_deduplicate_vertices` |
1860
- | Graph colouring | `core/graph/csr/csr_graph_color_greedy.js`, `csr_graph_validate_coloring.js` |
1861
- | Collider shapes and their SDFs | `core/geom/3d/shape/*` — closed-form `signed_distance_at_point` on every primitive |
1862
- | Collider AABBs, pose caching | `core/geom/3d/aabb/*`, `aabb3_transform_oriented_array` |
1863
- | Collider ECS wiring | `Collider`, `ColliderFlags`, `ColliderObserverSystem` (as the pattern) |
1864
- | "Opt a collider into a second simulation" | `FluidObstacle` + `FluidObstacleSystem`, `AcousticBody` |
1865
- | Collider index shape | `AcousticOccluderIndex` + `OccluderBody` |
1866
- | Component holds a built shared asset | `Collider#shape`, `ParticleEffect#program` — the convention `ClothRig#proxy` follows |
1867
- | Surface material combination | ~~`physics/contact/combine_material.js`~~ — **not reused**, see §8.3: cloth's `μ` is `Collider#friction · ClothCollider#friction_scale`, which is a scale on one material rather than a combination of two |
1868
- | Constraint-row doctrine | `physics/constraint/solve_constraints.js` |
1869
- | Subtree walk | `TransformAttachmentSystem#findChildrenOf` / `#countChildrenOf`, `TransformAttachment` |
1870
- | Animated targets | `mesh_apply_skeletal_vertex_skinning` (flat arrays, worker-safe) |
1871
- | Wind / aerodynamics | `FluidField#sampleVelocity` (in grid cells a second — §10 converts), `fluid_build_world_to_grid`, `GlobalFluidEffector` as the shape of the ambient *and* as the exact-ODE relaxation step `cloth_drag_response` copies |
1872
- | Worker offload | `ecs/async/WorkerSystem.js`, `SystemWorkerHost`, `SystemWorkerLoopback`, `WorkerFluidSystem` as the worked example |
1873
- | Fixed step | `EntityManager`'s fixed-step loop, gate and `fixedStepTick` |
1874
- | Transforms | `Transform64`, `Node3D` |
1875
- | Particle → render-vertex mapping | `Skin` — joints + inverse-bind matrices + the four-weight palette; cloth writes joints and the existing pass does the rest |
1876
- | Reaching a model's joints from its entity | `SceneBundleInstance` (`skins`, `nodes` by name), `register_instance_animation` |
1877
- | Deformation, bounds refresh, BLAS refit | `GPUMeshSkinningContext`, `shader_skin_matrix_prep`, `bounds_refresh/` — all of it inherited by driving joints rather than vertices |
1878
- | Non-finite tripwire pattern | `find_non_finite_physics_state.js` |
1879
- | Strippable assertions | `core/assert.js` |
1880
- | Serialisation | the `*SerializationAdapter` pattern and its binary codecs |
1881
-
1882
- ---
1883
-
1884
- ### 13.1 What is *not* reusable — render interpolation
1885
-
1886
- `PhysicsSystem`'s producer/`__interp_restore` contract was in the ledger above and M5 took it out,
1887
- because it cannot express a rigged garment. `Interpolated` is an **entity-keyed marker**: the
1888
- `InterpolationSystem` files snapshots under a `key` it assigns from the entity id at link time. A
1889
- rope's links are entities and a skinned garment's joints are not — `shade_node_to_entity_composition`
1890
- does not expand a skinned node, "and that is a boundary rather than an omission" (§3) — so half the
1891
- feature has nothing to put the component on.
1892
-
1893
- Cloth also needs only half of what that contract does. `__interp_restore` exists to hand the
1894
- simulation its authoritative pose back before it integrates, because the render blend writes into the
1895
- components the simulation reads. Cloth's state is `ClothState.position` in the cloth's local frame and
1896
- its write-back never touches the anchor it reads, so **cloth never reads back what it writes** and has
1897
- nothing to restore. It needs the publish half, over a pose per node rather than a component per entity.
1898
- §18 carries what that would cost. `MEASUREMENTS.md` §43.
1899
-
1900
- ---
1901
-
1902
- ## 14. New shared code this justifies
1903
-
1904
- Three pieces belong in `core`, not in `cloth`, because the cloth need exposes a gap the engine has
1905
- anyway:
1906
-
1907
- 1. **Analytic SDF gradients** for sphere, capsule, box, cylinder and plane, as free functions in
1908
- `core/geom/3d/shape/sdf/`. **M2 built them.** Before it, `signed_distance_gradient_at_point` on
1909
- sphere, capsule, box and cylinder was `compute_signed_distance_gradient_by_sampling` — four SDF
1910
- evaluations plus a normalise, for shapes whose gradients are two lines of algebra. The shape classes
1911
- call the new functions, and so do their `signed_distance_at_point`s, so there is one implementation
1912
- of each field rather than two that can drift; cloth calls the same functions on packed data.
1913
-
1914
- One thing the sampled version was quietly wrong about, and it is the reason a degenerate normal
1915
- needed a decision rather than a default: at a sphere's centre the forward difference returns
1916
- `(1,1,1)/√3`, which is unit length and entirely an artefact of sampling three positive offsets from
1917
- one point. The analytic functions return a documented arbitrary direction there instead, because a
1918
- gradient of zero carries no force and a particle that reaches the exact centre of a sphere would
1919
- stay there for ever — and "it does not get stuck" is the second design commitment in §1.
1920
-
1921
- 2. **A 3D uniform hash grid.** **M4 built it**, as `core/geom/3d/hash-grid/PointHashGrid3.js`, and
1922
- **not** modelled on `core/geom/2d/hash-grid/SpatialHashGrid.js` as this line asked for. The 2D
1923
- grid is a persistent incremental structure over a bounded cell array holding AABBs with
1924
- identity; a cloth's particles are points, carry no identity across steps, live in an unbounded
1925
- domain and all move every step, so the operation that matters is *rebuild* and insert/remove
1926
- never runs. `PointHashGrid3` is a counting sort — count, prefix-sum, scatter — into a
1927
- power-of-two hash of the integer cell, with a `forEachNear` that walks the 27 neighbouring
1928
- cells and de-duplicates the buckets they hash into. Useful anywhere a broadphase over many
1929
- small equal-sized *points* is wanted; anything with extent and identity still wants the 2D
1930
- grid's shape. `MEASUREMENTS.md` §34.
1931
-
1932
- 3. **`PlaneShape3D`.** **M2 built it**: the half-space `{ p : n·p − offset ≤ 0 }`, with the outward
1933
- normal and its offset, a constant gradient and no degenerate configuration anywhere. A box standing
1934
- in for a ground plane has to be large enough that nothing reaches its edge and thin enough that
1935
- nothing tunnels through it, and those pull against each other in `Float32`.
1936
-
1937
- It is the one shape in the family that is **unbounded**, and that is a real cost paid in three
1938
- places: `support` is left unimplemented, because `argmax(p·d)` over a half-space is at infinity for
1939
- all but one direction and a large finite point would be a silently wrong contact manifold rather
1940
- than a loud failure; `sample_random_point_in_volume` likewise, since there is no uniform
1941
- distribution on an infinite volume; and `compute_bounding_box` answers with `±Infinity` on the
1942
- unbounded axes, which is what the set is. A consumer that cannot hold an infinite AABB must not be
1943
- handed a plane — which is why `ClothColliderIndex` is a linear scan rather than a BVH, and why it
1944
- bounds a plane from its world normal instead of projecting that box.
1945
-
1946
- 4. **A shortest-arc quaternion**, `core/geom/3d/quaternion/quat3_shortest_arc.js` — the minimal,
1947
- twist-free rotation carrying one unit vector onto another. **M1 needed it and the family did not
1948
- have it**: `quat3_*` had `multiply`, `integrate`, `nlerp` and the uint32 codecs but nothing that
1949
- answers "orient this bone along that direction", which is the whole of §11.1's write-back. It is
1950
- also the one place in the family with a genuine discontinuity — no continuous choice of axis exists
1951
- on the whole sphere, so an antipodal pair has to pick one — and having it in `core` with a spec
1952
- around that case is better than having it inlined in a write-back loop without.
1953
-
1954
- Test helpers for all of it live inside the `.spec.js` files or in `__fixtures__`, per the repo rule —
1955
- a shared helper module would ship on npm.
1956
-
1957
- ---
1958
-
1959
- ## 15. MagicaCloth2: what we take, what we leave
1960
-
1961
- MagicaCloth2 is the reference and it is a good one. It has shipped, it is fast, and its feature set is
1962
- the accumulated answer to a decade of production complaints. Taken:
1963
-
1964
- - **Two cloth kinds over one solver.** MeshCloth and BoneCloth differ only in how particles are seeded
1965
- and how results are written back. This is right and §11 copies it — expressed as component presence
1966
- rather than an enum.
1967
- - **A proxy mesh decoupled from the render mesh**, with distance-based reduction and custom skinning
1968
- back to the render vertices. Also right; §11.3 does the same thing with `bt_mesh_simplify_by_error`.
1969
- - **The inertia model.** World and local inertia fractions, movement and rotation speed limits, an
1970
- anchor that cancels inertia for a character on a moving vehicle, teleport detection with Reset/Keep
1971
- modes, and a stabilisation window after reset. This is the least glamorous part of the package and
1972
- the most valuable; P6 and P7 are it, nearly verbatim.
1973
- - **Max-distance and backstop** as first-class constraints rather than debugging aids. P3 and P4 lean on
1974
- them harder than MagicaCloth does — here the leash is the correctness argument, not a polish pass.
1975
- - **Tether/LRA** as the answer to over-stretch at low iteration counts.
1976
-
1977
- Left:
1978
-
1979
- - **The parameter surface.** MagicaCloth2's serialised data is a dozen constraint blocks, several of them
1980
- animation curves over "depth", and its per-constraint files run to hundreds of lines of tuning. That is
1981
- the direct consequence of a PBD solver whose apparent stiffness depends on iteration count: when the
1982
- physical meaning of a slider moves with the budget, you end up shipping a curve per slider. AVBD's
1983
- parameters do not move that way, so §3.3 can be one small value object — and the per-particle profile
1984
- that MagicaCloth authors with curves falls out of `d_geo` instead (§3.4).
1985
- - **Angle restoration and angle limit constraints.** These exist to give PBD chains a restoring force
1986
- that bending alone cannot supply at low iteration counts. With a solver that converges, bend rows plus
1987
- tethers cover the same ground with less to configure.
1988
- - **A separate spring constraint.** Subsumed by the slack row with a non-zero radius.
1989
- - **A distance constraint solved twice per substep** (before and after collision). An artefact of the
1990
- Gauss-Seidel ordering; the augmented Lagrangian's dual update does this work.
1991
- - **Its collision layering model.** We already have `Collider`, layers and masks; a parallel system would
1992
- be a second thing to keep in sync.
1993
- - **Its wind zones.** We have a fluid simulator (§10).
1994
-
1995
- ---
1996
-
1997
- ## 16. State of the art
1998
-
1999
- **The line this design sits on.** PBD → XPBD → substepped XPBD → VBD → AVBD.
2000
-
2001
- - **XPBD** (Macklin, Müller & Chentanez) made PBD stiffness independent of iteration count by
2002
- introducing compliance and a per-constraint multiplier. **Small Steps in Physics Simulation**
2003
- (Macklin et al. 2019) then showed that *n* substeps with one iteration each beats one step with *n*
2004
- iterations — less constraint error, far less artificial damping, and better behaviour across a wider
2005
- stiffness range than Krylov-based implicit solvers. Together these are the state of practice: Chaos
2006
- Cloth, MagicaCloth2 and Blender's cloth all sit here.
2007
- - **Vertex Block Descent** (Chen, Liu, Yang & Yuksel, SIGGRAPH 2024) reframed the step as block
2008
- coordinate descent on implicit Euler's variational form, solving one 3×3 system per vertex per
2009
- iteration. Unconditionally stable at any iteration count, no line search, and it colours the vertex
2010
- graph rather than the constraint graph. Its reported failure mode is exactly the one that matters here:
2011
- it struggles to converge under high stiffness ratios and has no mechanism for enforcing hard
2012
- constraints — which is what a pinned garment on a rigid body *is*.
2013
- - **Augmented Vertex Block Descent** (Giles, Diaz & Yuksel, SIGGRAPH 2025) fixes precisely that with an
2014
- augmented Lagrangian: hard constraints at effectively infinite stiffness without the instability, plus
2015
- substantially better convergence under high stiffness ratios. Reference implementations exist in 2D,
2016
- in Metal, in PhysX, in Houdini, and — relevant to this engine — as a WebGPU solver.
2017
- - **Going Further With Vertex Block Descent** (Saillant et al., 2025) extends VBD to higher-order
2018
- elements and confirms that substepping improves its convergence — which is why §5.2 substeps rather
2019
- than only iterating.
2020
-
2021
- **The line this design does not sit on.** **Incremental Potential Contact** and its descendants —
2022
- **C-IPC** for codimensional objects, **GIPC** and **AGIPC** on GPU — guarantee intersection-free,
2023
- inversion-free results through log-barrier contact and continuous collision detection. They are the
2024
- correct answer when correctness is the requirement, and they are the wrong answer at a millisecond
2025
- budget. We take one idea from that family: IPC's smooth static/dynamic friction transition, which VBD
2026
- also adopts.
2027
-
2028
- **Self-collision** is where the field is currently moving, because parallel position solvers have made
2029
- it the bottleneck rather than the solve. **Air Meshes** (Müller et al. 2015) tessellate the space
2030
- between objects and prevent element inversion, which handles untangling robustly and automatically but
2031
- requires maintaining a volumetric mesh. **Global Intersection Analysis** (Baraff, Witkin & Kass,
2032
- "Untangling Cloth", 2003) is history-free and resolves tangles as soon as geometry permits — the
2033
- property P4(a) is built on, even though we implement a far cheaper approximation of it. Recent work on
2034
- hierarchical culling via discrete curvature targets the same bottleneck.
2035
-
2036
- **Industry.** Chaos Cloth (Unreal) is substepped XPBD with long-range attachments, max distance and
2037
- backstop distance/radius — the same safety constraints §7 relies on, which is useful independent
2038
- confirmation that they are what production actually needs. Comparative studies of VBD against PBD in
2039
- Unity report parity at low vertex counts and a widening VBD advantage as the count rises, which matches
2040
- the per-iteration cost story: VBD does more arithmetic per vertex and far less coordination.
2041
-
2042
- Sources:
2043
- [Vertex Block Descent](https://arxiv.org/abs/2403.06321) ·
2044
- [VBD project page](https://graphics.cs.utah.edu/research/projects/vbd/) ·
2045
- [Augmented Vertex Block Descent](https://graphics.cs.utah.edu/research/projects/avbd/) ·
2046
- [AVBD reference implementation](https://github.com/savant117/avbd-demo2d) ·
2047
- [WebGPU AVBD](https://github.com/jure/webphysics) ·
2048
- [Going Further With Vertex Block Descent](https://onlinelibrary.wiley.com/doi/10.1002/cav.70039) ·
2049
- [XPBD](https://matthias-research.github.io/pages/publications/XPBD.pdf) ·
2050
- [Small Steps in Physics Simulation](https://mmacklin.com/smallsteps.pdf) ·
2051
- [Untangling Cloth](https://dl.acm.org/doi/10.1145/882262.882357) ·
2052
- [Air Meshes for Robust Collision Handling](https://dl.acm.org/doi/10.1145/2766907) ·
2053
- [Codimensional IPC](https://arxiv.org/pdf/2012.04457) ·
2054
- [Efficient Self-Collision Culling via Discrete Curvature](https://www.mdpi.com/2227-7390/14/9/1504) ·
2055
- [VBD vs PBD in Unity](https://www.mdpi.com/2076-3417/14/23/11072) ·
2056
- [Chaos Cloth](https://dev.epicgames.com/documentation/en-us/unreal-engine/clothing-tool-in-unreal-engine)
2057
-
2058
- ---
2059
-
2060
- ## 17. Milestones
2061
-
2062
- Each milestone is shippable and each `done` names evidence that ran. Nothing is reported complete on
2063
- the strength of code existing.
2064
-
2065
- **M0 — the solver core. DONE, 2026-09-10.** `ClothState`, `cloth_step`, the vertex update, stretch and
2066
- bend rows, colouring. Pure functions over typed arrays; no ECS, no worker, no rendering.
2067
- *Proofs, all green:* finite-difference agreement between each row's analytic gradient and its numerical
2068
- gradient; the P1 property test (see the correction in §7 P1 — strict per-update energy monotonicity is
2069
- not true and the gate is what replaced it); a measured choice of α, β, γ and `k_min`/`k_max` with the
2070
- sweep recorded in `MEASUREMENTS.md`, nothing copied; the performance budget measured on real topology.
2071
- 43 tests across six spec files.
2072
-
2073
- **M0 changed the design in six places**, each with the measurement behind it in `MEASUREMENTS.md` and
2074
- each folded back into §5.2, §5.3, §6, §7 P1 and §7 P5 above. In one line each: the multiplier and the
2075
- penalty band apply only to *hard* rows; the band is a ratio of `m/h²` rather than a stiffness; the PSD
2076
- half of the geometric-stiffness term has to be kept; the 3×3 solve has to be an LDLᵀ; an SPD Hessian
2077
- gives a descent direction rather than a monotone step; and P5's jitter mechanism does not reach soft
2078
- rows, which need M1's damping.
2079
-
2080
- **Deliberately not in M0:** Chebyshev acceleration (§5.2) is gated on a particle not being in contact,
2081
- and contacts arrive at M2, so there is nothing here to measure it against. The velocity clamp named in
2082
- P1 is derived from `ClothDynamics#thickness` and belongs with the component that carries it — and it
2083
- would have masked the property M0 exists to measure.
2084
-
2085
- **M1 — chains end to end. DONE, 2026-09-10.** `Cloth`, `ClothFlags`, `ClothExclude`, `ClothDynamics`,
2086
- `ClothDynamicsFlags`, `cloth_dynamics_library.js`, `ClothSystem`, subtree seeding and write-back, the
2087
- slack and tether rows, the strain-limit row, the inertia model, teleport handling, sleep, and the
2088
- velocity clamp P1 deferred. No collision.
2089
- *Proofs, all green:* P2, P5, P6 and P7, each with a companion test that runs the same scenario with the
2090
- mechanism switched off; `ClothDynamics` value semantics and the measured mapping; three ordering tests
2091
- — the derived order, the recomposition idempotency that makes it not matter, and a bound on the
2092
- divergence when it is inverted; a second playground page, `playground/rig.html`, driving the real
2093
- ECS with a rope, a chain, a five-strand rig under one `Cloth`, a hanging sign with a `ClothExclude`,
2094
- and a nested pair. **43 tests across four spec files** — the leash row's 8, the system's 9, the
2095
- pathology budget's 11, the value semantics and the mapping's 15 — plus 5 for the shortest-arc
2096
- primitive and 4 more in `system_execution_order.spec.js`, for 52 in all. The engine suite is green
2097
- at 18,179.
2098
-
2099
- **M1 changed the design in seven places**, each with the measurement behind it in `MEASUREMENTS.md`
2100
- §9–15 and each folded back above. In one line each: three of §6's clamps had the wrong sign and would
2101
- never have bound; §3.4's leash radius needs a factor of two before `slack = 1` means "no leash"; §7
2102
- P1's velocity clamp cannot be derived from `thickness`, because that is a tunnelling bound and a
2103
- tunnelling bound is *below* ordinary cloth speeds; §11.1's chain bending has no diamond to build a
2104
- hinge on and uses a skip row instead; §11.1's write-back has to write the **local** transform as well
2105
- as the world one; the levers no longer close the frame budget one at a time; and the strain-limit row
2106
- buys a third of the over-stretch rather than the allowance it asks for, which makes `stretch` the
2107
- weakest field in the parameter set.
2108
-
2109
- **And one thing M1 got wrong before it got it right, which is worth recording because the wrong answer
2110
- was plausible.** §4.1 correctly identified that neither of the engine's two ordering mechanisms can
2111
- place cloth after a poser, and the first attempt was to add a third — an execution-phase number on
2112
- `System`, with a `PostPose` value. That put the vocabulary of animation into `engine/ecs`, which has
2113
- none, to buy an ordering the design should not have needed in the first place. Removing the need was
2114
- both smaller and better: writing the local transform makes the write-back idempotent, which turns the
2115
- placement into a bounded quality question and leaves the scheduler alone.
2116
-
2117
- **Deliberately not in M1:** P4(d)'s bounded escape hatch, which needs a per-particle residual monitor
2118
- and belongs with the collision rows that make a particle get *stuck* in the first place — a chain has
2119
- no configuration to be stuck in. Chebyshev acceleration, still gated on contacts that arrive at M2. `β`,
2120
- which M0 could not choose and M1 has no transient rows to choose it with either — every row M1 adds is
2121
- persistent. The serialization adapters: nothing in M1's scope list names them, and a codec written
2122
- before `ClothRig` exists would be revised by M3 before it was ever loaded — **true of one of the three;
2123
- the correction follows this block.**
2124
- Interpolation: cloth writes `Transform64` at the fixed rate and §12's producer/`__interp_restore`
2125
- contract is not wired, which shows as a cloth updating at the fixed rate rather than the frame rate
2126
- and is a visible-but-not-wrong gap. M5 is where it belongs, with the worker.
2127
- **This is the first shippable feature.**
2128
-
2129
- **Corrected 2026-09-13: the deferral above only ever fitted `ClothRig`'s adapter, and it was applied to
2130
- all three of them from here to the end of the plan.** It fits `ClothRigSerializationAdapter`, whose
2131
- component points at a built, shared, immutable asset and therefore needs the asset-identity story §3.2
2132
- only gestures at; and it fits `ClothProxy`'s own codec, which genuinely does depend on what §11.3
2133
- step 9 adds to a proxy. It never fitted `Cloth` or `ClothCollider`. Those two are scalars — `blend`,
2134
- `layer`, `mask`, `flags` and a `ClothDynamics` value object of ten more; `inflation`,
2135
- `friction_scale`, `layer`, `mask`, `flags` — and neither holds a proxy, a skin, or anything either
2136
- geometry path introduces. The evidence that the revision this sentence feared was never coming is M3
2137
- itself: it changed the design in four places (`MEASUREMENTS.md` §24–29) and not one of them was a
2138
- field on either component.
2139
-
2140
- What turned a sequencing call into a hole is that it was **re-deferred rather than re-examined**. M2
2141
- deferred all three "per M1's own reasoning"; M3 deferred them again for M1's reason while, in the same
2142
- list, deferring the unrigged path that the reason had just been made to wait on; and §11.3 step 9 is
2143
- in no milestone at all. A wait with no scheduled end is not a sequencing decision, and what it cost is
2144
- that M6 closed this plan with a subsystem a scene could not contain: a `Cloth` could be added from
2145
- code and from nowhere else. `ClothSerializationAdapter` and `ClothColliderSerializationAdapter` are
2146
- built and specced as of 2026-09-13, outside any milestone and with no `MEASUREMENTS.md` section,
2147
- because there is no measurement behind them — a codec has nothing to measure, and that file's sections
2148
- belong to milestones. `ClothRigSerializationAdapter` and the proxy codec stay deferred, on the reason
2149
- as narrowed here.
2150
-
2151
- **M2 — collision. DONE, 2026-09-10.** `ClothCollider`, `ClothColliderFlags`, `ClothColliderSystem`,
2152
- `ClothColliderIndex`, the packed record and its swept bounds, analytic SDF gradients and
2153
- `PlaneShape3D` in `core` (§14.1, §14.3), the contact row and its friction row, the backstop row, and a
2154
- measured `β` and contact penalty band.
2155
- *Proofs, all green:* the packed field dispatch agreeing with the shape objects it was extracted from,
2156
- across all five kinds under a non-axis-aligned pose; the index's swept bounds, layer filter, handle
2157
- reuse and plane handling; the contact row against Coulomb's own answer for a slope; friction's anchor
2158
- dragging cloth with a sliding surface; P3 measured swept against unswept from 2 to 40 m/s; the whole
2159
- stack through a real `EntityManager`, including a collider arriving, leaving, being filtered out and
2160
- waking a sleeping cloth. **53 tests across five new cloth spec files** — 15 for the collider layer, 21
2161
- for the two new rows, 12 for the ECS path — plus **21 in `core`** across two more, for the SDF family
2162
- and `PlaneShape3D`, and the M0 and M1 suites unchanged at 93. The engine suite is green at 18,251. A third playground page, `playground/collide.html`, with six collider
2163
- scenes, a sweep toggle and the contact band live.
2164
-
2165
- **M2 changed the design in six places**, each with the measurement behind it in `MEASUREMENTS.md`
2166
- §16–23 and each folded back above. In one line each: §8.2's swept pose cannot be a lerp of two
2167
- matrices, because that is not a rotation and it shrinks the collider by twice a garment's thickness at
2168
- the anchor's own rotation limit; sweeping the poses does nothing unless the contact **set** is swept
2169
- too, and without that a capsule at 20 m/s produced *no rows at all*; §6's collision and friction rows
2170
- cannot be soft penalties, because a soft row's whole force is 4.6 N against the 376 N an 8 m/s arrival
2171
- needs; friction's clamp is one-sided, because its `C` is a magnitude; §5.4's Jacobi treatment is not
2172
- needed for a collider contact, which is unary; and Chebyshev acceleration, which §5.2 carried as
2173
- available, loses on every axis and is gone.
2174
-
2175
- **Deliberately not in M2:** P4(d)'s bounded escape hatch, still — M2 has the contacts that make a
2176
- particle get stuck, and also measured the one stuck configuration whose *residual is zero*, which is
2177
- the quantity the escape hatch was to watch (§7 P4). It belongs with the self-collision of M4, where a
2178
- particle can be pinched between two rows rather than merely held by one. The backstop row is built and
2179
- specced, but nothing **authors** one: its plane comes from a garment's own skinned surface and a
2180
- skinned garment is M3. (**"and driven by the playground" was not true and has been removed** — no page
2181
- under `playground/` writes `ClothState#backstop_plane`, then or now. The row's own docblock made the
2182
- same claim and has been corrected. What holds it up is `cloth_backstop_row.spec.js`, which is a
2183
- solver-level spec rather than a scene, and that is the honest status of a row with no author.) Two-way coupling, per §2. The serialization adapters, per
2184
- M1's own reasoning — which, as M1 now records, reached only `ClothRig`'s. The other two were not
2185
- blocked by anything M2 had in hand or M3 was about to add, and this line is the first of the two
2186
- restatements that carried them anyway.
2187
-
2188
- **M3 — skinned garments, the rigged path. DONE, 2026-09-10.** `ClothRig`, `ClothProxy`,
2189
- `cloth_proxy_from_joints` (§11.3 steps 5–8), the rig seeder, and joint write-back into
2190
- `Node3D.transform_local`. **The unrigged path is not here** — §11.3 step 9's generated joint set and
2191
- generated skin — and §18 carries what M3 measured about it instead.
2192
- *Proofs, all green:* the P8 corpus for a joint tree, each refusal naming what an author can act on;
2193
- the rig path end to end through a real `EntityManager` — the authority claim, the tether under a
2194
- swinging shoulder, `blend = 0` reproducing the baked bind pose, a model that has not loaded waiting
2195
- rather than failing, a proxy that does not fit its skin refused once by name, and a rigged cape
2196
- colliding with a body; and the entity path unchanged beside it. **23 tests across two new spec
2197
- files** — 12 for the proxy build, 11 for the rig path — plus 14 unchanged for the pose query M3
2198
- refactored. The engine suite is green at 18,274. A fourth playground page,
2199
- `playground/garment.html`, with a `Node3D` skeleton, four garments and four motion profiles.
2200
-
2201
- **M3 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
2202
- §24–29 and each folded back above. In one line each: §11.2's writable handle is *not* what the
2203
- animation systems write, and cloth has to claim `TransformAuthority.CPU` to make its write-back
2204
- visible at all; §11.1's rig path is not "live" for a rigged garment, because cloth owns those joints
2205
- and their authored pose is the proxy's baked bind pose; §11.3 step 5's weight-derived fixed set is
2206
- the unrigged rule and the topology answers better for a rigged garment; and §11.2's "~60 output
2207
- joints" is four to six times short of what a four-weight palette needs for a few per cent of
2208
- fidelity.
2209
-
2210
- **And the strongest result is a number rather than a correction.** §11 claims the two paths are "the
2211
- solver, identical either way". Built as an entity subtree and as a skin's joints, the same garment
2212
- produces **bit-identical** particle trajectories across three fabrics and three motion profiles.
2213
- That is what the row builder and the pose reconstruction being *one* function each buys, and it is
2214
- why M3 moved both out of the entity path's files rather than copying them.
2215
-
2216
- **Deliberately not in M3:** the unrigged path, per §17's own sequencing and behind §18's measured
2217
- budget. §11.3 step 10's bind-pose collision validation, which `cloth_proxy_from_joints` cannot do
2218
- because it has a joint tree and no collider set — and which `MEASUREMENTS.md` §29 makes the argument
2219
- for, because a garment authored inside its own body reads as a *tether* failure and costs an hour to
2220
- diagnose. `ClothProxy`'s binary codec and `ClothRigSerializationAdapter`, for M1's own reason: a
2221
- codec written before the unrigged path exists would be revised before it was ever loaded. **Those two
2222
- only, corrected 2026-09-13.** As written this sentence carried `Cloth`'s and `ClothCollider`'s
2223
- adapters with them, over a set of three, for a reason that held for one — and it did so in the same
2224
- list that defers the unrigged path the reason had just been made to wait on, which is what closed the
2225
- loop: the dependency and the thing depending on it were deferred by the same paragraph. Neither scalar
2226
- component holds a proxy, so §3.2's asset-identity question — the real content of M1's reason — was
2227
- never theirs to answer. Both adapters were built on 2026-09-13; M1 carries the full correction.
2228
-
2229
- **M4 — self-collision. DONE, 2026-09-11.** `core/geom/3d/hash-grid/PointHashGrid3.js`, the
2230
- particle-particle row (`solver/constraint/cloth_self_row.js`), the pair search
2231
- (`solver/cloth_self_find.js`) with its exclusion set, and `ClothState`'s pair storage, Jacobi
2232
- scratch and `self_radius`. Wired into `ClothSystem` behind `ClothDynamicsFlags.SelfCollision`,
2233
- which three library entries now set. **M4b is not here** — point-triangle and edge-edge, per §9's
2234
- own deferral — and §34 is the argument for re-pricing it before anyone builds it.
2235
- *Proofs, all green:* the P4 tangle spec extended to self-intersection counts, as this line asks —
2236
- a folding sheet crossing itself, and the row removing more than half of those crossings where the
2237
- same sheet without it does not; the row's sign, and its Jacobi scratch being rebuilt rather than
2238
- accumulated so a pair that separates stops pushing; the exclusion rule against a stretch row, a
2239
- bend stencil and a pinned particle, and each pair found once with the lower index first; and the
2240
- grid's counting sort, its 27-cell walk, its de-duplication across hash collisions and its
2241
- independence from where in space the points sit; and the radius at both ends — a sheet's closest
2242
- ungoverned pair being exactly two hops, strips measuring their gap rather than their link length, and
2243
- a pinned pair not counting because no row will ever act on it. **22 tests across four new spec
2244
- files** — the grid's 9, the search's 4, the row's 4, the radius's 5 — plus the library's
2245
- self-collision assertion rewritten and `ClothState`'s buffer test extended. The engine suite is green
2246
- at 18,296. The garment playground gains a self-collision toggle and a live pair count beside its
2247
- contact count.
2248
-
2249
- **M4 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
2250
- §30–37 and each folded back above. In one line each: §9's radius is the **proxy's** resolution and
2251
- not the fabric's thickness, which is the difference between removing 21% of a sheet's
2252
- self-intersections and removing 82%; §14.2's 3D hash grid is *not* modelled on the 2D one, because
2253
- a cloth's particles are points without identity that all move every step, so the operation that
2254
- matters is rebuild and insert/remove never runs; §9's list of which entries set the flag was drawn
2255
- along what the garment is rather than how far the fabric bends, and leather — at the top of the
2256
- bend scale — does not tangle at all and is made slightly *worse* by the row; and M4a's normal is
2257
- the line between two particles rather than the surface orientation §9 promises, which satisfies
2258
- what P4(a) is actually asking for and does it more simply.
2259
-
2260
- **And one thing the design had no line for at all.** §9 never wrote a radius down, which is how
2261
- that question stayed open long enough to be got wrong three times. `max(thickness, shortest_row/2)`
2262
- has an upper end: a diameter reaching the closest pair *no row governs* puts a cloth which is not
2263
- folded at all in permanent contact with itself. Leather at 20 mm on a 20 mm proxy is exactly that
2264
- case and found 1,398 pairs a step on a 576-particle sheet that was barely folded (§36). And that
2265
- distance has to be **measured** rather than assumed to be two hops, because two hops is a fact
2266
- about sheets: the playground's cape has 8 cm links and an 8.5 cm gap between strips, half what the
2267
- link length implies, and the assumed ceiling left it with three pairs permanently in contact at
2268
- rest. `ClothState#self_free_pair` is measured once at seed, per particle, and the ceiling is two
2269
- fifths of it. On a sheet the two rules agree exactly, so §31–36 stand. `MEASUREMENTS.md` §37, §68.
2270
-
2271
- **And the playground earned its keep.** Every measurement from §31 to §36 is on a sheet, because a
2272
- sheet is the only thing here with a surface to pass through. Put the same row on the geometry M3
2273
- actually ships and it does something no sheet could have shown: **without it a cape settles at half
2274
- the width it was drawn at** — 0.178 m against a 0.340 m bind pose — because nothing holds five
2275
- independent strips apart and gravity swings them all into the same place. With it, 0.351 m. That is
2276
- not a transient tangle of the kind §32 measured; it is what the garment settles into.
2277
-
2278
- **Deliberately not in M4:** **conditional storage for the pair list.** `ClothState` is one buffer
2279
- sized at seed, and M4's three self-collision regions are 16% of it on the §17 workload — 100 KB a
2280
- garment — whether or not the flag is ever set, which four of the seven library entries never do.
2281
- Sizing them lazily would make `ClothDynamicsFlags.SelfCollision` a reseed trigger, which it is not
2282
- today, and that is a change to the reseed contract rather than a tidy-up. `MEASUREMENTS.md` §33.
2283
- M4b, above. A **swept pair search** — the set is discovered from the
2284
- positions the last step left, with no margin for the travel of the step about to run, which is the
2285
- shape of the gap §20 found in the contact set. §35 went looking for it and found the opposite:
2286
- widening the search a hundred and fifty times over does not move the crossing count, so what is
2287
- left is not a discovery problem but the tier's own stated limit, and it is M4b's. And **P4(d)'s
2288
- bounded escape hatch**, which M2's entry above handed to M4 on the grounds that self-collision is
2289
- what gives a residual monitor something to escape from — measured across ninety-six fold
2290
- realisations, a self-tangled sheet unties itself every time the gust stops, so M4 still has no
2291
- configuration to escape from. The case P4(d) names is a particle wedged between a collider *and* a
2292
- fold, which is collision and self-collision together, and M4 has not measured it.
2293
-
2294
- **M5 — the worker. DONE, 2026-09-12.** `WorkerClothSystem`, `ClothWorkerCore`, `cloth.worker.js`,
2295
- `makeClothWorker` and the protocol — plus the two pieces that had to exist for the parity gate to be
2296
- structural rather than hopeful: `ClothWorld`, which is every cloth in a scene and everything about
2297
- stepping one that is not arithmetic, owned by *both* systems; and `cloth_step_instance`, which is the
2298
- arithmetic, driven by a `Float64` parameter block that is also the command region's per-cloth record.
2299
- `ClothState` gains shared-memory backing, a descriptor, the collider table and a result region.
2300
- *Proofs, all green:* bit-parity over the **whole `ClothState` buffer at every step** of two runs — a
2301
- walking anchor for 240 steps, and colliders plus self-collision for 180 — with the write-back's
2302
- one-tick lag pinned tick by tick rather than asserted at the end; the state a worker steps being the
2303
- main thread's memory rather than a copy; a cloth being neither stepped **nor advanced** until its
2304
- registration is acknowledged, which is what keeps the two systems' step *sequences* identical rather
2305
- than merely similar; a re-seed retiring its registration and waiting again; a despawn between dispatch
2306
- and step not failing the bridge; sleep removing a cloth from the dispatch and `wake` putting it back;
2307
- shutdown settling the step in flight, releasing the gate and dropping the worker; and the derived
2308
- execution order landing `WorkerClothSystem` exactly where `ClothSystem` lands. **10 tests in one new
2309
- spec file**, with the M0–M4 suites unchanged at 183. The engine suite is green at 18,579.
2310
-
2311
- **M5 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
2312
- §38–44 and each folded back above. In one line each: §12's one buffer for the whole cloth world is one
2313
- buffer per cloth, because `reseed` resizes one while others are in flight; §12's collider table does
2314
- belong in that buffer and therefore needs a capacity rather than a growth policy, and the capacity is
2315
- bounded by the *scene* — thirty records for three characters — rather than by the garment; §12's
2316
- "`ClothDynamics` reaches the worker through the command region" is the *mapped* step parameters rather
2317
- than the component, because mapping them twice is two copies that cannot be proved equal; and §13's
2318
- reuse of `PhysicsSystem`'s interpolation contract does not hold, because that contract is entity-keyed
2319
- and a rigged garment's joints are not entities.
2320
-
2321
- **And the strongest result is again a number rather than a correction.** §12 asks the two systems for
2322
- bit-identical results. Over 420 steps across two scenes, comparing the entire state buffer after every
2323
- one of them, the number of differing bytes is **zero** — and it is zero by construction rather than by
2324
- luck, because there is one `ClothWorld`, one `cloth_step_instance`, and one parameter block that both
2325
- systems fill and neither system interprets. The main thread's share of a thousand-particle garment goes
2326
- from 3.62 ms to 0.18 ms; across the library's shapes, 92% to 96% of it crosses. `MEASUREMENTS.md` §40,
2327
- §41.
2328
-
2329
- **Deliberately not in M5:** **render interpolation**, which §18 said would land here and which M5
2330
- decided against with three findings rather than a deferral — the named contract is entity-keyed and
2331
- cannot reach a rigged garment's joints, cloth needs only the half of it that has no name, and the
2332
- worker removes the cheap implementation by owning `ClothState` outside the fixed pass. What it needs
2333
- instead is a published pose slot, and §18 now carries what that costs (about 0.12 ms a frame on the
2334
- §17 workload, which roughly triples the main thread's share of cloth and leaves it at a seventh of
2335
- its pre-M5 cost) and what it buys (half a link length a step at a walk, a whole one at a sprint).
2336
- `MEASUREMENTS.md` §43. **A playground page**, for a reason about the dev server rather than the cloth:
2337
- `SharedArrayBuffer` needs a cross-origin isolated page, and adding `COOP`/`COEP` to
2338
- `vite.config.mjs` changes how every other playground page in the package loads its subresources —
2339
- to demonstrate a bridge `SystemWorkerLoopback` already tests more precisely than a page could.
2340
- **Culling (§12.1)**, which is a policy over the same dispatch list sleep already uses and belongs with
2341
- a camera. And **a deeper pipeline**: `WorkerSystem` allows one step in flight and the fixed-step gate
2342
- means a cloth could not use a second.
2343
-
2344
- **M6 — wind. DONE, 2026-09-13.** §10 end to end: `wind/AbstractClothWind.js` and the two sources
2345
- behind it (`ClothAmbientWind`, `ClothFluidWind`), `wind/cloth_sample_air.js`,
2346
- `solver/cloth_wind_accelerate.js`, `ClothState`'s air and external-acceleration regions,
2347
- `cloth_drag_response`, the parameter block's fourteenth word, and `ClothWorld`'s wind source, clock
2348
- and wake test.
2349
- *Proofs, all green:* the normal-facing factor on both topologies, which is the whole of what M6 adds
2350
- to the physics — a sheet reading 1.000 broadside and 0.000 edge-on, a chain reading the reverse, and
2351
- neither a skip row nor a strain-limit row able to fabricate a plane out of a line; the two sources
2352
- through the interface, including a field answering in metres rather than grid cells and `varies()`
2353
- being a promise the sampler acts on; the ECS path end to end — a garment blown downwind, one inside a
2354
- `FluidComponent` reading the simulation while one ten metres away reads the weather, a settled banner
2355
- sleeping in a steady wind and waking on a gust, and a scene with no source producing the cloth M5
2356
- shipped **byte for byte** across 120 steps; the drag response closing a fraction of the gap to the
2357
- air that is strictly below one at every step length from 1/240 s to 1 s; and §12's parity gate
2358
- extended to a third run with wind live. **25 tests across three new spec files** — the term's 9, the
2359
- sources' 10, the ECS path's 6 — plus the parity spec's eleventh, for 26 in all, with the M0–M5
2360
- suites unchanged at 193. The engine suite is green at **18,638**, and the same suite run against M5's
2361
- own commit in a detached worktree reads 18,612 on this machine, so the difference is exactly M6's
2362
- twenty-six. A fifth playground page, `playground/wind.html`, with a banner hanging in a real
2363
- `FluidSimulator`'s wake.
2364
-
2365
- **M6 changed the design in five places**, each with the measurement behind it in `MEASUREMENTS.md`
2366
- §45–§52 and each folded back above. In one line each: §10's `|n·û|` cannot be written for a chain,
2367
- which has no normal and wants the complementary factor anyway, so both are one expression over the
2368
- span of the incident rows; §10's "samples its velocity field" needs a unit conversion, because
2369
- `FluidField` answers in grid cells a second and a garment blowing at a speed that depends on the
2370
- voxel size is not a garment; §10 never said which thread samples and it cannot be the worker's, so
2371
- the field stays where it is and three floats a particle cross in the cloth's own buffer; `drag`
2372
- cannot multiply the flow directly, because one acceleration held for a whole fixed step overshoots
2373
- the air once `rate·dt > 1`; and §12's thirteen-word block is fourteen, with a clock that has to count
2374
- steps rather than ticks or the two systems' weather diverges at step 1.
2375
-
2376
- **And the strongest result is again a number rather than a correction.** §10 offers the normal-facing
2377
- factor as the difference between a flag and a rope. In uniform air it is: turning the wind ninety
2378
- degrees at the same speed takes the banner's aerodynamic acceleration from **43.2 m/s² to 0.08**, a
2379
- factor of five hundred. In a **wake** it does nothing at all — 0.216 against 0.280 — because a wake
2380
- has velocity in every direction and no orientation of a surface is edge-on to one. That is not a
2381
- failure of the factor; it is the measurement saying §10's argument for reading a simulation is
2382
- stronger than §10 knew, and that the flag-versus-rope claim is a statement about laminar air.
2383
- `MEASUREMENTS.md` §51.
2384
-
2385
- **Deliberately not in M6:** **a spatially varying ambient.** The gust is uniform in space, so a long
2386
- banner does not see it arrive at one end before the other — and that is what makes the no-simulation
2387
- case cost one sample for a whole garment instead of one a particle (§48). Spatial structure is what a
2388
- fluid field is *for*, and a second, worse one inside the fallback is the duplication §10 exists to
2389
- avoid. **A blend at a field's edge**: a point inside a volume reads the volume and a point outside
2390
- reads the ambient, so a garment straddling the boundary sees a step rather than a ramp. Where the
2391
- ambient is the far field the volume relaxes toward — `GlobalFluidEffector`'s own `wind` — the step is
2392
- small and is mostly the disturbance the volume exists to carry; where it is not, the answer is a
2393
- volume large enough to hold what moves through it. **Two-way coupling into the fluid**, per §2's
2394
- one-way rule: cloth reads the air and does not disturb it, and anything that should has
2395
- `FluidObstacle`. And **per-substep sampling**, because §10 says per step and §48 measures the sample
2396
- at 58 ns a particle — the one part of this that is not free is the one part per-substep sampling
2397
- would double.
2398
-
2399
- **Post-M6 — six defects, six fixes, and the plan corrected where the code proved it wrong. DONE,
2400
- 2026-09-13.** No new mechanism and no new milestone: a pass over what M6 left, taken in the order the
2401
- defects were found.
2402
-
2403
- 1. **A cloth resting on a collider never slept.** §7 P5 has asked for "maximum particle displacement
2404
- over a window" since M0 and M1 built an instantaneous speed; four of the seven library fabrics
2405
- never slept at all on a floor, because a resting contact is a bounded limit cycle whose *speed* is
2406
- eighteen times the threshold and whose *travel* is a tenth of a millimetre a step.
2407
- `CLOTH_SLEEP_DISPLACEMENT_RATIO`, and every entry on a floor or a sphere sleeps.
2408
- `MEASUREMENTS.md` §53.
2409
- 2. **A static collider was tunnelled from 3 m/s.** M2 folded the collider's travel into the discovery
2410
- reach and argued the particle's own away with a velocity clamp three orders of magnitude too large
2411
- to be the bound. The reach carries `|v_i|·dt` too; what is left is the substep count, measured.
2412
- §7 P3, `MEASUREMENTS.md` §54.
2413
- 3. **Seeding took three things from the scene and gave none of them back**: the authored pose across a
2414
- re-seed, the authored pose on removal, and the GPU's authority over a rig's joints. And `ClothRig`
2415
- was read once rather than watched, so a rig arriving a fixed step late seeded a skeleton as a rope.
2416
- §11.1, §11.2, §18.
2417
- 4. **A collider arriving and a collider retiring were both asked about a pose.** A collider spawned
2418
- inside a sleeping cloth has not *moved*; a handle retired and reused inside one tick is the same
2419
- handle. The wake test compares the handle set, and a retired slot is cold for a step.
2420
- `MEASUREMENTS.md` §55.
2421
- 5. **The parity gate had one cloth in it.** A nested pair and a staggered spawn are two scenes in
2422
- which the two systems could differ and did — a tick's phases ran in different orders, the
2423
- write-back composed against a newer anchor than its own step, and the gust read a world clock that
2424
- moves while a second cloth is registering. §12, `MEASUREMENTS.md` §56, §57.
2425
- 6. **The plan said eight things the code does not do**, each corrected in place above: P4(d)'s escape
2426
- hatch and `ClothDiagnostics` recorded as never built rather than "on by default"; §5.2's constants
2427
- line brought to `β = 1e3` with the contact floor recorded and `γ = 1` named as the one unmeasured
2428
- constant left; §7 P1's gate stated as twice its measurements rather than as them; §3.3's `flags`
2429
- default; §6's claim about the library at 2×4, which was never true of `CLOTH_ROPE`; §8.3 and §13's
2430
- `combine_material.js`, which nothing imports; §3.2 and §11.2's joints-are-`Node3D`s, which the
2431
- engine's own Rows-Not-Nodes work made false; §11.3's vertex painting; and the backstop row's
2432
- playground, which does not exist. §18 gains the wind span rank as a limit, recorded not fixed.
2433
-
2434
- *Proofs, all green:* eleven specs that were red before their fix and green after — the P5 collider
2435
- sleep gate over the whole library × three scenes, the swept-versus-unswept throw at a static capsule
2436
- and its three companions, the re-seed and the removal on the entity path, the rig's authority and the
2437
- two `ClothRig` edges, the four collider-lifecycle cases, and the two parity scenes. **The cloth suite
2438
- is 251 tests across 26 files**, against M6's 234.
2439
-
2440
- **Deliberately not in this pass:** the `γ` sweep; tightening §7 P1's gate to its measurements;
2441
- building the escape hatch or `ClothDiagnostics`; render interpolation; culling; the unrigged path;
2442
- M4b; the wind span rank, which is recorded in §18 and not fixed; the `ClothRig` serialization adapter;
2443
- and the physics adapter registry gap.
2444
-
2445
- **Performance budget — measured at M0, re-measured at M1, and it still does not hold on its own.** One
2446
- character's full cloth complement — 1,024 particles at 2 substeps × 4 iterations, on a triangulated
2447
- patch carrying 16.8 row incidences per particle — cost **3.50 ms per frame** at best at M0, against a
2448
- 2 ms target. **M1's rows made it 4.61 ms**: the strain limit is 0.73 ms (it doubles the stretch table,
2449
- and the stretch rows are already 69% of the frame), the tether 0.36 ms, the slack row 0.10 ms. What
2450
- that costs the plan is stated below.
2451
-
2452
- The estimate was wrong in its reasoning as well as its number: **gathering neighbour positions is not
2453
- what dominates.** The controlled test is to relabel the source vertices at random, which destroys the
2454
- locality `cloth_color_order` provides and changes nothing else — same colouring, same rows, same
2455
- arithmetic. It costs nothing measurable at the 1,024 particles §17 budgets for (the whole state fits in
2456
- L2) and 11% at four times that size. So the *entire* memory-locality component of the gather is a few
2457
- per cent of the frame, and a layout optimisation has at most 11% to win.
2458
-
2459
- Deleting the workload piece by piece puts 69% of the frame in the stretch rows, 18% in the bend rows
2460
- and 7% in everything else. That is a split of what each row kind *causes*, not of the arithmetic in
2461
- its accumulate: the variants sit in different physical states, and an isolated loop over one accumulate
2462
- measures a converged mesh rather than a moving one and disagrees with the deletion by a factor of five
2463
- on the stretch row. `MEASUREMENTS.md` §7 carries the discrepancy rather than a story that resolves it.
2464
-
2465
- The levers, priced at M0 (`MEASUREMENTS.md` §7) and again on M1's row set (§14):
2466
-
2467
- | Lever | M0's rows | M1's rows |
2468
- |---|---|---|
2469
- | Fewer iterations — 1,024 at 2×**2** | **1.69 ms** inside | **2.38 ms** over |
2470
- | Fewer substeps — 1,024 at 1×4 | **1.68 ms** inside | **2.40 ms** over |
2471
- | Fewer particles — ~500 at 2×4 | **1.54 ms** inside | **2.53 ms** over |
2472
-
2473
- **No single lever reaches 2 ms any more.** Each of them lands inside when a second is spent alongside
2474
- it — 400 particles at 2×4 is 1.72 ms, 576 at 2×2 is 1.28 ms, 1,024 at 1×2 is 1.17 ms — so this is a
2475
- sizing decision rather than a wall, and it is one the library now makes explicitly: every garment entry
2476
- spends two levers and says which two. The barycentric binding fallback from §11.2 is still not needed.
2477
-
2478
- Every figure here is the best of four independent rounds, which agree to ±5%. Cost is close to linear
2479
- in both particles and sweeps, so any one lever is enough on its own and they compose. **Note what the worker does not do:** §12 moves this off the main thread, it does
2480
- not make it cheaper, so "2 ms on the worker" with several characters in flight is a scheduling question
2481
- this measurement does not answer.
2482
-
2483
- **Re-measured at M2, and the library is not re-sized.** M2 adds a broadphase pass per particle per
2484
- step, up to four contact rows per particle, and two field evaluations per contact per iteration. On the
2485
- same workload: 4.43 ms with no collider system against M1's 4.61 — agreement to 4%, inside M1's own
2486
- spread — 4.47 ms with a collider out of range, and 4.6 to 5.5 ms with one to twenty colliders touching.
2487
- The column is not monotone in the collider count, because the variants sit in different physical states
2488
- and a cloth draped over a sphere is not the same workload as one hanging free; §7's warning about
2489
- attributing cost by deletion applies unchanged. What it licenses is that the collision layer costs
2490
- between nothing and a quarter of a frame and does not scale with the collider count over the range a
2491
- character occupies. Every library entry's own budget is untouched. `MEASUREMENTS.md` §23.
2492
-
2493
- **Re-measured at M5, and no entry is re-sized — but not for the reason the table reads.** The absolute
2494
- column came back 15% to 25% above M4's, on a machine carrying nineteen other node processes at 30–38%
2495
- background CPU, which is exactly the swing M4's own lesson predicts. What licenses leaving the library
2496
- alone is a *paired* measurement instead: `cloth_library_bench.mjs` never touches
2497
- `WorkerClothSystem` — it drives `cloth_step` over a `ClothState` directly — so the only thing M5
2498
- changed that it can see is the buffer layout, and an interleaved A/B of the two layouts in one process
2499
- reads 0.95 to 0.98 with the two states staying bit-identical for 1,600 steps. §33's ordering stands
2500
- entry for entry. `MEASUREMENTS.md` §39, §42.
2501
-
2502
- **Re-measured at M6, and no entry is re-sized — now with a measured reason rather than only a
2503
- disclaimer.** §10's aerodynamic term, priced in the bench's own §33 scenario, moves each entry by
2504
- −2.7% to +9.3% across three runs, with the sign not monotone in `drag`; and with the *physics* held fixed so the two
2505
- sides simulate the same cloth, the whole feature is **one per cent of a step**. The rest of that
2506
- column is a different cloth — a rope blown horizontal costs 3.24× a rope hanging straight down, which
2507
- is §7's warning arriving in a new setting and is about the rope. Three runs of the same bench within
2508
- an hour put the §17 baseline row at 3.49, 3.83 and 4.26 ms, so the absolute column is still the
2509
- machine: M5's advice to re-measure it quietly stands, and M6 adds that a process count is a proxy for
2510
- load rather than a measure of it. `MEASUREMENTS.md` §49, §52.
2511
-
2512
- **And the budget now means something slightly different.** The worker moves the cost rather than
2513
- reducing it, so 2 ms is still 2 ms — but it is 2 ms on a thread that is not drawing the frame, and the
2514
- question it answers has become "does all the cloth in the scene fit in one worker tick" rather than
2515
- "does one garment fit in a frame". The main thread's share of the §17 workload is 0.18 ms.
2516
- `MEASUREMENTS.md` §41. Re-measure the absolute column on a quiet machine, and check the process count
2517
- first.
2518
-
2519
- **M7 — attachment points. DONE, 2026-10-10.** `ClothPin`, `ClothHold`, `cloth_holds.js`,
2520
- `cloth_geodesic.js`, `ClothPinSerializationAdapter`; the rows built from a particle graph rather than a
2521
- parent array; held particles carried through the step (`ClothState#kinematic_target`) and leash rows
2522
- measured from a particle (`leash_source`); the rig proxy's bones-to-hang-from and frame joint. There is
2523
- no anchor: a cloth is held by whatever pins name it, from none up. §19 is the design.
2524
- *Proofs, all green:* `cloth_held.spec.js` (five of six red against the previous solver),
2525
- `cloth_pin.spec.js` (free fall, a rope held at its top, a rope bridge, letting go and catching mid-swing,
2526
- a holder teleporting, a holder outrunning the cloth, holds pulled beyond the fabric, a pin before its
2527
- cloth, lifecycle and refusals), `cloth_geodesic.spec.js`, the adapter's, a two-shoulder cape in
2528
- `cloth_rig.spec.js`, and a worker parity run in which a second holder drags, lets go and takes hold
2529
- again — bit-identical every step for 240. Cloth suite 327, slow 7, physics 1,861. *Cost:* +1.4% and
2530
- +2.8% median a step on a 129- and a 1,025-particle rope rig, paired against the previous build in one
2531
- process. *Equivalence:* a rope pinned at its own top under M7 and the same rope under the anchor differ
2532
- by 1.9e-7 m after one step, and grow apart exactly as fast as two copies of the old build nudged 1e-7 m
2533
- apart — chaos, not a change. `MEASUREMENTS.md` §62.
2534
-
2535
- **M7, sheets. DONE, 2026-10-10.** `cloth_proxy_from_mesh`: a proxy from a skin with one joint per vertex
2536
- and the triangles over them — a stretch row per edge, a hinge per interior edge, the authoring mistakes
2537
- refused by name (§7 P8). `ClothProxy` carries its graph for both builders; hinge rows are built wherever
2538
- a proxy has hinges, each stiffened as a ratio of its own inertia; a joint with triangles round it is
2539
- turned by its surface rather than swung along a chain. §19.5 is the design.
2540
- *Proofs, all green:* `cloth_proxy_from_mesh.spec.js`, and `cloth_sheet.spec.js` through `ClothSystem` —
2541
- a flag held at its hoist by a pole, every library fabric swinging down from a held edge and coming to
2542
- rest, a free sheet draped on a sphere with every joint turned onto its surface, and a cloth in two
2543
- pieces of which only one is held. *Found:* a flag on a pole in still air turns about the pole, and the
2544
- solver did it before holds existed (§18). `MEASUREMENTS.md` §63.
2545
- *The stage's remaining scenes, later:* a sheet carried round by friction on a turning sphere, and a
2546
- circle skirt that flares on a turning body and settles, both in `cloth_sheet.slow.spec.js`. The third, a
2547
- pile of ribbons, found that a fold resting on a fold never comes to rest at the shipped self-collision
2548
- stiffness (§18), and is kept as the proof a change to it needs. `MEASUREMENTS.md` §64.
2549
-
2550
- **Post-M7 — a fold resting on a fold comes to rest. DONE, 2026-10-11.** `CLOTH_SELF_RATIO` goes from
2551
- 8 to 2 (§9, §18). *Proofs, red on 8 and green on 2:* in `cloth_sheet.slow.spec.js`, a pile of three
2552
- silk ribbons fanned 60°, which sleeps with no crossing at any step and its order kept, and the circle
2553
- skirt, asleep after its twirl; in `cloth_sheet.spec.js`, the sheet draped on a sphere, asleep. *Cost:*
2554
- §32's folding sheet goes from 2.09 to 2.98 crossings a sample over 48 gusts, and the library bench
2555
- does not move. The turning-sphere spec's 2° on where the sheet stops after the spin was one
2556
- realisation and is now 10°. `MEASUREMENTS.md` §65.
2557
-
2558
- ---
2559
-
2560
- ## 18. Risks and open questions
2561
-
2562
- **AVBD has a short track record for cloth. — MEASURED, and the risk was real.** VBD's paper covers
2563
- cloth; AVBD's public reference implementations are 2D and rigid-body-focused. The augmented Lagrangian
2564
- layer is well understood in the abstract, but its interaction with cloth's particular constraint mix —
2565
- many soft rows, a few hard ones, transient contacts — is not something anyone has published numbers on.
2566
- M0 found exactly that interaction to be where the design was wrong: **the multiplier must not touch a
2567
- soft row at all** (it drove a 1 kN/m spring 100× stiffer than Hooke), and **the penalty band is only
2568
- meaningful relative to `m/h²`**, which is a cloth-specific statement because cloth's mass term is four
2569
- orders of magnitude below a rigid body's. Both are fixed and measured (§5.2, `MEASUREMENTS.md` §3–4).
2570
- The core is now stable across 10,000 steps of deliberately absurd cloth, settles a hard chain to 1e-14 J
2571
- at 80 links deep, and reproduces Hooke exactly on a soft row. The §6 row abstraction still means the
2572
- descent loop could be replaced with substepped XPBD without touching the constraint definitions, and
2573
- that fallback still costs one module — but nothing measured so far argues for taking it.
2574
-
2575
- **Per-vertex Hessian assembly in JavaScript may not hit the budget. — MEASURED, and it does not.**
2576
- 3.50 ms against a 2 ms target for 1,024 particles at 2×4. *Resolved by:* the plan's own first lever —
2577
- half the iteration count, or half the particles, lands inside — so this is a sizing decision for M1's
2578
- library and M3's proxy rather than an open risk. What is *not* resolved is that the cost is
2579
- concentrated in the stretch row and only a third of that is the gather, so the obvious memory-layout
2580
- optimisation is aimed at the smaller half. `MEASUREMENTS.md` §7.
2581
-
2582
- **System ordering. — DISSOLVED rather than resolved.** §4.1 carries the reasoning; in short, the
2583
- derived score is a *sum* over what a system references, so touching more can only move a system
2584
- **earlier**, and a consumer of a posed hierarchy touches strictly more than the poser. There is no
2585
- declaration cloth could have made, and registration order does not break a non-tie. The engine has two
2586
- ordering mechanisms and neither expresses this.
2587
-
2588
- The answer is not a third mechanism, it is not needing one: cloth writes each node's **local**
2589
- transform alongside its world one, so a hierarchy recomposition reproduces the simulated pose rather
2590
- than replacing it — measured at 1.7e-15 m — and the placement stops being a correctness question.
2591
- *What is left, and it is a real cost:* running before the poser means the cloth's shape is one fixed
2592
- step stale, which measures at a few centimetres on a 1.6 m rope under ordinary motion and up to a
2593
- fifth of its own length at the sharpest reversal of a ±9 m/s square wave. It does not accumulate.
2594
- An application that cares registers accordingly and gets nothing, because this is not a tie; an
2595
- application that does not care is unaffected, which is the common case and the one this trades for.
2596
- `MEASUREMENTS.md` §15.
2597
-
2598
- **The unrigged path's joint budget. — MEASURED, and §11.2's guess was low.** A rigged garment costs
2599
- nothing new; an unrigged one needs a generated joint set, and how many joints a drape takes through a
2600
- four-weight palette was an open empirical question. M3 measured it: a 33×33 cape simulated at full
2601
- resolution, reconstructed by linear blend skinning from k-by-k fitted joints, error against the
2602
- reference. **RMS error is inversely proportional to the joint count** — the product is flat across a
2603
- factor of 72 in joints — and the worst case falls as about `N^−0.83`. At §11.2's "~60 output joints"
2604
- the worst-case error is 5% of the garment's own size, which is five grid cells and reads as faceting;
2605
- **80 to 120 joints** puts it inside one. A crumpled cloth needs about three times as many, so the
2606
- number is per-fabric rather than per-garment. `MEASUREMENTS.md` §28. The barycentric deformation pass
2607
- (§11.2) remains the designed fallback.
2608
-
2609
- **Per-joint frames. — MEASURED, and the degeneracy did not happen.** A particle has a position; a
2610
- joint needs an orientation, fitted from a neighbourhood every frame, and §18 expected "a fold that
2611
- collapses a triangle fan" to have no well-defined frame. Across 800 frames × up to 121 joints,
2612
- including a deliberately crumpled variant with bending at 1e-7 and a gust four times as hard, the
2613
- count of degenerate fits is **zero**. The mechanism is that a tangent is a difference across two
2614
- links and the stretch rows are hard, so each link holds its length and the only way the difference
2615
- vanishes is a fold of exactly 180° at the joint's own vertex. That is possible rather than forbidden,
2616
- so a future unrigged path keeps the epsilon and the fallback; it is no longer the thing to be most
2617
- worried about. `MEASUREMENTS.md` §28.
2618
-
2619
- **Quadratic bending cannot hold a curved rest shape.** A garment authored with a strong permanent curve
2620
- will relax flatter than intended. M0 measured the exact boundary: the stencil annihilates the rest shape
2621
- and **every affine image of it** — which is every in-plane deformation to first order, so a flat sheet
2622
- being sheared, stretched or rotated stays at zero bending energy — but it does *not* annihilate an
2623
- arbitrary planar configuration. Four coplanar particles that are not an affine image of the rest four
2624
- read as bent. *Mitigation:* accepted for the fabrics the library targets. If a case appears, the fix is
2625
- a rest-curvature offset in the stencil, which is a change to one row and not to the solver.
2626
-
2627
- **Swept collision is translation-first, and it is three mechanisms rather than one. — MEASURED.** P3
2628
- states the limit. M2 found that the half nobody had written down — sweeping the contact **set**, not
2629
- just the poses — was the half that mattered: without it a capsule at 20 m/s created zero rows and the
2630
- sheet was never touched. With the collider's travel folded into the discovery reach, 2 to 40 m/s all
2631
- behave. Post-M6 found the third: the *particle's* own travel, which M2 argued was covered by §7 P1's
2632
- velocity clamp and is not, by three orders of magnitude — so a **static** collider was tunnelled from
2633
- 3 m/s. That is fixed and measured (§7 P3, `MEASUREMENTS.md` §54). What remains is that all three are
2634
- built from the collider's *centre*: a collider spinning fast enough that a point on its surface
2635
- travels more than its own radius within one substep can still be missed. *Mitigation:* more substeps
2636
- in the affected `ClothDynamics`; genuine CCD stays out of scope. The shape of a fix, if one is ever
2637
- wanted, is to build the discovery margin from the swept **surface** travel — `|Δt| + θ·R` — which is
2638
- one more term and no new structure. `MEASUREMENTS.md` §20.
2639
-
2640
- **A handle is a slot index, and a slot was reused the instant it was retired. — FIXED.** §12 says a
2641
- cloth drops its warm-started contacts when the collider behind a handle changes, and `ClothColliderIndex`
2642
- docblocks said the same in two places; the mechanism was `cloth_contact_find` matching on the handle
2643
- alone, which can only work if the handle is *absent* for a step. `add` popped the retired slot
2644
- immediately, so a collider removed and another added within one tick were the same handle. Measured: a
2645
- particle settled on the old floor carried **lambda 0.0981 and a friction anchor in the old collider's
2646
- frame** onto the new one. `remove` now retires into a list `advance` drains one step later — every
2647
- cloth has by then stepped against an index the handle was not in, which is what makes the slot cold.
2648
- It composes with the wake test above: a sleeping cloth notices the handle leaving its set and wakes,
2649
- so it clears its slots before anything can take the number.
2650
-
2651
- **Open — the contact penalty ramps on the *unsigned* residual, and that is load-bearing by accident.
2652
- — MEASURED.** `cloth_contact_dual_update` climbs `k` by `β·|C|`, so a row that is **not touching**
2653
- ramps as hard as one that is, and with `γ = 1` nothing brings it back down: every contact in a scene
2654
- is at `k_max` within a few steps of being discovered, which makes §5.2's measured choice of `β`
2655
- describe a ramp that is not running. Post-M6 made this visible rather than introducing it — with the
2656
- particle's own travel in the discovery reach, rows now appear tens of centimetres out instead of
2657
- sixteen millimetres, so the pre-stiffening happens long before contact. *And the obvious repair is a
2658
- net loss:* making the ramp one-sided takes the speed at which a thrown particle is still stopped from
2659
- 12 m/s down to 9.5 with holes, at four substeps — trading cloth passing through a character, which is
2660
- visible, for a hair rope creeping for thirty-seven seconds instead of three, which is not. Recorded
2661
- rather than fixed; the thing that would actually resolve it is a `γ < 1` for contacts, which is
2662
- §5.2's own open number. `MEASUREMENTS.md` §54.
2663
-
2664
- **— RESOLVED the way it said, when the ramp became a ratio.** The cloth-skirt example turned the ramp
2665
- into `CLOTH_CONTACT_RAMP_RATIO` (§5.2), which made the pre-stiffening total — every row violated by
2666
- millimetres at the ceiling for good — and a resting pile stopped sleeping. `CLOTH_CONTACT_GAMMA = 0.8`
2667
- is the `γ < 1` for contacts, on both penalties. The one-sided ramp was measured again in ratio form and
2668
- lost again (a sweeping thigh through ten times as often, 6 → 4 m/s stopped at two substeps), so the
2669
- pre-stiffening stays and the decay is what bounds it. What the pair costs is static friction's
2670
- exactness, stated in §5.2. `MEASUREMENTS.md` §67.
2671
-
2672
- **Open — a rigid collider driven through pinned cloth wraps rather than parts.** A contact row bounds
2673
- the interpenetration; it cannot prevent it, because the cloth has nowhere to go. The playground's
2674
- spinning-bar scene is the case, and it settles at 18 mm inside a 25 mm bar. This is not a solver
2675
- failure — a kinematic collider is infinitely strong by construction and the cloth is inextensible and
2676
- pinned — but it is the configuration a garment author will find first, and the answer is authoring
2677
- (a collider that does not sweep through the garment's own plane) rather than a solver mechanism.
2678
-
2679
- **A soft-row cloth does not come to rest. — CLOSED by M1's damping, and the size is measured.** M0
2680
- measured a soft chain landing in a bounded limit cycle that 2,000 further steps do not move, and no
2681
- solver constant reaches it, because `α·γ` decays a multiplier and a soft row has none. `damping` does
2682
- reach it: on the configuration §6 measured, 2.90e-3 J undamped against 1.28e-6 J at the library's own
2683
- `damping = 0.12`, and 1.82e-12 J at the top of the range. `cloth_pathology.spec.js` gates on exactly
2684
- that pair.
2685
- *What this does not claim:* damping does not stop the step adding energy, it removes energy faster than
2686
- the step adds it. The equilibrium is bounded rather than zero — which is why the numbers keep falling
2687
- all the way to `damping = 1` instead of hitting a floor — and a cloth whose rows are soft and far above
2688
- the dissipative band is still a cloth being rescued rather than a cloth behaving. M1's mapping does not
2689
- produce one; the gate exists so a future mapping that does is caught. `MEASUREMENTS.md` §12.
2690
-
2691
- **The escape hatch could mask authoring errors.** P4(d) rescues a stuck particle, which means a badly
2692
- authored garment can look acceptable while relying on it. *Mitigation:* `ClothDiagnostics` counts
2693
- escape-hatch activations per cloth and surfaces them, so "this garment is being rescued 30 times a
2694
- second" is visible rather than invisible.
2695
-
2696
- **Open — two-way coupling.** Cloth does not push rigid bodies. The row model could carry it (the
2697
- collision row's multiplier is a force, and applying its negation to the collider's body is mechanically
2698
- straightforward), but the stability of that loop across two solvers with different time discretisations
2699
- is a real question and is not answered here.
2700
-
2701
- **Open — the fixed set for an unskinned surface, and M3 found there are three rules rather than
2702
- two.** Step 5 of §11.3 derives the fixed set from skin weights, which is right for a garment on a
2703
- skeleton. A **rigged** garment does not need weights at all: the topology answers, and a joint whose
2704
- parent is outside the proxy is the fixed set (§11.3, corrected). What is still open is the third
2705
- case — a flag, a curtain, a banner that is not skinned at all — where there is neither a joint tree
2706
- nor a weight to read. Those also sit at `slack ≈ 1` by necessity, so the tether carries all the
2707
- inextensibility, but the fixed set still has to come from the mesh boundary or from authoring, and
2708
- neither is built. It belongs with the unrigged path, which is where a mesh first becomes a particle
2709
- set.
2710
-
2711
- **M7 closed half of this.** The fixed set is authored now, by `ClothPin` (§19), for every case: a
2712
- flag's hoist is the vertices a pin on the pole names. **M7's sheets closed the mesh:**
2713
- `cloth_proxy_from_mesh` builds the proxy from triangles (§19.5). What is left is a way to draw one
2714
- (§19.6).
2715
-
2716
- **Open — LOD.** Distance-based particle-count reduction would need either multiple baked proxies per
2717
- garment or a runtime hierarchy. Culling (§12.1) covers the off-screen case; the far-but-visible case is
2718
- unaddressed.
2719
-
2720
- **Open — a wake has no direction, and the aerodynamic model has only one term. — MEASURED.** §10's
2721
- normal-facing factor is the whole of M6's aerodynamics, and in uniform air it does exactly what §10
2722
- promises: ninety degrees of flow angle is a factor of five hundred in the force a banner feels. In a
2723
- fluid wake it does nothing — 0.216 against 0.280 — because a wake has velocity in every direction.
2724
- What that leaves open is whether a cloth *in* a wake wants something the linear drag row does not
2725
- carry: the fluttering a real flag does in one comes from a pressure difference across the sheet and a
2726
- shed vortex, not from a relaxation toward a local velocity, and this design has no term for either.
2727
- *Mitigation:* none is needed for what M6 claims — the cloth reads the air the simulation produces and
2728
- is moved by it. It is named because "a banner in a wake looks alive" is what a reader will expect from
2729
- §10 and is not what a drag row alone produces. `MEASUREMENTS.md` §51.
2730
-
2731
- **Open — the aerodynamic factor decides what a particle *is* on a 14.5° threshold, and a bent chain
2732
- reads as a sheet.** §10's normal-facing factor is one expression over the rank of the span of a
2733
- particle's incident stretch rows (`cloth_wind_accelerate.js`): rank 2 is a surface and gets `|û·n|`,
2734
- rank 1 is a link of a chain and gets the complementary `√(1 − (û·t)²)`, rank 0 is a bead and gets 1.
2735
- Rank is decided by `CLOTH_WIND_SPAN_MIN = 0.25` — how much of a second incident direction has to
2736
- survive projection out of the first — and a quarter is **about fourteen and a half degrees**. Below it
2737
- two rows describe the same line and their cross product is a normal built out of rounding, which is
2738
- the failure the threshold exists to prevent and is why it is not smaller.
2739
-
2740
- What it costs is that a **chain bent past that angle at a link is a surface at that link**, and gets
2741
- the factor for one. A rope hanging straight reads as a rope; a rope draped over a collider reads as a
2742
- sheet at every link on the curve, and does so for as long as it is draped, because the rank is
2743
- recomputed from the configuration every step and the configuration is not changing. The magnitude is
2744
- §51's: turning the flow ninety degrees is a factor of five hundred in a banner's acceleration, so the
2745
- two branches are not close together, and a rope reading as a sheet catches the wind broadside where it
2746
- should be slicing it.
2747
-
2748
- *Not fixed, and recorded rather than guessed at.* The threshold is a single number doing two jobs — a
2749
- numerical guard against a degenerate cross product, and a physical statement about when a chain stops
2750
- being a line — and separating them is the shape of a fix: rank the span with a tolerance chosen for
2751
- conditioning, and blend between the two factors over an angular window chosen for looks. Both halves
2752
- would need a measurement neither M6 nor this pass has, and what a draped rope does in wind is a
2753
- question about a scene rather than about a step.
2754
-
2755
- **Open — the ambient is uniform in space and the field boundary is a step.** Two limits of §10's
2756
- fallback, both deliberate and both cheap to see: a gust lifts a whole banner at once rather than
2757
- travelling along it, and a garment carried out of a fluid volume crosses a discontinuity rather than a
2758
- ramp. The first is what makes the no-simulation case cost one sample rather than one a particle; the
2759
- second is small where the ambient is the far field the volume relaxes toward and visible where it is
2760
- not. A blend would want a falloff distance, which is an authored quantity of exactly the kind §3.4
2761
- spent a section removing.
2762
-
2763
- **Open — a fluid volume's wake is steady at a grid a frame can afford. — MEASURED, and it is the
2764
- fluid system's rather than cloth's.** At 18, 26 and 34 cells across, with the simulator's damping and
2765
- vorticity confinement at both ends of their range, the air a banner's width behind a solid post varies
2766
- by under 0.03 m/s: a velocity deficit, not a shedding street. The deficit is real and is a third of
2767
- the free stream, which is what the cloth reads; what is not there is the unsteadiness. Resolving a
2768
- shedding shear layer costs 15 ms a step at 26 cells and 35 at 34, against 5 ms at 18.
2769
- `MEASUREMENTS.md` §51.
2770
-
2771
- **Open — `stretch` is a weak control, and it is not obvious what would make it a strong one.** M1
2772
- measured the strain-limit row removing about a third of the peak over-stretch and never reaching its
2773
- own allowance, because it is a second hard row competing with the first for the same trust region and
2774
- the same penalty band (§6). A fabric's *extension* is therefore governed by the iteration budget more
2775
- than by the fabric, and the library says so by which entries build the row at all. Three things might
2776
- change that and none is free: a soft fabric row inside the dissipative band with the hard row acting
2777
- only as the limit (which reintroduces M0's settling problem at any realistic stiffness); a per-row
2778
- penalty band, making `k_max/(m/h²)` a material property rather than a global (which `MEASUREMENTS.md`
2779
- §4 shows trades stretchiness against settling); or accepting that near-inextensibility is what fabric
2780
- *is* and demoting `stretch` to a documented weak knob. M2's contacts do not bear on it either way.
2781
-
2782
- **Open — the write-back has no interpolation, and M5 decided it does not land with the worker.** Cloth
2783
- writes `Transform64` at the fixed rate and nothing blends between ticks, so a cloth updates at 60 Hz on
2784
- a 165 Hz display — 105 of every 165 frames repeat a pose. That is visible and it is not wrong: the
2785
- poses are authoritative, they are simply held. **How** visible is now measured, on M3's cape geometry:
2786
- a node covers 0.47 link lengths in a fixed step at a walk and 1.33 at a sprint (`MEASUREMENTS.md` §43).
2787
-
2788
- What M5 changed is the shape of the remaining work, in three findings that between them delete the
2789
- sentence this note used to end with.
2790
-
2791
- - **`PhysicsSystem`'s producer / `__interp_restore` contract cannot express half the feature.**
2792
- `Interpolated` is an entity-keyed marker and a rigged garment's joints were `Node3D`s, not entities
2793
- (§13.1). There was nothing to put the component on and no key to file a snapshot under.
2794
- **Post-M6: a joint is an entity now** (§3.2, §11.2), so this finding's *reason* is gone even though
2795
- its conclusion is not — the published-pose slot below is still the cheaper half and still the one
2796
- cloth needs, and an entity-keyed marker on a hundred joints a garment is not obviously better than
2797
- one array copied at join. What changed is that it is a trade rather than an impossibility.
2798
- - **Cloth needs only the publish half.** `__interp_restore` undoes render-time writes before the
2799
- simulation reads them; cloth's state is its particle array and its write-back never touches the
2800
- anchor it reads, so there is nothing to restore. The restore it *does* depend on — an anchor on a
2801
- physics-driven character — it already gets from `PhysicsSystem`.
2802
- - **The worker removes the cheap implementation.** A render-time pass cannot read
2803
- `ClothState.position`, because the worker owns it from dispatch until the next tick's join, which is
2804
- essentially all of the time outside the fixed pass. What is needed is a **published pose slot**: the
2805
- last two steps' node poses, copied out at join and blended at render time — the same double-buffered
2806
- publish slot `WorkerFluidSystem` names as the unbuilt prerequisite for its own render-time reads.
2807
-
2808
- The slot is nearly free to fill, because `cloth_write_back` already computes every node's local TRS
2809
- and would only have to keep it; what costs is the per-frame pass. For the §17 workload that is about
2810
- 0.12 ms a frame — 20.5 ms a second at 165 Hz, against the 10.9 ms a second M5 leaves on the main
2811
- thread and the 217 ms a second it cost before. **Interpolating a cloth would roughly triple the main
2812
- thread's share of it and leave it at a seventh of what it was**, which is the honest shape of the
2813
- trade: the feature is affordable because M5 happened, and it is not free. The cloth-shaped version is
2814
- *simpler* than the engine's, not harder: no restore pass, no serialization adapters, no log, and it
2815
- covers the rigged and entity paths identically because both end in a pose per node.
2816
-
2817
- **Open — the subtree is walked once.** A rope's links do not come and go, so `cloth_seed_subtree` runs
2818
- on first step and `ClothSystem#reseed` is how a caller says the structure changed. That is the same
2819
- contract `TransformAttachment#parent` already carries, and it is the right default; what it does not
2820
- have is a way to *notice*. An entity added under a live `Cloth` is silently not simulated until
2821
- someone re-seeds, which is a class of authoring mistake with no diagnostic behind it. `ClothDiagnostics`
2822
- is where one would go.
2823
-
2824
- **Post-M6 narrowed this: a `ClothRig` is not part of it and never was.** §3.1 says the discriminator
2825
- between the two paths is a component's *presence*, and `ClothSystem#dependencies` is
2826
- `[Cloth, Transform64]` — so a `ClothRig` added to a live cloth completed no tuple the system observed
2827
- and one removed broke none. A character whose rig arrived a fixed step after its `Cloth` simulated its
2828
- **whole skeleton as one rope** (in a dev build, through `gpu_authority_assert_cpu`, it threw), and one
2829
- whose rig was taken away kept driving the joints. Both edges are now watched by an `EntityObserver` on
2830
- `Cloth` + `ClothRig` inside `ClothWorld`, so the discriminator is read whenever it changes rather than
2831
- once. The **subtree** case above is untouched and stays open: an entity appearing under a live rope is
2832
- still a `reseed` the caller has to ask for.
2833
-
2834
- **Open — M7: a seam between two holders that move apart.** The animated pose hangs each particle from
2835
- its *nearest* held particle, so two holders moving relative to each other put a step in it where their
2836
- regions meet. It binds only where the slack row does — past `2·slack·d_geo`, which at a seam is the
2837
- widest radius the cloth has — and nothing measured has reached it. A geodesic-weighted blend of the
2838
- holders' motions is the shape of a fix if a garment ever does.
2839
-
2840
- **M7: a sheet held along a vertical line turned about it. — FIXED, by one more block of the descent.**
2841
- A flag on a pole in still air did not hang still: it turned about the pole as a rigid body, ten degrees
2842
- a second at rope's settings, for all 3,000 steps measured. A turn about a vertical hold line is the one
2843
- motion nothing restores — gravity is parallel to it, and every row ends on the axis or is an equal and
2844
- opposite pair — and AVBD stopped at a fabric's iteration count left a residual whose torque about that
2845
- axis was not zero, so the step's fixed point was a steady turn rather than rest. `damping` balanced the
2846
- torque rather than removing it: ten times the rate, a tenth of the speed. It was there with hard stretch
2847
- rows and nothing else, and in the solver from before holds existed, digit for digit. A fabric with
2848
- `slack < 1` came to rest only because its slack row caught the turn — a silk flag 123° round its pole,
2849
- cotton 74° — and one at `slack = 1`, rope or chain, never did. `MEASUREMENTS.md` §63.
2850
-
2851
- *The cause, measured.* A vertex update moves one particle, so each adds `x_i × m·Δx_i` of angular
2852
- momentum about the line, and the step's change about the pole is exactly `h·Σ x × R`, `R` the energy
2853
- gradient left when the iterations stop; the identity closes to four figures. The warm start keeps
2854
- `α·γ = 0.25` of each hard multiplier, so a cloth at rest re-solves three quarters of its weight every
2855
- substep and leaves that gradient every substep. Sweep order is not it: forward, backward and
2856
- alternating each step all read −0.46 at 8 iterations.
2857
-
2858
- *The fix* is the turn as a block of its own (§5.2, `cloth_hold_line.js`): one Newton step along it in
2859
- the last iteration, whenever the held particles lie on one line. The bare flag reads 0 to four figures
2860
- at every budget from 1 to 16 iterations, and every library fabric's flag sleeps: rope at step 416,
2861
- chain at 250 and cotton at 336, where they never did. It costs 3–9% of a sheet's step, and nothing on a
2862
- cloth held at one point, where it never runs. Carrying the multiplier instead — AVBD's Eq. 18, or the
2863
- reference code's post-stabilisation — stops the turn too, fails six to ten of the 345 cloth gates, and
2864
- diverges where its rows meet rows still on the old warm start. `MEASUREMENTS.md` §66.
2865
-
2866
- *Still open, measured and not fixed:*
2867
- - A single held point has three free turns and a cloth nothing holds has six; the block covers a line
2868
- only. A sheet hung from one corner turns at no more than 0.004, and silk's is awake at step 3,000.
2869
- - Where gravity does restore the turn, the block slows some sleeps: leather's lying sheet from 72 steps
2870
- to 165, and rope and chain on a pole tilted 30° from 474 to 574 and 264 to 312. Why is not measured.
2871
- - And in one case it keeps a cloth awake. Cotton held by its two top corners, after the hands see-saw
2872
- for 900 steps and stop, never sleeps under the block, creeping 1.7 mm per 30 steps at step 8,000;
2873
- before it, it slept at 1,378. Switched off at the stop it sleeps at 1,409, so it is the block's
2874
- ongoing action: it takes out the leftover turn about the corner line rigidly, about 7e-7 rad a
2875
- substep, and the sweeps put it back near a corner. Rope never sleeps after that see-saw in either
2876
- build. Running the block only on hold lines near the vertical would remove this and the item above,
2877
- at the price of a tolerance constant and a test against gravity; it was not done.
2878
- - Far from the frame's origin the correction is smaller than the coordinates' rounding. A bare flag
2879
- 100 m out turns at −0.025 at step 500 and −0.136 at 3,000, against −0.18 to −0.23 before. `ClothSystem`
2880
- steps a cloth in its own frame, where its coordinates are its own size.
2881
- - After a one-kilometre teleport neither build sleeps within 2,400 steps, rope, silk, hair or chain. It
2882
- was so before the block, and the cause is not measured.
2883
-
2884
- **A fold resting on a fold never came to rest. — FIXED, `CLOTH_SELF_RATIO` 8 → 2, and the cost is
2885
- measured.** §32 cleared `CLOTH_SELF_RATIO = 8` of adding energy on a folding sheet, whose
2886
- contacts come and go. A self-collision pair that carries weight stays in contact, so a resting fold is
2887
- a standing spring, which is the case M0's dissipative band was measured on. At 8 the step gains energy
2888
- through it:
2889
- - Two silk ribbons stacked parallel on a floor peak at 1.5 m/s after a 0.35 m/s drop, and never sleep.
2890
- - Two crossed at 90°, dropped from gaps of 4 to 12 cm, sleep only from the 6 and 8 cm drops. From the
2891
- others they creep 3 to 56 mm every five seconds.
2892
- - Piles of three to six reach up to 5.7 m/s, 1.3 to 3.8 times what their drop gives them, and are
2893
- left with 11 to 139 crossings. Every three-ribbon pile tried crosses.
2894
- - §63's draped sheet stays awake on its sphere, with 14 to 18 pairs found every step.
2895
- - A circle skirt stays awake after a twirl, with eight.
2896
-
2897
- At ratio 1 all of them sleep. Averaging a particle's Jacobi share over its pairs is not the fix.
2898
- *Fixed at 2*, the stiffest ratio at which cloth resting on itself reliably comes to rest. Of 225 ribbon
2899
- piles in silk, cotton and hair, the 135 that land without passing through themselves all sleep but
2900
- one, and so do the draped sheet and the skirt. At 4, five of 38 clean silk piles never do, and above
2901
- 2.25 the draped sheet's sleep is erratic. *What it costs* is §32's folding sheet: 2.09 → 2.98 crossings
2902
- a sample over 48 gusts, where ratio 1 would cost 3.74. The library bench does not move. The proof is a
2903
- three-ribbon pile fanned 60°, asserting no crossing at any step and the order kept. "No crossings at
2904
- rest" is not enough, because a ribbon can land right through another and lie under it. *Still open,
2905
- and not this:* piles land through themselves, 90 of 225 at 2, every one of them while landing. That is
2906
- §35's missing travel term in the pair search. `MEASUREMENTS.md` §64, §65.
2907
-
2908
- **The self radius is one per cloth, and a circle skirt's hem gets the waistband's. — FIXED: the
2909
- radius is the particle's own (§68); Lewin's two pair-row moves wait on their limb cost.**
2910
- `ClothState#self_radius` was `max(thickness, ½·shortest row)` capped at `0.4 × closest free pair`,
2911
- with both the shortest row and the closest free pair measured over the whole cloth. The cloth-skirt
2912
- example's rows run from 9.8 mm at
2913
- the waistband to 54 mm at the hem, so the hem gets a 6 mm radius against a 37–54 mm spacing — a row
2914
- diameter a fifth of the spacing where §9 wants one spacing — and the flutes pass through each other
2915
- at every sample: ~400 edge-through-triangle crossings, budget-independent (272 at 4×8), the example's
2916
- own README says "the flutes sometimes cross". `MEASUREMENTS.md` §67 measured three answers:
2917
-
2918
- - **The shipped formula applied per particle — LANDED (§68)**, `r_i = clamp(thickness, ½·shortest
2919
- row incident on i, 0.4 × closest free pair of i)`, pairs tested at `r_i + r_j`, both lengths seeded
2920
- into `ClothState#self_row` and `#self_free_pair` and the radius filled each step by
2921
- `cloth_self_radius_update`. Paired on the dance: crossings 422 → 102 a step (worst 618 → 180), a
2922
- thigh through in 0.6% → 0.0% of steps, the step 0.96× for 70 fewer contact rows with the hem held
2923
- off the legs, search and scratch 1.2× on three times the pairs; the twirled skirt's 16 crossings → 0,
2924
- and its gate pinned. It is still the sphere model, and a sharp fold still slips between particles.
2925
- - **Lewin's two moves on the pair row (GDC 2018):** the particle's predicted travel in the pair
2926
- search's reach — §35's missing term — and the pair's normal frozen for the step, which makes the row
2927
- a half-space a pair cannot pass through however far the other rows shove it. On §64's eleven ribbon
2928
- piles they take the pair row from 6 clean and 7 in order to 10 clean and 11 in order, every pile
2929
- asleep, the worst crossings 7.5 → 0.2 a scene, at no new row kind. On the skirt they do not touch
2930
- the crossings and they cost limbs: a thigh through in 1.1–1.9% of steps against 0.1%, and 1.2% for
2931
- the travel term on top of the per-particle radius (crossings 38). A half-space stays active after
2932
- its pair has slid apart tangentially, the artefact the talk names; gating it on the live distance
2933
- halves the cost and keeps the piles' result. Why a wider or stickier pair row puts a hem particle
2934
- into a thigh is not measured, and it is what keeps the two moves from landing with the radius.
2935
- - **Predictive point-triangle rows, as the talk's self collision,** as plain penalties into the
2936
- self row's Jacobi scratch: a four-particle row is only Jacobi-stable with each participant taking
2937
- its mass-weighted share of the correction (the pair row at ratio 2 sits exactly at the share sum
2938
- of 1, which is why §65 found it the stiffest that comes to rest), and with that split a ribbon
2939
- landing from 10 mm a step is stopped at 8 mm of separation. On their own they sleep 5 of 11 piles
2940
- and push the lowest ribbon into the floor; on the skirt they are unstable at every form tried (peak
2941
- speeds at the clamp, a thigh through in 5–7% of steps). They add the last pile only on top of the
2942
- pair row with Lewin's moves. The talk's own numbers say the same: 20 ms to 1 s a frame for the
2943
- general method, 0.1–0.2 ms only with authored layer order and cached contact sets. M4b stays what
2944
- it was: the thin-surface closure, not a quick win, and in this solver it wants a hard row with a
2945
- multiplier and a Gauss-Seidel treatment, not a penalty in the Jacobi scratch.
2946
-
2947
- ---
2948
-
2949
- ## 19. Attachment points
2950
-
2951
- Written for M7, after an adversarial review of the first draft by three reviewers against the code;
2952
- the review's findings are folded in below and listed in `MEASUREMENTS.md` §62.
2953
-
2954
- ### 19.1 The anchor was four jobs
2955
-
2956
- Node 0 was the frame the cloth was simulated in, the one pinned particle, the point every tether
2957
- measured from, and the root the rows were derived from. Only the first is a property of the entity the
2958
- `Cloth` is on, and splitting the other three off is the whole design:
2959
-
2960
- | Job | Now |
2961
- |---|---|
2962
- | Frame: the solve, gravity, colliders, wind, §7 P7 | The `Cloth` entity for a subtree; `ClothProxy#frame` for a rig |
2963
- | The fixed set | Held particles: any number, each held by an entity carrying a `ClothPin` |
2964
- | Tether centre and `d_geo` | The nearest held particle along the fabric |
2965
- | Root of the rows | A particle graph — `ClothInstance#edge`, `#skip`, `#hinge` |
2966
-
2967
- A cloth with no pins is the case where nothing is held, and it needs no code: every tether and slack
2968
- row has radius `Infinity`.
2969
-
2970
- ### 19.2 `ClothPin`
2971
-
2972
- On the holder, naming the cloth's entity and its particles by index — a proxy's particle order, or for
2973
- a subtree the breadth-first walk, where 0 is the `Cloth` entity. Every field required; references fixed
2974
- while attached; saved with its entity. A hold begins **where the particle is**: on a cloth's first step
2975
- that is the rest pose, mid-run it is a grab. Removing the pin lets go, and the particle leaves at the
2976
- speed its holder carried it. Destroying the cloth's entity removes the pins naming it (an entity id is
2977
- reused). A particle two pins name goes to the lower holder; a holder the cloth itself moves is refused,
2978
- once, by name.
2979
-
2980
- The world keeps one `ClothHold` per pin **outside** the seeded instance: the offsets a hold takes are
2981
- the hold's, so a re-seed does not move what is held. The observer only notices; everything a hold
2982
- writes into a `ClothState` is written in `ClothWorld#advance`, the one window in which the main thread
2983
- owns that state under both systems.
2984
-
2985
- A holder's pose is read off its `Transform64` when the CPU owns it and evaluated exactly, under the
2986
- clips of every model on its chain, when the GPU does; a holder whose chain is not complete yet holds
2987
- where it last held.
2988
-
2989
- ### 19.3 What a hold does in the step
2990
-
2991
- A held particle is kinematic: inverse mass zero, carried towards `kinematic_target` by an equal share
2992
- of what remains at every substep, so it lands exactly at the last one and a target equal to the
2993
- position moves nothing by a single bit. Chaos Cloth interpolates its kinematic particles the same way.
2994
- Held particles are not carried by the frame's inertia; they belong to their holders.
2995
-
2996
- **One tether per particle, to the nearest held particle.** Chaos tethers to the nearest of up to four
2997
- kinematic islands; that is four hard rows per particle, three of them idle on every cloth with one
2998
- hold, and two hard tethers to holders pulled apart are bounds no position satisfies. The fabric between
2999
- two holders already carries everything a second tether would. The search is one Dijkstra over the
3000
- fabric edges with every held particle a source, in double. A tether starts no tighter than where its
3001
- particle already is and only ever comes in, so a hold taken mid-run does not snap the cloth onto it.
3002
-
3003
- The **animated pose** is the rest pose hung from the nearest held particle and turned with its holder,
3004
- `s_i = x_p + R_p·(r_i − r_p)`; it keeps §3.4's factor of two exact. When no holder has moved in the
3005
- frame since the last step — every holder that rides its frame — nothing per step is recomputed.
3006
-
3007
- `reach` is the largest `d_geo`, with a particle no hold reaches counted at its rest distance from the
3008
- frame's origin.
3009
-
3010
- ### 19.4 The rig path
3011
-
3012
- A rig proxy's particles are the garment's joints and the bones it hangs from; cloth writes the first
3013
- and reads the second, each step, exactly, to express the garment's roots against. The builder finds
3014
- the bones, the frame and the pins; the spawn code adds the pins. `Cloth` and `ClothRig` stay on the
3015
- model's root, which is saved, rather than on a bone, which is not.
3016
-
3017
- ### 19.5 Sheets
3018
-
3019
- A sheet is a skin with **one joint per vertex**, every one hanging straight off one root joint, and
3020
- `cloth_proxy_from_mesh` builds its proxy from the triangles over them: a stretch row per edge, a hinge
3021
- per interior edge — the shared edge, then each triangle's opposite wing, which is `cloth_bend_row`'s
3022
- stencil — the root joint as the frame, and nothing pinned: a pin on a pole names the hoist. A triangle
3023
- with no area, an edge three triangles share, joints under two roots and a name the skin lacks are
3024
- refused by name. `ClothProxy` carries its graph (`edge`, `skip`, `hinge`, `triangle`) for both
3025
- builders.
3026
-
3027
- A hinge's stiffness is the skip row's rule over its own stencil, `bend ratio × (m / Σ w²) / h²`: a
3028
- hinge across a sliver, whose cotangent weights are large, is softer per unit of `C` for exactly the
3029
- reason its `C` is larger, and stays inside M0's dissipative band where a stiffness in newtons per metre
3030
- would leave it.
3031
-
3032
- A joint with triangles round it has no chain to swing along, and what its orientation is for is the
3033
- normal it turns its vertex's normal by. It takes the swing carrying the area-weighted normal of its
3034
- triangles in the animated pose onto the same in the simulated one, then the twist about that normal
3035
- carrying one fixed neighbour's direction onto where it is now. A joint hanging straight off the frame is
3036
- written in the frame.
3037
-
3038
- ### 19.6 Not done
3039
-
3040
- - **Drawing a sheet: the route exists, nothing takes it yet.** A skin is a `MeshSkin` on the mesh it
3041
- deforms and a rig names that entity (`38b2fe208`, `0e20799be`), so a sheet built in code needs no
3042
- loaded asset: a mesh entity with `ShadedGeometry` and a `MeshSkin` naming the vertex joints, named
3043
- by `ClothRig.from(proxy, mesh)`. Not built: a helper that makes that geometry, its skin and the
3044
- joints from the triangles, and anything that shows a sheet drawn.
3045
- - **The cost of a joint per vertex.** About 620 ns a joint a step, on the main thread, where
3046
- `WorkerClothSystem` leaves it: 0.25 ms for a 400-particle sheet. `MEASUREMENTS.md` §63.
3047
- - **Curved rest shapes.** The hinge stencil reads a curved rest as already bent (§18); a flag, a banner
3048
- and a circle skirt cut flat are what it serves.
3049
- - **Welding.** The builder takes a mesh that is already one joint per welded vertex.
3050
- - **A sheet held at one point, or by two holders that see-saw.** The hold-line block that stopped a
3051
- flag turning on its pole covers a line only, and after a see-saw it keeps a corner-held sheet awake
3052
- (§18).
3053
- - **A seam between holders that move apart** (§18).
3054
- - **A pile landing through itself.** A fold resting on a fold comes to rest since the self-collision row
3055
- went to 2 (§18). What is left is a pile passing through itself while it lands, which is §35's
3056
- missing travel term in the pair search.