56 lines
2.4 KiB
TypeScript
56 lines
2.4 KiB
TypeScript
import { type BufferGeometry, ConeGeometry, DoubleSide, Group, Mesh } from 'three'
|
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
import { GRASS_PRESETS } from './grass-presets'
|
|
import type { GrassNode, GrassPreset } from './grass-schema'
|
|
import type { SubMesh, VariantData } from './instanced'
|
|
import { mulberry32, naturalHeight } from './variant-utils'
|
|
import { windStandardMaterial } from './wind-node'
|
|
|
|
export function grassVariantKey(preset: GrassPreset, seed: number, bladeColor: string): string {
|
|
return `${preset}:${seed}:${bladeColor}`
|
|
}
|
|
|
|
const variantCache = new Map<string, VariantData>()
|
|
|
|
/** Cached procedural grass geometry for a (preset, seed, bladeColor). One
|
|
* generation per variant is shared across every instance — a whole lawn of the
|
|
* same tuft is a single InstancedMesh. */
|
|
export function getGrassVariant(node: GrassNode): VariantData {
|
|
const key = grassVariantKey(node.preset, node.seed, node.bladeColor)
|
|
const cached = variantCache.get(key)
|
|
if (cached) return cached
|
|
const group = buildGrass(node.preset, node.seed, node.bladeColor)
|
|
const subMeshes: SubMesh[] = group.children
|
|
.filter((c): c is Mesh => (c as Mesh).isMesh)
|
|
.map((mesh) => ({ geometry: mesh.geometry, material: mesh.material }))
|
|
const data: VariantData = { subMeshes, naturalHeight: naturalHeight(group) }
|
|
variantCache.set(key, data)
|
|
return data
|
|
}
|
|
|
|
/** A tuft of flattened, leaning blades merged into one geometry (one draw per
|
|
* instance). Deterministic in `seed` so the same variant renders identically. */
|
|
function buildGrass(preset: GrassPreset, seed: number, bladeColor: string): Group {
|
|
const spec = GRASS_PRESETS[preset] ?? GRASS_PRESETS.meadow
|
|
const rng = mulberry32(seed >>> 0)
|
|
const group = new Group()
|
|
const mat = windStandardMaterial({ color: bladeColor, roughness: 0.9, side: DoubleSide })
|
|
const h = spec.defaultHeight
|
|
|
|
const blades: BufferGeometry[] = []
|
|
for (let i = 0; i < spec.blades; i++) {
|
|
const bh = h * (0.6 + rng() * 0.6)
|
|
const blade = new ConeGeometry(0.02, bh, 3)
|
|
blade.scale(1, 1, 0.3) // flatten the cone into a blade
|
|
blade.translate(0, bh / 2, 0)
|
|
blade.rotateZ((rng() - 0.5) * 0.7) // lean
|
|
const angle = rng() * Math.PI * 2
|
|
blade.rotateY(angle)
|
|
const r = rng() * 0.07
|
|
blade.translate(Math.cos(angle) * r, 0, Math.sin(angle) * r)
|
|
blades.push(blade)
|
|
}
|
|
group.add(new Mesh(mergeGeometries(blades, false) ?? blades[0], mat))
|
|
return group
|
|
}
|