@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/index.js CHANGED
@@ -13,14 +13,14 @@ import {
13
13
  import {
14
14
  assembleChunk,
15
15
  buildChunk,
16
- checkResolution,
17
- chunkCenter,
18
- chunkLayout
16
+ chunkCenter
19
17
  } from "./chunk.js";
20
18
  import {
21
19
  COLLIDER_DELAY,
22
20
  ColliderSet,
21
+ chunkSpec,
23
22
  collidersOf,
23
+ sampleChunkAsync,
24
24
  skirtDepth
25
25
  } from "./colliders.js";
26
26
  import {
@@ -49,14 +49,87 @@ import {
49
49
  parseKey,
50
50
  unpackKey
51
51
  } from "./cube.js";
52
+ import { CubeSphere, NodeTree } from "./cube-sphere.js";
52
53
  import { PlanetFrame } from "./frame.js";
54
+ import {
55
+ checkResolution,
56
+ chunkIndices,
57
+ chunkLayout,
58
+ lockCode
59
+ } from "./grid-mesh.js";
60
+ import {
61
+ bakeTerrain,
62
+ blockKeys,
63
+ packHash,
64
+ readManifest,
65
+ terrainStats
66
+ } from "./heightfield/bake.js";
67
+ import {
68
+ tilesOf
69
+ } from "./heightfield/colliders.js";
70
+ import { Terrain, TerrainChunk } from "./heightfield/component.js";
71
+ import {
72
+ decodeHeightPng,
73
+ HeightmapAssetType,
74
+ HeightmapImporter,
75
+ Heightmaps
76
+ } from "./heightfield/heightmap.js";
77
+ import {
78
+ BAKE_VERSION,
79
+ BLOCK,
80
+ deflate,
81
+ inflate,
82
+ LEAF_SIDE,
83
+ PAGE,
84
+ SIDE
85
+ } from "./heightfield/kernel.js";
86
+ import { TerrainSurfaceMaterial } from "./heightfield/material.js";
87
+ import {
88
+ fsPackStore,
89
+ memoryPackStore,
90
+ PACK,
91
+ packOf,
92
+ readIndex,
93
+ writePack
94
+ } from "./heightfield/pack.js";
95
+ import {
96
+ bakeIsCurrent,
97
+ bakeProjectTerrains,
98
+ stackFor,
99
+ terrainCacheDir
100
+ } from "./heightfield/project.js";
101
+ import {
102
+ finestPage,
103
+ heightfieldRuntime,
104
+ heightfieldSample,
105
+ loadTerrainRegion,
106
+ pageHeight,
107
+ terrainHeightAt
108
+ } from "./heightfield/queries.js";
109
+ import { HeightfieldDebug } from "./heightfield/render.js";
110
+ import { HeightfieldRuntime } from "./heightfield/runtime.js";
111
+ import {
112
+ MAX_MATERIAL_LAYERS,
113
+ parseTerrainSource,
114
+ sourceDependencies,
115
+ terrainLayout
116
+ } from "./heightfield/source.js";
117
+ import {
118
+ TerrainSourceAsset,
119
+ TerrainSourceAssetType,
120
+ TerrainSourceImporter,
121
+ TerrainSources,
122
+ terrainJsonSchema
123
+ } from "./heightfield/source-asset.js";
124
+ import { compileStack, mainNoise } from "./heightfield/stack.js";
125
+ import { heightfieldUpdates, updateHeightfields } from "./heightfield/system.js";
53
126
  import {
54
127
  heightAt,
55
128
  planetHeightAt,
56
129
  planetRuntime,
57
130
  planetSurfaceAt,
58
- Terrain,
59
131
  TerrainState,
132
+ TerrainWorld,
60
133
  terrainSample
61
134
  } from "./heights.js";
62
135
  import {
@@ -70,16 +143,29 @@ import {
70
143
  sampleKernel,
71
144
  VERTEX_KERNEL
72
145
  } from "./kernel.js";
73
- import { measureErrors } from "./lod.js";
146
+ import {
147
+ adaptLodBias,
148
+ capErrors,
149
+ MAX_LOD_BIAS,
150
+ MORPH_WGSL,
151
+ measureErrors,
152
+ morphFactor
153
+ } from "./lod.js";
74
154
  import { OceanMaterial, PlanetMaterial, TERRAIN_SHADERS, TEXTURE_PERIOD } from "./material.js";
75
155
  import { resolvePlanet, terrainMap, terrainMethods } from "./methods.js";
76
156
  import { clearPlanetNav, planetAgents, planetNavMesh, updatePlanetNav } from "./nav.js";
77
- import { terrainBiomesOverlay, terrainCollidersOverlay, terrainLodOverlay } from "./overlays.js";
157
+ import {
158
+ terrainBiomesOverlay,
159
+ terrainCollidersOverlay,
160
+ terrainLodOverlay,
161
+ terrainPagesOverlay
162
+ } from "./overlays.js";
78
163
  import { COLLIDER_SPACING, PlanetRuntime } from "./planet.js";
79
164
  import { terrainPlugin, updatePlanets } from "./plugin.js";
80
165
  import {
81
166
  createChunkPoints,
82
167
  gridDirection,
168
+ NOISE_OFFSET,
83
169
  prepareChunkPoints,
84
170
  SNAP,
85
171
  sampleChunkPoints,
@@ -89,19 +175,22 @@ import {
89
175
  createSelection,
90
176
  NODE_BOUNDS,
91
177
  NODE_READY,
92
- NodeTree,
178
+ QuadTree,
93
179
  selectNodes
94
180
  } from "./quadtree.js";
95
181
  import { PlanetRender, renderOf, selectChunks, TerrainDebug } from "./render.js";
96
182
  import { createView, omniView, perspectiveView } from "./view.js";
97
183
  export {
184
+ BAKE_VERSION,
98
185
  BLENDED_BIOMES,
186
+ BLOCK,
99
187
  Biome,
100
188
  BiomeSet,
101
189
  COLLIDER_DELAY,
102
190
  COLLIDER_SPACING,
103
191
  Chunk,
104
192
  ColliderSet,
193
+ CubeSphere,
105
194
  EDGE_BOTTOM,
106
195
  EDGE_LEFT,
107
196
  EDGE_RIGHT,
@@ -110,14 +199,26 @@ export {
110
199
  GEN_HEIGHT,
111
200
  GEN_OCEAN,
112
201
  GEN_ROOT,
202
+ HeightfieldDebug,
203
+ HeightfieldRuntime,
204
+ HeightmapAssetType,
205
+ HeightmapImporter,
206
+ Heightmaps,
207
+ LEAF_SIDE,
113
208
  MAX_BIOMES,
114
209
  MAX_DEPTH,
115
210
  MAX_LAYERS,
211
+ MAX_LOD_BIAS,
212
+ MAX_MATERIAL_LAYERS,
116
213
  MAX_RADIUS,
214
+ MORPH_WGSL,
117
215
  NODE_BOUNDS,
118
216
  NODE_READY,
217
+ NOISE_OFFSET,
119
218
  NodeTree,
120
219
  OceanMaterial,
220
+ PACK,
221
+ PAGE,
121
222
  PARAMS_BYTES,
122
223
  POINT_FLOATS,
123
224
  Planet,
@@ -126,45 +227,80 @@ export {
126
227
  PlanetNav,
127
228
  PlanetRender,
128
229
  PlanetRuntime,
230
+ QuadTree,
231
+ SIDE,
129
232
  SNAP,
130
233
  TERRAIN_SHADERS,
131
234
  TEXTURE_PERIOD,
132
235
  Terrain,
133
236
  TerrainAnchor,
134
237
  TerrainBudget,
238
+ TerrainChunk,
135
239
  TerrainDebug,
240
+ TerrainSourceAsset,
241
+ TerrainSourceAssetType,
242
+ TerrainSourceImporter,
243
+ TerrainSources,
136
244
  TerrainState,
245
+ TerrainSurfaceMaterial,
246
+ TerrainWorld,
137
247
  VERTEX_KERNEL,
248
+ adaptLodBias,
138
249
  assembleChunk,
250
+ bakeIsCurrent,
251
+ bakeProjectTerrains,
252
+ bakeTerrain,
139
253
  biomeTable,
140
254
  biomeWeights,
141
255
  biomeWindow,
256
+ blockKeys,
142
257
  buildChunk,
258
+ capErrors,
143
259
  checkResolution,
144
260
  chunkCenter,
261
+ chunkIndices,
145
262
  chunkLayout,
263
+ chunkSpec,
146
264
  clearPlanetNav,
147
265
  collidersOf,
266
+ compileStack,
148
267
  createChunkPoints,
149
268
  createSelection,
150
269
  createView,
270
+ decodeHeightPng,
271
+ deflate,
151
272
  directionToFace,
152
273
  dominantBiome,
153
274
  effectiveTemperature,
154
275
  faceToDirection,
276
+ finestPage,
277
+ fsPackStore,
155
278
  gridDirection,
156
279
  heightAt,
280
+ heightfieldRuntime,
281
+ heightfieldSample,
282
+ heightfieldUpdates,
283
+ inflate,
157
284
  kernelGraph,
158
285
  keyString,
286
+ loadTerrainRegion,
287
+ lockCode,
288
+ mainNoise,
159
289
  maxDepthFor,
160
290
  measureErrors,
291
+ memoryPackStore,
292
+ morphFactor,
161
293
  neighborNode,
162
294
  nodeAt,
163
295
  nodeExtent,
164
296
  nodeSpacing,
165
297
  omniView,
298
+ packHash,
166
299
  packKey,
300
+ packOf,
301
+ pageHeight,
167
302
  parseKey,
303
+ parseTerrainSource,
168
304
  perspectiveView,
169
305
  planetAgents,
170
306
  planetHeightAt,
@@ -172,22 +308,36 @@ export {
172
308
  planetRuntime,
173
309
  planetSurfaceAt,
174
310
  prepareChunkPoints,
311
+ readIndex,
312
+ readManifest,
175
313
  renderOf,
176
314
  resolvePlanet,
315
+ sampleChunkAsync,
177
316
  sampleChunkPoints,
178
317
  sampleKernel,
179
318
  samplePoint,
180
319
  selectChunks,
181
320
  selectNodes,
182
321
  skirtDepth,
322
+ sourceDependencies,
323
+ stackFor,
183
324
  terrainBiomesOverlay,
325
+ terrainCacheDir,
184
326
  terrainCollidersOverlay,
327
+ terrainHeightAt,
328
+ terrainJsonSchema,
329
+ terrainLayout,
185
330
  terrainLodOverlay,
186
331
  terrainMap,
187
332
  terrainMethods,
333
+ terrainPagesOverlay,
188
334
  terrainPlugin,
189
335
  terrainSample,
336
+ terrainStats,
337
+ tilesOf,
190
338
  unpackKey,
339
+ updateHeightfields,
191
340
  updatePlanetNav,
192
- updatePlanets
341
+ updatePlanets,
342
+ writePack
193
343
  };
package/dist/lod.d.ts CHANGED
@@ -20,3 +20,30 @@ export interface ErrorSpec {
20
20
  * (not per node) keeps neighbors at one depth in the same band, so their shared edges morph alike.
21
21
  */
22
22
  export declare function measureErrors(spec: ErrorSpec): Float32Array;
23
+ /**
24
+ * Caps each depth's error at the spacing that projects to `vertexPixels` wherever it projects to
25
+ * `errorPixels` (spec 0043): a node then splits only while its children's vertices stay that far
26
+ * apart on screen. Rough terrain otherwise keeps splitting into sub-pixel triangles. Selection and
27
+ * morphing read the same capped table, so morph bands still end where splits happen. Errors never
28
+ * grow with depth, like the measured ones. `vertexPixels` 0 leaves the errors as they are.
29
+ */
30
+ export declare function capErrors(raw: Float32Array, spacing: (depth: number) => number, errorPixels: number, vertexPixels: number, out?: Float32Array): Float32Array;
31
+ /** Largest LOD bias the triangle budget may set (errorPixels × 16). */
32
+ export declare const MAX_LOD_BIAS = 16;
33
+ /**
34
+ * Steers a LOD bias (a multiplier on errorPixels for selection and morphing) toward a triangle
35
+ * budget: up 1% a frame while `drawn` is over `budget`, back down 0.5% a frame once under 85% of
36
+ * it, never below 1 (the surface's own settings) or above MAX_LOD_BIAS. Slow on purpose: the bias
37
+ * moves split distances, so morphs shift with it (2× in about a second). `budget` 0: no limit.
38
+ */
39
+ export declare function adaptLodBias(bias: number, drawn: number, budget: number): number;
40
+ /**
41
+ * Where a vertex is in its morph toward the parent level (0 its own, 1 the parent's), by its
42
+ * distance from the camera: across [0.5, 0.95] of `split`, the distance at which the parent would
43
+ * split (`errors[depth] × pixelsPerRadian / (errorPixels × lodBias)`). Merging takes a 10% margin
44
+ * past that, so a chunk is fully morphed before it goes. The vertex shaders inline the same
45
+ * formula (`MORPH_WGSL`); CPU replays of the vertex stage call this.
46
+ */
47
+ export declare function morphFactor(distance: number, split: number): number;
48
+ /** `morphFactor` in WGSL, for a `d` (distance) and `split` in scope. */
49
+ export declare const MORPH_WGSL = "clamp((d - 0.5 * split) / (0.45 * split), 0.0, 1.0)";
package/dist/lod.js CHANGED
@@ -39,6 +39,28 @@ function measureErrors(spec) {
39
39
  for (let d = spec.maxDepth - 1; d >= 0; d--) out[d] = Math.max(out[d], out[d + 1]);
40
40
  return out;
41
41
  }
42
+ function capErrors(raw, spacing, errorPixels, vertexPixels, out = new Float32Array(raw.length)) {
43
+ const k = vertexPixels > 0 ? errorPixels / vertexPixels : Number.POSITIVE_INFINITY;
44
+ for (let d = 0; d < raw.length; d++) out[d] = Math.min(raw[d], k * spacing(d));
45
+ for (let d = raw.length - 2; d >= 0; d--) out[d] = Math.max(out[d], out[d + 1]);
46
+ return out;
47
+ }
48
+ const MAX_LOD_BIAS = 16;
49
+ function adaptLodBias(bias, drawn, budget) {
50
+ if (budget > 0 && drawn > budget) return Math.min(MAX_LOD_BIAS, bias * 1.01);
51
+ if (budget <= 0 || drawn < budget * 0.85) return Math.max(1, bias / 1.005);
52
+ return bias;
53
+ }
54
+ function morphFactor(distance, split) {
55
+ if (!(split > 0)) return 0;
56
+ return Math.min(1, Math.max(0, (distance - 0.5 * split) / (0.45 * split)));
57
+ }
58
+ const MORPH_WGSL = "clamp((d - 0.5 * split) / (0.45 * split), 0.0, 1.0)";
42
59
  export {
43
- measureErrors
60
+ MAX_LOD_BIAS,
61
+ MORPH_WGSL,
62
+ adaptLodBias,
63
+ capErrors,
64
+ measureErrors,
65
+ morphFactor
44
66
  };
package/dist/material.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t } from "@aethervtt/shard-core";
2
2
  import { defineMaterial } from "@aethervtt/shard-render";
3
+ import { MORPH_WGSL } from "./lod.js";
3
4
  const TEXTURE_PERIOD = 1024;
4
5
  const frame = {
5
6
  center: t.vec4({
@@ -109,7 +110,7 @@ fn terrain_morph(position: vec3f) -> f32 {
109
110
  let split = lock.y * ${u}.camera.w;
110
111
  if (split > 0.0) {
111
112
  let d = distance(vertex_world(position), ${u}.camera.xyz);
112
- t = clamp((d - 0.5 * split) / (0.45 * split), 0.0, 1.0);
113
+ t = ${MORPH_WGSL};
113
114
  }
114
115
  let code = terrain_lock(grid.x, grid.y);
115
116
  if (code >= 0 && code < 4) {
package/dist/methods.js CHANGED
@@ -6,7 +6,13 @@ import {
6
6
  } from "@aethervtt/shard-core";
7
7
  import { encodePng, toBase64 } from "@aethervtt/shard-protocol";
8
8
  import { collidersOf } from "./colliders.js";
9
- import { Terrain, terrainSample } from "./heights.js";
9
+ import {
10
+ describeHeightfield,
11
+ heightfieldMap,
12
+ resolveHeightfield,
13
+ sampleHeightfield
14
+ } from "./heightfield/methods.js";
15
+ import { TerrainWorld, terrainSample } from "./heights.js";
10
16
  function scenePath(world, entity) {
11
17
  const member = findComponent("scene/SceneMember");
12
18
  if (!member || !world.isAlive(entity)) return null;
@@ -14,7 +20,7 @@ function scenePath(world, entity) {
14
20
  return v?.path || null;
15
21
  }
16
22
  function resolvePlanet(world, ref) {
17
- const planets = world.tryResource(Terrain)?.planets;
23
+ const planets = world.tryResource(TerrainWorld)?.planets;
18
24
  if (!planets || planets.size === 0) {
19
25
  throw new ShardError("terrain/no-planet", "There is no terrain/Planet in the world", {
20
26
  hint: "Add terrain/Planet (with a Grid) to an entity, and the terrain plugin to the app."
@@ -181,28 +187,65 @@ function terrainMap(rt, width, mode) {
181
187
  const planetField = t.json({
182
188
  description: "The planet: entity id or scene path (default: the only planet)."
183
189
  });
190
+ const named = (ref) => ref !== void 0 && ref !== null && ref !== "";
191
+ function wantsHeightfield(world, p) {
192
+ if (named(p.terrain)) return true;
193
+ if (named(p.planet)) return false;
194
+ const state = world.tryResource(TerrainWorld);
195
+ return (state?.planets.size ?? 0) === 0 && (state?.heightfields.size ?? 0) > 0;
196
+ }
197
+ const terrainField = t.json({
198
+ description: "A heightfield terrain (0071): entity id or scene path (default: the only one)."
199
+ });
184
200
  const terrainMethods = [
185
201
  {
186
202
  name: "terrain.describe",
187
- description: "Planet terrain as data: per planet, the chunks selected by depth (and partial ones waiting for children), requests and chunks in flight, pool usage, generation counts, collider chunks and anchors, the vertex spacing at the finest and collider depths, and the height, biome, and slope under the camera. Problems (a bad radius, a climate graph without temperature/moisture) and what it waits for are here too.",
188
- params: defineSchema("terrain/DescribeParams", { planet: planetField }),
203
+ description: "Terrain as data. Per planet: the chunks selected by depth (and partial ones waiting for children), requests and chunks in flight, pool usage, generation counts, collider chunks and anchors, the vertex spacing at the finest and collider depths, and the height, biome, and slope under the camera. Per heightfield terrain (0071): roots and depths, the bake (blocks current, stale, baking; the last bake\u2019s rebuilt blocks, time and heights clipped by heightRange), chunks selected by depth, pages resident, cached and read, the GPU pool, and collider tiles with their anchors. Problems and what each waits for are here too.",
204
+ params: defineSchema("terrain/DescribeParams", {
205
+ planet: planetField,
206
+ terrain: terrainField
207
+ }),
189
208
  handler: ({ world }, p) => {
190
- const planets = world.tryResource(Terrain)?.planets;
191
- if (!planets) return { planets: [] };
209
+ const state = world.tryResource(TerrainWorld);
210
+ if (!state) return { planets: [], terrains: [] };
211
+ if (p.terrain !== void 0 && p.terrain !== null && p.terrain !== "")
212
+ return {
213
+ planets: [],
214
+ terrains: [describeHeightfield(world, resolveHeightfield(world, p.terrain))]
215
+ };
192
216
  if (p.planet !== void 0 && p.planet !== null && p.planet !== "")
193
- return { planets: [describePlanet(world, resolvePlanet(world, p.planet))] };
194
- return { planets: [...planets.values()].map((rt) => describePlanet(world, rt)) };
217
+ return { planets: [describePlanet(world, resolvePlanet(world, p.planet))], terrains: [] };
218
+ return {
219
+ planets: [...state.planets.values()].map((rt) => describePlanet(world, rt)),
220
+ terrains: [...state.heightfields.values()].map((rt) => describeHeightfield(world, rt))
221
+ };
195
222
  }
196
223
  },
197
224
  {
198
225
  name: "terrain.sample",
199
- description: "The surface at points on a planet, from the CPU noise the colliders use (headless-safe): height (m above radius), underwater and water depth, slope (degrees), climate, and biome weights with the dominant biome. Points are directions from the planet center in its frame ([x, y, z], any length) or [lat, lon] in degrees (lat from the +Y axis\u2019s equator, lon around it).",
226
+ description: "The ground at points. On a planet, from the CPU noise the colliders use (headless-safe): height (m above radius), underwater and water depth, slope (degrees), climate, and biome weights with the dominant biome; points are directions from the center in its frame ([x, y, z], any length) or [lat, lon] in degrees. On a heightfield terrain (0071, `terrain` or `points`): height, normal, slope, and the two heaviest paint layers with the second\u2019s share, from its leaf pages (loaded first, so exactly the colliders\u2019 ground); points are [x, z] metres from its corner.",
200
227
  params: defineSchema("terrain/SampleParams", {
201
228
  planet: planetField,
202
- directions: t.json({ description: "Directions: [[x, y, z], \u2026] (at most 4096)." }),
203
- latlon: t.json({ description: "Latitudes and longitudes in degrees: [[lat, lon], \u2026]." })
229
+ terrain: terrainField,
230
+ directions: t.json({ description: "Planets: directions [[x, y, z], \u2026] (at most 4096)." }),
231
+ latlon: t.json({
232
+ description: "Planets: latitudes and longitudes in degrees: [[lat, lon], \u2026]."
233
+ }),
234
+ points: t.json({
235
+ description: "Heightfields: [[x, z], \u2026] metres from the corner (at most 4096)."
236
+ })
204
237
  }),
205
- handler: ({ world }, p) => {
238
+ handler: async ({ world }, p) => {
239
+ if (wantsHeightfield(world, p) || Array.isArray(p.points) && !named(p.planet)) {
240
+ const points = Array.isArray(p.points) ? p.points : [];
241
+ if (points.length === 0 || points.length > 4096) {
242
+ throw new ShardError("terrain/bad-points", "Pass 1 to 4096 points as [[x, z], \u2026]", {
243
+ hint: 'Points are metres from the terrain\u2019s corner, e.g. { "points": [[120, 300]] }.'
244
+ });
245
+ }
246
+ const rt2 = resolveHeightfield(world, p.terrain);
247
+ return { samples: await sampleHeightfield(world, rt2, points) };
248
+ }
206
249
  const rt = resolvePlanet(world, p.planet);
207
250
  usable(rt);
208
251
  const dirs = [];
@@ -238,15 +281,20 @@ const terrainMethods = [
238
281
  },
239
282
  {
240
283
  name: "terrain.map",
241
- description: 'An equirectangular PNG of a planet (width \xD7 width/2; latitude +90 at the top, longitude \u2212180 to 180 left to right): mode "biomes" colors land by its dominant biome\u2019s tint (shaded by slope), mode "height" dark lowlands to white peaks; water is blue, darker when deeper. One image answers "are there continents, oceans, and polar caps".',
284
+ description: 'A PNG of terrain. A planet: equirectangular (width \xD7 width/2; latitude +90 at the top, longitude \u2212180 to 180 left to right), "biomes" colors land by its dominant biome\u2019s tint (shaded by slope), "height" dark lowlands to white peaks; water is blue, darker when deeper. A heightfield terrain (0071): top-down (+X right, +Z down), "height", "slope" (flat black to 60\xB0 white), "layers" (the heaviest paint layer\u2019s tint), or "bake" (blocks green when current, amber when stale, cyan where the last bake rebuilt them).',
242
285
  params: defineSchema("terrain/MapParams", {
243
286
  planet: planetField,
287
+ terrain: terrainField,
244
288
  size: t.u32({ default: 256, min: 16, max: 1024, description: "Width in pixels." }),
245
- mode: t.enum(["biomes", "height"])
289
+ mode: t.enum(["biomes", "height", "layers", "slope", "bake"])
246
290
  }),
247
291
  handler: async ({ world }, p) => {
248
- const rt = resolvePlanet(world, p.planet);
249
- const image = terrainMap(rt, p.size, p.mode);
292
+ const mode = p.mode;
293
+ const image = wantsHeightfield(world, p) || (mode === "layers" || mode === "slope" || mode === "bake") && !named(p.planet) ? heightfieldMap(resolveHeightfield(world, p.terrain), p.size, mode) : terrainMap(
294
+ resolvePlanet(world, p.planet),
295
+ p.size,
296
+ mode === "height" ? "height" : "biomes"
297
+ );
250
298
  const png = await encodePng(image.data, image.width, image.height);
251
299
  return { width: image.width, height: image.height, data: toBase64(png) };
252
300
  }
@@ -1,5 +1,7 @@
1
1
  /** Chunk shading by depth: what `terrain-lod` turns on (the material's debug mode 3). */
2
2
  export declare const terrainLodOverlay: import("@aethervtt/shard-render").OverlayDef;
3
+ /** Heightfield chunks shaded by the page each draws, with page streaming in the colliders' colors. */
4
+ export declare const terrainPagesOverlay: import("@aethervtt/shard-render").OverlayDef;
3
5
  /** Chunk shading by dominant biome: what `terrain-biomes` turns on (debug mode 1). */
4
6
  export declare const terrainBiomesOverlay: import("@aethervtt/shard-render").OverlayDef;
5
7
  /** Collider chunks (their borders, on the surface) and the anchors that keep them. */
package/dist/overlays.js CHANGED
@@ -1,10 +1,17 @@
1
1
  import { defineOverlay } from "@aethervtt/shard-render";
2
- import { chunkLayout } from "./chunk.js";
3
2
  import { collidersOf } from "./colliders.js";
4
- import { Terrain } from "./heights.js";
3
+ import { chunkLayout } from "./grid-mesh.js";
4
+ import { tilesOf } from "./heightfield/colliders.js";
5
+ import { TerrainWorld } from "./heights.js";
5
6
  const terrainLodOverlay = defineOverlay({
6
7
  name: "terrain-lod",
7
- description: "Planet terrain shaded by LOD depth (a hue per level, darker toward edges locked to a coarser neighbor), to see where chunks split.",
8
+ description: "Terrain (planets and heightfields) shaded by LOD depth (a hue per level), to see where chunks split.",
9
+ draw: () => {
10
+ }
11
+ });
12
+ const terrainPagesOverlay = defineOverlay({
13
+ name: "terrain-pages",
14
+ description: "Heightfield terrain (0071) shaded by the GPU pool page each chunk draws (a hue per slot): pages arriving show as new hues, coarse resident pages as the large patches. terrain.describe lists resident pages and reads in flight.",
8
15
  draw: () => {
9
16
  }
10
17
  });
@@ -21,9 +28,9 @@ const a = new Float64Array(3);
21
28
  const b = new Float64Array(3);
22
29
  const terrainCollidersOverlay = defineOverlay({
23
30
  name: "terrain-colliders",
24
- description: "Planet collider chunks: each one\u2019s border on the surface (green; yellow while cached but not in use), and a circle per anchor showing the radius it keeps colliders within.",
31
+ description: "Collider chunks (planets) and tiles (heightfields): each one\u2019s border on the ground (green; yellow while cached but not in use), and a circle per anchor showing the radius it keeps colliders within.",
25
32
  draw: (world, g) => {
26
- const state = world.tryResource(Terrain);
33
+ const state = world.tryResource(TerrainWorld);
27
34
  if (!state) return;
28
35
  for (const rt of state.planets.values()) {
29
36
  if (!rt.ready) continue;
@@ -58,10 +65,45 @@ const terrainCollidersOverlay = defineOverlay({
58
65
  g.sphere(a, rt.anchorRadius[i], ANCHOR);
59
66
  }
60
67
  }
68
+ for (const rt of state.heightfields.values()) {
69
+ if (!rt.ready || !rt.layout) continue;
70
+ const size = rt.layout.leafSize;
71
+ const n = 65;
72
+ for (const tile of tilesOf(rt).tiles.values()) {
73
+ const color = tile.entity >= 0 ? COLLIDER : PENDING;
74
+ const x0 = tile.x * size;
75
+ const z0 = tile.z * size;
76
+ const point = (i, j, out) => rt.frame.pointToOrigin(
77
+ x0 + i / (n - 1) * size,
78
+ tile.heights[j * n + i] + 0.05,
79
+ z0 + j / (n - 1) * size,
80
+ out
81
+ );
82
+ for (let k = 0; k < n - 1; k += 4) {
83
+ const k1 = Math.min(n - 1, k + 4);
84
+ for (const [i0, j0, i1, j1] of [
85
+ [k, 0, k1, 0],
86
+ [k, n - 1, k1, n - 1],
87
+ [0, k, 0, k1],
88
+ [n - 1, k, n - 1, k1]
89
+ ]) {
90
+ point(i0, j0, a);
91
+ point(i1, j1, b);
92
+ g.line(a, b, color);
93
+ }
94
+ }
95
+ }
96
+ for (let i = 0; i < rt.anchors; i++) {
97
+ const o = i * 3;
98
+ rt.frame.pointToOrigin(rt.anchorPos[o], rt.anchorPos[o + 1], rt.anchorPos[o + 2], a);
99
+ g.sphere(a, rt.anchorRadius[i], ANCHOR);
100
+ }
101
+ }
61
102
  }
62
103
  });
63
104
  export {
64
105
  terrainBiomesOverlay,
65
106
  terrainCollidersOverlay,
66
- terrainLodOverlay
107
+ terrainLodOverlay,
108
+ terrainPagesOverlay
67
109
  };
package/dist/planet.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type AssetRef, type Entity, ShardError, type World } from '@aethervtt/shard-core';
2
2
  import { type NoiseGraph } from '@aethervtt/shard-noise';
3
3
  import { type BiomeSetValue, type BiomeTable, type BiomeValue } from './biomes.js';
4
+ import { NodeTree } from './cube-sphere.js';
4
5
  import { PlanetFrame } from './frame.js';
5
- import { NodeTree } from './quadtree.js';
6
6
  /** Collider chunks are the first depth whose vertices are at most this far apart (m). */
7
7
  export declare const COLLIDER_SPACING = 1;
8
8
  /** A planet's settings as numbers, read from its component when it changes. */
@@ -94,12 +94,7 @@ export declare class PlanetRuntime {
94
94
  private heightVersion;
95
95
  private climateVersion;
96
96
  private report;
97
- /**
98
- * Caps each depth's error at the spacing that projects to `vertexPixels` wherever it projects to
99
- * `errorPixels`: a node then splits only while its children's vertices stay that far apart on
100
- * screen. Rough terrain otherwise keeps splitting into sub-pixel triangles. Selection and
101
- * morphing read the same capped table, so morph bands still end where splits happen.
102
- */
97
+ /** Caps the measured errors by vertex spacing (`vertexPixels`, see `capErrors`). */
103
98
  private capErrors;
104
99
  /** Vertex spacing at a depth (m). */
105
100
  spacing(depth: number): number;
package/dist/planet.js CHANGED
@@ -7,12 +7,13 @@ import {
7
7
  BiomeSet,
8
8
  biomeTable
9
9
  } from "./biomes.js";
10
- import { checkResolution } from "./chunk.js";
11
10
  import { Planet } from "./components.js";
12
11
  import { MAX_RADIUS, maxDepthFor, nodeSpacing } from "./cube.js";
12
+ import { NodeTree } from "./cube-sphere.js";
13
13
  import { PlanetFrame } from "./frame.js";
14
- import { measureErrors } from "./lod.js";
15
- import { createSelection, NODE_BOUNDS, NodeTree } from "./quadtree.js";
14
+ import { checkResolution } from "./grid-mesh.js";
15
+ import { capErrors, measureErrors } from "./lod.js";
16
+ import { createSelection, NODE_BOUNDS } from "./quadtree.js";
16
17
  const COLLIDER_SPACING = 1;
17
18
  class PlanetRuntime {
18
19
  entity;
@@ -221,26 +222,15 @@ class PlanetRuntime {
221
222
  this.problem = err;
222
223
  world.tryResource(LogResource)?.error(err);
223
224
  }
224
- /**
225
- * Caps each depth's error at the spacing that projects to `vertexPixels` wherever it projects to
226
- * `errorPixels`: a node then splits only while its children's vertices stay that far apart on
227
- * screen. Rough terrain otherwise keeps splitting into sub-pixel triangles. Selection and
228
- * morphing read the same capped table, so morph bands still end where splits happen.
229
- */
225
+ /** Caps the measured errors by vertex spacing (`vertexPixels`, see `capErrors`). */
230
226
  capErrors(bump = true) {
231
227
  const s = this.settings;
232
228
  if (!s) return;
233
- const k = s.vertexPixels > 0 ? s.errorPixels / s.vertexPixels : Number.POSITIVE_INFINITY;
234
- const cap = (raw, out) => {
235
- const o = out.length === raw.length ? out : new Float32Array(raw.length);
236
- for (let d = 0; d < raw.length; d++) o[d] = Math.min(raw[d], k * this.spacing(d));
237
- for (let d = raw.length - 2; d >= 0; d--) o[d] = Math.max(o[d], o[d + 1]);
238
- return o;
239
- };
229
+ const spacing = (d) => this.spacing(d);
240
230
  const before = this.errors;
241
231
  const same = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
242
- const errors = cap(this.rawErrors, new Float32Array(this.rawErrors.length));
243
- const ocean = cap(this.rawOceanErrors, new Float32Array(this.rawOceanErrors.length));
232
+ const errors = capErrors(this.rawErrors, spacing, s.errorPixels, s.vertexPixels);
233
+ const ocean = capErrors(this.rawOceanErrors, spacing, s.errorPixels, s.vertexPixels);
244
234
  if (same(errors, before) && same(ocean, this.oceanErrors)) return;
245
235
  this.errors = errors;
246
236
  this.oceanErrors = ocean;