@aethervtt/shard-terrain 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/biomes.d.ts +65 -0
  4. package/dist/biomes.js +163 -0
  5. package/dist/chunk.d.ts +99 -0
  6. package/dist/chunk.js +304 -0
  7. package/dist/colliders.d.ts +86 -0
  8. package/dist/colliders.js +356 -0
  9. package/dist/components.d.ts +49 -0
  10. package/dist/components.js +137 -0
  11. package/dist/cube.d.ts +51 -0
  12. package/dist/cube.js +87 -0
  13. package/dist/frame.d.ts +21 -0
  14. package/dist/frame.js +63 -0
  15. package/dist/heights.d.ts +51 -0
  16. package/dist/heights.js +137 -0
  17. package/dist/index.d.ts +19 -0
  18. package/dist/index.js +193 -0
  19. package/dist/kernel.d.ts +47 -0
  20. package/dist/kernel.js +243 -0
  21. package/dist/lod.d.ts +22 -0
  22. package/dist/lod.js +44 -0
  23. package/dist/material.d.ts +14 -0
  24. package/dist/material.js +426 -0
  25. package/dist/methods.d.ts +12 -0
  26. package/dist/methods.js +259 -0
  27. package/dist/nav.d.ts +14 -0
  28. package/dist/nav.js +114 -0
  29. package/dist/overlays.d.ts +6 -0
  30. package/dist/overlays.js +67 -0
  31. package/dist/planet.d.ts +106 -0
  32. package/dist/planet.js +313 -0
  33. package/dist/plugin.d.ts +23 -0
  34. package/dist/plugin.js +98 -0
  35. package/dist/points.d.ts +62 -0
  36. package/dist/points.js +207 -0
  37. package/dist/quadtree.d.ts +153 -0
  38. package/dist/quadtree.js +587 -0
  39. package/dist/render.d.ts +179 -0
  40. package/dist/render.js +1222 -0
  41. package/dist/test-planet.d.ts +59 -0
  42. package/dist/test-planet.js +189 -0
  43. package/dist/testing.d.ts +42 -0
  44. package/dist/testing.js +96 -0
  45. package/dist/view.d.ts +12 -0
  46. package/dist/view.js +61 -0
  47. package/package.json +50 -0
