Learn Creative Coding (#54) - L-Systems: Growing Plants with Grammar

Learn Creative Coding (#54) - L-Systems: Growing Plants with Grammar

cc-banner

Eight episodes into the emergent systems arc. We started with binary automata (ep047), went to Game of Life (ep048), continuous automata (ep049), free-moving agents (ep050-051), and just spent two episodes on reaction-diffusion chemistry (ep052-053). All of those systems use grid cells or spatial agents with numerical rules. Today we change the formalism entirely. No grids. No floating-point fields. No coordinates. Just strings.

L-systems -- Lindenmayer systems -- grow complex structures from simple grammar rules. A Hungarian biologist named Aristid Lindenmayer invented them in 1968 to model how algae grow. The idea: start with a string of characters (the axiom), apply replacement rules to every character simultaneously, repeat. After a few iterations the string is long, but when you interpret it as drawing instructions, you get branching structures that look like actual plants. Trees, bushes, ferns, seaweed. From string manipulation.

This is not a simulation of physics. There's no force, no mass, no collision. It's pure grammar -- formal language theory applied to botany. And it produces some of the most convincing natural-looking structures in all of computer graphics. The key insight: recursive self-similarity (a branch looks like a smaller version of the whole tree) maps perfectly onto recursive string replacement. Nature's growth rules ARE grammar rules.

The simplest L-system: Lindenmayer's algae

Lindenmayer's original example modeled algae growth:

// axiom: start with a single A cell
let axiom = 'A';

// rules: A grows into AB, B matures into A
let rules = {
  'A': 'AB',
  'B': 'A'
};

function iterate(str, rules) {
  let result = '';
  for (let i = 0; i < str.length; i++) {
    const ch = str[i];
    if (rules[ch]) {
      result += rules[ch];
    } else {
      result += ch;  // no rule = keep unchanged
    }
  }
  return result;
}

// run 7 iterations
let current = axiom;
for (let gen = 0; gen < 7; gen++) {
  console.log('Gen ' + gen + ': ' + current + ' (length: ' + current.length + ')');
  current = iterate(current, rules);
}

Output:

Gen 0: A (length: 1)
Gen 1: AB (length: 2)
Gen 2: ABA (length: 3)
Gen 3: ABAAB (length: 5)
Gen 4: ABAABABA (length: 8)
Gen 5: ABAABABAABAAB (length: 13)
Gen 6: ABAABABAABAABABAABABA (length: 21)

See the string lengths? 1, 2, 3, 5, 8, 13, 21. That's the Fibonacci sequence. Lindenmayer didn't set out to make Fibonacci numbers -- he set out to model cell division. A young cell (A) grows into a mature cell with a new bud (AB). A bud (B) matures into a young cell (A). The Fibonacci growth pattern emerged from the biology. The grammar encoded a growth process and the mathematics fell out of it naturally.

No drawing yet -- just pure string manipulation. But this is the foundation everything else builds on. Every L-system works this way: take a string, apply replacement rules simultaneously to all characters, repeat. The rules are the DNA. The iterations are the growth.

Turtle graphics: turning strings into pictures

To draw L-systems we need turtle graphics. The concept comes from Logo (the programming language from the 60s). Imagine a turtle sitting on the canvas holding a pen. It understands a few commands:

  • F -- move forward by one step, drawing a line
  • + -- turn left by some angle
  • - -- turn right by some angle
  • [ -- save current position and angle (push to a stack)
  • ] -- restore saved position and angle (pop from the stack)

We walk through the generated string character by character. Each character is a turtle command. The turtle traces out the shape.

function drawLSystem(ctx, instructions, startX, startY, angle, stepLen, turnAngle) {
  let x = startX;
  let y = startY;
  let dir = angle;  // current heading in radians
  const stack = [];

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

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

    if (ch === 'F') {
      // move forward and draw
      x += Math.cos(dir) * stepLen;
      y += Math.sin(dir) * stepLen;
      ctx.lineTo(x, y);
    } else if (ch === 'f') {
      // move forward WITHOUT drawing
      x += Math.cos(dir) * stepLen;
      y += Math.sin(dir) * stepLen;
      ctx.moveTo(x, y);
    } else if (ch === '+') {
      dir -= turnAngle;  // turn left (counter-clockwise)
    } else if (ch === '-') {
      dir += turnAngle;  // turn right (clockwise)
    } else if (ch === '[') {
      stack.push({ x, y, dir });
    } else if (ch === ']') {
      const state = stack.pop();
      x = state.x;
      y = state.y;
      dir = state.dir;
      ctx.moveTo(x, y);
    }
  }

  ctx.stroke();
}

The push/pop mechanism ([ and ]) is what enables branching. When we hit [, we save where the turtle is. The turtle continues drawing (a branch). When we hit ], the turtle teleports back to the saved position and continues from there (draws another branch from the same junction). This is how one stem produces multiple branches -- the turtle visits each branch and returns to the fork point via the stack.

The trigonometry here is straight from episode 13. cos(dir) gives the x-component of movement, sin(dir) gives y. Turning changes the direction angle. That's it -- all the visual complexity comes from the string, not from the drawing code.

Koch curve: a fractal from four characters

The Koch snowflake is one of the most famous fractals. It's also a simple L-system:

const koch = {
  axiom: 'F',
  rules: { 'F': 'F+F--F+F' },
  angle: 60  // degrees
};

// generate
let str = koch.axiom;
for (let i = 0; i < 4; i++) {
  str = iterate(str, koch.rules);
}

// draw
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#44aa88';
ctx.lineWidth = 1;

const turnAngle = koch.angle * Math.PI / 180;
drawLSystem(ctx, str, 50, 400, 0, 3, turnAngle);

One rule: every straight segment F gets replaced by a kinked version F+F--F+F. After 4 iterations the string is huge but the shape is the classic Koch curve -- a jagged coastline-like boundary with infinite perimeter and finite area. That's a fractal defined in 10 characters of grammar.

The turn angle is 60 degrees because the Koch curve is built on equilateral triangle geometry. Change the angle to 90 and you get a different fractal. Change the rule to F+F-F-F+F and you get the Sierpinski arrowhead. The same drawing infrastructure handles all of them -- only the axiom, rules, and angle change.

Sierpinski triangle from grammar

const sierpinski = {
  axiom: 'A',
  rules: {
    'A': 'B-A-B',
    'B': 'A+B+A'
  },
  angle: 60
};

let str = sierpinski.axiom;
for (let i = 0; i < 6; i++) {
  str = iterate(str, sierpinski.rules);
}

// both A and B draw forward
function drawSierpinski(ctx, str, x, y, stepLen, turnAngle) {
  let dir = 0;
  ctx.beginPath();
  ctx.moveTo(x, y);

  for (let i = 0; i < str.length; i++) {
    const ch = str[i];
    if (ch === 'A' || ch === 'B') {
      x += Math.cos(dir) * stepLen;
      y += Math.sin(dir) * stepLen;
      ctx.lineTo(x, y);
    } else if (ch === '+') {
      dir -= turnAngle;
    } else if (ch === '-') {
      dir += turnAngle;
    }
  }
  ctx.stroke();
}

drawSierpinski(ctx, str, 50, 500, 2, Math.PI / 3);

Here both A and B mean "draw forward" but they have different replacement rules. This produces the Sierpinski triangle as a continuous curve -- the turtle traces the entire fractal without lifting the pen. The classic Sierpinski triangle (the one with triangular holes) drawn as a single connected path. Six iterations gives you enough detail that it looks like the real thing at screen resolution.

Dragon curve

The dragon curve is another classic. It's the shape you get if you fold a strip of paper in half repeatedly and then unfold it to 90-degree angles:

const dragon = {
  axiom: 'FX',
  rules: {
    'X': 'X+YF+',
    'Y': '-FX-Y'
  },
  angle: 90
};

let str = dragon.axiom;
for (let i = 0; i < 12; i++) {
  str = iterate(str, dragon.rules);
}

// X and Y don't draw - they're just structural
drawLSystem(ctx, str, 400, 300, 0, 4, Math.PI / 2);

X and Y are "structural" characters -- they have replacement rules but the turtle ignores them (only F draws). This is a common L-system technique: use extra symbols to control the growth pattern without directly affecting the drawing. The string grows according to one logic (the recursive folding of X and Y) but renders according to another (only F makes marks). After 12 iterations the dragon curve is a space-filling fractal that tiles the plane. Two copies fit together perfectly with no gaps.

Branching: where L-systems become plants

Everything so far has been single-line fractals. No branching. Now we use the stack -- [ and ] -- and suddenly we can grow trees:

const simpleTree = {
  axiom: 'F',
  rules: { 'F': 'FF+[+F-F-F]-[-F+F+F]' },
  angle: 22.5
};

let str = simpleTree.axiom;
for (let i = 0; i < 4; i++) {
  str = iterate(str, simpleTree.rules);
}

ctx.strokeStyle = '#2d5a27';
ctx.lineWidth = 1;
drawLSystem(ctx, str, 400, 580, -Math.PI / 2, 4, simpleTree.angle * Math.PI / 180);

The rule FF+[+F-F-F]-[-F+F+F] reads like: grow the trunk (FF), turn slightly and branch left ([+F-F-F]), then from the same point turn the other way and branch right ([-F+F+F]). Each branch contains F characters that get replaced by the same rule next generation -- so branches grow sub-branches, and those grow sub-sub-branches. Four iterations and you have a convincing 2D tree silhouette.

The starting angle is -Math.PI / 2 (pointing up) because trees grow upward. The turn angle of 22.5 degrees produces naturalistic branch angles. Try 45 degrees for a more angular tree, or 15 degrees for something willow-like with narrow forks.

Parametric drawing: shrinking branches

Real trees have thick trunks and thin branches. Each generation of branching is thinner and shorter than the previous one. We can achieve this by tracking the recursion depth:

function drawWithDepth(ctx, instructions, startX, startY, baseAngle, baseLen, turnAngle, shrinkFactor) {
  let x = startX;
  let y = startY;
  let dir = baseAngle;
  let len = baseLen;
  let depth = 0;
  const stack = [];

  ctx.beginPath();

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

    if (ch === 'F') {
      const currentLen = len * Math.pow(shrinkFactor, depth);
      ctx.lineWidth = Math.max(0.5, 5 - depth * 0.8);

      ctx.moveTo(x, y);
      x += Math.cos(dir) * currentLen;
      y += Math.sin(dir) * currentLen;
      ctx.lineTo(x, y);
      ctx.stroke();
      ctx.beginPath();
    } else if (ch === '+') {
      dir -= turnAngle;
    } else if (ch === '-') {
      dir += turnAngle;
    } else if (ch === '[') {
      stack.push({ x, y, dir, depth });
      depth++;
    } else if (ch === ']') {
      const state = stack.pop();
      x = state.x;
      y = state.y;
      dir = state.dir;
      depth = state.depth;
      ctx.moveTo(x, y);
    }
  }
}

