component

Flowers

Instanced seeded wildflowers (stem + colored head) scattered over a patch.

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

Tune every prop in the workshop →

Add it

npx @runek/cli add flowers

Pulls @runek/core@^0.15.0, three@^0.184.0.

Use it

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

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

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

{ "type": "Flowers", "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.
area [number, number] [6, 6] Patch extent [width, depth], in units.
count number 60
height number 0.4 Height, in units.
stemColor color palette foliage Stem color; 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/flowers.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add flowers fetches.

Browse the whole catalog in the gallery →.

Source (Flowers.tsx)
import { rng, useWorld, type Vec3 } from '@runek/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'

export interface FlowersProps {
  position?: Vec3
  rotation?: Vec3
  /** Patch extent `[width, depth]`, in units. */
  area?: [number, number]
  count?: number
  height?: number
  /** Stem color; defaults to the world palette's `foliage` slot. */
  stemColor?: string
  seed?: number
}

interface Bloom {
  x: number
  z: number
  h: number
  hue: number
}

const HUES = [12, 45, 0, 330, 280, 200]

/** Instanced wildflowers scattered over a patch: a green stem plus a colored
 *  head, seeded so the same seed plants the same meadow. Decorative (no collider). */
export function Flowers({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  area = [6, 6],
  count = 60,
  height = 0.4,
  stemColor,
  seed = 1,
}: FlowersProps) {
  const { unit, palette } = useWorld()
  const stem = stemColor ?? palette.foliage
  const w = area[0] * unit
  const d = area[1] * unit
  const h = height * unit
  const stemRef = useRef<THREE.InstancedMesh>(null)
  const headRef = useRef<THREE.InstancedMesh>(null)

  const blooms = useMemo<Bloom[]>(() => {
    const next = rng(seed)
    return Array.from({ length: count }, () => ({
      x: (next() - 0.5) * w,
      z: (next() - 0.5) * d,
      h: h * (0.7 + next() * 0.6),
      hue: next(),
    }))
  }, [w, d, h, count, seed])

  useLayoutEffect(() => {
    const sMesh = stemRef.current
    const hMesh = headRef.current
    if (!sMesh || !hMesh) return
    const dummy = new THREE.Object3D()
    const col = new THREE.Color()
    blooms.forEach((b, i) => {
      dummy.position.set(b.x, b.h / 2, b.z)
      dummy.scale.set(1, b.h / h, 1)
      dummy.updateMatrix()
      sMesh.setMatrixAt(i, dummy.matrix)

      dummy.position.set(b.x, b.h, b.z)
      dummy.scale.set(1, 1, 1)
      dummy.updateMatrix()
      hMesh.setMatrixAt(i, dummy.matrix)
      hMesh.setColorAt(i, col.setHSL(HUES[Math.floor(b.hue * HUES.length)] / 360, 0.7, 0.6))
    })
    sMesh.instanceMatrix.needsUpdate = true
    hMesh.instanceMatrix.needsUpdate = true
    if (hMesh.instanceColor) hMesh.instanceColor.needsUpdate = true
  }, [blooms, h])

  return (
    <group position={position} rotation={rotation}>
      <instancedMesh ref={stemRef} args={[undefined, undefined, count]} castShadow>
        <cylinderGeometry args={[0.01 * unit, 0.012 * unit, h, 5]} />
        <meshStandardMaterial color={stem} />
      </instancedMesh>
      <instancedMesh ref={headRef} args={[undefined, undefined, count]} castShadow>
        <icosahedronGeometry args={[0.05 * unit, 0]} />
        <meshStandardMaterial flatShading roughness={0.8} />
      </instancedMesh>
    </group>
  )
}