component

Clouds

Drifting clouds built from clustered soft blobs (no textures); layer above a Sky.

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

Tune every prop in the workshop →

Add it

npx @runek/cli add clouds

Pulls @react-three/fiber@^9.6.1, @runek/core@^0.15.0, three@^0.184.0.

Use it

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

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

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

{ "type": "Clouds", "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.
count number 8 Number of clouds.
area [number, number] [80, 80] Spread [width, depth], in units.
height number 24 Height above the origin, in units.
drift number 0.6 Drift speed along +X, in units/sec (0 holds still).
color color "#eef2f7"
seed number 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

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

Browse the whole catalog in the gallery →.

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

export interface CloudsProps {
  position?: Vec3
  rotation?: Vec3
  /** Number of clouds. */
  count?: number
  /** Spread `[width, depth]`, in units. */
  area?: [number, number]
  /** Height above the origin, in units. */
  height?: number
  /** Drift speed along +X, in units/sec (0 holds still). */
  drift?: number
  color?: string
  seed?: number
}

interface Puff {
  pos: Vec3
  r: number
}

/** Drifting clouds built from clustered soft blobs (no textures). Layer above a
 *  `Sky` for depth and motion. Decorative (no collider). */
export function Clouds({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  count = 8,
  area = [80, 80],
  height = 24,
  drift = 0.6,
  color = '#eef2f7',
  seed = 1,
}: CloudsProps) {
  const { unit } = useWorld()
  const w = area[0] * unit
  const d = area[1] * unit
  const hy = height * unit
  const PER = 5
  const total = count * PER
  const ref = useRef<THREE.InstancedMesh>(null)
  const groupRef = useRef<THREE.Group>(null)

  const puffs = useMemo<Puff[]>(() => {
    const next = rng(seed)
    const out: Puff[] = []
    for (let c = 0; c < count; c++) {
      const cx = (next() - 0.5) * w
      const cz = (next() - 0.5) * d
      const cy = hy + (next() - 0.5) * 0.15 * hy
      const scale = 1.4 + next() * 1.6
      for (let p = 0; p < PER; p++) {
        out.push({
          pos: [
            cx + (next() - 0.5) * 4 * scale * unit,
            cy + (next() - 0.5) * 1.2 * scale * unit,
            cz + (next() - 0.5) * 3 * scale * unit,
          ] as Vec3,
          r: (1.0 + next() * 1.2) * scale * unit,
        })
      }
    }
    return out
  }, [count, w, d, hy, seed, unit])

  useLayoutEffect(() => {
    const mesh = ref.current
    if (!mesh) return
    const dummy = new THREE.Object3D()
    puffs.forEach((p, i) => {
      dummy.position.set(...p.pos)
      dummy.scale.setScalar(p.r)
      dummy.updateMatrix()
      mesh.setMatrixAt(i, dummy.matrix)
    })
    mesh.instanceMatrix.needsUpdate = true
  }, [puffs])

  useFrame((_, delta) => {
    const g = groupRef.current
    if (!g || drift === 0) return
    g.position.x += drift * unit * delta
    if (g.position.x > w / 2) g.position.x -= w
  })

  return (
    <group position={position} rotation={rotation}>
      <group ref={groupRef}>
        <instancedMesh ref={ref} args={[undefined, undefined, total]}>
          <icosahedronGeometry args={[1, 1]} />
          <meshStandardMaterial
            color={color}
            roughness={1}
            transparent
            opacity={0.92}
            flatShading
          />
        </instancedMesh>
      </group>
    </group>
  )
}