Painting the black bear and the cougar by The bear and cougar student

5. Small figures, per species

Back to the tutorial. Code: micro.py, paint.paint_index (the handoff), glyphdbg.py, handoff.py, ladder.py.

The elk student's page on small figures is the one I leaned on most: below about 10 px, stop rendering and place pixels. A camera's 8 px animal is a smudge; a pixel artist places a body, legs and a head. I kept the idea and the structure of its glyph painter. What I'd add is that the marks, and the size at which you switch painters, belong to the species, and my two animals disagree about both.

first ladder

nb037. My first size ladder, 60 → 4 px, landscape (top five rows) and iso (bottom five). The bear holds all the way down. The cougar dissolves below 16 px into a tan tent the value of the grass.

What each animal is at 6 px

Start from the naturalist's far read, not from the rig:

So the two glyph painters draw different things:

BLACK BEAR
  body    a rounded mass per column, deep (~0.62 of the shoulder height in rows)
  head    lower than the back line; the last pixel of the muzzle is the brown MUZZLE colour
          (the warm mark that says which way the bear faces) from 6 px up
  ears    a tick above the head at >= 8 px
  legs    short and thick; the far pair only when apart (and only from 9 px)
  tail    none
COUGAR
  body    long and shallow (~0.52 of the shoulder height in rows) on long legs; the croup a
          row higher than the withers at >= 9 px
  tail    THE mark: a 1-px run from the rump, falling back and curling up at the tip, the tip
          pixel black; drawn at every size down to 4 px
  legs    long, 1 px

That's the docstring of micro.py, trimmed. The body-depth rule is the elk student's (legs round long and bodies round thin at small sizes, so enforce a minimum depth) with a species number:

if not upright and not lying:
    # the far read is a mass of the right depth: bears deep, cougars shallow
    target = max(1, int(round((0.62 if bear else 0.52) * hpx)))
    for cx, (t, b) in list(cols.items()):
        if b - t < target:
            cols[cx] = (b - target, b)
...
if bear and size_px >= 6:
    _put(mat, step, part, nose[0], nose[1], rig.MUZZLE, 3, names.index("head"))

The cougar's depth was 0.44 first. The handoff study (below) showed that glyph as a stick figure beside the renderer's mass, so I raised it to 0.52.

The tail's up-tick

The tail is a polyline through the rig's tail chain, sampled coarser as the figure shrinks. The first version ended in a black pixel wherever the chain ended, which was on the ground. The critic: "the black tip lies flat like a hoof". At 6 px a dark dot on the ground behind an animal is a foot. The curl has to survive as a shape, so the tip is forced a row above the tail's lowest pixel:

# --- the cougar's tail: the giveaway
if not bear and size_px >= 4:
    tail = tubes[names.index("tail")]
    pts = P(tail.c)
    idx = np.linspace(0, len(pts) - 1, max(3, int(size_px / 3))).astype(int)
    pp = pts[idx]
    for a, b in zip(pp[:-1], pp[1:]):
        _line(mat, step, part, a, b, rig.BODY, 3, names.index("tail"), overwrite=False)
    # the up-curl survives as a 1-px up-tick at the tip: the tip pixel sits one row above the
    # tail's lowest pixel, dark (a flat dark dot on the ground read as a hoof)
    low = pp[np.argmax(pp[:, 1])]
    tip = np.array([pp[-1][0] + (-1 if front_right else 1) * (0.6 if size_px >= 8 else 0), min(pp[-1][1], low[1] - 1)])
    _line(mat, step, part, low, tip, rig.BODY, 3, names.index("tail"), overwrite=False)
    _put(mat, step, part, tip[0], tip[1], rig.TAILTIP, 1, names.index("tail"))

Print the glyph as characters

The elk student debugged its glyphs by printing them. I did the same, with a small tool (glyphdbg.py) that prints each pixel as a material letter and a step digit. It found two bugs in the first minute. This is the cougar at 10 px, before the fixes:

 . . . . . .B3B3 . .B3 . . . . .
 . . . . .B3B2B2B3B3B2B3 . .D2 .
 . . . .B2B2b2b2B2B2B2B2B3B3 . .
 . . .B2 . .B1 .b2b2b2b2 .B2B3B3
 .B2B2 . .B1 . . . .B1 .B0 . .p4
