obOB STUDIO
← Back to blog
Physics

Why Every Gravity Simulator Needs a Softening Parameter

6 min readcosmic-gravity-sandbox

Gravity is proportional to 1/r², and as r approaches zero that goes to infinity. In a simulation that means a body gets an absurd velocity in a single frame and vanishes. The standard fix is to add a small constant to the denominator (a softening parameter) so the force stays finite when things get close. Mine is 5. It is the least glamorous line in the whole engine and things fall apart without it.

the divide-by-almost-zero problem

Here's the force equation again:

F = G · m₁ · m₂ / r²

Look at what happens as r shrinks. At distance 10 the denominator is 100. At distance 1, it's 1. At 0.1, it's 0.01. So the force is a hundred times stronger than at distance 1, and ten thousand times stronger than at 10.

At r = 0 you're dividing by zero, and JavaScript will cheerfully hand you Infinity without so much as a warning. Infinity times a direction is NaN. And once a single NaN gets into a position, it spreads through every subsequent calculation like a rumour, and your entire simulation is quietly full of nothing.

In real life this doesn't come up, because real objects have physical size and can't occupy the same point. In a simulation your bodies are mathematical points that happen to be drawn as circles, and points are perfectly happy to be nowhere apart.

what it looks like when it goes wrong

You don't usually get a crash. You get something much more entertaining.

Two bodies drift close. For one frame the distance is tiny, the force is enormous, and the acceleration is applied for a whole timestep as though it were constant across that entire step. Which it very much is not. The body receives a velocity that would be appropriate for a fraction of a millisecond, applied for a sixtieth of a second.

It leaves. Instantly. Straight line, off screen, gone forever, occasionally taking a companion with it.

This is sometimes called the slingshot of death, and the first time it happened to me I genuinely thought I'd found something interesting about the physics rather than a hole in my arithmetic.

the fix is embarrassingly small

You add a constant to the denominator so it can never reach zero:

const EPSILON = 5; // Softening parameter to prevent infinite forces

const force = (G * b1.mass * b2.mass) / (distSq + EPSILON * EPSILON);

That's it. distSq + 25, forever. When bodies are far apart, 25 is a rounding error against a distance squared in the tens of thousands and the physics is untouched. When they're right on top of each other, the denominator bottoms out at 25 instead of 0, and the force tops out at something large but survivable.

This isn't a hack I came up with to paper over a bug, which is what it looks like. It's a standard technique with a proper name (Plummer softening) and it's used in professional astrophysics codes for exactly the same reason. Real galaxy simulations soften too, because a simulated star is standing in for millions of real ones and pretending it's a point mass is already an approximation.

Knowing that made me feel considerably better about it, and I'd like to pass that comfort along, because "add a magic number to the denominator" feels like cheating right up until you find out everyone does it.

what you're trading away

Softening is a lie you tell on purpose, and it's worth knowing the shape of the lie.

Below roughly the softening distance, your gravity is wrong. Weaker than reality. Two bodies a couple of pixels apart will orbit more lazily than they should. You've traded accuracy at very small separations for the simulation not exploding, which is an excellent trade, but it is a trade.

Choosing the value is mostly taste. Too small and you haven't fixed anything; close encounters still fling things away. Too large and gravity gets mushy at ordinary distances and everything feels like it's moving through syrup. I landed on 5 by trying numbers until close passes stopped launching bodies into the void while distant orbits still looked crisp. Which, I recognise, is not a rigorous methodology. The sensible rule of thumb is to keep it around the typical radius of your bodies, and mine range from 2 to 30, so 5 is at least in the right neighbourhood.

the other line of defence

Softening handles bodies that get close. It doesn't handle bodies that actually overlap, and in my sandbox those are a separate concern. Anything closer than the sum of the two radii is treated as a collision and merged, annihilated or bounced depending on which mode you're in:

if (distSq < (b1.radius + b2.radius) ** 2) {
  handleCollision(i, j);
  ...
}

So there are two layers. Collision detection catches genuine overlaps and removes them from the problem. Softening catches everything in the awkward gap between "quite close" and "touching", where the force would otherwise spike hard enough to matter but nothing has technically hit anything.

Both are needed. I found this out in the usual order, which is to say I wrote the collision handling first, assumed I was covered, and then spent an evening watching planets teleport into the middle distance.

see it not exploding

You can watch the softening earn its keep in the gravity sandbox. Set collision mode to Elastic so bodies bounce instead of merging, then drop a few asteroids right next to a star. They'll whip past fast, which is correct, and they'll stay in the simulation, which is the part that took a magic number in a denominator.

The rest of the engine is written up in how to simulate gravity in JavaScript, and the other thing that quietly keeps orbits alive is the order of your integration lines.

ob

Written by Oliver

I build browser games and simulations on my own, everything here runs in a tab, with no installer and no account. The biggest is Oliver's Racers: procedural circuits in Godot 4, online multiplayer relayed by a Raspberry Pi in my room, and an Android build. Almost nothing here is imported artwork; the cars, trees and grandstands are built out of boxes and cylinders in code at load time.

More about me · See the projects