@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
@@ -0,0 +1,86 @@
1
+ import { type Entity, type Query, type World } from '@aethervtt/shard-core';
2
+ import { Mesh } from '@aethervtt/shard-mesh';
3
+ import type { Workers } from '@aethervtt/shard-platform';
4
+ import { type ChunkMesh } from './chunk.js';
5
+ import type { PlanetRuntime } from './planet.js';
6
+ import { type ChunkPoints } from './points.js';
7
+ /** Frames between asking for a collider chunk and it appearing. Always this many, pool or not. */
8
+ export declare const COLLIDER_DELAY = 2;
9
+ /** A cached collider chunk: its CPU mesh, the collider mesh, and its entity while wanted. */
10
+ export interface ColliderChunk {
11
+ key: string;
12
+ face: number;
13
+ depth: number;
14
+ x: number;
15
+ y: number;
16
+ mesh: ChunkMesh;
17
+ /** Surface triangles (no skirts), sharing the mesh's positions. */
18
+ collider: Mesh;
19
+ colliderRef: {
20
+ guid: string | undefined;
21
+ path: string | undefined;
22
+ type: 'Mesh';
23
+ };
24
+ /** The collider entity, or −1 while not wanted. */
25
+ entity: Entity;
26
+ /** Planet version it was built for. */
27
+ version: number;
28
+ /** Frame it was last wanted. */
29
+ used: number;
30
+ }
31
+ interface ColliderJob {
32
+ key: string;
33
+ face: number;
34
+ depth: number;
35
+ x: number;
36
+ y: number;
37
+ /** Frame it becomes a chunk. */
38
+ due: number;
39
+ version: number;
40
+ pts: ChunkPoints | undefined;
41
+ values: Float32Array | undefined;
42
+ temps: Float32Array | undefined;
43
+ moist: Float32Array | undefined;
44
+ /** All pool results are in. */
45
+ ready: boolean;
46
+ cancelled: boolean;
47
+ }
48
+ /** A planet's collider chunks and the jobs making them. */
49
+ export declare class ColliderSet {
50
+ readonly chunks: Map<string, ColliderChunk>;
51
+ readonly jobs: Map<string, ColliderJob>;
52
+ /** Keys wanted this frame, sorted. */
53
+ wanted: string[];
54
+ /** Chunks built on the main thread because the pool hadn't finished by the due frame. */
55
+ builtSync: number;
56
+ /** Chunks finished from pool results. */
57
+ builtPool: number;
58
+ }
59
+ export declare function collidersOf(planet: PlanetRuntime): ColliderSet;
60
+ export interface AnchorQueries {
61
+ anchors: Query;
62
+ characters: Query;
63
+ bodies: Query;
64
+ agents: Query;
65
+ }
66
+ export declare function anchorQueries(world: World): AnchorQueries;
67
+ /**
68
+ * Anchors near the planet this frame, in the planet frame: every TerrainAnchor (unless disabled),
69
+ * character, and awake dynamic body, with its reach.
70
+ */
71
+ export declare function gatherAnchors(world: World, planet: PlanetRuntime, q: AnchorQueries): void;
72
+ /** The collider nodes within reach of the planet's anchors, as sorted keys. */
73
+ export declare function wantedColliders(planet: PlanetRuntime): string[];
74
+ /** How far skirts hang: past any morph mismatch between neighbors at this depth (true errors). */
75
+ export declare function skirtDepth(planet: PlanetRuntime, depth: number): number;
76
+ /**
77
+ * Keeps the planet's collider chunks around its anchors (spec 0043): a chunk is asked for when an
78
+ * anchor comes within reach and appears exactly COLLIDER_DELAY frames later, from pool results when
79
+ * they're in and built on the main thread when they aren't, so which frame it appears never depends
80
+ * on thread timing (the world hash is the same on every host). Unwanted chunks lose their entity
81
+ * but stay cached (least recently used first out).
82
+ */
83
+ export declare function updateColliders(world: World, planet: PlanetRuntime, workers: Workers | undefined, frame: number, cacheSize: number): void;
84
+ /** Drops every collider chunk (planet despawned). */
85
+ export declare function clearColliders(world: World, planet: PlanetRuntime): void;
86
+ export {};
@@ -0,0 +1,356 @@
1
+ import { ChildOf, Derived } from "@aethervtt/shard-core";
2
+ import { Mesh } from "@aethervtt/shard-mesh";
3
+ import { NavAgent, NavSource } from "@aethervtt/shard-nav";
4
+ import { sampleNoiseAsync } from "@aethervtt/shard-noise";
5
+ import { CharacterController, Collider, PhysicsParked, RigidBody } from "@aethervtt/shard-physics";
6
+ import { Meshes } from "@aethervtt/shard-render";
7
+ import {
8
+ GlobalTransform,
9
+ placeInGrid,
10
+ propagateSubtree,
11
+ Transform
12
+ } from "@aethervtt/shard-transform";
13
+ import { assembleChunk, buildChunk, chunkLayout } from "./chunk.js";
14
+ import { Chunk, PlanetNav, TerrainAnchor } from "./components.js";
15
+ import { faceToDirection, keyString, nodeAt, nodeExtent } from "./cube.js";
16
+ import { createChunkPoints, prepareChunkPoints } from "./points.js";
17
+ const COLLIDER_DELAY = 2;
18
+ class ColliderSet {
19
+ chunks = /* @__PURE__ */ new Map();
20
+ jobs = /* @__PURE__ */ new Map();
21
+ /** Keys wanted this frame, sorted. */
22
+ wanted = [];
23
+ /** Chunks built on the main thread because the pool hadn't finished by the due frame. */
24
+ builtSync = 0;
25
+ /** Chunks finished from pool results. */
26
+ builtPool = 0;
27
+ }
28
+ function collidersOf(planet) {
29
+ let set = planet.parts.get("colliders");
30
+ if (!set) {
31
+ set = new ColliderSet();
32
+ planet.parts.set("colliders", set);
33
+ }
34
+ return set;
35
+ }
36
+ function anchorQueries(world) {
37
+ return {
38
+ anchors: world.query({ with: [TerrainAnchor, GlobalTransform] }),
39
+ characters: world.query({ with: [CharacterController, GlobalTransform] }),
40
+ bodies: world.query({ with: [RigidBody, GlobalTransform], without: [PhysicsParked] }),
41
+ agents: world.query({ with: [NavAgent, GlobalTransform] })
42
+ };
43
+ }
44
+ const seen = /* @__PURE__ */ new Set();
45
+ const scratch = new Float64Array(3);
46
+ function gatherAnchors(world, planet, q) {
47
+ const s = planet.settings;
48
+ seen.clear();
49
+ planet.anchors = 0;
50
+ planet.anchorEntity.length = 0;
51
+ const add = (entity, m, row, radius) => {
52
+ if (seen.has(entity)) return;
53
+ seen.add(entity);
54
+ planet.frame.pointToPlanet(m[row * 12 + 3], m[row * 12 + 7], m[row * 12 + 11], scratch);
55
+ const d = Math.sqrt(scratch[0] ** 2 + scratch[1] ** 2 + scratch[2] ** 2);
56
+ const top = s.radius * Math.max(s.shape[0], s.shape[1], s.shape[2]) + planet.highest;
57
+ if (d - top > radius) return;
58
+ const i = planet.anchors++;
59
+ if (planet.anchorPos.length < planet.anchors * 3) {
60
+ const pos = new Float64Array(planet.anchors * 6);
61
+ pos.set(planet.anchorPos);
62
+ planet.anchorPos = pos;
63
+ const r = new Float64Array(planet.anchors * 2);
64
+ r.set(planet.anchorRadius);
65
+ planet.anchorRadius = r;
66
+ }
67
+ planet.anchorPos[i * 3] = scratch[0];
68
+ planet.anchorPos[i * 3 + 1] = scratch[1];
69
+ planet.anchorPos[i * 3 + 2] = scratch[2];
70
+ planet.anchorRadius[i] = radius;
71
+ planet.anchorEntity.push(entity);
72
+ };
73
+ for (const table of q.anchors.tables) {
74
+ const enabled = table.column(TerrainAnchor, "enabled");
75
+ const radius = table.column(TerrainAnchor, "radius");
76
+ const m = table.column(GlobalTransform, "matrix");
77
+ for (let row = 0; row < table.count; row++) {
78
+ const entity = table.entities[row];
79
+ if (!enabled[row]) {
80
+ seen.add(entity);
81
+ continue;
82
+ }
83
+ add(entity, m, row, radius[row] > 0 ? radius[row] : s.colliderRadius);
84
+ }
85
+ }
86
+ if (world.has(planet.entity, PlanetNav)) {
87
+ const reach = Math.max(s.colliderRadius, world.get(planet.entity, PlanetNav).radius);
88
+ for (const table of q.agents.tables) {
89
+ const m = table.column(GlobalTransform, "matrix");
90
+ for (let row = 0; row < table.count; row++) add(table.entities[row], m, row, reach);
91
+ }
92
+ }
93
+ for (const table of q.characters.tables) {
94
+ const m = table.column(GlobalTransform, "matrix");
95
+ for (let row = 0; row < table.count; row++) add(table.entities[row], m, row, s.colliderRadius);
96
+ }
97
+ for (const table of q.bodies.tables) {
98
+ const kind = table.column(RigidBody, "kind");
99
+ const m = table.column(GlobalTransform, "matrix");
100
+ for (let row = 0; row < table.count; row++) {
101
+ if (kind[row] !== 0) continue;
102
+ add(table.entities[row], m, row, s.colliderRadius);
103
+ }
104
+ }
105
+ }
106
+ const node = new Float64Array(3);
107
+ const dir = new Float64Array(3);
108
+ const wantedKeys = /* @__PURE__ */ new Set();
109
+ function wantedColliders(planet) {
110
+ wantedKeys.clear();
111
+ const s = planet.settings;
112
+ const depth = planet.colliderDepth;
113
+ const n = 2 ** depth;
114
+ const ext = nodeExtent(depth);
115
+ const size = planet.spacing(depth) * (s.resolution - 1);
116
+ for (let i = 0; i < planet.anchors; i++) {
117
+ const ax = planet.anchorPos[i * 3];
118
+ const ay = planet.anchorPos[i * 3 + 1];
119
+ const az = planet.anchorPos[i * 3 + 2];
120
+ const r = planet.anchorRadius[i];
121
+ const len = Math.sqrt(ax * ax + ay * ay + az * az);
122
+ if (len === 0) continue;
123
+ nodeAt(ax / len, ay / len, az / len, depth, node);
124
+ const face = node[0];
125
+ const cx = node[1];
126
+ const cy = node[2];
127
+ const k = Math.ceil(r / size) + 1;
128
+ for (let dj = -k; dj <= k; dj++) {
129
+ for (let di = -k; di <= k; di++) {
130
+ let f = face;
131
+ let x = cx + di;
132
+ let y = cy + dj;
133
+ if (x < 0 || y < 0 || x >= n || y >= n) {
134
+ faceToDirection(face, -1 + (x + 0.5) * ext, -1 + (y + 0.5) * ext, dir);
135
+ nodeAt(dir[0], dir[1], dir[2], depth, node);
136
+ f = node[0];
137
+ x = node[1];
138
+ y = node[2];
139
+ }
140
+ faceToDirection(f, -1 + (x + 0.5) * ext, -1 + (y + 0.5) * ext, dir);
141
+ const px = dir[0] * len * s.shape[0] - ax;
142
+ const py = dir[1] * len * s.shape[1] - ay;
143
+ const pz = dir[2] * len * s.shape[2] - az;
144
+ if (Math.sqrt(px * px + py * py + pz * pz) - size * 0.75 > r) continue;
145
+ wantedKeys.add(keyString(f, depth, x, y));
146
+ }
147
+ }
148
+ }
149
+ return [...wantedKeys].sort();
150
+ }
151
+ function specFor(planet, face, depth, x, y) {
152
+ const s = planet.settings;
153
+ return {
154
+ face,
155
+ depth,
156
+ x,
157
+ y,
158
+ radius: s.radius,
159
+ shape: s.shape,
160
+ heightScale: s.heightScale,
161
+ seed: s.seed,
162
+ resolution: s.resolution,
163
+ height: planet.height,
164
+ climate: planet.climate,
165
+ morphError: planet.errors[depth] ?? 0,
166
+ skirtDepth: skirtDepth(planet, depth)
167
+ };
168
+ }
169
+ function skirtDepth(planet, depth) {
170
+ return 2 * (planet.rawErrors[Math.max(0, depth - 1)] ?? 0) + planet.spacing(depth);
171
+ }
172
+ function updateColliders(world, planet, workers, frame, cacheSize) {
173
+ const set = collidersOf(planet);
174
+ if (!planet.ready) return;
175
+ const wanted = wantedColliders(planet);
176
+ set.wanted = wanted;
177
+ const keep = new Set(wanted);
178
+ for (const key of wanted) {
179
+ const chunk = set.chunks.get(key);
180
+ if (chunk && chunk.version === planet.version) {
181
+ chunk.used = frame;
182
+ if (chunk.entity < 0) spawnCollider(world, planet, chunk);
183
+ continue;
184
+ }
185
+ let job = set.jobs.get(key);
186
+ if (job && job.version !== planet.version) {
187
+ job.cancelled = true;
188
+ set.jobs.delete(key);
189
+ job = void 0;
190
+ }
191
+ if (!job) {
192
+ const [face, depth, x, y] = key.split("/").map(Number);
193
+ job = startJob(planet, workers, key, face, depth, x, y, frame + COLLIDER_DELAY);
194
+ set.jobs.set(key, job);
195
+ }
196
+ if (frame < job.due) continue;
197
+ set.jobs.delete(key);
198
+ const spec = specFor(planet, job.face, job.depth, job.x, job.y);
199
+ let mesh;
200
+ if (job.ready && job.pts && job.values && job.temps && job.moist) {
201
+ mesh = assembleChunk(spec, job.pts, job.values, job.temps, job.moist);
202
+ set.builtPool++;
203
+ } else {
204
+ job.cancelled = true;
205
+ mesh = buildChunk(spec);
206
+ set.builtSync++;
207
+ }
208
+ const next = makeChunk(world, key, job, mesh, planet.version, frame);
209
+ if (chunk) replaceChunk(world, chunk);
210
+ set.chunks.set(key, next);
211
+ spawnCollider(world, planet, next);
212
+ }
213
+ for (const [key, job] of set.jobs) {
214
+ if (!keep.has(key)) {
215
+ job.cancelled = true;
216
+ set.jobs.delete(key);
217
+ }
218
+ }
219
+ for (const chunk of set.chunks.values()) {
220
+ if (!keep.has(chunk.key) && chunk.entity >= 0) despawnCollider(world, chunk);
221
+ }
222
+ if (set.chunks.size > cacheSize) {
223
+ const idle = [...set.chunks.values()].filter((c) => c.entity < 0).sort((a, b) => a.used - b.used || (a.key < b.key ? -1 : 1));
224
+ for (let i = 0; i < idle.length && set.chunks.size > cacheSize; i++) {
225
+ dropChunk(world, idle[i]);
226
+ set.chunks.delete(idle[i].key);
227
+ }
228
+ }
229
+ }
230
+ function startJob(planet, workers, key, face, depth, x, y, due) {
231
+ const job = {
232
+ key,
233
+ face,
234
+ depth,
235
+ x,
236
+ y,
237
+ due,
238
+ version: planet.version,
239
+ pts: void 0,
240
+ values: void 0,
241
+ temps: void 0,
242
+ moist: void 0,
243
+ ready: false,
244
+ cancelled: false
245
+ };
246
+ if (!workers || workers.size === 0) return job;
247
+ const s = planet.settings;
248
+ const pts = prepareChunkPoints(face, depth, x, y, s.resolution, s.radius, createChunkPoints());
249
+ job.pts = pts;
250
+ const count = pts.count;
251
+ job.values = new Float32Array(count);
252
+ job.temps = new Float32Array(count);
253
+ job.moist = new Float32Array(count);
254
+ const parts = [];
255
+ const origin = new Float64Array(4);
256
+ for (let g = 0; g < pts.groups; g++) {
257
+ const a = pts.start[g];
258
+ const b = pts.start[g + 1];
259
+ const local = new Float32Array((b - a) * 3);
260
+ for (let i = a; i < b; i++) {
261
+ const k = pts.order[i];
262
+ local[(i - a) * 3] = pts.local[k * 3];
263
+ local[(i - a) * 3 + 1] = pts.local[k * 3 + 1];
264
+ local[(i - a) * 3 + 2] = pts.local[k * 3 + 2];
265
+ }
266
+ origin[0] = pts.origins[g * 4];
267
+ origin[1] = pts.origins[g * 4 + 1];
268
+ origin[2] = pts.origins[g * 4 + 2];
269
+ 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
+ const run = (graph, target, node2) => {
274
+ if (!graph) return;
275
+ const out = new Float32Array(b - a);
276
+ parts.push(
277
+ sampleNoiseAsync(workers, graph, s.seed, local, out, {
278
+ origin: o,
279
+ ...node2 ? { node: node2 } : {}
280
+ }).then(() => scatter(target)(out))
281
+ );
282
+ };
283
+ run(planet.height, job.values);
284
+ run(planet.climate, job.temps, "temperature");
285
+ run(planet.climate, job.moist, "moisture");
286
+ }
287
+ Promise.all(parts).then(
288
+ () => {
289
+ if (!job.cancelled) job.ready = true;
290
+ },
291
+ () => {
292
+ }
293
+ );
294
+ return job;
295
+ }
296
+ function makeChunk(world, key, job, mesh, version, frame) {
297
+ const layout = chunkLayout(Math.round(Math.sqrt(mesh.heights.length)));
298
+ const collider = Mesh.create({ positions: mesh.data.positions, indices: layout.surfaceIndices });
299
+ const colliderRef = world.initResource(Meshes).add(collider, `terrain/collider/${key}`);
300
+ return {
301
+ key,
302
+ face: job.face,
303
+ depth: job.depth,
304
+ x: job.x,
305
+ y: job.y,
306
+ mesh,
307
+ collider,
308
+ colliderRef,
309
+ entity: -1,
310
+ version,
311
+ used: frame
312
+ };
313
+ }
314
+ function spawnCollider(world, planet, chunk) {
315
+ const e = world.spawn(
316
+ [RigidBody, { kind: "fixed" }],
317
+ [Collider, { shape: "trimesh", mesh: chunk.colliderRef }],
318
+ [Chunk, { planet: planet.entity, key: chunk.key, kind: "collider" }],
319
+ [ChildOf, { parent: planet.entity }],
320
+ Transform,
321
+ Derived
322
+ );
323
+ if (world.has(planet.entity, PlanetNav)) world.add(e, NavSource);
324
+ placeInGrid(world, e, planet.entity, chunk.mesh.center);
325
+ propagateSubtree(world, e);
326
+ chunk.entity = e;
327
+ }
328
+ function despawnCollider(world, chunk) {
329
+ if (world.isAlive(chunk.entity)) world.despawn(chunk.entity);
330
+ chunk.entity = -1;
331
+ }
332
+ function replaceChunk(world, chunk) {
333
+ despawnCollider(world, chunk);
334
+ dropChunk(world, chunk);
335
+ }
336
+ function dropChunk(world, chunk) {
337
+ if (chunk.colliderRef.guid) world.initResource(Meshes).delete(chunk.colliderRef.guid);
338
+ }
339
+ function clearColliders(world, planet) {
340
+ const set = collidersOf(planet);
341
+ for (const chunk of set.chunks.values()) replaceChunk(world, chunk);
342
+ for (const job of set.jobs.values()) job.cancelled = true;
343
+ set.chunks.clear();
344
+ set.jobs.clear();
345
+ }
346
+ export {
347
+ COLLIDER_DELAY,
348
+ ColliderSet,
349
+ anchorQueries,
350
+ clearColliders,
351
+ collidersOf,
352
+ gatherAnchors,
353
+ skirtDepth,
354
+ updateColliders,
355
+ wantedColliders
356
+ };
@@ -0,0 +1,49 @@
1
+ export declare const Planet: import("@aethervtt/shard-core").ComponentDef<{
2
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f64">;
3
+ readonly shape: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
4
+ readonly height: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"NoiseGraph"> | null, "object">;
5
+ readonly heightScale: import("@aethervtt/shard-core").FieldType<number, "f32">;
6
+ readonly seed: import("@aethervtt/shard-core").FieldType<number, "u32">;
7
+ readonly ocean: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
8
+ readonly seaLevel: import("@aethervtt/shard-core").FieldType<number, "f32">;
9
+ readonly climate: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"NoiseGraph"> | null, "object">;
10
+ readonly biomes: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"terrain/BiomeSet"> | null, "object">;
11
+ readonly resolution: import("@aethervtt/shard-core").FieldType<number, "u16">;
12
+ readonly minSpacing: import("@aethervtt/shard-core").FieldType<number, "f32">;
13
+ readonly errorPixels: import("@aethervtt/shard-core").FieldType<number, "f32">;
14
+ readonly vertexPixels: import("@aethervtt/shard-core").FieldType<number, "f32">;
15
+ readonly colliderRadius: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
+ readonly skirts: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
17
+ }>;
18
+ export declare const TerrainAnchor: import("@aethervtt/shard-core").ComponentDef<{
19
+ readonly enabled: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
20
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
21
+ }>;
22
+ export declare const PlanetNav: import("@aethervtt/shard-core").ComponentDef<{
23
+ readonly agentRadius: import("@aethervtt/shard-core").FieldType<number, "f32">;
24
+ readonly agentHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
25
+ readonly maxSlope: import("@aethervtt/shard-core").FieldType<number, "f32">;
26
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
27
+ }>;
28
+ export declare const Chunk: import("@aethervtt/shard-core").ComponentDef<{
29
+ readonly planet: import("@aethervtt/shard-core").FieldType<number | null, "f64">;
30
+ readonly key: import("@aethervtt/shard-core").FieldType<string, "object">;
31
+ readonly kind: import("@aethervtt/shard-core").FieldType<"collider" | "ocean" | "render", "u8">;
32
+ }>;
33
+ export interface TerrainBudgetValue {
34
+ /** Chunks generated per frame, at most. */
35
+ chunksPerFrame: number;
36
+ /**
37
+ * Terrain triangles on screen per planet, at most (0: no limit). Past it the planet's detail
38
+ * coarsens a little each frame (a bias on errorPixels) until it fits, and refines back when
39
+ * there's room: frame time holds at any resolution and on any GPU.
40
+ */
41
+ triangles: number;
42
+ /** GPU milliseconds per frame for generation (timestamp queries where available). */
43
+ msPerFrame: number;
44
+ /** Render chunk slots per planet (meshes kept, evicted least recently used). */
45
+ pool: number;
46
+ /** Collider chunks cached per planet. */
47
+ colliderCache: number;
48
+ }
49
+ export declare const TerrainBudget: import("@aethervtt/shard-core").ResourceDef<TerrainBudgetValue>;
@@ -0,0 +1,137 @@
1
+ import { defineComponent, defineResource, t } from "@aethervtt/shard-core";
2
+ import { Visibility } from "@aethervtt/shard-render";
3
+ import { Grid, Transform } from "@aethervtt/shard-transform";
4
+ const Planet = defineComponent(
5
+ "terrain/Planet",
6
+ {
7
+ radius: t.f64({
8
+ default: 4e3,
9
+ min: 1,
10
+ unit: "m",
11
+ description: "Surface radius before heights, 1 km to 50 000 km (Earth is 6 371 000)."
12
+ }),
13
+ shape: t.vec3({
14
+ default: [1, 1, 1],
15
+ min: 0.01,
16
+ description: "Ellipsoid axis ratios: [1, 1, 1] is a sphere, [1, 0.7, 1.2] a lumpy moon."
17
+ }),
18
+ height: t.handle("NoiseGraph", {
19
+ description: "Height graph (*.noise.json), output in [\u22121, 1], sampled on the sphere of `radius`. None: a smooth sphere."
20
+ }),
21
+ heightScale: t.f32({
22
+ default: 600,
23
+ min: 0,
24
+ unit: "m",
25
+ description: "Metres per unit of the height graph."
26
+ }),
27
+ seed: t.u32({ description: "Mixed into every graph: the same graphs make a new planet." }),
28
+ ocean: t.bool({ default: true, description: "A sea surface at seaLevel." }),
29
+ seaLevel: t.f32({ unit: "m", description: "Sea surface height above radius." }),
30
+ climate: t.handle("NoiseGraph", {
31
+ description: "Climate graph with nodes named `temperature` and `moisture` (each in [\u22121, 1]). None: both 0."
32
+ }),
33
+ biomes: t.handle("terrain/BiomeSet", {
34
+ description: "Biomes (*.biomes.json): what the surface looks like where."
35
+ }),
36
+ resolution: t.u16({
37
+ default: 33,
38
+ min: 5,
39
+ max: 129,
40
+ description: "Vertices per chunk edge, 2^n + 1."
41
+ }),
42
+ minSpacing: t.f32({
43
+ default: 0.4,
44
+ min: 0.01,
45
+ unit: "m",
46
+ description: "Finest vertex spacing: sets the deepest quadtree level."
47
+ }),
48
+ errorPixels: t.f32({
49
+ default: 2,
50
+ min: 0.1,
51
+ description: "A chunk splits when its geometric error covers more pixels than this."
52
+ }),
53
+ vertexPixels: t.f32({
54
+ default: 4,
55
+ min: 0,
56
+ description: "Finest on-screen vertex spacing: however rough the terrain, chunks stop splitting once their vertices are this many pixels apart (smaller triangles cost GPU time and show nothing; normal tiles keep the finer relief in the shading). Vertices then morph a few px per frame in fast descents instead of under one. 0: no limit."
57
+ }),
58
+ colliderRadius: t.f32({
59
+ default: 96,
60
+ min: 0,
61
+ unit: "m",
62
+ description: "Collider chunks (and full-detail rendering) within this distance of every TerrainAnchor, character, and dynamic body."
63
+ }),
64
+ skirts: t.bool({
65
+ default: true,
66
+ description: "Walls under chunk edges that hide cracks while neighbors change level."
67
+ })
68
+ },
69
+ {
70
+ description: "A planet you can see from orbit and walk on: a cube-sphere quadtree of chunks generated from noise graphs, with biomes, an ocean, colliders near bodies, and navmesh tiles near agents. Needs a Grid (its chunks are grid children); hiding it hides its chunks.",
71
+ requires: [Grid, Transform, Visibility]
72
+ }
73
+ );
74
+ const TerrainAnchor = defineComponent(
75
+ "terrain/TerrainAnchor",
76
+ {
77
+ enabled: t.bool({
78
+ default: true,
79
+ description: "False: this body gets no terrain colliders (a ship in orbit, a projectile)."
80
+ }),
81
+ radius: t.f32({
82
+ min: 0,
83
+ unit: "m",
84
+ description: "Collider chunks within this distance (0: the planet\u2019s colliderRadius)."
85
+ })
86
+ },
87
+ {
88
+ description: "Terrain colliders around this entity. Characters and dynamic bodies are anchors without it; add it with enabled: false to opt one out."
89
+ }
90
+ );
91
+ const PlanetNav = defineComponent(
92
+ "terrain/PlanetNav",
93
+ {
94
+ agentRadius: t.f32({ default: 0.4, min: 0, unit: "m" }),
95
+ agentHeight: t.f32({ default: 1.8, min: 0.01, unit: "m" }),
96
+ maxSlope: t.f32({ default: 45, min: 0, max: 89.9, unit: "deg" }),
97
+ radius: t.f32({
98
+ default: 150,
99
+ min: 1,
100
+ unit: "m",
101
+ description: "Navmesh around every NavAgent on the planet, this far out."
102
+ })
103
+ },
104
+ {
105
+ description: "Navigation on a planet: a NavMesh baked from collider chunks near NavAgents, in a tangent frame (up is radial) that follows them.",
106
+ requires: [Planet]
107
+ }
108
+ );
109
+ const Chunk = defineComponent(
110
+ "terrain/Chunk",
111
+ {
112
+ planet: t.entity({ readonly: true }),
113
+ key: t.string({ readonly: true, description: "face/depth/x/y" }),
114
+ kind: t.enum(["render", "ocean", "collider"], { readonly: true })
115
+ },
116
+ {
117
+ description: "A terrain chunk the planet spawned. Rebuilt from the planet; never saved.",
118
+ serialize: false
119
+ }
120
+ );
121
+ 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.",
123
+ init: () => ({
124
+ chunksPerFrame: 8,
125
+ triangles: 2e6,
126
+ msPerFrame: 1.5,
127
+ pool: 2048,
128
+ colliderCache: 256
129
+ })
130
+ });
131
+ export {
132
+ Chunk,
133
+ Planet,
134
+ PlanetNav,
135
+ TerrainAnchor,
136
+ TerrainBudget
137
+ };
package/dist/cube.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { directionToFace, faceToDirection } from '@aethervtt/shard-noise';
2
+ export { directionToFace, faceToDirection };
3
+ /** Deepest quadtree level: node keys hold 24 bits per axis. */
4
+ export declare const MAX_DEPTH = 24;
5
+ /** Largest planet radius (m): 0.4 m spacing at depth 24 reaches about this far. */
6
+ export declare const MAX_RADIUS = 50000000;
7
+ /** Edges of a node, counter-clockwise seen from outside: bottom (−v), right (+u), top (+v), left (−u). */
8
+ export declare const EDGE_BOTTOM = 0;
9
+ export declare const EDGE_RIGHT = 1;
10
+ export declare const EDGE_TOP = 2;
11
+ export declare const EDGE_LEFT = 3;
12
+ /** Face-coordinate side of a node at `depth`: a face spans [-1, 1], so 2 / 2^depth. */
13
+ export declare function nodeExtent(depth: number): number;
14
+ /** Metres between vertices at `depth` (along a face's middle): a face edge is a quarter circle. */
15
+ export declare function nodeSpacing(radius: number, depth: number, resolution: number): number;
16
+ /** The first depth whose vertex spacing is at most `minSpacing`, capped at `MAX_DEPTH`. */
17
+ export declare function maxDepthFor(radius: number, resolution: number, minSpacing: number): number;
18
+ /**
19
+ * Packs a node as two u32s: `face << 29 | depth << 24 | x` and `y` (3 + 5 + 24 + 24 bits). Stable
20
+ * across runs and machines, so caches and saves can key by it.
21
+ */
22
+ export declare function packKey(face: number, depth: number, x: number, y: number, out: Uint32Array, offset?: number): Uint32Array;
23
+ export interface NodeAddress {
24
+ face: number;
25
+ depth: number;
26
+ x: number;
27
+ y: number;
28
+ }
29
+ export declare function unpackKey(k0: number, k1: number): NodeAddress;
30
+ /** A node as text, `face/depth/x/y` (cache keys, describe output). */
31
+ export declare function keyString(face: number, depth: number, x: number, y: number): string;
32
+ export declare function parseKey(key: string): NodeAddress | undefined;
33
+ /**
34
+ * The node at `depth` containing a unit direction: its face and cell, written into `out` as
35
+ * [face, x, y].
36
+ */
37
+ export declare function nodeAt(x: number, y: number, z: number, depth: number, out: {
38
+ [i: number]: number;
39
+ }): {
40
+ [i: number]: number;
41
+ };
42
+ /**
43
+ * The same-depth neighbor across one edge, written into `out` as [face, x, y]. Across a cube edge
44
+ * it's on another face, found through the direction just past the edge's middle (the grids of two
45
+ * faces meet edge to edge at every depth).
46
+ */
47
+ export declare function neighborNode(face: number, depth: number, x: number, y: number, edge: number, out: {
48
+ [i: number]: number;
49
+ }): {
50
+ [i: number]: number;
51
+ };