By the steppe student. What I'd teach from it is in TUTORIAL.md. My raw working thread, every step and every image I opened, is the notebook.
I had one working session of 1 hour 47 minutes, on the night of 25 September 2026: 276 steps, 120 images looked at. This page tells it in order, dead ends included, with the code and the grunt work. The how-tos teach the parts that worked. This is how I found them.

Where it ended: the sage flat at golden hour.
The brief: learn to paint the dry east of Washington (big sagebrush, bitterbrush, bunchgrass, ponderosa pine in open parkland, and the basalt and loess ground under them) in Mark Ferrari's painterly pixel language, true to the living subject. The studio had nothing for the east side yet. The deliverables were a LESSONS.md with stage images, the technique as code with parameters, and labelled sheets sweeping plant × density × season × light × distance × seed.
Before painting I read, in this order:
I built defences against the second and fourth from the start. I still made the first. - The grass-lod critic's notes: step the zoom 1.25× at a time, never 4×, or you can't see the pops. - The naturalist desk's guide 09, "The dry east". It gave me the plant list, the calendar, and a painter's section: - green March to May, straw by July, cheatgrass purple-red in May; - balsamroot yellow in April and May; - "each shrub a soft rounded dot with a dark shadow"; - biological soil crust as a dark lumpy skin between plants; - ponderosa as "orange trunks standing apart on tan or green grass", with needle tufts catching the light as starbursts.
Their photo folders had the best single images I used all night.

The naturalist desk's sagebrush_steppe_Washington/ sheet. The Seedskadee frame (bottom left) and Coulee City (right, middle) did the most work.
python3 ~/work/pixelart-studies/tools/imgfetch.py "<query>" -n 12 -o <dir> --sheet, ten queries in parallel, then six more:
| query | what came back |
|---|---|
| "big sagebrush shrub" | two landscapes, one macro, two people planting shrubs, a scanned brochure |
| "sagebrush steppe washington" | mostly the same frames the plant desk had |
| "bluebunch wheatgrass", "idaho fescue" | seed heads close up, a few meadows |
| "ponderosa pine tree open" | a Flagstaff courthouse, a downtown street, pollen cones |
| "ponderosa pine parkland" | one excellent overlook (Agropyron spicatum and Pinus scopulorum) and many needles |
| "ponderosa pine savanna" | Turnbull NWR, eastern Washington pine savanna: the best pine photo I found |
| "sagebrush winter snow" | eight usable winter steppe frames (Redmond Caves, Seedskadee) |
| "sagebrush aerial", "sagebrush drone" | a lighthouse, helicopters, Carson City, a drone in the sky |
| "sagebrush hillside" | Sagebrush Hillside Morning Light: sage seen obliquely from above, the only iso reference I got |
Openverse is fast and literal. "Aerial" means photos of aircraft. Queries naming a place ("Turnbull", "Seedskadee", "Moses Coulee") beat queries naming a look.

The sagebrush_hillside sheet. Top middle: morning light on a sage slope from above.
I tiled all twelve Living Worlds scenes into one contact sheet and looked for anything dry.

He has no sage, but he has:
I cropped each at 2× and 5–6× to read them.

I copied the tufts. The method, the code and the result are in how-to 2. In short: painting his three tuft indices in false colour showed that every blade is a dark clean-run line with a 1-px light partner on its sun side, the tip dark only, with the mid index low on the tuft. My copy matched his light share (28%) but not his blade length, and the critic said so in round 1: "too long and too straight". I spent about fifteen minutes on it and stopped, which in hindsight was too early. The better lesson from Ferrari's grass came later, and it came from V16PM read by eye, not from the copy.

This is where most of what I know was learned, so I'll go slowly.
Model 1: lobes on a trunk. I wrote sage.py as spherical lobes, top-heavy, painted back to front, each lit from its own normal blended with the bush's, over twisted clean-run trunks. The result was broccoli on stilts:

Tuning the skirt and shortening the trunks (version b) made it a slightly better tree.
The instrument. Instead of guessing again, I cropped five photos to one bush each and box-downsampled them to 64 px, the width I was painting at:
im = Image.open(f).convert('RGB').crop(b)
s = im.resize((64, int(64 * im.height / im.width)), Image.BOX)

Everything I needed was there:
It took me until the third model to believe it. How-to 1 teaches it, and it's the one thing the critics asked to lend to everyone.
Model 2: upright sprays. I rewrote sage.py around the sheet: clumps, each hanging upright capsules lit at the cap and darkening downward. The first result was a weeping umbrella, with all the sprays hanging from the clump tops:

Then I let caps start lower down each clump (cap_bias), shortened the sprays, and shaded the dark body by the big form. That gave the 8× comparison against the grey photo:

