Skip to content

Detail Physics (MuJoCo)

CommercialBeta

Included in the public demo Compare editions

Detail physics answers one question: does this one operation work mechanically? A gripper that holds a part by contact and can let it slip. A part that jams in a fixture. A free body that gets shoved out of place. Cases whose answer depends on friction, on contact stiffness and on the force an axis can really produce.

It is not the tool for a general virtual commissioning run. Material flow stays where it is: conveyors, the part stream and dense accumulation keep running on kinematic transport, on Physics (Rapier) and in 2D interaction zones. Rapier and Box2D own the material flow; detail physics owns the detail areas.

The split is a cost decision, not a matter of taste. MuJoCo’s effort grows with the number of simultaneous contacts, not with the number of parts — and it grows most where contacts are permanent rather than passing. A walking robot’s feet touch and let go again; a part wedged in a fixture keeps every one of its contacts alive for as long as it sits there. Running a whole station this way buys computing time for information nobody asked for, which is why detail physics is always bounded by a region instead of being a switch for the scene.

So: if your question is cycle time, part flow, accumulation or a PLC program, you are on the wrong page — see Multi-Model Simulation. If your question is whether these jaws hold that part, read on.

Which question belongs on which engine Two columns sort questions by the engine that answers them. The left column, a question about the flow, lists how long does the cycle take, where do parts accumulate and does the part land in the bin; these are answered by kinematic transport, a Rapier physics zone or a Box2D 2D zone. The right column, a question about one operation, lists do these jaws hold that part, does it jam in the fixture and can the axis produce that force; these are answered by one detail region running MuJoCo, whose volume is the node's Box Collider world bounds. A dashed, crossed arrow leads from the detail region back to the left column: if the answer needs a contact that crosses the region boundary, it is a flow question again, and growing the region is not the way. A question about the flow “How long does the cycle take?” “Where do parts accumulate?” “Does the part land in the bin?” answered by Kinematic transport Rapier physics zone Box2D 2D zone A question about one operation “Do these jaws hold that part?” “Does it jam in the fixture?” “Can the axis produce that force?” answered by One detail region — MuJoCo volume = the Box Collider world bounds Needs a contact that crosses the boundary? Then it is a flow question again — a region that has to grow to enclose the contact is no longer a region.
Sort by the question, not by the engine. Everything on the left keeps running where it is; only the right column earns a region.

A MuJoCo Region marks the part of the model that is simulated in detail. Add it in the Editor to the node that holds everything belonging to the operation — the robot, the gripper, the fixture, the loose parts.

What the region simulates is its subtree in the hierarchy. Its box volume is a separate thing: it states where parts would change engine, and it is what the gizmo draws.

PropertyDescription
RegionEnabledOn by default. Switch it off to keep the authored region without simulating it; a disabled region loads no MuJoCo runtime at all.
TimestepSIntegration step in seconds, default 0.002. The region runs as many physics steps per frame as fit into the frame, capped at 16. Below about 1 ms per step the region therefore falls behind real time and runs in slow motion. If a trained policy is in the region, this must match its training step.
ShowGizmoOn by default. Draws the region volume as a violet wireframe box — distinct from the cyan 3D physics zone and the orange 2D zone. Turn it off once the placement is right.

The volume comes from a Box Collider on the same node, exactly as for a physics zone or a 2D interaction zone, so there is no second place that states it. Without a Box Collider the region falls back to the node’s own bounds.

The region also carries fields that are authored but not yet acted on: ExitBandM and LoadTreeMode belong to the engine handover at the boundary, and MjcfSource, MeshBase, Contract, TrunkBody, TrunkFreeJoint and GyroSensor to a hand-maintained physics model. See What this does not do yet.

It loads on demand. The detail-physics runtime, including its WebAssembly payload, is fetched the first time a loaded model declares a region. A model without one never pays for it.

Collision geometry — the one rule to internalise

Section titled “Collision geometry — the one rule to internalise”

