Add shellinertia support for primitives: sphere, capsule, cylinder, ellipsoid, and box.

PiperOrigin-RevId: 661407997
Change-Id: Icddac58c540c71ea26ac7a3be874788443bd1122
This commit is contained in:
Taylor Howell
2024-08-09 14:39:26 -07:00
committed by Copybara-Service
parent ec43ec7c30
commit 466368efc6
5 changed files with 653 additions and 79 deletions
+3 -2
View File
@@ -2451,14 +2451,15 @@ helps clarify the role of bodies and geoms in MuJoCo.
given mass, using the geom shape and the assumption of uniform density. The computed density is then used to obtain
the geom inertia. Recall that the geom mass and inertia are only used during compilation, to infer the body mass and
inertia if necessary. At runtime only the body inertial properties affect the simulation; the geom mass and inertia
are not even saved in mjModel.
are not saved in mjModel.
.. _body-geom-density:
:at:`density`: :at-val:`real, "1000"`
Material density used to compute the geom mass and inertia. The computation is based on the geom shape and the
assumption of uniform density. The internal default of 1000 is the density of water in SI units. This attribute is
used only when the mass attribute above is unspecified.
used only when the mass attribute above is unspecified. If `shellinertia` is "false" (the default), density has
semantics of mass/volume; if "true", it has semantics of mass/area.
.. _body-geom-shellinertia:
+9 -1
View File
@@ -2,6 +2,13 @@
Changelog
=========
Upcoming version (not yet released)
-----------------------------------
General
^^^^^^^
- :ref:`shellinertia <body-geom-shellinertia>` is now supported by all geom types.
Version 3.2.2 (Aug 8, 2024)
---------------------------
@@ -34,7 +41,8 @@ MJX
Python bindings
^^^^^^^^^^^^^^^
10. Added support for asset dictionary argument in ``mujoco.spec.from_file``, ``mujoco.spec.from_string`` and
``mujoco.spec.compile``.
``mujoco.spec.compile``.
Bug fixes
^^^^^^^^^
+8 -4
View File
@@ -851,10 +851,14 @@ Geoms (short for geometric primitive) are used to specify appearance and collisi
and is rigidly attached to that body. Multiple geoms can be attached to the same body. This is particularly useful in
light of the fact that MuJoCo's collision detector assumes that all geoms are convex (it internally replaces meshes with
their convex hulls if the meshes are not convex). Thus if you want to model a non-convex shape, you have to decompose it
into a union of convex geoms and attach all of them to the same body. Geoms can also have mass and inertia in the XML
model (or rather material density which is used to compute the mass and inertia), but that is only used to compute the
body mass and inertia in the model compiler. In the actual ``mjModel`` being simulated geoms do not have inertial
properties.
into a union of convex geoms and attach all of them to the same body.
A geom can also have density or mass values specified in the XML, which the model compiler uses to compute the parent
body's mass and inertia. Mass is either specified or computed from a geom's volume and :ref:`density
<body-geom-density>`. Inertia is computed from the mass, shape, and uniform density assumption. If the
:ref:`shellinertia <body-geom-shellinertia>` flag is set, mass is assumed to be uniformly distributed on the **surface**,
:at:`density` is interpreted as mass-per-area, and the inertia contribution to the parent body is computed accordingly.
In the actual ``mjModel`` being simulated, geoms do not have inertial properties.
Sites are light geoms. They have the same appearance properties but cannot participate in collisions and cannot be used
to infer body masses. On the other hand sites can do things that geoms cannot do: they can specify the volumes of touch
+257 -72
View File
@@ -38,6 +38,7 @@
#include <mujoco/mjmodel.h>
#include <mujoco/mjplugin.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mujoco.h>
#include "cc/array_safety.h"
#include "engine/engine_passive.h"
#include <mujoco/mjspec.h>
@@ -2057,10 +2058,8 @@ void mjCGeom::NameSpace(const mjCModel* m) {
// compute geom volume
// compute geom volume / surface area
double mjCGeom::GetVolume() const {
double height;
// get from mesh
if (type==mjGEOM_MESH || type==mjGEOM_SDF) {
if (mesh->id<0 || !((std::size_t) mesh->id <= model->Meshes().size())) {
@@ -2070,30 +2069,74 @@ double mjCGeom::GetVolume() const {
return mesh->GetVolumeRef(typeinertia);
}
// compute from geom shape
else {
switch (type) {
case mjGEOM_SPHERE:
return 4*mjPI*size[0]*size[0]*size[0]/3;
case mjGEOM_CAPSULE:
height = 2*size[1];
return mjPI*(size[0]*size[0]*height + 4*size[0]*size[0]*size[0]/3);
case mjGEOM_CYLINDER:
height = 2*size[1];
return mjPI*size[0]*size[0]*height;
case mjGEOM_ELLIPSOID:
return 4*mjPI*size[0]*size[1]*size[2]/3;
// compute from geom shape (type) and inertia (typeinertia)
switch (type) {
case mjGEOM_SPHERE: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
return 4 * mjPI * size[0] * size[0];
}
case mjINERTIA_VOLUME: {
return 4 * mjPI * size[0] * size[0] * size[0] / 3;
}
}
}
case mjGEOM_CAPSULE: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
double radius = size[0];
double height = 2 * size[1];
return 4 * mjPI * radius * radius + 2 * mjPI * radius * height;
}
case mjINERTIA_VOLUME: {
double height = 2 * size[1];
return mjPI * (size[0] * size[0] * height + 4 * size[0] * size[0] * size[0] / 3);
}
}
}
case mjGEOM_CYLINDER: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
double radius = size[0];
double height = 2 * size[1];
return 2 * mjPI * radius * radius + 2 * mjPI * radius * height;
}
case mjINERTIA_VOLUME: {
double height = 2 * size[1];
return mjPI * size[0] * size[0] * height;
}
}
}
case mjGEOM_ELLIPSOID: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
// Thomsen approximation
// https://www.numericana.com/answer/ellipsoid.htm#thomsen
double p = 1.6075;
double tmp = mju_pow(size[0] * size[1], p) +
mju_pow(size[1] * size[2], p) +
mju_pow(size[2] * size[0], p);
return 4 * mjPI * mju_pow(tmp / 3, 1 / p);
}
case mjINERTIA_VOLUME: {
return 4 * mjPI * size[0] * size[1] * size[2] / 3;
}
}
}
case mjGEOM_HFIELD:
case mjGEOM_BOX:
return size[0]*size[1]*size[2]*8;
case mjGEOM_BOX: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
return 8 * (size[0] * size[1] + size[1] * size[2] + size[2] * size[0]);
}
case mjINERTIA_VOLUME: {
return size[0] * size[1] * size[2] * 8;
}
}
}
default:
return 0;
}
}
}
@@ -2112,68 +2155,210 @@ void mjCGeom::SetBoundingVolume(mjCBoundingVolume* bv) const {
// set geom diagonal inertia given density
void mjCGeom::SetInertia(void) {
double height;
// get from mesh
if (type==mjGEOM_MESH || type==mjGEOM_SDF) {
if (mesh->id<0 || !((std::size_t) mesh->id <= model->Meshes().size())) {
if (type == mjGEOM_MESH || type == mjGEOM_SDF) {
if (mesh->id < 0 || !((std::size_t)mesh->id <= model->Meshes().size())) {
throw mjCError(this, "invalid mesh id in mesh geom");
}
double* boxsz = mesh->GetInertiaBoxPtr(typeinertia);
inertia[0] = mass_*(boxsz[1]*boxsz[1] + boxsz[2]*boxsz[2]) / 3;
inertia[1] = mass_*(boxsz[0]*boxsz[0] + boxsz[2]*boxsz[2]) / 3;
inertia[2] = mass_*(boxsz[0]*boxsz[0] + boxsz[1]*boxsz[1]) / 3;
inertia[0] = mass_ * (boxsz[1] * boxsz[1] + boxsz[2] * boxsz[2]) / 3;
inertia[1] = mass_ * (boxsz[0] * boxsz[0] + boxsz[2] * boxsz[2]) / 3;
inertia[2] = mass_ * (boxsz[0] * boxsz[0] + boxsz[1] * boxsz[1]) / 3;
return;
}
// compute from geom shape
else {
if (typeinertia)
throw mjCError(this, "typeinertia currently only available for meshes'%s' (id = %d)",
name.c_str(), id);
switch (type) {
case mjGEOM_SPHERE:
inertia[0] = inertia[1] = inertia[2] = 2*mass_*size[0]*size[0]/5;
return;
case mjGEOM_CAPSULE: {
height = 2*size[1];
double radius = size[0];
double sphere_mass = mass_*4*radius/(4*radius + 3*height); // mass*(sphere_vol/total_vol)
double cylinder_mass = mass_ - sphere_mass;
// cylinder part
inertia[0] = inertia[1] = cylinder_mass*(3*radius*radius + height*height)/12;
inertia[2] = cylinder_mass*radius*radius/2;
// add two hemispheres, displace along third axis
double sphere_inertia = 2*sphere_mass*radius*radius/5;
inertia[0] += sphere_inertia + sphere_mass*height*(3*radius + 2*height)/8;
inertia[1] += sphere_inertia + sphere_mass*height*(3*radius + 2*height)/8;
inertia[2] += sphere_inertia;
return;
// compute from geom shape (type) and inertia (typeinertia)
switch (type) {
case mjGEOM_SPHERE: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
inertia[0] = inertia[1] = inertia[2] = 2 * mass_ * size[0] * size[0] / 3;
return;
}
case mjINERTIA_VOLUME: {
inertia[0] = inertia[1] = inertia[2] = 2 * mass_ * size[0] * size[0] / 5;
return;
}
}
}
case mjGEOM_CAPSULE: {
double halfheight = size[1];
double height = 2 * size[1];
double radius = size[0];
switch (typeinertia) {
case mjINERTIA_SHELL: {
// surface area
double Asphere = 4 * mjPI * radius * radius;
double Acylinder = 2 * mjPI * radius * height;
double Atotal = Asphere + Acylinder;
case mjGEOM_CYLINDER:
height = 2*size[1];
inertia[0] = inertia[1] = mass_*(3*size[0]*size[0]+height*height)/12;
inertia[2] = mass_*size[0]*size[0]/2;
return;
// mass
double sphere_mass = mass_ * Asphere / Atotal; // mass*(sphere_area/total_area)
double cylinder_mass = mass_ - sphere_mass;
case mjGEOM_ELLIPSOID:
inertia[0] = mass_*(size[1]*size[1]+size[2]*size[2])/5;
inertia[1] = mass_*(size[0]*size[0]+size[2]*size[2])/5;
inertia[2] = mass_*(size[0]*size[0]+size[1]*size[1])/5;
return;
// cylinder part
inertia[0] = inertia[1] = cylinder_mass * (6 * radius * radius + height * height) / 12;
inertia[2] = cylinder_mass * radius * radius;
// add two hemispheres, displace along third axis
double sphere_inertia = 2 * sphere_mass * radius * radius / 3;
double hs_com = radius / 2; // hemisphere center of mass
double hs_pos = halfheight + hs_com; // hemisphere position
inertia[0] += sphere_inertia + sphere_mass * (hs_pos * hs_pos - hs_com * hs_com);
inertia[1] += sphere_inertia + sphere_mass * (hs_pos * hs_pos - hs_com * hs_com);
inertia[2] += sphere_inertia;
return;
}
case mjINERTIA_VOLUME: {
double sphere_mass =
mass_ * 4 * radius / (4 * radius + 3 * height); // mass*(sphere_vol/total_vol)
double cylinder_mass = mass_ - sphere_mass;
// cylinder part
inertia[0] = inertia[1] = cylinder_mass * (3 * radius * radius + height * height) / 12;
inertia[2] = cylinder_mass * radius * radius / 2;
// add two hemispheres, displace along third axis
double sphere_inertia = 2 * sphere_mass * radius * radius / 5;
inertia[0] += sphere_inertia + sphere_mass * height * (3 * radius + 2 * height) / 8;
inertia[1] += sphere_inertia + sphere_mass * height * (3 * radius + 2 * height) / 8;
inertia[2] += sphere_inertia;
return;
}
}
}
case mjGEOM_CYLINDER: {
double halfheight = size[1];
double height = 2 * halfheight;
double radius = size[0];
switch (typeinertia) {
case mjINERTIA_SHELL: {
// surface area
double Adisk = mjPI * radius * radius;
double Acylinder = 2 * mjPI * radius * height;
double Atotal = 2 * Adisk + Acylinder;
// mass
double mass_disk = mass_ * Adisk / Atotal;
double mass_cylinder = mass_ - 2 * mass_disk;
// cylinder contribution
inertia[0] = inertia[1] = mass_cylinder * (6 * radius * radius + height * height) / 12;
inertia[2] = mass_cylinder * radius * radius;
// disk inertia
double inertia_disk_x = mass_disk * radius * radius / 4 +
mass_disk * halfheight * halfheight;
double inertia_disk_z = mass_disk * radius * radius / 2;
// top and bottom disk contributions
inertia[0] += 2 * inertia_disk_x;
inertia[1] += 2 * inertia_disk_x;
inertia[2] += 2 * inertia_disk_z;
return;
}
case mjINERTIA_VOLUME: {
inertia[0] = inertia[1] = mass_ * (3 * radius * radius + height * height) / 12;
inertia[2] = mass_ * radius * radius / 2;
return;
}
}
}
case mjGEOM_ELLIPSOID: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
// approximate shell inertia by subtracting ellipsoid from expanded
// ellipsoid
double eps = 1e-6;
// solid volume (a)
double Va = 4 * mjPI * size[0] * size[1] * size[2] / 3;
// expanded volume (b)
double ae = size[0] + eps;
double be = size[1] + eps;
double ce = size[2] + eps;
double Vb = 4 * mjPI * ae * be * ce / 3;
// density
double density = mass_ / (Vb - Va);
// inertia
double mass_a = Va * density;
double inertia_a[3];
inertia_a[0] = mass_a * (size[1] * size[1] + size[2] * size[2]) / 5;
inertia_a[1] = mass_a * (size[0] * size[0] + size[2] * size[2]) / 5;
inertia_a[2] = mass_a * (size[0] * size[0] + size[1] * size[1]) / 5;
double mass_b = Vb * density;
double inertia_b[3];
inertia_b[0] = mass_b * (be * be + ce * ce) / 5;
inertia_b[1] = mass_b * (ae * ae + ce * ce) / 5;
inertia_b[2] = mass_b * (ae * ae + be * be) / 5;
// shell inertia
mju_sub3(inertia, inertia_b, inertia_a);
return;
}
case mjINERTIA_VOLUME: {
inertia[0] = mass_ * (size[1] * size[1] + size[2] * size[2]) / 5;
inertia[1] = mass_ * (size[0] * size[0] + size[2] * size[2]) / 5;
inertia[2] = mass_ * (size[0] * size[0] + size[1] * size[1]) / 5;
return;
}
}
}
case mjGEOM_HFIELD:
case mjGEOM_BOX:
inertia[0] = mass_*(size[1]*size[1]+size[2]*size[2])/3;
inertia[1] = mass_*(size[0]*size[0]+size[2]*size[2])/3;
inertia[2] = mass_*(size[0]*size[0]+size[1]*size[1])/3;
return;
case mjGEOM_BOX: {
switch (typeinertia) {
case mjINERTIA_SHELL: {
// length
double lx = 2 * size[0]; // side 0
double ly = 2 * size[1]; // side 1
double lz = 2 * size[2]; // side 2
default:
inertia[0] = inertia[1] = inertia[2] = 0;
return;
// surface area
double A0 = lx * ly; // side 0
double A1 = ly * lz; // side 1
double A2 = lz * lx; // side 2
double Atotal = 2 * (A0 + A1 + A2);
// side 0
double mass0 = mass_ * A0 / Atotal;
double Ix0 = mass0 * ly * ly / 12;
double Iy0 = mass0 * lx * lx / 12;
double Iz0 = mass0 * (lx * lx + ly * ly) / 12;
// side 1
double mass1 = mass_ * A1 / Atotal;
double Ix1 = mass1 * (ly * ly + lz * lz) / 12;
double Iy1 = mass1 * lz * lz / 12;
double Iz1 = mass1 * ly * ly / 12;
// side 3
double mass2 = mass_ * A2 / Atotal;
double Ix2 = mass2 * lz * lz / 12;
double Iy2 = mass2 * (lx * lx + lz * lz) / 12;
double Iz2 = mass2 * lx * lx / 12;
// total inertia
inertia[0] = 2 * (Ix0 + mass0 * size[2] * size[2] + Ix1 + Ix2 + mass2 * size[1] * size[1]);
inertia[1] = 2 * (Iy0 + mass0 * size[2] * size[2] + Iy1 + mass1 * size[0] * size[0] + Iy2);
inertia[2] = 2 * (Iz0 + Iz1 + mass1 * size[0] * size[0] + Iz2 + mass2 * size[1] * size[1]);
return;
}
case mjINERTIA_VOLUME: {
inertia[0] = mass_ * (size[1] * size[1] + size[2] * size[2]) / 3;
inertia[1] = mass_ * (size[0] * size[0] + size[2] * size[2]) / 3;
inertia[2] = mass_ * (size[0] * size[0] + size[1] * size[1]) / 3;
return;
}
}
default:
inertia[0] = inertia[1] = inertia[2] = 0;
return;
}
}
}
+376
View File
@@ -32,6 +32,8 @@
namespace mujoco {
namespace {
constexpr double kInertiaTol = 1e-6;
std::vector<mjtNum> AsVector(const mjtNum* array, int n) {
return std::vector<mjtNum>(array, array + n);
}
@@ -700,6 +702,380 @@ TEST_F(MjCGeomTest, CapsuleInertiaX) {
mj_deleteModel(model);
}
TEST_F(MjCGeomTest, ShellInertiaSphere) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="sphere"size="1.5" shellinertia="true"/>
</body>
<body>
<!-- mass is difference of body 4 and 3 masses -->
<geom type="sphere" size="1.5" mass="28.274333953857422" shellinertia="true"/>
</body>
<body>
<geom type="sphere" size="1.5" density="1e8"/>
</body>
<body>
<geom type="sphere" size="1.50000001" density="1e8"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1000> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
// radius
mjtNum r = 1.5;
mjtNum r2 = r * r;
// body 1: shell inertia
mjtNum mass1 = 4 * mjPI * r2 * 1000; // surface area * surface density
EXPECT_NEAR(m->body_mass[1], mass1, kInertiaTol);
mjtNum I1 = 2 * mass1 * r2 / 3;
EXPECT_NEAR(m->body_inertia[3], I1, kInertiaTol);
EXPECT_NEAR(m->body_inertia[4], I1, kInertiaTol);
EXPECT_NEAR(m->body_inertia[5], I1, kInertiaTol);
// body 2: shell inertia, with specified mass
mjtNum I2 = 2 * m->body_mass[2] * r2 / 3;
EXPECT_NEAR(m->body_inertia[6], I2, kInertiaTol);
EXPECT_NEAR(m->body_inertia[7], I2, kInertiaTol);
EXPECT_NEAR(m->body_inertia[8], I2, kInertiaTol);
mjtNum mass3 = m->body_mass[3];
mjtNum mass4 = m->body_mass[4];
EXPECT_FLOAT_EQ(mass4 - mass3, m->body_mass[2]);
// compute approximate shell inertia by subtracting inertias of massive bodies
// with small radius difference
mjtNum* inertia3 = m->body_inertia + 9;
mjtNum* inertia4 = m->body_inertia + 12;
mjtNum shell_inertia[3];
mju_sub3(shell_inertia, inertia4, inertia3);
EXPECT_NEAR(shell_inertia[0], m->body_inertia[6], kInertiaTol);
EXPECT_NEAR(shell_inertia[1], m->body_inertia[7], kInertiaTol);
EXPECT_NEAR(shell_inertia[2], m->body_inertia[8], kInertiaTol);
mj_deleteModel(m);
}
TEST_F(MjCGeomTest, ShellInertiaCapsule) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="capsule" size="0.1 0.25" shellinertia="true"/>
</body>
<!-- mass is difference of body 4 and 3 masses -->
<body>
<geom type="capsule" size="0.1 0.25" mass="4.3982325" shellinertia="true"/>
</body>
<body>
<geom type="capsule" size="0.1 0.25" density="1e8"/>
</body>
<body>
<geom type="capsule" size="0.1000001 0.25" density="1e8"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1000> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
// dimensions
mjtNum r = 0.1;
mjtNum r2 = r * r;
mjtNum hh = 0.25;
mjtNum h = 2 * hh;
mjtNum h2 = h * h;
// hemisphere
mjtNum hs_com = r / 2; // height of hemisphere center of mass
mjtNum hs_pos = hh + hs_com; // distance from origin to hemisphere com
// surface area
double Asphere = 4 * mjPI * r2; // sphere
double Acylinder = 2 * mjPI * r * h; // cylinder
double Atotal = Asphere + Acylinder;
// body 1: shell inertia
mjtNum mass1 = Atotal * 1000; // surface area * surface density
EXPECT_NEAR(m->body_mass[1], mass1, kInertiaTol);
mjtNum mass1_sphere = mass1 * Asphere / Atotal;
mjtNum mass1_cylinder = mass1 - mass1_sphere;
double sphere1_inertia = 2 * mass1_sphere * r2 / 3;
mjtNum I1x = mass1_cylinder * (6 * r2 + h2) / 12 + sphere1_inertia +
mass1_sphere * (hs_pos * hs_pos - hs_com * hs_com);
mjtNum I1z = mass1_cylinder * r2 + sphere1_inertia;
EXPECT_NEAR(m->body_inertia[3], I1x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[4], I1x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[5], I1z, kInertiaTol);
// body 2: shell inertia, with specified mass
mjtNum mass2 = 4.3982325;
EXPECT_NEAR(m->body_mass[2], mass2, kInertiaTol);
EXPECT_FLOAT_EQ(m->body_mass[4] - m->body_mass[3], m->body_mass[2]);
mjtNum mass2_sphere = mass2 * Asphere / Atotal;
mjtNum mass2_cylinder = mass2 - mass2_sphere;
double sphere2_inertia = 2 * mass2_sphere * r2 / 3;
mjtNum I2x = mass2_cylinder * (6 * r2 + h2) / 12 + sphere2_inertia +
mass2_sphere * (hs_pos * hs_pos - hs_com * hs_com);
mjtNum I2z = mass2_cylinder * r2 + sphere2_inertia;
EXPECT_NEAR(m->body_inertia[6], I2x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[7], I2x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[8], I2z, kInertiaTol);
// compute approximate shell inertia by subtracting inertias of massive bodies
// with small radius difference
mjtNum* inertia3 = m->body_inertia + 9;
mjtNum* inertia4 = m->body_inertia + 12;
mjtNum shell_inertia[3];
mju_sub3(shell_inertia, inertia4, inertia3);
EXPECT_NEAR(shell_inertia[0], m->body_inertia[6], kInertiaTol);
EXPECT_NEAR(shell_inertia[1], m->body_inertia[7], kInertiaTol);
EXPECT_NEAR(shell_inertia[2], m->body_inertia[8], kInertiaTol);
mj_deleteModel(m);
}
TEST_F(MjCGeomTest, ShellInertiaCylinder) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="cylinder" size="0.1 0.25" shellinertia="true"/>
</body>
<!-- mass is difference of body 4 and 3 masses -->
<body>
<geom type="cylinder" size="0.1 0.25" mass="3.7699139" shellinertia="true"/>
</body>
<body>
<geom type="cylinder" size="0.1 0.25" density="1e8"/>
</body>
<body>
<geom type="cylinder" size="0.1000001 0.2500001" density="1e8"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1000> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
// dimensions
mjtNum r = 0.1;
mjtNum hh = 0.25;
mjtNum r2 = r * r;
mjtNum h = 2 * hh;
mjtNum h2 = h * h;
// surface area
double Adisk = mjPI * r2; // disk
double Acylinder = 2 * mjPI * r * h; // cylinder
double Atotal = 2 * Adisk + Acylinder;
// body 1: shell inertia
mjtNum mass1 = Atotal * 1000; // surface area * surface density
EXPECT_NEAR(m->body_mass[1], mass1, kInertiaTol);
mjtNum mass1_disk = mass1 * Adisk / Atotal;
mjtNum mass1_cylinder = mass1 - 2 * mass1_disk;
mjtNum I1x = mass1_cylinder * (6 * r2 + h2) / 12 +
2 * (mass1_disk * r2 / 4 + mass1_disk * hh * hh);
mjtNum I1z = mass1_cylinder * r2 + mass1_disk * r2;
EXPECT_NEAR(m->body_inertia[3], I1x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[4], I1x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[5], I1z, kInertiaTol);
// body 2: shell inertia, with specified mass
mjtNum mass2 = 3.7699139;
EXPECT_NEAR(m->body_mass[2], mass2, kInertiaTol);
EXPECT_FLOAT_EQ(m->body_mass[4] - m->body_mass[3], m->body_mass[2]);
mjtNum mass2_disk = mass2 * Adisk / Atotal;
mjtNum mass2_cylinder = mass2 - 2 * mass2_disk;
mjtNum I2x = mass2_cylinder * (6 * r2 + h2) / 12 +
2 * (mass2_disk * r2 / 4 + mass2_disk * hh * hh);
mjtNum I2z = mass2_cylinder * r2 + mass2_disk * r2;
EXPECT_NEAR(m->body_inertia[6], I2x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[7], I2x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[8], I2z, kInertiaTol);
// compute approximate shell inertia by subtracting inertias of massive bodies
// with small radius difference
mjtNum* inertia3 = m->body_inertia + 9;
mjtNum* inertia4 = m->body_inertia + 12;
mjtNum shell_inertia[3];
mju_sub3(shell_inertia, inertia4, inertia3);
EXPECT_NEAR(shell_inertia[0], m->body_inertia[6], kInertiaTol);
EXPECT_NEAR(shell_inertia[1], m->body_inertia[7], kInertiaTol);
EXPECT_NEAR(shell_inertia[2], m->body_inertia[8], kInertiaTol);
mj_deleteModel(m);
}
TEST_F(MjCGeomTest, ShellInertiaEllipsoid) {
// test special case of ellipsoid with dimensions: a = b = c
// TODO(taylorhowell): add test for ellipsoid with dimensions: a != b != c
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="ellipsoid" size="0.1 0.1 0.1" shellinertia="true"/>
</body>
<body>
<!-- mass is difference of body 4 and 3 masses -->
<geom type="ellipsoid" size="0.1 0.1 0.1" mass="0.12566371" shellinertia="true"/>
</body>
<body>
<geom type="ellipsoid" size="0.1 0.1 0.1" density="1e8"/>
</body>
<body>
<geom type="ellipsoid" size="0.10000001 0.10000001 0.10000001" density="1e8"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1000> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
// dimensions
mjtNum r = 0.1;
mjtNum r2 = r * r;
// body 1: shell inertia
mjtNum mass1 = 4 * mjPI * r2 * 1000; // surface area * surface density
EXPECT_NEAR(m->body_mass[1], mass1, kInertiaTol);
mjtNum I1 = 2 * mass1 * r2 / 3;
// note: increased tolerance, this is due to ellipsoid approximation
EXPECT_NEAR(m->body_inertia[3], I1, 10 * kInertiaTol);
EXPECT_NEAR(m->body_inertia[4], I1, 10 * kInertiaTol);
EXPECT_NEAR(m->body_inertia[5], I1, 10 * kInertiaTol);
// body 2: shell inertia, with specified mass
mjtNum mass2 = 0.12566371;
EXPECT_NEAR(m->body_mass[2], mass2, kInertiaTol);
EXPECT_FLOAT_EQ(m->body_mass[4] - m->body_mass[3], m->body_mass[2]);
mjtNum I2 = 2 * m->body_mass[2] * r2 / 3;
EXPECT_NEAR(m->body_inertia[6], I2, 10 * kInertiaTol);
EXPECT_NEAR(m->body_inertia[7], I2, 10 * kInertiaTol);
EXPECT_NEAR(m->body_inertia[8], I2, 10 * kInertiaTol);
// compute approximate shell inertia by subtracting inertias of massive bodies
// with small radius difference
mjtNum* inertia3 = m->body_inertia + 9;
mjtNum* inertia4 = m->body_inertia + 12;
mjtNum shell_inertia[3];
mju_sub3(shell_inertia, inertia4, inertia3);
EXPECT_NEAR(shell_inertia[0], m->body_inertia[6], 10 * kInertiaTol);
EXPECT_NEAR(shell_inertia[1], m->body_inertia[7], 10 * kInertiaTol);
EXPECT_NEAR(shell_inertia[2], m->body_inertia[8], 10 * kInertiaTol);
mj_deleteModel(m);
}
TEST_F(MjCGeomTest, ShellInertiaBox) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body>
<geom type="box" size="0.1 0.2 0.3" shellinertia="true"/>
</body>
<!-- mass is difference of body 4 and 3 masses -->
<body>
<geom type="box" size="0.1 0.2 0.3" mass="8.800005" shellinertia="true"/>
</body>
<body>
<geom type="box" size="0.1 0.2 0.3" density="1e8"/>
</body>
<body>
<geom type="box" size="0.1000001 0.2000001 0.3000001" density="1e8"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1000> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
// dimensions
mjtNum dx = 0.1;
mjtNum dy = 0.2;
mjtNum dz = 0.3;
// length
mjtNum lx = 2 * dx;
mjtNum ly = 2 * dy;
mjtNum lz = 2 * dz;
// surface area
double A0 = lx * ly;
double A1 = ly * lz;
double A2 = lz * lx;
double Atotal = 2 * (A0 + A1 + A2);
// body 1: shell inertia
mjtNum mass1 = Atotal * 1000; // surface area * surface density
EXPECT_NEAR(m->body_mass[1], mass1, kInertiaTol);
mjtNum mass1_0 = mass1 * A0 / Atotal;
mjtNum mass1_1 = mass1 * A1 / Atotal;
mjtNum mass1_2 = mass1 * A2 / Atotal;
mjtNum I1x = 2 * (mass1_0 * ly * ly / 12 + mass1_0 * dz * dz +
mass1_1 * (ly * ly + lz * lz) / 12 +
mass1_2 * lz * lz / 12 + mass1_2 * dy * dy);
mjtNum I1y =
2 * (mass1_0 * lx * lx / 12 + mass1_0 * dz * dz + mass1_1 * lz * lz / 12 +
mass1_1 * dx * dx + mass1_2 * (lx * lx + lz * lz) / 12);
mjtNum I1z =
2 * (mass1_0 * (lx * lx + ly * ly) / 12 + mass1_1 * ly * ly / 12 +
mass1_1 * dx * dx + mass1_2 * lx * lx / 12 + mass1_2 * dy * dy);
EXPECT_NEAR(m->body_inertia[3], I1x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[4], I1y, kInertiaTol);
EXPECT_NEAR(m->body_inertia[5], I1z, kInertiaTol);
// body 2: shell inertia, with specified mass
mjtNum mass2 = 8.800005;
EXPECT_NEAR(m->body_mass[2], mass2, 1e-6);
EXPECT_FLOAT_EQ(m->body_mass[4] - m->body_mass[3], m->body_mass[2]);
mjtNum mass2_0 = mass2 * A0 / Atotal;
mjtNum mass2_1 = mass2 * A1 / Atotal;
mjtNum mass2_2 = mass2 * A2 / Atotal;
mjtNum I2x = 2 * (mass2_0 * ly * ly / 12 + mass2_0 * dz * dz +
mass2_1 * (ly * ly + lz * lz) / 12 +
mass2_2 * lz * lz / 12 + mass2_2 * dy * dy);
mjtNum I2y =
2 * (mass2_0 * lx * lx / 12 + mass2_0 * dz * dz + mass2_1 * lz * lz / 12 +
mass2_1 * dx * dx + mass2_2 * (lx * lx + lz * lz) / 12);
mjtNum I2z =
2 * (mass2_0 * (lx * lx + ly * ly) / 12 + mass2_1 * ly * ly / 12 +
mass2_1 * dx * dx + mass2_2 * lx * lx / 12 + mass2_2 * dy * dy);
EXPECT_NEAR(m->body_inertia[6], I2x, kInertiaTol);
EXPECT_NEAR(m->body_inertia[7], I2y, kInertiaTol);
EXPECT_NEAR(m->body_inertia[8], I2z, kInertiaTol);
// compute approximate shell inertia by subtracting inertias of massive bodies
// with small radius difference
mjtNum* inertia3 = m->body_inertia + 9;
mjtNum* inertia4 = m->body_inertia + 12;
mjtNum shell_inertia[3];
mju_sub3(shell_inertia, inertia4, inertia3);
EXPECT_NEAR(shell_inertia[0], m->body_inertia[6], kInertiaTol);
EXPECT_NEAR(shell_inertia[1], m->body_inertia[7], kInertiaTol);
EXPECT_NEAR(shell_inertia[2], m->body_inertia[8], kInertiaTol);
mj_deleteModel(m);
}
// ------------- test inertiagrouprange ----------------------------------------
TEST_F(MjCGeomTest, IgnoreGeomOutsideInertiagrouprange) {