Every [ increases depth. Every ] restores the previous depth. The step length shrinks by shrinkFactor (0.7 means each generation is 70% the length of its parent). Line width decreases with depth -- thick trunk, medium branches, thin twigs. Same string, same grammar, but the visual output goes from wireframe to something that actually reads as a tree.

A shrinkFactor of 0.65-0.75 is the sweet spot for most trees. Below 0.6 the branches get too short too fast and the tree looks stunted. Above 0.8 and the branches stay too long, producing an unrealistic even canopy.

Color by depth: trunk to canopy

Add color that shifts from brown (trunk) to green (leaves) based on depth:

function getColorForDepth(depth, maxDepth) {
  const t = Math.min(1, depth / maxDepth);

  // brown trunk to green leaves
  const r = Math.floor(80 - t * 50);
  const g = Math.floor(50 + t * 140);
  const b = Math.floor(20 + t * 20);

  return 'rgb(' + r + ',' + g + ',' + b + ')';
}

function drawColoredTree(ctx, instructions, startX, startY, baseAngle, baseLen, turnAngle, shrinkFactor) {
  let x = startX;
  let y = startY;
  let dir = baseAngle;
  let depth = 0;
  const maxDepth = 5;
  const stack = [];

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

    if (ch === 'F') {
      const currentLen = baseLen * Math.pow(shrinkFactor, depth);
      ctx.strokeStyle = getColorForDepth(depth, maxDepth);
      ctx.lineWidth = Math.max(0.5, 6 - depth * 1.2);

      ctx.beginPath();
      ctx.moveTo(x, y);
      x += Math.cos(dir) * currentLen;
      y += Math.sin(dir) * currentLen;
      ctx.lineTo(x, y);
      ctx.stroke();
    } else if (ch === '+') {
      dir -= turnAngle;
    } else if (ch === '-') {
      dir += turnAngle;
    } else if (ch === '[') {
      stack.push({ x, y, dir, depth });
      depth++;
    } else if (ch === ']') {
      const state = stack.pop();
      x = state.x;
      y = state.y;
      dir = state.dir;
      depth = state.depth;
      ctx.moveTo(x, y);
    }
  }
}

