Franka finger joint detaches from hand during grasping

Hi, I’m encountering an intermittent joint separation issue when simulating grasping with the default Franka robot in Isaac Sim 4.5 (and the same issue also exists in Isaac Sim 4.1).

  • I’m using the default Franka asset, with no modifications to the articulation structure.
  • The issue occurs between the finger joint and the hand, particularly during contact with objects.
  • CCD is enabled globally, and each object (converted from Objaverse assets via asset_converter.py) also has CCD enabled.
  • All colliders are set to Convex Decomposition, with Hull Vertex Limit = 64, Max Convex Hulls = 64, and Shrink Wrap enabled.
  • The mass of objects is set to 0.1.
  • Using ArticulationView set_joint_position_targets to control Franka.
  • During grasping, the finger visually drifts away from the hand, as shown in the attached video:

This doesn’t happen every time — it occurs randomly during interaction.


What I’ve tried:

  • Removed the Mimic Joint from /World/franka/panda_hand/panda_finger_joint2
  • Set Break Force of both finger joints to inf
  • Set Max Force in joint drive to 72000
  • Set Damping / Stiffness to 1000 and 10000 respectively
  • Tweaked solver iteration counts (position & velocity) from 1 to 256 on both articulation root and finger rigid bodies
  • Enabled Self Collisions for Franka
  • Set Rest Offset = 0.0 and Contact Offset = 0.02 for both finger colliders
  • Reduced Max Force of all joints (from 100000 to 100)
  • Changed collision approximation of both fingers to Convex Hull
  • Adjusted Voxel Resolution of both finger colliders
  • Disabled Shrink Wrap on attached object
  • Disabled CCD on attached object

Additional Notes:

Many of the imported Objaverse assets produce warnings like:

ConvexDecompositionTask: failed to cook GPU-compatible mesh, collision detection will fall back to CPU. Collisions with particles and deformables will not work with this mesh.

This happens frequently.


Physics Scene Settings:

def setup_physics_scene():
    stage = omni.usd.get_context().get_stage()
    physxSceneAPI = PhysxSchema.PhysxSceneAPI.Get(stage, "/physicsScene")
    physxSceneAPI.GetEnableGPUDynamicsAttr().Set(False)
    physxSceneAPI.GetEnableStabilizationAttr().Set(True)
    physxSceneAPI.GetEnableCCDAttr().Set(True)
    physxSceneAPI.GetBroadphaseTypeAttr().Set("GPU")
    physxSceneAPI.GetSolverTypeAttr().Set("TGS")
    physxSceneAPI.GetGpuTotalAggregatePairsCapacityAttr().Set(10 * 1024 * 1024)
    physxSceneAPI.GetGpuFoundLostAggregatePairsCapacityAttr().Set(10 * 1024 * 1024)

My Question:

In my understanding, the physical consistency of articulation joints should be strongly enforced in PhysX, especially with default assets and conservative parameters.

  • Why does this joint separation still occur?
  • Is there anything else I should try adjusting?

Any suggestions would be greatly appreciated!

Thanks in advance!

@axihelloworld thank you for posting your issue. It may be related to a known issue in Isaac Sim 4.5. Would you be able to share the code to reproduce the problem so I can help you debug?

Thank you very much for your reply. Our code includes many dependencies—for example, the grasp proposal relies on external services—so it may not be possible to provide a fully runnable version. However, I can describe the issue accurately. Specifically, we are using the default Franka asset provided by Isaac Sim and a mesh from Objaverse. This mesh appears to have certain convex hull characteristics.

The separation of the Franka gripper generally occurs during interpenetration scenarios. In our case, Franka is created directly using Franka(), and solver iterations and self-collision settings are configured afterward (as described above). One detail worth noting is how we set up colliders for the mesh objects: all collider parameters are applied to the top-level XFormPrim rather than to each individual mesh.

Here’s how we handle object addition:

