← gallery

vegetation

Trees

L-system trees grown by a 3D turtle, deterministic from seed.

Install

npx runek add trees

Pulls: core@react-three/rapier@^2.2.0three@^0.184.0

Props

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
}

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