package/dist/render.js ADDED
@@ -0,0 +1,1222 @@
1
+ import {
2
+ ChildOf,
3
+ Derived,
4
+ defineSystem,
5
+ ProfilerResource
6
+ } from "@aethervtt/shard-core";
7
+ import { Mesh } from "@aethervtt/shard-mesh";
8
+ import { computeOrigins } from "@aethervtt/shard-noise";
9
+ import {
10
+ Camera3d,
11
+ Cameras,
12
+ DebugOverlays,
13
+ GpuAssetsResource,
14
+ Graph,
15
+ InstanceData,
16
+ isOverlayOn,
17
+ MaterialAsset,
18
+ Materials,
19
+ Mesh3d,
20
+ Meshes,
21
+ MeshMaterial,
22
+ NotShadowCaster,
23
+ NotShadowReceiver,
24
+ RenderPhase,
25
+ Shaders,
26
+ Visibility
27
+ } from "@aethervtt/shard-render";
28
+ import { Time } from "@aethervtt/shard-runtime";
29
+ import { Texture, Textures, toHalf } from "@aethervtt/shard-texture";
30
+ import {
31
+ GlobalTransform,
32
+ placeInGrid,
33
+ propagateSubtree,
34
+ Transform
35
+ } from "@aethervtt/shard-transform";
36
+ import { chunkCenter, chunkIndices, chunkLayout } from "./chunk.js";
37
+ import { collidersOf, skirtDepth } from "./colliders.js";
38
+ import { Chunk, TerrainBudget } from "./components.js";
39
+ import { keyString } from "./cube.js";
40
+ import { Terrain } from "./heights.js";
41
+ import {
42
+ GEN_CLIMATE,
43
+ GEN_HEIGHT,
44
+ GEN_OCEAN,
45
+ GEN_ROOT,
46
+ kernelGraph,
47
+ PARAMS_BYTES,
48
+ POINT_FLOATS,
49
+ registerKernel
50
+ } from "./kernel.js";
51
+ import { OceanMaterial, PlanetMaterial, TERRAIN_SHADERS, TEXTURE_PERIOD } from "./material.js";
52
+ import { createChunkPoints, prepareChunkPoints, SNAP } from "./points.js";
53
+ import { NODE_READY, selectNodes } from "./quadtree.js";
54
+ import { createView, perspectiveView } from "./view.js";
55
+ const TERRAIN = 0;
56
+ const OCEAN = 1;
57
+ const NONE = -1;
58
+ class PlanetRender {
59
+ slots = [];
60
+ /** Vertex buffers shared by ARENA_SLOTS slots each, so chunks draw without rebinding. */
61
+ arenas = [];
62
+ material;
63
+ materialRef;
64
+ ocean;
65
+ oceanRef;
66
+ biomeTexture;
67
+ biomeRef;
68
+ biomeVersion = -1;
69
+ /** Every slot's normal tile (TILE_LAYER² layers of `tileGeometry` tiles), grown with the pool. */
70
+ tiles;
71
+ tilesRef;
72
+ jobs = [];
73
+ /** Each tree's walk stamp last frame (what was on screen then). */
74
+ lastStamp = [0, 0];
75
+ view = createView();
76
+ /** The selecting camera in the origin frame (xyz) and pixels per radian / errorPixels (w). */
77
+ camera = new Float32Array(4);
78
+ hasCamera = false;
79
+ gpu;
80
+ /**
81
+ * The planet version on screen. After an edit (hot reload) every chunk keeps its old contents
82
+ * until the new kernels compile; then every visible chunk regenerates in one frame, so old and
83
+ * new never meet at a seam.
84
+ */
85
+ shownVersion = -1;
86
+ /** The version whose generation pipelines are compiled. */
87
+ kernelsReady = -1;
88
+ /** Jobs per generation pass (moving average), to turn the pass's GPU time into ms per job. */
89
+ jobsPerPass = 0;
90
+ /** GPU milliseconds per job, from timestamp queries (0: not measured). */
91
+ msPerJob = 0;
92
+ /** Kernel graphs, refreshed with the planet's version. */
93
+ kernels;
94
+ stats = {
95
+ generated: 0,
96
+ fromColliders: 0,
97
+ readbacks: 0,
98
+ evicted: 0,
99
+ locks: 0,
100
+ gpuFrames: 0,
101
+ lastFrameJobs: 0
102
+ };
103
+ jobCounter = 0;
104
+ constructor(world) {
105
+ this.material = new MaterialAsset({ roughness: 0.9, baseColor: [1, 1, 1, 1] }, PlanetMaterial);
106
+ this.materialRef = world.resource(Materials).add(this.material, "terrain/planet");
107
+ this.ocean = new MaterialAsset({ roughness: 0.05, baseColor: [1, 1, 1, 1] }, OceanMaterial);
108
+ this.oceanRef = world.resource(Materials).add(this.ocean, "terrain/ocean");
109
+ }
110
+ nextJobId() {
111
+ return ++this.jobCounter;
112
+ }
113
+ }
114
+ function renderOf(world, rt) {
115
+ let r = rt.parts.get("render");
116
+ if (!r) {
117
+ r = new PlanetRender(world);
118
+ rt.parts.set("render", r);
119
+ }
120
+ return r;
121
+ }
122
+ const camPos = new Float64Array(3);
123
+ const right = new Float64Array(3);
124
+ const up = new Float64Array(3);
125
+ const forward = new Float64Array(3);
126
+ function normalize3(v) {
127
+ const l = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) || 1;
128
+ v[0] = v[0] / l;
129
+ v[1] = v[1] / l;
130
+ v[2] = v[2] / l;
131
+ }
132
+ function pickCamera(q) {
133
+ let best2;
134
+ let bestOrder = Infinity;
135
+ for (let t = 0; t < q.tables.length; t++) {
136
+ const table = q.tables[t];
137
+ const order2 = table.column(Camera3d, "order");
138
+ const active = table.column(Camera3d, "active");
139
+ for (let row = 0; row < table.count; row++) {
140
+ if (active[row] === 0) continue;
141
+ const o = order2[row];
142
+ const e = table.entities[row];
143
+ if (o < bestOrder || o === bestOrder && best2 && e < best2.entity) {
144
+ bestOrder = o;
145
+ best2 = { entity: e, table: t, row };
146
+ }
147
+ }
148
+ }
149
+ return best2;
150
+ }
151
+ function updateView(world, rt, pr, camera) {
152
+ const m = world.get(camera, GlobalTransform).matrix;
153
+ const cam = world.get(camera, Camera3d);
154
+ const data = world.tryResource(Cameras)?.get(camera);
155
+ const height = data && data.height > 1 ? data.displayHeight / data.pixelRatio : 720;
156
+ const aspect = data && data.height > 1 ? data.width / data.height : 16 / 9;
157
+ const fovY = cam.fovY * Math.PI / 180;
158
+ rt.frame.pointToPlanet(m[3], m[7], m[11], camPos);
159
+ rt.frame.vectorToPlanet(m[0], m[4], m[8], right);
160
+ rt.frame.vectorToPlanet(m[1], m[5], m[9], up);
161
+ rt.frame.vectorToPlanet(-m[2], -m[6], -m[10], forward);
162
+ normalize3(right);
163
+ normalize3(up);
164
+ normalize3(forward);
165
+ perspectiveView(pr.view, camPos, right, up, forward, fovY, aspect, height);
166
+ pr.camera[0] = m[3];
167
+ pr.camera[1] = m[7];
168
+ pr.camera[2] = m[11];
169
+ pr.camera[3] = pr.view.pixelsPerRadian / (rt.settings.errorPixels * rt.lodBias);
170
+ }
171
+ function selectionParams(rt, kind) {
172
+ const s = rt.settings;
173
+ const minShape = Math.min(s.shape[0], s.shape[1], s.shape[2]);
174
+ const lowest = kind === TERRAIN ? rt.lowest : s.seaLevel;
175
+ return {
176
+ maxDepth: rt.maxDepth,
177
+ errors: kind === TERRAIN ? rt.errors : rt.oceanErrors,
178
+ errorPixels: s.errorPixels * rt.lodBias,
179
+ occluder: Math.max(0, s.radius * minShape + Math.min(lowest, rt.lowest)),
180
+ colliderDepth: kind === TERRAIN ? rt.colliderDepth : rt.maxDepth,
181
+ anchorPos: rt.anchorPos,
182
+ anchorRadius: rt.anchorRadius,
183
+ anchors: kind === TERRAIN ? rt.anchors : 0
184
+ };
185
+ }
186
+ const chunkTriangles = (n) => 2 * (n - 1) * (n - 1);
187
+ function adaptDetail(rt, triangles) {
188
+ const drawn = rt.selection.renderedCount * chunkTriangles(rt.settings.resolution);
189
+ if (triangles > 0 && drawn > triangles) rt.lodBias = Math.min(16, rt.lodBias * 1.01);
190
+ else if (triangles <= 0 || drawn < triangles * 0.85) rt.lodBias = Math.max(1, rt.lodBias / 1.005);
191
+ }
192
+ const selectChunks = defineSystem({
193
+ name: "terrain/select",
194
+ description: "Selects each Planet\u2019s chunks for the camera (screen-space error, horizon and frustum culling, 2:1 balance), queues their GPU generation within TerrainBudget, and shows exactly the selected ones.",
195
+ setup: (world) => ({ cameras: world.query({ with: [Camera3d, GlobalTransform] }) }),
196
+ run: (s, world) => {
197
+ if (!world.tryResource(GpuAssetsResource)) return;
198
+ const state = world.resource(Terrain);
199
+ const frame = state.frame;
200
+ const budget = world.resource(TerrainBudget);
201
+ let jobs = budget.chunksPerFrame;
202
+ if (frame % 30 === 0) {
203
+ const timing = world.tryResource(ProfilerResource)?.timing("gpu:terrain/generate");
204
+ for (const rt of state.planets.values()) {
205
+ const pr = rt.parts.get("render");
206
+ if (pr && timing && pr.jobsPerPass > 0) pr.msPerJob = timing.avg / pr.jobsPerPass;
207
+ }
208
+ }
209
+ for (const rt of state.planets.values()) {
210
+ const pr = rt.parts.get("render");
211
+ if (pr && pr.msPerJob > 0)
212
+ jobs = Math.min(jobs, Math.max(1, Math.floor(budget.msPerFrame / pr.msPerJob)));
213
+ }
214
+ for (const rt of state.planets.values()) {
215
+ const pr = rt.parts.get("render");
216
+ if (pr) jobs -= pr.jobs.length;
217
+ }
218
+ const picked = pickCamera(s.cameras);
219
+ for (const rt of state.planets.values()) {
220
+ if (!rt.ready || !world.isAlive(rt.entity)) continue;
221
+ const pr = renderOf(world, rt);
222
+ updateMaterials(world, rt, pr);
223
+ if (!picked) {
224
+ pr.hasCamera = false;
225
+ hideAll(world, pr, frame);
226
+ continue;
227
+ }
228
+ pr.hasCamera = true;
229
+ updateView(world, rt, pr, picked.entity);
230
+ selectNodes(rt.tree, pr.view, selectionParams(rt, TERRAIN), frame, rt.selection);
231
+ adaptDetail(rt, budget.triangles);
232
+ const ocean = rt.settings.ocean;
233
+ if (ocean)
234
+ selectNodes(rt.ocean, pr.view, selectionParams(rt, OCEAN), frame, rt.oceanSelection);
235
+ const hold = syncVersion(rt, pr);
236
+ jobs -= queueJobs(
237
+ world,
238
+ rt,
239
+ pr,
240
+ rt.tree,
241
+ TERRAIN,
242
+ rt.selection,
243
+ frame,
244
+ jobs,
245
+ budget.pool,
246
+ hold
247
+ );
248
+ if (ocean)
249
+ jobs -= queueJobs(
250
+ world,
251
+ rt,
252
+ pr,
253
+ rt.ocean,
254
+ OCEAN,
255
+ rt.oceanSelection,
256
+ frame,
257
+ jobs,
258
+ budget.pool,
259
+ hold
260
+ );
261
+ showSelected(world, rt, pr, frame);
262
+ if (frame % 120 === 0) {
263
+ const release = (n) => releaseNode(pr, rt.tree, n);
264
+ rt.tree.prune(frame - 600, release);
265
+ rt.ocean.prune(frame - 600, (n) => releaseNode(pr, rt.ocean, n));
266
+ }
267
+ }
268
+ }
269
+ });
270
+ function syncVersion(rt, pr) {
271
+ if (pr.shownVersion === rt.version) return false;
272
+ let shown = false;
273
+ for (const slot of pr.slots) {
274
+ if (slot.shown) {
275
+ shown = true;
276
+ break;
277
+ }
278
+ }
279
+ if (shown && pr.kernelsReady !== rt.version) return true;
280
+ pr.shownVersion = rt.version;
281
+ const trees = [rt.tree, rt.ocean];
282
+ const stamps = [rt.selection.stamp, rt.oceanSelection.stamp];
283
+ for (let kind = 0; kind < 2; kind++) {
284
+ const tree = trees[kind];
285
+ for (let n = 0; n < tree.count; n++) {
286
+ if (!(tree.flags[n] & NODE_READY) || tree.gen[n] === rt.version || tree.rendered[n] === stamps[kind])
287
+ continue;
288
+ const slot = pr.slots[tree.slot[n]];
289
+ if (slot?.pending) continue;
290
+ tree.flags[n] &= ~NODE_READY;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+ function releaseNode(pr, tree, n) {
296
+ const id = tree.slot[n];
297
+ if (id === NONE) return;
298
+ const slot = pr.slots[id];
299
+ if (slot && slot.node === n) {
300
+ slot.node = NONE;
301
+ slot.gpuNode = NONE;
302
+ }
303
+ tree.slot[n] = NONE;
304
+ }
305
+ function hideAll(world, pr, frame) {
306
+ for (const slot of pr.slots) if (slot.shown) setShown(world, slot, false, frame);
307
+ }
308
+ function setShown(world, slot, shown, frame) {
309
+ if (shown) slot.used = frame;
310
+ if (slot.shown === shown) return;
311
+ slot.shown = shown;
312
+ world.set(slot.entity, Visibility, { mode: shown ? "inherit" : "hidden" });
313
+ }
314
+ const IDLE_FRAMES = 60;
315
+ function acquireSlot(world, rt, pr, kind, frame, pool, needed) {
316
+ for (const slot of pr.slots)
317
+ if (slot.node === NONE && slot.kind === kind && !slot.pending) return slot;
318
+ if (pr.slots.length < pool) return makeSlot(world, rt, pr, kind);
319
+ const tree = kind === TERRAIN ? rt.tree : rt.ocean;
320
+ let best2;
321
+ let bestRank = 0;
322
+ for (const slot of pr.slots) {
323
+ if (slot.kind !== kind || slot.pending) continue;
324
+ let rank = 0;
325
+ if (slot.node !== NONE) {
326
+ if (tree.neededAt[slot.node] >= frame) continue;
327
+ if (!needed && tree.used[slot.node] > frame - IDLE_FRAMES) continue;
328
+ rank = tree.used[slot.node] >= frame ? 1 : 0;
329
+ }
330
+ if (!best2 || rank < bestRank || rank === bestRank && slot.used < best2.used) {
331
+ best2 = slot;
332
+ bestRank = rank;
333
+ }
334
+ }
335
+ if (!best2) return void 0;
336
+ if (best2.node !== NONE) {
337
+ tree.slot[best2.node] = NONE;
338
+ tree.flags[best2.node] &= ~NODE_READY;
339
+ }
340
+ best2.node = NONE;
341
+ best2.gpuNode = NONE;
342
+ pr.stats.evicted++;
343
+ return best2;
344
+ }
345
+ const ARENA_SLOTS = 256;
346
+ const TILE_LAYER = 2048;
347
+ function tileGeometry(n) {
348
+ const size = n <= 33 ? 2 * (n - 1) + 1 : 0;
349
+ const perRow = size > 0 ? Math.floor(TILE_LAYER / size) : 1;
350
+ return { size, perRow, perLayer: perRow * perRow };
351
+ }
352
+ const slotStride = (vertexCount) => Math.ceil(vertexCount / 64) * 64;
353
+ function makeSlot(world, rt, pr, kind) {
354
+ const layout = chunkLayout(rt.settings.resolution);
355
+ const indices = rt.settings.skirts ? layout.indices : layout.surfaceIndices;
356
+ const stride = slotStride(layout.vertexCount);
357
+ const id = pr.slots.length;
358
+ const a = Math.floor(id / ARENA_SLOTS);
359
+ pr.arenas[a] ??= Mesh.gpu({
360
+ vertexCount: stride * ARENA_SLOTS,
361
+ indices,
362
+ bounds: [-1, -1, -1, 1, 1, 1]
363
+ });
364
+ const mesh = Mesh.gpu({
365
+ vertexCount: layout.vertexCount,
366
+ indices,
367
+ bounds: [-1, -1, -1, 1, 1, 1],
368
+ share: pr.arenas[a],
369
+ baseVertex: id % ARENA_SLOTS * stride
370
+ });
371
+ const meshRef = world.resource(Meshes).add(mesh, `terrain/chunk/${pr.slots.length}`);
372
+ const entity = world.spawn(
373
+ [Mesh3d, { mesh: meshRef }],
374
+ [MeshMaterial, { material: kind === TERRAIN ? pr.materialRef : pr.oceanRef }],
375
+ [Chunk, { planet: rt.entity, key: "", kind: kind === TERRAIN ? "render" : "ocean" }],
376
+ [ChildOf, { parent: rt.entity }],
377
+ [Visibility, { mode: "hidden" }],
378
+ [InstanceData, { x: 30, y: 0 }],
379
+ Transform,
380
+ Derived
381
+ );
382
+ if (kind === OCEAN) {
383
+ world.add(entity, NotShadowCaster);
384
+ world.add(entity, NotShadowReceiver);
385
+ }
386
+ const slot = {
387
+ id: pr.slots.length,
388
+ kind,
389
+ mesh,
390
+ meshRef,
391
+ entity,
392
+ node: NONE,
393
+ bits: 0,
394
+ fade: 0,
395
+ packed: 30,
396
+ cpu: void 0,
397
+ used: 0,
398
+ shown: false,
399
+ source: 0,
400
+ indexKey: 15,
401
+ pending: false,
402
+ gpuNode: NONE,
403
+ gpuVersion: -1,
404
+ jobId: 0,
405
+ center: new Float64Array(3)
406
+ };
407
+ pr.slots.push(slot);
408
+ return slot;
409
+ }
410
+ function assign(world, rt, slot, tree, n) {
411
+ const s = rt.settings;
412
+ slot.node = n;
413
+ tree.slot[n] = slot.id;
414
+ chunkCenter(tree.face[n], tree.depth[n], tree.x[n], tree.y[n], s.radius, s.shape, slot.center);
415
+ if (slot.kind === OCEAN) {
416
+ const k = (s.radius + s.seaLevel) / s.radius;
417
+ slot.center[0] = slot.center[0] * k;
418
+ slot.center[1] = slot.center[1] * k;
419
+ slot.center[2] = slot.center[2] * k;
420
+ }
421
+ placeInGrid(world, slot.entity, rt.entity, slot.center);
422
+ propagateSubtree(world, slot.entity);
423
+ world.set(slot.entity, Chunk, {
424
+ planet: rt.entity,
425
+ key: keyString(tree.face[n], tree.depth[n], tree.x[n], tree.y[n]),
426
+ kind: slot.kind === TERRAIN ? "render" : "ocean"
427
+ });
428
+ tree.bounds(n, slot.center, slot.mesh.bounds);
429
+ slot.cpu = void 0;
430
+ }
431
+ function setIndices(rt, slot, mask, stitch) {
432
+ const key = mask + 16 * stitch;
433
+ if (key === slot.indexKey) return;
434
+ slot.indexKey = key;
435
+ const s = rt.settings;
436
+ slot.mesh.setIndices(chunkIndices(s.resolution, mask, stitch, s.skirts));
437
+ }
438
+ function useCollider(world, rt, pr, slot, tree, n, chunk) {
439
+ if (slot.cpu === chunk && slot.source === 1) return;
440
+ const assets = world.resource(GpuAssetsResource);
441
+ const gm = assets.mesh(slot.mesh);
442
+ const queue = assets.gpu.device.queue;
443
+ const data = chunk.mesh.data;
444
+ const base = gm.baseVertex;
445
+ queue.writeBuffer(gm.positions, base * 12, data.positions);
446
+ queue.writeBuffer(gm.normals, base * 12, data.normals);
447
+ queue.writeBuffer(gm.uvs, base * 8, data.uvs);
448
+ queue.writeBuffer(gm.uvs1, base * 8, withTile(data.uvs1, rt.settings.resolution, slot.id));
449
+ queue.writeBuffer(gm.tangents, base * 16, data.tangents);
450
+ slot.cpu = chunk;
451
+ slot.source = 1;
452
+ slot.gpuNode = slot.node;
453
+ slot.gpuVersion = chunk.version;
454
+ slot.jobId = pr.nextJobId();
455
+ slot.pending = false;
456
+ if (tileGeometry(rt.settings.resolution).size > 0) queueJob(rt, pr, slot, tree, TERRAIN, n, true);
457
+ }
458
+ let tileScratch = new Float32Array(0);
459
+ function withTile(uvs1, resolution, tile) {
460
+ if (tileScratch.length < uvs1.length) tileScratch = new Float32Array(uvs1.length);
461
+ const out = tileScratch.subarray(0, uvs1.length);
462
+ const add = resolution * resolution * tile;
463
+ for (let i = 0; i < uvs1.length; i += 2) {
464
+ out[i] = uvs1[i] + add;
465
+ out[i + 1] = uvs1[i + 1];
466
+ }
467
+ return out;
468
+ }
469
+ function colliderFor(rt, tree, kind, n) {
470
+ if (kind !== TERRAIN || tree.depth[n] !== rt.colliderDepth) return void 0;
471
+ const chunk = collidersOf(rt).chunks.get(
472
+ keyString(tree.face[n], tree.depth[n], tree.x[n], tree.y[n])
473
+ );
474
+ return chunk && chunk.version === rt.version ? chunk : void 0;
475
+ }
476
+ let order = new Int32Array(256);
477
+ const best = new Int32Array(64);
478
+ function queueJobs(world, rt, pr, tree, kind, sel, frame, budget, pool, hold) {
479
+ const count = sel.requestedCount;
480
+ if (order.length < count) order = new Int32Array(count * 2);
481
+ const k = Math.max(0, Math.min(budget, best.length));
482
+ let chunks = 0;
483
+ let top = 0;
484
+ for (let i = 0; i < count; i++) {
485
+ const n = sel.requested[i];
486
+ if (tree.flags[n] & NODE_READY) continue;
487
+ const existing = tree.slot[n];
488
+ if (existing !== NONE && pr.slots[existing]?.pending) continue;
489
+ if (!hold && colliderFor(rt, tree, kind, n)) {
490
+ order[chunks++] = n;
491
+ continue;
492
+ }
493
+ const p = tree.priority[n];
494
+ if (k === 0 || top === k && tree.priority[best[top - 1]] >= p) continue;
495
+ let j = top < k ? top++ : k - 1;
496
+ while (j > 0 && tree.priority[best[j - 1]] < p) {
497
+ best[j] = best[j - 1];
498
+ j--;
499
+ }
500
+ best[j] = n;
501
+ }
502
+ let queued = 0;
503
+ for (let i = 0; i < chunks + top; i++) {
504
+ const n = i < chunks ? order[i] : best[i - chunks];
505
+ const existing = tree.slot[n];
506
+ const chunk = i < chunks ? colliderFor(rt, tree, kind, n) : void 0;
507
+ const needed = tree.neededAt[n] >= frame;
508
+ const slot = existing !== NONE ? pr.slots[existing] : acquireSlot(world, rt, pr, kind, frame, pool, needed);
509
+ if (!slot && !needed) continue;
510
+ if (!slot) break;
511
+ if (existing === NONE) assign(world, rt, slot, tree, n);
512
+ if (chunk) {
513
+ useCollider(world, rt, pr, slot, tree, n, chunk);
514
+ tree.flags[n] |= NODE_READY;
515
+ tree.gen[n] = rt.version;
516
+ tree.setHeights(n, chunk.mesh.minHeight, chunk.mesh.maxHeight);
517
+ tree.bounds(n, slot.center, slot.mesh.bounds);
518
+ pr.stats.fromColliders++;
519
+ continue;
520
+ }
521
+ queueJob(rt, pr, slot, tree, kind, n);
522
+ queued++;
523
+ }
524
+ for (let r = 0; r < sel.renderedCount; r++) {
525
+ const n = sel.rendered[r];
526
+ if (tree.gen[n] === rt.version) continue;
527
+ const slot = pr.slots[tree.slot[n]];
528
+ if (!slot) continue;
529
+ if (!hold) {
530
+ const chunk = colliderFor(rt, tree, kind, n);
531
+ if (chunk) {
532
+ useCollider(world, rt, pr, slot, tree, n, chunk);
533
+ tree.gen[n] = rt.version;
534
+ continue;
535
+ }
536
+ }
537
+ if (!slot.pending) queueJob(rt, pr, slot, tree, kind, n);
538
+ for (let p = tree.parent[n]; p !== NONE; p = tree.parent[p]) {
539
+ if (tree.gen[p] === rt.version || !(tree.flags[p] & NODE_READY)) continue;
540
+ const ps = pr.slots[tree.slot[p]];
541
+ if (ps && !ps.pending) queueJob(rt, pr, ps, tree, kind, p);
542
+ }
543
+ }
544
+ return queued;
545
+ }
546
+ const jobPoints = createChunkPoints();
547
+ function jobGrid(rt, slot, tree, n, resolution, programs, params, snap) {
548
+ const s = rt.settings;
549
+ const pts = prepareChunkPoints(
550
+ tree.face[n],
551
+ tree.depth[n],
552
+ tree.x[n],
553
+ tree.y[n],
554
+ resolution,
555
+ s.radius,
556
+ jobPoints,
557
+ snap
558
+ );
559
+ const count = pts.count;
560
+ const points = new Float32Array(count * POINT_FLOATS);
561
+ const u32 = new Uint32Array(points.buffer);
562
+ const sx = s.shape[0];
563
+ const sy = s.shape[1];
564
+ const sz = s.shape[2];
565
+ for (let k = 0; k < count; k++) {
566
+ const o = k * POINT_FLOATS;
567
+ const dx = pts.dirs[k * 3];
568
+ const dy = pts.dirs[k * 3 + 1];
569
+ const dz = pts.dirs[k * 3 + 2];
570
+ points[o] = pts.local[k * 3];
571
+ points[o + 1] = pts.local[k * 3 + 1];
572
+ points[o + 2] = pts.local[k * 3 + 2];
573
+ points[o + 3] = dx * sx * s.radius - slot.center[0];
574
+ points[o + 4] = dy * sy * s.radius - slot.center[1];
575
+ points[o + 5] = dz * sz * s.radius - slot.center[2];
576
+ points[o + 6] = dx;
577
+ points[o + 7] = dy;
578
+ points[o + 8] = dz;
579
+ u32[o + 9] = pts.group[k];
580
+ }
581
+ const groups = pts.groups;
582
+ const origins = (program) => {
583
+ if (!program) return new Int32Array(0);
584
+ const len = Math.max(8, program.zeroOrigins.length);
585
+ const out = new Int32Array(groups * len);
586
+ for (let g = 0; g < groups; g++) {
587
+ computeOrigins(
588
+ program.terms,
589
+ pts.origins.subarray(g * 4, g * 4 + 4),
590
+ out.subarray(g * len, g * len + program.zeroOrigins.length)
591
+ );
592
+ }
593
+ return out;
594
+ };
595
+ const buffer = new ArrayBuffer(PARAMS_BYTES);
596
+ const pu = new Uint32Array(buffer);
597
+ const pf = new Float32Array(buffer);
598
+ pu[0] = count;
599
+ pu[1] = pts.side;
600
+ pu[5] = s.seed;
601
+ pf[8] = sx;
602
+ pf[9] = sy;
603
+ pf[10] = sz;
604
+ pf[11] = s.heightScale;
605
+ params(pts, pu, pf);
606
+ return {
607
+ count,
608
+ points,
609
+ originsH: origins(programs[0]),
610
+ originsT: origins(programs[1]),
611
+ originsM: origins(programs[2]),
612
+ params: buffer
613
+ };
614
+ }
615
+ function tileSnap(rt, depth) {
616
+ const size = rt.spacing(depth) * (rt.settings.resolution - 1);
617
+ let snap = SNAP;
618
+ while (snap * 4 < size) snap *= 2;
619
+ return snap;
620
+ }
621
+ function queueJob(rt, pr, slot, tree, kind, n, normalsOnly = false) {
622
+ const s = rt.settings;
623
+ const depth = tree.depth[n];
624
+ const height = rt.height?.program;
625
+ const temperature = kind === TERRAIN && rt.climate ? rt.climate.programFor("temperature") : void 0;
626
+ const moisture = kind === TERRAIN && rt.climate ? rt.climate.programFor("moisture") : void 0;
627
+ const vertexGrid = normalsOnly ? void 0 : jobGrid(rt, slot, tree, n, s.resolution, [height, temperature, moisture], (_, pu, pf) => {
628
+ pu[2] = s.resolution;
629
+ pu[3] = s.resolution * s.resolution;
630
+ pu[4] = 4 * (s.resolution - 1);
631
+ pu[6] = (height ? GEN_HEIGHT : 0) | (temperature ? GEN_CLIMATE : 0) | (depth === 0 ? GEN_ROOT : 0) | (kind === OCEAN ? GEN_OCEAN : 0);
632
+ pu[7] = slot.id;
633
+ pf[12] = kind === OCEAN ? s.seaLevel : 0;
634
+ pf[13] = depth === 0 ? 0 : (kind === TERRAIN ? rt.errors : rt.oceanErrors)[depth];
635
+ pf[14] = skirtDepth(rt, depth);
636
+ });
637
+ const tile = tileGeometry(s.resolution);
638
+ const fine = kind === TERRAIN && tile.size > 0 ? jobGrid(
639
+ rt,
640
+ slot,
641
+ tree,
642
+ n,
643
+ tile.size,
644
+ [height],
645
+ (_, pu) => {
646
+ const cell = slot.id % tile.perLayer;
647
+ pu[2] = tile.size;
648
+ pu[3] = cell % tile.perRow * tile.size;
649
+ pu[4] = Math.floor(cell / tile.perRow) * tile.size;
650
+ pu[6] = height ? GEN_HEIGHT : 0;
651
+ pu[7] = Math.floor(slot.id / tile.perLayer);
652
+ },
653
+ tileSnap(rt, depth)
654
+ ) : void 0;
655
+ if (normalsOnly && !fine) return;
656
+ slot.pending = true;
657
+ slot.jobId = pr.nextJobId();
658
+ const empty = new Int32Array(0);
659
+ pr.jobs.push({
660
+ slot,
661
+ id: slot.jobId,
662
+ tree,
663
+ node: n,
664
+ kind,
665
+ version: rt.version,
666
+ normalsOnly,
667
+ count: vertexGrid?.count ?? 0,
668
+ points: vertexGrid?.points ?? new Float32Array(0),
669
+ originsH: vertexGrid?.originsH ?? empty,
670
+ originsT: vertexGrid?.originsT ?? empty,
671
+ originsM: vertexGrid?.originsM ?? empty,
672
+ params: vertexGrid?.params ?? new ArrayBuffer(PARAMS_BYTES),
673
+ fine
674
+ });
675
+ }
676
+ const FADE = 0.5;
677
+ function showSelected(world, rt, pr, frame) {
678
+ const dt = world.tryResource(Time)?.delta ?? 1 / 60;
679
+ const trees = [rt.tree, rt.ocean];
680
+ const selections = [rt.selection, rt.oceanSelection];
681
+ for (let kind = 0; kind < 2; kind++) {
682
+ const tree = trees[kind];
683
+ const sel = selections[kind];
684
+ if (kind === OCEAN && !rt.settings.ocean) continue;
685
+ const last = pr.lastStamp[kind];
686
+ for (let r = 0; r < sel.renderedCount; r++) {
687
+ const n = sel.rendered[r];
688
+ const slot = pr.slots[tree.slot[n]];
689
+ if (!slot) continue;
690
+ if (kind === TERRAIN && pr.shownVersion === rt.version) {
691
+ const chunk = colliderFor(rt, tree, TERRAIN, n);
692
+ if (chunk) useCollider(world, rt, pr, slot, tree, n, chunk);
693
+ }
694
+ let fade = slot.fade;
695
+ if (!slot.shown) {
696
+ const parent = tree.parent[n];
697
+ fade = parent !== NONE && tree.rendered[parent] === last ? 1 : 0;
698
+ } else if (fade > 0) fade = Math.max(0, fade - dt / FADE);
699
+ setShown(world, slot, true, frame);
700
+ const mask = tree.mask[n];
701
+ const l = tree.locks;
702
+ const o = n * 4;
703
+ let bits = 0;
704
+ let stitch = 0;
705
+ for (let e = 0; e < 4; e++) {
706
+ const lock = l[o + e];
707
+ bits |= (lock < 0 ? 0 : lock === 0 ? 1 : 2) << e * 2;
708
+ if (lock === 1) stitch |= 1 << e;
709
+ }
710
+ setIndices(rt, slot, mask, stitch);
711
+ const packed = fade + 2 * mask;
712
+ if (bits !== slot.bits || fade !== slot.fade || packed !== slot.packed) {
713
+ if (bits !== slot.bits) pr.stats.locks++;
714
+ slot.bits = bits;
715
+ slot.fade = fade;
716
+ slot.packed = packed;
717
+ world.set(slot.entity, InstanceData, { x: packed, y: bits });
718
+ }
719
+ }
720
+ pr.lastStamp[kind] = sel.stamp;
721
+ }
722
+ for (const slot of pr.slots) {
723
+ if (slot.used === frame || !slot.shown) continue;
724
+ const tree = slot.kind === TERRAIN ? rt.tree : rt.ocean;
725
+ const sel = slot.kind === TERRAIN ? rt.selection : rt.oceanSelection;
726
+ if (slot.node === NONE || tree.rendered[slot.node] !== sel.stamp)
727
+ setShown(world, slot, false, frame);
728
+ }
729
+ }
730
+ const origin = new Float64Array(3);
731
+ function updateMaterials(world, rt, pr) {
732
+ const s = rt.settings;
733
+ if (pr.biomeVersion !== rt.biomeVersion) {
734
+ pr.biomeVersion = rt.biomeVersion;
735
+ const table = rt.table;
736
+ const width = 6;
737
+ const data = new Float32Array(width * table.count * 4);
738
+ for (let b = 0; b < table.count; b++) {
739
+ const o = b * width * 4;
740
+ data.set(table.ranges.subarray(b * 12, b * 12 + 8), o);
741
+ data.set(table.tints.subarray(b * 4, b * 4 + 4), o + 8);
742
+ data[o + 12] = table.ranges[b * 12 + 8];
743
+ data[o + 13] = table.ranges[b * 12 + 9];
744
+ data.set(table.layers.subarray(b * 8, b * 8 + 4), o + 16);
745
+ for (let l = 0; l < 4; l++) {
746
+ const scale = table.layers[b * 8 + 4 + l];
747
+ data[o + 20 + l] = TEXTURE_PERIOD / Math.max(1, Math.round(TEXTURE_PERIOD / scale));
748
+ }
749
+ }
750
+ const bytes = new Uint8Array(toHalf(data).buffer);
751
+ const texture = Texture.create({
752
+ width,
753
+ height: table.count,
754
+ format: "rgba16float",
755
+ usage: "data",
756
+ mips: [bytes]
757
+ });
758
+ const textures = world.initResource(Textures);
759
+ if (pr.biomeRef?.guid) textures.delete(pr.biomeRef.guid);
760
+ pr.biomeTexture = texture;
761
+ pr.biomeRef = textures.add(texture, "terrain/biomes");
762
+ const v = pr.material.value;
763
+ v.biomeTable = pr.biomeRef;
764
+ v.albedoArray = rt.set?.albedo ?? null;
765
+ v.normalArray = rt.set?.normal ?? null;
766
+ v.ormArray = rt.set?.orm ?? null;
767
+ let farthest = 4;
768
+ for (let b = 0; b < table.count; b++)
769
+ for (let l = 0; l < 4; l++) farthest = Math.max(farthest, table.layers[b * 8 + 4 + l]);
770
+ v.biomeParams = [table.count, table.latitudeBias, table.snowLine, TEXTURE_PERIOD];
771
+ v.debugParams = [debugMode(world), Math.max(200, farthest * 50), rt.set?.orm ? 1 : 0, 0];
772
+ pr.material.version++;
773
+ }
774
+ const toPlanet = rt.frame.toPlanet;
775
+ rt.frame.pointToOrigin(0, 0, 0, origin);
776
+ const period = TEXTURE_PERIOD;
777
+ const wrap = (v) => (v % period + period) % period;
778
+ for (const material of [pr.material, pr.ocean]) {
779
+ const v = material.value;
780
+ v.center = [origin[0], origin[1], origin[2], s.radius];
781
+ v.rot0 = [toPlanet[0], toPlanet[1], toPlanet[2], wrap(toPlanet[3])];
782
+ v.rot1 = [toPlanet[4], toPlanet[5], toPlanet[6], wrap(toPlanet[7])];
783
+ v.rot2 = [toPlanet[8], toPlanet[9], toPlanet[10], wrap(toPlanet[11])];
784
+ v.camera = [pr.camera[0], pr.camera[1], pr.camera[2], pr.camera[3]];
785
+ const tile = tileGeometry(s.resolution);
786
+ const on = pr.tiles !== void 0 && material === pr.material && TerrainDebug.normalTiles;
787
+ v.tiles = [s.resolution, on ? tile.size : 0, tile.perRow, TILE_LAYER];
788
+ }
789
+ const debug = debugMode(world);
790
+ const dp = pr.material.value.debugParams;
791
+ if (dp[0] !== debug) {
792
+ dp[0] = debug;
793
+ pr.material.version++;
794
+ }
795
+ }
796
+ const TerrainDebug = {
797
+ mode: 0,
798
+ /** False shades from vertex normals even where normal tiles exist (comparisons, tests). */
799
+ normalTiles: true
800
+ };
801
+ function debugMode(world) {
802
+ const o = world.tryResource(DebugOverlays);
803
+ if (o && isOverlayOn(o, "terrain-lod")) return 3;
804
+ if (o && isOverlayOn(o, "terrain-biomes")) return 1;
805
+ return TerrainDebug.mode;
806
+ }
807
+ function planetGpu(pr, gpu) {
808
+ if (!pr.gpu || pr.gpu.gpu !== gpu || pr.gpu.generation !== gpu.generation) {
809
+ pr.gpu = { gpu, generation: gpu.generation, resources: [], readbacks: [] };
810
+ }
811
+ return pr.gpu;
812
+ }
813
+ function gridResources(d, label) {
814
+ return {
815
+ params: d.createBuffer({
816
+ label: `${label}/params`,
817
+ size: PARAMS_BYTES,
818
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
819
+ }),
820
+ points: d.createBuffer({
821
+ label: `${label}/points`,
822
+ size: 16,
823
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
824
+ }),
825
+ pointsSize: 16,
826
+ values: d.createBuffer({ label: `${label}/values`, size: 16, usage: GPUBufferUsage.STORAGE }),
827
+ valuesSize: 16,
828
+ origins: [void 0, void 0, void 0],
829
+ originSizes: [0, 0, 0]
830
+ };
831
+ }
832
+ function jobResources(pg, index) {
833
+ let r = pg.resources[index];
834
+ if (!r) {
835
+ const d = pg.gpu.device;
836
+ r = {
837
+ ...gridResources(d, "terrain"),
838
+ stats: d.createBuffer({
839
+ label: "terrain/stats",
840
+ size: 16,
841
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
842
+ }),
843
+ fine: gridResources(d, "terrain/tile")
844
+ };
845
+ pg.resources[index] = r;
846
+ }
847
+ return r;
848
+ }
849
+ function uploadGrid(pg, r, grid) {
850
+ const device = pg.gpu.device;
851
+ const storage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
852
+ const p = sized(pg, r.points, grid.points.byteLength, r.pointsSize, storage, "terrain/points");
853
+ r.points = p.buffer;
854
+ r.pointsSize = p.size;
855
+ const v = sized(
856
+ pg,
857
+ r.values,
858
+ grid.count * 12,
859
+ r.valuesSize,
860
+ GPUBufferUsage.STORAGE,
861
+ "terrain/values"
862
+ );
863
+ r.values = v.buffer;
864
+ r.valuesSize = v.size;
865
+ device.queue.writeBuffer(r.params, 0, grid.params);
866
+ device.queue.writeBuffer(r.points, 0, grid.points);
867
+ const entries = [
868
+ { binding: 0, resource: { buffer: r.params } },
869
+ { binding: 1, resource: { buffer: r.points } },
870
+ { binding: 2, resource: { buffer: r.values } }
871
+ ];
872
+ const origins = [grid.originsH, grid.originsT, grid.originsM];
873
+ for (let i = 0; i < 3; i++) {
874
+ const data = origins[i];
875
+ if (data.length === 0) continue;
876
+ const o = sized(
877
+ pg,
878
+ r.origins[i],
879
+ data.byteLength,
880
+ r.originSizes[i],
881
+ storage,
882
+ "terrain/origins"
883
+ );
884
+ r.origins[i] = o.buffer;
885
+ r.originSizes[i] = o.size;
886
+ device.queue.writeBuffer(o.buffer, 0, data);
887
+ entries.push({ binding: 3 + i, resource: { buffer: o.buffer } });
888
+ }
889
+ return entries;
890
+ }
891
+ function ensureTiles(world, rt, pr, encoder, after) {
892
+ const tile = tileGeometry(rt.settings.resolution);
893
+ if (tile.size === 0) return void 0;
894
+ const layers = Math.max(1, Math.ceil(pr.slots.length / tile.perLayer));
895
+ const assets = world.resource(GpuAssetsResource);
896
+ if (pr.tiles && pr.tiles.layers >= layers) return assets.texture(pr.tiles)?.texture;
897
+ const next = Texture.gpu({ width: TILE_LAYER, height: TILE_LAYER, layers, format: "rgba8unorm" });
898
+ const handle = assets.texture(next).texture;
899
+ const textures = world.initResource(Textures);
900
+ const old = pr.tiles;
901
+ if (old) {
902
+ const from = assets.texture(old)?.texture;
903
+ if (from) {
904
+ encoder.copyTextureToTexture(
905
+ { texture: from },
906
+ { texture: handle },
907
+ { width: TILE_LAYER, height: TILE_LAYER, depthOrArrayLayers: old.layers }
908
+ );
909
+ }
910
+ const ref = pr.tilesRef;
911
+ after(() => {
912
+ if (ref?.guid) textures.delete(ref.guid);
913
+ });
914
+ }
915
+ pr.tiles = next;
916
+ pr.tilesRef = textures.add(next, "terrain/normal-tiles");
917
+ pr.material.value.normalTiles = pr.tilesRef;
918
+ pr.material.version++;
919
+ return handle;
920
+ }
921
+ function sized(pg, buffer, size, have, usage, label) {
922
+ if (buffer && have >= size) return { buffer, size: have };
923
+ buffer?.destroy();
924
+ let n = Math.max(16, have);
925
+ while (n < size) n *= 2;
926
+ return { buffer: pg.gpu.device.createBuffer({ label, size: n, usage }), size: n };
927
+ }
928
+ const statsInit = new Int32Array([2147483647, -2147483647, 0, 0]);
929
+ function kernelsOf(rt, pr) {
930
+ if (!pr.kernels || pr.kernels.version !== rt.version) {
931
+ const terrain = [];
932
+ const ocean = [];
933
+ if (rt.height) {
934
+ terrain.push(kernelGraph(rt.height));
935
+ ocean.push(kernelGraph(rt.height));
936
+ }
937
+ if (rt.climate) {
938
+ terrain.push(kernelGraph(rt.climate, "temperature"), kernelGraph(rt.climate, "moisture"));
939
+ }
940
+ pr.kernels = { version: rt.version, terrain, ocean };
941
+ }
942
+ return pr.kernels;
943
+ }
944
+ function encodePlanet(world, rt, pr, encoder, gpu, after, timestamps) {
945
+ const library = world.resource(Shaders);
946
+ const assets = world.resource(GpuAssetsResource);
947
+ const pg = planetGpu(pr, gpu);
948
+ const k = kernelsOf(rt, pr);
949
+ const pipelines = [];
950
+ let tilePipelines = [void 0, void 0];
951
+ for (const kind of [TERRAIN, OCEAN]) {
952
+ const graphs = kind === TERRAIN ? k.terrain : k.ocean;
953
+ const height = rt.height ? graphs[0] : void 0;
954
+ const temperature = kind === TERRAIN && rt.climate ? graphs[rt.height ? 1 : 0] : void 0;
955
+ const moisture = kind === TERRAIN && rt.climate ? graphs[rt.height ? 2 : 1] : void 0;
956
+ const key = [height?.hash, temperature?.hash, moisture?.hash].map((h) => h ?? "-").join("_").replace(/[^a-z0-9_]/gi, "");
957
+ const roots = registerKernel(library, key, height, temperature, moisture);
958
+ const sampleModule = library.module(gpu, { root: roots.sample, label: "terrain sample" });
959
+ const vertexModule = library.module(gpu, { root: roots.vertices, label: "terrain vertices" });
960
+ if (kind === TERRAIN) {
961
+ const hKey = (height?.hash ?? "-").replace(/[^a-z0-9_]/gi, "");
962
+ const tileRoots = registerKernel(library, `${hKey}_h`, height, void 0, void 0);
963
+ const tileSample = library.module(gpu, {
964
+ root: tileRoots.sample,
965
+ label: "terrain tile sample"
966
+ });
967
+ const normals = library.module(gpu, { root: tileRoots.normals, label: "terrain normals" });
968
+ tilePipelines = [
969
+ tileSample ? gpu.pipelines.compute({
970
+ label: `terrain/tile-sample/${hKey}`,
971
+ layout: "auto",
972
+ compute: { module: tileSample, entryPoint: "main" }
973
+ }) : void 0,
974
+ normals ? gpu.pipelines.compute({
975
+ label: "terrain/normals",
976
+ layout: "auto",
977
+ compute: { module: normals, entryPoint: "main" }
978
+ }) : void 0
979
+ ];
980
+ }
981
+ pipelines[kind] = [
982
+ sampleModule ? gpu.pipelines.compute({
983
+ label: `terrain/sample/${key}`,
984
+ layout: "auto",
985
+ compute: { module: sampleModule, entryPoint: "main" }
986
+ }) : void 0,
987
+ vertexModule ? gpu.pipelines.compute({
988
+ label: "terrain/vertices",
989
+ layout: "auto",
990
+ compute: { module: vertexModule, entryPoint: "main" }
991
+ }) : void 0
992
+ ];
993
+ }
994
+ const ocean = rt.settings.ocean;
995
+ const tilesOn = tileGeometry(rt.settings.resolution).size > 0;
996
+ if (pipelines[TERRAIN][0] && pipelines[TERRAIN][1] && (!tilesOn || tilePipelines[0] && tilePipelines[1]) && (!ocean || pipelines[OCEAN][0] && pipelines[OCEAN][1]))
997
+ pr.kernelsReady = rt.version;
998
+ const tiles = tilesOn ? ensureTiles(world, rt, pr, encoder, after) : void 0;
999
+ const tile = tileGeometry(rt.settings.resolution);
1000
+ const stride = slotStride(chunkLayout(rt.settings.resolution).vertexCount);
1001
+ const device = gpu.device;
1002
+ let index = 0;
1003
+ const remaining = [];
1004
+ let pass;
1005
+ const copies = [];
1006
+ const readbacks = [];
1007
+ for (const job of pr.jobs) {
1008
+ const [sample, vertices] = pipelines[job.kind];
1009
+ if (job.slot.node !== job.node || job.slot.jobId !== job.id || job.version !== rt.version) {
1010
+ if (job.slot.jobId === job.id) job.slot.pending = false;
1011
+ continue;
1012
+ }
1013
+ const [tileSample, normals] = tilePipelines;
1014
+ const needsTile = job.fine !== void 0 && tiles !== void 0;
1015
+ if (!job.normalsOnly && (!sample || !vertices) || needsTile && (!tileSample || !normals) || job.version !== pr.shownVersion) {
1016
+ remaining.push(job);
1017
+ continue;
1018
+ }
1019
+ const r = jobResources(pg, index++);
1020
+ pass ??= encoder.beginComputePass({
1021
+ label: "terrain/generate",
1022
+ timestampWrites: timestamps("terrain/generate")
1023
+ });
1024
+ if (needsTile) {
1025
+ const fine = job.fine;
1026
+ const entries = uploadGrid(pg, r.fine, fine);
1027
+ pass.setPipeline(tileSample);
1028
+ pass.setBindGroup(
1029
+ 0,
1030
+ device.createBindGroup({
1031
+ label: "terrain/tile-sample",
1032
+ layout: tileSample.getBindGroupLayout(0),
1033
+ entries
1034
+ })
1035
+ );
1036
+ pass.dispatchWorkgroups(Math.ceil(fine.count / 64));
1037
+ const layer = Math.floor(job.slot.id / tile.perLayer);
1038
+ pass.setPipeline(normals);
1039
+ pass.setBindGroup(
1040
+ 0,
1041
+ device.createBindGroup({
1042
+ label: "terrain/normals",
1043
+ layout: normals.getBindGroupLayout(0),
1044
+ entries: [
1045
+ entries[0],
1046
+ entries[1],
1047
+ entries[2],
1048
+ {
1049
+ binding: 3,
1050
+ resource: tiles.createView({
1051
+ dimension: "2d",
1052
+ baseArrayLayer: layer,
1053
+ arrayLayerCount: 1
1054
+ })
1055
+ }
1056
+ ]
1057
+ })
1058
+ );
1059
+ pass.dispatchWorkgroups(Math.ceil(tile.size * tile.size / 64));
1060
+ }
1061
+ if (job.normalsOnly) {
1062
+ job.slot.pending = false;
1063
+ continue;
1064
+ }
1065
+ device.queue.writeBuffer(r.stats, 0, statsInit);
1066
+ const sampleEntries = uploadGrid(pg, r, job);
1067
+ const gm = assets.mesh(job.slot.mesh);
1068
+ const base = gm.baseVertex;
1069
+ const sampleGroup = device.createBindGroup({
1070
+ label: "terrain/sample",
1071
+ layout: sample.getBindGroupLayout(0),
1072
+ entries: sampleEntries
1073
+ });
1074
+ const vertexGroup = device.createBindGroup({
1075
+ label: "terrain/vertices",
1076
+ layout: vertices.getBindGroupLayout(0),
1077
+ entries: [
1078
+ { binding: 0, resource: { buffer: r.params } },
1079
+ { binding: 1, resource: { buffer: r.points } },
1080
+ { binding: 2, resource: { buffer: r.values } },
1081
+ { binding: 3, resource: { buffer: gm.positions, offset: base * 12, size: stride * 12 } },
1082
+ { binding: 4, resource: { buffer: gm.normals, offset: base * 12, size: stride * 12 } },
1083
+ { binding: 5, resource: { buffer: gm.uvs, offset: base * 8, size: stride * 8 } },
1084
+ { binding: 6, resource: { buffer: gm.uvs1, offset: base * 8, size: stride * 8 } },
1085
+ { binding: 7, resource: { buffer: gm.tangents, offset: base * 16, size: stride * 16 } },
1086
+ { binding: 8, resource: { buffer: r.stats } }
1087
+ ]
1088
+ });
1089
+ pass.setPipeline(sample);
1090
+ pass.setBindGroup(0, sampleGroup);
1091
+ pass.dispatchWorkgroups(Math.ceil(job.count / 64));
1092
+ pass.setPipeline(vertices);
1093
+ pass.setBindGroup(0, vertexGroup);
1094
+ const n = rt.settings.resolution;
1095
+ pass.dispatchWorkgroups(Math.ceil(n * n / 64));
1096
+ const readback = pg.readbacks.pop() ?? device.createBuffer({
1097
+ label: "terrain/readback",
1098
+ size: 16,
1099
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
1100
+ });
1101
+ copies.push(r);
1102
+ readbacks.push(readback);
1103
+ const { tree, node, slot } = job;
1104
+ tree.flags[node] |= NODE_READY;
1105
+ tree.gen[node] = job.version;
1106
+ slot.pending = false;
1107
+ slot.gpuNode = node;
1108
+ slot.gpuVersion = job.version;
1109
+ slot.source = 0;
1110
+ slot.cpu = void 0;
1111
+ pr.stats.generated++;
1112
+ const id = job.id;
1113
+ const version = job.version;
1114
+ after(() => {
1115
+ readback.mapAsync(GPUMapMode.READ).then(
1116
+ () => {
1117
+ const stats = new Int32Array(readback.getMappedRange().slice(0, 16));
1118
+ readback.unmap();
1119
+ pg.readbacks.push(readback);
1120
+ pr.stats.readbacks++;
1121
+ if (slot.jobId !== id || slot.node !== node || rt.version !== version) return;
1122
+ if (stats[0] > stats[1]) return;
1123
+ tree.setHeights(node, stats[0] / 1e3, stats[1] / 1e3);
1124
+ tree.bounds(node, slot.center, slot.mesh.bounds);
1125
+ },
1126
+ () => {
1127
+ }
1128
+ );
1129
+ });
1130
+ }
1131
+ if (pass) {
1132
+ pass.end();
1133
+ for (let i = 0; i < copies.length; i++)
1134
+ encoder.copyBufferToBuffer(copies[i].stats, 0, readbacks[i], 0, 16);
1135
+ }
1136
+ pr.stats.lastFrameJobs = index;
1137
+ if (index > 0) pr.jobsPerPass = pr.jobsPerPass === 0 ? index : pr.jobsPerPass * 0.9 + index * 0.1;
1138
+ pr.jobs = remaining;
1139
+ }
1140
+ const ownBytes = /* @__PURE__ */ new Map();
1141
+ function writeFrameUniforms(world, pr, gpu) {
1142
+ const assets = world.resource(GpuAssetsResource);
1143
+ for (const material of [pr.material, pr.ocean]) {
1144
+ const gm = assets.materials.get(material);
1145
+ const layout = material.type.layout;
1146
+ if (!gm?.ownBuffer || !layout || gm.version !== material.version) continue;
1147
+ let view = ownBytes.get(layout.size);
1148
+ if (!view) {
1149
+ view = new DataView(new ArrayBuffer(layout.size));
1150
+ ownBytes.set(layout.size, view);
1151
+ }
1152
+ layout.write(view, 0, material.value);
1153
+ gpu.device.queue.writeBuffer(gm.ownBuffer, 0, view.buffer, 0, layout.size);
1154
+ }
1155
+ }
1156
+ let lastFrame = -1;
1157
+ function registerNode(app) {
1158
+ const graph = app.world.tryResource(Graph);
1159
+ if (!graph) return;
1160
+ for (const [path, source] of Object.entries(TERRAIN_SHADERS)) {
1161
+ app.world.resource(Shaders).register(path, source, "@aethervtt/shard-terrain");
1162
+ }
1163
+ graph.addNode("terrain/generate", {
1164
+ kind: "raw",
1165
+ phase: RenderPhase.Setup,
1166
+ sideEffects: true,
1167
+ run(ctx) {
1168
+ const world = ctx.world;
1169
+ const frame = world.resource(Time).frame;
1170
+ if (frame === lastFrame) return;
1171
+ lastFrame = frame;
1172
+ const state = world.tryResource(Terrain);
1173
+ if (!state) return;
1174
+ for (const rt of state.planets.values()) {
1175
+ const pr = rt.parts.get("render");
1176
+ if (!pr || !rt.ready) continue;
1177
+ writeFrameUniforms(world, pr, ctx.gpu);
1178
+ if (pr.jobs.length > 0) {
1179
+ const t0 = performance.now();
1180
+ encodePlanet(
1181
+ world,
1182
+ rt,
1183
+ pr,
1184
+ ctx.encoder,
1185
+ ctx.gpu,
1186
+ (fn) => ctx.afterSubmit(fn),
1187
+ (name) => ctx.timestamps(name)
1188
+ );
1189
+ world.tryResource(ProfilerResource)?.record("terrain/encode", performance.now() - t0);
1190
+ }
1191
+ pr.stats.gpuFrames++;
1192
+ }
1193
+ }
1194
+ });
1195
+ }
1196
+ function cleanupRender(world, rt) {
1197
+ const pr = rt.parts.get("render");
1198
+ if (!pr) return;
1199
+ const assets = world.tryResource(GpuAssetsResource);
1200
+ const meshes = world.tryResource(Meshes);
1201
+ for (const arena of pr.arenas) assets?.releaseMesh(arena);
1202
+ for (const slot of pr.slots) {
1203
+ if (world.isAlive(slot.entity)) world.despawn(slot.entity);
1204
+ assets?.releaseMesh(slot.mesh);
1205
+ if (slot.meshRef.guid) meshes?.delete(slot.meshRef.guid);
1206
+ }
1207
+ const materials = world.tryResource(Materials);
1208
+ if (pr.materialRef.guid) materials?.delete(pr.materialRef.guid);
1209
+ if (pr.oceanRef.guid) materials?.delete(pr.oceanRef.guid);
1210
+ if (pr.biomeRef?.guid) world.tryResource(Textures)?.delete(pr.biomeRef.guid);
1211
+ rt.parts.delete("render");
1212
+ }
1213
+ export {
1214
+ PlanetRender,
1215
+ TerrainDebug,
1216
+ cleanupRender,
1217
+ registerNode,
1218
+ renderOf,
1219
+ selectChunks,
1220
+ slotStride,
1221
+ tileGeometry
1222
+ };