Biology · Algorithms · Fractals
📅 Березень 2026 ⏱ ≈ 10 хв читання 🎯 Beginner–Intermediate

L-Systems and Plant Growth — Lindenmayer grammars, turtle graphics and 3D branches

In 1968, botanist Aristid Lindenmayer devised a string-rewriting formalism to model cell division in algae. The same simple idea — replace symbols in parallel — generates convincing trees, ferns, seaweeds, coral and coastlines.

1. Grammar — axiom, alphabet and production rules

An L-system is a formal grammar G = (V, ω, P) where:

At each generation, all symbols in the string are rewritten simultaneously — this parallel replacement distinguishes L-systems from standard Chomsky grammars and is what produces self-similar structure naturally.

Algae (Lindenmayer 1968):
Variables: A B
Axiom: A
Rules: A → AB
       B → A

Generation 0: A
Generation 1: AB
Generation 2: ABA
Generation 3: ABAAB
Generation 4: ABAABABA
Generation 5: ABAABABAABAAB
Length grows as Fibonacci numbers: 1,2,3,5,8,13,…

2. Turtle graphics interpretation

An L-system string gains visual meaning by interpreting each character as a turtle command. The turtle has a position (x, y), a heading θ, and an optional stack for branching.

Symbol Command
F Move forward by step length d, drawing a line segment
f Move forward by d without drawing (jump)
+ Turn left by angle δ
Turn right by angle δ
[ Push current state (position + angle) onto stack
] Pop state from stack (return to saved position)
| Turn 180°
& Pitch down by δ (3D)
^ Pitch up by δ (3D)
\ Roll left by δ (3D)
/ Roll right by δ (3D)

The [ and ] stack operations are what create branches: push state at the base of a branch, recurse into the branch, pop back to the base — the fundamental idiom of all tree and shrub L-systems.

3. Classic L-systems

Koch Curve (δ = 60°)

Axiom: F
F → F+F−−F+F

Gen 4: FFFF… (1024 segments)

D = log(4)/log(3) ≈ 1.262

Sierpiński Triangle (δ = 60°)

Axiom: F−G−G
F → F−G+F+G−F
G → GG

3 recursive levels fills the triangle

D = log(3)/log(2) ≈ 1.585

Fractal Plant (δ = 25°)

Axiom: X
X → F+[[X]−X]−F[−FX]+X
F → FF

Gen 6 → realistic leafy branch

Classic branching shrub

Dragon Curve (δ = 90°)

Axiom: FX
X → X+YF+
Y → −FX−Y

Gen 16 → folded paper dragon

D = 2 (fills a plane region)

Bush (δ = 22.5°)

Axiom: F
F → FF+[+F−F−F]−[−F+F+F]

Gen 5 → dense bushy tree

Angle controls spread

Hilbert Curve (δ = 90°)

Axiom: A
A → +BF−AFA−FB+
B → −AF+BFB+FA−

Gen 6 → 64×64 space-filling path

D = 2, used for memory locality

4. JavaScript engine — expand and draw

The engine has two phases: string expansion (generate the L-system string up to n iterations) and turtle interpretation (walk the string and draw line segments on Canvas 2D).

// ── 1. L-system string expansion ──────────────────────────────────
function expand(axiom, rules, generations) {
  let str = axiom;
  for (let g = 0; g < generations; g++) {
    // Build output char by char — avoids repeated string concatenation
    const parts = [];
    for (const ch of str) {
      parts.push(rules[ch] ?? ch);  // replace if rule exists, else keep
    }
    str = parts.join('');
  }
  return str;
}

// ── 2. Turtle interpretation on Canvas 2D ─────────────────────────
function draw(ctx, str, x0, y0, angle0, step, delta) {
  const stack = [];
  let x = x0, y = y0, angle = angle0;
  const RAD = Math.PI / 180;

  ctx.beginPath();
  ctx.moveTo(x, y);

  for (const ch of str) {
    switch (ch) {
      case 'F':
        x += step * Math.cos(angle * RAD);
        y -= step * Math.sin(angle * RAD);
        ctx.lineTo(x, y);
        break;
      case 'f':
        x += step * Math.cos(angle * RAD);
        y -= step * Math.sin(angle * RAD);
        ctx.moveTo(x, y);
        break;
      case '+': angle += delta; break;
      case '-': angle -= delta; break;
      case '[': stack.push({ x, y, angle }); break;
      case ']':
        ({ x, y, angle } = stack.pop());
        ctx.moveTo(x, y);
        break;
    }
  }
  ctx.stroke();
}

Usage example — fractal plant at 6 generations

const rules = {
  X: 'F+[[X]-X]-F[-FX]+X',
  F: 'FF'
};
const str = expand('X', rules, 6);

