Far and away the simplest way to think about Newtonian physics is to think about point masses. Why? Because it means we don't have to think about rotation (and, indeed, everything we know about rotation and physics can be derived by dividing objects into infinitely many, infinitely small point masses).

So that's what we're going to do today. We'll talk about two simple ways to work with point masses that make them surprisingly flexible and useful.

A more detailed treatment of these ideas may be found in Advanced Character Physics by Thomas Jakobsen; a more modern take (with fluid and volume forces) is implemented in the NVIDIA FleX simulator (paper).

Given a point mass with mass \( m\) and position \(x\) we will write:

\begin{align} & v \equiv \dot{x} & \textrm{velocity is the time derivative of position}\\ & a \equiv \dot{v} \equiv \ddot{x} & \textrm{acceleration is the time derivative of position} \end{align}We then say that acceleration is proportional to applied force, \(f\):

\begin{align} & f = ma & \textrm{Newton's second law} \end{align}Note that for constant force over time \(t\) this equation integrates to the "impulse/momentum" view of point mass movement:

\begin{align} f = m dv \end{align} \begin{align} & ft = m\Delta v & \textrm{impulse is change in momentum} \end{align}We can also take the "energy view" of point mass movement:

\begin{align} \int_0^t f dx = \frac{1}{2}mv^2 \end{align}In a world with no applied force, point masses will move with constant velocity.

const Tick = 1.0/60.0;
const Mass = 2.0;
let position = {x:0.5, y:0.5};
let velocity = {x:0.5, y:-0.5};
function tick() {
position.x += Tick * velocity.x;
position.y += Tick * velocity.y;
}
function draw() {
point(position);
}

With a simple spring attaching the point to the origin, its path begins to curve.

But there's a problem here: the mass is gaining energy.

const Tick = 1.0/60.0;
const Mass = 2.0;
let position = {x:0.5, y:0.5};
let velocity = {x:0.5, y:-0.5};
const K = 10.0;
function tick() {
//spring force:
let force = { x:-K * position.x, y:-K * position.y };
//f = ma:
let acceleration = { x: force.x / Mass, y: force.y / Mass };
//update point based on velocity:
position.x += Tick * velocity.x;
position.y += Tick * velocity.y;
//update velocity based on acceleration:
velocity.x += Tick * acceleration.x;
velocity.y += Tick * acceleration.y;
}
function draw() {
point(position);
}

We could re-write this code without storing velocity (by estimating velocity from the previous position).

const Tick = 1.0/60.0;
const Mass = 2.0;
let oldPosition = {x:0.5+0.5*-Tick, y:0.5+-0.5*-Tick};
let position = {x:0.5, y:0.5};
const K = 10.0;
function tick() {
//estimate velocity from position change:
let velocity = {
x:(position.x - oldPosition.x)/Tick,
y:(position.y - oldPosition.y)/Tick
};
//spring force:
let force = {
x:-K * position.x,
y:-K * position.y
};
//f = ma:
let acceleration = {
x: force.x / Mass,
y: force.y / Mass,
};
//update velocity based on acceleration:
velocity.x += Tick * acceleration.x;
velocity.y += Tick * acceleration.y;
//store old position for later:
oldPosition.x = position.x;
oldPosition.y = position.y;
//update point based on velocity:
position.x += Tick * velocity.x;
position.y += Tick * velocity.y;
}
function draw() {
point(position);
}

But if we're doing away with velocity storage, we can actually go further (useful for hard constraints).

const Tick = 1.0/60.0;
const Mass = 2.0;
let oldPosition = {x:0.5+0.5*-Tick, y:0.5+-0.5*-Tick};
let position = {x:0.5, y:0.5};
const Length = Math.sqrt(2.0) * 0.5;
function tick() {
//update position:
let newPosition = {
x:position.x + (position.x - oldPosition.x),
y:position.y + (position.y - oldPosition.y)
};
//store old position for later:
oldPosition = position;
position = newPosition;
//correct the length:
let len = Math.sqrt(position.x * position.x + position.y * position.y);
position.x += (Length - len) / len * (position.x - 0.0);
position.y += (Length - len) / len * (position.y - 0.0);
}
function draw() {
point(position);
}

This gets very nice when considering lots of constraints.

const Tick = 1.0/60.0;
const Mass = 2.0;
const Gravity = 10.0;
let oldPositions = [ ];
let positions = [ ];
let constraints = [ ];
for (let i = 0; i < 10; ++i) {
positions.push({x:(i+1) * 0.2, y:0.9});
oldPositions.push({x:(i+1) * 0.2, y:0.9});
}
for (let i = 9; i >= 0; --i) {
constraints.push({
a:i-1, b:i, length:0.2
});
}
function tick() {
//update positions:
for (let i = 0; i < positions.length; ++i) {
let newPosition = {
x:positions[i].x + (positions[i].x - oldPositions[i].x),
y:positions[i].y + (positions[i].y - oldPositions[i].y)
};
//store old position for later:
oldPositions[i] = positions[i];
positions[i] = newPosition;
}
//update for gravity;
for (let i = 0; i < positions.length; ++i) {
positions[i].y -= 0.5 * Gravity * Tick * Tick;
}
//correct the positions:
for (let iter = 0; iter < 1; ++iter) {
constraints.forEach(function(c){
let pa = (c.a < 0 ? {x:0, y:0.9} : positions[c.a]);
let pb = positions[c.b];
let l = c.length;
let ab = {x:pb.x - pa.x, y:pb.y - pa.y};
let l0 = Math.sqrt(ab.x * ab.x + ab.y * ab.y);
if (l0 !== 0.0 ) {
pa.x += 0.5 * (l - l0) / l0 * -ab.x;
pa.y += 0.5 * (l - l0) / l0 * -ab.y;
pb.x += 0.5 * (l - l0) / l0 * ab.x;
pb.y += 0.5 * (l - l0) / l0 * ab.y;
}
});
}
}
function draw() {
for (let i = 0; i < positions.length; ++i) {
point(positions[i]);
}
}