@woosh/meep-engine 3.22.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts +118 -0
- package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts.map +1 -0
- package/src/core/geom/3d/hash-grid/PointHashGrid3.js +283 -0
- package/src/core/geom/3d/shape/BoxShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/BoxShape3D.js +6 -17
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.js +3 -14
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/CylinderShape3D.js +3 -21
- package/src/core/geom/3d/shape/PlaneShape3D.d.ts +105 -0
- package/src/core/geom/3d/shape/PlaneShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/PlaneShape3D.js +251 -0
- package/src/core/geom/3d/shape/SphereShape3D.js +3 -3
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +14 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +15 -0
- package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts +51 -0
- package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_box.js +113 -0
- package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts +46 -0
- package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_capsule.js +82 -0
- package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts +60 -0
- package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_cylinder.js +126 -0
- package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts +50 -0
- package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.js +53 -0
- package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts +52 -0
- package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_plane.js +61 -0
- package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts +41 -0
- package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts.map +1 -0
- package/src/core/geom/3d/shape/sdf/sdf3_sphere.js +71 -0
- package/src/core/process/ConcurrencyGate.d.ts +103 -0
- package/src/core/process/ConcurrencyGate.d.ts.map +1 -0
- package/src/core/process/ConcurrencyGate.js +207 -0
- package/src/engine/asset/AssetManager.d.ts +10 -0
- package/src/engine/asset/AssetManager.d.ts.map +1 -1
- package/src/engine/asset/AssetManager.js +10 -0
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +30 -13
- package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts +26 -0
- package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts.map +1 -0
- package/src/engine/graphics3/pose/collect_entity_playbacks.js +40 -0
- package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts.map +1 -1
- package/src/engine/graphics3/pose/query_entity_node_world_pose.js +74 -101
- package/src/engine/physics/cloth/MEASUREMENTS.md +1287 -4
- package/src/engine/physics/cloth/PLAN.md +1967 -1581
- package/src/engine/physics/cloth/build/ClothProxy.d.ts +147 -0
- package/src/engine/physics/cloth/build/ClothProxy.d.ts.map +1 -0
- package/src/engine/physics/cloth/build/ClothProxy.js +207 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts +70 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +256 -0
- package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +176 -0
- package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/ClothColliderIndex.js +435 -0
- package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts +12 -0
- package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/ClothColliderKind.js +38 -0
- package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts +120 -0
- package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/ClothColliderRecord.js +137 -0
- package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts +25 -0
- package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +190 -0
- package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +44 -0
- package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/cloth_collider_pack.js +173 -0
- package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts +94 -0
- package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -0
- package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +275 -0
- package/src/engine/physics/cloth/ecs/ClothCollider.d.ts +126 -0
- package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothCollider.js +199 -0
- package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +9 -0
- package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +32 -0
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +114 -0
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +243 -0
- package/src/engine/physics/cloth/ecs/ClothDynamicsFlags.js +66 -57
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +109 -1
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothInstance.js +479 -363
- package/src/engine/physics/cloth/ecs/ClothRig.d.ts +95 -0
- package/src/engine/physics/cloth/ecs/ClothRig.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothRig.js +140 -0
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +30 -1
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothSystem.js +1021 -773
- package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts +90 -0
- package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_build_rows.js +374 -0
- package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts +32 -3
- package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_dynamics_library.js +283 -254
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +59 -0
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +328 -0
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts +13 -0
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +288 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +58 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +199 -0
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +8 -63
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +317 -645
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts +61 -0
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back.js +212 -449
- package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts +49 -0
- package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_write_back_rig.js +115 -0
- package/src/engine/physics/cloth/playground/README.md +155 -1
- package/src/engine/physics/cloth/playground/collide.html +178 -0
- package/src/engine/physics/cloth/playground/collide_build.d.ts +95 -0
- package/src/engine/physics/cloth/playground/collide_build.d.ts.map +1 -0
- package/src/engine/physics/cloth/playground/collide_build.js +315 -0
- package/src/engine/physics/cloth/playground/collide_main.d.ts +2 -0
- package/src/engine/physics/cloth/playground/collide_main.d.ts.map +1 -0
- package/src/engine/physics/cloth/playground/collide_main.js +720 -0
- package/src/engine/physics/cloth/playground/garment.html +182 -0
- package/src/engine/physics/cloth/playground/garment_build.d.ts +78 -0
- package/src/engine/physics/cloth/playground/garment_build.d.ts.map +1 -0
- package/src/engine/physics/cloth/playground/garment_build.js +359 -0
- package/src/engine/physics/cloth/playground/garment_main.d.ts +2 -0
- package/src/engine/physics/cloth/playground/garment_main.d.ts.map +1 -0
- package/src/engine/physics/cloth/playground/garment_main.js +633 -0
- package/src/engine/physics/cloth/solver/ClothState.d.ts +208 -5
- package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/ClothState.js +302 -8
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +87 -0
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/cloth_contact_find.js +343 -0
- package/src/engine/physics/cloth/solver/cloth_self_find.d.ts +112 -0
- package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/cloth_self_find.js +448 -0
- package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts +4 -1
- package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +16 -1
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +112 -5
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_solver_constants.js +117 -6
- package/src/engine/physics/cloth/solver/cloth_step.d.ts +40 -9
- package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_step.js +122 -10
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +95 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +285 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts +129 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +501 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts +75 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.js +202 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/vgeo_viewer/README.md +18 -0
- package/src/shade/playground/vgeo_viewer/main.js +87 -50
- package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUDatabase.js +191 -62
- package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts +94 -2
- package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
- package/src/shade/renderer/buffer/table/GPUTypedTable.js +414 -2
- package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts +14 -0
- package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts.map +1 -0
- package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.js +13 -0
- package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts +17 -0
- package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts.map +1 -0
- package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.js +116 -0
- package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +9 -4
- package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
- package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +843 -734
- package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts +12 -0
- package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts.map +1 -1
- package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.js +13 -0
- package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +17 -0
- package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
- package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +185 -30
- package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +17 -0
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +141 -24
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +204 -64
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +110 -10
- package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +19 -0
- package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +19 -6
- package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +26 -7
- package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +173 -14
- package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +458 -51
- package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +49 -13
- package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +52 -14
- package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts +20 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.js +19 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts +27 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.js +48 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts +29 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.js +60 -0
- package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +8 -1
- package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +32 -2
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +258 -0
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.js +723 -0
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +49 -0
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.js +50 -0
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts +3 -0
- package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts.map +1 -1
- package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.js +121 -21
- package/src/shade/renderer/texture/virtual/README.md +6 -4
- package/src/shade/renderer/texture/virtual/VirtualTextureManager.d.ts.map +1 -1
- package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +7 -0
- package/src/shade/renderer/texture/virtual/source/VTSourceTiled.d.ts +3 -3
- package/src/shade/renderer/texture/virtual/source/VTSourceTiled.js +3 -3
- package/src/engine/ecs/system/SystemExecutionPhase.d.ts +0 -9
- package/src/engine/ecs/system/SystemExecutionPhase.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/NOTES.md +0 -128
- package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts +0 -6
- package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.js +0 -5
- package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts +0 -49
- package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.js +0 -264
- package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts +0 -44
- package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/VirtualTextureUsage.js +0 -228
- package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts +0 -3
- package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.js +0 -73
- package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts +0 -14
- package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.js +0 -85
- package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts +0 -12
- package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/debug/UsageDebugView.js +0 -63
- package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts +0 -12
- package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.js +0 -243
- package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts +0 -30
- package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.js +0 -39
- package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts +0 -9
- package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/compose_finger_print.js +0 -24
- package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts +0 -9
- package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/compose_tile_address.js +0 -26
- package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts +0 -11
- package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.js +0 -12
- package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts +0 -7
- package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.js +0 -16
- package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts +0 -7
- package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts.map +0 -1
- package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.js +0 -35
|
@@ -1,1581 +1,1967 @@
|
|
|
1
|
-
# Cloth — Design
|
|
2
|
-
|
|
3
|
-
> Design document. Written 2026-09-10; **M0 and
|
|
4
|
-
> it has been corrected in place with the measurement named beside it.
|
|
5
|
-
> is not; `MEASUREMENTS.md` is the evidence.
|
|
6
|
-
>
|
|
7
|
-
> Scope: a cloth and soft-chain simulator that lives inside `engine/physics`, collides against the
|
|
8
|
-
> `Collider` components the rest of the engine already authors, runs on a worker, and is safe to ship
|
|
9
|
-
> with its defaults untouched.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 1. What this is
|
|
14
|
-
|
|
15
|
-
One solver over particles, driven by components:
|
|
16
|
-
|
|
17
|
-
- **`Cloth`** — the thing that simulates. Put it on an entity and that entity's transform subtree
|
|
18
|
-
becomes a simulated chain: a rope, a chain, a hanging sign, a mooring line.
|
|
19
|
-
- **`ClothRig`** — add it alongside `Cloth` and the particles are the **joints of a skinned model**
|
|
20
|
-
rather than the entity subtree. This is what makes a cape, a skirt, a banner on a character.
|
|
21
|
-
- **`ClothCollider`** — the marker that opts an existing `(Collider, Transform64)` entity into being
|
|
22
|
-
something cloth collides with. This is the `RigidBody`-analogue: it says *this collider participates*,
|
|
23
|
-
and carries only what cloth needs that `Collider` does not already say.
|
|
24
|
-
|
|
25
|
-
That last component follows a pattern the engine already has twice. `AcousticBody` opts a collider
|
|
26
|
-
into the acoustic simulation and carries an `AcousticMaterial`; `FluidObstacle` opts a collider into
|
|
27
|
-
the fluid simulation and carries an inflation radius. `ClothCollider` is the third instance of the
|
|
28
|
-
same idea, and it should read like a sibling of those two, not like a new invention.
|
|
29
|
-
|
|
30
|
-
The design commitments, in priority order, because they conflict and the ordering is the design:
|
|
31
|
-
|
|
32
|
-
1. **It does not explode.** Not at 5 fps, not at 500, not when a parameter is absurd, not when a
|
|
33
|
-
character teleports, not when a collider is spawned inside the cloth.
|
|
34
|
-
2. **It does not get stuck.** The set of configurations the simulation can reach is bounded around
|
|
35
|
-
the animated pose, and error cannot accumulate across steps.
|
|
36
|
-
3. **It works untouched.** Adding a `Cloth` component with default values to a correctly authored
|
|
37
|
-
garment produces shippable motion. Tuning is an optimisation, never a prerequisite.
|
|
38
|
-
4. **It is cheap.** A CPU worker budget measured in low single-digit milliseconds per character.
|
|
39
|
-
5. **It is accurate.** Last, deliberately. Cloth that is 10% too stiff and never misbehaves beats
|
|
40
|
-
cloth that is correct and occasionally detonates.
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 2. Non-goals
|
|
45
|
-
|
|
46
|
-
- **Garment authoring.** No pattern cutting, no sewing, no pressure/inflation. Import a mesh.
|
|
47
|
-
- **Woven anisotropy.** No warp/weft/shear tensor. That needs UV-aligned quad topology and a
|
|
48
|
-
material model this design does not carry. Cloth here is isotropic with a bend modulus.
|
|
49
|
-
- **Penetration-free guarantees.** IPC-class methods (`C-IPC`, `GIPC`) guarantee no intersection ever,
|
|
50
|
-
at a cost per step this budget cannot pay. We guarantee bounded, self-correcting penetration.
|
|
51
|
-
- **Cloth pushing rigid bodies.** Coupling is one-way: colliders move cloth. Two-way coupling into
|
|
52
|
-
`PhysicsSystem`'s velocity solver is a later question and the constraint model leaves room for it
|
|
53
|
-
(§6), but it is not in scope.
|
|
54
|
-
- **Tearing, plasticity, burning.**
|
|
55
|
-
- **GPU simulation.** The engine has a mature WebGPU path and a particle VM that could host this, but
|
|
56
|
-
the requirement is a worker, and a worker is the cheaper, more portable, more debuggable answer.
|
|
57
|
-
Nothing in the data layout forecloses a GPU port; §12 keeps that door open.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## 3. Author-facing API
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
// An entity hierarchy — rope, chain, hanging sign, a prop built out of entities.
|
|
65
|
-
// Cloth simulates the transform subtree of the entity it is on. That entity is the anchor.
|
|
66
|
-
rope_top.add(Cloth.from(ClothDynamics.ROPE));
|
|
67
|
-
|
|
68
|
-
// A skinned model's garment — cape, skirt, banner, hair.
|
|
69
|
-
// Cloth simulates the JOINTS of the model instance this entity owns.
|
|
70
|
-
character.add(Cloth.from(ClothDynamics.SILK));
|
|
71
|
-
character.add(ClothRig.from(cape_proxy)); // cape_proxy: ClothProxy, built at import
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
There is no mode enum and no discriminator field. **`Cloth` alone simulates the entity subtree;
|
|
75
|
-
`Cloth` + `ClothRig` simulates a skinned model's joints** — component presence doing the job an enum
|
|
76
|
-
would otherwise do badly. The solver is identical either way; only seeding and write-back differ.
|
|
77
|
-
|
|
78
|
-
Those are two cases rather than one because **the engine has two hierarchies, and that is a decision
|
|
79
|
-
rather than an accident**. `shade_node_to_entity_composition` expands a loaded model into entities and
|
|
80
|
-
gives a `ShadedGeometry` to every node that draws — except a skinned one, which "is not expanded, and
|
|
81
|
-
that is a boundary rather than an omission": it contributes its transform and its name and no
|
|
82
|
-
geometry, because its vertices deform against a skeleton the renderer owns and the GPU poses, and
|
|
83
|
-
making those joints entities is a design this program does not do (D47). A rope prop is entities all
|
|
84
|
-
the way down. A character's cape is not, and never will be. §11 is where that lands.
|
|
85
|
-
|
|
86
|
-
### 3.1 `Cloth`
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
export class Cloth {
|
|
90
|
-
dynamics = new ClothDynamics(); // how it behaves — §3.3
|
|
91
|
-
blend = 1; // 0 = fully animated pose, 1 = fully simulated
|
|
92
|
-
layer = 1; // ClothCollider layer/mask filtering, RigidBody semantics
|
|
93
|
-
mask = 0xFFFFFFFF;
|
|
94
|
-
flags = ClothFlags.None;
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
**The anchor is the entity the component is on**, and nothing names anything. Without a `ClothRig`
|
|
99
|
-
the particle set is the entity's own transform subtree, walked through
|
|
100
|
-
`TransformAttachmentSystem#findChildrenOf`/`#countChildrenOf` — the parent-to-children index the
|
|
101
|
-
engine already maintains. Put `Cloth` on the top link of a rope and you get the rope; put it on a
|
|
102
|
-
node with several chains under it and you get all of them in **one coupled solve**, which is what
|
|
103
|
-
makes self-collision between them work.
|
|
104
|
-
|
|
105
|
-
Two rules keep the walk unambiguous:
|
|
106
|
-
|
|
107
|
-
- **A nested `Cloth` claims its own subtree.** The walk stops when it meets another `Cloth`, so
|
|
108
|
-
independent hanging parts of one prop partition naturally.
|
|
109
|
-
- **`ClothExclude`** is a tag component that prunes a subtree from the walk, for the rig that has a
|
|
110
|
-
non-simulated attachment point in the middle of a chain.
|
|
111
|
-
|
|
112
|
-
With a `ClothRig`, the entity is instead the one that owns the skinned model instance, and the
|
|
113
|
-
subtree walk does not run at all — the particle set comes from the proxy's joint list (§3.2).
|
|
114
|
-
|
|
115
|
-
`blend` is the escape hatch for cinematics, LOD and "turn it off for this one shot". It is a lerp
|
|
116
|
-
between the animated pose and the simulated pose applied at write-back, not a solver parameter, so it
|
|
117
|
-
is free and always safe. **M1 note:** writing it wakes a sleeping cloth, because sleep skips the
|
|
118
|
-
write-back as well as the step (§7 P5) and a `blend` written to a sleeping cloth would otherwise take
|
|
119
|
-
effect at whatever later moment something else happened to wake it.
|
|
120
|
-
|
|
121
|
-
### 3.2 `ClothRig`
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
export class ClothRig {
|
|
125
|
-
proxy = null; // ClothProxy — shared, immutable, built at import
|
|
126
|
-
skin = 0; // which of the instance's skins the proxy's joint indices are into
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
A `ClothProxy` **object**, not a path. This is the convention the engine already uses for a component
|
|
131
|
-
that points at a built, shared, immutable asset: `Collider#shape` holds an `AbstractShape3D` and is
|
|
132
|
-
documented as *not* serialised because shapes are shared assets the pipeline wires up;
|
|
133
|
-
`ParticleEffect` holds a `ParticleLayout` and a `ParticleProgram` the same way. `ClothRig` serialises
|
|
134
|
-
the proxy's identity and the skin ordinal, and the asset pipeline resolves the rest.
|
|
135
|
-
|
|
136
|
-
**The proxy addresses joints by index, never by name.** A joint is a `Node3D` inside a
|
|
137
|
-
`SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible by construction —
|
|
138
|
-
and reintroducing strings at runtime to work around that would be the wrong answer. Instead the
|
|
139
|
-
binding is resolved **once, at import**: the builder has `SceneBundleInstance#nodes` (the by-name map
|
|
140
|
-
the engine keeps because sockets and effect anchors are authored as bone names) and turns names into
|
|
141
|
-
integer indices into `Skin#joints`. The runtime component holds integers.
|
|
142
|
-
|
|
143
|
-
### 3.3 `ClothDynamics`
|
|
144
|
-
|
|
145
|
-
The parameters live in one value object, and the library is a set of instances of it.
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
export class ClothDynamics {
|
|
149
|
-
stretch = 0.7; // 0..1 normalised material coordinate
|
|
150
|
-
bend = 0.2; // 0..1
|
|
151
|
-
damping = 0.15; // 0..1
|
|
152
|
-
slack = 0.5; // 0..1 — how far the cloth may leave the animated pose (§3.4)
|
|
153
|
-
thickness = 0.02; // metres — particle radius, for collision offset and self-collision
|
|
154
|
-
inertia = 1; // 0..1 — how much of the anchor's world motion the cloth feels
|
|
155
|
-
drag = 0.1; // 0..1 — aerodynamic coupling to the air (§10)
|
|
156
|
-
substeps = 2; // solver budget
|
|
157
|
-
iterations = 4; // solver budget
|
|
158
|
-
flags = 0; // ClothDynamicsFlags.SelfCollision, ...
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The library is `ClothDynamics.SILK`, `.COTTON`, `.DENIM`, `.LEATHER`, `.ROPE`, `.CHAIN`, `.HAIR` —
|
|
163
|
-
frozen instances in `cloth_dynamics_library.js`. Assign one directly and it is shared, immutable and
|
|
164
|
-
free; call `.clone()` and you own a copy to edit. There is no field that silently rewrites other
|
|
165
|
-
fields, and no second "advanced" block: this *is* the parameter set, and the library is a set of
|
|
166
|
-
points in it that are known to be good rather than a mechanism.
|
|
167
|
-
|
|
168
|
-
The stiffnesses are normalised coordinates, not moduli in N/m — log-mapped internally so that `0.7`
|
|
169
|
-
means something an author can predict and `0.9` is not four orders of magnitude away from it. They are
|
|
170
|
-
independent, because the library carries the good combinations and that is protection enough; nothing
|
|
171
|
-
needs to be collapsed onto a single magic axis to keep an author out of trouble.
|
|
172
|
-
|
|
173
|
-
**M1 wrote that mapping (`cloth_dynamics_map.js`) and neither stiffness maps to newtons per metre.**
|
|
174
|
-
`stretch` maps to a *strain allowance*, because §6's measurement makes the fabric row hard and leaves
|
|
175
|
-
nothing for a stiffness to be. `bend` maps to a **ratio** of the row's own `m_eff/dt²` — at the fixed
|
|
176
|
-
step rather than the substep — for two reasons that are both wanted: the fabric's look becomes
|
|
177
|
-
invariant to how heavy the author made the particles, which is what a normalised knob should mean; and
|
|
178
|
-
since `k/(m/h²) = ratio/substeps²`, a bend row expressed that way is inside M0's dissipative band at
|
|
179
|
-
*any* substep count by construction, rather than by an author staying out of trouble. A stiffness in
|
|
180
|
-
N/m has neither property. The cost, stated: **bending saturates** at the top of that band, so a fabric
|
|
181
|
-
stiffer than `bend = 1` cannot be expressed as a soft row that also comes to rest.
|
|
182
|
-
|
|
183
|
-
`substeps` and `iterations` are budget rather than material, and they are here because a chain wants
|
|
184
|
-
more of both than a bedsheet does, so they are part of what "chain" means. Raising `iterations`
|
|
185
|
-
converges further toward the same answer (§5.1); raising `substeps` also reduces implicit Euler's
|
|
186
|
-
numerical damping, so it does change the drape slightly. Both are ordinary fields with ordinary
|
|
187
|
-
documentation, not hidden couplings.
|
|
188
|
-
|
|
189
|
-
`ClothDynamics` carries `equals` and `hash` like every other value type in the engine, so a component
|
|
190
|
-
holding one compares and hashes cheaply.
|
|
191
|
-
|
|
192
|
-
### 3.4 Slack, and why there is no radius in metres
|
|
193
|
-
|
|
194
|
-
`slack` is dimensionless. Each particle's leash radius is derived, not authored:
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
r_i = 2 · slack · d_geo,i
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**M1 corrected the factor of two, and it is a derivation rather than a fudge.** This paragraph
|
|
201
|
-
originally read `r_i = slack · d_geo,i`, which makes `slack = 1` a real and fairly tight constraint
|
|
202
|
-
instead of the absence of one: a rope hanging straight down cannot swing through ninety degrees,
|
|
203
|
-
because its tip has to travel `√2·d_geo` to get there and its leash is `d_geo`. The radius at which
|
|
204
|
-
the row is *provably* inert is `2·d_geo` — the tether already holds every particle within `d_geo` of
|
|
205
|
-
the anchor, the animated position it is leashed to is also within `d_geo` of the anchor since a chord
|
|
206
|
-
is never longer than the arc it subtends, so by the triangle inequality the two are never further
|
|
207
|
-
apart than that. With the factor of two in, `slack = 1` means exactly "no leash": the row cannot
|
|
208
|
-
bind, so it is not built at all, and there is no sentinel and no discontinuity to code around.
|
|
209
|
-
`MEASUREMENTS.md` §10.
|
|
210
|
-
|
|
211
|
-
where `d_geo,i` is the particle's baked geodesic distance to the fixed set. At `slack = 0` every
|
|
212
|
-
particle is welded to its animated position; at `slack = 1` a particle may be anywhere within its own
|
|
213
|
-
reach of that position. Three things fall out of that and none of them need a knob:
|
|
214
|
-
|
|
215
|
-
- **The profile is automatic.** A hem gets a long leash and a waistband gets a short one, because
|
|
216
|
-
that is what their geodesic depth says, not because someone painted it.
|
|
217
|
-
- **The fixed set falls out too.** A pinned particle has `d_geo = 0`, so `r_i = 0`, so the leash *is*
|
|
218
|
-
the pin. There is no separate attach constraint — one row covers both (§6).
|
|
219
|
-
- **There is no magic default.** The old formulation had a radius in metres with `0` meaning "derive
|
|
220
|
-
it from the mesh extent", which is the kind of overloaded sentinel that gets copied into a save file
|
|
221
|
-
and then means the wrong thing.
|
|
222
|
-
|
|
223
|
-
Slack is not a stretch allowance and does not govern inextensibility — the tether row does that, and
|
|
224
|
-
the two are different questions (§6). Slack asks how loosely the cloth is leashed to the animation;
|
|
225
|
-
the tether asks how far the fabric may extend from its anchor. A cape wants both: a moderate leash so
|
|
226
|
-
it follows the character, and a hard tether so it never becomes rubber.
|
|
227
|
-
|
|
228
|
-
### 3.5 `ClothCollider`
|
|
229
|
-
|
|
230
|
-
```js
|
|
231
|
-
export class ClothCollider {
|
|
232
|
-
inflation = 0; // extra thickness in metres, on top of ClothDynamics#thickness
|
|
233
|
-
friction_scale = 1; // multiplies the entity's Collider#friction
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
from the
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
one
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
row
|
|
625
|
-
|
|
626
|
-
**A
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
the
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
is
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
*
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
*
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
the
|
|
797
|
-
|
|
798
|
-
**
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
**
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
`
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
The
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
`
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
the
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
**
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
---
|
|
1463
|
-
|
|
1464
|
-
##
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
The
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
**
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
the
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
**
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1
|
+
# Cloth — Design
|
|
2
|
+
|
|
3
|
+
> Design document. Written 2026-09-10; **M0, M1, M2 and M3's rigged path are implemented**, and
|
|
4
|
+
> every place they contradicted it has been corrected in place with the measurement named beside it.
|
|
5
|
+
> §17 says what is done and what is not; `MEASUREMENTS.md` is the evidence.
|
|
6
|
+
>
|
|
7
|
+
> Scope: a cloth and soft-chain simulator that lives inside `engine/physics`, collides against the
|
|
8
|
+
> `Collider` components the rest of the engine already authors, runs on a worker, and is safe to ship
|
|
9
|
+
> with its defaults untouched.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. What this is
|
|
14
|
+
|
|
15
|
+
One solver over particles, driven by components:
|
|
16
|
+
|
|
17
|
+
- **`Cloth`** — the thing that simulates. Put it on an entity and that entity's transform subtree
|
|
18
|
+
becomes a simulated chain: a rope, a chain, a hanging sign, a mooring line.
|
|
19
|
+
- **`ClothRig`** — add it alongside `Cloth` and the particles are the **joints of a skinned model**
|
|
20
|
+
rather than the entity subtree. This is what makes a cape, a skirt, a banner on a character.
|
|
21
|
+
- **`ClothCollider`** — the marker that opts an existing `(Collider, Transform64)` entity into being
|
|
22
|
+
something cloth collides with. This is the `RigidBody`-analogue: it says *this collider participates*,
|
|
23
|
+
and carries only what cloth needs that `Collider` does not already say.
|
|
24
|
+
|
|
25
|
+
That last component follows a pattern the engine already has twice. `AcousticBody` opts a collider
|
|
26
|
+
into the acoustic simulation and carries an `AcousticMaterial`; `FluidObstacle` opts a collider into
|
|
27
|
+
the fluid simulation and carries an inflation radius. `ClothCollider` is the third instance of the
|
|
28
|
+
same idea, and it should read like a sibling of those two, not like a new invention.
|
|
29
|
+
|
|
30
|
+
The design commitments, in priority order, because they conflict and the ordering is the design:
|
|
31
|
+
|
|
32
|
+
1. **It does not explode.** Not at 5 fps, not at 500, not when a parameter is absurd, not when a
|
|
33
|
+
character teleports, not when a collider is spawned inside the cloth.
|
|
34
|
+
2. **It does not get stuck.** The set of configurations the simulation can reach is bounded around
|
|
35
|
+
the animated pose, and error cannot accumulate across steps.
|
|
36
|
+
3. **It works untouched.** Adding a `Cloth` component with default values to a correctly authored
|
|
37
|
+
garment produces shippable motion. Tuning is an optimisation, never a prerequisite.
|
|
38
|
+
4. **It is cheap.** A CPU worker budget measured in low single-digit milliseconds per character.
|
|
39
|
+
5. **It is accurate.** Last, deliberately. Cloth that is 10% too stiff and never misbehaves beats
|
|
40
|
+
cloth that is correct and occasionally detonates.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Non-goals
|
|
45
|
+
|
|
46
|
+
- **Garment authoring.** No pattern cutting, no sewing, no pressure/inflation. Import a mesh.
|
|
47
|
+
- **Woven anisotropy.** No warp/weft/shear tensor. That needs UV-aligned quad topology and a
|
|
48
|
+
material model this design does not carry. Cloth here is isotropic with a bend modulus.
|
|
49
|
+
- **Penetration-free guarantees.** IPC-class methods (`C-IPC`, `GIPC`) guarantee no intersection ever,
|
|
50
|
+
at a cost per step this budget cannot pay. We guarantee bounded, self-correcting penetration.
|
|
51
|
+
- **Cloth pushing rigid bodies.** Coupling is one-way: colliders move cloth. Two-way coupling into
|
|
52
|
+
`PhysicsSystem`'s velocity solver is a later question and the constraint model leaves room for it
|
|
53
|
+
(§6), but it is not in scope.
|
|
54
|
+
- **Tearing, plasticity, burning.**
|
|
55
|
+
- **GPU simulation.** The engine has a mature WebGPU path and a particle VM that could host this, but
|
|
56
|
+
the requirement is a worker, and a worker is the cheaper, more portable, more debuggable answer.
|
|
57
|
+
Nothing in the data layout forecloses a GPU port; §12 keeps that door open.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Author-facing API
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// An entity hierarchy — rope, chain, hanging sign, a prop built out of entities.
|
|
65
|
+
// Cloth simulates the transform subtree of the entity it is on. That entity is the anchor.
|
|
66
|
+
rope_top.add(Cloth.from(ClothDynamics.ROPE));
|
|
67
|
+
|
|
68
|
+
// A skinned model's garment — cape, skirt, banner, hair.
|
|
69
|
+
// Cloth simulates the JOINTS of the model instance this entity owns.
|
|
70
|
+
character.add(Cloth.from(ClothDynamics.SILK));
|
|
71
|
+
character.add(ClothRig.from(cape_proxy)); // cape_proxy: ClothProxy, built at import
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
There is no mode enum and no discriminator field. **`Cloth` alone simulates the entity subtree;
|
|
75
|
+
`Cloth` + `ClothRig` simulates a skinned model's joints** — component presence doing the job an enum
|
|
76
|
+
would otherwise do badly. The solver is identical either way; only seeding and write-back differ.
|
|
77
|
+
|
|
78
|
+
Those are two cases rather than one because **the engine has two hierarchies, and that is a decision
|
|
79
|
+
rather than an accident**. `shade_node_to_entity_composition` expands a loaded model into entities and
|
|
80
|
+
gives a `ShadedGeometry` to every node that draws — except a skinned one, which "is not expanded, and
|
|
81
|
+
that is a boundary rather than an omission": it contributes its transform and its name and no
|
|
82
|
+
geometry, because its vertices deform against a skeleton the renderer owns and the GPU poses, and
|
|
83
|
+
making those joints entities is a design this program does not do (D47). A rope prop is entities all
|
|
84
|
+
the way down. A character's cape is not, and never will be. §11 is where that lands.
|
|
85
|
+
|
|
86
|
+
### 3.1 `Cloth`
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
export class Cloth {
|
|
90
|
+
dynamics = new ClothDynamics(); // how it behaves — §3.3
|
|
91
|
+
blend = 1; // 0 = fully animated pose, 1 = fully simulated
|
|
92
|
+
layer = 1; // ClothCollider layer/mask filtering, RigidBody semantics
|
|
93
|
+
mask = 0xFFFFFFFF;
|
|
94
|
+
flags = ClothFlags.None;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**The anchor is the entity the component is on**, and nothing names anything. Without a `ClothRig`
|
|
99
|
+
the particle set is the entity's own transform subtree, walked through
|
|
100
|
+
`TransformAttachmentSystem#findChildrenOf`/`#countChildrenOf` — the parent-to-children index the
|
|
101
|
+
engine already maintains. Put `Cloth` on the top link of a rope and you get the rope; put it on a
|
|
102
|
+
node with several chains under it and you get all of them in **one coupled solve**, which is what
|
|
103
|
+
makes self-collision between them work.
|
|
104
|
+
|
|
105
|
+
Two rules keep the walk unambiguous:
|
|
106
|
+
|
|
107
|
+
- **A nested `Cloth` claims its own subtree.** The walk stops when it meets another `Cloth`, so
|
|
108
|
+
independent hanging parts of one prop partition naturally.
|
|
109
|
+
- **`ClothExclude`** is a tag component that prunes a subtree from the walk, for the rig that has a
|
|
110
|
+
non-simulated attachment point in the middle of a chain.
|
|
111
|
+
|
|
112
|
+
With a `ClothRig`, the entity is instead the one that owns the skinned model instance, and the
|
|
113
|
+
subtree walk does not run at all — the particle set comes from the proxy's joint list (§3.2).
|
|
114
|
+
|
|
115
|
+
`blend` is the escape hatch for cinematics, LOD and "turn it off for this one shot". It is a lerp
|
|
116
|
+
between the animated pose and the simulated pose applied at write-back, not a solver parameter, so it
|
|
117
|
+
is free and always safe. **M1 note:** writing it wakes a sleeping cloth, because sleep skips the
|
|
118
|
+
write-back as well as the step (§7 P5) and a `blend` written to a sleeping cloth would otherwise take
|
|
119
|
+
effect at whatever later moment something else happened to wake it.
|
|
120
|
+
|
|
121
|
+
### 3.2 `ClothRig`
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
export class ClothRig {
|
|
125
|
+
proxy = null; // ClothProxy — shared, immutable, built at import
|
|
126
|
+
skin = 0; // which of the instance's skins the proxy's joint indices are into
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A `ClothProxy` **object**, not a path. This is the convention the engine already uses for a component
|
|
131
|
+
that points at a built, shared, immutable asset: `Collider#shape` holds an `AbstractShape3D` and is
|
|
132
|
+
documented as *not* serialised because shapes are shared assets the pipeline wires up;
|
|
133
|
+
`ParticleEffect` holds a `ParticleLayout` and a `ParticleProgram` the same way. `ClothRig` serialises
|
|
134
|
+
the proxy's identity and the skin ordinal, and the asset pipeline resolves the rest.
|
|
135
|
+
|
|
136
|
+
**The proxy addresses joints by index, never by name.** A joint is a `Node3D` inside a
|
|
137
|
+
`SceneBundleInstance`, not an entity, so an `EntityReference` to one is impossible by construction —
|
|
138
|
+
and reintroducing strings at runtime to work around that would be the wrong answer. Instead the
|
|
139
|
+
binding is resolved **once, at import**: the builder has `SceneBundleInstance#nodes` (the by-name map
|
|
140
|
+
the engine keeps because sockets and effect anchors are authored as bone names) and turns names into
|
|
141
|
+
integer indices into `Skin#joints`. The runtime component holds integers.
|
|
142
|
+
|
|
143
|
+
### 3.3 `ClothDynamics`
|
|
144
|
+
|
|
145
|
+
The parameters live in one value object, and the library is a set of instances of it.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
export class ClothDynamics {
|
|
149
|
+
stretch = 0.7; // 0..1 normalised material coordinate
|
|
150
|
+
bend = 0.2; // 0..1
|
|
151
|
+
damping = 0.15; // 0..1
|
|
152
|
+
slack = 0.5; // 0..1 — how far the cloth may leave the animated pose (§3.4)
|
|
153
|
+
thickness = 0.02; // metres — particle radius, for collision offset and self-collision
|
|
154
|
+
inertia = 1; // 0..1 — how much of the anchor's world motion the cloth feels
|
|
155
|
+
drag = 0.1; // 0..1 — aerodynamic coupling to the air (§10)
|
|
156
|
+
substeps = 2; // solver budget
|
|
157
|
+
iterations = 4; // solver budget
|
|
158
|
+
flags = 0; // ClothDynamicsFlags.SelfCollision, ...
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The library is `ClothDynamics.SILK`, `.COTTON`, `.DENIM`, `.LEATHER`, `.ROPE`, `.CHAIN`, `.HAIR` —
|
|
163
|
+
frozen instances in `cloth_dynamics_library.js`. Assign one directly and it is shared, immutable and
|
|
164
|
+
free; call `.clone()` and you own a copy to edit. There is no field that silently rewrites other
|
|
165
|
+
fields, and no second "advanced" block: this *is* the parameter set, and the library is a set of
|
|
166
|
+
points in it that are known to be good rather than a mechanism.
|
|
167
|
+
|
|
168
|
+
The stiffnesses are normalised coordinates, not moduli in N/m — log-mapped internally so that `0.7`
|
|
169
|
+
means something an author can predict and `0.9` is not four orders of magnitude away from it. They are
|
|
170
|
+
independent, because the library carries the good combinations and that is protection enough; nothing
|
|
171
|
+
needs to be collapsed onto a single magic axis to keep an author out of trouble.
|
|
172
|
+
|
|
173
|
+
**M1 wrote that mapping (`cloth_dynamics_map.js`) and neither stiffness maps to newtons per metre.**
|
|
174
|
+
`stretch` maps to a *strain allowance*, because §6's measurement makes the fabric row hard and leaves
|
|
175
|
+
nothing for a stiffness to be. `bend` maps to a **ratio** of the row's own `m_eff/dt²` — at the fixed
|
|
176
|
+
step rather than the substep — for two reasons that are both wanted: the fabric's look becomes
|
|
177
|
+
invariant to how heavy the author made the particles, which is what a normalised knob should mean; and
|
|
178
|
+
since `k/(m/h²) = ratio/substeps²`, a bend row expressed that way is inside M0's dissipative band at
|
|
179
|
+
*any* substep count by construction, rather than by an author staying out of trouble. A stiffness in
|
|
180
|
+
N/m has neither property. The cost, stated: **bending saturates** at the top of that band, so a fabric
|
|
181
|
+
stiffer than `bend = 1` cannot be expressed as a soft row that also comes to rest.
|
|
182
|
+
|
|
183
|
+
`substeps` and `iterations` are budget rather than material, and they are here because a chain wants
|
|
184
|
+
more of both than a bedsheet does, so they are part of what "chain" means. Raising `iterations`
|
|
185
|
+
converges further toward the same answer (§5.1); raising `substeps` also reduces implicit Euler's
|
|
186
|
+
numerical damping, so it does change the drape slightly. Both are ordinary fields with ordinary
|
|
187
|
+
documentation, not hidden couplings.
|
|
188
|
+
|
|
189
|
+
`ClothDynamics` carries `equals` and `hash` like every other value type in the engine, so a component
|
|
190
|
+
holding one compares and hashes cheaply.
|
|
191
|
+
|
|
192
|
+
### 3.4 Slack, and why there is no radius in metres
|
|
193
|
+
|
|
194
|
+
`slack` is dimensionless. Each particle's leash radius is derived, not authored:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
r_i = 2 · slack · d_geo,i
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**M1 corrected the factor of two, and it is a derivation rather than a fudge.** This paragraph
|
|
201
|
+
originally read `r_i = slack · d_geo,i`, which makes `slack = 1` a real and fairly tight constraint
|
|
202
|
+
instead of the absence of one: a rope hanging straight down cannot swing through ninety degrees,
|
|
203
|
+
because its tip has to travel `√2·d_geo` to get there and its leash is `d_geo`. The radius at which
|
|
204
|
+
the row is *provably* inert is `2·d_geo` — the tether already holds every particle within `d_geo` of
|
|
205
|
+
the anchor, the animated position it is leashed to is also within `d_geo` of the anchor since a chord
|
|
206
|
+
is never longer than the arc it subtends, so by the triangle inequality the two are never further
|
|
207
|
+
apart than that. With the factor of two in, `slack = 1` means exactly "no leash": the row cannot
|
|
208
|
+
bind, so it is not built at all, and there is no sentinel and no discontinuity to code around.
|
|
209
|
+
`MEASUREMENTS.md` §10.
|
|
210
|
+
|
|
211
|
+
where `d_geo,i` is the particle's baked geodesic distance to the fixed set. At `slack = 0` every
|
|
212
|
+
particle is welded to its animated position; at `slack = 1` a particle may be anywhere within its own
|
|
213
|
+
reach of that position. Three things fall out of that and none of them need a knob:
|
|
214
|
+
|
|
215
|
+
- **The profile is automatic.** A hem gets a long leash and a waistband gets a short one, because
|
|
216
|
+
that is what their geodesic depth says, not because someone painted it.
|
|
217
|
+
- **The fixed set falls out too.** A pinned particle has `d_geo = 0`, so `r_i = 0`, so the leash *is*
|
|
218
|
+
the pin. There is no separate attach constraint — one row covers both (§6).
|
|
219
|
+
- **There is no magic default.** The old formulation had a radius in metres with `0` meaning "derive
|
|
220
|
+
it from the mesh extent", which is the kind of overloaded sentinel that gets copied into a save file
|
|
221
|
+
and then means the wrong thing.
|
|
222
|
+
|
|
223
|
+
Slack is not a stretch allowance and does not govern inextensibility — the tether row does that, and
|
|
224
|
+
the two are different questions (§6). Slack asks how loosely the cloth is leashed to the animation;
|
|
225
|
+
the tether asks how far the fabric may extend from its anchor. A cape wants both: a moderate leash so
|
|
226
|
+
it follows the character, and a hard tether so it never becomes rubber.
|
|
227
|
+
|
|
228
|
+
### 3.5 `ClothCollider`
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
export class ClothCollider {
|
|
232
|
+
inflation = 0; // extra thickness in metres, on top of ClothDynamics#thickness
|
|
233
|
+
friction_scale = 1; // multiplies the entity's Collider#friction
|
|
234
|
+
layer = 1; // RigidBody semantics, and see below
|
|
235
|
+
mask = 0xFFFFFFFF;
|
|
236
|
+
flags = 0; // ClothColliderFlags.Static
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Everything else is read from components the entity already has: geometry and friction from
|
|
241
|
+
`Collider`, pose from `Transform64`.
|
|
242
|
+
|
|
243
|
+
**M2 added the layer and the mask, because this paragraph originally said "layer filtering from the
|
|
244
|
+
`Cloth` side" and that is not a thing that can happen.** `Cloth#mask` is a mask *of collider layers*,
|
|
245
|
+
and §3.5 is also the paragraph that says a `ClothCollider` does not require a `RigidBody` — so there
|
|
246
|
+
is nowhere else in the tuple for a collider's layer to live. The pair is here with `RigidBody`'s own
|
|
247
|
+
semantics, symmetric: a cloth and a collider interact when each is in the other's mask.
|
|
248
|
+
|
|
249
|
+
**A `ClothCollider` does not require a `RigidBody`.** This is a deliberate divergence from
|
|
250
|
+
`FluidObstacle`, which does require one because it needs a wall velocity. Cloth's colliders are
|
|
251
|
+
overwhelmingly character bones — ordinary entities in a `TransformAttachment` hierarchy — and forcing
|
|
252
|
+
each of them to become a physics body to be seen by cloth would be a tax on the common case. Collider
|
|
253
|
+
velocity is derived from the pose delta between fixed steps, which the system caches anyway for swept
|
|
254
|
+
collision (§8.2). World props that *do* carry a `RigidBody` add a `ClothCollider` the same way and land
|
|
255
|
+
in the same index; the two cases are not distinguished.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 4. Components and systems
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
engine/physics/cloth/
|
|
263
|
+
ecs/
|
|
264
|
+
Cloth.js component: the simulated thing; anchor is its own entity
|
|
265
|
+
ClothFlags.js TeleportKeep, NeverSleep
|
|
266
|
+
ClothExclude.js tag: prune a subtree from the walk
|
|
267
|
+
ClothRig.js component: particles are a skinned model instance's joints
|
|
268
|
+
ClothDynamics.js value object: how it behaves
|
|
269
|
+
ClothDynamicsFlags.js SelfCollision, StrainLimit
|
|
270
|
+
cloth_dynamics_library.js SILK, COTTON, DENIM, LEATHER, ROPE, CHAIN, HAIR — frozen instances
|
|
271
|
+
cloth_dynamics_map.js normalised coordinates → row parameters; every constant measured
|
|
272
|
+
ClothCollider.js component: the RigidBody-analogue for collision
|
|
273
|
+
ClothColliderFlags.js
|
|
274
|
+
ClothSystem.js main-thread system: gather, step, write back
|
|
275
|
+
ClothInstance.js one live cloth: the binding between a subtree and a ClothState
|
|
276
|
+
cloth_seed_subtree.js §11.1's walk: entities → node_parent + animated_position
|
|
277
|
+
cloth_seed_rig.js §11.2's proxy: joints → node_parent + animated_position
|
|
278
|
+
cloth_build_rows.js everything after that, shared by both seeders
|
|
279
|
+
cloth_reconstruct_pose.js particles → a pose, shared by both write-backs
|
|
280
|
+
cloth_write_back.js that pose → Transform64 + TransformAttachment
|
|
281
|
+
cloth_write_back_rig.js that pose → Node3D.transform_local
|
|
282
|
+
cloth_gather_colliders.js the index's overlapping records, in the cloth's own frame
|
|
283
|
+
WorkerClothSystem.js drop-in worker variant (WorkerSystem subclass)
|
|
284
|
+
ClothWorkerCore.js
|
|
285
|
+
cloth.worker.js
|
|
286
|
+
makeClothWorker.js
|
|
287
|
+
cloth_worker_protocol.js
|
|
288
|
+
ClothSerializationAdapter.js
|
|
289
|
+
ClothRigSerializationAdapter.js
|
|
290
|
+
ClothColliderSerializationAdapter.js
|
|
291
|
+
collider/
|
|
292
|
+
ClothColliderIndex.js the index of live cloth colliders + their swept AABBs
|
|
293
|
+
ClothColliderRecord.js the packed layout: kind, params, both poses, bounds, interpolated pose
|
|
294
|
+
ClothColliderKind.js sphere, capsule, box, cylinder, plane
|
|
295
|
+
cloth_collider_pack.js AbstractShape3D → packed record
|
|
296
|
+
cloth_collider_bounds.js the swept AABB, shared by the index and each cloth's own table
|
|
297
|
+
cloth_collider_sdf.js pose interpolation, localisation, and the field dispatch
|
|
298
|
+
solver/
|
|
299
|
+
ClothState.js SoA particle + topology + constraint storage over one buffer
|
|
300
|
+
cloth_step.js the fixed step: predict, warm start, iterate, dual update, integrate
|
|
301
|
+
cloth_solve_vertex.js the per-vertex 3x3 block update
|
|
302
|
+
constraint/ one module per row type (§6)
|
|
303
|
+
cloth_color_particles.js graph colouring of the particle graph
|
|
304
|
+
constraint/cloth_leash_row.js §6's slack row and its tether row, which are one row model
|
|
305
|
+
build/
|
|
306
|
+
ClothProxy.js the built artefact
|
|
307
|
+
cloth_proxy_from_joints.js §11.3 steps 5–8, for a garment that is already rigged
|
|
308
|
+
cloth_proxy_build.js unrigged: source mesh → proxy (weld, simplify, extract, validate)
|
|
309
|
+
cloth_proxy_bind.js unrigged: render mesh → proxy binding weights
|
|
310
|
+
ClothDiagnostics.js dev-build tripwires
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Two systems:
|
|
314
|
+
|
|
315
|
+
- **`ClothColliderSystem`** observes `(Collider, Transform64, ClothCollider)` and maintains a
|
|
316
|
+
`ClothColliderIndex` — a flat array of packed records plus swept world AABBs. This is the same shape
|
|
317
|
+
as `AcousticOccluderIndex`/`OccluderBody` in the sound simulation, and for the same reason: the
|
|
318
|
+
consumers need a compact, pose-cached, broadphase-able view of a set of colliders that is not the
|
|
319
|
+
physics BVH. It cannot be the physics BVH, because `ColliderObserverSystem` deliberately skips
|
|
320
|
+
colliders with no owning body, and bone colliders have no body.
|
|
321
|
+
|
|
322
|
+
**M2 note: it has no `fixedUpdate`, and that is §4.1 applied a second time.** The index has to be
|
|
323
|
+
refreshed from the world's transforms *before* any cloth reads it, and the scheduler cannot express
|
|
324
|
+
that for exactly the reason §4.1 gives — a score is a sum over what a system references, so a system
|
|
325
|
+
can only be moved earlier by touching more, and `ClothSystem` (which reads and writes `Transform64`
|
|
326
|
+
and `TransformAttachment`) outscores one that only reads. So the answer is not a third mechanism, it
|
|
327
|
+
is one call site: `ClothSystem#fixedUpdate` calls `refresh()` at the top of its own step, before any
|
|
328
|
+
cloth is advanced. The collider poses are then read at exactly the moment the anchor poses are, which
|
|
329
|
+
is what makes a contact consistent rather than merely fresh. With no `ClothSystem` in the manager the
|
|
330
|
+
index is never refreshed, and nothing reads it either.
|
|
331
|
+
|
|
332
|
+
**And the index is a linear scan rather than a BVH**, which is a divergence from
|
|
333
|
+
`AcousticOccluderIndex` with two reasons. The query is one AABB per cloth per step against a set that
|
|
334
|
+
is a character's bones — a `ClothCollider` is an opt-in marker, not every collider in the scene — and
|
|
335
|
+
a tree over twenty leaves that all move every frame costs more to keep than to skip. And a
|
|
336
|
+
`PlaneShape3D` has an **unbounded** AABB, which a BVH cannot hold: every refit would grow the root to
|
|
337
|
+
infinity. If a scene ever wants thousands of cloth colliders, the shape of the fix is a BVH over the
|
|
338
|
+
bounded ones with the planes in a short list beside it.
|
|
339
|
+
|
|
340
|
+
- **`ClothSystem`** owns the simulation. Per fixed step: refresh the animated targets, gather the
|
|
341
|
+
overlapping colliders from the index, step, write back.
|
|
342
|
+
|
|
343
|
+
Cloth runs at the fixed rate and is interpolated for display, exactly as `PhysicsSystem` does — the
|
|
344
|
+
same producer/`__interp_restore` contract, so a cloth-driven `Transform64` is authoritative during the
|
|
345
|
+
fixed pass and interpolated outside it. Reusing the existing contract rather than inventing a second
|
|
346
|
+
one is what keeps a cloth-driven bone from fighting an animation-driven one.
|
|
347
|
+
|
|
348
|
+
### 4.1 Ordering, and why the answer is not to be ordered
|
|
349
|
+
|
|
350
|
+
Cloth wants to read the skeleton *after* animation has posed it for this step and *before* anything
|
|
351
|
+
renders. The engine has exactly two ways to order systems: **registration order**, and what each
|
|
352
|
+
system declares in `components_used`, from which writers are sorted before readers. There is
|
|
353
|
+
deliberately no `runs_before`/`runs_after`, because naming other systems is the coupling that design
|
|
354
|
+
exists to avoid.
|
|
355
|
+
|
|
356
|
+
**Cloth both reads and writes `Transform64`**, which puts it on both sides of that component's own
|
|
357
|
+
rule, and M1 established that neither mechanism can express the relation — for a structural reason
|
|
358
|
+
rather than an arithmetic one. `updateExecutionOrder` scores a system as a **sum** over the components
|
|
359
|
+
it references and sorts descending, so a score can only be *raised* by touching more components or
|
|
360
|
+
touching them at a higher access level. The only way to sort **later** is therefore to touch strictly
|
|
361
|
+
less than the system you have to follow, and a consumer of a posed hierarchy touches strictly more.
|
|
362
|
+
Registration order cannot rescue it either: ties break by registration, and this is not a tie —
|
|
363
|
+
`ClothSystem` outscores `TransformAttachmentSystem` and any plausible poser outright, registered first
|
|
364
|
+
or last.
|
|
365
|
+
|
|
366
|
+
**So the answer is not a third mechanism. The answer is that cloth must not care.** An earlier draft
|
|
367
|
+
of M1 added an execution-phase number to `System`, which put the words "pose" and "animation" into a
|
|
368
|
+
layer of the engine that has no such vocabulary, and bought an ordering the design should not have
|
|
369
|
+
needed. What M1 ships instead is a write-back that is **idempotent under recomposition**: cloth writes
|
|
370
|
+
each node's local `TransformAttachment#transform` alongside its world `Transform64`, so a hierarchy
|
|
371
|
+
recomposition — which is what happens whenever anything announces on the anchor — reproduces the
|
|
372
|
+
simulated pose instead of replacing it with the authored one. Measured at **1.7e-15 m** across a
|
|
373
|
+
16-link rope (`MEASUREMENTS.md` §15). With that, where cloth runs is a **quality** question.
|
|
374
|
+
|
|
375
|
+
And a small one. Running before the poser means reading last step's anchor pose, so the cloth's
|
|
376
|
+
*shape* is one fixed step stale; its *position* is not, because the attachment cascade carries the
|
|
377
|
+
whole subtree rigidly with the anchor from cloth's own local transforms. Measured on a 1.6 m rope: a
|
|
378
|
+
few centimetres under ordinary motion, up to a fifth of its own length at the sharpest velocity
|
|
379
|
+
reversal of a ±9 m/s square wave, and **not accumulating** — the same figure after fifteen seconds as
|
|
380
|
+
after two, because the leash and the tether bound the reachable set. `ClothSystem.spec.js` asserts the
|
|
381
|
+
idempotency, records the order the scheduler actually produces, and bounds that divergence.
|
|
382
|
+
|
|
383
|
+
What the declaration *does* buy is the half of the ordering that is real: cloth writes
|
|
384
|
+
`TransformAttachment#transform` and `TransformAttachmentSystem` reads it, so writer-before-reader puts
|
|
385
|
+
cloth first and the attachment system publishes its result. That falls out of `components_used` with
|
|
386
|
+
nothing else asked for.
|
|
387
|
+
|
|
388
|
+
**`InverseKinematicsSystem` is the same shape and reaches the same conclusion.** It reads world
|
|
389
|
+
`Transform64`s "the hierarchy has already composed" and writes local ones back — which is exactly the
|
|
390
|
+
arrangement above, and exactly why it is not fragile in the way its scoring suggests.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 5. The solver
|
|
395
|
+
|
|
396
|
+
**Augmented Vertex Block Descent** (Giles, Diaz & Yuksel, SIGGRAPH 2025) — VBD's per-vertex
|
|
397
|
+
Gauss-Seidel block coordinate descent on the implicit-Euler variational energy, with an augmented
|
|
398
|
+
Lagrangian in place of pure penalties.
|
|
399
|
+
|
|
400
|
+
### 5.1 Why this, and not XPBD
|
|
401
|
+
|
|
402
|
+
XPBD with small substeps is the incumbent: MagicaCloth2, Chaos Cloth, Blender, most of the field.
|
|
403
|
+
It works. Three properties push past it, and each maps onto a stated requirement:
|
|
404
|
+
|
|
405
|
+
- **Unconditional stability at any iteration count.** Every per-vertex update is a descent step on an
|
|
406
|
+
energy that is bounded below, so the update can be truncated at one iteration and the simulation is
|
|
407
|
+
still stable — just softer. That turns requirement 1 into a property of the update rule rather than
|
|
408
|
+
a property of tuning. XPBD is stable in practice, but its stability is a function of substep size
|
|
409
|
+
and stiffness; ours is not.
|
|
410
|
+
- **Parameters that keep their meaning.** AVBD targets the implicit-Euler solution, so a stiffness is
|
|
411
|
+
a material modulus and adding iterations moves the result *toward* the same answer rather than
|
|
412
|
+
changing which answer it is. Under XPBD, apparent stiffness and apparent damping both move when the
|
|
413
|
+
iteration budget moves, which is why cloth packages ship dozens of interacting sliders and
|
|
414
|
+
per-parameter animation curves to compensate. §3.3's small, independent parameter set is only
|
|
415
|
+
honest on a solver whose knobs are orthogonal to its budget.
|
|
416
|
+
|
|
417
|
+
This is iteration-count insensitivity, not substep insensitivity. Implicit Euler is numerically
|
|
418
|
+
damped by an amount that depends on `h`, so more substeps still means less artificial damping and
|
|
419
|
+
a livelier drape — which is why `substeps` is a documented field rather than a hidden constant.
|
|
420
|
+
- **Hard constraints without stiff penalties.** The augmented Lagrangian reaches effectively infinite
|
|
421
|
+
stiffness through an accumulating multiplier rather than a large `k`, so pins, tethers and slack
|
|
422
|
+
bounds are *hard* — enforced to convergence — while cloth stays soft, in the same sweep, without
|
|
423
|
+
the conditioning disaster a 10⁸ penalty would cause. VBD alone cannot do this; it is exactly the
|
|
424
|
+
limitation AVBD was written to fix.
|
|
425
|
+
|
|
426
|
+
Two smaller wins. Colouring the *particle* graph needs far fewer colours than colouring the
|
|
427
|
+
*constraint* graph — the VBD paper reports 8 colours for a 3,891-vertex tetrahedral mesh against 76
|
|
428
|
+
for its 14,802 tetrahedra — which matters for a future GPU port and for cache behaviour now. And the
|
|
429
|
+
per-vertex form is a natural fit for SoA typed arrays, which is what a worker needs anyway.
|
|
430
|
+
|
|
431
|
+
The cost is honest: more arithmetic per vertex than an XPBD projection, and a shorter track record
|
|
432
|
+
for cloth specifically. §18 carries both as risks with a measurement plan.
|
|
433
|
+
|
|
434
|
+
### 5.2 The step
|
|
435
|
+
|
|
436
|
+
Per substep, with `h` the substep length, `m_i` particle mass, `y_i` the inertial target:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
predict y_i = x_i + h·v_i + h²·a_ext,i
|
|
440
|
+
x_i ← x_i + h·v_i + h²·ã_i (adaptive initialisation)
|
|
441
|
+
|
|
442
|
+
warm start for each HARD constraint c: (soft rows: k_c = stiffness, λ_c = 0)
|
|
443
|
+
λ_c ← λ_c · α · γ
|
|
444
|
+
k_c ← clamp(k_c · γ, k_min·s_c, k_max·s_c) s_c = the row's own m_eff/h²
|
|
445
|
+
|
|
446
|
+
iterate repeat `iterations` times:
|
|
447
|
+
for each colour: (parallel within a colour)
|
|
448
|
+
for each particle i in colour:
|
|
449
|
+
f = -(m_i/h²)(x_i - y_i)
|
|
450
|
+
H = (m_i/h²)·I
|
|
451
|
+
for each constraint c on i:
|
|
452
|
+
C = C_c(x); g = ∂C_c/∂x_i
|
|
453
|
+
fu = k_c·C + λ_c
|
|
454
|
+
fc = clamp(fu, fmin_c, fmax_c)
|
|
455
|
+
f -= fc·g
|
|
456
|
+
if fc > 0: (row in tension)
|
|
457
|
+
H += (fc/L)·(I − g gᵀ) (geometric stiffness, PSD part)
|
|
458
|
+
if fc == fu: (row active — not at a clamp)
|
|
459
|
+
H += k_c·(g gᵀ)
|
|
460
|
+
x_i += H⁻¹·f (3×3 LDLᵀ, capped at the shortest incident row)
|
|
461
|
+
|
|
462
|
+
dual update, for each HARD constraint c:
|
|
463
|
+
λ_c ← clamp(k_c·C_c + λ_c, fmin_c, fmax_c)
|
|
464
|
+
k_c ← min(k_c + β·|C_c|, k_max·s_c)
|
|
465
|
+
|
|
466
|
+
integrate v_i = (x_i - x_i^prev)/h
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The Hessian contribution is gated on the row being *active*. A clamped row's force is constant with
|
|
470
|
+
respect to position, so its true second derivative is zero; adding `k·ggᵀ` anyway would make an
|
|
471
|
+
inactive unilateral constraint resist motion it should not touch. It contributes its (constant) force
|
|
472
|
+
to `f` and nothing to `H`.
|
|
473
|
+
|
|
474
|
+
`α ∈ [0,1]` regularises the multiplier carried between steps, `γ ∈ [0,1)` decays warm-started duals so
|
|
475
|
+
a resting configuration bleeds off accumulated force instead of ringing, and `β` is the penalty ramp.
|
|
476
|
+
These are solver internals, global, and deliberately **not** on `ClothDynamics` — they are not a
|
|
477
|
+
property of a fabric. The reference 2D implementation's defaults are a rigid-body tuning and will not
|
|
478
|
+
transfer; §17 M0 makes picking cloth values a measured step with its own spec, not a copied constant.
|
|
479
|
+
|
|
480
|
+
**M0 measured them, and four things about this pseudocode were wrong.** The sweep and its full grids
|
|
481
|
+
are in `MEASUREMENTS.md`; the corrections are folded into the listing above and are, in short:
|
|
482
|
+
|
|
483
|
+
1. **The five constants only govern *hard* rows.** A row whose material stiffness is finite is
|
|
484
|
+
representable as a plain penalty, and a plain penalty at the material stiffness *is* the material
|
|
485
|
+
law — there is nothing for a multiplier to add. Carrying one anyway drives `C` to zero and makes
|
|
486
|
+
every fabric inextensible regardless of what it was authored as: a 1 kN/m spring measured 100×
|
|
487
|
+
stiffer than Hooke. So a soft row is a pure penalty at its material stiffness and reads dead flat
|
|
488
|
+
against all five constants, which is what makes them honest as global internals. A row is **hard**
|
|
489
|
+
exactly when its stiffness is `Infinity` — §6's table already says `hard` in that column, and this
|
|
490
|
+
makes it literal rather than a threshold.
|
|
491
|
+
2. **`k_min` and `k_max` are ratios of `m/h²`, not stiffnesses in N/m.** Held at a fixed multiple of
|
|
492
|
+
the row's own inertial scale, a 20-link hard chain produced the same residual column across five
|
|
493
|
+
decades of particle mass; held at a fixed stiffness it produced a different answer for every mass.
|
|
494
|
+
This paragraph's own reasoning already names `k_max/(m/h²)` as the quantity that matters — it just
|
|
495
|
+
stopped one step short of making the ratio the parameter.
|
|
496
|
+
3. **`α` and `γ` act only through their product, and `α·γ ≥ 0.5` diverges.** At `α·γ ≤ 0.3` a 20-link
|
|
497
|
+
hard chain settles to rest with kinetic energy at 1e-9 J; at 0.5 it jitters; at 0.6 it detonates.
|
|
498
|
+
Residual improves monotonically towards that cliff, so the value is a margin decision:
|
|
499
|
+
**α·γ = 0.25**, a factor of two clear.
|
|
500
|
+
4. **`β` could not be chosen from M0 data.** Above about 1e4 every M0 scenario gives an identical
|
|
501
|
+
answer, and with `k_min = k_max` — which measured as good as any ramp for M0's persistent rows —
|
|
502
|
+
it has no effect at all. The ramp exists for **transient** rows, a contact that should arrive soft
|
|
503
|
+
rather than shock the mesh on its first frame, and M0 has none. M2 has to measure it.
|
|
504
|
+
|
|
505
|
+
Chosen: **α = 0.25, γ = 1, β = 1e5 (unexercised), k_min = k_max = 8·(m/h²)**. γ sits at 1 because its
|
|
506
|
+
second job — decaying the penalty — has nothing to do while the band is a single point, so the whole
|
|
507
|
+
decay lives in α, whose only job is the multiplier. When M2's contacts make the band real, γ becomes
|
|
508
|
+
live and has to be re-measured.
|
|
509
|
+
|
|
510
|
+
**Adaptive initialisation** is VBD's: seed the iteration at `x + h·v + h²·ã` where `ã` interpolates
|
|
511
|
+
between zero and full external acceleration according to whether the previous step looked like free
|
|
512
|
+
fall or like resting contact. Without it, a low iteration count over-stretches under gravity on the
|
|
513
|
+
first iteration and the tethers spend the rest of the budget pulling it back.
|
|
514
|
+
|
|
515
|
+
**Chebyshev acceleration** (`ω₁ = 1`, `ω₂ = 2/(2−ρ²)`, `ωₙ = 4/(4−ρ²ωₙ₋₁)`) is **not here**. M2
|
|
516
|
+
built it as specified — including the gate the VBD paper requires, disabled for any particle in
|
|
517
|
+
contact this step, since accelerating a vertex whose constraint set just changed is where the method's
|
|
518
|
+
stability argument stops applying — and measured it against the first contacts the system has.
|
|
519
|
+
|
|
520
|
+
It lost on every axis. On a sheet landing at 3 m/s it left the penetration inside the run-to-run
|
|
521
|
+
spread and made the settled kinetic energy 1.5× worse; on a hanging cape it traded a third off the
|
|
522
|
+
penetration for ten times the residual energy, which is a livelier cloth rather than a
|
|
523
|
+
better-converged one; it costs 2–5%; and past `ρ = 0.9` it diverges, with `ρ = 0.99` reaching a
|
|
524
|
+
non-positive-definite Hessian inside a second. A mechanism that costs, does not help, and detonates at
|
|
525
|
+
the top of its own range is worse than its absence. `MEASUREMENTS.md` §21 keeps the grid so a future
|
|
526
|
+
attempt starts from the numbers.
|
|
527
|
+
|
|
528
|
+
### 5.3 Why the update never explodes
|
|
529
|
+
|
|
530
|
+
`H = (m/h²)·I + Σ_tension (fc/L)·(I − g gᵀ) + Σ_active k_c·(g gᵀ)`. The mass term is positive
|
|
531
|
+
definite; every `k_c·g gᵀ` is a positive semidefinite rank-one term; every `(fc/L)·(I − g gᵀ)` with
|
|
532
|
+
`fc > 0` is positive semidefinite too. So `H` is symmetric positive definite by construction — for any
|
|
533
|
+
topology, any parameter values, any degenerate configuration, and regardless of how many rows are
|
|
534
|
+
clamped out. `H⁻¹f` is always a descent direction.
|
|
535
|
+
|
|
536
|
+
**Three things M0 measured changed this paragraph.** They are corrections to the mechanism, not to the
|
|
537
|
+
goal; the full evidence is in `MEASUREMENTS.md`.
|
|
538
|
+
|
|
539
|
+
**The geometric-stiffness term is kept where it is positive semidefinite, not dropped wholesale.**
|
|
540
|
+
For a distance row `∂²C/∂x² = (I − g gᵀ)/L`, which is PSD exactly when the row is in tension, so
|
|
541
|
+
`max(fc, 0)·∂²C/∂x²` can be added without touching the SPD argument — every term in `H` is still PSD
|
|
542
|
+
on top of a PD mass term, and there is still no threshold. Dropping it *all* was measured to make the
|
|
543
|
+
update overshoot sideways whenever `fc/L` passes `m/h²`, raising the vertex energy by 20% at a ratio
|
|
544
|
+
of 4 and by a factor of 40 at a ratio of 7e4. This is not "adding the indefinite term back": the
|
|
545
|
+
negative-semidefinite half, where the row is in compression, is still dropped, and dropping it there
|
|
546
|
+
is conservative rather than dangerous. Note the term is added whether or not the row is at a clamp —
|
|
547
|
+
a saturated row applies a constant-magnitude force that still swings as the row rotates.
|
|
548
|
+
|
|
549
|
+
**The 3×3 solve is an LDLᵀ factorisation, not a cofactor inverse.** They agree in exact arithmetic
|
|
550
|
+
and do not agree in `Float64`. A vertex carrying one very stiff row against a small mass term
|
|
551
|
+
assembles an `H` with a condition number in the billions, and the cofactor determinant — a sum of
|
|
552
|
+
triple products that has to cancel by that ratio squared — comes out **negative** on a matrix whose
|
|
553
|
+
eigenvalues are all positive (measured: eigenvalues 0.12, 0.12, 5.4e8; determinant −4.8e8 against a
|
|
554
|
+
true 7.75e6). The factorisation never forms those products; its pivots stayed positive throughout the
|
|
555
|
+
fuzz, and their product is the determinant to full precision. The dev-build assertion checks the
|
|
556
|
+
pivots, which *is* the positive-definiteness claim rather than a proxy for it.
|
|
557
|
+
|
|
558
|
+
**Positive definiteness gives a descent direction, not a monotone step.** `Δ = H⁻¹f` satisfies
|
|
559
|
+
`f·Δ > 0`, so the energy falls for a small enough step — and the update takes a full step of 1. Where
|
|
560
|
+
the quadratic model stops resembling the energy, a full step overshoots and climbs: measured falling
|
|
561
|
+
monotonically out to `t ≈ 0.001` of one step and then rising to 40× the starting energy at `t = 1`.
|
|
562
|
+
The step is therefore capped at the **shortest row incident on the particle** — the distance over
|
|
563
|
+
which "the direction to a neighbour" means anything, which is the mesh's own geometry rather than a
|
|
564
|
+
tuned constant, and which never binds for cloth in ordinary motion. With all three corrections in,
|
|
565
|
+
full-step energy increases on a realistic cape are 0.004% of updates and never exceed a fifth of the
|
|
566
|
+
local energy scale; across deliberately absurd cloth they are 1.5% and never exceed 1.0. §7 P1 states
|
|
567
|
+
the gate that follows from this.
|
|
568
|
+
|
|
569
|
+
A dev-build tripwire mirrors `find_non_finite_physics_state.js`: a post-step sweep for non-finite
|
|
570
|
+
particle state that names the first offender. It sits behind `assert.*` so it compiles out of consumer
|
|
571
|
+
bundles — the engine ships raw source, and a check moved out of `assert.*` stops being strippable.
|
|
572
|
+
|
|
573
|
+
### 5.4 Colouring
|
|
574
|
+
|
|
575
|
+
The particle graph is coloured so no constraint touches two particles of the same colour, using the
|
|
576
|
+
existing `core/graph/coloring/colorizeGraphGreedy.js` and checked by the existing
|
|
577
|
+
`validateGraphColoring.js`. Bending stencils span four particles, so the graph is the mesh graph plus
|
|
578
|
+
the bend diagonals — around 8–10 colours for typical garment topology. Colouring runs once, on a
|
|
579
|
+
`Graph` object, and is baked into the `ClothProxy` as colour ranges over a permuted particle order;
|
|
580
|
+
for a chain it runs at link time on a graph of a few dozen nodes. Nothing colours per step and the
|
|
581
|
+
runtime never sees a graph object.
|
|
582
|
+
|
|
583
|
+
Self-collision rows appear and vanish per step and cannot be pre-coloured. They are accumulated into
|
|
584
|
+
a per-particle scratch buffer and applied Jacobi-style within the sweep — the same mixed
|
|
585
|
+
Gauss-Seidel/Jacobi treatment VBD uses for dynamic collisions.
|
|
586
|
+
|
|
587
|
+
**M2 found that a *collider* contact needs none of that.** A contact against a collider is **unary**:
|
|
588
|
+
one particle against a surface that is fixed for the substep. It couples no two particles, so there is
|
|
589
|
+
no Gauss-Seidel versus Jacobi distinction to make and nothing to keep out of the colouring — it
|
|
590
|
+
accumulates in the vertex update exactly like a leash row. What is true of a contact is that it *comes
|
|
591
|
+
and goes*, which is a storage problem rather than an ordering one: M2's answer is a fixed four slots
|
|
592
|
+
per particle inside `ClothState`'s own buffer, rebuilt each step and keyed on the collider's index
|
|
593
|
+
handle so a multiplier survives the rebuild. Determinism comes from the *set* being built by one pass
|
|
594
|
+
in a fixed order over a fixed table. The paragraph above stands unchanged for M4's rows, which really
|
|
595
|
+
are pairs. `MEASUREMENTS.md` §18.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 6. Everything is one constraint row
|
|
600
|
+
|
|
601
|
+
Every feature in this system is a row in one table. A row is `(C(x), f_min, f_max, stiffness)`, and
|
|
602
|
+
the solver in §5.2 knows nothing else about it.
|
|
603
|
+
|
|
604
|
+
| Row | `C(x)` | `f_min` | `f_max` | Stiffness | Gives you |
|
|
605
|
+
|---|---|---|---|---|---|
|
|
606
|
+
| Stretch | `‖x_i − x_j‖ − L₀` | −∞ | +∞ | `k_stretch` | fabric that resists stretching |
|
|
607
|
+
| Bend | quadratic stencil `Σ w_k·x_k` | −∞ | +∞ | `k_bend` | fabric that resists folding |
|
|
608
|
+
| Slack | `‖x_i − s_i‖ − 2·slack·d_geo,i` | 0 | +∞ | hard | follows the animation; pins at `d_geo = 0` |
|
|
609
|
+
| Tether (LRA) | `‖x_i − p_anchor‖ − d_geo,i` | 0 | +∞ | hard | no rubber-band stretch |
|
|
610
|
+
| Backstop | `r_b − (x_i − b_i)·n_i` | 0 | +∞ | hard | never inside the body |
|
|
611
|
+
| Collision | `(r_i + infl) − sdf_c(x_i)` | 0 | +∞ | hard | colliders push cloth |
|
|
612
|
+
| Friction | tangential slip magnitude | 0 | `+μ·f_n` | hard | cloth grips |
|
|
613
|
+
| Self-collision | `‖x_i − x_j‖ − (r_i + r_j)` | −∞ | 0 | `k_self` | cloth doesn't pass through itself |
|
|
614
|
+
| Strain limit | `‖x_i − x_j‖ − L_max` | 0 | +∞ | hard | hard cap on elongation |
|
|
615
|
+
|
|
616
|
+
Unilateral rows fall out of the clamp: **`[0, +∞)` admits only forces that reduce `C`, `(−∞, 0]` only
|
|
617
|
+
forces that increase it**, and `(−∞, +∞)` is bilateral.
|
|
618
|
+
|
|
619
|
+
**M1 corrected three of those clamps and the sentence above them.** An earlier draft glossed the rule
|
|
620
|
+
as "`[0, +∞)` is push-only, `(−∞, 0]` is pull-only", which is true only relative to a particular sign
|
|
621
|
+
of `C` — the collision row writes `C` as `r − sdf` and the distance rows write it as `‖·‖ − r`, so the
|
|
622
|
+
same clamp means opposite things on them. Following the gloss rather than the rule gave Slack, Tether
|
|
623
|
+
and Strain limit `(−∞, 0]`, which under the solver's own `f -= fc·g` is the force that pushes a
|
|
624
|
+
distance row *apart*. Such a row does not bind weakly; it never binds at all, and it would drive a
|
|
625
|
+
particle away from the leash centre it is supposed to hold it near. `cloth_leash_row.spec.js`
|
|
626
|
+
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,
|
|
627
|
+
made literal. A hard row's penalty lives in `[k_min, k_max]·(m/h²)` and its multiplier carries the
|
|
628
|
+
load; a soft row is a pure penalty at its material stiffness and carries no multiplier at all. M0
|
|
629
|
+
measured why it has to be one or the other rather than a sliding scale (§5.2), and measured what the
|
|
630
|
+
multiplier actually buys: not a smaller residual — at a fixed budget a stiff penalty is comparable —
|
|
631
|
+
but **settling**. A hard 20-link chain comes to rest with 1e-14 J of kinetic energy where the same
|
|
632
|
+
chain on a penalty of any stiffness sits in a limit cycle at 1e-4 J. That is §7 P5, and it is the
|
|
633
|
+
augmented Lagrangian's real return.
|
|
634
|
+
|
|
635
|
+
`C` for a bending row is a **vector** — the discrete mean-curvature vector — so that row carries three
|
|
636
|
+
multipliers against one penalty, and its clamps are absent because a bending moment has no sign to be
|
|
637
|
+
one-sided about. Every other row in the table is scalar.
|
|
638
|
+
|
|
639
|
+
**M2 corrected the last two rows of that table, and both corrections come from rules the table already
|
|
640
|
+
states.** The collision and friction rows carried named stiffnesses `k_contact` and `k_friction`, and
|
|
641
|
+
by M0's own rule — a row is hard exactly when its stiffness is `Infinity` — that made both of them
|
|
642
|
+
soft penalties. A soft row's entire force is `k·C` with `k` capped at `8·(m/h²)`, which is **4.6 N at
|
|
643
|
+
one full thickness of penetration**; the hard row measured producing 11 N for a sheet arriving at
|
|
644
|
+
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.
|
|
645
|
+
Both rows are hard.
|
|
646
|
+
|
|
647
|
+
And friction's clamps were `±μ·f_n`, which is the *signed-slip* formulation — a row whose `C` is a
|
|
648
|
+
displacement along a chosen tangent. This row's `C` is the **magnitude** of the slip in the contact's
|
|
649
|
+
tangent plane, so `C ≥ 0` always and the negative half of that clamp names a force pulling the
|
|
650
|
+
particle further from where it stuck. `[0, μ·f_n]` is the same physics, and writing it as a magnitude
|
|
651
|
+
is what removes the tangent basis: a signed row needs two per contact, and any choice continuous frame
|
|
652
|
+
to frame carries state, which is exactly what §7 P4(a) exists to keep out. Measured against Coulomb:
|
|
653
|
+
the stick/slide transition sits on `tan θ` at 10°, 20°, 30°, 40° and 50°. `MEASUREMENTS.md` §18.
|
|
654
|
+
|
|
655
|
+
**There is no separate attach row.** A pinned particle is a slack row with `d_geo = 0`: the constraint
|
|
656
|
+
becomes `‖x_i − s_i‖ ≤ 0` under a pull-only clamp, which welds it. One row, one code path, and the
|
|
657
|
+
distinction between "pinned" and "loosely following" is a number rather than a branch. (The gradient of
|
|
658
|
+
`‖x − s‖` is undefined at `x = s`; the row is skipped below an epsilon, which is the standard guard and
|
|
659
|
+
is also exactly when it has nothing to do.)
|
|
660
|
+
|
|
661
|
+
**Slack and tether are different questions, and a cape needs both.** Slack bounds distance to the
|
|
662
|
+
particle's *animated position*; the tether bounds distance to the *anchor*. Slack alone implies a
|
|
663
|
+
tether bound of `(1 + slack)·d_geo` — enough to stop a catastrophe, not enough to stop visible rubber —
|
|
664
|
+
and for a garment with no meaningful animated pose to follow (a flag, a curtain, where `slack` is near
|
|
665
|
+
1 by necessity) the tether is doing all the inextensibility work on its own.
|
|
666
|
+
|
|
667
|
+
This mirrors the doctrine already written into `physics/constraint/solve_constraints.js`, whose
|
|
668
|
+
6-DOF joints are "one row model, parameterised by mode", with LOCKED and LIMITED differing only in
|
|
669
|
+
bias and clamp. Same idea, position level instead of velocity level. A reader who knows one knows the
|
|
670
|
+
other, and a new cloth feature is a new row rather than a new pass.
|
|
671
|
+
|
|
672
|
+
Two rows deserve a note.
|
|
673
|
+
|
|
674
|
+
**Bend is the quadratic model**, not a dihedral angle. Its Hessian is constant for a fixed rest shape,
|
|
675
|
+
so the stencil weights are precomputed at build time, `C` is linear in `x`, and `g` is a constant read
|
|
676
|
+
from the proxy — bending costs almost nothing per iteration, and M0 measured it at an eighth of a
|
|
677
|
+
stretch row per visit despite touching twice as many particles. The weights annihilate the rest shape
|
|
678
|
+
and every affine image of it, which is every in-plane deformation to first order; they do *not*
|
|
679
|
+
annihilate an arbitrary planar configuration, and that is the precise content of the near-isometric
|
|
680
|
+
assumption below. More importantly, it has **no singularity
|
|
681
|
+
at 0° or 180°**. Dihedral bending needs a normal, an angle and a sign, all of which degenerate when two
|
|
682
|
+
triangles become coplanar or invert, and that degeneracy is a classic source of exactly the twitching
|
|
683
|
+
this design exists to eliminate. The cost is that the quadratic model assumes a near-flat rest shape
|
|
684
|
+
and near-isometric deformation; a garment with a strongly curved rest shape it wants to hold will
|
|
685
|
+
under-resist. §18 carries that.
|
|
686
|
+
|
|
687
|
+
**Stretch is per-edge.** Edge springs on a triangle mesh do not resist shear on their own; the bend
|
|
688
|
+
rows and the slack bound between them keep the mesh from collapsing in practice, and a triangle-area
|
|
689
|
+
row is available for the cases where they do not. Full in-plane anisotropy is a non-goal (§2).
|
|
690
|
+
|
|
691
|
+
**A chain bends through a skip row, not through the quadratic stencil.** M1 found there is nothing to
|
|
692
|
+
build a hinge on: the stencil is a discrete mean curvature over a *surface*, four particles across the
|
|
693
|
+
diamond either side of a shared edge, and a rope has no diamond — `bt_edge_read_diamond` declines a
|
|
694
|
+
wire edge for exactly that reason and is right to. What a chain gets instead is an ordinary stretch
|
|
695
|
+
row between a node and its **grandparent**, at whatever distance the authored pose puts them. That is
|
|
696
|
+
this section's own doctrine applied once more, it costs no new row kind, and it keeps both properties
|
|
697
|
+
the quadratic model was chosen for: no normal, no angle and no sign, so nothing degenerates at 0° or
|
|
698
|
+
180°, and a fold is high energy rather than a singular configuration. It also does something the
|
|
699
|
+
quadratic stencil explicitly cannot (§18) — because its rest length is read from the authored pose
|
|
700
|
+
rather than assumed flat, **it holds a curved rest shape**. `MEASUREMENTS.md` §9.
|
|
701
|
+
|
|
702
|
+
**And a stretch row has to be hard, which this table's `k_stretch` does not say.** M0 measured that a
|
|
703
|
+
*penalty* stiffer than about `8·(m/h²)` makes the step add energy rather than dissipate it — the cloth
|
|
704
|
+
sustains a limit cycle forever, and raising the iteration count shrinks it without removing it. For a
|
|
705
|
+
half-gram particle at 60 Hz × 2 substeps that boundary sits near 60 N/m, which is a rubber band rather
|
|
706
|
+
than a fabric, so every stiffness an author would reach for is on the wrong side of it. The augmented
|
|
707
|
+
Lagrangian is therefore not a refinement over a stiff penalty for this row: it is the only formulation
|
|
708
|
+
of it that comes to rest, because its penalty is capped at the top of the dissipative band and the
|
|
709
|
+
multiplier carries the rest. M1's normalised `ClothDynamics#stretch` should map to `Infinity` with a
|
|
710
|
+
strain-limit row governing extension, not to a large finite `k`. `MEASUREMENTS.md` §6.
|
|
711
|
+
|
|
712
|
+
**M1 built that and measured what the strain-limit row is actually worth, and the answer is "a
|
|
713
|
+
third".** It removes 34–38% of the peak over-stretch, consistently, at every budget — and it **never
|
|
714
|
+
reaches its own allowance**: at `stretch = 1` it asks for 0.1% of strain and delivers 5.9% at one
|
|
715
|
+
iteration per substep, 2.2% at 2×2 and 0.9% at 2×4. The reason is structural. A strain-limit row is a
|
|
716
|
+
*second hard row on the same edge with the same gradient*, so it competes for the same trust region
|
|
717
|
+
and the same penalty band as the fabric row it is backing up; it can add force in a direction force is
|
|
718
|
+
already being added in, and nothing more. Below about `stretch = 0.35` its allowance is looser than
|
|
719
|
+
what the fabric row does unaided and it is simply inert.
|
|
720
|
+
|
|
721
|
+
So **`stretch` is the weakest field in §3.3's parameter set**, and the honest statement is that at low
|
|
722
|
+
budgets the bound belongs to the budget rather than to the fabric. The row is worth its 13–15% of a
|
|
723
|
+
garment frame where the residual is visible and is not worth it where the residual is already around
|
|
724
|
+
one per cent, which is why it is a flag — `ClothDynamicsFlags.StrainLimit` — rather than always on,
|
|
725
|
+
and why the library entries at 2×4 clear it. `MEASUREMENTS.md` §11.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## 7. Pathology budget
|
|
730
|
+
|
|
731
|
+
The requirement was cloth that does not fold up, get stuck, or spaz out. Each failure mode below gets
|
|
732
|
+
a named mechanism and a spec that fails without it. This table is the acceptance criterion for the
|
|
733
|
+
whole system, and every row's spec is a standing gate, not a one-off.
|
|
734
|
+
|
|
735
|
+
**P1 — Blow-up, NaN, unbounded velocity.**
|
|
736
|
+
*Mechanism:* SPD Hessian by construction (§5.3), so every update is a descent direction; the step
|
|
737
|
+
capped at the shortest incident row so a full Newton step cannot leave the region its model describes;
|
|
738
|
+
a per-particle velocity clamp; the `assert.*` non-finite tripwire.
|
|
739
|
+
|
|
740
|
+
**M1 landed the clamp and it is not derived from `thickness`.** This row asked for one "derived from
|
|
741
|
+
`thickness` and `h`", and `thickness` turns out to be the wrong length scale: it is a *contact*
|
|
742
|
+
radius, so a clamp built from it is a **tunnelling** bound, and a tunnelling bound sits below ordinary
|
|
743
|
+
cloth speeds rather than above them. Silk is 4 mm thick, so "a particle may not cross its own radius
|
|
744
|
+
in a step" is 0.24 m/s at 60 Hz — against the 29 m/s the same silk reaches behind a sprinting anchor.
|
|
745
|
+
Any multiple large enough to stop the clamp being a *behaviour* for silk is large enough to be
|
|
746
|
+
meaningless for leather, whose thickness is five times silk's and whose speeds are not.
|
|
747
|
+
|
|
748
|
+
The scale that works is the solver's own **correction authority**,
|
|
749
|
+
`shortest_row · substeps · iterations / dt` — the trust region caps a vertex update at the shortest
|
|
750
|
+
row incident on the particle, so that product is the whole of what one fixed step can undo. M1
|
|
751
|
+
measured a cloth losing its bounds past it (§7 P2 below), and the clamp is four times it: a bound on
|
|
752
|
+
the solver being asked to do more than it can, scaling with the mesh and the budget rather than with a
|
|
753
|
+
radius nothing reads until M2. The smallest headroom over it in the library is 5.7×. M2 still needs
|
|
754
|
+
the tunnelling bound — it is a different number for a different job and it arrives with the contacts.
|
|
755
|
+
`MEASUREMENTS.md` §13.
|
|
756
|
+
*Spec:* fuzz over random topologies and random `ClothDynamics` values including absurd ones, 10,000
|
|
757
|
+
steps, assert every particle finite. **M0 corrected the second half of this.** "The per-vertex energy
|
|
758
|
+
never increases across an update" does not follow from an SPD Hessian and is not true — SPD buys a
|
|
759
|
+
descent direction, and the update takes a full step (§5.3). What is asserted instead, and holds:
|
|
760
|
+
every LDLᵀ pivot positive on every update (which *is* positive definiteness, checked where it is
|
|
761
|
+
used); nothing non-finite across 10,000 steps of absurd cloth; and full-step energy increases bounded
|
|
762
|
+
and rare — 0.004% of updates on a realistic cape, never above a fifth of the local energy scale, and
|
|
763
|
+
1.5% / 1.0 across the absurd fuzz. Those measured figures are the thresholds the standing gate holds
|
|
764
|
+
the solver to, so a change that makes it worse fails.
|
|
765
|
+
|
|
766
|
+
**P2 — Over-stretch, the rubber cape.**
|
|
767
|
+
*Mechanism:* long-range attachments (Kim et al. 2012). Each particle carries the geodesic distance to
|
|
768
|
+
its nearest fixed particle, baked once, enforced as a hard unilateral row. Because it is hard rather
|
|
769
|
+
than a penalty, one iteration enforces it almost exactly — this is the row that makes low iteration
|
|
770
|
+
counts viable.
|
|
771
|
+
*Spec:* a 30-link × 3-strand rig at one iteration per substep, dropped through ninety degrees; assert
|
|
772
|
+
`max‖x − p_anchor‖ ≤ d_geo·(1+ε)` for every particle across 600 steps, and assert the same rig without
|
|
773
|
+
the rows does not hold.
|
|
774
|
+
*Measured, M1:* **1.0155 with the tether against 1.4650 without**, at 1×1. Four times the budget takes
|
|
775
|
+
the untethered rig to 1.0199, which is still worse than one iteration *with* tethers — so the tether
|
|
776
|
+
is not a cheaper way to buy convergence, it answers a question the per-edge rows cannot.
|
|
777
|
+
|
|
778
|
+
*Limit, stated, and M1 found it:* **the bound is conditional on the solver's correction authority.**
|
|
779
|
+
The trust region caps a vertex update at the shortest row incident on the particle (§5.3), so one
|
|
780
|
+
fixed step can move a particle at most `shortest_row · substeps · iterations`, and a displacement
|
|
781
|
+
larger than that is not one the tether has been given a chance to answer for. Measured: a 30-link
|
|
782
|
+
chain kicked at 16 m/s against 3.0 m/s of authority reaches **6.9× its own reach**, and the same kick
|
|
783
|
+
against 24 m/s of authority reaches 1.06×. Both recover — the excursion is transient, which is P4(c)
|
|
784
|
+
holding — but "no particle is ever further from the anchor than `d_geo`" is true only while the
|
|
785
|
+
velocity clamp of P1 keeps a particle inside what the solve can undo, and that is the reason the clamp
|
|
786
|
+
is built out of the same quantity. `MEASUREMENTS.md` §10.
|
|
787
|
+
|
|
788
|
+
**P3 — Tunnelling through the character.**
|
|
789
|
+
*Mechanism:* three layers. (a) Collision rows evaluate against the collider pose *interpolated to the
|
|
790
|
+
substep*, from the previous and current poses the index caches — cheap swept collision. (b) The slack
|
|
791
|
+
bound stops a particle long before it can travel far enough to cross a limb. (c) Substepping shrinks
|
|
792
|
+
per-step travel.
|
|
793
|
+
|
|
794
|
+
**M2 found (a) is two mechanisms and only had one of them.** Interpolating the poses does nothing
|
|
795
|
+
unless the contact **set** covers the sweep as well, and it did not: `cloth_contact_find` runs once
|
|
796
|
+
per step against the *end-of-step* pose, so a collider that crossed the cloth within the step is
|
|
797
|
+
already past it and no row is ever created. Measured on a capsule driven through a hanging sheet at
|
|
798
|
+
20 m/s: **zero contact rows for the whole pass**, with the sheet untouched — not tunnelled through so
|
|
799
|
+
much as never noticed. The fix is one subtraction per record per step, folding the collider's own
|
|
800
|
+
travel into the discovery reach, and it makes 2 to 40 m/s behave alike (94.8 rows a step at 20 m/s
|
|
801
|
+
against 0.0 without). `MEASUREMENTS.md` §20.
|
|
802
|
+
|
|
803
|
+
**And the interpolation is of a translation and a quaternion, not of two matrices.** §8.2 said
|
|
804
|
+
matrices; a component-wise lerp of two rotation matrices is not a rotation, and halfway through a turn
|
|
805
|
+
of `θ` it scales the shape it poses by `cos(θ/2)`. At the half-radian per step
|
|
806
|
+
`CLOTH_ANCHOR_ROTATION_LIMIT` already calls a spin, a 25 cm collider loses 7.8 mm — twice a silk
|
|
807
|
+
garment's thickness — halfway through every step. `MEASUREMENTS.md` §19.
|
|
808
|
+
|
|
809
|
+
*Limit, stated:* both halves are swept in translation first. A collider spinning fast enough that a
|
|
810
|
+
point on its surface travels more than its own radius within one substep can still be missed, because
|
|
811
|
+
the discovery margin is built from the *centre's* travel. Full CCD for cloth is out of scope.
|
|
812
|
+
*Spec:* a capsule driven through a hanging sheet at 2 to 40 m/s, swept against unswept; assert the
|
|
813
|
+
swept pass creates rows at every speed and that no particle spends a step more than half a collider
|
|
814
|
+
radius inside. `cloth_collision.spec.js` carries the ECS-level companion.
|
|
815
|
+
|
|
816
|
+
**P4 — Folds, tangles, and stays stuck.** The named requirement, so four mechanisms:
|
|
817
|
+
|
|
818
|
+
*(a) No history-dependent normals.* Self-collision and collision normals are derived from the
|
|
819
|
+
proxy's own surface orientation and the collider's SDF gradient at the current position — never from
|
|
820
|
+
"which side was this on last frame". A history-based scheme latches onto a wrong side and stays
|
|
821
|
+
wrong indefinitely; an orientation-based one resolves a tangle as soon as the surrounding geometry
|
|
822
|
+
permits. This is the "predefined surface orientation" branch of the untangling literature, chosen
|
|
823
|
+
specifically because it has no state that *can* get stuck.
|
|
824
|
+
|
|
825
|
+
*(b) No degenerate fold state.* Quadratic bending (§6) means a fully inverted 180° fold is not a
|
|
826
|
+
singular configuration — it is simply high energy, and high energy relaxes.
|
|
827
|
+
|
|
828
|
+
*(c) The slack bound bounds the reachable set.* Every particle is under a hard leash of radius
|
|
829
|
+
`slack·d_geo,i` around its animated position, so the set of configurations the simulation can occupy
|
|
830
|
+
is a bounded neighbourhood of the animation. **Error cannot accumulate** — however bad a step is, the
|
|
831
|
+
next step starts inside that same neighbourhood, and there is no drift mode that walks the cloth
|
|
832
|
+
somewhere it cannot return from. That is the guarantee, and note what it does *not* claim: the
|
|
833
|
+
animated pose itself can be imperfect. It is collision-free in the bind pose (the build validates
|
|
834
|
+
exactly that, §11.3, and fails loudly with the offending vertex and collider named), but an extreme
|
|
835
|
+
animation pose can skin a garment slightly into a thigh. When that happens the attractor is slightly
|
|
836
|
+
wrong and the cloth is slightly wrong with it — bounded and self-limiting, not divergent.
|
|
837
|
+
|
|
838
|
+
This is also why `slack` is the one number worth reaching for when a garment misbehaves: it is
|
|
839
|
+
literally the size of the neighbourhood the simulation is allowed to explore. **M1 note:** with §3.4's
|
|
840
|
+
corrected factor of two, `slack = 1` makes that neighbourhood the whole of what the tether already
|
|
841
|
+
allows, so the guarantee at `slack = 1` is the tether's rather than the leash's — which is the right
|
|
842
|
+
answer for a rope, and is why every chain in the library sits there.
|
|
843
|
+
|
|
844
|
+
**M2 note, and it is a limit rather than a gap.** The escape hatch below watches a *residual*, and
|
|
845
|
+
there is a configuration whose residual is zero and which is still stuck: a rope authored exactly
|
|
846
|
+
along an axis, compressed by a plane whose normal is exactly that axis, has no lateral component
|
|
847
|
+
anywhere in the system and folds into an accordion in which every link is at its rest length. That
|
|
848
|
+
is an exact equilibrium, so nothing has anything to act on. Measured: an 8-link rope on a floor
|
|
849
|
+
stops 18 cm above it and stays; **one part in a thousand of lateral lean** — a millimetre across a
|
|
850
|
+
10 cm link — and it lies down. Recorded rather than fixed, because the fix is noise injected by a
|
|
851
|
+
solver whose whole design commitment is that it needs none, and no authored scene is that
|
|
852
|
+
symmetric. `MEASUREMENTS.md` §22.
|
|
853
|
+
|
|
854
|
+
*(d) A bounded escape hatch.* If a particle's constraint residual stays above a threshold for N
|
|
855
|
+
consecutive steps, it blends to its animated position over a short window. Local, bounded, on by
|
|
856
|
+
default with a conservative threshold, and counted in `ClothDiagnostics` so that a garment relying on
|
|
857
|
+
it is visible rather than silently rescued.
|
|
858
|
+
|
|
859
|
+
*Spec:* deliberately invert half a sheet through itself, run, assert the residual falls below threshold
|
|
860
|
+
and the mesh is self-intersection-free within N steps; separately assert the escape hatch is not
|
|
861
|
+
triggered by a correctly authored garment under normal animation.
|
|
862
|
+
|
|
863
|
+
**P5 — Jitter at rest.**
|
|
864
|
+
*Mechanism:* the multiplier decay `α·γ < 1` bleeds accumulated force out of a resting configuration so
|
|
865
|
+
it cannot ring; per-cloth sleep when the maximum particle displacement over a window falls below a
|
|
866
|
+
threshold; wake on collider motion, anchor motion, wind change, or a `ClothDynamics` change.
|
|
867
|
+
*M0 found the gap, and it is wider than "nothing is damping it".* The mechanism reaches **hard** rows
|
|
868
|
+
only, because a soft row has no multiplier to decay — and a soft row whose penalty exceeds about
|
|
869
|
+
`8·(m/h²)` does not merely fail to dissipate, it makes the step **add** energy. Measured on a hanging
|
|
870
|
+
sheet: total mechanical energy climbing +6.8 J against a system whose entire gravitational potential
|
|
871
|
+
is 1.2 J, sustained indefinitely, and shrinking but never vanishing as iterations or substeps go up.
|
|
872
|
+
Below that ratio the same row is dissipative and settles. What stops it is `ClothDynamics#damping`,
|
|
873
|
+
which arrives with the component in M1 — so **damping is load-bearing for P5 rather than a taste
|
|
874
|
+
control**, and the M1 spec has to gate on a soft cloth actually settling. `MEASUREMENTS.md` §6.
|
|
875
|
+
*Spec:* a settled cloth reaches sleep within a bounded number of steps and its positions are bit-identical
|
|
876
|
+
across the following 300.
|
|
877
|
+
*M1 landed both halves and measured them.* Every library fabric, on a 12-link chain set swinging at
|
|
878
|
+
3 m/s, sleeps: **220 steps for denim, 770 for silk**, everything else between, and the order is the
|
|
879
|
+
damping order. Sleep stops the **write-back** as well as the step, which is what makes "bit-identical"
|
|
880
|
+
a fact rather than a hope — a pose that is not rewritten cannot drift, and one rewritten from unchanged
|
|
881
|
+
inputs is only *probably* identical. A cloth wakes on anchor motion, on a `ClothDynamics` change, on a
|
|
882
|
+
`blend` change, and on an explicit `ClothSystem#wake` for anything it cannot see.
|
|
883
|
+
|
|
884
|
+
And the damping gate, on the configuration §6 measured the limit cycle in — a 16-link rope with its
|
|
885
|
+
stretch rows forced soft at 1e5 N/m, a ratio of about 350 — after 1,500 steps: **2.90e-3 J undamped,
|
|
886
|
+
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
|
|
887
|
+
magnitude from a field the design first described as a taste control. Note what damping does *not* do:
|
|
888
|
+
it does not stop the step adding energy, it removes energy faster than the step adds it, which is why
|
|
889
|
+
the numbers keep falling to `damping = 1` instead of hitting a floor. `MEASUREMENTS.md` §12.
|
|
890
|
+
|
|
891
|
+
**P6 — Teleport and cut.**
|
|
892
|
+
*Mechanism:* anchor motion above a threshold in one step is a teleport — `Reset` (re-seed at the
|
|
893
|
+
animated pose, zero velocity) or `Keep` (carry the shape across). Default `Reset`, followed by a short
|
|
894
|
+
stabilisation window in which the velocity limit ramps back from nothing, so the first steps after a
|
|
895
|
+
reset cannot produce a whip.
|
|
896
|
+
*Spec:* teleport 1 km mid-swing; assert bounded velocity every step and convergence to the animated pose.
|
|
897
|
+
*Measured, M1:* `Reset` peaks at **0.000 m/s** across the 240 steps after the jump and lands on the
|
|
898
|
+
animated pose; `Keep` peaks at 5.58 m/s, which is the swing it was already carrying, and shifts by
|
|
899
|
+
0.078 m in local coordinates against 1.489 m for the reset. The threshold is `max(4·reach, 2 m)` per
|
|
900
|
+
step, and a 9 m/s sprint does not reach it for any library fabric.
|
|
901
|
+
|
|
902
|
+
**M1 named the mode differently and found `Keep` is free.** There is no `ClothTeleportMode`: the choice
|
|
903
|
+
is binary, so it is `ClothFlags.TeleportKeep`, one bit on the component that already carries the
|
|
904
|
+
cloth's own switches — a mode enum for two values is the discriminator field §3.1 exists to avoid. And
|
|
905
|
+
`Keep` is *literally* free: MagicaCloth2 applies the anchor delta rigidly to every particle, but in a
|
|
906
|
+
frame anchored to the cloth's own entity the particles are already anchor-relative, so "apply the
|
|
907
|
+
delta" is doing nothing. The implementation is a `return`.
|
|
908
|
+
|
|
909
|
+
**P7 — Fast locomotion: whipping, lag, centrifugal spray.**
|
|
910
|
+
*Mechanism:* the simulation runs in a **local frame anchored to the cloth's own entity**, with
|
|
911
|
+
`ClothDynamics#inertia` controlling how much of the anchor's world motion the cloth is allowed to
|
|
912
|
+
feel, plus anchor linear and angular speed limits. This is MagicaCloth2's `InertiaConstraint` idea and
|
|
913
|
+
it is the correct one: it makes "character sprints, cape behaves" a default rather than a tuning
|
|
914
|
+
exercise.
|
|
915
|
+
*Spec:* drive the anchor along a step-function velocity profile; assert bounded particle speed throughout.
|
|
916
|
+
*Measured, M1:* a square wave of ±9 m/s, one second a phase. Peak local particle speed runs 11.3 to
|
|
917
|
+
29.3 m/s across the library against clamps of 96 to 288, so the smallest headroom is **5.7×**; nothing
|
|
918
|
+
teleports, and nothing leaves its own reach by more than 0.35%. The anchor limits are derived rather
|
|
919
|
+
than authored — half a reach of travel and half a radian of rotation per fixed step, both expressed
|
|
920
|
+
against the cloth's own geometry so a 3 cm pendant and a 30 m mooring line get the same treatment.
|
|
921
|
+
`inertia` measures monotone and exactly zero at zero: 0, 0.20, 0.37, 0.52, 0.65 m of tip lag across
|
|
922
|
+
its range, which is what makes it a dial rather than a switch with a meaningless middle.
|
|
923
|
+
`MEASUREMENTS.md` §13.
|
|
924
|
+
|
|
925
|
+
**P8 — Degenerate authoring.**
|
|
926
|
+
*Mechanism:* build-time validation, not runtime tolerance. The proxy builder welds duplicates, drops
|
|
927
|
+
zero-area triangles, and refuses non-manifold and inconsistently-wound configurations. The existing
|
|
928
|
+
`bt_edge_read_diamond` already declines every configuration a bending stencil cannot be built on —
|
|
929
|
+
wire edges, boundary edges, non-manifold edges, non-triangular neighbours, inconsistent winding, and a
|
|
930
|
+
"diamond" that is really one doubled triangle — so the degenerate cases are rejected by a function that
|
|
931
|
+
has already thought about them rather than by new code.
|
|
932
|
+
*Spec:* a corpus of deliberately broken meshes, each producing a specific diagnostic and no proxy.
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
## 8. Collision against the physics world
|
|
937
|
+
|
|
938
|
+
### 8.1 The index
|
|
939
|
+
|
|
940
|
+
`ClothColliderSystem` maintains `ClothColliderIndex`: a flat array of packed records, each holding the
|
|
941
|
+
shape kind and its parameters, the world matrix, its cached inverse, the previous step's matrix, the
|
|
942
|
+
world AABB, friction and inflation. `OccluderBody` in the acoustic simulator is the same record and is
|
|
943
|
+
the shape to copy — including its precondition that the pose is rigid, so a distance in the shape's
|
|
944
|
+
local frame equals a distance in world space.
|
|
945
|
+
|
|
946
|
+
Per step, `ClothSystem` queries the index with the cloth's own fattened AABB and gets the overlapping
|
|
947
|
+
records. That set is packed into shared memory for the worker; nothing on the far side of the worker
|
|
948
|
+
boundary is an object.
|
|
949
|
+
|
|
950
|
+
### 8.2 Evaluating a collision
|
|
951
|
+
|
|
952
|
+
The collision row needs `sdf_c(x_i)` and its gradient. Bring the particle into the collider's local
|
|
953
|
+
frame, evaluate the closed-form SDF and gradient there, rotate the gradient back.
|
|
954
|
+
|
|
955
|
+
**The pose is a translation and a quaternion, and there is no cached inverse.** A rigid pose inverts by
|
|
956
|
+
conjugating its quaternion and subtracting its translation, which costs less than storing an inverse
|
|
957
|
+
and is exact rather than cached — and, more importantly, it is the only form the sweep can
|
|
958
|
+
interpolate. This paragraph originally said the sweep comes from "interpolating between the previous
|
|
959
|
+
and current **matrices**", and a component-wise lerp of two rotation matrices is not a rotation: it
|
|
960
|
+
scales the shape it poses by `cos(θ/2)`, which at the half-radian per step the anchor limit already
|
|
961
|
+
calls a spin is 7.8 mm off a 25 cm collider — twice a silk garment's thickness, every step.
|
|
962
|
+
`MEASUREMENTS.md` §19.
|
|
963
|
+
|
|
964
|
+
**And the sweep is two things, not one.** Interpolating the poses is useless unless the contact *set*
|
|
965
|
+
also covers the sweep; §7 P3 carries what that cost and what it now does.
|
|
966
|
+
|
|
967
|
+
**Supported kinds are analytic primitives**: sphere, capsule, box, cylinder, and a plane. That
|
|
968
|
+
restriction is universal in shipping cloth systems — MagicaCloth2 offers sphere, capsule and plane;
|
|
969
|
+
Chaos Cloth adds tapered capsules and convex hulls — and it is what makes the inner loop a handful of
|
|
970
|
+
arithmetic instead of a BVH descent. A `Collider` carrying a mesh or heightmap shape is **skipped with
|
|
971
|
+
one warning naming the entity**, and the documented answer is to add primitive `ClothCollider` children,
|
|
972
|
+
which is what an author would do anyway for a character.
|
|
973
|
+
|
|
974
|
+
The engine's shapes already carry the closed-form SDFs — `CapsuleShape3D#signed_distance_at_point`,
|
|
975
|
+
`BoxShape3D#signed_distance_at_point` and the rest are exact and inline. Their *gradients* are not:
|
|
976
|
+
`signed_distance_gradient_at_point` on capsule, box and cylinder all fall back to
|
|
977
|
+
`compute_signed_distance_gradient_by_sampling`, which is four SDF evaluations and a normalise. That is
|
|
978
|
+
fine for voxelising a fluid obstacle once per step. It is not fine in a per-particle, per-iteration
|
|
979
|
+
loop, so §14 makes the analytic gradients shared code that both sides use.
|
|
980
|
+
|
|
981
|
+
### 8.3 Friction
|
|
982
|
+
|
|
983
|
+
Coulomb friction as a row on the same contact, with `μ` combining `Collider#friction` with
|
|
984
|
+
`ClothCollider#friction_scale`. The combine rule is the one `physics/contact/combine_material.js`
|
|
985
|
+
already applies, so cloth and rigid bodies agree about what a surface feels like.
|
|
986
|
+
|
|
987
|
+
**M2 made three of those words exact.** The clamp is `[0, μ·f_n]` rather than `±μ·f_n`, because the
|
|
988
|
+
row's `C` is the *magnitude* of the tangential slip — §6 carries the derivation and what writing it
|
|
989
|
+
that way buys. `f_n` is the collision row's force **this iteration**, not its converged multiplier
|
|
990
|
+
from this step: the multiplier is one iteration stale and therefore exactly zero on a contact's first
|
|
991
|
+
iteration, so at the one iteration per substep §5.1 says the solver must be usable at, friction would
|
|
992
|
+
never act at all. And the static/dynamic separation needs no smooth transition function of its own —
|
|
993
|
+
it falls out of the clamp, because a row under the cap is enforced to convergence and sticks, and one
|
|
994
|
+
at the cap applies exactly `μ·f_n` against the slip direction and slides.
|
|
995
|
+
|
|
996
|
+
The row's anchor — where the particle stuck — is stored in the **collider's** own frame and read back
|
|
997
|
+
through the interpolated pose, which is what makes a sliding surface drag cloth with it instead of a
|
|
998
|
+
point in space holding it still.
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
## 9. Self-collision
|
|
1003
|
+
|
|
1004
|
+
Off by default — `ClothDynamicsFlags.SelfCollision`. It is the most expensive part of every cloth
|
|
1005
|
+
system that has it — MagicaCloth2's self-collision constraint is 2,564 lines against 606 for
|
|
1006
|
+
distance — and the pathology guarantees in §7 do not depend on it.
|
|
1007
|
+
|
|
1008
|
+
**M4 corrected which entries set it, and the axis the list was drawn along.** This paragraph used
|
|
1009
|
+
to name them by what the garment *is* — set on skirts, long coats and hair, clear on capes,
|
|
1010
|
+
ponytails and banners. What decides is **how far the fabric bends**. Measured on a folding sheet
|
|
1011
|
+
at each entry's own `bend`: silk, hair and cotton pass through themselves 8 to 11 times a sample
|
|
1012
|
+
and the row removes 71% to 81% of it; denim tangles a third as much; leather does not tangle at
|
|
1013
|
+
all — 0.18 crossings a sample — and the row makes it *worse*, because the few pairs it finds are
|
|
1014
|
+
noise it is adding to a fabric that was never going to fold that tightly. `CLOTH_SILK`,
|
|
1015
|
+
`CLOTH_COTTON` and `CLOTH_HAIR` set the flag; `CLOTH_DENIM`, `CLOTH_LEATHER` and both chains do
|
|
1016
|
+
not. `MEASUREMENTS.md` §33.
|
|
1017
|
+
|
|
1018
|
+
Two tiers:
|
|
1019
|
+
|
|
1020
|
+
**M4a — particle-particle repulsion. M4 built it.** A uniform hash grid over particles in the
|
|
1021
|
+
cloth's local frame; pairs closer than `r_i + r_j` get the unilateral row from §6. Pairs sharing a
|
|
1022
|
+
triangle or an edge are excluded, since they are already governed by stretch and bend rows and
|
|
1023
|
+
would otherwise fight them — in practice the exclusion is *sharing any row at all*, which is the
|
|
1024
|
+
same set plus a chain's skip rows.
|
|
1025
|
+
|
|
1026
|
+
**That `r` is not the fabric's thickness, and M4 got that wrong before it measured its way out.**
|
|
1027
|
+
It reads as the same `r` a collider contact uses, which is `ClothDynamics#thickness`; a fabric is
|
|
1028
|
+
4 to 20 mm thick and a proxy's particles are 2 to 8 cm apart, so at that radius the two layers of
|
|
1029
|
+
a fold are almost never in contact and the row finds 1.6 to 3.7 pairs across a whole garment. The
|
|
1030
|
+
radius is a property of **the proxy**: `ClothState#self_radius`, `max(thickness, shortest_row/2)`,
|
|
1031
|
+
so the row's diameter is one particle spacing. At the fabric's own 6 mm the row removes 21% of a
|
|
1032
|
+
sheet's self-intersections; at half the shortest row it removes 82%. `MEASUREMENTS.md` §31.
|
|
1033
|
+
|
|
1034
|
+
**It also needs a ceiling, and the ceiling is a measured length.** A diameter that reaches the
|
|
1035
|
+
closest pair *no row governs* puts a cloth which is not folded at all in permanent contact with
|
|
1036
|
+
itself — leather, 20 mm thick on a 20 mm proxy, found 1,398 pairs a step across 576 particles
|
|
1037
|
+
standing still (§36). On a sheet that distance is two hops and could have been assumed; on a
|
|
1038
|
+
rigged garment, whose proxy is strips with nothing joining one to the next, it is the gap the
|
|
1039
|
+
artist left between strips and is half what the link length implies. So `ClothInstance` measures
|
|
1040
|
+
it once at seed, in the bind pose, and the ceiling is two fifths of it. `MEASUREMENTS.md` §37.
|
|
1041
|
+
|
|
1042
|
+
"O(n) with a small constant" is right about the shape and optimistic about the constant: the pair
|
|
1043
|
+
search is linear in particles with no term in the pair count, and it is **12% of a frame** at the
|
|
1044
|
+
§17 workload, and most of everything self-collision costs. The rows themselves are nearly free.
|
|
1045
|
+
`MEASUREMENTS.md` §34. "The great majority of visible interpenetration" is 82% of it, and the
|
|
1046
|
+
stiffness that does it is `8·(m_eff/h²)`, chosen on the crossing count rather than on M0's
|
|
1047
|
+
dissipative band — a one-sided row that vanishes when satisfied shows no energy growth at 64×
|
|
1048
|
+
that. `MEASUREMENTS.md` §32.
|
|
1049
|
+
|
|
1050
|
+
*This tier cannot see cloth passing between particles.* A sheet thinner than its particle spacing can
|
|
1051
|
+
still slip through itself. Stated, not hidden.
|
|
1052
|
+
|
|
1053
|
+
**M4b — point-triangle and edge-edge.** The full stencil set, which closes that gap, over the same
|
|
1054
|
+
grid. Deferred behind measurement: it roughly triples the self-collision cost and the benefit is
|
|
1055
|
+
garment-dependent.
|
|
1056
|
+
|
|
1057
|
+
**M4 makes that cost estimate the first thing to re-measure.** The stencils would ride on a grid
|
|
1058
|
+
that has already been built, and the grid is where nearly all the money goes — tripling the rows
|
|
1059
|
+
triples something that is 5% to 20% of the feature. `MEASUREMENTS.md` §34.
|
|
1060
|
+
|
|
1061
|
+
Both tiers derive their normals from the proxy's surface orientation, per P4(a).
|
|
1062
|
+
|
|
1063
|
+
**M4a's normal is the line between the two particles, not a surface orientation**, because a
|
|
1064
|
+
particle pair has no surface in it. What P4(a) is actually asking for — nothing history-dependent,
|
|
1065
|
+
no "which side was this on last frame" to latch onto — the pair normal satisfies completely and
|
|
1066
|
+
more simply: `(x_i − x_j)/‖x_i − x_j‖` at the current position, recomputed every iteration, with
|
|
1067
|
+
no multiplier, no warm start and no identity across steps. The sentence above is right about M4b,
|
|
1068
|
+
where a point-triangle stencil genuinely needs an orientation to choose a side.
|
|
1069
|
+
|
|
1070
|
+
There is no 3D uniform hash grid in `core` today; there is a well-tested 2D one
|
|
1071
|
+
(`core/geom/2d/hash-grid/SpatialHashGrid.js`) whose structure is the template. §14.
|
|
1072
|
+
|
|
1073
|
+
**M4 built `core/geom/3d/hash-grid/PointHashGrid3.js` and it is not that structure.** The 2D grid
|
|
1074
|
+
is persistent and incremental — a bounded cell array, elements that are AABBs and carry identity,
|
|
1075
|
+
insert/remove/move as the primitives. Cloth particles are points with no identity across steps in
|
|
1076
|
+
an unbounded domain, and *every one of them moves every step*, so the incremental primitive never
|
|
1077
|
+
gets to be incremental. `PointHashGrid3` is a counting sort into a power-of-two hash and has no
|
|
1078
|
+
insert, no remove and no element identity at all. The idea carried over; not one line of the
|
|
1079
|
+
structure did. `MEASUREMENTS.md` §34.
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
## 10. Wind and aerodynamics
|
|
1084
|
+
|
|
1085
|
+
Cloth that does not move with the air reads as dead, and this engine has something better than a wind
|
|
1086
|
+
zone: a **fluid simulator**. `FluidField` exposes `sampleVelocity(out, x, y, z)` with trilinear
|
|
1087
|
+
filtering over a MAC grid, `fluid_build_world_to_grid` builds the world→grid transform, and
|
|
1088
|
+
`FluidObstacle` already voxelises physics colliders into the field so the air is genuinely disturbed by
|
|
1089
|
+
the bodies in the scene.
|
|
1090
|
+
|
|
1091
|
+
So the design is: **a cloth overlapping a `FluidComponent` samples its velocity field per particle and
|
|
1092
|
+
applies aerodynamic drag against it**, scaled by `ClothDynamics#drag`. The term is an external
|
|
1093
|
+
acceleration, `a_drag = drag · (u_air − v) · |n·û|`, with the normal-facing factor making a sheet catch
|
|
1094
|
+
the wind broadside and slice through it edge-on — the difference between a flag and a rope.
|
|
1095
|
+
|
|
1096
|
+
The payoff is that a character running past a cape gets a wake, because the fluid solver already
|
|
1097
|
+
produces one; a fire's convection lifts a banner; a door slamming pushes a curtain. None of that needs
|
|
1098
|
+
cloth-specific authoring, and none of it needs a wind system that duplicates what the fluid system
|
|
1099
|
+
already does.
|
|
1100
|
+
|
|
1101
|
+
Fallback when there is no fluid field in range: a `GlobalFluidEffector`-shaped ambient term — a
|
|
1102
|
+
constant `wind` vector plus a small band-limited noise for gusting — read from the same interface, so
|
|
1103
|
+
the cloth code has one path and does not branch on whether a simulation is present.
|
|
1104
|
+
|
|
1105
|
+
Cost control: sampling is per particle per *step*, not per iteration, and a cloth outside every fluid
|
|
1106
|
+
field's bounds skips it entirely. Sleep (P5) wakes on a wind change so a settled banner does not stay
|
|
1107
|
+
asleep through a gust.
|
|
1108
|
+
|
|
1109
|
+
This is deferred to M6 — it is the feature most likely to be cut for schedule, and nothing else depends
|
|
1110
|
+
on it.
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## 11. Geometry in, results out
|
|
1115
|
+
|
|
1116
|
+
**Cloth moves joints. It does not produce vertices.** The mapping from a simulated particle to a
|
|
1117
|
+
render vertex already exists in this engine and it is the **skin**: a `Skin` holds `joints: Node3D[]`
|
|
1118
|
+
plus inverse-bind matrices, every render vertex carries a four-entry weight palette into that joint
|
|
1119
|
+
list, and `node.global · inverse_bind` is what the GPU blends every frame. Cloth writes joint
|
|
1120
|
+
transforms; the existing skinning pass does the rest — including bounds refresh and BLAS refit, which
|
|
1121
|
+
a bespoke vertex-deformation path would have to reimplement.
|
|
1122
|
+
|
|
1123
|
+
That is the whole relation, and it is why there is no cloth-owned render mesh anywhere in this design.
|
|
1124
|
+
|
|
1125
|
+
### 11.1 Entity hierarchies — `Cloth` alone
|
|
1126
|
+
|
|
1127
|
+
One particle per entity in the transform subtree of the entity the component is on, walked through
|
|
1128
|
+
`TransformAttachmentSystem#findChildrenOf`, stopping at a nested `Cloth` or a `ClothExclude`. Stretch
|
|
1129
|
+
rows along parent-child links, bend rows along each chain, and the anchor entity itself is the fixed
|
|
1130
|
+
set — so `d_geo` is hop distance from the anchor and every derived quantity in §3.4 works unchanged.
|
|
1131
|
+
|
|
1132
|
+
Output is written back to the subtree entities' `Transform64` — position directly, rotation derived
|
|
1133
|
+
from the direction to the child, interpolated between parent-driven and child-driven orientation, which
|
|
1134
|
+
is what keeps a two-link chain from looking hinged.
|
|
1135
|
+
|
|
1136
|
+
**M1 made three of those words exact.**
|
|
1137
|
+
|
|
1138
|
+
*Both transforms are written: the world `Transform64` and the local
|
|
1139
|
+
`TransformAttachment#transform`.* Writing only the world one is cheaper and is a trap — the attachment
|
|
1140
|
+
system recomposes a child from its local whenever the parent announces, so any announcement arriving
|
|
1141
|
+
after cloth had written would replace the simulated pose with the authored one, and where in the fixed
|
|
1142
|
+
step cloth ran would be a *correctness* question. With the local written too, a recomposition
|
|
1143
|
+
reproduces exactly what cloth just wrote (measured at 1.7e-15 m), and §4.1's ordering problem stops
|
|
1144
|
+
being one. It is also the engine's own precedent: `InverseKinematicsSystem` reads world transforms the
|
|
1145
|
+
hierarchy has composed and writes local ones back, and §11.2's rig path writes `Node3D.transform_local`
|
|
1146
|
+
for the same reason — a local transform is what a joint angle *is*.
|
|
1147
|
+
|
|
1148
|
+
The authored pose is not lost by being overwritten. It is captured once, when the cloth seeds, and
|
|
1149
|
+
lives in the cloth from then on — which is what `blend = 0` returns to, what the leash centres are
|
|
1150
|
+
measured from, and what a re-seed of a live cloth reads instead of the shape the solver left behind.
|
|
1151
|
+
The consequence, stated: **in the entity path the animated pose is constant in the anchor's frame.**
|
|
1152
|
+
That is a property of the case rather than a limitation of the code — a subtree's authored pose is
|
|
1153
|
+
rigid relative to its anchor — but it does mean a subtree whose *links* are independently animated is
|
|
1154
|
+
not an M1 case.
|
|
1155
|
+
|
|
1156
|
+
**M3 corrected the sentence that used to end this paragraph**, which said that was the rig path and
|
|
1157
|
+
that there the animated pose "comes from the skeleton and is live". For a **rigged** garment it is
|
|
1158
|
+
not: cloth claims those joints (§11.2), so nothing else poses them, so their authored pose is the
|
|
1159
|
+
proxy's baked bind pose — constant in the anchor's frame, exactly as a rope's is. What moves is the
|
|
1160
|
+
anchor, and the anchor is a bone of the character's own animated skeleton. The live case is the
|
|
1161
|
+
**unrigged** path, where the targets are mesh vertices skinned by
|
|
1162
|
+
`mesh_apply_skeletal_vertex_skinning` and genuinely move under the animation.
|
|
1163
|
+
|
|
1164
|
+
*The rotation is a **swing** on top of the animated orientation*, `q = swing(d_animated → d_simulated)
|
|
1165
|
+
⊗ q_animated`, using the minimal (twist-free) arc. Twist is inherited from the animation rather than
|
|
1166
|
+
invented, because a chain of particles carries no twist degree of freedom and any twist this code
|
|
1167
|
+
produced would be a fabrication — and the whole thing degenerates correctly, since at `blend = 0` the
|
|
1168
|
+
simulated direction *is* the animated one and the swing is the identity. A node with both a parent and
|
|
1169
|
+
children takes the half-way rotation between the two swings; a leaf takes its parent's. The shortest
|
|
1170
|
+
arc had no primitive in `core` and now does (`quat3_shortest_arc`), which is §14's doctrine turning up
|
|
1171
|
+
a fourth time.
|
|
1172
|
+
|
|
1173
|
+
*Nothing is announced on a simulated node, and every pruned boundary is.* A `TRANSFORM64_EVENT_CHANGE`
|
|
1174
|
+
on a simulated node would make the attachment system recompose its children from the authored pose —
|
|
1175
|
+
undoing the write that was just made, and cascading, for a chain, quadratically. But the root of a
|
|
1176
|
+
`ClothExclude` subtree, and a nested `Cloth`'s anchor, are *not* particles and still have to move: the
|
|
1177
|
+
write-back poses each of them by hand against the link it hangs from and announces exactly there, which
|
|
1178
|
+
reaches everything below the cut through the ordinary cascade and reaches nothing above it.
|
|
1179
|
+
|
|
1180
|
+
This is the path for props that genuinely *are* entity hierarchies: a rope, a chain, a hanging sign, a
|
|
1181
|
+
mooring line, a rigid model whose parts were expanded into entities and each drew as a
|
|
1182
|
+
`ShadedGeometry`. It needs no new rendering code, because moving an entity's `Transform64` is already
|
|
1183
|
+
how anything moves.
|
|
1184
|
+
|
|
1185
|
+
### 11.2 Skinned garments — `Cloth` + `ClothRig`
|
|
1186
|
+
|
|
1187
|
+
A character's cape is not an entity hierarchy and cannot be made into one.
|
|
1188
|
+
`shade_node_to_entity_composition` expands a model into entities and attaches a `ShadedGeometry` to
|
|
1189
|
+
each node that draws — but a skinned node contributes its transform and its name and **no geometry**,
|
|
1190
|
+
because its vertices deform against a skeleton the renderer owns and the GPU poses. Its joints are
|
|
1191
|
+
`Node3D`s inside the entity's `SceneBundleInstance`, and making them entities is explicitly a design
|
|
1192
|
+
this program does not do (D47). So for a skinned garment there is no `ShadedGeometry` component and no
|
|
1193
|
+
bone entities: the geometry is registered with `GPUAnimationManager` through
|
|
1194
|
+
`register_instance_animation`, and the only writable handle is the joint's `Node3D.transform_local` —
|
|
1195
|
+
the same handle the animation systems write.
|
|
1196
|
+
|
|
1197
|
+
**And cloth has to *claim* those joints, which §11.2 originally read as though it did not.** The
|
|
1198
|
+
sentence above says the handle is "the same handle the animation systems write", and they do not
|
|
1199
|
+
write it: `TransformAuthority` is explicit that a node a clip drives is `GPU`, that "the CPU
|
|
1200
|
+
allocates and seeds the row, then never writes to it again", and that "the node's own
|
|
1201
|
+
`transform_local` stops describing it and should not be read as if it did" — and
|
|
1202
|
+
`instantiate_scene_bundle` stamps `GPU` on every node a clip channel targets. So cloth writing
|
|
1203
|
+
`transform_local` on a clip-driven joint would be writing into a field nothing reads.
|
|
1204
|
+
|
|
1205
|
+
`cloth_seed_rig` therefore sets `TransformAuthority.CPU` on every joint it drives. The consequence,
|
|
1206
|
+
stated: **a joint cloth simulates stops being animatable by a clip.** Two writers of one row is the
|
|
1207
|
+
race the enum exists to prevent, and for a cape that is the point of simulating it; for a garment
|
|
1208
|
+
whose joints are also keyed it is an authoring error, and the claim is where it surfaces.
|
|
1209
|
+
`MEASUREMENTS.md` §25.
|
|
1210
|
+
|
|
1211
|
+
**The anchor's world pose is computed, never read back.** A joint of an animated skeleton has no
|
|
1212
|
+
CPU-side `transform_global` — nothing maintains it — so the rig path evaluates the anchor with
|
|
1213
|
+
`pose_evaluate_world` over the clips the entity is playing, which is the engine's own sanctioned
|
|
1214
|
+
answer to "where is this bone" and the one `query_entity_node_world_pose` gives gameplay. A cape and
|
|
1215
|
+
a scabbard on the same shoulder are then placed by the same evaluator off the same clock.
|
|
1216
|
+
|
|
1217
|
+
Cloth therefore drives joints, and the proxy names which ones. **Three resolutions, deliberately:**
|
|
1218
|
+
|
|
1219
|
+
| tier | order of magnitude, skirt | what it is | who owns it |
|
|
1220
|
+
|---|---|---|---|
|
|
1221
|
+
| render mesh | ~5,000 vertices | `MeshletGeometry`, GPU-resident, never touched by cloth | the asset |
|
|
1222
|
+
| sim proxy | ~400 particles | what the solver steps | `ClothProxy` |
|
|
1223
|
+
| output joints | ~60 | what `Skin#joints` holds; the palette binds render vertices to these | `ClothProxy` |
|
|
1224
|
+
|
|
1225
|
+
Decoupling the middle tier from the bottom one is the point. Simulation wants enough particles to
|
|
1226
|
+
drape; skinning wants only enough joints to *describe* the drape, and a four-weight palette blends
|
|
1227
|
+
smoothly between far fewer of them than the simulation needs. Each output joint takes its position and
|
|
1228
|
+
its frame from a small neighbourhood of particles — a triangle fan or a fitted patch — so the joint
|
|
1229
|
+
count is a build-time choice about deformation fidelity rather than a consequence of solver
|
|
1230
|
+
resolution. That is also what disposes of the "one node per particle" cost worry: nothing requires
|
|
1231
|
+
one node per particle.
|
|
1232
|
+
|
|
1233
|
+
Two cases, and they differ only in where the joints and weights came from:
|
|
1234
|
+
|
|
1235
|
+
- **The garment is already rigged** — an artist made 8–20 cape joints and skinned the cape to them.
|
|
1236
|
+
This is the common case and by far the cheapest. The proxy is thin: joint indices, the topology
|
|
1237
|
+
connecting them, rest lengths, `d_geo`. **There is no binding step and no generated geometry — the
|
|
1238
|
+
artist's skin is the relation.** A cape, a ponytail, a coat tail, a loincloth: all of this.
|
|
1239
|
+
- **The garment has no usable rig** and wants per-vertex drape — a full skirt. The importer generates
|
|
1240
|
+
the joint set and generates a skin binding render vertices to it, then bakes both into the proxy.
|
|
1241
|
+
At runtime this is *indistinguishable* from the rigged case: the GPU sees an ordinary skin, the
|
|
1242
|
+
solver sees an ordinary particle set.
|
|
1243
|
+
|
|
1244
|
+
Animated targets — the `s_i` that the slack row, blending and P4 all need — come from
|
|
1245
|
+
`mesh_apply_skeletal_vertex_skinning`, which already takes flat positions, weights, indices and 4×3
|
|
1246
|
+
bone matrices and writes flat output. Typed arrays in, typed arrays out, no objects: it crosses the
|
|
1247
|
+
worker boundary as-is.
|
|
1248
|
+
|
|
1249
|
+
**What was rejected.** A dedicated deformation pass — cloth writing vertex positions into a dynamic
|
|
1250
|
+
geometry buffer, with render vertices bound barycentrically to proxy triangles — is the obvious
|
|
1251
|
+
alternative and is genuinely simpler *as a deformation*: one frame per triangle rather than per joint,
|
|
1252
|
+
no palette width to respect. It loses because `MeshletGeometry` is meshlet-clustered and BLAS-backed,
|
|
1253
|
+
so a new path has to reimplement per-frame vertex upload, meshlet bounds refresh, normal
|
|
1254
|
+
recomputation and BLAS refit — all of which the skinning path already does and has playgrounds for.
|
|
1255
|
+
It stays written down as the fallback if M3's measurements go badly.
|
|
1256
|
+
|
|
1257
|
+
### 11.3 Building a `ClothProxy`
|
|
1258
|
+
|
|
1259
|
+
At import, producing the object `ClothRig#proxy` points at. Almost entirely existing code.
|
|
1260
|
+
|
|
1261
|
+
For a **rigged** garment, only steps 5–8 run, over the joint graph rather than a mesh:
|
|
1262
|
+
|
|
1263
|
+
1. Load the source garment into a `BinaryTopology` (`bt_mesh_from_indexed_geometry`).
|
|
1264
|
+
2. Weld and clean (`geometry_deduplicate_vertices`, drop degenerate faces).
|
|
1265
|
+
3. Decimate to the target particle count with `bt_mesh_simplify_by_error` — quadric error metric, via
|
|
1266
|
+
`bt_mesh_compute_vertex_quadrics`. This is the standard distance-based reduction every cloth package
|
|
1267
|
+
does, with a principled metric instead of a hand-rolled one.
|
|
1268
|
+
4. Extract stretch rows from edges, bend stencils from `bt_edge_read_diamond`, and the boundary from
|
|
1269
|
+
`bt_collect_boundary_segments`.
|
|
1270
|
+
5. Derive the **fixed set automatically**: particles whose skin weights are dominated by joints
|
|
1271
|
+
*outside* the garment's own joint set are pinned. This is what makes "works untouched" true — a
|
|
1272
|
+
cape pinned at the shoulders needs no painting, because the skin data already says where the cape
|
|
1273
|
+
stops being cape.
|
|
1274
|
+
|
|
1275
|
+
**M3 found this is the unrigged rule and that the rigged path has a better one.** For a garment
|
|
1276
|
+
that is already rigged there are no mesh vertices to read weights for — the particles *are*
|
|
1277
|
+
joints — and the topology answers directly: a joint whose parent is outside the named set is a
|
|
1278
|
+
joint the garment hangs from, and they all have to hang from the same bone. That bone is the
|
|
1279
|
+
anchor and its `d_geo` is zero, so §3.4's derived quantities collapse onto it with nothing else
|
|
1280
|
+
saying "this one is pinned". Several chains from one bone are one garment and are §3.1's coupled
|
|
1281
|
+
solve; chains from *different* bones are refused, because there is then no single frame to
|
|
1282
|
+
simulate in.
|
|
1283
|
+
6. Bake `d_geo` — geodesic distance from every particle to the fixed set. This one array drives the
|
|
1284
|
+
tether row, the slack profile, and the pinning, so the three cannot disagree.
|
|
1285
|
+
7. Colour the particle graph (`colorizeGraphGreedy`, checked by `validateGraphColoring`), and permute
|
|
1286
|
+
the particle order so each colour is a contiguous range.
|
|
1287
|
+
8. Resolve joint **names to indices** into `Skin#joints`, using `SceneBundleInstance#nodes`. This is
|
|
1288
|
+
the only place a name appears anywhere in the system.
|
|
1289
|
+
9. **Unrigged only:** choose the output joint set, fit each joint's neighbourhood and bind pose, and
|
|
1290
|
+
generate the skin's weights and inverse-bind matrices for the render mesh.
|
|
1291
|
+
10. **Validate**: manifoldness, no zero-length edges, and — the P4(c) precondition — that the skinned
|
|
1292
|
+
bind pose is collision-free against the character's `ClothCollider` set.
|
|
1293
|
+
|
|
1294
|
+
Vertex painting (per-vertex stiffness or slack multiplier) is supported through an optional texture or
|
|
1295
|
+
vertex-colour channel and is never required. Steps 5 and 6 are the default, and the intent is that most
|
|
1296
|
+
garments never override them.
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
## 12. Data layout, threading, the worker
|
|
1301
|
+
|
|
1302
|
+
**One `SharedArrayBuffer` for the whole cloth world**, not one per instance. `WorkerSystem`'s model is
|
|
1303
|
+
a single shared buffer, a single command region and a single atomic publishing one in-flight step, and
|
|
1304
|
+
`WorkerFluidSystem` already shows how a variable set of simulated things fits that model: the command
|
|
1305
|
+
region carries a count and then a list of ids to step. Cloth does the same — each instance gets a
|
|
1306
|
+
region inside the buffer, and the command region names which instances are live this step, so sleeping
|
|
1307
|
+
(P5) and culled (§12.1) instances cost nothing but their absence from a list.
|
|
1308
|
+
|
|
1309
|
+
SoA throughout:
|
|
1310
|
+
|
|
1311
|
+
| Region | Type | Contents |
|
|
1312
|
+
|---|---|---|
|
|
1313
|
+
| particle state | `Float32Array` | `x`, `x_prev`, `v`, `y`, inverse mass, radius |
|
|
1314
|
+
| animated targets | `Float32Array` | `s`, backstop origin + normal |
|
|
1315
|
+
| topology | `Int32Array` | edge pairs, bend stencils, colour ranges, tether anchors |
|
|
1316
|
+
| rest data | `Float32Array` | rest lengths, bend weights, `d_geo` |
|
|
1317
|
+
| duals | `Float32Array` | `λ` and `k` per persistent constraint |
|
|
1318
|
+
| colliders | `Float32Array` | packed records for this step |
|
|
1319
|
+
| contacts | `Float32Array` | transient rows + their warm-start `λ`, keyed by (particle, collider slot) |
|
|
1320
|
+
|
|
1321
|
+
`ClothDynamics` reaches the worker through the command region, written every step — a dozen words into
|
|
1322
|
+
memory the worker already shares, cheaper than tracking dirtiness, and it keeps
|
|
1323
|
+
`cloth.dynamics.slack = 0.2` working live, which is the point of a value object rather than a preset
|
|
1324
|
+
that has to be re-applied.
|
|
1325
|
+
|
|
1326
|
+
`Float32` for particle state, not `Float64`. The simulation runs in the cloth's **local frame**, so
|
|
1327
|
+
coordinates are garment-sized rather than world-sized and f32 has ample precision; the payoff is half
|
|
1328
|
+
the bandwidth in the loop that dominates. The local frame is not a memory optimisation that happens to
|
|
1329
|
+
help — it is the same choice that makes P7's inertia handling natural, and the precision win follows.
|
|
1330
|
+
Write-back converts local f32 to world `Transform64` at the boundary, once per particle per step.
|
|
1331
|
+
|
|
1332
|
+
Transient contact rows need their multipliers warm-started across steps to be worth anything, so they
|
|
1333
|
+
are keyed by `(particle index, collider handle)` and matched on the next step. A collider that leaves
|
|
1334
|
+
the index drops its slot and its contacts start cold, which is correct.
|
|
1335
|
+
|
|
1336
|
+
**M2 made "slot" exact and it matters.** The key is the collider's **handle in the index**, not its
|
|
1337
|
+
record index in the cloth's own per-step table: that table is whatever this step's broadphase returned,
|
|
1338
|
+
in whatever order, so a record index names a different collider from one step to the next. Storage is a
|
|
1339
|
+
fixed `CLOTH_CONTACT_SLOTS` per particle inside `ClothState`'s single buffer — four, measured never to
|
|
1340
|
+
saturate on a twenty-collider scene and to average a tenth of one — which keeps §12's one-allocation
|
|
1341
|
+
property and bounds the worst case at the four *most violated* colliders, which is the four that matter.
|
|
1342
|
+
|
|
1343
|
+
**The worker.** `WorkerClothSystem` is a `WorkerSystem` subclass and follows `WorkerFluidSystem`
|
|
1344
|
+
exactly, because that pattern is already proven in this repo: `collect` packs colliders and animated
|
|
1345
|
+
targets into shared memory and writes the command words, a single `Atomics.store` publishes the step,
|
|
1346
|
+
`apply` reads the result on the next tick, and the fixed-step gate guarantees the worker is idle during
|
|
1347
|
+
every system's `fixedUpdate`. The split is the same shape too — arbitrary user-facing work (component
|
|
1348
|
+
reads, `Transform64` sync, index queries) stays on the main thread; the pure numeric step crosses.
|
|
1349
|
+
|
|
1350
|
+
The standing gate is the one the fluid system already holds itself to: **`ClothSystem` and
|
|
1351
|
+
`WorkerClothSystem` must produce bit-identical results.** Identical code over identical data in an
|
|
1352
|
+
identical order. That is what makes the worker a deployment choice rather than a behavioural fork, and
|
|
1353
|
+
it is why the core is a pure function over typed arrays from milestone zero even though the worker does
|
|
1354
|
+
not arrive until milestone five.
|
|
1355
|
+
|
|
1356
|
+
Testing the bridge needs no worker: `SystemWorkerLoopback` gives a real `SystemWorkerHost`, a real
|
|
1357
|
+
`SharedArrayBuffer` and real `Atomics` in-process, with message delivery and step execution as explicit
|
|
1358
|
+
events a spec can order. Only the thread is faked.
|
|
1359
|
+
|
|
1360
|
+
`SharedArrayBuffer` requires a cross-origin isolated page (`COOP`/`COEP`), which `WorkerFluidSystem`
|
|
1361
|
+
already documents. The main-thread `ClothSystem` is the fallback where that is not available, and it is
|
|
1362
|
+
not a degraded mode — it is the same code.
|
|
1363
|
+
|
|
1364
|
+
### 12.1 Culling
|
|
1365
|
+
|
|
1366
|
+
Cloth outside the view frustum stops stepping, which for a crowd is the difference between a budget and
|
|
1367
|
+
a problem. Resuming without a pop means blending from the animated pose back to the simulated one over
|
|
1368
|
+
a short window rather than snapping — the same `blend` machinery from §3.1, driven by the system
|
|
1369
|
+
instead of the author.
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
## 13. Reuse ledger
|
|
1374
|
+
|
|
1375
|
+
What this design takes from the engine rather than building:
|
|
1376
|
+
|
|
1377
|
+
| Need | Existing code |
|
|
1378
|
+
|---|---|
|
|
1379
|
+
| Vector and quaternion math | `core/geom/vec3/*` (95 functions), `core/geom/3d/quaternion/*` |
|
|
1380
|
+
| Mesh topology, adjacency, bending stencils | `core/geom/3d/topology/struct/binary/` — `BinaryTopology`, `bt_edge_read_diamond`, `bt_edge_is_boundary`, `bt_collect_boundary_segments` |
|
|
1381
|
+
| Proxy decimation | `bt_mesh_simplify_by_error`, `bt_mesh_compute_vertex_quadrics` |
|
|
1382
|
+
| Mesh cleanup | `geometry_deduplicate_vertices` |
|
|
1383
|
+
| Graph colouring | `core/graph/coloring/colorizeGraphGreedy.js`, `validateGraphColoring.js` |
|
|
1384
|
+
| Collider shapes and their SDFs | `core/geom/3d/shape/*` — closed-form `signed_distance_at_point` on every primitive |
|
|
1385
|
+
| Collider AABBs, pose caching | `core/geom/3d/aabb/*`, `aabb3_matrix4_project` |
|
|
1386
|
+
| Collider ECS wiring | `Collider`, `ColliderFlags`, `ColliderObserverSystem` (as the pattern) |
|
|
1387
|
+
| "Opt a collider into a second simulation" | `FluidObstacle` + `FluidObstacleSystem`, `AcousticBody` |
|
|
1388
|
+
| Collider index shape | `AcousticOccluderIndex` + `OccluderBody` |
|
|
1389
|
+
| Component holds a built shared asset | `Collider#shape`, `ParticleEffect#program` — the convention `ClothRig#proxy` follows |
|
|
1390
|
+
| Surface material combination | `physics/contact/combine_material.js` |
|
|
1391
|
+
| Constraint-row doctrine | `physics/constraint/solve_constraints.js` |
|
|
1392
|
+
| Subtree walk | `TransformAttachmentSystem#findChildrenOf` / `#countChildrenOf`, `TransformAttachment` |
|
|
1393
|
+
| Animated targets | `mesh_apply_skeletal_vertex_skinning` (flat arrays, worker-safe) |
|
|
1394
|
+
| Wind / aerodynamics | `FluidField#sampleVelocity`, `fluid_build_world_to_grid`, `GlobalFluidEffector` |
|
|
1395
|
+
| Worker offload | `ecs/async/WorkerSystem.js`, `SystemWorkerHost`, `SystemWorkerLoopback`, `WorkerFluidSystem` as the worked example |
|
|
1396
|
+
| Fixed step + render interpolation | `PhysicsSystem`'s producer/`__interp_restore` contract |
|
|
1397
|
+
| Transforms | `Transform64`, `Node3D` |
|
|
1398
|
+
| Particle → render-vertex mapping | `Skin` — joints + inverse-bind matrices + the four-weight palette; cloth writes joints and the existing pass does the rest |
|
|
1399
|
+
| Reaching a model's joints from its entity | `SceneBundleInstance` (`skins`, `nodes` by name), `register_instance_animation` |
|
|
1400
|
+
| Deformation, bounds refresh, BLAS refit | `GPUMeshSkinningContext`, `shader_skin_matrix_prep`, `bounds_refresh/` — all of it inherited by driving joints rather than vertices |
|
|
1401
|
+
| Non-finite tripwire pattern | `find_non_finite_physics_state.js` |
|
|
1402
|
+
| Strippable assertions | `core/assert.js` |
|
|
1403
|
+
| Serialisation | the `*SerializationAdapter` pattern and its binary codecs |
|
|
1404
|
+
|
|
1405
|
+
---
|
|
1406
|
+
|
|
1407
|
+
## 14. New shared code this justifies
|
|
1408
|
+
|
|
1409
|
+
Three pieces belong in `core`, not in `cloth`, because the cloth need exposes a gap the engine has
|
|
1410
|
+
anyway:
|
|
1411
|
+
|
|
1412
|
+
1. **Analytic SDF gradients** for sphere, capsule, box, cylinder and plane, as free functions in
|
|
1413
|
+
`core/geom/3d/shape/sdf/`. **M2 built them.** Before it, `signed_distance_gradient_at_point` on
|
|
1414
|
+
sphere, capsule, box and cylinder was `compute_signed_distance_gradient_by_sampling` — four SDF
|
|
1415
|
+
evaluations plus a normalise, for shapes whose gradients are two lines of algebra. The shape classes
|
|
1416
|
+
call the new functions, and so do their `signed_distance_at_point`s, so there is one implementation
|
|
1417
|
+
of each field rather than two that can drift; cloth calls the same functions on packed data.
|
|
1418
|
+
|
|
1419
|
+
One thing the sampled version was quietly wrong about, and it is the reason a degenerate normal
|
|
1420
|
+
needed a decision rather than a default: at a sphere's centre the forward difference returns
|
|
1421
|
+
`(1,1,1)/√3`, which is unit length and entirely an artefact of sampling three positive offsets from
|
|
1422
|
+
one point. The analytic functions return a documented arbitrary direction there instead, because a
|
|
1423
|
+
gradient of zero carries no force and a particle that reaches the exact centre of a sphere would
|
|
1424
|
+
stay there for ever — and "it does not get stuck" is the second design commitment in §1.
|
|
1425
|
+
|
|
1426
|
+
2. **A 3D uniform hash grid.** **M4 built it**, as `core/geom/3d/hash-grid/PointHashGrid3.js`, and
|
|
1427
|
+
**not** modelled on `core/geom/2d/hash-grid/SpatialHashGrid.js` as this line asked for. The 2D
|
|
1428
|
+
grid is a persistent incremental structure over a bounded cell array holding AABBs with
|
|
1429
|
+
identity; a cloth's particles are points, carry no identity across steps, live in an unbounded
|
|
1430
|
+
domain and all move every step, so the operation that matters is *rebuild* and insert/remove
|
|
1431
|
+
never runs. `PointHashGrid3` is a counting sort — count, prefix-sum, scatter — into a
|
|
1432
|
+
power-of-two hash of the integer cell, with a `forEachNear` that walks the 27 neighbouring
|
|
1433
|
+
cells and de-duplicates the buckets they hash into. Useful anywhere a broadphase over many
|
|
1434
|
+
small equal-sized *points* is wanted; anything with extent and identity still wants the 2D
|
|
1435
|
+
grid's shape. `MEASUREMENTS.md` §34.
|
|
1436
|
+
|
|
1437
|
+
3. **`PlaneShape3D`.** **M2 built it**: the half-space `{ p : n·p − offset ≤ 0 }`, with the outward
|
|
1438
|
+
normal and its offset, a constant gradient and no degenerate configuration anywhere. A box standing
|
|
1439
|
+
in for a ground plane has to be large enough that nothing reaches its edge and thin enough that
|
|
1440
|
+
nothing tunnels through it, and those pull against each other in `Float32`.
|
|
1441
|
+
|
|
1442
|
+
It is the one shape in the family that is **unbounded**, and that is a real cost paid in three
|
|
1443
|
+
places: `support` is left unimplemented, because `argmax(p·d)` over a half-space is at infinity for
|
|
1444
|
+
all but one direction and a large finite point would be a silently wrong contact manifold rather
|
|
1445
|
+
than a loud failure; `sample_random_point_in_volume` likewise, since there is no uniform
|
|
1446
|
+
distribution on an infinite volume; and `compute_bounding_box` answers with `±Infinity` on the
|
|
1447
|
+
unbounded axes, which is what the set is. A consumer that cannot hold an infinite AABB must not be
|
|
1448
|
+
handed a plane — which is why `ClothColliderIndex` is a linear scan rather than a BVH, and why it
|
|
1449
|
+
bounds a plane from its world normal instead of projecting that box.
|
|
1450
|
+
|
|
1451
|
+
4. **A shortest-arc quaternion**, `core/geom/3d/quaternion/quat3_shortest_arc.js` — the minimal,
|
|
1452
|
+
twist-free rotation carrying one unit vector onto another. **M1 needed it and the family did not
|
|
1453
|
+
have it**: `quat3_*` had `multiply`, `integrate`, `nlerp` and the uint32 codecs but nothing that
|
|
1454
|
+
answers "orient this bone along that direction", which is the whole of §11.1's write-back. It is
|
|
1455
|
+
also the one place in the family with a genuine discontinuity — no continuous choice of axis exists
|
|
1456
|
+
on the whole sphere, so an antipodal pair has to pick one — and having it in `core` with a spec
|
|
1457
|
+
around that case is better than having it inlined in a write-back loop without.
|
|
1458
|
+
|
|
1459
|
+
Test helpers for all of it live inside the `.spec.js` files or in `__fixtures__`, per the repo rule —
|
|
1460
|
+
a shared helper module would ship on npm.
|
|
1461
|
+
|
|
1462
|
+
---
|
|
1463
|
+
|
|
1464
|
+
## 15. MagicaCloth2: what we take, what we leave
|
|
1465
|
+
|
|
1466
|
+
MagicaCloth2 is the reference and it is a good one. It has shipped, it is fast, and its feature set is
|
|
1467
|
+
the accumulated answer to a decade of production complaints. Taken:
|
|
1468
|
+
|
|
1469
|
+
- **Two cloth kinds over one solver.** MeshCloth and BoneCloth differ only in how particles are seeded
|
|
1470
|
+
and how results are written back. This is right and §11 copies it — expressed as component presence
|
|
1471
|
+
rather than an enum.
|
|
1472
|
+
- **A proxy mesh decoupled from the render mesh**, with distance-based reduction and custom skinning
|
|
1473
|
+
back to the render vertices. Also right; §11.3 does the same thing with `bt_mesh_simplify_by_error`.
|
|
1474
|
+
- **The inertia model.** World and local inertia fractions, movement and rotation speed limits, an
|
|
1475
|
+
anchor that cancels inertia for a character on a moving vehicle, teleport detection with Reset/Keep
|
|
1476
|
+
modes, and a stabilisation window after reset. This is the least glamorous part of the package and
|
|
1477
|
+
the most valuable; P6 and P7 are it, nearly verbatim.
|
|
1478
|
+
- **Max-distance and backstop** as first-class constraints rather than debugging aids. P3 and P4 lean on
|
|
1479
|
+
them harder than MagicaCloth does — here the leash is the correctness argument, not a polish pass.
|
|
1480
|
+
- **Tether/LRA** as the answer to over-stretch at low iteration counts.
|
|
1481
|
+
|
|
1482
|
+
Left:
|
|
1483
|
+
|
|
1484
|
+
- **The parameter surface.** MagicaCloth2's serialised data is a dozen constraint blocks, several of them
|
|
1485
|
+
animation curves over "depth", and its per-constraint files run to hundreds of lines of tuning. That is
|
|
1486
|
+
the direct consequence of a PBD solver whose apparent stiffness depends on iteration count: when the
|
|
1487
|
+
physical meaning of a slider moves with the budget, you end up shipping a curve per slider. AVBD's
|
|
1488
|
+
parameters do not move that way, so §3.3 can be one small value object — and the per-particle profile
|
|
1489
|
+
that MagicaCloth authors with curves falls out of `d_geo` instead (§3.4).
|
|
1490
|
+
- **Angle restoration and angle limit constraints.** These exist to give PBD chains a restoring force
|
|
1491
|
+
that bending alone cannot supply at low iteration counts. With a solver that converges, bend rows plus
|
|
1492
|
+
tethers cover the same ground with less to configure.
|
|
1493
|
+
- **A separate spring constraint.** Subsumed by the slack row with a non-zero radius.
|
|
1494
|
+
- **A distance constraint solved twice per substep** (before and after collision). An artefact of the
|
|
1495
|
+
Gauss-Seidel ordering; the augmented Lagrangian's dual update does this work.
|
|
1496
|
+
- **Its collision layering model.** We already have `Collider`, layers and masks; a parallel system would
|
|
1497
|
+
be a second thing to keep in sync.
|
|
1498
|
+
- **Its wind zones.** We have a fluid simulator (§10).
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
## 16. State of the art
|
|
1503
|
+
|
|
1504
|
+
**The line this design sits on.** PBD → XPBD → substepped XPBD → VBD → AVBD.
|
|
1505
|
+
|
|
1506
|
+
- **XPBD** (Macklin, Müller & Chentanez) made PBD stiffness independent of iteration count by
|
|
1507
|
+
introducing compliance and a per-constraint multiplier. **Small Steps in Physics Simulation**
|
|
1508
|
+
(Macklin et al. 2019) then showed that *n* substeps with one iteration each beats one step with *n*
|
|
1509
|
+
iterations — less constraint error, far less artificial damping, and better behaviour across a wider
|
|
1510
|
+
stiffness range than Krylov-based implicit solvers. Together these are the state of practice: Chaos
|
|
1511
|
+
Cloth, MagicaCloth2 and Blender's cloth all sit here.
|
|
1512
|
+
- **Vertex Block Descent** (Chen, Liu, Yang & Yuksel, SIGGRAPH 2024) reframed the step as block
|
|
1513
|
+
coordinate descent on implicit Euler's variational form, solving one 3×3 system per vertex per
|
|
1514
|
+
iteration. Unconditionally stable at any iteration count, no line search, and it colours the vertex
|
|
1515
|
+
graph rather than the constraint graph. Its reported failure mode is exactly the one that matters here:
|
|
1516
|
+
it struggles to converge under high stiffness ratios and has no mechanism for enforcing hard
|
|
1517
|
+
constraints — which is what a pinned garment on a rigid body *is*.
|
|
1518
|
+
- **Augmented Vertex Block Descent** (Giles, Diaz & Yuksel, SIGGRAPH 2025) fixes precisely that with an
|
|
1519
|
+
augmented Lagrangian: hard constraints at effectively infinite stiffness without the instability, plus
|
|
1520
|
+
substantially better convergence under high stiffness ratios. Reference implementations exist in 2D,
|
|
1521
|
+
in Metal, in PhysX, in Houdini, and — relevant to this engine — as a WebGPU solver.
|
|
1522
|
+
- **Going Further With Vertex Block Descent** (Saillant et al., 2025) extends VBD to higher-order
|
|
1523
|
+
elements and confirms that substepping improves its convergence — which is why §5.2 substeps rather
|
|
1524
|
+
than only iterating.
|
|
1525
|
+
|
|
1526
|
+
**The line this design does not sit on.** **Incremental Potential Contact** and its descendants —
|
|
1527
|
+
**C-IPC** for codimensional objects, **GIPC** and **AGIPC** on GPU — guarantee intersection-free,
|
|
1528
|
+
inversion-free results through log-barrier contact and continuous collision detection. They are the
|
|
1529
|
+
correct answer when correctness is the requirement, and they are the wrong answer at a millisecond
|
|
1530
|
+
budget. We take one idea from that family: IPC's smooth static/dynamic friction transition, which VBD
|
|
1531
|
+
also adopts.
|
|
1532
|
+
|
|
1533
|
+
**Self-collision** is where the field is currently moving, because parallel position solvers have made
|
|
1534
|
+
it the bottleneck rather than the solve. **Air Meshes** (Müller et al. 2015) tessellate the space
|
|
1535
|
+
between objects and prevent element inversion, which handles untangling robustly and automatically but
|
|
1536
|
+
requires maintaining a volumetric mesh. **Global Intersection Analysis** (Baraff, Witkin & Kass,
|
|
1537
|
+
"Untangling Cloth", 2003) is history-free and resolves tangles as soon as geometry permits — the
|
|
1538
|
+
property P4(a) is built on, even though we implement a far cheaper approximation of it. Recent work on
|
|
1539
|
+
hierarchical culling via discrete curvature targets the same bottleneck.
|
|
1540
|
+
|
|
1541
|
+
**Industry.** Chaos Cloth (Unreal) is substepped XPBD with long-range attachments, max distance and
|
|
1542
|
+
backstop distance/radius — the same safety constraints §7 relies on, which is useful independent
|
|
1543
|
+
confirmation that they are what production actually needs. Comparative studies of VBD against PBD in
|
|
1544
|
+
Unity report parity at low vertex counts and a widening VBD advantage as the count rises, which matches
|
|
1545
|
+
the per-iteration cost story: VBD does more arithmetic per vertex and far less coordination.
|
|
1546
|
+
|
|
1547
|
+
Sources:
|
|
1548
|
+
[Vertex Block Descent](https://arxiv.org/abs/2403.06321) ·
|
|
1549
|
+
[VBD project page](https://graphics.cs.utah.edu/research/projects/vbd/) ·
|
|
1550
|
+
[Augmented Vertex Block Descent](https://graphics.cs.utah.edu/research/projects/avbd/) ·
|
|
1551
|
+
[AVBD reference implementation](https://github.com/savant117/avbd-demo2d) ·
|
|
1552
|
+
[WebGPU AVBD](https://github.com/jure/webphysics) ·
|
|
1553
|
+
[Going Further With Vertex Block Descent](https://onlinelibrary.wiley.com/doi/10.1002/cav.70039) ·
|
|
1554
|
+
[XPBD](https://matthias-research.github.io/pages/publications/XPBD.pdf) ·
|
|
1555
|
+
[Small Steps in Physics Simulation](https://mmacklin.com/smallsteps.pdf) ·
|
|
1556
|
+
[Untangling Cloth](https://dl.acm.org/doi/10.1145/882262.882357) ·
|
|
1557
|
+
[Air Meshes for Robust Collision Handling](https://dl.acm.org/doi/10.1145/2766907) ·
|
|
1558
|
+
[Codimensional IPC](https://arxiv.org/pdf/2012.04457) ·
|
|
1559
|
+
[Efficient Self-Collision Culling via Discrete Curvature](https://www.mdpi.com/2227-7390/14/9/1504) ·
|
|
1560
|
+
[VBD vs PBD in Unity](https://www.mdpi.com/2076-3417/14/23/11072) ·
|
|
1561
|
+
[Chaos Cloth](https://dev.epicgames.com/documentation/en-us/unreal-engine/clothing-tool-in-unreal-engine)
|
|
1562
|
+
|
|
1563
|
+
---
|
|
1564
|
+
|
|
1565
|
+
## 17. Milestones
|
|
1566
|
+
|
|
1567
|
+
Each milestone is shippable and each `done` names evidence that ran. Nothing is reported complete on
|
|
1568
|
+
the strength of code existing.
|
|
1569
|
+
|
|
1570
|
+
**M0 — the solver core. DONE, 2026-09-10.** `ClothState`, `cloth_step`, the vertex update, stretch and
|
|
1571
|
+
bend rows, colouring. Pure functions over typed arrays; no ECS, no worker, no rendering.
|
|
1572
|
+
*Proofs, all green:* finite-difference agreement between each row's analytic gradient and its numerical
|
|
1573
|
+
gradient; the P1 property test (see the correction in §7 P1 — strict per-update energy monotonicity is
|
|
1574
|
+
not true and the gate is what replaced it); a measured choice of α, β, γ and `k_min`/`k_max` with the
|
|
1575
|
+
sweep recorded in `MEASUREMENTS.md`, nothing copied; the performance budget measured on real topology.
|
|
1576
|
+
43 tests across six spec files.
|
|
1577
|
+
|
|
1578
|
+
**M0 changed the design in six places**, each with the measurement behind it in `MEASUREMENTS.md` and
|
|
1579
|
+
each folded back into §5.2, §5.3, §6, §7 P1 and §7 P5 above. In one line each: the multiplier and the
|
|
1580
|
+
penalty band apply only to *hard* rows; the band is a ratio of `m/h²` rather than a stiffness; the PSD
|
|
1581
|
+
half of the geometric-stiffness term has to be kept; the 3×3 solve has to be an LDLᵀ; an SPD Hessian
|
|
1582
|
+
gives a descent direction rather than a monotone step; and P5's jitter mechanism does not reach soft
|
|
1583
|
+
rows, which need M1's damping.
|
|
1584
|
+
|
|
1585
|
+
**Deliberately not in M0:** Chebyshev acceleration (§5.2) is gated on a particle not being in contact,
|
|
1586
|
+
and contacts arrive at M2, so there is nothing here to measure it against. The velocity clamp named in
|
|
1587
|
+
P1 is derived from `ClothDynamics#thickness` and belongs with the component that carries it — and it
|
|
1588
|
+
would have masked the property M0 exists to measure.
|
|
1589
|
+
|
|
1590
|
+
**M1 — chains end to end. DONE, 2026-09-10.** `Cloth`, `ClothFlags`, `ClothExclude`, `ClothDynamics`,
|
|
1591
|
+
`ClothDynamicsFlags`, `cloth_dynamics_library.js`, `ClothSystem`, subtree seeding and write-back, the
|
|
1592
|
+
slack and tether rows, the strain-limit row, the inertia model, teleport handling, sleep, and the
|
|
1593
|
+
velocity clamp P1 deferred. No collision.
|
|
1594
|
+
*Proofs, all green:* P2, P5, P6 and P7, each with a companion test that runs the same scenario with the
|
|
1595
|
+
mechanism switched off; `ClothDynamics` value semantics and the measured mapping; three ordering tests
|
|
1596
|
+
— the derived order, the recomposition idempotency that makes it not matter, and a bound on the
|
|
1597
|
+
divergence when it is inverted; a second playground page, `playground/rig.html`, driving the real
|
|
1598
|
+
ECS with a rope, a chain, a five-strand rig under one `Cloth`, a hanging sign with a `ClothExclude`,
|
|
1599
|
+
and a nested pair. **43 tests across four spec files** — the leash row's 8, the system's 9, the
|
|
1600
|
+
pathology budget's 11, the value semantics and the mapping's 15 — plus 5 for the shortest-arc
|
|
1601
|
+
primitive and 4 more in `system_execution_order.spec.js`, for 52 in all. The engine suite is green
|
|
1602
|
+
at 18,179.
|
|
1603
|
+
|
|
1604
|
+
**M1 changed the design in seven places**, each with the measurement behind it in `MEASUREMENTS.md`
|
|
1605
|
+
§9–15 and each folded back above. In one line each: three of §6's clamps had the wrong sign and would
|
|
1606
|
+
never have bound; §3.4's leash radius needs a factor of two before `slack = 1` means "no leash"; §7
|
|
1607
|
+
P1's velocity clamp cannot be derived from `thickness`, because that is a tunnelling bound and a
|
|
1608
|
+
tunnelling bound is *below* ordinary cloth speeds; §11.1's chain bending has no diamond to build a
|
|
1609
|
+
hinge on and uses a skip row instead; §11.1's write-back has to write the **local** transform as well
|
|
1610
|
+
as the world one; the levers no longer close the frame budget one at a time; and the strain-limit row
|
|
1611
|
+
buys a third of the over-stretch rather than the allowance it asks for, which makes `stretch` the
|
|
1612
|
+
weakest field in the parameter set.
|
|
1613
|
+
|
|
1614
|
+
**And one thing M1 got wrong before it got it right, which is worth recording because the wrong answer
|
|
1615
|
+
was plausible.** §4.1 correctly identified that neither of the engine's two ordering mechanisms can
|
|
1616
|
+
place cloth after a poser, and the first attempt was to add a third — an execution-phase number on
|
|
1617
|
+
`System`, with a `PostPose` value. That put the vocabulary of animation into `engine/ecs`, which has
|
|
1618
|
+
none, to buy an ordering the design should not have needed in the first place. Removing the need was
|
|
1619
|
+
both smaller and better: writing the local transform makes the write-back idempotent, which turns the
|
|
1620
|
+
placement into a bounded quality question and leaves the scheduler alone.
|
|
1621
|
+
|
|
1622
|
+
**Deliberately not in M1:** P4(d)'s bounded escape hatch, which needs a per-particle residual monitor
|
|
1623
|
+
and belongs with the collision rows that make a particle get *stuck* in the first place — a chain has
|
|
1624
|
+
no configuration to be stuck in. Chebyshev acceleration, still gated on contacts that arrive at M2. `β`,
|
|
1625
|
+
which M0 could not choose and M1 has no transient rows to choose it with either — every row M1 adds is
|
|
1626
|
+
persistent. `ClothSerializationAdapter` and its siblings: nothing in M1's scope list names them, and a
|
|
1627
|
+
codec written before `ClothRig` exists would be revised by M3 before it was ever loaded.
|
|
1628
|
+
Interpolation: cloth writes `Transform64` at the fixed rate and §12's producer/`__interp_restore`
|
|
1629
|
+
contract is not wired, which shows as a cloth updating at the fixed rate rather than the frame rate
|
|
1630
|
+
and is a visible-but-not-wrong gap. M5 is where it belongs, with the worker.
|
|
1631
|
+
**This is the first shippable feature.**
|
|
1632
|
+
|
|
1633
|
+
**M2 — collision. DONE, 2026-09-10.** `ClothCollider`, `ClothColliderFlags`, `ClothColliderSystem`,
|
|
1634
|
+
`ClothColliderIndex`, the packed record and its swept bounds, analytic SDF gradients and
|
|
1635
|
+
`PlaneShape3D` in `core` (§14.1, §14.3), the contact row and its friction row, the backstop row, and a
|
|
1636
|
+
measured `β` and contact penalty band.
|
|
1637
|
+
*Proofs, all green:* the packed field dispatch agreeing with the shape objects it was extracted from,
|
|
1638
|
+
across all five kinds under a non-axis-aligned pose; the index's swept bounds, layer filter, handle
|
|
1639
|
+
reuse and plane handling; the contact row against Coulomb's own answer for a slope; friction's anchor
|
|
1640
|
+
dragging cloth with a sliding surface; P3 measured swept against unswept from 2 to 40 m/s; the whole
|
|
1641
|
+
stack through a real `EntityManager`, including a collider arriving, leaving, being filtered out and
|
|
1642
|
+
waking a sleeping cloth. **53 tests across five new cloth spec files** — 15 for the collider layer, 21
|
|
1643
|
+
for the two new rows, 12 for the ECS path — plus **21 in `core`** across two more, for the SDF family
|
|
1644
|
+
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
|
|
1645
|
+
scenes, a sweep toggle and the contact band live.
|
|
1646
|
+
|
|
1647
|
+
**M2 changed the design in six places**, each with the measurement behind it in `MEASUREMENTS.md`
|
|
1648
|
+
§16–23 and each folded back above. In one line each: §8.2's swept pose cannot be a lerp of two
|
|
1649
|
+
matrices, because that is not a rotation and it shrinks the collider by twice a garment's thickness at
|
|
1650
|
+
the anchor's own rotation limit; sweeping the poses does nothing unless the contact **set** is swept
|
|
1651
|
+
too, and without that a capsule at 20 m/s produced *no rows at all*; §6's collision and friction rows
|
|
1652
|
+
cannot be soft penalties, because a soft row's whole force is 4.6 N against the 376 N an 8 m/s arrival
|
|
1653
|
+
needs; friction's clamp is one-sided, because its `C` is a magnitude; §5.4's Jacobi treatment is not
|
|
1654
|
+
needed for a collider contact, which is unary; and Chebyshev acceleration, which §5.2 carried as
|
|
1655
|
+
available, loses on every axis and is gone.
|
|
1656
|
+
|
|
1657
|
+
**Deliberately not in M2:** P4(d)'s bounded escape hatch, still — M2 has the contacts that make a
|
|
1658
|
+
particle get stuck, and also measured the one stuck configuration whose *residual is zero*, which is
|
|
1659
|
+
the quantity the escape hatch was to watch (§7 P4). It belongs with the self-collision of M4, where a
|
|
1660
|
+
particle can be pinched between two rows rather than merely held by one. The backstop row is built,
|
|
1661
|
+
specced and driven by the playground, but nothing **authors** one: its plane comes from a garment's own
|
|
1662
|
+
skinned surface and a skinned garment is M3. Two-way coupling, per §2. `ClothSerializationAdapter` and
|
|
1663
|
+
its siblings, per M1's own reasoning.
|
|
1664
|
+
|
|
1665
|
+
**M3 — skinned garments, the rigged path. DONE, 2026-09-10.** `ClothRig`, `ClothProxy`,
|
|
1666
|
+
`cloth_proxy_from_joints` (§11.3 steps 5–8), the rig seeder, and joint write-back into
|
|
1667
|
+
`Node3D.transform_local`. **The unrigged path is not here** — §11.3 step 9's generated joint set and
|
|
1668
|
+
generated skin — and §18 carries what M3 measured about it instead.
|
|
1669
|
+
*Proofs, all green:* the P8 corpus for a joint tree, each refusal naming what an author can act on;
|
|
1670
|
+
the rig path end to end through a real `EntityManager` — the authority claim, the tether under a
|
|
1671
|
+
swinging shoulder, `blend = 0` reproducing the baked bind pose, a model that has not loaded waiting
|
|
1672
|
+
rather than failing, a proxy that does not fit its skin refused once by name, and a rigged cape
|
|
1673
|
+
colliding with a body; and the entity path unchanged beside it. **23 tests across two new spec
|
|
1674
|
+
files** — 12 for the proxy build, 11 for the rig path — plus 14 unchanged for the pose query M3
|
|
1675
|
+
refactored. The engine suite is green at 18,274. A fourth playground page,
|
|
1676
|
+
`playground/garment.html`, with a `Node3D` skeleton, four garments and four motion profiles.
|
|
1677
|
+
|
|
1678
|
+
**M3 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
|
|
1679
|
+
§24–29 and each folded back above. In one line each: §11.2's writable handle is *not* what the
|
|
1680
|
+
animation systems write, and cloth has to claim `TransformAuthority.CPU` to make its write-back
|
|
1681
|
+
visible at all; §11.1's rig path is not "live" for a rigged garment, because cloth owns those joints
|
|
1682
|
+
and their authored pose is the proxy's baked bind pose; §11.3 step 5's weight-derived fixed set is
|
|
1683
|
+
the unrigged rule and the topology answers better for a rigged garment; and §11.2's "~60 output
|
|
1684
|
+
joints" is four to six times short of what a four-weight palette needs for a few per cent of
|
|
1685
|
+
fidelity.
|
|
1686
|
+
|
|
1687
|
+
**And the strongest result is a number rather than a correction.** §11 claims the two paths are "the
|
|
1688
|
+
solver, identical either way". Built as an entity subtree and as a skin's joints, the same garment
|
|
1689
|
+
produces **bit-identical** particle trajectories across three fabrics and three motion profiles.
|
|
1690
|
+
That is what the row builder and the pose reconstruction being *one* function each buys, and it is
|
|
1691
|
+
why M3 moved both out of the entity path's files rather than copying them.
|
|
1692
|
+
|
|
1693
|
+
**Deliberately not in M3:** the unrigged path, per §17's own sequencing and behind §18's measured
|
|
1694
|
+
budget. §11.3 step 10's bind-pose collision validation, which `cloth_proxy_from_joints` cannot do
|
|
1695
|
+
because it has a joint tree and no collider set — and which `MEASUREMENTS.md` §29 makes the argument
|
|
1696
|
+
for, because a garment authored inside its own body reads as a *tether* failure and costs an hour to
|
|
1697
|
+
diagnose. `ClothProxy`'s binary codec and `ClothRigSerializationAdapter`, for M1's own reason: a
|
|
1698
|
+
codec written before the unrigged path exists would be revised before it was ever loaded.
|
|
1699
|
+
|
|
1700
|
+
**M4 — self-collision. DONE, 2026-09-11.** `core/geom/3d/hash-grid/PointHashGrid3.js`, the
|
|
1701
|
+
particle-particle row (`solver/constraint/cloth_self_row.js`), the pair search
|
|
1702
|
+
(`solver/cloth_self_find.js`) with its exclusion set, and `ClothState`'s pair storage, Jacobi
|
|
1703
|
+
scratch and `self_radius`. Wired into `ClothSystem` behind `ClothDynamicsFlags.SelfCollision`,
|
|
1704
|
+
which three library entries now set. **M4b is not here** — point-triangle and edge-edge, per §9's
|
|
1705
|
+
own deferral — and §34 is the argument for re-pricing it before anyone builds it.
|
|
1706
|
+
*Proofs, all green:* the P4 tangle spec extended to self-intersection counts, as this line asks —
|
|
1707
|
+
a folding sheet crossing itself, and the row removing more than half of those crossings where the
|
|
1708
|
+
same sheet without it does not; the row's sign, and its Jacobi scratch being rebuilt rather than
|
|
1709
|
+
accumulated so a pair that separates stops pushing; the exclusion rule against a stretch row, a
|
|
1710
|
+
bend stencil and a pinned particle, and each pair found once with the lower index first; and the
|
|
1711
|
+
grid's counting sort, its 27-cell walk, its de-duplication across hash collisions and its
|
|
1712
|
+
independence from where in space the points sit; and the radius at both ends — a sheet's closest
|
|
1713
|
+
ungoverned pair being exactly two hops, strips measuring their gap rather than their link length, and
|
|
1714
|
+
a pinned pair not counting because no row will ever act on it. **22 tests across four new spec
|
|
1715
|
+
files** — the grid's 9, the search's 4, the row's 4, the radius's 5 — plus the library's
|
|
1716
|
+
self-collision assertion rewritten and `ClothState`'s buffer test extended. The engine suite is green
|
|
1717
|
+
at 18,296. The garment playground gains a self-collision toggle and a live pair count beside its
|
|
1718
|
+
contact count.
|
|
1719
|
+
|
|
1720
|
+
**M4 changed the design in four places**, each with the measurement behind it in `MEASUREMENTS.md`
|
|
1721
|
+
§30–37 and each folded back above. In one line each: §9's radius is the **proxy's** resolution and
|
|
1722
|
+
not the fabric's thickness, which is the difference between removing 21% of a sheet's
|
|
1723
|
+
self-intersections and removing 82%; §14.2's 3D hash grid is *not* modelled on the 2D one, because
|
|
1724
|
+
a cloth's particles are points without identity that all move every step, so the operation that
|
|
1725
|
+
matters is rebuild and insert/remove never runs; §9's list of which entries set the flag was drawn
|
|
1726
|
+
along what the garment is rather than how far the fabric bends, and leather — at the top of the
|
|
1727
|
+
bend scale — does not tangle at all and is made slightly *worse* by the row; and M4a's normal is
|
|
1728
|
+
the line between two particles rather than the surface orientation §9 promises, which satisfies
|
|
1729
|
+
what P4(a) is actually asking for and does it more simply.
|
|
1730
|
+
|
|
1731
|
+
**And one thing the design had no line for at all.** §9 never wrote a radius down, which is how
|
|
1732
|
+
that question stayed open long enough to be got wrong three times. `max(thickness, shortest_row/2)`
|
|
1733
|
+
has an upper end: a diameter reaching the closest pair *no row governs* puts a cloth which is not
|
|
1734
|
+
folded at all in permanent contact with itself. Leather at 20 mm on a 20 mm proxy is exactly that
|
|
1735
|
+
case and found 1,398 pairs a step on a 576-particle sheet that was barely folded (§36). And that
|
|
1736
|
+
distance has to be **measured** rather than assumed to be two hops, because two hops is a fact
|
|
1737
|
+
about sheets: the playground's cape has 8 cm links and an 8.5 cm gap between strips, half what the
|
|
1738
|
+
link length implies, and the assumed ceiling left it with three pairs permanently in contact at
|
|
1739
|
+
rest. `ClothInstance#closest_free_pair` is measured once at seed and the ceiling is two fifths of
|
|
1740
|
+
it. On a sheet the two rules agree exactly, so §31–36 stand. `MEASUREMENTS.md` §37.
|
|
1741
|
+
|
|
1742
|
+
**And the playground earned its keep.** Every measurement from §31 to §36 is on a sheet, because a
|
|
1743
|
+
sheet is the only thing here with a surface to pass through. Put the same row on the geometry M3
|
|
1744
|
+
actually ships and it does something no sheet could have shown: **without it a cape settles at half
|
|
1745
|
+
the width it was drawn at** — 0.178 m against a 0.340 m bind pose — because nothing holds five
|
|
1746
|
+
independent strips apart and gravity swings them all into the same place. With it, 0.351 m. That is
|
|
1747
|
+
not a transient tangle of the kind §32 measured; it is what the garment settles into.
|
|
1748
|
+
|
|
1749
|
+
**Deliberately not in M4:** **conditional storage for the pair list.** `ClothState` is one buffer
|
|
1750
|
+
sized at seed, and M4's three self-collision regions are 16% of it on the §17 workload — 100 KB a
|
|
1751
|
+
garment — whether or not the flag is ever set, which four of the seven library entries never do.
|
|
1752
|
+
Sizing them lazily would make `ClothDynamicsFlags.SelfCollision` a reseed trigger, which it is not
|
|
1753
|
+
today, and that is a change to the reseed contract rather than a tidy-up. `MEASUREMENTS.md` §33.
|
|
1754
|
+
M4b, above. A **swept pair search** — the set is discovered from the
|
|
1755
|
+
positions the last step left, with no margin for the travel of the step about to run, which is the
|
|
1756
|
+
shape of the gap §20 found in the contact set. §35 went looking for it and found the opposite:
|
|
1757
|
+
widening the search a hundred and fifty times over does not move the crossing count, so what is
|
|
1758
|
+
left is not a discovery problem but the tier's own stated limit, and it is M4b's. And **P4(d)'s
|
|
1759
|
+
bounded escape hatch**, which M2's entry above handed to M4 on the grounds that self-collision is
|
|
1760
|
+
what gives a residual monitor something to escape from — measured across ninety-six fold
|
|
1761
|
+
realisations, a self-tangled sheet unties itself every time the gust stops, so M4 still has no
|
|
1762
|
+
configuration to escape from. The case P4(d) names is a particle wedged between a collider *and* a
|
|
1763
|
+
fold, which is collision and self-collision together, and M4 has not measured it.
|
|
1764
|
+
|
|
1765
|
+
**M5 — the worker.** `WorkerClothSystem`, `ClothWorkerCore`, `cloth.worker.js`, the protocol.
|
|
1766
|
+
*Proofs:* bit-parity between `ClothSystem` and `WorkerClothSystem` as a standing gate; loopback specs
|
|
1767
|
+
covering step ordering, message ordering and shutdown; a measured main-thread cost reduction.
|
|
1768
|
+
|
|
1769
|
+
**M6 — wind.** Fluid-field sampling, aerodynamic drag, the ambient fallback.
|
|
1770
|
+
*Proofs:* a playground with a banner in a simulated wake; a measured cost showing the per-step sampling
|
|
1771
|
+
stays inside budget. Cuttable without affecting anything else.
|
|
1772
|
+
|
|
1773
|
+
**Performance budget — measured at M0, re-measured at M1, and it still does not hold on its own.** One
|
|
1774
|
+
character's full cloth complement — 1,024 particles at 2 substeps × 4 iterations, on a triangulated
|
|
1775
|
+
patch carrying 16.8 row incidences per particle — cost **3.50 ms per frame** at best at M0, against a
|
|
1776
|
+
2 ms target. **M1's rows made it 4.61 ms**: the strain limit is 0.73 ms (it doubles the stretch table,
|
|
1777
|
+
and the stretch rows are already 69% of the frame), the tether 0.36 ms, the slack row 0.10 ms. What
|
|
1778
|
+
that costs the plan is stated below.
|
|
1779
|
+
|
|
1780
|
+
The estimate was wrong in its reasoning as well as its number: **gathering neighbour positions is not
|
|
1781
|
+
what dominates.** The controlled test is to relabel the source vertices at random, which destroys the
|
|
1782
|
+
locality `cloth_color_order` provides and changes nothing else — same colouring, same rows, same
|
|
1783
|
+
arithmetic. It costs nothing measurable at the 1,024 particles §17 budgets for (the whole state fits in
|
|
1784
|
+
L2) and 11% at four times that size. So the *entire* memory-locality component of the gather is a few
|
|
1785
|
+
per cent of the frame, and a layout optimisation has at most 11% to win.
|
|
1786
|
+
|
|
1787
|
+
Deleting the workload piece by piece puts 69% of the frame in the stretch rows, 18% in the bend rows
|
|
1788
|
+
and 7% in everything else. That is a split of what each row kind *causes*, not of the arithmetic in
|
|
1789
|
+
its accumulate: the variants sit in different physical states, and an isolated loop over one accumulate
|
|
1790
|
+
measures a converged mesh rather than a moving one and disagrees with the deletion by a factor of five
|
|
1791
|
+
on the stretch row. `MEASUREMENTS.md` §7 carries the discrepancy rather than a story that resolves it.
|
|
1792
|
+
|
|
1793
|
+
The levers, priced at M0 (`MEASUREMENTS.md` §7) and again on M1's row set (§14):
|
|
1794
|
+
|
|
1795
|
+
| Lever | M0's rows | M1's rows |
|
|
1796
|
+
|---|---|---|
|
|
1797
|
+
| Fewer iterations — 1,024 at 2×**2** | **1.69 ms** inside | **2.38 ms** over |
|
|
1798
|
+
| Fewer substeps — 1,024 at 1×4 | **1.68 ms** inside | **2.40 ms** over |
|
|
1799
|
+
| Fewer particles — ~500 at 2×4 | **1.54 ms** inside | **2.53 ms** over |
|
|
1800
|
+
|
|
1801
|
+
**No single lever reaches 2 ms any more.** Each of them lands inside when a second is spent alongside
|
|
1802
|
+
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
|
|
1803
|
+
sizing decision rather than a wall, and it is one the library now makes explicitly: every garment entry
|
|
1804
|
+
spends two levers and says which two. The barycentric binding fallback from §11.2 is still not needed.
|
|
1805
|
+
|
|
1806
|
+
Every figure here is the best of four independent rounds, which agree to ±5%. Cost is close to linear
|
|
1807
|
+
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
|
|
1808
|
+
not make it cheaper, so "2 ms on the worker" with several characters in flight is a scheduling question
|
|
1809
|
+
this measurement does not answer.
|
|
1810
|
+
|
|
1811
|
+
**Re-measured at M2, and the library is not re-sized.** M2 adds a broadphase pass per particle per
|
|
1812
|
+
step, up to four contact rows per particle, and two field evaluations per contact per iteration. On the
|
|
1813
|
+
same workload: 4.43 ms with no collider system against M1's 4.61 — agreement to 4%, inside M1's own
|
|
1814
|
+
spread — 4.47 ms with a collider out of range, and 4.6 to 5.5 ms with one to twenty colliders touching.
|
|
1815
|
+
The column is not monotone in the collider count, because the variants sit in different physical states
|
|
1816
|
+
and a cloth draped over a sphere is not the same workload as one hanging free; §7's warning about
|
|
1817
|
+
attributing cost by deletion applies unchanged. What it licenses is that the collision layer costs
|
|
1818
|
+
between nothing and a quarter of a frame and does not scale with the collider count over the range a
|
|
1819
|
+
character occupies. Every library entry's own budget is untouched. `MEASUREMENTS.md` §23.
|
|
1820
|
+
|
|
1821
|
+
Re-measure at M4 and M5.
|
|
1822
|
+
|
|
1823
|
+
---
|
|
1824
|
+
|
|
1825
|
+
## 18. Risks and open questions
|
|
1826
|
+
|
|
1827
|
+
**AVBD has a short track record for cloth. — MEASURED, and the risk was real.** VBD's paper covers
|
|
1828
|
+
cloth; AVBD's public reference implementations are 2D and rigid-body-focused. The augmented Lagrangian
|
|
1829
|
+
layer is well understood in the abstract, but its interaction with cloth's particular constraint mix —
|
|
1830
|
+
many soft rows, a few hard ones, transient contacts — is not something anyone has published numbers on.
|
|
1831
|
+
M0 found exactly that interaction to be where the design was wrong: **the multiplier must not touch a
|
|
1832
|
+
soft row at all** (it drove a 1 kN/m spring 100× stiffer than Hooke), and **the penalty band is only
|
|
1833
|
+
meaningful relative to `m/h²`**, which is a cloth-specific statement because cloth's mass term is four
|
|
1834
|
+
orders of magnitude below a rigid body's. Both are fixed and measured (§5.2, `MEASUREMENTS.md` §3–4).
|
|
1835
|
+
The core is now stable across 10,000 steps of deliberately absurd cloth, settles a hard chain to 1e-14 J
|
|
1836
|
+
at 80 links deep, and reproduces Hooke exactly on a soft row. The §6 row abstraction still means the
|
|
1837
|
+
descent loop could be replaced with substepped XPBD without touching the constraint definitions, and
|
|
1838
|
+
that fallback still costs one module — but nothing measured so far argues for taking it.
|
|
1839
|
+
|
|
1840
|
+
**Per-vertex Hessian assembly in JavaScript may not hit the budget. — MEASURED, and it does not.**
|
|
1841
|
+
3.50 ms against a 2 ms target for 1,024 particles at 2×4. *Resolved by:* the plan's own first lever —
|
|
1842
|
+
half the iteration count, or half the particles, lands inside — so this is a sizing decision for M1's
|
|
1843
|
+
library and M3's proxy rather than an open risk. What is *not* resolved is that the cost is
|
|
1844
|
+
concentrated in the stretch row and only a third of that is the gather, so the obvious memory-layout
|
|
1845
|
+
optimisation is aimed at the smaller half. `MEASUREMENTS.md` §7.
|
|
1846
|
+
|
|
1847
|
+
**System ordering. — DISSOLVED rather than resolved.** §4.1 carries the reasoning; in short, the
|
|
1848
|
+
derived score is a *sum* over what a system references, so touching more can only move a system
|
|
1849
|
+
**earlier**, and a consumer of a posed hierarchy touches strictly more than the poser. There is no
|
|
1850
|
+
declaration cloth could have made, and registration order does not break a non-tie. The engine has two
|
|
1851
|
+
ordering mechanisms and neither expresses this.
|
|
1852
|
+
|
|
1853
|
+
The answer is not a third mechanism, it is not needing one: cloth writes each node's **local**
|
|
1854
|
+
transform alongside its world one, so a hierarchy recomposition reproduces the simulated pose rather
|
|
1855
|
+
than replacing it — measured at 1.7e-15 m — and the placement stops being a correctness question.
|
|
1856
|
+
*What is left, and it is a real cost:* running before the poser means the cloth's shape is one fixed
|
|
1857
|
+
step stale, which measures at a few centimetres on a 1.6 m rope under ordinary motion and up to a
|
|
1858
|
+
fifth of its own length at the sharpest reversal of a ±9 m/s square wave. It does not accumulate.
|
|
1859
|
+
An application that cares registers accordingly and gets nothing, because this is not a tie; an
|
|
1860
|
+
application that does not care is unaffected, which is the common case and the one this trades for.
|
|
1861
|
+
`MEASUREMENTS.md` §15.
|
|
1862
|
+
|
|
1863
|
+
**The unrigged path's joint budget. — MEASURED, and §11.2's guess was low.** A rigged garment costs
|
|
1864
|
+
nothing new; an unrigged one needs a generated joint set, and how many joints a drape takes through a
|
|
1865
|
+
four-weight palette was an open empirical question. M3 measured it: a 33×33 cape simulated at full
|
|
1866
|
+
resolution, reconstructed by linear blend skinning from k-by-k fitted joints, error against the
|
|
1867
|
+
reference. **RMS error is inversely proportional to the joint count** — the product is flat across a
|
|
1868
|
+
factor of 72 in joints — and the worst case falls as about `N^−0.83`. At §11.2's "~60 output joints"
|
|
1869
|
+
the worst-case error is 5% of the garment's own size, which is five grid cells and reads as faceting;
|
|
1870
|
+
**80 to 120 joints** puts it inside one. A crumpled cloth needs about three times as many, so the
|
|
1871
|
+
number is per-fabric rather than per-garment. `MEASUREMENTS.md` §28. The barycentric deformation pass
|
|
1872
|
+
(§11.2) remains the designed fallback.
|
|
1873
|
+
|
|
1874
|
+
**Per-joint frames. — MEASURED, and the degeneracy did not happen.** A particle has a position; a
|
|
1875
|
+
joint needs an orientation, fitted from a neighbourhood every frame, and §18 expected "a fold that
|
|
1876
|
+
collapses a triangle fan" to have no well-defined frame. Across 800 frames × up to 121 joints,
|
|
1877
|
+
including a deliberately crumpled variant with bending at 1e-7 and a gust four times as hard, the
|
|
1878
|
+
count of degenerate fits is **zero**. The mechanism is that a tangent is a difference across two
|
|
1879
|
+
links and the stretch rows are hard, so each link holds its length and the only way the difference
|
|
1880
|
+
vanishes is a fold of exactly 180° at the joint's own vertex. That is possible rather than forbidden,
|
|
1881
|
+
so a future unrigged path keeps the epsilon and the fallback; it is no longer the thing to be most
|
|
1882
|
+
worried about. `MEASUREMENTS.md` §28.
|
|
1883
|
+
|
|
1884
|
+
**Quadratic bending cannot hold a curved rest shape.** A garment authored with a strong permanent curve
|
|
1885
|
+
will relax flatter than intended. M0 measured the exact boundary: the stencil annihilates the rest shape
|
|
1886
|
+
and **every affine image of it** — which is every in-plane deformation to first order, so a flat sheet
|
|
1887
|
+
being sheared, stretched or rotated stays at zero bending energy — but it does *not* annihilate an
|
|
1888
|
+
arbitrary planar configuration. Four coplanar particles that are not an affine image of the rest four
|
|
1889
|
+
read as bent. *Mitigation:* accepted for the fabrics the library targets. If a case appears, the fix is
|
|
1890
|
+
a rest-curvature offset in the stencil, which is a change to one row and not to the solver.
|
|
1891
|
+
|
|
1892
|
+
**Swept collision is translation-first, and it is two mechanisms rather than one. — MEASURED.** P3
|
|
1893
|
+
states the limit. M2 found that the half nobody had written down — sweeping the contact **set**, not
|
|
1894
|
+
just the poses — was the half that mattered: without it a capsule at 20 m/s created zero rows and the
|
|
1895
|
+
sheet was never touched. With the collider's travel folded into the discovery reach, 2 to 40 m/s all
|
|
1896
|
+
behave. What remains is that both halves are built from the collider's *centre*: a collider spinning
|
|
1897
|
+
fast enough that a point on its surface travels more than its own radius within one substep can still
|
|
1898
|
+
be missed. *Mitigation:* more substeps in the affected `ClothDynamics`; genuine CCD stays out of scope.
|
|
1899
|
+
The shape of a fix, if one is ever wanted, is to build the discovery margin from the swept **surface**
|
|
1900
|
+
travel — `|Δt| + θ·R` — which is one more term and no new structure. `MEASUREMENTS.md` §20.
|
|
1901
|
+
|
|
1902
|
+
**Open — a rigid collider driven through pinned cloth wraps rather than parts.** A contact row bounds
|
|
1903
|
+
the interpenetration; it cannot prevent it, because the cloth has nowhere to go. The playground's
|
|
1904
|
+
spinning-bar scene is the case, and it settles at 18 mm inside a 25 mm bar. This is not a solver
|
|
1905
|
+
failure — a kinematic collider is infinitely strong by construction and the cloth is inextensible and
|
|
1906
|
+
pinned — but it is the configuration a garment author will find first, and the answer is authoring
|
|
1907
|
+
(a collider that does not sweep through the garment's own plane) rather than a solver mechanism.
|
|
1908
|
+
|
|
1909
|
+
**A soft-row cloth does not come to rest. — CLOSED by M1's damping, and the size is measured.** M0
|
|
1910
|
+
measured a soft chain landing in a bounded limit cycle that 2,000 further steps do not move, and no
|
|
1911
|
+
solver constant reaches it, because `α·γ` decays a multiplier and a soft row has none. `damping` does
|
|
1912
|
+
reach it: on the configuration §6 measured, 2.90e-3 J undamped against 1.28e-6 J at the library's own
|
|
1913
|
+
`damping = 0.12`, and 1.82e-12 J at the top of the range. `cloth_pathology.spec.js` gates on exactly
|
|
1914
|
+
that pair.
|
|
1915
|
+
*What this does not claim:* damping does not stop the step adding energy, it removes energy faster than
|
|
1916
|
+
the step adds it. The equilibrium is bounded rather than zero — which is why the numbers keep falling
|
|
1917
|
+
all the way to `damping = 1` instead of hitting a floor — and a cloth whose rows are soft and far above
|
|
1918
|
+
the dissipative band is still a cloth being rescued rather than a cloth behaving. M1's mapping does not
|
|
1919
|
+
produce one; the gate exists so a future mapping that does is caught. `MEASUREMENTS.md` §12.
|
|
1920
|
+
|
|
1921
|
+
**The escape hatch could mask authoring errors.** P4(d) rescues a stuck particle, which means a badly
|
|
1922
|
+
authored garment can look acceptable while relying on it. *Mitigation:* `ClothDiagnostics` counts
|
|
1923
|
+
escape-hatch activations per cloth and surfaces them, so "this garment is being rescued 30 times a
|
|
1924
|
+
second" is visible rather than invisible.
|
|
1925
|
+
|
|
1926
|
+
**Open — two-way coupling.** Cloth does not push rigid bodies. The row model could carry it (the
|
|
1927
|
+
collision row's multiplier is a force, and applying its negation to the collider's body is mechanically
|
|
1928
|
+
straightforward), but the stability of that loop across two solvers with different time discretisations
|
|
1929
|
+
is a real question and is not answered here.
|
|
1930
|
+
|
|
1931
|
+
**Open — the fixed set for an unskinned surface, and M3 found there are three rules rather than
|
|
1932
|
+
two.** Step 5 of §11.3 derives the fixed set from skin weights, which is right for a garment on a
|
|
1933
|
+
skeleton. A **rigged** garment does not need weights at all: the topology answers, and a joint whose
|
|
1934
|
+
parent is outside the proxy is the fixed set (§11.3, corrected). What is still open is the third
|
|
1935
|
+
case — a flag, a curtain, a banner that is not skinned at all — where there is neither a joint tree
|
|
1936
|
+
nor a weight to read. Those also sit at `slack ≈ 1` by necessity, so the tether carries all the
|
|
1937
|
+
inextensibility, but the fixed set still has to come from the mesh boundary or from authoring, and
|
|
1938
|
+
neither is built. It belongs with the unrigged path, which is where a mesh first becomes a particle
|
|
1939
|
+
set.
|
|
1940
|
+
|
|
1941
|
+
**Open — LOD.** Distance-based particle-count reduction would need either multiple baked proxies per
|
|
1942
|
+
garment or a runtime hierarchy. Culling (§12.1) covers the off-screen case; the far-but-visible case is
|
|
1943
|
+
unaddressed.
|
|
1944
|
+
|
|
1945
|
+
**Open — `stretch` is a weak control, and it is not obvious what would make it a strong one.** M1
|
|
1946
|
+
measured the strain-limit row removing about a third of the peak over-stretch and never reaching its
|
|
1947
|
+
own allowance, because it is a second hard row competing with the first for the same trust region and
|
|
1948
|
+
the same penalty band (§6). A fabric's *extension* is therefore governed by the iteration budget more
|
|
1949
|
+
than by the fabric, and the library says so by which entries build the row at all. Three things might
|
|
1950
|
+
change that and none is free: a soft fabric row inside the dissipative band with the hard row acting
|
|
1951
|
+
only as the limit (which reintroduces M0's settling problem at any realistic stiffness); a per-row
|
|
1952
|
+
penalty band, making `k_max/(m/h²)` a material property rather than a global (which `MEASUREMENTS.md`
|
|
1953
|
+
§4 shows trades stretchiness against settling); or accepting that near-inextensibility is what fabric
|
|
1954
|
+
*is* and demoting `stretch` to a documented weak knob. M2's contacts do not bear on it either way.
|
|
1955
|
+
|
|
1956
|
+
**Open — the write-back has no interpolation.** §12 gives cloth `PhysicsSystem`'s producer /
|
|
1957
|
+
`__interp_restore` contract, and M1 does not wire it: cloth writes `Transform64` at the fixed rate and
|
|
1958
|
+
nothing blends between ticks, so a cloth updates at 60 Hz on a 165 Hz display. That is visible and it
|
|
1959
|
+
is not wrong — the poses are authoritative, they are simply held. It lands with M5, where the worker
|
|
1960
|
+
makes the fixed-step producer contract load-bearing for a second reason.
|
|
1961
|
+
|
|
1962
|
+
**Open — the subtree is walked once.** A rope's links do not come and go, so `cloth_seed_subtree` runs
|
|
1963
|
+
on first step and `ClothSystem#reseed` is how a caller says the structure changed. That is the same
|
|
1964
|
+
contract `TransformAttachment#parent` already carries, and it is the right default; what it does not
|
|
1965
|
+
have is a way to *notice*. An entity added under a live `Cloth` is silently not simulated until
|
|
1966
|
+
someone re-seeds, which is a class of authoring mistake with no diagnostic behind it. `ClothDiagnostics`
|
|
1967
|
+
is where one would go.
|