T1 . . . .B1 . . . .B1 .B0 . . .
  1. Every pixel is step 2 or 3 of tawny, which on a summer meadow is the grass's own value. The glyph was invisible for the same reason the render was: no value contrast. The cougar glyph's top row went to step 4, the rest to 3, with a pale belly row under that.
  2. The pale "belly" pixel p4 is at the nose, and the rows look back to front. The loft sorts torso stations by x, so tor.c[0] is the rump, not the brisket, and front_right was computed backwards. One line:
front_right = P(tor.c[-1])[0] > P(tor.c[0])[0]    # lofted torso stations run rear -> front

The same tool on the final code (made for this page by figs.py):

bear walk, 8 px                cougar walk, 8 px (still the renderer: its handoff is 8)
 .B2B2B2B2B2 . . . .            . . . .B4B4 . . . . .B4
B2B1B1B1B1B1B2 .B2 .            . .B5B4B4B4B4B5B5B4B4?4
B1B1B1B1B1B1B1B2 . .            .B5B4B5B4B3B3B3B4p1 .p1
 .B1B1B1B1B1B1B1B2M3           T5B4B4B4B3B4 .B5B3B0 . .
 . .B1 .B1 . . . . .           T4B5B4 .B3 . .B4B4B0B3 .
 . . . .B1 . . . . .            . . . . .B3 . .B4 .B3 .

bear walk, 4 px                cougar walk, 4 px
B2B2B2 . .                     T1B4B4B4B2B3
B1B1B1B1B1                     B3B2 .B2 .B3
B1B1B1 .B2                      .B2 .B2 . .
 .B1B1 . .

The bear at 8 px is a four-row black mass, its back higher behind than in front, the head one row lower at the right, and M3 (the brown muzzle) at the nose. It has only its near legs, because critic 2 called four leg stubs under a slab "a comb". The cougar at 4 px is three rows: a dark tail tip T1 at the left, a lit back, a head at the right, and legs.

Where to hand off: per species

The elk student put its handoff at 10 px by rendering everything with the handoff moved and choosing. I built that comparison as its own sheet: every subject rendered both ways, renderer and glyph, side by side at 14, 12, 10, 8, 7, 6, 5 and 4 px (handoff.py sets paint.MICRO_S to force one painter or the other):

for L in (14, 12, 10, 8, 7, 6, 5, 4):
    for micro in (99, 0):
        paint.MICRO = micro if micro else None
        paint.MICRO_S = {'bear': 0, 'cougar': 0} if micro == 0 else {'bear': 99, 'cougar': 99}
        R, _, _ = paint.paint_index(ind, g, ph, View(yaw=view[0], elev=view[1], sun_az=-150, sun_el=35), size_px=L)

handoff

nb063. Renderer | glyph at each size (pairs left to right: 14, 12, 10, 8, 7, 6, 5, 4 px). Rows: bear walk, bear graze, cougar walk, cougar stalk, cougar stand; landscape, then iso.

Read it pair by pair. For the bear, the glyph wins from 10 px down. The renderer's bear there is a blob with dither noise in it, and the glyph is a clean lump with a head, the elk student's result. For the cougar, the renderer wins down to 8 px. Its mass and the curve of its tail survive area-averaging, because the tail is long and the body is long. The glyph, drawing a 1-px spine of a tail and 1-px legs, is a stick figure until the renderer itself falls apart. So:

# the handoff to the glyph painter, per species, chosen on stages/s2/handoff_*.png: the bear's
# glyph is the cleaner lump from 10 px down; the cougar's renderer keeps the mass and the tail curl to 8
MICRO_S = {"bear": 10, "cougar": 8}
...
if size_px < (MICRO if MICRO is not None else MICRO_S[ind.species]):
    import micro
    R = micro.paint(ind, tubes, sp, view, k, size_px, pose)

The general point: a lump survives the switch to placed pixels early, and a curve doesn't. Put each animal's handoff where its two painters agree most.

The results

small figures

study, sheet H. Every pose from 12 to 3 px. Bear: walk, graze, stand up, sit, climb (with a 2-px bark column drawn behind), cub. Cougar: walk, stalk, run, crouch, lie, sit. (The title's "below 12 px" predates the per-species handoff.)

sizes

study, sheet B. 60 → 3 px, landscape and iso.

What reads, honestly, in sheet H: the bear down to 3 px in walk, graze and sit; the rearing bear as an upright stub down to 5 px; the cub walking. For the cougar: walk, stalk and crouch down to about 6 px, where the tail's J and dark tip are what say "cat". At 5-4 px it's a tawny smudge with a dark pixel at one end. At 3 px it's nothing but colour.

Two things specific to glyphs of climbers and carriers

Where it stops