MuJoCo turns every collision mesh into its convex hull, and a convex hull has no hole. A board with a bore, convexified, is a board without a bore: a peg pushed at it stops on the surface and nothing in the simulation is wrong about that. Whatever has to behave as concave must be described as an arrangement of convex primitives.

Where the same peg ends up in all three cases Three panels follow one peg, and its height is the result. Left, labelled Authored: a peg above a plate with a round bore, an arrow pointing down into the bore. Middle, labelled What runs: the bore is dashed and crossed out because the convex hull filled it in, and the peg has come to rest on the top surface of the plate, a full plate thickness above where it was meant to be. Right, labelled Authored as primitives: two convex blocks with a real gap between them, and the peg has descended into the gap. The simulation reports success in all three cases; only the peg's height says which one was right. Authored The peg is meant to enter the bore. What runs It stops on the surface, a plate thickness high. Nothing is reported. Authored as primitives Two convex blocks, a real gap. The peg goes in.
All three runs complete without an error; only the peg's height differs. MuJoCo convexifies without a word, and realvirtual reports it as mjcf-mesh-convexified — but only you know whether that concavity was load-bearing.

A Collision Shape Set holds that arrangement.

PropertyDescription
ShapesThe convex primitives in author order. Each entry has a Kind (box, sphere, capsule, cylinder, ellipsoid), a Center in millimetres, a Size in millimetres and a Rotation in degrees, all in the node’s local frame. Add, remove, duplicate and reorder entries in the inspector.
ReplacesMeshAuthored but not read. A Collision Shape Set with at least one shape is already the node’s complete collision geometry: the node’s own collider is then not written at all, whatever this flag says. Emitting the hull alongside the primitives would fill exactly the opening the primitives keep open, so there is nothing for the flag to switch.

Size is read as half extents, and how many of its three numbers count depends on the kind: a box reads three half extents, a sphere one radius, a capsule and a cylinder a radius and a half length, an ellipsoid three radii. This is deliberately not the convention of the Box Collider next door, whose size is a full size in metres. A size component that is zero or negative is refused and reported as mjcf-shape-degenerate rather than quietly grown.

Two things make a decomposition cheaper than it looks. Primitives of the same body never collide with each other, so a decomposition needs no exclusions. And the contact class comes from the contact role, which keeps the parts of a decomposition that only stand on the floor from each generating a floor contact: a measurement on a segmented plate produced up to 112 contacts from 28 boxes lying on one plane, where the same plate standing on a single underside box has the four contacts a flat plate really has.

The role is a property of the node, not of the single primitive. Every primitive of a Collision Shape Set inherits the one profile resolved on its node, so a set cannot hold a grippy pad and a plain housing side by side. If one surface has to play a different role, give it a node of its own with its own Contact Profile.

Contact roles, and why Priority is not optional

Section titled “Contact roles, and why Priority is not optional”

A Contact Profile gives a surface a named role instead of a loose set of numbers. The numbers behind the roles come from a profile library that ships with the viewer.

PropertyDescription
ProfileThe role this surface plays: Pallet, Workpiece (the default), BeltSurface, StopperFace, GripperJaw, MachineFrame, FixtureBore or Custom. Every role but Custom takes its numbers from the library.
PriorityDefault 0. The higher-priority side of a contact supplies friction and compliance for the whole pairing.
SolmixDefault 1. Weight when the two sides’ contact compliance is averaged. No effect on friction.
FrictionSlide / FrictionSpin / FrictionRollSliding, torsional and rolling friction coefficients (1, 0.005, 0.0001). Read only with Profile set to Custom. Spin friction needs CondDim SlidingTorsional or above, rolling friction needs Full.
CondDimHow many dimensions a contact has: Frictionless, Sliding, SlidingTorsional (the default) or Full.
SolrefTimeConst / SolrefDampRatioTime constant (0.02 s) and damping ratio (1) of the contact’s reference spring-damper — how soft the contact is and how it settles.

