Alex's notes

GPT-4 makes L-Systems

Here is a transcript of an exchange I had with GPT-4 where I tried to get it to generate p5.js code that draws an L-system based tree. Comments in italics were added after-the-fact. I decided to remove the code because it takes up a lot of space and isn’t that interesting. I added some code examples to the end for reference.

Prompt

Here are some examples of L-systems copied from Wikipedia. Given these examples, make a new example of an L-system.

Redacted. Copy-pasted examples from Wikipedia

Response

Example 8: Spiral Fractal

variables : X Y
constants : + − [ ]
start  : X
rules  : (X → X[-YFX+X]+YF[-Y]+FX), (Y → +YFX[Y])
angle  : 45°

Here, F means “draw forward”, + means “turn left by angle”, and − means “turn right by angle”. X and Y do not correspond to any drawing action and are used to control the evolution of the curve. The square bracket “[” corresponds to saving the current values for position and angle, which are restored when the corresponding “]” is executed.

After a few iterations, the produced string will represent a spiral-like fractal pattern.

Prompt

Can you give me this example as p5.js code?

Response

Here is the L-system example as p5.js code. This code sets up the canvas, defines the L-system rules, and uses a turtle graphics system to draw the spiral fractal:

Code redacted.

This didn’t work — it made a big white canvas. However, I made a quick fix to GPT’s code with a one line change. Here is what GPT’s L-system looks like:

Prompt

Here is the source code for a p5.js program that generates an L-system.

Code redacted.

Using this code as a starting point, replace the comment that says “Implement L-system rules here” with some code that applies string replacement rules to the sentence variable. Your rules should draw a tree.

Response

Here’s an updated version of the provided code, implementing the L-system rules for a simple tree:

Code redacted.

This code creates a simple tree structure using the L-system rules. The applyRules function implements the string replacement rules, replacing “F” with “F[+F]F[-F]F”. The tree will be drawn when you run the code in the p5.js web editor (https://editor.p5js.org/). Click on the canvas to stop the loop.

Here is GPT’s first attempt at a tree:

Prompt

I ran this code in the p5.js editor and I have some feedback. The result has a branching structure, but the branches go off the sides of the canvas. Also, the structure does not “fan out” like a tree does. Could you modify the code to improve the look of the tree?

Response

I apologize for the initial result. I’ve modified the code to improve the look of the tree. The new code below adjusts the starting position, angle, and L-system rules to create a more tree-like appearance:

GPT’s second try:

Prompt

This looks better! However most of the tree is still off the edge of the canvas. Can you try again?

GPT’s third try:

This seemed like a dead end to me. Let’s try something different. This part impressed me more.

Prompt

Here is my version of the code that I think produces a nice looking tree. Can you explain why my code makes a nicer looking tree than yours?

Code redacted.

My tree:

Response

Your version of the code produces a nicer-looking tree for several reasons:

  1. Rule modification: In your applyRules function, you have changed the string replacement rules to “F[+F][-F][F]”. This modification results in more balanced branching, as it now has a mirrored structure where branches grow on both sides of the parent branch. Damn! I can’t believe that it can interpret the L-system replacement rule like that. It shows more than surface-level understanding.

  2. Random rotation: Instead of using a fixed angle, your code introduces randomness to the rotation by using rotate(random(-1, 0)) and rotate(random(0, 1)). This randomness creates a more natural and organic appearance, as real trees do not have perfectly symmetrical branches.

  3. Dynamic stroke weight: In your renderString function, you adjust the stroke weight based on the branch length: strokeWeight(branchLength / 20). This creates a more realistic visual effect, as branches in real trees tend to become thinner as they grow farther from the trunk. Love that GPT references the fact that branches in real trees become thinner as they grow farther from the trunk. The fact that it was able to make that connection from the code is SO CRAZY.

  4. Initial branch length: You’ve increased the initial branch length to 70. This change, in combination with your other modifications, helps produce a visually appealing tree structure that fits well within the canvas.

Overall, your code introduces a more organic and balanced appearance to the tree by incorporating randomness and more realistic visual features.

Wow! Its bullet points are exactly right and explained very clearly. This makes me think that GPT-4 would be really good at automatically writing git commits or PR descriptions. I’m sure there are already 50 libraries that do exactly that.

Appendix

The code that I initially gave GPT:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background('black');
  
  
  const axiom = "F";
  let sentence = axiom;
  for (let i = 0; i < 7; i++) {
    // Implement L-system rules here
  }
  
  
  translate(width / 2, height / 2);
  strokeWeight(3);
  stroke('white');
  renderString(sentence);
}

let branchLength = 140;
let angle = Math.PI / 4;

// This is the turtle graphics function that renders the string.
// It has the following rules:
// 
// F means go forward 100 pixels
// + means rotate to the right by PI / 4
// - means rotate to the left by PI /4
// [ means push the current transform onto the stack
// ] means pop the current transform onto the stack
function renderString(string) {
  for (let character of string) {
    if (character == 'F') {
      line(0, 0, 0, -100);
      translate(0, -100);
    } else if (character == '+') {
      rotate(angle);
    } else if (character == '-') {
      rotate(-angle);
    } else if (character == '[') {
      branchLength = branchLength / 1.5;
      push();
    } else if (character == ']') {
      branchLength = branchLength * 1.5;
      pop();
    }
  }
}

It’s first attempt at a tree:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background('black');

  const axiom = "F";
  let sentence = axiom;
  for (let i = 0; i < 5; i++) {
    sentence = applyRules(sentence);
  }

  translate(width / 2, height);
  strokeWeight(3);
  stroke('white');
  renderString(sentence);
}

let branchLength = 100;
let angle = Math.PI / 4;

function applyRules(s) {
  let newSentence = "";
  for (let character of s) {
    if (character == "F") {
      newSentence += "F[+F]F[-F]F";
    } else {
      newSentence += character;
    }
  }
  return newSentence;
}

function renderString(string) {
  for (let character of string) {
    if (character == 'F') {
      line(0, 0, 0, -branchLength);
      translate(0, -branchLength);
    } else if (character == '+') {
      rotate(angle);
    } else if (character == '-') {
      rotate(-angle);
    } else if (character == '[') {
      branchLength = branchLength / 1.5;
      push();
    } else if (character == ']') {
      branchLength = branchLength * 1.5;
      pop();
    }
  }
}

function mouseClicked() {
  noLoop();
}

My version of the tree:

function setup() {
  createCanvas(400, 400);
  noLoop();
}

function draw() {
  background('black');

  const axiom = "F";
  let sentence = axiom;
  for (let i = 0; i < 5; i++) {
    sentence = applyRules(sentence);
  }

  translate(width / 2, height - 30);
  strokeWeight(2);
  stroke('white');
  renderString(sentence);
}

let branchLength = 70;
let angle = 22.5;

function applyRules(s) {
  let newSentence = "";
  for (let character of s) {
    if (character == "F") {
      newSentence += "F[+F][-F][F]";
    } else {
      newSentence += character;
    }
  }
  return newSentence;
}

function renderString(string) {
  for (let character of string) {
    if (character == 'F') {
      strokeWeight(branchLength / 20)
      line(0, 0, 0, -branchLength);
      translate(0, -branchLength);
    } else if (character == '+') {
      rotate(random(-1, 0));
    } else if (character == '-') {
      rotate(random(0, 1));
    } else if (character == '[') {
      branchLength = branchLength * 0.8;
      push();
    } else if (character == ']') {
      branchLength = branchLength / 0.8;
      pop();
    }
  }
}
?
| / | | |\ \| \| | |/ | | O | | | \ | | | \|/ / | | | | \| \| o | O . | \ ---| / \ | \ \| / |\| - \ --- |--- |/|/| / \ . \|/|/ | | | - \|\ | | . /| \|/ | \|/| /|/| | | | . | -| |/ | |/ | | / \ |/ \| \| | /\|/ \| \---| - | || |/ | | \ \|/| -| ||\| | \ - | |/ |\|| | / \ \|/| |/| \| / - | - | \ | |- |/ \|/ \ / . | ./\|/ \| . | --- \ | - /| /| / / \ \|/|/ |\-- | /|/ \ / | | / - \|/ | -- \|/| - \ | / . \ |\|/ |\|/|./ / \ | | - | | |/ ---| \ | | |\|\|\| - \|/- \|/|\| | | | \ | \|| | |/ / \| . - | |/\ \| / | \ \| | - |/ / \ \ \ | / \| --- . \| ---- - | - |/ | - \ |/ / \|/ | \|/ \| --- | | | |/ | | | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^