import { Vec3 } from "../../../engine/math/Vec3" import { STRIDE, type Mesh } from "../../../engine/scene/Mesh" // Shared faceted-geometry primitives + the per-tree RNG used by concrete tree // prefab modules. export const TAU = Math.PI * 2 /** A tapered tube between two points (trunk or branch), `sides`-gonal. */ export function limb(mesh: Mesh, a: Vec3, b: Vec3, ra: number, rb: number, sides: number): void { const axis = Vec3.normalize(Vec3.sub(b, a)) const [u, v] = basis(axis) const len = Vec3.length(Vec3.sub(b, a)) const start = mesh.verts.length / STRIDE for (let i = 0; i <= sides; i++) { const angle = (i / sides) * TAU const dx = u.x * Math.cos(angle) + v.x * Math.sin(angle) const dy = u.y * Math.cos(angle) + v.y * Math.sin(angle) const dz = u.z * Math.cos(angle) + v.z * Math.sin(angle) const s = i / sides mesh.verts.push(a.x + dx * ra, a.y + dy * ra, a.z + dz * ra, s * 1.5, 0) mesh.verts.push(b.x + dx * rb, b.y + dy * rb, b.z + dz * rb, s * 1.5, len * 0.5) } for (let i = 0; i < sides; i++) { const p = start + i * 2 mesh.indices.push(p, p + 2, p + 3, p, p + 3, p + 1) } } /** A cone standing on a base ring, apex `height` above it (one spruce tier). */ export function cone(mesh: Mesh, base: Vec3, height: number, radius: number, sides: number): void { const start = mesh.verts.length / STRIDE mesh.verts.push(base.x, base.y + height, base.z, 0.5, 0) for (let i = 0; i <= sides; i++) { const angle = (i / sides) * TAU mesh.verts.push(base.x + Math.cos(angle) * radius, base.y, base.z + Math.sin(angle) * radius, (i / sides) * 2, 1) } for (let i = 0; i < sides; i++) { // Wound so the outer surface faces out, matching the backface-cull sign. mesh.indices.push(start, start + 2 + i, start + 1 + i) } } /** A lumpy low-poly sphere (one canopy blob). Per-ring radius wobble keeps it * organic without cracking the longitude seam. */ export function blob(mesh: Mesh, center: Vec3, radius: number, rand: () => number, seg = 5, rings = 3): void { const start = mesh.verts.length / STRIDE for (let r = 0; r <= rings; r++) { const phi = (r / rings) * Math.PI const cy = Math.cos(phi) const cr = Math.sin(phi) const scale = radius * (0.85 + rand() * 0.3) for (let s = 0; s <= seg; s++) { const theta = (s / seg) * TAU mesh.verts.push( center.x + cr * Math.cos(theta) * scale, center.y + cy * scale, center.z + cr * 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) } } } /** Linear interpolation, for the sapling -> full-grown ramps. */ export function lerp(a: number, b: number, t: number): number { return a + (b - a) * t } /** Deterministic 0..1 generator (mulberry32) seeded per tree. */ export 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 } } /** Two unit vectors spanning the plane perpendicular to `axis`. */ function basis(axis: Vec3): [Vec3, Vec3] { const ref = Math.abs(axis.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 } const u = Vec3.normalize(Vec3.cross(ref, axis)) return [u, Vec3.cross(axis, u)] }