import type { Vec3 } from "../../engine/math/Vec3" import { STRIDE, type Mesh } from "../../engine/scene/Mesh" import type { Material } from "../../engine/render/Material" import type { Prefab } from "../../engine/scene/Prefab" const TAU = Math.PI * 2 /** A low shrub: a tight cluster of small leafy blobs sitting on the ground. * Textured with the same leaf sheet as oak canopies, so it batches into the * chunk's foliage mesh. `size` is the overall spread; `seed` the per-bush wobble. */ export type Bush = { position: Vec3 size: number seed: number } /** Low-poly bush geometry, same faceted flat-shaded style as the trees. A few * overlapping jittered spheres read as a rounded shrub; blobs are closed and * wound outward, so backface culling is safe. `build` appends into a shared * (leaf-textured) mesh. */ export namespace Bush { export function create(material: Material): Prefab { return { position: (bush) => bush.position, bakeNear: (bush, batch) => build(bush, batch.mesh(material)), } } export function build(bush: Bush, mesh: Mesh): void { const rand = rng(bush.seed) // A handful of smaller overlapping lumps reads as a soft shrub; one big // sphere reads as a boulder. const blobs = 3 + Math.floor(rand() * 3) const r = bush.size * 0.42 for (let i = 0; i < blobs; i++) { const angle = rand() * TAU const dist = i === 0 ? 0 : bush.size * 0.5 * rand() const cx = bush.position.x + Math.cos(angle) * dist const cz = bush.position.z + Math.sin(angle) * dist const cy = bush.position.y + r * (0.5 + rand() * 0.4) blob(mesh, cx, cy, cz, r * (0.55 + rand() * 0.3), rand) } } /** A small lumpy low-poly sphere, wound outward (matches the oak canopy blob). */ function blob( mesh: Mesh, cx: number, cy: number, cz: number, radius: number, rand: () => number, ): void { const seg = 6 const rings = 4 const start = mesh.verts.length / STRIDE for (let r = 0; r <= rings; r++) { const phi = (r / rings) * Math.PI const cyv = Math.cos(phi) const crv = Math.sin(phi) const scale = radius * (0.9 + rand() * 0.18) for (let s = 0; s <= seg; s++) { const theta = (s / seg) * TAU mesh.verts.push( cx + crv * Math.cos(theta) * scale, cy + cyv * scale, cz + crv * Math.sin(theta) * scale, (s / seg) * 2, (r / rings) * 2, ) } } const row = seg + 1 for (let r = 0; r < rings; r++) { for (let s = 0; s < seg; s++) { const p = start + r * row + s mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row) } } } /** Deterministic 0..1 generator (mulberry32) seeded per bush. */ function rng(seed: number): () => number { let a = seed >>> 0 return () => { a = (a + 0x6D2B79F5) | 0 let t = Math.imul(a ^ (a >>> 15), 1 | a) t ^= t + Math.imul(t ^ (t >>> 7), 61 | t) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } }