[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
3.0 kB
65 lines
1import { merge } from '../../../ops'
2import { setup, blade, heightShade } from '../../../build'
3import type { Mesh } from '../../../core/mesh'
4import type { Vec3 } from '../../../core/types'
5import type { OptionSchema, OptionInput } from '../../../core/schema'
6
7export const grassSchema = {
8 seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' },
9 blades: { type: 'integer', default: 16, min: 3, max: 60, label: 'Blades' },
10 height: { type: 'range', default: 0.6, min: 0.15, max: 1.6, step: 0.05, label: 'Height' },
11 bladeWidth: { type: 'range', default: 0.035,min: 0.01, max: 0.1, step: 0.005, label: 'Blade Width' },
12 spread: { type: 'range', default: 0.1, min: 0, max: 0.4, step: 0.01, label: 'Base Spread' },
13 fan: { type: 'range', default: 0.25, min: 0, max: 0.6, step: 0.02, label: 'Tip Fan' },
14 curve: { type: 'range', default: 0.8, min: 0, max: 1.5, step: 0.05, label: 'Curve' },
15 segments: { type: 'integer', default: 4, min: 2, max: 8, label: 'Segments' },
16 colors: { type: 'color-array', default: ['#3a6a1e', '#4e8a28', '#7ab33e'], min: 1, max: 6, label: 'Colors' },
17} satisfies OptionSchema
18
19export type GrassOptions = Partial<OptionInput<typeof grassSchema>> & { preset?: string }
20
21export const grassPresets: Record<string, Partial<GrassOptions>> = {
22 default: {},
23 tall: { height: 1.2, blades: 22, curve: 0.5 },
24 dry: { colors: ['#7a6a28', '#9a8a3a', '#b7a64a'], curve: 1.0 },
25 lush: { blades: 30, colors: ['#2f6a1e', '#3f8a26', '#62ad36'] },
26}
27
28export function grass(options: GrassOptions = {}): Mesh {
29 const { o, rng } = setup(grassSchema, options, grassPresets)
30 const shapeRng = rng.stream('shape')
31
32 const blades: Mesh[] = []
33 for (let i = 0; i < o.blades; i++) {
34 // Base position within a small disc.
35 const baseAngle = shapeRng() * Math.PI * 2
36 const baseDist = Math.sqrt(shapeRng()) * o.spread
37 const bx = Math.cos(baseAngle) * baseDist
38 const bz = Math.sin(baseAngle) * baseDist
39
40 // Each blade arcs over in a random direction, biased outward from the clump.
41 const outAngle = baseDist > 1e-4 ? baseAngle : shapeRng() * Math.PI * 2
42 const leanAngle = outAngle + (shapeRng() - 0.5) * Math.PI * o.fan
43 const ldx = Math.cos(leanAngle)
44 const ldz = Math.sin(leanAngle)
45
46 const h = o.height * (0.7 + shapeRng() * 0.5)
47 const phiMax = Math.max(0.001, o.curve * (0.6 + shapeRng() * 0.8))
48 const R = h / phiMax
49 const w = o.bladeWidth * (0.7 + shapeRng() * 0.5)
50
51 const segs = o.segments
52 const path: Vec3[] = []
53 for (let j = 0; j <= segs; j++) {
54 const t = j / segs
55 const phi = phiMax * t
56 const y = R * Math.sin(phi)
57 const hd = R * (1 - Math.cos(phi))
58 path.push([bx + ldx * hd, y, bz + ldz * hd])
59 }
60
61 blades.push(blade(path, { width: w }).vertexColor(heightShade(o.colors, h)))
62 }
63
64 return merge(...blades)
65}