@aethervtt/shard-terrain 0.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/biomes.d.ts +65 -0
  4. package/dist/biomes.js +163 -0
  5. package/dist/chunk.d.ts +99 -0
  6. package/dist/chunk.js +304 -0
  7. package/dist/colliders.d.ts +86 -0
  8. package/dist/colliders.js +356 -0
  9. package/dist/components.d.ts +49 -0
  10. package/dist/components.js +137 -0
  11. package/dist/cube.d.ts +51 -0
  12. package/dist/cube.js +87 -0
  13. package/dist/frame.d.ts +21 -0
  14. package/dist/frame.js +63 -0
  15. package/dist/heights.d.ts +51 -0
  16. package/dist/heights.js +137 -0
  17. package/dist/index.d.ts +19 -0
  18. package/dist/index.js +193 -0
  19. package/dist/kernel.d.ts +47 -0
  20. package/dist/kernel.js +243 -0
  21. package/dist/lod.d.ts +22 -0
  22. package/dist/lod.js +44 -0
  23. package/dist/material.d.ts +14 -0
  24. package/dist/material.js +426 -0
  25. package/dist/methods.d.ts +12 -0
  26. package/dist/methods.js +259 -0
  27. package/dist/nav.d.ts +14 -0
  28. package/dist/nav.js +114 -0
  29. package/dist/overlays.d.ts +6 -0
  30. package/dist/overlays.js +67 -0
  31. package/dist/planet.d.ts +106 -0
  32. package/dist/planet.js +313 -0
  33. package/dist/plugin.d.ts +23 -0
  34. package/dist/plugin.js +98 -0
  35. package/dist/points.d.ts +62 -0
  36. package/dist/points.js +207 -0
  37. package/dist/quadtree.d.ts +153 -0
  38. package/dist/quadtree.js +587 -0
  39. package/dist/render.d.ts +179 -0
  40. package/dist/render.js +1222 -0
  41. package/dist/test-planet.d.ts +59 -0
  42. package/dist/test-planet.js +189 -0
  43. package/dist/testing.d.ts +42 -0
  44. package/dist/testing.js +96 -0
  45. package/dist/view.d.ts +12 -0
  46. package/dist/view.js +61 -0
  47. package/package.json +50 -0
