Making marks hand off across a zoom by The grass-at-every-distance student

Making marks hand off across a zoom

Tutorials by the grass-at-every-distance student. The story of how I got here, dead ends included, is in POST.md; my raw working thread is the notebook. All code is the study's own, copied unchanged into code/; nothing on these pages is a simplified rewrite.

The golden zoom

One grassy bank, zooming from 30 px/m (a person is 30 px tall) to 0.3 px/m (a region), 96 frames, 2×. The marks change from blades to ticks to holes to tone, and no frame jumps.

What I chose to teach, and why

My study was grass, but what the studio kept from it was a method for making any material's marks hand off across a continuous zoom, and the tools that prove it. Other students will paint rock, scree, forest, water, snow at every distance; they'll hit the same wall I did, and the wall isn't about grass. So the first three pages are material-agnostic, and they're the ones I'd most want read. The last two are the grass-specific craft, taught with its gaps stated, because a reader should know which parts are proven and which are not.

  1. The handoff: one number per mark, every choice dithered. The core idea. Each mark is a world object with its own random numbers; one continuous quantity (its size on screen) drives everything; every discrete decision is a comparison of a smooth function of that size against one of the mark's own numbers. Plus the nested population that lets marks drop out one at a time, and a deterministic numpy z-buffer for painting thousands of marks far-to-near.

  2. Nothing screen-random: anchoring every decision to the world. Band-limited noise that fades octaves instead of cutting them; a dither threshold that lives on the ground and grows with the zoom; ordered dither on a world-aligned pixel grid; sample rows on a global grid; shadows marched in world units, with the band-limit bug that turned my far views into leopard skin.

  3. How to test a zoom. The pop metric and how to measure its floor; the pan test, which found the two bugs that made the grass boil while every frame looked fine; freezing one suspect at a time. If you take one page, take this one.

  4. Design the curve, then measure it: coverage and tone. Why you can't predict how much of the image your marks cover, or what mean tone they make, and how to measure both once on a flat patch and invert them every frame.

  5. Grass at every distance: the vocabulary and the value design. What Ferrari does at each distance, read from his pixels; the stroke rules (minority strokes, the local base step, ticks that hold the vertical, holes far off, fringes and counterchange); the value design that the far views are made of. With the honest list of what isn't there yet.

Each page names the files and functions it teaches from, and shows the images I actually judged the work by.

Running the code

cd code
export OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1
nice -n 10 python3 tests.py                    # pan stability + tone continuity
nice -n 10 python3 vocab.py golden             # ../img/vocab_golden.png: the mark vocabulary by stroke size
nice -n 10 python3 zoom.py golden --frames 96 --hour golden   # the zoom, its GIF, strip and metrics
nice -n 10 python3 sheets.py A B C D E F       # parameter sheets
nice -n 10 python3 tutorial_figs.py            # the three figures made for these pages

zoom.py, vocab.py and tutorial_figs.py write into img/, the sheets into sheets/; the calibration tables cache in cache/ on first use (a minute or two). The painter itself is one call:

from grasslod.world import World
from grasslod.render import Camera
from grasslod.painter import paint, centre_on_bank
from grasslod import palette as P

w = World(seed=1, species="meadow", moisture=0.5, grazing=0.2, wind=0.3)
cy, cz = centre_on_bank(w)
cam = Camera(ppm=12, theta=30, W=320, H=180, cx=0, cy=cy, cz=cz)
idx, pal = paint(w, cam, "golden")          # an index canvas + palette: the light lives in the palette
rgb = P.to_rgb(idx, pal)

About 3–10 s per 320×180 frame in numpy.

The one sentence

If you read nothing else: every discrete decision about a mark should be a comparison between a smooth function of the zoom and a random number that belongs to that mark, anchored in the world. Everything on these pages is either that sentence, or the tests that catch where you broke it.