Core concept

A body’s motion is decided by three numbers you rarely write down and almost always get from somewhere: its mass, the position of its centre of mass, and its inertia tensor (how hard it is to spin about each axis). MuJoCo gives you two ways to set them.

From geoms (the default). Each geom contributes the mass and inertia of a solid of its shape, at a uniform density (1000 kg/m³ unless you set density, or a fixed mass if you set that instead). MuJoCo sums the contributions of all geoms in the body, with the parallel-axis theorem, into one body inertia.

Explicitly. An <inertial pos="..." mass="..." diaginertia="..."/> element (or fullinertia for six numbers) states the body’s inertial properties directly, as a robot manufacturer’s datasheet or a CAD export gives them.

[!established] Which one wins: compiler/inertiafromgeom With the default setting "auto", MuJoCo infers mass and inertia from geoms only for bodies that have no <inertial> element. "true" always infers from geoms and overrides <inertial>; "false" never infers, and a body without <inertial> is then a compile error. The documentation adds that "true" is useful for imported models whose inertias are “seemingly arbitrary” and “too large compared to the mass”.

The second half of this lesson is about scale: a real robot model has dozens of bodies and hundreds of geoms, and default classes are how you keep friction, colours, joint damping and actuator gains consistent across all of them.

Visual intuition

The three-finger hand in the lab is written once per finger structure and three times per finger placement. Open its source in the editor and find childclass="finger": every joint, geom and actuator inside those bodies takes its defaults from the finger class, so the three fingers cannot drift apart when someone edits one of them.

```lab playground {“dock”: true, “model”: “hand3”, “height”: 240, “editorHeight”: 260, “title”: “Defaults in a real model”}


Try: change `<joint ... damping="0.02"/>` inside `<default class="finger">` to `damping="2"` and run. All six finger joints change at once. Then use the **mass scale** slider in the parameter panel and watch which motions change (the falling objects do not care; the gantry servo's tracking does).

## Mathematics

### Mass and inertia of the primitives

For a uniform solid of density $\rho$, the mass is $m = \rho V$ and the inertia tensor about the centre of mass, in the shape's own frame, is diagonal. With half-sizes $a, b, c$ for a box, radius $r$ for a sphere, and radius $r$ with cylinder length $h$ (twice the half-length) for a capsule:

$$\text{box: } m = 8\rho abc,\quad I = \tfrac{m}{3}\,\mathrm{diag}(b^2+c^2,\ a^2+c^2,\ a^2+b^2)$$

$$\text{sphere: } m = \tfrac43 \pi \rho r^3,\quad I = \tfrac25 m r^2\,\mathbb 1$$

$$\text{capsule: } m = m_c + m_s,\ \ m_c = \rho \pi r^2 h,\ \ m_s = \tfrac43 \rho \pi r^3$$

$$I_{xx} = I_{yy} = m_c\,\frac{3r^2 + h^2}{12} + \tfrac25 m_s r^2 + m_s\,\frac{h(3r+2h)}{8}, \qquad I_{zz} = \tfrac12 m_c r^2 + \tfrac25 m_s r^2.$$

> [!derivation] Where the capsule's last term comes from
> The two hemispheres together have mass $m_s$. Each hemisphere's centroid lies $3r/8$ from its flat face, so at distance $d = h/2 + 3r/8$ from the capsule's centre, and a hemisphere's moment of inertia about its own centroid (transverse axis) is $\tfrac{83}{320} m r^2$. The parallel-axis theorem gives, for both together, $m_s\left(\tfrac{83}{320} r^2 + d^2\right) = m_s\left(\tfrac25 r^2 + \tfrac{h(3r + 2h)}{8}\right)$ after expanding $d^2$. This is the expression in MuJoCo's compiler source (`src/user/user_objects.cc`, capsule case).

### Combining geoms into one body

If a body has geoms $g$ with masses $m_g$, centres $\mathbf c_g$ and inertias $I_g$ (rotated into the body frame), the body's centre of mass and inertia about it are

$$\mathbf c = \frac{\sum_g m_g \mathbf c_g}{\sum_g m_g}, \qquad I = \sum_g \left( I_g + m_g\left(\|\mathbf d_g\|^2 \mathbb 1 - \mathbf d_g \mathbf d_g^{\top}\right) \right),\ \ \mathbf d_g = \mathbf c_g - \mathbf c .$$

MuJoCo then diagonalizes $I$: it stores the three **principal moments** in `body_inertia` and the rotation to the principal axes in `body_iquat`, with the centre of mass in `body_ipos`. Level 4.2 works with these.

## Implementation

```io
INPUT: three MJCF strings embedded in the script
PROCESS: compile primitives at default density and compare with the formulas; compile an explicit inertial; resolve nested default classes
OUTPUT: compiled mass and inertia next to textbook values; the resolved geom attributes

