@aethervtt/shard-terrain 0.0.1 → 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.
Files changed (76) hide show
  1. package/dist/biomes.d.ts +1 -0
  2. package/dist/biomes.js +3 -0
  3. package/dist/chunk.d.ts +0 -44
  4. package/dist/chunk.js +2 -127
  5. package/dist/colliders.d.ts +16 -1
  6. package/dist/colliders.js +39 -24
  7. package/dist/components.d.ts +5 -0
  8. package/dist/components.js +7 -2
  9. package/dist/cube-sphere.d.ts +50 -0
  10. package/dist/cube-sphere.js +224 -0
  11. package/dist/grid-mesh.d.ts +49 -0
  12. package/dist/grid-mesh.js +208 -0
  13. package/dist/heightfield/bake.d.ts +120 -0
  14. package/dist/heightfield/bake.js +494 -0
  15. package/dist/heightfield/colliders.d.ts +39 -0
  16. package/dist/heightfield/colliders.js +152 -0
  17. package/dist/heightfield/component.d.ts +14 -0
  18. package/dist/heightfield/component.js +59 -0
  19. package/dist/heightfield/deflate.d.ts +11 -0
  20. package/dist/heightfield/deflate.js +484 -0
  21. package/dist/heightfield/heightmap.d.ts +27 -0
  22. package/dist/heightfield/heightmap.js +160 -0
  23. package/dist/heightfield/kernel.d.ts +277 -0
  24. package/dist/heightfield/kernel.js +1033 -0
  25. package/dist/heightfield/material.d.ts +13 -0
  26. package/dist/heightfield/material.js +394 -0
  27. package/dist/heightfield/methods.d.ts +104 -0
  28. package/dist/heightfield/methods.js +218 -0
  29. package/dist/heightfield/pack.d.ts +59 -0
  30. package/dist/heightfield/pack.js +127 -0
  31. package/dist/heightfield/pages.d.ts +91 -0
  32. package/dist/heightfield/pages.js +244 -0
  33. package/dist/heightfield/project.d.ts +40 -0
  34. package/dist/heightfield/project.js +55 -0
  35. package/dist/heightfield/queries.d.ts +48 -0
  36. package/dist/heightfield/queries.js +150 -0
  37. package/dist/heightfield/render.d.ts +108 -0
  38. package/dist/heightfield/render.js +727 -0
  39. package/dist/heightfield/root-grid.d.ts +38 -0
  40. package/dist/heightfield/root-grid.js +109 -0
  41. package/dist/heightfield/runtime.d.ts +105 -0
  42. package/dist/heightfield/runtime.js +312 -0
  43. package/dist/heightfield/source-asset.d.ts +39 -0
  44. package/dist/heightfield/source-asset.js +372 -0
  45. package/dist/heightfield/source.d.ts +151 -0
  46. package/dist/heightfield/source.js +402 -0
  47. package/dist/heightfield/stack.d.ts +29 -0
  48. package/dist/heightfield/stack.js +158 -0
  49. package/dist/heightfield/system.d.ts +22 -0
  50. package/dist/heightfield/system.js +165 -0
  51. package/dist/heightfield/testing.d.ts +317 -0
  52. package/dist/heightfield/testing.js +441 -0
  53. package/dist/heightfield/worker.js +47 -0
  54. package/dist/heights.d.ts +8 -3
  55. package/dist/heights.js +8 -4
  56. package/dist/index.d.ts +24 -7
  57. package/dist/index.js +158 -8
  58. package/dist/lod.d.ts +27 -0
  59. package/dist/lod.js +23 -1
  60. package/dist/material.js +2 -1
  61. package/dist/methods.js +64 -16
  62. package/dist/overlays.d.ts +2 -0
  63. package/dist/overlays.js +48 -6
  64. package/dist/planet.d.ts +2 -7
  65. package/dist/planet.js +8 -18
  66. package/dist/plugin.d.ts +10 -3
  67. package/dist/plugin.js +48 -8
  68. package/dist/quadtree.d.ts +61 -39
  69. package/dist/quadtree.js +44 -208
  70. package/dist/render.d.ts +7 -1
  71. package/dist/render.js +18 -8
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +41 -5
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +43 -3
  76. package/package.json +16 -16
