4.2.1 Fractal Trees
Recursive branching with three parameters — depth, angle, and length ratio. The same rule produces pines, oaks, and weeping willows depending on how you tune them.
Overview
Trees are the natural-world poster child for fractal geometry. The trunk splits into branches, each branch splits into smaller branches, each twig terminates in still-smaller twigs — and at every scale the geometry looks similar. Mandelbrot devoted a chapter of The Fractal Geometry of Nature to this exact observation [1]. The reason is not just aesthetic: fractal branching solves a real engineering problem — maximise leaf-surface exposure to sunlight, distribute water through xylem, and stay mechanically stable — with a tiny ruleset, which is why evolution rediscovered it independently in plants, rivers, blood vessels, and lightning bolts [2].
This lesson is the natural sibling of 4.1.5. The Sierpinski triangle was an iterated function system with three contractions. A binary fractal tree is an IFS with two contractions per parent branch: rotate left, scale down, recurse; rotate right, scale down, recurse. Once you see that structure, the rest is parameter-tuning.
Learning objectives
- Implement recursive binary branching with
PIL.ImageDrawand polar-to-Cartesian step calculations. - Tune the three branch parameters —
depth,branch_angle,length_ratio— to produce visually distinct tree silhouettes. - Add stochastic perturbation (random angle/length jitter) to convert geometric trees into organic ones.
- Recognise binary trees as a two-contraction IFS — the same paradigm as 4.1.5, with rotation/scaling instead of translation.
Quick start — a single tree
import math
from PIL import Image, ImageDraw
def draw_branch(draw, x, y, length, angle, depth):
if depth == 0:
return
ex = x + length * math.sin(angle)
ey = y - length * math.cos(angle) # subtract: image y points down
draw.line([(x, y), (ex, ey)], fill=(101, 67, 33), width=max(1, depth // 2))
new_length = length * 0.7
draw_branch(draw, ex, ey, new_length, angle - math.radians(25), depth - 1)
draw_branch(draw, ex, ey, new_length, angle + math.radians(25), depth - 1)
img = Image.new('RGB', (512, 512), 'white')
draw_branch(ImageDraw.Draw(img), 256, 462, 120, 0, 8)
img.save('fractal_tree.png')
Core concepts
Concept 1 — Why trees are fractal
Self-similarity in real trees is not a metaphor — it is the visible signature of the recursive growth program in the plant’s apical meristem. At every growth event, the tip either elongates (extending the current branch) or branches (creating two new tips) [3]. The number, angle, and length ratios of these branchings are determined by genetics; the resulting shape is whatever satisfies three competing pressures:
- Maximise light capture — wider angles spread leaves into more sky.
- Minimise self-shading — branches that overlap waste their own canopy.
- Stay mechanically stable — too much weight on a long thin branch breaks it.
Hisao Honda’s classical 1971 model showed that just three parameters — branching angle, length ratio, and total depth — already produce silhouettes that match real species closely enough for botanical identification [4]. Pines have narrow angles, oaks have wide angles, willows have steep starting angles.
Concept 2 — The recursive algorithm
The recursion has the standard fractal anatomy: base case + recursive case.
def draw_branch(draw, x, y, length, angle, depth, branch_angle, length_ratio):
# Base case
if depth == 0:
return
# Compute endpoint with polar-to-Cartesian
end_x = x + length * math.sin(angle)
end_y = y - length * math.cos(angle) # subtract: y grows downward
# Draw this branch
thickness = max(1, depth // 2) # tapers with depth
draw.line([(x, y), (end_x, end_y)],
fill=(101, 67, 33), width=thickness)
# Recursive case: two child branches
new_length = length * length_ratio
draw_branch(draw, end_x, end_y, new_length,
angle - branch_angle, depth - 1, branch_angle, length_ratio)
draw_branch(draw, end_x, end_y, new_length,
angle + branch_angle, depth - 1, branch_angle, length_ratio) Three things to notice. First, the angle is measured from vertical — angle = 0 points straight up. The trig formulas $\sin$/$\cos$ assume that convention. Second, end_y = y - length * cos(angle) because Pillow’s coordinate system has y growing downward; subtracting moves the endpoint up. Third, the thickness taper max(1, depth // 2) is purely cosmetic but doubles the visual realism — real branches taper, and so should yours.
Concept 3 — Three parameters, infinite trees
The three knobs:
- Depth controls how many levels of branching occur. Each level doubles the branch count: depth $n$ produces “two to the n+1 minus one” branches. Past depth 12 the recursion limit and the runtime both become problems.
- Branch angle controls how wide the tree spreads. Small angles (10–15°) give pine-like silhouettes that lift toward the sky. Wide angles (40°+) give oak-like spreading canopies.
- Length ratio controls how quickly the branches shrink. Below 0.5 the tree is sparse and obviously fractal; above 0.8 the children almost equal their parent and the tree fills out into a dense ball.
Exercises
Three exercises in Execute → Modify → Create order: run the default tree, tune the three parameters, then add randomness to make it organic.
Run the default tree
Run fractal_tree.py and examine the output. The script renders the depth-8, 25°, 0.7-ratio tree above.
Reflection questions
- How many branches in a depth-8 tree?
- Why does the code use
y - length * cos(angle)for the endpoint, noty + length * cos(angle)? - What would happen if
length_ratiowere greater than 1.0? - Identify the two contractions of this tree’s IFS. Each is a rotation + scaling. What rotation angle and what scale factor?
Answers
Branch count — $2^9 - 1 = 511$ branches. Each level of recursion doubles the branches; the geometric series $1 + 2 + 4 + \ldots + 2^8$ sums to $2^9 - 1$.
Why subtract — Pillow’s image coordinates have $(0, 0)$ at the top-left and $y$ growing downward. Since we want the tree to grow up (toward smaller $y$), we subtract. Forget this and the tree grows underground.
Length ratio > 1 — children would be longer than their parents. The total tree height would diverge as depth increases; the recursion would either eventually overflow Python’s call-stack limit (default ~1000) or fill the canvas with overlapping segments before that.
The two contractions — rotate by $-25°$, scale by 0.7; rotate by $+25°$, scale by 0.7. Apply both to the tree, union the results, add the trunk back, and you reproduce the original tree exactly. This is the Hutchinson–Barnsley IFS form.
Sweep parameters to make four trees
Edit fractal_tree.py to produce four named tree styles. Only branch_angle, length_ratio, depth, and start_angle should change.
Goals
- Narrow pine —
branch_angle = 12°,length_ratio = 0.75,depth = 9. - Spreading oak —
branch_angle = 40°,length_ratio = 0.72,depth = 7. - Weeping willow — start with a slight downward
start_angle = math.radians(20)and usebranch_angle = 50°,length_ratio = 0.78. - Decorative spiral —
branch_angle = 15°on the left child andbranch_angle = 50°on the right child — asymmetric branching. Hint: pass two different angles into the recursive calls.
Goal 1 — what to expect
A tall, narrow silhouette — branches stay close to the trunk. Pine trees, conifers, and Italian cypresses look like this in real life because their branching evolved to grow vertically toward the sun in dense forests.
Goal 2 — what to expect
A broad, spreading canopy. Wide branching distributes leaves laterally — exactly what an oak in open ground does because there is no neighbour competition for vertical sunlight.
Goal 3 — what to expect
Branches tilt and droop. Starting at a non-zero angle rotates the whole tree, and a steep branch angle plus moderate ratio gives the characteristic willow silhouette — long, drooping side branches that nearly touch the ground.
Goal 4 — what to expect
A lopsided, spiralling tree. The left children always rotate by a small angle and the right by a large one, breaking bilateral symmetry. The asymmetric IFS still has a well-defined attractor; it just isn’t a symmetric one.
Add organic randomness
Convert the deterministic tree into a stochastic one by jittering each recursion’s angle and length ratio. Start from the deterministic version in the starter, then complete the TODOs.
import math
import random
from PIL import Image, ImageDraw
def draw_branch_organic(draw, x, y, length, angle, depth, branch_angle, length_ratio):
if depth == 0 or length < 2:
return
# TODO 1: jitter the angle by uniform(-0.15, 0.15) radians (~ ±9°)
# TODO 2: jitter the length ratio by uniform(-0.1, +0.1), clipped to [0.4, 0.9]
# TODO 3: 10% chance to skip drawing this branch entirely (organic gaps)
end_x = x + length * math.sin(angle)
end_y = y - length * math.cos(angle)
draw.line([(x, y), (end_x, end_y)], fill=(101, 67, 33), width=max(1, depth // 2))
new_length = length * length_ratio
draw_branch_organic(draw, end_x, end_y, new_length,
angle - branch_angle, depth - 1, branch_angle, length_ratio)
draw_branch_organic(draw, end_x, end_y, new_length,
angle + branch_angle, depth - 1, branch_angle, length_ratio)
random.seed(0)
img = Image.new('RGB', (512, 512), 'white')
draw_branch_organic(ImageDraw.Draw(img), 256, 462, 120, 0, 9, math.radians(25), 0.72)
img.save('natural_tree.png') Hint 1 — jitter the angle
angle += random.uniform(-0.15, 0.15)Insert this before computing end_x/end_y. Each branch picks its own random offset, so the perturbation is independent at every recursion level.
Hint 2 — jitter the length ratio
length_ratio = max(0.4, min(0.9, length_ratio + random.uniform(-0.1, 0.1)))Clipping prevents both shrinkage too fast (branches vanish) and growth (children longer than parents → recursion explosion).
Hint 3 — random branch dropout
if random.random() < 0.1:
returnInsert this before drawing. 10% of branches are skipped, producing the characteristic asymmetric gaps of real trees. Raise it to 0.3 for sparse winter trees.
Complete solution
import math, random
from PIL import Image, ImageDraw
def draw_branch_organic(draw, x, y, length, angle, depth, branch_angle, length_ratio):
if depth == 0 or length < 2:
return
if random.random() < 0.1:
return
angle += random.uniform(-0.15, 0.15)
length_ratio = max(0.4, min(0.9, length_ratio + random.uniform(-0.1, 0.1)))
end_x = x + length * math.sin(angle)
end_y = y - length * math.cos(angle)
draw.line([(x, y), (end_x, end_y)], fill=(101, 67, 33), width=max(1, depth // 2))
new_length = length * length_ratio
draw_branch_organic(draw, end_x, end_y, new_length,
angle - branch_angle, depth - 1, branch_angle, length_ratio)
draw_branch_organic(draw, end_x, end_y, new_length,
angle + branch_angle, depth - 1, branch_angle, length_ratio)
random.seed(0)
img = Image.new('RGB', (512, 512), 'white')
draw_branch_organic(ImageDraw.Draw(img), 256, 462, 120, 0, 9, math.radians(25), 0.72)
img.save('natural_tree.png')
Make it your own
- Seasonal palette. Add green-leaf circles when
depth <= 1. Mix red and yellow circles viarandom.random()for autumn. - Wind. Multiply every branch’s angle by
1 + 0.1 * math.sin(time)— pass a single globaltimeparameter through the recursion. Render frames at $t = 0, 0.1, 0.2, \ldots$ for an animated GIF. - Forest. Run the tree 10 times at different
start_xpositions with different random seeds. Varylength_ratioandbranch_angleper tree. Stack them at different startingyvalues for a depth-of-field forest.
Downloads
fractal_tree.py — deterministic reference fractal_tree_starter.py — Exercise 3 skeletonSummary
Common pitfalls to avoid
- Adding instead of subtracting
cos(angle). Pillow’s y-axis points down. Subtract or your tree grows into the floor. - Depth > 11 with the default Python recursion limit (1000). Two to the twelfth = 4,096 branch calls — manageable, but call stack depth grows linearly with the binary-tree depth. Raise
sys.setrecursionlimitor refactor to an iterative version if you need very deep trees. - Length ratio ≥ 1. Children grow longer than parents; recursion explodes. Keep
length_ratiostrictly below 1. - Forgetting
length < 2short-circuit. With jitter, a child branch can become microscopically short and waste recursion levels. Bail out when sub-pixel.
References
- [1] Mandelbrot, B. B. (1982). The Fractal Geometry of Nature. W. H. Freeman. ISBN 978-0-7167-1186-5.
- [2] West, G. B., Brown, J. H., & Enquist, B. J. (1999). A general model for the structure and allometry of plant vascular systems. Nature, 400, 664–667. doi:10.1038/23251
- [3] Prusinkiewicz, P., & Lindenmayer, A. (1990). The Algorithmic Beauty of Plants. Springer-Verlag. ISBN 978-0-387-97297-8.
- [4] Honda, H. (1971). Description of the form of trees by the parameters of the tree-like body. Journal of Theoretical Biology, 31(2), 331–338. doi:10.1016/0022-5193(71)90191-3
- [5] Eloy, C. (2011). Leonardo’s rule, self-similarity, and wind-induced stresses in trees. Physical Review Letters, 107(25), 258101. doi:10.1103/PhysRevLett.107.258101
- [6] Barnsley, M. F. (1988). Fractals Everywhere. Academic Press. ISBN 978-0-12-079062-9.
- [7] Shiffman, D. (2012). The Nature of Code: Simulating Natural Systems with Processing. natureofcode.com.