前言
上一篇文章我們用遞迴畫出了碎形樹,但那棵樹的結構是寫死在程式碼裡的。如果我想畫一棵不同形態的植物——比如蕨類、灌木、或是花朵——我就得重寫整個遞迴邏輯。
有沒有一種更優雅的方式,只要改幾個「規則」就能生成完全不同的植物形態?
有。答案是 L-System(Lindenmayer System)。
L-System 是 1968 年由匈牙利植物學家 Aristid Lindenmayer 提出的文法系統,最初用來描述植物的生長模式。它的核心思想驚人地簡單:用字串替換規則來描述結構的演化,然後用 turtle graphics 把字串畫成圖形。
我一直覺得 L-System 是程式設計中最美的概念之一——它就像一門語言,用幾條文法規則就能「寫出」一棵植物。更神奇的是,不同的規則會長出完全不同的植物。這不就是 DNA 的隱喻嗎?
L-System 文法規則
L-System 由三個部分組成:
- 字母表(Alphabet):系統中使用的字元集合
- 公理(Axiom):初始字串,生長的種子
- 規則(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 展現了一個深刻的觀點:複雜的有機結構可以用簡單的文法規則來描述。改幾個字元,就能從灌木變成蕨類;加一點隨機性,同一棵植物就能千變萬化。
這篇文章我們學了:
- L-System 的三要素:字母表、公理、規則
- 字串迭代的實作
- Turtle graphics 的繪製邏輯
- 五種不同植物的文法規則
- 隨機化文法與角度擾動
- 動態生長動畫的技巧
如果你對 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 作品集