The World Vol 3 · Forces of Nature
ch 27 / 105
Chapter 27

Wind Is a Function of Place

The wind at a position

Both forces this volume has built give everybody the same answer. Gravity hands over a constant somebody typed. The point of attraction works a vector out of two positions the program was already holding, and one of them never moves. Neither has ever needed to ask a body where in the world it happens to be, because a constant does not vary and a point is a single coordinate the whole calculation is arranged around.

Air is not obliging like that. A force may be a function of position: hand it a place and it hands back the push acting there. Out over the open water of The Hollow nothing stands in its way and it runs at full strength. Down in the lee of the rim it barely moves. Along the rim it turns aside, because air that cannot pass through rock goes around it.

There are two ways to keep a fact like that. Give every body its own wind and update it as the body travels, which puts a copy of the weather inside every leaf and makes each leaf responsible for something true of the ground. Or write the wind down once, over the ground itself, and let whatever needs it come and ask.

A second force belongs beside it, and it asks its question of the body instead of the ground: drag, the push back that exists only while something is moving. Put the two in one accumulator and the world acquires a speed limit that neither of them sets alone. That limit can be worked out on paper before the program runs, and then found in the run's own columns.

Four cell centres

The ground is already a grid: twelve cells across and eight down, sixteen world pixels to a cell, built from a seed and holding rock, soil and water. A wind laid over it is one vector per cell. What each cell gets is decided by two things about it. How much of the open wind reaches that kind of ground, since water has nothing standing on it, soil has the valley floor's scrub, and a rock cell is the rim itself. And whether any neighbour is rock, because air arriving at a wall has to spill along it.

Storing a vector per cell leaves a question the ground does not answer. A body is at (32.000, 32.000), which is no cell's centre and never will be, since positions are floats and centres are every sixteenth pixel. Rounding to the cell it stands in would make the wind change in one jump as a leaf crossed a boundary, and a leaf that jumps is the sort of thing you see on screen. The field answers instead with a blend: the four cell centres nearest that position, each weighted by how near it is.

▣ Build · stage 1 — the wind, written down and asked
// internal/field/lay.go

// Lay is a wind written down cell by cell: one force for every cell of
// the world's grid, and an answer for any position in between. It is the
// first thing in this world that is a function of place rather than a
// number: hand it where a body is standing and it hands back what the
// air there is doing to it.
type Lay struct {
	Cols, Rows int
	T          float64 // cell size, in world pixels

	cell []Vec2
}

// NewLay writes down one force per cell by asking at for each of them.
// The grid is walked in row order, so two runs build the same table.
func NewLay(cols, rows int, t float64, at func(cx, cy int) Vec2) *Lay {
	l := &Lay{Cols: cols, Rows: rows, T: t, cell: make([]Vec2, cols*rows)}
	for cy := 0; cy < rows; cy++ {
		for cx := 0; cx < cols; cx++ {
			l.cell[cy*cols+cx] = at(cx, cy)
		}
	}
	return l
}

// Cell is the force written down for one cell. Coordinates off the grid
// read as the nearest edge cell, so a body drifting past the rim keeps
// getting an answer instead of an index panic.
func (l *Lay) Cell(cx, cy int) Vec2 {
	cx = clamp(cx, 0, l.Cols-1)
	cy = clamp(cy, 0, l.Rows-1)
	return l.cell[cy*l.Cols+cx]
}

func clamp(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}

// At is the force at any position on the field: the four cell centres
// nearest that position, each weighted by how near it is. The four are
// added in one fixed order, so the answer is the same on every run.
func (l *Lay) At(p Vec2) Vec2 {
	u := p.X/l.T - 0.5
	v := p.Y/l.T - 0.5
	i, j := int(math.Floor(u)), int(math.Floor(v))
	fx, fy := u-float64(i), v-float64(j)

	tl := l.Cell(i, j).Scale((1 - fx) * (1 - fy))
	tr := l.Cell(i+1, j).Scale(fx * (1 - fy))
	bl := l.Cell(i, j+1).Scale((1 - fx) * fy)
	br := l.Cell(i+1, j+1).Scale(fx * fy)
	return tl.Add(tr).Add(bl).Add(br)
}

The two lines that subtract a half are the only awkward part. A cell's force belongs at its centre, and cell 2's centre is at pixel 40, not 32, so dividing the position by the cell size gives the wrong origin by half a cell. Subtract the half and the whole number part names the cell whose centre is up and to the left, while the fraction says how far past that centre the body has got. Those two fractions are the weights: at half a cell past in both directions all four weights are 0.25, and directly on a centre one weight is 1 and the rest are 0.

Four scaled vectors are then added in the order the code lists them, top row before bottom row, which is the same rule the accumulator lives by. A sum whose order is decided anywhere else is a sum the seed does not control.

▣ Build · stage 2 — what the ground makes of the wind
// cmd/wind/main.go

// base is the wind out on open soil: half a world pixel per tick per
// tick, blowing east.
var base = field.Vec2{X: 0.5, Y: 0}

