How to paint willow scrub by The willow student

5. The edge

Back to the tutorial. Code: fringe, wand_tips in code/foliage.py; the edge block and airy holes in paint_willow, code/willow.py.

A leafy mass has no outline. Its boundary is where the texture ends: a crest of clump caps, lit where it faces the sun, with marks above the line, sky between clumps, and on a willow, wand tips running out past the mass. A smooth contour reads as a magazine cut-out.

the edge at 8x

The finished crest at 8×, from the construction figure on the front page.

The four edge passes

In paint_willow, after the marks, in this order:

1. Lit wand tips. From the silhouette's upper edge, where the wand direction points out of the mass, run a 1–5-px clean run out along the wand, with leaflets off its sides and a lit tip:

# wand_tips
ox, oy = -gx_[y, x], -gy_[y, x]                  # outward normal of the smoothed mask
if up_only and oy > 0.35: continue               # not on the underside
if dx * ox + dy * oy < 0.2: continue             # the wand must point outward here
ang = snap_dir(dx + 0.3 * ox, dy + 0.3 * oy)
pts = stroke_run(x, y, ang, L + 1)[1:]

2. Dark ticks on the shadow side, the same function at 1.4× the rate, in the shadow ramp. Against a light sky a dark edge is a found edge, and in Ferrari's hedge it's still broken.

3. Caps and holes (fringe). Along every pixel open above: with some probability open a 1–2-px hole into the mass (the pixel below becomes a lit peak), or add a 1–4-px cap on top, sometimes with a single pixel above it. Then light the crest pixel itself with probability rim_p × light. On the lit side rim_p=0.9; on the shadow side more holes and caps (hole_p=0.3, bump_p=0.35) and no light.

4. Airy holes. Willow is see-through near the top and sides. Near the original silhouette (within 6 px), in the upper part of the crown, where the pixel is dark base and the clump field is low, open holes to the sky with smooth noise:

dist_in = ndi.distance_transform_edt(m)          # distance into the *original* silhouette
hole = (M & up_part & (dist_in < ah["depth"]) & (hn < ah["amount"])
        & (np.maximum(S, 0) <= 1) & (MAT != SILVER) & (CLv < ah.get("cl", 0.4)))
M = M & ~hole

Measure dist_in from the original silhouette, not from the painted mask. Once the clumps leave little gaps inside the mass, distance from the painted mask is small everywhere and the holes punch floating sky specks into the middle of a big bush. That was a real bug. There's also a guard: any sky still showing 3 px or more inside the original mass (and not an airy hole) is filled with the shadow base.

What it replaced

smooth contour

A smooth dark contour on the shadow side, and a ledge.

the old crest

An early crest beside the photo and Ferrari's, before the lit tips and holes.

Still short

The shadow-side contour is still smoother than Ferrari's found edges: his shadowed hedge crest breaks into single leaf ticks and 1-px holes almost everywhere, mine only every few pixels. That was the last open note from the critics on edges, and I'd push hole_p and the tick rate further on the shadow side before anything else.