```python file=examples/l2_2_mass_inertia_defaults.py “"”Lesson 2.2: where mass and inertia come from, and how defaults resolve.

INPUT MJCF strings in this file PROCESS (1) compile one body per primitive shape at the default density and compare MuJoCo’s mass and principal inertia with the textbook formulas; (2) show that an explicit replaces what the geoms would give; (3) resolve a nested default-class hierarchy and print what each geom got OUTPUT tables of compiled values next to hand-computed ones

Run: python examples/l2_2_mass_inertia_defaults.py “””

import math

import mujoco import numpy as np

RHO = 1000.0 # MuJoCo’s default geom density, kg/m^3 (the density of water)

SHAPES = “””

”””

DEFAULTS = “””

”””

def textbook(name: str) -> tuple[float, np.ndarray]: if name == “box”: a, b, c = 0.1, 0.05, 0.02 # half-sizes m = RHO * 8 * a * b * c return m, m / 3 * np.array([b * b + c * c, a * a + c * c, a * a + b * b]) if name == “sphere”: r = 0.05 m = RHO * 4 / 3 * math.pi * r3 return m, np.full(3, 0.4 * m * r * r) if name == “capsule”: # solid cylinder plus two hemispheres r, h = 0.03, 0.2 # radius, cylinder length (2 x half-length) m_cyl, m_sph = RHO * math.pi * r * r * h, RHO * 4 / 3 * math.pi * r3 i_cyl_xy, i_cyl_z = m_cyl * (3 * r * r + h * h) / 12, m_cyl * r * r / 2 # hemisphere centroids sit 3r/8 beyond each end of the cylinder i_sph_xy = 0.4 * m_sph * r * r + m_sph * h * (3 * r + 2 * h) / 8 i_sph_z = 0.4 * m_sph * r * r return m_cyl + m_sph, np.array([i_cyl_xy + i_sph_xy, i_cyl_xy + i_sph_xy, i_cyl_z + i_sph_z]) raise ValueError(name)

def shapes() -> None: model = mujoco.MjModel.from_xml_string(SHAPES) print(f” {‘body’:<9}{‘mass (kg)’:>12}{‘textbook’:>12} principal inertia (kg m^2), MuJoCo / textbook”) for name in (“box”, “sphere”, “capsule”): b = model.body(name) m_ref, i_ref = textbook(name) print(f” {name:<9}{b.mass[0]:>12.6f}{m_ref:>12.6f} {np.array2string(b.inertia, precision=8)} / “ f”{np.array2string(i_ref, precision=8)}”) e = model.body(“explicit”) print(f” explicit : mass {e.mass[0]} kg, inertia {e.inertia} (the box geom is ignored for mass)")

def defaults() -> None: model = mujoco.MjModel.from_xml_string(DEFAULTS) for g in range(model.ngeom): geom = model.geom(g) print(f” {geom.name:<18} friction={geom.friction[0]:<4} rgba={np.round(geom.rgba, 2)}”)

if name == “main”: print(“mass and inertia from geoms, density 1000 kg/m^3:”) shapes() print(“default classes:”) defaults()

Output:

```text
mass and inertia from geoms, density 1000 kg/m^3:
  body        mass (kg)    textbook   principal inertia (kg m^2), MuJoCo / textbook
  box          0.800000    0.800000   [0.00077333 0.00277333 0.00333333] / [0.00077333 0.00277333 0.00333333]
  sphere       0.523599    0.523599   [0.0005236 0.0005236 0.0005236] / [0.0005236 0.0005236 0.0005236]
  capsule      0.678584    0.678584   [0.00343835 0.00343835 0.00029518] / [0.00343835 0.00343835 0.00029518]
  explicit <inertial>: mass 2.0 kg, inertia [0.01 0.02 0.03] (the box geom is ignored for mass)
default classes:
  inherits_metal     friction=0.3  rgba=[0.5 0.5 0.5 1. ]
  explicit_class     friction=0.3  rgba=[0.8 0.1 0.1 1. ]
  attribute_wins     friction=1.5  rgba=[0.8 0.1 0.1 1. ]
  top_level_default  friction=0.8  rgba=[0.5 0.5 0.5 1. ]

[!warning] Density 1000 is water, not robot A 10 cm aluminium link built from geoms with no density or mass gets the density of water, about 37% of aluminium’s. Robot models built from primitives should state mass per geom, or density, or an <inertial>. This course’s arm sets mass on every geom.

Default classes

Defaults are inherited down two trees at once: the class tree (nested <default class="..."> elements) and the body tree (via childclass). The rules, all visible in the output above:

  1. The unnamed top-level <default> (class main) applies to every element that names no other class.
  2. A nested class inherits everything from its parent class and overrides what it sets (painted_metal keeps metal’s friction and changes the colour).
  3. childclass="metal" on a body makes metal the default class for every element inside that body and its descendants, unless an element names its own class.
  4. An attribute written on the element itself always wins over any class (attribute_wins).

