Move tactile sensor to the engine.
PiperOrigin-RevId: 788863133 Change-Id: I3172ebb7641fa8146469da25cba7db794760e7b1
This commit is contained in:
committed by
Copybara-Service
parent
89f4789085
commit
51babec9c5
@@ -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** (*)
|
||||
|
||||
@@ -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>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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]); \
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user