14 / GRAPHICS
Proce-Tree — Procedural Tree Generator
A real-time procedural tree grown from a resource-flow model — branches that steer away from their own crowded canopy, cross-sectional area conserved at every fork, depth-decaying splits, and camera-facing instanced leaves, in Three.js.
- JavaScript
- Three.js
- Vite
- Growth model
- resource-flow + area
- Branch steering
- canopy-density avoidance
- Leaves
- instanced billboards
- Engine
- ~520 LOC
A procedural tree that grows rather than being drawn. Each frame the root is fed a “growth resource” (0.075 per tick). The resource flows up the tree, thickening branches and pushing tips outward until they split, with two botanical constraints baked into the rules: mass is conserved, and branches avoid crowding their own canopy. The README calls it “L-system-inspired”, but there is no string rewriting anywhere in it. It’s a small resource-flow simulation over a binary branch tree.
The growth model: a resource that flows and conserves area
A branch is a node with a length, a cross-sectional area, and two children. Calling grow(feed)
(tree.js:52) does two things. A tip extends by ∛feed and converts the rest of its feed into
extra area. An already-split branch decides how much feed to keep vs pass on to its children. That
split is where the physics lives: with area conservation on, the pass ratio is
(A.area + B.area) / (A.area + B.area + this.area) (tree.js:77-79), so the parent only thickens
in proportion to the area it already carries relative to its children. The result is a trunk that
genuinely tapers into its limbs instead of every branch ballooning equally. Radius is recovered from
area the honest way, r = √(area/π).
feed f (this frame)
|
v
+------------------------------+
| parent branch (area = self) |
+------------------------------+
| |
| keep: | pass:
| f * self/(A+B+self) | f * (A+B)/(A+B+self)
v |
thickens parent +------+------+
| |
v v
+----------+ +----------+
| child A | | child B |
| area = A | | area = B |
+----------+ +----------+
Fig. 1 — feed flow at a fork: the pass ratio conserves cross-sectional area, which is what makes the trunk taper into its limbs.
A tip splits once it passes a length threshold that decays with depth, splitsize · e^(−decay·depth)
(tree.js:65-68, splitsize = 2.2, decay = 0.1), so the crown forks readily while the trunk
stays long. The way a real tree does. Growth stops recursing once the feed reaching a subtree drops
below 1e-5, which is what keeps the per-frame traversal bounded.
Branches that avoid their own canopy
When a branch splits, the two children don’t just inherit a fixed angle. leafdensity()
(tree.js:140-179) walks the subtree to compute a weighted-average position of the surrounding
leaves, then returns a direction that points away from that centroid, blended with a little noise
(globalDirectedness = 0.7 weights the two). The new branch directions are built perpendicular to
the parent and lerped back toward it by the feed ratio. Growth fills empty space and self-shadowing
drops — a cheap trick, but a convincing one, and it’s what makes the canopy read as organic.
top-down view at a fork
* *
* c * leaves of the subtree;
* * c = weighted-average
. position (leafdensity)
.
. d = direction from fork to c
.
o fork point
\
\ children steer along -d
\ (away from c) blended
v with noise; then lerped
back toward the parent
by the feed ratio
Fig. 2 — canopy avoidance: new children steer away from the weighted leaf centroid, into the empty side of the canopy.
Rendering: instanced billboard leaves
Leaves only spawn past a minimum depth (globalLeafMinDepth = 3), 30 per terminal branch, scattered
with a seeded hash (hashRand(ID + i), tree.js:215-217) so each branch’s foliage is consistent
frame-to-frame rather than flickering. constructBillboardLeafMatrices (tree.js:251-266) then
recomposes every leaf matrix from its position, the camera’s quaternion and a uniform 0.2 scale,
so each leaf turns to face the viewer. They all render through a single InstancedMesh
(main.js:169-187): thousands of leaves in one draw call.
every frame
-----------
feed the root
|
v
grow() recurses ........ tips extend by feed^(1/3);
| forks split feed by area
v
rebuild branch geometry dispose + rebuild every
| tapered cylinder; no
v incremental update
rebuild leaf matrices .. hashRand(ID+i) keeps the
| foliage stable; each one
v takes the camera's rotation
one InstancedMesh draw thousands of leaves in
a single draw call
Fig. 3 — the per-frame pipeline: feed in at the root, one instanced draw call out.
Honest scope
This is a focused generative-graphics piece, not a production renderer. It uses Three.js’s built-in
MeshBasicMaterial throughout: no custom GLSL, no lighting. Branches are 8-sided tapered cylinders
(top radius = 0.525 × bottom); the “billboard” leaves are actually unit BoxGeometry cubes scaled
to 0.2 and rotated to the camera — cheap and convincing at distance, but boxes, not textured quads.
A leaf PNG is loaded in main.js and then never bound to the leaf material, so it is dead code.
It grows a single tree. No wind, no pruning, no terrain placement. And it disposes and rebuilds all
branch geometry from scratch every frame rather than updating incrementally — fine at this scale,
but not tuned for a forest. The interesting part is the algorithm: a small, honest model where area
conservation and density-avoidance do the heavy lifting.
ROLE
Sole author. Designed the growth algorithm — a binary-splitting branch model fed by a "growth resource" that is consumed in proportion to cross-sectional area and conserved across each fork, with branches that compute a weighted-average canopy position and grow *away* from it — plus the Three.js scene, the per-frame branch geometry, and the camera-facing instanced leaf billboards.
DATES / 2026