[!recommendation] Name classes after physical meaning Classes called finger, pad, visual, collision, table_surface survive edits better than classes called geom1 or blue. Put contact parameters (friction, condim, solref) on classes for contact roles (pad, object), and rendering properties on classes for visual roles. Separating visual geoms (contype="0" conaffinity="0", a high group) from collision geoms is the standard pattern for detailed meshes; Level 9.1 explains the collision filtering.

Global options

<option> holds the simulation settings that apply to the whole model, the ones Lesson 1.3 measured:

Attribute Default (3.14.0) Meaning
timestep 0.002 seconds per step
gravity 0 0 -9.81 m/s²; MuJoCo’s viewer and tools assume $z$ up
integrator Euler Lesson 1.3
cone pyramidal friction cone shape (Level 9.3)
impratio 1 friction-to-normal impedance ratio, elliptic cones only
solver Newton constraint solver: PGS, CG or Newton (Level 20.2)
iterations, tolerance 100, 1e-8 solver budget and early-termination threshold
density, viscosity 0, 0 of the surrounding medium; zero disables fluid forces

and <option><flag .../></option> switches pipeline features on and off (contacts, gravity, warm start, energy computation and more). <compiler> holds settings that apply when the model is compiled: angle, eulerseq, autolimits, inertiafromgeom, inertiagrouprange, boundmass, boundinertia, balanceinertia, settotalmass, meshdir, texturedir.

Debugging

Error: mass and inertia of moving bodies must be larger than mjMINVAL. A body with a joint has no mass: its only geoms have mass="0" or are excluded from inertia, or it has no geoms. Give it an <inertial> or a geom with mass; boundmass and boundinertia exist as quick fixes for imported models with massless dummy links.

A robot model “tips over” although it looks balanced. Check model.body_ipos (centre of mass in the body frame) and the subtree masses: a mesh-derived inertia with the wrong scale or an <inertial pos> in the wrong frame moves the centre of mass outside the support.

inertia must satisfy A + B >= C. The diagonal inertia you typed is not physically possible (it violates the triangle inequality any real mass distribution obeys). Fix the numbers; balanceinertia="true" hides the error by averaging them.

Exercise

Model a 0.5 kg aluminium-like link as a capsule 0.3 m long and 0.02 m in radius by setting mass, then compute by hand the inertia MuJoCo should give, and check it. Then attach a 0.2 kg sphere of radius 0.03 m at one end and predict the combined body’s centre of mass and principal inertia before compiling.

Challenge

Take any URDF or MJCF robot model you use in research and check its inertias: for every body compute the equivalent uniform box (the box with the same mass and principal moments) and compare its size with the body’s collision geometry. List bodies whose equivalent box is more than twice the geometry’s size in any dimension. Those are the bodies whose dynamics you should not trust.

Research connection

System identification (Level 18) exists because inertias in robot models are often wrong. A policy trained on a model whose link inertias are off by a factor of two may still work if it is robust to that error, but you cannot know without measuring. Report whether inertias came from CAD, a datasheet, identification, or geometric approximation.

{"id": "2.2-check", "title": "Knowledge check", "questions": [
  {"kind": "numeric", "q": "A box geom with half-sizes 0.05, 0.05, 0.05 m and no <code>mass</code> or <code>density</code>. What mass (kg) does MuJoCo give it?",
   "answer": 1.0, "tol": 0.001, "unit": "kg",
   "explain": "<p>Full edge 0.1 m, volume 0.001 m³, times the default density 1000 kg/m³ = 1 kg.</p>"},
  {"kind": "mcq", "q": "A body has an <code>&lt;inertial&gt;</code> element and three geoms. The compiler setting is the default. What determines the body's mass?",
   "options": ["The sum of the geoms' masses", "The <code>&lt;inertial&gt;</code> element", "Both, added together", "Whichever is larger"],
   "answer": 1,
   "explain": "<p>With <code>inertiafromgeom=\"auto\"</code>, inference from geoms happens only when <code>&lt;inertial&gt;</code> is missing.</p>"},
  {"kind": "mcq", "q": "A geom inside <code>&lt;body childclass=\"A\"&gt;</code> has <code>class=\"B\"</code> and <code>friction=\"0.9\"</code>. Class A sets friction 0.3 and class B sets 0.6. What friction does the geom get?",
   "options": ["0.3", "0.6", "0.9", "The average"],
   "answer": 2,
   "explain": "<p>An attribute on the element always wins. Without it, the element's own <code>class</code> would beat the <code>childclass</code>.</p>"}
]}

Next

Lesson 2.3 adds what makes a model a robot: actuators, sensors, tendons and equality constraints, and the keyframes that put it in a known pose.