Tactile sensors#
Tactile sensors turn a link’s surface into a grid of sensing points, or taxels, and read contact geometry directly from the scene’s signed-distance fields (SDFs) or sampled point clouds rather than from solver contact impulses. They give you a dense taxel field at arbitrary locations without adding contacts to the solver, at the cost of being an approximation. Because the layout is link-local, a regular grid imitates a taxel array on a fingertip or a sensor pad.
You describe the layout once with probe_local_pos, a set of (x, y, z) offsets in the link-local frame (an (N, 3) set or an (M, N, 3) planar grid), and the probes move rigidly with the link. Genesis World provides a helper for a planar grid, genesis.utils.geom.generate_grid_points_on_plane(lo, hi, normal, nx, ny), which returns an (ny, nx, 3) array; n_probes is the flattened probe count.
Two families share this interface but estimate contact differently:
SDF-query probes (
ContactProbe,ContactDepthProbe, andKinematicTaxel) query the signed distance from each probe to nearby collision geometry directly. They need no list of target links.Point-cloud probes (
ElastomerTaxelandProximityTaxel) sample a point cloud from the meshes named intrack_link_idx(global link indices) and measure against those points.n_sample_pointssets the sample budget.
Readings are geometric estimates, not solver impulses, and are uncalibrated. Treat them as relative signals unless you tune the coefficients to your setup. The taxels also expose hardware-style imperfections for sim-to-real robustness. For how sensors are sampled, read back, and batched, see the sensors overview.
Choosing a sensor#
Sensor |
|
Shape |
Frame / units |
|---|---|---|---|
|
in-contact flag per probe |
|
bool |
|
penetration depth per probe |
|
m |
|
|
each |
link frame, N / N·m |
|
marker displacement per probe |
|
link frame, m |
|
|
each |
link-local, N / N·m |
Two interactive demos drive these sensors:
examples/sensors/tactile_franka.pysensorizes both Franka fingertips and lets you teleoperate a grasp (--sensor {depth,kinematic,elastomer,proximity}).examples/sensors/tactile_sandbox.pypresses controllable objects into a fixed taxel pad (box or dome) across four parallel environments (--sensor {contact,depth,kinematic,elastomer,proximity}), selecting the depth backend with--contact-depth-query {sdf,raycast}and enabling the imperfections below with--noise.
Contact depth and contact probes#
The simplest probes report geometry alone. ContactDepthProbe returns the penetration depth at each probe in meters; ContactProbe thresholds that depth into a per-probe boolean.
depth_probe = scene.add_sensor(
gs.sensors.ContactDepthProbe(
entity_idx=franka.idx,
link_idx_local=franka.get_link("left_finger").idx_local,
probe_local_pos=probe_local_pos.reshape(-1, 3),
probe_radius=0.002,
)
)
depth = depth_probe.read() # shape ([n_envs,] n_probes), m
probe_radius is the taxel’s sensing radius, not a tolerance: each reading is depth = max(0, probe_radius - signed_distance_to_surface), so a larger radius both extends the detection range and adds a constant offset to the reported depth. The SDF-query probes select the contact-depth backend with contact_depth_query: "sdf" (default) queries each geom’s analytic SDF grid, while "raycast" walks the rigid solver’s collision-mesh BVH and takes the signed distance to the nearest triangle (sharing the BVH with RaycasterSensor). The mode is class-wide: all sensors of the same class must agree.
ContactProbe gates its boolean output with a Schmitt trigger to suppress chatter: it latches on when depth reaches contact_threshold (default 1e-4 m) and releases when depth falls back to release_threshold (default equals contact_threshold, i.e. no hysteresis; may be negative to require separation before release).
Kinematic taxels#
KinematicTaxel adds a spring-damper force model on top of the depth query. For each taxel it estimates a force from penetration along the probe normal and a torque from the twist, using the probe’s motion relative to the object it touches:
s = penetration ** normal_exponent
F = -normal_stiffness * s * n - normal_damping * s * v_n - shear_scalar * v_t
where n is the probe normal, v_n / v_t are the normal and tangential relative velocities. Use normal_exponent=1.5 for Hertzian (spherical) contact; the default 1.0 is a linear spring.
taxel = scene.add_sensor(
gs.sensors.KinematicTaxel(
entity_idx=franka.idx,
link_idx_local=franka.get_link("left_finger").idx_local,
probe_local_pos=probe_local_pos.reshape(-1, 3),
probe_radius=0.002,
normal_stiffness=5000.0,
normal_exponent=1.5,
)
)
data = taxel.read()
data.force # shape ([n_envs,] n_probes, 3), N, link frame
data.torque # shape ([n_envs,] n_probes, 3), N·m, link frame
read() returns a KinematicTaxelReturnType named tuple, so data.force and data.torque unpack by name.
Elastomer taxels#
ElastomerTaxel models a soft tactile skin without simulating deformation. Each probe reports a 3D marker displacement caused by local indentation and shear against the tracked geometry, computed with a HydroShear-style model. It is the right choice when you want the visual “dot displacement” signal of a vision-based tactile sensor.
tactile = scene.add_sensor(
gs.sensors.ElastomerTaxel(
entity_idx=franka.idx,
link_idx_local=franka.get_link("left_finger").idx_local,
probe_local_pos=probe_local_pos, # (ny, nx, 3) grid, or (N, 3)
probe_local_normal=(0.0, -1.0, 0.0),
probe_radius=0.002,
track_link_idx=(cube.base_link_idx,), # global link idx to sense against
dilate_scale=10.0, # gain on normal indentation
shear_scale=100.0, # gain on tangential slip
)
)
displacement = tactile.read() # shape ([n_envs,] n_probes, 3), m
dilate_scale and shear_scale scale the indentation and shear response; lambda_d and lambda_s control how far each effect spreads across neighboring markers. When probe_local_pos is a regular planar grid with a single shared normal, the dilation term is computed with an FFT to keep large arrays fast. The shear anchor is gated by the same contact_threshold / release_threshold Schmitt trigger (a tracked point begins anchoring shear at contact_threshold penetration and releases once it separates back to release_threshold).
Proximity taxels#
ProximityTaxel estimates per-taxel force and torque from a point cloud sampled on the tracked meshes, within a spherical sensing volume of radius probe_radius around each taxel. It reads before hard contact, so it captures near-touch as well as touch.
proximity = scene.add_sensor(
gs.sensors.ProximityTaxel(
entity_idx=pad.idx,
link_idx_local=0,
probe_local_pos=probe_local_pos.reshape(-1, 3),
probe_local_normal=(0.0, 0.0, 1.0),
probe_radius=0.008,
track_link_idx=(obj.base_link_idx,),
n_sample_points=4000,
stiffness=200.0,
shear_coupling=100.0, # 0.0 disables shear, leaving only the normal channel
)
)
data = proximity.read()
data.force # shape ([n_envs,] n_probes, 3), N, link-local
data.torque # shape ([n_envs,] n_probes, 3), N·m, link-local
Like KinematicTaxel, it returns a named tuple (ProximityTaxelReturnType) with force and torque fields.
Sensor imperfections#
On top of the generic per-sensor imperfections (noise, bias, resolution, delay, jitter, available on every sensor), the tactile probe and taxel sensors expose hardware-style imperfections. They apply to the measured branch only: read() returns the imperfect signal, while read_ground_truth() stays clean.
Option(s) |
Models |
Applies to |
|---|---|---|
|
Viscoelastic (single-Maxwell) hysteresis: a step input overshoots by |
All probes and taxels |
|
Additive uniform noise (meters) on the effective sensing radius used by the measured branch. |
All probes and taxels |
|
Per-taxel multiplicative gain on the measured contact depth (scalar or per-taxel array); with a range, resampled uniformly on each |
All probes and taxels |
|
Per-taxel Bernoulli chance of going dead on each |
All probes and taxels |
|
Gaussian spatial crosstalk: each taxel’s force/torque bleeds onto its grid neighbors ( |
Grid taxels: |
|
Explicit point-spread kernel for spatial crosstalk (odd dims; center tap is the self weight). Mutually exclusive with the Gaussian options. |
Grid taxels: |
Spatial crosstalk requires a regular planar (M, N, 3) grid probe_local_pos; pad irregular layouts with probe_radius=0 filler taxels, which read zero and are skipped.
import genesis.utils.misc as misc
taxel = scene.add_sensor(
gs.sensors.KinematicTaxel(
entity_idx=platform.idx,
link_idx_local=0,
probe_local_pos=grid_positions, # (M, N, 3) grid
probe_radius=0.004,
# measured-branch imperfections:
hysteresis_strength=0.5,
hysteresis_tau=0.1,
probe_radius_noise=0.001,
probe_gain=1.5,
crosstalk_kernel=misc.gaussian_crosstalk_kernel(3, 3, sigma=1.0),
)
)
Pass --noise to tactile_sandbox.py to enable these imperfections in the interactive demo.
See also#
Contact: solver-based link-level contact, contact force, and joint torque.
Sensors overview: sampling rate,
read_ground_truth(), batchedscene.read_sensors(), noise, delay, andhistory_length.Extending Genesis World → Sensors: the sensor pipeline and how to add your own sensor type.