Tutorial · Intermediate · ~60 min
Three.js · L-Systems · InstancedMesh

GPU-Accelerated L-System Trees

L-systems (Lindenmayer systems) describe complex branching structures with simple rewrite rules. This tutorial generates a realistic 3D oak, birch, or fractal fern from an axiom string, then renders 100,000+ branch instances at 60 FPS using THREE.InstancedMesh — one draw call for the entire forest.

1L-system grammar and string generation

An L-system consists of an axiom (starting string) and a set of production rules. At each generation, every symbol is simultaneously replaced by its rule's right-hand side:

// Classic 3D branching tree (Honda's tree L-system variant) const lsystem = { axiom: 'A', rules: { 'A': 'F[+A][-A][&A][^A]', // trunk → 4 rotated branches 'F': 'FF', // segment grows }, angle: 25.7 // degrees }; function expand(lsystem, generations) { let str = lsystem.axiom; for (let i = 0; i < generations; i++) { let next = ''; for (const c of str) next += lsystem.rules[c] ?? c; // replace if rule exists, else keep str = next; } return str; } // At generation 5, a simple tree produces ~20,000 characters const treeString = expand(lsystem, 5); console.log('String length:', treeString.length); // ~21,764
Symbol meanings: F = draw forward, +/- = yaw ±angle, &/^ = pitch ±angle, \// = roll ±angle, [ = push stack, ] = pop stack.

2Turtle graphics interpretation in 3D

Interpret the string by moving a "turtle" through 3D space. Maintain a matrix stack for branch nesting:

import { Matrix4, Vector3, Quaternion, Euler } from 'three'; function interpretLSystem(str, segLength, angle) { const branches = []; // [{matrix, level}] let matrix = new Matrix4(); // current turtle pose const stack = []; const DEG = Math.PI / 180; for (const c of str) { switch (c) { case 'F': { // Record branch segment as a matrix branches.push(matrix.clone()); // Move turtle forward along local +Y matrix = matrix.clone().multiply( new Matrix4().makeTranslation(0, segLength, 0)); break; } case '+': matrix.multiply(new Matrix4().makeRotationZ( angle * DEG)); break; case '-': matrix.multiply(new Matrix4().makeRotationZ(-angle * DEG)); break; case '&': matrix.multiply(new Matrix4().makeRotationX( angle * DEG)); break; case '^': matrix.multiply(new Matrix4().makeRotationX(-angle * DEG)); break; case '\\': matrix.multiply(new Matrix4().makeRotationY( angle * DEG)); break; case '/': matrix.multiply(new Matrix4().makeRotationY(-angle * DEG)); break; case '[': stack.push(matrix.clone()); break; case ']': matrix = stack.pop(); break; } } return branches; }

3Stochastic L-systems

Deterministic L-systems produce perfectly regular fractals. Add randomness by specifying multiple rules with probabilities for the same symbol:

const stochasticTree = { axiom: 'A', rules: { 'A': [ { p: 0.33, s: 'F[+A][-A]' }, { p: 0.33, s: 'F[+A][&A]' }, { p: 0.34, s: 'F[-A][^A][+A]' }, ], 'F': [{ p: 1.0, s: 'FF' }] }, angle: 22.5 }; function expandStochastic(lsystem, gens, seed = 42) { let rng = seed; const rand = () => { rng = (rng * 1664525 + 1013904223) & 0xffffffff; return (rng >>> 0) / 0xffffffff; }; let str = lsystem.axiom; for (let g = 0; g < gens; g++) { let next = ''; for (const c of str) { const candidates = lsystem.rules[c]; if (!candidates) { next += c; continue; } let r = rand(), cumulative = 0; for (const { p, s } of candidates) { cumulative += p; if (r <= cumulative) { next += s; break; } } } str = next; } return str; }

4InstancedMesh — one draw call

THREE.InstancedMesh renders N copies of the same geometry in a single draw call by sending all transformation matrices to the GPU as a buffer. A single cylinder branch repeated 20,000 times is trivially fast:

const SEG_LENGTH = 0.2, SEG_RADIUS = 0.015; const branches = interpretLSystem(treeString, SEG_LENGTH, 25.7); const branchGeo = new THREE.CylinderGeometry(SEG_RADIUS, SEG_RADIUS * 1.4, SEG_LENGTH, 5, 1); const branchMat = new THREE.MeshLambertMaterial({ color: 0x5c3b1e }); const iMesh = new THREE.InstancedMesh(branchGeo, branchMat, branches.length); iMesh.castShadow = true; const tmpMatrix = new THREE.Matrix4(); branches.forEach((mat, i) => { // Rotate cylinder (default along Y) to align with branch direction iMesh.setMatrixAt(i, mat); }); iMesh.instanceMatrix.needsUpdate = true; scene.add(iMesh);
For a forest with 100 trees of 20,000 branches each (2M total), use a single InstancedMesh with capacity 2,000,000. This draws the entire forest in one GPU call. On an M1 Mac, this runs at 60 FPS at 1440p.

5Leaf billboards

Add leaves as camera-facing sprites at branch endpoints. Mark a special leaf symbol L in the L-system and collect their positions during interpretation:

// In interpretLSystem, also track leaf positions case 'L': { const pos = new Vector3().applyMatrix4(matrix); leafPositions.push(pos); break; } // Render as InstancedMesh of flat PlaneGeometry const leafGeo = new THREE.PlaneGeometry(0.3, 0.3); const leafTex = new THREE.TextureLoader().load('leaf.png'); const leafMat = new THREE.MeshBasicMaterial({ map: leafTex, side: THREE.DoubleSide, alphaTest: 0.5, transparent: true }); const leafMesh = new THREE.InstancedMesh(leafGeo, leafMat, leafPositions.length); // In animation loop: rotate each leaf to face camera const cameraDir = new Vector3(); function updateLeaves() { camera.getWorldDirection(cameraDir); const q = new THREE.Quaternion().setFromUnitVectors( new THREE.Vector3(0, 0, 1), cameraDir.negate()); const r = new THREE.Matrix4().makeRotationFromQuaternion(q); leafPositions.forEach((pos, i) => { leafMesh.setMatrixAt(i, r.clone().setPosition(pos)); }); leafMesh.instanceMatrix.needsUpdate = true; }