def set_colliders(prim_path, collision_approximation="convexDecomposition", convex_hulls = None):
    remove_colliders(prim_path)
    prim = get_prim_at_path(prim_path)
    collider = UsdPhysics.CollisionAPI.Apply(prim)
    mesh_collider = UsdPhysics.MeshCollisionAPI.Apply(prim)
    mesh_collider.CreateApproximationAttr().Set(collision_approximation)
    collider.GetCollisionEnabledAttr().Set(True)
    if collision_approximation == "convexDecomposition":
        collision_api = PhysxSchema.PhysxConvexDecompositionCollisionAPI.Apply(prim)
        collision_api.CreateHullVertexLimitAttr().Set(64)
        if convex_hulls is not None:
            collision_api.CreateMaxConvexHullsAttr().Set(convex_hulls)
        else:
            collision_api.CreateMaxConvexHullsAttr().Set(64)
        collision_api.CreateMinThicknessAttr().Set(0.1)
        collision_api.CreateShrinkWrapAttr().Set(True)
        collision_api.CreateErrorPercentageAttr().Set(0.1)
        # collision_api = PhysxSchema.PhysxCollisionAPI.Apply(prim)
        # collision_api.CreateContactOffsetAttr().Set(0.1)

By the way, the physics dt of our code is 1/60.

I’m afraid this issue is not limited to Isaac Sim 4.5. In fact, many of the aforementioned attempts were initially conducted on Isaac Sim 4.1, and we also did some brief testing on version 4.2. The problem persists across all these versions.

This is our code of trying to grasp difference objects, which contains many dependencies and therefore cannot run directly. Most of the code is focused on adding and placing objects, followed by computing the grasp pose, etc. In the final part, we use set_joint_position_targets to control the Franka robot.

import os
import random
import sys
import json
import numpy as np
import argparse

parser = argparse.ArgumentParser()
args = parser.parse_args()

current_dir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(current_dir)

from isaacsim import SimulationApp

simulation_app = SimulationApp({"headless": False})  # False

from genmanip.core.loading.loading import (
    relate_franka_from_data,
    load_world_xform_prim,
    get_object_list,
    create_camera_list,
)
from genmanip.core.pointcloud.pointcloud import (
    get_mesh_info,
    get_mesh_info_by_load,
    get_current_meshList,
    objectList2meshList,
)
from genmanip.core.random_place.random_place import place_object_to_object_by_relation
from genmanip.core.robot.franka import get_franka_PD_controller
from genmanip.core.sensor.camera import set_camera_look_at, get_src
from genmanip.core.usd_utils.prim_utils import (
    add_usd_to_world,
    resize_object,
    set_colliders,
    resize_object_by_lwh,
)
from genmanip.demogen.planning.planning import prepare_grasp_motion_planning_payload
from genmanip.thirdparty.anygrasp import get_init_grasp
from genmanip.thirdparty.curobo_planner import CuroboFrankaPlanner
from genmanip.utils.file_utils import load_yaml, load_default_config
from genmanip.utils.pc_utils import compute_mesh_bbox, compute_aabb_lwh
from genmanip.utils.utils import setup_logger
from omni.isaac.core.utils.prims import delete_prim  # type: ignore
from omni.isaac.core import World  # type: ignore
import numpy as np
import random
from tqdm import tqdm
from filelock import SoftFileLock
from pathlib import Path
import json


simulation_app._carb_settings.set("/physics/cooking/ujitsoCollisionCooking", False)

logger = setup_logger()
default_config = load_default_config(
    current_dir=current_dir, config_name="__None__.json", anygrasp_mode="local"
)
ASSETS_DIR = default_config["ASSETS_DIR"]
TEST_USD_NAME = default_config["TEST_USD_NAME"]
TABLE_UID = "aa49db8a801d402dac6cf1579536502c"
camera_data = load_yaml("configs/cameras/fixed_camera.yml")
world = World()
scene_xform, uuid = load_world_xform_prim(
    os.path.join(ASSETS_DIR, "scene_usds/base_scenes/base.usda")
)
print(uuid)
camera_list = create_camera_list(camera_data, uuid)
franka_list = [relate_franka_from_data(uuid)]
for franka in franka_list:
    world.scene.add(franka)