Read this before you pick a role. MuJoCo maximises friction element-wise between two surfaces: the rougher side always wins, so a pairing cannot be expressed by two independent entries, and one grippy pad would pull every other pairing up with it. The expressible form is Priority on the side that sets the tone. The library gives priority 1 to the roles that should set it — GripperJaw, FixtureBore and StopperFace — and 0 to everything else. If you choose one of those roles and leave Priority at 0, you silently get a pad that does not behave like a pad. That case is reported as mjcf-profile-priority-not-applied; the pairing itself still runs.

CondDim defaults to SlidingTorsional on purpose, not to MuJoCo’s own value, which has no spin friction. Measured: without spin friction a gripped part rotated out of the jaws within five seconds and was caught again elsewhere — a successful run that turned into a jam with nothing reported.

What the jaws can hold is the product of force and friction — and the friction is the half that usually goes undeclared. Holding capacity scales with both: for a 15 mm two-finger gripper, a force-closure calculation gives 0.51 kg at a friction coefficient of 0.1 and 3.57 kg at 0.7, at the same clamping force. That is a factor of seven from the pad alone, which is why the profile is worth checking before the force is raised — not because the force does not matter.

The library numbers are declared, not measured. Each entry states its own provenance, and today they are plausible, reproducible starting assumptions — they are not datasheet values and must not be quoted as such. A surface without any profile is reported as mjcf-geom-no-profile, because then nothing has declared a friction.

A Free Body is a body the simulation moves: a block, a pallet, an obstacle, the workpiece itself. Each tick its pose is written back onto the scene node, so what you see is where the physics put it.

PropertyDescription
EnabledOn by default. Switch it off to keep the declaration without the body.
MassKgMass in kilograms. 0 means derive it from the node’s mechanism body data. A moving body with no mass at all is refused by MuJoCo and reported as mjcf-body-no-mass; a coarser mass source than the authored one is reported as mjcf-mass-degraded.

Enabled and MassKg are the whole component today. CondDim, FrictionSlide, LinearDamping, AngularDamping and Restitution are authored but not read: the contact behaviour of a free body comes from the Contact Profile resolved on its node, exactly as for every other surface. Put the friction and the contact dimensionality there, not here. See What this does not do yet.

A Movable declares that the user may move a node, where a Free Body declares that the physics may. Both can sit on the same node: a block you place by hand and the simulation then pushes around.

PropertyDescription
EnabledOn by default.
AxesWhich directions a drag may change: XZ (the floor plane, default), XYZ, X, Y or Z.
SnapMmGrid the drag snaps to, in millimetres. 0 is free movement.
ResetOnStopAuthored but not acted on. The component captures the authored position and can return to it, but nothing in the viewer asks it to on a simulation stop.

Dragging a Movable with the mouse is not wired up in the viewer yet, so the whole component is a declaration today — see What this does not do yet.

Put a Dynamic Axis on the node of a Drive and the drive stops being a kinematic setter. Its setpoint becomes the command of a force-limited actuator, the solver moves the body, and the joint position that comes out is written back as the drive’s actual position. A kinematic axis has no following error by construction. A dynamic one has, and on a real machine that error is the overload signal.

The axis is not switched off and no second motion path appears: whatever wrote the setpoint before — an IK path, a bound PLC signal, a logic step — keeps writing it. The travel limits also stay where they are: the actuator’s command range follows Drive → LowerLimit / UpperLimit and is not a field here.

