How the ASCII physics engine works

Fourteen steps, from a grid of characters to lit, coloured balls you can throw. Every step has a live demo and the code that runs it. Updated for v0.3: 24-bit colour and configurable character density.

Try: drag a ball and let go to throw it.
  1. Split the screen into cells
  2. Measure how much a shape covers
  3. Turn brightness into a character
  4. Measure each character's ink
  5. Cut the darkness, dither the steps
  6. Shade each ball in colour
  7. Turn light into colour and characters
  8. Light the floor, cast shadows
  9. Move things in small steps
  10. Resolve collisions
  11. Test only nearby balls
  12. Add friction so balls roll
  13. Pull with a spring, not a leash
  14. Paint 24-bit colour fast
01

Split the screen into character cells

Every character sits in a fixed box called a cell. A monospace font makes every cell the same size. Measure one character to get that size.

Cells are roughly twice as tall as they are wide. So keep all physics in pixels, and convert to cells only when drawing. If you measure distances in cells, a circle comes out as a tall egg.

Tap a cell.
Try: tap any cell to see its column and row. Both shapes use the same radius number.

02

Measure how much of each cell a shape covers

Test a few points inside each cell against the shape. The share of points that land inside is the cell's coverage: 0 means empty, 1 means full.

One point per cell gives jumpy, all-or-nothing edges. Four points in a 2×2 pattern (called supersampling) give in-between values, so edges look smooth. Only test the cells inside the ball's bounding box (the rectangle around it).

Try: drag the circle slowly. Switch to 1 sample and watch edge cells flip between empty and full.

03

Turn brightness into a character

A ramp is a string of characters ordered from empty to dense. The simplest mapping: multiply the brightness (0 to 1) by the ramp's last index, then round. The result picks the character.

This assumes each step along the ramp adds the same amount of ink. Step 4 shows that it doesn't, and fixes it.


04

Measure how much ink each character really uses New in v0.3

Characters don't add ink in even steps. In the classic ramp, % and @ carry far more ink than the characters before them. So equal steps in brightness give uneven steps on screen.

Measure instead. Draw each character in white on a small hidden canvas and add up its pixels. The share of lit pixels is that character's ink. Then give each brightness the character whose ink is closest.

Measure once per font size and keep the result. Each font, and each size, draws characters differently.

Try: compare the two rows against the smooth bar. The measured row brightens evenly; the position row jumps at the end.

05

Cut the darkness and dither the steps New in v0.3

Very faint light turns into a haze of stray dots across the whole screen. Set a black cutoff: any cell darker than it stays blank. Then stretch the remaining range, so the dimmest visible cell starts at the first character.

A ramp has only about ten steps, so a smooth gradient shows bands. Dithering hides them. Before picking the character, add a small offset that follows a fixed 4×4 pattern (a Bayer matrix, the numbers 0 to 15 arranged so neighbours differ). Nearby cells then alternate between two characters, and your eye blends them into the tone between.

Try: set the cutoff to 0 to see the haze. Turn dither on and look at the edge of the light pool.

06

Shade each ball as a lit sphere, in colour

Treat each flat circle as the top of a sphere. At a point (dx, dy) inside the circle, measured in radii, the sphere's height is nz = √(1 − dx² − dy²). The arrow (dx, dy, nz) is the surface normal: the direction that bit of surface faces.

Work out light separately for red, green and blue. Each ball has an albedo: the share of each colour it reflects. Diffuse light (how directly the surface faces the lamp) is tinted by the albedo. The highlight is a mirror bounce, so it takes the lamp's colour, not the ball's. That is why steel's highlight matches the lamp.

Light fades with the square of distance, so a ball twice as far away gets a quarter of the light.

Lamp
Try: drag to move the lamp. Pick the blue lamp with the rubber ball, then with foam. Turn diffuse off to see the highlight alone.

07

Turn light into colour and characters New in v0.3

Light adds up, so keep it as plain numbers that are allowed to go above 1. This is called linear light. Convert it to what the screen can show only at the very end, in three moves.

First, tone map. 1 − e^(−light × exposure) squeezes any amount of light into 0 to 1. Very bright light rolls off gently instead of clipping to flat white. Exposure works like a camera's: higher is brighter.

Second, gamma-encode: raise each value to the power 1 ÷ gamma, with gamma 2.2 by default. Screens don't show light in even steps, and without this step, shadows come out too dark.

Third, split the brightness between the character and its colour. On screen, a cell looks as bright as its character's ink × its colour's brightness. With the tone split at 0, the character's density carries all the shading and colours stay at full strength. At 1, every lit cell uses the densest character and the colour carries all the shading. Values in between share the work.

