2026-08-08 14:15:32 +02:00
|
|
|
import { Vec3 } from "../../../engine/math/Vec3"
|
|
|
|
|
import 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"
|
|
|
|
|
import type { Tree } from "../Tree"
|
2026-08-07 19:11:43 +02:00
|
|
|
import { blob, lerp, limb, TAU, rng } from "./treekit"
|
|
|
|
|
|
|
|
|
|
// Oak: short tapered trunk, a couple of branches, a broad cluster of rounded canopy
|
|
|
|
|
// blobs (bushy, wider than tall). Trunk = brown bark, foliage = oak leaf.
|
|
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
export type Oak = Prefab<Tree>
|
2026-08-08 14:15:32 +02:00
|
|
|
|
2026-08-24 15:17:53 +02:00
|
|
|
export namespace Oak {
|
|
|
|
|
export function create(trunk: Material, foliage: Material): Oak {
|
|
|
|
|
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(tree) {
|
|
|
|
|
if (tree.growth <= 0.35) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const radius = tree.growth * 0.3 + 0.15
|
|
|
|
|
return {
|
|
|
|
|
shape: "box",
|
|
|
|
|
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,
|
|
|
|
|
leaves: 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.8, 7, g)
|
|
|
|
|
const rTrunk = lerp(0.04, 0.32, g)
|
|
|
|
|
const forkY = base.y + h * 0.5
|
|
|
|
|
const canopyY = base.y + h * 0.72
|
|
|
|
|
const blobR = h * 0.3
|
|
|
|
|
if (lod === "impostor") {
|
|
|
|
|
// One low-poly blob on a stubby trunk -- reads as an oak at distance.
|
2026-08-24 15:17:53 +02:00
|
|
|
limb(
|
|
|
|
|
trunk,
|
|
|
|
|
base,
|
|
|
|
|
{ x: base.x, y: forkY, z: base.z },
|
|
|
|
|
rTrunk,
|
|
|
|
|
rTrunk * 0.6,
|
|
|
|
|
3,
|
|
|
|
|
)
|
2026-08-07 19:11:43 +02:00
|
|
|
blob(leaves, { x: base.x, y: canopyY, z: base.z }, blobR * 1.15, rand, 4, 2)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
limb(trunk, base, { x: base.x, y: forkY, z: base.z }, rTrunk, rTrunk * 0.6, 5)
|
|
|
|
|
|
|
|
|
|
const spread = h * 0.32
|
|
|
|
|
// Central blob plus, as it grows, a couple offset ones -> broad bushy crown.
|
|
|
|
|
const blobs = 1 + Math.round(g * 2)
|
|
|
|
|
for (let i = 0; i < blobs; i++) {
|
|
|
|
|
const angle = rand() * TAU
|
|
|
|
|
const rad = i === 0 ? 0 : spread * (0.5 + rand() * 0.5)
|
|
|
|
|
const center = {
|
|
|
|
|
x: base.x + Math.cos(angle) * rad,
|
|
|
|
|
y: canopyY + (rand() - 0.4) * spread,
|
|
|
|
|
z: base.z + Math.sin(angle) * rad,
|
|
|
|
|
}
|
|
|
|
|
blob(leaves, center, blobR * (0.7 + rand() * 0.4), rand)
|
|
|
|
|
}
|
|
|
|
|
// Grown oaks throw out a few branches, each tipped with a leaf tuft.
|
|
|
|
|
if (g > 0.55) {
|
|
|
|
|
const branches = 2 + Math.round(rand())
|
|
|
|
|
for (let i = 0; i < branches; i++) {
|
|
|
|
|
const angle = rand() * TAU
|
2026-08-24 15:17:53 +02:00
|
|
|
const dir = Vec3.normalize({
|
|
|
|
|
x: Math.cos(angle),
|
|
|
|
|
y: 1.2,
|
|
|
|
|
z: Math.sin(angle),
|
|
|
|
|
})
|
2026-08-07 19:11:43 +02:00
|
|
|
const start = { x: base.x, y: base.y + h * 0.42, z: base.z }
|
|
|
|
|
const end = Vec3.add(start, Vec3.scale(dir, h * 0.3))
|
|
|
|
|
limb(trunk, start, end, rTrunk * 0.4, rTrunk * 0.2, 4)
|
|
|
|
|
blob(leaves, end, blobR * 0.6, rand)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|