2026-08-24 15:54:44 +02:00
|
|
|
import type { Mesh } from "engine/scene/Mesh"
|
|
|
|
|
import type { Material } from "engine/render/Material"
|
|
|
|
|
import type { Prefab } from "engine/scene/Prefab"
|
2026-08-24 15:17:53 +02:00
|
|
|
import type { Tree } from "../Tree"
|
2026-08-07 19:11:43 +02:00
|
|
|
import { cone, lerp, limb, rng } from "./treekit"
|
|
|
|
|
|
|
|
|
|
// Spruce: tall thin trunk under stacked cones that narrow to a point (tiered, taller
|
|
|
|
|
// than wide). Trunk = brown bark, foliage = spruce needle.
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
export type Spruce = Prefab<Tree>
|
2026-08-08 14:15:32 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
export namespace Spruce {
|
|
|
|
|
export function create(trunk: Material, foliage: Material): Spruce {
|
|
|
|
|
return {
|
|
|
|
|
position: (tree) => tree.position,
|
|
|
|
|
bakeNear: (tree, batch) =>
|
|
|
|
|
build(tree, batch.mesh(trunk), batch.mesh(foliage), "full"),
|
|
|
|
|
bakeFar: (tree, batch) =>
|
|
|
|
|
build(tree, batch.mesh(trunk), batch.mesh(foliage), "impostor"),
|
|
|
|
|
collider: treeCollider,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function treeCollider(tree: Tree) {
|
|
|
|
|
if (tree.growth <= 0.35) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const radius = tree.growth * 0.2 + 0.15
|
|
|
|
|
return {
|
|
|
|
|
shape: "box" as const,
|
|
|
|
|
minX: tree.position.x - radius,
|
|
|
|
|
maxX: tree.position.x + radius,
|
|
|
|
|
minZ: tree.position.z - radius,
|
|
|
|
|
maxZ: tree.position.z + radius,
|
|
|
|
|
top: tree.position.y + 3,
|
|
|
|
|
standable: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function build(
|
|
|
|
|
tree: Tree,
|
|
|
|
|
trunk: Mesh,
|
|
|
|
|
needles: Mesh,
|
|
|
|
|
lod: "full" | "impostor",
|
|
|
|
|
): void {
|
2026-08-07 19:11:43 +02:00
|
|
|
const base = tree.position
|
|
|
|
|
const g = tree.growth
|
|
|
|
|
const rand = rng(tree.seed)
|
|
|
|
|
const h = lerp(0.6, 9, g)
|
|
|
|
|
const rTrunk = lerp(0.03, 0.2, g)
|
|
|
|
|
const impostor = lod === "impostor"
|
2026-08-24 15:17:53 +02:00
|
|
|
limb(
|
|
|
|
|
trunk,
|
|
|
|
|
base,
|
|
|
|
|
{ x: base.x, y: base.y + h, z: base.z },
|
|
|
|
|
rTrunk,
|
|
|
|
|
rTrunk * 0.25,
|
|
|
|
|
impostor ? 3 : 5,
|
|
|
|
|
)
|
2026-08-07 19:11:43 +02:00
|
|
|
|
|
|
|
|
// Stacked cones: widest low, shrinking to a point up top -> conical tiers. The
|
|
|
|
|
// impostor keeps the first two tiers at low sides (same seed => aligned).
|
|
|
|
|
const tiers = impostor ? 2 : 2 + Math.round(g * 3)
|
|
|
|
|
const sides = impostor ? 4 : 6
|
|
|
|
|
const bottom = base.y + h * 0.1
|
|
|
|
|
const span = h * 0.9
|
|
|
|
|
for (let i = 0; i < tiers; i++) {
|
|
|
|
|
const t = i / tiers
|
|
|
|
|
const y = bottom + t * span * 0.82
|
|
|
|
|
const radius = lerp(h * 0.3, h * 0.05, t) * (0.9 + rand() * 0.2)
|
|
|
|
|
const coneH = (span / tiers) * 1.9
|
|
|
|
|
cone(needles, { x: base.x, y, z: base.z }, coneH, radius, sides)
|
|
|
|
|
}
|
|
|
|
|
}
|