// exposure is how much of the base wind reaches one kind of ground: open
// water has nothing standing in the way, soil has the valley floor's
// scrub, and a rock cell is the rim itself.
func exposure(t sim.Terrain) float64 {
	switch t {
	case sim.Water:
		return 1.6
	case sim.Soil:
		return 1.0
	}
	return 0.2
}

// windAt is the force written down for one cell: the base wind scaled by
// what the ground there lets through, plus a spill away from every rock
// neighbour, because air that cannot go through a wall goes around it.
func windAt(g *sim.Grid, cx, cy int) field.Vec2 {
	here, err := g.At(sim.Coord{X: cx, Y: cy})
	if err != nil {
		return field.Vec2{}
	}
	w := base.Scale(exposure(here))
	for _, d := range []sim.Coord{{X: -1}, {X: 1}, {Y: -1}, {Y: 1}} {
		n := sim.Coord{X: cx + d.X, Y: cy + d.Y}
		t, err := g.At(n)
		if err != nil || t == sim.Rock {
			w = w.Add(field.Vec2{X: -float64(d.X), Y: -float64(d.Y)}.Scale(0.15))
		}
	}
	return w
}

// lay builds the whole wind from a world: one force per cell, sampled at
// any position by the four cells nearest it.
func lay(g *sim.Grid) *field.Lay {
	return field.NewLay(g.W, g.H, Tile, func(cx, cy int) field.Vec2 {
		return windAt(g, cx, cy)
	})
}
$ go run ./cmd/wind -mode lay
wind: a 12x8 valley from seed 5, base 0.500,0.000
      ground                 wind, in pixels per tick per tick
      ############   0.10 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.10
      #..........#   0.25 0.67 0.52 0.52 0.52 0.52 0.52 0.52 0.52 0.52 0.38 0.05
      #..........#   0.25 0.65 0.50 0.50 0.50 0.50 0.50 0.50 0.50 0.50 0.35 0.05
      #.~~~~~....#   0.25 0.65 0.80 0.80 0.80 0.80 0.80 0.50 0.50 0.50 0.35 0.05
      #.~~~~~~...#   0.25 0.65 0.80 0.80 0.80 0.80 0.80 0.80 0.50 0.50 0.35 0.05
      #...~~~~...#   0.25 0.65 0.50 0.50 0.80 0.80 0.80 0.80 0.50 0.50 0.35 0.05
      #...~......#   0.25 0.67 0.52 0.52 0.81 0.52 0.52 0.52 0.52 0.52 0.38 0.05
      ############   0.10 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.18 0.10

      one transect east along row 3, through the pond and out the far shore
      position       cell     the field's answer
      24.000,56.000   1,3 soil   0.650,0.000
      32.000,56.000   2,3 water  0.725,0.000
      40.000,56.000   2,3 water  0.800,0.000
      104.000,56.000  6,3 water  0.800,0.000
      112.000,56.000  7,3 soil   0.650,0.000
      120.000,56.000  7,3 soil   0.500,0.000

      the four cells nearest 32.000,32.000, and what each is worth
      cell 1,1  centre   24.0,24.0  wind 0.650,0.150  weight 0.25
      cell 2,1  centre   40.0,24.0  wind 0.500,0.150  weight 0.25
      cell 1,2  centre   24.0,40.0  wind 0.650,0.000  weight 0.25
      cell 2,2  centre   40.0,40.0  wind 0.500,0.000  weight 0.25
      the field's answer at 32.000,32.000: 0.575,0.075

The pond stands out at 0.80 against the floor's 0.50, and the column beside the eastern rim reads 0.05, the quietest air in the valley: the wall's spill points west and takes 0.45 out of the 0.50 arriving there. Along the top and bottom the spill points inward, so row 1 reads 0.52 where row 2 reads 0.50: the extra 0.02 is all a sideways 0.15 adds to the length of a vector already 0.50 long. The table prints lengths, and the vectors under it point in different directions.

The transect is the blend at work. At pixel 24 the body is on cell 1's centre and gets cell 1's 0.650 exactly. At pixel 40 it is on cell 2's centre and gets 0.800. At pixel 32, halfway between, it gets 0.725, and the cell column says it is standing in cell 2 while half of what it feels comes from cell 1. The pond's edge is a cliff in the ground and a ramp in the wind, which is what a leaf crossing the shore should feel.

The last block is a position on no centre at all. Four cells, each a quarter, two of them carrying a downward 0.150 spilled off the rim above, and the answer 0.575, 0.075 is a vector that appears in none of them. That number is the point of the whole arrangement: the field has more places in it than it has cells.

One position, four cells, one answer Four boxes stacked in a column, one per cell: cell 1 comma 1 carrying the wind 0.650 comma 0.150 at weight 0.25, cell 2 comma 1 carrying 0.500 comma 0.150 at weight 0.25, cell 1 comma 2 carrying 0.650 comma 0.000 at weight 0.25, and cell 2 comma 2 carrying 0.500 comma 0.000 at weight 0.25. A line leads from each of the four into one highlighted box on the right holding 0.575 comma 0.075, labelled the field's answer. Two labels beneath it read: each weight is how near, that cell's centre lies. THE FIELD'S ANSWER AT 32, 32 cell 1,1 0.650, 0.150 ×0.25 cell 2,1 0.500, 0.150 ×0.25 cell 1,2 0.650, 0.000 ×0.25 cell 2,2 0.500, 0.000 ×0.25 0.575, 0.075 the field's answer each weight is how near that cell's centre lies

