2026-08-08 14:15:32 +02:00
|
|
|
import type { Vec3 } from "../../engine/math/Vec3"
|
|
|
|
|
import { STRIDE, type Mesh } from "../../engine/scene/Mesh"
|
2026-08-24 15:17:53 +02:00
|
|
|
import type { Material } from "../../engine/render/Material"
|
|
|
|
|
import type { Prefab } from "../../engine/scene/Prefab"
|
2026-08-04 23:03:55 +02:00
|
|
|
|
|
|
|
|
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 {
|
2026-08-24 15:17:53 +02:00
|
|
|
export function create(material: Material): Prefab<Bush> {
|
|
|
|
|
return {
|
|
|
|
|
position: (bush) => bush.position,
|
|
|
|
|
bakeNear: (bush, batch) => build(bush, batch.mesh(material)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 23:03:55 +02:00
|
|
|
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). */
|
2026-08-24 15:17:53 +02:00
|
|
|
function blob(
|
|
|
|
|
mesh: Mesh,
|
|
|
|
|
cx: number,
|
|
|
|
|
cy: number,
|
|
|
|
|
cz: number,
|
|
|
|
|
radius: number,
|
|
|
|
|
rand: () => number,
|
|
|
|
|
): void {
|
2026-08-04 23:03:55 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|