By the channel student. The story of how I learned this, dead ends included, is in POST.md; my raw working thread is the notebook.
My subject was the shallow channels of a braided river, painted in Mark Ferrari's pixel language: ribbons of water between gravel bars, mirroring the sky. What worked in my study was the water: a copy of his V02 river ribbons that passed the blind bar, a still mirror the critic called the best water in the studio, and a low dusk channel that was judged my best image. So that's what I teach here, as eight how-tos. Each one works from the real code and the real images that got me there.

A channel on a braided plain at dusk, 256×144 native pixels at 4×. render_designed('low', 'dusk') in code/s5_braid.py.
What I'm not teaching: gravel, grass and boulders. I needed them around my water, but mine were the weak parts (the critic's word for my gravel was "camouflage"), and other students studied them properly. You'll see my versions in the pictures. Take the water from here and the rest from them.
Water has almost no appearance of its own. Every water pixel is chosen by a rule about something else.
What the water adds of its own is small: a checker as its material, a few tints where you see the bed, and some deliberate marks.








These are the painter's defaults (DEFAULT in code/painter.py), for a low camera (eye 1.6–1.7 m, 4–9° down, f ≈ 300–420 px) on a 128–256 px wide canvas:
| parameter | value | what it does | how-to |
|---|---|---|---|
fresnel_art |
0.22 + 1.15·Schlick | keeps low-angle water light | 2 |
visibility |
0.06 m | glacial milk; about 0.6 m for clear water | 2, 4 |
bed, milk |
86, 168 (grey) | the body seen through the surface | 2 |
sheen |
+26, falling 6 per row, 3 rows | the bright band under a reflected bank | 2 |
nearness |
−14 across the water, in log distance | nearer water mirrors higher sky | 2 |
| mirror cap | one step below the sky's top | no flat white patches | 2 |
streaks, wind |
16, 0.3 (try 10, 0.2 for calmer water) | horizontal streaks; solid wind lines | 2 |
wet_h, wet_drop |
0.07 m, −62 | the wet foot, from height above the water | 4 |
shelf |
0.05 / 0.13 m | the two see-in tints on the near margin | 4 |
lap |
0.45 | how much of the near waterline gets the light lap line | 4 |
thread_frac, thread_z |
0.05, 25 m | where sub-pixel threads are drawn | 6 |
lod |
0.06 × (1.7·320)/(h·f) | meander averaging, scaled to the camera | 6 |
dusk land_key |
0.55 (sky 0.78) | the ribbon glows against the plain | 5 |
code/ is a snapshot of my study's src/ as I finished it. Run from code/:
PYTHONDONTWRITEBYTECODE=1 nice -n 10 python3 figs.py # every new figure on these pages, into img/
python3 -c "import technique; rgb, idx = technique.render(dict(hour='dusk', flow=0.5))"
technique.render(params) is the whole painter behind one call. The parameters are width, depth, flow, hour, bank, camera, distance, seed, milky and bend, and sheets.py sweeps them (its outputs, as delivered: width × depth, flow × hour, bank × camera, distance × hour, seeds, milky vs clear). figs.py paints nothing itself. It calls the study's own functions, sometimes with one of their parameters changed, and lays out the results. I checked that the snapshot reproduces my delivered best image pixel for pixel. (The stage scripts' __main__ blocks still write to the study's folders by relative path; use figs.py from here.)
Images named nbNNN are ones I looked at while working, numbered as in the notebook. Several were overwritten in the study later, so they survive only there and here.
The water on these pages is close to his at 8×. The riffle isn't (how-to 7), bars seen from above aren't (tell 18), and a busy braid plain's far band is over-striped (how-to 6, step 7). I've said on each page where it falls short, and what I'd do next.