Tutorial · Intermediate · ~50 min
Physics · Mathematics · Euler · Verlet · RK4 · JavaScript

Numerical Integration for Physics — Euler, Verlet & RK4

Physics simulations evolve state over time by approximating continuous equations with discrete time steps. The choice of integrator determines stability, energy conservation, and accuracy. This guide compares four integrators on a spring-mass system, from the simplest forward Euler to the gold-standard RK4.

1The problem: integrating ODEs

A physics body has state: position x and velocity v. Newton's second law gives acceleration: a = F/m. Integration means advancing the state forward by a small time step dt:

// State: position x, velocity v // Acceleration: a = F(x, v) / m // Spring-mass: F = -k * x - b * v (spring + damping) const k = 50; // spring constant const b = 0.5; // damping coefficient const m = 1; // mass function acceleration(x, v) { return (-k * x - b * v) / m; } // Initial conditions let x = 1.0; // stretched 1 unit from equilibrium let v = 0.0; // at rest const dt = 0.016; // 16ms / frame ≈ 60fps

2Forward (Explicit) Euler — simple but unstable

Uses current derivative to extrapolate state. Fast and simple, but adds energy to the system — springs eventually explode at large dt.

function stepEuler(x, v, dt) { const a = acceleration(x, v); const xNew = x + v * dt; // position uses OLD velocity const vNew = v + a * dt; // velocity uses OLD acceleration return [xNew, vNew]; } // Energy drifts upward over time — amplitude increases // STABLE only when: dt < 2 / sqrt(k/m) ≈ 0.28s for k=50, m=1
Forward Euler is first-order accurate (error ∝ dt). For a spring with k=50 and m=1, the critical timestep is ~0.28s. A 60fps game loop (dt≈0.016s) is safely inside this limit, but large springs at low framerates will blow up.

3Symplectic Euler — energy conserving

One small change: update velocity first using current force, then update position using the new velocity. This makes the integrator symplectic — it conserves a modified Hamiltonian and oscillators don't gain energy.

function stepSymplecticEuler(x, v, dt) { const a = acceleration(x, v); const vNew = v + a * dt; // velocity first (NEW) const xNew = x + vNew * dt; // position uses NEW velocity return [xNew, vNew]; } // Energy is bounded — the spring oscillates stably forever // This is what most game physics engines (Bullet, Box2D) use internally // Same cost as forward Euler — just two lines swapped!

4Verlet integration — position-based

Velocity Verlet stores the previous position rather than explicit velocity, giving second-order accuracy and good energy conservation. Popular in cloth, molecular dynamics (GROMACS), and position-based dynamics.

// Velocity Verlet (leapfrog variant) function stepVerlet(x, v, dt) { const a = acceleration(x, v); const xNew = x + v * dt + 0.5 * a * dt * dt; const aNew = acceleration(xNew, v); // acceleration at new position const vNew = v + 0.5 * (a + aNew) * dt; // average acceleration return [xNew, vNew]; } // Classic Verlet (no explicit velocity — uses x_prev): let xPrev = x - v * dt; // initialise x_prev function stepClassicVerlet(x, xPrev, dt) { const a = acceleration(x, 0); // velocity-independent forces only const xNew = 2 * x - xPrev + a * dt * dt; const vApprox = (xNew - xPrev) / (2 * dt); // central difference return [xNew, x, vApprox]; }
Classic Verlet cannot use velocity-dependent forces (like damping) directly — use Velocity Verlet or the leapfrog variant for damped systems.

5Runge-Kutta 4 — the gold standard

RK4 computes four slope estimates per step and takes a weighted average. Fourth-order accuracy means halving dt reduces error 16×. The cost is four force evaluations per step.

function stepRK4(x, v, dt) { // Each k = [dx, dv] derivative pair const k1 = deriv(x, v ); const k2 = deriv(x + k1[0]*dt/2, v + k1[1]*dt/2); const k3 = deriv(x + k2[0]*dt/2, v + k2[1]*dt/2); const k4 = deriv(x + k3[0]*dt, v + k3[1]*dt ); const xNew = x + (dt/6) * (k1[0] + 2*k2[0] + 2*k3[0] + k4[0]); const vNew = v + (dt/6) * (k1[1] + 2*k2[1] + 2*k3[1] + k4[1]); return [xNew, vNew]; } // Derivative function [ẋ = v, v̇ = a(x,v)] function deriv(x, v) { return [v, acceleration(x, v)]; } // RK4 is 4× more expensive than Euler but allows much larger dt // while maintaining accuracy — ideal for orbital mechanics and stiff systems

6Choosing an integrator and fixed timestep

Integrator Order Energy Eval/step Use case
Forward Euler 1st Gains 1 Teaching only
Symplectic Euler 1st Conserves 1 Games, particles
Velocity Verlet 2nd Conserves 2 Cloth, bodies
RK4 4th ~Conserves 4 Orbital, precision

Always use a fixed timestep with an accumulator to decouple rendering from physics:

const FIXED_DT = 1 / 120; // 120 Hz physics let accumulator = 0; let prevTime = performance.now() / 1000; function loop(nowMs) { const now = nowMs / 1000; const frameTime = Math.min(now - prevTime, 0.1); // cap at 100ms (tab unfocus) prevTime = now; accumulator += frameTime; while (accumulator >= FIXED_DT) { [x, v] = stepSymplecticEuler(x, v, FIXED_DT); accumulator -= FIXED_DT; } // Interpolate for smooth rendering (alpha = leftover fraction) const alpha = accumulator / FIXED_DT; const xRender = x; // or lerp(xPrev, x, alpha) for sub-frame smoothness mesh.position.x = xRender; requestAnimationFrame(loop); }
The accumulator pattern is the industry standard (Gaffer On Games: "Fix Your Timestep!"). Without it, physics runs differently at 30fps vs 144fps and simulation outcomes change with display refresh rate.