The color interpolation is trivially simple but the effect is dramatic. Brown at depth 0 (trunk), progressivly greener as depth increases (branches, then twigs, then leaf-level). Combined with the thickness reduction, you get a tree that genuinely reads as "thick brown trunk with thin green canopy."

Stochastic L-systems: natural variation

Nature doesn't repeat exactly. Two branches from the same fork don't grow at the same angle or the same length. Stochastic L-systems add randomness by providing multiple possible replacements for the same symbol, each with a probability:

const stochasticRules = {
  'F': [
    { prob: 0.4, replacement: 'FF+[+F-F]-[-F+F]' },
    { prob: 0.3, replacement: 'FF-[-F+F]+[+F-F]' },
    { prob: 0.3, replacement: 'FF+[+F]-[-F]' }
  ]
};

function iterateStochastic(str, rules) {
  let result = '';
  for (let i = 0; i < str.length; i++) {
    const ch = str[i];
    if (rules[ch]) {
      // pick a random rule based on probabilities
      const roll = Math.random();
      let cumulative = 0;
      let picked = rules[ch][0].replacement;

      for (const option of rules[ch]) {
        cumulative += option.prob;
        if (roll <= cumulative) {
          picked = option.replacement;
          break;
        }
      }
      result += picked;
    } else {
      result += ch;
    }
  }
  return result;
}

