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.
dtfor the impulse-to-force conversion (force = impulse / dt) is taken automatically from the laststep()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]