PropertyDescription
SetpointSourceA declaration of intent, not a switch. It does not select who writes the setpoint: the actuator always follows the drive’s commanded position, whoever wrote it. Its one effect is a load-time check — policy on an axis that a path planner also commands is reported as axis-role-conflict. operator is reserved for manual jogging, which does not exist yet.
ActuatorKindposition (the default), velocity or motor.
Kp / KvProportional and derivative gain of the servo (80000 and 0).
ForceRangeMin / ForceRangeMaxForce or torque limit of the actuator. Both 0 means unlimited, which is reported as mjcf-axis-no-force-limit: an axis that cannot saturate cannot refuse, and this is the one number the overload report and the grip force both hang on.
GearAuthored but not read. A transmission ratio is expressed by the separate Drive_Gear component, which is the implemented route and couples one setpoint to several joints.
ArmatureAdded joint inertia, default 0.1. A declared starting assumption, and the right knob when a stiff servo needs a smaller step — raise this before lowering TimestepS.
Damping / FrictionLossViscous joint damping, and a constant friction force to overcome before the joint moves. Both 0.
BaseFollowingErrorThe structural share of the following error that the overload report subtracts. In the drive’s own unit. See below.
FollowingErrorLimitWindow on the net error, i.e. after the base share is subtracted. 0 switches monitoring off.
FollowingErrorTimeMsHow long the net error must stay outside the window before the axis is reported, default 200 ms.
ArrivalToleranceArrival tolerance for path and logic-step waits. 0 derives it from the base share plus the window, never below 0.01.

Why there is a base following error at all

Section titled “Why there is a base following error at all”

MuJoCo’s position actuator is a P+D controller without an integral term. A permanent gravity sag of torque over Kp therefore remains even with no contact at all, and the axis never gets any closer to its setpoint than that. Compare the raw error against a window and the viewer reports permanent overload — the servo working exactly as designed, filling the Problems panel.

Declare the sag in BaseFollowingError and only what exceeds it is reported. Measured on a 39 kg six-axis arm: 1.07° of axis sag at the stock gain of a public robot model, and 0.10° at the Kp this component defaults to.

Leaving the window is a report, not a trip. The Overload signal goes true and a Problems entry appears that names the net error, the raw error, the base share, the window and the actuator force. The motion keeps running.

Grip load: the jam shows up at the gripper

Section titled “Grip load: the jam shows up at the gripper”

Put a Grip Load on the node of a Grip to declare how it holds a part — and to measure whether it still does.

PropertyDescription
Modeformfit (the default) parents the part to the jaws, as gripping always has. The part then cannot slip, and PartSlip reads 0 — not because nothing slipped, but because nothing was measured. Only forcefit lets the physics contact hold the part, and only there is the slip a real measurement.
GripForceDeclared clamping force per jaw in newtons. 0 means not declared, and only the measured actuator force is shown.
SlipLimitHow far the part may travel inside the jaws, in millimetres, before it counts as slipping. 0 switches the check off.
SlipTimeMsHow long the slip must stay above the limit before it is reported, default 200 ms.

Slip is measured in the jaw frame, since the grasp began, not in world space — the jaws are moving, after all. In formfit the measurement does not happen at all, so a zero there carries no information: read it only in forcefit.

This is where you look first, and that is a measured result, not a preference. Three failure cases of one joining attempt were measured against a successful run:

CaseAxis following errorPart slip in the jaws
Wrong rotation0.07–0.08°, as in the good run20.0 mm
Wrong shape0.07–0.08°, as in the good run20.3 mm
Skew pushdistinguishable; one axis saturated 71 % of the time16.7 mm

In two of three cases the axes behaved exactly as in the run that worked, while the part slid 17 to 20 mm. An overload model that knows only following error and torque sees nothing there. The reason is mechanical: a two-jaw gripper transmits at most twice force times friction along the joining axis — about 50 N for the catalogue gripper used — while the arm pushes with over 100 N at the tool. The part gives way before the axis does.

Every quantity leaves the simulation as a signal, so it is visible in the signal list, forceable, recordable, and readable from a controller over realvirtual CONNECT. They are PLC inputs: the viewer writes them, a controller reads them, exactly as a real drive does with its following error. Each one sits in the Signals folder of its component root, prefixed with the node’s name so six axes of one robot do not collide.

