Біологія · Алгоритми · Фрактали
📅 Березень 2026 ⏱ ≈ 10 хв читання 🎯 Початковий–середній · Останнє оновлення: 28 травня 2026 р.

L-системи та ріст рослин — граматики Ліндемаєра, черепашача графіка та 3D-гілки

У 1968 році ботанік Арістід Ліндемаєр розробив формалізм переписування рядків для моделювання поділу клітин у водоростях. Та сама проста ідея — замінювати символи паралельно — породжує переконливі дерева, папороті, морські водорості, корали та берегові лінії.

1. Граматика — аксіома, алфавіт і правила породження

L-система — це формальна граматика G = (V, ω, P), де:

На кожному поколінні усі символи рядка переписуються одночасно — саме ця паралельна заміна відрізняє L-системи від стандартних граматик Хомського і природно породжує самоподібну структуру.

Водорості (Ліндемаєр, 1968):
Змінні: A B
Аксіома: A
Правила: A → AB
       B → A

Покоління 0: A
Покоління 1: AB
Покоління 2: ABA
Покоління 3: ABAAB
Покоління 4: ABAABABA
Покоління 5: ABAABABAABAAB
Довжина зростає як числа Фібоначчі: 1,2,3,5,8,13,…

2. Інтерпретація черепашачої графіки

Рядок L-системи набуває візуального змісту, якщо інтерпретувати кожен символ як команду черепашки. Черепашка має позицію (x, y), напрямок θ та необов'язковий стек для розгалуження.

Символ Команда
F Рух уперед на довжину кроку d з прокресленням відрізка
f Рух уперед на d без прокреслення (стрибок)
+ Поворот ліворуч на кут δ
Поворот праворуч на кут δ
[ Покласти поточний стан (позиція + кут) у стек
] Зняти стан зі стека (повернутися до збереженої позиції)
| Поворот на 180°
& Нахил униз на δ (3D)
^ Нахил угору на δ (3D)
\ Крен ліворуч на δ (3D)
/ Крен праворуч на δ (3D)

Операції зі стеком [ та ] якраз і створюють гілки: покласти стан біля основи гілки, заглибитися в гілку, повернутися до основи — фундаментальний прийом усіх L-систем дерев і кущів.

3. Класичні L-системи

Крива Коха (δ = 60°)

Аксіома: F
F → F+F−−F+F

Покоління 4: FFFF… (1024 сегменти)

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

Трикутник Серпінського (δ = 60°)

Аксіома: F−G−G
F → F−G+F+G−F
G → GG

3 рекурсивні рівні заповнюють трикутник

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

Фрактальна рослина (δ = 25°)

Аксіома: X
X → F+[[X]−X]−F[−FX]+X
F → FF

Покоління 6 → реалістична листяна гілка

Класичний гіллястий кущ

Крива дракона (δ = 90°)

Аксіома: FX
X → X+YF+
Y → −FX−Y

Покоління 16 → складений паперовий дракон

D = 2 (заповнює ділянку площини)

Кущ (δ = 22.5°)

Аксіома: F
F → FF+[+F−F−F]−[−F+F+F]

Покоління 5 → густе кущисте дерево

Кут керує розкидом

Крива Гільберта (δ = 90°)

Аксіома: A
A → +BF−AFA−FB+
B → −AF+BFB+FA−

Покоління 6 → заповнюючий простір шлях 64×64

D = 2, використовується для локальності пам'яті

4. JavaScript-рушій — розгортання та малювання

Рушій має дві фази: розгортання рядка (згенерувати рядок L-системи до n ітерацій) та інтерпретацію черепашки (пройти рядком і намалювати відрізки на Canvas 2D).

// ── 1. Розгортання рядка L-системи ──────────────────────────────────
function expand(axiom, rules, generations) {
  let str = axiom;
  for (let g = 0; g < generations; g++) {
    // Будуємо вивід символ за символом — уникаємо повторної конкатенації рядків
    const parts = [];
    for (const ch of str) {
      parts.push(rules[ch] ?? ch);  // замінити, якщо є правило, інакше лишити
    }
    str = parts.join('');
  }
  return str;
}

// ── 2. Інтерпретація черепашки на 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();
}

Приклад використання — фрактальна рослина на 6 поколіннях

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,  // початкова x
  canvas.height - 20, // початкова y (внизу по центру)
  90,                 // початковий кут (угору)
  5,                  // довжина кроку (зменшується з поколінням)
  25                  // кут дельта
);
Вибух довжини рядка: правила на кшталт F → FF подвоюють довжину рядка щопокоління. Покоління 10 дає 2¹⁰ = 1024 сегменти, покоління 20 — понад мільйон. Для L-систем з високими поколіннями взагалі не матеріалізуйте рядок, а використовуйте рекурсивний інтерпретатор зі лічильником глибини — той самий результат, стала пам'ять стека.

5. Стохастичні та контекстно-залежні правила

Стохастичні L-системи

Замініть детерміноване правило A → xyz набором альтернативних породжень, кожне з імовірністю, що в сумі дає 1. Двократний запуск тієї самої граматики дає різні рослини — це необхідно, щоб уникнути штучної одноманітності детермінованих систем.

// Стохастична L-система — F має дві продукції, що обираються випадково
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;
}

Контекстно-залежні правила

Правило може зумовлювати заміну лівим чи правим сусідом: A<B>C → D означає «замінити B на D, коли йому передує A і за ним іде C». Це моделює міжклітинну хімічну сигналізацію — початкову біологічну мотивацію роботи Ліндемаєра.

6. 3D L-системи у Three.js

Для 3D-розгалуження черепашка несе матрицю обертання 3×3 (вектори напрямку H, лівий L, верхній U), а не один кут напрямку. 3D-символи &, ^, \, / застосовують матриці обертання навколо кожної осі.

// 3D-черепашка — система координат напрямок (H), лівий (L), верхній (U)
import * as THREE from 'three';

class Turtle3D {
  constructor() {
    this.pos   = new THREE.Vector3();
    this.H     = new THREE.Vector3(0, 1, 0);  // напрямок: угору
    this.L     = new THREE.Vector3(-1, 0, 0); // лівий
    this.U     = new THREE.Vector3(0, 0, 1);  // верхній вектор
    this.stack = [];
    this.lines = [];  // Масив пар Vector3 [від, до]
  }

  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;
      }
    }
  }
}

Побудуйте об'єкт Three.js LineSegments зі зібраних пар ліній:

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

Подивіться, як виникає зграйна поведінка лише з трьох простих правил — розділення, вирівнювання та згуртованість.

Відкрити Boids →

7. Відкриті L-системи та параметричні розширення

Описані досі версії L-систем є закритими: граматика не взаємодіє з середовищем. Книга Прусінкевича та Ліндемаєра 1990 року The Algorithmic Beauty of Plants розширює формалізм у кількох напрямах:

The Algorithmic Beauty of Plants (Прусінкевич і Ліндемаєр, 1990) доступна безкоштовно онлайн і містить сотні рослинних L-систем з повними таблицями параметрів — основний довідник для всіх, хто реалізує симуляцію рослин.