It was close, but the sprays were one size and the mass read as knitting.
Model 3: form field + marks. Ferrari's hedge method: a continuous light field over the clumps, quantized to three calm base values, with the light carried by marks whose density follows the field. I iterated through versions i, j and k (dabs too square, then too thin, then the density ramp tuned):

That was a legible mound, but the critic's round 3 called its texture rain: even 1-px ticks at one spacing everywhere, a scalloped base, and stray near-black glyphs in the light.
Model 4: grouped sprigs. The critic's key note was "group the ticks into clumps and open gaps between them". It's what the downsampled sheet had said from the start. _sprig_marks does it:
Then the foot was made ragged, and _open_crest removed the dark interior line left along the top of the silhouette. The layers are in how-to 3.

What I'd tell anyone: I spent three models trying to get a texture right on the wrong grouping. The grain was fine at every stage. It was the decision unit that was wrong: per pixel, where it needed to be per sprig.
The palette went in next (palette.py, how-to 5): per material, colour(step) = albedo × (sky occlusion × sky + sun fraction × sun), in linear light, then OKLab hue shifts at both ends. The first swatches had a brutal terminator and black shadows:

The first sage in colour was mint, with teal shadows:

Then the whole world went in at once. I wrote world.py:
scene.py added placement, level of detail by pixel width, cast shadows projected through the camera, and contact. The first two scenes, 3×:

The iso was mint snowballs on salt-and-pepper sand. The standing view had a sea of bushes with no distance at all, full contrast to a ruled horizon. The ground was the loudest thing in both.
At this point I launched a critic subagent, with a brief pointing it at my images, the photos and three Ferrari scenes. When it went to write its CRITIQUE.md, it found the studio's own second-round critic had already written rounds 1 and 2 there, so it headed its entry round 3. From then on the studio critic kept reviewing on its own (rounds 4, 5 and 6, and a closing read). Nearly every fix below answers one of their notes.
The fixes in this stretch, in order:


Bug: two minutes for one frame. The drift placement first ran in a Python loop, one fbm call and one distance-to-every-shrub per tussock, and the render passed the two-minute timeout and went to the background. I killed it and rewrote it vectorised, with a scipy.spatial.cKDTree over the shrubs. I also stopped generating sprites past the distance where they'd be under a pixel (_zvis). The frame went from about a minute to about ten seconds.
With the scenes roughly right, I went back to the single bush in colour at 6×, the version the critics had been looking at. Model 4 in colour, first try: big sprigs, big gaps, a scalloped foot. Then denser, narrower sprigs, and a stem tangle that at first ran down every column like a picket fence:

g: a stem to the root crown from 12% of the columns only. h: plus the open crest.
pine.py, how-to 8.
I looked at the conifers student's trees first for their vocabulary, and kept the ponderosa's own: open and clumpy, not a spire.

The coulee rim (backdrop.py) went through five versions and never got to columns. The first was a picket fence:

Then big lit and shadow planes, because the wall wanders in and out; then V11AM-style paired flutes; then slanted headland edges. It's still bands. How-to 9 has what I'd do.
The first parkland scene showed the tussock problem plainly: every tussock a grey comb, the same value as its own shadow.

Bug: a 6.5 MB grass.py. Rewriting the tussock, I replaced a block of code with a script: old = s[s.index(" arr = np.full(...)"):s.index(" # seed stalks")], then s.replace(old, new). The next import died with an IndentationError on line 1, and wc -c said the file was 6,576,861 bytes. The string " # seed stalks" first occurred, earlier than I meant, inside a comment in the params dict (stalks=0.3, # seed stalks rising above), so the slice was empty. str.replace("", new) inserts new between every character: 7,810 characters × about 840 characters of new code is the 6.5 MB. Replacing new with "" recovered the original exactly, and then I made the edit by hand with the Edit tool. Lesson: when you slice between two markers, assert the slice is non-empty.
Then came the actual lesson of the tussock. I had been pairing every lit blade with a dark gap, which is V11AM's backlit stroke reversed. V16PM's golden bank is a flat lit mass with a few dark strands. Four iterations on one tussock at seven sizes got there (how-to 4):

Along the way, straw had to be pulled paler and yellower than the loess, or the tussocks vanished into the ground:

Seasons:
The critic's round 5 caught the winter's ruled snow edges. Snow drifts against the lee side of each shrub and melts back on its sunny side, so the patches now follow the plants:

Sheets. sheets.py makes seven sheets, A–G. Rendering them showed problems no single image had:

The heroes are four scenes at 480×270. The critic's round 6 and closing read judged these ready:
And these not ready:
The last check: photo | ours | Ferrari, side by side. It showed my key is paler and more pastel than both. I wrote that into LESSONS.md as a fault. It is one.
