30 min

What a physics engine actually does

Every step, a simulator guesses where everything will be, finds every place that guess is illegal, and solves one system for the forces that make it legal again.

Where you are. You know what a simulator buys you and what it costs. This lesson opens the box, so that “the simulator stepped the world” stops being a black phrase before you install one.

Push a mug an inch

Push a mug an inch across your desk. It slides, slows, stops. Nothing dramatic.

Now write the program that produces that. Gravity pulls the mug down: one line, a constant. Your hand pushes it sideways: another line. Both are easy, because both are formulas that hold everywhere, all the time, whether or not anything is touching.

Then ask the hard question. Why did the mug not go through the desk?

There is no formula for that. There is no force of desk. There is only a fact the world insists on, that two solid things do not occupy the same place, and somewhere in your program something has to notice that the mug is about to be a millimetre inside the wood and invent exactly the force needed to prevent it. Not too little, or the mug sinks. Not too much, or it launches.

Falling is arithmetic. Not falling through the desk is a negotiation. Everything hard about a physics engine lives on the second side.

The idea in one paragraph

A physics engine runs one loop. Each pass covers a fixed slice of simulated time, usually between one and five milliseconds. In that slice it works out where every body currently is, then finds every place the world is about to become illegal, and then solves, all at once, for the extra forces that keep it legal. Then it commits the result, advances its clock, and does it again. The first part is arithmetic you met at school. The last part is a small optimisation problem, built and solved from scratch several hundred times per simulated second, and every tuning knob, every performance cliff and every strange behaviour a simulator has ever shown you comes from it.

One step, end to end

the cost, the tuning knobs and the strange behaviour all live in this one box 1 place it all joint angles into world positions 2 find what is illegal overlaps, joint limits, tendon limits, welds 3 solve one system every constraint force, agreed on at once 4 commit integrate, then t += dt and again, 1 to 5 ms of simulated time later nothing here knows about frames, rendering, or your control loop's rate a 2 ms step means 500 passes for one second of robot time
The four stages of a single simulation step, and the loop that repeats them

Wider than the screen; scroll it sideways.

Stage one is bookkeeping: from the joint angles, work out where every link, every geometric shape and every attachment point actually sits in the world. This is forward kinematics, which you wrote by hand in Module 1, run over the whole robot.

Stage two is collision detection: look for pairs of shapes that overlap, or are about to, and record each one as a contact with a location, a direction and a depth. Joint limits and other restrictions get collected here too, because they are the same kind of fact.

Stage three is the solve, and the rest of this lesson is about it.

Stage four commits: take the accelerations that came out of the solve, integrate them into new velocities and positions, and add one timestep to the clock.

The one-line difference between two integrators

The obvious way to advance a body is the one you would write without thinking. Compute the acceleration from the forces, add acceleration times dt to the velocity, add velocity times dt to the position. Two lines. Their order looks like a matter of taste.

It is not.

explicit Euler semi-implicit Euler a = force / mass x = x + v · dt v = v + a · dt v as it arrived the position moves using a velocity that is already stale 20 s on a spring: energy × 439,000,000 a = force / mass v = v + a · dt x = x + v · dt the position moves using the velocity from this step 20 s on a spring: energy × 1.05 same forces, same timestep, same three lines: only the order of the last two changed
The same three lines in two orders, and what twenty simulated seconds does to each

Wider than the screen; scroll it sideways.

If the position update runs first, it uses the velocity as it arrived, which is already out of date by the end of the step. The error is small and it points the same way every time, so it accumulates. Run a mass on a spring for twenty simulated seconds at a 10 ms timestep and the exercise below measures the total energy growing by a factor of about 439 million. The spring did not gain energy. The integrator manufactured it.

Swap the two lines so the velocity updates first and the position uses the new value, and the same twenty seconds ends with 1.05 times the energy it started with. Same forces, same timestep, one line moved.

That is why engines offer a menu rather than one rule, trading cost per step against how large a step stays sane. The Menagerie SO-101 scene selects one called implicitfast, which handles damping and other velocity-dependent forces more stably than the plain version and lets the scene run at five milliseconds instead of one.

Keeping the mug out of the desk, badly

Now the negotiation. The obvious approach is the one everyone tries first: pretend there is a very stiff spring just under the surface. Overlap by a millimetre, get pushed back proportionally, with some damping so it does not bounce forever.

It works, and it is a trap, and the exercise measures exactly why. Drop a one-kilogram ball from thirty centimetres at a two-millisecond timestep:

spring stiffnesshow deep the ball sankstable
1,00074.9 mmyes
10,00021.8 mmyes
100,0006.8 mmyes
1,000,0004.1 mmyes
10,000,000it explodedno

At the soft end the floor behaves like a mattress. To get penetration down to a tenth of a millimetre, which is where a floor starts looking solid, that spring needs a stiffness around 590 million, and a spring that stiff is stable only if your timestep is under about 82 microseconds. You have just multiplied your cost by twenty-four to make the floor look like a floor, and you have not touched friction yet.

Keeping the mug out of the desk, properly

Engines do something different. Instead of asking “what force should I apply”, they ask “what must be true at the end of this step”, and then solve for whatever force makes that true.

The requirement for a contact is a single sentence: at the end of this step, the two objects must not be moving further into each other. Written down, that is one linear condition on the velocities, and the unknown is the impulse along the contact normal. One contact, one unknown, one condition, and the answer is arithmetic rather than tuning.

Two details make it a real solver. First, a contact can push and can never pull, so the impulse is clamped at zero; if the solution wants to be negative, the correct answer is that the contact is not active. Second, contacts do not come one at a time.

three things happening the cube 1 2 the wrist, wound to its stop 3 becomes five rows in one system 1 left finger, push apart 1 left finger, resist sliding 2 right finger, push apart 2 right finger, resist sliding 3 wrist joint, go no further one solve all five forces, agreeing with each other settle row 1 on its own and row 2 moves the cube again, undoing it a real scene has hundreds of rows, and this happens 500 times a second
One grasp becomes five rows in one system, solved together

Wider than the screen; scroll it sideways.

A cube held between two fingers has at least two contacts, each with a push-apart condition and a resist-sliding condition, and if the wrist is against its travel limit that is another row of the same kind. Settle the left finger on its own and the right finger’s correction moves the cube again, undoing it. So every contact, every friction direction, every joint limit and every weld becomes a row in one system, and the engine solves for all the forces together.

The exercise makes the batching visible. Stack a ball on a ball on the floor and let the solver make only one pass over the two rows per step, and the lower contact settles four hundred micrometres deep. Give it twenty passes and it settles at essentially zero. Nothing about the physics changed; only how thoroughly the rows were reconciled with each other. That is what a solver iteration count buys, and why it is a tuning knob.

Soft on purpose

There is one more thing to accept, and it surprises people. MuJoCo lets a contact carry a real pushing force while the two objects are still overlapping. It does this deliberately, and the documentation gives three reasons: real materials genuinely deform under load, a soft formulation avoids the numerical explosions the penalty spring showed you, and, less obviously, softness makes the whole system differentiable, which is what allows gradients to be taken through a simulation at all.

The softness is not one setting but a pair of them, and you will meet them again in lesson 12. solref is a spring and damper pair describing how quickly a violated constraint gets pulled back toward being satisfied. solimp sets how hard the constraint is in the first place, through a regulariser that behaves like an inverse stiffness: drive it to zero and the constraint becomes perfectly rigid.

Friction is the same machinery with one extra complication. The set of friction forces a contact can produce forms a cone around the normal direction, and the honest cone is round, which makes the solve a harder class of problem. The cheap approximation replaces it with a pyramid of flat faces, and it is the default because most of the time the difference does not show.

The same three stages, different bets

Every engine does place, detect, solve, commit. They differ in what they optimise for.

enginethe bet it makeson a Mac
MuJoCoaccurate contact for articulated bodies, one scene at a time, fast enough to watcha plain pip wheel on Apple silicon
MJXthe same physics rebuilt to run thousands of scenes in parallel on a GPU or TPU; about ten times slower than MuJoCo for a single sceneruns, but pointless on a laptop
Isaac Labphotorealistic rendering plus massively parallel reinforcement learningno; needs an NVIDIA RTX GPU and Linux or Windows
Genesisone engine covering rigid bodies, cloth, fluids and grains togetherits documentation claims full Apple silicon support
Gazeboa whole robot system: sensor plugins, fleets, ROS 2 integrationofficially Ubuntu; macOS is best-effort
PyBullettiny, dependency-light, a decade of tutorials written against itno Apple silicon wheel, so it builds from source

Check yourself

1. A simulation runs correctly at a 0.5 ms timestep and explodes at 5 ms. What have you learned, and what have you not?

You have learned that something in the scene is stiff relative to the step: a stiff contact, a high actuator gain, a strong spring. You have not learned that 0.5 ms is correct. Shrinking the step hides the symptom by making the constant-force assumption inside each step less wrong. The diagnosis is to find the stiff element, not to keep halving dt until the picture stops moving.

