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).

  • For CUDA output tensors, enable DirectGPU TensorAPI before creating the PhysX instance; physxScene:enableGPUDynamics=true alone only selects GPU dynamics. See GPU Warmup and Determinism.

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.

  • Create contact bindings once outside simulation loops and reuse them. In Python, use the context-manager form or call cb.destroy() when finished; otherwise garbage collection emits ResourceWarning when it eventually releases the native binding.

  • 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.

    • Detailed contact data: contact forces and separations use [C, 1]; positions and normals use [C, 3]; all are indexed by [S, F] count/start-index tensors.

    • Detailed friction data: friction forces and points use [C, 3] buffers indexed by [S, F] count/start-index tensors.

Python#

Full Binding + Destroy#


    # --- 1. Initialize SDK and load scene ---
    PhysX.set_cpu_mode(True)
    physx = PhysX()
    stage = None
    data_dir = Path(__file__).resolve().parent.parent / "data"

    try:
        stage = attach_scene(physx, data_dir / "boxes_falling_on_groundplane.usda", "ovphysx-contact-sample")
        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)
        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#

    /* 1. Initialize SDK */
    r = ovphysx_initialize();
    if (!check_result(r, "ovphysx_initialize")) return 1;

    ovphysx_create_args args = OVPHYSX_CREATE_ARGS_DEFAULT;

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

    /* 2. Populate ovstage from USD and attach it */
    ovphysx_sample_stage_attachment_t stage_attachment = {0};
    if (!ovphysx_sample_attach_usd_with_ovstage(
            handle, OVPHYSX_TEST_DATA "/boxes_falling_on_groundplane.usda", &stage_attachment)) {
        ovphysx_destroy_instance(handle); ovphysx_shutdown(); 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_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle); ovphysx_shutdown(); 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_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        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);
        if (!check_enqueue(er, "ovphysx_step")) {
            ovphysx_destroy_contact_binding(handle, cb);
            ovphysx_sample_destroy_stage(handle, &stage_attachment);
            ovphysx_destroy_instance(handle);
            ovphysx_shutdown();
            return 1;
        }
    }
    if (!wait_op(handle, er.op_index, "step")) {
        ovphysx_destroy_contact_binding(handle, cb);
        ovphysx_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        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_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        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_sample_destroy_stage(handle, &stage_attachment);
        ovphysx_destroy_instance(handle);
        ovphysx_shutdown();
        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);

    printf("Contact binding sample completed successfully\n");

    /* 8. Destroy contact binding */
    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]

Detailed Contact and Friction Data#

Use cb.max_contact_data_count to allocate reusable flat buffers. For each sensor/filter pair, counts[s, f] and start_indices[s, f] identify the valid slice inside the flat buffers.

cb.sensor_paths returns the resolved sensor paths in row order. cb.filter_paths returns a nested [sensor][filter] list in column order.

Create the binding with filter_patterns, filters_per_sensor > 0, and max_contact_data_count > 0 before calling read_contact_data() or read_friction_data(). The aggregate read_net_forces() and read_force_matrix() calls do not require this detailed-contact capacity. counts and start_indices may be int32 or uint32; NumPy’s default integer dtype is usually int64, so allocate these arrays with an explicit dtype.

C = cb.max_contact_data_count
contact_forces = np.zeros((C, 1), dtype=np.float32)
positions = np.zeros((C, 3), dtype=np.float32)
normals = np.zeros((C, 3), dtype=np.float32)
separations = np.zeros((C, 1), dtype=np.float32)
counts = np.zeros((cb.sensor_count, cb.filter_count), dtype=np.int32)
starts = np.zeros((cb.sensor_count, cb.filter_count), dtype=np.int32)

cb.read_contact_data(
    contact_forces,
    positions,
    normals,
    separations,
    counts,
    starts,
)

s = 0
f = 0
start = starts[s, f]
stop = start + counts[s, f]
sensor_filter_positions = positions[start:stop]

friction_forces = np.zeros((C, 3), dtype=np.float32)
friction_points = np.zeros((C, 3), dtype=np.float32)
friction_counts = np.zeros((cb.sensor_count, cb.filter_count), dtype=np.int32)
friction_starts = np.zeros((cb.sensor_count, cb.filter_count), dtype=np.int32)

cb.read_friction_data(
    friction_forces,
    friction_points,
    friction_counts,
    friction_starts,
)

Friction data is per friction anchor, not a pre-summed [S, F, 3] pair force. To build a pair-level friction force tensor, sum friction_forces[start:stop] for each (sensor, filter) pair using the matching friction_counts and friction_starts entries.

This flat representation matches the underlying PhysX tensor API and avoids a fixed per-pair contact-point dimension. Build a padded [S, F, K, ...] view in application code only if that layout is useful for a specific algorithm.