Figure 27.1 — the four cells a body at (32, 32) lies between, and the vector none of them holds.

The field is ready, so put things in it. Sixteen bodies of four different masses are released along the western side from the seed's own stream, and every tick each one asks the field where it is standing and hands the answer to ApplyForce. Nothing else pushes on them: the valley is seen from above, so nothing here falls.

▣ Build · stage 3 — sixteen bodies and one question a tick
// cmd/wind/main.go — the tick

for i := range bodies {
	f := l.At(bodies[i].Pos)
	bodies[i].ApplyForce(f)
	bodies[i].Step()
}
$ go run ./cmd/wind -mode valley -ticks 60 -drag=false
wind: 16 bodies on the seed 5 valley, base 0.500,0.000
      no drag at all
  tick  on the field  fastest    mean         frame           numbers
     0            16    0.000   0.000  6f2c67e383c5  a0dada96a2cd0116
    20            12   18.905  10.027  701b90a34739  54dfb866ccb7ac01
    40             4   16.905  10.989  9a26cdc5ef49  663c70af9eac8bc8
    60             0   14.974  10.225  89930306f1ad  ab562de6eb15dca8

Every one of them is gone inside six seconds, and the field is 192 pixels wide. The fastest body was crossing at nearly 19 pixels a tick, which is more than a cell every tick. Nothing here is broken. A force applied over and over with nothing opposing it builds speed without limit, exactly as it did when the only force was gravity, and a wind that never stops pushing is a wind that eventually throws everything it touches off the edge of the world. Real air prevents that by pushing back on whatever is moving through it.

Drag against velocity

Drag is the force a body meets by travelling: the stuff it is passing through has to be shoved out of the way, and shoving costs. It always points against the velocity, and it grows as the velocity grows. How fast it grows is the modelling decision, and there are two answers in ordinary use.

∑ Interlude — two ways for a push to grow with speed

Take a coefficient of 0.25, one number standing for how thick the stuff is. The first law makes the drag proportional to the speed: at 2 pixels a tick it is 0.5, at 4 it is 1.0, double for double. The second makes it proportional to the speed multiplied by itself: at 2 it is 1.0, at 4 it is 4.0, four times for double.

linear: F = B × v
quadratic: F = B × v × v

speed vlinear, B × vquadratic, B × v × vquadratic ÷ linear
0.50.1250.0620.5
10.2500.2501.0
20.5001.0002.0
41.0004.0004.0
82.00016.0008.0
164.00064.00016.0

The two agree at a speed of 1 and part company in both directions from there. Real air behaves like the second law once a thing is moving quickly, and like the first when it is small and slow. Both are in the literature and both are defensible.

This world takes the linear one, for a reason that has less to do with air than with ticks. A tick applies a force for a whole tenth of a second, so a force that outgrows the motion it opposes will overshoot: it can remove more speed than the body had. Under the linear law whether that can happen depends on the coefficient divided by the mass and on nothing else, so it is one check made once when the numbers are chosen. Under the quadratic law it depends on the speed, and the speed is exactly the quantity nobody can promise a bound on in advance. A coefficient that behaves at 2 pixels a tick can be catastrophic at 6.

vthe body's velocity, in world pixels per tick; when it appears as a plain number it is the speed, the length of that vector
Bthe drag coefficient: one number saying how thick the stuff is, 0.25 for this world's air
B × vthe linear law's force: the coefficient scaling the velocity, pointing the other way
B × v × vthe quadratic law's force: the same, multiplied by the speed a second time
▣ Build · stage 4 — the drag, and the speed it settles at
// internal/field/drag.go

// Drag is what a body pushing through air or water gets back: a force
// against the velocity, growing in proportion to it. B is how thick the
// stuff is; nothing here scales with the body's size, because nothing in
// this world has one yet.
type Drag struct{ B float64 }

// Force is the drag on one body this tick.
func (d Drag) Force(b Body) Vec2 { return b.Vel.Scale(-d.B) }

// Terminal is the speed at which this drag exactly cancels a force of
// the given size. Above it the drag wins and the body slows; below it
// the force wins and the body speeds up.
func (d Drag) Terminal(force float64) float64 { return force / d.B }
$ go run ./cmd/wind -mode drift -ticks 120
wind: a steady 0.500,0.000 on masses 0.5 and 4.0, drag B 0.25
      predicted ceiling for both: 2.000 pixels per tick
  tick   v(m=0.5)   drag(m=0.5)     v(m=4)     drag(m=4)
     1     1.0000       -0.0000     0.1250       -0.0000
     2     1.5000       -0.2500     0.2422       -0.0312
     3     1.7500       -0.3750     0.3521       -0.0605
     4     1.8750       -0.4375     0.4550       -0.0880
     5     1.9375       -0.4688     0.5516       -0.1138
    10     1.9980       -0.4990     0.9511       -0.2203
    20     2.0000       -0.5000     1.4499       -0.3533
    40     2.0000       -0.5000     1.8487       -0.4596
    80     2.0000       -0.5000     1.9886       -0.4969
   120     2.0000       -0.5000     1.9991       -0.4998
      after 120 ticks: 2.000000 and 1.999134
      they are 1.000 and 1.000 of the way to the ceiling

