Contact Binding: Reading Contact Forces#

Contact bindings let you read contact forces between sensor bodies and filter bodies. A sensor is a rigid body prim (or a set of prims matched by a USD path pattern) whose contacts you want to measure. A filter is a second set of bodies whose contacts with each sensor you want to isolate.

No extra USD authoring is needed beyond the rigid bodies themselves.

Prerequisites#

  • Complete the Tensor Bindings tutorial.

  • Your USD scene has rigid body prims in contact (or that will come into contact during simulation).

Key Concepts#

  • Create the binding before the first step whose contacts you want to observe. The binding registers an internal contact-report callback. No contact data exists until at least one step() has completed.

  • Reading before the first step returns all-zeros tensors.

  • dt for the impulse-to-force conversion (force = impulse / dt) is taken automatically from the last step() call. You do not pass it manually.

  • Result tensor shapes:

    • Net forces: [S, 3] — one 3-D force vector per matched sensor prim.

    • Force matrix: [S, F, 3] — force vectors per (sensor, filter) pair.

Python#

Full binding + destroy#

    # --- 1. Initialize SDK and load scene ---
    physx = PhysX(device="cpu")
    data_dir = Path(__file__).resolve().parent.parent / "data"
    physx.add_usd(str(data_dir / "boxes_falling_on_groundplane.usda"))
    physx.wait_all()

    # --- 2. Create a contact binding BEFORE the first step ---
    # sensor_patterns: bodies whose contact forces you want to read.
    # filter_patterns: bodies to measure contacts against (one per sensor).
    # The binding must be created before any step() call whose contacts you
    # want to observe.
    cb = physx.create_contact_binding(
        sensor_patterns=["/World/Cube1"],
        filter_patterns=["/World/GroundPlane/CollisionMesh"],
        filters_per_sensor=1,
        max_contact_data_count=256,
    )

    sensor_count = cb.sensor_count   # number of matched sensor prims
    filter_count = cb.filter_count   # number of filter prims per sensor

    print(f"Sensors: {sensor_count}, filters per sensor: {filter_count}")

    # --- 3. Simulate until boxes land ---
    for _ in range(120):
        physx.step(1.0 / 60.0, 0.0)
    physx.wait_all()

    # --- 4. Read net contact forces: shape [S, 3] ---
    # dt is taken automatically from the last step() call.
    net_forces = np.zeros((sensor_count, 3), dtype=np.float32)
    cb.read_net_forces(net_forces)
    print("Net contact forces [S, 3]:", net_forces)

    # --- 5. Read contact force matrix: shape [S, F, 3] ---
    force_matrix = np.zeros((sensor_count, filter_count, 3), dtype=np.float32)
    cb.read_force_matrix(force_matrix)
    print("Contact force matrix [S, F, 3]:", force_matrix)

    # --- 6. Clean up first demo ---
    cb.destroy()

C#

{
    DLTensor t;
    memset(&t, 0, sizeof(DLTensor));
    *out_data  = (float*)calloc(rows * cols, sizeof(float));
    *out_shape = (int64_t*)malloc(2 * sizeof(int64_t));
    (*out_shape)[0] = (int64_t)rows;
    (*out_shape)[1] = (int64_t)cols;
    t.data         = *out_data;
    t.ndim         = 2;
    t.shape        = *out_shape;
    t.strides      = NULL;
    t.byte_offset  = 0;
    t.dtype.code   = kDLFloat;
    t.dtype.bits   = 32;
    t.dtype.lanes  = 1;
    t.device.device_type = kDLCPU;
    t.device.device_id   = 0;
    return t;
}

/* Allocate a 3-D float32 DLTensor on the CPU. Caller frees data and shape. */
static DLTensor make_tensor_f32_3d(size_t d0, size_t d1, size_t d2,
                                   float** out_data, int64_t** out_shape)
{
    DLTensor t;
    memset(&t, 0, sizeof(DLTensor));
    *out_data  = (float*)calloc(d0 * d1 * d2, sizeof(float));
    *out_shape = (int64_t*)malloc(3 * sizeof(int64_t));
    (*out_shape)[0] = (int64_t)d0;
    (*out_shape)[1] = (int64_t)d1;
    (*out_shape)[2] = (int64_t)d2;
    t.data         = *out_data;
    t.ndim         = 3;
    t.shape        = *out_shape;
    t.strides      = NULL;
    t.byte_offset  = 0;
    t.dtype.code   = kDLFloat;
    t.dtype.bits   = 32;
    t.dtype.lanes  = 1;
    t.device.device_type = kDLCPU;
    t.device.device_id   = 0;
    return t;
}

