@aethervtt/shard-scatter 0.0.0-stage → 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,702 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import {
3
+ ChildOf,
4
+ Derived,
5
+ defineResource,
6
+ hashSeed,
7
+ ShardError
8
+ } from "@aethervtt/shard-core";
9
+ import { NoiseGraphs } from "@aethervtt/shard-noise";
10
+ import { Collider } from "@aethervtt/shard-physics";
11
+ import {
12
+ findGenerator,
13
+ Generated,
14
+ procgen,
15
+ requestOf
16
+ } from "@aethervtt/shard-procgen";
17
+ import {
18
+ Lod,
19
+ MaterialAsset,
20
+ Materials,
21
+ Mesh3d,
22
+ Meshes,
23
+ MeshMaterial,
24
+ Visibility,
25
+ VisibilityRange
26
+ } from "@aethervtt/shard-render";
27
+ import { LogResource } from "@aethervtt/shard-runtime";
28
+ import { PrefabInstance } from "@aethervtt/shard-scene";
29
+ import { Planet } from "@aethervtt/shard-terrain";
30
+ import { Transform } from "@aethervtt/shard-transform";
31
+ import {
32
+ isRemoved,
33
+ Prop,
34
+ Removed,
35
+ removedKey,
36
+ ScatterChunk
37
+ } from "./components.js";
38
+ import { GENERATOR_LOOKS, Vegetation } from "./material.js";
39
+ import { compileRules, PROP } from "./rules.js";
40
+ import { ScatterSet } from "./set.js";
41
+ import {
42
+ P_CELL,
43
+ P_ITEM,
44
+ P_QX,
45
+ P_RADIUS,
46
+ P_SCALE,
47
+ P_VARIANT,
48
+ P_X,
49
+ PLACEMENT_STRIDE
50
+ } from "./surface.js";
51
+ const PLACEMENT_DELAY = 2;
52
+ const KEEP = 1.15;
53
+ function chunkState(id, chunk, frame, distance = Number.POSITIVE_INFINITY) {
54
+ return {
55
+ id,
56
+ chunk,
57
+ raw: void 0,
58
+ placements: void 0,
59
+ job: void 0,
60
+ root: -1,
61
+ wanted: -1,
62
+ kept: -1,
63
+ distance,
64
+ used: frame,
65
+ spawned: 0,
66
+ next: 0
67
+ };
68
+ }
69
+ class SurfaceScatter {
70
+ surface;
71
+ /** What the compiled rules came from (sets as JSON, seeds, planet contents). */
72
+ key = "";
73
+ /** The surface's content version last refreshed against. */
74
+ content = "";
75
+ problem = null;
76
+ waiting = null;
77
+ /** [rule][item][variant] */
78
+ variants = [];
79
+ chunks = /* @__PURE__ */ new Map();
80
+ /** Chunks spawned or waiting to, by id. */
81
+ ready = false;
82
+ stats = [];
83
+ /** Foliage rules' GPU layers (with a renderer), by rule index, and the rules' version they're for. */
84
+ foliage = /* @__PURE__ */ new Map();
85
+ foliageVersion = -1;
86
+ /** Props spawned and despawned last frame, main-thread ms of the scatter system. */
87
+ spawnedLast = 0;
88
+ despawnedLast = 0;
89
+ ms = 0;
90
+ /** Main-thread ms spawning and despawning props last frame. */
91
+ spawnMs = 0;
92
+ constructor(surface) {
93
+ this.surface = surface;
94
+ }
95
+ }
96
+ class ScatterState {
97
+ surfaces = /* @__PURE__ */ new Map();
98
+ frame = 0;
99
+ workers;
100
+ /** Set when an asset (re)loaded: surfaces re-resolve their sets. */
101
+ dirty = true;
102
+ /** True while scatter despawns its own chunks (so props aren't recorded as removed). */
103
+ despawning = false;
104
+ /** Default vegetation materials, per generator name. */
105
+ materials = /* @__PURE__ */ new Map();
106
+ /** Props despawned outside scatter since the last frame (checked, then recorded as removed). */
107
+ pendingRemovals = [];
108
+ }
109
+ const Scatter = defineResource("scatter/Scatter", {
110
+ description: "Per-surface scatter state: compiled rules, placed chunks, spawned props.",
111
+ init: () => new ScatterState()
112
+ });
113
+ function resolveAsset(world, ref, get) {
114
+ if (!ref || ref.guid === void 0 && ref.path === void 0) return void 0;
115
+ if (ref.guid !== void 0) {
116
+ const v2 = get(ref.guid);
117
+ if (v2 !== void 0) return v2;
118
+ }
119
+ const server = assetServer(world);
120
+ const entry = server.entry(ref);
121
+ if (!entry) return null;
122
+ if (entry.state === "failed") return null;
123
+ const v = get(entry.guid);
124
+ if (v !== void 0) return v;
125
+ void server.request(entry.guid).catch(() => {
126
+ });
127
+ return null;
128
+ }
129
+ const refName = (ref) => ref?.path ?? ref?.guid ?? "(none)";
130
+ function planetSources(world, rt) {
131
+ const sets = world.initResource(ScatterSet.store);
132
+ const sources = [];
133
+ const own = world.get(rt.entity, Planet).scatter;
134
+ const set = resolveAsset(world, own, (g) => sets.get({ guid: g }));
135
+ if (set === null) return { sources, waiting: `scatter set ${refName(own)}` };
136
+ if (set) sources.push({ path: refName(own), set, biome: -1 });
137
+ const biomes = rt.set?.biomes ?? [];
138
+ for (let b = 0; b < rt.biomes.length; b++) {
139
+ const ref = rt.biomes[b]?.scatter;
140
+ const bs = resolveAsset(world, ref, (g) => sets.get({ guid: g }));
141
+ if (bs === null) return { sources, waiting: `scatter set ${refName(ref)}` };
142
+ if (bs) sources.push({ path: `${refName(biomes[b])}>${refName(ref)}`, set: bs, biome: b });
143
+ }
144
+ return { sources, waiting: null };
145
+ }
146
+ function planetScatters(world, rt) {
147
+ if (world.get(rt.entity, Planet).scatter) return true;
148
+ for (const b of rt.biomes) if (b?.scatter) return true;
149
+ return false;
150
+ }
151
+ function refreshSurface(world, ss, sources, seed, contentKey) {
152
+ const key = JSON.stringify([sources.map((s) => [s.path, s.biome, s.set]), seed, contentKey]);
153
+ if (key !== ss.key) {
154
+ ss.key = key;
155
+ ss.problem = null;
156
+ clearChunks(world, ss);
157
+ let rules2;
158
+ try {
159
+ rules2 = compileRules(sources, seed);
160
+ } catch (err) {
161
+ ss.problem = err;
162
+ world.tryResource(LogResource)?.error(err);
163
+ configure(ss, [], []);
164
+ return false;
165
+ }
166
+ const masks2 = rules2.map(() => void 0);
167
+ configure(ss, rules2, masks2);
168
+ ss.variants = rules2.map((rule) => rule.items.map((item) => variantsOf(world, rule, item)));
169
+ ss.stats.length = 0;
170
+ for (let i = 0; i < rules2.length; i++)
171
+ ss.stats.push({ chunks: 0, placed: 0, spawned: 0, candidates: 0 });
172
+ ss.ready = false;
173
+ }
174
+ if (ss.problem) return false;
175
+ ss.waiting = null;
176
+ const rules = ss.surface.rules;
177
+ const graphs = world.initResource(NoiseGraphs);
178
+ const masks = [];
179
+ for (const rule of rules) {
180
+ const g = resolveAsset(world, rule.noise, (guid) => graphs.get({ guid }));
181
+ if (g === null) {
182
+ ss.waiting = `noise mask ${refName(rule.noise)} of rule "${rule.name}"`;
183
+ return false;
184
+ }
185
+ masks.push(g);
186
+ }
187
+ if (masks.some((m, i) => m !== ss.surface.masks[i])) configure(ss, rules, masks);
188
+ if (!ss.ready) {
189
+ const meshes = world.initResource(Meshes);
190
+ for (const [r, items] of ss.variants.entries()) {
191
+ for (const variants of items) {
192
+ for (const v of variants) {
193
+ if (v.ready) continue;
194
+ if (v.prefab) {
195
+ v.ready = true;
196
+ continue;
197
+ }
198
+ const mesh = resolveAsset(world, v.mesh, (g) => meshes.get({ guid: g }));
199
+ if (!mesh) {
200
+ ss.waiting = `${refName(v.mesh)} (rule "${rules[r].name}")`;
201
+ continue;
202
+ }
203
+ measure(v, mesh);
204
+ v.lods = lodRefs(world, v);
205
+ let lods = true;
206
+ for (const lod of v.lods) {
207
+ if (!resolveAsset(world, lod, (g) => meshes.get({ guid: g }))) {
208
+ ss.waiting = `${refName(lod)} (rule "${rules[r].name}")`;
209
+ lods = false;
210
+ }
211
+ }
212
+ if (!lods) continue;
213
+ v.ready = true;
214
+ }
215
+ }
216
+ }
217
+ if (ss.waiting) return false;
218
+ ss.ready = true;
219
+ }
220
+ return true;
221
+ }
222
+ function configure(ss, rules, masks) {
223
+ ss.surface.configure(rules, masks, ss.surface.version + 1);
224
+ }
225
+ function variantsOf(world, rule, item) {
226
+ const out = [];
227
+ const blank = () => ({
228
+ request: void 0,
229
+ mesh: null,
230
+ lods: [],
231
+ prefab: null,
232
+ material: item.material,
233
+ height: 1,
234
+ minY: 0,
235
+ radius: item.radius || (rule.spacing > 0 ? rule.spacing / 2 : 0.5),
236
+ half: [0.5, 0.5, 0.5],
237
+ mid: [0, 0.5, 0],
238
+ ready: false
239
+ });
240
+ if (item.prefab) {
241
+ const v = { ...blank(), prefab: item.prefab };
242
+ v.height = v.radius * 2;
243
+ out.push(v);
244
+ return out;
245
+ }
246
+ const gen = findGenerator(item.generator);
247
+ if (!gen) {
248
+ throw new ShardError("procgen/unknown-generator", `No generator named "${item.generator}"`, {
249
+ hint: "Engine generators: shard/Rock, shard/Tree, shard/Bush, shard/GrassClump, shard/Crystal."
250
+ });
251
+ }
252
+ if (gen.output !== "mesh") {
253
+ throw new ShardError(
254
+ "scatter/item-not-mesh",
255
+ `Rule "${rule.name}" places ${gen.name}, which makes ${gen.output}, not a mesh`,
256
+ { hint: "Scatter items are mesh generators or prefabs." }
257
+ );
258
+ }
259
+ for (let i = 0; i < item.variants; i++) {
260
+ const request = requestOf(gen, item.params, hashSeed(rule.seed, i));
261
+ const entry = procgen(world).entryFor(request);
262
+ out.push({
263
+ ...blank(),
264
+ request,
265
+ mesh: { type: "Mesh", guid: entry.guid, path: entry.path },
266
+ material: item.material ?? defaultMaterial(world, gen.name)
267
+ });
268
+ }
269
+ return out;
270
+ }
271
+ function defaultMaterial(world, generator) {
272
+ const state = world.resource(Scatter);
273
+ let ref = state.materials.get(generator);
274
+ if (ref) return ref;
275
+ const look = GENERATOR_LOOKS[generator] ?? GENERATOR_LOOKS["shard/Rock"];
276
+ const material = new MaterialAsset(
277
+ {
278
+ baseTint: look.baseTint,
279
+ tipTint: look.tipTint,
280
+ roughness: look.roughness,
281
+ sway: look.sway,
282
+ doubleSided: look.doubleSided ?? false
283
+ },
284
+ Vegetation
285
+ );
286
+ ref = world.initResource(Materials).add(material, `scatter/${generator}`);
287
+ state.materials.set(generator, ref);
288
+ return ref;
289
+ }
290
+ function measure(v, mesh) {
291
+ const b = mesh.bounds;
292
+ v.height = Math.max(1e-3, b[4] - b[1]);
293
+ v.minY = b[1];
294
+ v.radius ||= 0;
295
+ const r = Math.max(Math.abs(b[0]), Math.abs(b[3]), Math.abs(b[2]), Math.abs(b[5]));
296
+ if (!(v.radius > 0) || v.request) v.radius = r;
297
+ v.half = [(b[3] - b[0]) / 2, (b[4] - b[1]) / 2, (b[5] - b[2]) / 2];
298
+ v.mid = [(b[3] + b[0]) / 2, (b[4] + b[1]) / 2, (b[5] + b[2]) / 2];
299
+ }
300
+ function lodRefs(world, v) {
301
+ if (!v.request || !v.mesh) return [];
302
+ const record = procgen(world).recordOf(v.mesh.guid);
303
+ const out = [];
304
+ const labels = (record?.assets ?? []).map((a) => a.label).filter((l) => /^LOD\d+$/.test(l)).sort((a, b) => Number(a.slice(3)) - Number(b.slice(3)));
305
+ for (const label of labels) {
306
+ const e = procgen(world).entryFor(v.request, label);
307
+ out.push({ type: "Mesh", guid: e.guid, path: e.path });
308
+ }
309
+ return out;
310
+ }
311
+ function clearChunks(world, ss) {
312
+ const state = world.resource(Scatter);
313
+ state.despawning = true;
314
+ try {
315
+ for (const c of ss.chunks.values()) {
316
+ if (c.job) c.job.cancelled = true;
317
+ if (c.root >= 0 && world.isAlive(c.root)) world.despawn(c.root);
318
+ }
319
+ } finally {
320
+ state.despawning = false;
321
+ }
322
+ ss.chunks.clear();
323
+ }
324
+ const viewers = { points: new Float64Array(24), cameras: 0, anchors: 0 };
325
+ const order = [];
326
+ const deps = [];
327
+ function updateProps(world, ss, frame, budget, workers) {
328
+ const surface = ss.surface;
329
+ surface.viewers(world, viewers);
330
+ const total = viewers.cameras + viewers.anchors;
331
+ for (const rule of surface.rules) {
332
+ if (rule.kind !== PROP) continue;
333
+ const s = ss.stats[rule.index];
334
+ s.chunks = 0;
335
+ surface.chunksNear(rule, viewers.points, 0, total, rule.range * KEEP, (chunk, distance) => {
336
+ const id = `${rule.index}|${chunk.key}`;
337
+ let c = ss.chunks.get(id);
338
+ if (!c) {
339
+ c = chunkState(id, chunk, frame, distance);
340
+ ss.chunks.set(id, c);
341
+ }
342
+ if (c.kept !== frame) {
343
+ c.kept = frame;
344
+ c.distance = distance;
345
+ s.chunks++;
346
+ } else c.distance = Math.min(c.distance, distance);
347
+ if (distance <= rule.range) c.wanted = frame;
348
+ c.used = frame;
349
+ });
350
+ }
351
+ deps.length = 0;
352
+ for (const c of ss.chunks.values()) {
353
+ if (c.wanted !== frame) continue;
354
+ const rule = surface.rules[c.chunk.rule];
355
+ if (rule.avoid.length === 0) continue;
356
+ c.deps ??= rule.avoid.flatMap((a) => neighborChunks(ss, surface.rules[a], c.chunk, frame));
357
+ for (const d of c.deps) deps.push(d);
358
+ }
359
+ for (const d of deps) {
360
+ d.used = frame;
361
+ if (d.wanted !== frame) d.needed = frame;
362
+ }
363
+ order.length = 0;
364
+ for (const c of ss.chunks.values()) {
365
+ if ((c.wanted === frame || c.needed === frame) && !c.raw && !c.job) order.push(c);
366
+ }
367
+ order.sort((a, b) => a.distance - b.distance || (a.id < b.id ? -1 : 1));
368
+ for (let i = 0; i < order.length && i < budget.chunksPerFrame; i++) {
369
+ const c = order[i];
370
+ c.job = surface.startPlacement(c.chunk, frame + PLACEMENT_DELAY, workers);
371
+ }
372
+ order.length = 0;
373
+ for (const c of ss.chunks.values()) if (c.job && c.job.due <= frame) order.push(c);
374
+ order.sort((a, b) => a.id < b.id ? -1 : 1);
375
+ for (const c of order) finish(ss, c);
376
+ order.length = 0;
377
+ for (const c of ss.chunks.values()) {
378
+ if (c.wanted !== frame || !c.raw) continue;
379
+ if (c.deps && !c.deps.every((d) => d.raw)) continue;
380
+ if (c.root < 0 || c.placements && c.next < c.placements.count) order.push(c);
381
+ }
382
+ order.sort((a, b) => a.distance - b.distance || (a.id < b.id ? -1 : 1));
383
+ let spawned = 0;
384
+ const t0 = performance.now();
385
+ for (const c of order) {
386
+ if (spawned >= budget.propsPerFrame) break;
387
+ const fp = finalPlacements(ss, c, frame);
388
+ spawned += spawnChunk(world, ss, c, fp, budget.propsPerFrame - spawned);
389
+ }
390
+ ss.spawnedLast = spawned;
391
+ let despawned = 0;
392
+ const state = world.resource(Scatter);
393
+ state.despawning = true;
394
+ try {
395
+ for (const c of ss.chunks.values()) {
396
+ if (c.root >= 0 && c.kept !== frame) {
397
+ if (world.isAlive(c.root)) world.despawn(c.root);
398
+ despawned += c.spawned;
399
+ ss.stats[c.chunk.rule].spawned -= c.spawned;
400
+ c.root = -1;
401
+ c.spawned = 0;
402
+ c.next = 0;
403
+ }
404
+ }
405
+ } finally {
406
+ state.despawning = false;
407
+ }
408
+ ss.spawnMs = performance.now() - t0;
409
+ ss.despawnedLast = despawned;
410
+ if (ss.chunks.size > budget.cache) trimCache(ss, budget.cache, frame);
411
+ }
412
+ function finish(ss, c) {
413
+ const job = c.job;
414
+ c.job = void 0;
415
+ if (job.cancelled) return;
416
+ c.raw = ss.surface.finishPlacement(job);
417
+ const s = ss.stats[c.chunk.rule];
418
+ s.placed += c.raw.count;
419
+ s.candidates += c.raw.candidates;
420
+ }
421
+ function rawPlacements(ss, c, frame) {
422
+ if (!c.raw) {
423
+ if (!c.job) c.job = ss.surface.startPlacement(c.chunk, frame, void 0);
424
+ finish(ss, c);
425
+ }
426
+ return c.raw;
427
+ }
428
+ const probe = new Float64Array(3);
429
+ const tangent = new Float64Array(3);
430
+ function finalPlacements(ss, c, frame) {
431
+ if (c.placements) return c.placements;
432
+ const raw = rawPlacements(ss, c, frame);
433
+ const rule = ss.surface.rules[c.chunk.rule];
434
+ fillRadii(ss, raw);
435
+ if (rule.avoid.length === 0 || raw.count === 0) {
436
+ c.placements = raw;
437
+ return raw;
438
+ }
439
+ const keep = new Uint8Array(raw.count).fill(1);
440
+ const near = [];
441
+ const cx = c.chunk.center[0];
442
+ const cy = c.chunk.center[1];
443
+ const cz = c.chunk.center[2];
444
+ let extent = 0;
445
+ for (let i = 0; i < raw.count; i++) {
446
+ const o = i * PLACEMENT_STRIDE;
447
+ const x = raw.data[o + P_X];
448
+ const y = raw.data[o + P_X + 1];
449
+ const z = raw.data[o + P_X + 2];
450
+ extent = Math.max(extent, x * x + y * y + z * z);
451
+ }
452
+ extent = Math.sqrt(extent);
453
+ for (const a of rule.avoid) {
454
+ const avoided = ss.surface.rules[a];
455
+ for (const other of c.deps ?? neighborChunks(ss, avoided, c.chunk, frame)) {
456
+ if (other.chunk.rule !== a) continue;
457
+ const op = finalPlacements(ss, other, frame);
458
+ fillRadii(ss, op);
459
+ for (let j = 0; j < op.count; j++) {
460
+ const oo = j * PLACEMENT_STRIDE;
461
+ const ox = other.chunk.center[0] + op.data[oo + P_X] - cx;
462
+ const oy = other.chunk.center[1] + op.data[oo + P_X + 1] - cy;
463
+ const oz = other.chunk.center[2] + op.data[oo + P_X + 2] - cz;
464
+ const r = op.data[oo + P_RADIUS];
465
+ const reach = extent + r;
466
+ if (ox * ox + oy * oy + oz * oz > reach * reach) continue;
467
+ near.push(ox, oy, oz, r);
468
+ }
469
+ }
470
+ }
471
+ let lo0 = Infinity;
472
+ let hi0 = -Infinity;
473
+ let lo1 = Infinity;
474
+ let hi1 = -Infinity;
475
+ let lo2 = Infinity;
476
+ let hi2 = -Infinity;
477
+ for (let i = 0; i < raw.count; i++) {
478
+ const o = i * PLACEMENT_STRIDE + P_X;
479
+ lo0 = Math.min(lo0, raw.data[o]);
480
+ hi0 = Math.max(hi0, raw.data[o]);
481
+ lo1 = Math.min(lo1, raw.data[o + 1]);
482
+ hi1 = Math.max(hi1, raw.data[o + 1]);
483
+ lo2 = Math.min(lo2, raw.data[o + 2]);
484
+ hi2 = Math.max(hi2, raw.data[o + 2]);
485
+ }
486
+ const axis = hi0 - lo0 >= hi1 - lo1 && hi0 - lo0 >= hi2 - lo2 ? 0 : hi1 - lo1 >= hi2 - lo2 ? 1 : 2;
487
+ const order2 = new Int32Array(raw.count);
488
+ const coord = new Float64Array(raw.count);
489
+ for (let i = 0; i < raw.count; i++) order2[i] = i;
490
+ order2.sort(
491
+ (a, b) => raw.data[a * PLACEMENT_STRIDE + P_X + axis] - raw.data[b * PLACEMENT_STRIDE + P_X + axis]
492
+ );
493
+ for (let i = 0; i < raw.count; i++)
494
+ coord[i] = raw.data[order2[i] * PLACEMENT_STRIDE + P_X + axis];
495
+ for (let k = 0; k < near.length; k += 4) {
496
+ const ox = near[k];
497
+ const oy = near[k + 1];
498
+ const oz = near[k + 2];
499
+ const r = near[k + 3];
500
+ const r2 = r * r;
501
+ const along = axis === 0 ? ox : axis === 1 ? oy : oz;
502
+ let a = 0;
503
+ let b = raw.count;
504
+ while (a < b) {
505
+ const m = a + b >> 1;
506
+ if (coord[m] < along - r) a = m + 1;
507
+ else b = m;
508
+ }
509
+ for (let s = a; s < raw.count && coord[s] <= along + r; s++) {
510
+ const i = order2[s];
511
+ if (!keep[i]) continue;
512
+ const o = i * PLACEMENT_STRIDE;
513
+ const dx = raw.data[o + P_X] - ox;
514
+ const dy = raw.data[o + P_X + 1] - oy;
515
+ const dz = raw.data[o + P_X + 2] - oz;
516
+ if (dx * dx + dy * dy + dz * dz < r2) keep[i] = 0;
517
+ }
518
+ }
519
+ let n = 0;
520
+ const data = new Float32Array(raw.data.length);
521
+ for (let i = 0; i < raw.count; i++) {
522
+ if (!keep[i]) continue;
523
+ data.set(
524
+ raw.data.subarray(i * PLACEMENT_STRIDE, (i + 1) * PLACEMENT_STRIDE),
525
+ n * PLACEMENT_STRIDE
526
+ );
527
+ n++;
528
+ }
529
+ c.placements = { chunk: raw.chunk, count: n, data, candidates: raw.candidates };
530
+ return c.placements;
531
+ }
532
+ function fillRadii(ss, p) {
533
+ const variants = ss.variants[p.chunk.rule];
534
+ for (let i = 0; i < p.count; i++) {
535
+ const o = i * PLACEMENT_STRIDE;
536
+ const v = variants[p.data[o + P_ITEM]][p.data[o + P_VARIANT]];
537
+ p.data[o + P_RADIUS] = v.radius * p.data[o + P_SCALE];
538
+ }
539
+ }
540
+ function neighborChunks(ss, avoided, chunk, frame) {
541
+ const out = /* @__PURE__ */ new Map();
542
+ const surface = ss.surface;
543
+ let reach = 0;
544
+ for (const items of ss.variants[avoided.index])
545
+ for (const v of items) reach = Math.max(reach, v.radius * avoided.scaleMax);
546
+ const span = chunk.extent + reach;
547
+ surface.upAt(chunk.center[0], chunk.center[1], chunk.center[2], probe);
548
+ const ux = probe[0];
549
+ const uy = probe[1];
550
+ const uz = probe[2];
551
+ const ax = Math.abs(uy) < 0.9 ? 0 : 1;
552
+ const ay = Math.abs(uy) < 0.9 ? 1 : 0;
553
+ let e1x = ay * uz;
554
+ let e1y = -ax * uz;
555
+ let e1z = ax * uy - ay * ux;
556
+ const l = Math.sqrt(e1x * e1x + e1y * e1y + e1z * e1z) || 1;
557
+ e1x /= l;
558
+ e1y /= l;
559
+ e1z /= l;
560
+ tangent[0] = uy * e1z - uz * e1y;
561
+ tangent[1] = uz * e1x - ux * e1z;
562
+ tangent[2] = ux * e1y - uy * e1x;
563
+ for (let j = -2; j <= 2; j++) {
564
+ for (let i = -2; i <= 2; i++) {
565
+ const s = span / 2;
566
+ const x = chunk.center[0] + (e1x * i + tangent[0] * j) * s;
567
+ const y = chunk.center[1] + (e1y * i + tangent[1] * j) * s;
568
+ const z = chunk.center[2] + (e1z * i + tangent[2] * j) * s;
569
+ const found = surface.chunkAt(avoided, x, y, z);
570
+ const id = `${avoided.index}|${found.key}`;
571
+ if (out.has(id)) continue;
572
+ let c = ss.chunks.get(id);
573
+ if (!c) {
574
+ c = chunkState(id, found, frame);
575
+ ss.chunks.set(id, c);
576
+ }
577
+ out.set(id, c);
578
+ }
579
+ }
580
+ return [...out.values()].sort((a, b) => a.id < b.id ? -1 : 1);
581
+ }
582
+ function spawnChunk(world, ss, c, p, allowance) {
583
+ const rule = ss.surface.rules[c.chunk.rule];
584
+ const variants = ss.variants[rule.index];
585
+ const removed = world.initResource(Removed);
586
+ if (c.root < 0) {
587
+ c.root = world.spawn(
588
+ [ScatterChunk, { surface: ss.surface.entity, rule: rule.id, chunk: c.chunk.key }],
589
+ Transform,
590
+ Visibility,
591
+ Derived
592
+ );
593
+ ss.surface.placeChunkRoot(world, c.root, c.chunk);
594
+ c.next = 0;
595
+ }
596
+ let n = 0;
597
+ while (c.next < p.count && n < allowance) {
598
+ const i = c.next++;
599
+ const o = i * PLACEMENT_STRIDE;
600
+ const index = p.data[o + P_CELL];
601
+ if (isRemoved(removed, removedKey(rule.id, c.chunk.key, index))) continue;
602
+ const v = variants[p.data[o + P_ITEM]][p.data[o + P_VARIANT]];
603
+ const s = p.data[o + P_SCALE];
604
+ const qx = p.data[o + P_QX];
605
+ const qy = p.data[o + P_QX + 1];
606
+ const qz = p.data[o + P_QX + 2];
607
+ const qw = p.data[o + P_QX + 3];
608
+ const ux = 2 * (qx * qy - qw * qz);
609
+ const uy = 1 - 2 * (qx * qx + qz * qz);
610
+ const uz = 2 * (qy * qz + qw * qx);
611
+ const sink = rule.sink * v.height * s;
612
+ const transform = {
613
+ translation: [
614
+ p.data[o + P_X] - ux * sink,
615
+ p.data[o + P_X + 1] - uy * sink,
616
+ p.data[o + P_X + 2] - uz * sink
617
+ ],
618
+ rotation: [qx, qy, qz, qw],
619
+ scale: [s, s, s]
620
+ };
621
+ v.parts ??= partsOf(rule, v);
622
+ const parts = v.parts;
623
+ spawnAll(world, [
624
+ ...parts,
625
+ [Transform, transform],
626
+ [Prop, { index }],
627
+ [ChildOf, { parent: c.root }]
628
+ ]);
629
+ n++;
630
+ }
631
+ c.spawned += n;
632
+ ss.stats[rule.index].spawned += n;
633
+ return n;
634
+ }
635
+ function partsOf(rule, v) {
636
+ const parts = [Generated, Derived];
637
+ if (v.prefab) {
638
+ parts.push([PrefabInstance, { prefab: v.prefab }]);
639
+ return parts;
640
+ }
641
+ parts.push(
642
+ [Mesh3d, { mesh: v.mesh }],
643
+ [MeshMaterial, { material: v.material }],
644
+ [VisibilityRange, { start: 0, end: rule.range }]
645
+ );
646
+ if (v.lods.length > 0) {
647
+ const levels = [{ mesh: v.mesh, screenSize: 0.12 }];
648
+ const sizes = [0.04, 0];
649
+ for (let l = 0; l < v.lods.length; l++)
650
+ levels.push({ mesh: v.lods[l], screenSize: sizes[Math.min(l, sizes.length - 1)] });
651
+ levels[levels.length - 1].screenSize = 0;
652
+ parts.push([Lod, { levels }]);
653
+ }
654
+ const collider = colliderOf(rule, v);
655
+ if (collider) parts.push([Collider, collider]);
656
+ return parts;
657
+ }
658
+ const spawnAll = (world, parts) => world.spawn(...parts);
659
+ function colliderOf(rule, v) {
660
+ switch (rule.collider) {
661
+ case "none":
662
+ return void 0;
663
+ case "convex":
664
+ return v.mesh ? { shape: "convex", mesh: v.lods.at(-1) ?? v.mesh } : void 0;
665
+ case "trimesh":
666
+ return v.mesh ? { shape: "trimesh", mesh: v.lods[0] ?? v.mesh } : void 0;
667
+ case "ball":
668
+ return { shape: "ball", radius: Math.max(v.radius, v.height / 2) * 0.8 };
669
+ case "cuboid":
670
+ return { shape: "cuboid", halfExtents: v.half };
671
+ }
672
+ }
673
+ function trimCache(ss, size, frame) {
674
+ const idle = [];
675
+ for (const c of ss.chunks.values()) if (c.root < 0 && !c.job && c.used !== frame) idle.push(c);
676
+ idle.sort((a, b) => a.used - b.used || (a.id < b.id ? -1 : 1));
677
+ for (let i = 0; i < idle.length && ss.chunks.size > size; i++) {
678
+ const c = idle[i];
679
+ if (c.raw) {
680
+ const s = ss.stats[c.chunk.rule];
681
+ s.placed -= c.raw.count;
682
+ s.candidates -= c.raw.candidates;
683
+ }
684
+ ss.chunks.delete(c.id);
685
+ }
686
+ }
687
+ export {
688
+ PLACEMENT_DELAY,
689
+ Scatter,
690
+ ScatterState,
691
+ SurfaceScatter,
692
+ chunkState,
693
+ clearChunks,
694
+ defaultMaterial,
695
+ finalPlacements,
696
+ neighborChunks,
697
+ planetScatters,
698
+ planetSources,
699
+ refreshSurface,
700
+ resolveAsset,
701
+ updateProps
702
+ };