object_list = get_object_list(uuid, scene_xform, TABLE_UID)
meshDict = objectList2meshList(object_list)
for obj in object_list.values():
    set_colliders(obj.prim_path, "convexDecomposition")
world.reset()
while (
    get_src(camera_list["obs_camera"], "depth") is None
    or get_src(camera_list["realsense"], "depth") is None
    or get_src(camera_list["camera1"], "depth") is None
):
    world.step()
franka_cfg = load_yaml("assets/robots/configs/franka.yml")
planner = CuroboFrankaPlanner(franka_cfg, world, franka_list[0].prim_path)
default_joint_positions = franka_list[0].get_joint_positions()
from object_utils.object_pool import ObjectPool

usd_list = os.listdir(os.path.join(ASSETS_DIR, "object_usds/objaverse_usd/"))
print(len(usd_list))
usd_list = [usd_path for usd_path in usd_list if usd_path.endswith(".usd")]
usd_list.sort()
print(len(usd_list))
object_pool = ObjectPool(os.path.join(current_dir, "assets/objects/object_v7.pickle"))
result_dir = Path(os.path.join(current_dir, "saved/filtered_objects"))
result_dir.mkdir(parents=True, exist_ok=True)
log_lock = SoftFileLock(str(result_dir / "log.lock"))
log_file = result_dir / "log.json"
with log_lock:
    if not log_file.exists():
        with open(log_file, "w") as f:
            json.dump({}, f)
