@aethervtt/shard-procgen 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ export declare const RockGenerator: import("..").Generator<{
2
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
3
+ readonly roughness: import("@aethervtt/shard-core").FieldType<number, "f32">;
4
+ readonly flatness: import("@aethervtt/shard-core").FieldType<number, "f32">;
5
+ readonly facets: import("@aethervtt/shard-core").FieldType<number, "u8">;
6
+ readonly detail: import("@aethervtt/shard-core").FieldType<number, "u8">;
7
+ }, "mesh">;
8
+ export declare const TreeGenerator: import("..").Generator<{
9
+ readonly height: import("@aethervtt/shard-core").FieldType<number, "f32">;
10
+ readonly trunkRadius: import("@aethervtt/shard-core").FieldType<number, "f32">;
11
+ readonly levels: import("@aethervtt/shard-core").FieldType<number, "u8">;
12
+ readonly branching: import("@aethervtt/shard-core").FieldType<number, "u8">;
13
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
14
+ readonly leafDensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
15
+ readonly leafSize: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
+ }, "mesh">;
17
+ export declare const BushGenerator: import("..").Generator<{
18
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
19
+ readonly stems: import("@aethervtt/shard-core").FieldType<number, "u8">;
20
+ readonly leafDensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
21
+ }, "mesh">;
22
+ export declare const GrassClumpGenerator: import("..").Generator<{
23
+ readonly blades: import("@aethervtt/shard-core").FieldType<number, "u8">;
24
+ readonly height: import("@aethervtt/shard-core").FieldType<number, "f32">;
25
+ readonly width: import("@aethervtt/shard-core").FieldType<number, "f32">;
26
+ readonly bend: import("@aethervtt/shard-core").FieldType<number, "f32">;
27
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
28
+ }, "mesh">;
29
+ export declare const CrystalGenerator: import("..").Generator<{
30
+ readonly count: import("@aethervtt/shard-core").FieldType<number, "u8">;
31
+ readonly length: import("@aethervtt/shard-core").FieldType<number, "f32">;
32
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
33
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
34
+ readonly sides: import("@aethervtt/shard-core").FieldType<number, "u8">;
35
+ }, "mesh">;
36
+ /** Every engine generator (importing this module defines them). */
37
+ export declare const ENGINE_GENERATORS: readonly [import("..").Generator<{
38
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
39
+ readonly roughness: import("@aethervtt/shard-core").FieldType<number, "f32">;
40
+ readonly flatness: import("@aethervtt/shard-core").FieldType<number, "f32">;
41
+ readonly facets: import("@aethervtt/shard-core").FieldType<number, "u8">;
42
+ readonly detail: import("@aethervtt/shard-core").FieldType<number, "u8">;
43
+ }, "mesh">, import("..").Generator<{
44
+ readonly height: import("@aethervtt/shard-core").FieldType<number, "f32">;
45
+ readonly trunkRadius: import("@aethervtt/shard-core").FieldType<number, "f32">;
46
+ readonly levels: import("@aethervtt/shard-core").FieldType<number, "u8">;
47
+ readonly branching: import("@aethervtt/shard-core").FieldType<number, "u8">;
48
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
49
+ readonly leafDensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
50
+ readonly leafSize: import("@aethervtt/shard-core").FieldType<number, "f32">;
51
+ }, "mesh">, import("..").Generator<{
52
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
53
+ readonly stems: import("@aethervtt/shard-core").FieldType<number, "u8">;
54
+ readonly leafDensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
55
+ }, "mesh">, import("..").Generator<{
56
+ readonly blades: import("@aethervtt/shard-core").FieldType<number, "u8">;
57
+ readonly height: import("@aethervtt/shard-core").FieldType<number, "f32">;
58
+ readonly width: import("@aethervtt/shard-core").FieldType<number, "f32">;
59
+ readonly bend: import("@aethervtt/shard-core").FieldType<number, "f32">;
60
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
61
+ }, "mesh">, import("..").Generator<{
62
+ readonly count: import("@aethervtt/shard-core").FieldType<number, "u8">;
63
+ readonly length: import("@aethervtt/shard-core").FieldType<number, "f32">;
64
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
65
+ readonly spread: import("@aethervtt/shard-core").FieldType<number, "f32">;
66
+ readonly sides: import("@aethervtt/shard-core").FieldType<number, "u8">;
67
+ }, "mesh">];
@@ -0,0 +1,355 @@
1
+ import { Rng, t } from "@aethervtt/shard-core";
2
+ import { DEG, sinCos } from "@aethervtt/shard-mesh";
3
+ import { defineGenerator } from "../generator.js";
4
+ import { fbm } from "./shape-noise.js";
5
+ const sc = new Float64Array(2);
6
+ function standOnGround(b, sink) {
7
+ let lo = Number.POSITIVE_INFINITY;
8
+ let hi = Number.NEGATIVE_INFINITY;
9
+ for (let i = 0; i < b.vertexCount; i++) {
10
+ const y = b.positions[i * 3 + 1];
11
+ if (y < lo) lo = y;
12
+ if (y > hi) hi = y;
13
+ }
14
+ b.translate(0, -lo - (hi - lo) * sink, 0);
15
+ }
16
+ const RockGenerator = defineGenerator("shard/Rock", {
17
+ description: "A boulder: a noisy, faceted, flattened icosphere sitting on its origin, part-buried. LODs at 50% and 20%.",
18
+ params: {
19
+ radius: t.f32({ default: 1, min: 0.05, unit: "m", description: "Rough size before noise." }),
20
+ roughness: t.f32({
21
+ default: 0.5,
22
+ min: 0,
23
+ max: 1,
24
+ description: "Lumpiness: 0 smooth, 1 craggy."
25
+ }),
26
+ flatness: t.f32({
27
+ default: 0.3,
28
+ min: 0,
29
+ max: 1,
30
+ description: "Squash toward a slab: 0 round, 1 flat."
31
+ }),
32
+ facets: t.u8({
33
+ default: 6,
34
+ max: 32,
35
+ description: "Flat cuts across the surface, for fractured stone (0: none)."
36
+ }),
37
+ detail: t.u8({
38
+ default: 4,
39
+ min: 1,
40
+ max: 6,
41
+ description: "Icosphere subdivisions: 4 is 2 562 vertices."
42
+ })
43
+ },
44
+ output: "mesh",
45
+ version: 1,
46
+ run(ctx, p) {
47
+ const b = ctx.mesh.create().icosphere(p.detail, 1);
48
+ const rng = ctx.rng;
49
+ const sx = 0.85 + rng.float() * 0.3;
50
+ const sz = 0.85 + rng.float() * 0.3;
51
+ const sy = (0.75 + rng.float() * 0.25) * (1 - 0.6 * p.flatness);
52
+ b.scale(sx, sy, sz);
53
+ const pos = b.positions;
54
+ for (let f = 0; f < p.facets; f++) {
55
+ let x = rng.float() * 2 - 1;
56
+ let y = rng.float() * 1.6 - 0.4;
57
+ let z = rng.float() * 2 - 1;
58
+ const len = Math.sqrt(x * x + y * y + z * z) || 1;
59
+ x /= len;
60
+ y /= len;
61
+ z /= len;
62
+ const d = 0.62 + rng.float() * 0.25;
63
+ for (let i = 0; i < b.vertexCount; i++) {
64
+ const k = pos[i * 3] * x + pos[i * 3 + 1] * y + pos[i * 3 + 2] * z - d;
65
+ if (k <= 0) continue;
66
+ pos[i * 3] = pos[i * 3] - x * k;
67
+ pos[i * 3 + 1] = pos[i * 3 + 1] - y * k;
68
+ pos[i * 3 + 2] = pos[i * 3 + 2] - z * k;
69
+ }
70
+ }
71
+ b.normals();
72
+ const seed = ctx.childSeed("shape");
73
+ b.displace((q) => p.roughness * 0.22 * fbm(seed, q[0] * 1.7, q[1] * 1.7, q[2] * 1.7, 4));
74
+ b.scale(p.radius);
75
+ standOnGround(b, 0.15);
76
+ for (let i = 0; i < b.vertexCount; i++) {
77
+ const q = b.positions;
78
+ const shade = 0.5 + 0.5 * fbm(seed + 7, q[i * 3] * 3, q[i * 3 + 1] * 3, q[i * 3 + 2] * 3, 2);
79
+ b.uv1(i, 0, shade);
80
+ }
81
+ b.normals({ angle: p.facets > 0 ? 50 : 180 }).uvsTriplanar(Math.max(0.25, p.radius));
82
+ return ctx.mesh.finish(b, { tangents: true, lods: [0.5, 0.2] });
83
+ }
84
+ });
85
+ const TreeGenerator = defineGenerator("shard/Tree", {
86
+ description: "A broadleaf tree: a branching skeleton skinned with tubes, leaf cards on the outer branches. LODs at 40% and 12%.",
87
+ params: {
88
+ height: t.f32({ default: 8, min: 0.5, unit: "m", description: "About how tall it grows." }),
89
+ trunkRadius: t.f32({ default: 0.22, min: 0.01, unit: "m" }),
90
+ levels: t.u8({ default: 2, max: 4, description: "Branching levels below the trunk." }),
91
+ branching: t.u8({ default: 5, min: 1, max: 12, description: "Children per branch." }),
92
+ spread: t.f32({
93
+ default: 42,
94
+ min: 0,
95
+ max: 90,
96
+ unit: "deg",
97
+ description: "Angle between a branch and its parent."
98
+ }),
99
+ leafDensity: t.f32({
100
+ default: 10,
101
+ min: 0,
102
+ description: "Leaf cards per metre of outer branch."
103
+ }),
104
+ leafSize: t.f32({ default: 0.45, min: 0.01, unit: "m" })
105
+ },
106
+ output: "mesh",
107
+ version: 1,
108
+ run(ctx, p) {
109
+ const b = ctx.mesh.create();
110
+ const sk = ctx.mesh.treeSkeleton({
111
+ seed: ctx.childSeed("skeleton"),
112
+ trunk: { length: p.height * 0.55, radius: p.trunkRadius, segments: 8 },
113
+ levels: p.levels,
114
+ branches: p.branching,
115
+ spread: p.spread,
116
+ lengthScale: 0.62,
117
+ start: 0.4,
118
+ gravity: 0.05
119
+ });
120
+ ctx.mesh.tubeAlong(b, sk, { sides: 8 });
121
+ b.uv1All(0, 0.5);
122
+ const leaves = ctx.mesh.leafCards(b, sk, {
123
+ seed: ctx.childSeed("leaves"),
124
+ density: p.leafDensity,
125
+ size: p.leafSize,
126
+ levels: p.levels > 0 ? [p.levels, Math.max(1, p.levels - 1)] : [0]
127
+ });
128
+ shadeCards(b, leaves, ctx.rng);
129
+ return ctx.mesh.finish(b, { lods: [0.4, 0.12] });
130
+ }
131
+ });
132
+ const BushGenerator = defineGenerator("shard/Bush", {
133
+ description: "A shrub: several short branching stems from one point, densely leaved. LODs at 40% and 12%.",
134
+ params: {
135
+ radius: t.f32({ default: 0.8, min: 0.05, unit: "m", description: "About how wide it grows." }),
136
+ stems: t.u8({ default: 6, min: 1, max: 24 }),
137
+ leafDensity: t.f32({ default: 18, min: 0, description: "Leaf cards per metre of branch." })
138
+ },
139
+ output: "mesh",
140
+ version: 1,
141
+ run(ctx, p) {
142
+ const b = ctx.mesh.create();
143
+ const rng = ctx.rng;
144
+ let leaves = -1;
145
+ for (let s = 0; s < p.stems; s++) {
146
+ const sk = ctx.mesh.treeSkeleton({
147
+ seed: ctx.childSeed(`stem${s}`),
148
+ trunk: {
149
+ length: p.radius * (0.9 + rng.float() * 0.5),
150
+ radius: p.radius * 0.025,
151
+ segments: 4
152
+ },
153
+ levels: 1,
154
+ branches: 3,
155
+ spread: 35,
156
+ lengthScale: 0.6,
157
+ gravity: 0.2,
158
+ start: 0.3
159
+ });
160
+ sinCos(s / p.stems * Math.PI * 2 + rng.float() * 0.6, sc);
161
+ const lean = 0.5 + rng.float() * 0.35;
162
+ sinCos(lean, sc2);
163
+ const ax = sc[1];
164
+ const az = sc[0];
165
+ const first = b.vertexCount;
166
+ ctx.mesh.tubeAlong(b, sk, { sides: 5 });
167
+ b.uv1All(0, 0.4, first);
168
+ const cards = ctx.mesh.leafCards(b, sk, {
169
+ seed: ctx.childSeed(`leaves${s}`),
170
+ density: p.leafDensity,
171
+ size: p.radius * 0.3,
172
+ levels: [0, 1],
173
+ droop: 0.1
174
+ });
175
+ if (leaves < 0) leaves = cards;
176
+ shadeCards(b, cards, rng);
177
+ b.transform(rotationAbout(-az, 0, ax, sc2[0], sc2[1]), first);
178
+ }
179
+ return ctx.mesh.finish(b, { lods: [0.4, 0.12] });
180
+ }
181
+ });
182
+ const sc2 = new Float64Array(2);
183
+ const GrassClumpGenerator = defineGenerator("shard/GrassClump", {
184
+ description: "A clump of grass blades: tapered, bent strips from one root. One-sided (foliage draws two-sided); uv1.x runs 0 at the root to 1 at the tip, for wind and tint. LODs: 5/8 of the blades, wider, at 2 segments; then 5/16 at 1 (a triangle each).",
185
+ params: {
186
+ blades: t.u8({ default: 12, min: 1, max: 64 }),
187
+ height: t.f32({ default: 0.55, min: 0.01, unit: "m" }),
188
+ width: t.f32({
189
+ default: 0.045,
190
+ min: 1e-3,
191
+ unit: "m",
192
+ description: "Blade width at the root."
193
+ }),
194
+ bend: t.f32({ default: 0.35, min: 0, max: 1, description: "How far blades curve over." }),
195
+ spread: t.f32({
196
+ default: 0.18,
197
+ min: 0,
198
+ unit: "m",
199
+ description: "Radius the roots scatter over."
200
+ })
201
+ },
202
+ output: "mesh",
203
+ version: 2,
204
+ run(ctx, p) {
205
+ const clump = (segments, keep) => {
206
+ const b = ctx.mesh.create();
207
+ const rng = new Rng(ctx.childSeed("blades"));
208
+ for (let k = 0; k < p.blades; k++) {
209
+ sinCos(rng.float() * Math.PI * 2, sc);
210
+ const r = Math.sqrt(rng.float()) * p.spread;
211
+ const rx = sc[1] * r;
212
+ const rz = sc[0] * r;
213
+ const dx = r > 0 ? rx / r : 1;
214
+ const dz = r > 0 ? rz / r : 0;
215
+ sinCos(r > 0 ? (rng.float() - 0.5) * 1.2 : rng.float() * Math.PI * 2, sc);
216
+ const fx = dx * sc[1] - dz * sc[0];
217
+ const fz = dx * sc[0] + dz * sc[1];
218
+ const h = p.height * (0.6 + rng.float() * 0.5);
219
+ const w = p.width * (0.7 + rng.float() * 0.6) / keep;
220
+ const bend = p.bend * (0.5 + rng.float());
221
+ const shade = rng.float();
222
+ if (k * 0.618033988749895 % 1 >= keep) continue;
223
+ const first = b.vertexCount;
224
+ const rows = segments === 1 ? 1 : segments + 1;
225
+ for (let s = 0; s < rows; s++) {
226
+ const f = s / segments;
227
+ const out = bend * h * f * f;
228
+ const y = h * f * (1 - 0.35 * bend * f);
229
+ const half = w / 2 * (1 - f * 0.92);
230
+ const cx = rx + fx * out;
231
+ const cz = rz + fz * out;
232
+ const wx = -fz * half;
233
+ const wz = fx * half;
234
+ const ty = h * (1 - 0.7 * bend * f);
235
+ const tf = 2 * bend * h * f;
236
+ const nx = fx * ty;
237
+ const ny = -tf;
238
+ const nz = fz * ty;
239
+ const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
240
+ b.vertex(cx - wx, y, cz - wz, nx / nl, ny / nl, nz / nl, 0, 1 - f);
241
+ b.vertex(cx + wx, y, cz + wz, nx / nl, ny / nl, nz / nl, 1, 1 - f);
242
+ b.uv1(first + s * 2, f, shade);
243
+ b.uv1(first + s * 2 + 1, f, shade);
244
+ }
245
+ if (segments === 1) {
246
+ const out = bend * h;
247
+ const tip = b.vertex(
248
+ rx + fx * out,
249
+ h * (1 - 0.35 * bend),
250
+ rz + fz * out,
251
+ fx * h * (1 - 0.7 * bend),
252
+ -2 * bend * h,
253
+ fz * h * (1 - 0.7 * bend),
254
+ 0.5,
255
+ 0
256
+ );
257
+ b.uv1(tip, 1, shade);
258
+ b.triangle(first, tip, first + 1);
259
+ continue;
260
+ }
261
+ for (let s = 0; s < segments; s++) {
262
+ const a = first + s * 2;
263
+ b.quad(a, a + 2, a + 3, a + 1);
264
+ }
265
+ }
266
+ return b.finish();
267
+ };
268
+ return { kind: "mesh", mesh: clump(4, 1), lods: [clump(2, 0.625), clump(1, 0.3125)] };
269
+ }
270
+ });
271
+ const CrystalGenerator = defineGenerator("shard/Crystal", {
272
+ description: "A crystal cluster: pointed prisms fanning out from a base, with hard edges. LODs at 50% and 20%.",
273
+ params: {
274
+ count: t.u8({ default: 6, min: 1, max: 32, description: "Prisms in the cluster." }),
275
+ length: t.f32({ default: 1.2, min: 0.01, unit: "m", description: "The longest prism." }),
276
+ radius: t.f32({ default: 0.14, min: 5e-3, unit: "m" }),
277
+ spread: t.f32({
278
+ default: 28,
279
+ min: 0,
280
+ max: 80,
281
+ unit: "deg",
282
+ description: "How far prisms tilt from upright."
283
+ }),
284
+ sides: t.u8({ default: 6, min: 3, max: 12 })
285
+ },
286
+ output: "mesh",
287
+ version: 1,
288
+ run(ctx, p) {
289
+ const b = ctx.mesh.create();
290
+ const rng = ctx.rng;
291
+ for (let k = 0; k < p.count; k++) {
292
+ const main = k === 0;
293
+ const len = p.length * (main ? 1 : 0.35 + rng.float() * 0.5);
294
+ const r = p.radius * (main ? 1 : 0.5 + rng.float() * 0.5) * (len / p.length) ** 0.3;
295
+ const first = b.vertexCount;
296
+ b.lathe([r * 0.85, -r, r, len * 0.72, 0, len], { sides: p.sides, caps: true });
297
+ for (let i = first; i < b.vertexCount; i++) {
298
+ const y = b.positions[i * 3 + 1];
299
+ b.uv1(i, Math.max(0, Math.min(1, y / len)), rng.float());
300
+ }
301
+ const tilt = (main ? 0.3 : 0.6 + rng.float() * 0.5) * p.spread * DEG;
302
+ sinCos(rng.float() * Math.PI * 2, sc);
303
+ sinCos(tilt, sc2);
304
+ const m = rotationAbout(sc[1], 0, sc[0], sc2[0], sc2[1]);
305
+ const off = main ? 0 : r * 1.4;
306
+ m[12] = -sc[0] * off;
307
+ m[14] = sc[1] * off;
308
+ b.transform(m, first);
309
+ }
310
+ standOnGround(b, 0.08);
311
+ b.normals({ angle: 30 });
312
+ return ctx.mesh.finish(b, { lods: [0.5, 0.2] });
313
+ }
314
+ });
315
+ function rotationAbout(x, y, z, s, c) {
316
+ const len = Math.sqrt(x * x + y * y + z * z) || 1;
317
+ x /= len;
318
+ y /= len;
319
+ z /= len;
320
+ const k = 1 - c;
321
+ const m = new Float64Array(16);
322
+ m[0] = c + x * x * k;
323
+ m[1] = y * x * k + z * s;
324
+ m[2] = z * x * k - y * s;
325
+ m[4] = x * y * k - z * s;
326
+ m[5] = c + y * y * k;
327
+ m[6] = z * y * k + x * s;
328
+ m[8] = x * z * k + y * s;
329
+ m[9] = y * z * k - x * s;
330
+ m[10] = c + z * z * k;
331
+ m[15] = 1;
332
+ return m;
333
+ }
334
+ function shadeCards(b, first, rng) {
335
+ let shade = rng.float();
336
+ for (let i = first; i < b.vertexCount; i++) {
337
+ if ((i - first) % 12 === 0) shade = rng.float();
338
+ b.uv1(i, 1, shade);
339
+ }
340
+ }
341
+ const ENGINE_GENERATORS = [
342
+ RockGenerator,
343
+ TreeGenerator,
344
+ BushGenerator,
345
+ GrassClumpGenerator,
346
+ CrystalGenerator
347
+ ];
348
+ export {
349
+ BushGenerator,
350
+ CrystalGenerator,
351
+ ENGINE_GENERATORS,
352
+ GrassClumpGenerator,
353
+ RockGenerator,
354
+ TreeGenerator
355
+ };
@@ -0,0 +1,4 @@
1
+ /** Smooth value noise in [−1, 1]. */
2
+ export declare function valueNoise(seed: number, x: number, y: number, z: number): number;
3
+ /** Fractal sum of `octaves` value-noise layers (halving amplitude, doubling frequency), ~[−1, 1]. */
4
+ export declare function fbm(seed: number, x: number, y: number, z: number, octaves: number): number;
@@ -0,0 +1,46 @@
1
+ import { hash32 } from "@aethervtt/shard-core";
2
+ const INV = 1 / 4294967296;
3
+ function lattice(seed, x, y, z) {
4
+ return hash32(seed, x, y, z) * INV * 2 - 1;
5
+ }
6
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
7
+ function valueNoise(seed, x, y, z) {
8
+ const xi = Math.floor(x);
9
+ const yi = Math.floor(y);
10
+ const zi = Math.floor(z);
11
+ const u = fade(x - xi);
12
+ const v = fade(y - yi);
13
+ const w = fade(z - zi);
14
+ const a = lattice(seed, xi, yi, zi);
15
+ const b = lattice(seed, xi + 1, yi, zi);
16
+ const c = lattice(seed, xi, yi + 1, zi);
17
+ const d = lattice(seed, xi + 1, yi + 1, zi);
18
+ const e = lattice(seed, xi, yi, zi + 1);
19
+ const f = lattice(seed, xi + 1, yi, zi + 1);
20
+ const g = lattice(seed, xi, yi + 1, zi + 1);
21
+ const h = lattice(seed, xi + 1, yi + 1, zi + 1);
22
+ const ab = a + (b - a) * u;
23
+ const cd = c + (d - c) * u;
24
+ const ef = e + (f - e) * u;
25
+ const gh = g + (h - g) * u;
26
+ const lo = ab + (cd - ab) * v;
27
+ const hi = ef + (gh - ef) * v;
28
+ return lo + (hi - lo) * w;
29
+ }
30
+ function fbm(seed, x, y, z, octaves) {
31
+ let sum = 0;
32
+ let amp = 0.5;
33
+ let norm = 0;
34
+ let f = 1;
35
+ for (let o = 0; o < octaves; o++) {
36
+ sum += valueNoise(seed + o * 1013, x * f, y * f, z * f) * amp;
37
+ norm += amp;
38
+ amp *= 0.5;
39
+ f *= 2.03;
40
+ }
41
+ return sum / norm;
42
+ }
43
+ export {
44
+ fbm,
45
+ valueNoise
46
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { findNondeterminism, type NondeterministicCall } from './check.js';
2
2
  export { type ContextState, createContext, type GenContext, guardNondeterminism, type LoadedDependency, type NoiseApi, } from './context.js';
3
+ export { BushGenerator, CrystalGenerator, ENGINE_GENERATORS, GrassClumpGenerator, RockGenerator, TreeGenerator, } from './engine/generators.js';
4
+ export { fbm, valueNoise } from './engine/shape-noise.js';
3
5
  export { allGenerators, canonicalParams, codeHashOf, defineGenerator, type Fragment, type FragmentEntity, findGenerator, type Generator, type GeneratorOptions, type GenParams, type GenRequest, guidOf, hashString, identityOf, type MeshResult, type OutputAsset, type OutputSpec, parseProceduralRef, proceduralPath, type RunResult, requestOf, requireGenerator, setGeneratorCodeHashes, type TextureData, } from './generator.js';
4
6
  export { GeneratorImporter, type GenFile, parseGenFile } from './importer.js';
5
7
  export { Generated, GeneratorInstance, instanceRequest } from './instance.js';
package/dist/index.js CHANGED
@@ -3,6 +3,15 @@ import {
3
3
  createContext,
4
4
  guardNondeterminism
5
5
  } from "./context.js";
6
+ import {
7
+ BushGenerator,
8
+ CrystalGenerator,
9
+ ENGINE_GENERATORS,
10
+ GrassClumpGenerator,
11
+ RockGenerator,
12
+ TreeGenerator
13
+ } from "./engine/generators.js";
14
+ import { fbm, valueNoise } from "./engine/shape-noise.js";
6
15
  import {
7
16
  allGenerators,
8
17
  canonicalParams,
@@ -54,15 +63,21 @@ import {
54
63
  import { genFileSchema } from "./schema.js";
55
64
  import { contactSheet, drawLabel, parseSeeds } from "./sheet.js";
56
65
  export {
66
+ BushGenerator,
67
+ CrystalGenerator,
57
68
  DataAssetType,
69
+ ENGINE_GENERATORS,
58
70
  Generated,
59
71
  GeneratedData,
60
72
  GeneratorAssetType,
61
73
  GeneratorBindings,
62
74
  GeneratorImporter,
63
75
  GeneratorInstance,
76
+ GrassClumpGenerator,
64
77
  ProcgenResource,
65
78
  ProcgenRuntime,
79
+ RockGenerator,
80
+ TreeGenerator,
66
81
  allGenerators,
67
82
  canonicalParams,
68
83
  codeHashOf,
@@ -75,6 +90,7 @@ export {
75
90
  encodeOutput,
76
91
  encodeRecord,
77
92
  executeJob,
93
+ fbm,
78
94
  findGenerator,
79
95
  findNondeterminism,
80
96
  genFileSchema,
@@ -105,5 +121,6 @@ export {
105
121
  runJob,
106
122
  setGeneratorCodeHashes,
107
123
  toJson,
124
+ valueNoise,
108
125
  warmGeneratorWorkers
109
126
  };
package/dist/job.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { findAssetType } from "@aethervtt/shard-assets";
2
2
  import { ShardError } from "@aethervtt/shard-core";
3
+ import { loadMeshSimplifier } from "@aethervtt/shard-mesh";
3
4
  import { useNoiseKernel } from "@aethervtt/shard-noise";
4
5
  import {
5
6
  createContext,
@@ -47,6 +48,7 @@ async function executeJob(job) {
47
48
  const gen = requireGenerator(job.generator);
48
49
  if (job.noise) useNoiseKernel(job.noise, job.noiseSimd ?? true);
49
50
  const deps = await loadDependencies(job);
51
+ await loadMeshSimplifier();
50
52
  const state = { children: [], warnings: [] };
51
53
  const params = gen.params.deserialize(job.params);
52
54
  const ctx = createContext(gen, job, deps, job.chain, state);
@@ -1,4 +1,4 @@
1
- import { type MeshData } from '@aethervtt/shard-mesh';
1
+ import { type LeafCardOptions, MeshBuilder, type MeshData, type SimplifyOptions, type TreeSkeleton, type TreeSkeletonOptions } from '@aethervtt/shard-mesh';
2
2
  import type { MeshResult } from './generator.js';
3
3
  /**
4
4
  * Mesh building inside a generator. Deterministic across hosts: only +, *, and sqrt (no trig),
@@ -14,11 +14,26 @@ export interface MeshBuilderApi {
14
14
  primitive(name: 'box' | 'cube' | 'sphere' | 'plane' | 'cylinder' | 'cone' | 'capsule' | 'torus', params?: Record<string, number>): MeshData;
15
15
  /** Recomputes smooth normals from the triangles (area-weighted). */
16
16
  computeNormals(mesh: MeshData): MeshData;
17
+ /** A procedural modelling builder (0045): shapes, deformation, merge, weld, normals, UVs. */
18
+ create(): MeshBuilder;
19
+ /** A builder holding a copy of `mesh`. */
20
+ from(mesh: MeshData): MeshBuilder;
21
+ /** A recursive branch graph for trees and bushes (seeded). */
22
+ treeSkeleton(options: TreeSkeletonOptions): TreeSkeleton;
23
+ /** Skins a skeleton with tubes, appended to `b`; returns the first vertex added. */
24
+ tubeAlong(b: MeshBuilder, skeleton: TreeSkeleton, options?: {
25
+ sides?: number;
26
+ }): number;
27
+ /** Leaf cards along a skeleton's branches, appended to `b`; returns the first vertex added. */
28
+ leafCards(b: MeshBuilder, skeleton: TreeSkeleton, options: LeafCardOptions): number;
29
+ /** Simplified copies at fractions of the triangle count (meshoptimizer). */
30
+ simplifyLods(mesh: MeshData, fractions: readonly number[], options?: SimplifyOptions): MeshData[];
17
31
  /**
18
- * Finishes a mesh: optional normals and tangents, and levels of detail as fractions of the
19
- * triangle count (`lods: [0.5, 0.2]`), made by vertex clustering.
32
+ * Finishes a mesh (data or a builder): optional normals and tangents, and levels of detail as
33
+ * fractions of the triangle count (`lods: [0.5, 0.2]`), simplified by meshoptimizer (UV seams
34
+ * kept, each level's vertices a subset of the source's).
20
35
  */
21
- finish(mesh: MeshData, options?: {
36
+ finish(mesh: MeshData | MeshBuilder, options?: {
22
37
  normals?: boolean;
23
38
  tangents?: boolean;
24
39
  lods?: readonly number[];
@@ -5,9 +5,14 @@ import {
5
5
  cone,
6
6
  cube,
7
7
  cylinder,
8
+ leafCards,
9
+ MeshBuilder,
8
10
  plane,
11
+ simplifyLods,
9
12
  sphere,
10
- torus
13
+ torus,
14
+ treeSkeleton,
15
+ tubeAlong
11
16
  } from "@aethervtt/shard-mesh";
12
17
  function icosphere(detail) {
13
18
  if (!Number.isInteger(detail) || detail < 0 || detail > 8) {
@@ -289,112 +294,19 @@ function computeTangents(mesh) {
289
294
  }
290
295
  return out;
291
296
  }
292
- function cluster(mesh, cells) {
293
- const p = mesh.positions;
294
- const count = p.length / 3;
295
- let minX = Infinity;
296
- let minY = Infinity;
297
- let minZ = Infinity;
298
- let maxX = -Infinity;
299
- let maxY = -Infinity;
300
- let maxZ = -Infinity;
301
- for (let v = 0; v < count; v++) {
302
- minX = Math.min(minX, p[v * 3]);
303
- minY = Math.min(minY, p[v * 3 + 1]);
304
- minZ = Math.min(minZ, p[v * 3 + 2]);
305
- maxX = Math.max(maxX, p[v * 3]);
306
- maxY = Math.max(maxY, p[v * 3 + 1]);
307
- maxZ = Math.max(maxZ, p[v * 3 + 2]);
308
- }
309
- const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1;
310
- const cell = size / cells;
311
- const map = new Int32Array(count);
312
- const byKey = /* @__PURE__ */ new Map();
313
- const sums = [];
314
- const members = [];
315
- for (let v = 0; v < count; v++) {
316
- const ix = Math.min(cells - 1, Math.floor((p[v * 3] - minX) / cell));
317
- const iy = Math.min(cells - 1, Math.floor((p[v * 3 + 1] - minY) / cell));
318
- const iz = Math.min(cells - 1, Math.floor((p[v * 3 + 2] - minZ) / cell));
319
- const key = (ix * cells + iy) * cells + iz;
320
- let c = byKey.get(key);
321
- if (c === void 0) {
322
- c = members.length;
323
- byKey.set(key, c);
324
- members.push(0);
325
- sums.push(0, 0, 0);
326
- }
327
- map[v] = c;
328
- members[c] = members[c] + 1;
329
- sums[c * 3] = sums[c * 3] + p[v * 3];
330
- sums[c * 3 + 1] = sums[c * 3 + 1] + p[v * 3 + 1];
331
- sums[c * 3 + 2] = sums[c * 3 + 2] + p[v * 3 + 2];
332
- }
333
- const tri = triangles(mesh);
334
- const kept = [];
335
- for (let t = 0; t < tri.length; t += 3) {
336
- const a = map[tri[t]];
337
- const b = map[tri[t + 1]];
338
- const c = map[tri[t + 2]];
339
- if (a !== b && b !== c && c !== a) kept.push(a, b, c);
340
- }
341
- const n = members.length;
342
- const positions = new Float32Array(n * 3);
343
- for (let c = 0; c < n; c++) {
344
- positions[c * 3] = sums[c * 3] / members[c];
345
- positions[c * 3 + 1] = sums[c * 3 + 1] / members[c];
346
- positions[c * 3 + 2] = sums[c * 3 + 2] / members[c];
347
- }
348
- const out = {
349
- positions,
350
- indices: n > 65535 ? new Uint32Array(kept) : new Uint16Array(kept)
351
- };
352
- for (const [name, width] of [
353
- ["uvs", 2],
354
- ["colors", 4]
355
- ]) {
356
- const src = mesh[name];
357
- if (!src) continue;
358
- const acc = new Float64Array(n * width);
359
- for (let v = 0; v < count; v++)
360
- for (let k = 0; k < width; k++)
361
- acc[map[v] * width + k] = acc[map[v] * width + k] + src[v * width + k];
362
- const avg = new Float32Array(n * width);
363
- for (let c = 0; c < n; c++)
364
- for (let k = 0; k < width; k++) avg[c * width + k] = acc[c * width + k] / members[c];
365
- out[name] = avg;
366
- }
367
- return out;
368
- }
369
- function simplify(mesh, fraction) {
370
- const target = Math.max(4, Math.floor(triangles(mesh).length / 3 * fraction));
371
- let lo = 1;
372
- let hi = 256;
373
- let best = cluster(mesh, 2);
374
- for (let step = 0; step < 8 && lo <= hi; step++) {
375
- const cells = lo + hi >> 1;
376
- const candidate = cluster(mesh, cells);
377
- const tris = (candidate.indices?.length ?? 0) / 3;
378
- if (tris <= target) {
379
- best = candidate;
380
- lo = cells + 1;
381
- } else hi = cells - 1;
382
- }
383
- return best;
384
- }
385
- function finish(mesh, options = {}) {
297
+ function finish(input, options = {}) {
298
+ const mesh = input instanceof MeshBuilder ? input.finish() : input;
386
299
  const withNormals = (m) => options.normals === true || !m.normals ? computeNormals(m) : m;
387
300
  const withTangents = (m) => options.tangents ? { ...m, tangents: computeTangents(m) } : m;
388
301
  const base = withTangents(withNormals(mesh));
389
- const lods = [];
390
302
  for (const fraction of options.lods ?? []) {
391
303
  if (!(fraction > 0 && fraction < 1)) {
392
304
  throw new ShardError("procgen/bad-mesh", `LOD fractions are in (0, 1), got ${fraction}`, {
393
305
  hint: "lods: [0.5, 0.2] keeps about half, then a fifth, of the triangles."
394
306
  });
395
307
  }
396
- lods.push(withTangents(computeNormals(simplify(mesh, fraction))));
397
308
  }
309
+ const lods = options.lods?.length ? simplifyLods(base, options.lods) : [];
398
310
  return { kind: "mesh", mesh: base, lods };
399
311
  }
400
312
  const meshBuilder = {
@@ -414,6 +326,12 @@ const meshBuilder = {
414
326
  return out;
415
327
  },
416
328
  computeNormals,
329
+ create: () => MeshBuilder.create(),
330
+ from: (mesh) => MeshBuilder.from(mesh),
331
+ treeSkeleton,
332
+ tubeAlong,
333
+ leafCards,
334
+ simplifyLods,
417
335
  finish
418
336
  };
419
337
  export {
package/dist/methods.js CHANGED
@@ -1,4 +1,4 @@
1
- import { assetServer } from "@aethervtt/shard-assets";
1
+ import { assetServer, findAssetPreview } from "@aethervtt/shard-assets";
2
2
  import {
3
3
  defineSchema,
4
4
  isPlainObject,
@@ -48,6 +48,8 @@ async function resolveGenerator(world, target2) {
48
48
  if (exact) return { gen: exact, params: {} };
49
49
  const matches = allGenerators().filter((g) => g.name.endsWith(`/${name}`));
50
50
  if (matches.length === 1) return { gen: matches[0], params: {} };
51
+ const own = matches.filter((g) => !g.name.startsWith("shard/"));
52
+ if (own.length === 1) return { gen: own[0], params: {} };
51
53
  throw new ShardError("procgen/unknown-generator", `No generator "${target2}"`, {
52
54
  hint: matches.length ? `Several match: ${matches.map((g) => g.name).join(", ")}.` : `Generators: ${allGenerators().map((g) => g.name).join(", ") || "(none)"}.`
53
55
  });
@@ -136,7 +138,7 @@ const procgenMethods = [
136
138
  },
137
139
  {
138
140
  name: "procgen.preview",
139
- description: 'A PNG of a generator output: a mesh in a neutral studio light from a three-quarter view, a texture as is, entities as a scene framed on their bounds. With seeds ("1-9" or [1, 4, 9]) it is a labelled contact sheet, one cell per seed: change a param, preview nine seeds, compare.',
141
+ description: 'A PNG of a generator output: a mesh in a neutral studio light from a three-quarter view, a texture as is, entities as a scene framed on their bounds. With seeds ("1-9" or [1, 4, 9]) it is a labelled contact sheet, one cell per seed: change a param, preview nine seeds, compare. A *.scatter.json path previews the set on a 64 m patch, from above and at eye level.',
140
142
  params: defineSchema("procgen/PreviewParams", {
141
143
  generator: target,
142
144
  params: t.json({ description: "Param values." }),
@@ -145,9 +147,21 @@ const procgenMethods = [
145
147
  size: t.u32({ default: 256, min: 32, max: 1024, description: "Pixels per cell." })
146
148
  }),
147
149
  handler: async ({ world }, p) => {
150
+ const size = p.size;
151
+ const asset = assetServer(world).entry(p.generator);
152
+ if (asset && !p.generator.includes(".gen.json") && findAssetPreview(asset.type)) {
153
+ const image2 = await findAssetPreview(asset.type)(world, asset.path, size * 2, size, {});
154
+ const png2 = await encodePng(image2.data, image2.width, image2.height);
155
+ return {
156
+ width: image2.width,
157
+ height: image2.height,
158
+ data: toBase64(png2),
159
+ asset: asset.path,
160
+ type: asset.type
161
+ };
162
+ }
148
163
  const found = await resolveGenerator(world, p.generator);
149
164
  const params = { ...found.params, ...paramsOf(p.params) };
150
- const size = p.size;
151
165
  const seeds = p.seeds === void 0 || p.seeds === null ? void 0 : parseSeeds(p.seeds);
152
166
  let image;
153
167
  const keys = [];
package/dist/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { assetServer } from "@aethervtt/shard-assets";
2
2
  import { definePlugin } from "@aethervtt/shard-runtime";
3
3
  import { updateInstances } from "@aethervtt/shard-scene";
4
+ import * as engineModule from "./engine/generators.js";
4
5
  import * as importerModule from "./importer.js";
5
6
  import * as instanceModule from "./instance.js";
6
7
  import { procgenMethods } from "./methods.js";
@@ -10,7 +11,8 @@ import { procgen } from "./runtime.js";
10
11
  function procgenPlugin(options = {}) {
11
12
  return definePlugin({
12
13
  name: "procgen",
13
- provides: [importerModule, instanceModule, refsModule, runtimeModule],
14
+ // The engine's generators (shard/Rock, shard/Tree, …) are defined by importing their module.
15
+ provides: [engineModule, importerModule, instanceModule, refsModule, runtimeModule],
14
16
  build(app) {
15
17
  const runtime = procgen(app.world).configure({
16
18
  ...options.fs ? { fs: options.fs } : {},
package/dist/worker.d.ts CHANGED
@@ -6,5 +6,5 @@ import { type GenJob } from './job.js';
6
6
  export declare function runGeneratorJob(job: GenJob): Promise<{
7
7
  packed: Uint8Array;
8
8
  }>;
9
- /** Nothing: loading this module (and the project's code) is the point. */
10
- export declare function warmUp(): true;
9
+ /** Nothing: loading this module (the engine's generators and the project's code) is the point. */
10
+ export declare function warmUp(): boolean;
package/dist/worker.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ENGINE_GENERATORS } from "./engine/generators.js";
1
2
  import { executeJob } from "./job.js";
2
3
  import { encodeRecord } from "./record.js";
3
4
  async function runGeneratorJob(job) {
@@ -5,7 +6,7 @@ async function runGeneratorJob(job) {
5
6
  return { packed: encodeRecord({ key: "", ...result, bytes: 0 }) };
6
7
  }
7
8
  function warmUp() {
8
- return true;
9
+ return ENGINE_GENERATORS.length > 0;
9
10
  }
10
11
  export {
11
12
  runGeneratorJob,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-procgen",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,16 +20,16 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@aethervtt/shard-assets": "0.1.0",
24
- "@aethervtt/shard-core": "0.1.0",
25
- "@aethervtt/shard-mesh": "0.1.0",
26
- "@aethervtt/shard-noise": "0.1.0",
27
- "@aethervtt/shard-platform": "0.1.0",
28
- "@aethervtt/shard-protocol": "0.1.0",
29
- "@aethervtt/shard-render": "0.1.0",
30
- "@aethervtt/shard-runtime": "0.1.0",
31
- "@aethervtt/shard-scene": "0.1.0",
32
- "@aethervtt/shard-texture": "0.1.0"
23
+ "@aethervtt/shard-assets": "0.2.1",
24
+ "@aethervtt/shard-core": "0.2.1",
25
+ "@aethervtt/shard-mesh": "0.2.1",
26
+ "@aethervtt/shard-noise": "0.2.1",
27
+ "@aethervtt/shard-platform": "0.2.1",
28
+ "@aethervtt/shard-protocol": "0.2.1",
29
+ "@aethervtt/shard-render": "0.2.1",
30
+ "@aethervtt/shard-runtime": "0.2.1",
31
+ "@aethervtt/shard-scene": "0.2.1",
32
+ "@aethervtt/shard-texture": "0.2.1"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",
@@ -41,6 +41,6 @@
41
41
  "provenance": true
42
42
  },
43
43
  "shard": {
44
- "buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
44
+ "buildSha": "772eb0d2b8a5fc499105708f4ae92c58788cc9ea"
45
45
  }
46
46
  }