// each run produces a different tree
let tree1 = 'F';
let tree2 = 'F';
for (let i = 0; i < 4; i++) {
  tree1 = iterateStochastic(tree1, stochasticRules);
  tree2 = iterateStochastic(tree2, stochasticRules);
}
// tree1 and tree2 are different strings -> different shapes

Every time you run the generation, you get a different tree. Same species (same ruleset), different individual (different random choices). The first rule produces symmetric branching, the second favors right-heavy growth, the third is simpler with fewer branches. Mix them and every tree is unique but recognizable as the same type.

This is how you'd generate a forest -- run the stochastic L-system 20 times with different random seeds and you get 20 different trees that all look like they belong to the same species. No two are identical but they share structural DNA. Exactly like nature.

Classic L-system library

Here are several well-known L-systems you can plug into the drawing code. Each produces a visually distinct plant structure:

const lsystems = {
  // bush (dense, shrubby)
  bush: {
    axiom: 'F',
    rules: { 'F': 'FF-[-F+F+F]+[+F-F-F]' },
    angle: 22.5,
    iterations: 4
  },

  // fern (asymmetric, one-sided fronds)
  fern: {
    axiom: 'X',
    rules: {
      'X': 'F+[[X]-X]-F[-FX]+X',
      'F': 'FF'
    },
    angle: 25,
    iterations: 5
  },

  // weed (thin, upright, sparse)
  weed: {
    axiom: 'F',
    rules: { 'F': 'F[+F]F[-F]F' },
    angle: 25.7,
    iterations: 4
  },

  // binary tree (perfectly symmetric)
  binaryTree: {
    axiom: 'F',
    rules: { 'F': 'F[-F]F[+F]F' },
    angle: 30,
    iterations: 4
  },

  // seaweed (flowing, asymmetric curves)
  seaweed: {
    axiom: 'F',
    rules: { 'F': 'FF+[+F-F-F]-[-F+F+F]' },
    angle: 20,
    iterations: 4
  }
};