SignalWhat it carries
<Axis>.FollowingErrorThe net following error, in the drive’s own unit — the raw error minus BaseFollowingError. It is the same number the overload window is compared against, so with a base share declared this signal does not carry the raw error.
<Axis>.IsTorqueThe force or torque the actuator is currently producing — newtons on a linear axis, newton-metres on a rotary one.
<Axis>.SaturatedTrue while the actuator sits at its force limit.
<Axis>.OverloadTrue while the net following error is outside its window.
<Grip>.PartSlipHow far the held part has travelled inside the jaws, in millimetres. Always 0 in formfit, where nothing is measured.
<Grip>.GripForceThe clamping force at the jaws, in newtons.
<Grip>.PartSlippingTrue while the slip is above its limit.

Nothing has to be bound for this to work. An unbound signal is still a signal.

The Problems panel is the second output, and every entry names the node it belongs to. The ones you will meet most:

EntryWhat it means
mujoco-region-failedThe region’s physics model would not load. The region is out; the other regions of the model keep running.
mjcf-mesh-convexifiedA collision mesh was replaced by its convex hull. Decide whether that concavity mattered.
mjcf-shape-degenerateA collision primitive has a zero or negative extent and was refused.
mjcf-geom-no-profileA surface has no Contact Profile, so nothing declared its friction.
mjcf-profile-priority-not-appliedA role that should set the tone of its pairings carries Priority 0.
mjcf-axis-no-force-limitA Dynamic Axis has no force limit, so it cannot saturate and cannot refuse.
mjcf-body-no-mass / mjcf-body-no-geometry / mjcf-mass-degradedA body has no mass, no collision geometry, or a coarser mass source than the authored one.
axis-overloadThe net following error left its window. A report; the motion continues.
grip-slipThe held part is slipping in the jaws.

Cost grows with the number of simultaneous contacts, not with the number of parts, so there is no figure per body that would mean anything. A few bodies in permanent contact can cost more than many that only touch in passing — which also means the cost of a region is a property of your model, not of the component. Measure the region on the hardware you will run it on, with the frame budget you have.

Four signs that a detail region is not the right answer:

  • The region has to grow to enclose a contact. Every contact across the boundary forces an extension. A region that ends up covering half the scene is not a region.
  • Many bodies are in permanent contact with each other. That is a 2D zone’s job: seen from above, conveyed goods are a 2D problem anyway, and Box2D carries dense flow at a fraction of the cost of 3D contact.
  • The parameters are not obtainable. A region with guessed friction and guessed contact stiffness produces a picture, not a statement — and a picture is cheaper to produce kinematically.
  • A step below 1 ms seems necessary. That happens with stiff servos and no Armature. Get the armature value first; do not lower the step.
  • Parts do not cross the region boundary. There is no handover between conveyor transport and a detail region: a part is not taken over at the box and not given back. What a region simulates is what is authored inside it. ExitBandM and LoadTreeMode are the authored half of that handover and currently have no effect.
  • Inserting a part into a fixture is not a working example. In the current measurement the jaws clamp at 42 N, carry the part about 180 mm and then lose it. Holding and slipping are measurable today; completing a fit is not.
  • Dragging a Movable by hand is not wired up. The component declares who may move the node, along which axes and on which grid, and the physics-side Free Body is what actually moves today. ResetOnStop belongs to that unwired half: the authored position is captured, but nothing restores it on a stop.
  • Several authored fields have no consumer yet. They load, validate and save, and nothing reads them: ReplacesMesh on a Collision Shape Set, Gear on a Dynamic Axis (use Drive_Gear), and CondDim, FrictionSlide, LinearDamping, AngularDamping and Restitution on a Free Body (use a Contact Profile). SetpointSource is read only for the load-time role check, not to choose who writes the setpoint.
  • There is no manual mode. No cartesian jogging, and SetpointSource operator is reserved rather than implemented.
  • Nothing trips. An overload or a slip is reported and the motion path keeps running. Stopping on it is your logic’s decision.
  • The friction and compliance numbers are declared. They are reproducible starting assumptions with stated provenance, not measurements of your surfaces.
  • A hand-maintained physics model cannot be supplied. MjcfSource and MeshBase are authored but not read; the model is always generated from the components.