前言

上一篇文章我們用遞迴畫出了碎形樹,但那棵樹的結構是寫死在程式碼裡的。如果我想畫一棵不同形態的植物——比如蕨類、灌木、或是花朵——我就得重寫整個遞迴邏輯。

有沒有一種更優雅的方式,只要改幾個「規則」就能生成完全不同的植物形態?

有。答案是 L-System(Lindenmayer System)

L-System 是 1968 年由匈牙利植物學家 Aristid Lindenmayer 提出的文法系統,最初用來描述植物的生長模式。它的核心思想驚人地簡單:用字串替換規則來描述結構的演化,然後用 turtle graphics 把字串畫成圖形

我一直覺得 L-System 是程式設計中最美的概念之一——它就像一門語言,用幾條文法規則就能「寫出」一棵植物。更神奇的是,不同的規則會長出完全不同的植物。這不就是 DNA 的隱喻嗎?


L-System 文法規則

L-System 由三個部分組成:

  1. 字母表(Alphabet):系統中使用的字元集合
  2. 公理(Axiom):初始字串,生長的種子
  3. 規則(Rules):字串替換規則,定義每一代如何演化

最簡單的例子:

字母表:F, +, -, [, ]
公理:F
規則:F → F[+F]F[-F]F

這表示:每一代,所有的 F 都會被替換成 F[+F]F[-F]F

經過幾代之後:

  • 第 0 代:F
  • 第 1 代:F[+F]F[-F]F
  • 第 2 代:F[+F]F[-F]F[+F[+F]F[-F]F]F[+F]F[-F]F[-F[+F]F[-F]F]F[+F]F[-F]F
  • ……字串呈指數膨脹

每個字元的含義:

  • F:前進並畫線
  • +:順時針旋轉
  • -:逆時針旋轉
  • [:保存當前狀態(push)
  • ]:恢復上一個狀態(pop)

字串迭代

先實作字串的迭代(generation)邏輯:

class LSystem {
  constructor(axiom, rules) {
    this.axiom = axiom;
    this.rules = rules;
    this.sentence = axiom;
    this.generation = 0;
  }

generate() { let next = ''; for (let i = 0; i < this.sentence.length; i++) { let ch = this.sentence[i]; let found = false;

for (let rule of this.rules) { if (ch === rule.from) { next += rule.to; found = true; break; } }

// 如果沒有對應規則,原樣保留 if (!found) { next += ch; } }

this.sentence = next; this.generation++; }

getSentence() { return this.sentence; }

reset() { this.sentence = this.axiom; this.generation = 0; } }

使用方式:

let lsys = new LSystem('F', [
  { from: 'F', to: 'F[+F]F[-F]F' }
]);

lsys.generate(); // 第 1 代 lsys.generate(); // 第 2 代 lsys.generate(); // 第 3 代 lsys.generate(); // 第 4 代

console.log(lsys.getSentence()); // 非常長的字串...


Turtle Graphics 繪製

有了字串,接下來要把它「畫」出來。我們用 turtle graphics 的方式:想像一隻烏龜在畫布上走路,根據字元指令前進、轉彎、或跳到之前的位置。

class Turtle {
  constructor(x, y, angle, stepLength) {
    this.pos = createVector(x, y);
    this.angle = -HALF_PI;  // 初始朝上
    this.stepLength = stepLength;
    this.angleStep = angle;  // 每次旋轉的角度
    this.stack = [];
  }

draw(sentence) { for (let i = 0; i < sentence.length; i++) { let ch = sentence[i];

switch (ch) { case 'F': case 'G': // 有些 L-System 用 G 表示前進 let newX = this.pos.x + this.stepLength * cos(this.angle); let newY = this.pos.y + this.stepLength * sin(this.angle); line(this.pos.x, this.pos.y, newX, newY); this.pos.set(newX, newY); break;

case 'f': // 前進但不畫線 this.pos.x += this.stepLength * cos(this.angle); this.pos.y += this.stepLength * sin(this.angle); break;

case '+': this.angle += this.angleStep; break;

case '-': this.angle -= this.angleStep; break;

case '[': this.stack.push({ x: this.pos.x, y: this.pos.y, angle: this.angle }); break;

case ']': let state = this.stack.pop(); this.pos.set(state.x, state.y); this.angle = state.angle; break; } } } }

[] 就是 push/pop 操作——它們讓烏龜可以「記住」某個分叉點,先去畫一根分支,畫完再回到分叉點畫另一根分支。這正是植物分枝的核心機制。


不同植物形態

L-System 最迷人的地方在於:改幾個字元,就能長出完全不同的植物

灌木型

let lsys = new LSystem('F', [
  { from: 'F', to: 'FF+[+F-F-F]-[-F+F+F]' }
]);
let angle = radians(22.5);
let generations = 4;

蕨類

let lsys = new LSystem('X', [
  { from: 'X', to: 'F+[[X]-X]-F[-FX]+X' },
  { from: 'F', to: 'FF' }
]);
let angle = radians(25);
let generations = 6;

注意這裡用了 X 作為輔助字元——X 不會被繪製,只參與字串替換。這讓規則可以更靈活。

直立型植物

let lsys = new LSystem('X', [
  { from: 'X', to: 'F[+X]F[-X]+X' },
  { from: 'F', to: 'FF' }
]);
let angle = radians(20);
let generations = 6;

花朵型

let lsys = new LSystem('X', [
  { from: 'X', to: 'F[+X][-X]FX' },
  { from: 'F', to: 'FF' }
]);
let angle = radians(25.7);
let generations = 6;

完整範例:互動式 L-System

讓我們整合所有部分,做一個可以切換不同植物、調整參數的互動範例:

let lsys;
let turtle;
let presets;
let currentPreset = 0;

function setup() { createCanvas(800, 700);

presets = [ { name: '灌木', axiom: 'F', rules: [{ from: 'F', to: 'FF+[+F-F-F]-[-F+F+F]' }], angle: 22.5, generations: 4, stepLength: 6 }, { name: '蕨類', axiom: 'X', rules: [ { from: 'X', to: 'F+[[X]-X]-F[-FX]+X' }, { from: 'F', to: 'FF' } ], angle: 25, generations: 6, stepLength: 3 }, { name: '直立草', axiom: 'X', rules: [ { from: 'X', to: 'F[+X]F[-X]+X' }, { from: 'F', to: 'FF' } ], angle: 20, generations: 6, stepLength: 3 }, { name: '花朵', axiom: 'X', rules: [ { from: 'X', to: 'F[+X][-X]FX' }, { from: 'F', to: 'FF' } ], angle: 25.7, generations: 6, stepLength: 2 }, { name: '雜草', axiom: 'F', rules: [{ from: 'F', to: 'F[+F]F[-F]F' }], angle: 25.7, generations: 4, stepLength: 4 } ];

loadPreset(currentPreset); noLoop(); }

function loadPreset(index) { let p = presets[index]; lsys = new LSystem(p.axiom, p.rules);

for (let i = 0; i < p.generations; i++) { lsys.generate(); }

turtle = new Turtle( width / 2, height - 20, radians(p.angle), p.stepLength ); }

function draw() { background(30, 30, 40); let p = presets[currentPreset];

// 根據生長深度改變顏色 drawColoredLSystem(lsys.getSentence(), turtle, p);

// 顯示資訊 fill(200); noStroke(); textSize(16); text('Plant: ' + p.name, 20, 30); text('Generation: ' + p.generations, 20, 55); text('Press 1-5 to switch / R to regenerate', 20, 80); }

function drawColoredLSystem(sentence, t, preset) { let pos = createVector(t.pos.x, t.pos.y); let ang = -HALF_PI; let stack = []; let depth = 0;

for (let i = 0; i < sentence.length; i++) { let ch = sentence[i];

switch (ch) { case 'F': case 'G': let newX = pos.x + t.stepLength * cos(ang); let newY = pos.y + t.stepLength * sin(ang);

// 深度越深,顏色越綠越亮 let green = map(depth, 0, 20, 80, 200); let alpha = map(depth, 0, 20, 255, 150); let sw = map(depth, 0, 20, 3, 0.5);

stroke(60, green, 50, alpha); strokeWeight(max(sw, 0.5)); line(pos.x, pos.y, newX, newY); pos.set(newX, newY); break;

case '+': ang += t.angleStep; break;

case '-': ang -= t.angleStep; break;

case '[': stack.push({ x: pos.x, y: pos.y, angle: ang, depth: depth }); depth++; break;

case ']': let state = stack.pop(); pos.set(state.x, state.y); ang = state.angle; depth = state.depth; break; } } }

function keyPressed() { if (key >= '1' && key <= '5') { currentPreset = int(key) - 1; loadPreset(currentPreset); redraw(); } if (key === 'r' || key === 'R') { loadPreset(currentPreset); redraw(); } }


隨機化 L-System

固定的規則會產生完全相同的結果。但自然界的植物每一棵都不同。我們可以加入隨機文法(stochastic grammar)

class StochasticLSystem {
  constructor(axiom, rules) {
    this.axiom = axiom;
    this.rules = rules;  // 每個 from 可以有多個 to,帶機率
    this.sentence = axiom;
    this.generation = 0;
  }

generate() { let next = ''; for (let i = 0; i < this.sentence.length; i++) { let ch = this.sentence[i]; let candidates = this.rules.filter(r => r.from === ch);

if (candidates.length > 0) { // 按機率選擇一個規則 let r = random(); let cumulative = 0; let chosen = candidates[0].to;

for (let rule of candidates) { cumulative += rule.prob; if (r <= cumulative) { chosen = rule.to; break; } } next += chosen; } else { next += ch; } }

this.sentence = next; this.generation++; } }

// 使用範例 let lsys = new StochasticLSystem('F', [ { from: 'F', to: 'F[+F]F[-F]F', prob: 0.33 }, { from: 'F', to: 'F[+F]F', prob: 0.33 }, { from: 'F', to: 'FF-[-F+F+F]+[+F-F-F]', prob: 0.34 } ]);

每次執行,同一個字元可能被替換成不同的規則,產生不同形態的植物。這就像是同一個物種的基因表達差異。


角度隨機化

除了文法層面的隨機化,你也可以在繪製時加入角度擾動:

case '+':
  ang += t.angleStep + random(-0.05, 0.05);
  break;

case '-': ang -= t.angleStep + random(-0.05, 0.05); break;

以及步長的隨機化:

case 'F':
  let step = t.stepLength * random(0.9, 1.1);
  let newX = pos.x + step * cos(ang);
  let newY = pos.y + step * sin(ang);
  // ...
  break;

這些微小的隨機擾動會讓植物看起來更有生命感——每根枝條都略有彎曲,每段莖節都略有長短。


動態生長動畫

到目前為止,我們的植物是一瞬間畫完的。如果想要看到「生長過程」,可以逐字元繪製:

let charIndex = 0;

function draw() { // 每幀多畫幾個字元 let charsPerFrame = 50;

for (let i = 0; i < charsPerFrame && charIndex < sentence.length; i++) { processChar(sentence[charIndex]); charIndex++; } }

或者更進階——每隔幾秒增加一代,讓植物一層一層長出來:

let currentGen = 0;
let maxGen = 5;

function draw() { if (frameCount % 120 === 0 && currentGen < maxGen) { currentGen++; lsys.reset(); for (let i = 0; i < currentGen; i++) { lsys.generate(); } // 每一代縮短步長,保持整體大小 turtle.stepLength = 100 / pow(2, currentGen); }

background(30, 30, 40); drawColoredLSystem(lsys.getSentence(), turtle, presets[currentPreset]); }


小結

L-System 展現了一個深刻的觀點:複雜的有機結構可以用簡單的文法規則來描述。改幾個字元,就能從灌木變成蕨類;加一點隨機性,同一棵植物就能千變萬化。

這篇文章我們學了:

  1. L-System 的三要素:字母表、公理、規則
  2. 字串迭代的實作
  3. Turtle graphics 的繪製邏輯
  4. 五種不同植物的文法規則
  5. 隨機化文法與角度擾動
  6. 動態生長動畫的技巧

如果你對 L-System 產生了興趣,我強烈建議你去讀 Prusinkiewicz 和 Lindenmayer 合著的《The Algorithmic Beauty of Plants》——這本書可以免費線上閱讀,裡面有大量精美的 L-System 植物圖形和對應的文法規則。

延伸閱讀

  • 《The Algorithmic Beauty of Plants》(Prusinkiewicz & Lindenmayer) — L-System 的聖經,免費線上版本
  • Daniel Shiffman, Coding Train: “L-System Fractal Trees” 教學
  • Paul Bourke 的 L-System 規則收藏 — 數十種不同植物的文法規則
  • p5.js 社群中的 L-System 作品集