Tap a cell to follow its light through each step.
Try: tap the ball's highlight, then its dark edge. Move the tone split from 0 to 1 and watch the characters and colours trade places.

08

Light the floor and cast shadows

The background is a floor at height 0. The lamp hangs above it at height z. A floor point at distance d from the lamp receives light in proportion to (z ÷ d)³. That combines the inverse-square fade with the tilt of the light hitting the floor at an angle.

To find shadows, draw a straight line from the floor point up to the lamp. If the line passes through a ball, the point is in shadow. Each ball is a sphere resting on the floor, so its centre sits at height r. Fade the shadow where the line only grazes the ball. That fade gives a soft edge.

Tap the floor to test a point.
Try: drag the lamp or the ball. Tap the floor to draw the line to the lamp; it turns pink when a ball blocks it.

09

Move things in small steps

Each step, add acceleration to velocity, then add velocity to position. Updating velocity first keeps the simulation stable (this order is called semi-implicit Euler).

Split each frame into several substeps. Otherwise a fast ball can jump clean past a thin wall in one big step, without ever overlapping it. This is called tunnelling. Smaller steps make each jump shorter than the ball is wide.

Fire a few shots at each setting.
Try: fire 5 shots with 1 substep and count how many pass through. Then do the same with 4. Dots mark where the ball was checked.

10

Resolve collisions without losing momentum

Two balls collide when the distance between their centres is less than the sum of their radii. Fix each collision in two moves.

First, push the balls apart so they stop overlapping. The lighter ball moves further. Second, change their velocities with an impulse (a sudden push) along the line between the centres. Give both balls the same impulse in opposite directions. That keeps total momentum (mass × velocity, added up) unchanged.

Restitution, written e, sets how much speed comes back: 1 is a perfect bounce, 0 means the balls stick.

Left Right
Pick two materials and fire.

11

Only test balls that are near each other

Testing every pair costs n × (n − 1) ÷ 2 checks: 2,415 checks for 70 balls, every substep. Instead, drop each ball into a grid square as wide as the largest ball.

Two balls can only touch if they sit in the same square or neighbouring squares. So each ball checks its own square plus the 8 around it. This cheap filter is called the broad phase. The exact circle test from step 10 is the narrow phase.

Try: follow the yellow ball. Only balls inside its shaded 3×3 block get tested against it.

12

Add friction so balls roll

A ball on the floor touches it at one point, at the bottom. That point moves at the ball's speed plus the speed from its spin (spin × radius). If the point is moving, the ball is sliding.

Friction pushes against the sliding. The same push slows the ball down and spins it up. Once the contact point stops moving, the ball rolls. Friction can never be stronger than grip (called mu) × how hard the ball presses on the floor.

Grip
Try: slide with grip 0: the ball skates and the stripe never turns. With 0.8 it starts rolling almost at once.

13

Pull with a spring, not a leash

Don't teleport a held ball to your finger. Attach it with a spring instead. The spring pulls harder the further the ball is from your finger. A damper, a brake that grows with speed, stops endless wobbling.

Acceleration is force ÷ mass. So the same spring snaps a foam ball into place but drags a steel ball, which lags, overshoots and sags under gravity. When you let go, the ball keeps the velocity the spring gave it. That is the throw.

Try: drag anywhere. Both balls hang from springs attached to your finger. Move fast and stop suddenly.

14

Paint 24-bit colour fast, then run the loop New in v0.3

Changing the fill colour for each of 3,000 cells, every frame, is slow. Paint in layers instead.

First, write each cell's colour into a tiny image with one pixel per cell, for example 62 × 54 pixels. Second, draw every character in plain white on a hidden canvas, one call per row. Third, set the compositing mode to "source-in" and stretch the tiny image over the white text, with smoothing off. Only pixels that already hold text get painted, and each character takes its own cell's colour.

That gives full 24-bit colour for about 60 text calls instead of thousands. For glow, stretch the tiny image again with smoothing on, faintly, behind the text. Each lit cell then bleeds soft light into its neighbours.

Try: step through the four layers. Drag a ball in each one.

Every frame runs the same layers in the same order. Cap the frame time at 1/30 s. A long pause, such as switching apps, would otherwise produce one giant step that throws balls through walls.

input → finger positions, tools physics → steps 9 to 13 light → floor and shadows (8), shaded balls (2, 6) tone → tone map, gamma, cutoff, split (5, 7) characters → measured ink, dither (4, 5) paint → colour image + white text + glow (14)

Everything above runs together in the full engine. Its Display settings panel exposes every control from steps 4, 5 and 7.

Open the engine