package/dist/plugin.js ADDED
@@ -0,0 +1,98 @@
1
+ import { AssetServerResource, assetServer } from "@aethervtt/shard-assets";
2
+ import { defineSystem, onRemove, PostUpdate } from "@aethervtt/shard-core";
3
+ import { computeVisibility } from "@aethervtt/shard-render";
4
+ import { definePlugin } from "@aethervtt/shard-runtime";
5
+ import { TransformSystems } from "@aethervtt/shard-transform";
6
+ import * as biomesModule from "./biomes.js";
7
+ import { anchorQueries, clearColliders, gatherAnchors, updateColliders } from "./colliders.js";
8
+ import * as componentsModule from "./components.js";
9
+ import { Planet, TerrainBudget } from "./components.js";
10
+ import * as heightsModule from "./heights.js";
11
+ import { Terrain } from "./heights.js";
12
+ import * as materialModule from "./material.js";
13
+ import { terrainMethods } from "./methods.js";
14
+ import { clearPlanetNav, updatePlanetNav } from "./nav.js";
15
+ import * as overlaysModule from "./overlays.js";
16
+ import { PlanetRuntime } from "./planet.js";
17
+ import { cleanupRender, registerNode, selectChunks } from "./render.js";
18
+ const watching = /* @__PURE__ */ new WeakSet();
19
+ function watchAssets(world) {
20
+ if (watching.has(world) || !world.tryResource(AssetServerResource)) return;
21
+ watching.add(world);
22
+ assetServer(world).onEvent((event) => {
23
+ if (event.kind === "loaded" || event.kind === "modified" || event.kind === "failed")
24
+ world.resource(Terrain).dirty = true;
25
+ });
26
+ }
27
+ const updatePlanets = defineSystem({
28
+ name: "terrain/planets",
29
+ description: "Resolves each Planet\u2019s graphs and biomes, measures its LOD errors, and keeps collider chunks (fixed trimesh bodies) around characters, dynamic bodies, and TerrainAnchors.",
30
+ setup: (world) => ({
31
+ planets: world.query({ with: [Planet] }),
32
+ anchors: anchorQueries(world)
33
+ }),
34
+ run: (s, world) => {
35
+ const state = world.resource(Terrain);
36
+ watchAssets(world);
37
+ state.frame++;
38
+ const frame = state.frame;
39
+ for (const table of s.planets.tables) {
40
+ for (let row = 0; row < table.count; row++) {
41
+ const entity = table.entities[row];
42
+ if (!state.planets.has(entity)) state.planets.set(entity, new PlanetRuntime(entity));
43
+ }
44
+ }
45
+ const budget = world.resource(TerrainBudget);
46
+ const dirty = state.dirty;
47
+ state.dirty = false;
48
+ for (const rt of state.planets.values()) {
49
+ if (!world.isAlive(rt.entity)) continue;
50
+ rt.refresh(world, dirty);
51
+ rt.frame.update(world, rt.entity);
52
+ if (!rt.ready) continue;
53
+ gatherAnchors(world, rt, s.anchors);
54
+ updateColliders(world, rt, state.workers, frame, budget.colliderCache);
55
+ updatePlanetNav(world, rt, s.anchors.agents);
56
+ }
57
+ }
58
+ });
59
+ function terrainPlugin(options = {}) {
60
+ return definePlugin({
61
+ name: "terrain",
62
+ provides: [biomesModule, componentsModule, heightsModule, materialModule, overlaysModule],
63
+ dependencies: ["core/transform"],
64
+ build(app) {
65
+ const world = app.world;
66
+ const state = world.initResource(Terrain);
67
+ state.workers = options.workers;
68
+ world.initResource(TerrainBudget);
69
+ world.observe(onRemove(Planet), ({ entity, world: world2 }) => {
70
+ const rt = world2.resource(Terrain).planets.get(entity);
71
+ if (!rt) return;
72
+ clearColliders(world2, rt);
73
+ clearPlanetNav(world2, rt);
74
+ cleanupRender(world2, rt);
75
+ for (const cleanup of terrainCleanups) cleanup(world2, rt);
76
+ world2.resource(Terrain).planets.delete(entity);
77
+ });
78
+ app.addSystems(PostUpdate, updatePlanets.after(TransformSystems));
79
+ app.addMethod(...terrainMethods);
80
+ app.addSystems(PostUpdate, selectChunks.after(updatePlanets).before(computeVisibility));
81
+ for (const extend of terrainExtensions) extend(app);
82
+ },
83
+ async ready(app) {
84
+ registerNode(app);
85
+ for (const hook of terrainReadyHooks) await hook(app);
86
+ }
87
+ });
88
+ }
89
+ const terrainExtensions = [];
90
+ const terrainReadyHooks = [];
91
+ const terrainCleanups = [];
92
+ export {
93
+ terrainCleanups,
94
+ terrainExtensions,
95
+ terrainPlugin,
96
+ terrainReadyHooks,
97
+ updatePlanets
98
+ };
@@ -0,0 +1,62 @@
1
+ import { type NoiseGraph } from '@aethervtt/shard-noise';
2
+ /**
3
+ * Noise origins sit on a lattice this many metres apart in the planet frame. A point samples
4
+ * relative to its nearest lattice point, which depends only on where the point is, so every chunk
5
+ * that has a vertex (a neighbor, a parent, a coarser neighbor at a 2:1 edge) computes the same
6
+ * height for it, bit for bit. Offsets stay under ~55 m, which keeps fine octaves' lattice
7
+ * coordinates in the range 0041's CPU/GPU tolerance is measured over.
8
+ */
9
+ export declare const SNAP = 64;
10
+ /**
11
+ * Terrain noise is sampled at `direction × radius + NOISE_OFFSET`. Canonical points on cube edges
12
+ * have exactly equal coordinates (x = z), which puts them exactly on a tie of OpenSimplex2's
13
+ * rotated lattice, where the GPU's fused multiply-adds can pick the other lattice branch than the
14
+ * CPU and differ past 0041's tolerance. An asymmetric offset of tens of metres moves the terrain's
15
+ * points off every such plane; to the terrain it's just a shift of the noise.
16
+ */
17
+ export declare const NOISE_OFFSET: readonly [13.71, 27.13, 41.92];
18
+ /**
19
+ * The unit direction of grid point (gi, gj) of a face at a grid resolution of `size` segments per
20
+ * face edge. Points on a face's boundary belong to two or three faces; they're computed from the
21
+ * lowest-numbered one, so every chunk sharing the point gets the same bits. Points past the boundary
22
+ * (a chunk's outer border ring) extrapolate the face's plane.
23
+ */
24
+ export declare function gridDirection(face: number, gi: number, gj: number, size: number, out: Float64Array, o: number): void;
25
+ /**
26
+ * A chunk's sample points: its (n + 2)² grid (one-vertex border for normals) as canonical unit
27
+ * directions, grouped by noise origin (lattice point), with each point's f32 offset from its
28
+ * origin on the noise sphere (direction × radius).
29
+ */
30
+ export interface ChunkPoints {
31
+ /** Points per side: n + 2. */
32
+ side: number;
33
+ count: number;
34
+ dirs: Float64Array;
35
+ /** Group (origin) of each point. */
36
+ group: Int32Array;
37
+ groups: number;
38
+ /** Origins (f64 xyzw, w = 0), 4 per group. */
39
+ origins: Float64Array;
40
+ /** Offsets from the point's origin (f32 xyz), per point. */
41
+ local: Float32Array;
42
+ /** Points sorted by group: group g is order[start[g] .. start[g + 1]). */
43
+ order: Int32Array;
44
+ start: Int32Array;
45
+ }
46
+ export declare function createChunkPoints(): ChunkPoints;
47
+ /**
48
+ * Fills `out` with a node's sample points, grouped by lattice origins `snap` metres apart. Without
49
+ * `snap`, each point's lattice follows its level (the coarsest depth it's a vertex at, from its
50
+ * grid index): SNAP for points only fine chunks have, up to a quarter of a chunk at that level (at
51
+ * most 4 096 m) for coarser vertices. Every chunk sampling a point picks the same lattice, so shared vertices stay
52
+ * bit-identical across chunks and depths, while a coarse chunk's points fall into a few groups
53
+ * instead of one each (their per-octave origins were most of a job's upload).
54
+ */
55
+ export declare function prepareChunkPoints(face: number, depth: number, x: number, y: number, resolution: number, radius: number, out: ChunkPoints, snap?: number): ChunkPoints;
56
+ /**
57
+ * Samples a graph at every point on the CPU, one kernel batch per origin, into `out` (per point).
58
+ * `node` samples an intermediate node (the climate graph's temperature and moisture).
59
+ */
60
+ export declare function sampleChunkPoints(graph: NoiseGraph, seed: number, pts: ChunkPoints, out: Float32Array, node?: string): void;
61
+ /** One point's height-graph value (a gameplay query): relative to its own lattice origin. */
62
+ export declare function samplePoint(graph: NoiseGraph, seed: number, dx: number, dy: number, dz: number, radius: number, out: Float32Array, node?: string): number;
package/dist/points.js ADDED
@@ -0,0 +1,207 @@
1
+ import { sampleOffset } from "@aethervtt/shard-noise";
2
+ import { faceToDirection } from "./cube.js";
3
+ const SNAP = 64;
4
+ const NOISE_OFFSET = [13.71, 27.13, 41.92];
5
+ const FACES = [
6
+ [1, 0, 0, 0, 0, -1, 0, 1, 0],
7
+ [-1, 0, 0, 0, 0, 1, 0, 1, 0],
8
+ [0, 1, 0, 1, 0, 0, 0, 0, -1],
9
+ [0, -1, 0, 1, 0, 0, 0, 0, 1],
10
+ [0, 0, 1, 1, 0, 0, 0, 1, 0],
11
+ [0, 0, -1, -1, 0, 0, 0, 1, 0]
12
+ ];
13
+ const QUARTER_PI = Math.PI / 4;
14
+ const probe = new Float64Array(3);
15
+ function gridDirection(face, gi, gj, size, out, o) {
16
+ faceToDirection(face, -1 + 2 * gi / size, -1 + 2 * gj / size, out, o);
17
+ if (face === 0 || gi !== 0 && gi !== size && gj !== 0 && gj !== size) return;
18
+ const x = out[o];
19
+ const y = out[o + 1];
20
+ const z = out[o + 2];
21
+ for (let f = 0; f < face; f++) {
22
+ const a = FACES[f];
23
+ const n = x * a[0] + y * a[1] + z * a[2];
24
+ if (n <= 0.5) continue;
25
+ const u = Math.atan((x * a[3] + y * a[4] + z * a[5]) / n) / QUARTER_PI;
26
+ const v = Math.atan((x * a[6] + y * a[7] + z * a[8]) / n) / QUARTER_PI;
27
+ if (Math.abs(u) > 1 + 1e-9 || Math.abs(v) > 1 + 1e-9) continue;
28
+ const ci = Math.round((u + 1) / 2 * size);
29
+ const cj = Math.round((v + 1) / 2 * size);
30
+ faceToDirection(f, -1 + 2 * ci / size, -1 + 2 * cj / size, probe, 0);
31
+ out[o] = probe[0];
32
+ out[o + 1] = probe[1];
33
+ out[o + 2] = probe[2];
34
+ return;
35
+ }
36
+ }
37
+ function createChunkPoints() {
38
+ return {
39
+ side: 0,
40
+ count: 0,
41
+ dirs: new Float64Array(0),
42
+ group: new Int32Array(0),
43
+ groups: 0,
44
+ origins: new Float64Array(0),
45
+ local: new Float32Array(0),
46
+ order: new Int32Array(0),
47
+ start: new Int32Array(0)
48
+ };
49
+ }
50
+ let tableCell = new Int32Array(0);
51
+ let tableGroup = new Int32Array(0);
52
+ const chunkMetres = (radius, depth) => radius * Math.PI / 2 / 2 ** depth;
53
+ function trailingZeros(i, cap) {
54
+ if (i === 0) return cap;
55
+ let v = i < 0 ? -i : i;
56
+ let z = 0;
57
+ while ((v & 1) === 0 && z < cap) {
58
+ v >>>= 1;
59
+ z++;
60
+ }
61
+ return z;
62
+ }
63
+ const levelSnaps = new Float64Array(32);
64
+ const MAX_SNAP = 4096;
65
+ function prepareChunkPoints(face, depth, x, y, resolution, radius, out, snap) {
66
+ const side = resolution + 2;
67
+ const count = side * side;
68
+ if (out.dirs.length < count * 3) {
69
+ out.dirs = new Float64Array(count * 3);
70
+ out.group = new Int32Array(count);
71
+ out.local = new Float32Array(count * 3);
72
+ out.order = new Int32Array(count);
73
+ out.origins = new Float64Array(count * 4);
74
+ out.start = new Int32Array(count + 1);
75
+ }
76
+ out.side = side;
77
+ out.count = count;
78
+ const size = 2 ** depth * (resolution - 1);
79
+ const gi0 = x * (resolution - 1) - 1;
80
+ const gj0 = y * (resolution - 1) - 1;
81
+ const slots = highBit(count * 4);
82
+ if (tableGroup.length < slots) {
83
+ tableCell = new Int32Array(slots * 4);
84
+ tableGroup = new Int32Array(slots);
85
+ }
86
+ const levels = Math.min(depth, 31);
87
+ for (let L = 0; L <= levels; L++) {
88
+ let sn = SNAP;
89
+ const size2 = chunkMetres(radius, L);
90
+ while (sn * 4 < size2 && sn < MAX_SNAP) sn *= 2;
91
+ levelSnaps[L] = snap ?? sn;
92
+ }
93
+ const segBits = Math.round(Math.log2(resolution - 1));
94
+ const mask = slots - 1;
95
+ tableGroup.fill(-1, 0, slots);
96
+ let groups = 0;
97
+ const d = out.dirs;
98
+ for (let J = 0; J < side; J++) {
99
+ for (let I = 0; I < side; I++) {
100
+ const k = I + J * side;
101
+ const gi = gi0 + I;
102
+ const gj = gj0 + J;
103
+ gridDirection(face, gi, gj, size, d, k * 3);
104
+ const tz = Math.min(trailingZeros(gi, depth + segBits), trailingZeros(gj, depth + segBits));
105
+ const level = Math.max(0, depth - Math.max(0, tz));
106
+ const sn = levelSnaps[Math.min(level, levels)];
107
+ const px = d[k * 3] * radius + NOISE_OFFSET[0];
108
+ const py = d[k * 3 + 1] * radius + NOISE_OFFSET[1];
109
+ const pz = d[k * 3 + 2] * radius + NOISE_OFFSET[2];
110
+ const cx = Math.round(px / sn);
111
+ const cy = Math.round(py / sn);
112
+ const cz = Math.round(pz / sn);
113
+ let h = (hashCell(cx, cy, cz) ^ Math.imul(sn, 668265261)) & mask;
114
+ while (tableGroup[h] !== -1 && (tableCell[h * 4] !== cx || tableCell[h * 4 + 1] !== cy || tableCell[h * 4 + 2] !== cz || tableCell[h * 4 + 3] !== sn))
115
+ h = h + 1 & mask;
116
+ let g;
117
+ if (tableGroup[h] !== -1) g = tableGroup[h];
118
+ else {
119
+ tableCell[h * 4] = cx;
120
+ tableCell[h * 4 + 1] = cy;
121
+ tableCell[h * 4 + 2] = cz;
122
+ tableCell[h * 4 + 3] = sn;
123
+ g = groups++;
124
+ tableGroup[h] = g;
125
+ out.origins[g * 4] = cx * sn;
126
+ out.origins[g * 4 + 1] = cy * sn;
127
+ out.origins[g * 4 + 2] = cz * sn;
128
+ out.origins[g * 4 + 3] = 0;
129
+ }
130
+ out.group[k] = g;
131
+ out.local[k * 3] = px - cx * sn;
132
+ out.local[k * 3 + 1] = py - cy * sn;
133
+ out.local[k * 3 + 2] = pz - cz * sn;
134
+ }
135
+ }
136
+ out.groups = groups;
137
+ const start = out.start;
138
+ start.fill(0, 0, groups + 1);
139
+ for (let k = 0; k < count; k++) start[out.group[k] + 1]++;
140
+ for (let g = 0; g < groups; g++) start[g + 1] += start[g];
141
+ for (let k = 0; k < count; k++) {
142
+ const g = out.group[k];
143
+ out.order[start[g]] = k;
144
+ start[g]++;
145
+ }
146
+ for (let g = groups; g > 0; g--) start[g] = start[g - 1];
147
+ start[0] = 0;
148
+ return out;
149
+ }
150
+ function highBit(n) {
151
+ let p = 1;
152
+ while (p < n) p *= 2;
153
+ return p;
154
+ }
155
+ function hashCell(x, y, z) {
156
+ let h = Math.imul(x, 2654435761) ^ Math.imul(y, 2246822507) ^ Math.imul(z, 3266489909);
157
+ h ^= h >>> 15;
158
+ return h >>> 0;
159
+ }
160
+ let gathered = new Float32Array(0);
161
+ let sampled = new Float32Array(0);
162
+ const origin = new Float64Array(4);
163
+ function sampleChunkPoints(graph, seed, pts, out, node) {
164
+ if (gathered.length < pts.count * 3) {
165
+ gathered = new Float32Array(pts.count * 3);
166
+ sampled = new Float32Array(pts.count);
167
+ }
168
+ for (let g = 0; g < pts.groups; g++) {
169
+ const a = pts.start[g];
170
+ const b = pts.start[g + 1];
171
+ for (let i = a; i < b; i++) {
172
+ const k = pts.order[i];
173
+ gathered[(i - a) * 3] = pts.local[k * 3];
174
+ gathered[(i - a) * 3 + 1] = pts.local[k * 3 + 1];
175
+ gathered[(i - a) * 3 + 2] = pts.local[k * 3 + 2];
176
+ }
177
+ origin[0] = pts.origins[g * 4];
178
+ origin[1] = pts.origins[g * 4 + 1];
179
+ origin[2] = pts.origins[g * 4 + 2];
180
+ const values = sampled.subarray(0, b - a);
181
+ sampleOffset(graph, seed, origin, gathered.subarray(0, (b - a) * 3), values, node);
182
+ for (let i = a; i < b; i++) out[pts.order[i]] = values[i - a];
183
+ }
184
+ }
185
+ function samplePoint(graph, seed, dx, dy, dz, radius, out, node) {
186
+ const px = dx * radius + NOISE_OFFSET[0];
187
+ const py = dy * radius + NOISE_OFFSET[1];
188
+ const pz = dz * radius + NOISE_OFFSET[2];
189
+ origin[0] = Math.round(px / SNAP) * SNAP;
190
+ origin[1] = Math.round(py / SNAP) * SNAP;
191
+ origin[2] = Math.round(pz / SNAP) * SNAP;
192
+ pointLocal[0] = px - origin[0];
193
+ pointLocal[1] = py - origin[1];
194
+ pointLocal[2] = pz - origin[2];
195
+ sampleOffset(graph, seed, origin, pointLocal, out, node);
196
+ return out[0];
197
+ }
198
+ const pointLocal = new Float32Array(3);
199
+ export {
200
+ NOISE_OFFSET,
201
+ SNAP,
202
+ createChunkPoints,
203
+ gridDirection,
204
+ prepareChunkPoints,
205
+ sampleChunkPoints,
206
+ samplePoint
207
+ };
@@ -0,0 +1,153 @@
1
+ /** The node's mesh exists (GPU dispatched, or a CPU mesh assigned): it can render. */
2
+ export declare const NODE_READY = 1;
3
+ /** Heights are known exactly (CPU mesh, or the GPU readback arrived). */
4
+ export declare const NODE_BOUNDS = 2;
5
+ /**
6
+ * One cube-sphere quadtree per planet surface (terrain or ocean), stored as flat typed arrays
7
+ * indexed by node, so the per-frame walk allocates nothing. The six roots are nodes 0–5; children
8
+ * are made on first split as a block of four consecutive nodes, and freed as a block when their
9
+ * subtree hasn't been needed for a while.
10
+ */
11
+ export declare class NodeTree {
12
+ capacity: number;
13
+ count: number;
14
+ face: Uint8Array<ArrayBuffer>;
15
+ depth: Uint8Array<ArrayBuffer>;
16
+ x: Uint32Array<ArrayBuffer>;
17
+ y: Uint32Array<ArrayBuffer>;
18
+ parent: Int32Array<ArrayBuffer>;
19
+ /** First of four children (in order (2x, 2y), (2x+1, 2y), (2x, 2y+1), (2x+1, 2y+1)), or −1. */
20
+ child: Int32Array<ArrayBuffer>;
21
+ flags: Uint8Array<ArrayBuffer>;
22
+ /** Bounding sphere in the planet frame (f64): center xyz, radius. */
23
+ sphere: Float64Array<ArrayBuffer>;
24
+ /** Height range (m above radius): exact once NODE_BOUNDS, else inherited or the planet's. */
25
+ minH: Float32Array<ArrayBuffer>;
26
+ maxH: Float32Array<ArrayBuffer>;
27
+ /** Horizon test directions (unit, 9 per node). */
28
+ dirs: Float64Array<ArrayBuffer>;
29
+ /** Stamps: last walk (frame), rendered and culled (walk stamp), forced split, last needed (frame). */
30
+ visited: Uint32Array<ArrayBuffer>;
31
+ rendered: Uint32Array<ArrayBuffer>;
32
+ forced: Uint32Array<ArrayBuffer>;
33
+ culled: Uint32Array<ArrayBuffer>;
34
+ used: Uint32Array<ArrayBuffer>;
35
+ /** Pass stamp when the node was last requested, so each pass lists it once. */
36
+ requestedAt: Uint32Array<ArrayBuffer>;
37
+ /** Frame the node was last split (children walked): merging then takes a margin. */
38
+ splitAt: Uint32Array<ArrayBuffer>;
39
+ /**
40
+ * Frame the node was last needed on screen: rendered, or a visible child a split waits for.
41
+ * Prefetched and out-of-view nodes are only `used`; a full pool evicts them first.
42
+ */
43
+ neededAt: Uint32Array<ArrayBuffer>;
44
+ /** Render slot holding its mesh, or −1. */
45
+ slot: Int32Array<ArrayBuffer>;
46
+ /** Projected error (px) and distance when last requested: generation priority. */
47
+ priority: Float32Array<ArrayBuffer>;
48
+ distance: Float64Array<ArrayBuffer>;
49
+ /** Edge locks when rendered: bottom, right, top, left (−1, 0, 1). */
50
+ locks: Int8Array<ArrayBuffer>;
51
+ /** Quadrants drawn when rendered (15: all; fewer while a split waits for some children). */
52
+ mask: Uint8Array<ArrayBuffer>;
53
+ /** The planet version the node's mesh was made for: older is stale (it renders until replaced). */
54
+ gen: Uint32Array<ArrayBuffer>;
55
+ /**
56
+ * 1 / cos(half the widest angle between neighboring test points, diagonals included): lifting
57
+ * the points by it puts the surface between them inside their hull (horizon test, bulge).
58
+ */
59
+ lift: Float64Array<ArrayBuffer>;
60
+ /** Starts of freed child blocks. */
61
+ private blocks;
62
+ radius: number;
63
+ readonly shape: Float64Array<ArrayBuffer>;
64
+ /** Height range for nodes whose heights aren't known yet. */
65
+ lowest: number;
66
+ highest: number;
67
+ constructor(capacity?: number);
68
+ private grow;
69
+ /** Clears every node and makes the six roots (after a planet's shape or graphs change). */
70
+ reset(radius: number, shape: ArrayLike<number>, lowest: number, highest: number): void;
71
+ /** Root of a face (nodes 0–5). */
72
+ root(face: number): number;
73
+ private init;
74
+ /** Children of `n`, made if needed. Returns the first (the others follow it). */
75
+ split(n: number): number;
76
+ /** Frees `n`'s subtree. `release` is called for every freed node that holds a render slot. */
77
+ merge(n: number, release: (node: number) => void): void;
78
+ /** Live nodes (roots plus allocated blocks). */
79
+ get live(): number;
80
+ /** Sets a node's exact height range and recomputes its bounds. */
81
+ setHeights(n: number, minH: number, maxH: number): void;
82
+ /** Bounding sphere of the node's 9 test points at its lowest and highest heights, plus bulge. */
83
+ updateSphere(n: number): void;
84
+ /**
85
+ * Axis-aligned bounds of the node relative to `center` (its chunk's reference point), from its
86
+ * test points at its lowest and highest heights plus the bulge between them.
87
+ */
88
+ bounds(n: number, center: ArrayLike<number>, out: {
89
+ [i: number]: number;
90
+ }): void;
91
+ /** The node at (face, depth, x, y) if the tree has it, else the deepest existing ancestor. */
92
+ find(face: number, depth: number, x: number, y: number): number;
93
+ /** Frees subtrees nobody has needed since frame `before`. */
94
+ prune(before: number, release: (node: number) => void): void;
95
+ /** Returns whether `n` and its whole subtree are idle. */
96
+ private pruneNode;
97
+ }
98
+ /** What selection sees of the camera, in the planet frame (f64). */
99
+ export interface SelectionView {
100
+ /** Camera position. */
101
+ position: Float64Array;
102
+ /** Planes (nx, ny, nz, d), inside where n · p + d ≥ 0; `planes` of them. */
103
+ frustum: Float64Array;
104
+ planes: number;
105
+ /**
106
+ * A wider frustum (1.5× the field of view), same count: nodes just outside the view are
107
+ * generated ahead of nodes far outside, so turning or moving shows them ready.
108
+ */
109
+ wide: Float64Array;
110
+ /** Pixels per radian at the view center: viewport height / (2 tan(fovY / 2)). */
111
+ pixelsPerRadian: number;
112
+ }
113
+ export interface SelectionParams {
114
+ maxDepth: number;
115
+ /** Geometric error per depth (m). A depth-d node splits by errors[d + 1]. */
116
+ errors: Float32Array;
117
+ errorPixels: number;
118
+ /** Occluder radius for the horizon test (the lowest the surface gets), 0 for none. */
119
+ occluder: number;
120
+ /** Depth colliders use: forced near anchors, and the deepest rendered there. */
121
+ colliderDepth: number;
122
+ /** Anchor positions in the planet frame (xyz) and their radii; `anchors` of them. */
123
+ anchorPos: Float64Array;
124
+ anchorRadius: Float64Array;
125
+ anchors: number;
126
+ }
127
+ /** A selection's output: rendered nodes, and nodes to generate with their priority. */
128
+ export interface Selection {
129
+ rendered: Int32Array;
130
+ renderedCount: number;
131
+ requested: Int32Array;
132
+ requestedCount: number;
133
+ /** Splits waiting for children: the parent renders meanwhile. */
134
+ waiting: number;
135
+ /** The first child each waiting split is missing (debugging and terrain.describe). */
136
+ missing: Int32Array;
137
+ /** Walks this frame (2:1 balance adds some). */
138
+ passes: number;
139
+ /** Stamp of the final walk: `tree.rendered[n] === stamp` for nodes rendered this frame. */
140
+ stamp: number;
141
+ }
142
+ export declare function createSelection(): Selection;
143
+ /**
144
+ * Picks the nodes to render this frame (spec 0043): walks the six trees, culls nodes behind the
145
+ * horizon or outside the frustum, splits a node when its children's geometric error projects past
146
+ * `errorPixels` (or an anchor needs collider depth nearby, where nothing goes deeper), and renders
147
+ * a split node's children only once all of the visible ones are ready, so a split never shows a
148
+ * hole. Then enforces 2:1 balance between neighbors (marking coarse nodes to split and walking
149
+ * again) and sets each rendered node's edge locks. Allocates nothing once its arrays are big enough.
150
+ */
151
+ export declare function selectNodes(tree: NodeTree, view: SelectionView, params: SelectionParams, frame: number, out: Selection): void;
152
+ /** Whether any anchor is within its radius of the node's bounds. */
153
+ export declare function nearAnchor(tree: NodeTree, n: number, params: SelectionParams): boolean;