L Systems
In 1968, a Hungarian biologist named Aristid Lindenmayer wanted to understand how plants grow. He realized that plant growth could be modeled using simple string replacements, creating what is now known as an L-System.
The Axiom and the Rules:
"F") 2. We define a Rule that says how to replace that letter. (e.g., "F" -> "F+F-F-F+F") 3. The letters aren't just text; they are commands for a robot (a "Turtle") to draw a shape! 4. F means "Draw a line forward". + means "Turn Right". - means "Turn Left".1# Defining the Rules of Nature2def define_l_system_grammar():3 """Setting up the alphabet and the rules."""45 # The starting point (Axiom)6 axiom = "F"78 # The Rules: Every time you see 'F', replace it with this string!9 # F = Draw Forward10 # + = Turn Right11 # - = Turn Left12 rules = {13 "F": "F+F-F-F+F"14 }1516 return axiom, rules
The true power of an L-System comes from Recursion. We don't just apply the rule once; we take the resulting text string, and feed it back into the rule engine again!
Exponential Growth:
F2. Gen 1: F+F-F-F+F3. Gen 2: Every single F in Gen 1 is replaced by the entire rule again! The string explodes into 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. 4. Within 4 or 5 generations, a simple 5-character rule produces a string that is tens of thousands of characters long. This perfectly mimics how plant cells divide and multiply.1# Replacing Text with Text2def expand_grammar(current_string, rules):3 """The recursive heart of the algorithm."""45 next_string = ""67 # Read every character one by one8 for char in current_string:910 # If there's a rule for this letter, swap it!11 if char in rules:12 next_string += rules[char]1314 # Otherwise, just keep the letter as it is15 else:16 next_string += char1718 return next_string
In 2D, a turtle only needs to know its X/Y position and a single Angle (e.g., facing 90 degrees North). But to generate a 3D tree, the turtle must be able to pitch up/down and roll left/right like an airplane.
Local Coordinate Systems:
+ (Yaw Right) is read, the turtle rotates its Heading and Left vectors around its Up vector using a mathematical Rotation Matrix. 3. The grammar alphabet is expanded to include Pitch (& and ^) and Roll (\ and /). 4. By combining these commands, the turtle can twist and bend organically through 3D space.1# Navigating in 3D Space2def rotate_turtle(current_direction, axis, angle):3 """The Matrix handles the hard math for us."""45 # + = Turn around Z axis (Yaw)6 # & = Pitch down around X axis7 # ^ = Pitch up around X axis8 # \ = Roll left around Y axis9 # / = Roll right around Y axis1011 # We use a 3x3 Rotation Matrix to perfectly calculate the new 3D vector12 rotation_matrix = Matrix.Rotation(angle, axis)1314 new_direction = rotation_matrix * current_direction1516 return new_direction
A single line of text can only draw a single, continuous, squiggly line. But real trees have branches that split off and stop, while the main trunk keeps growing. How do we tell the Turtle to draw a branch and then come back?
Push and Pop:
[ and ]. 2. When the Turtle reads [, it saves its exact current Position and Rotation onto a memory Stack. 3. The Turtle continues drawing the sub-branch normally. 4. When the Turtle reads ], it stops drawing, grabs the saved memory from the Stack, and instantly teleports back to the main trunk! 5. Because it is a Stack (Last-In, First-Out), branches can have sub-branches, which can have sub-sub-branches indefinitely.1# Creating Branches2def process_branching_string(char, turtle, stack):3 """How to grow a leaf and return to the main trunk."""45 # '[' pushes the current State onto the Stack6 if char == "[":7 current_state = {8 "position": turtle.position.copy(),9 "heading": turtle.heading.copy()10 }11 stack.push(current_state)1213 # ']' pops the State off the Stack14 elif char == "]":15 saved_state = stack.pop()1617 # Teleport the turtle back to where it was!18 turtle.position = saved_state["position"]19 turtle.heading = saved_state["heading"]
Up until now, the Turtle has just been drawing mathematical 1D lines in empty space. But real trees have volume. To generate a 3D model we can render or 3D print, we must convert those 1D lines into solid 3D geometry.
Meshing the Skeleton:
1# Generating Real 3D Geometry2def build_3d_branch(start_point, end_point, thickness):3 """Converting invisible math lines into solid wood."""45 # 1. The Turtle drew a line from start to end6 vector = end_point - start_point7 length = length(vector)89 # 2. We generate a 3D Cylinder geometry10 branch_mesh = Cylinder(radius=thickness, height=length)1112 # 3. We move the cylinder to exactly halfway between the points13 midpoint = (start_point + end_point) / 214 branch_mesh.position = midpoint1516 # 4. We rotate the cylinder so it aligns perfectly with the vector17 branch_mesh.align_to_vector(vector)1819 return branch_mesh
If every branch we drew was the exact same length and thickness, our tree would look like a bunch of identical PVC pipes glued together. Nature doesn't work like that. As trees branch out, they taper.
Decay Variables:
F), we multiply those variables by a decay factor (e.g., 0.85). 3. Because this happens before we push the state to the Stack ([), the sub-branches inherit the smaller size! 4. This guarantees that the main trunk is massive, the branches are medium, and the outer twigs are incredibly thin and short.1# Natural Tapering2def build_tree_branch(char, turtle):3 """Making it look organic."""45 # 1. As the turtle moves forward, it draws a branch6 if char == "F":78 # Draw the cylinder using the CURRENT thickness9 build_3d_branch(turtle.position, thickness=turtle.current_thickness)1011 # 2. Every time a branch grows, it gets slightly thinner!12 # This creates the natural tapering of a real tree trunk.13 turtle.current_thickness = turtle.current_thickness * 0.851415 # 3. The branch also gets slightly shorter!16 turtle.current_length = turtle.current_length * 0.90
Spawning thousands of 3D cylinder meshes for dense fractal trees is highly expensive. Using a 2D HTML5 Canvas, we can expand our grammar up to 6 generations (generating over 15,000 branch segments) and draw them instantly at 60 FPS.
Botanical Mechanics:
1# L-System parser drawing to a 2D canvas2def draw_l_system(canvas_ctx, expanded_string, angle_rad, step_size):3 stack = []4 x, y = 0.0, 0.05 heading = -Math.PI / 2 # Upwards67 for cmd in expanded_string:8 if cmd == 'F':9 nx = x + cos(heading) * step_size10 ny = y + sin(heading) * step_size11 canvas_ctx.line(x, y, nx, ny)12 x, y = nx, ny13 elif cmd == '+':14 heading += angle_rad15 elif cmd == '-':16 heading -= angle_rad17 elif cmd == '[':18 stack.append((x, y, heading))19 elif cmd == ']':20 x, y, heading = stack.pop()