Two bodies eight times apart in mass, one steady wind, and both of them stop speeding up at 2 pixels a tick. The light one is inside a thousandth of it by tick 10 and the heavy one needs the whole 120 to get that close, but the number they approach is the same, and the drag column says why: at 2.0 the drag reads 0.5000 against a wind of 0.5000, so the accumulator receives two forces that add to nothing and the velocity has no reason to change again.

That is a different run from one with wind alone. With no drag, the same wind keeps pushing the lighter body ahead faster forever. Here the ratio closes. Weight scales with mass and drag does not, so drag is the force that eventually makes every body agree.

The -0.0000 in the first row is not a misprint. The drag is computed from the velocity at the start of the tick, and at the start of tick 1 both bodies were standing still, so the force is zero scaled by minus a quarter. Floating point keeps a negative zero and prints it.

∑ Interlude — the ceiling, worked out before the run

A body stops speeding up when the forces on it cancel. Under a driving force F and a linear drag, that is F equal to B multiplied by the speed, so the speed it happens at is F divided by B. With the wind's 0.5 and the air's 0.25 that is 2.0 pixels a tick, which is the number the run printed as a prediction before it ran, and printed again as a measurement after 120 ticks.

ceiling = F / B

The mass is absent from that expression, and its absence is the whole result: the ceiling belongs to the wind and the air, not to the thing being blown. Mass decides only how long the arrival takes. One tick of the update, with the two forces written out, says how:

v(next) = v + (F − B × v) / m

Each tick, the gap between the body and its ceiling is multiplied by 1 minus B over m. For the light body B over m is 0.5, so half the gap closes every tick and the velocity runs 1.0, 1.5, 1.75, 1.875, 1.9375, which is the table's first five rows. For the heavy body B over m is 0.0625, so 93.75% of the gap survives each tick and the approach takes twenty times as long. Neither arrives exactly; both are inside a ten-thousandth long before anyone could see the difference.

Drop the same two bodies instead of blowing them, and F becomes each body's own weight, which is not the same number for both. The leaf's weight is 0.5 times 0.5, giving a ceiling of 1.0. The stone's is 4 times 0.5, giving 8.0.

Fthe driving force: what is pushing the body along, the wind here and the weight further down
mthe body's mass, unchanged in meaning since the accumulator was built
F / Bthe ceiling: the speed at which the drag has grown to exactly cancel the driving force
B / mthe fraction of the remaining gap closed each tick, and the number that decides whether a drag is safe at this tick length
▣ Build · stage 5 — a leaf and a stone, dropped in still air
// cmd/wind/main.go

// fall drops the same two bodies through still air, where the force that
// drives them is their own weight and so is not the same for both.
func fall(ticks int) {
	leaf := field.NewBody(field.Vec2{}, 0.5)
	stone := field.NewBody(field.Vec2{}, 4)
	...
	for t := 1; t <= ticks; t++ {
		for _, b := range []*field.Body{&leaf, &stone} {
			b.ApplyForce(b.Weight(gravity))
			b.ApplyForce(air.Force(*b))
			b.Step()
		}
	}
}
$ go run ./cmd/wind -mode fall -ticks 120
wind: dropped in still air, gravity 0.000,0.500, drag B 0.25
      mass 0.5: weight 0.250, so the ceiling is 1.000 pixels per tick
      mass 4.0: weight 2.000, so the ceiling is 8.000 pixels per tick
  tick   v(leaf)   fell    v(stone)   fell
     1    0.5000    0.5      0.5000    0.5
     2    0.7500    1.2      0.9688    1.5
     3    0.8750    2.1      1.4082    2.9
     4    0.9375    3.1      1.8202    4.7
     8    0.9961    7.0      3.2262   15.6
    16    1.0000   15.0      5.1514   50.7
    32    1.0000   31.0      6.9857  151.2
    64    1.0000   63.0      7.8714  393.9
   120    1.0000  119.0      7.9965  840.1
      the leaf is at 1.00000 of its ceiling, the stone at 0.99957

On tick 1 they fall at the same rate, because both start at rest and drag on something at rest is nothing. From tick 2 they separate, and after twelve seconds the stone has fallen 840 pixels and the leaf 119. With no air in the calculation the two fall in step; with air, the leaf falls slowly because the air pushes back hard for the leaf's mass. Here is that sentence as a column of numbers. The stone's weight is eight times the leaf's and the air treats their velocities identically, so the stone needs eight times the speed before the drag can match it.

The pond coefficient failure