int main(void)
{
    ovphysx_result_t r;
    ovphysx_enqueue_result_t er;

    /* 1. Initialize SDK */
    ovphysx_create_args args = OVPHYSX_CREATE_ARGS_DEFAULT;
    args.device = OVPHYSX_DEVICE_CPU;

    ovphysx_handle_t handle = 0;
    r = ovphysx_create_instance(&args, &handle);
    if (!check_result(r, "ovphysx_create_instance")) return 1;

    /* 2. Load scene */
    ovphysx_usd_handle_t usd_handle = 0;
    er = ovphysx_add_usd(handle,
                         ovphysx_cstr(OVPHYSX_TEST_DATA "/boxes_falling_on_groundplane.usda"),
                         (ovphysx_string_t){NULL, 0}, &usd_handle);
    if (!check_enqueue(er, "ovphysx_add_usd")) { ovphysx_destroy_instance(handle); return 1; }
    if (!wait_op(handle, er.op_index, "add_usd")) { ovphysx_destroy_instance(handle); return 1; }

    /* 3. Create contact binding BEFORE the first step.
     *    sensor: the falling box.  filter: the ground plane. */
    ovphysx_string_t sensors[1];
    sensors[0] = ovphysx_cstr("/World/Cube1");

    ovphysx_string_t filters[1];
    filters[0] = ovphysx_cstr("/World/GroundPlane/CollisionMesh");

    ovphysx_contact_binding_handle_t cb = 0;
    r = ovphysx_create_contact_binding(
        handle,
        sensors, 1,     /* 1 sensor pattern */
        filters, 1,     /* 1 filter pattern per sensor */
        256,            /* max raw contact pairs */
        &cb);
    if (!check_result(r, "ovphysx_create_contact_binding")) {
        ovphysx_destroy_instance(handle); return 1;
    }

    /* 4. Query matched sensor / filter counts */
    int32_t sensor_count = 0, filter_count = 0;
    r = ovphysx_get_contact_binding_spec(handle, cb, &sensor_count, &filter_count);
    if (!check_result(r, "ovphysx_get_contact_binding_spec")) {
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_destroy_instance(handle);
        return 1;
    }
    printf("Sensors: %d  Filters per sensor: %d\n", sensor_count, filter_count);

    /* 5. Simulate until the box lands */
    for (int i = 0; i < 120; i++) {
        er = ovphysx_step(handle, 1.0f / 60.0f, 0.0f);
        if (!check_enqueue(er, "ovphysx_step")) {
            ovphysx_destroy_contact_binding(handle, cb);
            ovphysx_destroy_instance(handle);
            return 1;
        }
    }
    if (!wait_op(handle, er.op_index, "step")) {
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_destroy_instance(handle);
        return 1;
    }

    /* 6. Read net contact forces: shape [S, 3].
     *    dt is taken automatically from the last ovphysx_step() call. */
    float* net_data   = NULL;
    int64_t* net_shp  = NULL;
    DLTensor net_tensor = make_tensor_f32_2d(
        (size_t)sensor_count, 3, &net_data, &net_shp);

    r = ovphysx_read_contact_net_forces(handle, cb, &net_tensor);
    if (!check_result(r, "ovphysx_read_contact_net_forces")) {
        free(net_data); free(net_shp);
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_destroy_instance(handle);
        return 1;
    }
    printf("Net contact forces [%d, 3]:\n", sensor_count);
    for (int s = 0; s < sensor_count; s++) {
        printf("  sensor %d: fx=%.3f  fy=%.3f  fz=%.3f\n",
               s,
               net_data[s * 3 + 0],
               net_data[s * 3 + 1],
               net_data[s * 3 + 2]);
    }
    free(net_data); free(net_shp);

    /* 7. Read contact force matrix: shape [S, F, 3]. */
    float* mat_data   = NULL;
    int64_t* mat_shp  = NULL;
    DLTensor mat_tensor = make_tensor_f32_3d(
        (size_t)sensor_count, (size_t)filter_count, 3,
        &mat_data, &mat_shp);

    r = ovphysx_read_contact_force_matrix(handle, cb, &mat_tensor);
    if (!check_result(r, "ovphysx_read_contact_force_matrix")) {
        free(mat_data); free(mat_shp);
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_destroy_instance(handle);
        return 1;
    }
    printf("Contact force matrix [%d, %d, 3]:\n", sensor_count, filter_count);
    for (int s = 0; s < sensor_count; s++) {
        for (int f = 0; f < filter_count; f++) {
            int base = (s * filter_count + f) * 3;
            printf("  [%d][%d]: fx=%.3f  fy=%.3f  fz=%.3f\n",
                   s, f,
                   mat_data[base + 0],
                   mat_data[base + 1],
                   mat_data[base + 2]);
        }
    }
    free(mat_data); free(mat_shp);

    /* 8. Destroy contact binding and SDK */
    ovphysx_destroy_contact_binding(handle, cb);

Unfiltered contacts#

Pass filter_patterns=None and filters_per_sensor=0 to collect contacts with all bodies:

cb = physx.create_contact_binding(
    sensor_patterns=["/World/robot/ee"],
    max_contact_data_count=512,
)

In C:

ovphysx_string_t sensors[] = { ovphysx_cstr("/World/robot/ee") };
ovphysx_contact_binding_handle_t cb;
ovphysx_create_contact_binding(handle, sensors, 1, NULL, 0, 512, &cb);

Multiple sensors and filters#

The filter_patterns array is flat and must have length len(sensor_patterns) * filters_per_sensor. Each block of filters_per_sensor entries corresponds to one sensor:

# 2 sensors, 2 filters each -> 4 filter entries total
cb = physx.create_contact_binding(
    sensor_patterns=["/World/robot_0/ee", "/World/robot_1/ee"],
    filter_patterns=[
        "/World/obstacle_A", "/World/obstacle_B",  # filters for robot_0/ee
        "/World/obstacle_A", "/World/obstacle_B",  # filters for robot_1/ee
    ],
    filters_per_sensor=2,
)
# force_matrix shape: [2, 2, 3]