···
11
11
bush, bushSchema, bushPresets,
12
12
grass, grassSchema, grassPresets,
13
13
fern, fernSchema, fernPresets,
14
14
+
flower, flowerSchema, flowerPresets,
15
15
+
deadTree, deadTreeSchema, deadTreePresets,
16
16
+
rock, rockSchema, rockPresets,
14
17
} from '../src/generators'
15
18
import { createEditor } from './editor/editor'
16
19
import type { OptionSchema } from '../src/core/schema'
···
32
35
bush: { label: 'Bush', gen: bush, schema: bushSchema, presets: bushPresets, sizeKey: 'size', defaultSize: 0.6 },
33
36
grass: { label: 'Grass', gen: grass, schema: grassSchema, presets: grassPresets, sizeKey: 'height', defaultSize: 0.6 },
34
37
fern: { label: 'Fern', gen: fern, schema: fernSchema, presets: fernPresets, sizeKey: 'length', defaultSize: 1.0 },
38
38
+
flower: { label: 'Flower', gen: flower, schema: flowerSchema, presets: flowerPresets, sizeKey: 'height', defaultSize: 0.5 },
39
39
+
dead: { label: 'Dead', gen: deadTree, schema: deadTreeSchema, presets: deadTreePresets, sizeKey: 'height', defaultSize: 1.5 },
40
40
+
rock: { label: 'Rock', gen: rock, schema: rockSchema, presets: rockPresets, sizeKey: 'size', defaultSize: 0.6 },
35
41
}
36
42
37
43
// --- Renderer ---
···
1
1
+
import { tube, merge } from '../ops'
2
2
+
import { createRng } from '../core/rng'
3
3
+
import { Mesh } from '../core/mesh'
4
4
+
import type { Vec3 } from '../core/types'
5
5
+
6
6
+
export interface BranchTip {
7
7
+
/** End point of a terminal limb — where foliage/leaves/fruit can attach. */
8
8
+
position: Vec3
9
9
+
/** Growth direction at the tip. */
10
10
+
direction: Vec3
11
11
+
/** Radius at the tip. */
12
12
+
radius: number
13
13
+
/** Recursion depth remaining when this tip was emitted (0 = outermost). */
14
14
+
depth: number
15
15
+
}
16
16
+
17
17
+
export interface BranchResult {
18
18
+
/** Merged tapered limbs (uncolored — apply your own vertexColor/faceColor). */
19
19
+
mesh: Mesh
20
20
+
/** Attachment points at the end of every terminal limb. */
21
21
+
tips: BranchTip[]
22
22
+
}
23
23
+
24
24
+
export interface BranchesOptions {
25
25
+
/** RNG to draw from (pass a generator stream for determinism). Falls back to `seed`. */
26
26
+
rng?: () => number
27
27
+
seed?: number
28
28
+
/** Base of the trunk. Default origin. */
29
29
+
start?: Vec3
30
30
+
/** Initial growth direction. Default up. */
31
31
+
direction?: Vec3
32
32
+
/** Length of the root limb. */
33
33
+
length: number
34
34
+
/** Base radius of the root limb. */
35
35
+
radius: number
36
36
+
/** Recursion levels (number of limb generations). */
37
37
+
depth: number
38
38
+
/** Children spawned at each split. Default 2. */
39
39
+
children?: number | [number, number]
40
40
+
/** Child length = parent length × this. Default 0.72. */
41
41
+
lengthFalloff?: number
42
42
+
/** Child radius = parent radius at the attach point × this. Default 0.82. */
43
43
+
radiusFalloff?: number
44
44
+
/** Angle (radians) children diverge from the parent. Default 0.7. */
45
45
+
spread?: number
46
46
+
/** Pull of each limb toward world-up, 0..1. Default 0.25. */
47
47
+
upBias?: number
48
48
+
/** Random meander of a limb (radians, total). Default 0.4. */
49
49
+
wander?: number
50
50
+
/** Path points per limb. Default 5. */
51
51
+
segments?: number
52
52
+
/** Taper-curve exponent for a strand's radius (radius·(1−t)^taper). Lower keeps the body thick (sharpening only near the tip); higher thins sooner. Default 0.5. */
53
53
+
taper?: number
54
54
+
/** Tube radial sides. Default 5. */
55
55
+
sides?: number
56
56
+
/** Fraction up a limb where side branches begin sprouting. Default 0.3. */
57
57
+
splitStart?: number
58
58
+
}
59
59
+
60
60
+
function normalize(v: Vec3): Vec3 {
61
61
+
const l = Math.hypot(v[0], v[1], v[2]) || 1
62
62
+
return [v[0] / l, v[1] / l, v[2] / l]
63
63
+
}
64
64
+
function cross(a: Vec3, b: Vec3): Vec3 {
65
65
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
66
66
+
}
67
67
+
function perpendicular(d: Vec3): Vec3 {
68
68
+
const a: Vec3 = Math.abs(d[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]
69
69
+
return normalize(cross(d, a))
70
70
+
}
71
71
+
/** Rotate direction `d` by `angle` away from itself, in azimuth `az` around `d`. */
72
72
+
function diverge(d: Vec3, angle: number, az: number): Vec3 {
73
73
+
const p = perpendicular(d)
74
74
+
const q = cross(d, p) // second perpendicular
75
75
+
const pr: Vec3 = [
76
76
+
p[0] * Math.cos(az) + q[0] * Math.sin(az),
77
77
+
p[1] * Math.cos(az) + q[1] * Math.sin(az),
78
78
+
p[2] * Math.cos(az) + q[2] * Math.sin(az),
79
79
+
]
80
80
+
const c = Math.cos(angle), s = Math.sin(angle)
81
81
+
return normalize([d[0] * c + pr[0] * s, d[1] * c + pr[1] * s, d[2] * c + pr[2] * s])
82
82
+
}
83
83
+
function towardUp(d: Vec3, t: number): Vec3 {
84
84
+
return normalize([d[0] * (1 - t), d[1] * (1 - t) + t, d[2] * (1 - t)])
85
85
+
}
86
86
+
87
87
+
/**
88
88
+
* Grow a recursive tapered branch skeleton — the engine behind dead trees, bare
89
89
+
* limbs, coral, and (with foliage attached at the returned tips) leafy trees.
90
90
+
*/
91
91
+
export function branches(options: BranchesOptions): BranchResult {
92
92
+
const rng = options.rng ?? createRng(options.seed ?? 0)
93
93
+
const childrenOpt = options.children ?? 3
94
94
+
const lengthFalloff = options.lengthFalloff ?? 0.7
95
95
+
const radiusFalloff = options.radiusFalloff ?? 0.82
96
96
+
const spread = options.spread ?? 0.8
97
97
+
const upBias = options.upBias ?? 0.22
98
98
+
const wander = options.wander ?? 0.5
99
99
+
const segments = options.segments ?? 6
100
100
+
const taperExp = options.taper ?? 0.5
101
101
+
const sides = options.sides ?? 5
102
102
+
const splitStart = options.splitStart ?? 0.2
103
103
+
104
104
+
const limbs: Mesh[] = []
105
105
+
const tips: BranchTip[] = []
106
106
+
107
107
+
// Each strand is ONE continuous axis (trunk or branch), built as a single tube that
108
108
+
// tapers smoothly from its base to a twig point. Laterals branch off along it; each
109
109
+
// lateral is itself a strand. There are no per-segment trunk joints to gap.
110
110
+
function strand(start: Vec3, dir: Vec3, length: number, radius: number, depth: number) {
111
111
+
let d = normalize(dir)
112
112
+
let pos = start
113
113
+
const path: Vec3[] = [pos]
114
114
+
const stepLen = length / segments
115
115
+
for (let s = 0; s < segments; s++) {
116
116
+
d = diverge(d, (wander / segments) * (0.5 + rng()), rng() * Math.PI * 2)
117
117
+
d = towardUp(d, upBias * 0.15)
118
118
+
pos = [pos[0] + d[0] * stepLen, pos[1] + d[1] * stepLen, pos[2] + d[2] * stepLen]
119
119
+
path.push(pos)
120
120
+
}
121
121
+
// Smooth taper from base to a point at the tip; the top is naturally a thin twig.
122
122
+
const radiusAt = (t: number) => radius * Math.pow(Math.max(0, 1 - t), taperExp)
123
123
+
limbs.push(tube(path, radiusAt, sides, false))
124
124
+
tips.push({ position: pos, direction: d, radius: radius * 0.25, depth })
125
125
+
126
126
+
if (depth <= 1) return
127
127
+
128
128
+
const n = Array.isArray(childrenOpt)
129
129
+
? Math.round(childrenOpt[0] + rng() * (childrenOpt[1] - childrenOpt[0]))
130
130
+
: childrenOpt
131
131
+
132
132
+
// Laterals sprout from points along the strand, each starting on the parent axis so
133
133
+
// its base is buried inside the parent tube → connected joints, no caps, no gaps.
134
134
+
for (let c = 0; c < n; c++) {
135
135
+
const f = splitStart + (0.9 - splitStart) * ((c + 0.5 + (rng() - 0.5) * 0.6) / n)
136
136
+
const fi = Math.max(0, Math.min(segments - 1e-6, f * segments))
137
137
+
const i0 = Math.floor(fi)
138
138
+
const ft = fi - i0
139
139
+
const a = path[i0], b = path[i0 + 1]
140
140
+
const apos: Vec3 = [a[0] + (b[0] - a[0]) * ft, a[1] + (b[1] - a[1]) * ft, a[2] + (b[2] - a[2]) * ft]
141
141
+
const adir = normalize([b[0] - a[0], b[1] - a[1], b[2] - a[2]])
142
142
+
const arad = radiusAt(f)
143
143
+
144
144
+
let cd = diverge(adir, spread * (0.6 + rng() * 0.7), rng() * Math.PI * 2)
145
145
+
cd = towardUp(cd, upBias)
146
146
+
strand(apos, cd, length * lengthFalloff * (0.7 + rng() * 0.4), arad * radiusFalloff, depth - 1)
147
147
+
}
148
148
+
}
149
149
+
150
150
+
strand(options.start ?? [0, 0, 0], options.direction ?? [0, 1, 0], options.length, options.radius, options.depth)
151
151
+
152
152
+
return { mesh: merge(...limbs), tips }
153
153
+
}
···
4
4
export { facetShade, heightShade, type FacetShadeOptions } from './shade'
5
5
export { scatterOnSurface, type SurfacePoint, type ScatterOnSurfaceOptions } from './surface'
6
6
export { blade, type BladeOptions } from './blade'
7
7
+
export { branches, type BranchesOptions, type BranchResult, type BranchTip } from './branches'
···
1
1
export * from './vegetation'
2
2
+
export * from './rocks'
···
1
1
+
export { rock, rockSchema, rockPresets, type RockOptions } from './rock'
···
1
1
+
import { setup, foliageBlob, facetShade } from '../../build'
2
2
+
import { pickRandom } from '../../color'
3
3
+
import { UberNoise } from '../../noise'
4
4
+
import type { Mesh } from '../../core/mesh'
5
5
+
import type { OptionSchema, OptionInput } from '../../core/schema'
6
6
+
7
7
+
export const rockSchema = {
8
8
+
seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' },
9
9
+
size: { type: 'range', default: 0.6, min: 0.2, max: 2.5, step: 0.05, label: 'Size' },
10
10
+
detail: { type: 'range', default: 0.45, min: 0.2, max: 0.9, step: 0.05, label: 'Detail' },
11
11
+
noise: { type: 'range', default: 0.45, min: 0.1, max: 0.8, step: 0.05, label: 'Lumpiness' },
12
12
+
squash: { type: 'range', default: 0.7, min: 0.3, max: 1.2, step: 0.05, label: 'Squash' },
13
13
+
flatten: { type: 'range', default: 0.25, min: 0, max: 0.5, step: 0.05, label: 'Flat Base' },
14
14
+
jitter: { type: 'range', default: 0.02, min: 0, max: 0.1, step: 0.005, label: 'Jitter' },
15
15
+
mossColors: { type: 'color-array', default: [], min: 0, max: 4, label: 'Moss Colors' },
16
16
+
mossAngle: { type: 'range', default: 40, min: 0, max: 80, step: 5, label: 'Moss Min Angle (°)' },
17
17
+
colors: { type: 'color-array', default: ['#56514b', '#6a645c', '#7e776d'], min: 1, max: 6, label: 'Rock Colors' },
18
18
+
} satisfies OptionSchema
19
19
+
20
20
+
export type RockOptions = Partial<OptionInput<typeof rockSchema>> & { preset?: string }
21
21
+
22
22
+
export const rockPresets: Record<string, Partial<RockOptions>> = {
23
23
+
default: {},
24
24
+
boulder: { size: 1.4, squash: 0.85, noise: 0.3, flatten: 0.3 },
25
25
+
sharp: { noise: 0.7, detail: 0.3, squash: 1.0 },
26
26
+
mossy: { mossColors: ['#3f6a2a', '#4e7d33', '#5c8c3a'], noise: 0.4 },
27
27
+
slate: { squash: 0.45, flatten: 0.4, colors: ['#48504f', '#586160', '#6a7270'] },
28
28
+
}
29
29
+
30
30
+
export function rock(options: RockOptions = {}): Mesh {
31
31
+
const { o, rng } = setup(rockSchema, options, rockPresets)
32
32
+
const shapeRng = rng.stream('shape')
33
33
+
const colorRng = rng.stream('color')
34
34
+
const mossRng = rng.stream('moss')
35
35
+
36
36
+
const colorNoise = new UberNoise({ seed: colorRng.seed(), scale: 2 })
37
37
+
const hasMoss = o.mossColors.length > 0
38
38
+
const mossNoise = hasMoss ? new UberNoise({ seed: mossRng.seed(), scale: 2.5 }) : null
39
39
+
const mossThreshold = Math.sin((o.mossAngle * Math.PI) / 180)
40
40
+
41
41
+
// Lumpy faceted blob, flattened vertically.
42
42
+
const blob = foliageBlob({
43
43
+
radius: o.size,
44
44
+
detail: o.size * o.detail,
45
45
+
noiseSeed: shapeRng.seed(),
46
46
+
noiseScale: 0.8,
47
47
+
noiseOctaves: 2,
48
48
+
noiseAmount: o.noise,
49
49
+
jitter: o.size * o.jitter,
50
50
+
jitterSeed: shapeRng.seed(),
51
51
+
}).scale(1, o.squash, 1)
52
52
+
53
53
+
// Cut a flat base and rest it on the ground (y = 0).
54
54
+
const baseY = -o.size * o.squash * (1 - o.flatten)
55
55
+
const base = pickRandom(o.colors, colorRng)
56
56
+
const moss = hasMoss ? pickRandom(o.mossColors, mossRng) : null
57
57
+
58
58
+
return blob
59
59
+
.warp((p) => (p[1] < baseY ? [p[0], baseY, p[2]] : p))
60
60
+
.translate(0, -baseY, 0)
61
61
+
.faceColor(facetShade({
62
62
+
base,
63
63
+
noise: colorNoise,
64
64
+
ambient: 0.5,
65
65
+
range: 0.5,
66
66
+
noiseAmount: 0.12,
67
67
+
snow: moss && mossNoise
68
68
+
? { color: moss, noise: mossNoise, threshold: mossThreshold, noiseAmount: 0.25 }
69
69
+
: undefined,
70
70
+
}))
71
71
+
}
···
1
1
+
import { sphere } from '../../../primitives'
2
2
+
import { merge } from '../../../ops'
3
3
+
import { setup, trunk, blade } from '../../../build'
4
4
+
import type { Mesh } from '../../../core/mesh'
5
5
+
import type { Vec3 } from '../../../core/types'
6
6
+
import type { OptionSchema, OptionInput } from '../../../core/schema'
7
7
+
8
8
+
export const flowerSchema = {
9
9
+
seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' },
10
10
+
height: { type: 'range', default: 0.5, min: 0.15, max: 1.2, step: 0.05, label: 'Stem Height' },
11
11
+
stemRadius: { type: 'range', default: 0.012,min: 0.004,max: 0.03, step: 0.002, label: 'Stem Radius' },
12
12
+
lean: { type: 'range', default: 0.12, min: 0, max: 0.4, step: 0.02, label: 'Lean' },
13
13
+
petals: { type: 'integer', default: 9, min: 3, max: 18, label: 'Petals' },
14
14
+
petalLength: { type: 'range', default: 0.13, min: 0.05, max: 0.3, step: 0.01, label: 'Petal Length' },
15
15
+
petalWidth: { type: 'range', default: 0.06, min: 0.02, max: 0.14, step: 0.005, label: 'Petal Width' },
16
16
+
petalLift: { type: 'range', default: 0.45, min: 0, max: 1.2, step: 0.05, label: 'Petal Lift' },
17
17
+
centerSize: { type: 'range', default: 0.045,min: 0.02, max: 0.1, step: 0.005, label: 'Center Size' },
18
18
+
leaves: { type: 'integer', default: 2, min: 0, max: 4, label: 'Leaves' },
19
19
+
leafLength: { type: 'range', default: 0.16, min: 0.05, max: 0.35, step: 0.01, label: 'Leaf Length' },
20
20
+
petalColor: { type: 'color', default: '#e0556b', label: 'Petal Color' },
21
21
+
centerColor: { type: 'color', default: '#f0c040', label: 'Center Color' },
22
22
+
stemColors: { type: 'color-array', default: ['#2f6a1e', '#3f8526'], min: 1, max: 4, label: 'Stem Colors' },
23
23
+
} satisfies OptionSchema
24
24
+
25
25
+
export type FlowerOptions = Partial<OptionInput<typeof flowerSchema>> & { preset?: string }
26
26
+
27
27
+
export const flowerPresets: Record<string, Partial<FlowerOptions>> = {
28
28
+
default: {},
29
29
+
daisy: { petals: 14, petalColor: '#f3f0ee', centerColor: '#f0b830', petalWidth: 0.04, petalLength: 0.16 },
30
30
+
tulip: { petals: 6, petalLift: 1.0, petalColor: '#d23a52', petalWidth: 0.1, petalLength: 0.18, centerSize: 0.03 },
31
31
+
poppy: { petals: 5, petalColor: '#d8392b', petalWidth: 0.12, petalLift: 0.6 },
32
32
+
dandelion: { petals: 18, petalColor: '#f2cf33', petalWidth: 0.025, petalLength: 0.1, petalLift: 0.7 },
33
33
+
}
34
34
+
35
35
+
function norm(x: number, y: number, z: number): Vec3 {
36
36
+
const l = Math.hypot(x, y, z) || 1
37
37
+
return [x / l, y / l, z / l]
38
38
+
}
39
39
+
40
40
+
export function flower(options: FlowerOptions = {}): Mesh {
41
41
+
const { o, rng } = setup(flowerSchema, options, flowerPresets)
42
42
+
const shapeRng = rng.stream('shape')
43
43
+
44
44
+
const leanAngle = shapeRng() * Math.PI * 2
45
45
+
const leanX = Math.cos(leanAngle) * o.lean
46
46
+
const leanZ = Math.sin(leanAngle) * o.lean
47
47
+
48
48
+
const parts: Mesh[] = []
49
49
+
50
50
+
// Stem.
51
51
+
parts.push(trunk({
52
52
+
height: o.height,
53
53
+
baseRadius: o.stemRadius,
54
54
+
topRadius: o.stemRadius * 0.7,
55
55
+
taper: 1,
56
56
+
lean: [leanX, leanZ],
57
57
+
noiseSeed: shapeRng.seed(),
58
58
+
noiseScale: 6,
59
59
+
noiseAmount: 0.06,
60
60
+
segments: 5,
61
61
+
heightSegments: 4,
62
62
+
colors: o.stemColors,
63
63
+
}))
64
64
+
65
65
+
const head: Vec3 = [leanX, o.height, leanZ]
66
66
+
67
67
+
// Flower center.
68
68
+
parts.push(
69
69
+
sphere({ radius: o.centerSize, widthSegments: 6, heightSegments: 4 })
70
70
+
.scale(1, 0.6, 1)
71
71
+
.translate(head[0], head[1], head[2])
72
72
+
.vertexColor(o.centerColor),
73
73
+
)
74
74
+
75
75
+
// Petals arranged in a ring, lifting up into a cup.
76
76
+
for (let i = 0; i < o.petals; i++) {
77
77
+
const angle = (i / o.petals) * Math.PI * 2 + (shapeRng() - 0.5) * 0.15
78
78
+
const ca = Math.cos(angle), sa = Math.sin(angle)
79
79
+
const dir = norm(ca, o.petalLift * 1.4, sa)
80
80
+
const start: Vec3 = [head[0] + ca * o.centerSize, head[1], head[2] + sa * o.centerSize]
81
81
+
const segs = 3
82
82
+
const path: Vec3[] = []
83
83
+
for (let j = 0; j <= segs; j++) {
84
84
+
const t = j / segs
85
85
+
path.push([
86
86
+
start[0] + dir[0] * o.petalLength * t,
87
87
+
start[1] + dir[1] * o.petalLength * t,
88
88
+
start[2] + dir[2] * o.petalLength * t,
89
89
+
])
90
90
+
}
91
91
+
parts.push(
92
92
+
blade(path, { width: (t) => o.petalWidth * Math.sin(Math.min(1, t) * Math.PI) })
93
93
+
.vertexColor(o.petalColor),
94
94
+
)
95
95
+
}
96
96
+
97
97
+
// Leaves partway up the stem.
98
98
+
for (let i = 0; i < o.leaves; i++) {
99
99
+
const t0 = 0.3 + (i / Math.max(1, o.leaves)) * 0.4
100
100
+
const angle = shapeRng() * Math.PI * 2
101
101
+
const ca = Math.cos(angle), sa = Math.sin(angle)
102
102
+
const dir = norm(ca, 0.35, sa)
103
103
+
const base: Vec3 = [leanX * t0 * t0, o.height * t0, leanZ * t0 * t0]
104
104
+
const segs = 3
105
105
+
const path: Vec3[] = []
106
106
+
for (let j = 0; j <= segs; j++) {
107
107
+
const t = j / segs
108
108
+
const droop = 0.25 * o.leafLength * t * t
109
109
+
path.push([
110
110
+
base[0] + dir[0] * o.leafLength * t,
111
111
+
base[1] + dir[1] * o.leafLength * t - droop,
112
112
+
base[2] + dir[2] * o.leafLength * t,
113
113
+
])
114
114
+
}
115
115
+
parts.push(
116
116
+
blade(path, { width: (t) => o.leafLength * 0.3 * Math.sin(Math.min(1, t) * Math.PI) })
117
117
+
.vertexColor(o.stemColors[o.stemColors.length - 1]),
118
118
+
)
119
119
+
}
120
120
+
121
121
+
return merge(...parts)
122
122
+
}
···
1
1
export { grass, grassSchema, grassPresets, type GrassOptions } from './grass'
2
2
export { fern, fernSchema, fernPresets, type FernOptions } from './fern'
3
3
+
export { flower, flowerSchema, flowerPresets, type FlowerOptions } from './flower'
···
1
1
+
import { setup, branches, heightShade } from '../../../build'
2
2
+
import type { Mesh } from '../../../core/mesh'
3
3
+
import type { OptionSchema, OptionInput } from '../../../core/schema'
4
4
+
5
5
+
export const deadTreeSchema = {
6
6
+
seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' },
7
7
+
height: { type: 'range', default: 3, min: 1.5, max: 6, step: 0.1, label: 'Trunk Length' },
8
8
+
trunkRadius: { type: 'range', default: 0.16, min: 0.05, max: 0.4, step: 0.01, label: 'Trunk Radius' },
9
9
+
levels: { type: 'integer', default: 4, min: 2, max: 5, label: 'Branch Levels' },
10
10
+
branches: { type: 'integer', default: 3, min: 2, max: 4, label: 'Splits' },
11
11
+
spread: { type: 'range', default: 0.8, min: 0.3, max: 1.3, step: 0.05, label: 'Spread' },
12
12
+
upBias: { type: 'range', default: 0.22, min: 0, max: 0.6, step: 0.02, label: 'Upward Bias' },
13
13
+
wander: { type: 'range', default: 0.5, min: 0, max: 1, step: 0.05, label: 'Gnarl' },
14
14
+
lengthFalloff: { type: 'range', default: 0.7, min: 0.5, max: 0.85, step: 0.02, label: 'Length Falloff' },
15
15
+
radiusFalloff: { type: 'range', default: 0.82, min: 0.6, max: 0.95, step: 0.02, label: 'Radius Falloff' },
16
16
+
taper: { type: 'range', default: 0.5, min: 0.3, max: 1.4, step: 0.05, label: 'Taper' },
17
17
+
segments: { type: 'integer', default: 6, min: 4, max: 12, label: 'Smoothness' },
18
18
+
sides: { type: 'integer', default: 5, min: 4, max: 8, label: 'Trunk Sides' },
19
19
+
colors: { type: 'color-array', default: ['#2e2419', '#4a3d2e', '#6b5e50'], min: 2, max: 6, label: 'Bark Colors' },
20
20
+
} satisfies OptionSchema
21
21
+
22
22
+
export type DeadTreeOptions = Partial<OptionInput<typeof deadTreeSchema>> & { preset?: string }
23
23
+
24
24
+
export const deadTreePresets: Record<string, Partial<DeadTreeOptions>> = {
25
25
+
default: {},
26
26
+
gnarled: { spread: 1.1, wander: 0.9, upBias: 0.08 },
27
27
+
tall: { height: 4.5, upBias: 0.4, spread: 0.6, levels: 5 },
28
28
+
stump: { height: 1.0, levels: 3, trunkRadius: 0.3, branches: 4, taper: 1.3 },
29
29
+
}
30
30
+
31
31
+
export function deadTree(options: DeadTreeOptions = {}): Mesh {
32
32
+
const { o, rng } = setup(deadTreeSchema, options, deadTreePresets)
33
33
+
34
34
+
const { mesh } = branches({
35
35
+
rng: rng.stream('shape'),
36
36
+
length: o.height,
37
37
+
radius: o.trunkRadius,
38
38
+
depth: o.levels,
39
39
+
children: o.branches,
40
40
+
spread: o.spread,
41
41
+
upBias: o.upBias,
42
42
+
wander: o.wander,
43
43
+
lengthFalloff: o.lengthFalloff,
44
44
+
radiusFalloff: o.radiusFalloff,
45
45
+
taper: o.taper,
46
46
+
segments: o.segments,
47
47
+
sides: o.sides,
48
48
+
})
49
49
+
50
50
+
const top = mesh.boundingBox.max.y || o.height
51
51
+
return mesh.vertexColor(heightShade(o.colors, top))
52
52
+
}
···
1
1
export { tree, treeSchema, treePresets, type TreeOptions } from './common-tree'
2
2
export { pine, pineSchema, pinePresets, type PineOptions } from './pine-tree'
3
3
export { palm, palmSchema, palmPresets, type PalmOptions } from './palm-tree'
4
4
+
export { deadTree, deadTreeSchema, deadTreePresets, type DeadTreeOptions } from './dead-tree'
···
26
26
export { scatterOnSphere } from './core/scatter'
27
27
28
28
// Build — composable model primitives
29
29
-
export { setup, trunk, foliageBlob, facetShade, heightShade, scatterOnSurface, blade } from './build'
30
30
-
export type { TrunkOptions, FoliageBlobOptions, FacetShadeOptions, SurfacePoint, ScatterOnSurfaceOptions, BladeOptions } from './build'
29
29
+
export { setup, trunk, foliageBlob, facetShade, heightShade, scatterOnSurface, blade, branches } from './build'
30
30
+
export type { TrunkOptions, FoliageBlobOptions, FacetShadeOptions, SurfacePoint, ScatterOnSurfaceOptions, BladeOptions, BranchesOptions, BranchResult, BranchTip } from './build'
31
31
32
32
// Schema & options
33
33
export { resolveOptions } from './core/schema'
···
40
40
export { bush, bushSchema, bushPresets } from './generators'
41
41
export { grass, grassSchema, grassPresets } from './generators'
42
42
export { fern, fernSchema, fernPresets } from './generators'
43
43
+
export { flower, flowerSchema, flowerPresets } from './generators'
44
44
+
export { deadTree, deadTreeSchema, deadTreePresets } from './generators'
45
45
+
export { rock, rockSchema, rockPresets } from './generators'
···
99
99
* Simple loft variant: sweep a radius along a path to make a tube/branch shape.
100
100
* radiusFn maps t (0-1 along path) to radius at that point.
101
101
*/
102
102
-
export function tube(path: Vec3[], radiusFn: number | ((t: number) => number), segments: number = 6): Mesh {
102
102
+
export function tube(path: Vec3[], radiusFn: number | ((t: number) => number), segments: number = 6, caps: boolean = true): Mesh {
103
103
const rFn = typeof radiusFn === 'number' ? () => radiusFn : radiusFn
104
104
105
105
// Generate circle cross-sections at each path point
···
138
138
}
139
139
}
140
140
141
141
-
// Caps
142
142
-
const startCenter = positions.length / 3
143
143
-
positions.push(path[0][0], path[0][1], path[0][2])
144
144
-
for (let j = 0; j < segments; j++) {
145
145
-
indices.push(startCenter, (j + 1) % segments, j)
146
146
-
}
141
141
+
// Caps (optional — omit for branch junctions where the cap discs would z-fight)
142
142
+
if (caps) {
143
143
+
const startCenter = positions.length / 3
144
144
+
positions.push(path[0][0], path[0][1], path[0][2])
145
145
+
for (let j = 0; j < segments; j++) {
146
146
+
indices.push(startCenter, (j + 1) % segments, j)
147
147
+
}
147
148
148
148
-
const endCenter = positions.length / 3
149
149
-
positions.push(path[pathLen - 1][0], path[pathLen - 1][1], path[pathLen - 1][2])
150
150
-
const endOff = (pathLen - 1) * segments
151
151
-
for (let j = 0; j < segments; j++) {
152
152
-
indices.push(endCenter, endOff + j, endOff + (j + 1) % segments)
149
149
+
const endCenter = positions.length / 3
150
150
+
positions.push(path[pathLen - 1][0], path[pathLen - 1][1], path[pathLen - 1][2])
151
151
+
const endOff = (pathLen - 1) * segments
152
152
+
for (let j = 0; j < segments; j++) {
153
153
+
indices.push(endCenter, endOff + j, endOff + (j + 1) % segments)
154
154
+
}
153
155
}
154
156
155
157
const geo = new THREE.BufferGeometry()
···
1
1
import { describe, it, expect } from 'vitest'
2
2
-
import { setup, trunk, foliageBlob, facetShade, heightShade, scatterOnSurface, blade } from '../src/build'
2
2
+
import { setup, trunk, foliageBlob, facetShade, heightShade, scatterOnSurface, blade, branches } from '../src/build'
3
3
import { sphere, box, plane } from '../src/primitives'
4
4
import type { OptionSchema } from '../src/core/schema'
5
5
import type { Vec3 } from '../src/core/types'
···
129
129
it('reaches the path extent', () => {
130
130
const m = blade(path, { width: 0.1 })
131
131
expect(m.boundingBox.max.y).toBeGreaterThan(1.0)
132
132
+
})
133
133
+
})
134
134
+
135
135
+
describe('branches', () => {
136
136
+
const opts = { seed: 1, length: 1, radius: 0.15, depth: 4, children: 2 as const }
137
137
+
138
138
+
it('produces a limb mesh and a tip per strand', () => {
139
139
+
const r = branches(opts)
140
140
+
expect(r.mesh.vertexCount).toBeGreaterThan(0)
141
141
+
// One tip per strand (every axis ends in a twig); 2 children over 4 levels → 1+2+4+8 = 15.
142
142
+
expect(r.tips.length).toBe(15)
143
143
+
})
144
144
+
145
145
+
it('tips carry a position, a unit direction, and a radius', () => {
146
146
+
const r = branches(opts)
147
147
+
for (const tip of r.tips) {
148
148
+
expect(Math.hypot(...tip.direction)).toBeCloseTo(1, 5)
149
149
+
expect(tip.radius).toBeGreaterThan(0)
150
150
+
}
151
151
+
})
152
152
+
153
153
+
it('grows upward from the base', () => {
154
154
+
const r = branches(opts)
155
155
+
expect(r.mesh.boundingBox.max.y).toBeGreaterThan(0.8)
156
156
+
})
157
157
+
158
158
+
it('is deterministic for the same seed', () => {
159
159
+
const a = branches(opts).mesh.positions
160
160
+
const b = branches(opts).mesh.positions
161
161
+
expect(Array.from(a)).toEqual(Array.from(b))
162
162
+
})
163
163
+
164
164
+
it('more depth → more limbs (more vertices)', () => {
165
165
+
const shallow = branches({ ...opts, depth: 2 }).mesh.vertexCount
166
166
+
const deep = branches({ ...opts, depth: 4 }).mesh.vertexCount
167
167
+
expect(deep).toBeGreaterThan(shallow)
132
168
})
133
169
})
134
170
···
1
1
import { describe, it, expect } from 'vitest'
2
2
-
import { tree, pine, palm, bush, grass, fern } from '../src/generators'
2
2
+
import { tree, pine, palm, bush, grass, fern, flower, deadTree, rock } from '../src/generators'
3
3
import type { Mesh } from '../src/core/mesh'
4
4
5
5
const generators = [
···
9
9
{ name: 'bush', gen: bush },
10
10
{ name: 'grass', gen: grass },
11
11
{ name: 'fern', gen: fern },
12
12
+
{ name: 'flower', gen: flower },
13
13
+
{ name: 'dead', gen: deadTree },
14
14
+
{ name: 'rock', gen: rock },
12
15
] as const
13
16
14
17
// Generators that support snow (trees + shrubs). Grass/ferns don't take snow options.
···
23
26
bush: { verts: 3600 },
24
27
grass: { verts: 768 },
25
28
fern: { verts: 6480 },
29
29
+
flower: { verts: 654 },
30
30
+
dead: { verts: 1400 },
31
31
+
rock: { verts: 1440 },
26
32
}
27
33
28
34
function allFinite(m: Mesh): boolean {