With drag added, the valley behaves. The bodies reach a drift speed instead of a catastrophe, and the speed differs across the map because the wind does, which was the entire point of writing the wind down cell by cell.

$ go run ./cmd/wind -mode valley -ticks 120
wind: 16 bodies on the seed 5 valley, base 0.500,0.000
      air B 0.25, capped
  tick  on the field  fastest    mean         frame           numbers
     0            16    0.000   0.000  6f2c67e383c5  a0dada96a2cd0116
    20            16    3.200   2.405  d0fe27c6fd07  0e252fcfd622acb1
    40            16    2.798   2.354  fa21ca0c39b2  21070dee1dfad7d6
    60            16    2.778   1.811  3fafcc1c594d  5c384ade299c7718
    80            15    1.673   0.615  904aa03394a9  69350e3fc66352fc
   100            12    0.320   0.079  1eb6424b287c  e3de3d60bca02833
   120            12    0.160   0.048  062813d7148d  30c6b39d9a8da53a

Read the fastest column against the map from stage 2. At tick 20 something is doing 3.200, and 3.200 is 0.800 divided by 0.25: that body is out over the pond, at the ceiling the strongest air in the valley sets. By tick 100 the fastest thing anywhere is doing 0.320, because the survivors have drifted into the lee of the eastern rim where the wind reads 0.05 and the ceiling is a fifth of a pixel a tick. The bodies pile up in the quiet air and stay there. Nobody wrote that behaviour; it is a consequence of the wind being different in different places, which is a thing this world was not able to say two sections ago.

The next change is the one that breaks it. Drag is a property of the stuff a body is moving through, and the map has two kinds of stuff on it. A leaf out over open water is dragging against water, which is far thicker than air. Making the coefficient a function of place as well is four lines, and unlike the wind it takes the cell whole rather than blending: a body is in the pond or it is not.

▣ Build · stage 6 — a second field, of one number each
// cmd/wind/main.go

var (
	air  = field.Drag{B: 0.25} // how thick the air is
	pond = field.Drag{B: 1.25} // and the water, five times over
)

// dragAt is which stuff a body at p is moving through. It is the second
// field in this program and the plainer kind: one value per cell, read
// from the cell the body stands in and not blended with its neighbours.
func dragAt(g *sim.Grid, wet bool, p field.Vec2) field.Drag {
	if !wet {
		return air
	}
	c := sim.Coord{X: int(p.X) / Tile, Y: int(p.Y) / Tile}
	if t, err := g.At(c); err == nil && t == sim.Water {
		return pond
	}
	return air
}

Two fields now, and they are not the same kind of thing. The wind is a vector per cell and gets blended, because a leaf feels a little more air as it approaches open water. The drag is one number per cell and gets read whole, because the leaf is either on the water or off it and there is no halfway. Which one a field needs is a question about the quantity, not about the code: a blend is right when the thing being sampled really does vary smoothly, and wrong when it names a state.

⚠ Worked failure — the leaf that vibrated across the shore

Switch the pond on and nothing announces a problem. No crash, no NaN, nothing thrown off the field, and a summary that reads like a slightly sluggish version of the run before it. Body 4 is a mass of 0.5 released over open water, so ask the program for every number that body sees:

$ go run ./cmd/wind -mode valley -ticks 20 -wet -cap=false -watch 4 -from 1 -to 16
wind: 16 bodies on the seed 5 valley, base 0.500,0.000
      air B 0.25, pond B 1.25, uncapped
watching body 4, mass 0.5
  tick  on the field  fastest    mean         frame           numbers
     0            16    0.000   0.000  6f2c67e383c5  a0dada96a2cd0116
t=  1  B 1.25  wind 0.742,0.000  drag -0.000,-0.000  vel 1.483,0.000  pos 35.267,66.402
t=  2  B 1.25  wind 0.756,0.000  drag -1.854,-0.000  vel -0.714,0.000  pos 34.553,66.402
t=  3  B 1.25  wind 0.749,0.000  drag 0.892,-0.000  vel 2.569,0.000  pos 37.121,66.402
t=  4  B 1.25  wind 0.773,0.000  drag -3.211,-0.000  vel -2.307,0.000  pos 34.814,66.402
t=  5  B 1.25  wind 0.751,0.000  drag 2.884,-0.000  vel 4.963,0.000  pos 39.778,66.402
t=  6  B 1.25  wind 0.798,0.000  drag -6.204,-0.000  vel -5.849,0.000  pos 33.929,66.402
t=  7  B 1.25  wind 0.743,0.000  drag 7.311,-0.000  vel 10.260,0.000  pos 44.189,66.402
t=  8  B 1.25  wind 0.800,0.000  drag -12.825,-0.000  vel -13.790,0.000  pos 30.399,66.402
t=  9  B 0.25  wind 0.710,0.000  drag 3.447,-0.000  vel -5.475,0.000  pos 24.924,66.402
t= 10  B 0.25  wind 0.659,0.000  drag 1.369,-0.000  vel -1.420,0.000  pos 23.503,66.402
t= 11  B 0.25  wind 0.638,0.000  drag 0.355,-0.000  vel 0.565,0.000  pos 24.069,66.402
t= 12  B 0.25  wind 0.651,0.000  drag -0.141,-0.000  vel 1.584,0.000  pos 25.652,66.402
t= 13  B 0.25  wind 0.665,0.000  drag -0.396,-0.000  vel 2.123,0.000  pos 27.775,66.402
t= 14  B 0.25  wind 0.685,0.000  drag -0.531,-0.000  vel 2.432,0.000  pos 30.208,66.402
t= 15  B 0.25  wind 0.708,0.000  drag -0.608,-0.000  vel 2.633,0.000  pos 32.840,66.402
t= 16  B 1.25  wind 0.733,0.000  drag -3.291,-0.000  vel -2.483,0.000  pos 30.357,66.402
    20            16    2.695   1.268  8f1707c23bf3  80091690cb58f08f

