component

Hedge

A trimmed green wall with seeded surface tufts; blocks like a Wall.

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Add it

npx @runek/cli add hedge

Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.

Use it

import { Hedge } from './runek/Hedge'

<Hedge position={[0, 0, 0]} seed={1} />

Or as a node in a world file (worlds as data):

{ "type": "Hedge", "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.
length number 4 Length along local X, in units.
height number 1.2 Height, in units.
depth number 0.6 Depth, in units.
color color palette foliage Defaults to the world palette’s foliage slot.
seed number 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

registry manifest /r/components/hedge.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add hedge fetches.

Browse the whole catalog in the gallery →.

Source (Hedge.tsx)
import { CuboidCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useMemo } from 'react'
import * as THREE from 'three'

export interface HedgeProps {
  position?: Vec3
  rotation?: Vec3
  /** Length along local X, in units. */
  length?: number
  height?: number
  depth?: number
  /** Defaults to the world palette's `foliage` slot. */
  color?: string
  seed?: number
}

interface Tuft {
  pos: Vec3
  r: number
  color: string
}

/** A trimmed green wall: a solid clipped box dressed with seeded surface tufts.
 *  It blocks like a Wall, so it owns a cuboid collider. Fence's vegetation cousin. */
export function Hedge({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  length = 4,
  height = 1.2,
  depth = 0.6,
  color,
  seed = 1,
}: HedgeProps) {
  const { unit, palette } = useWorld()
  const foliage = color ?? palette.foliage
  const L = length * unit
  const H = height * unit
  const D = depth * unit

  const tufts = useMemo<Tuft[]>(() => {
    const next = rng(seed)
    const base = new THREE.Color(foliage)
    const tint = new THREE.Color()
    const n = Math.max(10, Math.round(length * 5))
    return Array.from({ length: n }, () => {
      const shade = 0.78 + next() * 0.44
      const side = next() < 0.5 ? 1 : -1
      return {
        pos: [(next() - 0.5) * L, next() * H, (side * D) / 2] as Vec3,
        r: (0.12 + next() * 0.13) * unit,
        color: tint.copy(base).multiplyScalar(shade).getStyle(),
      }
    })
  }, [L, H, D, length, foliage, seed, unit])

  return (
    <RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
      <CuboidCollider args={[L / 2, H / 2, D / 2]} position={[0, H / 2, 0]} />
      <mesh castShadow receiveShadow position={[0, H / 2, 0]}>
        <boxGeometry args={[L, H, D]} />
        <meshStandardMaterial color={foliage} roughness={0.95} flatShading />
      </mesh>
      {tufts.map((t) => (
        <mesh
          key={`tuft-${t.pos[0].toFixed(3)}:${t.pos[1].toFixed(3)}:${t.pos[2].toFixed(2)}`}
          castShadow
          position={t.pos}
        >
          <icosahedronGeometry args={[t.r, 0]} />
          <meshStandardMaterial color={t.color} flatShading roughness={0.95} />
        </mesh>
      ))}
    </RigidBody>
  )
}

Hedge.groundSitting = true