const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#22c55e';
ctx.lineWidth   = 0.7;
draw(ctx, str,
  canvas.width / 2,  // start x
  canvas.height - 20, // start y (bottom centre)
  90,                 // initial angle (up)
  5,                  // step length (shrinks with gen)
  25                  // delta angle
);
String length explosion: rules like F → FF double the string length each generation. Generation 10 produces 2¹⁰ = 1024 segments, gen 20 over a million. For high-generation L-systems skip string materialisation entirely and use a recursive interpreter with a depth counter — same result, constant stack space.

5. Stochastic and context-sensitive rules

Stochastic L-systems

Replace a deterministic rule A → xyz with a set of alternative productions each with a probability that sums to 1. Running the same grammar twice produces different plants — essential for avoiding the artificial uniformity of deterministic systems.

// Stochastic L-system — F has two productions chosen by random
const stochastic = {
  F: [
    { weight: 0.33, result: 'F[+F]F[-F]F' },
    { weight: 0.33, result: 'F[+F]F'       },
    { weight: 0.34, result: 'F[-F]F'       },
  ]
};

function expandStochastic(axiom, rules, gens) {
  let str = axiom;
  for (let g = 0; g < gens; g++) {
    const parts = [];
    for (const ch of str) {
      if (rules[ch]) {
        const r = Math.random();
        let cumW = 0;
        for (const prod of rules[ch]) {
          cumW += prod.weight;
          if (r < cumW) { parts.push(prod.result); break; }
        }
      } else {
        parts.push(ch);
      }
    }
    str = parts.join('');
  }
  return str;
}

Context-sensitive rules

A rule can condition replacement on the left or right neighbour: A<B>C → D means "replace B by D when preceded by A and followed by C". This models inter-cell chemical signalling — the original biological motivation for Lindenmayer's work.

6. 3D L-systems in Three.js

For 3D branching the turtle carries a 3×3 rotation matrix (heading H, left L, up U vectors) rather than a single heading angle. The 3D symbols &, ^, \, / apply rotation matrices around each axis.

// 3D turtle — heading (H), left (L), up (U) frame
import * as THREE from 'three';

class Turtle3D {
  constructor() {
    this.pos   = new THREE.Vector3();
    this.H     = new THREE.Vector3(0, 1, 0);  // heading: up
    this.L     = new THREE.Vector3(-1, 0, 0); // left
    this.U     = new THREE.Vector3(0, 0, 1);  // up-vector
    this.stack = [];
    this.lines = [];  // Array of [from, to] Vector3 pairs
  }

  rotate(axis, angleRad) {
    const q = new THREE.Quaternion().setFromAxisAngle(axis, angleRad);
    this.H.applyQuaternion(q);
    this.L.applyQuaternion(q);
    this.U.applyQuaternion(q);
  }

  forward(step) {
    const from = this.pos.clone();
    this.pos.addScaledVector(this.H, step);
    this.lines.push([from, this.pos.clone()]);
  }

  interpret(str, step, delta) {
    const r = delta * Math.PI / 180;
    for (const ch of str) {
      switch (ch) {
        case 'F': this.forward(step); break;
        case '+': this.rotate(this.U,  r); break;
        case '-': this.rotate(this.U, -r); break;
        case '&': this.rotate(this.L,  r); break;
        case '^': this.rotate(this.L, -r); break;
        case '/': this.rotate(this.H,  r); break;
        case '\\': this.rotate(this.H, -r); break;
        case '[': this.stack.push({
          pos: this.pos.clone(),
          H: this.H.clone(), L: this.L.clone(), U: this.U.clone()
        }); break;
        case ']': {
          const s = this.stack.pop();
          this.pos.copy(s.pos); this.H.copy(s.H);
          this.L.copy(s.L); this.U.copy(s.U);
        } break;
      }
    }
  }
}

Build a Three.js LineSegments object from the collected line pairs:

function buildMesh(lines, scene) {
  const positions = [];
  for (const [a, b] of lines) {
    positions.push(a.x, a.y, a.z, b.x, b.y, b.z);
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  const mat = new THREE.LineBasicMaterial({ color: 0x22c55e, linewidth: 1 });
  scene.add(new THREE.LineSegments(geo, mat));
}

🐦 Boids Simulation

Watch emergent flocking behaviour from just three simple rules — separation, alignment and cohesion.

Open Boids →

7. Open L-systems and parametric extensions

The version of L-systems described so far are closed: the grammar has no interaction with the environment. Prusinkiewicz and Lindenmayer's 1990 book The Algorithmic Beauty of Plants extends the formalism in several ways:

The Algorithmic Beauty of Plants (Prusinkiewicz & Lindenmayer, 1990) is available freely online and contains hundreds of plant L-systems with full parameter tables — the definitive reference for anyone implementing plant simulation.