2. Two integrators differ by which velocity the position update reads. Why does a difference that small produce a factor of 439 million?

Because the error is systematic, not random, and it compounds. Using the pre-update velocity mis-estimates the position in the same direction every step, and on an oscillating system that direction happens to add energy. Twenty seconds at a 10 ms step is two thousand compounding steps, and any per-step gain above one becomes enormous when raised to the two-thousandth power.

3. Why is “detect the collision, then apply a bounce force” the wrong shape for an engine?

Because contacts are not independent. Applying a force at one contact changes the velocities that the next contact’s decision depends on, so resolving them in sequence means each fix partially undoes the previous one. A held cube, a stack of boxes and a foot on the ground are all cases where several contacts must agree. Engines therefore assemble all the constraints into one system and solve for every force together.

4. Your cube rests 0.1 mm inside the table. Is this a bug? What would you change to make it smaller, and what would it cost?

Not a bug. Soft contact is deliberate: it matches how real materials behave, it avoids the numerical explosions stiff penalty forces cause, and it keeps the formulation differentiable. To reduce the penetration you can make the constraint harder through its impedance settings, shrink the timestep, or raise the solver iteration count. Each costs compute, and driving the constraint toward perfectly rigid gives back the stability problems softness was there to avoid.

5. Why does a grasp expose contact-solver weaknesses that walking or reaching do not?

Because a grasp is a sustained, multi-contact balance. Holding an object requires several contacts to agree on friction and normal forces for thousands of consecutive steps, so any small under-resolution accumulates into visible slip. Reaching has no contact at all and walking has intermittent contact that gets re-established constantly. That is why the arm models set a round friction cone and a high friction-to-normal impedance ratio.

6. In the stacked-ball experiment, one solver pass leaves the lower contact 0.39 mm deep and twenty passes leave it near zero. What is the iteration count actually buying?

Agreement between rows. Each pass fixes one row using the current velocities, and fixing the second row disturbs the first, so a single pass leaves the system only partly reconciled and the residual shows up as extra penetration. More passes converge the rows toward a joint solution. It buys consistency, not accuracy of the physical model, and it is the knob to reach for when stacks sink or grasps slip.

Do this

Run code/one_step.py and fill in the three TODO(you) functions. It is numpy only, takes about three seconds, and by the end you will have written the entire machinery this lesson describes, in about sixty lines.

  1. The integrator. One variable decides whether the position update reads the old velocity or the new one. Set it both ways and watch the spring’s energy.
  2. Contact as a force. Add the penalty spring. Then run the stiffness sweep and find the point where it explodes at a 2 ms step.
  3. Contact as a constraint. Solve for the impulse instead of applying a force. Run it at 2, 10, 20 and 50 ms and notice that nothing explodes at any of them, but the resting penetration grows with the square of the timestep.
  4. Two contacts. This one is written for you. Read it, then run it with one solver sweep and with twenty.

Three things to try afterwards, each a one-line change:

  • Set softness = 5.0 in drop_solved and the resting depth goes from 98 micrometres to 981, ten times softer for a ten times larger number. Now set it to 0.0 and the ball settles about ten micrometres above the floor instead: with no regulariser the correction erases the entire violation every step and slightly overshoots. That parameter is doing the job solimp does in MuJoCo, and the overshoot is why perfectly rigid is not the free win it looks like.
  • Set beta = 1.0, so the constraint tries to erase all of the penetration in one step rather than a fifth of it. The resting depth drops by a factor of five, to about 20 micrometres. That is the job solref does. Then rerun the 50 ms case and compare: the same setting still leaves 12 millimetres, because at a large timestep there is simply more violation to undo per step.
  • In stack_solved, keep sweeps at 1 and raise sim_time from 2 seconds to 20. Decide before you run it whether the penetration will keep growing or settle at a fixed depth, then check. The answer separates a system that is merely under-solved from one that is diverging, and the two look identical in a single screenshot.

What you can now do

You can describe what happens inside one simulation step: place every body, find everything about to be illegal, solve one system for the forces that fix it, integrate, advance the clock. You can explain why two integrators differing by one line differ by eight orders of magnitude after twenty seconds, why engines solve contacts together instead of handling them one at a time, and why a cube resting slightly inside a table is the design rather than a defect. You have written all four of those pieces yourself, which means when lesson 6 calls mj_step, you know what it is doing.

What you can now do

You can describe a simulation step end to end, explain why two integrators differ by one line, and say why contacts are solved together rather than applied one at a time.