The velocity column alternates in sign on every single tick, and the numbers get bigger each time it does: 1.483, then −0.714, then 2.569, then −2.307, then 4.963, −5.849, 10.260, −13.790. That body is not drifting east; it is vibrating in place and doubling its swing every couple of ticks, until at tick 8 one backwards lurch of nearly fourteen pixels throws it clean out of the pond and onto dry cells, where B drops back to 0.25 and it recovers in three ticks. It then drifts east, re-enters the water at tick 16 and starts over.

The arithmetic is small enough to do by hand. Drag on this body is 1.25 times its velocity, and the tick divides by a mass of 0.5, so the acceleration handed back is 2.5 times the velocity, pointing the other way. A velocity of 1.483 therefore collects −3.708 of acceleration in one tick, which lands it at −2.225, and the wind's 1.512 carries it up to the −0.714 the run printed. The drag did not slow this body. It turned it round and sent it back half as fast again as it came.

Everything that number is built from is defensible on its own. Water really is several times thicker than air. A light leaf really is more affected by it. The coefficient divided by the mass is 2.5, and any value of that ratio above 2 makes the vibration grow instead of settling, so the two defensible choices multiplied together produce an indefensible one. Neither the wind nor the accumulator is involved; the same failure appears with no wind at all.

What makes it nasty is that it is quiet. No panic, no infinity, no NaN. The tick 20 hashes differ from the correct run's in both columns, and both runs look like plausible worlds in a summary. On screen the leaf jitters, and a jitter is the sort of thing a reader blames on the renderer.

A drag is a force that opposes motion. Whatever coefficient it was handed, it has no business creating motion in the opposite direction, and that sentence can be written as two lines of code: work out the drag, and if it is stronger than the amount needed to bring the body to a standstill this tick, use that amount instead. A force of the body's mass times its speed removes exactly all of the speed, and never more.

▣ Build · stage 7 — the most a drag is allowed to do
// internal/field/drag.go

// Force is the drag on one body this tick. The last two lines are the
// cap: a drag may bring a body to rest inside one tick and may not push
// it the other way, whatever B and the mass work out to.
func (d Drag) Force(b Body) Vec2 {
	f := b.Vel.Scale(-d.B)
	if stop := b.Mass * b.Vel.Len(); f.Len() > stop {
		f = b.Vel.Unit().Scale(-stop)
	}
	return f
}

// Bare is the same drag with the cap taken off: the arithmetic straight
// out of the definition, kept so the two can be run against each other.
func (d Drag) Bare(b Body) Vec2 { return b.Vel.Scale(-d.B) }
$ go run ./cmd/wind -mode valley -ticks 10 -wet -watch 4 -from 1 -to 10
wind: 16 bodies on the seed 5 valley, base 0.500,0.000
      air B 0.25, pond B 1.25, capped
watching body 4, mass 0.5
  tick  on the field  fastest    mean         frame           numbers
     0            16    0.000   0.000  6f2c67e383c5  a0dada96a2cd0116
t=  1  B 1.25  wind 0.742,0.000  drag -0.000,-0.000  vel 1.483,0.000  pos 35.267,66.402
t=  2  B 1.25  wind 0.756,0.000  drag -0.742,-0.000  vel 1.511,0.000  pos 36.778,66.402
t=  3  B 1.25  wind 0.770,0.000  drag -0.756,-0.000  vel 1.540,0.000  pos 38.317,66.402
t=  4  B 1.25  wind 0.784,0.000  drag -0.770,-0.000  vel 1.568,0.000  pos 39.886,66.402
t=  5  B 1.25  wind 0.799,0.000  drag -0.784,-0.000  vel 1.598,0.000  pos 41.484,66.402
t=  6  B 1.25  wind 0.800,0.000  drag -0.799,-0.000  vel 1.600,0.000  pos 43.084,66.402
t=  7  B 1.25  wind 0.800,0.000  drag -0.800,-0.000  vel 1.600,0.000  pos 44.684,66.402
t=  8  B 1.25  wind 0.800,0.000  drag -0.800,-0.000  vel 1.600,0.000  pos 46.284,66.402
t=  9  B 1.25  wind 0.800,0.000  drag -0.800,-0.000  vel 1.600,0.000  pos 47.884,66.402
t= 10  B 1.25  wind 0.800,0.000  drag -0.800,-0.000  vel 1.600,0.000  pos 49.484,66.402

