@aethervtt/shard-terrain 0.1.0 → 0.2.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.
- package/dist/biomes.d.ts +1 -0
- package/dist/biomes.js +3 -0
- package/dist/chunk.d.ts +0 -44
- package/dist/chunk.js +2 -127
- package/dist/colliders.d.ts +16 -1
- package/dist/colliders.js +39 -24
- package/dist/components.d.ts +5 -0
- package/dist/components.js +7 -2
- package/dist/cube-sphere.d.ts +50 -0
- package/dist/cube-sphere.js +224 -0
- package/dist/grid-mesh.d.ts +49 -0
- package/dist/grid-mesh.js +208 -0
- package/dist/heightfield/bake.d.ts +120 -0
- package/dist/heightfield/bake.js +494 -0
- package/dist/heightfield/colliders.d.ts +39 -0
- package/dist/heightfield/colliders.js +152 -0
- package/dist/heightfield/component.d.ts +14 -0
- package/dist/heightfield/component.js +59 -0
- package/dist/heightfield/deflate.d.ts +11 -0
- package/dist/heightfield/deflate.js +484 -0
- package/dist/heightfield/heightmap.d.ts +27 -0
- package/dist/heightfield/heightmap.js +160 -0
- package/dist/heightfield/kernel.d.ts +277 -0
- package/dist/heightfield/kernel.js +1033 -0
- package/dist/heightfield/material.d.ts +13 -0
- package/dist/heightfield/material.js +394 -0
- package/dist/heightfield/methods.d.ts +104 -0
- package/dist/heightfield/methods.js +218 -0
- package/dist/heightfield/pack.d.ts +59 -0
- package/dist/heightfield/pack.js +127 -0
- package/dist/heightfield/pages.d.ts +91 -0
- package/dist/heightfield/pages.js +244 -0
- package/dist/heightfield/project.d.ts +40 -0
- package/dist/heightfield/project.js +55 -0
- package/dist/heightfield/queries.d.ts +48 -0
- package/dist/heightfield/queries.js +150 -0
- package/dist/heightfield/render.d.ts +108 -0
- package/dist/heightfield/render.js +727 -0
- package/dist/heightfield/root-grid.d.ts +38 -0
- package/dist/heightfield/root-grid.js +109 -0
- package/dist/heightfield/runtime.d.ts +105 -0
- package/dist/heightfield/runtime.js +312 -0
- package/dist/heightfield/source-asset.d.ts +39 -0
- package/dist/heightfield/source-asset.js +372 -0
- package/dist/heightfield/source.d.ts +151 -0
- package/dist/heightfield/source.js +402 -0
- package/dist/heightfield/stack.d.ts +29 -0
- package/dist/heightfield/stack.js +158 -0
- package/dist/heightfield/system.d.ts +22 -0
- package/dist/heightfield/system.js +165 -0
- package/dist/heightfield/testing.d.ts +317 -0
- package/dist/heightfield/testing.js +441 -0
- package/dist/heightfield/worker.js +47 -0
- package/dist/heights.d.ts +8 -3
- package/dist/heights.js +8 -4
- package/dist/index.d.ts +24 -7
- package/dist/index.js +158 -8
- package/dist/lod.d.ts +27 -0
- package/dist/lod.js +23 -1
- package/dist/material.js +2 -1
- package/dist/methods.js +64 -16
- package/dist/overlays.d.ts +2 -0
- package/dist/overlays.js +48 -6
- package/dist/planet.d.ts +2 -7
- package/dist/planet.js +8 -18
- package/dist/plugin.d.ts +10 -3
- package/dist/plugin.js +48 -8
- package/dist/quadtree.d.ts +61 -39
- package/dist/quadtree.js +44 -208
- package/dist/render.d.ts +7 -1
- package/dist/render.js +8 -6
- package/dist/test-planet.d.ts +39 -0
- package/dist/test-planet.js +40 -4
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +42 -2
- package/package.json +16 -16
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { QuadSurface, QuadTree, SelectionParams, SelectionView } from '../quadtree.js';
|
|
2
|
+
/**
|
|
3
|
+
* A heightfield's roots as a quadtree surface (spec 0071): a grid of `rootsX` × `rootsZ` square
|
|
4
|
+
* roots of `rootSize` metres, in the terrain's frame (x along +X, height along +Y, a node's y index
|
|
5
|
+
* along +Z). A node is a box from its square and height range; there's no horizon. Neighbors
|
|
6
|
+
* cross into the next root and stop at the terrain's edge.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RootGrid implements QuadSurface {
|
|
9
|
+
roots: number;
|
|
10
|
+
rootsX: number;
|
|
11
|
+
rootsZ: number;
|
|
12
|
+
rootSize: number;
|
|
13
|
+
/**
|
|
14
|
+
* Called for every new node: may set its height range (`tree.setHeights`-style, flags included)
|
|
15
|
+
* and its ready flag and slot, from what's known of its page.
|
|
16
|
+
*/
|
|
17
|
+
onInit: ((tree: QuadTree, n: number, depth: number, x: number, z: number) => void) | undefined;
|
|
18
|
+
/** Sets the grid. The tree must be reset after. */
|
|
19
|
+
configure(rootsX: number, rootsZ: number, rootSize: number): void;
|
|
20
|
+
grow(): void;
|
|
21
|
+
/** A node's global coordinates at its depth: [x, z]. */
|
|
22
|
+
globalOf(tree: QuadTree, n: number, out: {
|
|
23
|
+
[i: number]: number;
|
|
24
|
+
}): void;
|
|
25
|
+
private readonly g;
|
|
26
|
+
initNode(tree: QuadTree, n: number): void;
|
|
27
|
+
/** The node's square: [x0, z0, size]. */
|
|
28
|
+
private square;
|
|
29
|
+
updateSphere(tree: QuadTree, n: number): void;
|
|
30
|
+
bounds(tree: QuadTree, n: number, center: ArrayLike<number>, out: {
|
|
31
|
+
[i: number]: number;
|
|
32
|
+
}): void;
|
|
33
|
+
/** Outside the frustum: the box's corner furthest along a plane's normal is behind it. */
|
|
34
|
+
culled(tree: QuadTree, n: number, view: SelectionView, _params: SelectionParams): boolean;
|
|
35
|
+
neighbor(root: number, depth: number, x: number, y: number, edge: number, out: {
|
|
36
|
+
[i: number]: number;
|
|
37
|
+
}): boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { EDGE_BOTTOM, EDGE_RIGHT, EDGE_TOP } from "../cube.js";
|
|
2
|
+
class RootGrid {
|
|
3
|
+
roots = 1;
|
|
4
|
+
rootsX = 1;
|
|
5
|
+
rootsZ = 1;
|
|
6
|
+
rootSize = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Called for every new node: may set its height range (`tree.setHeights`-style, flags included)
|
|
9
|
+
* and its ready flag and slot, from what's known of its page.
|
|
10
|
+
*/
|
|
11
|
+
onInit;
|
|
12
|
+
/** Sets the grid. The tree must be reset after. */
|
|
13
|
+
configure(rootsX, rootsZ, rootSize) {
|
|
14
|
+
this.rootsX = rootsX;
|
|
15
|
+
this.rootsZ = rootsZ;
|
|
16
|
+
this.roots = rootsX * rootsZ;
|
|
17
|
+
this.rootSize = rootSize;
|
|
18
|
+
}
|
|
19
|
+
grow() {
|
|
20
|
+
}
|
|
21
|
+
/** A node's global coordinates at its depth: [x, z]. */
|
|
22
|
+
globalOf(tree, n, out) {
|
|
23
|
+
const root = tree.root[n];
|
|
24
|
+
const d = tree.depth[n];
|
|
25
|
+
out[0] = root % this.rootsX * 2 ** d + tree.x[n];
|
|
26
|
+
out[1] = Math.floor(root / this.rootsX) * 2 ** d + tree.y[n];
|
|
27
|
+
}
|
|
28
|
+
g = new Float64Array(2);
|
|
29
|
+
initNode(tree, n) {
|
|
30
|
+
if (!this.onInit) return;
|
|
31
|
+
this.globalOf(tree, n, this.g);
|
|
32
|
+
this.onInit(tree, n, tree.depth[n], this.g[0], this.g[1]);
|
|
33
|
+
}
|
|
34
|
+
/** The node's square: [x0, z0, size]. */
|
|
35
|
+
square(tree, n, out) {
|
|
36
|
+
this.globalOf(tree, n, out);
|
|
37
|
+
const size = this.rootSize / 2 ** tree.depth[n];
|
|
38
|
+
out[0] = out[0] * size;
|
|
39
|
+
out[1] = out[1] * size;
|
|
40
|
+
out[2] = size;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
updateSphere(tree, n) {
|
|
44
|
+
this.square(tree, n, sq);
|
|
45
|
+
const size = sq[2];
|
|
46
|
+
const lo = tree.minH[n];
|
|
47
|
+
const hi = tree.maxH[n];
|
|
48
|
+
const o = n * 4;
|
|
49
|
+
tree.sphere[o] = sq[0] + size / 2;
|
|
50
|
+
tree.sphere[o + 1] = (lo + hi) / 2;
|
|
51
|
+
tree.sphere[o + 2] = sq[1] + size / 2;
|
|
52
|
+
const hy = (hi - lo) / 2;
|
|
53
|
+
tree.sphere[o + 3] = Math.sqrt(size * size * 0.5 + hy * hy) + 1e-3;
|
|
54
|
+
}
|
|
55
|
+
bounds(tree, n, center, out) {
|
|
56
|
+
this.square(tree, n, sq);
|
|
57
|
+
const size = sq[2];
|
|
58
|
+
out[0] = sq[0] - center[0];
|
|
59
|
+
out[1] = tree.minH[n] - center[1];
|
|
60
|
+
out[2] = sq[1] - center[2];
|
|
61
|
+
out[3] = sq[0] + size - center[0];
|
|
62
|
+
out[4] = tree.maxH[n] - center[1];
|
|
63
|
+
out[5] = sq[1] + size - center[2];
|
|
64
|
+
}
|
|
65
|
+
/** Outside the frustum: the box's corner furthest along a plane's normal is behind it. */
|
|
66
|
+
culled(tree, n, view, _params) {
|
|
67
|
+
const s = tree.sphere;
|
|
68
|
+
const o = n * 4;
|
|
69
|
+
const f = view.frustum;
|
|
70
|
+
this.square(tree, n, sq);
|
|
71
|
+
const size = sq[2];
|
|
72
|
+
const lo = tree.minH[n];
|
|
73
|
+
const hi = tree.maxH[n];
|
|
74
|
+
for (let p = 0; p < view.planes; p++) {
|
|
75
|
+
const q = p * 4;
|
|
76
|
+
const nx = f[q];
|
|
77
|
+
const ny = f[q + 1];
|
|
78
|
+
const nz = f[q + 2];
|
|
79
|
+
const dist = nx * s[o] + ny * s[o + 1] + nz * s[o + 2] + f[q + 3];
|
|
80
|
+
if (dist < -s[o + 3]) return true;
|
|
81
|
+
if (dist > s[o + 3]) continue;
|
|
82
|
+
const x = nx >= 0 ? sq[0] + size : sq[0];
|
|
83
|
+
const y = ny >= 0 ? hi : lo;
|
|
84
|
+
const z = nz >= 0 ? sq[1] + size : sq[1];
|
|
85
|
+
if (nx * x + ny * y + nz * z + f[q + 3] < 0) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
neighbor(root, depth, x, y, edge, out) {
|
|
90
|
+
const n = 2 ** depth;
|
|
91
|
+
let gx = root % this.rootsX * n + x;
|
|
92
|
+
let gz = Math.floor(root / this.rootsX) * n + y;
|
|
93
|
+
if (edge === EDGE_BOTTOM) gz--;
|
|
94
|
+
else if (edge === EDGE_RIGHT) gx++;
|
|
95
|
+
else if (edge === EDGE_TOP) gz++;
|
|
96
|
+
else gx--;
|
|
97
|
+
if (gx < 0 || gz < 0 || gx >= this.rootsX * n || gz >= this.rootsZ * n) return false;
|
|
98
|
+
const rx = Math.floor(gx / n);
|
|
99
|
+
const rz = Math.floor(gz / n);
|
|
100
|
+
out[0] = rz * this.rootsX + rx;
|
|
101
|
+
out[1] = gx - rx * n;
|
|
102
|
+
out[2] = gz - rz * n;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const sq = new Float64Array(3);
|
|
107
|
+
export {
|
|
108
|
+
RootGrid
|
|
109
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type AssetRef, type Entity, ShardError, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { PlatformFileSystem, Workers } from '@aethervtt/shard-platform';
|
|
3
|
+
import { PlanetFrame } from '../frame.js';
|
|
4
|
+
import { QuadTree } from '../quadtree.js';
|
|
5
|
+
import { type BakeManifest, type BakeReport } from './bake.js';
|
|
6
|
+
import { type Stack } from './kernel.js';
|
|
7
|
+
import { type PackStore } from './pack.js';
|
|
8
|
+
import { PageStore } from './pages.js';
|
|
9
|
+
import { RootGrid } from './root-grid.js';
|
|
10
|
+
import type { TerrainLayout } from './source.js';
|
|
11
|
+
import { type TerrainSourceAsset } from './source-asset.js';
|
|
12
|
+
/** GPU bytes a page takes in the pool: its texels and its control texels. */
|
|
13
|
+
export declare function pageGpuBytes(layout: TerrainLayout): number;
|
|
14
|
+
/** Coarse levels kept loaded by default: the deepest whose pages, with every coarser level's, fit here. */
|
|
15
|
+
export declare const RESIDENT_BYTES: number;
|
|
16
|
+
export interface HeightfieldSettings {
|
|
17
|
+
source: AssetRef | null;
|
|
18
|
+
errorPixels: number;
|
|
19
|
+
vertexPixels: number;
|
|
20
|
+
colliderRadius: number;
|
|
21
|
+
skirts: boolean;
|
|
22
|
+
residentDepth: number;
|
|
23
|
+
}
|
|
24
|
+
/** Where a terrain is in getting ready to stream. */
|
|
25
|
+
export type BakeState = 'idle' | 'checking' | 'baking' | 'current' | 'failed';
|
|
26
|
+
/**
|
|
27
|
+
* Everything the terrain keeps per `Terrain` entity (spec 0071): its source and compiled stack,
|
|
28
|
+
* its packs (baked on first use when missing or stale), the pages it has read, its quadtree over
|
|
29
|
+
* the root grid with the per-depth errors from the bake, its frame, and its anchors. Colliders and
|
|
30
|
+
* the render side keep their state in `parts`.
|
|
31
|
+
*/
|
|
32
|
+
export declare class HeightfieldRuntime {
|
|
33
|
+
readonly entity: Entity;
|
|
34
|
+
settings: HeightfieldSettings | undefined;
|
|
35
|
+
tick: number;
|
|
36
|
+
asset: TerrainSourceAsset | undefined;
|
|
37
|
+
private assetVersion;
|
|
38
|
+
stack: Stack | undefined;
|
|
39
|
+
layout: TerrainLayout | undefined;
|
|
40
|
+
/** Bumps when what the pages hold changes (a source edit, after its rebake). */
|
|
41
|
+
version: number;
|
|
42
|
+
problem: ShardError | null;
|
|
43
|
+
waiting: string | null;
|
|
44
|
+
store: PackStore | undefined;
|
|
45
|
+
pages: PageStore | undefined;
|
|
46
|
+
manifest: BakeManifest | undefined;
|
|
47
|
+
bake: BakeState;
|
|
48
|
+
lastBake: BakeReport | undefined;
|
|
49
|
+
/** Blocks the last bake rebaked ("bx,bz"), for terrain.map's bake mode. */
|
|
50
|
+
dirtied: string[];
|
|
51
|
+
readonly grid: RootGrid;
|
|
52
|
+
readonly tree: QuadTree;
|
|
53
|
+
readonly selection: import("..").Selection;
|
|
54
|
+
/** Per depth: the measured errors from the bake (selection's table, 0043's convention), and capped. */
|
|
55
|
+
rawErrors: Float32Array;
|
|
56
|
+
errors: Float32Array;
|
|
57
|
+
lodBias: number;
|
|
58
|
+
/** Coarse levels kept loaded; the pages of depths up to it are pinned. */
|
|
59
|
+
residentDepth: number;
|
|
60
|
+
/** Resident pages loaded: the GPU side can draw coarse ground. */
|
|
61
|
+
streaming: boolean;
|
|
62
|
+
readonly frame: PlanetFrame;
|
|
63
|
+
anchorPos: Float64Array<ArrayBuffer>;
|
|
64
|
+
anchorRadius: Float64Array<ArrayBuffer>;
|
|
65
|
+
anchorEntity: Entity[];
|
|
66
|
+
anchors: number;
|
|
67
|
+
readonly parts: Map<string, unknown>;
|
|
68
|
+
/** The source hash the pages and manifest match. */
|
|
69
|
+
private bakedHash;
|
|
70
|
+
private generation;
|
|
71
|
+
constructor(entity: Entity);
|
|
72
|
+
/** Whether colliders and height queries can run: the source and its assets are loaded. */
|
|
73
|
+
get ready(): boolean;
|
|
74
|
+
get lo(): number;
|
|
75
|
+
get hi(): number;
|
|
76
|
+
/** Vertex spacing at a depth (m). */
|
|
77
|
+
spacing(depth: number): number;
|
|
78
|
+
/** Metres a node at `depth` covers. */
|
|
79
|
+
nodeSize(depth: number): number;
|
|
80
|
+
/** New tree nodes take what's known of their page: its exact height range. */
|
|
81
|
+
private initNode;
|
|
82
|
+
/**
|
|
83
|
+
* Brings the runtime up to date with its component and source: reads settings, resolves and
|
|
84
|
+
* compiles the source, and (in the background) checks its packs, bakes them if they're missing
|
|
85
|
+
* or stale, and loads the coarse levels.
|
|
86
|
+
*/
|
|
87
|
+
refresh(world: World, env: {
|
|
88
|
+
fs: PlatformFileSystem | undefined;
|
|
89
|
+
workers: Workers | undefined;
|
|
90
|
+
}): void;
|
|
91
|
+
private report;
|
|
92
|
+
/** Errors capped by vertex spacing (`vertexPixels`), as planets have them. */
|
|
93
|
+
capErrors(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Checks the packs against the source; bakes them when missing or stale (into the project's
|
|
96
|
+
* cache where the file service writes, else in memory); then reads the manifest's errors and
|
|
97
|
+
* loads the coarse levels. A later edit runs it again; pages already drawn are replaced in place.
|
|
98
|
+
*/
|
|
99
|
+
private prepare;
|
|
100
|
+
private readonly g;
|
|
101
|
+
/** A tree node's height range from its pack entry, when known. */
|
|
102
|
+
refreshBounds(n: number): void;
|
|
103
|
+
/** The tree node for a page, if the tree has one at that depth. */
|
|
104
|
+
nodeOf(depth: number, x: number, z: number): number;
|
|
105
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
3
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { PlanetFrame } from "../frame.js";
|
|
5
|
+
import { capErrors } from "../lod.js";
|
|
6
|
+
import { createSelection, NODE_BOUNDS, NODE_READY, QuadTree } from "../quadtree.js";
|
|
7
|
+
import { bakeTerrain, readManifest } from "./bake.js";
|
|
8
|
+
import { Terrain } from "./component.js";
|
|
9
|
+
import { dequantize, LEAF_SIDE, PAGE } from "./kernel.js";
|
|
10
|
+
import { fsPackStore, memoryPackStore } from "./pack.js";
|
|
11
|
+
import { nodeKey, PageStore } from "./pages.js";
|
|
12
|
+
import { stackFor, terrainCacheDir } from "./project.js";
|
|
13
|
+
import { RootGrid } from "./root-grid.js";
|
|
14
|
+
import { TerrainSources } from "./source-asset.js";
|
|
15
|
+
function pageGpuBytes(layout) {
|
|
16
|
+
return LEAF_SIDE * LEAF_SIDE * 4 + (layout.cells + 1) * (layout.cells + 1) * 4;
|
|
17
|
+
}
|
|
18
|
+
const RESIDENT_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
class HeightfieldRuntime {
|
|
20
|
+
entity;
|
|
21
|
+
settings;
|
|
22
|
+
tick = -1;
|
|
23
|
+
asset;
|
|
24
|
+
assetVersion = -1;
|
|
25
|
+
stack;
|
|
26
|
+
layout;
|
|
27
|
+
/** Bumps when what the pages hold changes (a source edit, after its rebake). */
|
|
28
|
+
version = 0;
|
|
29
|
+
problem = null;
|
|
30
|
+
waiting = null;
|
|
31
|
+
store;
|
|
32
|
+
pages;
|
|
33
|
+
manifest;
|
|
34
|
+
bake = "idle";
|
|
35
|
+
lastBake;
|
|
36
|
+
/** Blocks the last bake rebaked ("bx,bz"), for terrain.map's bake mode. */
|
|
37
|
+
dirtied = [];
|
|
38
|
+
grid = new RootGrid();
|
|
39
|
+
tree = new QuadTree(this.grid, 1024);
|
|
40
|
+
selection = createSelection();
|
|
41
|
+
/** Per depth: the measured errors from the bake (selection's table, 0043's convention), and capped. */
|
|
42
|
+
rawErrors = new Float32Array(2);
|
|
43
|
+
errors = new Float32Array(2);
|
|
44
|
+
lodBias = 1;
|
|
45
|
+
/** Coarse levels kept loaded; the pages of depths up to it are pinned. */
|
|
46
|
+
residentDepth = 0;
|
|
47
|
+
/** Resident pages loaded: the GPU side can draw coarse ground. */
|
|
48
|
+
streaming = false;
|
|
49
|
+
frame = new PlanetFrame();
|
|
50
|
+
anchorPos = new Float64Array(0);
|
|
51
|
+
anchorRadius = new Float64Array(0);
|
|
52
|
+
anchorEntity = [];
|
|
53
|
+
anchors = 0;
|
|
54
|
+
parts = /* @__PURE__ */ new Map();
|
|
55
|
+
/** The source hash the pages and manifest match. */
|
|
56
|
+
bakedHash = "";
|
|
57
|
+
generation = 0;
|
|
58
|
+
constructor(entity) {
|
|
59
|
+
this.entity = entity;
|
|
60
|
+
this.grid.onInit = (tree, n, depth, x, z) => this.initNode(tree, n, depth, x, z);
|
|
61
|
+
}
|
|
62
|
+
/** Whether colliders and height queries can run: the source and its assets are loaded. */
|
|
63
|
+
get ready() {
|
|
64
|
+
return this.problem === null && this.stack !== void 0 && this.layout !== void 0;
|
|
65
|
+
}
|
|
66
|
+
get lo() {
|
|
67
|
+
return this.asset?.source.heightRange[0] ?? 0;
|
|
68
|
+
}
|
|
69
|
+
get hi() {
|
|
70
|
+
return this.asset?.source.heightRange[1] ?? 1;
|
|
71
|
+
}
|
|
72
|
+
/** Vertex spacing at a depth (m). */
|
|
73
|
+
spacing(depth) {
|
|
74
|
+
const l = this.layout;
|
|
75
|
+
return l ? l.rootSize / 2 ** depth / PAGE : 1;
|
|
76
|
+
}
|
|
77
|
+
/** Metres a node at `depth` covers. */
|
|
78
|
+
nodeSize(depth) {
|
|
79
|
+
return (this.layout?.rootSize ?? 1) / 2 ** depth;
|
|
80
|
+
}
|
|
81
|
+
/** New tree nodes take what's known of their page: its exact height range. */
|
|
82
|
+
initNode(tree, n, depth, x, z) {
|
|
83
|
+
const e = this.pages?.entry(depth, x, z);
|
|
84
|
+
if (!e) return;
|
|
85
|
+
tree.minH[n] = dequantize(e.min, this.lo, this.hi);
|
|
86
|
+
tree.maxH[n] = dequantize(e.max, this.lo, this.hi);
|
|
87
|
+
tree.flags[n] |= NODE_BOUNDS;
|
|
88
|
+
const ready = this.parts.get("ready");
|
|
89
|
+
if (ready) {
|
|
90
|
+
const slot = ready(nodeKey(depth, x, z));
|
|
91
|
+
if (slot >= 0) {
|
|
92
|
+
tree.flags[n] |= NODE_READY;
|
|
93
|
+
tree.slot[n] = slot;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Brings the runtime up to date with its component and source: reads settings, resolves and
|
|
99
|
+
* compiles the source, and (in the background) checks its packs, bakes them if they're missing
|
|
100
|
+
* or stale, and loads the coarse levels.
|
|
101
|
+
*/
|
|
102
|
+
refresh(world, env) {
|
|
103
|
+
const table = world.entityTable(this.entity);
|
|
104
|
+
const tick = table.changedTicks(Terrain)[world.entityRow(this.entity)];
|
|
105
|
+
let settingsChanged = false;
|
|
106
|
+
if (tick !== this.tick || !this.settings) {
|
|
107
|
+
this.tick = tick;
|
|
108
|
+
const v = world.get(this.entity, Terrain);
|
|
109
|
+
this.settings = {
|
|
110
|
+
source: v.source,
|
|
111
|
+
errorPixels: v.errorPixels,
|
|
112
|
+
vertexPixels: v.vertexPixels,
|
|
113
|
+
colliderRadius: v.colliderRadius,
|
|
114
|
+
skirts: v.skirts,
|
|
115
|
+
residentDepth: v.residentDepth
|
|
116
|
+
};
|
|
117
|
+
settingsChanged = true;
|
|
118
|
+
}
|
|
119
|
+
const ref = this.settings.source;
|
|
120
|
+
if (!ref || ref.guid === void 0 && ref.path === void 0) {
|
|
121
|
+
this.waiting = "a source (Terrain.source)";
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const asset = resolve(world, ref);
|
|
125
|
+
if (!asset) {
|
|
126
|
+
this.waiting = `terrain source ${ref.path ?? ref.guid}`;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (asset !== this.asset || asset.version !== this.assetVersion) {
|
|
130
|
+
try {
|
|
131
|
+
this.stack = stackFor(world, asset);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const e = err;
|
|
134
|
+
if (e.code === "terrain/not-ready") {
|
|
135
|
+
this.waiting = e.path ?? "its assets";
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.report(world, e);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const layoutChanged = JSON.stringify(asset.layout) !== JSON.stringify(this.layout);
|
|
142
|
+
this.asset = asset;
|
|
143
|
+
this.assetVersion = asset.version;
|
|
144
|
+
this.problem = null;
|
|
145
|
+
this.waiting = null;
|
|
146
|
+
if (layoutChanged) {
|
|
147
|
+
this.layout = asset.layout;
|
|
148
|
+
this.grid.configure(asset.layout.rootsX, asset.layout.rootsZ, asset.layout.rootSize);
|
|
149
|
+
this.tree.resetRoots(this.lo, this.hi);
|
|
150
|
+
this.store = env.fs ? fsPackStore(env.fs, terrainCacheDir(guidOf(world, ref) ?? "memory")) : memoryPackStore();
|
|
151
|
+
this.pages = new PageStore(this.store, asset.layout, this.lo, this.hi, env.workers);
|
|
152
|
+
this.manifest = void 0;
|
|
153
|
+
this.streaming = false;
|
|
154
|
+
this.bakedHash = "";
|
|
155
|
+
this.rawErrors = new Float32Array(asset.layout.depth + 2);
|
|
156
|
+
this.errors = new Float32Array(asset.layout.depth + 2);
|
|
157
|
+
for (let d = 0; d <= asset.layout.depth; d++) this.rawErrors[d] = this.spacing(d) * 8;
|
|
158
|
+
settingsChanged = true;
|
|
159
|
+
}
|
|
160
|
+
void this.prepare(world, env);
|
|
161
|
+
}
|
|
162
|
+
if (settingsChanged) this.capErrors();
|
|
163
|
+
}
|
|
164
|
+
report(world, err) {
|
|
165
|
+
this.problem = err;
|
|
166
|
+
this.waiting = null;
|
|
167
|
+
world.tryResource(LogResource)?.error(err);
|
|
168
|
+
}
|
|
169
|
+
/** Errors capped by vertex spacing (`vertexPixels`), as planets have them. */
|
|
170
|
+
capErrors() {
|
|
171
|
+
const s = this.settings;
|
|
172
|
+
if (!s) return;
|
|
173
|
+
this.errors = capErrors(
|
|
174
|
+
this.rawErrors,
|
|
175
|
+
(d) => this.spacing(d),
|
|
176
|
+
s.errorPixels,
|
|
177
|
+
s.vertexPixels,
|
|
178
|
+
this.errors.length === this.rawErrors.length ? this.errors : void 0
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Checks the packs against the source; bakes them when missing or stale (into the project's
|
|
183
|
+
* cache where the file service writes, else in memory); then reads the manifest's errors and
|
|
184
|
+
* loads the coarse levels. A later edit runs it again; pages already drawn are replaced in place.
|
|
185
|
+
*/
|
|
186
|
+
async prepare(world, env) {
|
|
187
|
+
const generation = ++this.generation;
|
|
188
|
+
const asset = this.asset;
|
|
189
|
+
const stack = this.stack;
|
|
190
|
+
const pages = this.pages;
|
|
191
|
+
let store = this.store;
|
|
192
|
+
this.bake = "checking";
|
|
193
|
+
try {
|
|
194
|
+
let manifest = await readManifest(store);
|
|
195
|
+
if (manifest?.sourceHash !== asset.hash) {
|
|
196
|
+
if (!store.writable) {
|
|
197
|
+
store = memoryPackStore();
|
|
198
|
+
this.store = store;
|
|
199
|
+
pages.store = store;
|
|
200
|
+
}
|
|
201
|
+
this.bake = "baking";
|
|
202
|
+
const report = await bakeTerrain(asset, stack, store, {
|
|
203
|
+
...env.workers ? { workers: env.workers } : {},
|
|
204
|
+
warn: (problem) => world.tryResource(LogResource)?.warn(problem.message, { code: problem.code })
|
|
205
|
+
});
|
|
206
|
+
if (generation !== this.generation) return;
|
|
207
|
+
this.lastBake = report;
|
|
208
|
+
this.dirtied = report.dirtied;
|
|
209
|
+
manifest = await readManifest(store);
|
|
210
|
+
}
|
|
211
|
+
if (generation !== this.generation) return;
|
|
212
|
+
if (!manifest)
|
|
213
|
+
throw new ShardError("terrain/bake-failed", "The terrain bake wrote no manifest", {
|
|
214
|
+
hint: "Check the project cache is writable (.shard/cache/terrain), then run `shard terrain bake --force`."
|
|
215
|
+
});
|
|
216
|
+
const rebaked = this.bakedHash !== "" && this.bakedHash !== manifest.sourceHash;
|
|
217
|
+
this.manifest = manifest;
|
|
218
|
+
this.bakedHash = manifest.sourceHash;
|
|
219
|
+
if (rebaked) {
|
|
220
|
+
pages.clear();
|
|
221
|
+
this.version++;
|
|
222
|
+
}
|
|
223
|
+
const D = this.layout.depth;
|
|
224
|
+
const raw = new Float32Array(D + 2);
|
|
225
|
+
for (let d = 1; d <= D + 1; d++) raw[d] = manifest.depthErrors[d - 1] ?? 0;
|
|
226
|
+
raw[0] = raw[1] * 2;
|
|
227
|
+
for (let d = D; d >= 0; d--) raw[d] = Math.max(raw[d], raw[d + 1]);
|
|
228
|
+
this.rawErrors = raw;
|
|
229
|
+
this.capErrors();
|
|
230
|
+
const per = pageGpuBytes(this.layout);
|
|
231
|
+
let resident = this.settings.residentDepth;
|
|
232
|
+
if (resident < 0) {
|
|
233
|
+
resident = 0;
|
|
234
|
+
let bytes = 0;
|
|
235
|
+
for (let d = 0; d <= D; d++) {
|
|
236
|
+
const [nx, nz] = pages.nodesAt(d);
|
|
237
|
+
bytes += nx * nz * per;
|
|
238
|
+
if (bytes > RESIDENT_BYTES) break;
|
|
239
|
+
resident = d;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
this.residentDepth = Math.min(D, resident);
|
|
243
|
+
const loads = [];
|
|
244
|
+
for (let d = 0; d <= this.residentDepth; d++) {
|
|
245
|
+
const [nx, nz] = pages.nodesAt(d);
|
|
246
|
+
for (let z = 0; z < nz; z++) {
|
|
247
|
+
for (let x = 0; x < nx; x++) {
|
|
248
|
+
pages.pin(nodeKey(d, x, z));
|
|
249
|
+
loads.push(pages.load(d, x, z));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
await Promise.all(loads);
|
|
254
|
+
if (generation !== this.generation) return;
|
|
255
|
+
for (let n = 0; n < this.tree.count; n++) this.refreshBounds(n);
|
|
256
|
+
this.bake = "current";
|
|
257
|
+
this.streaming = true;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (generation !== this.generation) return;
|
|
260
|
+
this.bake = "failed";
|
|
261
|
+
this.report(
|
|
262
|
+
world,
|
|
263
|
+
err instanceof ShardError ? err : new ShardError("terrain/bake-failed", String(err))
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
g = new Float64Array(2);
|
|
268
|
+
/** A tree node's height range from its pack entry, when known. */
|
|
269
|
+
refreshBounds(n) {
|
|
270
|
+
this.grid.globalOf(this.tree, n, this.g);
|
|
271
|
+
const e = this.pages?.entry(this.tree.depth[n], this.g[0], this.g[1]);
|
|
272
|
+
if (!e) return;
|
|
273
|
+
this.tree.setHeights(
|
|
274
|
+
n,
|
|
275
|
+
dequantize(e.min, this.lo, this.hi),
|
|
276
|
+
dequantize(e.max, this.lo, this.hi)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
/** The tree node for a page, if the tree has one at that depth. */
|
|
280
|
+
nodeOf(depth, x, z) {
|
|
281
|
+
const l = this.layout;
|
|
282
|
+
if (!l) return -1;
|
|
283
|
+
const rx = x >> depth;
|
|
284
|
+
const rz = z >> depth;
|
|
285
|
+
if (rx >= l.rootsX || rz >= l.rootsZ) return -1;
|
|
286
|
+
const mask = 2 ** depth - 1;
|
|
287
|
+
const n = this.tree.find(rz * l.rootsX + rx, depth, x & mask, z & mask);
|
|
288
|
+
return this.tree.depth[n] === depth ? n : -1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function guidOf(world, ref) {
|
|
292
|
+
if (ref.guid !== void 0) return ref.guid;
|
|
293
|
+
return world.tryResource(TerrainSources) ? assetServer(world).entry(ref)?.guid : void 0;
|
|
294
|
+
}
|
|
295
|
+
function resolve(world, ref) {
|
|
296
|
+
const store = world.initResource(TerrainSources);
|
|
297
|
+
if (ref.guid !== void 0) {
|
|
298
|
+
const v = store.get(ref);
|
|
299
|
+
if (v) return v;
|
|
300
|
+
}
|
|
301
|
+
const server = assetServer(world);
|
|
302
|
+
const entry = server.entry(ref);
|
|
303
|
+
if (!entry) return void 0;
|
|
304
|
+
if (entry.state !== "failed") void server.request(entry.guid).catch(() => {
|
|
305
|
+
});
|
|
306
|
+
return store.get({ guid: entry.guid });
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
HeightfieldRuntime,
|
|
310
|
+
RESIDENT_BYTES,
|
|
311
|
+
pageGpuBytes
|
|
312
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AssetStore } from '@aethervtt/shard-assets';
|
|
2
|
+
import { type TerrainLayout, type TerrainSource } from './source.js';
|
|
3
|
+
/** An asset a terrain source uses: its guid, type, and the hash of its imported contents. */
|
|
4
|
+
export interface SourceDependency {
|
|
5
|
+
guid: string;
|
|
6
|
+
type: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
}
|
|
9
|
+
/** What `*.terrain.json` imports to (spec 0071). */
|
|
10
|
+
export interface TerrainSourceArtifact {
|
|
11
|
+
format: 1;
|
|
12
|
+
source: TerrainSource;
|
|
13
|
+
layout: TerrainLayout;
|
|
14
|
+
/** Every asset it uses, by project path. */
|
|
15
|
+
deps: Record<string, SourceDependency>;
|
|
16
|
+
/** SHA-256 of the source and its dependencies' hashes: changes with anything a bake reads. */
|
|
17
|
+
hash: string;
|
|
18
|
+
}
|
|
19
|
+
/** A loaded terrain source: what a `Terrain` entity bakes and streams. */
|
|
20
|
+
export declare class TerrainSourceAsset {
|
|
21
|
+
source: TerrainSource;
|
|
22
|
+
layout: TerrainLayout;
|
|
23
|
+
deps: Record<string, SourceDependency>;
|
|
24
|
+
hash: string;
|
|
25
|
+
/** Bumps on hot reload. */
|
|
26
|
+
version: number;
|
|
27
|
+
constructor(a: TerrainSourceArtifact);
|
|
28
|
+
}
|
|
29
|
+
export declare const TerrainSources: import("@aethervtt/shard-core").ResourceDef<AssetStore<TerrainSourceAsset, "terrain/TerrainSource">>;
|
|
30
|
+
export declare const TerrainSourceAssetType: import("@aethervtt/shard-assets").AssetTypeDef<TerrainSourceAsset>;
|
|
31
|
+
/**
|
|
32
|
+
* `*.terrain.json`: a heightfield terrain's layer stack (spec 0071), validated with pointers into
|
|
33
|
+
* the file, its assets resolved to guids and recorded as import dependencies (editing a noise graph
|
|
34
|
+
* or heightmap re-imports it, and the next bake rebakes the blocks it touches).
|
|
35
|
+
*/
|
|
36
|
+
export declare const TerrainSourceImporter: import("@aethervtt/shard-assets").ImporterDef;
|
|
37
|
+
/** The JSON Schema of `*.terrain.json` (written to `.shard/schemas/terrain.schema.json`). */
|
|
38
|
+
export declare function terrainJsonSchema(): Record<string, unknown>;
|
|
39
|
+
export declare const terrainSchema: () => unknown;
|