for usd_path in usd_list:
    uid = usd_path.split(".")[0]
    try:
        process_lock = SoftFileLock(str(result_dir / f"{uid}.lock"), timeout=0)
        with process_lock:
            with log_lock:
                with open(log_file, "r") as f:
                    log_data = json.load(f)
            if uid in log_data:
                continue
            usd_path = os.path.join(ASSETS_DIR, "object_usds/objaverse_usd/", usd_path)
            object_list[uid] = add_usd_to_world(
                asset_path=usd_path,
                prim_path=f"/World/{uuid}/obj_{uid}",
                name=f"obj_{uid}",
                translation=None,
                orientation=[0.5, 0.5, 0.5, 0.5],
                scale=[0.01, 0.01, 0.01],  # [1.0, 1.0, 1.0],
                add_rigid_body=True,
                add_colliders=True,
                collision_approximation="convexDecomposition",
            )
            object_info = object_pool.get_object_info(uid)
            print(f"Finish adding object {uid}")
            meshDict = objectList2meshList(
                object_list,
                os.path.join(
                    default_config["ASSETS_DIR"],
                    "mesh_data",
                    "grasp_detection",
                ),
            )
            meshlist = get_current_meshList(object_list, meshDict)
            resize_object(
                object_list[uid],
                random.uniform(
                    object_info["scale"][0],
                    object_info["scale"][1],
                ),
                meshlist[uid],
            )
            meshDict[uid] = get_mesh_info_by_load(
                object_list[uid],
                os.path.join(
                    default_config["ASSETS_DIR"],
                    "mesh_data",
                    "grasp_detection",
                    f"{uid}.obj",
                ),
            )
            franka_list[0].set_joint_positions(default_joint_positions)
            meshlist = get_current_meshList(object_list, meshDict)
            min_thickness = 0.1
            aabb = compute_mesh_bbox(meshlist[uid])
            if np.min(compute_aabb_lwh(aabb)) > min_thickness:
                l, w, h = compute_aabb_lwh(aabb)
                min_thickness_ratio = min_thickness / np.min([l, w])
                min_thickness_ratio = max(
                    min_thickness_ratio, min_thickness / np.min([l, w])
                )
                l *= min_thickness_ratio
                w *= min_thickness_ratio
                h *= min_thickness_ratio
                resize_object_by_lwh(
                    object_list[uid],
                    l=l,
                    w=w,
                    h=h,
                    mesh=meshlist[uid],
                )
                meshDict[uid] = get_mesh_info_by_load(
                    object_list[uid],
                    os.path.join(
                        default_config["ASSETS_DIR"],
                        "mesh_data",
                        "grasp_detection",
                        f"{uid}.obj",
                    ),
                )
            IS_OK = place_object_to_object_by_relation(
                uid,
                "00000000000000000000000000000000",
                object_list,
                meshDict,
                "on",
                platform_uid="00000000000000000000000000000000",
            )
            meshlist = get_current_meshList(object_list, meshDict)
            aabb = compute_mesh_bbox(meshlist[uid])
            center = aabb.get_max_bound() + aabb.get_max_bound()
            center = center / 2
            target_center = [0.0, 0.0]
            position = object_list[uid].get_world_pose()[0]
            tar_position = position
            tar_position[:2] = center[:2] - target_center - position[:2]
            object_list[uid].set_world_pose(position=tar_position)
            set_camera_look_at(
                camera_list["camera1"],
                object_list[uid],
                azimuth=180.0,
            )
            for _ in tqdm(range(15)):
                world.step(render=True)
            meshlist = get_current_meshList(object_list, meshDict)
            try:
                init_grasp = get_init_grasp(
                    camera_list["camera1"],
                    meshlist[uid],
                    address=default_config["ANYGRASP_ADDR"],
                    port=default_config["ANYGRASP_PORT"],
                )
            except Exception as e:
                print(e)
                delete_prim(f"/World/{uuid}/obj_{uid}")
                object_list.pop(uid)
                meshDict.pop(uid)
                continue
            action_list = prepare_grasp_motion_planning_payload(init_grasp, steps=30)
            world = World()
            franka_view = get_franka_PD_controller(franka_list[0], [2.0] * 9)
            paths = []
            object_position = object_list[uid].get_world_pose()[0]
            for idx, target in tqdm(enumerate(action_list)):
                position = target["translation"]
                franka_p, _ = franka_list[0].get_world_pose()
                position = position - franka_p
                orientation = target["orientation"]
                results = planner.plan(
                    position,
                    orientation,
                    franka_list[0].get_joints_state(),
                )
                actions = []
                if results is not None:
                    for res in results:
                        if target["grasp"]:
                            actions.append(np.concatenate([res, [0.00, 0.00]]).tolist())
                        else:
                            actions.append(np.concatenate([res, [0.04, 0.04]]).tolist())
                    if idx == 1:
                        for _ in range(10):
                            actions.append(
                                np.concatenate([actions[-1][:7], [0.0, 0.0]]).tolist()
                            )
                while actions:
                    action = actions.pop(0)
                    franka_view.set_joint_position_targets(action)
                    world.step(render=True)
            after_object_position = object_list[uid].get_world_pose()[0]
            is_success = False
            if (
                after_object_position[2] - object_position[2] > 0.1
                and abs(after_object_position[0] - object_position[0]) < 0.2
                and abs(after_object_position[1] - object_position[1]) < 0.2
            ):
                is_success = True
                print("Success")
            else:
                print("Failed")
            with log_lock:
                with open(log_file, "r") as f:
                    log_data = json.load(f)
                log_data[uid] = is_success
                with open(log_file, "w") as f:
                    json.dump(log_data, f)
            delete_prim(f"/World/{uuid}/obj_{uid}")
            object_list.pop(uid)
            meshDict.pop(uid)
    except Exception as e:
        if uid in object_list and uid in meshDict:
            delete_prim(f"/World/{uuid}/obj_{uid}")
            object_list.pop(uid)
            meshDict.pop(uid)
        print(e)
        continue
simulation_app.close()

We add object by:

