Skip to main content
`Ctrl`+`K`

# 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()

### Context-manager form (recommended)

        # [tutorial-context-manager]
        with physx.create_contact_binding(sensor_patterns=["/World/Cube1"]) as cb2:
            for _ in range(60):
                physx.step(1.0 / 60.0, 0.0)
            physx.wait_all()
            out = np.zeros((cb2.sensor_count, 3), dtype=np.float32)
            cb2.read_net_forces(out)

## 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]