@aethervtt/shard-scatter 0.0.0-stage → 0.2.0

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,231 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import { NoiseGraph } from "@aethervtt/shard-noise";
3
+ import { procgenPlugin } from "@aethervtt/shard-procgen";
4
+ import { ScenePlugin } from "@aethervtt/shard-scene";
5
+ import { Biome, BiomeSet, Planet } from "@aethervtt/shard-terrain";
6
+ import { planetApp } from "@aethervtt/shard-terrain/testing";
7
+ import { Transform } from "@aethervtt/shard-transform";
8
+ import { Prop, propIdentity } from "./components.js";
9
+ import { scatterPlugin } from "./plugin.js";
10
+ import { Scatter } from "./runtime.js";
11
+ import { ScatterSet } from "./set.js";
12
+ const TEST_RADIUS = 2e4;
13
+ const TEST_HEIGHT = {
14
+ output: "h",
15
+ nodes: {
16
+ hills: { fbm: { source: "simplex", octaves: 5, frequency: 4e-4, seed: 21 } },
17
+ ridges: { ridged: { source: "simplex", octaves: 4, frequency: 15e-5, seed: 22 } },
18
+ h: { add: ["hills", { multiply: ["ridges", 0.6] }] }
19
+ }
20
+ };
21
+ const TEST_CLIMATE = {
22
+ output: "temperature",
23
+ nodes: {
24
+ temperature: { add: [{ fbm: { octaves: 2, frequency: 1e-4, seed: 23 } }, 0.6] },
25
+ moisture: { fbm: { octaves: 3, frequency: 12e-4, seed: 24 } }
26
+ }
27
+ };
28
+ const GRASSLAND_SET = {
29
+ rules: [
30
+ {
31
+ name: "boulders",
32
+ items: [{ generator: "shard/Rock", params: { radius: 0.9, detail: 3 }, variants: 4 }],
33
+ density: 3e-3,
34
+ spacing: 6,
35
+ align: 0.7,
36
+ scale: [0.6, 1.5],
37
+ sink: 0.15,
38
+ collider: "convex",
39
+ range: 300
40
+ },
41
+ {
42
+ name: "bushes",
43
+ items: [{ generator: "shard/Bush", variants: 4 }],
44
+ density: 0.02,
45
+ spacing: 2.5,
46
+ masks: { slope: [0, 22] },
47
+ scale: [0.7, 1.3],
48
+ avoid: ["boulders"],
49
+ range: 150
50
+ },
51
+ {
52
+ name: "grass",
53
+ kind: "foliage",
54
+ items: [{ generator: "shard/GrassClump", variants: 4 }],
55
+ density: 5,
56
+ masks: { slope: [0, 25] },
57
+ align: 0.4,
58
+ scale: [0.7, 1.4],
59
+ avoid: ["boulders"],
60
+ range: 50
61
+ }
62
+ ]
63
+ };
64
+ const FOREST_SET = {
65
+ rules: [
66
+ {
67
+ name: "trees",
68
+ items: [{ generator: "shard/Tree", variants: 6 }],
69
+ density: 0.02,
70
+ spacing: 4,
71
+ masks: { slope: [0, 25] },
72
+ scale: [0.7, 1.3],
73
+ sink: 0.02,
74
+ collider: "ball",
75
+ range: 400
76
+ },
77
+ {
78
+ name: "bushes",
79
+ items: [{ generator: "shard/Bush", params: { radius: 0.6 }, variants: 3 }],
80
+ density: 0.05,
81
+ spacing: 1.5,
82
+ avoid: ["trees"],
83
+ range: 120
84
+ },
85
+ {
86
+ name: "grass",
87
+ kind: "foliage",
88
+ items: [{ generator: "shard/GrassClump", params: { height: 0.4 }, variants: 3 }],
89
+ density: 2,
90
+ avoid: ["trees"],
91
+ range: 40
92
+ }
93
+ ]
94
+ };
95
+ const ROCK_SET = {
96
+ rules: [
97
+ {
98
+ name: "boulders",
99
+ items: [{ generator: "shard/Rock", params: { radius: 1.5, roughness: 0.6 }, variants: 6 }],
100
+ density: 0.012,
101
+ spacing: 5,
102
+ align: 1,
103
+ scale: [0.5, 2],
104
+ sink: 0.25,
105
+ collider: "convex",
106
+ range: 400
107
+ },
108
+ {
109
+ name: "crystals",
110
+ items: [{ generator: "shard/Crystal", variants: 4 }],
111
+ density: 4e-3,
112
+ spacing: 6,
113
+ align: 0.5,
114
+ avoid: ["boulders"],
115
+ range: 250
116
+ }
117
+ ]
118
+ };
119
+ async function scatterPlanet(gpu, platform, options = {}) {
120
+ const p = await planetApp(gpu, {
121
+ radius: TEST_RADIUS,
122
+ heightScale: 250,
123
+ height: await NoiseGraph.create(TEST_HEIGHT),
124
+ climate: await NoiseGraph.create(TEST_CLIMATE),
125
+ seed: 3,
126
+ width: options.width ?? 160,
127
+ heightPx: options.height ?? 100,
128
+ msaa: options.msaa,
129
+ renderScale: options.renderScale,
130
+ physics: false,
131
+ clearColor: [0.45, 0.6, 0.85, 1],
132
+ extra: [
133
+ ScenePlugin,
134
+ procgenPlugin(),
135
+ scatterPlugin({ workers: options.workers ? platform.workers : void 0 }),
136
+ ...options.extra ?? []
137
+ ]
138
+ });
139
+ const w = p.world;
140
+ await assetServer(w).configure({ platform }).scan();
141
+ const sets = w.initResource(ScatterSet.store);
142
+ const grassland = sets.add(ScatterSet.deserialize(GRASSLAND_SET), "grassland.scatter");
143
+ const forest = sets.add(ScatterSet.deserialize(FOREST_SET), "forest.scatter");
144
+ const rocks = sets.add(ScatterSet.deserialize(ROCK_SET), "rocks.scatter");
145
+ const biomes = w.initResource(Biome.store);
146
+ const json = (ref) => ({ guid: ref.guid, path: ref.path });
147
+ const biome = (value) => json(biomes.add(Biome.deserialize(value)));
148
+ const set = w.initResource(BiomeSet.store).add(
149
+ BiomeSet.deserialize({
150
+ biomes: [
151
+ biome({
152
+ moisture: [-2, 0.15],
153
+ slope: [0, 24],
154
+ tint: [0.32, 0.5, 0.18, 1],
155
+ scatter: json(grassland)
156
+ }),
157
+ biome({
158
+ moisture: [0.15, 2],
159
+ slope: [0, 24],
160
+ tint: [0.12, 0.3, 0.1, 1],
161
+ scatter: json(forest)
162
+ }),
163
+ biome({ slope: [24, 90], tint: [0.42, 0.4, 0.37, 1], scatter: json(rocks) })
164
+ ],
165
+ latitudeBias: 0
166
+ })
167
+ );
168
+ const everywhere = options.planetSet ? sets.add(
169
+ ScatterSet.deserialize(options.planetSet),
170
+ "planet.scatter"
171
+ ) : null;
172
+ w.set(p.planet, Planet, { biomes: set, scatter: everywhere });
173
+ return { ...p, sets: [grassland, forest, rocks] };
174
+ }
175
+ async function settleScatter(p, maxFrames = 600) {
176
+ let quiet = 0;
177
+ for (let f = 0; f < maxFrames; f++) {
178
+ p.app.update(1 / 60);
179
+ await new Promise((r) => setTimeout(r, 1));
180
+ const state = p.world.resource(Scatter);
181
+ let busy = state.surfaces.size === 0;
182
+ for (const ss of state.surfaces.values()) {
183
+ if (ss.problem) throw ss.problem;
184
+ if (!ss.ready) busy = true;
185
+ for (const c of ss.chunks.values()) {
186
+ if (c.job) busy = true;
187
+ const spawning = c.root < 0 || c.placements !== void 0 && c.next < c.placements.count;
188
+ if (c.wanted === state.frame && spawning) busy = true;
189
+ }
190
+ for (const fr of ss.foliage.values())
191
+ for (const c of fr.chunks.values()) if (c.job || c.slot < 0) busy = true;
192
+ }
193
+ quiet = busy ? 0 : quiet + 1;
194
+ if (quiet >= 3) return f;
195
+ }
196
+ return maxFrames;
197
+ }
198
+ function placementChecksum(world) {
199
+ const rows = [];
200
+ const f = new Float32Array(10);
201
+ const u = new Uint32Array(f.buffer);
202
+ for (const table of world.query({ with: [Prop, Transform] }).tables) {
203
+ for (let row = 0; row < table.count; row++) {
204
+ const e = table.entities[row];
205
+ const p = propIdentity(world, e);
206
+ const t = world.get(e, Transform);
207
+ f.set(t.translation, 0);
208
+ f.set(t.rotation, 3);
209
+ f.set(t.scale, 7);
210
+ rows.push(`${p.rule}|${p.chunk}|${p.index}|${Array.from(u).join(",")}`);
211
+ }
212
+ }
213
+ rows.sort();
214
+ let h = 2166136261;
215
+ for (const r of rows) {
216
+ for (let i = 0; i < r.length; i++) h = Math.imul(h ^ r.charCodeAt(i), 16777619);
217
+ h = Math.imul(h ^ 10, 16777619);
218
+ }
219
+ return { checksum: (h >>> 0).toString(16).padStart(8, "0"), props: rows.length };
220
+ }
221
+ export {
222
+ FOREST_SET,
223
+ GRASSLAND_SET,
224
+ ROCK_SET,
225
+ TEST_CLIMATE,
226
+ TEST_HEIGHT,
227
+ TEST_RADIUS,
228
+ placementChecksum,
229
+ scatterPlanet,
230
+ settleScatter
231
+ };
@@ -0,0 +1,9 @@
1
+ import type { Query, World } from '@aethervtt/shard-core';
2
+ /** The query `cameraPositions` reads: every 3D camera with a world transform. */
3
+ export declare function cameraQuery(world: World): Query;
4
+ /**
5
+ * Calls `visit` with each active camera's world position (origin-relative, as GlobalTransform
6
+ * holds it) and its index; returns how many. Headless runs have cameras too (scenes spawn them),
7
+ * so scatter ranges work the same with or without a GPU.
8
+ */
9
+ export declare function cameraPositions(_world: World, cameras: Query, visit: (x: number, y: number, z: number, index: number) => void): number;
@@ -0,0 +1,22 @@
1
+ import { Camera3d } from "@aethervtt/shard-render";
2
+ import { GlobalTransform } from "@aethervtt/shard-transform";
3
+ function cameraQuery(world) {
4
+ return world.query({ with: [Camera3d, GlobalTransform] });
5
+ }
6
+ function cameraPositions(_world, cameras, visit) {
7
+ let n = 0;
8
+ for (const table of cameras.tables) {
9
+ const active = table.column(Camera3d, "active");
10
+ const m = table.column(GlobalTransform, "matrix");
11
+ for (let row = 0; row < table.count; row++) {
12
+ if (!active[row]) continue;
13
+ visit(m[row * 12 + 3], m[row * 12 + 7], m[row * 12 + 11], n);
14
+ n++;
15
+ }
16
+ }
17
+ return n;
18
+ }
19
+ export {
20
+ cameraPositions,
21
+ cameraQuery
22
+ };
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-scatter",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./testing": {
13
+ "types": "./dist/testing.d.ts",
14
+ "default": "./dist/testing.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "@aethervtt/shard-assets": "0.2.0",
24
+ "@aethervtt/shard-core": "0.2.0",
25
+ "@aethervtt/shard-mesh": "0.2.0",
26
+ "@aethervtt/shard-noise": "0.2.0",
27
+ "@aethervtt/shard-physics": "0.2.0",
28
+ "@aethervtt/shard-platform": "0.2.0",
29
+ "@aethervtt/shard-procgen": "0.2.0",
30
+ "@aethervtt/shard-protocol": "0.2.0",
31
+ "@aethervtt/shard-render": "0.2.0",
32
+ "@aethervtt/shard-runtime": "0.2.0",
33
+ "@aethervtt/shard-scene": "0.2.0",
34
+ "@aethervtt/shard-shader": "0.2.0",
35
+ "@aethervtt/shard-terrain": "0.2.0",
36
+ "@aethervtt/shard-texture": "0.2.0",
37
+ "@aethervtt/shard-transform": "0.2.0"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/aethervtt/shard.git",
42
+ "directory": "packages/scatter"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ },
48
+ "shard": {
49
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
50
+ }
51
+ }