@aethervtt/shard-terrain 0.1.0 → 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 +8 -6
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +40 -4
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +42 -2
  76. package/package.json +16 -16
package/dist/quadtree.js CHANGED
@@ -1,12 +1,13 @@
1
- import { EDGE_LEFT, faceToDirection, neighborNode, nodeExtent } from "./cube.js";
1
+ import { EDGE_LEFT } from "./cube.js";
2
2
  const NODE_READY = 1;
3
3
  const NODE_BOUNDS = 2;
4
4
  const NONE = -1;
5
- const POINTS = 9;
6
- class NodeTree {
5
+ class QuadTree {
6
+ surface;
7
7
  capacity = 0;
8
8
  count = 0;
9
- face = new Uint8Array(0);
9
+ /** The root each node is under (a cube face on a planet, a grid cell on a heightfield). */
10
+ root = new Uint8Array(0);
10
11
  depth = new Uint8Array(0);
11
12
  x = new Uint32Array(0);
12
13
  y = new Uint32Array(0);
@@ -14,13 +15,11 @@ class NodeTree {
14
15
  /** First of four children (in order (2x, 2y), (2x+1, 2y), (2x, 2y+1), (2x+1, 2y+1)), or −1. */
15
16
  child = new Int32Array(0);
16
17
  flags = new Uint8Array(0);
17
- /** Bounding sphere in the planet frame (f64): center xyz, radius. */
18
+ /** Bounding sphere in the surface's frame (f64): center xyz, radius. */
18
19
  sphere = new Float64Array(0);
19
- /** Height range (m above radius): exact once NODE_BOUNDS, else inherited or the planet's. */
20
+ /** Height range (m): exact once NODE_BOUNDS, else inherited or the surface's. */
20
21
  minH = new Float32Array(0);
21
22
  maxH = new Float32Array(0);
22
- /** Horizon test directions (unit, 9 per node). */
23
- dirs = new Float64Array(0);
24
23
  /** Stamps: last walk (frame), rendered and culled (walk stamp), forced split, last needed (frame). */
25
24
  visited = new Uint32Array(0);
26
25
  rendered = new Uint32Array(0);
@@ -36,7 +35,7 @@ class NodeTree {
36
35
  * Prefetched and out-of-view nodes are only `used`; a full pool evicts them first.
37
36
  */
38
37
  neededAt = new Uint32Array(0);
39
- /** Render slot holding its mesh, or −1. */
38
+ /** Render slot holding its mesh (or its page), or −1. */
40
39
  slot = new Int32Array(0);
41
40
  /** Projected error (px) and distance when last requested: generation priority. */
42
41
  priority = new Float32Array(0);
@@ -45,21 +44,15 @@ class NodeTree {
45
44
  locks = new Int8Array(0);
46
45
  /** Quadrants drawn when rendered (15: all; fewer while a split waits for some children). */
47
46
  mask = new Uint8Array(0);
48
- /** The planet version the node's mesh was made for: older is stale (it renders until replaced). */
47
+ /** The content version the node's mesh was made for: older is stale (it renders until replaced). */
49
48
  gen = new Uint32Array(0);
50
- /**
51
- * 1 / cos(half the widest angle between neighboring test points, diagonals included): lifting
52
- * the points by it puts the surface between them inside their hull (horizon test, bulge).
53
- */
54
- lift = new Float64Array(0);
55
49
  /** Starts of freed child blocks. */
56
50
  blocks = [];
57
- radius = 1;
58
- shape = new Float64Array([1, 1, 1]);
59
51
  /** Height range for nodes whose heights aren't known yet. */
60
52
  lowest = 0;
61
53
  highest = 0;
62
- constructor(capacity = 1024) {
54
+ constructor(surface, capacity = 1024) {
55
+ this.surface = surface;
63
56
  this.grow(capacity);
64
57
  }
65
58
  grow(capacity) {
@@ -68,7 +61,7 @@ class NodeTree {
68
61
  next.set(old);
69
62
  return next;
70
63
  };
71
- this.face = re(this.face);
64
+ this.root = re(this.root);
72
65
  this.depth = re(this.depth);
73
66
  this.x = re(this.x);
74
67
  this.y = re(this.y);
@@ -78,7 +71,6 @@ class NodeTree {
78
71
  this.sphere = re(this.sphere, 4);
79
72
  this.minH = re(this.minH);
80
73
  this.maxH = re(this.maxH);
81
- this.dirs = re(this.dirs, POINTS * 3);
82
74
  this.visited = re(this.visited);
83
75
  this.rendered = re(this.rendered);
84
76
  this.forced = re(this.forced);
@@ -93,27 +85,21 @@ class NodeTree {
93
85
  this.locks = re(this.locks, 4);
94
86
  this.mask = re(this.mask);
95
87
  this.gen = re(this.gen);
96
- this.lift = re(this.lift);
88
+ this.surface.grow(capacity);
97
89
  this.capacity = capacity;
98
90
  }
99
- /** Clears every node and makes the six roots (after a planet's shape or graphs change). */
100
- reset(radius, shape, lowest, highest) {
101
- this.count = 6;
91
+ /** Clears every node and makes the roots (after the surface's shape or contents change). */
92
+ resetRoots(lowest, highest) {
93
+ const roots = this.surface.roots;
94
+ if (roots > this.capacity) this.grow(Math.max(roots, this.capacity * 2));
95
+ this.count = roots;
102
96
  this.blocks.length = 0;
103
- this.radius = radius;
104
- this.shape[0] = shape[0];
105
- this.shape[1] = shape[1];
106
- this.shape[2] = shape[2];
107
97
  this.lowest = lowest;
108
98
  this.highest = highest;
109
- for (let f = 0; f < 6; f++) this.init(f, f, 0, 0, 0, NONE);
99
+ for (let r = 0; r < roots; r++) this.init(r, r, 0, 0, 0, NONE);
110
100
  }
111
- /** Root of a face (nodes 0–5). */
112
- root(face) {
113
- return face;
114
- }
115
- init(n, face, depth, x, y, parent) {
116
- this.face[n] = face;
101
+ init(n, root, depth, x, y, parent) {
102
+ this.root[n] = root;
117
103
  this.depth[n] = depth;
118
104
  this.x[n] = x;
119
105
  this.y[n] = y;
@@ -134,28 +120,6 @@ class NodeTree {
134
120
  this.gen[n] = 0;
135
121
  this.mask[n] = 15;
136
122
  this.locks.fill(-1, n * 4, n * 4 + 4);
137
- const ext = nodeExtent(depth);
138
- const u0 = -1 + x * ext;
139
- const v0 = -1 + y * ext;
140
- let k = n * POINTS * 3;
141
- for (let j = 0; j < 3; j++) {
142
- for (let i = 0; i < 3; i++) {
143
- faceToDirection(face, u0 + ext * i / 2, v0 + ext * j / 2, this.dirs, k);
144
- k += 3;
145
- }
146
- }
147
- let widest = 0;
148
- const base = n * POINTS * 3;
149
- for (let a = 0; a < POINTS; a++) {
150
- for (let b = a + 1; b < POINTS; b++) {
151
- if (Math.abs(a % 3 - b % 3) > 1 || Math.abs(Math.floor(a / 3) - Math.floor(b / 3)) > 1)
152
- continue;
153
- const d = this.dirs;
154
- const cos = d[base + a * 3] * d[base + b * 3] + d[base + a * 3 + 1] * d[base + b * 3 + 1] + d[base + a * 3 + 2] * d[base + b * 3 + 2];
155
- widest = Math.max(widest, Math.acos(Math.min(1, cos)));
156
- }
157
- }
158
- this.lift[n] = 1 / Math.cos(widest / 2) + 1e-9;
159
123
  if (parent !== NONE && this.flags[parent] & NODE_BOUNDS) {
160
124
  this.minH[n] = this.minH[parent];
161
125
  this.maxH[n] = this.maxH[parent];
@@ -163,7 +127,8 @@ class NodeTree {
163
127
  this.minH[n] = this.lowest;
164
128
  this.maxH[n] = this.highest;
165
129
  }
166
- this.updateSphere(n);
130
+ this.surface.initNode(this, n);
131
+ this.surface.updateSphere(this, n);
167
132
  }
168
133
  /** Children of `n`, made if needed. Returns the first (the others follow it). */
169
134
  split(n) {
@@ -176,11 +141,11 @@ class NodeTree {
176
141
  c = this.count;
177
142
  this.count += 4;
178
143
  }
179
- const face = this.face[n];
144
+ const root = this.root[n];
180
145
  const depth = this.depth[n] + 1;
181
146
  const x = this.x[n] * 2;
182
147
  const y = this.y[n] * 2;
183
- for (let i = 0; i < 4; i++) this.init(c + i, face, depth, x + (i & 1), y + (i >> 1), n);
148
+ for (let i = 0; i < 4; i++) this.init(c + i, root, depth, x + (i & 1), y + (i >> 1), n);
184
149
  this.child[n] = c;
185
150
  return c;
186
151
  }
@@ -208,96 +173,17 @@ class NodeTree {
208
173
  this.flags[n] |= NODE_BOUNDS;
209
174
  this.updateSphere(n);
210
175
  }
211
- /** Bounding sphere of the node's 9 test points at its lowest and highest heights, plus bulge. */
176
+ /** Recomputes the node's bounding sphere from its height range. */
212
177
  updateSphere(n) {
213
- const d = this.dirs;
214
- const s = this.shape;
215
- const lo = this.radius + this.minH[n];
216
- const hi = this.radius + this.maxH[n];
217
- let minX = Infinity;
218
- let minY = Infinity;
219
- let minZ = Infinity;
220
- let maxX = -Infinity;
221
- let maxY = -Infinity;
222
- let maxZ = -Infinity;
223
- for (let p = 0; p < POINTS; p++) {
224
- const o2 = (n * POINTS + p) * 3;
225
- for (let k = 0; k < 2; k++) {
226
- const r = k === 0 ? lo : hi;
227
- const x = d[o2] * s[0] * r;
228
- const y = d[o2 + 1] * s[1] * r;
229
- const z = d[o2 + 2] * s[2] * r;
230
- if (x < minX) minX = x;
231
- if (y < minY) minY = y;
232
- if (z < minZ) minZ = z;
233
- if (x > maxX) maxX = x;
234
- if (y > maxY) maxY = y;
235
- if (z > maxZ) maxZ = z;
236
- }
237
- }
238
- const cx = (minX + maxX) / 2;
239
- const cy = (minY + maxY) / 2;
240
- const cz = (minZ + maxZ) / 2;
241
- let r2 = 0;
242
- for (let p = 0; p < POINTS; p++) {
243
- const o2 = (n * POINTS + p) * 3;
244
- for (let k = 0; k < 2; k++) {
245
- const r = k === 0 ? lo : hi;
246
- const x = d[o2] * s[0] * r - cx;
247
- const y = d[o2 + 1] * s[1] * r - cy;
248
- const z = d[o2 + 2] * s[2] * r - cz;
249
- const q = x * x + y * y + z * z;
250
- if (q > r2) r2 = q;
251
- }
252
- }
253
- const bulge = hi * (this.lift[n] - 1) + 1e-3;
254
- const o = n * 4;
255
- this.sphere[o] = cx;
256
- this.sphere[o + 1] = cy;
257
- this.sphere[o + 2] = cz;
258
- this.sphere[o + 3] = Math.sqrt(r2) + bulge;
178
+ this.surface.updateSphere(this, n);
259
179
  }
260
- /**
261
- * Axis-aligned bounds of the node relative to `center` (its chunk's reference point), from its
262
- * test points at its lowest and highest heights plus the bulge between them.
263
- */
180
+ /** Axis-aligned bounds of the node relative to `center` (its chunk's reference point). */
264
181
  bounds(n, center, out) {
265
- const d = this.dirs;
266
- const s = this.shape;
267
- const lo = this.radius + this.minH[n];
268
- const hi = this.radius + this.maxH[n];
269
- let minX = Infinity;
270
- let minY = Infinity;
271
- let minZ = Infinity;
272
- let maxX = -Infinity;
273
- let maxY = -Infinity;
274
- let maxZ = -Infinity;
275
- for (let p = 0; p < POINTS; p++) {
276
- const o = (n * POINTS + p) * 3;
277
- for (let k = 0; k < 2; k++) {
278
- const r = k === 0 ? lo : hi;
279
- const x = d[o] * s[0] * r - center[0];
280
- const y = d[o + 1] * s[1] * r - center[1];
281
- const z = d[o + 2] * s[2] * r - center[2];
282
- if (x < minX) minX = x;
283
- if (y < minY) minY = y;
284
- if (z < minZ) minZ = z;
285
- if (x > maxX) maxX = x;
286
- if (y > maxY) maxY = y;
287
- if (z > maxZ) maxZ = z;
288
- }
289
- }
290
- const bulge = hi * (this.lift[n] - 1) + 1e-3;
291
- out[0] = minX - bulge;
292
- out[1] = minY - bulge;
293
- out[2] = minZ - bulge;
294
- out[3] = maxX + bulge;
295
- out[4] = maxY + bulge;
296
- out[5] = maxZ + bulge;
182
+ this.surface.bounds(this, n, center, out);
297
183
  }
298
- /** The node at (face, depth, x, y) if the tree has it, else the deepest existing ancestor. */
299
- find(face, depth, x, y) {
300
- let n = face;
184
+ /** The node at (root, depth, x, y) if the tree has it, else the deepest existing ancestor. */
185
+ find(root, depth, x, y) {
186
+ let n = root;
301
187
  for (let k = 0; k < depth; k++) {
302
188
  const c = this.child[n];
303
189
  if (c === NONE) return n;
@@ -308,7 +194,7 @@ class NodeTree {
308
194
  }
309
195
  /** Frees subtrees nobody has needed since frame `before`. */
310
196
  prune(before, release) {
311
- for (let f = 0; f < 6; f++) this.pruneNode(f, before, release);
197
+ for (let r = 0; r < this.surface.roots; r++) this.pruneNode(r, before, release);
312
198
  }
313
199
  /** Returns whether `n` and its whole subtree are idle. */
314
200
  pruneNode(n, before, release) {
@@ -349,67 +235,17 @@ function selectNodes(tree, view, params, frame, out) {
349
235
  out.requestedCount = 0;
350
236
  out.waiting = 0;
351
237
  out.passes++;
352
- for (let f = 0; f < 6; f++) {
353
- tree.visited[f] = frame;
354
- tree.used[f] = frame;
355
- if (culledNode(tree, f, view, params)) tree.culled[f] = passStamp;
356
- else visit(tree, f, view, params, frame, out);
238
+ for (let r = 0; r < tree.surface.roots; r++) {
239
+ tree.visited[r] = frame;
240
+ tree.used[r] = frame;
241
+ if (tree.surface.culled(tree, r, view, params)) tree.culled[r] = passStamp;
242
+ else visit(tree, r, view, params, frame, out);
357
243
  }
358
244
  if (!balance(tree, frame, out)) break;
359
245
  }
360
246
  out.stamp = passStamp;
361
247
  setLocks(tree, out);
362
248
  }
363
- function culledNode(tree, n, view, params) {
364
- const s = tree.sphere;
365
- const o = n * 4;
366
- const cx = s[o];
367
- const cy = s[o + 1];
368
- const cz = s[o + 2];
369
- const r = s[o + 3];
370
- const f = view.frustum;
371
- const d = tree.dirs;
372
- const sh = tree.shape;
373
- const lo = tree.radius + tree.minH[n];
374
- const hi = (tree.radius + tree.maxH[n]) * tree.lift[n];
375
- for (let p = 0; p < view.planes; p++) {
376
- const q = p * 4;
377
- const nx = f[q];
378
- const ny = f[q + 1];
379
- const nz = f[q + 2];
380
- const dist = nx * cx + ny * cy + nz * cz + f[q + 3];
381
- if (dist < -r) return true;
382
- if (dist > r) continue;
383
- let inside = false;
384
- for (let k = 0; k < POINTS && !inside; k++) {
385
- const i = (n * POINTS + k) * 3;
386
- const dx = d[i] * sh[0];
387
- const dy = d[i + 1] * sh[1];
388
- const dz = d[i + 2] * sh[2];
389
- const along = nx * dx + ny * dy + nz * dz;
390
- if (along * lo + f[q + 3] >= 0 || along * hi + f[q + 3] >= 0) inside = true;
391
- }
392
- if (!inside) return true;
393
- }
394
- if (params.occluder <= 0) return false;
395
- const inv = 1 / params.occluder;
396
- const px = view.position[0] / sh[0] * inv;
397
- const py = view.position[1] / sh[1] * inv;
398
- const pz = view.position[2] / sh[2] * inv;
399
- const vh2 = px * px + py * py + pz * pz - 1;
400
- if (vh2 <= 0) return false;
401
- const top = hi * inv;
402
- for (let k = 0; k < POINTS; k++) {
403
- const i = (n * POINTS + k) * 3;
404
- const tx = d[i] * top - px;
405
- const ty = d[i + 1] * top - py;
406
- const tz = d[i + 2] * top - pz;
407
- const dot = -(tx * px + ty * py + tz * pz);
408
- if (dot <= vh2) return false;
409
- if (dot * dot / (tx * tx + ty * ty + tz * tz) <= vh2) return false;
410
- }
411
- return true;
412
- }
413
249
  function nearView(tree, n, view) {
414
250
  const s = tree.sphere;
415
251
  const o = n * 4;
@@ -489,7 +325,7 @@ function visit(tree, n, view, params, frame, out) {
489
325
  tree.visited[k] = frame;
490
326
  tree.used[k] = frame;
491
327
  tree.neededAt[k] = frame;
492
- if (culledNode(tree, k, view, params)) {
328
+ if (tree.surface.culled(tree, k, view, params)) {
493
329
  tree.culled[k] = passStamp;
494
330
  if (!(tree.flags[k] & NODE_READY)) {
495
331
  const d = nodeDistance(tree, k, view.position);
@@ -527,7 +363,7 @@ function visit(tree, n, view, params, frame, out) {
527
363
  if (tree.flags[k] & NODE_READY) continue;
528
364
  const d = nodeDistance(tree, k, view.position);
529
365
  const e = view.pixelsPerRadian * params.errors[depth + 1] / Math.max(d, 1e-3);
530
- const scale = !culledNode(tree, k, view, params) ? 1 : nearView(tree, k, view) ? NEAR_VIEW : OUT_OF_VIEW;
366
+ const scale = !tree.surface.culled(tree, k, view, params) ? 1 : nearView(tree, k, view) ? NEAR_VIEW : OUT_OF_VIEW;
531
367
  request(tree, k, e * scale, d, frame, out);
532
368
  }
533
369
  }
@@ -544,7 +380,8 @@ function balance(tree, frame, out) {
544
380
  const depth = tree.depth[n];
545
381
  if (depth < 2) continue;
546
382
  for (let edge = 0; edge <= EDGE_LEFT; edge++) {
547
- neighborNode(tree.face[n], depth, tree.x[n], tree.y[n], edge, neighbor);
383
+ if (!tree.surface.neighbor(tree.root[n], depth, tree.x[n], tree.y[n], edge, neighbor))
384
+ continue;
548
385
  const m = tree.find(neighbor[0], depth, neighbor[1], neighbor[2]);
549
386
  const k = renderedAbove(tree, m);
550
387
  if (k === NONE) continue;
@@ -566,8 +403,7 @@ function setLocks(tree, out) {
566
403
  const depth = tree.depth[n];
567
404
  for (let edge = 0; edge <= EDGE_LEFT; edge++) {
568
405
  let lock = -1;
569
- if (depth > 0) {
570
- neighborNode(tree.face[n], depth, tree.x[n], tree.y[n], edge, neighbor);
406
+ if (depth > 0 && tree.surface.neighbor(tree.root[n], depth, tree.x[n], tree.y[n], edge, neighbor)) {
571
407
  const m = tree.find(neighbor[0], depth, neighbor[1], neighbor[2]);
572
408
  const k = renderedAbove(tree, m);
573
409
  if (k !== NONE) lock = tree.depth[k] < depth ? 1 : -1;
@@ -580,7 +416,7 @@ function setLocks(tree, out) {
580
416
  export {
581
417
  NODE_BOUNDS,
582
418
  NODE_READY,
583
- NodeTree,
419
+ QuadTree,
584
420
  createSelection,
585
421
  nearAnchor,
586
422
  selectNodes
package/dist/render.d.ts CHANGED
@@ -5,9 +5,9 @@ import { MaterialAsset } from '@aethervtt/shard-render';
5
5
  import type { App } from '@aethervtt/shard-runtime';
6
6
  import { Texture } from '@aethervtt/shard-texture';
7
7
  import { type ColliderChunk } from './colliders.js';
8
+ import type { NodeTree } from './cube-sphere.js';
8
9
  import { type KernelGraph } from './kernel.js';
9
10
  import type { PlanetRuntime } from './planet.js';
10
- import { type NodeTree } from './quadtree.js';
11
11
  /**
12
12
  * One render chunk: an entity drawing its range of an arena's vertex buffers, which a GPU job
13
13
  * writes (or a collider chunk's vertices are copied into).
@@ -138,6 +138,12 @@ export declare class PlanetRender {
138
138
  nextJobId(): number;
139
139
  }
140
140
  export declare function renderOf(world: World, rt: PlanetRuntime): PlanetRender;
141
+ /** The camera that drives selection: the active one with the lowest `order`, then the lowest entity. */
142
+ export declare function pickCamera(q: Query): {
143
+ entity: Entity;
144
+ table: number;
145
+ row: number;
146
+ } | undefined;
141
147
  /**
142
148
  * Chooses each planet's chunks for the selecting camera (spec 0043): walks both quadtrees, gives
143
149
  * new nodes pool slots and queues their GPU jobs within the frame's budget (nodes at collider depth
package/dist/render.js CHANGED
@@ -34,11 +34,12 @@ import {
34
34
  propagateSubtree,
35
35
  Transform
36
36
  } from "@aethervtt/shard-transform";
37
- import { chunkCenter, chunkIndices, chunkLayout } from "./chunk.js";
37
+ import { chunkCenter } from "./chunk.js";
38
38
  import { collidersOf, skirtDepth } from "./colliders.js";
39
39
  import { Chunk, Planet, TerrainBudget } from "./components.js";
40
40
  import { keyString } from "./cube.js";
41
- import { Terrain } from "./heights.js";
41
+ import { chunkIndices, chunkLayout } from "./grid-mesh.js";
42
+ import { TerrainWorld } from "./heights.js";
42
43
  import {
43
44
  GEN_CLIMATE,
44
45
  GEN_HEIGHT,
@@ -49,6 +50,7 @@ import {
49
50
  POINT_FLOATS,
50
51
  registerKernel
51
52
  } from "./kernel.js";
53
+ import { adaptLodBias } from "./lod.js";
52
54
  import { OceanMaterial, PlanetMaterial, TERRAIN_SHADERS, TEXTURE_PERIOD } from "./material.js";
53
55
  import { createChunkPoints, prepareChunkPoints, SNAP } from "./points.js";
54
56
  import { NODE_READY, selectNodes } from "./quadtree.js";
@@ -187,8 +189,7 @@ function selectionParams(rt, kind) {
187
189
  const chunkTriangles = (n) => 2 * (n - 1) * (n - 1);
188
190
  function adaptDetail(rt, triangles) {
189
191
  const drawn = rt.selection.renderedCount * chunkTriangles(rt.settings.resolution);
190
- if (triangles > 0 && drawn > triangles) rt.lodBias = Math.min(16, rt.lodBias * 1.01);
191
- else if (triangles <= 0 || drawn < triangles * 0.85) rt.lodBias = Math.max(1, rt.lodBias / 1.005);
192
+ rt.lodBias = adaptLodBias(rt.lodBias, drawn, triangles);
192
193
  }
193
194
  const selectChunks = defineSystem({
194
195
  name: "terrain/select",
@@ -196,7 +197,7 @@ const selectChunks = defineSystem({
196
197
  setup: (world) => ({ cameras: world.query({ with: [Camera3d, GlobalTransform] }) }),
197
198
  run: (s, world) => {
198
199
  if (!world.tryResource(GpuAssetsResource)) return;
199
- const state = world.resource(Terrain);
200
+ const state = world.resource(TerrainWorld);
200
201
  const frame = state.frame;
201
202
  const budget = world.resource(TerrainBudget);
202
203
  let jobs = budget.chunksPerFrame;
@@ -1177,7 +1178,7 @@ function registerNode(app) {
1177
1178
  const frame = world.resource(Time).frame;
1178
1179
  if (frame === lastFrame) return;
1179
1180
  lastFrame = frame;
1180
- const state = world.tryResource(Terrain);
1181
+ const state = world.tryResource(TerrainWorld);
1181
1182
  if (!state) return;
1182
1183
  for (const rt of state.planets.values()) {
1183
1184
  const pr = rt.parts.get("render");
@@ -1222,6 +1223,7 @@ export {
1222
1223
  PlanetRender,
1223
1224
  TerrainDebug,
1224
1225
  cleanupRender,
1226
+ pickCamera,
1225
1227
  registerNode,
1226
1228
  renderOf,
1227
1229
  selectChunks,
@@ -19,6 +19,8 @@ export interface PlanetOptions {
19
19
  errorPixels?: number;
20
20
  /** MSAA samples (default 1). */
21
21
  msaa?: 1 | 4;
22
+ /** A fixed render scale (0051): the view renders at it and is upscaled, as a window view is. */
23
+ renderScale?: number;
22
24
  /** Planet.vertexPixels (default: the component's). */
23
25
  vertexPixels?: number;
24
26
  minSpacing?: number;
@@ -57,3 +59,40 @@ export declare function sunOver(p: PlanetApp, direction: ArrayLike<number>, elev
57
59
  * sphere of the lowest possible surface (radius − heightScale) is terrain whatever the LOD.
58
60
  */
59
61
  export declare function holes(data: Uint8Array, W: number, H: number, FOV: number, eye: ArrayLike<number>, right: number[], up: number[], forward: number[], lowest: number): number;
62
+ /**
63
+ * 0043's Earth descent (the bench's `terrain/descent-frame` and 0075's `planet-descent` scenario):
64
+ * the height graph of an Earth-sized planet with 3 km of relief.
65
+ */
66
+ export declare const EARTH_HEIGHT: {
67
+ output: string;
68
+ nodes: {
69
+ continents: {
70
+ fbm: {
71
+ source: string;
72
+ octaves: number;
73
+ frequency: number;
74
+ seed: number;
75
+ };
76
+ };
77
+ ridges: {
78
+ ridged: {
79
+ source: string;
80
+ octaves: number;
81
+ frequency: number;
82
+ seed: number;
83
+ };
84
+ };
85
+ h: {
86
+ add: (string | {
87
+ multiply: (string | number)[];
88
+ })[];
89
+ };
90
+ };
91
+ };
92
+ /**
93
+ * The Earth descent's camera path: from 40 000 km to 2 m over `frames` frames, exponentially,
94
+ * looking at the ground ahead. `look(f)` places the camera at frame `f` (held at 2 m after).
95
+ */
96
+ export declare function earthDescent(p: PlanetApp, radius: number, frames: number): {
97
+ look(frame: number): void;
98
+ };
@@ -23,12 +23,18 @@ import {
23
23
  TransformPlugin
24
24
  } from "@aethervtt/shard-transform";
25
25
  import { Planet, TerrainBudget } from "./components.js";
26
- import { Terrain } from "./heights.js";
26
+ import { heightAt, TerrainWorld } from "./heights.js";
27
27
  import { terrainPlugin } from "./plugin.js";
28
28
  async function planetApp(gpu, o) {
29
29
  const app = new App().addPlugin(TransformPlugin);
30
30
  if (gpu)
31
- app.addPlugin(renderPlugin({ gpu, windowView: false }), forwardPlugin({ msaa: o.msaa ?? 1 }));
31
+ app.addPlugin(
32
+ renderPlugin({ gpu, windowView: false }),
33
+ forwardPlugin({
34
+ msaa: o.msaa ?? 1,
35
+ ...o.renderScale ? { renderScale: { mode: "fixed", scale: o.renderScale } } : {}
36
+ })
37
+ );
32
38
  if (o.physics) app.addPlugin(physics3dPlugin());
33
39
  app.addPlugin(terrainPlugin(), ...o.extra ?? []);
34
40
  await app.init();
@@ -66,7 +72,8 @@ async function planetApp(gpu, o) {
66
72
  label: "terrain-test",
67
73
  width: o.width ?? 96,
68
74
  height: o.heightPx ?? 64,
69
- pixelRatio: o.pixelRatio ?? 1
75
+ pixelRatio: o.pixelRatio ?? 1,
76
+ renderScale: o.renderScale !== void 0
70
77
  });
71
78
  const targetRef = world.resource(RenderTargets).add(target, "terrain-test");
72
79
  camera = world.spawn(
@@ -103,7 +110,7 @@ async function planetApp(gpu, o) {
103
110
  camera,
104
111
  sun,
105
112
  view,
106
- runtime: () => world.resource(Terrain).planets.get(planet)
113
+ runtime: () => world.resource(TerrainWorld).planets.get(planet)
107
114
  };
108
115
  }
109
116
  function placeCamera(p, eye, target) {
@@ -179,8 +186,37 @@ function holes(data, W, H, FOV, eye, right, up, forward, lowest) {
179
186
  }
180
187
  return count;
181
188
  }
189
+ const EARTH_HEIGHT = {
190
+ output: "h",
191
+ nodes: {
192
+ continents: { fbm: { source: "simplex", octaves: 7, frequency: 3e-6, seed: 1 } },
193
+ ridges: { ridged: { source: "simplex", octaves: 8, frequency: 3e-4, seed: 2 } },
194
+ h: { add: ["continents", { multiply: ["ridges", 0.15] }] }
195
+ }
196
+ };
197
+ function earthDescent(p, radius, frames) {
198
+ const rt = p.runtime();
199
+ const n = [0.3, 0.9, 0.3].map((v, _, a) => v / Math.hypot(a[0], a[1], a[2]));
200
+ const east = [n[2], 0, -n[0]].map((v, _, a) => v / Math.hypot(a[0], a[1], a[2]));
201
+ const ground = Math.max(0, heightAt(rt, n[0], n[1], n[2]));
202
+ const ratio = (2 / 4e7) ** (1 / frames);
203
+ return {
204
+ look(frame) {
205
+ const altitude = 4e7 * ratio ** Math.min(frame, frames);
206
+ const eye = n.map((v) => v * (radius + ground + altitude));
207
+ const ahead = Math.min(altitude * 1.2, radius * 0.5);
208
+ placeCamera(
209
+ p,
210
+ eye,
211
+ n.map((v, k) => v * (radius + ground) + east[k] * ahead)
212
+ );
213
+ }
214
+ };
215
+ }
182
216
  export {
217
+ EARTH_HEIGHT,
183
218
  capture,
219
+ earthDescent,
184
220
  holes,
185
221
  placeCamera,
186
222
  planetApp,
package/dist/testing.d.ts CHANGED
@@ -40,3 +40,5 @@ export interface WalkResult {
40
40
  * all deterministic.
41
41
  */
42
42
  export declare function walkChecksum(characters?: number, seconds?: number): Promise<WalkResult>;
43
+ export { capture as captureHeightfield, type HeightfieldApp, type HeightfieldOptions, type HeightfieldWalk, heightfieldApp, heightfieldWalk, holes as heightfieldHoles, lookAt as lookAtTerrain, settleHeightfield, sourceAsset, untilStreaming, VALLEY_HILLS, valleySource, } from './heightfield/testing.js';
44
+ export { capture, EARTH_HEIGHT, earthDescent, holes, type PlanetApp, type PlanetOptions, placeCamera, planetApp, settleTerrain, sunOver, } from './test-planet.js';
package/dist/testing.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  } from "@aethervtt/shard-transform";
20
20
  import { collidersOf } from "./colliders.js";
21
21
  import { Planet } from "./components.js";
22
- import { planetHeightAt, Terrain } from "./heights.js";
22
+ import { planetHeightAt, TerrainWorld } from "./heights.js";
23
23
  import { terrainPlugin } from "./plugin.js";
24
24
  const WALK_PLANET = {
25
25
  output: "h",
@@ -79,7 +79,7 @@ async function walkChecksum(characters = 4, seconds = 3) {
79
79
  f64.set(p, k * 3);
80
80
  w.despawn(c);
81
81
  }
82
- const colliders = collidersOf(w.resource(Terrain).planets.get(planet)).chunks.size;
82
+ const colliders = collidersOf(w.resource(TerrainWorld).planets.get(planet)).chunks.size;
83
83
  new DataView(bytes.buffer).setUint32(characters * 24, colliders, true);
84
84
  let hash = 2166136261;
85
85
  for (let i = 0; i < bytes.length; i++) hash = Math.imul(hash ^ bytes[i], 16777619);
@@ -90,7 +90,47 @@ async function walkChecksum(characters = 4, seconds = 3) {
90
90
  ms: performance.now() - t0
91
91
  };
92
92
  }
93
+ import {
94
+ capture,
95
+ heightfieldApp,
96
+ heightfieldWalk,
97
+ holes,
98
+ lookAt,
99
+ settleHeightfield,
100
+ sourceAsset,
101
+ untilStreaming,
102
+ VALLEY_HILLS,
103
+ valleySource
104
+ } from "./heightfield/testing.js";
105
+ import {
106
+ capture as capture2,
107
+ EARTH_HEIGHT,
108
+ earthDescent,
109
+ holes as holes2,
110
+ placeCamera,
111
+ planetApp,
112
+ settleTerrain,
113
+ sunOver
114
+ } from "./test-planet.js";
93
115
  export {
116
+ EARTH_HEIGHT,
117
+ VALLEY_HILLS,
94
118
  WALK_PLANET,
119
+ capture2 as capture,
120
+ capture as captureHeightfield,
121
+ earthDescent,
122
+ heightfieldApp,
123
+ holes as heightfieldHoles,
124
+ heightfieldWalk,
125
+ holes2 as holes,
126
+ lookAt as lookAtTerrain,
127
+ placeCamera,
128
+ planetApp,
129
+ settleHeightfield,
130
+ settleTerrain,
131
+ sourceAsset,
132
+ sunOver,
133
+ untilStreaming,
134
+ valleySource,
95
135
  walkChecksum
96
136
  };