package/dist/biomes.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare const Biome: import("@aethervtt/shard-assets").DataType<{
15
15
  readonly slope: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
16
16
  readonly blend: import("@aethervtt/shard-core").FieldType<number, "f32">;
17
17
  readonly tint: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
18
+ readonly scatter: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"scatter/ScatterSet"> | null, "object">;
18
19
  }, "terrain/Biome">;
19
20
  export declare const BiomeSet: import("@aethervtt/shard-assets").DataType<{
20
21
  readonly biomes: import("@aethervtt/shard-core").FieldType<(import("@aethervtt/shard-core").AssetRef<"terrain/Biome"> | null)[], "object">;
package/dist/biomes.js CHANGED
@@ -38,6 +38,9 @@ const Biome = defineDataType(
38
38
  tint: t.color({
39
39
  default: [1, 1, 1, 1],
40
40
  description: "Multiplies the albedo (the whole color when the set has no textures)."
41
+ }),
42
+ scatter: t.handle("scatter/ScatterSet", {
43
+ description: "Props and foliage where this biome is the heaviest (*.scatter.json; needs the scatter plugin)."
41
44
  })
42
45
  },
43
46
  {
package/dist/chunk.d.ts CHANGED
@@ -1,50 +1,6 @@
1
1
  import type { MeshData } from '@aethervtt/shard-mesh';
2
2
  import type { NoiseGraph } from '@aethervtt/shard-noise';
3
3
  import { type ChunkPoints } from './points.js';
4
- /**
5
- * How a chunk's vertices are laid out, per resolution `n` (vertices per edge, 2^k + 1). The border
6
- * ring comes first (so edge locks rewrite one contiguous block), then the interior row by row, then
7
- * one skirt vertex under each ring vertex. Every chunk of a resolution shares one index buffer.
8
- */
9
- export interface ChunkLayout {
10
- resolution: number;
11
- /** Ring vertices: 4 (n − 1), counter-clockwise from (0, 0) seen from outside. */
12
- ring: number;
13
- /** Surface vertices: n². */
14
- surface: number;
15
- /** Surface plus skirt: n² + 4 (n − 1). */
16
- vertexCount: number;
17
- /** Vertex index of grid point (i, j), at `i + j * n`. */
18
- index: Uint16Array;
19
- /** Grid point of each ring vertex: i, j pairs. */
20
- ringPoints: Uint16Array;
21
- /** Triangles: the surface, then the skirts (`chunkIndices` with every quadrant, no stitching). */
22
- indices: Uint16Array;
23
- /** Triangles without skirts (the surface only): colliders, and planets with skirts off. */
24
- surfaceIndices: Uint16Array;
25
- }
26
- /**
27
- * A chunk's triangles (cached per combination, so chunks share the array and its GPU index buffer).
28
- * `mask` picks quadrants (bit q: quadrant q, in child order (x, y), (x+1, y), (x, y+1),
29
- * (x+1, y+1)): a split waiting for children draws its own quadrants where they're missing and the
30
- * children elsewhere. `stitch` bit e marks edge e (0 bottom, 1 right, 2 top, 3 left) as meeting a
31
- * coarser neighbor: that edge's triangles use only its even vertices, the neighbor's, so there's no
32
- * T-junction (a vertex on the neighbor's edge only up to rounding shows pixel-sized holes).
33
- *
34
- * Quads fill the interior; each edge's strip (between the border and the first inner row) is
35
- * zipped per half, so no triangle crosses a quadrant boundary. Skirts follow the drawn border.
36
- */
37
- export declare function chunkIndices(n: number, mask: number, stitch: number, skirts: boolean): Uint16Array;
38
- /** Throws `terrain/bad-resolution` unless `n` is 2^k + 1 (5 to 129). */
39
- export declare function checkResolution(n: number): void;
40
- export declare function chunkLayout(n: number): ChunkLayout;
41
- /**
42
- * A vertex's lock code (uv1.x): its edge for ring vertices (0 bottom, 1 right, 2 top, 3 left, where
43
- * the instance's lock bits apply), the quadrant boundary for vertices on the center lines (4 and 5
44
- * the vertical line's lower and upper half, 6 and 7 the horizontal line's left and right half, 8
45
- * the center; locked to this level where a partial draw meets a child), else −1.
46
- */
47
- export declare function lockCode(i: number, j: number, n: number, vi: number, ring: number): number;
48
4
  /** What a chunk is made from: its node, the planet's numbers, and its graphs. */
49
5
  export interface ChunkSpec {
50
6
  face: number;
package/dist/chunk.js CHANGED
@@ -1,131 +1,10 @@
1
- import { ShardError } from "@aethervtt/shard-core";
2
1
  import { faceToDirection, nodeExtent } from "./cube.js";
2
+ import { chunkLayout } from "./grid-mesh.js";
3
3
  import {
4
4
  createChunkPoints,
5
5
  prepareChunkPoints,
6
6
  sampleChunkPoints
7
7
  } from "./points.js";
8
- const layouts = /* @__PURE__ */ new Map();
9
- const sets = /* @__PURE__ */ new Map();
10
- function chunkIndices(n, mask, stitch, skirts) {
11
- const key = `${n}:${mask}:${stitch}:${skirts}`;
12
- let out = sets.get(key);
13
- if (out) return out;
14
- const layout = chunkLayout(n);
15
- const index = layout.index;
16
- const side = n - 1;
17
- const h = side / 2;
18
- const parts = [];
19
- const quadrant = (i, j) => (i >= h ? 1 : 0) + (j >= h ? 2 : 0);
20
- for (let j = 1; j < side - 1; j++) {
21
- for (let i = 1; i < side - 1; i++) {
22
- if (!(mask & 1 << quadrant(i, j))) continue;
23
- const a = index[i + j * n];
24
- const b = index[i + 1 + j * n];
25
- const c = index[i + 1 + (j + 1) * n];
26
- const d = index[i + (j + 1) * n];
27
- parts.push(a, b, c, a, c, d);
28
- }
29
- }
30
- const at = (e, s, depth) => {
31
- const i = e === 0 ? s : e === 1 ? side - depth : e === 2 ? side - s : depth;
32
- const j = e === 0 ? depth : e === 1 ? s : e === 2 ? side - depth : side - s;
33
- return index[i + j * n];
34
- };
35
- const skirt = [];
36
- const outer = [];
37
- const inner = [];
38
- for (let e = 0; e < 4; e++) {
39
- const step = stitch & 1 << e ? 2 : 1;
40
- for (let half = 0; half < 2; half++) {
41
- const s0 = half * h;
42
- const s1 = s0 + h;
43
- const m = s0 + h / 2;
44
- const q = e === 0 ? quadrant(m, 0) : e === 1 ? quadrant(side, m) : e === 2 ? quadrant(side - m, side) : quadrant(0, side - m);
45
- if (!(mask & 1 << q)) continue;
46
- outer.length = 0;
47
- inner.length = 0;
48
- for (let s = s0; s <= s1; s += step) outer.push(s);
49
- for (let s = Math.max(1, s0); s <= Math.min(side - 1, s1); s++) inner.push(s);
50
- let a = 0;
51
- let b = 0;
52
- while (a < outer.length - 1 || b < inner.length - 1) {
53
- const advanceOuter = b === inner.length - 1 || a < outer.length - 1 && outer[a + 1] <= inner[b + 1];
54
- if (advanceOuter) {
55
- parts.push(at(e, outer[a], 0), at(e, outer[a + 1], 0), at(e, inner[b], 1));
56
- a++;
57
- } else {
58
- parts.push(at(e, outer[a], 0), at(e, inner[b + 1], 1), at(e, inner[b], 1));
59
- b++;
60
- }
61
- }
62
- if (!skirts) continue;
63
- for (let k = 0; k + 1 < outer.length; k++) {
64
- const va = at(e, outer[k], 0);
65
- const vb = at(e, outer[k + 1], 0);
66
- const sa = layout.surface + va;
67
- const sb = layout.surface + vb;
68
- skirt.push(va, sa, vb, vb, sa, sb, va, vb, sa, vb, sb, sa);
69
- }
70
- }
71
- }
72
- for (const v of skirt) parts.push(v);
73
- out = Uint16Array.from(parts);
74
- sets.set(key, out);
75
- return out;
76
- }
77
- function checkResolution(n) {
78
- const k = Math.log2(n - 1);
79
- if (!Number.isInteger(k) || k < 2 || k > 7) {
80
- throw new ShardError("terrain/bad-resolution", `Planet resolution ${n} isn't 2^n + 1`, {
81
- hint: "Use 17, 33 (the default), 65, or 129 vertices per chunk edge."
82
- });
83
- }
84
- }
85
- function chunkLayout(n) {
86
- let layout = layouts.get(n);
87
- if (layout) return layout;
88
- checkResolution(n);
89
- const ring = 4 * (n - 1);
90
- const surface = n * n;
91
- const index = new Uint16Array(surface);
92
- const ringPoints = new Uint16Array(ring * 2);
93
- let r = 0;
94
- const put = (i, j) => {
95
- index[i + j * n] = r;
96
- ringPoints[r * 2] = i;
97
- ringPoints[r * 2 + 1] = j;
98
- r++;
99
- };
100
- for (let i = 0; i < n - 1; i++) put(i, 0);
101
- for (let j = 0; j < n - 1; j++) put(n - 1, j);
102
- for (let i = n - 1; i > 0; i--) put(i, n - 1);
103
- for (let j = n - 1; j > 0; j--) put(0, j);
104
- let v = ring;
105
- for (let j = 1; j < n - 1; j++) for (let i = 1; i < n - 1; i++) index[i + j * n] = v++;
106
- layout = {
107
- resolution: n,
108
- ring,
109
- surface,
110
- vertexCount: surface + ring,
111
- index,
112
- ringPoints,
113
- indices: new Uint16Array(0),
114
- surfaceIndices: new Uint16Array(0)
115
- };
116
- layouts.set(n, layout);
117
- layout.indices = chunkIndices(n, 15, 0, true);
118
- layout.surfaceIndices = chunkIndices(n, 15, 0, false);
119
- return layout;
120
- }
121
- function lockCode(i, j, n, vi, ring) {
122
- if (vi < ring) return Math.floor(vi / (n - 1));
123
- const h = (n - 1) / 2;
124
- if (i === h && j === h) return 8;
125
- if (i === h) return j < h ? 4 : 5;
126
- if (j === h) return i < h ? 6 : 7;
127
- return -1;
128
- }
129
8
  let values = new Float32Array(0);
130
9
  let temps = new Float32Array(0);
131
10
  let moist = new Float32Array(0);
@@ -296,9 +175,5 @@ function assembleChunk(spec, pts, values2, temps2, moist2) {
296
175
  export {
297
176
  assembleChunk,
298
177
  buildChunk,
299
- checkResolution,
300
- chunkCenter,
301
- chunkIndices,
302
- chunkLayout,
303
- lockCode
178
+ chunkCenter
304
179
  };
@@ -1,7 +1,7 @@
1
1
  import { type Entity, type Query, type World } from '@aethervtt/shard-core';
2
2
  import { Mesh } from '@aethervtt/shard-mesh';
3
3
  import type { Workers } from '@aethervtt/shard-platform';
4
- import { type ChunkMesh } from './chunk.js';
4
+ import { type ChunkMesh, type ChunkSpec } from './chunk.js';
5
5
  import type { PlanetRuntime } from './planet.js';
6
6
  import { type ChunkPoints } from './points.js';
7
7
  /** Frames between asking for a collider chunk and it appearing. Always this many, pool or not. */
@@ -71,6 +71,7 @@ export declare function anchorQueries(world: World): AnchorQueries;
71
71
  export declare function gatherAnchors(world: World, planet: PlanetRuntime, q: AnchorQueries): void;
72
72
  /** The collider nodes within reach of the planet's anchors, as sorted keys. */
73
73
  export declare function wantedColliders(planet: PlanetRuntime): string[];
74
+ export declare function chunkSpec(planet: PlanetRuntime, face: number, depth: number, x: number, y: number): ChunkSpec;
74
75
  /** How far skirts hang: past any morph mismatch between neighbors at this depth (true errors). */
75
76
  export declare function skirtDepth(planet: PlanetRuntime, depth: number): number;
76
77
  /**
@@ -81,6 +82,20 @@ export declare function skirtDepth(planet: PlanetRuntime, depth: number): number
81
82
  * but stay cached (least recently used first out).
82
83
  */
83
84
  export declare function updateColliders(world: World, planet: PlanetRuntime, workers: Workers | undefined, frame: number, cacheSize: number): void;
85
+ /** A chunk's sample points with their graphs sampled on the pool (`done` resolves when all are in). */
86
+ export interface ChunkSampling {
87
+ pts: ChunkPoints;
88
+ values: Float32Array;
89
+ temps: Float32Array;
90
+ moist: Float32Array;
91
+ done: Promise<void>;
92
+ }
93
+ /**
94
+ * Samples a chunk's height and climate on the worker pool: `assembleChunk(chunkSpec(…), pts, values,
95
+ * temps, moist)` then gives the same mesh `buildChunk` does inline. Colliders and scatter's foliage
96
+ * patches (0045) use it.
97
+ */
98
+ export declare function sampleChunkAsync(planet: PlanetRuntime, workers: Workers, face: number, depth: number, x: number, y: number): ChunkSampling;
84
99
  /** Drops every collider chunk (planet despawned). */
85
100
  export declare function clearColliders(world: World, planet: PlanetRuntime): void;
86
101
  export {};
package/dist/colliders.js CHANGED
@@ -10,9 +10,10 @@ import {
10
10
  propagateSubtree,
11
11
  Transform
12
12
  } from "@aethervtt/shard-transform";
13
- import { assembleChunk, buildChunk, chunkLayout } from "./chunk.js";
13
+ import { assembleChunk, buildChunk } from "./chunk.js";
14
14
  import { Chunk, PlanetNav, TerrainAnchor } from "./components.js";
15
15
  import { faceToDirection, keyString, nodeAt, nodeExtent } from "./cube.js";
16
+ import { chunkLayout } from "./grid-mesh.js";
16
17
  import { createChunkPoints, prepareChunkPoints } from "./points.js";
17
18
  const COLLIDER_DELAY = 2;
18
19
  class ColliderSet {
@@ -148,7 +149,7 @@ function wantedColliders(planet) {
148
149
  }
149
150
  return [...wantedKeys].sort();
150
151
  }
151
- function specFor(planet, face, depth, x, y) {
152
+ function chunkSpec(planet, face, depth, x, y) {
152
153
  const s = planet.settings;
153
154
  return {
154
155
  face,
@@ -195,7 +196,7 @@ function updateColliders(world, planet, workers, frame, cacheSize) {
195
196
  }
196
197
  if (frame < job.due) continue;
197
198
  set.jobs.delete(key);
198
- const spec = specFor(planet, job.face, job.depth, job.x, job.y);
199
+ const spec = chunkSpec(planet, job.face, job.depth, job.x, job.y);
199
200
  let mesh;
200
201
  if (job.ready && job.pts && job.values && job.temps && job.moist) {
201
202
  mesh = assembleChunk(spec, job.pts, job.values, job.temps, job.moist);
@@ -244,13 +245,31 @@ function startJob(planet, workers, key, face, depth, x, y, due) {
244
245
  cancelled: false
245
246
  };
246
247
  if (!workers || workers.size === 0) return job;
248
+ const sampling = sampleChunkAsync(planet, workers, face, depth, x, y);
249
+ job.pts = sampling.pts;
250
+ job.values = sampling.values;
251
+ job.temps = sampling.temps;
252
+ job.moist = sampling.moist;
253
+ sampling.done.then(
254
+ () => {
255
+ if (!job.cancelled) job.ready = true;
256
+ },
257
+ () => {
258
+ }
259
+ );
260
+ return job;
261
+ }
262
+ function sampleChunkAsync(planet, workers, face, depth, x, y) {
247
263
  const s = planet.settings;
248
264
  const pts = prepareChunkPoints(face, depth, x, y, s.resolution, s.radius, createChunkPoints());
249
- job.pts = pts;
250
265
  const count = pts.count;
251
- job.values = new Float32Array(count);
252
- job.temps = new Float32Array(count);
253
- job.moist = new Float32Array(count);
266
+ const out = {
267
+ pts,
268
+ values: new Float32Array(count),
269
+ temps: new Float32Array(count),
270
+ moist: new Float32Array(count),
271
+ done: Promise.resolve()
272
+ };
254
273
  const parts = [];
255
274
  const origin = new Float64Array(4);
256
275
  for (let g = 0; g < pts.groups; g++) {
@@ -267,31 +286,25 @@ function startJob(planet, workers, key, face, depth, x, y, due) {
267
286
  origin[1] = pts.origins[g * 4 + 1];
268
287
  origin[2] = pts.origins[g * 4 + 2];
269
288
  const o = Float64Array.from(origin);
270
- const scatter = (target) => (values) => {
271
- for (let i = a; i < b; i++) target[pts.order[i]] = values[i - a];
272
- };
273
289
  const run = (graph, target, node2) => {
274
290
  if (!graph) return;
275
- const out = new Float32Array(b - a);
291
+ const values = new Float32Array(b - a);
276
292
  parts.push(
277
- sampleNoiseAsync(workers, graph, s.seed, local, out, {
293
+ sampleNoiseAsync(workers, graph, s.seed, local, values, {
278
294
  origin: o,
279
295
  ...node2 ? { node: node2 } : {}
280
- }).then(() => scatter(target)(out))
296
+ }).then(() => {
297
+ for (let i = a; i < b; i++) target[pts.order[i]] = values[i - a];
298
+ })
281
299
  );
282
300
  };
283
- run(planet.height, job.values);
284
- run(planet.climate, job.temps, "temperature");
285
- run(planet.climate, job.moist, "moisture");
301
+ run(planet.height, out.values);
302
+ run(planet.climate, out.temps, "temperature");
303
+ run(planet.climate, out.moist, "moisture");
286
304
  }
287
- Promise.all(parts).then(
288
- () => {
289
- if (!job.cancelled) job.ready = true;
290
- },
291
- () => {
292
- }
293
- );
294
- return job;
305
+ out.done = Promise.all(parts).then(() => {
306
+ });
307
+ return out;
295
308
  }
296
309
  function makeChunk(world, key, job, mesh, version, frame) {
297
310
  const layout = chunkLayout(Math.round(Math.sqrt(mesh.heights.length)));
@@ -347,9 +360,11 @@ export {
347
360
  COLLIDER_DELAY,
348
361
  ColliderSet,
349
362
  anchorQueries,
363
+ chunkSpec,
350
364
  clearColliders,
351
365
  collidersOf,
352
366
  gatherAnchors,
367
+ sampleChunkAsync,
353
368
  skirtDepth,
354
369
  updateColliders,
355
370
  wantedColliders
@@ -14,6 +14,7 @@ export declare const Planet: import("@aethervtt/shard-core").ComponentDef<{
14
14
  readonly vertexPixels: import("@aethervtt/shard-core").FieldType<number, "f32">;
15
15
  readonly colliderRadius: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
16
  readonly skirts: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
17
+ readonly scatter: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"scatter/ScatterSet"> | null, "object">;
17
18
  }>;
18
19
  export declare const TerrainAnchor: import("@aethervtt/shard-core").ComponentDef<{
19
20
  readonly enabled: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
@@ -45,5 +46,9 @@ export interface TerrainBudgetValue {
45
46
  pool: number;
46
47
  /** Collider chunks cached per planet. */
47
48
  colliderCache: number;
49
+ /** Heightfields (0071): GPU page pool slots per terrain (about 22 KB each). */
50
+ pages: number;
51
+ /** Heightfields: pages uploaded to the pool per frame, at most. */
52
+ pagesPerFrame: number;
48
53
  }
49
54
  export declare const TerrainBudget: import("@aethervtt/shard-core").ResourceDef<TerrainBudgetValue>;
@@ -64,6 +64,9 @@ const Planet = defineComponent(
64
64
  skirts: t.bool({
65
65
  default: true,
66
66
  description: "Walls under chunk edges that hide cracks while neighbors change level."
67
+ }),
68
+ scatter: t.handle("scatter/ScatterSet", {
69
+ description: "Props and foliage everywhere on the planet (*.scatter.json; needs the scatter plugin). Each biome can add its own (Biome.scatter)."
67
70
  })
68
71
  },
69
72
  {
@@ -119,13 +122,15 @@ const Chunk = defineComponent(
119
122
  }
120
123
  );
121
124
  const TerrainBudget = defineResource("terrain/Budget", {
122
- description: "How much terrain work a frame may do: chunk generations, GPU milliseconds, and how many chunks stay cached.",
125
+ description: "How much terrain work a frame may do: chunk generations, GPU milliseconds, page uploads, and how many chunks and pages stay cached.",
123
126
  init: () => ({
124
127
  chunksPerFrame: 8,
125
128
  triangles: 2e6,
126
129
  msPerFrame: 1.5,
127
130
  pool: 2048,
128
- colliderCache: 256
131
+ colliderCache: 256,
132
+ pages: 1024,
133
+ pagesPerFrame: 16
129
134
  })
130
135
  });
131
136
  export {
@@ -0,0 +1,50 @@
1
+ import { type QuadSurface, QuadTree, type SelectionParams, type SelectionView } from './quadtree.js';
2
+ /**
3
+ * The cube-sphere as a quadtree surface (spec 0043): six roots, one per cube face, projected onto
4
+ * an ellipsoid of `radius` and `shape`. Each node keeps 9 test directions (corners, edge middles,
5
+ * center) for its bounds and the horizon test, and how far to lift them over the surface's bulge.
6
+ */
7
+ export declare class CubeSphere implements QuadSurface {
8
+ readonly roots = 6;
9
+ radius: number;
10
+ readonly shape: Float64Array<ArrayBuffer>;
11
+ /** Horizon test directions (unit, 9 per node). */
12
+ dirs: Float64Array<ArrayBuffer>;
13
+ /**
14
+ * 1 / cos(half the widest angle between neighboring test points, diagonals included): lifting
15
+ * the points by it puts the surface between them inside their hull (horizon test, bulge).
16
+ */
17
+ lift: Float64Array<ArrayBuffer>;
18
+ grow(capacity: number): void;
19
+ initNode(tree: QuadTree, n: number): void;
20
+ /** Bounding sphere of the node's 9 test points at its lowest and highest heights, plus bulge. */
21
+ updateSphere(tree: QuadTree, n: number): void;
22
+ /**
23
+ * Axis-aligned bounds of the node relative to `center`, from its test points at its lowest and
24
+ * highest heights plus the bulge between them.
25
+ */
26
+ bounds(tree: QuadTree, n: number, center: ArrayLike<number>, out: {
27
+ [i: number]: number;
28
+ }): void;
29
+ culled(tree: QuadTree, n: number, view: SelectionView, params: SelectionParams): boolean;
30
+ /** Across a cube edge the neighbor is on another face: there always is one. */
31
+ neighbor(root: number, depth: number, x: number, y: number, edge: number, out: {
32
+ [i: number]: number;
33
+ }): boolean;
34
+ }
35
+ /**
36
+ * A planet surface's quadtree (terrain or ocean): a `QuadTree` on a `CubeSphere`. The six roots
37
+ * are nodes 0–5, one per face.
38
+ */
39
+ export declare class NodeTree extends QuadTree {
40
+ readonly cube: CubeSphere;
41
+ constructor(capacity?: number);
42
+ /** Each node's cube face (its root). */
43
+ get face(): Uint8Array;
44
+ get radius(): number;
45
+ get shape(): Float64Array;
46
+ /** Clears every node and makes the six roots (after a planet's shape or graphs change). */
47
+ reset(radius: number, shape: ArrayLike<number>, lowest: number, highest: number): void;
48
+ /** Root of a face (nodes 0–5). */
49
+ rootOf(face: number): number;
50
+ }