component
Hut
A round hut: a post-ribbed wall on a stone base, a framed doorway and window, under a shaggy thatch cone; one cylinder collider.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add hut
Pulls @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.
Use it
import { Hut } from './runek/Hut'
<Hut position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "Hut", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
radius |
number |
3 |
Wall radius, in units. |
wallHeight |
number |
2.9 |
Wall height at the eaves, in units. |
roofHeight |
number |
1.5 |
Conical roof height above the eaves, in units. |
doorWidth |
number |
1.3 |
Doorway width at the front (local +Z), in units. |
wallColor |
color |
Wall color; defaults to the palette’s wall. |
|
roofColor |
color |
Roof color; defaults to the palette’s roof. |
|
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. |
Registry manifest
registry manifest /r/components/hut.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add hut fetches.
Browse the whole catalog in the gallery →.
Source (Hut.tsx)
import { CylinderCollider, RigidBody } from '@react-three/rapier'
import { rng, useWorld, type WorldComponentProps } from '@runek/core'
import { useEffect, useMemo } from 'react'
import { ConeGeometry, DoubleSide } from 'three'
export interface HutProps extends WorldComponentProps {
/** Wall radius, in units. */
radius?: number
/** Wall height at the eaves, in units. */
wallHeight?: number
/** Conical roof height above the eaves, in units. */
roofHeight?: number
/** Doorway width at the front (local +Z), in units. */
doorWidth?: number
/** Wall color; defaults to the palette's `wall`. */
wallColor?: string
/** Roof color; defaults to the palette's `roof`. */
roofColor?: string
}
/**
* A round hut: a cylindrical wall with a front doorway under a shaggy conical thatch roof. The
* wall is ringed by post ribs on a low stone base, the roof is seed-jittered with a ragged fringe,
* protruding rafter tips, and a topknot finial, and a small framed window sits at the back. The
* doorway faces local +Z. A single cylinder collider makes it a solid obstacle (the doorway is
* visual — the hut is composed, not entered). For a square house with a gable roof, use `House`.
*/
export function Hut({
position = [0, 0, 0],
rotation = [0, 0, 0],
radius = 3,
wallHeight = 2.9,
roofHeight = 1.5,
doorWidth = 1.3,
wallColor,
roofColor,
seed = 1,
}: HutProps) {
const { unit, palette } = useWorld()
const wall = wallColor ?? palette.wall
const roof = roofColor ?? palette.roof
const post = palette.woodDark
const R = radius * unit
const H = wallHeight * unit
const roofH = roofHeight * unit
const postR = 0.1 * unit
// Leave a gap in the wall centred on +Z (three's cylinder: x=R·sinθ, z=R·cosθ, so θ=0 is +Z).
const half = Math.asin(Math.min(0.9, doorWidth / 2 / radius))
const postZ = R * Math.cos(half)
const postX = R * Math.sin(half)
const doorH = Math.min(2 * unit, H - 0.4 * unit)
const coneR = R * 1.28
const slant = Math.hypot(coneR, roofH)
// Shaggy thatch: a cone whose vertices are seed-jittered, with the rim drooping unevenly so the
// silhouette reads as a straw fringe rather than a machined lampshade.
const thatch = useMemo(() => {
const g = new ConeGeometry(coneR, roofH, 20, 4)
g.translate(0, roofH / 2, 0) // base at local y=0 (the eaves)
const next = rng(seed)
const amp = coneR * 0.045
const pos = g.attributes.position
for (let i = 0; i < pos.count; i++) {
const y = pos.getY(i)
const apex = y > roofH - 0.01
if (apex) continue
pos.setX(i, pos.getX(i) + (next() * 2 - 1) * amp)
pos.setZ(i, pos.getZ(i) + (next() * 2 - 1) * amp)
const rim = y < 0.01
pos.setY(i, y + (next() * 2 - 1) * amp * 0.6 - (rim ? next() * roofH * 0.14 : 0))
}
pos.needsUpdate = true
g.computeVertexNormals()
return g
}, [coneR, roofH, seed])
useEffect(() => () => thatch.dispose(), [thatch])
// Post ribs around the wall (skipping the doorway arc) and rafter tips under the fringe.
const ribs = useMemo(() => {
const arc = Math.PI * 2 - half * 2 - 0.5
return Array.from({ length: 9 }, (_, i) => half + 0.25 + (arc * (i + 0.5)) / 9)
}, [half])
const rafters = useMemo(() => Array.from({ length: 8 }, (_, i) => (Math.PI * 2 * i) / 8), [])
const rafterTilt = Math.atan2(coneR, roofH)
return (
<RigidBody type="fixed" colliders={false} position={position} rotation={rotation}>
<CylinderCollider args={[H / 2, R]} position={[0, H / 2, 0]} />
{/* low stone base the wall sits on */}
<mesh position={[0, 0.15 * unit, 0]} castShadow receiveShadow>
<cylinderGeometry args={[R * 1.05, R * 1.1, 0.3 * unit, 24]} />
<meshStandardMaterial color={palette.stone} roughness={1} flatShading />
</mesh>
{/* a dark interior floor, so the doorway reads as an opening into the hut */}
<mesh position={[0, 0.31 * unit, 0]} receiveShadow>
<cylinderGeometry args={[R * 0.94, R * 0.94, 0.06 * unit, 24]} />
<meshStandardMaterial color={palette.bark} roughness={1} />
</mesh>
{/* wall: a cylindrical shell with the doorway arc omitted */}
<mesh position={[0, H / 2, 0]} castShadow receiveShadow>
<cylinderGeometry args={[R, R, H, 32, 1, true, half, Math.PI * 2 - half * 2]} />
<meshStandardMaterial color={wall} side={DoubleSide} roughness={0.9} />
</mesh>
{/* wall filling above the doorway — the opening stops at the lintel */}
<mesh position={[0, (doorH + H) / 2, 0]} castShadow receiveShadow>
<cylinderGeometry args={[R, R, H - doorH, 8, 1, true, -half, half * 2]} />
<meshStandardMaterial color={wall} side={DoubleSide} roughness={0.9} />
</mesh>
{/* post ribs around the wall, so it reads as timber-and-daub rather than smooth plaster */}
{ribs.map((a) => (
<mesh
key={a}
position={[Math.sin(a) * (R + 0.02 * unit), H / 2, Math.cos(a) * (R + 0.02 * unit)]}
castShadow
>
<cylinderGeometry args={[0.055 * unit, 0.07 * unit, H * 0.97, 6]} />
<meshStandardMaterial color={palette.wood} roughness={0.9} />
</mesh>
))}
{/* lintel + door posts framing the opening */}
<mesh position={[0, doorH, postZ]} castShadow>
<boxGeometry args={[postX * 2 + postR * 2, 0.14 * unit, postR * 2]} />
<meshStandardMaterial color={post} roughness={0.8} />
</mesh>
<mesh position={[postX, doorH / 2, postZ]} castShadow>
<cylinderGeometry args={[postR, postR, doorH, 8]} />
<meshStandardMaterial color={post} roughness={0.8} />
</mesh>
<mesh position={[-postX, doorH / 2, postZ]} castShadow>
<cylinderGeometry args={[postR, postR, doorH, 8]} />
<meshStandardMaterial color={post} roughness={0.8} />
</mesh>
{/* a small framed window at the back */}
<group rotation={[0, Math.PI, 0]}>
<mesh position={[0, H * 0.55, R]} castShadow>
<boxGeometry args={[0.7 * unit, 0.8 * unit, 0.06 * unit]} />
<meshStandardMaterial color={post} roughness={0.8} />
</mesh>
<mesh position={[0, H * 0.55, R + 0.03 * unit]}>
<boxGeometry args={[0.52 * unit, 0.62 * unit, 0.06 * unit]} />
<meshStandardMaterial color={palette.bark} roughness={1} />
</mesh>
</group>
{/* shaggy thatch cone, overhanging the eaves */}
<mesh geometry={thatch} position={[0, H, 0]} castShadow>
<meshStandardMaterial color={roof} roughness={1} flatShading side={DoubleSide} />
</mesh>
{/* rafter tips poking out under the fringe */}
{rafters.map((a) => (
<group key={a} rotation={[0, a, 0]}>
<mesh
position={[coneR * 0.65, H + roofH * 0.35, 0]}
rotation={[0, 0, rafterTilt]}
castShadow
>
<cylinderGeometry args={[0.04 * unit, 0.05 * unit, slant * 0.85, 6]} />
<meshStandardMaterial color={post} roughness={0.9} />
</mesh>
</group>
))}
{/* topknot finial where the thatch is bound off */}
<mesh position={[0, H + roofH + 0.12 * unit, 0]} castShadow>
<cylinderGeometry args={[0.05 * unit, 0.05 * unit, 0.55 * unit, 6]} />
<meshStandardMaterial color={post} roughness={0.9} />
</mesh>
<mesh position={[0, H + roofH + 0.1 * unit, 0]} castShadow>
<coneGeometry args={[0.2 * unit, 0.3 * unit, 8]} />
<meshStandardMaterial color={roof} roughness={1} flatShading />
</mesh>
</RigidBody>
)
}
Hut.groundSitting = true