2026-08-08 14:15:32 +02:00
|
|
|
import { Vec3 } from "../../../engine/math/Vec3"
|
|
|
|
|
import { STRIDE, type Mesh } from "../../../engine/scene/Mesh"
|
2026-08-07 19:11:43 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
// Shared faceted-geometry primitives + the per-tree RNG used by concrete tree
|
|
|
|
|
// prefab modules.
|
2026-08-07 19:11:43 +02:00
|
|
|
|
|
|
|
|
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)]
|
|
|
|
|
}
|