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; }