Move tactile sensor to the engine.

PiperOrigin-RevId: 788863133
Change-Id: I3172ebb7641fa8146469da25cba7db794760e7b1
This commit is contained in:
Alessio Quaglino
2025-07-30 05:26:39 -07:00
committed by Copybara-Service
parent 89f4789085
commit 51babec9c5
28 changed files with 418 additions and 687 deletions
+36
View File
@@ -7539,6 +7539,42 @@ Extraction
:at:`name`, :at:`noise`, :at:`user`:
See :ref:`CSensor`.
.. _sensor-tactile:
:el-prefix:`sensor/` |-| **tactile** (*)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. image:: images/XMLreference/tactile.png
:align: right
:width: 30%
:target: https://github.com/google-deepmind/mujoco/blob/main/model/tactile/tactile.xml
The tactile sensor returns the penetration pressure and the sliding velocities in the tangent frame at given points
between the geom associated with the sensor and the SDF geoms in contact with it. We define the penetration pressure as
a function of the penetration depth :math:`p(d) = \frac{d}{d_{max}-d}`, which is zero at the surface and goes to
infinity as the maximum depth is reached. The sensor is associated with a geom and a mesh. It is activated by the
contact between its associated geom and other geoms. The vertices of the mesh, when positioned in the geom frame, are
the points at which sensor values are computed, so the dimension of the output is 3 times the number of vertices in the
mesh. The mesh must have 3 normal vectors per vertex, which are used to compute the tangent frame. If the penetration
depth is positive (no contact), then all values are 0 for the corresponding vertex. Only contacts with geoms of type SDF
contribute to the sensor output. The sensor can be visualized by enabling the visualization of contact points.
.. _sensor-tactile-geom:
:at:`geom`: :at-val:`string, required`
Name of the geom to associate the tactile sensor with.
.. _sensor-tactile-mesh:
:at:`mesh`: :at-val:`string, required`
Name of the mesh to associate the tactile sensor with. The mesh will be created by the sensor.
.. _sensor-tactile-name:
.. _sensor-tactile-user:
:at:`name`, :at:`user`:
See :ref:`CSensor`.
.. _sensor-e_potential:
:el-prefix:`sensor/` |-| **e_potential** (*)
+7
View File
@@ -1284,6 +1284,13 @@
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_| sensor |br| |_| |L| | | .. table:: |
| :ref:`tactile | \* | :class: mjcf-attributes |
| <sensor-tactile>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`name<sensor-tactile-name>` | :ref:`geom<sensor-tactile-geom>` | :ref:`mesh<sensor-tactile-mesh>` | :ref:`user<sensor-tactile-user>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_| sensor |br| |_| |L| | | .. table:: |
| :ref:`plugin | \* | :class: mjcf-attributes |
| <sensor-plugin>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+2
View File
@@ -12,6 +12,8 @@ General
- Added the :ref:`contact<sensor-contact>` sensor, for reporting contact information according to user-defined criteria.
The purpose of the :el:`contact` sensor is to report contact-related information in a fixed-size array. This is useful
as input to learning-based agents and in environment logic.
- Added the :ref:`tactile<sensor-tactile>` sensor, for measuring the penetration depth between two objects at given
points and the sliding velocities in the tangent frame. The sensor reports tactile data only when colliding with SDFs.
- Removed the SdfLib plugin and the dependency on `SdfLib <https://github.com/UPC-ViRVIG/SdfLib>`__. SDFs are now
supported natively in mjModel.
- Removed ``oct_depth`` from :ref:`mjvOption` (unused).
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+3
View File
@@ -739,6 +739,9 @@ typedef enum mjtSensor_ { // type of sensor
mjSENS_E_KINETIC, // kinetic energy
mjSENS_CLOCK, // simulation time
// sensors related to SDFs
mjSENS_TACTILE, // tactile sensor
// plugin-controlled sensors
mjSENS_PLUGIN, // plugin-controlled
+3
View File
@@ -377,6 +377,9 @@ typedef enum mjtSensor_ { // type of sensor
mjSENS_E_KINETIC, // kinetic energy
mjSENS_CLOCK, // simulation time
// sensors related to SDFs
mjSENS_TACTILE, // tactile sensor
// plugin-controlled sensors
mjSENS_PLUGIN, // plugin-controlled
+3
View File
@@ -753,8 +753,11 @@ class Model(PyTreeNode):
mesh_bvhnum: np.ndarray
mesh_octadr: np.ndarray
mesh_octnum: np.ndarray
mesh_normaladr: np.ndarray
mesh_normalnum: np.ndarray
mesh_graphadr: np.ndarray
mesh_vert: np.ndarray
mesh_normal: np.ndarray
mesh_face: np.ndarray
mesh_graph: np.ndarray
mesh_pos: np.ndarray
@@ -1,5 +1,6 @@
<mujoco model="touchtest">
<compiler autolimits="true"/>
<option sdf_initpoints="20"/>
<extension>
<plugin plugin="mujoco.sdf.gear">
@@ -7,14 +8,6 @@
<config key="alpha" value="0"/>
</instance>
</plugin>
<plugin plugin="mujoco.sensor.touch_stress">
<instance name="touch_stress">
<config key="size" value="37 37"/>
<config key="fov" value="45 45"/>
<config key="gamma" value="0"/>
<config key="nchannel" value="3"/>
</instance>
</plugin>
</extension>
<asset>
@@ -24,6 +17,8 @@
<mesh name="gear">
<plugin instance="gear"/>
</mesh>
<mesh name="sphere" builtin="wedge" params="37 37 45 45 0" scale=".3 .3 .3"/>
<mesh name="box" builtin="plate" params="37 37" scale=".3 .5 .2"/>
</asset>
<visual>
@@ -47,18 +42,25 @@
<plugin instance="gear"/>
</geom>
<body name="ball" pos="0 0 1">
<body name="ball" pos="-1 1 1">
<joint name="x" type="slide" axis="1 0 0" damping="1"/>
<joint name="y" type="slide" axis="0 1 0" damping="1"/>
<joint name="z" type="slide" axis="0 0 1"/>
<joint name="rx" axis="1 0 0" springdamper="0.2 1"/>
<joint name="ry" axis="0 1 0" springdamper="0.2 1"/>
<geom type="sphere" size=".3" mass="0.1" rgba=".5 .5 .5 .3"/>
<site name="touch"/>
<geom name="ball" type="mesh" mesh="sphere" mass="0" contype="0" conaffinity="0" rgba=".5 .5 .5 0"/>
</body>
<body name="finger" pos="0 0 1">
<freejoint/>
<geom type="box" size=".3 .5 .2" mass="0.1" rgba=".5 .5 .5 .3"/>
<geom name="finger" type="mesh" mesh="box" mass="0" contype="0" conaffinity="0" rgba=".5 .5 .5 0"/>
</body>
</worldbody>
<sensor>
<plugin instance="touch_stress" objtype="site" objname="touch"/>
<tactile geom="ball" mesh="sphere"/>
<tactile geom="finger" mesh="box"/>
</sensor>
</mujoco>
-2
View File
@@ -21,8 +21,6 @@ set(MUJOCO_SENSOR_SRCS
register.cc
touch_grid.h
touch_grid.cc
touch_stress.h
touch_stress.cc
)
add_library(sensor SHARED)
-2
View File
@@ -14,13 +14,11 @@
#include <mujoco/mjplugin.h>
#include "touch_grid.h"
#include "touch_stress.h"
namespace mujoco::plugin::sensor {
mjPLUGIN_LIB_INIT {
TouchGrid::RegisterPlugin();
TouchStress::RegisterPlugin();
}
} // namespace mujoco::plugin::sensor
-578
View File
@@ -1,578 +0,0 @@
// Copyright 2023 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "touch_stress.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <sstream>
#include <string>
#include <unordered_set>
#include <vector>
#include <mujoco/mjdata.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjplugin.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mjvisualize.h>
#include <mujoco/mujoco.h>
namespace mujoco::plugin::sensor {
namespace {
// Checks that a plugin config attribute exists.
bool CheckAttr(const std::string& input) {
char* end;
std::string value = input;
value.erase(std::remove_if(value.begin(), value.end(), isspace), value.end());
strtod(value.c_str(), &end);
return end == value.data() + value.size();
}
// Converts a string into a numeric vector
template <typename T>
void ReadVector(std::vector<T>& output, const std::string& input) {
std::stringstream ss(input);
std::string item;
char delim = ' ';
while (getline(ss, item, delim)) {
CheckAttr(item);
output.push_back(strtod(item.c_str(), nullptr));
}
}
// Evenly spaced numbers over a specified interval.
void LinSpace(mjtNum lower, mjtNum upper, int n, mjtNum array[]) {
mjtNum increment = n > 1 ? (upper - lower) / (n - 1) : 0;
for (int i = 0; i < n; ++i) {
*array = lower;
++array;
lower += increment;
}
}
// Parametrized linear/quintic interpolated nonlinearity.
mjtNum Fovea(mjtNum x, mjtNum gamma) {
// Quick return.
if (!gamma) return x;
// Foveal deformation.
mjtNum g = mjMAX(0, mjMIN(1, gamma));
return g*mju_pow(x, 5) + (1 - g)*x;
}
// Make bin edges.
void BinEdges(mjtNum* x_edges, mjtNum* y_edges, int size[2], mjtNum fov[2],
mjtNum gamma) {
// Make unit bin edges.
LinSpace(-1, 1, size[0] + 1, x_edges);
LinSpace(-1, 1, size[1] + 1, y_edges);
// Apply foveal deformation.
for (int i = 0; i < size[0] + 1; i++) {
x_edges[i] = Fovea(x_edges[i], gamma);
}
for (int i = 0; i < size[1] + 1; i++) {
y_edges[i] = Fovea(y_edges[i], gamma);
}
// Scale by field-of-view.
mju_scl(x_edges, x_edges, fov[0]*mjPI / 180, size[0] + 1);
mju_scl(y_edges, y_edges, fov[1]*mjPI / 180, size[1] + 1);
}
// Transform spherical (azimuth, elevation, radius) to Cartesian (x,y,z).
void SphericalToCartesian(const mjtNum aer[3], mjtNum xyz[3]) {
mjtNum a = aer[0], e = aer[1], r = aer[2];
xyz[0] = r * mju_cos(e) * mju_sin(a);
xyz[1] = r * mju_sin(e);
xyz[2] = -r * mju_cos(e) * mju_cos(a);
}
// Transform Cartesian (x,y,z) to spherical (azimuth, elevation, radius).
void CartesianToSpherical(const mjtNum xyz[3], mjtNum aer[3]) {
mjtNum x = xyz[0], y = xyz[1], z = xyz[2];
aer[0] = mju_atan2(x, -z);
aer[1] = mju_atan2(y, mju_sqrt(x*x + z*z));
aer[2] = mju_sqrt(x*x + z*z + y*y);
}
// Tangent frame in Cartesian coordinates.
void TangentFrame(const mjtNum aer[3], mjtNum mat[9]) {
mjtNum a = aer[0], e = aer[1], r = aer[2];
mjtNum ta[3] = {r * mju_cos(e) * mju_cos(a), 0,
r * mju_cos(e) * mju_sin(a)};
mjtNum te[3] = {-r * mju_sin(e) * mju_sin(a), r * mju_cos(e),
r * mju_sin(e) * mju_cos(a)};
mju_normalize3(ta);
mju_normalize3(te);
mju_copy3(mat, ta);
mju_copy3(mat+3, te);
mju_cross(mat+6, te, ta);
}
} // namespace
// Creates a TouchStress instance if all config attributes are defined and
// within their allowed bounds.
TouchStress* TouchStress::Create(const mjModel* m, mjData* d,
int instance) {
if (CheckAttr(std::string(mj_getPluginConfig(m, instance, "gamma"))) &&
CheckAttr(std::string(mj_getPluginConfig(m, instance, "nchannel")))) {
// nchannel
int nchannel = strtod(mj_getPluginConfig(m, instance, "nchannel"), nullptr);
if (!nchannel) nchannel = 1;
if (nchannel < 1 || nchannel > 3) {
mju_error("nchannel must be between 1 and 3");
return nullptr;
}
// size
std::vector<int> size;
std::string size_str = std::string(mj_getPluginConfig(m, instance, "size"));
ReadVector(size, size_str.c_str());
if (size.size()!= 2) {
mju_error("Both horizontal and vertical resolutions must be specified");
return nullptr;
}
if (size[0] <= 0 || size[1] <= 0) {
mju_error("Horizontal and vertical resolutions must be positive");
return nullptr;
}
// field of view
std::vector<mjtNum> fov;
std::string fov_str = std::string(mj_getPluginConfig(m, instance, "fov"));
ReadVector(fov, fov_str.c_str());
if (fov.size()!= 2) {
mju_error(
"Both horizontal and vertical fields of view must be specified");
return nullptr;
}
if (fov[0] <= 0 || fov[0] > 180) {
mju_error("`fov[0]` must be a float between (0, 180] degrees");
return nullptr;
}
if (fov[1] <= 0 || fov[1] > 90) {
mju_error("`fov[1]` must be a float between (0, 90] degrees");
return nullptr;
}
// gamma
mjtNum gamma = strtod(mj_getPluginConfig(m, instance, "gamma"), nullptr);
if (gamma < 0 || gamma > 1) {
mju_error("`gamma` must be a nonnegative float between [0, 1]");
return nullptr;
}
return new TouchStress(m, d, instance, nchannel, size.data(), fov.data(),
gamma);
} else {
mju_error("Invalid or missing parameters in touch_grid sensor plugin");
return nullptr;
}
}
TouchStress::TouchStress(const mjModel* m, mjData* d, int instance,
int nchannel, int size[2], mjtNum fov[2], mjtNum gamma)
: nchannel_(nchannel),
size_{size[0], size[1]},
fov_{fov[0], fov[1]},
gamma_(gamma) {
// Make sure sensor is attached to a site.
for (int i = 0; i < m->nsensor; ++i) {
if (m->sensor_type[i] == mjSENS_PLUGIN && m->sensor_plugin[i] == instance) {
if (m->sensor_objtype[i] != mjOBJ_SITE) {
mju_error("Touch Grid sensor must be attached to a site");
}
}
}
// Get sensor id.
for (id_ = 0; id_ < m->nsensor; ++id_) {
if (m->sensor_type[id_] == mjSENS_PLUGIN &&
m->sensor_plugin[id_] == instance) {
break;
}
}
// Get parent weld id.
int site_id = m->sensor_objid[id_];
int parent_body = m->site_bodyid[site_id];
parent_weld_ = m->body_weldid[parent_body];
// Get geom id.
int collision_geoms = 0;
for (int i = 0; i < m->body_geomnum[parent_body]; ++i) {
int geom_id = m->body_geomadr[parent_body]+i;
if (m->geom_contype[geom_id] || m->geom_conaffinity[geom_id]) {
collision_geoms++;
geom_id_ = geom_id;
}
}
if (collision_geoms == 0) {
mju_error("Touch sensor requires a body with at least one collision geom");
}
// Create bin edges.
std::vector<mjtNum> x_edges(size[0] + 1, 0);
std::vector<mjtNum> y_edges(size[1] + 1, 0);
BinEdges(x_edges.data(), y_edges.data(), size_, fov_, gamma_);
dist_ = m->geom_rbound[geom_id_];
pos_.resize(3*size[0]*size[1], 0);
mat_.resize(9*size[0]*size[1], 0);
// Precompute spherical coordinates.
for (int i = 0; i < size[0]; i++) {
for (int j = 0; j < size[1]; j++) {
mjtNum aer[3];
aer[0] = 0.5*(x_edges[i+1]+x_edges[i]);
aer[1] = 0.5*(y_edges[j+1]+y_edges[j]);
aer[2] = m->geom_rbound[geom_id_];
SphericalToCartesian(aer, pos_.data() + 3 * (i * size[1] + j));
TangentFrame(aer, mat_.data() + 9 * (i * size[1] + j));
}
}
}
void TouchStress::Reset(const mjModel* m, int instance) {}
void TouchStress::Compute(const mjModel* m, mjData* d, int instance) {
mj_markStack(d);
// Clear sensordata and distance matrix.
mjtNum* sensordata = d->sensordata + m->sensor_adr[id_];
mju_zero(sensordata, m->sensor_dim[id_]);
// Get site id.
int site_id = m->sensor_objid[id_];
// Count contacts and get contact geom ids.
std::unordered_set<int> contact_geom_ids;
for (int i = 0; i < d->ncon; i++) {
int body1 = m->body_weldid[m->geom_bodyid[d->contact[i].geom1]];
int body2 = m->body_weldid[m->geom_bodyid[d->contact[i].geom2]];
if (body1 == parent_weld_) {
contact_geom_ids.insert(d->contact[i].geom2);
}
if (body2 == parent_weld_) {
contact_geom_ids.insert(d->contact[i].geom1);
}
}
// No contacts, return.
if (contact_geom_ids.empty()) {
mj_freeStack(d);
return;
}
// All of the quadrature points are contact points.
int ncon = size_[0]*size_[1];
// Get site frame.
mjtNum* site_pos = d->site_xpos + 3*site_id;
mjtNum* site_mat = d->site_xmat + 9*site_id;
// Allocate contact forces and positions.
mjtNum* forcesT = mj_stackAllocNum(d, ncon*3);
mju_zero(forcesT, ncon*3);
// Iterate over colliding geoms.
for (auto geom : contact_geom_ids) {
int body = m->geom_bodyid[geom];
// Get sdf plugin of the geoms.
int sdf_instance[2] = {-1, geom_id_};
mjtGeom geomtype[2] = {mjGEOM_SDF, mjGEOM_SPHERE};
const mjpPlugin* sdf_ptr[2] = {NULL, NULL};
if (m->geom_type[geom] == mjGEOM_SDF) {
sdf_instance[0] = m->geom_plugin[geom];
sdf_ptr[0] = mjc_getSDF(m, geom);
} else if (m->geom_type[geom] == mjGEOM_MESH) {
sdf_instance[0] = m->geom_dataid[geom];
geomtype[0] = (mjtGeom)m->geom_type[geom];
} else {
sdf_instance[0] = geom;
geomtype[0] = (mjtGeom)m->geom_type[geom];
}
// Skip mesh geoms not having an octree.
if (geomtype[0] == mjGEOM_MESH &&
m->mesh_octadr[m->geom_dataid[geom]] == -1) {
continue;
}
// Set SDF parameters.
mjSDF geom_sdf;
geom_sdf.id = &sdf_instance[0];
geom_sdf.type = mjSDFTYPE_SINGLE;
geom_sdf.plugin = &sdf_ptr[0];
geom_sdf.geomtype = &geomtype[0];
mjSDF sensor_sdf;
sensor_sdf.id = &sdf_instance[1];
sensor_sdf.type = mjSDFTYPE_SINGLE;
sensor_sdf.plugin = &sdf_ptr[1];
sensor_sdf.geomtype = &geomtype[1];
// Get forces and positions in spherical coordinates.
int node = 0;
for (int j = 0; j < size_[1]; j++) {
for (int i = 0; i < size_[0]; i++) {
// Position in site frame.
mjtNum* pos = pos_.data() + 3*(i*size_[1] + j);
mjtNum* mat = mat_.data() + 9*(i*size_[1] + j);
// Position in global frame.
mjtNum xpos[3];
mju_mulMatVec3(xpos, site_mat, pos);
mju_addTo3(xpos, site_pos);
// Position in other geom frame.
mjtNum lpos[3], tmp[3];
mju_sub3(tmp, xpos, d->geom_xpos + 3*geom);
mju_mulMatTVec3(lpos, d->geom_xmat + 9*geom, tmp);
// SDF plugins are in the original mesh frame.
if (sdf_ptr[0] != NULL) {
mjtNum mesh_mat[9];
mju_quat2Mat(mesh_mat, m->mesh_quat + 4 * m->geom_dataid[geom]);
mju_mulMatVec3(lpos, mesh_mat, lpos);
mju_addTo3(lpos, m->mesh_pos + 3 * m->geom_dataid[geom]);
}
// Compute distance.
mjtNum depth = mju_min(mjc_distance(m, d, &geom_sdf, lpos), 0);
if (depth == 0) {
node++;
continue;
}
// Get velocity in global frame.
mjtNum vel_sensor[6], vel_other[6], vel_rel[3];
mju_transformSpatial(
vel_sensor, d->cvel + 6 * parent_weld_, 0, xpos,
d->subtree_com + 3 * m->body_rootid[parent_weld_], NULL);
mju_transformSpatial(
vel_other, d->cvel + 6 * body, 0, d->geom_xpos + 3 * geom,
d->subtree_com + 3 * m->body_rootid[body], NULL);
mju_sub3(vel_rel, vel_sensor+3, vel_other+3);
// Get contact force/torque, rotate into node frame.
mjtNum frc[3];
mjtNum normal[3];
mjtNum kMaxDepth = 0.05;
mjtNum pressure = 1 / (kMaxDepth - depth) - 1 / kMaxDepth;
mjc_gradient(m, d, &sensor_sdf, normal, pos);
mju_scl3(frc, normal, pressure);
// one row of mat^T * force
forcesT[0*ncon + node] = mat[2]*frc[0] + mat[5]*frc[1] + mat[8]*frc[2];
forcesT[1*ncon + node] = mju_abs(mju_dot3(vel_rel, mat + 0));
forcesT[2*ncon + node] = mju_abs(mju_dot3(vel_rel, mat + 3));
node++;
}
}
// Compute sensor output.
for (int c = 0; c < nchannel_; c++) {
if (!mju_isZero(forcesT + c*ncon, ncon)) {
mju_addTo(sensordata + c*ncon, forcesT + c*ncon, size_[0]*size_[1]);
}
}
}
mj_freeStack(d);
}
// Thickness of taxel-visualization boxes relative to contact distance.
static const mjtNum kRelativeThickness = 0.02;
void TouchStress::Visualize(const mjModel* m, mjData* d, const mjvOption* opt,
mjvScene* scn, int instance) {
if (!opt->flags[mjVIS_CONTACTPOINT]) {
return;
}
mj_markStack(d);
// Get sensor data.
mjtNum* sensordata = d->sensordata + m->sensor_adr[id_];
// Get maximum absolute normal force.
mjtNum maxval = 0;
int frame = size_[0]*size_[1];
for (int j=0; j < frame; j++) {
maxval = mju_max(maxval, mju_abs(sensordata[j]));
}
// If no normal force readings, quick return.
if (!maxval) {
mj_freeStack(d);
return;
}
// Get site id and frame.
int site_id = m->sensor_objid[id_];
mjtNum* site_pos = d->site_xpos + 3*site_id;
mjtNum* site_mat = d->site_xmat + 9*site_id;
mjtNum site_quat[4];
mju_mat2Quat(site_quat, site_mat);
// Draw geoms.
for (int i=0; i < size_[0]; i++) {
for (int j=0; j < size_[1]; j++) {
if (dist_ < mjMINVAL) {
continue;
}
if (scn->ngeom >= scn->maxgeom) {
mj_warning(d, mjWARN_VGEOMFULL, scn->maxgeom);
mj_freeStack(d);
return;
} else {
// position of center and neighbor in local frame
mjtNum pos[3];
mjtNum pos1[3];
int k = i < size_[0]-1 ? i+1 : i-1;
int l = j < size_[1]-1 ? j+1 : j-1;
mju_copy3(pos, pos_.data() + 3*(i*size_[1]+j));
mju_copy3(pos1, pos_.data() + 3*(k*size_[1]+l));
// position in spherical coordinates
mjtNum aer[3];
mjtNum aer1[3];
CartesianToSpherical(pos, aer);
CartesianToSpherical(pos1, aer1);
// size
mjtNum size[3];
size[0] = .5*dist_*mju_abs(aer1[0]-aer[0]);
size[1] = .5*dist_*mju_abs(aer1[1]-aer[1]);
size[2] = dist_*kRelativeThickness;
// position in global frame
mju_scl3(pos, pos, 1-kRelativeThickness);
mju_mulMatVec3(pos, site_mat, pos);
mju_addTo3(pos, site_pos);
// orientation
mjtNum a_quat[4];
mjtNum site_y[3] = {-site_mat[1], -site_mat[4], -site_mat[7]};
mju_axisAngle2Quat(a_quat, site_y, aer[0]);
mjtNum e_quat[4];
mjtNum site_x[3] = {site_mat[0], site_mat[3], site_mat[6]};
mju_axisAngle2Quat(e_quat, site_x, aer[1]);
mjtNum quat[4];
mju_mulQuat(quat, e_quat, site_quat);
mju_mulQuat(quat, a_quat, quat);
mjtNum mat[9];
mju_quat2Mat(mat, quat);
// color
float rgba[4] = {1, 1, 1, 1.0};
for (int k=0; k < mjMIN(nchannel_, 3); k++) {
rgba[k] = mju_abs(sensordata[k*frame + j*size_[0] + i]) / maxval;
}
// draw box geom
mjvGeom* thisgeom = scn->geoms + scn->ngeom;
mjv_initGeom(thisgeom, mjGEOM_BOX, size, pos, mat, rgba);
thisgeom->objtype = mjOBJ_UNKNOWN;
thisgeom->objid = id_;
thisgeom->category = mjCAT_DECOR;
thisgeom->segid = scn->ngeom;
scn->ngeom++;
}
}
}
mj_freeStack(d);
}
void TouchStress::RegisterPlugin() {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
plugin.name = "mujoco.sensor.touch_stress";
plugin.capabilityflags |= mjPLUGIN_SENSOR;
// Parameterized by 4 attributes.
const char* attributes[] = {"nchannel", "size", "fov", "gamma"};
plugin.nattribute = sizeof(attributes) / sizeof(attributes[0]);
plugin.attributes = attributes;
// Stateless.
plugin.nstate = +[](const mjModel* m, int instance) { return 0; };
// Sensor dimension = nchannel * size[0] * size[1]
plugin.nsensordata = +[](const mjModel* m, int instance, int sensor_id) {
int nchannel = strtod(mj_getPluginConfig(m, instance, "nchannel"), nullptr);
if (!nchannel) nchannel = 1;
std::vector<int> size;
std::string size_str = std::string(mj_getPluginConfig(m, instance, "size"));
ReadVector(size, size_str.c_str());
return nchannel * size[0] * size[1];
};
// Can only run after forces have been computed.
plugin.needstage = mjSTAGE_ACC;
// Initialization callback.
plugin.init = +[](const mjModel* m, mjData* d, int instance) {
auto* TouchStress = TouchStress::Create(m, d, instance);
if (!TouchStress) {
return -1;
}
d->plugin_data[instance] = reinterpret_cast<uintptr_t>(TouchStress);
return 0;
};
// Destruction callback.
plugin.destroy = +[](mjData* d, int instance) {
delete reinterpret_cast<TouchStress*>(d->plugin_data[instance]);
d->plugin_data[instance] = 0;
};
// Reset callback.
plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data,
int instance) {
auto* TouchStress = reinterpret_cast<class TouchStress*>(plugin_data);
TouchStress->Reset(m, instance);
};
// Compute callback.
plugin.compute =
+[](const mjModel* m, mjData* d, int instance, int capability_bit) {
auto* TouchStress =
reinterpret_cast<class TouchStress*>(d->plugin_data[instance]);
TouchStress->Compute(m, d, instance);
};
// Visualization callback.
plugin.visualize = +[](const mjModel* m, mjData* d, const mjvOption* opt,
mjvScene* scn, int instance) {
auto* TouchStress =
reinterpret_cast<class TouchStress*>(d->plugin_data[instance]);
TouchStress->Visualize(m, d, opt, scn, instance);
};
// Register the plugin.
mjp_registerPlugin(&plugin);
}
} // namespace mujoco::plugin::sensor
-78
View File
@@ -1,78 +0,0 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_
#define MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_
#include <vector>
#include <mujoco/mjdata.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mjvisualize.h>
namespace mujoco::plugin::sensor {
// A touch grid sensor is associated with a site and senses contact stresses
// between the site's parent body and all other bodies. The site's
// frame determines the orientation of the sensor with the same convention used
// for cameras and lights: the sensor points in the frame's negative-Z
// direction, so the X and Y axes correspond to horizontal and vertical
// directions respectively.
//
// The output of the sensor is a stack of 3 "touch images" corresponding to
// forces in the local frame of the taxels. Forces are in the [z, x, y] order,
// corresponding to the ordering in contact frames: [normal, tangent, tangent].
//
// The sensor has 6 parameters:
// 1. (int) Number of channels [1-3]. Defaults to 1.
// 2. (int) Horizontal resolution.
// 3. (int) Vertical resolution.
// 4. (float) Horizontal field-of-view (fov_x), in degrees.
// 5. (float) Vertical field-of-view (fov_y), in degrees.
// 6. (float) Foveal deformation. Defaults to 0.
class TouchStress {
public:
static TouchStress* Create(const mjModel* m, mjData* d, int instance);
TouchStress(TouchStress&&) = default;
~TouchStress() = default;
void Reset(const mjModel* m, int instance);
void Compute(const mjModel* m, mjData* d, int instance);
void Visualize(const mjModel* m, mjData* d, const mjvOption* opt,
mjvScene* scn, int instance);
static void RegisterPlugin();
int nchannel_; // number of channels (1-3)
int size_[2]; // horizontal and vertical resolution
mjtNum fov_[2]; // horizontal and vertical field of view, in degrees
mjtNum gamma_; // foveal deformation
private:
TouchStress(const mjModel* m, mjData* d, int instance, int nchannel,
int* size, mjtNum* fov_x, mjtNum gamma);
mjtNum dist_;
std::vector<mjtNum> pos_;
std::vector<mjtNum> mat_;
int id_;
int parent_weld_;
int geom_id_;
};
} // namespace mujoco::plugin::sensor
#endif // MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_
+3 -2
View File
@@ -390,8 +390,9 @@ ENUMS: Mapping[str, EnumDecl] = dict([
('mjSENS_E_POTENTIAL', 43),
('mjSENS_E_KINETIC', 44),
('mjSENS_CLOCK', 45),
('mjSENS_PLUGIN', 46),
('mjSENS_USER', 47),
('mjSENS_TACTILE', 46),
('mjSENS_PLUGIN', 47),
('mjSENS_USER', 48),
]),
)),
('mjtStage',
+3
View File
@@ -1011,6 +1011,9 @@ PYBIND11_MODULE(_specs, m) {
if (mjs_makeMesh(self, mjMESH_BUILTIN_WEDGE, params, 5)) {
throw pybind11::value_error(mjs_getError(mjs_getSpec(self->element)));
}
self->scale[0] = radius;
self->scale[1] = radius;
self->scale[2] = radius;
},
py::arg("resolution") = std::array<int, 2>{0, 0}, py::arg("radius"),
py::arg("fov") = std::array<double, 2>{0, 0}, py::arg("gamma") = 0);
+2 -1
View File
@@ -487,10 +487,11 @@ class SpecsTest(absltest.TestCase):
def test_make_mesh(self):
spec = mujoco.MjSpec()
mesh = spec.add_mesh(name='wedge')
mesh.make_wedge(resolution=[25, 25], radius=1, fov=[90, 45], gamma=0)
mesh.make_wedge(resolution=[25, 25], radius=.1, fov=[90, 45], gamma=0)
model = spec.compile()
self.assertEqual(model.nmesh, 1)
self.assertEqual(model.nmeshvert, 25 * 25)
np.testing.assert_array_equal(model.mesh_scale[0], [0.1, 0.1, 0.1])
def test_compile_errors_with_line_info(self):
spec = mujoco.MjSpec()
+15
View File
@@ -2160,6 +2160,7 @@ static int sensorSize(mjtSensor sensor_type, int sensor_dim) {
return 4;
case mjSENS_CONTACT:
case mjSENS_TACTILE:
case mjSENS_USER:
return sensor_dim;
@@ -2588,6 +2589,20 @@ const char* mj_validateReferences(const mjModel* m) {
if (nobj != -1 && (m->sensor_refid[i] < -1 || m->sensor_refid[i] >= nobj)) {
return "Invalid model: invalid sensor_refid";
}
if (sensor_type == mjSENS_TACTILE) {
int obj_id = m->sensor_objid[i];
int parent_body = m->geom_bodyid[obj_id];
int collision_geoms = 0;
for (int b = 0; b < m->body_geomnum[parent_body]; ++b) {
int geom_id = m->body_geomadr[parent_body]+b;
if (m->geom_contype[geom_id] || m->geom_conaffinity[geom_id]) {
collision_geoms++;
}
}
if (collision_geoms == 0) {
return "Touch sensor requires a body with at least one collision geom";
}
}
}
for (int i=0; i < m->nexclude; i++) {
int exclude_body1 = (m->exclude_signature[i] & 0xFFFF);
+180
View File
@@ -21,6 +21,7 @@
#include <mujoco/mjplugin.h>
#include <mujoco/mjsan.h> // IWYU pragma: keep
#include "engine/engine_callback.h"
#include "engine/engine_collision_sdf.h"
#include "engine/engine_core_smooth.h"
#include "engine/engine_crossplatform.h"
#include "engine/engine_io.h"
@@ -1082,6 +1083,185 @@ void mj_sensorAcc(const mjModel* m, mjData* d) {
}
break;
case mjSENS_TACTILE: // tactile
{
mj_markStack(d);
// get parent weld id
int mesh_id = m->sensor_objid[i];
int geom_id = m->sensor_refid[i];
int parent_body = m->geom_bodyid[geom_id];
int parent_weld = m->body_weldid[parent_body];
int nchannel = m->sensor_dim[i] / m->mesh_vertnum[mesh_id];
// clear sensordata and distance matrix
mjtNum* sensordata = d->sensordata + m->sensor_adr[i];
mju_zero(sensordata, m->sensor_dim[i]);
// count contacts and get contact geom ids
// TODO: use a more efficient C version of unordered_set
int* contact_geom_ids = mj_stackAllocInt(d, d->ncon);
int ncontact = 0;
for (int k = 0; k < d->ncon; k++) {
int body1 = m->body_weldid[m->geom_bodyid[d->contact[k].geom1]];
int body2 = m->body_weldid[m->geom_bodyid[d->contact[k].geom2]];
if (body1 == parent_weld) {
int add = 1;
for (int j = 0; j < ncontact; j++) {
if (contact_geom_ids[j] == d->contact[k].geom2) {
add = 0;
break;
}
}
if (add) {
contact_geom_ids[ncontact] = d->contact[k].geom2;
ncontact++;
}
}
if (body2 == parent_weld) {
int add = 1;
for (int j = 0; j < ncontact; j++) {
if (contact_geom_ids[j] == d->contact[k].geom1) {
add = 0;
break;
}
}
if (add) {
contact_geom_ids[ncontact] = d->contact[k].geom1;
ncontact++;
}
}
}
// no contacts, return
if (ncontact == 0) {
mj_freeStack(d);
break;
}
// all of the quadrature points are contact points
int ncon = m->mesh_vertnum[mesh_id];
// get site frame
mjtNum* geom_pos = d->geom_xpos + 3*geom_id;
mjtNum* geom_mat = d->geom_xmat + 9*geom_id;
// allocate contact forces and positions
mjtNum* forcesT = mj_stackAllocNum(d, ncon*3);
mju_zero(forcesT, ncon*3);
// iterate over colliding geoms
for (int g = 0; g < ncontact; g++) {
int geom = contact_geom_ids[g];
int body = m->geom_bodyid[geom];
// get sdf plugin of the geoms
int sdf_instance[2] = {-1, -1};
mjtGeom geomtype[2] = {mjGEOM_SDF, mjGEOM_SPHERE};
const mjpPlugin* sdf_ptr[2] = {NULL, NULL};
if (m->geom_type[geom] == mjGEOM_SDF) {
sdf_instance[0] = m->geom_plugin[geom];
sdf_ptr[0] = mjc_getSDF(m, geom);
} else if (m->geom_type[geom] == mjGEOM_MESH) {
sdf_instance[0] = m->geom_dataid[geom];
geomtype[0] = (mjtGeom)m->geom_type[geom];
} else {
sdf_instance[0] = geom;
geomtype[0] = (mjtGeom)m->geom_type[geom];
}
// skip mesh geoms not having an octree
if (geomtype[0] == mjGEOM_MESH &&
m->mesh_octadr[m->geom_dataid[geom]] == -1) {
continue;
}
// set SDF parameters
mjSDF geom_sdf;
geom_sdf.id = &sdf_instance[0];
geom_sdf.type = mjSDFTYPE_SINGLE;
geom_sdf.plugin = &sdf_ptr[0];
geom_sdf.geomtype = &geomtype[0];
// get forces in mesh coordinates
int node = 0;
float* mesh_vert = m->mesh_vert + 3*m->mesh_vertadr[mesh_id];
float* mesh_normal = m->mesh_normal + 3*m->mesh_normaladr[mesh_id];
for (int j = 0; j < ncon; j++) {
// position in site frame
mjtNum pos[3] = {mesh_vert[3 * j + 0], mesh_vert[3 * j + 1],
mesh_vert[3 * j + 2]};
// position in global frame
mjtNum xpos[3];
mju_mulMatVec3(xpos, geom_mat, pos);
mju_addTo3(xpos, geom_pos);
// position in other geom frame
mjtNum lpos[3];
mju_sub3(tmp, xpos, d->geom_xpos + 3*geom);
mju_mulMatTVec3(lpos, d->geom_xmat + 9*geom, tmp);
// SDF plugins are in the original mesh frame
if (sdf_ptr[0] != NULL) {
mjtNum mesh_mat[9];
mju_quat2Mat(mesh_mat, m->mesh_quat + 4 * m->geom_dataid[geom]);
mju_mulMatVec3(lpos, mesh_mat, lpos);
mju_addTo3(lpos, m->mesh_pos + 3 * m->geom_dataid[geom]);
}
// compute distance
mjtNum depth = mju_min(mjc_distance(m, d, &geom_sdf, lpos), 0);
if (depth == 0) {
node++;
continue;
}
// get velocity in global frame
mjtNum vel_sensor[6], vel_other[6], vel_rel[3];
mju_transformSpatial(
vel_sensor, d->cvel + 6 * parent_weld, 0, xpos,
d->subtree_com + 3 * m->body_rootid[parent_weld], NULL);
mju_transformSpatial(
vel_other, d->cvel + 6 * body, 0, d->geom_xpos + 3 * geom,
d->subtree_com + 3 * m->body_rootid[body], NULL);
mju_sub3(vel_rel, vel_sensor+3, vel_other+3);
mjtNum normal[3] = {mesh_normal[9 * j + 0], mesh_normal[9 * j + 1],
mesh_normal[9 * j + 2]};
mjtNum tang1[3] = {mesh_normal[9 * j + 3], mesh_normal[9 * j + 4],
mesh_normal[9 * j + 5]};
mjtNum tang2[3] = {mesh_normal[9 * j + 6], mesh_normal[9 * j + 7],
mesh_normal[9 * j + 8]};
// get contact force/torque, rotate into node frame
mju_rotVecQuat(normal, normal, m->mesh_quat + 4 * mesh_id);
mju_rotVecQuat(tang1, tang1, m->mesh_quat + 4 * mesh_id);
mju_rotVecQuat(tang2, tang2, m->mesh_quat + 4 * mesh_id);
mjtNum force[3];
mjtNum kMaxDepth = 0.05;
mjtNum pressure = depth / (kMaxDepth - depth);
mju_scl3(force, normal, pressure);
// one row of mat^T * force
forcesT[0*ncon + node] = mju_dot3(force, normal);
forcesT[1*ncon + node] = mju_abs(mju_dot3(vel_rel, tang1));
forcesT[2*ncon + node] = mju_abs(mju_dot3(vel_rel, tang2));
node++;
}
}
// compute sensor output
for (int c = 0; c < nchannel; c++) {
if (!mju_isZero(forcesT + c*ncon, ncon)) {
mju_addTo(sensordata + c*ncon, forcesT + c*ncon, ncon);
}
}
mj_freeStack(d);
}
break;
case mjSENS_ACCELEROMETER: // accelerometer
// tmp = site acceleration, in site frame
mj_objectAcceleration(m, d, mjOBJ_SITE, objid, tmp, 1);
+80
View File
@@ -844,6 +844,86 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt,
}
}
// tactile sensor
category = mjCAT_DECOR;
objtype = mjOBJ_UNKNOWN;
if (vopt->flags[mjVIS_CONTACTPOINT]) {
mj_markStack(d);
for (int id = 0; id < m->nsensor; id++) {
if (m->sensor_type[id] == mjSENS_TACTILE) {
// get site id and frame
int mesh_id = m->sensor_objid[id];
int geom_id = m->sensor_refid[id];
mjtNum* geom_pos = d->geom_xpos + 3*geom_id;
mjtNum* geom_mat = d->geom_xmat + 9*geom_id;
mjtNum geom_quat[4];
mju_mat2Quat(geom_quat, geom_mat);
// get sensor data
mjtNum* sensordata = d->sensordata + m->sensor_adr[id];
int nchannel = m->sensor_dim[id] / m->mesh_vertnum[mesh_id];
// get maximum absolute normal force
mjtNum maxval = 0;
for (int j=0; j < m->mesh_vertnum[mesh_id]; j++) {
maxval = mju_max(maxval, mju_abs(sensordata[j]));
}
// if no normal force readings, quick return
if (!maxval || m->geom_rbound[geom_id] < mjMINVAL) {
continue;
}
// draw geoms
float* mesh_vert = m->mesh_vert + 3*m->mesh_vertadr[mesh_id];
int* face = m->mesh_face + 3*m->mesh_faceadr[mesh_id];
for (int i=0; i < m->mesh_facenum[mesh_id]; i++) {
if (scn->ngeom >= scn->maxgeom) {
mj_warning(d, mjWARN_VGEOMFULL, scn->maxgeom);
mj_freeStack(d);
return;
} else {
// triangle in global frame
mjtNum pos[3][3];
for (int j = 0; j < 3; j++) {
mjtNum v[3] = {mesh_vert[3 * face[3 * i + j] + 0],
mesh_vert[3 * face[3 * i + j] + 1],
mesh_vert[3 * face[3 * i + j] + 2]};
mju_mulMatVec3(pos[j], geom_mat, v);
mju_addTo3(pos[j], geom_pos);
}
// color
float rgba[4] = {0, 0, 0, 1.0};
mjtNum nval[3] = {0, 0, 0};
for (int r = 0; r < mjMIN(nchannel, 3); r++) {
for (int j = 0; j < 3; j++) {
mjtNum val = sensordata[r*m->mesh_vertnum[mesh_id] + face[3*i+j]];
rgba[r] += mju_abs(val) / maxval;
if (val) {
nval[r] += 1;
}
}
if (nval[r]) {
rgba[r] /= nval[r];
}
}
// draw triangles, one per side
for (int j = 0; j < 2; j++) {
START
makeTriangle(thisgeom, pos[0], pos[j ? 1 : 2], pos[j ? 2 : 1], rgba);
thisgeom->objid = id;
FINISH
}
}
}
}
}
mj_freeStack(d);
}
// inertia
objtype = mjOBJ_BODY;
if (vopt->flags[mjVIS_INERTIA]) {
+5
View File
@@ -1166,6 +1166,11 @@ int mjs_sensorDim(const mjsSensor* sensor) {
case mjSENS_CONTACT:
return sensor->intprm[2] * mju_condataSize(sensor->intprm[0]);
case mjSENS_TACTILE:
return 3 * static_cast<const mjCMesh*>(
static_cast<mjCSensor*>(sensor->element)->get_obj())
->nvert();
case mjSENS_USER:
return sensor->dim;
+24 -2
View File
@@ -6720,9 +6720,12 @@ void mjCSensor::ResolveReferences(const mjCModel* m) {
throw mjCError(this, "unrecognized name '%s' of sensorized object", objname_.c_str());
}
// if geom mark it as non visual
// if geom or mesh, mark it as non visual
if (objtype == mjOBJ_GEOM) {
((mjCGeom*)obj)->SetNotVisual();
static_cast<mjCGeom*>(obj)->SetNotVisual();
}
if (objtype == mjOBJ_MESH) {
static_cast<mjCMesh*>(obj)->SetNotVisual();
}
} else if (type != mjSENS_E_POTENTIAL &&
@@ -6746,6 +6749,14 @@ void mjCSensor::ResolveReferences(const mjCModel* m) {
throw mjCError(this, "unrecognized name '%s' of object", refname_.c_str());
}
// if geom or mesh, mark it as non visual
if (reftype == mjOBJ_GEOM) {
static_cast<mjCGeom*>(ref)->SetNotVisual();
}
if (reftype == mjOBJ_MESH) {
static_cast<mjCMesh*>(ref)->SetNotVisual();
}
// must be attached to object with spatial frame
if (reftype != mjOBJ_BODY && reftype != mjOBJ_XBODY &&
reftype != mjOBJ_GEOM && reftype != mjOBJ_SITE && reftype != mjOBJ_CAMERA) {
@@ -7147,6 +7158,17 @@ void mjCSensor::Compile(void) {
}
break;
case mjSENS_TACTILE:
needstage = mjSTAGE_ACC;
datatype = mjDATATYPE_REAL;
if (objtype != mjOBJ_MESH) {
throw mjCError(this, "sensor must be associated with a mesh");
}
if (reftype != mjOBJ_GEOM) {
throw mjCError(this, "sensor must be associated with a geom");
}
break;
case mjSENS_PLUGIN:
datatype = mjDATATYPE_REAL; // no noise added to plugin sensors, this attribute is unused
+3
View File
@@ -1799,6 +1799,9 @@ class mjCSensor : public mjCSensor_, private mjsSensor {
const std::string& get_objname() { return spec_objname_; }
const std::string& get_refname() { return spec_refname_; }
const mjCBase* get_obj() { return obj; }
const mjCBase* get_ref() { return ref; }
private:
void Compile(void); // compiler
void CopyFromSpec();
+13
View File
@@ -492,6 +492,7 @@ const char* MJCF[nMJCF][mjXATTRNUM] = {
{"clock", "*", "4", "name", "cutoff", "noise", "user"},
{"user", "*", "9", "name", "objtype", "objname", "datatype", "needstage",
"dim", "cutoff", "noise", "user"},
{"tactile", "*", "4", "name", "geom", "mesh", "user"},
{"plugin", "*", "9", "name", "plugin", "instance", "cutoff", "objtype", "objname", "reftype", "refname",
"user"},
{"<"},
@@ -4297,6 +4298,18 @@ void mjXReader::Sensor(XMLElement* section) {
}
}
// tactile sensor
if (type == "tactile") {
sensor->type = mjSENS_TACTILE;
sensor->reftype = mjOBJ_GEOM;
ReadAttrTxt(elem, "geom", refname, /*required=*/true);
// associate the sensor with a mesh
sensor->objtype = mjOBJ_MESH;
ReadAttrTxt(elem, "mesh", objname, /*required=*/true);
mjs_setString(sensor->objname, objname.c_str());
}
else if (type == "plugin") {
sensor->type = mjSENS_PLUGIN;
OnePlugin(elem, &sensor->plugin);
+1 -1
View File
@@ -101,7 +101,7 @@ class mjXReader : public mjXBase {
};
// MJCF schema
#define nMJCF 240
#define nMJCF 241
extern const char* MJCF[nMJCF][mjXATTRNUM];
#endif // MUJOCO_SRC_XML_XML_NATIVE_READER_H_
+5
View File
@@ -2238,6 +2238,11 @@ void mjXWriter::Sensor(XMLElement* root) {
WriteAttrKey(elem, "reduce", reduce_map, reduce_sz, sensor->intprm[1], 0);
}
break;
case mjSENS_TACTILE:
elem = InsertEnd(section, "tactile");
WriteAttrTxt(elem, "geom", sensor->get_refname());
WriteAttrTxt(elem, "mesh", sensor->get_objname());
break;
// global sensors
case mjSENS_E_POTENTIAL:
elem = InsertEnd(section, "potential");
+1 -1
View File
@@ -37,7 +37,7 @@ using ::testing::DoubleNear;
using ::testing::HasSubstr;
using ::testing::NotNull;
constexpr int kNumTruePlugins = 9;
constexpr int kNumTruePlugins = 8;
constexpr int kNumFakePlugins = 30;
constexpr int kNumTestPlugins = 4;
+12 -7
View File
@@ -232,12 +232,16 @@ mjtNum CompareModel(const mjModel* m1, const mjModel* m2,
MJMODEL_POINTERS_PREAMBLE(m1);
// compare ints, exclude nbuffer because it hides the actual difference
#define X(name) \
if constexpr (std::string_view(#name) != "nbuffer") { \
if (m1->name != m2->name) { \
maxdif = std::abs((long)m1->name - (long)m2->name); \
field = #name; \
} \
// TODO(kylebayes): re-enable poly comparisons.
#define X(name) \
if constexpr (std::string_view(#name) != "nbuffer" && \
std::string_view(#name) != "nmeshpolymap" && \
std::string_view(#name) != "nmeshpolyvert" && \
std::string_view(#name) != "nmeshpoly") { \
if (m1->name != m2->name) { \
maxdif = std::abs((long)m1->name - (long)m2->name); \
field = #name; \
} \
}
MJMODEL_INTS
#undef X
@@ -247,7 +251,8 @@ mjtNum CompareModel(const mjModel* m1, const mjModel* m2,
// those are sensitive to numerical differences when meshes are perfectly
// symmetric.
#define X(type, name, nr, nc) \
if (strncmp(#name, "bvh_", 4) && strncmp(#name, "flex_vert0", 4)) { \
if (strncmp(#name, "bvh_", 4) && strncmp(#name, "flex_vert0", 4) && \
strncmp(#name, "mesh_poly", 4)) { \
for (int r = 0; r < m1->nr; r++) { \
for (int c = 0; c < nc; c++) { \
dif = Compare(m1->name[r * nc + c], m2->name[r * nc + c]); \
+1
View File
@@ -1386,6 +1386,7 @@ TEST_F(XMLWriterTest, WriteReadCompare) {
absl::StrContains(p.path().string(), "shark_") ||
absl::StrContains(p.path().string(), "spheremesh") ||
// exclude files that fail the comparison test
absl::StrContains(p.path().string(), "tactile") ||
absl::StrContains(p.path().string(), "makemesh") ||
absl::StrContains(p.path().string(), "usd") ||
absl::StrContains(p.path().string(), "torus_maxhull") ||
+3 -2
View File
@@ -398,8 +398,9 @@ public enum mjtSensor : int{
mjSENS_E_POTENTIAL = 43,
mjSENS_E_KINETIC = 44,
mjSENS_CLOCK = 45,
mjSENS_PLUGIN = 46,
mjSENS_USER = 47,
mjSENS_TACTILE = 46,
mjSENS_PLUGIN = 47,
mjSENS_USER = 48,
}
public enum mjtStage : int{
mjSTAGE_NONE = 0,