def add_usd_to_world(
    asset_path: str,
    prim_path: str,
    name: str,
    translation: Optional[Sequence[float]] = None,
    orientation: Optional[Sequence[float]] = None,
    scale: Optional[Sequence[float]] = None,
    add_rigid_body: bool = False,
    add_colliders: bool = False,
    collision_approximation="convexDecomposition",
):
    reference = add_reference_to_stage(usd_path=asset_path, prim_path=prim_path)
    prim_path = str(reference.GetPrimPath())
    prim = XFormPrim(
        prim_path,
        name=name,
        translation=translation,
        orientation=orientation,
        scale=scale,
    )
    usd_prim = prim.prim
    if not usd_prim.IsValid():
        print(f"Prim at path {prim_path} is not valid.")
        return prim
    if add_rigid_body:
        set_rigid_body(prim_path)
        print(f"RigidBodyAPI applied to {prim_path}")
    if add_colliders:
        set_colliders(prim_path, collision_approximation)
        print(f"CollisionAPI applied to {prim_path}")


def set_rigid_body(prim_path):
    prim = get_prim_at_path(prim_path)
    rigid_body_api = UsdPhysics.RigidBodyAPI.Apply(prim)
    set_rigid_body_CCD(prim_path, True)
    return prim


def set_rigid_body_CCD(prim_path, ccd_enabled):
    prim = get_prim_at_path(prim_path)
    rigid_body = PhysxSchema.PhysxRigidBodyAPI.Apply(prim)
    rigid_body.CreateEnableCCDAttr().Set(ccd_enabled)
    return prim

def remove_colliders(prim_path):
    prim = get_prim_at_path(prim_path)
    schema_list = prim.GetAppliedSchemas()
    if "PhysicsCollisionAPI" in schema_list:
        prim.RemoveAPI(UsdPhysics.CollisionAPI)
    if "PhysicsMeshCollisionAPI" in schema_list:
        prim.RemoveAPI(UsdPhysics.MeshCollisionAPI)
    if "PhysxConvexHullCollisionAPI" in schema_list:
        prim.RemoveAPI(PhysxSchema.PhysxConvexHullCollisionAPI)
    if "PhysxConvexDecompositionCollisionAPI" in schema_list:
        prim.RemoveAPI(PhysxSchema.PhysxConvexDecompositionCollisionAPI)
    if "PhysxSDFMeshCollisionAPI" in schema_list:
        prim.RemoveAPI(PhysxSchema.PhysxSDFMeshCollisionAPI)
    if "PhysxTriangleMeshCollisionAPI" in schema_list:
        prim.RemoveAPI(PhysxSchema.PhysxTriangleMeshCollisionAPI)
    for child in prim.GetAllChildren():
        remove_colliders(str(child.GetPath()))

def set_colliders(prim_path, collision_approximation="convexDecomposition", convex_hulls = None):
    remove_colliders(prim_path)
    prim = get_prim_at_path(prim_path)
    collider = UsdPhysics.CollisionAPI.Apply(prim)
    mesh_collider = UsdPhysics.MeshCollisionAPI.Apply(prim)
    mesh_collider.CreateApproximationAttr().Set(collision_approximation)
    collider.GetCollisionEnabledAttr().Set(True)
    if collision_approximation == "convexDecomposition":
        collision_api = PhysxSchema.PhysxConvexDecompositionCollisionAPI.Apply(prim)
        collision_api.CreateHullVertexLimitAttr().Set(64)
        if convex_hulls is not None:
            collision_api.CreateMaxConvexHullsAttr().Set(convex_hulls)
        else:
            collision_api.CreateMaxConvexHullsAttr().Set(64)
        collision_api.CreateMinThicknessAttr().Set(0.1)
        collision_api.CreateShrinkWrapAttr().Set(True)
        collision_api.CreateErrorPercentageAttr().Set(0.1)

and add franka by:

def relate_franka_from_data(scene_uid):
    robot = Franka(
        prim_path=f"/World/{scene_uid}/franka",
    )
    robot.set_solver_position_iteration_count(32)
    robot.set_enabled_self_collisions(True)
    robot.set_stabilization_threshold(0.005)
    robot.set_solver_velocity_iteration_count(16)
    return robot