The same body, the same seed, the same pond, and it crosses the water at a steady 1.600 pixels a tick. Every drag reading from tick 2 onward is the previous tick's wind with its sign flipped, which is the cap doing precisely what it says: it removes the speed the wind put on and nothing further.

The cap is not free, and it is important to be exact about what it cost. Where it binds, this body is no longer obeying the drag law at all. The law's ceiling over the pond would be 0.800 divided by 1.25, which is 0.64 pixels a tick. What the run settles at is 1.600, which is 0.800 divided by the mass of 0.5. A cap that binds every tick has quietly replaced the physics with itself. It is a guard against nonsense, not a model, and a coefficient that keeps it engaged permanently is a coefficient chosen wrongly.

A capped drag

◆ Note — one coefficient for a leaf and a stone

Every body in these runs meets the same B, so the air treats a pebble and a leaf as equally hard to push through it. Real drag depends on how much of the body faces the flow, and the bodies in this world have a mass and a position and nothing else. Where a size exists, the coefficient becomes a property of the pair, thick stuff and broad body, and the sums in this chapter do not change: it is still one force handed to the accumulator.

With the cap in, the pond becomes a place instead of a fault. Bodies cross the open floor at around 2, speed up to 3.2 over the water where the wind is strongest, and slow in the lee of the rim, and none of them leaves the field.

$ go run ./cmd/wind -mode valley -ticks 120 -wet -shot leaves.png
wind: 16 bodies on the seed 5 valley, base 0.500,0.000
      air B 0.25, pond B 1.25, capped
  tick  on the field  fastest    mean         frame           numbers
     0            16    0.000   0.000  6f2c67e383c5  a0dada96a2cd0116
    20            16    2.587   1.314  706d625dca25  9fa7101e77d24765
    40            16    2.286   1.171  ddc3acc8ada6  c6490d00135eea11
    60            16    2.016   1.171  7acb8059319b  0eb131698614af42
    80            16    1.634   0.878  163570f2bd48  41d0a8ed320e6010
   100            16    1.477   0.439  842f0cb729f1  ebca34284cf35d14
   120            16    1.977   0.520  0ff1d3c7b045  b96b7ad89a06005d
wrote leaves.png

Sixteen released and sixteen still on the field twelve seconds later. The picture is the valley from above, the pond a dark slab across the middle of it, and a scatter of pale dots most of which have collected in a column against the eastern rim, with a few still out over the water. Run the command a second time and all fourteen hashes come back identical, in both columns: the seed placed the bodies, the ground was built from the same seed, and every force since then has been arithmetic performed in an order the code chose.

A field function

The accumulator was built so that anything could add a force without knowing about anything else. This chapter widens what a force is allowed to depend on, and that freedom is the one the world leans on. A force can be a constant. It can be computed from the body it acts on, as drag is from the velocity and weight is from the mass. Or it can be looked up from where the body is, which makes it a function from a position to a vector: a field. Once a world can say that, a great deal of what looks like behaviour stops having to be written as behaviour.

Sampling is what makes a field cheap. There are 96 cells in this valley and no limit on the number of positions in it, and the field never enumerates the positions: it stores the coarse thing, the ground, and computes the fine thing on demand from the four cells nearest whoever asked. That trade appears everywhere a continuous quantity has to live on a discrete world. Light under a canopy, moisture in soil, how strongly a smell reaches one place from another, the pull of a slope on something rolling down it. Each is one number or one vector per cell and an interpolation for everything in between, and each one is Lay with a different name on it.

Drag is the other half, and it earns its place by closing a loop rather than by being faithful to air. Without it, every force in this world is an open-ended instruction to go faster, and sixteen bodies leave a 192-pixel field in six seconds. With it, a force gets an answer back that grows as the motion grows, until the two cancel and the motion stops changing. That ceiling can be predicted with one division, checked against a run, and used to size things: the wind that carries a leaf the width of the valley in six seconds is the wind whose ceiling is about three pixels a tick, and nothing about that number has to be guessed at.

The cap generalises past drag entirely. A fixed tick samples a force once and then spends it for the whole tick, so any force computed as a fraction of the quantity it is reducing can remove more of that quantity than exists. Drag reduces velocity, a bite reduces energy, a dry wind reduces the water standing in a cell, and all three need the same sentence written into them: a force that opposes a quantity may take it to zero and may not take it past. Where the cap fires often the coefficient is wrong; where it never fires it costs one comparison a body a tick.

A valley with weather in it

✓ Checkpoint — what you can now do
  • Given a grid of per-cell vectors and a position between four of them, I can produce the four weights and the blended answer, and check them against -mode lay.
  • I can say why the wind is stored on the coarse grid and blended on demand, and what a leaf does at a cell boundary when it is not.
  • Given a driving force and a drag coefficient, I can state the ceiling before running anything, and say which of the two bodies reaches it first and why the other one eventually gets there too.
  • I can explain why two masses share a ceiling under wind and have different ones under their own weight.
  • Shown a velocity that changes sign every tick and grows, I compute the coefficient divided by the mass and know whether the vibration settles or runs away.
  • I can say what a capped drag has stopped modelling, and read the run's own numbers to tell whether the cap is engaged.
