component
Trees
L-system trees grown by a 3D turtle, deterministic from seed.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add trees
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { Trees } from './runek/Trees'
<Trees position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Trees", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
position |
[x, y, z] |
[0, 0, 0] |
Position [x, y, z] in units (1 unit = 1 m, Y-up). |
rotation |
[x, y, z] |
[0, 0, 0] |
Euler rotation [x, y, z] in radians. |
seed |
number |
1 |
Seed for the deterministic variation: same seed, same result. |
iterations |
number |
2 |
|
segmentLength |
number |
0.7 |
Base branch length, in units. |
angle |
number |
0.5 |
Branching angle, in radians. |
trunkColor |
color |
palette bark |
Defaults to the world palette’s bark slot. |
leafColor |
color |
palette foliage |
Defaults to the world palette’s foliage slot. |
Registry manifest
registry manifest /r/components/trees.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add trees fetches.
Browse the whole catalog in the gallery →.
Source (Trees.tsx)
import { CylinderCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'
export interface TreesProps {
position?: Vec3
rotation?: Vec3
seed?: number
iterations?: number
/** Base branch length, in units. */
segmentLength?: number
/** Branching angle, in radians. */
angle?: number
/** Defaults to the world palette's `bark` slot. */
trunkColor?: string
/** Defaults to the world palette's `foliage` slot. */
leafColor?: string
}
interface Branch {
position: THREE.Vector3
quaternion: THREE.Quaternion
length: number
radius: number
}
interface Leaf {
position: THREE.Vector3
size: number
/** Per-leaf brightness multiplier for color variation. */
shade: number
}
const AXIOM = 'F'
const RULE = 'FF[+F][-F][&F][^F]'
const UP = new THREE.Vector3(0, 1, 0)
const YAW_AXIS = new THREE.Vector3(0, 0, 1)
const PITCH_AXIS = new THREE.Vector3(1, 0, 0)
function expand(iterations: number): string {
let s = AXIOM
for (let i = 0; i < iterations; i++) s = s.replace(/F/g, RULE)
return s
}
function buildTree(seed: number, iterations: number, segLen: number, angle: number) {
const symbols = expand(iterations)
const next = rng(seed)
const jitter = () => (next() - 0.5) * angle * 0.5
const branches: Branch[] = []
const leaves: Leaf[] = []
const stack: { pos: THREE.Vector3; quat: THREE.Quaternion; depth: number }[] = []
const delta = new THREE.Quaternion()
let pos = new THREE.Vector3()
let quat = new THREE.Quaternion()
let depth = 0
const addLeaf = (at: THREE.Vector3) => {
leaves.push({ position: at.clone(), size: segLen * 0.45, shade: 0.8 + next() * 0.4 })
}
for (const ch of symbols) {
switch (ch) {
case 'F': {
const len = segLen * 0.8 ** depth * (0.85 + next() * 0.3)
const dir = UP.clone().applyQuaternion(quat)
const start = pos.clone()
pos = start.clone().addScaledVector(dir, len)
const mid = start.clone().add(pos).multiplyScalar(0.5)
branches.push({
position: mid,
quaternion: new THREE.Quaternion().setFromUnitVectors(UP, dir),
length: len,
radius: segLen * 0.12 * 0.7 ** depth,
})
break
}
case '+':
quat.multiply(delta.setFromAxisAngle(YAW_AXIS, angle + jitter()))
break
case '-':
quat.multiply(delta.setFromAxisAngle(YAW_AXIS, -angle + jitter()))
break
case '&':
quat.multiply(delta.setFromAxisAngle(PITCH_AXIS, angle + jitter()))
break
case '^':
quat.multiply(delta.setFromAxisAngle(PITCH_AXIS, -angle + jitter()))
break
case '[':
stack.push({ pos: pos.clone(), quat: quat.clone(), depth })
depth++
break
case ']': {
addLeaf(pos)
const saved = stack.pop()
if (saved) {
pos = saved.pos
quat = saved.quat
depth = saved.depth
}
break
}
}
}
addLeaf(pos)
return { branches, leaves }
}
/**
* A single procedural tree grown from a bracketed L-system.
* Branches and leaves render as one instanced mesh each, so a forest stays cheap.
*/
export function Trees({
position = [0, 0, 0],
rotation = [0, 0, 0],
seed = 1,
iterations = 2,
segmentLength = 0.7,
angle = 0.5,
trunkColor,
leafColor,
}: TreesProps) {
const { unit, palette } = useWorld()
const bark = trunkColor ?? palette.bark
const foliage = leafColor ?? palette.foliage
const segLen = segmentLength * unit
const branchesRef = useRef<THREE.InstancedMesh>(null)
const leavesRef = useRef<THREE.InstancedMesh>(null)
const { branches, leaves } = useMemo(
() => buildTree(seed, iterations, segLen, angle),
[seed, iterations, segLen, angle],
)
useLayoutEffect(() => {
const mesh = branchesRef.current
if (!mesh) return
const dummy = new THREE.Object3D()
branches.forEach((b, i) => {
dummy.position.copy(b.position)
dummy.quaternion.copy(b.quaternion)
dummy.scale.set(b.radius, b.length, b.radius)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
})
mesh.count = branches.length
mesh.instanceMatrix.needsUpdate = true
}, [branches])
useLayoutEffect(() => {
const mesh = leavesRef.current
if (!mesh) return
const dummy = new THREE.Object3D()
const base = new THREE.Color(foliage)
const tint = new THREE.Color()
leaves.forEach((leaf, i) => {
dummy.position.copy(leaf.position)
dummy.rotation.set(0, 0, 0)
dummy.scale.setScalar(leaf.size)
dummy.updateMatrix()
mesh.setMatrixAt(i, dummy.matrix)
mesh.setColorAt(i, tint.copy(base).multiplyScalar(leaf.shade))
})
mesh.count = leaves.length
mesh.instanceMatrix.needsUpdate = true
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true
}, [leaves, foliage])
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CylinderCollider args={[segLen, segLen * 0.2]} position={[0, segLen, 0]} />
<instancedMesh
key={`b${branches.length}`}
ref={branchesRef}
args={[undefined, undefined, branches.length]}
castShadow
>
{/* unit cylinder, tapered; instances scale x/z by radius and y by length */}
<cylinderGeometry args={[0.7, 1, 1, 5]} />
<meshStandardMaterial color={bark} roughness={0.95} />
</instancedMesh>
<instancedMesh
key={`l${leaves.length}`}
ref={leavesRef}
args={[undefined, undefined, leaves.length]}
castShadow
>
<icosahedronGeometry args={[1, 0]} />
<meshStandardMaterial flatShading />
</instancedMesh>
</RigidBody>
)
}
Trees.groundSitting = true