From 90af5e386a5ca6eb2bad0bb1755b831e41fad6d9 Mon Sep 17 00:00:00 2001 From: yuecideng Date: Fri, 18 Sep 2026 23:27:58 +0800 Subject: [PATCH] fix(sim): restore VisACD selection through Spawn --- embodichain/lab/sim/shapes.py | 6 +++- embodichain/lab/sim/spawn/descriptors.py | 16 +++++++--- tests/sim/spawn/test_descriptors.py | 39 ++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/embodichain/lab/sim/shapes.py b/embodichain/lab/sim/shapes.py index 9a5ffb1bf..96305ef67 100755 --- a/embodichain/lab/sim/shapes.py +++ b/embodichain/lab/sim/shapes.py @@ -65,7 +65,11 @@ class MeshCollisionCfg: """Maximum hull count for ``convex_decomposition``; must be at least two.""" acd_method: Literal["visacd", "coacd", "vhacd"] | None = None - """Approximate-convex-decomposition implementation.""" + """Approximate-convex-decomposition implementation. + + ``None`` selects ``visacd`` when compiling mesh collision geometry. + VisACD requires a DexSim build with CUDA/OptiX support. + """ sdf_resolution: int | None = None """Maximum SDF grid resolution; valid only for the ``sdf`` strategy.""" diff --git a/embodichain/lab/sim/spawn/descriptors.py b/embodichain/lab/sim/spawn/descriptors.py index beba18ddb..1d3d7e351 100644 --- a/embodichain/lab/sim/spawn/descriptors.py +++ b/embodichain/lab/sim/spawn/descriptors.py @@ -302,7 +302,7 @@ def rigid_desc_from_cfg( newton_solver_type=newton_solver_type, ) ) - geometry, approximation, max_hulls = _compile_geometry(cfg) + geometry, approximation, max_hulls, acd_method = _compile_geometry(cfg) material_ref, material_entry = _compile_visual_material( uid, cfg.shape.visual_material ) @@ -310,6 +310,10 @@ def rigid_desc_from_cfg( geometry, approximation=approximation, ) + if approximation == CollisionApproximation.CONVEX_DECOMPOSITION: + # Construct the declared field rather than attaching a dynamic attribute + # that older DexSim versions would silently ignore during cooking. + collision = replace(collision, decomp_algorithm=acd_method) collision.enable_collision = physics.collision_enabled collision.decomp_max_hulls = max_hulls collision.dexsim = _compile_default_collision(physics) @@ -1563,7 +1567,7 @@ def _compile_newton_collision( def _compile_geometry( cfg: RigidObjectCfg, -) -> tuple[GeometryDesc, CollisionApproximation, int]: +) -> tuple[GeometryDesc, CollisionApproximation, int, str]: shape = cfg.shape if isinstance(shape, MeshCfg): geometry = _mesh_geometry_from_cfg(shape, segment_name=cfg.uid or "mesh") @@ -1586,10 +1590,10 @@ def _compile_geometry( # RigidObject after Spawn has created the file-backed mesh. if ( collision_cfg.approximation == "convex_decomposition" - and acd_method not in ("visacd", "coacd") + and acd_method not in ("visacd", "coacd", "vhacd") ): raise ValueError( - "Spawn supports only acd_method='visacd' or 'coacd' " + "Spawn supports only acd_method='visacd', 'coacd', or 'vhacd' " "for convex_decomposition." ) if collision_cfg.sdf_resolution is not None: @@ -1602,13 +1606,14 @@ def _compile_geometry( geometry, approximation, max(1, max_hulls), + acd_method, ) if isinstance(shape, CubeCfg): size = tuple(float(value) for value in shape.size) if len(size) != 3 or any(value <= 0 for value in size): raise ValueError("CubeCfg.size must contain three positive values.") - return GeometryDesc.cube(size), CollisionApproximation.NONE, 1 + return GeometryDesc.cube(size), CollisionApproximation.NONE, 1, "coacd" if isinstance(shape, SphereCfg): if shape.radius <= 0: @@ -1617,6 +1622,7 @@ def _compile_geometry( GeometryDesc.sphere(float(shape.radius)), CollisionApproximation.NONE, 1, + "coacd", ) raise NotImplementedError( diff --git a/tests/sim/spawn/test_descriptors.py b/tests/sim/spawn/test_descriptors.py index c10564988..d9f816500 100644 --- a/tests/sim/spawn/test_descriptors.py +++ b/tests/sim/spawn/test_descriptors.py @@ -1009,6 +1009,40 @@ def test_dynamic_triangle_mesh_collision_is_rejected_before_spawn() -> None: rigid_desc_from_cfg(cfg) +@pytest.mark.parametrize( + ("method", "expected"), + [(None, "visacd"), ("visacd", "visacd"), ("coacd", "coacd"), ("vhacd", "vhacd")], +) +def test_spawn_forwards_convex_decomposition_algorithm( + method: str | None, expected: str +) -> None: + cfg = RigidObjectCfg.from_dict( + { + "uid": "mesh", + "shape": { + "shape_type": "Mesh", + "fpath": "mesh.glb", + "collision": { + "approximation": "convex_decomposition", + "max_hulls": 8, + "acd_method": method, + }, + }, + } + ) + # Isolate geometry routing from the separate body mass/inertia contract. + with patch( + "embodichain.lab.sim.spawn.descriptors._compile_rigid_physics", + return_value=RigidBodyPhysicsDesc.dynamic(), + ): + descriptor, _ = rigid_desc_from_cfg(cfg) + + collision = descriptor.collisions[0] + assert collision.approximation == CollisionApproximation.CONVEX_DECOMPOSITION + assert collision.decomp_max_hulls == 8 + assert collision.decomp_algorithm == expected + + def test_spawn_rejects_unsupported_convex_decomposition_method() -> None: cfg = RigidObjectCfg( uid="mesh", @@ -1017,12 +1051,13 @@ def test_spawn_rejects_unsupported_convex_decomposition_method() -> None: collision=MeshCollisionCfg( approximation="convex_decomposition", max_hulls=4, - acd_method="vhacd", + acd_method="coacd", ), ), ) - with pytest.raises(ValueError, match="acd_method='visacd' or 'coacd'"): + cfg.shape.collision.acd_method = "invalid" + with pytest.raises(ValueError, match="acd_method"): rigid_desc_from_cfg(cfg)