DEV Community

Cover image for Growing fake tree rings in numpy: a 3D printed coaster where no two match
Black
Black

Posted on

Growing fake tree rings in numpy: a 3D printed coaster where no two match

Tree ring coaster render

Most "wood slice" coasters you can print are concentric circles with a bumpy edge. Real log slices are messier, and that mess is what makes them look real: the pith is never in the middle, the rings wobble, some years are fat and some are thin, and the wood cracks as it dries.

I wrote a small generator that grows a different slice from every random seed. Each coaster in the set is one seed. Here are the pieces.

1. Grow the years

A tree adds one ring per year, and the width depends on how good the year was. A cumulative sum of random widths gives exactly that:

rng = np.random.default_rng(seed)
years = np.cumsum(rng.uniform(1.4, 3.4, 40))   # ring radii in mm
Enter fullscreen mode Exit fullscreen mode

2. Move the pith and wobble the rings

The centre of a real trunk is almost never centred. I offset it by up to 6 mm, then measure everything from there. To stop the rings being perfect circles, a few low sine harmonics push them in and out, and the push fades to zero near the pith so the first rings stay tight:

px, py = rng.uniform(-6, 6, 2)
wa = rng.uniform(0, 2*math.pi, 4)
wk = rng.uniform(0.3, 0.9, 4)

tp = np.arctan2(Y - py, X - px)
rp = np.hypot(X - px, Y - py)
w = sum(wk[i]*np.sin((i + 2)*tp + wa[i]) for i in range(4)) * np.clip(rp/30, 0, 1)
q = rp + w            # "ring coordinate": which year are we in?
Enter fullscreen mode Exit fullscreen mode

3. Carve the late-wood lines

For each point, find the nearest year boundary and cut a narrow gaussian groove there. That groove is the dark late-wood line you see on real wood. A very faint sine between the lines adds grain:

k = np.searchsorted(years, q).clip(1, len(years) - 1)
d = np.minimum(np.abs(q - years[k]), np.abs(q - years[k - 1]))
h = T - 0.85*np.exp(-(d/0.5)**2)
h -= 0.06*np.sin(q*5 + tp*2)
Enter fullscreen mode Exit fullscreen mode

searchsorted is the trick that keeps this vectorised: the whole height field for a 0.3 mm grid is a few numpy calls, no Python loop over rings.

4. Drying cracks

Wood shrinks more around the rings than across them, so checks open from the bark inward and are widest at the edge. Two or three V-shaped cuts per slice, each with its own angle and length:

for a, L in cracks:
    da = np.abs(np.arctan2(np.sin(t - a), np.cos(t - a))) * r   # distance from crack line
    u = np.clip((r - (inner - L))/L, 0, 1)                     # 0 at crack tip, 1 at bark
    wd = 0.2 + 1.3*u
    h -= np.where(r > inner - L, np.clip(1 - da/wd, 0, 1)*(1.6*u + 0.3), 0)
Enter fullscreen mode Exit fullscreen mode

5. Bark

The outline is a circle plus three sines at 3, 7 and 13 lobes, so it is lumpy but not noisy. The bark itself sits 1.3 mm proud of the face (which also makes a lip that keeps drips on the coaster), with two layers of nested sines for the furrows. A short smooth step blends wood into bark, like the cambium layer.

6. Mesh it

All of that is one height function h(x, y). Turn it into a signed distance field and mesh it on a grid:

return np.minimum(np.minimum(h - Z, Z), Rb - r)
m = grid_level_set(f, bounds, 0.3)
Enter fullscreen mode Exit fullscreen mode

Four shallow recesses on the bottom take felt pads.

Result

Four 100 mm coasters, about 7 mm thick, no two alike. They print flat with no supports, and a filament swap at the bark height gives a two-tone look with no painting.

The set is on Cults3D: Tree Ring Coasters, set of 4.

Other designs and early access: patreon.com/123black

Top comments (0)