// render any of them
function renderLSystem(name) {
  const sys = lsystems[name];
  let str = sys.axiom;
  for (let i = 0; i < sys.iterations; i++) {
    str = iterate(str, sys.rules);
  }

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawWithDepth(ctx, str, canvas.width / 2, canvas.height - 20,
    -Math.PI / 2, 8, sys.angle * Math.PI / 180, 0.7);
}

The fern uses an X symbol that doesn't draw but has a complex replacement rule. F just doubles (FF), making stems longer each generation. X is the branching logic -- it controls WHERE branches happen without directly producing line segments. This separation of concerns (growth logic vs drawing) is a powerful pattern. The structural symbols define the architecture; the drawing symbols just fill in the lines.

Try each of these. The bush is dense and round. The fern is distinctly one-sided with recurving fronds. The weed grows straight up with small side shoots. The binary tree is perfectly symmetric (unnatural but mathematically clean). The seaweed sways and curves. All from the same iterate-and-draw pipeline -- only the rules differ.

Interactive explorer

The creative exercise: build a text-input L-system explorer. Type an axiom and rules, set the angle and iterations with sliders, watch the result update live:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;

// UI elements
const axiomInput = document.getElementById('axiom');       // text input
const rulesInput = document.getElementById('rules');       // text input: "F=FF+[+F-F]-[-F+F]"
const angleSlider = document.getElementById('angle');      // range 5-90
const iterSlider = document.getElementById('iterations');  // range 1-6

function parseRules(ruleStr) {
  const rules = {};
  const parts = ruleStr.split(',');
  for (const part of parts) {
    const [key, val] = part.trim().split('=');
    if (key && val) rules[key.trim()] = val.trim();
  }
  return rules;
}

function generate() {
  const axiom = axiomInput.value || 'F';
  const rules = parseRules(rulesInput.value || 'F=FF+[+F-F]-[-F+F]');
  const angle = parseFloat(angleSlider.value) || 25;
  const iterations = parseInt(iterSlider.value) || 3;

  // generate string
  let str = axiom;
  for (let i = 0; i < iterations; i++) {
    str = iterate(str, rules);
    // safety: stop if string gets too long
    if (str.length > 500000) break;
  }

  // auto-calculate step length based on string complexity
  const fCount = (str.match(/F/g) || []).length;
  const stepLen = Math.max(1, Math.min(10, 2000 / Math.sqrt(fCount)));

  // draw
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = '#4a7c3f';
  ctx.lineWidth = 1;

  const turnAngle = angle * Math.PI / 180;
  drawLSystem(ctx, str, canvas.width / 2, canvas.height - 30,
    -Math.PI / 2, stepLen, turnAngle);
}

// update on any input change
axiomInput.addEventListener('input', generate);
rulesInput.addEventListener('input', generate);
angleSlider.addEventListener('input', generate);
iterSlider.addEventListener('input', generate);

generate();

The string length safety check is important. L-systems grow exponentially -- if a rule replaces 1 character with 5, after 6 iterations you have 5^6 = 15,625 characters. After 10 iterations you'd have 9.7 million. The browser will hang if you try to draw a string with millions of F commands. Cap it at 500,000 characters and warn the user if they hit the limit.

The auto step-length calculation is a nice touch. More F's in the string means more line segments, which means shorter segments needed to fit on screen. Taking 2000 / sqrt(fCount) gives a reasonable scaling that keeps the output roughly the same size regardless of iteration count.

Seeded randomness for reproducible variation

Combine stochastic L-systems with seeded randomness (from episode 24) and you can generate unique but reproducible plants:

// simple seeded PRNG (from ep024)
function mulberry32(seed) {
  return function() {
    seed |= 0; seed = seed + 0x6D2B79F5 | 0;
    let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

function iterateSeeded(str, rules, rng) {
  let result = '';
  for (let i = 0; i < str.length; i++) {
    const ch = str[i];
    if (rules[ch]) {
      const roll = rng();
      let cumulative = 0;
      let picked = rules[ch][0].replacement;
      for (const option of rules[ch]) {
        cumulative += option.prob;
        if (roll <= cumulative) {
          picked = option.replacement;
          break;
        }
      }
      result += picked;
    } else {
      result += ch;
    }
  }
  return result;
}

// same seed = same tree every time
function growTree(seed) {
  const rng = mulberry32(seed);
  let str = 'F';
  for (let i = 0; i < 4; i++) {
    str = iterateSeeded(str, stochasticRules, rng);
  }
  return str;
}

// tree #42 is always the same tree
const myTree = growTree(42);

Seed 42 always produces the same tree. Seed 43 produces a different tree. You can generate a forest by drawing trees at different positions with consecutive seeds. Each tree is unique but the whole forest is deterministic -- refresh the page and it's exactly the same forest. This is the same principle we used for generative art in episode 24, applied to botanical structures.

What makes L-systems special

Why do L-systems look more natural than, say, randomly placing lines at random angles? Two reasons:

First, self-similarity. Every branch is a smaller version of the tree it's attached to. When the rule for F contains F inside it, every branch grows sub-branches using the same rule. The structure is fractal -- zoom into a branch and it looks like the whole tree at a smaller scale. Real plants have this property. A branch of a fern looks like a small fern. A broccoli floret looks like a small broccoli. L-systems encode this self-similarity directly in the recursive substitution.

Second, parallelism. In an L-system, ALL characters get replaced simultaneously in each generation. This models how real plants grow -- all buds grow at the same time, not one after another. The entire organism advances one generation at once. This parallel application is what distinguishes L-systems from sequential fractal construction (where you'd build one branch completely before starting the next).

These two properties together -- self-similarity and parallel growth -- produce structures that match real plant morphology better than almost any other procedural technique. The only thing missing is physics (gravity pulling branches down, light-seeking making them curve toward the sun). We'll address that kind of thing when we revisit L-systems later in the 3D section of this series. But even without physics, the basic L-system tree is already more convincing than anything you could build by manually placing lines :-)

The connection to everything else

L-systems connect beautifully to what we've already built. The turtle graphics use trigonometry from episode 13. The stochastic version uses seeded randomness from episode 24. The fractal curves (Koch, Sierpinski, dragon) are cousins of the Mandelbrot and Julia fractals from episodes 41-42 -- different construction method, same self-similar nature.

And they connect forward. Next episode we'll expand on L-systems with parametric rules and context-sensitive grammars that produce even more realistic botanical structures -- not just the branching pattern but the actual thickness, curvature, and leaf placement of real trees. The grammar gets more sophisticated but the core mechanism stays the same: strings in, replacement rules applied, visual structure out.

Beyond that, L-systems are one example of a broader category: growth simulations. Agent-based crawlers that lay down trails. Erosion algorithms that carve terrain. Slime mold models that optimize networks. All of these share the same philosophy as L-systems -- simple local rules, applied repeatedly, producing complex global structure. The emergent systems arc continues.

't Komt erop neer...

  • L-systems (Lindenmayer systems) grow complex structures from simple grammar rules. An axiom (starting string), replacement rules, and iteration count. Every character gets replaced simultaneously each generation -- parallel rewriting that models biological growth
  • Turtle graphics interpret the generated string as drawing commands: F = draw forward, + = turn left, - = turn right, [ = save position (push stack), ] = restore position (pop stack). The push/pop mechanism enables branching -- the turtle visits each branch and teleports back to the fork
  • Classic fractal curves (Koch, Sierpinski, dragon) are L-systems with no branching. Different axioms, rules, and angles produce wildly different curves from the same iterate-and-draw pipeline
  • Branching rules with [ and ] produce plant-like structures. A rule like F -> FF+[+F-F]-[-F+F] grows a stem, branches left, returns, branches right. After 4 iterations: a convincing tree silhouette from 20 characters of grammar
  • Parametric drawing adds depth-based shrinking (shorter branches each generation), line width reduction (thick trunk to thin twigs), and color interpolation (brown to green). Same string, vastly more convincing visual output
  • Stochastic L-systems pick randomly between multiple replacement rules with assigned probabilities. Each run produces a different individual tree from the same species ruleset. Combined with seeded PRNG from episode 24, unique but reproducible plants
  • L-systems look natural because of self-similarity (branches contain smaller copies of themselves via recursive rules) and parallel growth (all characters replaced simultaneously, modeling how real buds grow concurrently)

Sallukes! Thanks for reading.

X

@femdev



0
0
0.000
0 comments