⚡ Exercises — try first, then reveal
Exercise 1 — read the picture off the table. Using only the per-cell wind from -mode lay and the air's B of 0.25, work out the drift ceiling over the pond, over open soil, and in the column beside the eastern rim. Then predict where the bodies will be after 120 ticks and check it.

Divide each cell's wind by 0.25: the pond's 0.80 gives 3.2, open soil's 0.50 gives 2.0, and the eastern lee's 0.05 gives 0.2. So a body should cross the middle of the valley faster than it crosses the floor, and then almost stop when it reaches the far side. The fastest column follows that exactly, hitting 3.200 at tick 20 while bodies are over the water and falling to 0.320 by tick 100.

$ go run ./cmd/wind -mode valley -ticks 120 | tail -4
    60            16    2.778   1.811  3fafcc1c594d  5c384ade299c7718
    80            15    1.673   0.615  904aa03394a9  69350e3fc66352fc
   100            12    0.320   0.079  1eb6424b287c  e3de3d60bca02833
   120            12    0.160   0.048  062813d7148d  30c6b39d9a8da53a

Twelve of sixteen are still on the field. The four that are gone crossed the eastern rim before the quiet air could stop them, since the rim cells themselves read 0.18 and not 0.05. What is left is a crowd of dots against the right-hand wall: a windbreak nobody wrote.

Exercise 2 — choose the pond's coefficient properly. The cap rescued a badly chosen number. Work out the largest B the pond can have with the lightest body in the world at 0.5, set it, and check that the cap never fires.

The vibration begins when B divided by the mass exceeds 1, so with a lightest mass of 0.5 the pond's B must stay under 0.5. Take 0.40 and the drag law governs the whole run: the pond's ceiling becomes 0.800 divided by 0.40, which is 2.000, and that is what the body settles at.

// cmd/wind/main.go
pond = field.Drag{B: 0.40}
$ go run ./cmd/wind -mode valley -ticks 10 -wet -watch 4 -from 5 -to 10 | tail -6
t=  5  B 0.40  wind 0.800,0.000  drag -0.785,-0.000  vel 1.992,0.000  pos 42.936,66.402
t=  6  B 0.40  wind 0.800,0.000  drag -0.797,-0.000  vel 1.998,0.000  pos 44.935,66.402
t=  7  B 0.40  wind 0.800,0.000  drag -0.799,-0.000  vel 2.000,0.000  pos 46.934,66.402
t=  8  B 0.40  wind 0.800,0.000  drag -0.800,-0.000  vel 2.000,0.000  pos 48.934,66.402
t=  9  B 0.40  wind 0.800,0.000  drag -0.800,-0.000  vel 2.000,0.000  pos 50.934,66.402
t= 10  B 0.40  wind 0.800,0.000  drag -0.800,-0.000  vel 2.000,0.000  pos 52.934,66.402

The cap would fire at a force above 0.5 times 2.000, which is 1.0, and the largest drag here is 0.800. It never fires. The cost is that the pond is now only 1.6 times as thick as the air instead of five times, so a body slows less over water than a pond ought to slow it. That is the trade in the open: this world's tick can afford thick water or light leaves, not both.

Exercise 3 — find where the quadratic law turns around. Give one body of mass 0.5 a velocity and one tick of drag alone, under both laws, at speeds 1 through 4. Predict the speed at which the quadratic one starts sending the body backwards.

The quadratic drag's acceleration is B over m times the speed multiplied by itself, and it exceeds the speed once the speed passes m divided by B, here 0.5 over 0.25, which is 2.

// cmd/exp/main.go — the loop, and nothing else
for _, v := range []float64{1, 2, 3, 4} {
	lin := field.NewBody(field.Vec2{}, m)
	lin.Vel = field.Vec2{X: v}
	lin.ApplyForce(lin.Vel.Scale(-B))
	lin.Step()

	quad := field.NewBody(field.Vec2{}, m)
	quad.Vel = field.Vec2{X: v}
	quad.ApplyForce(quad.Vel.Scale(-B * quad.Vel.Len()))
	quad.Step()

	fmt.Printf("  %5.1f %14.3f %17.3f\n", v, lin.Vel.X, quad.Vel.X)
}
$ go run ./cmd/exp
one tick of drag alone, B 0.25, mass 0.5
  speed   linear after   quadratic after
    1.0          0.500             0.500
    2.0          1.000             0.000
    3.0          1.500            -1.500
    4.0          2.000            -4.000

At 2 the quadratic law stops the body dead in one tick. At 3 it sends it back at 1.5, and at 4 it sends it back at 4.0, faster than it was going. The linear column halves the speed at every entry and never changes its sign, because B over m is 0.5 and no speed alters that. The same cap would rescue the quadratic law too, and it would then be engaged above 2 pixels a tick, which is most of the time.